bmad-method 6.3.1-next.9 → 6.4.0

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 (140) hide show
  1. package/package.json +3 -2
  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-12-complete.md +6 -0
  35. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/SKILL.md +70 -1
  36. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +41 -0
  37. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
  38. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/SKILL.md +97 -1
  39. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +42 -0
  40. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
  41. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/SKILL.md +99 -1
  42. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +42 -0
  43. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
  44. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
  45. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
  46. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/SKILL.md +86 -1
  47. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +41 -0
  48. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
  49. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +69 -1
  50. package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +41 -0
  51. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
  52. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/SKILL.md +88 -1
  53. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +41 -0
  54. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
  55. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/SKILL.md +76 -1
  56. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +41 -0
  57. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
  58. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
  59. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
  60. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +46 -7
  61. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/customize.toml +41 -0
  62. package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +6 -0
  63. package/src/bmm-skills/4-implementation/bmad-code-review/SKILL.md +85 -1
  64. package/src/bmm-skills/4-implementation/bmad-code-review/customize.toml +41 -0
  65. package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-04-present.md +6 -0
  66. package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +296 -1
  67. package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +41 -0
  68. package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +424 -1
  69. package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +41 -0
  70. package/src/bmm-skills/4-implementation/bmad-dev-story/SKILL.md +480 -1
  71. package/src/bmm-skills/4-implementation/bmad-dev-story/customize.toml +41 -0
  72. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +171 -1
  73. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +41 -0
  74. package/src/bmm-skills/4-implementation/bmad-quick-dev/SKILL.md +106 -1
  75. package/src/bmm-skills/4-implementation/bmad-quick-dev/customize.toml +41 -0
  76. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-05-present.md +6 -0
  77. package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +6 -0
  78. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1507 -1
  79. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +41 -0
  80. package/src/bmm-skills/4-implementation/bmad-sprint-planning/SKILL.md +294 -1
  81. package/src/bmm-skills/4-implementation/bmad-sprint-planning/customize.toml +41 -0
  82. package/src/bmm-skills/4-implementation/bmad-sprint-status/SKILL.md +292 -1
  83. package/src/bmm-skills/4-implementation/bmad-sprint-status/customize.toml +41 -0
  84. package/src/bmm-skills/module.yaml +49 -0
  85. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
  86. package/src/core-skills/bmad-customize/SKILL.md +111 -0
  87. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
  88. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
  89. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  90. package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
  91. package/src/core-skills/module-help.csv +1 -0
  92. package/src/core-skills/module.yaml +2 -0
  93. package/src/scripts/resolve_config.py +176 -0
  94. package/src/scripts/resolve_customization.py +230 -0
  95. package/tools/installer/commands/install.js +13 -0
  96. package/tools/installer/core/config.js +4 -1
  97. package/tools/installer/core/install-paths.js +11 -5
  98. package/tools/installer/core/installer.js +181 -94
  99. package/tools/installer/core/manifest-generator.js +339 -184
  100. package/tools/installer/core/manifest.js +86 -86
  101. package/tools/installer/ide/platform-codes.yaml +6 -0
  102. package/tools/installer/modules/channel-plan.js +203 -0
  103. package/tools/installer/modules/channel-resolver.js +241 -0
  104. package/tools/installer/modules/community-manager.js +130 -23
  105. package/tools/installer/modules/custom-module-manager.js +160 -19
  106. package/tools/installer/modules/external-manager.js +235 -32
  107. package/tools/installer/modules/official-modules.js +58 -12
  108. package/tools/installer/modules/registry-client.js +139 -7
  109. package/tools/installer/modules/registry-fallback.yaml +8 -0
  110. package/tools/installer/modules/version-resolver.js +336 -0
  111. package/tools/installer/project-root.js +54 -0
  112. package/tools/installer/ui.js +561 -50
  113. package/tools/platform-codes.yaml +6 -0
  114. package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
  115. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
  116. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
  117. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
  118. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
  119. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
  120. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
  121. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
  122. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
  123. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
  124. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
  125. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
  126. package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
  127. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
  128. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
  129. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
  130. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
  131. package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
  132. package/src/bmm-skills/4-implementation/bmad-code-review/workflow.md +0 -55
  133. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
  134. package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
  135. package/src/bmm-skills/4-implementation/bmad-dev-story/workflow.md +0 -450
  136. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
  137. package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +0 -76
  138. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
  139. package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +0 -263
  140. package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +0 -261
