bmad-plus 0.12.2 → 0.14.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 (127) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +96 -461
  3. package/package.json +12 -5
  4. package/readme-international/README.de.md +42 -9
  5. package/readme-international/README.es.md +43 -10
  6. package/readme-international/README.fr.md +42 -9
  7. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
  8. package/src/bmad-plus/module.yaml +270 -220
  9. package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
  10. package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
  11. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
  12. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
  13. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
  14. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
  15. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
  16. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
  19. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
  20. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
  21. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
  22. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
  23. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
  24. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
  70. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
  71. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
  72. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
  73. package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
  74. package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
  75. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
  76. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
  77. package/src/bmad-plus/packs/pack-memory/README.md +6 -0
  78. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
  79. package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
  80. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
  81. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
  82. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
  83. package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
  84. package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
  85. package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
  86. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
  87. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
  88. package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
  89. package/tools/bmad-plus-npx.js +4 -2
  90. package/tools/build/adapters.config.js +60 -51
  91. package/tools/build/check-counts.js +52 -54
  92. package/tools/build/check-install-contract.js +450 -0
  93. package/tools/build/generate-adapters.js +252 -56
  94. package/tools/build/generate.js +364 -14
  95. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  96. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  97. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  98. package/tools/build/generated-adapters/AGENTS.md +20 -7
  99. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  100. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  101. package/tools/build/generated-adapters/GEMINI.md +20 -7
  102. package/tools/build/module.template.yaml +82 -0
  103. package/tools/build/pack-delivery.js +78 -0
  104. package/tools/cli/bmad-plus-cli.js +16 -1
  105. package/tools/cli/commands/doctor.js +49 -217
  106. package/tools/cli/commands/install.js +109 -164
  107. package/tools/cli/commands/studio.js +68 -0
  108. package/tools/cli/commands/uninstall.js +173 -65
  109. package/tools/cli/commands/update-check.js +31 -0
  110. package/tools/cli/commands/update-policy.js +39 -0
  111. package/tools/cli/commands/update.js +102 -113
  112. package/tools/cli/i18n.js +60 -0
  113. package/tools/cli/lib/ide-config.js +4 -261
  114. package/tools/cli/lib/install-manifest.js +17 -0
  115. package/tools/cli/lib/installation-health.js +360 -0
  116. package/tools/cli/lib/installed-adapters.js +89 -0
  117. package/tools/cli/lib/npm-runner.js +177 -0
  118. package/tools/cli/lib/pack-copy.js +62 -66
  119. package/tools/cli/lib/packs.js +546 -3
  120. package/tools/cli/lib/python-health.js +233 -0
  121. package/tools/cli/lib/python-provision.js +2 -2
  122. package/tools/cli/lib/studio.js +310 -0
  123. package/tools/cli/lib/update-check.js +153 -0
  124. package/tools/cli/lib/update-dispatch.js +182 -0
  125. package/tools/cli/lib/update-policy.js +90 -0
  126. package/tools/cli/lib/update-transaction.js +334 -0
  127. package/tools/release/publication-content.js +901 -0
@@ -42,7 +42,7 @@
42
42
  const fs = require('node:fs');
43
43
  const path = require('node:path');
44
44
  const { execFileSync } = require('node:child_process');
45
- const { loadRegistry, DEFAULT_REGISTRY_PATH } = require('./generate');
45
+ const { loadRegistry, buildDerived, DEFAULT_REGISTRY_PATH } = require('./generate');
46
46
 
47
47
  const REPO_ROOT = path.join(__dirname, '..', '..');
48
48
 
