bmad-plus 0.14.0 → 0.17.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 (66) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +55 -18
  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 +11 -3
  7. package/readme-international/README.de.md +20 -9
  8. package/readme-international/README.es.md +21 -10
  9. package/readme-international/README.fr.md +20 -9
  10. package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
  11. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +147 -8
  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/categories/implementation/create-story.md +3 -1
  17. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +2 -0
  18. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +5 -1
  19. package/src/bmad-plus/packs/pack-memory/README.md +29 -4
  20. package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +21 -1
  21. package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
  22. package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
  23. package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
  24. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +47 -10
  25. package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
  26. package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -67
  27. package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +158 -0
  28. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
  29. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
  30. package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
  31. package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +628 -0
  32. package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +442 -0
  33. package/src/bmad-plus/skills/bmad-plus-uat/templates/README.md +63 -0
  34. package/src/bmad-plus/skills/bmad-plus-uat/templates/example-uat-spec.json +95 -0
  35. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-release-gate.mjs +127 -0
  36. package/src/bmad-plus/skills/bmad-plus-uat/templates/uat-spec-present.mjs +77 -0
  37. package/tools/build/check-install-contract.js +202 -4
  38. package/tools/build/generate.js +16 -0
  39. package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
  40. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
  41. package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
  42. package/tools/build/generated-adapters/AGENTS.md +1 -1
  43. package/tools/build/generated-adapters/CLAUDE.md +1 -1
  44. package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
  45. package/tools/build/generated-adapters/GEMINI.md +1 -1
  46. package/tools/cli/bmad-plus-cli.js +15 -12
  47. package/tools/cli/commands/autoconfig.js +4 -2
  48. package/tools/cli/commands/doctor.js +1 -0
  49. package/tools/cli/commands/install.js +21 -2
  50. package/tools/cli/commands/memory-journal-cmd.js +119 -19
  51. package/tools/cli/commands/nexus.js +111 -0
  52. package/tools/cli/commands/uat.js +405 -0
  53. package/tools/cli/i18n.js +10 -0
  54. package/tools/cli/lib/README-memory-journal.md +19 -8
  55. package/tools/cli/lib/installation-health.js +6 -0
  56. package/tools/cli/lib/memory-journal.js +0 -0
  57. package/tools/cli/lib/memory-outcomes.js +293 -0
  58. package/tools/cli/lib/memory-store.js +139 -0
  59. package/tools/cli/lib/nexus-process.js +377 -0
  60. package/tools/cli/lib/nexus.js +1532 -0
  61. package/tools/cli/lib/pack-copy.js +39 -11
  62. package/tools/cli/lib/packs.js +17 -3
  63. package/tools/cli/lib/uat.js +887 -0
  64. package/tools/maintain/upstream-candidate.js +456 -0
  65. package/tools/release/publication-content.js +4 -1
  66. package/tools/release/supply-chain.js +282 -0
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gate G3 — production waits for a human run that was read and classified.
4
+ *
5
+ * Resolves the recipe covering the version in package.json, then runs
6
+ * `bmad-plus uat gate` on it. Exit 0 only when the gate passes: a finished run,
7
+ * answering the current revision of the recipe, with every failure classified and
8
+ * every writing step confirmed read-only.
9
+ *
10
+ * node scripts/uat-release-gate.mjs
11
+ * node scripts/uat-release-gate.mjs --id myproduct-1.4.0
12
+ * node scripts/uat-release-gate.mjs --cli node_modules/bmad-plus/tools/cli/bmad-plus-cli.js
13
+ * BMAD_PLUS_CLI=/path/to/bmad-plus-cli.js node scripts/uat-release-gate.mjs
14
+ *
15
+ * Exit codes mirror the command: 0 passed · 1 failed · 2 awaiting a run ·
16
+ * 3 the run answered another revision · 4 the gate could not be run at all.
17
+ *
18
+ * The gate establishes that the run is complete, current and classified. It never
19
+ * establishes that the tester looked at the right place: that stays human-observed.
20
+ */
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { spawnSync } from 'node:child_process';
24
+
25
+ const args = process.argv.slice(2);
26
+ const option = (name, fallback) => {
27
+ const index = args.indexOf(`--${name}`);
28
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
29
+ };
30
+ const root = path.resolve(option('project', process.cwd()));
31
+ const directory = option('dir', '_bmad-output/uat');
32
+ const specs = path.join(root, directory, 'specs');
33
+
34
+ function resolveId() {
35
+ const explicit = option('id', null);
36
+ if (explicit) return explicit;
37
+ const file = path.join(root, 'package.json');
38
+ if (!fs.existsSync(file)) throw new Error(`no package.json in ${root}; pass --id <recipe>`);
39
+ const version = JSON.parse(fs.readFileSync(file, 'utf8')).version;
40
+ if (!fs.existsSync(specs)) throw new Error(`no recipe folder at ${specs}`);
41
+ const covering = fs
42
+ .readdirSync(specs)
43
+ .filter((name) => name.endsWith('.json'))
44
+ .filter((name) => {
45
+ try {
46
+ const spec = JSON.parse(fs.readFileSync(path.join(specs, name), 'utf8'));
47
+ return Array.isArray(spec.versions) && spec.versions.includes(version);
48
+ } catch {
49
+ return false;
50
+ }
51
+ });
52
+ if (covering.length !== 1) {
53
+ throw new Error(
54
+ covering.length
55
+ ? `${covering.length} recipes name ${version}; pass --id to choose one`
56
+ : `no recipe names ${version}; write it before deploying`
57
+ );
58
+ }
59
+ return covering[0].replace(/\.json$/, '');
60
+ }
61
+
62
+ /**
63
+ * The CLI's own entry point, run by this Node: an explicit one, or the copy
64
+ * installed in the project. Never a shell, never `npx` — a `.cmd` shim cannot be
65
+ * spawned without a shell on Windows, and a gate must not depend on a download.
66
+ */
67
+ function command() {
68
+ const candidates = [
69
+ option('cli', null),
70
+ process.env.BMAD_PLUS_CLI,
71
+ path.join(root, 'node_modules', 'bmad-plus', 'tools', 'cli', 'bmad-plus-cli.js'),
72
+ ].filter(Boolean);
73
+ for (const candidate of candidates) {
74
+ const resolved = path.resolve(root, candidate);
75
+ if (fs.existsSync(resolved)) return resolved;
76
+ }
77
+ throw new Error(
78
+ 'no BMAD+ CLI to run the gate with. Install bmad-plus in this project ' +
79
+ '(npm install --save-dev bmad-plus) or set BMAD_PLUS_CLI to its bmad-plus-cli.js. ' +
80
+ 'A missing gate is not a passing gate.'
81
+ );
82
+ }
83
+
84
+ try {
85
+ const id = resolveId();
86
+ const cli = command();
87
+ const child = spawnSync(
88
+ process.execPath,
89
+ [cli, 'uat', 'gate', id, '--directory', root, '--dir', directory, '--json'],
90
+ { encoding: 'utf8', timeout: 120000, windowsHide: true }
91
+ );
92
+
93
+ // The verdict is read from the gate's own answer, not guessed from an exit code:
94
+ // a CLI that never ran also exits non-zero, and that is a different problem.
95
+ let verdict = null;
96
+ try {
97
+ const parsed = JSON.parse(child.stdout);
98
+ if (parsed && typeof parsed.status === 'string') verdict = parsed;
99
+ } catch {
100
+ verdict = null;
101
+ }
102
+ if (!verdict) {
103
+ console.error(
104
+ `uat: the gate could not be run for ${id}` +
105
+ `${child.error ? ` (${child.error.message})` : ''}.\n` +
106
+ `${(child.stdout || '').trim()}${(child.stderr || '').trim()}\n` +
107
+ 'Install the CLI in this project, or set BMAD_PLUS_CLI to its path. ' +
108
+ 'A missing gate is not a passing gate.'
109
+ );
110
+ process.exit(4);
111
+ }
112
+
113
+ const explanation = {
114
+ passed: `${id}: human-observed, complete, current and triaged.`,
115
+ failed: `${id} did not pass its gate — production stays where it is.`,
116
+ awaiting: `${id} has no finished run yet — someone still has to play it.`,
117
+ stale: `${id} was answered on another revision of the recipe — replay the affected steps.`,
118
+ };
119
+ const code = { passed: 0, failed: 1, awaiting: 2, stale: 3 }[verdict.status] ?? 4;
120
+ const report = code === 0 ? console.log : console.error;
121
+ report(explanation[verdict.status] || `${id}: unexpected gate status "${verdict.status}".`);
122
+ for (const reason of verdict.reasons || []) report(` - ${reason}`);
123
+ process.exit(code);
124
+ } catch (error) {
125
+ console.error(`uat: ${error.message}`);
126
+ process.exit(4);
127
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gate G1 — a version does not leave without saying what a person must check.
4
+ *
5
+ * Fails when no acceptance recipe covers the version in package.json. Copy this
6
+ * file into your project and run it wherever your other checks run: a pre-push
7
+ * hook, a test script, a CI step. It needs nothing but Node.
8
+ *
9
+ * node scripts/uat-spec-present.mjs
10
+ * node scripts/uat-spec-present.mjs --version 1.4.0 --dir _bmad-output/uat --json
11
+ *
12
+ * Exit 0 when a recipe covers the version, 1 otherwise. A reminder printed after
13
+ * a deployment protects nothing; this one runs before.
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ const args = process.argv.slice(2);
19
+ const option = (name, fallback) => {
20
+ const index = args.indexOf(`--${name}`);
21
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
22
+ };
23
+ const json = args.includes('--json');
24
+ const root = path.resolve(option('project', process.cwd()));
25
+ const directory = path.resolve(root, option('dir', '_bmad-output/uat'));
26
+
27
+ function projectVersion() {
28
+ const explicit = option('version', null);
29
+ if (explicit) return explicit;
30
+ const file = path.join(root, 'package.json');
31
+ if (!fs.existsSync(file)) {
32
+ throw new Error(`no package.json in ${root}; pass --version <x.y.z>`);
33
+ }
34
+ const version = JSON.parse(fs.readFileSync(file, 'utf8')).version;
35
+ if (!version) throw new Error('package.json declares no version');
36
+ return version;
37
+ }
38
+
39
+ /** Recipes that name this version, whether they cover one version or a campaign. */
40
+ function covering(version) {
41
+ const specs = path.join(directory, 'specs');
42
+ if (!fs.existsSync(specs)) return [];
43
+ return fs
44
+ .readdirSync(specs)
45
+ .filter((name) => name.endsWith('.json'))
46
+ .filter((name) => {
47
+ try {
48
+ const spec = JSON.parse(fs.readFileSync(path.join(specs, name), 'utf8'));
49
+ // A grouped recipe covers a version because it NAMES it, never because its
50
+ // file name suggests a range.
51
+ return Array.isArray(spec.versions) && spec.versions.includes(version);
52
+ } catch {
53
+ return false;
54
+ }
55
+ })
56
+ .map((name) => name.replace(/\.json$/, ''));
57
+ }
58
+
59
+ try {
60
+ const version = projectVersion();
61
+ const found = covering(version);
62
+ if (json) {
63
+ console.log(JSON.stringify({ version, directory, covering: found, ok: found.length > 0 }, null, 2));
64
+ } else if (found.length) {
65
+ console.log(`uat: ${version} is covered by ${found.join(', ')}`);
66
+ } else {
67
+ console.error(
68
+ `uat: no acceptance recipe covers ${version}.\n` +
69
+ `Write ${path.relative(root, path.join(directory, 'specs'))}/<product>-${version}.json before shipping,\n` +
70
+ `or add ${version} to the versions of a grouped recipe. Ask your agent: "write the recette for this delivery".`
71
+ );
72
+ }
73
+ process.exit(found.length ? 0 : 1);
74
+ } catch (error) {
75
+ console.error(`uat: ${error.message}`);
76
+ process.exit(1);
77
+ }
@@ -238,24 +238,221 @@ function packAndExtract(tempRoot, env) {
238
238
  ],
239
239
  { cwd: REPO_ROOT, env, timeout: 120000 }
240
240
  );
