diff --git a/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md b/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md
index 5d2b0add9..d6eaef376 100644
--- a/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md
+++ b/src/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md
@@ -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
diff --git a/src/bmm/workflows/4-implementation/correct-course/workflow.md b/src/bmm/workflows/4-implementation/correct-course/workflow.md
index 9b0149e75..1fe79da87 100644
--- a/src/bmm/workflows/4-implementation/correct-course/workflow.md
+++ b/src/bmm/workflows/4-implementation/correct-course/workflow.md
@@ -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`
@@ -28,6 +28,6 @@ web_bundle: false
- Validate against checklist at {installed_path}/checklist.md using src/core/tasks/validate-workflow.md
+ Validate against checklist at {installed_path}/checklist.md using {project-root}/_bmad/core/tasks/validate-workflow.md
diff --git a/test/test-installation-components.js b/test/test-installation-components.js
index f955aa2d9..15fe1a087 100644
--- a/test/test-installation-components.js
+++ b/test/test-installation-components.js
@@ -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
// ============================================================
diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js
index f64f72109..22bc21f10 100644
--- a/tools/cli/installers/lib/core/manifest-generator.js
+++ b/tools/cli/installers/lib/core/manifest-generator.js
@@ -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>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
- const standaloneMatch = content.match(/]+standalone="true"/);
- standalone = !!standaloneMatch;
+ const standaloneTrueMatch = content.match(/]+standalone="true"/i);
+ const standaloneFalseMatch = content.match(/]+standalone="false"/i);
+ const internalMatch = content.match(/]+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>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
- const standaloneMatch = content.match(/]+standalone="true"/);
- standalone = !!standaloneMatch;
+ const standaloneTrueMatch = content.match(/]+standalone="true"/i);
+ const standaloneFalseMatch = content.match(/]+standalone="false"/i);
+ const internalMatch = content.match(/]+internal="true"/i);
+ if (standaloneFalseMatch) {
+ standalone = false;
+ } else if (standaloneTrueMatch) {
+ standalone = true;
+ } else {
+ standalone = !internalMatch;
+ }
}
// Build relative path for installation
diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js
index 6a0b5adf1..831227164 100644
--- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js
+++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js
@@ -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;
}
/**