bmad-plus 0.14.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.
- package/CHANGELOG.md +52 -0
- package/README.md +34 -15
- package/SECURITY.md +71 -0
- package/THIRD-PARTY-LICENSES.md +349 -0
- package/osint-agent-package/README.md +1 -1
- package/package.json +9 -3
- package/readme-international/README.de.md +14 -8
- package/readme-international/README.es.md +15 -9
- package/readme-international/README.fr.md +14 -8
- package/src/bmad-plus/agents/agent-architect-dev/SKILL.md +11 -13
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +147 -8
- package/src/bmad-plus/agents/agent-quality/SKILL.md +41 -11
- package/src/bmad-plus/data/role-triggers.yaml +19 -0
- package/src/bmad-plus/module-help.csv +1 -0
- package/src/bmad-plus/module.yaml +1 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +3 -1
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +2 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +5 -1
- package/src/bmad-plus/packs/pack-memory/README.md +29 -4
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +21 -1
- package/src/bmad-plus/packs/pack-memory/shared/karpathy-guardrails.md +3 -3
- package/src/bmad-plus/packs/pack-memory/shared/memory-protocol.md +27 -3
- package/src/bmad-plus/packs/pack-memory/zecher-agent.md +18 -2
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +47 -10
- package/src/bmad-plus/skills/bmad-plus-parallel/SKILL.md +17 -3
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +76 -67
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +144 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-results.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-spec.schema.json +121 -0
- package/src/bmad-plus/skills/bmad-plus-uat/ref/uat-triage.schema.json +60 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +552 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/strings.json +362 -0
- package/tools/build/check-install-contract.js +202 -4
- package/tools/build/generate.js +16 -0
- package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
- package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
- package/tools/build/generated-adapters/AGENTS.md +1 -1
- package/tools/build/generated-adapters/CLAUDE.md +1 -1
- package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
- package/tools/build/generated-adapters/GEMINI.md +1 -1
- package/tools/cli/bmad-plus-cli.js +15 -12
- package/tools/cli/commands/doctor.js +1 -0
- package/tools/cli/commands/install.js +21 -2
- package/tools/cli/commands/memory-journal-cmd.js +119 -19
- package/tools/cli/commands/nexus.js +111 -0
- package/tools/cli/commands/uat.js +389 -0
- package/tools/cli/lib/README-memory-journal.md +19 -8
- package/tools/cli/lib/installation-health.js +6 -0
- package/tools/cli/lib/memory-journal.js +0 -0
- package/tools/cli/lib/memory-outcomes.js +293 -0
- package/tools/cli/lib/memory-store.js +139 -0
- package/tools/cli/lib/nexus-process.js +377 -0
- package/tools/cli/lib/nexus.js +1532 -0
- package/tools/cli/lib/pack-copy.js +39 -11
- package/tools/cli/lib/packs.js +17 -3
- package/tools/cli/lib/uat.js +869 -0
- package/tools/maintain/upstream-candidate.js +456 -0
- package/tools/release/publication-content.js +3 -1
- package/tools/release/supply-chain.js +282 -0
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
|
24
|
-
| Node stdlib only |
|
|
25
|
-
|
|
|
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)` —
|
|
88
|
-
|
|
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
|