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,405 @@
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
+ if (target.endsWith('.json')) {
21
+ const file = path.resolve(target);
22
+ if (!fs.existsSync(file)) fail(`no spec at ${file}`);
23
+ return uat.loadSpec(file);
24
+ }
25
+ const direct = path.join(paths.specs, `${target}.json`);
26
+ if (fs.existsSync(direct)) return uat.loadSpec(direct);
27
+ // A recipe is named by its id, not by its file: a copied template keeps its own name
28
+ // (the shipped example-uat-spec.json holds the id example-1.0.0).
29
+ if (fs.existsSync(paths.specs)) {
30
+ for (const name of fs.readdirSync(paths.specs).filter((entry) => entry.endsWith('.json'))) {
31
+ try {
32
+ const loaded = uat.loadSpec(path.join(paths.specs, name));
33
+ if (loaded.spec.id === target) return loaded;
34
+ } catch {
35
+ // an unreadable file cannot be the recipe asked for
36
+ }
37
+ }
38
+ }
39
+ fail(`no spec with the id "${target}" in ${paths.specs}`);
40
+ return null;
41
+ }
42
+
43
+ function readStdin() {
44
+ try {
45
+ return fs.readFileSync(0, 'utf8');
46
+ } catch {
47
+ fail('nothing on standard input');
48
+ return '';
49
+ }
50
+ }
51
+
52
+ function print(json, payload, lines) {
53
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, ...payload }, null, 2));
54
+ else for (const line of lines) console.log(line);
55
+ }
56
+
57
+ function runsFor(spec, paths) {
58
+ const dir = path.join(paths.results, spec.id);
59
+ return uat.readRuns(fs.existsSync(dir) ? dir : paths.results, spec);
60
+ }
61
+
62
+ function triageFile(spec, paths) {
63
+ return path.join(paths.triage, `${spec.id}.json`);
64
+ }
65
+
66
+ /** The project's communication language, so a tester reads the page in their own. */
67
+ function projectLanguage(projectDir) {
68
+ try {
69
+ const config = yaml.load(
70
+ fs.readFileSync(path.join(projectDir, '_bmad', 'config.yaml'), 'utf8')
71
+ );
72
+ return (config && config.communication_language) || null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function serve(spec, paths, options) {
79
+ const page = path.join(paths.pages, `uat-${spec.id}.html`);
80
+ if (!fs.existsSync(page)) fail(`no page at ${page} — run "bmad-plus uat build ${spec.id}" first`);
81
+ const html = fs.readFileSync(page);
82
+ const resultsDir = path.join(paths.results, spec.id);
83
+ fs.mkdirSync(resultsDir, { recursive: true });
84
+ const port = Number.parseInt(options.port || '4173', 10);
85
+
86
+ const server = http.createServer((request, response) => {
87
+ const url = new URL(request.url, 'http://127.0.0.1');
88
+ const send = (code, body, type = 'application/json') => {
89
+ response.writeHead(code, { 'content-type': type, 'cache-control': 'no-store' });
90
+ response.end(body);
91
+ };
92
+ if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
93
+ return send(200, html, 'text/html; charset=utf-8');
94
+ }
95
+ if (request.method === 'GET' && url.pathname === '/__uat/ping') {
96
+ return send(
97
+ 200,
98
+ JSON.stringify({ uat: true, specId: spec.id, specSha256: uat.specHash(spec) })
99
+ );
100
+ }
101
+ if (request.method === 'PUT' && url.pathname.startsWith('/__uat/results/')) {
102
+ const runId = url.pathname.slice('/__uat/results/'.length);
103
+ if (!RUN_ID.test(runId)) return send(400, JSON.stringify({ error: 'invalid runId' }));
104
+ let size = 0;
105
+ const chunks = [];
106
+ request.on('data', (chunk) => {
107
+ size += chunk.length;
108
+ if (size > MAX_BODY) {
109
+ request.destroy();
110
+ return;
111
+ }
112
+ chunks.push(chunk);
113
+ });
114
+ request.on('end', () => {
115
+ try {
116
+ const run = uat.normalizeRun(JSON.parse(Buffer.concat(chunks).toString('utf8')), spec);
117
+ if (run.runId !== runId || run.specId !== spec.id)
118
+ fail('run identity does not match the request');
119
+ fs.writeFileSync(path.join(resultsDir, `${runId}.json`), JSON.stringify(run, null, 2));
120
+ const s = run.summary;
121
+ console.log(
122
+ `${new Date().toISOString().slice(11, 19)} ${runId} — ${s.passed} passed, ${s.failed} failed, ${s.blocked} blocked, ${s.unanswered} to do${run.finishedAt ? ' — finished' : ''}`
123
+ );
124
+ send(200, JSON.stringify({ stored: runId }));
125
+ } catch (error) {
126
+ send(400, JSON.stringify({ error: error.message }));
127
+ }
128
+ });
129
+ return undefined;
130
+ }
131
+ return send(404, JSON.stringify({ error: 'not found' }));
132
+ });
133
+
134
+ // Loopback only: a tester on another machine uses the artifact or the file (decision D5).
135
+ server.listen(port, '127.0.0.1', () => {
136
+ console.log(`uat serve: http://127.0.0.1:${port} (${spec.id}, ${spec.steps.length} steps)`);
137
+ console.log(`results → ${resultsDir}`);
138
+ console.log('Ctrl+C to stop.');
139
+ });
140
+ return server;
141
+ }
142
+
143
+ module.exports = {
144
+ command: 'uat <action> [target]',
145
+ aliases: ['recette'],
146
+ description: 'Human acceptance recipes: lint, build, serve, import, read, gate, order',
147
+ options: [
148
+ ['-d, --directory <path>', 'Project directory'],
149
+ ['--dir <path>', 'Recipe folder inside the project', uat.DEFAULT_DIR],
150
+ [
151
+ '--src <path>',
152
+ 'Source folder checked for on-screen labels (repeatable)',
153
+ (value, all) => [...all, value],
154
+ [],
155
+ ],
156
+ ['--language <code>', 'Page language (code or name); defaults to the project language'],
157
+ ['--input <file>', 'Results file to import ("-" reads standard input)'],
158
+ ['--port <number>', 'Port for serve (loopback only)'],
159
+ ['--emit-check', 'Write the self-contained Nexus verifier next to the recipe'],
160
+ ['--json', 'Machine-readable output'],
161
+ ],
162
+ action: (action, target, options = {}) => {
163
+ const projectDir = path.resolve(options.directory || process.cwd());
164
+ const paths = uat.layout(projectDir, options.dir || uat.DEFAULT_DIR);
165
+ const json = Boolean(options.json);
166
+ try {
167
+ if (action === 'lint') {
168
+ const { spec, legacy } = resolveSpec(target, paths);
169
+ const report = uat.lintSpec(spec, { sources: options.src });
170
+ print(json, { action, specId: spec.id, legacy, ...report }, [
171
+ `${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` : ''}`,
172
+ ...report.errors.map((line) => ` error ${line}`),
173
+ ...report.warnings.map((line) => ` warning ${line}`),
174
+ report.errors.length ? '' : ' no error',
175
+ ]);
176
+ process.exitCode = report.errors.length ? 1 : 0;
177
+ return;
178
+ }
179
+
180
+ if (action === 'build') {
181
+ const { spec } = resolveSpec(target, paths);
182
+ const report = uat.lintSpec(spec, { sources: options.src });
183
+ if (report.errors.length) {
184
+ print(json, { action, specId: spec.id, status: 'error', errors: report.errors }, [
185
+ `${spec.id}: not built, ${report.errors.length} error(s)`,
186
+ ...report.errors.map((line) => ` error ${line}`),
187
+ ]);
188
+ process.exitCode = 1;
189
+ return;
190
+ }
191
+ const page = uat.buildPage(spec, {
192
+ language: options.language || projectLanguage(projectDir),
193
+ });
194
+ fs.mkdirSync(paths.pages, { recursive: true });
195
+ const file = path.join(paths.pages, `uat-${spec.id}.html`);
196
+ fs.writeFileSync(file, page.html, 'utf8');
197
+ print(
198
+ json,
199
+ {
200
+ action,
201
+ specId: spec.id,
202
+ file,
203
+ specSha256: page.sha256,
204
+ language: page.language,
205
+ languages: Object.keys(uat.STRINGS),
206
+ warnings: report.warnings,
207
+ },
208
+ [
209
+ `${file} — ${spec.steps.length} steps, ${(page.html.length / 1024).toFixed(0)} KiB, spec ${page.sha256.slice(0, 12)}`,
210
+ `opens in ${uat.STRINGS[page.language].name}; the tester can switch to any of the ${Object.keys(uat.STRINGS).length} languages on the page`,
211
+ ...report.warnings.map((line) => ` warning ${line}`),
212
+ ]
213
+ );
214
+ return;
215
+ }
216
+
217
+ if (action === 'serve') {
218
+ const { spec } = resolveSpec(target, paths);
219
+ serve(spec, paths, options);
220
+ return;
221
+ }
222
+
223
+ if (action === 'import') {
224
+ const { spec } = resolveSpec(target, paths);
225
+ if (!options.input) fail('--input <file> or --input - is required');
226
+ const fromStdin = options.input === '-';
227
+ const source = fromStdin
228
+ ? readStdin()
229
+ : fs.readFileSync(path.resolve(options.input), 'utf8');
230
+ const document = JSON.parse(source);
231
+ // The first artifact exports carried neither runId nor specId; the file name holds the run.
232
+ if (!document.runId && !fromStdin) {
233
+ const marked = /(\d{8}-[0-9a-z-]+)$/.exec(path.basename(options.input, '.json'));
234
+ if (marked) document.runId = marked[1];
235
+ }
236
+ if (!document.specId) document.specId = spec.id;
237
+ const run = uat.normalizeRun(document, spec);
238
+ if (run.specId !== spec.id) fail(`these results answer ${run.specId}, not ${spec.id}`);
239
+ const dir = path.join(paths.results, spec.id);
240
+ fs.mkdirSync(dir, { recursive: true });
241
+ const file = path.join(dir, `${run.runId}.json`);
242
+ fs.writeFileSync(file, JSON.stringify(run, null, 2));
243
+ print(json, { action, specId: spec.id, runId: run.runId, file, summary: run.summary }, [
244
+ `${file} — ${run.summary.passed} passed, ${run.summary.failed} failed, ${run.summary.blocked} blocked, ${run.summary.unanswered} to do`,
245
+ ]);
246
+ return;
247
+ }
248
+
249
+ if (action === 'read') {
250
+ const { spec } = resolveSpec(target, paths);
251
+ const runs = runsFor(spec, paths);
252
+ if (!runs.length) {
253
+ print(json, { action, specId: spec.id, status: 'awaiting', runs: [] }, [
254
+ `${spec.id}: no run yet`,
255
+ ]);
256
+ process.exitCode = 2;
257
+ return;
258
+ }
259
+ const payload = runs.map((run) => ({
260
+ runId: run.runId,
261
+ tester: run.tester,
262
+ startedAt: run.startedAt,
263
+ finishedAt: run.finishedAt,
264
+ stale: run.stale,
265
+ unsigned: run.unsigned,
266
+ summary: run.summary,
267
+ failures: uat.failures(run, spec),
268
+ unanswered: uat.unanswered(run, spec),
269
+ overallNote: run.overallNote,
270
+ file: run.file,
271
+ }));
272
+ const lines = [];
273
+ for (const run of payload) {
274
+ const s = run.summary;
275
+ lines.push('');
276
+ lines.push(
277
+ `■ ${spec.product} ${spec.versions.join(' · ')} — ${run.tester} — ${String(run.startedAt).slice(0, 16).replace('T', ' ')}${run.finishedAt ? ', finished' : ', IN PROGRESS'}`
278
+ );
279
+ lines.push(
280
+ ` ${s.passed} passed · ${s.failed} failed · ${s.blocked} blocked · ${s.skipped} skipped · ${s.unanswered} to do (${run.file})`
281
+ );
282
+ if (run.stale)
283
+ lines.push(' ⚠ answered another revision of the spec — replay the affected steps');
284
+ else if (run.unsigned)
285
+ lines.push(' ⚠ run without a spec fingerprint (page built before fingerprints)');
286
+ if (run.overallNote) lines.push(` overall: ${run.overallNote}`);
287
+ for (const failure of run.failures) {
288
+ lines.push(
289
+ ` ✗ ${failure.step}/${failure.expect} (${failure.state}) — ${failure.title}`
290
+ );
291
+ if (failure.missingFromSpec)
292
+ lines.push(' expectation: ABSENT from the current spec');
293
+ else if (failure.text) lines.push(` expectation: ${failure.text}`);
294
+ lines.push(
295
+ failure.note
296
+ ? ` seen instead: ${failure.note.replace(/\s+/g, ' ').trim()}`
297
+ : ' (no note — ask the tester what they saw)'
298
+ );
299
+ }
300
+ if (!run.failures.length) lines.push(' ✓ nothing failed');
301
+ }
302
+ print(json, { action, specId: spec.id, runs: payload }, lines);
303
+ process.exitCode = payload[0].stale
304
+ ? 3
305
+ : payload.some((run) => run.failures.length)
306
+ ? 1
307
+ : 0;
308
+ return;
309
+ }
310
+
311
+ if (action === 'gate') {
312
+ const { spec } = resolveSpec(target, paths);
313
+ const runs = runsFor(spec, paths);
314
+ const triage = uat.loadTriage(triageFile(spec, paths));
315
+ const verdict = uat.gate({ spec, runs, triage });
316
+ let check = null;
317
+ if (options.emitCheck) {
318
+ fs.mkdirSync(paths.checks, { recursive: true });
319
+ check = path.join(paths.checks, `gate-${spec.id}.cjs`);
320
+ fs.writeFileSync(
321
+ check,
322
+ uat.emitCheck({
323
+ specId: spec.id,
324
+ specSha256: uat.specHash(spec),
325
+ dir: options.dir || uat.DEFAULT_DIR,
326
+ writeSteps: spec.steps.filter((step) => step.writes).map((step) => step.id),
327
+ }),
328
+ 'utf8'
329
+ );
330
+ }
331
+ print(
332
+ json,
333
+ {
334
+ action,
335
+ specId: spec.id,
336
+ status: verdict.status,
337
+ reasons: verdict.reasons,
338
+ runId: verdict.run?.runId || null,
339
+ check,
340
+ },
341
+ [
342
+ `${spec.id}: ${verdict.status}${verdict.run ? ` (run ${verdict.run.runId} by ${verdict.run.tester})` : ''}`,
343
+ ...verdict.reasons.map((reason) => ` - ${reason}`),
344
+ check ? ` verifier: ${check}` : '',
345
+ verdict.status === 'passed'
346
+ ? ' human-observed: complete, current and triaged. It does not establish that the tester looked at the right place.'
347
+ : '',
348
+ ].filter(Boolean)
349
+ );
350
+ process.exitCode = { passed: 0, failed: 1, awaiting: 2, stale: 3 }[verdict.status];
351
+ return;
352
+ }
353
+
354
+ if (action === 'order') {
355
+ const specs = (fs.existsSync(paths.specs) ? fs.readdirSync(paths.specs) : [])
356
+ .filter((name) => name.endsWith('.json'))
357
+ .map((name) => uat.loadSpec(path.join(paths.specs, name)).spec);
358
+ if (!specs.length) fail(`no spec in ${paths.specs}`);
359
+ const { order, collisions, cycles } = uat.playOrder(specs);
360
+ const body = [
361
+ '# Play order',
362
+ '',
363
+ 'Generated by `bmad-plus uat order`. A witness written by one recipe and read by another',
364
+ 'decides the order; a step meant "for later" belongs to its own page.',
365
+ '',
366
+ ...order.map((id, index) => `${index + 1}. \`${id}\``),
367
+ '',
368
+ ...(collisions.length
369
+ ? [
370
+ '## Shared witnesses',
371
+ '',
372
+ ...collisions.map(
373
+ (c) =>
374
+ `- \`${c.witness}\`: written by \`${c.writtenBy}\`, read by \`${c.readBy}\``
375
+ ),
376
+ '',
377
+ ]
378
+ : []),
379
+ ...(cycles.length
380
+ ? ['## Cycles — decide by hand', '', ...cycles.map((c) => `- ${c}`), '']
381
+ : []),
382
+ ].join('\n');
383
+ fs.mkdirSync(paths.root, { recursive: true });
384
+ const file = path.join(paths.root, 'ORDER.md');
385
+ fs.writeFileSync(file, body, 'utf8');
386
+ print(json, { action, order, collisions, cycles, file }, [
387
+ file,
388
+ ...order.map((id, index) => `${index + 1}. ${id}`),
389
+ ...cycles.map((c) => `cycle: ${c}`),
390
+ ]);
391
+ process.exitCode = cycles.length ? 1 : 0;
392
+ return;
393
+ }
394
+
395
+ fail(`unknown action "${action}" (lint, build, serve, import, read, gate, order)`);
396
+ } catch (error) {
397
+ if (json)
398
+ console.log(
399
+ JSON.stringify({ schemaVersion: 1, status: 'error', message: error.message }, null, 2)
400
+ );
401
+ else console.error(`uat: ${error.message}`);
402
+ process.exitCode = 1;
403
+ }
404
+ },
405
+ };
package/tools/cli/i18n.js CHANGED
@@ -21,6 +21,7 @@ const LANGUAGES = {
21
21
  selected_packs: 'Selected packs',
22
22
  enter_name: 'Your first name (agents use it to greet you)',
23
23
  comm_language: 'Communication language',
24
+ uat_environment: 'Test environment for human acceptance recipes (URL, blank if none)',
24
25
  exec_mode: 'Execution mode',
25
26
  exec_manual: 'Manual — You call agents yourself',
26
27
  exec_autopilot: 'Autopilot — Nexus manages the full pipeline',
@@ -109,6 +110,7 @@ const LANGUAGES = {
109
110
  selected_packs: 'Packs sélectionnés',
110
111
  enter_name: 'Ton prénom (les agents l\'utilisent pour te saluer)',
111
112
  comm_language: 'Langue de communication',
113
+ uat_environment: 'Environnement de test pour les recettes humaines (URL, vide si aucun)',
112
114
  exec_mode: 'Mode d\'exécution',
113
115
  exec_manual: 'Manuel — Tu appelles les agents toi-même',
114
116
  exec_autopilot: 'Autopilot — Nexus gère tout le pipeline',
@@ -195,6 +197,7 @@ const LANGUAGES = {
195
197
  selected_packs: 'Packs seleccionados',
196
198
  enter_name: 'Tu nombre (los agentes lo usan para saludarte)',
197
199
  comm_language: 'Idioma de comunicación',
200
+ uat_environment: 'Entorno de pruebas para las recetas de aceptación (URL, vacío si no hay)',
198
201
  exec_mode: 'Modo de ejecución',
199
202
  exec_manual: 'Manual — Tú llamas a los agentes',
200
203
  exec_autopilot: 'Autopilot — Nexus gestiona todo el pipeline',
@@ -281,6 +284,7 @@ const LANGUAGES = {
281
284
  selected_packs: 'Ausgewählte Packs',
282
285
  enter_name: 'Dein Vorname (Agenten verwenden ihn zur Begrüßung)',
283
286
  comm_language: 'Kommunikationssprache',
287
+ uat_environment: 'Testumgebung für menschliche Abnahmen (URL, leer wenn keine)',
284
288
  exec_mode: 'Ausführungsmodus',
285
289
  exec_manual: 'Manuell — Du rufst Agenten selbst auf',
286
290
  exec_autopilot: 'Autopilot — Nexus verwaltet die gesamte Pipeline',
@@ -367,6 +371,7 @@ const LANGUAGES = {
367
371
  selected_packs: 'Packs selecionados',
368
372
  enter_name: 'Seu nome (os agentes usam para te cumprimentar)',
369
373
  comm_language: 'Idioma de comunicação',
374
+ uat_environment: 'Ambiente de teste para receitas de aceitação humana (URL, vazio se não houver)',
370
375
  exec_mode: 'Modo de execução',
371
376
  exec_manual: 'Manual — Você chama os agentes',
372
377
  exec_autopilot: 'Autopilot — Nexus gerencia todo o pipeline',
@@ -453,6 +458,7 @@ const LANGUAGES = {
453
458
  selected_packs: 'Выбранные пакеты',
454
459
  enter_name: 'Ваше имя (агенты используют его для приветствия)',
455
460
  comm_language: 'Язык общения',
461
+ uat_environment: 'Тестовая среда для ручной приёмки (URL, пусто если нет)',
456
462
  exec_mode: 'Режим выполнения',
457
463
  exec_manual: 'Ручной — Вы вызываете агентов сами',
458
464
  exec_autopilot: 'Автопилот — Nexus управляет всем конвейером',
@@ -539,6 +545,7 @@ const LANGUAGES = {
539
545
  selected_packs: '已选择的包',
540
546
  enter_name: '您的名字(代理会用它来问候您)',
541
547
  comm_language: '沟通语言',
548
+ uat_environment: '人工验收的测试环境(URL,没有则留空)',
542
549
  exec_mode: '执行模式',
543
550
  exec_manual: '手动 — 您自己调用代理',
544
551
  exec_autopilot: '自动驾驶 — Nexus 管理整个流程',
@@ -625,6 +632,7 @@ const LANGUAGES = {
625
632
  selected_packs: 'חבילות שנבחרו',
626
633
  enter_name: 'השם שלך (הסוכנים משתמשים בו לברכך)',
627
634
  comm_language: 'שפת תקשורת',
635
+ uat_environment: 'סביבת בדיקה לבדיקות קבלה אנושיות (כתובת URL, ריק אם אין)',
628
636
  exec_mode: 'מצב ביצוע',
629
637
  exec_manual: 'ידני — אתה קורא לסוכנים בעצמך',
630
638
  exec_autopilot: 'טייס אוטומטי — Nexus מנהל את כל הצינור',
@@ -711,6 +719,7 @@ const LANGUAGES = {
711
719
  selected_packs: '選択されたパック',
712
720
  enter_name: 'あなたの名前(エージェントが挨拶に使います)',
713
721
  comm_language: 'コミュニケーション言語',
722
+ uat_environment: '人による受け入れ確認のテスト環境(URL、なければ空欄)',
714
723
  exec_mode: '実行モード',
715
724
  exec_manual: '手動 — 自分でエージェントを呼び出す',
716
725
  exec_autopilot: 'オートパイロット — Nexusがパイプライン全体を管理',
@@ -797,6 +806,7 @@ const LANGUAGES = {
797
806
  selected_packs: 'Pack selezionati',
798
807
  enter_name: 'Il tuo nome (gli agenti lo usano per salutarti)',
799
808
  comm_language: 'Lingua di comunicazione',
809
+ uat_environment: 'Ambiente di test per le verifiche di accettazione umana (URL, vuoto se nessuno)',
800
810
  exec_mode: 'Modalità di esecuzione',
801
811
  exec_manual: 'Manuale — Chiami tu gli agenti',
802
812
  exec_autopilot: 'Autopilot — Nexus gestisce l\'intera pipeline',
@@ -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
@@ -40,6 +40,12 @@ function collectInstallationHealth({
40
40
  inventory: { status: 'unknown', trackedFiles: 0 },
41
41
  packs: [],
42
42
  integrations: [],
43
+ processExecution: DERIVED.diagnostics.processExecution
44
+ ? {
45
+ ...DERIVED.diagnostics.processExecution,
46
+ availability: 'not-probed',
47
+ }
48
+ : null,
43
49
  runtimes: [],
44
50
  checks: [],
45
51
  summary: { passed: 0, warnings: 0, errors: 0 },
Binary file