feat: Extract BMGD module and implement workflow vendoring

This commit extracts game development functionality from BMM into a standalone
BMGD (BMad Game Development) module and implements workflow vendoring to enable
module independence.

BMGD Module Creation:
- Moved agents: game-designer, game-dev, game-architect from BMM to BMGD
- Moved team config: team-gamedev
- Created new Game Dev Scrum Master agent using workflow vendoring pattern
- Reorganized workflows into industry-standard game dev phases:
  * Phase 1 (Preproduction): brainstorm-game, game-brief
  * Phase 2 (Design): gdd, narrative
  * Phase 3 (Technical): game-architecture
  * Phase 4 (Production): vendored from BMM workflows
- Updated all module metadata and config_source references

Workflow Vendoring Feature:
- Enables modules to copy workflows from other modules during installation
- Build-time process that updates config_source in vendored workflows
- New agent YAML attribute: workflow-install (build-time metadata)
- Final compiled agents use workflow-install value for workflow attribute
- Implementation in module manager: vendorCrossModuleWorkflows()
- Allows standalone module installation without forced dependencies

Technical Changes:
- tools/cli/lib/yaml-xml-builder.js: Use workflow-install for workflow attribute
- tools/cli/installers/lib/modules/manager.js: Add vendoring functions
- tools/schema/agent.js: Add workflow-install to menu item schema
- Updated 3 documentation files with workflow vendoring details

BMM Workflow Updates:
- workflow-status/init: Added game detection checkpoint
- workflow-status/paths/game-design.yaml: Redirect to BMGD module
- prd/instructions.md: Route game projects to BMGD
- research/instructions-market.md: Reference BMGD for game development

Documentation:
- Created comprehensive BMGD module README
- Added workflow vendoring documentation
- Updated BMB agent creation and module creation guides
This commit is contained in:
Brian Madison
2025-11-05 20:44:22 -06:00
parent bc76d25be6
commit f84e18760f
66 changed files with 2527 additions and 226 deletions

View File

@@ -112,6 +112,10 @@ class ModuleManager {
await fs.remove(targetPath);
}
// Vendor cross-module workflows BEFORE copying
// This reads source agent.yaml files and copies referenced workflows
await this.vendorCrossModuleWorkflows(sourcePath, targetPath, moduleName);
// Copy module files with filtering
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
@@ -435,6 +439,130 @@ class ModuleManager {
}
}
/**
* Vendor cross-module workflows referenced in agent files
* Scans SOURCE agent.yaml files for workflow-install and copies workflows to destination
* @param {string} sourcePath - Source module path
* @param {string} targetPath - Target module path (destination)
* @param {string} moduleName - Module name being installed
*/
async vendorCrossModuleWorkflows(sourcePath, targetPath, moduleName) {
const sourceAgentsPath = path.join(sourcePath, 'agents');
// Check if source agents directory exists
if (!(await fs.pathExists(sourceAgentsPath))) {
return; // No agents to process
}
// Get all agent YAML files from source
const agentFiles = await fs.readdir(sourceAgentsPath);
const yamlFiles = agentFiles.filter((f) => f.endsWith('.agent.yaml') || f.endsWith('.yaml'));
if (yamlFiles.length === 0) {
return; // No YAML agent files
}
let workflowsVendored = false;
for (const agentFile of yamlFiles) {
const agentPath = path.join(sourceAgentsPath, agentFile);
const agentYaml = yaml.load(await fs.readFile(agentPath, 'utf8'));
// Check if agent has menu items with workflow-install
const menuItems = agentYaml?.agent?.menu || [];
const workflowInstallItems = menuItems.filter((item) => item['workflow-install']);
if (workflowInstallItems.length === 0) {
continue; // No workflow-install in this agent
}
if (!workflowsVendored) {
console.log(chalk.cyan(`\n Vendoring cross-module workflows for ${moduleName}...`));
workflowsVendored = true;
}
console.log(chalk.dim(` Processing: ${agentFile}`));
for (const item of workflowInstallItems) {
const sourceWorkflowPath = item.workflow; // Where to copy FROM
const installWorkflowPath = item['workflow-install']; // Where to copy TO
// Parse SOURCE workflow path
// Example: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/bmad\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch) {
console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`));
continue;
}
const [, sourceModule, sourceWorkflowSubPath] = sourceMatch;
// Parse INSTALL workflow path
// Example: {project-root}/bmad/bmgd/workflows/4-production/create-story/workflow.yaml
const installMatch = installWorkflowPath.match(/\{project-root\}\/bmad\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) {
console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`));
continue;
}
const installWorkflowSubPath = installMatch[2];
// Determine actual filesystem paths
const sourceModulePath = path.join(this.modulesSourcePath, sourceModule);
const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, ''));
const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.yaml$/, ''));
// Check if source workflow exists
if (!(await fs.pathExists(actualSourceWorkflowPath))) {
console.warn(chalk.yellow(` Source workflow not found: ${actualSourceWorkflowPath}`));
continue;
}
// Copy the entire workflow folder
console.log(
chalk.dim(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`,
),
);
await fs.ensureDir(path.dirname(actualDestWorkflowPath));
await fs.copy(actualSourceWorkflowPath, actualDestWorkflowPath, { overwrite: true });
// Update the workflow.yaml config_source reference
const workflowYamlPath = path.join(actualDestWorkflowPath, 'workflow.yaml');
if (await fs.pathExists(workflowYamlPath)) {
await this.updateWorkflowConfigSource(workflowYamlPath, moduleName);
}
}
}
if (workflowsVendored) {
console.log(chalk.green(` ✓ Workflow vendoring complete\n`));
}
}
/**
* 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"
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);
if (updatedYaml !== yamlContent) {
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
console.log(chalk.dim(` Updated config_source to: bmad/${newModuleName}/config.yaml`));
}
}
/**
* Run module-specific installer if it exists
* @param {string} moduleName - Name of the module

View File

@@ -311,7 +311,15 @@ class YamlXmlBuilder {
const attrs = [`cmd="${trigger}"`];
// Add handler attributes
if (item.workflow) attrs.push(`workflow="${item.workflow}"`);
// If workflow-install exists, use its value for workflow attribute (vendoring)
// workflow-install is build-time metadata - tells installer where to copy workflows
// The final XML should only have workflow pointing to the install location
if (item['workflow-install']) {
attrs.push(`workflow="${item['workflow-install']}"`);
} else if (item.workflow) {
attrs.push(`workflow="${item.workflow}"`);
}
if (item['validate-workflow']) attrs.push(`validate-workflow="${item['validate-workflow']}"`);
if (item.exec) attrs.push(`exec="${item.exec}"`);
if (item.tmpl) attrs.push(`tmpl="${item.tmpl}"`);

View File

@@ -170,6 +170,7 @@ function buildMenuItemSchema() {
trigger: createNonEmptyString('agent.menu[].trigger'),
description: createNonEmptyString('agent.menu[].description'),
workflow: createNonEmptyString('agent.menu[].workflow').optional(),
'workflow-install': createNonEmptyString('agent.menu[].workflow-install').optional(),
'validate-workflow': createNonEmptyString('agent.menu[].validate-workflow').optional(),
exec: createNonEmptyString('agent.menu[].exec').optional(),
action: createNonEmptyString('agent.menu[].action').optional(),