bmad-method 6.0.0-alpha.14 → 6.0.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.coderabbit.yaml +36 -0
  2. package/{CODE_OF_CONDUCT.md → .github/CODE_OF_CONDUCT.md} +4 -4
  3. package/CHANGELOG.md +136 -408
  4. package/README.md +4 -1
  5. package/docs/custom-content-installation.md +245 -0
  6. package/docs/index.md +2 -2
  7. package/docs/installers-bundlers/installers-modules-platforms-reference.md +6 -5
  8. package/docs/web-bundles-gemini-gpt-guide.md +1 -1
  9. package/example-custom-content/README.md +4 -0
  10. package/example-custom-content/agents/commit-poet/commit-poet.agent.yaml +1 -1
  11. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/instructions.md +1 -1
  12. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/docs.md +1 -1
  13. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/installers.md +1 -1
  14. package/example-custom-content/agents/toolsmith/toolsmith-sidecar/knowledge/modules.md +2 -2
  15. package/example-custom-content/agents/toolsmith/toolsmith.agent.yaml +1 -1
  16. package/example-custom-content/{custom.yaml → module.yaml} +1 -0
  17. package/example-custom-content/workflows/quiz-master/steps/step-01-init.md +2 -2
  18. package/example-custom-content/workflows/quiz-master/steps/step-02-q1.md +1 -1
  19. package/example-custom-content/workflows/quiz-master/steps/step-03-q2.md +1 -1
  20. package/example-custom-content/workflows/quiz-master/steps/step-04-q3.md +1 -1
  21. package/example-custom-content/workflows/quiz-master/steps/step-05-q4.md +1 -1
  22. package/example-custom-content/workflows/quiz-master/steps/step-06-q5.md +1 -1
  23. package/example-custom-content/workflows/quiz-master/steps/step-07-q6.md +1 -1
  24. package/example-custom-content/workflows/quiz-master/steps/step-08-q7.md +1 -1
  25. package/example-custom-content/workflows/quiz-master/steps/step-09-q8.md +1 -1
  26. package/example-custom-content/workflows/quiz-master/steps/step-10-q9.md +1 -1
  27. package/example-custom-content/workflows/quiz-master/steps/step-11-q10.md +1 -1
  28. package/example-custom-content/workflows/quiz-master/steps/step-12-results.md +1 -1
  29. package/example-custom-content/workflows/quiz-master/workflow.md +1 -1
  30. package/example-custom-module/mwm/README.md +5 -0
  31. package/example-custom-module/mwm/agents/cbt-coach/cbt-coach.agent.yaml +1 -0
  32. package/example-custom-module/mwm/agents/crisis-navigator.agent.yaml +3 -2
  33. package/example-custom-module/mwm/agents/meditation-guide.agent.yaml +3 -2
  34. package/example-custom-module/mwm/agents/wellness-companion/wellness-companion.agent.yaml +1 -0
  35. package/example-custom-module/mwm/{_module-installer/install-config.yaml → module.yaml} +1 -0
  36. package/package.json +1 -1
  37. package/src/core/_module-installer/installer.js +1 -1
  38. package/src/modules/bmb/_module-installer/installer.js +1 -1
  39. package/src/modules/bmb/docs/agents/index.md +1 -1
  40. package/src/modules/bmb/workflows/create-module/steps/step-04-structure.md +3 -3
  41. package/src/modules/bmb/workflows/create-module/steps/step-05-config.md +1 -1
  42. package/src/modules/bmb/workflows/create-module/steps/step-08-installer.md +8 -8
  43. package/src/modules/bmb/workflows/create-module/steps/step-09-documentation.md +2 -1
  44. package/src/modules/bmb/workflows/create-module/steps/step-10-roadmap.md +3 -2
  45. package/src/modules/bmb/workflows/create-module/steps/step-11-validate.md +3 -3
  46. package/src/modules/bmb/workflows/create-module/templates/installer.template.js +1 -1
  47. package/src/modules/bmb/workflows/create-module/validation.md +3 -3
  48. package/src/modules/bmb/workflows/create-workflow/steps/step-01-init.md +1 -1
  49. package/src/modules/bmb/workflows/create-workflow/steps/step-07-build.md +1 -1
  50. package/src/modules/bmgd/README.md +2 -1
  51. package/src/modules/bmm/_module-installer/installer.js +1 -1
  52. package/src/modules/bmm/_module-installer/platform-specifics/claude-code.js +1 -1
  53. package/src/modules/bmm/_module-installer/platform-specifics/windsurf.js +1 -1
  54. package/src/modules/cis/_module-installer/installer.js +1 -1
  55. package/tools/cli/README.md +4 -4
  56. package/tools/cli/installers/lib/core/config-collector.js +16 -8
  57. package/tools/cli/installers/lib/core/custom-module-cache.js +239 -0
  58. package/tools/cli/installers/lib/core/detector.js +8 -4
  59. package/tools/cli/installers/lib/core/installer.js +815 -23
  60. package/tools/cli/installers/lib/core/manifest-generator.js +176 -13
  61. package/tools/cli/installers/lib/core/manifest.js +47 -0
  62. package/tools/cli/installers/lib/custom/handler.js +150 -20
  63. package/tools/cli/installers/lib/modules/manager.js +78 -32
  64. package/tools/cli/lib/agent/compiler.js +3 -11
  65. package/tools/cli/lib/agent/installer.js +2 -1
  66. package/tools/cli/lib/cli-utils.js +21 -4
  67. package/tools/cli/lib/ui.js +499 -11
  68. package/tools/maintainer/review-pr-README.md +55 -0
  69. package/tools/maintainer/review-pr.md +242 -0
  70. package/tools/migrate-custom-module-paths.js +124 -0
  71. package/bmad-method-6.0.0-alpha.14.tgz +0 -0
  72. package/docs/custom-agent-installation.md +0 -137
  73. package/example-custom-content/workflows/quiz-master/workflow-plan-quiz-master.md +0 -269
  74. /package/src/core/{_module-installer/install-config.yaml → module.yaml} +0 -0
  75. /package/src/modules/bmb/{_module-installer/install-config.yaml → module.yaml} +0 -0
  76. /package/src/modules/bmb/workflows/create-module/templates/{install-config.template.yaml → module.template.yaml} +0 -0
  77. /package/src/modules/bmgd/{_module-installer/install-config.yaml → module.yaml} +0 -0
  78. /package/src/modules/bmm/{_module-installer/install-config.yaml → module.yaml} +0 -0
  79. /package/src/modules/cis/{_module-installer/install-config.yaml → module.yaml} +0 -0