241
- const packed = JSON.parse(output);
242
- assert(Array.isArray(packed) && packed.length === 1, 'npm pack must produce exactly one package');
241
+ // npm 11 reports a list, npm 12 an object keyed by package name.
242
+ const reported = JSON.parse(output);
243
+ const packed = Array.isArray(reported) ? reported : Object.values(reported);
244
+ assert(
245
+ packed.length === 1 && typeof packed[0]?.filename === 'string',
246
+ 'npm pack must produce exactly one package'
247
+ );
243
248
  const archive = resolveWithin(destination, packed[0].filename);
244
- const entries = run('tar', ['-tzf', archive], { cwd: destination, env }).trim().split(/\r?\n/);
249
+ // The archive already sits in cwd, and it is named relatively on purpose: GNU tar
250
+ // reads a Windows "C:\..." argument as a remote host and refuses to open it.
251
+ const archiveName = path.basename(archive);
252
+ const entries = run('tar', ['-tzf', archiveName], { cwd: destination, env })
253
+ .trim()
254
+ .split(/\r?\n/);
245
255
  for (const entry of entries) {
246
256
  assert(entry.startsWith('package/'), `Unexpected npm archive entry: ${entry}`);
247
257
  resolveWithin(destination, entry);
248
258
  }
249
- run('tar', ['-xzf', archive, '-C', destination], { cwd: destination, env });
259
+ run('tar', ['-xzf', archiveName, '-C', '.'], { cwd: destination, env });
250
260
  const packageRoot = path.join(destination, 'package');
