diff --git a/src/core/module.yaml b/src/core/module.yaml
index 22712f9a..af89aaa0 100644
--- a/src/core/module.yaml
+++ b/src/core/module.yaml
@@ -2,31 +2,26 @@ header: "BMADโข Core Configuration"
subheader: "Configure the core settings for your BMADโข installation.\nThese settings will be used across all modules and agents."
user_name:
- prompt: "What shall the agents call you?"
+ prompt: "What shall the agents call you (TIP: Use a team name if using with a group)?"
default: "BMad"
result: "{value}"
communication_language:
- prompt: "Preferred Chat Language/Style? (English, Mandarin, English Pirate, etc...)"
+ prompt: "Preferred chat language/style? (English, Mandarin, English Pirate, etc...)"
default: "English"
result: "{value}"
document_output_language:
- prompt: "Preferred Document Output Language?"
+ prompt: "Preferred document output language?"
default: "{communication_language}"
result: "{value}"
+output_folder:
+ prompt: "Where should AI generated artifacts be saved across all modules?"
+ default: "docs"
+ result: "{project-root}/{value}"
+
agent_sidecar_folder:
prompt: "Where should users agent sidecar memory folders be stored?"
default: ".bmad-user-memory"
result: "{project-root}/{value}"
-
-output_folder:
- prompt: "Where should AI Generated Artifacts be saved across all modules?"
- default: "docs"
- result: "{project-root}/{value}"
-
-install_user_docs:
- prompt: "Install user documentation and optimized agent intelligence to each selected modules docs folder?"
- default: true
- result: "{value}"
diff --git a/src/modules/bmb/module.yaml b/src/modules/bmb/module.yaml
index 1329cd63..77dc1ce3 100644
--- a/src/modules/bmb/module.yaml
+++ b/src/modules/bmb/module.yaml
@@ -11,8 +11,6 @@ subheader: "Configure the settings for the BoMB Factory!\nThe agent, workflow an
## user_name
## communication_language
## output_folder
-## install_user_docs
-## kb_install
custom_stand_alone_location:
prompt: "Where do custom agents and workflows get stored?"
diff --git a/src/modules/bmm/module.yaml b/src/modules/bmm/module.yaml
index ed988217..2dd59d28 100644
--- a/src/modules/bmm/module.yaml
+++ b/src/modules/bmm/module.yaml
@@ -11,8 +11,6 @@ subheader: "Agent and Workflow Configuration for this module"
## user_name
## communication_language
## output_folder
-## install_user_docs
-## kb_install
project_name:
prompt: "What is the title of your project you will be working on?"
@@ -21,9 +19,8 @@ project_name:
user_skill_level:
prompt:
- - "What is your technical experience level?"
- - "This affects how agents explain concepts to you (NOT document content)."
- - "Documents are always concise for LLM efficiency."
+ - "What is your development experience level?"
+ - "This affects how agents explain concepts in chat."
default: "intermediate"
result: "{value}"
single-select:
@@ -35,7 +32,7 @@ user_skill_level:
label: "Expert - Deep technical knowledge, be direct and technical"
sprint_artifacts:
- prompt: "Where should Sprint Artifacts be stored (sprint status, stories, story context, temp context, etc...)?"
+ prompt: "Where should sprint artifacts be stored (sprint status, stories, retrospectives)?"
default: "{output_folder}/sprint-artifacts"
result: "{project-root}/{value}"
diff --git a/src/modules/bmm/tasks/daily-standup.xml b/src/modules/bmm/tasks/daily-standup.xml
deleted file mode 100644
index d41c362c..00000000
--- a/src/modules/bmm/tasks/daily-standup.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
- MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER
- DO NOT skip steps or change the sequence
- HALT immediately when halt-conditions are met
- Each action tag within a step tag is a REQUIRED action to complete that step
- Sections outside flow (validation, output, critical-context) provide essential context - review and apply throughout execution
-
-
-
- Check for stories folder at {project-root}{output_folder}/stories/
- Find current story by identifying highest numbered story file
- Read story status (In Progress, Ready for Review, etc.)
- Extract agent notes from Dev Agent Record, TEA Results, PO Notes sections
- Check for next story references from epics
- Identify blockers from story sections
-
-
-
-
-
-
-
- Each agent provides three items referencing real story data
- What I see: Their perspective on current work, citing story sections (1-2 sentences)
- What concerns me: Issues from their domain or story blockers (1-2 sentences)
- What I suggest: Actionable recommendations for progress (1-2 sentences)
-
-
-
-
-
-
-
-
-
- Primary: Sarah (PO), Mary (Analyst), Winston (Architect)
- Secondary: Murat (TEA), James (Dev)
-
-
- Primary: Sarah (PO), Bob (SM), James (Dev)
- Secondary: Murat (TEA)
-
-
- Primary: Winston (Architect), James (Dev), Murat (TEA)
- Secondary: Sarah (PO)
-
-
- Primary: James (Dev), Murat (TEA), Winston (Architect)
- Secondary: Sarah (PO)
-
-
-
-
- This task extends party-mode with agile-specific structure
- Time-box responses (standup = brief)
- Focus on actionable items from real story data when available
- End with clear next steps
- No deep dives (suggest breakout if needed)
- If no stories folder detected, run general standup format
-
-
\ No newline at end of file
diff --git a/src/modules/cis/module.yaml b/src/modules/cis/module.yaml
index cd4cdfb2..b188b0ad 100644
--- a/src/modules/cis/module.yaml
+++ b/src/modules/cis/module.yaml
@@ -10,6 +10,3 @@ subheader: "No Configuration needed - uses Core Config only."
## user_name
## communication_language
## output_folder
-## install_user_docs
-## kb_install
-
diff --git a/tools/cli/bundlers/bundle-web.js b/tools/cli/bundlers/bundle-web.js
deleted file mode 100755
index 8bb84868..00000000
--- a/tools/cli/bundlers/bundle-web.js
+++ /dev/null
@@ -1,179 +0,0 @@
-const { WebBundler } = require('./web-bundler');
-const chalk = require('chalk');
-const { program } = require('commander');
-const path = require('node:path');
-const fs = require('fs-extra');
-
-program.name('bundle-web').description('Generate web bundles for BMAD agents and teams').version('1.0.0');
-
-program
- .command('all')
- .description('Bundle all modules')
- .option('-o, --output ', 'Output directory', 'web-bundles')
- .action(async (options) => {
- try {
- const bundler = new WebBundler(null, options.output);
- await bundler.bundleAll();
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-program
- .command('rebundle')
- .description('Clean and rebundle all modules')
- .option('-o, --output ', 'Output directory', 'web-bundles')
- .action(async (options) => {
- try {
- // Clean output directory first
- const outputDir = path.isAbsolute(options.output) ? options.output : path.join(process.cwd(), options.output);
-
- if (await fs.pathExists(outputDir)) {
- console.log(chalk.cyan(`๐งน Cleaning ${options.output}...`));
- await fs.emptyDir(outputDir);
- }
-
- // Bundle all
- const bundler = new WebBundler(null, options.output);
- await bundler.bundleAll();
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-program
- .command('module ')
- .description('Bundle a specific module')
- .option('-o, --output ', 'Output directory', 'web-bundles')
- .action(async (moduleName, options) => {
- try {
- const bundler = new WebBundler(null, options.output);
- const result = await bundler.bundleModule(moduleName);
-
- if (result.agents.length === 0 && result.teams.length === 0) {
- console.log(chalk.yellow(`No agents or teams found in module: ${moduleName}`));
- } else {
- console.log(chalk.green(`\nโจ Successfully bundled ${result.agents.length} agents and ${result.teams.length} teams`));
- }
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-program
- .command('agent ')
- .description('Bundle a specific agent')
- .option('-o, --output ', 'Output directory', 'web-bundles')
- .action(async (moduleName, agentFile, options) => {
- try {
- const bundler = new WebBundler(null, options.output);
-
- // Ensure .md extension
- if (!agentFile.endsWith('.md')) {
- agentFile += '.md';
- }
-
- // Pre-discover module for complete manifests
- await bundler.preDiscoverModule(moduleName);
-
- await bundler.bundleAgent(moduleName, agentFile, false);
- console.log(chalk.green(`\nโจ Successfully bundled agent: ${agentFile}`));
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-program
- .command('team ')
- .description('Bundle a specific team')
- .option('-o, --output ', 'Output directory', 'web-bundles')
- .action(async (moduleName, teamFile, options) => {
- try {
- const bundler = new WebBundler(null, options.output);
-
- // Ensure .yaml or .yml extension
- if (!teamFile.endsWith('.yaml') && !teamFile.endsWith('.yml')) {
- teamFile += '.yaml';
- }
-
- // Pre-discover module for complete manifests
- await bundler.preDiscoverModule(moduleName);
-
- await bundler.bundleTeam(moduleName, teamFile);
- console.log(chalk.green(`\nโจ Successfully bundled team: ${teamFile}`));
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-program
- .command('list')
- .description('List available modules and agents')
- .action(async () => {
- try {
- const bundler = new WebBundler();
- const modules = await bundler.discoverModules();
-
- console.log(chalk.cyan.bold('\n๐ฆ Available Modules:\n'));
-
- for (const module of modules) {
- console.log(chalk.bold(` ${module}/`));
-
- const modulePath = path.join(bundler.modulesPath, module);
- const agents = await bundler.discoverAgents(modulePath);
- const teams = await bundler.discoverTeams(modulePath);
-
- if (agents.length > 0) {
- console.log(chalk.gray(' agents/'));
- for (const agent of agents) {
- console.log(chalk.gray(` - ${agent}`));
- }
- }
-
- if (teams.length > 0) {
- console.log(chalk.gray(' teams/'));
- for (const team of teams) {
- console.log(chalk.gray(` - ${team}`));
- }
- }
- }
-
- console.log('');
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-program
- .command('clean')
- .description('Remove all web bundles')
- .action(async () => {
- try {
- const fs = require('fs-extra');
- const outputDir = path.join(process.cwd(), 'web-bundles');
-
- if (await fs.pathExists(outputDir)) {
- await fs.remove(outputDir);
- console.log(chalk.green('โ Web bundles directory cleaned'));
- } else {
- console.log(chalk.yellow('Web bundles directory does not exist'));
- }
- } catch (error) {
- console.error(chalk.red('Error:'), error.message);
- process.exit(1);
- }
- });
-
-// Parse command line arguments
-program.parse(process.argv);
-
-// Show help if no command provided
-if (process.argv.slice(2).length === 0) {
- program.outputHelp();
-}
diff --git a/tools/cli/bundlers/test-analyst.js b/tools/cli/bundlers/test-analyst.js
deleted file mode 100644
index 88c75954..00000000
--- a/tools/cli/bundlers/test-analyst.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const { WebBundler } = require('./web-bundler');
-const chalk = require('chalk');
-const path = require('node:path');
-
-async function testAnalystBundle() {
- console.log(chalk.cyan.bold('\n๐งช Testing Analyst Agent Bundle\n'));
-
- try {
- const bundler = new WebBundler();
-
- // Load web activation first
- await bundler.loadWebActivation();
-
- // Bundle just the analyst agent from bmm module
- // Only bundle the analyst for testing
- const agentPath = path.join(bundler.modulesPath, 'bmm', 'agents', 'analyst.md');
- await bundler.bundleAgent('bmm', 'analyst.md');
-
- console.log(chalk.green.bold('\nโ
Test completed successfully!\n'));
- } catch (error) {
- console.error(chalk.red('\nโ Test failed:'), error.message);
- console.error(error.stack);
- process.exit(1);
- }
-}
-
-// Run test
-testAnalystBundle();
diff --git a/tools/cli/bundlers/test-bundler.js b/tools/cli/bundlers/test-bundler.js
deleted file mode 100755
index 6e17cc2e..00000000
--- a/tools/cli/bundlers/test-bundler.js
+++ /dev/null
@@ -1,118 +0,0 @@
-const { WebBundler } = require('./web-bundler');
-const chalk = require('chalk');
-const fs = require('fs-extra');
-const path = require('node:path');
-
-async function testWebBundler() {
- console.log(chalk.cyan.bold('\n๐งช Testing Web Bundler\n'));
-
- const bundler = new WebBundler();
- let passedTests = 0;
- let failedTests = 0;
-
- // Test 1: Load web activation
- try {
- await bundler.loadWebActivation();
- console.log(chalk.green('โ Web activation loaded successfully'));
- passedTests++;
- } catch (error) {
- console.error(chalk.red('โ Failed to load web activation:'), error.message);
- failedTests++;
- }
-
- // Test 2: Discover modules
- try {
- const modules = await bundler.discoverModules();
- console.log(chalk.green(`โ Discovered ${modules.length} modules:`, modules.join(', ')));
- passedTests++;
- } catch (error) {
- console.error(chalk.red('โ Failed to discover modules:'), error.message);
- failedTests++;
- }
-
- // Test 3: Bundle analyst agent
- try {
- const result = await bundler.bundleAgent('bmm', 'analyst.md');
-
- // Check if bundle was created
- const bundlePath = path.join(bundler.outputDir, 'bmm', 'agents', 'analyst.xml');
- if (await fs.pathExists(bundlePath)) {
- const content = await fs.readFile(bundlePath, 'utf8');
-
- // Validate bundle structure
- const hasAgent = content.includes('');
- const activationBeforePersona = content.indexOf('');
- const hasManifests =
- content.includes('') && content.includes('');
- const hasDependencies = content.includes('');
-
- console.log(chalk.green('โ Analyst bundle created successfully'));
- console.log(chalk.gray(` - Has agent tag: ${hasAgent ? 'โ' : 'โ'}`));
- console.log(chalk.gray(` - Has activation: ${hasActivation ? 'โ' : 'โ'}`));
- console.log(chalk.gray(` - Has persona: ${hasPersona ? 'โ' : 'โ'}`));
- console.log(chalk.gray(` - Activation before persona: ${activationBeforePersona ? 'โ' : 'โ'}`));
- console.log(chalk.gray(` - Has manifests: ${hasManifests ? 'โ' : 'โ'}`));
- console.log(chalk.gray(` - Has dependencies: ${hasDependencies ? 'โ' : 'โ'}`));
-
- if (hasAgent && hasActivation && hasPersona && activationBeforePersona && hasManifests && hasDependencies) {
- passedTests++;
- } else {
- console.error(chalk.red('โ Bundle structure validation failed'));
- failedTests++;
- }
- } else {
- console.error(chalk.red('โ Bundle file not created'));
- failedTests++;
- }
- } catch (error) {
- console.error(chalk.red('โ Failed to bundle analyst agent:'), error.message);
- failedTests++;
- }
-
- // Test 4: Bundle a different agent (architect which exists)
- try {
- const result = await bundler.bundleAgent('bmm', 'architect.md');
- const bundlePath = path.join(bundler.outputDir, 'bmm', 'agents', 'architect.xml');
-
- if (await fs.pathExists(bundlePath)) {
- console.log(chalk.green('โ Architect bundle created successfully'));
- passedTests++;
- } else {
- console.error(chalk.red('โ Architect bundle file not created'));
- failedTests++;
- }
- } catch (error) {
- console.error(chalk.red('โ Failed to bundle architect agent:'), error.message);
- failedTests++;
- }
-
- // Test 5: Bundle all agents in a module
- try {
- const results = await bundler.bundleModule('bmm');
- console.log(chalk.green(`โ Bundled ${results.agents.length} agents from bmm module`));
- passedTests++;
- } catch (error) {
- console.error(chalk.red('โ Failed to bundle bmm module:'), error.message);
- failedTests++;
- }
-
- // Summary
- console.log(chalk.bold('\n๐ Test Results:'));
- console.log(chalk.green(` Passed: ${passedTests}`));
- console.log(chalk.red(` Failed: ${failedTests}`));
-
- if (failedTests === 0) {
- console.log(chalk.green.bold('\nโ
All tests passed!\n'));
- } else {
- console.log(chalk.red.bold(`\nโ ${failedTests} test(s) failed\n`));
- process.exit(1);
- }
-}
-
-// Run tests
-testWebBundler().catch((error) => {
- console.error(chalk.red('Fatal error:'), error);
- process.exit(1);
-});
diff --git a/tools/cli/bundlers/web-bundler.js b/tools/cli/bundlers/web-bundler.js
deleted file mode 100644
index f0d10715..00000000
--- a/tools/cli/bundlers/web-bundler.js
+++ /dev/null
@@ -1,1754 +0,0 @@
-const path = require('node:path');
-const fs = require('fs-extra');
-const chalk = require('chalk');
-const yaml = require('js-yaml');
-const { DependencyResolver } = require('../installers/lib/core/dependency-resolver');
-const { XmlHandler } = require('../lib/xml-handler');
-const { YamlXmlBuilder } = require('../lib/yaml-xml-builder');
-const { AgentPartyGenerator } = require('../lib/agent-party-generator');
-const xml2js = require('xml2js');
-const { getProjectRoot, getSourcePath, getModulePath } = require('../lib/project-root');
-
-class WebBundler {
- constructor(sourceDir = null, outputDir = 'web-bundles') {
- this.sourceDir = sourceDir || getSourcePath();
- this.outputDir = path.isAbsolute(outputDir) ? outputDir : path.join(getProjectRoot(), outputDir);
- this.modulesPath = getSourcePath('modules');
- this.utilityPath = getSourcePath('utility');
-
- this.dependencyResolver = new DependencyResolver();
- this.xmlHandler = new XmlHandler();
- this.yamlBuilder = new YamlXmlBuilder();
-
- // Cache for resolved dependencies to avoid duplicates
- this.dependencyCache = new Map();
-
- // Discovered agents and teams for manifest generation
- this.discoveredAgents = [];
- this.discoveredTeams = [];
-
- // Temporary directory for generated manifests
- this.tempDir = path.join(process.cwd(), '.bundler-temp');
- this.tempManifestDir = path.join(this.tempDir, '.bmad', '_cfg');
-
- // Bundle statistics
- this.stats = {
- totalAgents: 0,
- bundledAgents: 0,
- skippedAgents: 0,
- failedAgents: 0,
- invalidXml: 0,
- warnings: [],
- };
- }
-
- /**
- * Main entry point to bundle all modules
- */
- async bundleAll() {
- console.log(chalk.cyan.bold('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
- console.log(chalk.cyan.bold(' ๐ Web Bundle Generation'));
- console.log(chalk.cyan.bold('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n'));
-
- try {
- // Vendor cross-module workflows FIRST
- 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) {
- await this.preDiscoverModule(module);
- }
-
- // Create temporary manifest files
- await this.createTempManifests();
-
- // Process all modules
- for (const module of modules) {
- await this.bundleModule(module);
- }
-
- // Display summary
- this.displaySummary();
- } finally {
- // Clean up temp files
- await this.cleanupTempFiles();
- }
- }
-
- /**
- * Bundle a specific module
- */
- async bundleModule(moduleName) {
- const modulePath = path.join(this.modulesPath, moduleName);
-
- if (!(await fs.pathExists(modulePath))) {
- console.log(chalk.yellow(`Module ${moduleName} not found`));
- return { module: moduleName, agents: [], teams: [] };
- }
-
- console.log(chalk.bold(`\n๐ฆ Bundling module: ${moduleName}`));
-
- const results = {
- module: moduleName,
- agents: [],
- 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
- await this.preDiscoverModule(moduleName);
-
- // Ensure temp manifests exist (might not exist if called directly)
- if (!(await fs.pathExists(this.tempManifestDir))) {
- await this.createTempManifests();
- }
-
- // Process agents
- const agents = await this.discoverAgents(modulePath);
- for (const agent of agents) {
- try {
- await this.bundleAgent(moduleName, agent, false); // false = don't track again
- results.agents.push(agent);
- } catch (error) {
- console.error(` Failed to bundle agent ${agent}:`, error.message);
- }
- }
-
- // Process teams
- const teams = await this.discoverTeams(modulePath);
- for (const team of teams) {
- try {
- await this.bundleTeam(moduleName, team);
- results.teams.push(team);
- } catch (error) {
- console.error(` Failed to bundle team ${team}:`, error.message);
- }
- }
-
- return results;
- }
-
- /**
- * Bundle a single agent
- */
- async bundleAgent(moduleName, agentFile, shouldTrack = true) {
- const agentName = agentFile.endsWith('.agent.yaml') ? path.basename(agentFile, '.agent.yaml') : path.basename(agentFile, '.md');
- this.stats.totalAgents++;
-
- 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);
-
- // Check if agent file exists
- if (!(await fs.pathExists(agentPath))) {
- this.stats.failedAgents++;
- console.log(chalk.red(` โ Agent file not found`));
- throw new Error(`Agent file not found: ${agentPath}`);
- }
-
- let content;
- let agentXml;
-
- // Handle YAML agents - build in-memory to XML
- if (agentFile.endsWith('.agent.yaml')) {
- // Check for webskip flag in YAML before building
- const yamlContent = await fs.readFile(agentPath, 'utf8');
- const agentYaml = yaml.load(yamlContent);
-
- if (agentYaml?.agent?.webskip === true) {
- this.stats.skippedAgents++;
- console.log(chalk.gray(` โ Skipped (webskip="true")`));
- return;
- }
-
- // Build agent from YAML (no customize file for web bundles)
- const xmlContent = await this.yamlBuilder.buildFromYaml(agentPath, null, {
- includeMetadata: false, // Don't include build metadata in web bundles
- forWebBundle: true, // Use web-specific activation fragments
- });
-
- content = xmlContent;
- agentXml = this.extractAgentXml(xmlContent);
- } else {
- // Legacy MD format - read and extract XML
- content = await fs.readFile(agentPath, 'utf8');
- agentXml = this.extractAgentXml(content);
- }
-
- if (!agentXml) {
- this.stats.failedAgents++;
- console.log(chalk.red(` โ No agent XML found in ${agentFile}`));
- return;
- }
-
- // Check if agent has bundle="false" attribute
- if (this.shouldSkipBundling(agentXml)) {
- this.stats.skippedAgents++;
- console.log(chalk.gray(` โ Skipped (bundle="false")`));
- return;
- }
-
- // Process {project-root} references in agent XML
- agentXml = this.processProjectRootReferences(agentXml);
-
- // Track for manifest generation BEFORE generating manifests (if not pre-discovered)
- if (shouldTrack) {
- const agentDetails = AgentPartyGenerator.extractAgentDetails(content, moduleName, agentName);
- if (agentDetails) {
- this.discoveredAgents.push(agentDetails);
- }
- }
-
- // Resolve dependencies with warning tracking
- const dependencyWarnings = [];
- const { dependencies, skippedWorkflows } = await this.resolveAgentDependencies(agentXml, moduleName, dependencyWarnings);
-
- if (dependencyWarnings.length > 0) {
- this.stats.warnings.push({ agent: agentName, warnings: dependencyWarnings });
- }
-
- // Check for module's default-party.csv and include it as agent manifest
- const defaultPartyPath = path.join(this.modulesPath, moduleName, 'teams', 'default-party.csv');
- if (await fs.pathExists(defaultPartyPath)) {
- const partyContent = await fs.readFile(defaultPartyPath, 'utf8');
- // Process any placeholders in the CSV content
- const processedPartyContent = this.processProjectRootReferences(partyContent);
- // Wrap as text to preserve raw CSV format in CDATA
- const wrappedParty = this.wrapContentInXml(processedPartyContent, 'bmad/_cfg/agent-manifest.csv', 'text');
- dependencies.set('bmad/_cfg/agent-manifest.csv', wrappedParty);
- console.log(chalk.gray(` + Added party manifest from module default-party.csv`));
- }
-
- // Remove commands for skipped workflows from agent XML
- if (skippedWorkflows.length > 0) {
- agentXml = this.removeSkippedWorkflowCommands(agentXml, skippedWorkflows);
- }
-
- // Build the bundle (no manifests for individual agents)
- const bundle = this.buildAgentBundle(agentXml, dependencies);
-
- // Validate XML
- const isValid = await this.validateXml(bundle);
- if (!isValid) {
- this.stats.invalidXml++;
- console.log(chalk.red(` โ Invalid XML generated!`));
- }
-
- // Format XML for readability
- const formattedBundle = this.formatXml(bundle);
-
- // Write bundle to output
- const outputPath = path.join(this.outputDir, moduleName, 'agents', `${agentName}.xml`);
- await fs.ensureDir(path.dirname(outputPath));
- await fs.writeFile(outputPath, formattedBundle, 'utf8');
-
- this.stats.bundledAgents++;
- const statusIcon = isValid ? chalk.green('โ') : chalk.yellow('โ ');
- console.log(` ${statusIcon} Bundled: ${agentName}.xml${isValid ? '' : chalk.yellow(' (invalid XML)')}`);
- }
-
- /**
- * Bundle a team - includes orchestrator and all agents with their dependencies
- */
- async bundleTeam(moduleName, teamFile) {
- const teamName = path.basename(teamFile, path.extname(teamFile));
- console.log(chalk.dim(` โ Processing team: ${teamName}`));
-
- const teamPath = path.join(this.modulesPath, moduleName, 'teams', teamFile);
-
- // Check if team file exists
- if (!(await fs.pathExists(teamPath))) {
- console.log(chalk.red(` โ Team file not found`));
- throw new Error(`Team file not found: ${teamPath}`);
- }
-
- // Read and parse team YAML
- const teamContent = await fs.readFile(teamPath, 'utf8');
- const teamConfig = yaml.load(teamContent);
-
- if (!teamConfig || !teamConfig.bundle) {
- console.log(chalk.red(` โ Invalid team configuration`));
- return;
- }
-
- // Start building the team bundle
- const dependencies = new Map();
- const processed = new Set();
- const allAgentXmls = [];
- const warnings = [];
-
- // Check if team has a party CSV file (agent manifest)
- const hasPartyFile = teamConfig.party && teamConfig.party.endsWith('.csv');
- if (hasPartyFile) {
- // Load the party CSV and add it as bmad/_cfg/agent-manifest.csv
- const partyPath = path.join(path.dirname(teamPath), teamConfig.party.replace(/^\.\//, ''));
- if (await fs.pathExists(partyPath)) {
- const partyContent = await fs.readFile(partyPath, 'utf8');
- // Process any placeholders in the CSV content
- const processedPartyContent = this.processProjectRootReferences(partyContent);
- // Wrap as text/csv to preserve raw CSV format in CDATA
- const wrappedParty = this.wrapContentInXml(processedPartyContent, 'bmad/_cfg/agent-manifest.csv', 'text');
- dependencies.set('bmad/_cfg/agent-manifest.csv', wrappedParty);
- console.log(chalk.gray(` + Added agent manifest from: ${teamConfig.party}`));
- } else {
- console.log(chalk.yellow(` โ Party file not found: ${partyPath}`));
- }
- }
-
- // 1. First, always add the bmad-web-orchestrator (XML file only, no transformation needed)
- const orchestratorXmlPath = path.join(this.sourceDir, 'core', 'agents', 'bmad-web-orchestrator.agent.xml');
-
- if (await fs.pathExists(orchestratorXmlPath)) {
- // Read the XML file directly - no transformation needed
- const xmlContent = await fs.readFile(orchestratorXmlPath, 'utf8');
- let orchestratorXml = xmlContent.trim();
-
- // Process {project-root} references
- orchestratorXml = this.processProjectRootReferences(orchestratorXml);
-
- // Inject help/exit menu items only (orchestrator has its own activation)
- orchestratorXml = this.injectHelpExitMenuItems(orchestratorXml);
-
- // Resolve orchestrator dependencies
- const { dependencies: orchDeps } = await this.resolveAgentDependencies(orchestratorXml, 'core', warnings);
-
- // Merge orchestrator dependencies
- for (const [id, content] of orchDeps) {
- if (!dependencies.has(id)) {
- dependencies.set(id, content);
- }
- }
-
- // Add orchestrator XML first
- allAgentXmls.push(orchestratorXml);
- console.log(chalk.gray(` + Added orchestrator: bmad-web-orchestrator`));
- } else {
- console.log(chalk.yellow(` โ Orchestrator not found at: ${orchestratorXmlPath}`));
- }
-
- // 2. Determine which agents to include
- let agentsToBundle = [];
-
- if (teamConfig.agents === '*' || (Array.isArray(teamConfig.agents) && teamConfig.agents.includes('*'))) {
- // Include all agents from the module
- const agentsPath = path.join(this.modulesPath, moduleName, 'agents');
- if (await fs.pathExists(agentsPath)) {
- const agentFiles = await fs.readdir(agentsPath);
- agentsToBundle = agentFiles
- .filter((file) => file.endsWith('.agent.yaml') || (file.endsWith('.md') && !file.toLowerCase().includes('readme')))
- .map((file) => file.replace(/\.(agent\.yaml|md)$/, ''));
- }
- } else if (Array.isArray(teamConfig.agents)) {
- // Include specific agents listed
- agentsToBundle = teamConfig.agents;
- } else {
- console.log(chalk.yellow(` โ No agents specified in team configuration`));
- }
-
- // 3. Process each agent and their dependencies
- for (const agentName of agentsToBundle) {
- // Try YAML first, then MD
- let agentPath = path.join(this.modulesPath, moduleName, 'agents', `${agentName}.agent.yaml`);
- let isYaml = await fs.pathExists(agentPath);
-
- if (!isYaml) {
- agentPath = path.join(this.modulesPath, moduleName, 'agents', `${agentName}.md`);
- if (!(await fs.pathExists(agentPath))) {
- console.log(chalk.yellow(` โ Agent not found: ${agentName}`));
- continue;
- }
- }
-
- let agentXml;
-
- if (isYaml) {
- // Check for webskip flag in YAML
- const yamlContent = await fs.readFile(agentPath, 'utf8');
- const agentYaml = yaml.load(yamlContent);
-
- if (agentYaml?.agent?.webskip === true) {
- console.log(chalk.gray(` โ Skipped agent (webskip="true"): ${agentName}`));
- continue;
- }
-
- // Build YAML agent in-memory - skip activation for team agents (orchestrator handles it)
- const xmlContent = await this.yamlBuilder.buildFromYaml(agentPath, null, {
- includeMetadata: false,
- skipActivation: true, // Skip activation for team agents
- });
- agentXml = this.extractAgentXml(xmlContent);
- } else {
- // Read legacy MD agent
- const agentContent = await fs.readFile(agentPath, 'utf8');
- agentXml = this.extractAgentXml(agentContent);
- }
-
- if (!agentXml) {
- console.log(chalk.yellow(` โ No XML found in agent: ${agentName}`));
- continue;
- }
-
- // Skip agents with bundle="false"
- if (this.shouldSkipBundling(agentXml)) {
- console.log(chalk.gray(` โ Skipped agent (bundle="false"): ${agentName}`));
- continue;
- }
-
- // Process {project-root} references
- agentXml = this.processProjectRootReferences(agentXml);
-
- // Resolve agent dependencies
- const agentWarnings = [];
- const { dependencies: agentDeps, skippedWorkflows } = await this.resolveAgentDependencies(agentXml, moduleName, agentWarnings);
-
- if (agentWarnings.length > 0) {
- warnings.push({ agent: agentName, warnings: agentWarnings });
- }
-
- // Remove commands for skipped workflows from agent XML
- if (skippedWorkflows.length > 0) {
- agentXml = this.removeSkippedWorkflowCommands(agentXml, skippedWorkflows);
- }
-
- // Merge agent dependencies (deduplicate)
- for (const [id, content] of agentDeps) {
- if (!dependencies.has(id)) {
- dependencies.set(id, content);
- }
- }
-
- // Skip web activation injection for team agents - orchestrator handles everything
- // Only inject help/exit menu items if missing
- agentXml = this.injectHelpExitMenuItems(agentXml);
-
- // Add agent XML to the collection
- allAgentXmls.push(agentXml);
- console.log(chalk.gray(` + Added agent: ${agentName}`));
- }
-
- // 4. Build the team bundle XML
- const bundle = this.buildTeamBundle(teamConfig.bundle, allAgentXmls, dependencies);
-
- // 5. Validate XML
- const isValid = await this.validateXml(bundle);
- if (!isValid) {
- console.log(chalk.red(` โ Invalid XML generated for team!`));
- }
-
- // Format XML for readability
- const formattedBundle = this.formatXml(bundle);
-
- // 6. Write bundle to output
- const outputPath = path.join(this.outputDir, moduleName, 'teams', `${teamName}.xml`);
- await fs.ensureDir(path.dirname(outputPath));
- await fs.writeFile(outputPath, formattedBundle, 'utf8');
-
- const statusIcon = isValid ? chalk.green('โ') : chalk.yellow('โ ');
- console.log(` ${statusIcon} Bundled team: ${teamName}.xml${isValid ? '' : chalk.yellow(' (invalid XML)')}`);
-
- // Track warnings
- if (warnings.length > 0) {
- this.stats.warnings.push(...warnings);
- }
- }
-
- /**
- * Build the final team bundle XML
- */
- buildTeamBundle(teamMetadata, agentXmls, dependencies) {
- const parts = ['', '', ' ', ' '];
-
- for (const agentXml of agentXmls) {
- // Indent each agent XML properly (add 4 spaces to each line)
- const indentedAgent = agentXml
- .split('\n')
- .map((line) => ' ' + line)
- .join('\n');
- parts.push(indentedAgent);
- }
-
- parts.push(' ');
-
- // Add all dependencies
- if (dependencies && dependencies.size > 0) {
- parts.push('', ' ', ' ');
-
- for (const [id, content] of dependencies) {
- // All dependencies are now consistently wrapped in elements
- // Indent properly (add 4 spaces to each line)
- const indentedContent = content
- .split('\n')
- .map((line) => ' ' + line)
- .join('\n');
- parts.push(indentedContent);
- }
-
- parts.push(' ');
- }
-
- parts.push('');
-
- 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
- // Support both {project-root}/bmad/... and {project-root}/.bmad/... patterns
- 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
- // Support both old format (bmad) and new format (.bmad)
- 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
- */
- async preDiscoverModule(moduleName) {
- const modulePath = path.join(this.modulesPath, moduleName);
-
- // Clear any previously discovered agents for this module
- this.discoveredAgents = this.discoveredAgents.filter((a) => a.module !== moduleName);
-
- // Discover agents
- const agentsPath = path.join(modulePath, 'agents');
- if (await fs.pathExists(agentsPath)) {
- const files = await fs.readdir(agentsPath);
- for (const file of files) {
- if (file.endsWith('.agent.yaml') || (file.endsWith('.md') && !file.toLowerCase().includes('readme'))) {
- const agentPath = path.join(agentsPath, file);
- let content;
-
- if (file.endsWith('.agent.yaml')) {
- // Check for webskip flag in YAML
- const yamlContent = await fs.readFile(agentPath, 'utf8');
- const agentYaml = yaml.load(yamlContent);
-
- if (agentYaml?.agent?.webskip === true) {
- continue; // Skip this agent
- }
-
- // Build YAML agent in-memory
- content = await this.yamlBuilder.buildFromYaml(agentPath, null, {
- includeMetadata: false,
- });
- } else {
- // Read legacy MD agent
- content = await fs.readFile(agentPath, 'utf8');
- }
-
- const agentXml = this.extractAgentXml(content);
-
- if (agentXml) {
- // Skip agents with bundle="false"
- if (this.shouldSkipBundling(agentXml)) {
- continue;
- }
-
- const agentName = file.endsWith('.agent.yaml') ? path.basename(file, '.agent.yaml') : path.basename(file, '.md');
- // Use the shared generator to extract agent details (pass full content)
- const agentDetails = AgentPartyGenerator.extractAgentDetails(content, moduleName, agentName);
- if (agentDetails) {
- this.discoveredAgents.push(agentDetails);
- }
- }
- }
- }
- }
-
- // TODO: Discover teams when implemented
- }
-
- /**
- * Extract agent XML from markdown content
- */
- extractAgentXml(content) {
- // Try 4 backticks first (can contain 3 backtick blocks inside)
- let match = content.match(/````xml\s*([\s\S]*?)````/);
- if (!match) {
- // Fall back to 3 backticks if no 4-backtick block found
- match = content.match(/```xml\s*([\s\S]*?)```/);
- }
-
- if (match) {
- const xmlContent = match[1];
- const agentMatch = xmlContent.match(/]*>[\s\S]*?<\/agent>/);
- return agentMatch ? agentMatch[0] : null;
- }
-
- // Fall back to direct extraction
- match = content.match(/]*>[\s\S]*?<\/agent>/);
- return match ? match[0] : null;
- }
-
- /**
- * Resolve all dependencies for an agent
- */
- async resolveAgentDependencies(agentXml, moduleName, warnings = []) {
- const dependencies = new Map();
- const processed = new Set();
- const skippedWorkflows = [];
-
- // Extract file references from agent XML
- const { refs, workflowRefs } = this.extractFileReferences(agentXml);
-
- // Process regular file references
- for (const ref of refs) {
- await this.processFileDependency(ref, dependencies, processed, moduleName, warnings);
- }
-
- // Process workflow references with special handling
- for (const workflowRef of workflowRefs) {
- const result = await this.processWorkflowDependency(workflowRef, dependencies, processed, moduleName, warnings);
- if (result && result.skipped) {
- skippedWorkflows.push(workflowRef);
- }
- }
-
- return { dependencies, skippedWorkflows };
- }
-
- /**
- * Extract file references from agent XML
- */
- extractFileReferences(xml) {
- const refs = new Set();
- const workflowRefs = new Set();
-
- // Remove agent id attribute to prevent it from being treated as a dependency
- // The id attribute is just a metadata identifier, not a file reference
- const xmlWithoutAgentId = xml.replace(/]*id="[^"]*"[^>]*>/, (match) => {
- return match.replace(/\sid="[^"]*"/, '');
- });
-
- // Match various file reference patterns
- const patterns = [
- /exec="([^"]+)"/g, // Command exec paths
- /tmpl="([^"]+)"/g, // Template paths
- /data="([^"]+)"/g, // Data file paths
- /file="([^"]+)"/g, // Generic file refs
- /src="([^"]+)"/g, // Source paths
- /system-prompts="([^"]+)"/g,
- /tools="([^"]+)"/g,
- /knowledge="([^"]+)"/g,
- /{project-root}\/([^"'\s<>]+)/g, // Legacy {project-root} paths
- /\bbmad\/([^"'\s<>]+)/g, // Direct bmad/ paths (after .bmad replacement)
- ];
-
- for (const pattern of patterns) {
- let match;
- // Use the XML with agent id removed for pattern matching
- while ((match = pattern.exec(xmlWithoutAgentId)) !== null) {
- let filePath = match[1];
- // Remove {project-root} prefix if present
- filePath = filePath.replace(/^{project-root}\//, '');
- // Remove .bmad prefix if present (should be rare, mostly replaced already)
- filePath = filePath.replace(/^.bmad\//, 'bmad/');
-
- // For bmad/ pattern, prepend 'bmad/' since it was captured without it
- if (pattern.source.includes(String.raw`\bbmad\/`)) {
- filePath = 'bmad/' + filePath;
- }
-
- // Skip obvious placeholder/example paths
- if (filePath && !filePath.includes('path/to/') && !filePath.includes('example') && !filePath.includes('...')) {
- refs.add(filePath);
- }
- }
- }
-
- // Extract workflow references from agent files
- const workflowPatterns = [
- /workflow="([^"]+)"/g, // Menu items with workflow attribute
- /validate-workflow="([^"]+)"/g, // Validation workflow references
- ];
-
- for (const pattern of workflowPatterns) {
- let match;
- // Use original xml for workflow patterns (they don't conflict with agent id)
- while ((match = pattern.exec(xml)) !== null) {
- let workflowPath = match[1];
- workflowPath = workflowPath.replace(/^{project-root}\//, '');
- // Remove .bmad prefix if present and replace with bmad
- workflowPath = workflowPath.replace(/^.bmad\//, 'bmad/');
-
- // Skip obvious placeholder/example paths
- if (workflowPath && workflowPath.endsWith('.yaml') && !workflowPath.includes('path/to/') && !workflowPath.includes('example')) {
- workflowRefs.add(workflowPath);
- }
- }
- }
-
- return { refs: [...refs], workflowRefs: [...workflowRefs] };
- }
-
- /**
- * Remove commands from agent XML that reference skipped workflows
- */
- removeSkippedWorkflowCommands(agentXml, skippedWorkflows) {
- let modifiedXml = agentXml;
-
- // For each skipped workflow, find and remove menu items and commands
- for (const workflowPath of skippedWorkflows) {
- // Need to escape special regex characters in the path
- const escapedPath = workflowPath.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
-
- // Pattern 1: Remove - tags with workflow attribute
- // Match:
- ...
- const itemWorkflowPattern = new RegExp(`\\s*- ]*workflow="[^"]*${escapedPath}"[^>]*>.*?
\\s*`, 'gs');
- modifiedXml = modifiedXml.replace(itemWorkflowPattern, '');
- }
-
- return modifiedXml;
- }
-
- /**
- * Process a file dependency recursively
- */
- async processFileDependency(filePath, dependencies, processed, moduleName, warnings = []) {
- // Skip workflow YAML files - they're handled by processWorkflowDependency
- if (filePath.includes('/workflow') && filePath.endsWith('workflow.yaml')) {
- return;
- }
-
- // Skip if already processed
- if (processed.has(filePath)) {
- return;
- }
- processed.add(filePath);
-
- // Skip agent-manifest.csv manifest for web bundles (agents are already bundled)
- if (filePath === 'bmad/_cfg/agent-manifest.csv' || filePath.endsWith('/agent-manifest.csv')) {
- return;
- }
-
- // Handle wildcard patterns
- if (filePath.includes('*')) {
- await this.processWildcardDependency(filePath, dependencies, processed, moduleName, warnings);
- return;
- }
-
- // Resolve actual file path
- const actualPath = this.resolveFilePath(filePath, moduleName);
-
- if (!actualPath || !(await fs.pathExists(actualPath))) {
- warnings.push(filePath);
- return;
- }
-
- // Skip if it's a directory
- const stats = await fs.stat(actualPath);
- if (stats.isDirectory()) {
- // Silently skip directories - they're not file dependencies
- return;
- }
-
- // Read file content
- let content = await fs.readFile(actualPath, 'utf8');
-
- // Process {project-root} references
- content = this.processProjectRootReferences(content);
-
- // Extract dependencies from frontmatter if present
- const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
- if (frontmatterMatch) {
- const frontmatter = frontmatterMatch[1];
- // Look for dependencies in frontmatter
- const depMatch = frontmatter.match(/dependencies:\s*\[(.*?)\]/);
- if (depMatch) {
- const deps = depMatch[1].match(/['"]([^'"]+)['"]/g);
- if (deps) {
- for (const dep of deps) {
- let depPath = dep.replaceAll(/['"]/g, '').replace(/^{project-root}\//, '');
- depPath = depPath.replace(/^.bmad\//, 'bmad/');
- if (depPath && !processed.has(depPath)) {
- await this.processFileDependency(depPath, dependencies, processed, moduleName, warnings);
- }
- }
- }
- }
- // Look for template references
- const templateMatch = frontmatter.match(/template:\s*\[(.*?)\]/);
- if (templateMatch) {
- const templates = templateMatch[1].match(/['"]([^'"]+)['"]/g);
- if (templates) {
- for (const template of templates) {
- let templatePath = template.replaceAll(/['"]/g, '').replace(/^{project-root}\//, '');
- templatePath = templatePath.replace(/^.bmad\//, 'bmad/');
- if (templatePath && !processed.has(templatePath)) {
- await this.processFileDependency(templatePath, dependencies, processed, moduleName, warnings);
- }
- }
- }
- }
- }
-
- // Extract XML from markdown if applicable
- const ext = path.extname(actualPath).toLowerCase();
- let processedContent = content;
-
- switch (ext) {
- case '.md': {
- // Try to extract XML from markdown - handle both 3 and 4 backtick blocks
- // First try 4 backticks (which can contain 3 backtick blocks inside)
- let xmlMatches = [...content.matchAll(/````xml\s*([\s\S]*?)````/g)];
-
- // If no 4-backtick blocks, try 3 backticks
- if (xmlMatches.length === 0) {
- xmlMatches = [...content.matchAll(/```xml\s*([\s\S]*?)```/g)];
- }
-
- const xmlBlocks = [];
-
- for (const match of xmlMatches) {
- if (match[1]) {
- xmlBlocks.push(match[1].trim());
- }
- }
-
- if (xmlBlocks.length > 0) {
- // For XML content, just include it directly (it's already valid XML)
- processedContent = xmlBlocks.join('\n\n');
- } else {
- // No XML blocks found, skip non-XML markdown files
- return;
- }
-
- break;
- }
- case '.csv': {
- // CSV files need special handling - convert to XML file-index
- const lines = content.split('\n').filter((line) => line.trim());
- if (lines.length === 0) return;
-
- const headers = lines[0].split(',').map((h) => h.trim());
- const rows = lines.slice(1);
-
- const indexParts = [``];
- indexParts.push(' ');
-
- // Track files referenced in CSV for additional bundling
- const referencedFiles = new Set();
-
- for (const row of rows) {
- const values = row.split(',').map((v) => v.trim());
- if (values.every((v) => !v)) continue;
-
- indexParts.push(' - ');
- for (const [i, header] of headers.entries()) {
- const value = values[i] || '';
- const tagName = header.toLowerCase().replaceAll(/[^a-z0-9]/g, '_');
- indexParts.push(` <${tagName}>${value}${tagName}>`);
-
- // Track referenced files
- if (header.toLowerCase().includes('file') && value.endsWith('.md')) {
- // Build path relative to CSV location
- const csvDir = path.dirname(actualPath);
- const refPath = path.join(csvDir, value);
- if (fs.existsSync(refPath)) {
- const refId = filePath.replace('index.csv', value);
- referencedFiles.add(refId);
- }
- }
- }
- indexParts.push('
');
- }
-
- indexParts.push(' ', '');
-
- // Store the XML version wrapped in a file element
- const csvXml = indexParts.join('\n');
- const wrappedCsv = `\n${csvXml}\n`;
- dependencies.set(filePath, wrappedCsv);
-
- // Process referenced files from CSV
- for (const refId of referencedFiles) {
- if (!processed.has(refId)) {
- await this.processFileDependency(refId, dependencies, processed, moduleName, warnings);
- }
- }
-
- return;
- }
- case '.xml': {
- // XML files can be included directly
- processedContent = content;
- break;
- }
- default: {
- // For other non-XML file types, skip them
- return;
- }
- }
-
- // Determine file type for wrapping
- let fileType = 'text';
- if (ext === '.xml' || (ext === '.md' && processedContent.trim().startsWith('<'))) {
- fileType = 'xml';
- } else
- switch (ext) {
- case '.yaml':
- case '.yml': {
- fileType = 'yaml';
-
- break;
- }
- case '.json': {
- fileType = 'json';
-
- break;
- }
- case '.md': {
- fileType = 'md';
-
- break;
- }
- // No default
- }
-
- // Wrap content in file element and store
- const wrappedContent = this.wrapContentInXml(processedContent, filePath, fileType);
- dependencies.set(filePath, wrappedContent);
-
- // Recursively scan for more dependencies
- const { refs: nestedRefs } = this.extractFileReferences(processedContent);
- for (const ref of nestedRefs) {
- await this.processFileDependency(ref, dependencies, processed, moduleName, warnings);
- }
- }
-
- /**
- * Process a workflow YAML file and its bundle files
- */
- async processWorkflowDependency(workflowPath, dependencies, processed, moduleName, warnings = []) {
- // Skip if already processed
- if (processed.has(workflowPath)) {
- return { skipped: false };
- }
- processed.add(workflowPath);
-
- // Resolve actual file path
- const actualPath = this.resolveFilePath(workflowPath, moduleName);
-
- if (!actualPath || !(await fs.pathExists(actualPath))) {
- warnings.push(workflowPath);
- return { skipped: true };
- }
-
- // Read and parse YAML file
- const yamlContent = await fs.readFile(actualPath, 'utf8');
- let workflowConfig;
-
- try {
- workflowConfig = yaml.load(yamlContent);
- } catch (error) {
- warnings.push(`${workflowPath} (invalid YAML: ${error.message})`);
- return { skipped: true };
- }
-
- // Check if web_bundle is explicitly set to false
- if (workflowConfig.web_bundle === false) {
- // Mark this workflow as skipped so we can remove the command from agent
- return { skipped: true, workflowPath };
- }
-
- // Create YAML content with only web_bundle section (flattened)
- let bundleYamlContent;
- if (workflowConfig.web_bundle && typeof workflowConfig.web_bundle === 'object') {
- // Only include the web_bundle content, flattened to root level
- bundleYamlContent = yaml.dump(workflowConfig.web_bundle);
- } else {
- // If no web_bundle section, include full YAML
- bundleYamlContent = yamlContent;
- }
-
- // Process {project-root} and .bmad references in the YAML content
- bundleYamlContent = this.processProjectRootReferences(bundleYamlContent);
-
- // Include the YAML file with only web_bundle content, wrapped in XML
- // Process the workflow path to create a clean ID
- let yamlId = workflowPath.replace(/^{project-root}\//, '');
- yamlId = yamlId.replace(/^.bmad\//, 'bmad/');
- const wrappedYaml = this.wrapContentInXml(bundleYamlContent, yamlId, 'yaml');
- dependencies.set(yamlId, wrappedYaml);
-
- // Always include core workflow task when processing workflows
- await this.includeCoreWorkflowFiles(dependencies, processed, moduleName, warnings);
-
- // Check if advanced elicitation is enabled
- if (workflowConfig.web_bundle && workflowConfig.web_bundle.use_advanced_elicitation) {
- await this.includeAdvancedElicitationFiles(dependencies, processed, moduleName, warnings);
- }
-
- // Process web_bundle_files if they exist
- if (workflowConfig.web_bundle && workflowConfig.web_bundle.web_bundle_files) {
- const bundleFiles = workflowConfig.web_bundle.web_bundle_files;
-
- for (const bundleFilePath of bundleFiles) {
- // Process the file path to create a clean ID for checking if already processed
- let cleanFilePath = bundleFilePath.replace(/^{project-root}\//, '');
- cleanFilePath = cleanFilePath.replace(/^.bmad\//, 'bmad/');
-
- if (processed.has(cleanFilePath)) {
- continue;
- }
-
- const bundleActualPath = this.resolveFilePath(bundleFilePath, moduleName);
-
- if (!bundleActualPath || !(await fs.pathExists(bundleActualPath))) {
- // Use the cleaned path in warnings (with .bmad replaced)
- warnings.push(cleanFilePath);
- continue;
- }
-
- // Check if this is another workflow.yaml file - if so, recursively process it
- if (bundleFilePath.endsWith('workflow.yaml')) {
- // Recursively process this workflow and its dependencies
- await this.processWorkflowDependency(bundleFilePath, dependencies, processed, moduleName, warnings);
- } else {
- // Regular file - process normally
- processed.add(cleanFilePath);
-
- // Read the file content
- let fileContent = await fs.readFile(bundleActualPath, 'utf8');
- const fileExt = path.extname(bundleActualPath).toLowerCase().replace('.', '');
-
- // Process {project-root} references before wrapping
- fileContent = this.processProjectRootReferences(fileContent);
-
- // Wrap in XML with proper escaping
- const wrappedContent = this.wrapContentInXml(fileContent, cleanFilePath, fileExt);
- dependencies.set(cleanFilePath, wrappedContent);
- }
- }
- }
-
- return { skipped: false };
- }
-
- /**
- * Include core workflow task files
- */
- async includeCoreWorkflowFiles(dependencies, processed, moduleName, warnings = []) {
- const coreWorkflowPath = 'bmad/core/tasks/workflow.xml';
-
- if (processed.has(coreWorkflowPath)) {
- return;
- }
- processed.add(coreWorkflowPath);
-
- const actualPath = this.resolveFilePath(coreWorkflowPath, moduleName);
-
- if (!actualPath || !(await fs.pathExists(actualPath))) {
- warnings.push(coreWorkflowPath);
- return;
- }
-
- let fileContent = await fs.readFile(actualPath, 'utf8');
- // Process {project-root} and .bmad references
- fileContent = this.processProjectRootReferences(fileContent);
- const wrappedContent = this.wrapContentInXml(fileContent, coreWorkflowPath, 'xml');
- dependencies.set(coreWorkflowPath, wrappedContent);
- }
-
- /**
- * Include advanced elicitation files
- */
- async includeAdvancedElicitationFiles(dependencies, processed, moduleName, warnings = []) {
- const elicitationFiles = ['bmad/core/tasks/advanced-elicitation.xml', 'bmad/core/tasks/advanced-elicitation-methods.csv'];
-
- for (const filePath of elicitationFiles) {
- if (processed.has(filePath)) {
- continue;
- }
- processed.add(filePath);
-
- const actualPath = this.resolveFilePath(filePath, moduleName);
-
- if (!actualPath || !(await fs.pathExists(actualPath))) {
- warnings.push(filePath);
- continue;
- }
-
- let fileContent = await fs.readFile(actualPath, 'utf8');
- // Process {project-root} and .bmad references
- fileContent = this.processProjectRootReferences(fileContent);
- const fileExt = path.extname(actualPath).toLowerCase().replace('.', '');
- const wrappedContent = this.wrapContentInXml(fileContent, filePath, fileExt);
- dependencies.set(filePath, wrappedContent);
- }
- }
-
- /**
- * Wrap file content in XML with proper escaping
- */
- wrapContentInXml(content, id, type = 'text') {
- // For XML files, include directly without CDATA (they're already valid XML)
- if (type === 'xml') {
- // XML files can be included directly as they're already well-formed
- // Just wrap in a file element
- return `\n${content}\n`;
- }
-
- // For all other file types, use CDATA to preserve content exactly
- // Escape any ]]> sequences in the content by splitting CDATA sections
- // Replace ]]> with ]]]]> to properly escape it within CDATA
- const escapedContent = content.replaceAll(']]>', ']]]]>');
-
- // Use CDATA to preserve content exactly as-is, including special characters
- return ``;
- }
-
- /**
- * Process wildcard dependency patterns
- */
- async processWildcardDependency(pattern, dependencies, processed, moduleName, warnings = []) {
- // Remove {project-root} prefix
- pattern = pattern.replace(/^{project-root}\//, '');
- // Replace .bmad with bmad
- pattern = pattern.replace(/^.bmad\//, 'bmad/');
-
- // Get directory and file pattern
- const lastSlash = pattern.lastIndexOf('/');
- const dirPath = pattern.slice(0, Math.max(0, lastSlash));
- const filePattern = pattern.slice(Math.max(0, lastSlash + 1));
-
- // Resolve directory path without checking file existence
- let dir;
- if (dirPath.startsWith('bmad/')) {
- // Remove bmad/ prefix
- const actualPath = dirPath.replace(/^bmad\//, '');
-
- // Try different path mappings for directories
- const possibleDirs = [
- // Try as module path: bmad/cis/... -> src/modules/cis/...
- path.join(this.sourceDir, 'modules', actualPath),
- // Try as direct path: bmad/core/... -> src/core/...
- path.join(this.sourceDir, actualPath),
- ];
-
- for (const testDir of possibleDirs) {
- if (fs.existsSync(testDir)) {
- dir = testDir;
- break;
- }
- }
- }
-
- if (!dir) {
- warnings.push(`${pattern} (could not resolve directory)`);
- return;
- }
- if (!(await fs.pathExists(dir))) {
- warnings.push(pattern);
- return;
- }
-
- // Read directory and match files
- const files = await fs.readdir(dir);
- let matchedFiles = [];
-
- if (filePattern === '*.*') {
- matchedFiles = files;
- } else if (filePattern.startsWith('*.')) {
- const ext = filePattern.slice(1);
- matchedFiles = files.filter((f) => f.endsWith(ext));
- } else {
- // Simple glob matching
- const regex = new RegExp('^' + filePattern.replace('*', '.*') + '$');
- matchedFiles = files.filter((f) => regex.test(f));
- }
-
- // Process each matched file
- for (const file of matchedFiles) {
- const fullPath = dirPath + '/' + file;
- if (!processed.has(fullPath)) {
- await this.processFileDependency(fullPath, dependencies, processed, moduleName, warnings);
- }
- }
- }
-
- /**
- * Resolve file path relative to project
- */
- resolveFilePath(filePath, moduleName) {
- // Remove {project-root} prefix
- filePath = filePath.replace(/^{project-root}\//, '');
-
- // Check temp directory first for _cfg files
- if (filePath.startsWith('bmad/_cfg/')) {
- const filename = filePath.split('/').pop();
- const tempPath = path.join(this.tempManifestDir, filename);
- if (fs.existsSync(tempPath)) {
- return tempPath;
- }
- }
-
- let actualPath = filePath;
-
- if (filePath.startsWith('bmad/')) {
- // Remove bmad/ prefix
- actualPath = filePath.replace(/^bmad\//, '');
-
- // Check if it's a module-specific file (cis, bmm, etc) or core file
- const parts = actualPath.split('/');
- const firstPart = parts[0];
-
- // Try different path mappings
- const possiblePaths = [
- // Try in temp directory first
- path.join(this.tempDir, filePath),
- // Try as module path: bmad/cis/... -> src/modules/cis/...
- path.join(this.sourceDir, 'modules', actualPath),
- // Try as direct path: bmad/core/... -> src/core/...
- path.join(this.sourceDir, actualPath),
- // Try without any prefix in src
- path.join(this.sourceDir, parts.slice(1).join('/')),
- // Try in project root
- path.join(this.sourceDir, '..', actualPath),
- // Try original with bmad
- path.join(this.sourceDir, '..', filePath),
- ];
-
- for (const testPath of possiblePaths) {
- if (fs.existsSync(testPath)) {
- return testPath;
- }
- }
- }
-
- // Try standard paths for non-bmad files
- const basePaths = [
- this.sourceDir, // src directory
- path.join(this.modulesPath, moduleName), // Current module
- path.join(this.sourceDir, '..'), // Project root
- ];
-
- for (const basePath of basePaths) {
- const fullPath = path.join(basePath, actualPath);
- if (fs.existsSync(fullPath)) {
- return fullPath;
- }
- }
-
- return null;
- }
-
- /**
- * Process and remove {project-root} references
- */
- processProjectRootReferences(content) {
- // Remove {project-root}/ prefix (with slash)
- content = content.replaceAll('{project-root}/', '');
- // Also remove {project-root} without slash
- content = content.replaceAll('{project-root}', '');
- return content;
- }
-
- /**
- * Escape special XML characters in text content
- */
- escapeXmlText(text) {
- return text
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
- .replaceAll("'", ''');
- }
-
- /**
- * Escape XML content while preserving XML tags
- */
- escapeXmlContent(content) {
- const tagPattern = /<([^>]+)>/g;
- const parts = [];
- let lastIndex = 0;
- let match;
-
- while ((match = tagPattern.exec(content)) !== null) {
- if (match.index > lastIndex) {
- parts.push(this.escapeXmlText(content.slice(lastIndex, match.index)));
- }
- parts.push('<' + match[1] + '>');
- lastIndex = match.index + match[0].length;
- }
-
- if (lastIndex < content.length) {
- parts.push(this.escapeXmlText(content.slice(lastIndex)));
- }
-
- return parts.join('');
- }
-
- /**
- * Inject help and exit menu items into agent XML
- */
- injectHelpExitMenuItems(agentXml) {
- // Check if menu already has help and exit
- const hasHelp = agentXml.includes('cmd="*help"') || agentXml.includes('trigger="*help"');
- const hasExit = agentXml.includes('cmd="*exit"') || agentXml.includes('trigger="*exit"');
-
- if (hasHelp && hasExit) {
- return agentXml; // Already has both, skip injection
- }
-
- // Find the menu section
- const menuMatch = agentXml.match(/( tag
- const newMenuContent = menuContent.replace(/(\s*)<\/menu>/, `\n${menuItems.join('\n')}\n${indent}`);
- return agentXml.replace(menuContent, newMenuContent);
- }
-
- /**
- * Inject web activation instructions into agent XML
- */
- injectWebActivation(agentXml) {
- // First, always inject help/exit menu items
- agentXml = this.injectHelpExitMenuItems(agentXml);
-
- // Load the web activation template
- const activationPath = path.join(this.sourceDir, 'utility', 'models', 'agent-activation-web.xml');
-
- if (!fs.existsSync(activationPath)) {
- console.warn(chalk.yellow('Warning: agent-activation-web.xml not found, skipping activation injection'));
- return agentXml;
- }
-
- const activationXml = fs.readFileSync(activationPath, 'utf8');
-
- // For web bundles, ALWAYS replace existing activation with web activation
- // This is because fragment-based activation assumes filesystem access which won't work in web bundles
- const hasActivation = agentXml.includes(']*>[\s\S]*?<\/activation>/, activationXml);
- return injectedXml;
- }
-
- // Check for critical-actions block (legacy)
- const hasCriticalActions = agentXml.includes('[\s\S]*?<\/critical-actions>/, activationXml);
- return injectedXml;
- }
-
- // If no critical-actions, inject before closing tag
- const closingTagMatch = agentXml.match(/(\s*)<\/agent>/);
- if (!closingTagMatch) {
- console.warn(chalk.yellow('Warning: Could not find tag for activation injection'));
- return agentXml;
- }
-
- // Inject the activation block before the closing tag
- // Properly indent each line of the activation XML
- const indent = closingTagMatch[1];
- const indentedActivation = activationXml
- .split('\n')
- .map((line) => (line.trim() ? indent + line : ''))
- .join('\n');
-
- const injectedXml = agentXml.replace(/(\s*)<\/agent>/, `\n${indentedActivation}\n${indent}`);
-
- return injectedXml;
- }
-
- /**
- * Build the final agent bundle XML
- */
- buildAgentBundle(agentXml, dependencies) {
- // Web activation is now handled by fragments during YAML building
- // agentXml = this.injectWebActivation(agentXml);
-
- const parts = [
- '',
- '',
- ' ',
- ' ' + agentXml.replaceAll('\n', '\n '),
- ];
-
- // Add dependencies (all are now consistently wrapped in elements)
- if (dependencies && dependencies.size > 0) {
- parts.push('\n ');
- for (const [id, content] of dependencies) {
- // All dependencies are now wrapped in elements
- // Indent properly
- const indentedContent = content
- .split('\n')
- .map((line) => ' ' + line)
- .join('\n');
- parts.push(indentedContent);
- }
- }
-
- parts.push('');
-
- return parts.join('\n');
- }
-
- /**
- * Discover all modules
- */
- async discoverModules() {
- const modules = [];
-
- if (!(await fs.pathExists(this.modulesPath))) {
- console.log(chalk.yellow('No modules directory found'));
- return modules;
- }
-
- const entries = await fs.readdir(this.modulesPath, { withFileTypes: true });
-
- for (const entry of entries) {
- if (entry.isDirectory()) {
- modules.push(entry.name);
- }
- }
-
- return modules;
- }
-
- /**
- * Discover agents in a module
- */
- async discoverAgents(modulePath) {
- const agents = [];
- const agentsPath = path.join(modulePath, 'agents');
-
- if (!(await fs.pathExists(agentsPath))) {
- return agents;
- }
-
- const files = await fs.readdir(agentsPath);
-
- for (const file of files) {
- // Look for .agent.yaml files (new format) or .md files (legacy format)
- if (file.endsWith('.agent.yaml') || (file.endsWith('.md') && !file.toLowerCase().includes('readme'))) {
- agents.push(file);
- }
- }
-
- return agents;
- }
-
- /**
- * Discover all teams in a module
- */
- async discoverTeams(modulePath) {
- const teams = [];
- const teamsPath = path.join(modulePath, 'teams');
-
- if (!(await fs.pathExists(teamsPath))) {
- return teams;
- }
-
- const files = await fs.readdir(teamsPath);
-
- for (const file of files) {
- if (file.endsWith('.yaml') || file.endsWith('.yml')) {
- teams.push(file);
- }
- }
-
- return teams;
- }
-
- /**
- * Extract agent name from XML
- */
- getAgentName(xml) {
- const match = xml.match(/]*name="([^"]+)"/);
- return match ? match[1] : 'Unknown';
- }
-
- /**
- * Extract agent description from XML
- */
- getAgentDescription(xml) {
- const match = xml.match(/([^<]+)<\/description>/);
- return match ? match[1] : '';
- }
-
- /**
- * Check if agent should be skipped for bundling
- */
- shouldSkipBundling(xml) {
- // Check for bundle="false" attribute in the agent tag
- const match = xml.match(/]*bundle="false"[^>]*>/);
- return match !== null;
- }
-
- /**
- * Create temporary manifest files
- */
- async createTempManifests() {
- // Ensure temp directory exists
- await fs.ensureDir(this.tempManifestDir);
-
- // Generate agent-manifest.csv using shared generator
- const agentPartyPath = path.join(this.tempManifestDir, 'agent-manifest.csv');
- await AgentPartyGenerator.writeAgentParty(agentPartyPath, this.discoveredAgents, { forWeb: true });
-
- console.log(chalk.dim(' โ Created temporary manifest files'));
- }
-
- /**
- * Clean up temporary files
- */
- async cleanupTempFiles() {
- if (await fs.pathExists(this.tempDir)) {
- await fs.remove(this.tempDir);
- console.log(chalk.dim('\nโ Cleaned up temporary files'));
- }
- }
-
- /**
- * Validate XML content
- */
- async validateXml(xmlContent) {
- try {
- await xml2js.parseStringPromise(xmlContent, {
- strict: true,
- explicitArray: false,
- });
- return true;
- } catch {
- return false;
- }
- }
-
- /**
- * Format XML content for readability
- */
- formatXml(xml) {
- const TAB = ' '; // 2 spaces
- let result = '';
- let depth = 0;
-
- // Split by tags while preserving them
- const parts = xml.split(/(<[^>]+>)/g);
-
- for (let i = 0; i < parts.length; i++) {
- const part = parts[i];
- if (!part) continue;
-
- if (part.startsWith('');
- const tagName = part.match(/<(\w+)/)?.[1];
-
- // Check if next part is simple text content
- const nextPart = parts[i + 1];
- const hasSimpleContent = nextPart && !nextPart.startsWith('<') && nextPart.trim().length > 0 && nextPart.trim().length <= 100;
-
- if (hasSimpleContent && parts[i + 2] && parts[i + 2] === `${tagName}>`) {
- // Simple tag with inline content: content
- result += TAB.repeat(depth) + part + nextPart.trim() + parts[i + 2] + '\n';
- i += 2; // Skip content and closing tag
- } else {
- // Multi-line tag
- result += TAB.repeat(depth) + part + '\n';
- if (!isSelfClosing) {
- depth++;
- }
- }
- } else {
- // Text content between tags
- const trimmed = part.trim();
- if (trimmed) {
- result += TAB.repeat(depth) + trimmed + '\n';
- }
- }
- }
-
- return result;
- }
-
- /**
- * Display summary statistics
- */
- displaySummary() {
- console.log(chalk.cyan.bold('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
- console.log(chalk.cyan.bold(' SUMMARY'));
- console.log(chalk.cyan.bold('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n'));
-
- console.log(chalk.bold('Bundle Statistics:'));
- console.log(` Total agents found: ${this.stats.totalAgents}`);
- console.log(` Successfully bundled: ${chalk.green(this.stats.bundledAgents)}`);
- if (this.stats.skippedAgents > 0) {
- console.log(` Skipped (webskip/bundle): ${chalk.gray(this.stats.skippedAgents)}`);
- }
-
- if (this.stats.failedAgents > 0) {
- console.log(` Failed to bundle: ${chalk.red(this.stats.failedAgents)}`);
- }
-
- if (this.stats.invalidXml > 0) {
- console.log(` Invalid XML bundles: ${chalk.yellow(this.stats.invalidXml)}`);
- }
-
- // Display warnings summary
- // Check if there are actually any warnings with content
- const hasActualWarnings = this.stats.warnings.some((w) => w && w.warnings && w.warnings.length > 0);
-
- if (hasActualWarnings) {
- console.log(chalk.yellow('\nโ Missing Dependencies by Agent:'));
-
- // Group and display warnings by agent
- for (const agentWarning of this.stats.warnings) {
- if (agentWarning && agentWarning.warnings && agentWarning.warnings.length > 0) {
- console.log(chalk.bold(`\n ${agentWarning.agent}:`));
- // Display unique warnings for this agent
- const uniqueWarnings = [...new Set(agentWarning.warnings)];
- for (const warning of uniqueWarnings) {
- console.log(chalk.dim(` โข ${warning}`));
- }
- }
- }
- } else {
- console.log(chalk.green('\nโ No missing dependencies'));
- }
-
- // Final status
- if (this.stats.invalidXml > 0) {
- console.log(chalk.yellow('\nโ Some bundles have invalid XML. Please review the output.'));
- } else if (this.stats.failedAgents > 0) {
- console.log(chalk.yellow('\nโ Some agents failed to bundle. Please review the errors.'));
- } else {
- console.log(chalk.green('\nโจ All bundles generated successfully!'));
- }
-
- console.log(chalk.cyan.bold('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n'));
- }
-}
-
-module.exports = { WebBundler };
diff --git a/tools/cli/lib/replace-project-root.js b/tools/cli/lib/replace-project-root.js
deleted file mode 100644
index 8230d7fb..00000000
--- a/tools/cli/lib/replace-project-root.js
+++ /dev/null
@@ -1,239 +0,0 @@
-/**
- * Utility function to replace {project-root} placeholders with actual installation target
- * Used during BMAD installation to set correct paths in agent and task files
- */
-
-const fs = require('node:fs');
-const path = require('node:path');
-
-/**
- * Replace {project-root} and {output_folder}/ placeholders in a single file
- * @param {string} filePath - Path to the file to process
- * @param {string} projectRoot - The actual project root path to substitute (must include trailing slash)
- * @param {string} docOut - The document output path (with leading slash)
- * @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing
- * @returns {boolean} - True if replacements were made, false otherwise
- */
-function replacePlaceholdersInFile(filePath, projectRoot, docOut = '/docs', removeCompletely = false) {
- try {
- let content = fs.readFileSync(filePath, 'utf8');
- const originalContent = content;
-
- if (removeCompletely) {
- // Remove placeholders entirely (for bundling)
- content = content.replaceAll('{project-root}', '');
- content = content.replaceAll('{output_folder}/', '');
- } else {
- // Handle the combined pattern first to avoid double slashes
- if (projectRoot && docOut) {
- // Replace {project-root}{output_folder}/ combinations first
- // Remove leading slash from docOut since projectRoot has trailing slash
- // Add trailing slash to docOut
- const docOutNoLeadingSlash = docOut.replace(/^\//, '');
- const docOutWithTrailingSlash = docOutNoLeadingSlash.endsWith('/') ? docOutNoLeadingSlash : docOutNoLeadingSlash + '/';
- content = content.replaceAll('{project-root}{output_folder}/', projectRoot + docOutWithTrailingSlash);
- }
-
- // Then replace remaining individual placeholders
- if (projectRoot) {
- content = content.replaceAll('{project-root}', projectRoot);
- }
-
- if (docOut) {
- // For standalone {output_folder}/, keep the leading slash and add trailing slash
- const docOutWithTrailingSlash = docOut.endsWith('/') ? docOut : docOut + '/';
- content = content.replaceAll('{output_folder}/', docOutWithTrailingSlash);
- }
- }
-
- if (content !== originalContent) {
- fs.writeFileSync(filePath, content, 'utf8');
- return true;
- }
-
- return false;
- } catch (error) {
- console.error(`Error processing file ${filePath}:`, error.message);
- return false;
- }
-}
-
-/**
- * Legacy function name for backward compatibility
- */
-function replaceProjectRootInFile(filePath, projectRoot, removeCompletely = false) {
- return replacePlaceholdersInFile(filePath, projectRoot, '/docs', removeCompletely);
-}
-
-/**
- * Recursively replace {project-root} and {output_folder}/ in all files in a directory
- * @param {string} dirPath - Directory to process
- * @param {string} projectRoot - The actual project root path to substitute (or null to remove)
- * @param {string} docOut - The document output path (with leading slash)
- * @param {Array} extensions - File extensions to process (default: ['.md', '.xml', '.yaml'])
- * @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing
- * @param {boolean} verbose - If true, show detailed output for each file
- * @returns {Object} - Stats object with counts of files processed and modified
- */
-function replacePlaceholdersInDirectory(
- dirPath,
- projectRoot,
- docOut = '/docs',
- extensions = ['.md', '.xml', '.yaml'],
- removeCompletely = false,
- verbose = false,
-) {
- const stats = {
- processed: 0,
- modified: 0,
- errors: 0,
- };
-
- function processDirectory(currentPath) {
- try {
- const items = fs.readdirSync(currentPath, { withFileTypes: true });
-
- for (const item of items) {
- const fullPath = path.join(currentPath, item.name);
-
- if (item.isDirectory()) {
- // Skip node_modules and .git directories
- if (item.name !== 'node_modules' && item.name !== '.git') {
- processDirectory(fullPath);
- }
- } else if (item.isFile()) {
- // Check if file has one of the target extensions
- const ext = path.extname(item.name).toLowerCase();
- if (extensions.includes(ext)) {
- stats.processed++;
- if (replacePlaceholdersInFile(fullPath, projectRoot, docOut, removeCompletely)) {
- stats.modified++;
- if (verbose) {
- console.log(`โ Updated: ${fullPath}`);
- }
- }
- }
- }
- }
- } catch (error) {
- console.error(`Error processing directory ${currentPath}:`, error.message);
- stats.errors++;
- }
- }
-
- processDirectory(dirPath);
- return stats;
-}
-
-/**
- * Legacy function for backward compatibility
- */
-function replaceProjectRootInDirectory(dirPath, projectRoot, extensions = ['.md', '.xml'], removeCompletely = false) {
- return replacePlaceholdersInDirectory(dirPath, projectRoot, '/docs', extensions, removeCompletely);
-}
-
-/**
- * Replace placeholders in a list of specific files
- * @param {Array} filePaths - Array of file paths to process
- * @param {string} projectRoot - The actual project root path to substitute (or null to remove)
- * @param {string} docOut - The document output path (with leading slash)
- * @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing
- * @returns {Object} - Stats object with counts of files processed and modified
- */
-function replacePlaceholdersInFiles(filePaths, projectRoot, docOut = '/docs', removeCompletely = false) {
- const stats = {
- processed: 0,
- modified: 0,
- errors: 0,
- };
-
- for (const filePath of filePaths) {
- stats.processed++;
- try {
- if (replacePlaceholdersInFile(filePath, projectRoot, docOut, removeCompletely)) {
- stats.modified++;
- console.log(`โ Updated: ${filePath}`);
- }
- } catch (error) {
- console.error(`Error processing file ${filePath}:`, error.message);
- stats.errors++;
- }
- }
-
- return stats;
-}
-
-/**
- * Legacy function for backward compatibility
- */
-function replaceProjectRootInFiles(filePaths, projectRoot, removeCompletely = false) {
- return replacePlaceholdersInFiles(filePaths, projectRoot, '/docs', removeCompletely);
-}
-
-/**
- * Main installation helper - replaces {project-root} and {output_folder}/ during BMAD installation
- * @param {string} installPath - Path where BMAD is being installed
- * @param {string} targetProjectRoot - The project root to set in the files (slash will be added)
- * @param {string} docsOutputPath - The documentation output path (relative to project root)
- * @param {boolean} verbose - If true, show detailed output
- * @returns {Object} - Installation stats
- */
-function processInstallation(installPath, targetProjectRoot, docsOutputPath = 'docs', verbose = false) {
- // Ensure project root has trailing slash since usage is like {project-root}/bmad
- const projectRootWithSlash = targetProjectRoot.endsWith('/') ? targetProjectRoot : targetProjectRoot + '/';
-
- // Ensure docs path has leading slash (for internal use) but will add trailing slash during replacement
- const normalizedDocsPath = docsOutputPath.replaceAll(/^\/+|\/+$/g, '');
- const docOutPath = normalizedDocsPath ? `/${normalizedDocsPath}` : '/docs';
-
- if (verbose) {
- console.log(`\nReplacing {project-root} with: ${projectRootWithSlash}`);
- console.log(`Replacing {output_folder}/ with: ${docOutPath}/`);
- console.log(`Processing files in: ${installPath}\n`);
- }
-
- const stats = replacePlaceholdersInDirectory(installPath, projectRootWithSlash, docOutPath, ['.md', '.xml', '.yaml'], false, verbose);
-
- if (verbose) {
- console.log('\n--- Installation Processing Complete ---');
- }
- console.log(`Files processed: ${stats.processed}`);
- console.log(`Files modified: ${stats.modified}`);
- if (stats.errors > 0) {
- console.log(`Errors encountered: ${stats.errors}`);
- }
-
- return stats;
-}
-
-/**
- * Bundle helper - removes {project-root}/ references for web bundling
- * @param {string} bundlePath - Path where files are being bundled
- * @returns {Object} - Bundle stats
- */
-function processBundleRemoval(bundlePath) {
- console.log(`\nRemoving {project-root}/ references for bundling`);
- console.log(`Processing files in: ${bundlePath}\n`);
-
- const stats = replaceProjectRootInDirectory(bundlePath, null, ['.md', '.xml'], true);
-
- console.log('\n--- Bundle Processing Complete ---');
- console.log(`Files processed: ${stats.processed}`);
- console.log(`Files modified: ${stats.modified}`);
- if (stats.errors > 0) {
- console.log(`Errors encountered: ${stats.errors}`);
- }
-
- return stats;
-}
-
-module.exports = {
- replacePlaceholdersInFile,
- replacePlaceholdersInDirectory,
- replacePlaceholdersInFiles,
- replaceProjectRootInFile,
- replaceProjectRootInDirectory,
- replaceProjectRootInFiles,
- processInstallation,
- processBundleRemoval,
-};
diff --git a/tools/cli/regenerate-manifests.js b/tools/cli/regenerate-manifests.js
deleted file mode 100644
index c370497b..00000000
--- a/tools/cli/regenerate-manifests.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const path = require('node:path');
-const { ManifestGenerator } = require('./installers/lib/core/manifest-generator');
-
-async function regenerateManifests() {
- const generator = new ManifestGenerator();
- const targetDir = process.argv[2];
-
- // List of modules to include in manifests
- const selectedModules = ['bmb', 'bmm', 'cis'];
-
- console.log('Regenerating manifests with relative paths...');
- console.log('Target directory: .bmad');
-
- try {
- const result = await generator.generateManifests('.bmad', selectedModules, [], { ides: [] });
- console.log('โ Manifests generated successfully:');
- console.log(` - ${result.workflows} workflows`);
- console.log(` - ${result.agents} agents`);
- console.log(` - ${result.tasks} tasks`);
- console.log(` - ${result.files} files in files-manifest.csv`);
- } catch (error) {
- console.error('Error generating manifests:', error);
- process.exit(1);
- }
-}
-
-regenerateManifests();
diff --git a/tools/cli/test-yaml-builder.js b/tools/cli/test-yaml-builder.js
deleted file mode 100644
index 1c5bf9bd..00000000
--- a/tools/cli/test-yaml-builder.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Test script for YAML โ XML agent builder
- * Usage: node tools/cli/test-yaml-builder.js
- */
-
-const path = require('node:path');
-const { YamlXmlBuilder } = require('./lib/yaml-xml-builder');
-const { getProjectRoot } = require('./lib/project-root');
-
-async function test() {
- console.log('Testing YAML โ XML Agent Builder\n');
-
- const builder = new YamlXmlBuilder();
- const projectRoot = getProjectRoot();
-
- // Paths
- const agentYamlPath = path.join(projectRoot, 'src/modules/bmm/agents/pm.agent.yaml');
- const outputPath = path.join(projectRoot, 'test-output-pm.md');
-
- console.log(`Source: ${agentYamlPath}`);
- console.log(`Output: ${outputPath}\n`);
-
- try {
- const result = await builder.buildAgent(
- agentYamlPath,
- null, // No customize file for this test
- outputPath,
- { includeMetadata: true },
- );
-
- console.log('โ Build successful!');
- console.log(` Output: ${result.outputPath}`);
- console.log(` Source hash: ${result.sourceHash}`);
- console.log('\nGenerated XML file at:', outputPath);
- console.log('Review the output to verify correctness.\n');
- } catch (error) {
- console.error('โ Build failed:', error.message);
- console.error(error.stack);
- process.exit(1);
- }
-}
-
-test();
diff --git a/tools/flattener/ignoreRules.js b/tools/flattener/ignoreRules.js
index 512f7166..b825edea 100644
--- a/tools/flattener/ignoreRules.js
+++ b/tools/flattener/ignoreRules.js
@@ -6,7 +6,7 @@ const ignore = require('ignore');
// These complement .gitignore and are applied regardless of VCS presence.
const DEFAULT_PATTERNS = [
// Project/VCS
- '**/.bmad-method/**',
+ '**/_bmad/**',
'**/.git/**',
'**/.svn/**',
'**/.hg/**',