bmad-plus 0.13.0 → 0.16.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 (135) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +113 -487
  3. package/SECURITY.md +71 -0
  4. package/THIRD-PARTY-LICENSES.md +349 -0
  5. package/osint-agent-package/README.md +1 -1
  6. package/package.json +14 -3
  7. package/readme-international/README.de.md +18 -8
  8. package/readme-international/README.es.md +19 -9
  9. package/readme-international/README.fr.md +18 -8
  10. package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
  11. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +148 -9
  12. package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
  13. package/src/bmad-plus/data/role-triggers.yaml +19 -0
  14. package/src/bmad-plus/module-help.csv +1 -0
  15. package/src/bmad-plus/module.yaml +1 -0
  16. package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
  17. package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
  19. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
  20. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
  21. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
  22. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
  23. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
  24. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +54 -426
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +24 -80
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +64 -482
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
  77. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
  78. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
  79. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
  80. package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
  81. package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
  82. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
  83. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
  84. package/src/bmad-plus/packs/pack-memory/README.md +35 -4
  85. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +33 -6
  86. package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
  87. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
  88. package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
  89. package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
  90. package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
  91. package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
  92. package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
  93. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +49 -12
  94. package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
  95. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -65
  96. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +144 -0
  97. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
  98. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
  99. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
  100. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +552 -0
  101. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +362 -0
  102. package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
  103. package/tools/build/check-install-contract.js +367 -17
  104. package/tools/build/generate.js +229 -40
  105. package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
  106. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
  107. package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
  108. package/tools/build/generated-adapters/AGENTS.md +1 -1
  109. package/tools/build/generated-adapters/CLAUDE.md +1 -1
  110. package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
  111. package/tools/build/generated-adapters/GEMINI.md +1 -1
  112. package/tools/build/pack-delivery.js +78 -0
  113. package/tools/cli/bmad-plus-cli.js +15 -12
  114. package/tools/cli/commands/doctor.js +50 -189
  115. package/tools/cli/commands/install.js +22 -3
  116. package/tools/cli/commands/memory-journal-cmd.js +119 -19
  117. package/tools/cli/commands/nexus.js +111 -0
  118. package/tools/cli/commands/studio.js +68 -0
  119. package/tools/cli/commands/uat.js +389 -0
  120. package/tools/cli/lib/README-memory-journal.md +19 -8
  121. package/tools/cli/lib/installation-health.js +366 -0
  122. package/tools/cli/lib/memory-journal.js +0 -0
  123. package/tools/cli/lib/memory-outcomes.js +293 -0
  124. package/tools/cli/lib/memory-store.js +139 -0
  125. package/tools/cli/lib/nexus-process.js +377 -0
  126. package/tools/cli/lib/nexus.js +1532 -0
  127. package/tools/cli/lib/pack-copy.js +39 -11
  128. package/tools/cli/lib/packs.js +134 -11
  129. package/tools/cli/lib/python-health.js +233 -0
  130. package/tools/cli/lib/python-provision.js +2 -2
  131. package/tools/cli/lib/studio.js +310 -0
  132. package/tools/cli/lib/uat.js +869 -0
  133. package/tools/maintain/upstream-candidate.js +456 -0
  134. package/tools/release/publication-content.js +903 -0
  135. package/tools/release/supply-chain.js +282 -0
