refactor: remove YAML workflow code paths from CLI installer pipeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-03-08 15:09:21 -06:00
parent 6ceacf7dc1
commit 73d19baf7e
8 changed files with 97 additions and 258 deletions

View File

@ -148,7 +148,7 @@ class ManifestGenerator {
/** /**
* Recursively walk a module directory tree, collecting skill directories. * Recursively walk a module directory tree, collecting skill directories.
* A skill directory is one that contains both a bmad-skill-manifest.yaml with * A skill directory is one that contains both a bmad-skill-manifest.yaml with
* type: skill AND a workflow.md (or workflow.yaml) file. * type: skill AND a workflow.md file.
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths). * Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
*/ */
async collectSkills() { async collectSkills() {
@ -172,76 +172,66 @@ class ManifestGenerator {
// Check this directory for skill manifest + workflow file // Check this directory for skill manifest + workflow file
const manifest = await this.loadSkillManifest(dir); const manifest = await this.loadSkillManifest(dir);
// Try both workflow.md and workflow.yaml const workflowFile = 'workflow.md';
const workflowFilenames = ['workflow.md', 'workflow.yaml']; const workflowPath = path.join(dir, workflowFile);
for (const workflowFile of workflowFilenames) { if (await fs.pathExists(workflowPath)) {
const workflowPath = path.join(dir, workflowFile);
if (!(await fs.pathExists(workflowPath))) continue;
const artifactType = this.getArtifactType(manifest, workflowFile); const artifactType = this.getArtifactType(manifest, workflowFile);
if (artifactType !== 'skill') continue; if (artifactType === 'skill') {
// Read and parse the workflow file
try {
const rawContent = await fs.readFile(workflowPath, 'utf8');
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
// Read and parse the workflow file
try {
const rawContent = await fs.readFile(workflowPath, 'utf8');
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
let workflow;
if (workflowFile === 'workflow.yaml') {
workflow = yaml.parse(content);
} else {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) { if (!frontmatterMatch) {
if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`); if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`);
continue; } else {
const workflow = yaml.parse(frontmatterMatch[1]);
if (!workflow || !workflow.name || !workflow.description) {
if (debug) console.log(`[DEBUG] collectSkills: skipped (missing name/description): ${workflowPath}`);
} else {
// Build path relative from module root
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
const installPath = relativePath
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${workflowFile}`
: `${this.bmadFolderName}/${moduleName}/${workflowFile}`;
// Skills derive canonicalId from directory name — never from manifest
if (manifest && manifest.__single && manifest.__single.canonicalId) {
console.warn(
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
);
}
const canonicalId = path.basename(dir);
this.skills.push({
name: workflow.name,
description: this.cleanForCSV(workflow.description),
module: moduleName,
path: installPath,
canonicalId,
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
});
// Add to files list
this.files.push({
type: 'skill',
name: workflow.name,
module: moduleName,
path: installPath,
});
this.skillClaimedDirs.add(dir);
if (debug) {
console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`);
}
}
} }
workflow = yaml.parse(frontmatterMatch[1]); } catch (error) {
if (debug) console.log(`[DEBUG] collectSkills: failed to parse ${workflowPath}: ${error.message}`);
} }
if (!workflow || !workflow.name || !workflow.description) {
if (debug) console.log(`[DEBUG] collectSkills: skipped (missing name/description): ${workflowPath}`);
continue;
}
// Build path relative from module root
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
const installPath = relativePath
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${workflowFile}`
: `${this.bmadFolderName}/${moduleName}/${workflowFile}`;
// Skills derive canonicalId from directory name — never from manifest
if (manifest && manifest.__single && manifest.__single.canonicalId) {
console.warn(
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
);
}
const canonicalId = path.basename(dir);
this.skills.push({
name: workflow.name,
description: this.cleanForCSV(workflow.description),
module: moduleName,
path: installPath,
canonicalId,
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
});
// Add to files list
this.files.push({
type: 'skill',
name: workflow.name,
module: moduleName,
path: installPath,
});
this.skillClaimedDirs.add(dir);
if (debug) {
console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`);
}
break; // Successfully claimed — skip remaining workflow filenames
} catch (error) {
if (debug) console.log(`[DEBUG] collectSkills: failed to parse ${workflowPath}: ${error.message}`);
} }
} }
@ -260,11 +250,11 @@ class ManifestGenerator {
} }
} }
if (hasSkillType && debug) { if (hasSkillType && debug) {
const hasWorkflow = workflowFilenames.some((f) => entries.some((e) => e.name === f)); const hasWorkflow = entries.some((e) => e.name === workflowFile);
if (hasWorkflow) { if (hasWorkflow) {
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`); console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`);
} else { } else {
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md/workflow.yaml: ${dir}`); console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md: ${dir}`);
} }
} }
} }
@ -308,7 +298,7 @@ class ManifestGenerator {
} }
/** /**
* Recursively find and parse workflow.yaml and workflow.md files * Recursively find and parse workflow.md files
*/ */
async getWorkflowsFromPath(basePath, moduleName, subDir = 'workflows') { async getWorkflowsFromPath(basePath, moduleName, subDir = 'workflows') {
const workflows = []; const workflows = [];
@ -326,7 +316,7 @@ class ManifestGenerator {
return workflows; return workflows;
} }
// Recursively find workflow.yaml files // Recursively find workflow.md files
const findWorkflows = async (dir, relativePath = '') => { const findWorkflows = async (dir, relativePath = '') => {
// Skip directories already claimed as skills // Skip directories already claimed as skills
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dir)) return; if (this.skillClaimedDirs && this.skillClaimedDirs.has(dir)) return;
@ -345,7 +335,6 @@ class ManifestGenerator {
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
await findWorkflows(fullPath, newRelativePath); await findWorkflows(fullPath, newRelativePath);
} else if ( } else if (
entry.name === 'workflow.yaml' ||
entry.name === 'workflow.md' || entry.name === 'workflow.md' ||
(entry.name.startsWith('workflow-') && entry.name.endsWith('.md')) (entry.name.startsWith('workflow-') && entry.name.endsWith('.md'))
) { ) {
@ -358,21 +347,15 @@ class ManifestGenerator {
const rawContent = await fs.readFile(fullPath, 'utf8'); const rawContent = await fs.readFile(fullPath, 'utf8');
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
let workflow; // Parse MD workflow with YAML frontmatter
if (entry.name === 'workflow.yaml') { const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
// Parse YAML workflow if (!frontmatterMatch) {
workflow = yaml.parse(content); if (debug) {
} else { console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
// Parse MD workflow with YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
if (debug) {
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
}
continue; // Skip MD files without frontmatter
} }
workflow = yaml.parse(frontmatterMatch[1]); continue; // Skip MD files without frontmatter
} }
const workflow = yaml.parse(frontmatterMatch[1]);
if (debug) { if (debug) {
console.log(`[DEBUG] Parsed: name="${workflow.name}", description=${workflow.description ? 'OK' : 'MISSING'}`); console.log(`[DEBUG] Parsed: name="${workflow.name}", description=${workflow.description ? 'OK' : 'MISSING'}`);
@ -1343,7 +1326,7 @@ class ManifestGenerator {
// Check for manifest in this directory // Check for manifest in this directory
const manifest = await this.loadSkillManifest(dir); const manifest = await this.loadSkillManifest(dir);
if (manifest) { if (manifest) {
const type = this.getArtifactType(manifest, 'workflow.md') || this.getArtifactType(manifest, 'workflow.yaml'); const type = this.getArtifactType(manifest, 'workflow.md');
if (type === 'skill') return true; if (type === 'skill') return true;
} }

View File

@ -289,7 +289,7 @@ class BaseIdeSetup {
// Get core workflows // Get core workflows
const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows'); const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows');
if (await fs.pathExists(coreWorkflowsPath)) { if (await fs.pathExists(coreWorkflowsPath)) {
const coreWorkflows = await this.findWorkflowYamlFiles(coreWorkflowsPath); const coreWorkflows = await this.findWorkflowFiles(coreWorkflowsPath);
workflows.push( workflows.push(
...coreWorkflows.map((w) => ({ ...coreWorkflows.map((w) => ({
...w, ...w,
@ -304,7 +304,7 @@ class BaseIdeSetup {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') { if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows'); const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows');
if (await fs.pathExists(moduleWorkflowsPath)) { if (await fs.pathExists(moduleWorkflowsPath)) {
const moduleWorkflows = await this.findWorkflowYamlFiles(moduleWorkflowsPath); const moduleWorkflows = await this.findWorkflowFiles(moduleWorkflowsPath);
workflows.push( workflows.push(
...moduleWorkflows.map((w) => ({ ...moduleWorkflows.map((w) => ({
...w, ...w,
@ -324,11 +324,11 @@ class BaseIdeSetup {
} }
/** /**
* Recursively find workflow.yaml files * Recursively find workflow.md files
* @param {string} dir - Directory to search * @param {string} dir - Directory to search
* @returns {Array} List of workflow file info objects * @returns {Array} List of workflow file info objects
*/ */
async findWorkflowYamlFiles(dir) { async findWorkflowFiles(dir) {
const workflows = []; const workflows = [];
if (!(await fs.pathExists(dir))) { if (!(await fs.pathExists(dir))) {
@ -342,14 +342,17 @@ class BaseIdeSetup {
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Recursively search subdirectories // Recursively search subdirectories
const subWorkflows = await this.findWorkflowYamlFiles(fullPath); const subWorkflows = await this.findWorkflowFiles(fullPath);
workflows.push(...subWorkflows); workflows.push(...subWorkflows);
} else if (entry.isFile() && entry.name === 'workflow.yaml') { } else if (entry.isFile() && entry.name === 'workflow.md') {
// Read workflow.yaml to get name and standalone property // Read workflow.md frontmatter to get name and standalone property
try { try {
const yaml = require('yaml'); const yaml = require('yaml');
const content = await fs.readFile(fullPath, 'utf8'); const content = await fs.readFile(fullPath, 'utf8');
const workflowData = yaml.parse(content); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!frontmatterMatch) continue;
const workflowData = yaml.parse(frontmatterMatch[1]);
if (workflowData && workflowData.name) { if (workflowData && workflowData.name) {
// Workflows are standalone by default unless explicitly false // Workflows are standalone by default unless explicitly false

View File

@ -232,16 +232,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
for (const artifact of artifacts) { for (const artifact of artifacts) {
if (artifact.type === 'workflow-command') { if (artifact.type === 'workflow-command') {
// Use different template based on workflow type (YAML vs MD) const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`;
// Default to 'default' template type, but allow override via config const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, 'default-workflow');
const workflowTemplateType = artifact.isYamlWorkflow
? config.yaml_workflow_template || `${templateType}-workflow-yaml`
: config.md_workflow_template || `${templateType}-workflow`;
// Fall back to default templates if specific ones don't exist
const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
// workflowTemplateType already contains full name (e.g., 'gemini-workflow-yaml'), so pass empty artifactType
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'workflow', extension); const filename = this.generateFilename(artifact, 'workflow', extension);

View File

@ -63,7 +63,7 @@ function toDashPath(relativePath) {
} }
// Strip common file extensions to avoid double extensions in generated filenames // Strip common file extensions to avoid double extensions in generated filenames
// e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow' // e.g., 'create-story.xml' → 'create-story', 'workflow.md' → 'workflow'
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/); const parts = withoutExt.split(/[/\\]/);

View File

@ -9,7 +9,6 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD
*/ */
class WorkflowCommandGenerator { class WorkflowCommandGenerator {
constructor(bmadFolderName = BMAD_FOLDER_NAME) { constructor(bmadFolderName = BMAD_FOLDER_NAME) {
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
this.bmadFolderName = bmadFolderName; this.bmadFolderName = bmadFolderName;
} }
@ -67,7 +66,7 @@ class WorkflowCommandGenerator {
for (const workflow of allWorkflows) { for (const workflow of allWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir); const commandContent = await this.generateCommandContent(workflow, bmadDir);
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.yaml) // Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
let workflowRelPath = workflow.path || ''; let workflowRelPath = workflow.path || '';
// Normalize path separators for cross-platform compatibility // Normalize path separators for cross-platform compatibility
workflowRelPath = workflowRelPath.replaceAll('\\', '/'); workflowRelPath = workflowRelPath.replaceAll('\\', '/');
@ -85,11 +84,8 @@ class WorkflowCommandGenerator {
workflowRelPath = `${match[1]}/${match[2]}`; workflowRelPath = `${match[1]}/${match[2]}`;
} }
} }
// 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({ artifacts.push({
type: 'workflow-command', type: 'workflow-command',
isYamlWorkflow: isYamlWorkflow, // For template selection
name: workflow.name, name: workflow.name,
description: workflow.description || `${workflow.name} workflow`, description: workflow.description || `${workflow.name} workflow`,
module: workflow.module, module: workflow.module,
@ -125,17 +121,14 @@ class WorkflowCommandGenerator {
* Generate command content for a workflow * Generate command content for a workflow
*/ */
async generateCommandContent(workflow, bmadDir) { async generateCommandContent(workflow, bmadDir) {
// Determine template based on workflow file type const templatePath = path.join(__dirname, '../templates/workflow-commander.md');
const isMarkdownWorkflow = workflow.path.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 // Load the template
const template = await fs.readFile(templatePath, 'utf8'); const template = await fs.readFile(templatePath, 'utf8');
// Convert source path to installed path // Convert source path to installed path
// From: /Users/.../src/bmm/workflows/.../workflow.yaml // From: /Users/.../src/bmm/workflows/.../workflow.md
// To: {project-root}/_bmad/bmm/workflows/.../workflow.yaml // To: {project-root}/_bmad/bmm/workflows/.../workflow.md
let workflowPath = workflow.path; let workflowPath = workflow.path;
// Extract the relative path from source // Extract the relative path from source
@ -218,10 +211,9 @@ class WorkflowCommandGenerator {
## Execution ## Execution
When running any workflow: When running any workflow:
1. LOAD {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml 1. LOAD the workflow.md file at the path shown above
2. Pass the workflow path as 'workflow-config' parameter 2. READ its entire contents and follow its directions exactly
3. Follow workflow.xml instructions EXACTLY 3. Save outputs after EACH section
4. Save outputs after EACH section
## Modes ## Modes
- Normal: Full interaction - Normal: Full interaction

View File

@ -762,14 +762,8 @@ class ModuleManager {
} }
} }
// Check if this is a workflow.yaml file // Copy the file with placeholder replacement
if (file.endsWith('workflow.yaml')) { await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
await fs.ensureDir(path.dirname(targetFile));
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
} else {
// Copy the file with placeholder replacement
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
}
// Track the file if callback provided // Track the file if callback provided
if (fileTrackingCallback) { if (fileTrackingCallback) {
@ -778,91 +772,6 @@ class ModuleManager {
} }
} }
/**
* Copy workflow.yaml file with web_bundle section stripped
* Preserves comments, formatting, and line breaks
* @param {string} sourceFile - Source workflow.yaml file path
* @param {string} targetFile - Target workflow.yaml file path
*/
async copyWorkflowYamlStripped(sourceFile, targetFile) {
// Read the source YAML file
let yamlContent = await fs.readFile(sourceFile, 'utf8');
// IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML
// Otherwise parsing will fail on the placeholder
yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName);
try {
// First check if web_bundle exists by parsing
const workflowConfig = yaml.parse(yamlContent);
if (workflowConfig.web_bundle === undefined) {
// No web_bundle section, just write (placeholders already replaced above)
await fs.writeFile(targetFile, yamlContent, 'utf8');
return;
}
// Find the line that starts web_bundle
const lines = yamlContent.split('\n');
let startIdx = -1;
let endIdx = -1;
let baseIndent = 0;
// Find the start of web_bundle section
for (const [i, line] of lines.entries()) {
const match = line.match(/^(\s*)web_bundle:/);
if (match) {
startIdx = i;
baseIndent = match[1].length;
break;
}
}
if (startIdx === -1) {
// web_bundle not found in text (shouldn't happen), copy as-is
await fs.writeFile(targetFile, yamlContent, 'utf8');
return;
}
// Find the end of web_bundle section
// It ends when we find a line with same or less indentation that's not empty/comment
endIdx = startIdx;
for (let i = startIdx + 1; i < lines.length; i++) {
const line = lines[i];
// Skip empty lines and comments
if (line.trim() === '' || line.trim().startsWith('#')) {
continue;
}
// Check indentation
const indent = line.match(/^(\s*)/)[1].length;
if (indent <= baseIndent) {
// Found next section at same or lower indentation
endIdx = i - 1;
break;
}
}
// If we didn't find an end, it goes to end of file
if (endIdx === startIdx) {
endIdx = lines.length - 1;
}
// Remove the web_bundle section (including the line before if it's just a blank line)
const newLines = [...lines.slice(0, startIdx), ...lines.slice(endIdx + 1)];
// Clean up any double blank lines that might result
const strippedYaml = newLines.join('\n').replaceAll(/\n\n\n+/g, '\n\n');
// Placeholders already replaced at the beginning of this function
await fs.writeFile(targetFile, strippedYaml, 'utf8');
} catch {
// If anything fails, just copy the file as-is
await prompts.log.warn(` Could not process ${path.basename(sourceFile)}, copying as-is`);
await fs.copy(sourceFile, targetFile, { overwrite: true });
}
}
/** /**
* Compile .agent.yaml files to .md format in modules * Compile .agent.yaml files to .md format in modules
@ -1169,9 +1078,7 @@ class ModuleManager {
const installWorkflowPath = item['workflow-install']; // Where to copy TO const installWorkflowPath = item['workflow-install']; // Where to copy TO
// Parse SOURCE workflow path // Parse SOURCE workflow path
// Handle both _bmad placeholder and hardcoded 'bmad' // Example: {project-root}/_bmad/bmm/workflows/4-implementation/create-story/workflow.md
// Example: {project-root}/_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/); const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch) { if (!sourceMatch) {
await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`); await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`);
@ -1181,8 +1088,7 @@ class ModuleManager {
const [, sourceModule, sourceWorkflowSubPath] = sourceMatch; const [, sourceModule, sourceWorkflowSubPath] = sourceMatch;
// Parse INSTALL workflow path // Parse INSTALL workflow path
// Handle_bmad // Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.md
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml
const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/); const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) { if (!installMatch) {
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`); await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`);
@ -1192,9 +1098,9 @@ class ModuleManager {
const installWorkflowSubPath = installMatch[2]; const installWorkflowSubPath = installMatch[2];
const sourceModulePath = getModulePath(sourceModule); const sourceModulePath = getModulePath(sourceModule);
const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')); const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.md$/, ''));
const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')); const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.md$/, ''));
// Check if source workflow exists // Check if source workflow exists
if (!(await fs.pathExists(actualSourceWorkflowPath))) { if (!(await fs.pathExists(actualSourceWorkflowPath))) {
@ -1204,18 +1110,12 @@ class ModuleManager {
// Copy the entire workflow folder // Copy the entire workflow folder
await prompts.log.message( await prompts.log.message(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`, ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.md$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.md$/, '')}`,
); );
await fs.ensureDir(path.dirname(actualDestWorkflowPath)); await fs.ensureDir(path.dirname(actualDestWorkflowPath));
// Copy the workflow directory recursively with placeholder replacement // Copy the workflow directory recursively with placeholder replacement
await this.copyDirectoryWithPlaceholderReplacement(actualSourceWorkflowPath, actualDestWorkflowPath); await this.copyDirectoryWithPlaceholderReplacement(actualSourceWorkflowPath, actualDestWorkflowPath);
// Update the workflow.yaml config_source reference
const workflowYamlPath = path.join(actualDestWorkflowPath, 'workflow.yaml');
if (await fs.pathExists(workflowYamlPath)) {
await this.updateWorkflowConfigSource(workflowYamlPath, moduleName);
}
} }
} }
@ -1224,27 +1124,6 @@ class ModuleManager {
} }
} }
/**
* Update workflow.yaml config_source to point to new module
* @param {string} workflowYamlPath - Path to workflow.yaml file
* @param {string} newModuleName - New module name to reference
*/
async updateWorkflowConfigSource(workflowYamlPath, newModuleName) {
let yamlContent = await fs.readFile(workflowYamlPath, 'utf8');
// Replace config_source: "{project-root}/_bmad/OLD_MODULE/config.yaml"
// with config_source: "{project-root}/_bmad/NEW_MODULE/config.yaml"
// Note: At this point _bmad has already been replaced with actual folder name
const configSourcePattern = /config_source:\s*["']?\{project-root\}\/[^/]+\/[^/]+\/config\.yaml["']?/g;
const newConfigSource = `config_source: "{project-root}/${this.bmadFolderName}/${newModuleName}/config.yaml"`;
const updatedYaml = yamlContent.replaceAll(configSourcePattern, newConfigSource);
if (updatedYaml !== yamlContent) {
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
await prompts.log.message(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`);
}
}
/** /**
* Create directories declared in module.yaml's `directories` key * Create directories declared in module.yaml's `directories` key

View File

@ -39,12 +39,7 @@ class AgentAnalyzer {
if (Array.isArray(execArray)) { if (Array.isArray(execArray)) {
for (const exec of execArray) { for (const exec of execArray) {
if (exec.route) { if (exec.route) {
// Check if route is a workflow or exec profile.usedAttributes.add('exec');
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
profile.usedAttributes.add('workflow');
} else {
profile.usedAttributes.add('exec');
}
} }
if (exec.workflow) profile.usedAttributes.add('workflow'); if (exec.workflow) profile.usedAttributes.add('workflow');
if (exec.action) profile.usedAttributes.add('action'); if (exec.action) profile.usedAttributes.add('action');

View File

@ -229,12 +229,7 @@ function processExecArray(execArray) {
} }
if (exec.route) { if (exec.route) {
// Determine if it's a workflow or exec based on file extension or context result.route = exec.route;
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
result.workflow = exec.route;
} else {
result.route = exec.route;
}
} }
if (exec.data !== null && exec.data !== undefined) { if (exec.data !== null && exec.data !== undefined) {