fix: address PR review findings from triage
- Fix regex capture group index in module manager workflow path parsing - Remove stale workflow handler references from handler-multi.txt - Replace workflow with multi in activation-steps dispatch contract - Remove dead validate-workflow emission from compiler and xml-builder - Align commands.md wording to remove engine references - Fix relativePath anchoring in _base-ide.js recursive directory scans - Remove dead code from workflow-command-generator (unused template, generateCommandContent, writeColonArtifacts, writeDashArtifacts) - Delete unused workflow-commander.md template - Add regression test for workflow path regex
This commit is contained in:
parent
61fe92a129
commit
a5aa8f8499
|
|
@ -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 |
|
||||
| --- | --- |
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@
|
|||
<step n="{HELP_STEP}">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 <example>`/bmad-help where should I start with an idea I have that does XYZ`</example></step>
|
||||
<step n="{HALT_STEP}">STOP and WAIT for user input - do NOT execute menu items automatically - accept number or cmd trigger or fuzzy command match</step>
|
||||
<step n="{INPUT_STEP}">On user input: Number → process menu item[n] | Text → case-insensitive substring match | Multiple matches → ask user to clarify | No match → show "Not recognized"</step>
|
||||
<step n="{EXECUTE_STEP}">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</step>
|
||||
<step n="{EXECUTE_STEP}">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</step>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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<string>} 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<string>} 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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}"`);
|
||||
|
|
|
|||
|
|
@ -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}"`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue