bmad-method 6.3.1-next.2 → 6.3.1-next.21

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 (160) hide show
  1. package/package.json +3 -3
  2. package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +51 -36
  3. package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +90 -0
  4. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +50 -33
  5. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/customize.toml +81 -0
  6. package/src/bmm-skills/1-analysis/bmad-document-project/SKILL.md +57 -1
  7. package/src/bmm-skills/1-analysis/bmad-document-project/customize.toml +41 -0
  8. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md +1 -0
  9. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/full-scan-instructions.md +1 -0
  10. package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +48 -9
  11. package/src/bmm-skills/1-analysis/bmad-prfaq/customize.toml +41 -0
  12. package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +4 -0
  13. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +44 -9
  14. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +47 -0
  15. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/contextual-discovery.md +8 -7
  16. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/draft-and-review.md +6 -5
  17. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/finalize.md +4 -1
  18. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/guided-elicitation.md +3 -2
  19. package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +91 -1
  20. package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +41 -0
  21. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +6 -0
  22. package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +91 -1
  23. package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +41 -0
  24. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +6 -0
  25. package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +91 -1
  26. package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +41 -0
  27. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +6 -0
  28. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +50 -35
  29. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.toml +85 -0
  30. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +50 -31
  31. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/customize.toml +60 -0
  32. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/SKILL.md +99 -1
  33. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/customize.toml +41 -0
  34. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-08-scoping.md +70 -23
  35. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md +1 -1
  36. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
  37. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/SKILL.md +70 -1
  38. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +41 -0
  39. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
  40. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/SKILL.md +97 -1
  41. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +42 -0
  42. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
  43. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/SKILL.md +99 -1
  44. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +42 -0
  45. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
  46. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
  47. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
  48. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/SKILL.md +86 -1
  49. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +41 -0
  50. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
  51. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +69 -1
  52. package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +41 -0
  53. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
  54. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/SKILL.md +88 -1
  55. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +41 -0
  56. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
  57. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/SKILL.md +76 -1
  58. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +41 -0
  59. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
  60. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
  61. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
  62. package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +296 -1
  63. package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +41 -0
  64. package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +412 -1
  65. package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +41 -0
  66. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +171 -1
  67. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +41 -0
  68. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1507 -1
  69. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +41 -0
  70. package/src/bmm-skills/module.yaml +49 -0
  71. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
  72. package/src/core-skills/bmad-customize/SKILL.md +111 -0
  73. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
  74. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
  75. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  76. package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
  77. package/src/core-skills/module-help.csv +1 -0
  78. package/src/core-skills/module.yaml +3 -0
  79. package/src/scripts/resolve_config.py +176 -0
  80. package/src/scripts/resolve_customization.py +230 -0
  81. package/tools/installer/cli-utils.js +0 -137
  82. package/tools/installer/commands/install.js +13 -0
  83. package/tools/installer/commands/status.js +1 -1
  84. package/tools/installer/commands/uninstall.js +1 -1
  85. package/tools/installer/core/config.js +4 -1
  86. package/tools/installer/core/existing-install.js +1 -1
  87. package/tools/installer/core/install-paths.js +12 -6
  88. package/tools/installer/core/installer.js +182 -95
  89. package/tools/installer/core/manifest-generator.js +347 -190
  90. package/tools/installer/core/manifest.js +49 -642
  91. package/tools/installer/file-ops.js +1 -1
  92. package/tools/installer/fs-native.js +116 -0
  93. package/tools/installer/ide/_config-driven.js +1 -1
  94. package/tools/installer/ide/platform-codes.js +1 -1
  95. package/tools/installer/ide/shared/path-utils.js +0 -145
  96. package/tools/installer/ide/shared/skill-manifest.js +1 -1
  97. package/tools/installer/message-loader.js +1 -1
  98. package/tools/installer/modules/channel-plan.js +203 -0
  99. package/tools/installer/modules/channel-resolver.js +241 -0
  100. package/tools/installer/modules/community-manager.js +131 -24
  101. package/tools/installer/modules/custom-module-manager.js +161 -47
  102. package/tools/installer/modules/external-manager.js +236 -73
  103. package/tools/installer/modules/official-modules.js +61 -63
  104. package/tools/installer/modules/plugin-resolver.js +1 -1
  105. package/tools/installer/modules/registry-client.js +133 -12
  106. package/tools/installer/modules/registry-fallback.yaml +8 -0
  107. package/tools/installer/modules/version-resolver.js +336 -0
  108. package/tools/installer/project-root.js +55 -1
  109. package/tools/installer/prompts.js +0 -106
  110. package/tools/installer/ui.js +457 -51
  111. package/tools/migrate-custom-module-paths.js +1 -1
  112. package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
  113. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
  114. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
  115. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
  116. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
  117. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
  118. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
  119. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
  120. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
  121. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
  122. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
  123. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
  124. package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
  125. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
  126. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
  127. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
  128. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
  129. package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
  130. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
  131. package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
  132. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
  133. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
  134. package/tools/installer/ide/shared/agent-command-generator.js +0 -180
  135. package/tools/installer/ide/shared/bmad-artifacts.js +0 -208
  136. package/tools/installer/ide/shared/module-injections.js +0 -136
  137. package/tools/installer/ide/templates/agent-command-template.md +0 -14
  138. package/tools/installer/ide/templates/combined/antigravity.md +0 -8
  139. package/tools/installer/ide/templates/combined/default-agent.md +0 -15
  140. package/tools/installer/ide/templates/combined/default-task.md +0 -10
  141. package/tools/installer/ide/templates/combined/default-tool.md +0 -10
  142. package/tools/installer/ide/templates/combined/default-workflow.md +0 -6
  143. package/tools/installer/ide/templates/combined/gemini-agent.toml +0 -14
  144. package/tools/installer/ide/templates/combined/gemini-task.toml +0 -11
  145. package/tools/installer/ide/templates/combined/gemini-tool.toml +0 -11
  146. package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +0 -16
  147. package/tools/installer/ide/templates/combined/gemini-workflow.toml +0 -14
  148. package/tools/installer/ide/templates/combined/kiro-agent.md +0 -16
  149. package/tools/installer/ide/templates/combined/kiro-task.md +0 -9
  150. package/tools/installer/ide/templates/combined/kiro-tool.md +0 -9
  151. package/tools/installer/ide/templates/combined/kiro-workflow.md +0 -7
  152. package/tools/installer/ide/templates/combined/opencode-agent.md +0 -15
  153. package/tools/installer/ide/templates/combined/opencode-task.md +0 -13
  154. package/tools/installer/ide/templates/combined/opencode-tool.md +0 -13
  155. package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +0 -16
  156. package/tools/installer/ide/templates/combined/opencode-workflow.md +0 -16
  157. package/tools/installer/ide/templates/combined/rovodev.md +0 -9
  158. package/tools/installer/ide/templates/combined/trae.md +0 -9
  159. package/tools/installer/ide/templates/combined/windsurf-workflow.md +0 -10
  160. package/tools/installer/ide/templates/split/.gitkeep +0 -0
