Compare commits

...

5 Commits

14 changed files with 117 additions and 62 deletions

View File

@ -1,10 +1,10 @@
<handler type="workflow">
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 yaml path as 'workflow-config' parameter to those instructions
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.yaml path is "todo", inform user the workflow hasn't been implemented yet
</handler>
6. If workflow-config path normalizes to "todo" (trimmed, case-insensitive), inform user the workflow hasn't been implemented yet
</handler>

View File

@ -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;
}

View File

@ -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);
}
/**

View File

@ -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);
}

View File

@ -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.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from @_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

View File

@ -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.

View File

@ -6,9 +6,9 @@ description: '{{description}}'
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
<steps CRITICAL="TRUE">
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 yaml path @{project-root}/{{bmadFolderName}}/{{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}}/{{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
</steps>

View File

@ -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 `/`)

View File

@ -9,7 +9,7 @@ IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the c
<steps CRITICAL="TRUE">
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 yaml 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
</steps>

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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:
<steps CRITICAL="TRUE">
1. Always LOAD the FULL @_bmad/core/tasks/workflow.xml
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config @{{workflow_path}}
3. Pass the yaml path {{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
</steps>

View File

@ -2,4 +2,4 @@
description: '{{description}}'
---
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{{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 `/`)