diff --git a/src/utility/agent-components/handler-workflow.txt b/src/utility/agent-components/handler-workflow.txt index 16d7e29ad..f9ada8a34 100644 --- a/src/utility/agent-components/handler-workflow.txt +++ b/src/utility/agent-components/handler-workflow.txt @@ -1,10 +1,10 @@ - When menu item has: workflow="path/to/workflow.yaml": + When menu item has: workflow="path/to/workflow-config": 1. CRITICAL: Always LOAD {project-root}/_bmad/core/tasks/workflow.xml 2. Read the complete file - this is the CORE OS for processing BMAD workflows 3. Pass the workflow-config path as 'workflow-config' parameter to those instructions 4. Follow workflow.xml instructions precisely following all steps 5. Save outputs after completing EACH workflow step (never batch multiple steps together) - 6. If workflow-config path is "todo", inform user the workflow hasn't been implemented yet + 6. If workflow-config path normalizes to "todo" (trimmed, case-insensitive), inform user the workflow hasn't been implemented yet diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 85196cf76..117922856 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -361,6 +361,42 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} `; } + /** + * Normalize artifact path values to a relative path inside the BMAD folder. + * @param {string} rawPath - Raw path value from artifact metadata + * @returns {string} Normalized relative path + */ + normalizeArtifactPath(rawPath) { + if (!rawPath || typeof rawPath !== 'string') { + return ''; + } + + let normalized = rawPath.replaceAll('\\', '/').trim(); + + // Strip {project-root}/ prefix if present. + normalized = normalized.replace(/^\{project-root\}\//, ''); + + // Strip configured/legacy BMAD folder prefixes from absolute or relative paths. + const escapedFolderName = this.bmadFolderName.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); + const configuredFolderMatch = normalized.match(new RegExp(`(?:^|/)${escapedFolderName}/(.+)$`)); + if (configuredFolderMatch) { + normalized = configuredFolderMatch[1]; + } else { + const legacyFolderMatch = normalized.match(/(?:^|\/)_bmad\/(.+)$/); + if (legacyFolderMatch) { + normalized = legacyFolderMatch[1]; + } + } + + // Final cleanup for relative-only path contracts. + normalized = normalized.replace(/^[A-Za-z]:\//, ''); // Windows drive prefix + normalized = normalized.replace(/^\.\/+/, ''); + normalized = normalized.replace(/^\/+/, ''); + normalized = normalized.replaceAll(/\/{2,}/g, '/'); + + return normalized; + } + /** * Render template with artifact data * @param {string} template - Template content @@ -372,18 +408,18 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} let pathToUse = artifact.relativePath || ''; switch (artifact.type) { case 'agent-launcher': { - pathToUse = artifact.agentPath || artifact.relativePath || ''; + pathToUse = this.normalizeArtifactPath(artifact.agentPath || artifact.relativePath || ''); break; } case 'workflow-command': { - pathToUse = artifact.workflowPath || artifact.relativePath || ''; + pathToUse = this.normalizeArtifactPath(artifact.workflowPath || artifact.relativePath || ''); break; } case 'task': case 'tool': { - pathToUse = artifact.path || artifact.relativePath || ''; + pathToUse = this.normalizeArtifactPath(artifact.path || artifact.relativePath || ''); break; } diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 0915c306b..d39edc055 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -79,8 +79,8 @@ class AgentCommandGenerator { .replaceAll('{{module}}', agent.module) .replaceAll('{{path}}', agentPathInModule) .replaceAll('{{description}}', agent.description || `${agent.name} agent`) - .replaceAll('_bmad', this.bmadFolderName) - .replaceAll('_bmad', '_bmad'); + .replaceAll('{{bmadFolderName}}', this.bmadFolderName) + .replaceAll('_bmad', this.bmadFolderName); } /** 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 d94e77db1..324361bde 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -13,6 +13,57 @@ class WorkflowCommandGenerator { this.bmadFolderName = bmadFolderName; } + /** + * Escape regex metacharacters in dynamic path segments. + * @param {string} value - Raw string value + * @returns {string} Regex-escaped value + */ + escapeRegex(value) { + return String(value).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); + } + + /** + * Normalize workflow paths to a relative path inside the BMAD folder. + * Result never starts with '/', '{project-root}/', or '{bmadFolderName}/'. + * @param {string} workflowPath - Raw workflow path + * @returns {string} Normalized relative workflow path + */ + normalizeWorkflowRelativePath(workflowPath) { + if (!workflowPath || typeof workflowPath !== 'string') { + return ''; + } + + let normalized = workflowPath.replaceAll('\\', '/').trim(); + + // Strip {project-root}/ prefix if present. + normalized = normalized.replace(/^\{project-root\}\//, ''); + + // Normalize source paths (e.g. .../src/bmm/... -> bmm/...). + const srcMatch = normalized.match(/(?:^|\/)src\/([^/]+)\/(.+)/); + if (srcMatch) { + normalized = `${srcMatch[1]}/${srcMatch[2]}`; + } else { + // Strip configured/legacy BMAD folder prefixes from absolute or relative paths. + const configuredFolderMatch = normalized.match(new RegExp(`(?:^|/)${this.escapeRegex(this.bmadFolderName)}/(.+)$`)); + if (configuredFolderMatch) { + normalized = configuredFolderMatch[1]; + } else { + const legacyFolderMatch = normalized.match(/(?:^|\/)_bmad\/(.+)$/); + if (legacyFolderMatch) { + normalized = legacyFolderMatch[1]; + } + } + } + + // Final cleanup for relative-only contract. + normalized = normalized.replace(/^[A-Za-z]:\//, ''); // Windows drive prefix + normalized = normalized.replace(/^\.\/+/, ''); + normalized = normalized.replace(/^\/+/, ''); + normalized = normalized.replaceAll(/\/{2,}/g, '/'); + + return normalized; + } + /** * Generate workflow commands from the manifest CSV * @param {string} projectDir - Project directory @@ -67,24 +118,8 @@ class WorkflowCommandGenerator { 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.yaml) - let workflowRelPath = workflow.path || ''; - // Normalize path separators for cross-platform compatibility - workflowRelPath = workflowRelPath.replaceAll('\\', '/'); - // Remove _bmad/ prefix if present to get relative path from project root - // Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...) - if (workflowRelPath.includes('_bmad/')) { - const parts = workflowRelPath.split(/_bmad\//); - if (parts.length > 1) { - workflowRelPath = parts.slice(1).join('/'); - } - } else if (workflowRelPath.includes('/src/')) { - // Normalize source paths (e.g. .../src/bmm/...) to relative module path (e.g. bmm/...) - const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/); - if (match) { - workflowRelPath = `${match[1]}/${match[2]}`; - } - } + // Calculate a relative workflow path (e.g., bmm/workflows/.../workflow.yaml). + const workflowRelPath = this.normalizeWorkflowRelativePath(workflow.path || ''); // Determine if this is a YAML workflow (use normalized path which is guaranteed to be a string) const isYamlWorkflow = workflowRelPath.endsWith('.yaml') || workflowRelPath.endsWith('.yml'); artifacts.push({ @@ -124,39 +159,23 @@ class WorkflowCommandGenerator { * Generate command content for a workflow */ async generateCommandContent(workflow, bmadDir) { + const normalizedWorkflowPath = this.normalizeWorkflowRelativePath(workflow.path || ''); + // Determine template based on workflow file type - const isMarkdownWorkflow = workflow.path.endsWith('workflow.md'); + const isMarkdownWorkflow = normalizedWorkflowPath.endsWith('workflow.md'); const templateName = isMarkdownWorkflow ? 'workflow-commander.md' : 'workflow-command-template.md'; const templatePath = path.join(path.dirname(this.templatePath), templateName); // Load the appropriate template const template = await fs.readFile(templatePath, 'utf8'); - // Convert source path to installed path - // From: /Users/.../src/bmm/workflows/.../workflow.yaml - // To: {project-root}/_bmad/bmm/workflows/.../workflow.yaml - 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('{{workflow_path}}', normalizedWorkflowPath) + .replaceAll('{{bmadFolderName}}', this.bmadFolderName) .replaceAll('_bmad', this.bmadFolderName); } diff --git a/tools/cli/installers/lib/ide/templates/agent-command-template.md b/tools/cli/installers/lib/ide/templates/agent-command-template.md index 0f0c2e20d..2e25ff7f7 100644 --- a/tools/cli/installers/lib/ide/templates/agent-command-template.md +++ b/tools/cli/installers/lib/ide/templates/agent-command-template.md @@ -6,7 +6,7 @@ description: '{{description}}' You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. -1. LOAD the FULL agent file from {project-root}/_bmad/{{module}}/agents/{{path}} +1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{module}}/agents/{{path}} 2. READ its entire contents - this contains the complete agent persona, menu, and instructions 3. Execute ALL activation steps exactly as written in the agent file 4. Follow the agent's persona and menu system precisely diff --git a/tools/cli/installers/lib/ide/templates/combined/antigravity.md b/tools/cli/installers/lib/ide/templates/combined/antigravity.md index 88e806e9d..1c11eb246 100644 --- a/tools/cli/installers/lib/ide/templates/combined/antigravity.md +++ b/tools/cli/installers/lib/ide/templates/combined/antigravity.md @@ -3,6 +3,6 @@ name: '{{name}}' description: '{{description}}' --- -Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}} +Read the entire workflow file at: {project-root}/{{bmadFolderName}}/{{workflow_path}} Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md b/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md index 071214728..01dc763e5 100644 --- a/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md +++ b/tools/cli/installers/lib/ide/templates/combined/default-workflow-yaml.md @@ -7,8 +7,8 @@ IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the c 1. Always LOAD the FULL {project-root}/{{bmadFolderName}}/core/tasks/workflow.xml -2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config {project-root}/{{bmadFolderName}}/{{path}} -3. Pass the workflow-config path {project-root}/{{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions +2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config "{project-root}/{{bmadFolderName}}/{{path}}" (where {{path}} is relative and does not start with `/`) +3. Pass the workflow-config path "{project-root}/{{bmadFolderName}}/{{path}}" as 'workflow-config' parameter to the workflow.xml instructions 4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions 5. Save outputs after EACH section when generating any documents from templates diff --git a/tools/cli/installers/lib/ide/templates/combined/default-workflow.md b/tools/cli/installers/lib/ide/templates/combined/default-workflow.md index c8ad40459..c5311914e 100644 --- a/tools/cli/installers/lib/ide/templates/combined/default-workflow.md +++ b/tools/cli/installers/lib/ide/templates/combined/default-workflow.md @@ -3,4 +3,4 @@ name: '{{name}}' description: '{{description}}' --- -IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL {project-root}/{{bmadFolderName}}/{{path}}, READ its entire contents and follow its directions exactly! +IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL "{project-root}/{{bmadFolderName}}/{{path}}", READ its entire contents and follow its directions exactly! ({{path}} must be relative and must not start with `/`) diff --git a/tools/cli/installers/lib/ide/templates/combined/kiro-workflow-yaml.md b/tools/cli/installers/lib/ide/templates/combined/kiro-workflow-yaml.md index 18d37f4eb..20fd0c789 100644 --- a/tools/cli/installers/lib/ide/templates/combined/kiro-workflow-yaml.md +++ b/tools/cli/installers/lib/ide/templates/combined/kiro-workflow-yaml.md @@ -9,7 +9,7 @@ IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the c 1. Always LOAD the FULL #[[file:{{bmadFolderName}}/core/tasks/workflow.xml]] 2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config #[[file:{{bmadFolderName}}/{{path}}]] -3. Pass the workflow-config path {{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions +3. Pass the workflow-config path {{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions (Kiro intentionally uses a project-relative workflow-config path for #[[file:...]] compatibility; other templates may use {project-root}/... form) 4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions 5. Save outputs after EACH section when generating any documents from templates diff --git a/tools/cli/installers/lib/ide/templates/combined/rovodev.md b/tools/cli/installers/lib/ide/templates/combined/rovodev.md index 066945ee5..77aff07b0 100644 --- a/tools/cli/installers/lib/ide/templates/combined/rovodev.md +++ b/tools/cli/installers/lib/ide/templates/combined/rovodev.md @@ -4,6 +4,6 @@ --- -Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}} +Read the entire workflow file at: {project-root}/{{bmadFolderName}}/{{workflow_path}} Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/trae.md b/tools/cli/installers/lib/ide/templates/combined/trae.md index b4d43d7af..d496a2bd6 100644 --- a/tools/cli/installers/lib/ide/templates/combined/trae.md +++ b/tools/cli/installers/lib/ide/templates/combined/trae.md @@ -4,6 +4,6 @@ ## Instructions -Read the entire workflow file at: {project-root}/_bmad/{{workflow_path}} +Read the entire workflow file at: {project-root}/{{bmadFolderName}}/{{workflow_path}} Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md b/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md index 6366425c7..b60c4340e 100644 --- a/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md +++ b/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md @@ -5,6 +5,6 @@ auto_execution_mode: "iterate" # {{name}} -Read the entire workflow file at {project-root}/_bmad/{{workflow_path}} +Read the entire workflow file at {project-root}/{{bmadFolderName}}/{{workflow_path}} Follow all instructions in the workflow file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/workflow-command-template.md b/tools/cli/installers/lib/ide/templates/workflow-command-template.md index 915e64566..56ae997ad 100644 --- a/tools/cli/installers/lib/ide/templates/workflow-command-template.md +++ b/tools/cli/installers/lib/ide/templates/workflow-command-template.md @@ -5,9 +5,9 @@ description: '{{description}}' IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded: -1. Always LOAD the FULL {project-root}/_bmad/core/tasks/workflow.xml -2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config {project-root}/{{workflow_path}} -3. Pass the workflow-config path {project-root}/{{workflow_path}} as 'workflow-config' parameter to the workflow.xml instructions +1. Always LOAD the FULL "{project-root}/{{bmadFolderName}}/core/tasks/workflow.xml" +2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config "{project-root}/{{bmadFolderName}}/{{workflow_path}}" (where {{workflow_path}} is relative and does not start with `/`) +3. Pass the workflow-config path "{project-root}/{{bmadFolderName}}/{{workflow_path}}" as 'workflow-config' parameter to the workflow.xml instructions 4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions 5. Save outputs after EACH section when generating any documents from templates diff --git a/tools/cli/installers/lib/ide/templates/workflow-commander.md b/tools/cli/installers/lib/ide/templates/workflow-commander.md index 66eee15d1..9078b4cb3 100644 --- a/tools/cli/installers/lib/ide/templates/workflow-commander.md +++ b/tools/cli/installers/lib/ide/templates/workflow-commander.md @@ -2,4 +2,4 @@ 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! +IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL "{project-root}/{{bmadFolderName}}/{{workflow_path}}", READ its entire contents and follow its directions exactly! ({{workflow_path}} must be relative and must not start with `/`)