@@ -1,15 +1,9 @@
1
1
  const path = require('node:path');
2
- const fs = require('fs-extra');
2
+ const fs = require('../fs-native');
3
3
  const yaml = require('yaml');
4
4
  const crypto = require('node:crypto');
5
- const csv = require('csv-parse/sync');
6
- const { getSourcePath, getModulePath } = require('../project-root');
5
+ const { resolveInstalledModuleYaml } = require('../project-root');
7
6
  const prompts = require('../prompts');
8
- const {
9
- loadSkillManifest: loadSkillManifestShared,
10
- getCanonicalId: getCanonicalIdShared,
11
- getArtifactType: getArtifactTypeShared,
12
- } = require('../ide/shared/skill-manifest');
13
7
 
14
8
  // Load package.json for version info
15
9
  const packageJson = require('../../../package.json');
@@ -26,21 +20,6 @@ class ManifestGenerator {
26
20
  this.selectedIdes = [];
27
21
  }
28
22
 
29
- /** Delegate to shared skill-manifest module */
30
- async loadSkillManifest(dirPath) {
31
- return loadSkillManifestShared(dirPath);
32
- }
33
-
34
- /** Delegate to shared skill-manifest module */
35
- getCanonicalId(manifest, filename) {
36
- return getCanonicalIdShared(manifest, filename);
37
- }
38
-
39
- /** Delegate to shared skill-manifest module */
40
- getArtifactType(manifest, filename) {
41
- return getArtifactTypeShared(manifest, filename);
42
- }
43
-
44
23
  /**
45
24
  * Clean text for CSV output by normalizing whitespace.
46
25
  * Note: Quote escaping is handled by escapeCsv() at write time.
@@ -98,17 +77,21 @@ class ManifestGenerator {
98
77
  // Collect skills first (populates skillClaimedDirs before legacy collectors run)
99
78
  await this.collectSkills();
100
79
 
101
- // Collect agent data - use updatedModules which includes all installed modules
102
- await this.collectAgents(this.updatedModules);
80
+ // Collect agent essence from each module's source module.yaml `agents:` array
81
+ await this.collectAgentsFromModuleYaml();
103
82
 
104
83
  // Write manifest files and collect their paths
84
+ const [teamConfigPath, userConfigPath] = await this.writeCentralConfig(bmadDir, options.moduleConfigs || {});
105
85
  const manifestFiles = [
106
86
  await this.writeMainManifest(cfgDir),
107
87
  await this.writeSkillManifest(cfgDir),
108
- await this.writeAgentManifest(cfgDir),
88
+ teamConfigPath,
89
+ userConfigPath,
109
90
  await this.writeFilesManifest(cfgDir),
110
91
  ];
111
92
 
93
+ await this.ensureCustomConfigStubs(bmadDir);
94
+
112
95
  return {
113
96
  skills: this.skills.length,
114
97
  agents: this.agents.length,
@@ -150,24 +133,13 @@ class ManifestGenerator {
150
133
  const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
151
134
 
152
135
  if (skillMeta) {
153
- // Load manifest when present (for agent metadata)
154
- const manifest = await this.loadSkillManifest(dir);
155
- const artifactType = this.getArtifactType(manifest, skillFile);
156
-
157
136
  // Build path relative from module root (points to SKILL.md — the permanent entrypoint)
158
137
  const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
159
138
  const installPath = relativePath
160
139
  ? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
161
140
  : `${this.bmadFolderName}/${moduleName}/${skillFile}`;
162
141
 
163
- // Native SKILL.md entrypoints derive canonicalId from directory name.
164
- // Agent entrypoints may keep canonicalId metadata for compatibility, so
165
- // only warn for non-agent SKILL.md directories.
166
- if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
167
- console.warn(
168
- `Warning: Native entrypoint manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for SKILL.md directories (directory name is the canonical ID)`,
169
- );
170
- }
142
+ // Native SKILL.md entrypoints always derive canonicalId from directory name.
171
143
  const canonicalId = dirName;
172
144
 
173
145
  this.skills.push({
@@ -193,11 +165,13 @@ class ManifestGenerator {
193
165
  }
194
166
  }
195
167
 
196
- // Recurse into subdirectories
197
- for (const entry of entries) {
198
- if (!entry.isDirectory()) continue;
199
- if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
200
- await walk(path.join(dir, entry.name));
168
+ // Recurse into subdirectories — but not inside a discovered skill
169
+ if (!skillMeta) {
170
+ for (const entry of entries) {
171
+ if (!entry.isDirectory()) continue;
172
+ if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
173
+ await walk(path.join(dir, entry.name));
174
+ }
201
175
  }
202
176
  };
203
177
 
@@ -261,106 +235,60 @@ class ManifestGenerator {
261
235
  }
262
236
 
263
237
  /**
264
- * Collect all agents from selected modules by walking their directory trees.
238
+ * Collect agents from each installed module's source module.yaml `agents:` array.
239
+ * Essence fields (code, name, title, icon, description) are authored in module.yaml;
240
+ * `team` defaults to module code when not set; `module` is always the owning module.
265
241
  */