@@ -84,12 +84,6 @@ function isFrozenHistoryRow(line) {
84
84
 
85
85
  /* ── Derivation ─────────────────────────────────────────────────────────── */
86
86
 
87
- /** Sum of list lengths for a given key across a pack's categories. */
88
- function sumCategories(pack, key) {
89
- if (!Array.isArray(pack.categories)) return 0;
90
- return pack.categories.reduce((n, c) => n + (Array.isArray(c[key]) ? c[key].length : 0), 0);
91
- }
92
-
93
87
  /** Count *.md agent files on disk for a packaged pack's categories/ tree. */
94
88
  function countAgentFilesOnDisk(packDir) {
95
89
  const root = path.join(REPO_ROOT, 'src', 'bmad-plus', 'packs', packDir, 'categories');
@@ -111,36 +105,28 @@ function countWorkflowFilesOnDisk(packDir) {
111
105
  return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
112
106
  }
113
107
 
114
- /** Installer languages, read from the CLI's own i18n module (its SSOT). */
115
- function deriveLanguages() {
116
- const { LANGUAGES } = require(path.join(REPO_ROOT, 'tools', 'cli', 'i18n.js'));
117
- return Object.keys(LANGUAGES);
118
- }
119
-
120
108
  /**
121
109
  * Derive every count this gate enforces, from registry.yaml + the filesystem.
122
110
  * Cross-checks the registry against the files on disk: if they disagree, the
123
111
  * derivation itself is untrustworthy and that is reported as a drift.
124
112
  */
125
- function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
126
- const registry = loadRegistry(registryPath);
127
- const packs = registry.packs;
113
+ function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH, options = {}) {
114
+ const registry = typeof registryPath === 'string' ? loadRegistry(registryPath) : registryPath;
115
+ const derived = buildDerived(registry, options);
116
+ const packs = derived.packs;
128
117
  const shield = packs.shield;
129
118
  const devStudio = packs['dev-studio'];
130
-
131
- const shieldAgents = sumCategories(shield, 'agents');
132
- const shieldWorkflows = (shield.workflows || []).length;
133
- const shieldCategories = (shield.categories || []).length;
134
- const shieldFrameworks = (shield.compliance_tags || []).length;
135
-
136
- const installerAgents = Object.values(packs).reduce((n, p) => n + p.agents.length, 0);
137
- const devStudioSubAgents = (devStudio.sub_agents || []).length;
138
- const devStudioWorkflows =
139
- (devStudio.workflows || []).length + sumCategories(devStudio, 'workflows');
119
+ const { installerAgents, totalAgents } = derived;
120
+ const shieldAgents = shield.categoryAgentCount;
121
+ const shieldWorkflows = shield.workflowCount;
122
+ const shieldCategories = shield.categoryCount;
123
+ const shieldFrameworks = shield.frameworkCount;
124
+ const devStudioSubAgents = devStudio.subAgentCount;
125
+ const devStudioWorkflows = devStudio.workflowCount;
140
126
 
141
127
  const mismatches = [];
142
- const onDiskAgents = countAgentFilesOnDisk(shield.pack_dir);
143
- const onDiskWorkflows = countWorkflowFilesOnDisk(shield.pack_dir);
128
+ const onDiskAgents = countAgentFilesOnDisk(registry.packs.shield.pack_dir);
129
+ const onDiskWorkflows = countWorkflowFilesOnDisk(registry.packs.shield.pack_dir);
144
130
  if (onDiskAgents !== null && onDiskAgents !== shieldAgents) {
145
131
  mismatches.push(
146
132
  `registry.yaml declares ${shieldAgents} shield category agents but ${onDiskAgents} agent files exist on disk`
@@ -152,26 +138,24 @@ function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
152
138
  );
153
139
  }
154
140
 
155
- // Every distinct agent a user can talk to: the installer entry points plus
156
- // the specialists the two packaged orchestrators route to.
157
- const totalAgents = installerAgents + shieldAgents + devStudioSubAgents;
158
141
  // Sizes of the declared per-category agent groups ("5 agents", "6 agents"…),
159
142
  // which are legitimate claims wherever a category is described.
160
- const perCategorySizes = new Set((shield.categories || []).map((c) => (c.agents || []).length));
143
+ const perCategorySizes = new Set(shield.categoryAgentCounts);
161
144
  // Sizes of the per-pack installer rosters ("3 agents (Scout, Chief, Judge)").
162
- const perPackInstallerSizes = new Set(Object.values(packs).map((p) => p.agents.length));
145
+ const perPackInstallerSizes = new Set(Object.values(packs).map((p) => p.installerAgentCount));
163
146
 
164
147
  return {
165
- packCount: Object.keys(packs).length,
148
+ packCount: derived.packCount,
166
149
  installerAgents,
167
150
  shieldAgents,
168
151
  shieldWorkflows,
169
152
  shieldCategories,
170
153
  shieldFrameworks,
154
+ shieldReferenceFiles: shield.referenceFiles,
171
155
  devStudioSubAgents,
172
156
  devStudioWorkflows,
173
157
  totalAgents,
174
- languages: deriveLanguages(),
158
+ languages: derived.languages,
175
159
  perCategorySizes,
176
160
  // The COMPLETE vocabulary of numbers that may legitimately precede "agents"
177
161
  // anywhere in the product's own copy. Default-deny: a value outside this set
@@ -188,7 +172,7 @@ function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
188
172
  ]),
189
173
  // Same idea for the workflow dimension. Anchored lines get graded against
190
174
  // the one pack they name; unanchored ones must still be *some* real total.
191
- workflowCounts: new Set([shieldWorkflows, devStudioWorkflows]),
175
+ workflowCounts: new Set(Object.values(packs).map((p) => p.workflowCount).filter(Boolean)),
192
176
  derivationMismatches: mismatches,
193
177
  };