@@ -0,0 +1,366 @@
1
+ /** Read-only by default; optional Python import probes never provision or repair. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const semver = require('semver');
5
+ const { DERIVED } = require('./packs');
6
+ const { readInstallManifest } = require('./install-manifest');
7
+ const { inventoryOf, collectManagedFiles, MANIFEST } = require('./update-transaction');
8
+ const { fileHash } = require('./pack-copy');
9
+ const { contentHash } = require('./installed-adapters');
10
+ const {
11
+ safeTarget,
12
+ GENERATED_MARKER,
13
+ USER_CONFIG_MARKER,
14
+ } = require('../../build/generate-adapters');
15
+
16
+ const PACKAGE_ROOT = path.resolve(__dirname, '../../..');
17
+
18
+ function inspectFile(root, file) {
19
+ try {
20
+ return { state: 'present', bytes: fs.readFileSync(safeTarget(root, file)) };
21
+ } catch (error) {
22
+ return { state: error.code === 'ENOENT' ? 'missing' : 'unreadable' };
23
+ }
24
+ }
25
+
26
+ function collectInstallationHealth({
27
+ projectDir = process.cwd(),
28
+ packageRoot = PACKAGE_ROOT,
29
+ cliVersion = require('../../../package.json').version,
30
+ nodeVersion = process.versions.node,
31
+ verifyPython = false,
32
+ pythonRunner,
33
+ } = {}) {
34
+ projectDir = path.resolve(projectDir);
35
+ const report = {
36
+ schemaVersion: 1,
37
+ projectDir,
38
+ status: 'ok',
39
+ versions: { installed: null, cli: cliVersion, published: null },
40
+ inventory: { status: 'unknown', trackedFiles: 0 },
41
+ packs: [],
42
+ integrations: [],
43
+ processExecution: DERIVED.diagnostics.processExecution
44
+ ? {
45
+ ...DERIVED.diagnostics.processExecution,
46
+ availability: 'not-probed',
47
+ }
48
+ : null,
49
+ runtimes: [],
50
+ checks: [],
51
+ summary: { passed: 0, warnings: 0, errors: 0 },
52
+ };
53
+ function check(code, status, message, extra = {}) {
54
+ report.checks.push({ code, status, message, ...extra });
55
+ }
56
+ function finish() {
57
+ for (const entry of report.checks) {
58
+ report.summary[
59
+ entry.status === 'pass' ? 'passed' : entry.status === 'warning' ? 'warnings' : 'errors'
60
+ ]++;
61
+ }
62
+ report.status = report.summary.errors ? 'error' : report.summary.warnings ? 'warning' : 'ok';
63
+ return report;
64
+ }
65
+
66
+ const manifestFile = inspectFile(projectDir, MANIFEST);
67
+ if (manifestFile.state !== 'present') {
68
+ check(
69
+ manifestFile.state === 'missing' ? 'installation.missing' : 'manifest.unreadable',
70
+ 'error',
71
+ manifestFile.state === 'missing'
72
+ ? 'BMAD+ is not installed in this directory.'
73
+ : 'Installation manifest cannot be read safely.',
74
+ { path: MANIFEST }
75
+ );
76
+ return finish();
77
+ }
78
+ let manifest;
79
+ try {
80
+ manifest = readInstallManifest(safeTarget(projectDir, MANIFEST));
81
+ } catch (error) {
82
+ check('manifest.invalid', 'error', 'Invalid installation manifest: ' + error.message, {
83
+ path: MANIFEST,
84
+ });
85
+ return finish();
86
+ }
87
+ report.versions.installed = manifest.version;
88
+ check('manifest.valid', 'pass', 'Manifest found: v' + manifest.version);
89
+ check(
90
+ 'version.cli',
91
+ manifest.version === cliVersion ? 'pass' : 'warning',
92
+ manifest.version === cliVersion
93
+ ? 'Installed and executing CLI versions match.'
94
+ : 'Version mismatch: installed v' + manifest.version + ', executing CLI v' + cliVersion + '.'
95
+ );
96
+
97
+ const selectedPacks = [...new Set(manifest.packs)];
98
+ const validPacks = selectedPacks.filter((id) => Object.hasOwn(DERIVED.packs, id));
99
+ for (const id of selectedPacks) {
100
+ if (!validPacks.includes(id))
101
+ check('pack.unknown', 'error', 'Unknown installed pack: ' + id, { pack: id });
102
+ }
103
+ if (!selectedPacks.includes('core'))
104
+ check('pack.core-missing', 'error', 'The required Core pack is not selected.');
105
+
106
+ if (selectedPacks.includes('dev-studio')) {
107
+ try {
108
+ const studio = require('./studio');
109
+ const resolved = studio.validatePack(studio.installedPack(projectDir));
110
+ studio.resolveConfig(projectDir);
111
+ check(
112
+ 'studio.resources',
113
+ 'pass',
114
+ `Dev Studio: ${resolved.catalog.workflows.length} routes and ${resolved.resources.length} resources resolved; host execution is not verified.`
115
+ );
116
+ } catch (error) {
117
+ check(
118
+ 'studio.resources',
119
+ 'error',
120
+ 'Dev Studio cannot prepare its workflows: ' + error.message,
121
+ { path: '.agents/skills/pack-dev-studio' }
122
+ );
123
+ }
124
+ }
125
+
126
+ const inventory = inventoryOf(manifest);
127
+ const validInventory =
128
+ inventory &&
129
+ Object.values(inventory).every((hash) => typeof hash === 'string') &&
130
+ typeof manifest.fileInventory.complete === 'boolean';
131
+ report.inventory.status =
132
+ manifest.fileInventory === undefined
133
+ ? 'legacy'
134
+ : !validInventory
135
+ ? 'invalid'
136
+ : manifest.fileInventory.complete
137
+ ? 'complete'
138
+ : 'incomplete';
139
+ const owned = validInventory ? inventory : {};
140
+ report.inventory.trackedFiles = Object.keys(owned).length;
141
+
142
+ const expectedFiles = new Set(['_bmad/module.yaml', '_bmad/module-help.csv']);
143
+ for (const id of validPacks) {
144
+ const pack = DERIVED.packs[id];
145
+ report.packs.push({ id, runtimes: pack.runtimes, requiredResources: pack.requiredResources });
146
+ for (const file of pack.requiredResources) expectedFiles.add(file);
147
+ }
148
+ // Check delivery against the executing package, even if a manifest omits a file.
149
+ try {
150
+ for (const { file } of collectManagedFiles({ projectDir, packageRoot, packs: validPacks }))
151
+ expectedFiles.add(file);
152
+ } catch (error) {
153
+ check('package.unreadable', 'error', 'Cannot inspect the executing package: ' + error.message);
154
+ }
155
+ const resources = new Set([...expectedFiles, ...Object.keys(owned)]);
156
+ for (const file of [...resources].sort()) {
157
+ if (!Object.hasOwn(owned, file) && report.inventory.status === 'complete')
158
+ report.inventory.status = 'incomplete';
159
+ const observed = inspectFile(projectDir, file);
160
+ if (observed.state !== 'present') {
161
+ check(
162
+ 'file.' + observed.state,
163
+ 'error',
164
+ observed.state === 'missing'
165
+ ? 'Missing required file: ' + file
166
+ : 'File cannot be read safely: ' + file,
167
+ { path: file }
168
+ );
169
+ } else if (Object.hasOwn(owned, file)) {
170
+ const unchanged = fileHash(observed.bytes) === owned[file];
171
+ check(
172
+ unchanged ? 'file.unchanged' : 'file.modified',
173
+ unchanged ? 'pass' : 'warning',
174
+ unchanged
175
+ ? 'Managed file unchanged.'
176
+ : 'Managed file modified locally; review before updating.',
177
+ { path: file }
178
+ );
179
+ } else {
180
+ check('file.unowned', 'warning', 'File present without a recorded ownership hash.', {
181
+ path: file,
182
+ });
183
+ }
184
+ }
185
+ if (manifest.fileInventory?.complete && report.inventory.status === 'incomplete') {
186
+ check(
187
+ 'inventory.omissions',
188
+ 'warning',
189
+ 'Inventory claims completeness but omits expected files.'
190
+ );
191
+ }
192
+ check(
193
+ 'inventory.' + report.inventory.status,
194
+ report.inventory.status === 'complete'
195
+ ? 'pass'
196
+ : report.inventory.status === 'invalid'
197
+ ? 'error'
198
+ : 'warning',
199
+ 'File ownership inventory: ' + report.inventory.status + '.'
200
+ );
201
+ const config = inspectFile(projectDir, '_bmad/config.yaml');
202
+ check(
203
+ 'config.' + config.state,
204
+ config.state === 'present' ? 'pass' : 'error',
205
+ config.state === 'present'
206
+ ? 'Project configuration present.'
207
+ : 'Project configuration missing or unreadable.',
208
+ { path: '_bmad/config.yaml' }
209
+ );
210
+
211
+ const contract = DERIVED.diagnostics;
212
+ const declaredTools = Object.keys(contract.integrations);
213
+ let selectedTools = [];
214
+ if (manifest.ides === undefined) {
215
+ selectedTools = declaredTools.filter(
216
+ (tool) =>
217
+ inspectFile(projectDir, contract.integrations[tool].instructionFile).state !== 'missing'
218
+ );
219
+ check(
220
+ 'integrations.legacy',
221
+ 'warning',
222
+ 'IDE selection was not recorded; existing adapter paths were inspected.'
223
+ );
224
+ } else if (
225
+ !Array.isArray(manifest.ides) ||
226
+ manifest.ides.some((id) => typeof id !== 'string' || !declaredTools.includes(id))
227
+ ) {
228
+ check(
229
+ 'integrations.invalid',
230
+ 'error',
231
+ 'Manifest IDE selection contains invalid or unknown targets.'
232
+ );
233
+ } else selectedTools = [...new Set(manifest.ides)];
234
+
235
+ const adapterPaths =
236
+ selectedTools.length || manifest.ides === undefined
237
+ ? [
238
+ ...new Set([
239
+ DERIVED.targets.spine,
240
+ ...selectedTools.map((tool) => contract.integrations[tool].instructionFile),
241
+ ]),
242
+ ]
243
+ : [];
244
+ const ownership = new Map();
245
+ const hashes = manifest.adapterHashes;
246
+ const validHashes =
247
+ hashes === undefined ||
248
+ (hashes &&
249
+ typeof hashes === 'object' &&
250
+ !Array.isArray(hashes) &&
251
+ Object.entries(hashes).every(
252
+ ([file, hash]) =>
253
+ [DERIVED.targets.spine, ...DERIVED.targets.adapters.map((entry) => entry.file)].includes(
254
+ file
255
+ ) &&
256
+ typeof hash === 'string' &&
257
+ /^[a-f0-9]{64}$/.test(hash)
258
+ ));
259
+ if (!validHashes) check('adapters.invalid', 'error', 'Adapter ownership hashes are invalid.');
260
+ for (const file of adapterPaths) {
261
+ const observed = inspectFile(projectDir, file);
262
+ let state = observed.state;
263
+ if (state === 'present') {
264
+ const text = observed.bytes.toString('utf8');
265
+ const previous = validHashes && hashes && Object.hasOwn(hashes, file) ? hashes[file] : null;
266
+ state =
267
+ text.includes(GENERATED_MARKER) && !text.includes(USER_CONFIG_MARKER)
268
+ ? 'adopted'
269
+ : !previous || !text.includes(USER_CONFIG_MARKER)
270
+ ? 'unowned'
271
+ : contentHash(text) === previous
272
+ ? 'unchanged'
273
+ : 'modified';
274
+ }
275
+ ownership.set(file, state);
276
+ check(
277
+ 'adapter.' + state,
278
+ ['missing', 'unreadable'].includes(state)
279
+ ? 'error'
280
+ : state === 'unchanged'
281
+ ? 'pass'
282
+ : 'warning',
283
+ 'Instruction file: ' + state + '.',
284
+ { path: file }
285
+ );
286
+ }
287
+ report.integrations = selectedTools.map((tool) => ({
288
+ tool,
289
+ ...contract.integrations[tool],
290
+ ownership: ownership.get(contract.integrations[tool].instructionFile),
291
+ hostVerified: false,
292
+ }));
293
+
294
+ const nodeMinimum = contract.runtimeMinimums.node;
295
+ const nodeReady = Boolean(semver.valid(nodeVersion) && semver.gte(nodeVersion, nodeMinimum));
296
+ report.runtimes.push({
297
+ runtime: 'node',
298
+ minimum: nodeMinimum,
299
+ version: nodeVersion,
300
+ status: nodeReady ? 'verified' : 'unsupported',
301
+ });
302
+ check(
303
+ 'runtime.node',
304
+ nodeReady ? 'pass' : 'error',
305
+ 'Executing Node.js ' + nodeVersion + '; minimum ' + nodeMinimum + '.'
306
+ );
307
+ for (const id of validPacks.filter((pack) => DERIVED.packs[pack].runtimes.includes('python'))) {
308
+ const requirements = DERIVED.pythonPacks[id].requirements.join('/');
309
+ const delivery = inspectFile(packageRoot, requirements).state;
310
+ const environmentFile = '.bmad/venv/' + id + '/pyvenv.cfg';
311
+ const environment = inspectFile(projectDir, environmentFile).state;
312
+ const runtime = {
313
+ runtime: 'python',
314
+ pack: id,
315
+ minimum: contract.runtimeMinimums.python,
316
+ requirements,
317
+ packageRequirements: delivery,
318
+ environment,
319
+ imports: 'not-verified',
320
+ };
321
+ report.runtimes.push(runtime);
322
+ check(
323
+ 'runtime.requirements',
324
+ delivery === 'present' ? 'pass' : 'error',
325
+ delivery === 'present'
326
+ ? 'Python requirements are available in the executing package.'
327
+ : 'Python requirements are missing or unreadable in the executing package.',
328
+ { pack: id, path: requirements, scope: 'package' }
329
+ );
330
+ if (verifyPython) {
331
+ const { probePythonRuntime } = require('./python-health');
332
+ const probe = probePythonRuntime({
333
+ projectDir,
334
+ packId: id,
335
+ minimum: contract.runtimeMinimums.python,
336
+ verifyModules: DERIVED.pythonPacks[id].verifyModules,
337
+ runner: pythonRunner,
338
+ });
339
+ Object.assign(runtime, probe);
340
+ check(
341
+ 'runtime.python-probe',
342
+ probe.status === 'verified' ? 'pass' : 'warning',
343
+ probe.message +
344
+ (probe.status === 'verified'
345
+ ? ''
346
+ : ' Use bmad-plus install --provision-python to provision selected Python packs.'),
347
+ {
348
+ pack: id,
349
+ path: environmentFile,
350
+ }
351
+ );
352
+ } else {
353
+ check(
354
+ 'runtime.python-unverified',
355
+ 'warning',
356
+ 'Python environment ' +
357
+ environment +
358
+ '; interpreter version and imports have not been executed. Use doctor --verify-python to probe them.',
359
+ { pack: id, path: environmentFile }
360
+ );
361
+ }
362
+ }
363
+ return finish();
364
+ }
365
+
366
+ module.exports = { collectInstallationHealth };
Binary file
@@ -0,0 +1,293 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const nexus = require('./nexus');
5
+ const store = require('./memory-store');
6
+
7
+ const OUTCOMES = '.bmad/memory/outcomes.ndjson';
8
+ const BOOST = 0.25;
9
+ const digest = (value) =>
10
+ crypto
11
+ .createHash('sha256')
12
+ .update(typeof value === 'string' ? value : JSON.stringify(value))
13
+ .digest('hex');
14
+ const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
15
+
16
+ function requiredText(value, label, maximum = 1000) {
17
+ if (typeof value !== 'string' || !value.trim() || value.length > maximum)
18
+ throw new Error(label + ' must be non-empty bounded text.');
19
+ return value;
20
+ }
21
+ function scopePath(value) {
22
+ if (
23
+ typeof value !== 'string' ||
24
+ !value ||
25
+ value.includes('\\') ||
26
+ value.startsWith('/') ||
27
+ value.includes(':') ||
28
+ value.split('/').some((p) => !p || p === '.' || p === '..')
29
+ ) {
30
+ throw new Error('Outcome scope must be a normalized relative path.');
31
+ }
32
+ return value;
33
+ }
34
+ function within(child, parent) {
35
+ return child === parent || child.startsWith(parent + '/');
36
+ }
37
+ function overlaps(a, b) {
38
+ return a.some((left) => b.some((right) => within(left, right) || within(right, left)));
39
+ }
40
+ function scopes(values) {
41
+ if (!Array.isArray(values) || !values.length || values.length > 30)
42
+ throw new Error('An explicit non-empty project scope is required.');
43
+ return [...new Set(values.map(scopePath))].sort();
44
+ }
45
+ function sourceSection(baseDir, input) {
46
+ if (
47
+ !input ||
48
+ !/^(?:\.agents|\.bmad)\/memory\/(?:decisions|lessons|patterns)\.md$/.test(input.file)
49
+ )
50
+ throw new Error(
51
+ 'Outcome memory source must be a project decisions, lessons or patterns Markdown file.'
52
+ );
53
+ const heading = requiredText(input.heading, 'Memory heading', 300);
54
+ const sections = require('./memory-journal')._internal.splitSections(
55
+ store.readText(baseDir, input.file),
56
+ input.file
57
+ );
58
+ const matches = sections.filter((section) => section.heading === heading);
59
+ if (matches.length !== 1) throw new Error('Memory heading must identify exactly one section.');
60
+ const text = (matches[0].heading + '\n' + matches[0].text).trim();
61
+ return { file: input.file, heading, textSha256: digest(text) };
62
+ }
63
+
64
+ /** Read-only adapter: Nexus owns the actual verification and freshness policy. */
65
+ function acceptedSource(baseDir, runId, taskId) {
66
+ const run = nexus.inspectRun(baseDir, runId);
67
+ if (run.lock || run.recoveryLock)
68
+ throw new Error(
69
+ 'Nexus state is being written or recovered; retry observation after its owner finishes.'
70
+ );
71
+ const task = run.tasks.find((item) => item.id === taskId);
72
+ const observation = run.observations.find((item) => item.taskId === taskId);
73
+ const attempt = task?.attempts.at(-1);
74
+ if (
75
+ !task ||
76
+ task.integration !== 'accepted' ||
77
+ !observation?.evidenceEligible ||
78
+ observation.stale ||
79
+ attempt?.execution !== 'completed' ||
80
+ attempt.verification.status !== 'passed'
81
+ ) {
82
+ throw new Error(
83
+ 'Outcome source is not an accepted task with current independently verified evidence.'
84
+ );
85
+ }
86
+ if (
87
+ task.integrationReceipt?.attemptId !== attempt.id ||
88
+ !equal(task.integrationReceipt.artifactHashes, attempt.verification.artifactHashes)
89
+ ) {
90
+ throw new Error('Nexus acceptance does not bind the current attempt and artifacts.');
91
+ }
92
+ const definition = run.plan.tasks.find((item) => item.id === taskId);
93
+ return {
94
+ kind: 'nexus',
95
+ runId,
96
+ taskId,
97
+ attemptId: attempt.id,
98
+ verificationId: attempt.verification.id,
99
+ verificationSha256: digest(attempt.verification),
100
+ acceptedAt: task.acceptedAt,
101
+ artifactHashes: task.integrationReceipt.artifactHashes,
102
+ resourceHashes: definition.resourceHashes,
103
+ scope: definition.scope,
104
+ objective: definition.objective,
105
+ baseline: attempt.baseline || null,
106
+ };
107
+ }
108
+
109
+ function readOutcomes(baseDir) {
110
+ const text = store.readText(baseDir, OUTCOMES, { missing: true });
111
+ if (text === null) return [];
112
+ if (text && !text.endsWith('\n'))
113
+ throw new Error('Outcome store has an incomplete final record.');
114
+ const records = text
115
+ .split('\n')
116
+ .filter(Boolean)
117
+ .map((line) => JSON.parse(line));
118
+ if (records.length > 1000) throw new Error('Outcome store exceeds 1000 records.');
119
+ const seen = new Set();
120
+ const attempts = new Set();
121
+ for (const record of records) {
122
+ const { id, ...body } = record;
123
+ if (
124
+ body.schemaVersion !== 1 ||
125
+ body.kind !== 'accepted-outcome' ||
126
+ id !== digest(body) ||
127
+ seen.has(id)
128
+ )
129
+ throw new Error('Outcome receipt integrity or uniqueness check failed.');
130
+ if (
131
+ !Array.isArray(record.supersedes) ||
132
+ !Array.isArray(record.contradicts) ||
133
+ [...record.supersedes, ...record.contradicts].some((ref) => !seen.has(ref))
134
+ )
135
+ throw new Error('Outcome relationships must identify earlier receipts.');
136
+ const attemptKey = [record.source?.runId, record.source?.taskId, record.source?.attemptId].join(
137
+ '\0'
138
+ );
139
+ if (!record.source?.attemptId || attempts.has(attemptKey))
140
+ throw new Error('Duplicate or missing outcome attempt identity.');
141
+ attempts.add(attemptKey);
142
+ seen.add(id);
143
+ }
144
+ return records;
145
+ }
146
+
147
+ function observeOutcome(baseDir, input, { now } = {}) {
148
+ if (!input || typeof input !== 'object') throw new Error('Outcome input is required.');
149
+ if (typeof now !== 'string' || !Number.isFinite(Date.parse(now)))
150
+ throw new Error('Outcome timestamp must be injected as an ISO string.');
151
+ return store.withMemoryLock(baseDir, () => {
152
+ const records = readOutcomes(baseDir);
153
+ const source = acceptedSource(
154
+ baseDir,
155
+ requiredText(input.runId, 'Run ID'),
156
+ requiredText(input.taskId, 'Task ID')
157
+ );
158
+ if (
159
+ records.some(
160
+ (record) =>
161
+ record.source.runId === source.runId &&
162
+ record.source.taskId === source.taskId &&
163
+ record.source.attemptId === source.attemptId
164
+ )
165
+ )
166
+ throw new Error('Duplicate outcome: this task attempt already supplied a receipt.');
167
+ const memory = sourceSection(baseDir, input.memory);
168
+ const scope = scopes(input.scope);
169
+ if (scope.some((item) => !source.scope.some((parent) => within(item, parent))))
170
+ throw new Error('Outcome scope exceeds the accepted task scope.');
171
+ const relationships = {};
172
+ for (const key of ['supersedes', 'contradicts']) {
173
+ const values = input[key] || [];
174
+ if (!Array.isArray(values) || values.length > 30 || new Set(values).size !== values.length)
175
+ throw new Error('Outcome relationships must be a unique bounded receipt-ID list.');
176
+ for (const id of values) {
177
+ const prior = records.find((record) => record.id === id);
178
+ if (!prior || !overlaps(scope, prior.scope))
179
+ throw new Error('Related outcome must exist in the same applicable project scope.');
180
+ }
181
+ relationships[key] = [...values].sort();
182
+ }
183
+ if (relationships.supersedes.some((id) => relationships.contradicts.includes(id)))
184
+ throw new Error('A receipt cannot be both superseded and contradicted.');
185
+ const body = {
186
+ schemaVersion: 1,
187
+ kind: 'accepted-outcome',
188
+ ts: now,
189
+ memory,
190
+ scope,
191
+ source,
192
+ interpretation: requiredText(input.interpretation, 'Lesson interpretation', 2000),
193
+ verification: { status: 'passed', acceptance: 'accepted', lessonCausality: 'unmeasured' },
194
+ ...relationships,
195
+ };
196
+ // Re-read after loading relationships/Markdown; do not persist a torn source observation.
197
+ if (
198
+ !equal(source, acceptedSource(baseDir, source.runId, source.taskId)) ||
199
+ !equal(memory, sourceSection(baseDir, input.memory))
200
+ )
201
+ throw new Error('Outcome sources changed while being observed.');
202
+ if (records.length >= 1000) throw new Error('Outcome store is full; archive it explicitly.');
203
+ return store.appendRecord(baseDir, OUTCOMES, { id: digest(body), ...body });
204
+ });
205
+ }
206
+
207
+ function inspectOutcomes(baseDir, { contextScope } = {}) {
208
+ const selectedScope = contextScope === undefined ? null : scopes(contextScope);
209
+ const records = readOutcomes(baseDir);
210
+ const superseded = new Set(records.flatMap((record) => record.supersedes));
211
+ const contradictory = new Set();
212
+ for (const record of records.filter((record) => !superseded.has(record.id))) {
213
+ for (const other of record.contradicts) {
214
+ if (!superseded.has(other)) {
215
+ contradictory.add(record.id);
216
+ contradictory.add(other);
217
+ }
218
+ }
219
+ }
220
+ const conflictingAnchors = new Set(
221
+ records
222
+ .filter((record) => contradictory.has(record.id))
223
+ .map((record) => record.memory.file + '\0' + record.memory.heading)
224
+ );
225
+ return records.map((record) => {
226
+ let reason = null;
227
+ if (superseded.has(record.id)) reason = 'superseded';
228
+ else if (conflictingAnchors.has(record.memory.file + '\0' + record.memory.heading))
229
+ reason = 'contradictory';
230
+ else if (selectedScope && !overlaps(record.scope, selectedScope))
231
+ reason = 'outside-query-scope';
232
+ else {
233
+ try {
234
+ if (!equal(sourceSection(baseDir, record.memory), record.memory))
235
+ reason = 'stale-memory-section';
236
+ else if (
237
+ !equal(acceptedSource(baseDir, record.source.runId, record.source.taskId), record.source)
238
+ )
239
+ reason = 'stale-outcome-source';
240
+ } catch (error) {
241
+ reason = 'unavailable-or-ineligible-source: ' + error.message;
242
+ }
243
+ }
244
+ return { ...record, eligible: reason === null, reason };
245
+ });
246
+ }
247
+
248
+ function rankWithEvidence(baseDir, candidates, { contextScope } = {}) {
249
+ const status = inspectOutcomes(baseDir, { contextScope: scopes(contextScope) });
250
+ const key = (value) => value.file + '\0' + value.heading;
251
+ const results = [];
252
+ for (const entry of candidates) {
253
+ const matching =
254
+ entry.kind === 'note'
255
+ ? status.filter(
256
+ (record) => key(record.memory) === key({ file: entry.sourceFile, heading: entry.ref })
257
+ )
258
+ : [];
259
+ if (!matching.length) {
260
+ results.push({ ...entry, evidence: { status: 'unverified', receiptIds: [], boost: 0 } });
261
+ continue;
262
+ }
263
+ // A stale, contradicted or superseded anchor must not silently fall back to an authoritative note.
264
+ const eligible = matching.filter(
265
+ (record) => record.eligible && record.memory.textSha256 === digest(entry.text)
266
+ );
267
+ if (!eligible.length) continue;
268
+ const reward = require('./memory-journal').computeReward({ acceptance: true });
269
+ results.push({
270
+ ...entry,
271
+ lexicalScore: entry.score,
272
+ score: entry.score * (1 + BOOST * reward),
273
+ evidence: {
274
+ status: 'accepted-current',
275
+ receiptIds: eligible.map((record) => record.id),
276
+ reward,
277
+ boost: BOOST,
278
+ lessonCausality: 'unmeasured',
279
+ },
280
+ });
281
+ }
282
+ return results;
283
+ }
284
+
285
+ module.exports = {
286
+ observeOutcome,
287
+ inspectOutcomes,
288
+ readOutcomes,
289
+ rankWithEvidence,
290
+ acceptedSource,
291
+ OUTCOMES,
292
+ BOOST,
293
+ };