diff --git a/docs/reference/commands.md b/docs/reference/commands.md
index 54f318e9a..0de99157c 100644
--- a/docs/reference/commands.md
+++ b/docs/reference/commands.md
@@ -88,7 +88,7 @@ See [Agents](./agents.md) for the full list of default agents and their triggers
### Workflow Skills
-Workflow skills run a structured, multi-step process without loading an agent persona first. They load the workflow engine and pass a specific workflow configuration.
+Workflow skills run a structured, multi-step process without loading an agent persona first. They load a workflow configuration and follow its steps.
| Example skill | Purpose |
| --- | --- |
diff --git a/src/utility/agent-components/activation-steps.txt b/src/utility/agent-components/activation-steps.txt
index 2b5e6eefe..abcc0e6fa 100644
--- a/src/utility/agent-components/activation-steps.txt
+++ b/src/utility/agent-components/activation-steps.txt
@@ -11,4 +11,4 @@
Let {user_name} know they can type command `/bmad-help` at any time to get advice on what to do next, and that they can combine that with what they need help with `/bmad-help where should I start with an idea I have that does XYZ`
STOP and WAIT for user input - do NOT execute menu items automatically - accept number or cmd trigger or fuzzy command match
On user input: Number → process menu item[n] | Text → case-insensitive substring match | Multiple matches → ask user to clarify | No match → show "Not recognized"
- When processing a menu item: Check menu-handlers section below - extract any attributes from the selected menu item (workflow, exec, tmpl, data, action) and follow the corresponding handler instructions
\ No newline at end of file
+ When processing a menu item: Check menu-handlers section below - extract any attributes from the selected menu item (exec, tmpl, data, action, multi) and follow the corresponding handler instructions
\ No newline at end of file
diff --git a/src/utility/agent-components/handler-multi.txt b/src/utility/agent-components/handler-multi.txt
index 20169315a..e05be2390 100644
--- a/src/utility/agent-components/handler-multi.txt
+++ b/src/utility/agent-components/handler-multi.txt
@@ -4,10 +4,9 @@
2. Parse all nested handlers within the multi item
3. For each nested handler:
- Use the 'match' attribute for fuzzy matching user input (or Exact Match of character code in brackets [])
- - Process based on handler attributes (exec, workflow, action)
+ - Process based on handler attributes (exec, action)
4. When user input matches a handler's 'match' pattern:
- For exec="path/to/file.md": follow the `handler type="exec"` instructions
- - For workflow="path/to/workflow.md": follow the `handler type="workflow"` instructions
- For action="...": Perform the specified action directly
5. Support both exact matches and fuzzy matching based on the match attribute
6. If no handler matches, prompt user to choose from available options
diff --git a/test/test-workflow-path-regex.js b/test/test-workflow-path-regex.js
new file mode 100644
index 000000000..488d69b76
--- /dev/null
+++ b/test/test-workflow-path-regex.js
@@ -0,0 +1,88 @@
+/**
+ * Workflow Path Regex Tests
+ *
+ * Tests that the source and install workflow path regexes in ModuleManager
+ * extract the correct capture groups (module name and workflow sub-path).
+ *
+ * Usage: node test/test-workflow-path-regex.js
+ */
+
+// ANSI colors
+const colors = {
+ reset: '\u001B[0m',
+ green: '\u001B[32m',
+ red: '\u001B[31m',
+ cyan: '\u001B[36m',
+ dim: '\u001B[2m',
+};
+
+let passed = 0;
+let failed = 0;
+
+function assert(condition, testName, errorMessage = '') {
+ if (condition) {
+ console.log(`${colors.green}✓${colors.reset} ${testName}`);
+ passed++;
+ } else {
+ console.log(`${colors.red}✗${colors.reset} ${testName}`);
+ if (errorMessage) {
+ console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
+ }
+ failed++;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// These regexes are extracted from ModuleManager.vendorWorkflowDependencies()
+// in tools/cli/installers/lib/modules/manager.js
+// ---------------------------------------------------------------------------
+
+// Source regex (line ~1081) — uses non-capturing group for _bmad
+const SOURCE_REGEX = /\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/;
+
+// Install regex (line ~1091) — uses non-capturing group for _bmad,
+// consistent with source regex
+const INSTALL_REGEX = /\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/;
+
+// ---------------------------------------------------------------------------
+// Test data
+// ---------------------------------------------------------------------------
+const sourcePath = '{project-root}/_bmad/bmm/workflows/4-implementation/create-story/workflow.md';
+const installPath = '{project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.md';
+
+console.log(`\n${colors.cyan}Workflow Path Regex Tests${colors.reset}\n`);
+
+// --- Source regex tests (these should pass — source regex is correct) ---
+
+const sourceMatch = sourcePath.match(SOURCE_REGEX);
+
+assert(sourceMatch !== null, 'Source regex matches source path');
+assert(
+ sourceMatch && sourceMatch[1] === 'bmm',
+ 'Source regex group [1] is the module name',
+ `Expected "bmm", got "${sourceMatch && sourceMatch[1]}"`,
+);
+assert(
+ sourceMatch && sourceMatch[2] === '4-implementation/create-story/workflow.md',
+ 'Source regex group [2] is the workflow sub-path',
+ `Expected "4-implementation/create-story/workflow.md", got "${sourceMatch && sourceMatch[2]}"`,
+);
+
+// --- Install regex tests (group [2] returns module name, not sub-path) ---
+
+const installMatch = installPath.match(INSTALL_REGEX);
+
+assert(installMatch !== null, 'Install regex matches install path');
+
+// This is the critical test: installMatch[2] should be the workflow sub-path,
+// because the code uses it as `installWorkflowSubPath`.
+// With the bug, installMatch[2] is "bmgd" (module name) instead of the sub-path.
+assert(
+ installMatch && installMatch[2] === '4-production/create-story/workflow.md',
+ 'Install regex group [2] is the workflow sub-path (used as installWorkflowSubPath)',
+ `Expected "4-production/create-story/workflow.md", got "${installMatch && installMatch[2]}"`,
+);
+
+// --- Summary ---
+console.log(`\n${passed} passed, ${failed} failed\n`);
+process.exit(failed > 0 ? 1 : 0);
diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js
index 39dfa576b..a09222868 100644
--- a/tools/cli/installers/lib/ide/_base-ide.js
+++ b/tools/cli/installers/lib/ide/_base-ide.js
@@ -326,9 +326,11 @@ class BaseIdeSetup {
/**
* Recursively find workflow.md files
* @param {string} dir - Directory to search
+ * @param {string} [rootDir] - Original root directory (used internally for recursion)
* @returns {Array} List of workflow file info objects
*/
- async findWorkflowFiles(dir) {
+ async findWorkflowFiles(dir, rootDir = null) {
+ rootDir = rootDir || dir;
const workflows = [];
if (!(await fs.pathExists(dir))) {
@@ -342,7 +344,7 @@ class BaseIdeSetup {
if (entry.isDirectory()) {
// Recursively search subdirectories
- const subWorkflows = await this.findWorkflowFiles(fullPath);
+ const subWorkflows = await this.findWorkflowFiles(fullPath, rootDir);
workflows.push(...subWorkflows);
} else if (entry.isFile() && entry.name === 'workflow.md') {
// Read workflow.md frontmatter to get name and standalone property
@@ -360,7 +362,7 @@ class BaseIdeSetup {
workflows.push({
name: workflowData.name,
path: fullPath,
- relativePath: path.relative(dir, fullPath),
+ relativePath: path.relative(rootDir, fullPath),
filename: entry.name,
description: workflowData.description || '',
standalone: standalone,
@@ -379,9 +381,11 @@ class BaseIdeSetup {
* Scan a directory for files with specific extension(s)
* @param {string} dir - Directory to scan
* @param {string|Array} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
+ * @param {string} [rootDir] - Original root directory (used internally for recursion)
* @returns {Array} List of file info objects
*/
- async scanDirectory(dir, ext) {
+ async scanDirectory(dir, ext, rootDir = null) {
+ rootDir = rootDir || dir;
const files = [];
if (!(await fs.pathExists(dir))) {
@@ -398,7 +402,7 @@ class BaseIdeSetup {
if (entry.isDirectory()) {
// Recursively scan subdirectories
- const subFiles = await this.scanDirectory(fullPath, ext);
+ const subFiles = await this.scanDirectory(fullPath, ext, rootDir);
files.push(...subFiles);
} else if (entry.isFile()) {
// Check if file matches any of the extensions
@@ -407,7 +411,7 @@ class BaseIdeSetup {
files.push({
name: path.basename(entry.name, matchedExt),
path: fullPath,
- relativePath: path.relative(dir, fullPath),
+ relativePath: path.relative(rootDir, fullPath),
filename: entry.name,
});
}
@@ -421,9 +425,11 @@ class BaseIdeSetup {
* Scan a directory for files with specific extension(s) and check standalone attribute
* @param {string} dir - Directory to scan
* @param {string|Array} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
+ * @param {string} [rootDir] - Original root directory (used internally for recursion)
* @returns {Array} List of file info objects with standalone property
*/
- async scanDirectoryWithStandalone(dir, ext) {
+ async scanDirectoryWithStandalone(dir, ext, rootDir = null) {
+ rootDir = rootDir || dir;
const files = [];
if (!(await fs.pathExists(dir))) {
@@ -440,7 +446,7 @@ class BaseIdeSetup {
if (entry.isDirectory()) {
// Recursively scan subdirectories
- const subFiles = await this.scanDirectoryWithStandalone(fullPath, ext);
+ const subFiles = await this.scanDirectoryWithStandalone(fullPath, ext, rootDir);
files.push(...subFiles);
} else if (entry.isFile()) {
// Check if file matches any of the extensions
@@ -484,7 +490,7 @@ class BaseIdeSetup {
files.push({
name: path.basename(entry.name, matchedExt),
path: fullPath,
- relativePath: path.relative(dir, fullPath),
+ relativePath: path.relative(rootDir, fullPath),
filename: entry.name,
standalone: standalone,
});
diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js
index 091286893..ed8c3e508 100644
--- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js
+++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js
@@ -1,8 +1,7 @@
const path = require('node:path');
const fs = require('fs-extra');
const csv = require('csv-parse/sync');
-const prompts = require('../../../../lib/prompts');
-const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
+const { BMAD_FOLDER_NAME } = require('./path-utils');
/**
* Generates command files for each workflow in the manifest
@@ -12,46 +11,6 @@ class WorkflowCommandGenerator {
this.bmadFolderName = bmadFolderName;
}
- /**
- * Generate workflow commands from the manifest CSV
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- */
- async generateWorkflowCommands(projectDir, bmadDir) {
- const workflows = await this.loadWorkflowManifest(bmadDir);
-
- if (!workflows) {
- await prompts.log.warn('Workflow manifest not found. Skipping command generation.');
- return { generated: 0 };
- }
-
- // ALL workflows now generate commands - no standalone filtering
- const allWorkflows = workflows;
-
- // Base commands directory
- const baseCommandsDir = path.join(projectDir, '.claude', 'commands', 'bmad');
-
- let generatedCount = 0;
-
- // Generate a command file for each workflow, organized by module
- for (const workflow of allWorkflows) {
- const moduleWorkflowsDir = path.join(baseCommandsDir, workflow.module, 'workflows');
- await fs.ensureDir(moduleWorkflowsDir);
-
- const commandContent = await this.generateCommandContent(workflow, bmadDir);
- const commandPath = path.join(moduleWorkflowsDir, `${workflow.name}.md`);
-
- await fs.writeFile(commandPath, commandContent);
- generatedCount++;
- }
-
- // Also create a workflow launcher README in each module
- const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
- await this.createModuleWorkflowLaunchers(baseCommandsDir, groupedWorkflows);
-
- return { generated: generatedCount };
- }
-
async collectWorkflowArtifacts(bmadDir) {
const workflows = await this.loadWorkflowManifest(bmadDir);
@@ -65,7 +24,6 @@ class WorkflowCommandGenerator {
const artifacts = [];
for (const workflow of allWorkflows) {
- const commandContent = await this.generateCommandContent(workflow, bmadDir);
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
let workflowRelPath = workflow.path || '';
// Normalize path separators for cross-platform compatibility
@@ -92,7 +50,6 @@ class WorkflowCommandGenerator {
canonicalId: workflow.canonicalId || '',
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
workflowPath: workflowRelPath, // Relative path to actual workflow file
- content: commandContent,
sourcePath: workflow.path,
});
}
@@ -117,43 +74,6 @@ class WorkflowCommandGenerator {
};
}
- /**
- * Generate command content for a workflow
- */
- async generateCommandContent(workflow, bmadDir) {
- const templatePath = path.join(__dirname, '../templates/workflow-commander.md');
-
- // Load the template
- const template = await fs.readFile(templatePath, 'utf8');
-
- // Convert source path to installed path
- // From: /Users/.../src/bmm/workflows/.../workflow.md
- // To: {project-root}/_bmad/bmm/workflows/.../workflow.md
- let workflowPath = workflow.path;
-
- // Extract the relative path from source
- if (workflowPath.includes('/src/bmm/')) {
- // bmm is directly under src/
- const match = workflowPath.match(/\/src\/bmm\/(.+)/);
- if (match) {
- workflowPath = `${this.bmadFolderName}/bmm/${match[1]}`;
- }
- } else if (workflowPath.includes('/src/core/')) {
- const match = workflowPath.match(/\/src\/core\/(.+)/);
- if (match) {
- workflowPath = `${this.bmadFolderName}/core/${match[1]}`;
- }
- }
-
- // Replace template variables
- return template
- .replaceAll('{{name}}', workflow.name)
- .replaceAll('{{module}}', workflow.module)
- .replaceAll('{{description}}', workflow.description)
- .replaceAll('{{workflow_path}}', workflowPath)
- .replaceAll('_bmad', this.bmadFolderName);
- }
-
/**
* Create workflow launcher files for each module
*/
@@ -254,58 +174,6 @@ When running any workflow:
skip_empty_lines: true,
});
}
-
- /**
- * Write workflow command artifacts using underscore format (Windows-compatible)
- * Creates flat files like: bmad_bmm_correct-course.md
- *
- * @param {string} baseCommandsDir - Base commands directory for the IDE
- * @param {Array} artifacts - Workflow artifacts
- * @returns {number} Count of commands written
- */
- async writeColonArtifacts(baseCommandsDir, artifacts) {
- let writtenCount = 0;
-
- for (const artifact of artifacts) {
- if (artifact.type === 'workflow-command') {
- // Convert relativePath to underscore format: bmm/workflows/correct-course.md → bmad_bmm_correct-course.md
- const flatName = toColonPath(artifact.relativePath);
- const commandPath = path.join(baseCommandsDir, flatName);
- await fs.ensureDir(path.dirname(commandPath));
- await fs.writeFile(commandPath, artifact.content);
- writtenCount++;
- }
- }
-
- return writtenCount;
- }
-
- /**
- * Write workflow command artifacts using dash format (NEW STANDARD)
- * Creates flat files like: bmad-bmm-correct-course.md
- *
- * Note: Workflows do NOT have bmad-agent- prefix - only agents do.
- *
- * @param {string} baseCommandsDir - Base commands directory for the IDE
- * @param {Array} artifacts - Workflow artifacts
- * @returns {number} Count of commands written
- */
- async writeDashArtifacts(baseCommandsDir, artifacts) {
- let writtenCount = 0;
-
- for (const artifact of artifacts) {
- if (artifact.type === 'workflow-command') {
- // Convert relativePath to dash format: bmm/workflows/correct-course.md → bmad-bmm-correct-course.md
- const flatName = toDashPath(artifact.relativePath);
- const commandPath = path.join(baseCommandsDir, flatName);
- await fs.ensureDir(path.dirname(commandPath));
- await fs.writeFile(commandPath, artifact.content);
- writtenCount++;
- }
- }
-
- return writtenCount;
- }
}
module.exports = { WorkflowCommandGenerator };
diff --git a/tools/cli/installers/lib/ide/templates/workflow-commander.md b/tools/cli/installers/lib/ide/templates/workflow-commander.md
deleted file mode 100644
index 66eee15d1..000000000
--- a/tools/cli/installers/lib/ide/templates/workflow-commander.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-description: '{{description}}'
----
-
-IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL {project-root}/{{workflow_path}}, READ its entire contents and follow its directions exactly!
diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js
index b2d288af7..7ac85678b 100644
--- a/tools/cli/installers/lib/modules/manager.js
+++ b/tools/cli/installers/lib/modules/manager.js
@@ -1088,7 +1088,7 @@ class ModuleManager {
// Parse INSTALL workflow path
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.md
- const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
+ const installMatch = installWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) {
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`);
continue;
diff --git a/tools/cli/lib/agent/compiler.js b/tools/cli/lib/agent/compiler.js
index 3e72c6674..cc91f9bd6 100644
--- a/tools/cli/lib/agent/compiler.js
+++ b/tools/cli/lib/agent/compiler.js
@@ -186,7 +186,6 @@ function buildNestedHandlers(triggers) {
// Add handler attributes based on exec data
if (execData.route) attrs.push(`exec="${execData.route}"`);
- if (execData['validate-workflow']) attrs.push(`validate-workflow="${execData['validate-workflow']}"`);
if (execData.action) attrs.push(`action="${execData.action}"`);
if (execData.data) attrs.push(`data="${execData.data}"`);
if (execData.tmpl) attrs.push(`tmpl="${execData.tmpl}"`);
diff --git a/tools/cli/lib/yaml-xml-builder.js b/tools/cli/lib/yaml-xml-builder.js
index 43af99f15..f4f8e2f5a 100644
--- a/tools/cli/lib/yaml-xml-builder.js
+++ b/tools/cli/lib/yaml-xml-builder.js
@@ -408,7 +408,6 @@ class YamlXmlBuilder {
// Add handler attributes based on exec data
if (execData.route) attrs.push(`exec="${execData.route}"`);
- if (execData['validate-workflow']) attrs.push(`validate-workflow="${execData['validate-workflow']}"`);
if (execData.action) attrs.push(`action="${execData.action}"`);
if (execData.data) attrs.push(`data="${execData.data}"`);
if (execData.tmpl) attrs.push(`tmpl="${execData.tmpl}"`);