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,111 @@
1
+ /** Persist host-owned work, reconcile interruptions and verify current artifacts. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const nexus = require('../lib/nexus');
5
+
6
+ function inputFile(file, option) {
7
+ if (!file) throw new Error(option + ' requires a JSON file.');
8
+ const absolute = path.resolve(file);
9
+ const stat = fs.lstatSync(absolute);
10
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) {
11
+ throw new Error(option + ' must name a regular JSON file no larger than 1 MiB.');
12
+ }
13
+ const value = JSON.parse(fs.readFileSync(absolute, 'utf8'));
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
15
+ throw new Error(option + ' must contain a JSON object.');
16
+ }
17
+ return value;
18
+ }
19
+
20
+ function summary(result) {
21
+ if (!result.tasks) return JSON.stringify(result, null, 2);
22
+ return [
23
+ `Nexus run ${result.id} — durable task coordination`,
24
+ ...result.tasks.map((task) => {
25
+ const attempt = task.attempts.at(-1);
26
+ const observation = result.observations?.find((item) => item.taskId === task.id);
27
+ return (
28
+ `${task.id}: ${attempt?.execution || 'not started'}; verification=${attempt?.verification.status || 'pending'}; acceptance=${task.integration}` +
29
+ (attempt
30
+ ? `\n Attempt: ${attempt.id}; ${attempt.backend.kind}: ${attempt.backend.id}/${attempt.backend.sessionId}`
31
+ : '') +
32
+ (observation
33
+ ? `\n ${observation.hint}${observation.stale ? ' STALE: ' + observation.staleReason : ''}`
34
+ : '')
35
+ );
36
+ }),
37
+ 'Use inspect --json for recorded evidence and reconciliation observations.',
38
+ ].join('\n');
39
+ }
40
+
41
+ module.exports = {
42
+ command: 'nexus <action> [run] [task]',
43
+ description:
44
+ 'Launch or coordinate attempts, recover interrupted work and verify current artifacts',
45
+ options: [
46
+ ['-d, --directory <path>', 'Project directory'],
47
+ ['--plan <file>', 'JSON task plan for create'],
48
+ ['--input <file>', 'JSON observation or host identity for a mutation'],
49
+ ['--json', 'Print durable state and evidence as JSON'],
50
+ ],
51
+ action: async (action, runId, taskId, options = {}) => {
52
+ try {
53
+ const directory = path.resolve(options.directory || process.cwd());
54
+ let result;
55
+ if (action === 'create' && !runId && !taskId) {
56
+ if (options.input) throw new Error('create uses --plan.');
57
+ result = nexus.createRun(directory, inputFile(options.plan, '--plan'));
58
+ } else if (action === 'recover-lock' && !runId && !taskId) {
59
+ if (options.plan) throw new Error('recover-lock uses --input.');
60
+ result = nexus.recoverLock(directory, inputFile(options.input, '--input'));
61
+ } else if (action === 'inspect' && !taskId) {
62
+ if (options.input || options.plan)
63
+ throw new Error('inspect is read-only and takes no input file.');
64
+ result = nexus.inspectRun(directory, runId);
65
+ } else {
66
+ const mutations = {
67
+ start: nexus.startTask,
68
+ launch: nexus.launchTask,
69
+ collect: nexus.collectTask,
70
+ record: nexus.recordAttempt,
71
+ reconcile: nexus.reconcileAttempt,
72
+ verify: nexus.verifyTask,
73
+ retry: nexus.retryTask,
74
+ cancel: nexus.cancelTask,
75
+ accept: nexus.acceptTask,
76
+ };
77
+ if (!Object.hasOwn(mutations, action) || !runId || !taskId || options.plan) {
78
+ throw new Error(
79
+ 'Use nexus create --plan FILE, inspect RUN, or start|launch|collect|record|reconcile|verify|retry|cancel|accept RUN TASK.'
80
+ );
81
+ }
82
+ const needsInput = !['verify', 'accept'].includes(action);
83
+ if (!needsInput && options.input)
84
+ throw new Error(action + ' executes its recorded contract and takes no input file.');
85
+ result = await mutations[action](
86
+ directory,
87
+ runId,
88
+ taskId,
89
+ action === 'launch' && !options.input
90
+ ? {}
91
+ : needsInput
92
+ ? inputFile(options.input, '--input')
93
+ : undefined
94
+ );
95
+ }
96
+ const latest = result.tasks?.find((task) => task.id === taskId)?.attempts.at(-1);
97
+ process.exitCode =
98
+ (action === 'verify' && latest?.verification.status !== 'passed') ||
99
+ (action === 'launch' && latest?.process?.receipt?.outcome !== 'completed') ||
100
+ (action === 'collect' && latest?.execution !== 'completed')
101
+ ? 1
102
+ : 0;
103
+ console.log(options.json ? JSON.stringify(result, null, 2) : summary(result));
104
+ } catch (error) {
105
+ process.exitCode = 1;
106
+ if (options.json)
107
+ console.log(JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }));
108
+ else console.error('Nexus: ' + error.message);
109
+ }
110
+ },
111
+ };
@@ -0,0 +1,68 @@
1
+ /** Inspect installed Dev Studio routes and prepare actual host instructions. */
2
+ const path = require('node:path');
3
+ const { installedPack, validatePack, prepare } = require('../lib/studio');
4
+
5
+ module.exports = {
6
+ command: 'studio <action> [workflow]',
7
+ description: 'List Dev Studio workflows or prepare read-only execution context',
8
+ options: [
9
+ ['-d, --directory <path>', 'Installed project directory'],
10
+ ['--request <text>', 'Task scope for the host assistant'],
11
+ [
12
+ '--input <file>',
13
+ 'Project-relative input file (repeatable)',
14
+ (file, files) => [...files, file],
15
+ [],
16
+ ],
17
+ ['--json', 'Print machine-readable context and evidence'],
18
+ ],
19
+ action: (action, workflow, options = {}) => {
20
+ try {
21
+ const projectDir = path.resolve(options.directory || process.cwd());
22
+ let result;
23
+ if (action === 'list' && !workflow) {
24
+ const { catalog, resources } = validatePack(installedPack(projectDir));
25
+ result = {
26
+ schemaVersion: 1,
27
+ execution: 'host-managed',
28
+ ...catalog,
29
+ validatedResources: resources.length,
30
+ };
31
+ } else if (action === 'prepare' && workflow) {
32
+ result = prepare({ projectDir, workflow, request: options.request, inputs: options.input });
33
+ } else throw new Error('Use studio list or studio prepare WORKFLOW.');
34
+ process.exitCode = result.status === 'needs-input' ? 2 : 0;
35
+ if (options.json) console.log(JSON.stringify(result, null, 2));
36
+ else if (action === 'list')
37
+ console.log(
38
+ result.workflows.map((w) => `${w.id} (${w.agent}; input: ${w.input})`).join('\n')
39
+ );
40
+ else
41
+ console.log(
42
+ [
43
+ `${result.workflow}: ${result.status} — prepared for the host, not executed.`,
44
+ `Project: ${result.projectDir}`,
45
+ `Request: ${result.request || '(none supplied)'}`,
46
+ `Resolved configuration:\n${JSON.stringify(result.config.values, null, 2)}`,
47
+ `Defaulted fields: ${result.config.defaultsUsed.join(', ') || '(none)'}`,
48
+ `Proposed report: ${result.outputPath}`,
49
+ ...result.missingInputs,
50
+ ...result.instructions.map(
51
+ (item) => `\n--- ${item.path} (${item.sha256}) ---\n${item.content}`
52
+ ),
53
+ ...result.inputs.map(
54
+ (item) => `\n--- Project input: ${item.path} (${item.sha256}) ---\n${item.content}`
55
+ ),
56
+ result.previousReport
57
+ ? `\n--- Continue existing report ---\n${result.previousReport.content}`
58
+ : '',
59
+ ].join('\n')
60
+ );
61
+ } catch (error) {
62
+ process.exitCode = 1;
63
+ if (options.json)
64
+ console.log(JSON.stringify({ schemaVersion: 1, status: 'error', error: error.message }));
65
+ else console.error('Dev Studio: ' + error.message);
66
+ }
67
+ },
68
+ };
@@ -0,0 +1,389 @@
1
+ /** Build, serve and read human acceptance recipes (recette); the gate stays evidence-bound. */
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const http = require('node:http');
6
+ const path = require('node:path');
7
+ const { URL } = require('node:url');
8
+ const yaml = require('js-yaml');
9
+ const uat = require('../lib/uat');
10
+
11
+ const MAX_BODY = 4 * 1024 * 1024;
12
+ const RUN_ID = /^[0-9a-z][0-9a-z-]{0,63}$/;
13
+
14
+ function fail(message) {
15
+ throw new Error(message);
16
+ }
17
+
18
+ function resolveSpec(target, paths) {
19
+ if (!target) fail('an id or a spec file is required');
20
+ const file = target.endsWith('.json')
21
+ ? path.resolve(target)
22
+ : path.join(paths.specs, `${target}.json`);
23
+ if (!fs.existsSync(file)) fail(`no spec at ${file}`);
24
+ return uat.loadSpec(file);
25
+ }
26
+
27
+ function readStdin() {
28
+ try {
29
+ return fs.readFileSync(0, 'utf8');
30
+ } catch {
31
+ fail('nothing on standard input');
32
+ return '';
33
+ }
34
+ }
35
+
36
+ function print(json, payload, lines) {
37
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
38
+ else for (const line of lines) console.log(line);
39
+ }
40
+
41
+ function runsFor(spec, paths) {
42
+ const dir = path.join(paths.results, spec.id);
43
+ return uat.readRuns(fs.existsSync(dir) ? dir : paths.results, spec);
44
+ }
45
+
46
+ function triageFile(spec, paths) {
47
+ return path.join(paths.triage, `${spec.id}.json`);
48
+ }
49
+
50
+ /** The project's communication language, so a tester reads the page in their own. */
51
+ function projectLanguage(projectDir) {
52
+ try {
53
+ const config = yaml.load(
54
+ fs.readFileSync(path.join(projectDir, '_bmad', 'config.yaml'), 'utf8')
55
+ );
56
+ return (config && config.communication_language) || null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function serve(spec, paths, options) {
63
+ const page = path.join(paths.pages, `uat-${spec.id}.html`);
64
+ if (!fs.existsSync(page)) fail(`no page at ${page} — run "bmad-plus uat build ${spec.id}" first`);
65
+ const html = fs.readFileSync(page);
66
+ const resultsDir = path.join(paths.results, spec.id);
67
+ fs.mkdirSync(resultsDir, { recursive: true });
68
+ const port = Number.parseInt(options.port || '4173', 10);
69
+
70
+ const server = http.createServer((request, response) => {
71
+ const url = new URL(request.url, 'http://127.0.0.1');
72
+ const send = (code, body, type = 'application/json') => {
73
+ response.writeHead(code, { 'content-type': type, 'cache-control': 'no-store' });
74
+ response.end(body);
75
+ };
76
+ if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
77
+ return send(200, html, 'text/html; charset=utf-8');
78
+ }
79
+ if (request.method === 'GET' && url.pathname === '/__uat/ping') {
80
+ return send(
81
+ 200,
82
+ JSON.stringify({ uat: true, specId: spec.id, specSha256: uat.specHash(spec) })
83
+ );
84
+ }
85
+ if (request.method === 'PUT' && url.pathname.startsWith('/__uat/results/')) {
86
+ const runId = url.pathname.slice('/__uat/results/'.length);
87
+ if (!RUN_ID.test(runId)) return send(400, JSON.stringify({ error: 'invalid runId' }));
88
+ let size = 0;
89
+ const chunks = [];
90
+ request.on('data', (chunk) => {
91
+ size += chunk.length;
92
+ if (size > MAX_BODY) {
93
+ request.destroy();
94
+ return;
95
+ }
96
+ chunks.push(chunk);
97
+ });
98
+ request.on('end', () => {
99
+ try {
100
+ const run = uat.normalizeRun(JSON.parse(Buffer.concat(chunks).toString('utf8')), spec);
101
+ if (run.runId !== runId || run.specId !== spec.id)
102
+ fail('run identity does not match the request');
103
+ fs.writeFileSync(path.join(resultsDir, `${runId}.json`), JSON.stringify(run, null, 2));
104
+ const s = run.summary;
105
+ console.log(
106
+ `${new Date().toISOString().slice(11, 19)} ${runId} — ${s.passed} passed, ${s.failed} failed, ${s.blocked} blocked, ${s.unanswered} to do${run.finishedAt ? ' — finished' : ''}`
107
+ );
108
+ send(200, JSON.stringify({ stored: runId }));
109
+ } catch (error) {
110
+ send(400, JSON.stringify({ error: error.message }));
111
+ }
112
+ });
113
+ return undefined;
114
+ }
115
+ return send(404, JSON.stringify({ error: 'not found' }));
116
+ });
117
+
118
+ // Loopback only: a tester on another machine uses the artifact or the file (decision D5).
119
+ server.listen(port, '127.0.0.1', () => {
120
+ console.log(`uat serve: http://127.0.0.1:${port} (${spec.id}, ${spec.steps.length} steps)`);
121
+ console.log(`results → ${resultsDir}`);
122
+ console.log('Ctrl+C to stop.');
123
+ });
124
+ return server;
125
+ }
126
+
127
+ module.exports = {
128
+ command: 'uat <action> [target]',
129
+ aliases: ['recette'],
130
+ description: 'Human acceptance recipes: lint, build, serve, import, read, gate, order',
131
+ options: [
132
+ ['-d, --directory <path>', 'Project directory'],
133
+ ['--dir <path>', 'Recipe folder inside the project', uat.DEFAULT_DIR],
134
+ [
135
+ '--src <path>',
136
+ 'Source folder checked for on-screen labels (repeatable)',
137
+ (value, all) => [...all, value],
138
+ [],
139
+ ],
140
+ ['--language <code>', 'Page language (code or name); defaults to the project language'],
141
+ ['--input <file>', 'Results file to import ("-" reads standard input)'],
142
+ ['--port <number>', 'Port for serve (loopback only)'],
143
+ ['--emit-check', 'Write the self-contained Nexus verifier next to the recipe'],
144
+ ['--json', 'Machine-readable output'],
145
+ ],
146
+ action: (action, target, options = {}) => {
147
+ const projectDir = path.resolve(options.directory || process.cwd());
148
+ const paths = uat.layout(projectDir, options.dir || uat.DEFAULT_DIR);
149
+ const json = Boolean(options.json);
150
+ try {
151
+ if (action === 'lint') {
152
+ const { spec, legacy } = resolveSpec(target, paths);
153
+ const report = uat.lintSpec(spec, { sources: options.src });
154
+ print(json, { action, specId: spec.id, legacy, ...report }, [
155
+ `${spec.id}: ${spec.steps.length} steps, ${spec.steps.reduce((n, s) => n + s.expect.length, 0)} expectations, ${report.labels} on-screen labels${report.scannedFiles ? ` checked against ${report.scannedFiles} source files` : ''}`,
156
+ ...report.errors.map((line) => ` error ${line}`),
157
+ ...report.warnings.map((line) => ` warning ${line}`),
158
+ report.errors.length ? '' : ' no error',
159
+ ]);
160
+ process.exitCode = report.errors.length ? 1 : 0;
161
+ return;
162
+ }
163
+
164
+ if (action === 'build') {
165
+ const { spec } = resolveSpec(target, paths);
166
+ const report = uat.lintSpec(spec, { sources: options.src });
167
+ if (report.errors.length) {
168
+ print(json, { action, specId: spec.id, status: 'error', errors: report.errors }, [
169
+ `${spec.id}: not built, ${report.errors.length} error(s)`,
170
+ ...report.errors.map((line) => ` error ${line}`),
171
+ ]);
172
+ process.exitCode = 1;
173
+ return;
174
+ }
175
+ const page = uat.buildPage(spec, {
176
+ language: options.language || projectLanguage(projectDir),
177
+ });
178
+ fs.mkdirSync(paths.pages, { recursive: true });
179
+ const file = path.join(paths.pages, `uat-${spec.id}.html`);
180
+ fs.writeFileSync(file, page.html, 'utf8');
181
+ print(
182
+ json,
183
+ {
184
+ action,
185
+ specId: spec.id,
186
+ file,
187
+ specSha256: page.sha256,
188
+ language: page.language,
189
+ languages: Object.keys(uat.STRINGS),
190
+ warnings: report.warnings,
191
+ },
192
+ [
193
+ `${file} — ${spec.steps.length} steps, ${(page.html.length / 1024).toFixed(0)} KiB, spec ${page.sha256.slice(0, 12)}`,
194
+ `opens in ${uat.STRINGS[page.language].name}; the tester can switch to any of the ${Object.keys(uat.STRINGS).length} languages on the page`,
195
+ ...report.warnings.map((line) => ` warning ${line}`),
196
+ ]
197
+ );
198
+ return;
199
+ }
200
+
201
+ if (action === 'serve') {
202
+ const { spec } = resolveSpec(target, paths);
203
+ serve(spec, paths, options);
204
+ return;
205
+ }
206
+
207
+ if (action === 'import') {
208
+ const { spec } = resolveSpec(target, paths);
209
+ if (!options.input) fail('--input <file> or --input - is required');
210
+ const fromStdin = options.input === '-';
211
+ const source = fromStdin
212
+ ? readStdin()
213
+ : fs.readFileSync(path.resolve(options.input), 'utf8');
214
+ const document = JSON.parse(source);
215
+ // The first artifact exports carried neither runId nor specId; the file name holds the run.
216
+ if (!document.runId && !fromStdin) {
217
+ const marked = /(\d{8}-[0-9a-z-]+)$/.exec(path.basename(options.input, '.json'));
218
+ if (marked) document.runId = marked[1];
219
+ }
220
+ if (!document.specId) document.specId = spec.id;
221
+ const run = uat.normalizeRun(document, spec);
222
+ if (run.specId !== spec.id) fail(`these results answer ${run.specId}, not ${spec.id}`);
223
+ const dir = path.join(paths.results, spec.id);
224
+ fs.mkdirSync(dir, { recursive: true });
225
+ const file = path.join(dir, `${run.runId}.json`);
226
+ fs.writeFileSync(file, JSON.stringify(run, null, 2));
227
+ print(json, { action, specId: spec.id, runId: run.runId, file, summary: run.summary }, [
228
+ `${file} — ${run.summary.passed} passed, ${run.summary.failed} failed, ${run.summary.blocked} blocked, ${run.summary.unanswered} to do`,
229
+ ]);
230
+ return;
231
+ }
232
+
233
+ if (action === 'read') {
234
+ const { spec } = resolveSpec(target, paths);
235
+ const runs = runsFor(spec, paths);
236
+ if (!runs.length) {
237
+ print(json, { action, specId: spec.id, status: 'awaiting', runs: [] }, [
238
+ `${spec.id}: no run yet`,
239
+ ]);
240
+ process.exitCode = 2;
241
+ return;
242
+ }
243
+ const payload = runs.map((run) => ({
244
+ runId: run.runId,
245
+ tester: run.tester,
246
+ startedAt: run.startedAt,
247
+ finishedAt: run.finishedAt,
248
+ stale: run.stale,
249
+ unsigned: run.unsigned,
250
+ summary: run.summary,
251
+ failures: uat.failures(run, spec),
252
+ unanswered: uat.unanswered(run, spec),
253
+ overallNote: run.overallNote,
254
+ file: run.file,
255
+ }));
256
+ const lines = [];
257
+ for (const run of payload) {
258
+ const s = run.summary;
259
+ lines.push('');
260
+ lines.push(
261
+ `■ ${spec.product} ${spec.versions.join(' · ')} — ${run.tester} — ${String(run.startedAt).slice(0, 16).replace('T', ' ')}${run.finishedAt ? ', finished' : ', IN PROGRESS'}`
262
+ );
263
+ lines.push(
264
+ ` ${s.passed} passed · ${s.failed} failed · ${s.blocked} blocked · ${s.skipped} skipped · ${s.unanswered} to do (${run.file})`
265
+ );
266
+ if (run.stale)
267
+ lines.push(' ⚠ answered another revision of the spec — replay the affected steps');
268
+ else if (run.unsigned)
269
+ lines.push(' ⚠ run without a spec fingerprint (page built before fingerprints)');
270
+ if (run.overallNote) lines.push(` overall: ${run.overallNote}`);
271
+ for (const failure of run.failures) {
272
+ lines.push(
273
+ ` ✗ ${failure.step}/${failure.expect} (${failure.state}) — ${failure.title}`
274
+ );
275
+ if (failure.missingFromSpec)
276
+ lines.push(' expectation: ABSENT from the current spec');
277
+ else if (failure.text) lines.push(` expectation: ${failure.text}`);
278
+ lines.push(
279
+ failure.note
280
+ ? ` seen instead: ${failure.note.replace(/\s+/g, ' ').trim()}`
281
+ : ' (no note — ask the tester what they saw)'
282
+ );
283
+ }
284
+ if (!run.failures.length) lines.push(' ✓ nothing failed');
285
+ }
286
+ print(json, { action, specId: spec.id, runs: payload }, lines);
287
+ process.exitCode = payload[0].stale
288
+ ? 3
289
+ : payload.some((run) => run.failures.length)
290
+ ? 1
291
+ : 0;
292
+ return;
293
+ }
294
+
295
+ if (action === 'gate') {
296
+ const { spec } = resolveSpec(target, paths);
297
+ const runs = runsFor(spec, paths);
298
+ const triage = uat.loadTriage(triageFile(spec, paths));
299
+ const verdict = uat.gate({ spec, runs, triage });
300
+ let check = null;
301
+ if (options.emitCheck) {
302
+ fs.mkdirSync(paths.checks, { recursive: true });
303
+ check = path.join(paths.checks, `gate-${spec.id}.cjs`);
304
+ fs.writeFileSync(
305
+ check,
306
+ uat.emitCheck({
307
+ specId: spec.id,
308
+ specSha256: uat.specHash(spec),
309
+ dir: options.dir || uat.DEFAULT_DIR,
310
+ writeSteps: spec.steps.filter((step) => step.writes).map((step) => step.id),
311
+ }),
312
+ 'utf8'
313
+ );
314
+ }
315
+ print(
316
+ json,
317
+ {
318
+ action,
319
+ specId: spec.id,
320
+ status: verdict.status,
321
+ reasons: verdict.reasons,
322
+ runId: verdict.run?.runId || null,
323
+ check,
324
+ },
325
+ [
326
+ `${spec.id}: ${verdict.status}${verdict.run ? ` (run ${verdict.run.runId} by ${verdict.run.tester})` : ''}`,
327
+ ...verdict.reasons.map((reason) => ` - ${reason}`),
328
+ check ? ` verifier: ${check}` : '',
329
+ verdict.status === 'passed'
330
+ ? ' human-observed: complete, current and triaged. It does not establish that the tester looked at the right place.'
331
+ : '',
332
+ ].filter(Boolean)
333
+ );
334
+ process.exitCode = { passed: 0, failed: 1, awaiting: 2, stale: 3 }[verdict.status];
335
+ return;
336
+ }
337
+
338
+ if (action === 'order') {
339
+ const specs = (fs.existsSync(paths.specs) ? fs.readdirSync(paths.specs) : [])
340
+ .filter((name) => name.endsWith('.json'))
341
+ .map((name) => uat.loadSpec(path.join(paths.specs, name)).spec);
342
+ if (!specs.length) fail(`no spec in ${paths.specs}`);
343
+ const { order, collisions, cycles } = uat.playOrder(specs);
344
+ const body = [
345
+ '# Play order',
346
+ '',
347
+ 'Generated by `bmad-plus uat order`. A witness written by one recipe and read by another',
348
+ 'decides the order; a step meant "for later" belongs to its own page.',
349
+ '',
350
+ ...order.map((id, index) => `${index + 1}. \`${id}\``),
351
+ '',
352
+ ...(collisions.length
353
+ ? [
354
+ '## Shared witnesses',
355
+ '',
356
+ ...collisions.map(
357
+ (c) =>
358
+ `- \`${c.witness}\`: written by \`${c.writtenBy}\`, read by \`${c.readBy}\``
359
+ ),
360
+ '',
361
+ ]
362
+ : []),
363
+ ...(cycles.length
364
+ ? ['## Cycles — decide by hand', '', ...cycles.map((c) => `- ${c}`), '']
365
+ : []),
366
+ ].join('\n');
367
+ fs.mkdirSync(paths.root, { recursive: true });
368
+ const file = path.join(paths.root, 'ORDER.md');
369
+ fs.writeFileSync(file, body, 'utf8');
370
+ print(json, { action, order, collisions, cycles, file }, [
371
+ file,
372
+ ...order.map((id, index) => `${index + 1}. ${id}`),
373
+ ...cycles.map((c) => `cycle: ${c}`),
374
+ ]);
375
+ process.exitCode = cycles.length ? 1 : 0;
376
+ return;
377
+ }
378
+
379
+ fail(`unknown action "${action}" (lint, build, serve, import, read, gate, order)`);
380
+ } catch (error) {
381
+ if (json)
382
+ console.log(
383
+ JSON.stringify({ schemaVersion: 1, status: 'error', message: error.message }, null, 2)
384
+ );
385
+ else console.error(`uat: ${error.message}`);
386
+ process.exitCode = 1;
387
+ }
388
+ },
389
+ };
@@ -1,9 +1,10 @@
1
1
  # memory-journal.js — Karpathy Learning Layer core (Pillar 3)
2
2
 
3
3
  Portable data structures + helpers for the BMAD+ **memory → reward → reinforcement** loop.
4
- Prompt-level learning only: scores steer retrieval and pattern promotion — there is
5
- **no base-model fine-tuning** (see `audit/2026-07-01/north-star/registry.yaml` →
6
- `memory.reward_signal.applies_to`).
4
+ Legacy manually supplied rewards can propose pattern promotion; they do not steer
5
+ recall. The optional `memory-outcomes.js` layer derives a bounded recall boost from
6
+ current accepted Nexus evidence. There is **no base-model fine-tuning**, and a
7
+ retrieved memory is not evidence of improved downstream task quality.
7
8
 
8
9
  Builds **on top of** the existing `pack-memory` (Zecher, Karpathy guardrails G1–G4,
9
10
  `decisions/lessons/patterns/context.md` templates). It never modifies those files or
@@ -12,6 +13,8 @@ Builds **on top of** the existing `pack-memory` (Zecher, Karpathy guardrails G1
12
13
  ```
13
14
  .bmad/memory/journal.ndjson ← structured event log (this module, north-star scope)
14
15
  .bmad/memory/promotions.ndjson ← governance queue (always PROPOSED)
16
+ .bmad/memory/outcomes.ndjson ← immutable, provenance-backed accepted outcome receipts
17
+ .bmad/memory/writer.lock ← shared synchronous writer mutex
15
18
  .agents/memory/*.md ← human memory (pack-memory, current layout) — READ ONLY here
16
19
  .bmad/memory/*.md ← human memory (north-star layout) — READ ONLY here
17
20
  ```
@@ -20,9 +23,9 @@ Builds **on top of** the existing `pack-memory` (Zecher, Karpathy guardrails G1
20
23
 
21
24
  | Rule | Enforcement |
22
25
  |---|---|
23
- | No hidden clock / randomness | `ts` is a **required, caller-injected** field; ids are content hashes (sha256). The module never calls `Date.now()` or `Math.random()` — at import or runtime. |
24
- | Node stdlib only | `fs`, `path`, `crypto`. No network, no native deps → runs identically under every CLI. |
25
- | Journal is append-only, corruption-tolerant | `readJournal`/`readPromotions` skip torn lines instead of throwing (concurrent CLIs may write). |
26
+ | Injected event clock | `ts` is a **required, caller-injected** field; event/receipt IDs are content hashes. The storage mutex separately uses a random owner token; it does not affect event IDs. |
27
+ | Node stdlib only | No network or additional service is required for project-local memory. Evidence inspection reads Nexus state and current filesystem identities. |
28
+ | Serialized writes | Journal, scores, promotions and outcomes share the same project mutex. Legacy readers skip corrupt lines; new appends refuse an incomplete final line. Outcome parsing fails on corruption. |
26
29
  | Promotions are never auto-applied | `proposePromotion()` only emits `status: 'PROPOSED'`; `appendPromotion()` **forces** `PROPOSED` + clears approval fields on disk even for tampered records; `assertPromotionApplicable()` throws unless `status === 'APPROVED'` **and** `approvedBy` names a human/Shield reviewer. |
27
30
 
28
31
  ## API
@@ -64,6 +67,14 @@ Sources merged: `journal.ndjson` events + `### `-sectioned entries from
64
67
  and `.agents/memory/` (current pack-memory layout), plus `<portfolioDir>/memory/*.md`
65
68
  when `scope: 'portfolio'`.
66
69
 
70
+ For opt-in project-local evidence ranking, pass `ranking: 'evidence'` and
71
+ `contextScope: ['src/component']`. Relevant notes with current accepted Nexus evidence
72
+ receive at most a 25% boost; failed/unverified/duplicate observations cannot supply it.
73
+ Stale, explicitly superseded or contradictory evidence is excluded, while unsupported
74
+ notes remain labeled `unverified`. Alternative backends and portfolio scope are not
75
+ part of evidence ranking. See the [outcome contract](../../../docs/specs/memory-outcomes.md)
76
+ for observation input, freshness checks, limitations and recovery.
77
+
67
78
  **ChromaDB seam** — pass `backend: { search(query, opts) }` and ranking is delegated
68
79
  wholesale to it. The intended production backend is the existing RAG stack
69
80
  (`mcp-server/rag.py`: ChromaDB + SentenceTransformers — `registry.yaml → memory.index`).
@@ -84,8 +95,8 @@ score = mj.updatePatternScore(score, reward, { ts: eventTs });
84
95
 
85
96
  Two complementary estimators per pattern:
86
97
 
87
- - **Elo** (`k=32`, baseline 1200): `elo' = elo + K·(reward − expected)` — fast-moving,
88
- ordinal, used to **rank** patterns in recall. Fresh pattern + reward 1 → 1216.
98
+ - **Elo** (`k=32`, baseline 1200): `elo' = elo + K·(reward − expected)` — a legacy
99
+ descriptive score; it is not consumed by recall. Fresh pattern + reward 1 → 1216.
89
100
  - **Decayed Bayesian (Beta)**: evidence decays multiplicatively toward the uniform
90
101
  prior (1,1) — per-update (`decay=0.98`) and time-based (`halfLifeDays=90`, only when
91
102
  `ts` is injected) — so `mean = α/(α+β)` tracks the **recent** success rate, used for