@@ -2,14 +2,8 @@ const path = require('node:path');
2
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({
@@ -263,106 +235,60 @@ class ManifestGenerator {
263
235
  }
264
236
 
265
237
  /**
266
- * 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.
267
241
  */
268
- async collectAgents(selectedModules) {
242
+ async collectAgentsFromModuleYaml() {
269
243
  this.agents = [];
270
244
  const debug = process.env.BMAD_DEBUG_MANIFEST === 'true';
271
245
 
272
- // Walk each module's full directory tree looking for type:agent manifests
273
246
  for (const moduleName of this.updatedModules) {
274
- const modulePath = path.join(this.bmadDir, moduleName);
275
- if (!(await fs.pathExists(modulePath))) continue;
276
-
277
- const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug);
278
- this.agents.push(...moduleAgents);
279
- }
280
-
281
- // Get standalone agents from bmad/agents/ directory
282
- const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
283
- if (await fs.pathExists(standaloneAgentsDir)) {
284
- const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug);
285
- this.agents.push(...standaloneAgents);
286
- }
287
-
288
- if (debug) {
289
- console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`);
290
- }
291
- }
292
-
293
- /**
294
- * Recursively walk a directory tree collecting agents.
295
- * Discovers agents via directory with bmad-skill-manifest.yaml containing type: agent
296
- *
297
- * @param {string} dirPath - Current directory being scanned
298
- * @param {string} moduleName - Module this directory belongs to
299
- * @param {string} relativePath - Path relative to the module root (for install path construction)
300
- * @param {boolean} debug - Emit debug messages
301
- */
302
- async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) {
303
- const agents = [];
304
- let entries;
305
- try {
306
- entries = await fs.readdir(dirPath, { withFileTypes: true });
307
- } catch {
308
- return agents;
309
- }
310
-
311
- for (const entry of entries) {
312
- if (!entry.isDirectory()) continue;
313
- if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
314
-
315
- const fullPath = path.join(dirPath, entry.name);
316
-
317
- // Check for type:agent manifest BEFORE checking skillClaimedDirs —
318
- // agent dirs may be claimed by collectSkills for IDE installation,
319
- // but we still need them in agent-manifest.csv.
320
- const dirManifest = await this.loadSkillManifest(fullPath);
321
- if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') {
322
- const m = dirManifest.__single;
323
- const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
324
- const agentModule = m.module || moduleName;
325
- const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`;
326
-
327
- agents.push({
328
- name: m.name || entry.name,
329
- displayName: m.displayName || m.name || entry.name,
330
- title: m.title || '',
331
- icon: m.icon || '',
332
- capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '',
333
- role: m.role ? this.cleanForCSV(m.role) : '',
334
- identity: m.identity ? this.cleanForCSV(m.identity) : '',
335
- communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
336
- principles: m.principles ? this.cleanForCSV(m.principles) : '',
337
- module: agentModule,
338
- path: installPath,
339
- canonicalId: m.canonicalId || '',
340
- });
341
-
342
- this.files.push({
343
- type: 'agent',
344
- name: m.name || entry.name,
345
- module: agentModule,
346
- path: installPath,
347
- });
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
+ }
348
258
 
349
- if (debug) {
350
- console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`);
351
- }
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}`);
352
264
  continue;
353
265
  }
354
266
 
355
- // Skip directories claimed by collectSkills (non-agent type skills)
356
- // avoids recursing into skill trees that can't contain agents.
357
- 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
+ }
358
281
 