194
178
  }
@@ -228,21 +212,18 @@ const FOREIGN_TABLE_HEADER_RE = /^\s*\|\s*BMAD-METHOD\s*\|/i;
228
212
  * `\s` already covers U+00A0/U+202F, which the French copy uses before
229
213
  * punctuation.
230
214
  *
231
- * `-` and `_` are deliberately NOT separators. This repo is full of agent
232
- * IDENTIFIERS `iso27001-agent`, `nist-800-53-agent`, `section508-agent`,
233
- * `iso42001-agent` and a hyphen-tolerant gap would read every one of them as
234
- * a claim of "27001 agents". A qualifier may still *contain* a hyphen
235
- * ("multi-role", "GRC-Compliance-"); it just may not START with one.
215
+ * Hyphens allow compounds such as "12-language". The leading token guard in
216
+ * claimRe prevents digits inside identifiers such as `nist-800-53-agent` from
217
+ * becoming counts; the separator itself need not suppress legitimate claims.
236
218
  */
237
- const GAP = '[\\s*`]*';
219
+ const GAP = '[\\s*`-]*';
238
220
 
239
221
  /** One qualifier word: hyphenated and accented forms included. */
240
222
  const WORD = '[A-Za-zÀ-ÖØ-öø-ÿ][A-Za-zÀ-ÖØ-öø-ÿ-]*';
241
223
 
242
224
  /**
243
- * How many qualifiers a claim may carry. The bound is load-bearing: it is what
244
- * stops a match from walking across a whole sentence and inventing a claim
245
- * ("5 reasons why you should hire agents").
225
+ * Maximum qualifiers echoed in diagnostics. Longer adjective runs are still
226
+ * graded; clause/dimension boundaries below prevent cross-sentence claims.
246
227
  */
247
228
  const MAX_QUALIFIERS = 4;
248
229
 
