fix: restore runtime workflow paths and standalone parsing

This commit is contained in:
Dicky Moore 2026-02-08 01:13:23 +00:00
parent 3feb0d378f
commit e86fa2ee9e
5 changed files with 172 additions and 18 deletions

View File

@ -15,9 +15,6 @@ epicsTemplate: '{workflow_path}/templates/epics-template.md'
# Task References
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.md'
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
# Template References
epicsTemplate: '{workflow_path}/templates/epics-template.md'
---
# Step 1: Validate Prerequisites and Extract Requirements

View File

@ -17,7 +17,7 @@ web_bundle: false
- `project_knowledge`
- `sprint_status` = `{implementation_artifacts}/sprint-status.yaml`
- `date` (system-generated)
- `installed_path` = `src/bmm/workflows/4-implementation/correct-course`
- `installed_path` = `{project-root}/_bmad/bmm/workflows/4-implementation/correct-course`
- `default_output_file` = `{planning_artifacts}/sprint-change-proposal-{date}.md`
<workflow>
@ -28,6 +28,6 @@ web_bundle: false
</step>
<step n="2" goal="Validate proposal quality">
<invoke-task>Validate against checklist at {installed_path}/checklist.md using src/core/tasks/validate-workflow.md</invoke-task>
<invoke-task>Validate against checklist at {installed_path}/checklist.md using {project-root}/_bmad/core/tasks/validate-workflow.md</invoke-task>
</step>
</workflow>

View File

@ -558,6 +558,126 @@ web_bundle:
console.log('');
// ============================================================
// Test 15: Correct-Course Installed Path Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 15: Correct-Course Installed Path Guard${colors.reset}\n`);
try {
const workflowPath = path.join(projectRoot, 'src', 'bmm', 'workflows', '4-implementation', 'correct-course', 'workflow.md');
const content = await fs.readFile(workflowPath, 'utf8');
assert(
content.includes('`installed_path` = `{project-root}/_bmad/bmm/workflows/4-implementation/correct-course`'),
'Correct-course workflow uses installed runtime path',
);
assert(
content.includes('{project-root}/_bmad/core/tasks/validate-workflow.md'),
'Correct-course workflow uses installed validate-workflow task path',
);
} catch (error) {
assert(false, 'Correct-course installed path guard runs', error.message);
}
console.log('');
// ============================================================
// Test 16: Task/Tool Standalone and CRLF Parsing Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 16: Task/Tool Standalone + CRLF Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-standalone-crlf-'));
const coreTasksDir = path.join(tmpRoot, '_bmad', 'core', 'tasks');
const coreToolsDir = path.join(tmpRoot, '_bmad', 'core', 'tools');
await fs.ensureDir(coreTasksDir);
await fs.ensureDir(coreToolsDir);
await fs.writeFile(
path.join(coreTasksDir, 'default-task.md'),
`---
name: default-task
displayName: Default Task
description: Defaults to standalone
---
`,
);
await fs.writeFile(
path.join(coreTasksDir, 'internal-task.md'),
`---
name: internal-task
displayName: Internal Task
description: Hidden task
internal: true
---
`,
);
await fs.writeFile(
path.join(coreTasksDir, 'crlf-task.md'),
'---\r\nname: crlf-task\r\ndisplayName: CRLF Task\r\ndescription: Parsed from CRLF\r\nstandalone: true\r\n---\r\n',
);
await fs.writeFile(
path.join(coreToolsDir, 'default-tool.md'),
`---
name: default-tool
displayName: Default Tool
description: Defaults to standalone
---
`,
);
await fs.writeFile(
path.join(coreToolsDir, 'internal-tool.md'),
`---
name: internal-tool
displayName: Internal Tool
description: Hidden tool
internal: true
---
`,
);
await fs.writeFile(
path.join(coreToolsDir, 'crlf-tool.md'),
'---\r\nname: crlf-tool\r\ndisplayName: CRLF Tool\r\ndescription: Parsed from CRLF\r\nstandalone: true\r\n---\r\n',
);
const manifestGenerator = new ManifestGenerator();
const tasks = await manifestGenerator.getTasksFromDir(coreTasksDir, 'core');
const tools = await manifestGenerator.getToolsFromDir(coreToolsDir, 'core');
const defaultTask = tasks.find((task) => task.name === 'default-task');
const internalTask = tasks.find((task) => task.name === 'internal-task');
const crlfTask = tasks.find((task) => task.name === 'crlf-task');
const defaultTool = tools.find((tool) => tool.name === 'default-tool');
const internalTool = tools.find((tool) => tool.name === 'internal-tool');
const crlfTool = tools.find((tool) => tool.name === 'crlf-tool');
assert(defaultTask?.standalone === true, 'Tasks default to standalone when standalone key is omitted');
assert(internalTask?.standalone === false, 'Tasks marked internal are excluded from standalone commands');
assert(crlfTask?.description === 'Parsed from CRLF', 'CRLF task frontmatter is parsed correctly');
assert(defaultTool?.standalone === true, 'Tools default to standalone when standalone key is omitted');
assert(internalTool?.standalone === false, 'Tools marked internal are excluded from standalone commands');
assert(crlfTool?.description === 'Parsed from CRLF', 'CRLF tool frontmatter is parsed correctly');
const taskToolGenerator = new TaskToolCommandGenerator();
assert(taskToolGenerator.isStandalone({}) === true, 'Task/tool command filter defaults missing standalone metadata to visible');
assert(
taskToolGenerator.isStandalone({ standalone: 'false' }) === false,
'Task/tool command filter hides entries explicitly marked standalone=false',
);
await fs.remove(tmpRoot);
} catch (error) {
assert(false, 'Task/tool standalone and CRLF guard runs', error.message);
}
console.log('');
// ============================================================
// Summary
// ============================================================

