bmad-plus 0.12.1 → 0.13.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 (176) hide show
  1. package/CHANGELOG.md +637 -580
  2. package/README.md +123 -120
  3. package/osint-agent-package/agents/osint-investigator.md +12 -0
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
  9. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
  10. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
  11. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
  12. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
  13. package/package.json +106 -91
  14. package/readme-international/README.de.md +625 -594
  15. package/readme-international/README.es.md +642 -611
  16. package/readme-international/README.fr.md +640 -609
  17. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +2 -0
  18. package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
  19. package/src/bmad-plus/data/role-triggers.yaml +52 -0
  20. package/src/bmad-plus/module.yaml +333 -283
  21. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
  22. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
  23. package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
  24. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
  25. package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
  77. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
  78. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
  79. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
  80. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
  81. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
  82. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
  83. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
  84. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
  85. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
  86. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
  87. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
  88. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
  89. package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
  90. package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
  91. package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
  92. package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
  93. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
  94. package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
  95. package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
  96. package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
  97. package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
  98. package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
  99. package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
  100. package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
  101. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
  102. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
  103. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +303 -0
  104. package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
  105. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +409 -0
  106. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
  107. package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
  108. package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
  109. package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
  110. package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
  111. package/src/bmad-plus/packs/pack-shield/README.md +6 -6
  112. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
  113. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
  114. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
  115. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
  116. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
  117. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
  118. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
  119. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
  120. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
  121. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
  122. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
  123. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
  124. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
  125. package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
  126. package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
  127. package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
  128. package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
  129. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
  130. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
  131. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
  132. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
  133. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
  134. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
  135. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
  136. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +1 -1
  137. package/tools/bmad-plus-npx.js +4 -2
  138. package/tools/build/adapters.config.js +60 -51
  139. package/tools/build/check-counts.js +626 -0
  140. package/tools/build/check-install-contract.js +298 -0
  141. package/tools/build/generate-adapters.js +252 -56
  142. package/tools/build/generate.js +187 -10
  143. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  144. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  145. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  146. package/tools/build/generated-adapters/AGENTS.md +20 -7
  147. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  148. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  149. package/tools/build/generated-adapters/GEMINI.md +20 -7
  150. package/tools/build/module.template.yaml +82 -0
  151. package/tools/cli/bmad-plus-cli.js +16 -1
  152. package/tools/cli/commands/autoconfig.js +470 -470
  153. package/tools/cli/commands/doctor.js +205 -233
  154. package/tools/cli/commands/install.js +543 -501
  155. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  156. package/tools/cli/commands/memory.js +195 -195
  157. package/tools/cli/commands/scan.js +348 -348
  158. package/tools/cli/commands/uninstall.js +209 -101
  159. package/tools/cli/commands/update-check.js +31 -0
  160. package/tools/cli/commands/update-policy.js +39 -0
  161. package/tools/cli/commands/update.js +123 -134
  162. package/tools/cli/i18n.js +905 -845
  163. package/tools/cli/lib/README-memory-journal.md +125 -125
  164. package/tools/cli/lib/ide-config.js +10 -259
  165. package/tools/cli/lib/install-manifest.js +17 -0
  166. package/tools/cli/lib/installed-adapters.js +89 -0
  167. package/tools/cli/lib/npm-runner.js +177 -0
  168. package/tools/cli/lib/pack-copy.js +62 -66
  169. package/tools/cli/lib/packs.js +437 -3
  170. package/tools/cli/lib/python-provision.js +508 -508
  171. package/tools/cli/lib/stack-detect.js +102 -102
  172. package/tools/cli/lib/update-check.js +153 -0
  173. package/tools/cli/lib/update-dispatch.js +182 -0
  174. package/tools/cli/lib/update-policy.js +90 -0
  175. package/tools/cli/lib/update-transaction.js +334 -0
  176. package/tools/cli/lib/validate.js +50 -50
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BMAD+ Build — hand-typed count drift checker (audit NS-01 / ARCH-04 / DOC-06)
4
+ *
5
+ * Every "N agents" / "N workflows" / "N packs" claim in a shipped surface is a
6
+ * fact that MUST be derivable from registry.yaml (+ the files on disk). This
7
+ * script derives the truth, scans the tracked truth surfaces for hand-typed
8
+ * numbers that contradict it, and exits non-zero listing every drift.
9
+ *
10
+ * Why this exists: `- **Shield** (GRC) — 38 compliance agents` shipped into
11
+ * every generated user config for months. 38 was not a typo — it was
12
+ * 27 agents + 11 workflows summed into one dimension. A definition error
13
+ * re-drifts on every pack change, so it needs a gate, not a one-off correction.
14
+ *
15
+ * DIMENSIONS ARE THE POINT. 38 is a legitimate number in this repo (Dev Studio
16
+ * has 38 workflows); it is only wrong when attached to `agents`. Every rule
17
+ * below therefore checks a (subject, dimension) pair, never a bare integer.
18
+ *
19
+ * Usage:
20
+ * node tools/build/check-counts.js # verify (exit 1 on drift)
21
+ * node tools/build/check-counts.js --print # print the derived truth and exit 0
22
+ *
23
+ * The test-count claim ("N tests") is the one number here that cannot be
24
+ * derived statically — it requires running Jest. So it is gated by an env var:
25
+ *
26
+ * BMAD_PLUS_TEST_COUNT unset/empty → test claims are listed as NOT GATED and
27
+ * the run says OK (PARTIAL). Never fails:
28
+ * a local `node tools/build/check-counts.js`
29
+ * must not need a full Jest run first.
30
+ * BMAD_PLUS_TEST_COUNT=<integer> → every "N tests" claim must equal it.
31
+ * CI arms this with the Jest-reported
32
+ * total, which is the only run that
33
+ * proves the README's test number.
34
+ * BMAD_PLUS_TEST_COUNT=<garbage> → exit 1. A misspelled total must not
35
+ * masquerade as coverage.
36
+ *
37
+ * Author: Laurent Rochetta
38
+ */
39
+
40
+ 'use strict';
41
+
42
+ const fs = require('node:fs');
43
+ const path = require('node:path');
44
+ const { execFileSync } = require('node:child_process');
45
+ const { loadRegistry, buildDerived, DEFAULT_REGISTRY_PATH } = require('./generate');
46
+
47
+ const REPO_ROOT = path.join(__dirname, '..', '..');
48
+
49
+ /**
50
+ * Files whose numeric claims describe the CURRENT product. Anything not listed
51
+ * is out of scope by construction, so the gate can never be "fixed" by quietly
52
+ * moving a lie into an unscanned file — adding a surface is an explicit edit.
53
+ */
54
+ const TRUTH_SURFACES = [
55
+ 'README.md',
56
+ 'README-DIST.md',
57
+ 'readme-international/README.fr.md',
58
+ 'readme-international/README.es.md',
59
+ 'readme-international/README.de.md',
60
+ 'src/bmad-plus/module.yaml',
61
+ 'src/bmad-plus/packs/pack-shield/README.md',
62
+ 'src/bmad-plus/packs/pack-shield/SKILL.md',
63
+ 'src/bmad-plus/packs/pack-shield/shield-orchestrator.md',
64
+ 'tools/cli/lib/ide-config.js',
65
+ 'tools/build/adapters.config.js',
66
+ ];
67
+
68
+ /**
69
+ * NOT scanned, deliberately:
70
+ * - CHANGELOG.md, audit/**, docs/research/** — historical records. They
71
+ * legitimately quote past and *wrong* numbers ("the advertised 38 agents")
72
+ * verbatim; gating them would force rewriting history to satisfy a linter.
73
+ * - .agents/** — gitignored local install output, not a source.
74
+ * - upstream/** — third-party clone.
75
+ * Release-history table rows inside a scanned README are skipped for the same
76
+ * reason (see isFrozenHistoryRow): v0.6.0 really did ship 30 workflows and
77
+ * v0.12.0 really did ship 333 tests. Those are true statements about the past.
78
+ */
79
+ const HISTORY_ROW_RE = /^\|\s*\*\*\d+\.\d+\.\d+\*\*\s*\|/;
80
+
81
+ function isFrozenHistoryRow(line) {
82
+ return HISTORY_ROW_RE.test(line);
83
+ }
84
+
85
+ /* ── Derivation ─────────────────────────────────────────────────────────── */
86
+
87
+ /** Count *.md agent files on disk for a packaged pack's categories/ tree. */
88
+ function countAgentFilesOnDisk(packDir) {
89
+ const root = path.join(REPO_ROOT, 'src', 'bmad-plus', 'packs', packDir, 'categories');
90
+ if (!fs.existsSync(root)) return null;
91
+ let n = 0;
92
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
93
+ // `workflows/` sits under categories/ but is NOT an agent category — it is
94
+ // the directory whose 11 files got summed into the agent total to make 38.
95
+ if (!entry.isDirectory() || entry.name === 'workflows') continue;
96
+ n += fs.readdirSync(path.join(root, entry.name)).filter((f) => f.endsWith('.md')).length;
97
+ }
98
+ return n;
99
+ }
100
+
101
+ /** Count *.md workflow files on disk for a packaged pack. */
102
+ function countWorkflowFilesOnDisk(packDir) {
103
+ const dir = path.join(REPO_ROOT, 'src', 'bmad-plus', 'packs', packDir, 'categories', 'workflows');
104
+ if (!fs.existsSync(dir)) return null;
105
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
106
+ }
107
+
108
+ /**
109
+ * Derive every count this gate enforces, from registry.yaml + the filesystem.
110
+ * Cross-checks the registry against the files on disk: if they disagree, the
111
+ * derivation itself is untrustworthy and that is reported as a drift.
112
+ */
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;
117
+ const shield = packs.shield;
118
+ const devStudio = packs['dev-studio'];
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;
126
+
127
+ const mismatches = [];
128
+ const onDiskAgents = countAgentFilesOnDisk(registry.packs.shield.pack_dir);
129
+ const onDiskWorkflows = countWorkflowFilesOnDisk(registry.packs.shield.pack_dir);
130
+ if (onDiskAgents !== null && onDiskAgents !== shieldAgents) {
131
+ mismatches.push(
132
+ `registry.yaml declares ${shieldAgents} shield category agents but ${onDiskAgents} agent files exist on disk`
133
+ );
134
+ }
135
+ if (onDiskWorkflows !== null && onDiskWorkflows !== shieldWorkflows) {
136
+ mismatches.push(
137
+ `registry.yaml declares ${shieldWorkflows} shield workflows but ${onDiskWorkflows} workflow files exist on disk`
138
+ );
139
+ }
140
+
141
+ // Sizes of the declared per-category agent groups ("5 agents", "6 agents"…),
142
+ // which are legitimate claims wherever a category is described.
143
+ const perCategorySizes = new Set(shield.categoryAgentCounts);
144
+ // Sizes of the per-pack installer rosters ("3 agents (Scout, Chief, Judge)").
145
+ const perPackInstallerSizes = new Set(Object.values(packs).map((p) => p.installerAgentCount));
146
+
147
+ return {
148
+ packCount: derived.packCount,
149
+ installerAgents,
150
+ shieldAgents,
151
+ shieldWorkflows,
152
+ shieldCategories,
153
+ shieldFrameworks,
154
+ shieldReferenceFiles: shield.referenceFiles,
155
+ devStudioSubAgents,
156
+ devStudioWorkflows,
157
+ totalAgents,
158
+ languages: derived.languages,
159
+ perCategorySizes,
160
+ // The COMPLETE vocabulary of numbers that may legitimately precede "agents"
161
+ // anywhere in the product's own copy. Default-deny: a value outside this set
162
+ // is a number nobody can derive, so it is drift by definition. This is what
163
+ // gates the 47s — "47 agents" stays green only while 47 is still derivable,
164
+ // and every occurrence turns red the day the roster changes.
165
+ agentCounts: new Set([
166
+ installerAgents,
167
+ shieldAgents,
168
+ devStudioSubAgents,
169
+ totalAgents,
170
+ ...perCategorySizes,
171
+ ...perPackInstallerSizes,
172
+ ]),
173
+ // Same idea for the workflow dimension. Anchored lines get graded against
174
+ // the one pack they name; unanchored ones must still be *some* real total.
175
+ workflowCounts: new Set(Object.values(packs).map((p) => p.workflowCount).filter(Boolean)),
176
+ derivationMismatches: mismatches,
177
+ };
178
+ }
179
+
180
+ /* ── Rules ──────────────────────────────────────────────────────────────── */
181
+
182
+ const SHIELD_ANCHOR =
183
+ /shield|\bGRC\b|compliance[- ]agent|agents? de conformité|agentes de cumplimiento|Compliance-Agenten/i;
184
+ const DEV_STUDIO_ANCHOR = /dev[- ]studio/i;
185
+
186
+ /**
187
+ * Lines whose agent count describes the UPSTREAM project, not this one:
188
+ * "BMAD-METHOD ... 9 specialized agents" is a true statement about a different
189
+ * product, so the roster rule must not grade it. Scoped to the agent dimension
190
+ * only — a line may name BMAD-METHOD and still make a claim of its own
191
+ * ("Fork of BMAD-METHOD — ... CLI installer in 10 languages"), and that claim
192
+ * stays gated.
193
+ */
194
+ const FOREIGN_SUBJECT_ANCHOR = /BMAD-METHOD/i;
195
+
196
+ /**
197
+ * `| BMAD-METHOD | BMAD+ |` opens the upstream comparison table, whose rows
198
+ * carry a foreign count in the left column and ours in the right ("| 9
199
+ * specialized agents | **47 agents** across 9 packs |") without repeating the
200
+ * marker. The rows are recognised positionally, like isFrozenHistoryRow.
201
+ */
202
+ const FOREIGN_TABLE_HEADER_RE = /^\s*\|\s*BMAD-METHOD\s*\|/i;
203
+
204
+ /* A claim is `<number> <qualifiers…> <dimension noun>`. The three constants
205
+ * below define what may sit between the number and the noun; every rule is
206
+ * built from them by claimRe(), so a fix to the CLASS lands on every dimension
207
+ * at once instead of on one hand-listed adjective. */
208
+
209
+ /**
210
+ * What may SEPARATE the pieces of a claim: whitespace and markdown
211
+ * emphasis/code markers — "**38** agents", "`38` agents", "**38 agents**".
212
+ * `\s` already covers U+00A0/U+202F, which the French copy uses before
213
+ * punctuation.
214
+ *
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.
218
+ */
219
+ const GAP = '[\\s*`-]*';
220
+
221
+ /** One qualifier word: hyphenated and accented forms included. */
222
+ const WORD = '[A-Za-zÀ-ÖØ-öø-ÿ][A-Za-zÀ-ÖØ-öø-ÿ-]*';
223
+
224
+ /**
225
+ * Maximum qualifiers echoed in diagnostics. Longer adjective runs are still
226
+ * graded; clause/dimension boundaries below prevent cross-sentence claims.
227
+ */
228
+ const MAX_QUALIFIERS = 4;
229
+
230
+ /**
231
+ * Words that TERMINATE a qualifier run instead of decorating it:
232
+ * - dimension nouns — the number belongs to THAT dimension, not to the one
233
+ * further right. "11 workflow agents" is a *workflow* count (graded by the
234
+ * workflows rule), and summing it into the agent dimension is precisely how
235
+ * 27 + 11 became 38.
236
+ * - connectives and articles — without them a single match could span two
237
+ * independent claims ("9 packs and 27 agents").
238
+ */
239
+ const NOT_A_QUALIFIER = new Set(
240
+ `agent agents agente agentes agenten workflow workflows pack packs test tests
241
+ category categories catégorie catégories categoría categorías kategorie kategorien
242
+ framework frameworks language languages langue langues idioma idiomas sprache sprachen
243
+ skill skills phase phases level levels
244
+ and or plus with across of in on for to the a an
245
+ et ou des les la le du dans sur pour avec y con de en und oder mit für`.split(/\s+/)
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(' '));
251
+
252
+ /**
253
+ * Quantity approximators sitting in front of the number. "over 47 agents" is
254
+ * the same marketing hedge as "47+ agents" and must die the same way — the
255
+ * roster is an exact, derivable integer. Deliberately excludes "about" and
256
+ * "around", which read as "on the subject of" far too often to be safe.
257
+ */
258
+ const APPROX_PREFIX_RE = new RegExp(
259
+ `(?:over|more than|nearly|almost|approximately|approx\\.?|up to|~|≈` +
260
+ `|plus de|environ|más de|mehr als)${GAP}$`,
261
+ 'i'
262
+ );
263
+
264
+ /**
265
+ * Build a /g/ claim regex for one dimension's noun alternatives.
266
+ *
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.
270
+ */
271
+ function claimRe(noun) {
272
+ return new RegExp(
273
+ `(?<![\\w.-])(\\d+)${GAP}(\\+)?${GAP}((?:${WORD}[\\s*\u0060-]+)*?)(${noun})\\b`,
274
+ 'gi'
275
+ );
276
+ }
277
+
278
+ const AGENTS_RE = claimRe('agents?|agentes|agenten');
279
+ const WORKFLOWS_RE = claimRe('workflows?');
280
+ const CATEGORIES_RE = claimRe('categories|catégories|categorías|Kategorien');
281
+ const PACKS_RE = claimRe('packs?');
282
+ const TESTS_RE = claimRe('tests?');
283
+ const FRAMEWORKS_RE = claimRe('frameworks?');
284
+ const LANGUAGES_RE = claimRe('languages?|langues?|idiomas?|Sprachen?');
285
+ const REFERENCE_FILES_RE = claimRe('reference files?');
286
+
287
+ /**
288
+ * Collect every claim `re` finds on `line`.
289
+ * `re` must be a /g/ regex; it is reset before use so it is safe to reuse.
290
+ * Dimension/clause crossings are discarded. Connectives truncate the echoed
291
+ * qualifiers without discarding a claim such as "38 compliance and GRC agents".
292
+ * @returns {{value:number, plus:boolean, approx:boolean, text:string}[]}
293
+ */
294
+ function matchAll(re, line) {
295
+ re.lastIndex = 0;
296
+ const out = [];
297
+ let m;
298
+ while ((m = re.exec(line)) !== null) {
299
+ const qualifiers = (m[3] || '').split(/[^A-Za-zÀ-ÖØ-öø-ÿ]+/).filter(Boolean);
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));
303
+ out.push({
304
+ value: Number(m[1]),
305
+ plus: m[2] === '+',
306
+ approx: APPROX_PREFIX_RE.test(line.slice(0, m.index)),
307
+ // Markers are dropped from the echoed text so a bolded claim reports as
308
+ // `38 compliance agents`, not `38** compliance agents`. file:line locates it.
309
+ text: [m[1] + (m[2] || ''), ...bounded, m[4]].join(' '),
310
+ });
311
+ }
312
+ return out;
313
+ }
314
+
315
+ /**
316
+ * Check one line against the derived truth.
317
+ * @param {string} line
318
+ * @param {object} truth output of deriveTruth()
319
+ * @param {{foreignSubject?: boolean}} [opts] foreignSubject suppresses the
320
+ * product-roster rule only (the line counts a DIFFERENT product's agents).
321
+ * @returns {string[]} human-readable drift descriptions (empty when the line is fine)
322
+ */
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
+ }
329
+ const problems = [];
330
+ const shieldLine = SHIELD_ANCHOR.test(line);
331
+ const devStudioLine = DEV_STUDIO_ANCHOR.test(line);
332
+
333
+ if (shieldLine) {
334
+ for (const { value, text } of matchAll(AGENTS_RE, line)) {
335
+ // Per-category headings ("5 agents") are legitimate; only reject the
336
+ // values that misstate the pack-wide total. Anything that is not a
337
+ // declared per-category size and not 27 is a conflation.
338
+ if (value !== truth.shieldAgents && !isPerCategorySize(value, truth)) {
339
+ problems.push(
340
+ `"${text.trim()}" — Shield has ${truth.shieldAgents} compliance agents` +
341
+ (value === truth.shieldAgents + truth.shieldWorkflows
342
+ ? ` (${truth.shieldAgents} agents + ${truth.shieldWorkflows} workflows conflated into one "agents" figure)`
343
+ : '')
344
+ );
345
+ }
346
+ }
347
+ for (const { value, text } of matchAll(WORKFLOWS_RE, line)) {
348
+ if (value !== truth.shieldWorkflows) {
349
+ problems.push(`"${text.trim()}" — Shield has ${truth.shieldWorkflows} workflows`);
350
+ }
351
+ }
352
+ for (const { value, text } of matchAll(CATEGORIES_RE, line)) {
353
+ if (value !== truth.shieldCategories) {
354
+ problems.push(
355
+ `"${text.trim()}" — Shield has ${truth.shieldCategories} agent categories` +
356
+ (value === truth.shieldCategories + 1
357
+ ? ' (the workflows/ directory is not a category)'
358
+ : '')
359
+ );
360
+ }
361
+ }
362
+ }
363
+
364
+ if (devStudioLine) {
365
+ for (const { value, text } of matchAll(WORKFLOWS_RE, line)) {
366
+ if (value !== truth.devStudioWorkflows) {
367
+ problems.push(`"${text.trim()}" — Dev Studio has ${truth.devStudioWorkflows} workflows`);
368
+ }
369
+ }
370
+ }
371
+
372
+ // A "+" (or an "over"/"more than") on an agent total is always a marketing
373
+ // approximation: the roster is an exact, derivable integer. This is the rule
374
+ // that kills "56+ agents" — and "over 56 agents", which is the same hedge
375
+ // wearing a different hat.
376
+ for (const { plus, approx, text } of matchAll(AGENTS_RE, line)) {
377
+ if (!plus && !approx) continue;
378
+ problems.push(
379
+ `"${text.trim()}" — agent totals are exact and derivable, not approximate` +
380
+ ` (the roster is ${truth.totalAgents}; "56+" came from Dev Studio's *skills* count)`
381
+ );
382
+ }
383
+
384
+ // The roster rule. Shield-anchored lines are graded by the stricter rule
385
+ // above; everywhere else, any number attached to "agents" must be one this
386
+ // repo can actually derive. Default-deny is the point: it is what makes the
387
+ // tracked "47 agents" claims fail the day the roster stops being 47, and what
388
+ // catches a number ("999") that corresponds to nothing at all.
389
+ if (!shieldLine && !foreignSubject) {
390
+ for (const { value, plus, approx, text } of matchAll(AGENTS_RE, line)) {
391
+ if (plus || approx) continue; // already reported as an approximation
392
+ if (!truth.agentCounts.has(value)) {
393
+ problems.push(
394
+ `"${text.trim()}" — no derived agent count equals ${value} (roster: ` +
395
+ `${truth.totalAgents} total = ${truth.installerAgents} installer + ` +
396
+ `${truth.shieldAgents} shield + ${truth.devStudioSubAgents} dev-studio sub-agents)`
397
+ );
398
+ }
399
+ }
400
+ }
401
+
402
+ // Same default-deny for the workflow dimension. Anchored lines are graded
403
+ // against the one pack they name; an unanchored line must still quote a real
404
+ // total (this is what catches a pack description whose own name sits on the
405
+ // line above it).
406
+ if (!shieldLine && !devStudioLine) {
407
+ for (const { value, text } of matchAll(WORKFLOWS_RE, line)) {
408
+ if (!truth.workflowCounts.has(value)) {
409
+ problems.push(
410
+ `"${text.trim()}" — no pack has ${value} workflows (Shield ${truth.shieldWorkflows},` +
411
+ ` Dev Studio ${truth.devStudioWorkflows})`
412
+ );
413
+ }
414
+ }
415
+ }
416
+
417
+ // Packs and languages are exact derivable integers, so — like the agent
418
+ // roster — they get no hedge. Modelling each hedge's arithmetic instead would
419
+ // mean conceding that "10+ languages" (>= 10, true) and "more than 10
420
+ // languages" (> 10, false) are different claims about the same 10; the honest
421
+ // rule is that a number you can derive is simply stated.
422
+ for (const { value, plus, approx, text } of matchAll(PACKS_RE, line)) {
423
+ if (plus || approx) {
424
+ problems.push(
425
+ `"${text.trim()}" — the pack count is exact and derivable, not approximate` +
426
+ ` (the registry declares ${truth.packCount})`
427
+ );
428
+ } else if (value !== truth.packCount) {
429
+ problems.push(`"${text.trim()}" — the registry declares ${truth.packCount} packs`);
430
+ }
431
+ }
432
+
433
+ for (const { value, plus, approx, text } of matchAll(LANGUAGES_RE, line)) {
434
+ if (plus || approx) {
435
+ problems.push(
436
+ `"${text.trim()}" — the language count is exact and derivable, not approximate` +
437
+ ` (tools/cli/i18n.js declares ${truth.languages.length})`
438
+ );
439
+ } else if (value !== truth.languages.length) {
440
+ problems.push(
441
+ `"${text.trim()}" — tools/cli/i18n.js declares ${truth.languages.length} languages` +
442
+ ` (${truth.languages.join(', ')})`
443
+ );
444
+ }
445
+ }
446
+
447
+ // Frameworks are the one dimension gated as a LOWER BOUND. Unlike the rosters
448
+ // above, the exact claims are per-category groupings ("AI Governance (3
449
+ // frameworks)") that are NOT derivable — registry categories carry agents,
450
+ // not compliance_tags — so an equality rule would fire on true statements.
451
+ // The defect that remains checkable is the overclaim: promising more
452
+ // frameworks than the pack actually tags. "25+" stays honest while 26 exist.
453
+ for (const { value, text } of matchAll(FRAMEWORKS_RE, line)) {
454
+ if (value > truth.shieldFrameworks) {
455
+ problems.push(
456
+ `"${text.trim()}" — Shield tags ${truth.shieldFrameworks} compliance frameworks`
457
+ );
458
+ }
459
+ }
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
+
467
+ return problems;
468
+ }
469
+
470
+ /** Per-category agent-group sizes that may legitimately appear near "agents". */
471
+ function isPerCategorySize(value, truth) {
472
+ return truth.perCategorySizes.has(value);
473
+ }
474
+
475
+ /* ── Scan ───────────────────────────────────────────────────────────────── */
476
+
477
+ /** Files tracked by git, restricted to the declared truth surfaces. */
478
+ function trackedTruthSurfaces() {
479
+ let tracked;
480
+ try {
481
+ tracked = new Set(
482
+ execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
483
+ .split('\n')
484
+ .filter(Boolean)
485
+ );
486
+ } catch {
487
+ tracked = null; // not a git checkout (e.g. an npm tarball) → scan what exists
488
+ }
489
+ return TRUTH_SURFACES.filter((f) => {
490
+ if (tracked && !tracked.has(f)) return false;
491
+ return fs.existsSync(path.join(REPO_ROOT, f));
492
+ });
493
+ }
494
+
495
+ /**
496
+ * Scan the truth surfaces.
497
+ * @returns {{ ok: boolean, drifts: string[], unverified: string[], truth: object }}
498
+ */
499
+ function check({ registryPath = DEFAULT_REGISTRY_PATH, testCount = null } = {}) {
500
+ const truth = deriveTruth(registryPath);
501
+ const drifts = [...truth.derivationMismatches];
502
+ const unverified = [];
503
+
504
+ for (const file of trackedTruthSurfaces()) {
505
+ const lines = fs.readFileSync(path.join(REPO_ROOT, file), 'utf8').split(/\r?\n/);
506
+ let inForeignTable = false;
507
+ lines.forEach((line, i) => {
508
+ if (isFrozenHistoryRow(line)) return;
509
+ if (FOREIGN_TABLE_HEADER_RE.test(line)) {
510
+ inForeignTable = true;
511
+ return;
512
+ }
513
+ if (inForeignTable && !line.trimStart().startsWith('|')) inForeignTable = false;
514
+
515
+ const foreignSubject = inForeignTable || FOREIGN_SUBJECT_ANCHOR.test(line);
516
+ for (const problem of checkLine(line, truth, { foreignSubject })) {
517
+ drifts.push(`${file}:${i + 1}: ${problem}`);
518
+ }
519
+ for (const { value, text } of matchAll(TESTS_RE, line)) {
520
+ if (testCount === null) {
521
+ unverified.push(`${file}:${i + 1}: "${text.trim()}"`);
522
+ } else if (value !== testCount) {
523
+ drifts.push(`${file}:${i + 1}: "${text.trim()}" — the suite reports ${testCount} tests`);
524
+ }
525
+ }
526
+ });
527
+ }
528
+
529
+ return { ok: drifts.length === 0, drifts, unverified, truth };
530
+ }
531
+
532
+ /**
533
+ * Read the test-count rule's arming variable.
534
+ * Unset/empty means "not armed" (never a failure). Anything else must be a
535
+ * plain non-negative integer: `Number(" ")` is 0 and `Number("-1")` is an
536
+ * integer, so a bare Number()+isInteger check would silently gate against a
537
+ * garbage total.
538
+ * @returns {{armed: boolean, testCount: number|null, error: string|null}}
539
+ */
540
+ function readTestCountEnv(raw) {
541
+ if (typeof raw !== 'string' || raw.trim() === '') {
542
+ return { armed: false, testCount: null, error: null };
543
+ }
544
+ if (!/^\d+$/.test(raw.trim())) {
545
+ return {
546
+ armed: true,
547
+ testCount: null,
548
+ error: `BMAD_PLUS_TEST_COUNT must be a non-negative integer (got "${raw}")`,
549
+ };
550
+ }
551
+ return { armed: true, testCount: Number(raw.trim()), error: null };
552
+ }
553
+
554
+ /* ── CLI ────────────────────────────────────────────────────────────────── */
555
+
556
+ function main(argv) {
557
+ const args = argv.slice(2);
558
+ const { armed, testCount, error } = readTestCountEnv(process.env.BMAD_PLUS_TEST_COUNT);
559
+ if (error) {
560
+ console.error(error);
561
+ return 1;
562
+ }
563
+
564
+ const result = check({ testCount });
565
+
566
+ if (args.includes('--print')) {
567
+ const { derivationMismatches, ...facts } = result.truth;
568
+ void derivationMismatches;
569
+ const asArray = (_k, v) => (v instanceof Set ? [...v].sort((a, b) => a - b) : v);
570
+ console.log(JSON.stringify(facts, asArray, 2));
571
+ return 0;
572
+ }
573
+
574
+ const partial = result.unverified.length > 0;
575
+
576
+ if (result.ok) {
577
+ console.log(
578
+ `${partial ? 'OK (PARTIAL)' : 'OK'} — no count drift. Derived from registry.yaml + files on disk: ` +
579
+ `${result.truth.shieldAgents} shield agents + ${result.truth.shieldWorkflows} shield workflows, ` +
580
+ `${result.truth.devStudioWorkflows} dev-studio workflows, ${result.truth.totalAgents} agents total, ` +
581
+ `${result.truth.packCount} packs, ${result.truth.languages.length} languages.`
582
+ );
583
+ } else {
584
+ console.error('COUNT DRIFT DETECTED — hand-typed numbers contradict registry.yaml:');
585
+ for (const d of result.drifts) console.error(` - ${d}`);
586
+ console.error(
587
+ 'Fix the claim (or registry.yaml if the registry is what is wrong), then re-run:'
588
+ );
589
+ console.error(' node tools/build/check-counts.js');
590
+ }
591
+
592
+ // Reported on BOTH paths, on purpose. A green run with the variable unset
593
+ // must not read as full coverage — and a red run must not bury the fact that
594
+ // the test claims went unchecked on top of whatever else broke.
595
+ if (partial) {
596
+ console.warn('');
597
+ console.warn(
598
+ `!! NOT GATED — ${result.unverified.length} test-count claim(s) were NOT checked ` +
599
+ `(BMAD_PLUS_TEST_COUNT unset):`
600
+ );
601
+ for (const u of result.unverified) console.warn(` ${u}`);
602
+ console.warn(' This run does not cover those numbers. To gate them, pass the');
603
+ console.warn(' Jest-reported total:');
604
+ console.warn(' BMAD_PLUS_TEST_COUNT=<total> node tools/build/check-counts.js');
605
+ } else if (armed) {
606
+ console.log(`Test-count claims gated against BMAD_PLUS_TEST_COUNT=${testCount}.`);
607
+ }
608
+
609
+ return result.ok ? 0 : 1;
610
+ }
611
+
612
+ if (require.main === module) {
613
+ process.exitCode = main(process.argv);
614
+ }
615
+
616
+ module.exports = {
617
+ REPO_ROOT,
618
+ TRUTH_SURFACES,
619
+ deriveTruth,
620
+ checkLine,
621
+ isFrozenHistoryRow,
622
+ readTestCountEnv,
623
+ trackedTruthSurfaces,
624
+ check,
625
+ main,
626
+ };