@@ -263,6 +244,10 @@ const NOT_A_QUALIFIER = new Set(
263
244
  and or plus with across of in on for to the a an
264
245
  et ou des les la le du dans sur pour avec y con de en und oder mit für`.split(/\s+/)
265
246
  );
247
+ const DIMENSION_WORDS = new Set(
248
+ 'agent agents agente agentes agenten workflow workflows pack packs test tests category categories catégorie catégories categoría categorías kategorie kategorien framework frameworks language languages langue langues idioma idiomas sprache sprachen skill skills phase phases level levels reference files'.split(' ')
249
+ );
250
+ const CLAUSE_WORDS = new Set('why you we they should would could must can will'.split(' '));
266
251
 
267
252
  /**
268
253
  * Quantity approximators sitting in front of the number. "over 47 agents" is
@@ -279,14 +264,13 @@ const APPROX_PREFIX_RE = new RegExp(
279
264
  /**
280
265
  * Build a /g/ claim regex for one dimension's noun alternatives.
281
266
  *
282
- * The leading `\b` is load-bearing: it forbids the number from starting inside
283
- * a word. Without it a qualifier run happily bridges the stray digit in "E2E
284
- * tests" into a claim of "2 tests", and reads `section508-agent` as a count.
285
- * A claim's number is always a whole token.
267
+ * The leading guard forbids a number inside a word, version or identifier.
268
+ * Qualifier tokens require separators, so an unbounded adjective run does not
269
+ * create ambiguous character-by-character partitions when no noun follows.
286
270
  */
287
271
  function claimRe(noun) {
288
272
  return new RegExp(
289
- `\\b(\\d+)${GAP}(\\+)?${GAP}((?:${WORD}${GAP}){0,${MAX_QUALIFIERS}}?)(?:${noun})\\b`,
273
+ `(?<![\\w.-])(\\d+)${GAP}(\\+)?${GAP}((?:${WORD}[\\s*\u0060-]+)*?)(${noun})\\b`,
290
274
  'gi'
291
275
  );
292
276
  }
@@ -298,12 +282,13 @@ const PACKS_RE = claimRe('packs?');
298
282
  const TESTS_RE = claimRe('tests?');
299
283
  const FRAMEWORKS_RE = claimRe('frameworks?');
300
284
  const LANGUAGES_RE = claimRe('languages?|langues?|idiomas?|Sprachen?');
285
+ const REFERENCE_FILES_RE = claimRe('reference files?');
301
286
 
302
287
  /**
303
288
  * Collect every claim `re` finds on `line`.
304
289
  * `re` must be a /g/ regex; it is reset before use so it is safe to reuse.
305
- * Matches whose qualifier run crosses a NOT_A_QUALIFIER word are dropped: the
306
- * number belongs to a different dimension (or a different sentence).
290
+ * Dimension/clause crossings are discarded. Connectives truncate the echoed
291
+ * qualifiers without discarding a claim such as "38 compliance and GRC agents".
307
292
  * @returns {{value:number, plus:boolean, approx:boolean, text:string}[]}
308
293
  */
309
294
  function matchAll(re, line) {
@@ -312,14 +297,16 @@ function matchAll(re, line) {
312
297
  let m;
313
298
  while ((m = re.exec(line)) !== null) {
314
299
  const qualifiers = (m[3] || '').split(/[^A-Za-zÀ-ÖØ-öø-ÿ]+/).filter(Boolean);
315
- if (qualifiers.some((w) => NOT_A_QUALIFIER.has(w.toLowerCase()))) continue;
300
+ if (qualifiers.some((w) => DIMENSION_WORDS.has(w.toLowerCase()) || CLAUSE_WORDS.has(w.toLowerCase()))) continue;
301
+ const connective = qualifiers.findIndex((w) => NOT_A_QUALIFIER.has(w.toLowerCase()));
302
+ const bounded = qualifiers.slice(0, Math.min(connective < 0 ? qualifiers.length : connective, MAX_QUALIFIERS));
316
303
  out.push({
317
304
  value: Number(m[1]),
318
305
  plus: m[2] === '+',
319
306
  approx: APPROX_PREFIX_RE.test(line.slice(0, m.index)),
320
307
  // Markers are dropped from the echoed text so a bolded claim reports as
321
308
  // `38 compliance agents`, not `38** compliance agents`. file:line locates it.
322
- text: m[0].replace(/[*`]+/g, ' ').replace(/\s+/g, ' ').trim(),
309
+ text: [m[1] + (m[2] || ''), ...bounded, m[4]].join(' '),
323
310
  });
324
311
  }
325
312
  return out;
@@ -334,6 +321,11 @@ function matchAll(re, line) {
334
321
  * @returns {string[]} human-readable drift descriptions (empty when the line is fine)
335
322
  */
336
323
  function checkLine(line, truth, { foreignSubject = false } = {}) {
324
+ // In a comparison table only the first cell describes the upstream product.
325
+ if (foreignSubject && line.trimStart().startsWith('|')) {
326
+ const ownCells = line.trim().split('|').slice(2, -1).join(' | ');
327
+ return checkLine(ownCells, truth);
328
+ }
337
329
  const problems = [];
338
330
  const shieldLine = SHIELD_ANCHOR.test(line);
339
331
  const devStudioLine = DEV_STUDIO_ANCHOR.test(line);
@@ -466,6 +458,12 @@ function checkLine(line, truth, { foreignSubject = false } = {}) {
466
458
  }
467
459
  }
468
460
 
461
+ for (const { value, plus, approx, text } of matchAll(REFERENCE_FILES_RE, line)) {
462
+ if (value !== truth.shieldReferenceFiles || plus || approx) {
463
+ problems.push(`"${text}" — Shield has ${truth.shieldReferenceFiles} reference files on disk`);
464
+ }
465
+ }
466
+
469
467
  return problems;
470
468
  }