@@ -41,7 +41,11 @@ class ManifestGenerator {
41
41
  // Deduplicate modules list to prevent duplicates
42
42
  this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
43
43
  this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned
44
- this.preservedModules = preservedModules; // These stay as-is in CSVs
44
+
45
+ // For CSV manifests, we need to include ALL modules that are installed
46
+ // preservedModules controls which modules stay as-is in the CSV (don't get rescanned)
47
+ // But all modules should be included in the final manifest
48
+ this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules
45
49
  this.bmadDir = bmadDir;
46
50
  this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '.bmad' or 'bmad')
47
51
  this.allInstalledFiles = installedFiles;
@@ -61,14 +65,14 @@ class ManifestGenerator {
61
65
  // Collect workflow data
62
66
  await this.collectWorkflows(selectedModules);
63
67
 
64
- // Collect agent data
65
- await this.collectAgents(selectedModules);
68
+ // Collect agent data - use updatedModules which includes all installed modules
69
+ await this.collectAgents(this.updatedModules);
66
70
 
67
71
  // Collect task data
68
- await this.collectTasks(selectedModules);
72
+ await this.collectTasks(this.updatedModules);
69
73
 
70
74
  // Collect tool data
71
- await this.collectTools(selectedModules);
75
+ await this.collectTools(this.updatedModules);
72
76
 
73
77
  // Write manifest files and collect their paths
74
78
  const manifestFiles = [
@@ -450,6 +454,21 @@ class ManifestGenerator {
450
454
  async writeMainManifest(cfgDir) {
451
455
  const manifestPath = path.join(cfgDir, 'manifest.yaml');
452
456
 
457
+ // Read existing manifest to preserve custom modules
458
+ let existingCustomModules = [];
459
+ if (await fs.pathExists(manifestPath)) {
460
+ try {
461
+ const existingContent = await fs.readFile(manifestPath, 'utf8');
462
+ const existingManifest = yaml.load(existingContent);
463
+ if (existingManifest && existingManifest.customModules) {
464
+ existingCustomModules = existingManifest.customModules;
465
+ }
466
+ } catch {
467
+ // If we can't read the existing manifest, continue without preserving custom modules
468
+ console.warn('Warning: Could not read existing manifest to preserve custom modules');
469
+ }
470
+ }
471
+
453
472
  const manifest = {
454
473
  installation: {
455
474
  version: packageJson.version,
@@ -457,6 +476,7 @@ class ManifestGenerator {
457
476
  lastUpdated: new Date().toISOString(),
458
477
  },
459
478
  modules: this.modules,
479
+ customModules: existingCustomModules, // Preserve custom modules
460
480
  ides: this.selectedIdes,
461
481
  };
462
482
 
@@ -562,12 +582,47 @@ class ManifestGenerator {
562
582
  async writeWorkflowManifest(cfgDir) {
563
583
  const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
564
584
 
585
+ // Read existing manifest to preserve entries
586
+ const existingEntries = new Map();
587
+ if (await fs.pathExists(csvPath)) {
588
+ const content = await fs.readFile(csvPath, 'utf8');
589
+ const lines = content.split('\n').filter((line) => line.trim());
590
+
591
+ // Skip header
592
+ for (let i = 1; i < lines.length; i++) {
593
+ const line = lines[i];
594
+ if (line) {
595
+ // Parse CSV (simple parsing assuming no commas in quoted fields)
596
+ const parts = line.split('","');
597
+ if (parts.length >= 4) {
598
+ const name = parts[0].replace(/^"/, '');
599
+ const module = parts[2];
600
+ existingEntries.set(`${module}:${name}`, line);
601
+ }
602
+ }
603
+ }
604
+ }
605
+
565
606
  // Create CSV header - removed standalone column as ALL workflows now generate commands
566
607
  let csv = 'name,description,module,path\n';
567
608
 
568
- // Add all workflows - no standalone property needed anymore
609
+ // Combine existing and new workflows
610
+ const allWorkflows = new Map();
611
+
612
+ // Add existing entries
613
+ for (const [key, value] of existingEntries) {
614
+ allWorkflows.set(key, value);
615
+ }
616
+
617
+ // Add/update new workflows
569
618
  for (const workflow of this.workflows) {
570
- csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`;
619
+ const key = `${workflow.module}:${workflow.name}`;
620
+ allWorkflows.set(key, `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"`);
621
+ }
622
+
623
+ // Write all workflows
624
+ for (const [, value] of allWorkflows) {
625
+ csv += value + '\n';
571
626
  }
572
627
 
573
628
  await fs.writeFile(csvPath, csv);
@@ -581,12 +636,50 @@ class ManifestGenerator {
581
636
  async writeAgentManifest(cfgDir) {
582
637
  const csvPath = path.join(cfgDir, 'agent-manifest.csv');
583
638
 
639
+ // Read existing manifest to preserve entries
640
+ const existingEntries = new Map();
641
+ if (await fs.pathExists(csvPath)) {
642
+ const content = await fs.readFile(csvPath, 'utf8');
643
+ const lines = content.split('\n').filter((line) => line.trim());
644
+
645
+ // Skip header
646
+ for (let i = 1; i < lines.length; i++) {
647
+ const line = lines[i];
648
+ if (line) {
649
+ // Parse CSV (simple parsing assuming no commas in quoted fields)
650
+ const parts = line.split('","');
651
+ if (parts.length >= 11) {
652
+ const name = parts[0].replace(/^"/, '');
653
+ const module = parts[8];
654
+ existingEntries.set(`${module}:${name}`, line);
655
+ }
656
+ }
657
+ }
658
+ }
659
+
584
660
  // Create CSV header with persona fields
585
661
  let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
586
662
 
587
- // Add all agents
663
+ // Combine existing and new agents, preferring new data for duplicates
664
+ const allAgents = new Map();
665
+
666
+ // Add existing entries
667
+ for (const [key, value] of existingEntries) {
668
+ allAgents.set(key, value);
669
+ }
670
+
671
+ // Add/update new agents
588
672
  for (const agent of this.agents) {
589
- csv += `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"\n`;
673
+ const key = `${agent.module}:${agent.name}`;
674
+ allAgents.set(
675
+ key,
676
+ `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
677
+ );
678
+ }
679
+
680
+ // Write all agents
681
+ for (const [, value] of allAgents) {
682
+ csv += value + '\n';
590
683
  }
591
684
 
592
685
  await fs.writeFile(csvPath, csv);
@@ -600,12 +693,47 @@ class ManifestGenerator {
600
693
  async writeTaskManifest(cfgDir) {
601
694
  const csvPath = path.join(cfgDir, 'task-manifest.csv');
602
695
 
696
+ // Read existing manifest to preserve entries
697
+ const existingEntries = new Map();
698
+ if (await fs.pathExists(csvPath)) {
699
+ const content = await fs.readFile(csvPath, 'utf8');
700
+ const lines = content.split('\n').filter((line) => line.trim());
701
+
702
+ // Skip header
703
+ for (let i = 1; i < lines.length; i++) {
704
+ const line = lines[i];
705
+ if (line) {
706
+ // Parse CSV (simple parsing assuming no commas in quoted fields)
707
+ const parts = line.split('","');
708
+ if (parts.length >= 6) {
709
+ const name = parts[0].replace(/^"/, '');
710
+ const module = parts[3];
711
+ existingEntries.set(`${module}:${name}`, line);
712
+ }
713
+ }
714
+ }
715
+ }
716
+
603
717
  // Create CSV header with standalone column
604
718
  let csv = 'name,displayName,description,module,path,standalone\n';
605
719
 
606
- // Add all tasks
720
+ // Combine existing and new tasks
721
+ const allTasks = new Map();
722
+
723
+ // Add existing entries
724
+ for (const [key, value] of existingEntries) {
725
+ allTasks.set(key, value);
726
+ }
727
+
728
+ // Add/update new tasks
607
729
  for (const task of this.tasks) {
608
- csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"\n`;
730
+ const key = `${task.module}:${task.name}`;
731
+ allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
732
+ }
733
+
734
+ // Write all tasks
735
+ for (const [, value] of allTasks) {
736
+ csv += value + '\n';
609
737
  }
610
738
 
611
739
  await fs.writeFile(csvPath, csv);
@@ -619,12 +747,47 @@ class ManifestGenerator {
619
747
  async writeToolManifest(cfgDir) {
620
748
  const csvPath = path.join(cfgDir, 'tool-manifest.csv');
621
749
 
750
+ // Read existing manifest to preserve entries
751
+ const existingEntries = new Map();
752
+ if (await fs.pathExists(csvPath)) {
753
+ const content = await fs.readFile(csvPath, 'utf8');
754
+ const lines = content.split('\n').filter((line) => line.trim());
755
+
756
+ // Skip header
757
+ for (let i = 1; i < lines.length; i++) {
758
+ const line = lines[i];
759
+ if (line) {
760
+ // Parse CSV (simple parsing assuming no commas in quoted fields)
761
+ const parts = line.split('","');
762
+ if (parts.length >= 6) {
763
+ const name = parts[0].replace(/^"/, '');
764
+ const module = parts[3];
765
+ existingEntries.set(`${module}:${name}`, line);
766
+ }
767
+ }
768
+ }
769
+ }
770
+
622
771
  // Create CSV header with standalone column
623
772
  let csv = 'name,displayName,description,module,path,standalone\n';
624
773
 
625
- // Add all tools
774
+ // Combine existing and new tools
775
+ const allTools = new Map();
776
+
777
+ // Add existing entries
778
+ for (const [key, value] of existingEntries) {
779
+ allTools.set(key, value);
780
+ }
781
+
782
+ // Add/update new tools
626
783
  for (const tool of this.tools) {
627
- csv += `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"\n`;
784
+ const key = `${tool.module}:${tool.name}`;
785
+ allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
786
+ }
787
+
788
+ // Write all tools
789
+ for (const [, value] of allTools) {
790
+ csv += value + '\n';
628
791
  }
629
792
 
630
793
  await fs.writeFile(csvPath, csv);
@@ -61,6 +61,7 @@ class Manifest {
61
61
  installDate: manifestData.installation?.installDate,
62
62
  lastUpdated: manifestData.installation?.lastUpdated,
63
63
  modules: manifestData.modules || [],
64
+ customModules: manifestData.customModules || [],
64
65
  ides: manifestData.ides || [],
65
66
  };
66
67
  } catch (error) {
@@ -93,6 +94,7 @@ class Manifest {
93
94
  lastUpdated: manifest.lastUpdated,
94
95
  },
95
96
  modules: manifest.modules || [],
97
+ customModules: manifest.customModules || [],
96
98
  ides: manifest.ides || [],
97
99
  };
98
100
 
@@ -535,6 +537,51 @@ class Manifest {
535
537
 
536
538
  return configs;
537
539
  }
540
+ /**
541
+ * Add a custom module to the manifest with its source path
542
+ * @param {string} bmadDir - Path to bmad directory
543
+ * @param {Object} customModule - Custom module info
544
+ */
545
+ async addCustomModule(bmadDir, customModule) {
546
+ const manifest = await this.read(bmadDir);
547
+ if (!manifest) {
548
+ throw new Error('No manifest found');
549
+ }
550
+
551
+ if (!manifest.customModules) {
552
+ manifest.customModules = [];
553
+ }
554
+
555
+ // Check if custom module already exists
556
+ const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id);
557
+ if (existingIndex === -1) {
558
+ // Add new entry
559
+ manifest.customModules.push(customModule);
560
+ } else {
561
+ // Update existing entry
562
+ manifest.customModules[existingIndex] = customModule;
563
+ }
564
+
565
+ await this.update(bmadDir, { customModules: manifest.customModules });
566
+ }
567
+
568
+ /**
569
+ * Remove a custom module from the manifest
570
+ * @param {string} bmadDir - Path to bmad directory
571
+ * @param {string} moduleId - Module ID to remove
572
+ */
573
+ async removeCustomModule(bmadDir, moduleId) {
574
+ const manifest = await this.read(bmadDir);
575
+ if (!manifest || !manifest.customModules) {
576
+ return;
577
+ }
578
+
579
+ const index = manifest.customModules.findIndex((m) => m.id === moduleId);
580
+ if (index !== -1) {
581
+ manifest.customModules.splice(index, 1);
582
+ await this.update(bmadDir, { customModules: manifest.customModules });
583
+ }
584
+ }
538
585
  }
539
586
 
540
587
  module.exports = { Manifest };
@@ -3,6 +3,7 @@ const fs = require('fs-extra');
3
3
  const chalk = require('chalk');
4
4
  const yaml = require('js-yaml');
5
5
  const { FileOps } = require('../../../lib/file-ops');
6
+ const { XmlHandler } = require('../../../lib/xml-handler');
6
7
 
7
8
  /**
8
9
  * Handler for custom content (custom.yaml)
@@ -11,6 +12,7 @@ const { FileOps } = require('../../../lib/file-ops');
11
12
  class CustomHandler {
12
13
  constructor() {
13
14
  this.fileOps = new FileOps();
15
+ this.xmlHandler = new XmlHandler();
14
16
  }
15
17
 
16
18
  /**
@@ -52,6 +54,12 @@ class CustomHandler {
52
54
  } else if (entry.name === 'custom.yaml') {
53
55
  // Found a custom.yaml file
54
56
  customPaths.push(fullPath);
57
+ } else if (
58
+ entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
59
+ // Skip if it's in src/modules (those are standard modules)
60
+ !fullPath.includes(path.join('src', 'modules'))
61
+ ) {
62
+ customPaths.push(fullPath);
55
63
  }
56
64
  }
57
65
  } catch {
@@ -66,37 +74,44 @@ class CustomHandler {
66
74
  }
67
75
 
68
76
  /**
69
- * Get custom content info from a custom.yaml file
70
- * @param {string} customYamlPath - Path to custom.yaml file
77
+ * Get custom content info from a custom.yaml or module.yaml file
78
+ * @param {string} configPath - Path to config file
79
+ * @param {string} projectRoot - Project root directory for calculating relative paths
71
80
  * @returns {Object|null} Custom content info
72
81
  */
73
- async getCustomInfo(customYamlPath) {
82
+ async getCustomInfo(configPath, projectRoot = null) {
74
83
  try {
75
- const configContent = await fs.readFile(customYamlPath, 'utf8');
84
+ const configContent = await fs.readFile(configPath, 'utf8');
76
85
 
77
86
  // Try to parse YAML with error handling
78
87
  let config;
79
88
  try {
80
89
  config = yaml.load(configContent);
81
90
  } catch (parseError) {
82
- console.warn(chalk.yellow(`Warning: YAML parse error in ${customYamlPath}:`, parseError.message));
91
+ console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
83
92
  return null;
84
93
  }
85
94
 
86
- const customDir = path.dirname(customYamlPath);
87
- const relativePath = path.relative(process.cwd(), customDir);
95
+ // Check if this is an module.yaml (module) or custom.yaml (custom content)
96
+ const isInstallConfig = configPath.endsWith('module.yaml');
97
+ const configDir = path.dirname(configPath);
98
+
99
+ // Use provided projectRoot or fall back to process.cwd()
100
+ const basePath = projectRoot || process.cwd();
101
+ const relativePath = path.relative(basePath, configDir);
88
102
 
89
103
  return {
90
- id: config.code || path.basename(customDir),
91
- name: config.name || `Custom: ${path.basename(customDir)}`,
92
- description: config.description || 'Custom agents and workflows',
93
- path: customDir,
104
+ id: config.code || 'unknown-code',
105
+ name: config.name,
106
+ description: config.description || '',
107
+ path: configDir,
94
108
  relativePath: relativePath,
95
109
  defaultSelected: config.default_selected === true,
96
110
  config: config,
111
+ isInstallConfig: isInstallConfig, // Track which type this is
97
112
  };
98
113
  } catch (error) {
99
- console.warn(chalk.yellow(`Warning: Failed to read ${customYamlPath}:`, error.message));
114
+ console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
100
115
  return null;
101
116
  }
102
117
  }
@@ -128,10 +143,10 @@ class CustomHandler {
128
143
  await fs.ensureDir(bmadAgentsDir);
129
144
  await fs.ensureDir(bmadWorkflowsDir);
130
145
 
131
- // Process agents - copy entire agents directory structure
146
+ // Process agents - compile and copy agents
132
147
  const agentsDir = path.join(customPath, 'agents');
133
148
  if (await fs.pathExists(agentsDir)) {
134
- await this.copyDirectory(agentsDir, bmadAgentsDir, results, fileTrackingCallback, config);
149
+ await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);
135
150
 
136
151
  // Count agent files
137
152
  const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
@@ -236,13 +251,20 @@ class CustomHandler {
236
251
  // Copy with placeholder replacement for text files
237
252
  const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json'];
238
253
  if (textExtensions.some((ext) => entry.name.endsWith(ext))) {
239
- await this.fileOps.copyFile(sourcePath, targetPath, {
240
- bmadFolder: config.bmad_folder || 'bmad',
241
- userName: config.user_name || 'User',
242
- communicationLanguage: config.communication_language || 'English',
243
- outputFolder: config.output_folder || 'docs',
244
- });
254
+ // Read source content
255
+ let content = await fs.readFile(sourcePath, 'utf8');
256
+
257
+ // Replace placeholders
258
+ content = content.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
259
+ content = content.replaceAll('{user_name}', config.user_name || 'User');
260
+ content = content.replaceAll('{communication_language}', config.communication_language || 'English');
261
+ content = content.replaceAll('{output_folder}', config.output_folder || 'docs');
262
+
263
+ // Write to target
264
+ await fs.ensureDir(path.dirname(targetPath));
265
+ await fs.writeFile(targetPath, content, 'utf8');
245
266
  } else {
267
+ // Copy binary files as-is
246
268
  await fs.copy(sourcePath, targetPath);
247
269
  }
248
270
 
@@ -261,6 +283,114 @@ class CustomHandler {
261
283
  }
262
284
  }
263
285
  }
286
+
287
+ /**
288
+ * Compile .agent.yaml files to .md format and handle sidecars
289
+ * @param {string} sourceAgentsPath - Source agents directory
290
+ * @param {string} targetAgentsPath - Target agents directory
291
+ * @param {string} bmadDir - BMAD installation directory
292
+ * @param {Object} config - Configuration for placeholder replacement
293
+ * @param {Function} fileTrackingCallback - Optional callback to track installed files
294
+ * @param {Object} results - Results object to update
295
+ */
296
+ async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
297
+ // Get all .agent.yaml files recursively
298
+ const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
299
+
300
+ for (const agentFile of agentFiles) {
301
+ const relativePath = path.relative(sourceAgentsPath, agentFile);
302
+ const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
303
+
304
+ await fs.ensureDir(targetDir);
305
+
306
+ const agentName = path.basename(agentFile, '.agent.yaml');
307
+ const targetMdPath = path.join(targetDir, `${agentName}.md`);
308
+ // Use the actual bmadDir if available (for when installing to temp dir)
309
+ const actualBmadDir = config._bmadDir || bmadDir;
310
+ const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);
311
+
312
+ // Read and compile the YAML
313
+ try {
314
+ const yamlContent = await fs.readFile(agentFile, 'utf8');
315
+ const { compileAgent } = require('../../../lib/agent/compiler');
316
+
317
+ // Create customize template if it doesn't exist
318
+ if (!(await fs.pathExists(customizePath))) {
319
+ const { getSourcePath } = require('../../../lib/project-root');
320
+ const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
321
+ if (await fs.pathExists(genericTemplatePath)) {
322
+ // Copy with placeholder replacement
323
+ let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
324
+ templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
325
+ await fs.writeFile(customizePath, templateContent, 'utf8');
326
+ console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
327
+ }
328
+ }
329
+
330
+ // Compile the agent
331
+ const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });
332
+
333
+ // Replace placeholders in the compiled content
334
+ let processedXml = xml;
335
+ processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
336
+ processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
337
+ processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
338
+ processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');
339
+
340
+ // Write the compiled MD file
341
+ await fs.writeFile(targetMdPath, processedXml, 'utf8');
342
+
343
+ // Check if agent has sidecar
344
+ let hasSidecar = false;
345
+ try {
346
+ const yamlLib = require('yaml');
347
+ const agentYaml = yamlLib.parse(yamlContent);
348
+ hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
349
+ } catch {
350
+ // Continue without sidecar processing
351
+ }
352
+
353
+ // Copy sidecar files if agent has hasSidecar flag
354
+ if (hasSidecar && config.agent_sidecar_folder) {
355
+ const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
356
+
357
+ // Resolve agent sidecar folder path
358
+ const projectDir = path.dirname(bmadDir);
359
+ const resolvedSidecarFolder = config.agent_sidecar_folder
360
+ .replaceAll('{project-root}', projectDir)
361
+ .replaceAll('{bmad_folder}', path.basename(bmadDir));
362
+
363
+ // Create sidecar directory for this agent
364
+ const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
365
+ await fs.ensureDir(agentSidecarDir);
366
+
367
+ // Copy sidecar files
368
+ const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);
369
+
370
+ if (sidecarResult.copied.length > 0) {
371
+ console.log(chalk.dim(` Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
372
+ }
373
+ if (sidecarResult.preserved.length > 0) {
374
+ console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
375
+ }
376
+ }
377
+
378
+ // Track the file
379
+ if (fileTrackingCallback) {
380
+ fileTrackingCallback(targetMdPath);
381
+ }
382
+
383
+ console.log(
384
+ chalk.dim(
385
+ ` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
386
+ ),
387
+ );
388
+ } catch (error) {
389
+ console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
390
+ results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
391
+ }
392
+ }
393
+ }
264
394
  }
265
395
 
266
396
  module.exports = { CustomHandler };