359
- // Recurse into subdirectories
360
- const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
361
- const subDirAgents = await this.getAgentsFromDirRecursive(fullPath, moduleName, newRelativePath, debug);
362
- agents.push(...subDirAgents);
282
+ if (debug) {
283
+ console.log(
284
+ `[DEBUG] collectAgentsFromModuleYaml: ${moduleName} contributed ${moduleDef.agents.length} agents from ${moduleYamlPath}`,
285
+ );
286
+ }
363
287
  }
364
288
 
365
- return agents;
289
+ if (debug) {
290
+ console.log(`[DEBUG] collectAgentsFromModuleYaml: total agents found: ${this.agents.length}`);
291
+ }
366
292
  }
367
293
 
368
294
  /**
@@ -423,7 +349,22 @@ class ManifestGenerator {
423
349
  npmPackage: versionInfo.npmPackage,
424
350
  repoUrl: versionInfo.repoUrl,
425
351
  };
426
- 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;
427
368
  updatedModules.push(moduleEntry);
428
369
  }
429
370
 
@@ -478,77 +419,236 @@ class ManifestGenerator {
478
419
  }
479
420
 
480
421
  /**
481
- * Write agent manifest CSV
482
- * @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
483
427
  */
484
- async writeAgentManifest(cfgDir) {
485
- const csvPath = path.join(cfgDir, 'agent-manifest.csv');
486
- const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
487
-
488
- // Read existing manifest to preserve entries
489
- const existingEntries = new Map();
490
- if (await fs.pathExists(csvPath)) {
491
- const content = await fs.readFile(csvPath, 'utf8');
492
- const records = csv.parse(content, {
493
- columns: true,
494
- skip_empty_lines: true,
495
- });
496
- for (const record of records) {
497
- 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
+ );
498
461
  }
499
462
  }
500
463
 
501
- // Create CSV header with persona fields and canonicalId
502
- 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
+ }
503
542
 
504
- // Combine existing and new agents, preferring new data for duplicates
505
- 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
+ }
506
569
 
507
- // Add existing entries
508
- for (const [key, value] of existingEntries) {
509
- 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
+ }
510
592
  }
511
593
 
512
- // Add/update new agents
513
594
  for (const agent of this.agents) {
514
- const key = `${agent.module}:${agent.name}`;
515
- allAgents.set(key, {
516
- name: agent.name,
517
- displayName: agent.displayName,
518
- title: agent.title,
519
- icon: agent.icon,
520
- capabilities: agent.capabilities,
521
- role: agent.role,
522
- identity: agent.identity,
523
- communicationStyle: agent.communicationStyle,
524
- principles: agent.principles,
525
- module: agent.module,
526
- path: agent.path,
527
- canonicalId: agent.canonicalId || '',
528
- });
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);
529
602
  }
530
603
 
531
- // Write all agents
532
- for (const [, record] of allAgents) {
533
- const row = [
534
- escapeCsv(record.name),
535
- escapeCsv(record.displayName),
536
- escapeCsv(record.title),
537
- escapeCsv(record.icon),
538
- escapeCsv(record.capabilities),
539
- escapeCsv(record.role),
540
- escapeCsv(record.identity),
541
- escapeCsv(record.communicationStyle),
542
- escapeCsv(record.principles),
543
- escapeCsv(record.module),
544
- escapeCsv(record.path),
545
- escapeCsv(record.canonicalId),
546
- ].join(',');
547
- csvContent += row + '\n';
604
+ for (const body of preservedBlocks) {
605
+ teamLines.push(body, '');
548
606
  }
549
607
 
550
- await fs.writeFile(csvPath, csvContent);
551
- 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
+ }
552
652
  }
553
653
 
554
654
  /**
@@ -694,4 +794,59 @@ class ManifestGenerator {
694
794
  }
695
795
  }
696
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
+
697
852
  module.exports = { ManifestGenerator };