471
469
 
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify that real source and npm-package installs deliver the adapter contract.
4
+ * Uses existing dependencies, temporary projects and an isolated brain; no
5
+ * lifecycle scripts, dependency installation or Python provisioning are run.
6
+ *
7
+ * Run: node tools/build/check-install-contract.js
8
+ * Author: Laurent Rochetta
9
+ */
10
+ 'use strict';
11
+
12
+ const assert = require('node:assert/strict');
13
+ const fs = require('node:fs');
14
+ const os = require('node:os');
15
+ const path = require('node:path');
16
+ const crypto = require('node:crypto');
17
+ const { spawnSync } = require('node:child_process');
18
+
19
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
20
+ const TEMP_PREFIX = 'bmad-install-contract-';
21
+
22
+ function resolveWithin(root, relative) {
23
+ assert(typeof relative === 'string' && relative.length > 0, 'Expected a relative file path');
24
+ assert(
25
+ !path.win32.isAbsolute(relative) && !path.posix.isAbsolute(relative),
26
+ `Absolute path: ${relative}`
27
+ );
28
+ const target = path.resolve(root, relative);
29
+ const fromRoot = path.relative(root, target);
30
+ assert(
31
+ fromRoot &&
32
+ !path.isAbsolute(fromRoot) &&
33
+ !fromRoot.startsWith(`..${path.sep}`) &&
34
+ fromRoot !== '..',
35
+ `Path escapes project: ${relative}`
36
+ );
37
+ return target;
38
+ }
39
+
40
+ function sameSelection(actual, expected, label) {
41
+ assert(Array.isArray(actual), `Manifest ${label} must be an array`);
42
+ assert.deepEqual(
43
+ [...actual].sort(),
44
+ [...expected].sort(),
45
+ `Manifest ${label} differs from requested selection`
46
+ );
47
+ }
48
+
49
+ /** Compare the installed bytes, normalizing only Windows/Unix line endings. */
50
+ function verifyInstallation(
51
+ projectDir,
52
+ { packs, tools, derived, ideConfigs, renderFiles, requireInventory = false }
53
+ ) {
54
+ const manifest = JSON.parse(
55
+ fs.readFileSync(path.join(projectDir, '_bmad', '.bmad-plus-install.json'), 'utf8')
56
+ );
57
+ sameSelection(manifest.packs, packs, 'packs');
58
+ sameSelection(manifest.ides, tools, 'ides');
59
+ assert(typeof manifest.user === 'string' && manifest.user, 'Manifest user is missing');
60
+ assert(
61
+ typeof manifest.language === 'string' && manifest.language,
62
+ 'Manifest language is missing'
63
+ );
64
+
65
+ const files = renderFiles(derived, {
66
+ packs,
67
+ userName: manifest.user,
68
+ language: manifest.language,
69
+ tools: manifest.ides,
70
+ });
71
+ const expectedPaths = new Set(files.map(({ file }) => file));
72
+ assert.equal(expectedPaths.size, files.length, 'Renderer returned duplicate output paths');
73
+ assert.equal(
74
+ derived.targets.spine,
75
+ 'AGENTS.md',
76
+ 'Install contract requires the root AGENTS.md spine'
77
+ );
78
+ assert(
79
+ expectedPaths.has(derived.targets.spine),
80
+ 'Renderer must include the root AGENTS.md spine'
81
+ );
82
+ for (const tool of tools) {
83
+ assert(expectedPaths.has(ideConfigs[tool].configFile), `Renderer omitted the ${tool} adapter`);
84
+ }
85
+
86
+ for (const { file, content } of files) {
87
+ const target = resolveWithin(projectDir, file);
88
+ assert(fs.existsSync(target), `Missing installed file: ${file}`);
89
+ const actual = fs.readFileSync(target, 'utf8').replace(/\r\n/g, '\n');
90
+ const expected = content.replace(/\r\n/g, '\n');
91
+ assert(actual === expected, `Installed adapter differs from renderer: ${file}`);
92
+ }
93
+ for (const { configFile } of Object.values(ideConfigs)) {
94
+ assert(
95
+ expectedPaths.has(configFile) || !fs.existsSync(resolveWithin(projectDir, configFile)),
96
+ `Unselected adapter was installed: ${configFile}`
97
+ );
98
+ }
99
+ if (requireInventory) {
100
+ assert.equal(manifest.fileInventory?.schemaVersion, 1, 'Missing file inventory');
101
+ const { collectManagedFiles } = require('../cli/lib/update-transaction');
102
+ const managedFiles = collectManagedFiles({ projectDir, packs });
103
+ assert.deepEqual(
104
+ Object.keys(manifest.fileInventory.files).sort(),
105
+ managedFiles.map((entry) => entry.file).sort(),
106
+ 'Inventory does not cover exactly the selected framework files'
107
+ );
108
+ for (const { file } of managedFiles) {
109
+ const actualHash = crypto
110
+ .createHash('sha256')
111
+ .update(fs.readFileSync(resolveWithin(projectDir, file)))
112
+ .digest('hex');
113
+ assert.equal(
114
+ manifest.fileInventory.files[file],
115
+ actualHash,
116
+ `Inventory differs from installed bytes: ${file}`
117
+ );
118
+ }
119
+ }
120
+ return files.length;
121
+ }
122
+
123
+ function verifyDiagnostics(report, { packs, tools, derived }) {
124
+ assert.equal(report.schemaVersion, 1, 'Unsupported diagnostic report');
125
+ assert.equal(report.versions.installed, derived.product.version);
126
+ assert.equal(report.versions.cli, derived.product.version);
127
+ assert.equal(report.versions.published, null, 'Doctor must not invent npm freshness evidence');
128
+ sameSelection(
129
+ report.packs.map(({ id }) => id),
130
+ packs,
131
+ 'diagnostic packs'
132
+ );
133
+ sameSelection(
134
+ report.integrations.map(({ tool }) => tool),
135
+ tools,
136
+ 'diagnostic integrations'
137
+ );
138
+ for (const integration of report.integrations) {
139
+ assert.deepEqual(
140
+ integration,
141
+ {
142
+ tool: integration.tool,
143
+ ...derived.diagnostics.integrations[integration.tool],
144
+ ownership: 'unchanged',
145
+ hostVerified: false,
146
+ },
147
+ 'An installed adapter must not imply verified host execution'
148
+ );
149
+ }
150
+ const expectedErrors = [];
151
+ assert.deepEqual(
152
+ report.checks
153
+ .filter(({ status }) => status === 'error')
154
+ .map(({ code, path: file }) => `${code}:${file}`)
155
+ .sort(),
156
+ expectedErrors.sort(),
157
+ 'Every advertised pack must install without delivery errors'
158
+ );
159
+ assert.equal(report.summary.errors, expectedErrors.length);
160
+ assert.equal(report.inventory.status, 'complete');
161
+ assert(report.inventory.trackedFiles > 0, 'Doctor did not inspect the ownership inventory');
162
+ assert.equal(
163
+ report.checks.filter(({ code }) => code === 'file.unchanged').length,
164
+ report.inventory.trackedFiles,
165
+ 'Every freshly installed managed file must have matching ownership'
166
+ );
167
+ assert(!report.checks.some(({ code }) => ['file.modified', 'file.unowned'].includes(code)));
168
+ for (const runtime of report.runtimes.filter(({ runtime }) => runtime === 'python')) {
169
+ assert.equal(
170
+ runtime.imports,
171
+ 'not-verified',
172
+ 'Presence of requirements cannot prove Python imports'
173
+ );
174
+ assert.equal(runtime.environment, 'missing', 'Contract checks must not provision Python');
175
+ }
176
+ const expectedStatus = expectedErrors.length
177
+ ? 'error'
178
+ : report.summary.warnings
179
+ ? 'warning'
180
+ : 'ok';
181
+ assert.equal(report.status, expectedStatus);
182
+ }
183
+
184
+ function run(command, args, { cwd, env, timeout = 60000, expectedExitCode = 0 }) {
185
+ const result = spawnSync(command, args, {
186
+ cwd,
187
+ env,
188
+ timeout,
189
+ encoding: 'utf8',
190
+ maxBuffer: 16 * 1024 * 1024,
191
+ windowsHide: true,
192
+ stdio: ['ignore', 'pipe', 'pipe'],
193
+ });
194
+ if (result.error || result.status !== expectedExitCode) {
195
+ const reason = result.error
196
+ ? result.error.message
197
+ : `exit ${result.status}, signal ${result.signal || 'none'}`;
198
+ throw new Error(
199
+ `${path.basename(command)} ${args.join(' ')}: ${reason}\n${result.stdout || ''}${result.stderr || ''}`
200
+ );
201
+ }
202
+ return result.stdout;
203
+ }
204
+
205
+ /** Invoke npm without a shell, including Windows where npm.cmd is not executable. */
206
+ function npmCommand(env) {
207
+ const candidates = [
208
+ env.npm_execpath,
209
+ path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
210
+ ...(env.PATH || env.Path || '')
211
+ .split(path.delimiter)
212
+ .map((dir) => path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js')),
213
+ ];
214
+ const cli = candidates.find(
215
+ (candidate) => candidate && /npm-cli\.js$/i.test(candidate) && fs.existsSync(candidate)
216
+ );
217
+ if (cli) return { command: process.execPath, args: [cli] };
218
+ assert(process.platform !== 'win32', 'Could not locate npm-cli.js beside Node or on PATH');
219
+ return { command: 'npm', args: [] };
220
+ }
221
+
222
+ function packAndExtract(tempRoot, env) {
223
+ const destination = path.join(tempRoot, 'distribution');
224
+ fs.mkdirSync(destination);
225
+ const npm = npmCommand(env);
226
+ const output = run(
227
+ npm.command,
228
+ [
229
+ ...npm.args,
230
+ 'pack',
231
+ '--ignore-scripts',
232
+ '--offline',
233
+ '--json',
234
+ '--pack-destination',
235
+ destination,
236
+ '--cache',
237
+ path.join(tempRoot, 'npm-cache'),
238
+ ],
239
+ { cwd: REPO_ROOT, env, timeout: 120000 }
240
+ );
241
+ const packed = JSON.parse(output);
242
+ assert(Array.isArray(packed) && packed.length === 1, 'npm pack must produce exactly one package');
243
+ const archive = resolveWithin(destination, packed[0].filename);
244
+ const entries = run('tar', ['-tzf', archive], { cwd: destination, env }).trim().split(/\r?\n/);
245
+ for (const entry of entries) {
246
+ assert(entry.startsWith('package/'), `Unexpected npm archive entry: ${entry}`);
247
+ resolveWithin(destination, entry);
248
+ }
249
+ run('tar', ['-xzf', archive, '-C', destination], { cwd: destination, env });
250
+ const packageRoot = path.join(destination, 'package');
251
+ assert(
252
+ !fs.existsSync(path.join(packageRoot, 'registry.yaml')),
253
+ 'npm contract must work without registry.yaml'
254
+ );
255
+ assert(fs.statSync(packageRoot).isDirectory(), 'npm package directory is missing');
256
+ return packageRoot;
257
+ }
258
+
259
+ function runMatrix(packageRoot, variant, tempRoot, env, reference) {
260
+ const allPacks = Object.keys(reference.packDefinitions).filter(
261
+ (id) => !reference.packDefinitions[id].disabled
262
+ );
263
+ const allTools = Object.keys(reference.ideConfigs);
264
+ assert(allTools.length > 0, 'IDE_CONFIGS must expose at least one target');
265
+ const selections = [
266
+ { argument: 'core', packs: ['core'] },
267
+ { argument: 'core,osint', packs: ['core', 'osint'] },
268
+ { argument: 'core,memory', packs: ['core', 'memory'] },
269
+ { argument: 'all', packs: allPacks },
270
+ ];
271
+ const toolSelections = [...allTools.map((tool) => [tool]), allTools];
272
+ let installations = 0;
273
+ let files = 0;
274
+ for (const selection of selections) {
275
+ for (const tools of toolSelections) {
276
+ const label = `${variant}/${selection.argument}/${tools.join(',')}`;
277
+ const projectDir = path.join(tempRoot, `${variant}-project-${++installations}`);
278
+ fs.mkdirSync(projectDir);
279
+ try {
280
+ run(
281
+ process.execPath,
282
+ [
283
+ path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
284
+ 'install',
285
+ '--yes',
286
+ '--lang',
287
+ 'en',
288
+ '--directory',
289
+ projectDir,
290
+ '--packs',
291
+ selection.argument,
292
+ '--tools',
293
+ tools.join(','),
294
+ ],
295
+ { cwd: projectDir, env }
296
+ );
297
+ files += verifyInstallation(projectDir, { ...reference, packs: selection.packs, tools });
298
+ const expectedExitCode = 0;
299
+ const diagnosis = run(
300
+ process.execPath,
301
+ [
302
+ path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
303
+ 'doctor',
304
+ '--json',
305
+ '--directory',
306
+ projectDir,
307
+ ],
308
+ { cwd: projectDir, env, expectedExitCode }
309
+ );
310
+ verifyDiagnostics(JSON.parse(diagnosis), {
311
+ packs: selection.packs,
312
+ tools,
313
+ derived: reference.derived,
314
+ variant,
315
+ });
316
+ if (selection.packs.includes('dev-studio')) {
317
+ const list = JSON.parse(
318
+ run(
319
+ process.execPath,
320
+ [
321
+ path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
322
+ 'studio',
323
+ 'list',
324
+ '--directory',
325
+ projectDir,
326
+ '--json',
327
+ ],
328
+ { cwd: projectDir, env }
329
+ )
330
+ );
331
+ assert.equal(list.workflows.length, reference.derived.packs['dev-studio'].workflowCount);
332
+ const prepared = JSON.parse(
333
+ run(
334
+ process.execPath,
335
+ [
336
+ path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
337
+ 'studio',
338
+ 'prepare',
339
+ 'create-ux-design',
340
+ '--request',
341
+ 'Design the sign-in flow',
342
+ '--directory',
343
+ projectDir,
344
+ '--json',
345
+ ],
346
+ { cwd: projectDir, env }
347
+ )
348
+ );
349
+ assert.equal(prepared.status, 'ready');
350
+ assert.equal(prepared.executed, false);
351
+ assert(prepared.instructions.some((item) => item.path.endsWith('/create-ux-design.md')));
352
+ assert(!fs.existsSync(path.join(projectDir, prepared.outputPath)));
353
+ }
354
+ if (tools.length === allTools.length) {
355
+ const output = run(
356
+ process.execPath,
357
+ [
358
+ path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
359
+ 'update-check',
360
+ '--offline',
361
+ '--json',
362
+ '--directory',
363
+ projectDir,
364
+ ],
365
+ { cwd: projectDir, env }
366
+ );
367
+ const checked = JSON.parse(output);
368
+ assert.equal(
369
+ checked.status,
370
+ 'unknown',
371
+ 'Offline check must not claim the latest release'
372
+ );
373
+ assert.equal(checked.updateAvailable, null);
374
+ assert.equal(checked.canAutoApply, false);
375
+ }
376
+ } catch (error) {
377
+ throw new Error(`${label}: ${error.message}`, { cause: error });
378
+ }
379
+ }
380
+ console.log(
381
+ `OK ${variant}: ${selection.argument} across ${toolSelections.length} tool selections`
382
+ );
383
+ }
384
+ return { installations, files };
385
+ }
386
+
387
+ function main() {
388
+ const { PACKS, DERIVED } = require('../cli/lib/packs');
389
+ const { IDE_CONFIGS } = require('../cli/lib/ide-config');
390
+ const { generateUserFiles } = require('./generate-adapters');
391
+ assert(
392
+ DERIVED && typeof generateUserFiles === 'function',
393
+ 'Generated install contract exports are missing'
394
+ );
395
+ const reference = {
396
+ packDefinitions: PACKS,
397
+ derived: DERIVED,
398
+ ideConfigs: IDE_CONFIGS,
399
+ renderFiles: generateUserFiles,
400
+ requireInventory: true,
401
+ };
402
+ const tempParent = fs.realpathSync(os.tmpdir());
403
+ const tempRoot = fs.mkdtempSync(path.join(tempParent, TEMP_PREFIX));
404
+ try {
405
+ const brain = path.join(tempRoot, 'brain');
406
+ fs.mkdirSync(brain);
407
+ const env = {
408
+ ...process.env,
409
+ BMAD_PLUS_BRAIN: brain,
410
+ NODE_PATH: path.join(REPO_ROOT, 'node_modules'),
411
+ NO_COLOR: '1',
412
+ NPM_CONFIG_UPDATE_NOTIFIER: 'false',
413
+ };
414
+ const source = runMatrix(REPO_ROOT, 'source', tempRoot, env, reference);
415
+ const packageRoot = packAndExtract(tempRoot, env);
416
+ const { checkPythonDelivery } = require('./pack-delivery');
417
+ const { loadRegistry } = require('./generate');
418
+ assert.deepEqual(
419
+ checkPythonDelivery(loadRegistry(), {
420
+ packageRoot,
421
+ packageJson: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')),
422
+ }),
423
+ [],
424
+ 'Declared Python resources must exist in the real npm archive'
425
+ );
426
+ const packaged = runMatrix(packageRoot, 'npm', tempRoot, env, reference);
427
+ console.log(
428
+ `Install contract passed: ${source.installations + packaged.installations} installs and doctor JSON reports, ${source.files + packaged.files} files compared (source + npm without registry.yaml).`
429
+ );
430
+ return 0;
431
+ } finally {
432
+ // Delete only this mkdtemp-owned direct child of the verified temp directory.
433
+ assert(
434
+ path.dirname(tempRoot) === tempParent && path.basename(tempRoot).startsWith(TEMP_PREFIX)
435
+ );
436
+ assert(fs.realpathSync(tempRoot) === tempRoot && !fs.lstatSync(tempRoot).isSymbolicLink());
437
+ fs.rmSync(tempRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
438
+ }
439
+ }
440
+
441
+ if (require.main === module) {
442
+ try {
443
+ process.exitCode = main();
444
+ } catch (error) {
445
+ console.error(`Install contract failed: ${error.message}`);
446
+ process.exitCode = 1;
447
+ }
448
+ }
449
+
450
+ module.exports = { main, resolveWithin, verifyInstallation };