266
- async collectAgents(selectedModules) {
242
+ async collectAgentsFromModuleYaml() {
267
243
  this.agents = [];
268
244
  const debug = process.env.BMAD_DEBUG_MANIFEST === 'true';
269
245
 
270
- // Walk each module's full directory tree looking for type:agent manifests
271
246
  for (const moduleName of this.updatedModules) {
272
- const modulePath = path.join(this.bmadDir, moduleName);
273
- if (!(await fs.pathExists(modulePath))) continue;
274
-
275
- const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug);
276
- this.agents.push(...moduleAgents);
277
- }
278
-
279
- // Get standalone agents from bmad/agents/ directory
280
- const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
281
- if (await fs.pathExists(standaloneAgentsDir)) {
282
- const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug);
283
- this.agents.push(...standaloneAgents);
284
- }
285
-
286
- if (debug) {
287
- console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`);
288
- }
289
- }
290
-
291
- /**
292
- * Recursively walk a directory tree collecting agents.
293
- * Discovers agents via directory with bmad-skill-manifest.yaml containing type: agent
294
- *
295
- * @param {string} dirPath - Current directory being scanned
296
- * @param {string} moduleName - Module this directory belongs to
297
- * @param {string} relativePath - Path relative to the module root (for install path construction)
298
- * @param {boolean} debug - Emit debug messages
299
- */
300
- async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) {
301
- const agents = [];
302
- let entries;
303
- try {
304
- entries = await fs.readdir(dirPath, { withFileTypes: true });
305
- } catch {
306
- return agents;
307
- }
308
-
309
- for (const entry of entries) {
310
- if (!entry.isDirectory()) continue;
311
- if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
312
-
313
- const fullPath = path.join(dirPath, entry.name);
314
-
315
- // Check for type:agent manifest BEFORE checking skillClaimedDirs —
316
- // agent dirs may be claimed by collectSkills for IDE installation,
317
- // but we still need them in agent-manifest.csv.
318
- const dirManifest = await this.loadSkillManifest(fullPath);
319
- if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') {
320
- const m = dirManifest.__single;
321
- const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
322
- const agentModule = m.module || moduleName;
323
- const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`;
324
-
325
- agents.push({
326
- name: m.name || entry.name,
327
- displayName: m.displayName || m.name || entry.name,
328
- title: m.title || '',
329
- icon: m.icon || '',
330
- capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '',
331
- role: m.role ? this.cleanForCSV(m.role) : '',
332
- identity: m.identity ? this.cleanForCSV(m.identity) : '',
333
- communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
334
- principles: m.principles ? this.cleanForCSV(m.principles) : '',
335
- module: agentModule,
336
- path: installPath,
337
- canonicalId: m.canonicalId || '',
338
- });
339
-
340
- this.files.push({
341
- type: 'agent',
342
- name: m.name || entry.name,
343
- module: agentModule,
344
- path: installPath,
345
- });
247
+ const moduleYamlPath = await resolveInstalledModuleYaml(moduleName);
248
+ if (!moduleYamlPath) {
249
+ // External modules live in ~/.bmad/cache/external-modules, not src/modules.
250
+ // Warn rather than silently skip so missing agent rosters don't vanish
251
+ // from config.toml without notice.
252
+ console.warn(
253
+ `[warn] collectAgentsFromModuleYaml: could not locate module.yaml for '${moduleName}'. ` +
254
+ `Agents declared by this module will not be written to config.toml.`,
255
+ );
256
+ continue;
257
+ }
346
258
 
347
- if (debug) {
348
- console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`);
349
- }
259
+ let moduleDef;
260
+ try {
261
+ moduleDef = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
262
+ } catch (error) {
263
+ if (debug) console.log(`[DEBUG] collectAgentsFromModuleYaml: failed to parse ${moduleYamlPath}: ${error.message}`);
350
264
  continue;
351
265
  }
352
266
 
353
- // Skip directories claimed by collectSkills (non-agent type skills)
354
- // avoids recursing into skill trees that can't contain agents.
355
- if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue;
267
+ if (!moduleDef || !Array.isArray(moduleDef.agents)) continue;
268
+
269
+ for (const entry of moduleDef.agents) {
270
+ if (!entry || typeof entry.code !== 'string') continue;
271
+ this.agents.push({
272
+ code: entry.code,
273
+ name: entry.name || '',
274
+ title: entry.title || '',
275
+ icon: entry.icon || '',
276
+ description: entry.description || '',
277
+ module: moduleName,
278
+ team: entry.team || moduleName,
279
+ });
280
+ }
356
281
 
357
- // Recurse into subdirectories
358
- const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
359
- const subDirAgents = await this.getAgentsFromDirRecursive(fullPath, moduleName, newRelativePath, debug);
360
- agents.push(...subDirAgents);
282
+ if (debug) {
283
+ console.log(
284
+ `[DEBUG] collectAgentsFromModuleYaml: ${moduleName} contributed ${moduleDef.agents.length} agents from ${moduleYamlPath}`,
285
+ );
286
+ }
361
287
  }
362
288
 
363
- return agents;
289
+ if (debug) {
290
+ console.log(`[DEBUG] collectAgentsFromModuleYaml: total agents found: ${this.agents.length}`);
291
+ }
364
292
  }
365
293
 
366
294
  /**
@@ -421,7 +349,22 @@ class ManifestGenerator {
421
349
  npmPackage: versionInfo.npmPackage,
422
350
  repoUrl: versionInfo.repoUrl,
423
351
  };
424
- if (versionInfo.localPath) moduleEntry.localPath = versionInfo.localPath;
352
+ // Preserve channel/sha from the resolution (external/community/custom)
353
+ // or from the existing entry if this is a no-change rewrite.
354
+ const channel = versionInfo.channel ?? existing?.channel;
355
+ const sha = versionInfo.sha ?? existing?.sha;
356
+ if (channel) moduleEntry.channel = channel;
357
+ if (sha) moduleEntry.sha = sha;
358
+ if (versionInfo.localPath || existing?.localPath) {
359
+ moduleEntry.localPath = versionInfo.localPath || existing.localPath;
360
+ }
361
+ if (versionInfo.rawSource || existing?.rawSource) {
362
+ moduleEntry.rawSource = versionInfo.rawSource || existing.rawSource;
363
+ }
364
+ const regTag = versionInfo.registryApprovedTag ?? existing?.registryApprovedTag;
365
+ const regSha = versionInfo.registryApprovedSha ?? existing?.registryApprovedSha;
366
+ if (regTag) moduleEntry.registryApprovedTag = regTag;
367
+ if (regSha) moduleEntry.registryApprovedSha = regSha;
425
368
  updatedModules.push(moduleEntry);
426
369
  }
427
370
 
@@ -476,77 +419,236 @@ class ManifestGenerator {
476
419
  }
477
420
 
478
421
  /**
479
- * Write agent manifest CSV
480
- * @returns {string} Path to the manifest file
422
+ * Write central _bmad/config.toml with [core], [modules.<code>], [agents.<code>] tables.
423
+ * Install-owned. Team-scope answers config.toml; user-scope answers → config.user.toml.
424
+ * Both files are regenerated on every install. User overrides live in
425
+ * _bmad/custom/config.toml and _bmad/custom/config.user.toml (never touched by installer).
426
+ * @returns {string[]} Paths to the written config files
481
427
  */
482
- async writeAgentManifest(cfgDir) {
483
- const csvPath = path.join(cfgDir, 'agent-manifest.csv');
484
- const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
485
-
486
- // Read existing manifest to preserve entries
487
- const existingEntries = new Map();
488
- if (await fs.pathExists(csvPath)) {
489
- const content = await fs.readFile(csvPath, 'utf8');
490
- const records = csv.parse(content, {
491
- columns: true,
492
- skip_empty_lines: true,
493
- });
494
- for (const record of records) {
495
- existingEntries.set(`${record.module}:${record.name}`, record);
428
+ async writeCentralConfig(bmadDir, moduleConfigs) {
429
+ const teamPath = path.join(bmadDir, 'config.toml');
430
+ const userPath = path.join(bmadDir, 'config.user.toml');
431
+
432
+ // Load each module's source module.yaml to determine scope per prompt key.
433
+ // Default scope is 'team' when the prompt doesn't declare one.
434
+ // When a module.yaml is unreadable we warn — for known official modules
435
+ // this means user-scoped keys (e.g. user_name) could mis-file into the
436
+ // team config, so the operator should notice.
437
+ const scopeByModuleKey = {};
438
+ for (const moduleName of this.updatedModules) {
439
+ const moduleYamlPath = await resolveInstalledModuleYaml(moduleName);
440
+ if (!moduleYamlPath) {
441
+ console.warn(
442
+ `[warn] writeCentralConfig: could not locate module.yaml for '${moduleName}'. ` +
443
+ `Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`,
444
+ );
445
+ continue;
446
+ }
447
+ try {
448
+ const parsed = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
449
+ if (!parsed || typeof parsed !== 'object') continue;
450
+ scopeByModuleKey[moduleName] = {};
451
+ for (const [key, value] of Object.entries(parsed)) {
452
+ if (value && typeof value === 'object' && 'prompt' in value) {
453
+ scopeByModuleKey[moduleName][key] = value.scope === 'user' ? 'user' : 'team';
454
+ }
455
+ }
456
+ } catch (error) {
457
+ console.warn(
458
+ `[warn] writeCentralConfig: could not parse module.yaml for '${moduleName}' (${error.message}). ` +
459
+ `Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`,
460
+ );
496
461
  }
497
462
  }
498
463
 
499
- // Create CSV header with persona fields and canonicalId
500
- let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId\n';
464
+ // Core keys are always known (core module.yaml is built-in). These are
465
+ // the only keys allowed in [core]; they must be stripped from every
466
+ // non-core module bucket because legacy _bmad/{mod}/config.yaml files
467
+ // spread core values into each module. Core belongs in [core] only —
468
+ // workflows that need user_name/language/etc. read [core] directly.
469
+ const coreKeys = new Set(Object.keys(scopeByModuleKey.core || {}));
470
+
471
+ // Partition a module's answered config into team vs user buckets.
472
+ // For non-core modules: strip core keys always; when we know the module's
473
+ // own schema, also drop keys it doesn't declare. Unknown-schema modules
474
+ // (external / marketplace) fall through with their remaining answers as
475
+ // team so they don't vanish from the config.
476
+ const partition = (moduleName, cfg, onlyDeclaredKeys = false) => {
477
+ const team = {};
478
+ const user = {};
479
+ const scopes = scopeByModuleKey[moduleName] || {};
480
+ const isCore = moduleName === 'core';
481
+ for (const [key, value] of Object.entries(cfg || {})) {
482
+ if (!isCore && coreKeys.has(key)) continue;
483
+ if (onlyDeclaredKeys && !(key in scopes)) continue;
484
+ if (scopes[key] === 'user') {
485
+ user[key] = value;
486
+ } else {
487
+ team[key] = value;
488
+ }
489
+ }
490
+ return { team, user };
491
+ };
492
+
493
+ const teamHeader = [
494
+ '# ─────────────────────────────────────────────────────────────────',
495
+ '# Installer-managed. Regenerated on every install — treat as read-only.',
496
+ '#',
497
+ '# Direct edits to this file will be overwritten on the next install.',
498
+ '# To change an install answer durably, re-run the installer (your prior',
499
+ '# answers are remembered as defaults). To pin a value regardless of',
500
+ '# install answers, or to add custom agents / override descriptors, use:',
501
+ '# _bmad/custom/config.toml (team, committed)',
502
+ '# _bmad/custom/config.user.toml (personal, gitignored)',
503
+ '# Those files are never touched by the installer.',
504
+ '# ─────────────────────────────────────────────────────────────────',
505
+ '',
506
+ ];
507
+
508
+ const userHeader = [
509
+ '# ─────────────────────────────────────────────────────────────────',
510
+ '# Installer-managed. Regenerated on every install — treat as read-only.',
511
+ '# Holds install answers scoped to YOU personally.',
512
+ '#',
513
+ '# Direct edits to this file will be overwritten on the next install.',
514
+ '# To change an answer durably, re-run the installer (your prior answers',
515
+ '# are remembered as defaults). For pinned overrides or custom sections',
516
+ '# the installer does not know about, use _bmad/custom/config.user.toml',
517
+ '# — it is never touched by the installer.',
518
+ '# ─────────────────────────────────────────────────────────────────',
519
+ '',
520
+ ];
521
+
522
+ const teamLines = [...teamHeader];
523
+ const userLines = [...userHeader];
524
+
525
+ // [core] — split into team and user
526
+ const coreConfig = moduleConfigs.core || {};
527
+ const { team: coreTeam, user: coreUser } = partition('core', coreConfig);
528
+ if (Object.keys(coreTeam).length > 0) {
529
+ teamLines.push('[core]');
530
+ for (const [key, value] of Object.entries(coreTeam)) {
531
+ teamLines.push(`${key} = ${formatTomlValue(value)}`);
532
+ }
533
+ teamLines.push('');
534
+ }
535
+ if (Object.keys(coreUser).length > 0) {
536
+ userLines.push('[core]');
537
+ for (const [key, value] of Object.entries(coreUser)) {
538
+ userLines.push(`${key} = ${formatTomlValue(value)}`);
539
+ }
540
+ userLines.push('');
541
+ }
501
542
 
502
- // Combine existing and new agents, preferring new data for duplicates
503
- const allAgents = new Map();
543
+ // [modules.<code>] split per module
544
+ for (const moduleName of this.updatedModules) {
545
+ if (moduleName === 'core') continue;
546
+ const cfg = moduleConfigs[moduleName];
547
+ if (!cfg || Object.keys(cfg).length === 0) continue;
548
+ // Only filter out spread-from-core pollution when we actually know
549
+ // this module's prompt schema. For external/marketplace modules whose
550
+ // module.yaml isn't in the src tree, fall through as all-team so we
551
+ // don't drop their real answers.
552
+ const haveSchema = Object.keys(scopeByModuleKey[moduleName] || {}).length > 0;
553
+ const { team: modTeam, user: modUser } = partition(moduleName, cfg, haveSchema);
554
+ if (Object.keys(modTeam).length > 0) {
555
+ teamLines.push(`[modules.${moduleName}]`);
556
+ for (const [key, value] of Object.entries(modTeam)) {
557
+ teamLines.push(`${key} = ${formatTomlValue(value)}`);
558
+ }
559
+ teamLines.push('');
560
+ }
561
+ if (Object.keys(modUser).length > 0) {
562
+ userLines.push(`[modules.${moduleName}]`);
563
+ for (const [key, value] of Object.entries(modUser)) {
564
+ userLines.push(`${key} = ${formatTomlValue(value)}`);
565
+ }
566
+ userLines.push('');
567
+ }
568
+ }
504
569
 
505
- // Add existing entries
506
- for (const [key, value] of existingEntries) {
507
- allAgents.set(key, value);
570
+ // [agents.<code>] always team (agent roster is organizational).
571
+ // Freshly collected agents come from module.yaml this run. If a module
572
+ // was preserved (e.g. during quickUpdate when its source isn't available),
573
+ // its module.yaml wasn't read — so its agents aren't in `this.agents` and
574
+ // would silently disappear from the roster. Preserve those existing
575
+ // [agents.*] blocks verbatim from the prior config.toml.
576
+ const freshAgentCodes = new Set(this.agents.map((a) => a.code));
577
+ const contributingModules = new Set(this.agents.map((a) => a.module));
578
+ const preservedModules = this.updatedModules.filter((m) => !contributingModules.has(m));
579
+ const preservedBlocks = [];
580
+ if (preservedModules.length > 0 && (await fs.pathExists(teamPath))) {
581
+ try {
582
+ const prev = await fs.readFile(teamPath, 'utf8');
583
+ for (const block of extractAgentBlocks(prev)) {
584
+ if (freshAgentCodes.has(block.code)) continue;
585
+ if (block.module && preservedModules.includes(block.module)) {
586
+ preservedBlocks.push(block.body);
587
+ }
588
+ }
589
+ } catch (error) {
590
+ console.warn(`[warn] writeCentralConfig: could not read prior config.toml to preserve agents: ${error.message}`);
591
+ }
508
592
  }
509
593
 
510
- // Add/update new agents
511
594
  for (const agent of this.agents) {
512
- const key = `${agent.module}:${agent.name}`;
513
- allAgents.set(key, {
514
- name: agent.name,
515
- displayName: agent.displayName,
516
- title: agent.title,
517
- icon: agent.icon,
518
- capabilities: agent.capabilities,
519
- role: agent.role,
520
- identity: agent.identity,
521
- communicationStyle: agent.communicationStyle,
522
- principles: agent.principles,
523
- module: agent.module,
524
- path: agent.path,
525
- canonicalId: agent.canonicalId || '',
526
- });
595
+ const agentLines = [`[agents.${agent.code}]`, `module = ${formatTomlValue(agent.module)}`, `team = ${formatTomlValue(agent.team)}`];
596
+ if (agent.name) agentLines.push(`name = ${formatTomlValue(agent.name)}`);
597
+ if (agent.title) agentLines.push(`title = ${formatTomlValue(agent.title)}`);
598
+ if (agent.icon) agentLines.push(`icon = ${formatTomlValue(agent.icon)}`);
599
+ if (agent.description) agentLines.push(`description = ${formatTomlValue(agent.description)}`);
600
+ agentLines.push('');
601
+ teamLines.push(...agentLines);
527
602
  }
528
603
 
529
- // Write all agents
530
- for (const [, record] of allAgents) {
531
- const row = [
532
- escapeCsv(record.name),
533
- escapeCsv(record.displayName),
534
- escapeCsv(record.title),
535
- escapeCsv(record.icon),
536
- escapeCsv(record.capabilities),
537
- escapeCsv(record.role),
538
- escapeCsv(record.identity),
539
- escapeCsv(record.communicationStyle),
540
- escapeCsv(record.principles),
541
- escapeCsv(record.module),
542
- escapeCsv(record.path),
543
- escapeCsv(record.canonicalId),
544
- ].join(',');
545
- csvContent += row + '\n';
604
+ for (const body of preservedBlocks) {
605
+ teamLines.push(body, '');
546
606
  }
547
607
 
548
- await fs.writeFile(csvPath, csvContent);
549
- return csvPath;
608
+ const teamContent = teamLines.join('\n').replace(/\n+$/, '\n');
609
+ const userContent = userLines.join('\n').replace(/\n+$/, '\n');
610
+ await fs.writeFile(teamPath, teamContent);
611
+ await fs.writeFile(userPath, userContent);
612
+ return [teamPath, userPath];
613
+ }
614
+
615
+ /**
616
+ * Create empty _bmad/custom/config.toml and _bmad/custom/config.user.toml stubs
617
+ * on first install only. Installer never touches these files again after creation.
618
+ */
619
+ async ensureCustomConfigStubs(bmadDir) {
620
+ const customDir = path.join(bmadDir, 'custom');
621
+ await fs.ensureDir(customDir);
622
+
623
+ const stubs = [
624
+ {
625
+ file: path.join(customDir, 'config.toml'),
626
+ header: [
627
+ '# Team / enterprise overrides for _bmad/config.toml.',
628
+ '# Committed to the repo — applies to every developer on the project.',
629
+ '# Tables deep-merge over base config; keyed entries merge by key.',
630
+ '# Example: override an agent descriptor, or add a new agent.',
631
+ '#',
632
+ '# [agents.bmad-agent-pm]',
633
+ '# description = "Prefers short, bulleted PRDs over narrative drafts."',
634
+ '',
635
+ ],
636
+ },
637
+ {
638
+ file: path.join(customDir, 'config.user.toml'),
639
+ header: [
640
+ '# Personal overrides for _bmad/config.toml.',
641
+ '# NOT committed (gitignored) — applies only to your local install.',
642
+ '# Wins over both base config and team overrides.',
643
+ '',
644
+ ],
645
+ },
646
+ ];
647
+
648
+ for (const { file, header } of stubs) {
649
+ if (await fs.pathExists(file)) continue;
650
+ await fs.writeFile(file, header.join('\n'));
651
+ }
550
652
  }
551
653
 
552
654
  /**
@@ -692,4 +794,59 @@ class ManifestGenerator {
692
794
  }
693
795
  }
694
796
 
797
+ /**
798
+ * Format a JS scalar as a TOML value literal.
799
+ * Handles strings (quoted + escaped), booleans, numbers, and arrays of scalars.
800
+ * Objects are not expected at this emit path.
801
+ */
802
+ function formatTomlValue(value) {
803
+ if (value === null || value === undefined) return '""';
804
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
805
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
806
+ if (Array.isArray(value)) return `[${value.map((v) => formatTomlValue(v)).join(', ')}]`;
807
+ const str = String(value);
808
+ const escaped = str
809
+ .replaceAll('\\', '\\\\')
810
+ .replaceAll('"', String.raw`\"`)
811
+ .replaceAll('\n', String.raw`\n`)
812
+ .replaceAll('\r', String.raw`\r`)
813
+ .replaceAll('\t', String.raw`\t`);
814
+ return `"${escaped}"`;
815
+ }
816
+
817
+ /**
818
+ * Extract [agents.<code>] blocks from a previously-emitted config.toml.
819
+ * We only need this for roster preservation — the file is our own controlled
820
+ * output, so a simple line scanner is safer than adding a TOML parser
821
+ * dependency. Each block runs from its `[agents.<code>]` header until the
822
+ * next `[` heading or EOF; the `module = "..."` line inside drives which
823
+ * entries we keep on the next write.
824
+ * @returns {Array<{code: string, module: string | null, body: string}>}
825
+ */
826
+ function extractAgentBlocks(tomlContent) {
827
+ const blocks = [];
828
+ const lines = tomlContent.split('\n');
829
+ let i = 0;
830
+ while (i < lines.length) {
831
+ const header = lines[i].match(/^\[agents\.([^\]]+)]\s*$/);
832
+ if (!header) {
833
+ i++;
834
+ continue;
835
+ }
836
+ const code = header[1];
837
+ const blockLines = [lines[i]];
838
+ let moduleName = null;
839
+ i++;
840
+ while (i < lines.length && !lines[i].startsWith('[')) {
841
+ blockLines.push(lines[i]);
842
+ const m = lines[i].match(/^module\s*=\s*"((?:[^"\\]|\\.)*)"\s*$/);
843
+ if (m) moduleName = m[1];
844
+ i++;
845
+ }
846
+ while (blockLines.length > 1 && blockLines.at(-1) === '') blockLines.pop();
847
+ blocks.push({ code, module: moduleName, body: blockLines.join('\n') });
848
+ }
849
+ return blocks;
850
+ }
851
+
695
852
  module.exports = { ManifestGenerator };