View File

@ -383,18 +383,25 @@ class ManifestGenerator {
let name = file.replace(/\.(xml|md)$/, '');
let displayName = name;
let description = '';
let standalone = false;
let standalone = true;
if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tasks
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) {
try {
const frontmatter = yaml.parse(frontmatterMatch[1]);
const frontmatter = yaml.parse(frontmatterMatch[1]) || {};
name = frontmatter.name || name;
displayName = frontmatter.displayName || frontmatter.name || name;
description = frontmatter.description || '';
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
const isInternal = frontmatter.internal === true || frontmatter.internal === 'true';
if (frontmatter.standalone === true || frontmatter.standalone === 'true') {
standalone = true;
} else if (frontmatter.standalone === false || frontmatter.standalone === 'false') {
standalone = false;
} else {
standalone = !isInternal;
}
} catch {
// If YAML parsing fails, use defaults
}
@ -408,8 +415,16 @@ class ManifestGenerator {
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
standalone = !!standaloneMatch;
const standaloneTrueMatch = content.match(/<task[^>]+standalone="true"/i);
const standaloneFalseMatch = content.match(/<task[^>]+standalone="false"/i);
const internalMatch = content.match(/<task[^>]+internal="true"/i);
if (standaloneFalseMatch) {
standalone = false;
} else if (standaloneTrueMatch) {
standalone = true;
} else {
standalone = !internalMatch;
}
}
// Build relative path for installation
@ -472,18 +487,25 @@ class ManifestGenerator {
let name = file.replace(/\.(xml|md)$/, '');
let displayName = name;
let description = '';
let standalone = false;
let standalone = true;
if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tools
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) {
try {
const frontmatter = yaml.parse(frontmatterMatch[1]);
const frontmatter = yaml.parse(frontmatterMatch[1]) || {};
name = frontmatter.name || name;
displayName = frontmatter.displayName || frontmatter.name || name;
description = frontmatter.description || '';
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
const isInternal = frontmatter.internal === true || frontmatter.internal === 'true';
if (frontmatter.standalone === true || frontmatter.standalone === 'true') {
standalone = true;
} else if (frontmatter.standalone === false || frontmatter.standalone === 'false') {
standalone = false;
} else {
standalone = !isInternal;
}
} catch {
// If YAML parsing fails, use defaults
}
@ -497,8 +519,16 @@ class ManifestGenerator {
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
standalone = !!standaloneMatch;
const standaloneTrueMatch = content.match(/<tool[^>]+standalone="true"/i);
const standaloneFalseMatch = content.match(/<tool[^>]+standalone="false"/i);
const internalMatch = content.match(/<tool[^>]+internal="true"/i);
if (standaloneFalseMatch) {
standalone = false;
} else if (standaloneTrueMatch) {
standalone = true;
} else {
standalone = !internalMatch;
}
}
// Build relative path for installation

View File

@ -24,7 +24,14 @@ class TaskToolCommandGenerator {
* @returns {boolean} True when item should be exposed as a command
*/
isStandalone(item) {
return item?.standalone === 'true' || item?.standalone === true;
if (item?.standalone === false || item?.standalone === 'false') {
return false;
}
if (item?.internal === true || item?.internal === 'true') {
return false;
}
// Backward-compatible default: entries are user-facing unless explicitly hidden.
return true;
}
/**