feat: Add workflow vendoring to web bundler

The web bundler now performs workflow vendoring before bundling agents,
similar to the module installer. This ensures that workflows referenced
via workflow-install attributes are copied from their source locations
to their destination locations before the bundler attempts to resolve
and bundle them.

Changes:
- Added vendorCrossModuleWorkflows() method to WebBundler class
- Added updateWorkflowConfigSource() helper method
- Integrated vendoring into bundleAll(), bundleModule(), and bundleAgent()
- Workflows are vendored before agent discovery and bundling
- Config_source is updated in vendored workflows to reference target module

This fixes missing dependency warnings for BMGD agents that vendor
BMM workflows for Phase 4 (Production) workflows.
This commit is contained in:
Brian Madison 2025-11-05 21:05:08 -06:00
parent f84e18760f
commit 281eac3373
1 changed files with 103 additions and 1 deletions

View File

@ -51,8 +51,13 @@ class WebBundler {
console.log(chalk.cyan.bold('═══════════════════════════════════════════════\n')); console.log(chalk.cyan.bold('═══════════════════════════════════════════════\n'));
try { try {
// Pre-discover all modules to generate complete manifests // Vendor cross-module workflows FIRST
const modules = await this.discoverModules(); const modules = await this.discoverModules();
for (const module of modules) {
await this.vendorCrossModuleWorkflows(module);
}
// Pre-discover all modules to generate complete manifests
for (const module of modules) { for (const module of modules) {
await this.preDiscoverModule(module); await this.preDiscoverModule(module);
} }
@ -92,6 +97,9 @@ class WebBundler {
teams: [], teams: [],
}; };
// Vendor cross-module workflows first (if not already done by bundleAll)
await this.vendorCrossModuleWorkflows(moduleName);
// Pre-discover all agents and teams for manifest generation // Pre-discover all agents and teams for manifest generation
await this.preDiscoverModule(moduleName); await this.preDiscoverModule(moduleName);
@ -134,6 +142,9 @@ class WebBundler {
console.log(chalk.dim(` → Processing: ${agentName}`)); console.log(chalk.dim(` → Processing: ${agentName}`));
// Vendor cross-module workflows first (if not already done)
await this.vendorCrossModuleWorkflows(moduleName);
const agentPath = path.join(this.modulesPath, moduleName, 'agents', agentFile); const agentPath = path.join(this.modulesPath, moduleName, 'agents', agentFile);
// Check if agent file exists // Check if agent file exists
@ -433,6 +444,97 @@ class WebBundler {
return parts.join('\n'); return parts.join('\n');
} }
/**
* Vendor cross-module workflows for a module
* Scans source agent YAML files for workflow-install attributes and copies workflows
*/
async vendorCrossModuleWorkflows(moduleName) {
const modulePath = path.join(this.modulesPath, moduleName);
const agentsPath = path.join(modulePath, 'agents');
if (!(await fs.pathExists(agentsPath))) {
return;
}
// Find all agent YAML files
const files = await fs.readdir(agentsPath);
const yamlFiles = files.filter((f) => f.endsWith('.agent.yaml'));
for (const agentFile of yamlFiles) {
const agentPath = path.join(agentsPath, agentFile);
const agentYaml = yaml.load(await fs.readFile(agentPath, 'utf8'));
const menuItems = agentYaml?.agent?.menu || [];
const workflowInstallItems = menuItems.filter((item) => item['workflow-install']);
for (const item of workflowInstallItems) {
const sourceWorkflowPath = item.workflow;
const installWorkflowPath = item['workflow-install'];
if (!sourceWorkflowPath || !installWorkflowPath) {
continue;
}
// Parse paths to extract module and workflow location
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/bmad\/([^/]+)\/workflows\/(.+)/);
const installMatch = installWorkflowPath.match(/\{project-root\}\/bmad\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch || !installMatch) {
continue;
}
const sourceModule = sourceMatch[1];
const sourceWorkflowRelPath = sourceMatch[2];
const installModule = installMatch[1];
const installWorkflowRelPath = installMatch[2];
// Build actual filesystem paths
const actualSourceWorkflowPath = path.join(this.modulesPath, sourceModule, 'workflows', sourceWorkflowRelPath);
const actualDestWorkflowPath = path.join(this.modulesPath, installModule, 'workflows', installWorkflowRelPath);
// Check if source workflow exists
if (!(await fs.pathExists(actualSourceWorkflowPath))) {
console.log(chalk.yellow(` ⚠ Source workflow not found for vendoring: ${sourceWorkflowPath}`));
continue;
}
// Check if destination already exists (skip if already vendored)
if (await fs.pathExists(actualDestWorkflowPath)) {
continue;
}
// Get workflow directory (workflow.yaml is in a directory with other files)
const sourceWorkflowDir = path.dirname(actualSourceWorkflowPath);
const destWorkflowDir = path.dirname(actualDestWorkflowPath);
// Copy entire workflow directory
await fs.copy(sourceWorkflowDir, destWorkflowDir, { overwrite: false });
// Update config_source in the vendored workflow.yaml
const workflowYamlPath = actualDestWorkflowPath;
if (await fs.pathExists(workflowYamlPath)) {
await this.updateWorkflowConfigSource(workflowYamlPath, installModule);
}
console.log(chalk.dim(` → Vendored workflow: ${sourceWorkflowRelPath}${installModule}/workflows/${installWorkflowRelPath}`));
}
}
}
/**
* Update config_source in a vendored workflow YAML file
*/
async updateWorkflowConfigSource(workflowYamlPath, newModuleName) {
let yamlContent = await fs.readFile(workflowYamlPath, 'utf8');
// Replace config_source with new module reference
const configSourcePattern = /config_source:\s*["']?\{project-root\}\/bmad\/[^/]+\/config\.yaml["']?/g;
const newConfigSource = `config_source: "{project-root}/bmad/${newModuleName}/config.yaml"`;
const updatedYaml = yamlContent.replaceAll(configSourcePattern, newConfigSource);
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
}
/** /**
* Pre-discover all agents and teams in a module for manifest generation * Pre-discover all agents and teams in a module for manifest generation
*/ */