251
261
  assert(
252
262
  !fs.existsSync(path.join(packageRoot, 'registry.yaml')),
253
263
  'npm contract must work without registry.yaml'
254
264
  );
255
265
  assert(fs.statSync(packageRoot).isDirectory(), 'npm package directory is missing');
266
+ for (const file of ['LICENSE', 'THIRD-PARTY-LICENSES.md', 'SECURITY.md']) {
267
+ assert(fs.statSync(path.join(packageRoot, file)).isFile(), `npm archive is missing ${file}`);
268
+ }
269
+ assert(
270
+ !fs.existsSync(path.join(packageRoot, 'src/bmad-plus/packs/pack-seo/scripts/__pycache__')),
271
+ 'npm archive must exclude local Python caches'
272
+ );
256
273
  return packageRoot;
257
274
  }
258
275
 
276
+ function verifyNexusConsumer(packageRoot, projectDir, env) {
277
+ const invoke = (args) =>
278
+ JSON.parse(
279
+ run(
280
+ process.execPath,
281
+ [
282
+ path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
283
+ 'nexus',
284
+ ...args,
285
+ '--directory',
286
+ projectDir,
287
+ '--json',
288
+ ],
289
+ { cwd: projectDir, env }
290
+ )
291
+ );
292
+ const input = (name, value) => {
293
+ const file = path.join(projectDir, name);
294
+ fs.writeFileSync(file, JSON.stringify(value));
295
+ return file;
296
+ };
297
+ const instruction = '.agents/skills/agent-orchestrator/SKILL.md';
298
+ assert(
299
+ fs
300
+ .readFileSync(path.join(projectDir, instruction), 'utf8')
301
+ .includes('Durable host-managed attempts')
302
+ );
303
+ fs.mkdirSync(path.join(projectDir, 'checks'));
304
+ fs.mkdirSync(path.join(projectDir, 'result'));
305
+ fs.writeFileSync(
306
+ path.join(projectDir, 'checks/accepted.cjs'),
307
+ "const fs=require('node:fs');const assert=require('node:assert/strict');assert.equal(fs.readFileSync('result/value.txt','utf8'),'accepted');\n"
308
+ );
309
+ const plan = input('nexus-plan.json', {
310
+ id: 'consumer',
311
+ tasks: [
312
+ {
313
+ id: 'work',
314
+ objective: 'Verify a real relocated npm/source consumer',
315
+ scope: ['result'],
316
+ artifacts: ['result/value.txt'],
317
+ resources: [instruction, 'checks/accepted.cjs'],
318
+ checks: [{ id: 'actual-output', command: 'node', args: ['checks/accepted.cjs'] }],
319
+ },
320
+ ],
321
+ });
322
+ invoke(['create', '--plan', plan]);
323
+ const backend = { kind: 'host', id: 'consumer-check', sessionId: 'observed-attempt' };
324
+ const started = invoke([
325
+ 'start',
326
+ 'consumer',
327
+ 'work',
328
+ '--input',
329
+ input('nexus-host.json', { backend }),
330
+ ]);
331
+ const attemptId = started.tasks[0].attempts[0].id;
332
+ fs.writeFileSync(path.join(projectDir, 'result/value.txt'), 'accepted');
333
+ // A separate CLI process reconciles an interrupted host without repeating its work.
334
+ invoke([
335
+ 'reconcile',
336
+ 'consumer',
337
+ 'work',
338
+ '--input',
339
+ input('nexus-observation.json', {
340
+ attemptId,
341
+ backend,
342
+ ownerStopped: true,
343
+ outcome: 'completed',
344
+ summary: 'Test host stopped after writing its artifact; independent checker follows.',
345
+ }),
346
+ ]);
347
+ const checked = invoke(['verify', 'consumer', 'work']);
348
+ assert.equal(checked.tasks[0].attempts[0].verification.status, 'passed');
349
+ const accepted = invoke(['accept', 'consumer', 'work']);
350
+ assert.equal(accepted.tasks[0].integration, 'accepted');
351
+ assert.equal(accepted.tasks[0].integrationReceipt.gitOperation, null);
352
+ const observed = invoke(['inspect', 'consumer']);
353
+ assert.equal(observed.observations[0].evidenceEligible, true);
354
+ assert.equal(observed.observations[0].stale, false);
355
+
356
+ fs.mkdirSync(path.join(projectDir, 'processed'));
357
+ fs.writeFileSync(path.join(projectDir, 'checks/task.txt'), 'Write the supervised result.\n');
358
+ fs.writeFileSync(
359
+ path.join(projectDir, 'checks/worker.cjs'),
360
+ "require('node:fs').writeFileSync('processed/value.txt','supervised');\n"
361
+ );
362
+ fs.writeFileSync(
363
+ path.join(projectDir, 'checks/process.cjs'),
364
+ "require('node:assert/strict').equal(require('node:fs').readFileSync('processed/value.txt','utf8'),'supervised');\n"
365
+ );
366
+ invoke([
367
+ 'create',
368
+ '--plan',
369
+ input('process-plan.json', {
370
+ id: 'process-consumer',
371
+ tasks: [
372
+ {
373
+ id: 'work',
374
+ objective: 'Verify the packaged foreground process backend',
375
+ scope: ['processed'],
376
+ artifacts: ['processed/value.txt'],
377
+ resources: [instruction, 'checks/process.cjs'],
378
+ checks: [{ id: 'process-output', command: 'node', args: ['checks/process.cjs'] }],
379
+ execution: {
380
+ adapter: 'command',
381
+ command: 'node',
382
+ args: ['checks/worker.cjs'],
383
+ resources: ['checks/worker.cjs'],
384
+ input: 'checks/task.txt',
385
+ },
386
+ },
387
+ ],
388
+ }),
389
+ ]);
390
+ const launched = invoke(['launch', 'process-consumer', 'work']);
391
+ const processAttempt = launched.tasks[0].attempts[0];
392
+ assert.equal(processAttempt.process.receipt.outcome, 'completed');
393
+ invoke([
394
+ 'collect',
395
+ 'process-consumer',
396
+ 'work',
397
+ '--input',
398
+ input('collect.json', {
399
+ attemptId: processAttempt.id,
400
+ backend: processAttempt.backend,
401
+ }),
402
+ ]);
403
+ assert.equal(
404
+ invoke(['verify', 'process-consumer', 'work']).tasks[0].attempts[0].verification.status,
405
+ 'passed'
406
+ );
407
+ assert.equal(invoke(['accept', 'process-consumer', 'work']).tasks[0].integration, 'accepted');
408
+
409
+ const mem = (args) =>
410
+ JSON.parse(
411
+ run(
412
+ process.execPath,
413
+ [
414
+ path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
415
+ 'mem',
416
+ ...args,
417
+ '--directory',
418
+ projectDir,
419
+ '--json',
420
+ ],
421
+ { cwd: projectDir, env }
422
+ )
423
+ );
424
+ const memoryFile = '.agents/memory/patterns.md';
425
+ fs.mkdirSync(path.dirname(path.join(projectDir, memoryFile)), { recursive: true });
426
+ fs.appendFileSync(
427
+ path.join(projectDir, memoryFile),
428
+ '\n### Consumer supervision evidence\nVerify the supervised artifact before accepting process work.\n'
429
+ );
430
+ input('memory-observation.json', {
431
+ runId: 'process-consumer',
432
+ taskId: 'work',
433
+ memory: { file: memoryFile, heading: 'Consumer supervision evidence' },
434
+ scope: ['processed'],
435
+ interpretation:
436
+ 'Consumer fixture links this note to the accepted result; causality is unmeasured.',
437
+ });
438
+ mem(['observe', '--input', 'memory-observation.json']);
439
+ const recall = () =>
440
+ mem([
441
+ 'recall',
442
+ 'Consumer supervision evidence',
443
+ '--ranking',
444
+ 'evidence',
445
+ '--context-scope',
446
+ 'processed',
447
+ ]);
448
+ assert(recall().results.some((entry) => entry.ref === 'Consumer supervision evidence'));
449
+ fs.writeFileSync(path.join(projectDir, 'processed/value.txt'), 'changed after acceptance');
450
+ assert(
451
+ !recall().results.some((entry) => entry.ref === 'Consumer supervision evidence'),
452
+ 'Packaged memory must reject evidence after its accepted artifact changes'
453
+ );
454
+ }
455
+
259
456
  function runMatrix(packageRoot, variant, tempRoot, env, reference) {
260
457
  const allPacks = Object.keys(reference.packDefinitions).filter(
261
458
  (id) => !reference.packDefinitions[id].disabled
@@ -313,6 +510,7 @@ function runMatrix(packageRoot, variant, tempRoot, env, reference) {
313
510
  derived: reference.derived,
314
511
  variant,
315
512
  });
513
+ if (installations === 1) verifyNexusConsumer(packageRoot, projectDir, env);
316
514
  if (selection.packs.includes('dev-studio')) {
317
515
  const list = JSON.parse(
318
516
  run(
@@ -189,6 +189,21 @@ function validateRegistry(registry) {
189
189
  'registry.yaml: targets.integration must declare the delivered instruction integration'
190
190
  );
191
191
  }
192
+ const processBackend = registry.targets?.optional_process_backend;
193
+ if (
194
+ processBackend &&
195
+ (!isDeepStrictEqual(processBackend.adapters, ['command', 'codex-exec']) ||
196
+ processBackend.supervisor !== 'foreground' ||
197
+ processBackend.launch !== 'explicit-plan' ||
198
+ processBackend.collect !== 'exact-attempt-receipt' ||
199
+ processBackend.cancellation !== 'original-owner-direct-child' ||
200
+ processBackend.verification !== 'independent-required' ||
201
+ processBackend.scheduling !== 'none')
202
+ ) {
203
+ throw new Error(
204
+ 'registry.yaml: optional process backend differs from the delivered Nexus contract'
205
+ );
206
+ }
192
207
  for (const [runtime, config] of Object.entries(registry.runtimes || {})) {
193
208
  if (
194
209
  typeof config?.min_version !== 'string' ||
@@ -359,6 +374,7 @@ function buildDerived(registry, { sourceRoot = path.join(REPO_ROOT, 'src', 'bmad
359
374
  pythonPacks,
360
375
  diagnostics: {
361
376
  schemaVersion: 1,
377
+ processExecution: registry.targets.optional_process_backend || null,
362
378
  runtimeMinimums: Object.fromEntries(
363
379
  Object.entries(registry.runtimes).map(([id, runtime]) => [id, runtime.min_version])
364
380
  ),
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
113
113
 
114
114
  ## Registry facts (computed from registry.yaml — never hand-typed)
115
115
 
116
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
116
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
117
117
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
118
118
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
119
119
  - Installer agents (14 across all packs):
@@ -118,7 +118,7 @@ After a successful update, reread the project spine and relevant installed skill
118
118
 
119
119
  ## Registry facts (computed from registry.yaml — never hand-typed)
120
120
 
121
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
121
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
122
122
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
123
123
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
124
124
  - Installer agents (14 across all packs):
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
113
113
 
114
114
  ## Registry facts (computed from registry.yaml — never hand-typed)
115
115
 
116
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
116
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
117
117
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
118
118
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
119
119
  - Installer agents (14 across all packs):
@@ -111,7 +111,7 @@ After a successful update, reread the project spine and relevant installed skill
111
111
 
112
112
  ## Registry facts (computed from registry.yaml — never hand-typed)
113
113
 
114
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
114
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
115
115
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
116
116
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
117
117
  - Installer agents (14 across all packs):
@@ -114,7 +114,7 @@ After a successful update, reread the project spine and relevant installed skill
114
114
 
115
115
  ## Registry facts (computed from registry.yaml — never hand-typed)
116
116
 
117
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
117
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
118
118
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
119
119
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
120
120
  - Installer agents (14 across all packs):
@@ -113,7 +113,7 @@ After a successful update, reread the project spine and relevant installed skill
113
113
 
114
114
  ## Registry facts (computed from registry.yaml — never hand-typed)
115
115
 
116
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
116
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
117
117
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
118
118
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
119
119
  - Installer agents (14 across all packs):
@@ -118,7 +118,7 @@ After a successful update, reread the project spine and relevant installed skill
118
118
 
119
119
  ## Registry facts (computed from registry.yaml — never hand-typed)
120
120
 
121
- - Product: BMAD+ v0.14.0 (derived from BMAD-METHOD v6.6.0)
121
+ - Product: BMAD+ v0.17.0 (derived from BMAD-METHOD v6.6.0)
122
122
  - Models supported: claude, gpt, gemini, local (model-agnostic by contract)
123
123
  - Packs (9): Core, OSINT, Maker, Shield, SEO, Memory, Dev Studio, Backup, Animated
124
124
  - Installer agents (14 across all packs):
@@ -45,9 +45,7 @@ for (const option of install.options || []) {
45
45
  installCmd.action(install.action);
46
46
 
47
47
  // Uninstall command
48
- const uninstallCmd = program
49
- .command('uninstall')
50
- .description('Remove BMAD+ from your project');
48
+ const uninstallCmd = program.command('uninstall').description('Remove BMAD+ from your project');
51
49
  for (const option of uninstall.options || []) {
52
50
  uninstallCmd.option(...option);
53
51
  }
@@ -64,9 +62,7 @@ for (const option of update.options || []) {
64
62
  updateCmd.action(update.action);
65
63
 
66
64
  // Doctor command
67
- const doctorCmd = program
68
- .command('doctor')
69
- .description('Check BMAD+ installation integrity');
65
+ const doctorCmd = program.command('doctor').description('Check BMAD+ installation integrity');
70
66
 
71
67
  for (const option of doctor.options || []) {
72
68
  doctorCmd.option(...option);
@@ -81,7 +77,9 @@ const scanCmd = program
81
77
  for (const option of scan.options || []) {
82
78
  scanCmd.option(...option);
83
79
  }
84
- scanCmd.action((scanPath, options) => scan.action({ ...options, directory: scanPath || options.directory }));
80
+ scanCmd.action((scanPath, options) =>
81
+ scan.action({ ...options, directory: scanPath || options.directory })
82
+ );
85
83
 
86
84
  // Memory command
87
85
  const memoryCmd = program
@@ -95,9 +93,7 @@ memoryCmd.action(memory.action);
95
93
 
96
94
  // Mem command (Karpathy memory loop — journal recall/write/reinforce)
97
95
  const memJournal = require('./commands/memory-journal-cmd');
98
- const memJournalCmd = program
99
- .command(memJournal.command)
100
- .description(memJournal.description);
96
+ const memJournalCmd = program.command(memJournal.command).description(memJournal.description);
101
97
 
102
98
  for (const option of memJournal.options || []) {
103
99
  memJournalCmd.option(...option);
@@ -116,15 +112,22 @@ for (const option of autoconfig.options || []) {
116
112
  autoconfigCmd.action(autoconfig.action);
117
113
 
118
114
  // Version discovery and explicit per-project update policy.
119
- for (const modulePath of ['./commands/update-check', './commands/update-policy', './commands/studio']) {
115
+ for (const modulePath of [
116
+ './commands/update-check',
117
+ './commands/update-policy',
118
+ './commands/studio',
119
+ './commands/nexus',
120
+ './commands/uat',
121
+ ]) {
120
122
  const command = require(modulePath);
121
123
  const configured = program.command(command.command).description(command.description);
124
+ if (command.aliases) configured.aliases(command.aliases);
122
125
  for (const option of command.options || []) configured.option(...option);
123
126
  configured.action(command.action);
124
127
  }
125
128
 
126
129
  // Await asynchronous registry checks and updates before completing the CLI.
127
- program.parseAsync(process.argv).catch(error => {
130
+ program.parseAsync(process.argv).catch((error) => {
128
131
  console.error(`BMAD+: ${error.message}`);
129
132
  process.exitCode = 1;
130
133
  });
@@ -87,7 +87,9 @@ function calculateHealth(structure) {
87
87
  return { pct, checks };
88
88
  }
89
89
 
90
- function recommendPacks(stack, structure, health) {
90
+ // `health` is measured elsewhere and does not change the recommendation: a project
91
+ // with failing checks needs the same packs as a healthy one.
92
+ function recommendPacks(stack, structure) {
91
93
  const packs = ['core', 'memory']; // Always
92
94
  const reasons = {
93
95
  core: 'Essential multi-role agents (Atlas, Forge, Sentinel, Nexus)',
@@ -215,7 +217,7 @@ module.exports = {
215
217
  const stack = detectStack(projectDir);
216
218
  const structure = analyzeStructure(projectDir);
217
219
  const health = calculateHealth(structure);
218
- const { packs, reasons } = recommendPacks(stack, structure, health);
220
+ const { packs, reasons } = recommendPacks(stack, structure);
219
221
  const recs = generateRecommendations(stack, structure, health);
220
222
 
221
223
  spinner.stop('Analysis complete');
@@ -45,6 +45,7 @@ module.exports = {
45
45
  'Installed: v' + report.versions.installed + ' | Executing CLI: v' + report.versions.cli,
46
46
  'Ownership inventory: ' + report.inventory.status,
47
47
  'Integrations use host-managed execution; host capabilities were not probed.',
48
+ 'Optional Nexus process execution requires an explicit plan; executable availability was not probed.',
48
49
  ].join('\n'),
49
50
  'BMAD+ Health Report'
50
51
  );