From bd20d75998ab6de044424dafc2a92fbfdb0e4dac Mon Sep 17 00:00:00 2001 From: Dicky Moore Date: Sun, 8 Feb 2026 01:13:23 +0000 Subject: [PATCH] fix: restore runtime workflow paths and standalone parsing --- .../steps/step-01-validate-prerequisites.md | 3 - .../correct-course/workflow.md | 4 +- test/test-installation-components.js | 120 ++++++++++++++++++ .../installers/lib/core/manifest-generator.js | 54 ++++++-- .../ide/shared/task-tool-command-generator.js | 9 +- 5 files changed, 172 insertions(+), 18 deletions(-) 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 6690d5c9c..3cfa572f3 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 @@ -25,7 +25,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; } /**