ruvnet-brain 4.3.40 → 4.4.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 (48) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +111 -27
  3. package/kb/brain-profile.mjs +17 -2
  4. package/kb/forge-update.mjs +6 -2
  5. package/kb/lifecycle-evidence-retention.mjs +12 -9
  6. package/kb/refresh-run.mjs +17 -1
  7. package/kb/update-storage-transaction.mjs +33 -5
  8. package/package.json +1 -1
  9. package/plugin/.claude-plugin/plugin.json +1 -1
  10. package/plugin/.codex-plugin/plugin.json +1 -1
  11. package/plugin/hooks/codex-hooks.json +2 -2
  12. package/plugin/hooks/hooks.json +1 -1
  13. package/plugin/scripts/capability-registry.mjs +3 -3
  14. package/plugin/scripts/codex-hook-wrapper.mjs +7 -2
  15. package/plugin/scripts/design-wall.sh +1 -0
  16. package/plugin/scripts/ground-before-write.sh +1 -0
  17. package/plugin/scripts/ground-ruvnet.sh +3 -3
  18. package/plugin/scripts/grounding-answer.mjs +129 -0
  19. package/plugin/scripts/grounding-stamp.sh +32 -31
  20. package/plugin/scripts/grounding-turn-evidence.mjs +99 -5
  21. package/plugin/scripts/grounding-turn-gate.mjs +25 -6
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/kling-preflight.sh +1 -0
  24. package/plugin/scripts/learn-capture.sh +1 -0
  25. package/plugin/scripts/project-progression-sources.mjs +16 -4
  26. package/plugin/scripts/project-progression-store.mjs +49 -1
  27. package/plugin/scripts/protect-brain-state.sh +1 -0
  28. package/plugin/scripts/route-dispatch.sh +1 -0
  29. package/plugin/scripts/session-snapshot-hook.mjs +224 -38
  30. package/plugin/scripts/session-start-health.mjs +24 -3
  31. package/plugin/scripts/session-start-update-plane.mjs +1 -1
  32. package/plugin/scripts/update-apply.mjs +22 -2
  33. package/scripts/console-instances.mjs +145 -0
  34. package/scripts/console-runtime-identity.mjs +2 -0
  35. package/scripts/corpus-canary.mjs +130 -18
  36. package/scripts/customer-seams.mjs +84 -0
  37. package/scripts/customer-state-matrix.mjs +363 -0
  38. package/scripts/full-suite-gate.mjs +155 -0
  39. package/scripts/grounding-turn-replay.mjs +11 -3
  40. package/scripts/hook-qualify-core.mjs +346 -0
  41. package/scripts/hook-qualify-hosts.mjs +115 -0
  42. package/scripts/hook-qualify.mjs +101 -0
  43. package/scripts/host-cli.mjs +115 -0
  44. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  45. package/scripts/route-gold-rank.mjs +156 -0
  46. package/scripts/route-index-memory.mjs +51 -0
  47. package/scripts/route-latency-warm.mjs +123 -0
  48. package/scripts/wired-check.mjs +17 -3
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ // scripts/customer-state-matrix.mjs — apply ONE published release, through the REAL customer door, to a
3
+ // COPY of a real installed brain in ONE customer state at a time, and record what happened.
4
+ //
5
+ // WHY (2026-09-28 .. 09-30): three customer-path defects shipped in two days, each invisible to the unit
6
+ // suite and to scripts/corpus-canary.mjs, because the canary exercises exactly ONE machine state — a clean
7
+ // install at the newest approved runtime with no private stores. Customers are not in that state. This
8
+ // harness is the canary's install seam (installCustomer, customerEnv, customerManifestUrl are imported,
9
+ // not re-implemented), widened from one state to a derived matrix:
10
+ //
11
+ // * BASES are real installs: `npm install ruvnet-brain@<v>` + that version's OWN installer + that
12
+ // version's OWN published archive (corpus-canary installCustomer). Runtimes are DERIVED from the live
13
+ // release list as offsets behind the candidate (N-1, N-5, N-11), never hand-typed versions.
14
+ // * STATES are derived one-factor-at-a-time from STATE_AXES: every non-baseline value of every axis is
15
+ // one scenario on top of the N-1 baseline. A state is a mutation of a COPY (APFS clone) of a base.
16
+ // * The DOOR is what customers run: `npx ruvnet-brain@latest --update` = the candidate package's own
17
+ // bin/install.mjs --update (updater self-upgrade, lock, refresh receipt, fallback and host sync all
18
+ // real). The ONE substitution: SOURCE.json's releases/latest origin is a 127.0.0.1 mirror of the REAL
19
+ // release payload whose asset URLs serve the REAL signed bytes downloaded once. Signature, digest,
20
+ // coverage and runtime-identity checks all run as shipped.
21
+ //
22
+ // node scripts/customer-state-matrix.mjs --work <dir> --tag vX.Y.Z --zip <ruvnet-brain.zip> \
23
+ // --release-json <release.json> --package <dir containing node_modules/ruvnet-brain> --list
24
+ // | --base N-1 | --scenario <id> [--node <path>]
25
+ // Scenarios run strictly one at a time; each writes <work>/results/<id>.json.
26
+ import crypto from 'node:crypto';
27
+ import fs from 'node:fs';
28
+ import http from 'node:http';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+ import { spawn, spawnSync } from 'node:child_process';
32
+ import { pathToFileURL } from 'node:url';
33
+ import { customerEnv, customerManifestUrl, installCustomer, installedModules } from './corpus-canary.mjs';
34
+ import { addPrivateStore, resolveRuntime } from './customer-seams.mjs';
35
+ export { resolveRuntime };
36
+ import { doctorSmokeArgs } from './installed-brain-health.mjs';
37
+
38
+ export const REPO = 'stuinfla/ruvnet-brain';
39
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
40
+ const writeJson = (file, value) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); };
41
+ const sha256File = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
42
+
43
+ /**
44
+ * The axes a real customer machine varies on. `baseline` is the clean N-1 customer; every other value
45
+ * becomes exactly one scenario. Runtime values are offsets, resolved against the live release list.
46
+ */
47
+ export const STATE_AXES = Object.freeze([
48
+ { axis: 'runtime', baseline: 'N-1', values: ['N-1', 'N-5', 'N-11'] },
49
+ { axis: 'profile', baseline: 'complete', values: ['complete', 'ruvector'] },
50
+ { axis: 'overlay', baseline: 'none', values: ['none', 'private-store'] },
51
+ { axis: 'leftovers', baseline: 'none', values: ['none', 'redundant-copies', 'two-preserved-generations'] },
52
+ { axis: 'lock', baseline: 'none', values: ['none', 'dead-owner'] },
53
+ { axis: 'transaction', baseline: 'none', values: ['none', 'killed-during-candidate-build', 'killed-after-old-rename'] },
54
+ { axis: 'location', baseline: 'home-cache', values: ['home-cache', 'symlinked-cache', 'symlinked-brain-home', 'relocated-env'] },
55
+ { axis: 'reader', baseline: 'present', values: ['present', 'no-node-modules', 'no-model-cache'] },
56
+ { axis: 'network', baseline: 'online', values: ['online', 'manifest-rate-limited'] },
57
+ // How the update is started: the npx installer door, or the INSTALLED updater run directly (the
58
+ // session-start hint 'cd ~/.cache/ruvnet-brain/kb && node forge-update.mjs --apply', old cron lines).
59
+ { axis: 'door', baseline: 'npx-update', values: ['npx-update', 'installed-updater'] },
60
+ ]);
61
+
62
+ /** One-factor-at-a-time derivation: the baseline scenario plus one scenario per non-baseline value. */
63
+ export function deriveScenarios(axes = STATE_AXES) {
64
+ const baseline = Object.fromEntries(axes.map(({ axis, baseline: value }) => [axis, value]));
65
+ const scenarios = [{ id: 'baseline', state: baseline }];
66
+ for (const { axis, baseline: value, values } of axes) {
67
+ for (const option of values) if (option !== value) scenarios.push({ id: `${axis}=${option}`, state: { ...baseline, [axis]: option } });
68
+ }
69
+ return scenarios;
70
+ }
71
+
72
+ const run = (cmd, args, opts = {}) => {
73
+ const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, ...opts });
74
+ if (r.error || r.status !== 0) throw new Error(`${path.basename(cmd)} ${args.join(' ')} failed: ${r.error?.message || r.stderr || r.stdout}`);
75
+ return r;
76
+ };
77
+ const clone = (from, to) => { fs.mkdirSync(path.dirname(to), { recursive: true }); run('cp', ['-cR', from, to]); };
78
+ const kbOf = (home) => path.join(home, '.cache', 'ruvnet-brain', 'kb');
79
+
80
+ /** Files a public bundle never owns — private overlay probes are measured over exactly these. */
81
+ function privateFileDigests(kbDir, name) {
82
+ return Object.fromEntries(fs.readdirSync(kbDir).filter((f) => f === name || f.startsWith(`${name}.`) || f.startsWith(`${name}-`))
83
+ .sort().map((f) => [f, sha256File(path.join(kbDir, f))]));
84
+ }
85
+
86
+ /** The mutations. Each takes a cloned home and returns facts the judge needs. */
87
+ const PREPARE = {
88
+ runtime: async () => ({}),
89
+ profile: async (value, { home, kbDir, packageRoot }) => {
90
+ if (value !== 'ruvector') return {};
91
+ writeJson(path.join(home, '.config', 'ruvnet-brain', 'settings.json'), { settings: { brainProfile: 'ruvector' } });
92
+ // The candidate package's own profile writer (the Console and the installer call this same function).
93
+ const { applyBrainProfile } = await import(pathToFileURL(path.join(packageRoot, 'kb', 'brain-profile.mjs')).href);
94
+ return { profileApplied: applyBrainProfile(kbDir, 'ruvector').removedStores?.length ?? null };
95
+ },
96
+ overlay: async (value, { kbDir, sandbox, packageRoot }) => (value === 'private-store'
97
+ ? { privateStore: await addPrivateStore({ kbDir, scratch: sandbox, writerRoot: packageRoot }) } : {}),
98
+ leftovers: async (value, { kbDir }) => {
99
+ if (value === 'none') return {};
100
+ const stamp = Date.now();
101
+ if (value === 'redundant-copies') {
102
+ clone(kbDir, `${kbDir}.bak-${stamp}`);
103
+ clone(kbDir, `${kbDir}.install-preserved-${stamp}`);
104
+ } else {
105
+ // Two prior generations the installer preserved (`npx ruvnet-brain --force` twice): each differs
106
+ // from live by one file, exactly like a real older generation would.
107
+ for (const n of [1, 2]) {
108
+ const dir = `${kbDir}.install-preserved-${stamp}${n}`;
109
+ clone(kbDir, dir);
110
+ fs.appendFileSync(path.join(dir, 'capability-cards.md'), `\n<!-- generation ${n} -->\n`);
111
+ }
112
+ }
113
+ return {};
114
+ },
115
+ lock: async (value, { kbDir }) => {
116
+ if (value === 'none') return {};
117
+ // The exact v3 owner record an updater killed by a reboot leaves behind: same host, a pid that is dead.
118
+ const dead = spawnSync(process.execPath, ['-e', 'process.stdout.write(String(process.pid))'], { encoding: 'utf8' });
119
+ const runId = `${Date.now()}-deadbeefdeadbeef`;
120
+ const brainHome = path.dirname(kbDir);
121
+ const lock = path.join(brainHome, `.${path.basename(kbDir)}.refresh-run.lock`);
122
+ writeJson(path.join(lock, 'owner.json'), { schemaVersion: 3, runId, pid: Number(dead.stdout), token: crypto.randomBytes(24).toString('hex'),
123
+ kbDir, brainHome, receiptPath: path.join(brainHome, 'refresh-runs', `${runId}.json`), host: os.hostname(),
124
+ executable: process.execPath, processStart: 'Thu Jan 1 00:00:00 2026', startedAt: new Date().toISOString(),
125
+ receiptSeed: { action: 'nightly', desiredVersion: null } });
126
+ return { deadPid: Number(dead.stdout) };
127
+ },
128
+ transaction: async (value, { kbDir }) => {
129
+ if (value === 'none') return {};
130
+ // The likely interruption: the REAL door, SIGKILLed (laptop lid, Ctrl-C, a TTL supervisor) while it
131
+ // is building the sibling candidate — the longest phase. runScenario performs that first run.
132
+ if (value === 'killed-during-candidate-build') return { killFirstRunWhen: `${path.basename(kbDir)}.next-` };
133
+ // A REAL crash: the installed runtime's own runStorageTransaction, killed right after it renamed live away.
134
+ const source = `${kbDir}.crash-source`;
135
+ clone(kbDir, source);
136
+ fs.appendFileSync(path.join(source, 'capability-cards.md'), '\n<!-- interrupted candidate -->\n');
137
+ const driver = path.join(path.dirname(kbDir), 'crash-driver.mjs');
138
+ fs.writeFileSync(driver, `import { runStorageTransaction } from ${JSON.stringify(pathToFileURL(path.join(kbDir, 'update-storage-transaction.mjs')).href)};
139
+ runStorageTransaction({ liveDir: ${JSON.stringify(kbDir)}, sourceDir: ${JSON.stringify(source)}, transactionId: 'crash-${Date.now()}',
140
+ checkpoint: (state) => { if (state === 'OLD_RENAMED') process.kill(process.pid, 'SIGKILL'); } });\n`);
141
+ const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
142
+ fs.rmSync(driver); fs.rmSync(source, { recursive: true, force: true });
143
+ return { crashSignal: r.signal, liveMissingAfterCrash: !fs.existsSync(kbDir) };
144
+ },
145
+ location: async (value, { home }) => {
146
+ const cache = path.join(home, '.cache');
147
+ if (value === 'symlinked-cache') {
148
+ fs.renameSync(cache, path.join(home, 'external-cache'));
149
+ fs.symlinkSync(path.join(home, 'external-cache'), cache);
150
+ } else if (value === 'symlinked-brain-home') {
151
+ fs.renameSync(path.join(cache, 'ruvnet-brain'), path.join(home, 'external-brain'));
152
+ fs.symlinkSync(path.join(home, 'external-brain'), path.join(cache, 'ruvnet-brain'));
153
+ } else if (value === 'relocated-env') {
154
+ fs.renameSync(path.join(cache, 'ruvnet-brain'), path.join(home, 'external-brain'));
155
+ return { env: { RUVNET_BRAIN_HOME: path.join(home, 'external-brain'), RUVNET_BRAIN_KB: path.join(home, 'external-brain', 'kb') } };
156
+ }
157
+ return {};
158
+ },
159
+ reader: async (value, { kbDir }) => {
160
+ if (value === 'no-node-modules') fs.rmSync(path.join(kbDir, 'node_modules'), { recursive: true, force: true });
161
+ if (value === 'no-model-cache') fs.rmSync(path.join(path.dirname(kbDir), 'models'), { recursive: true, force: true });
162
+ return {};
163
+ },
164
+ door: async () => ({}),
165
+ network: async (value) => (value === 'manifest-rate-limited' ? { mirror: { manifestStatus: 403 } } : {}),
166
+ };
167
+
168
+ /** A 127.0.0.1 GitHub: the REAL release payload, asset URLs re-pointed at the REAL bytes on disk. */
169
+ async function startMirror({ release, zip, manifestStatus = 200 }) {
170
+ const files = { 'ruvnet-brain.zip': zip, 'ruvnet-brain.zip.sig': `${zip}.sig` };
171
+ const hits = [];
172
+ const server = http.createServer((req, res) => {
173
+ hits.push(req.url);
174
+ if (req.url === `/repos/${REPO}/releases/latest`) {
175
+ if (manifestStatus !== 200) { res.writeHead(manifestStatus, { 'content-type': 'application/json' }).end('{"message":"API rate limit exceeded"}'); return; }
176
+ const origin = `http://127.0.0.1:${server.address().port}`;
177
+ const assets = release.assets.map((a) => (files[a.name] ? { ...a, browser_download_url: `${origin}/dl/${a.name}` } : a));
178
+ res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ...release, assets }));
179
+ return;
180
+ }
181
+ const name = decodeURIComponent((/^\/dl\/(.+)$/.exec(req.url) || [])[1] || '');
182
+ if (files[name]) { res.writeHead(200, { 'content-length': fs.statSync(files[name]).size }); fs.createReadStream(files[name]).pipe(res); return; }
183
+ res.writeHead(404).end('not mirrored');
184
+ });
185
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
186
+ return { origin: `http://127.0.0.1:${server.address().port}`, hits, close: () => new Promise((r) => server.close(r)) };
187
+ }
188
+
189
+ /** The one change the harness makes to an install: the origin of the channel it already polls. */
190
+ export function pointAtMirror(kbDir, origin) {
191
+ const file = path.join(kbDir, 'SOURCE.json');
192
+ const source = readJson(file);
193
+ const expected = customerManifestUrl({ repo: REPO });
194
+ if (source.canonicalManifestUrl !== expected) throw new Error(`installed SOURCE.json polls ${source.canonicalManifestUrl}, not ${expected}`);
195
+ writeJson(file, { ...source, canonicalManifestUrl: customerManifestUrl({ apiBase: origin, repo: REPO }) });
196
+ }
197
+
198
+ function newestRefreshReceipt(brainHome) {
199
+ const dir = path.join(brainHome, 'refresh-runs');
200
+ if (!fs.existsSync(dir)) return null;
201
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => path.join(dir, f));
202
+ files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
203
+ return files.length ? readJson(files[0]) : null;
204
+ }
205
+
206
+ /** A real query through the installed reader, then the installed citation verifier. */
207
+ async function searchWorks({ kbDir, brainHome, nodeBin, repo }) {
208
+ const args = doctorSmokeArgs(kbDir).map((a) => (a === 'ruvnet-brain' ? repo : a));
209
+ const r = spawnSync(nodeBin, args, { cwd: kbDir, encoding: 'utf8', timeout: 600_000,
210
+ env: { ...process.env, KB_MODEL_CACHE: path.join(brainHome, 'models'), RUVNET_BRAIN_QUERY_DEADLINE_MS: '240000' } });
211
+ if (r.status !== 0 || !String(r.stdout).trim()) return { ok: false, detail: `exit ${r.status} ${r.signal || ''} ${String(r.stderr).slice(-300)}` };
212
+ try {
213
+ const { verifyGrounding } = await import(pathToFileURL(path.join(kbDir, 'verify-citation.mjs')).href);
214
+ const v = await verifyGrounding(r.stdout, kbDir);
215
+ return { ok: v.grounded === true, detail: v.grounded ? v.receipt.path : v.reason };
216
+ } catch (error) { return { ok: false, detail: `verifier: ${error.message}` }; }
217
+ }
218
+
219
+ function siblings(kbDir) {
220
+ try { return fs.readdirSync(path.dirname(fs.realpathSync(kbDir))).filter((n) => n.includes(path.basename(kbDir)) && n !== path.basename(kbDir)); }
221
+ catch { return ['(kb parent unreadable)']; }
222
+ }
223
+
224
+ export async function runScenario({ scenario, work, tag, zip, release, packageRoot, bases, nodeBin = process.execPath }) {
225
+ const live = path.join(work, 'live');
226
+ const home = path.join(live, 'home');
227
+ fs.rmSync(live, { recursive: true, force: true });
228
+ const runtime = bases[scenario.state.runtime];
229
+ clone(path.join(runtime.dir, 'home'), home);
230
+ fs.mkdirSync(path.join(live, 'tmp'), { recursive: true });
231
+ let kbDir = kbOf(home);
232
+ const facts = { sandbox: live, packageRoot, home, kbDir };
233
+ const prepared = {};
234
+ for (const { axis } of STATE_AXES) Object.assign(prepared, await PREPARE[axis](scenario.state[axis], facts));
235
+ const env = { ...customerEnv({ home, work: live }), PATH: [path.dirname(nodeBin), '/usr/bin', '/bin'].join(path.delimiter), ...(prepared.env || {}) };
236
+ kbDir = env.RUVNET_BRAIN_KB;
237
+ const brainHome = env.RUVNET_BRAIN_HOME;
238
+ const mirror = await startMirror({ release, zip, ...(prepared.mirror || {}) });
239
+ // A crash between the two renames leaves no live tree; touching the rollback copy would falsify the
240
+ // very receipt identity recovery checks, so that state is measured as found.
241
+ const hasLive = fs.existsSync(path.join(kbDir, 'SOURCE.json'));
242
+ const before = hasLive ? { source: readJson(path.join(kbDir, 'SOURCE.json')), generations: readJson(path.join(kbDir, 'RVF-GENERATIONS.json')),
243
+ modules: installedModules(kbDir), private: prepared.privateStore ? privateFileDigests(kbDir, prepared.privateStore.name) : null }
244
+ : { source: {}, generations: { stores: null }, modules: [], private: null };
245
+ if (hasLive) pointAtMirror(kbDir, mirror.origin);
246
+ // Apparent bytes under the sandbox (home + TMPDIR). The volume is shared with other work, so statfs
247
+ // deltas are noise; du over the sandbox is this run's own footprint (APFS clones count at full size).
248
+ const du = () => Number(spawnSync('du', ['-sk', live], { encoding: 'utf8' }).stdout.split(/\s/)[0]) * 1024;
249
+ const bytes0 = du();
250
+ let peakBytes = bytes0;
251
+ const sampler = setInterval(() => { peakBytes = Math.max(peakBytes, du()); }, 10_000);
252
+ const openDoor = (killWhen = null) => new Promise((resolve) => {
253
+ const direct = scenario.state.door === 'installed-updater';
254
+ const child = spawn(nodeBin, direct ? ['forge-update.mjs', '--apply'] : [path.join(packageRoot, 'bin', 'install.mjs'), '--update', '--no-nightly-prompt'],
255
+ { cwd: direct ? kbDir : home, env, stdio: ['ignore', 'pipe', 'pipe'], detached: Boolean(killWhen) });
256
+ let output = '';
257
+ let watch = null;
258
+ if (killWhen) {
259
+ watch = setInterval(() => {
260
+ let names = [];
261
+ try { names = fs.readdirSync(fs.realpathSync(path.dirname(kbDir))); } catch { /* mid-rename */ }
262
+ if (names.some((n) => n.startsWith(killWhen))) {
263
+ clearInterval(watch);
264
+ setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already gone */ } }, 5000);
265
+ }
266
+ }, 500);
267
+ }
268
+ child.stdout.on('data', (c) => { output += c; });
269
+ child.stderr.on('data', (c) => { output += c; });
270
+ child.on('close', (code, signal) => { if (watch) clearInterval(watch); resolve({ code, signal, output }); });
271
+ });
272
+ if (prepared.killFirstRunWhen) {
273
+ const first = await openDoor(prepared.killFirstRunWhen);
274
+ prepared.firstRun = { exit: first.code, signal: first.signal, siblings: siblings(kbDir) };
275
+ fs.writeFileSync(path.join(work, 'results', `${scenario.id}.first-run.log`), first.output);
276
+ }
277
+ const started = Date.now();
278
+ const door = await openDoor();
279
+ clearInterval(sampler);
280
+ const wallMs = Date.now() - started;
281
+ await mirror.close();
282
+ fs.writeFileSync(path.join(work, 'results', `${scenario.id}.log`), door.output);
283
+ const receipt = newestRefreshReceipt(brainHome);
284
+ const after = fs.existsSync(path.join(kbDir, 'SOURCE.json')) ? readJson(path.join(kbDir, 'SOURCE.json')) : null;
285
+ const generationsAfter = fs.existsSync(path.join(kbDir, 'RVF-GENERATIONS.json')) ? readJson(path.join(kbDir, 'RVF-GENERATIONS.json')) : null;
286
+ const update = receipt?.phases?.find((p) => p.phase === 'update');
287
+ const search = after ? await searchWorks({ kbDir, brainHome, nodeBin, repo: scenario.state.profile === 'ruvector' ? 'ruvector' : 'ruvnet-brain' }) : { ok: false, detail: 'no KB' };
288
+ return {
289
+ id: scenario.id, state: scenario.state, runtime: runtime.version, candidate: tag, node: spawnSync(nodeBin, ['--version'], { encoding: 'utf8' }).stdout.trim(),
290
+ prepared: { ...prepared, env: undefined, mirror: undefined, killFirstRunWhen: undefined },
291
+ door: { exit: door.code, signal: door.signal, fallback: /falling back to a fresh install|authenticated staged recovery/.test(door.output) ? 'fired' : 'no',
292
+ lastLines: door.output.split('\n').filter((l) => /✗|ERROR|refus|fail|DONE|warn|⚠/i.test(l)).slice(-12) },
293
+ refreshReceipt: receipt ? { status: receipt.status, terminalVerdict: receipt.terminalVerdict,
294
+ updateTerminalVerdict: update?.evidence?.terminalVerdict ?? update?.evidence?.updateResult?.terminalVerdict ?? null,
295
+ failedPhase: receipt.phases?.find((p) => p.status === 'FAIL')?.phase || null,
296
+ updaterReason: receipt.phases?.find((p) => p.status === 'FAIL')?.evidence?.updateResult?.reason?.slice(0, 600) || null } : null,
297
+ storesAdvanced: Boolean(after) && after.releaseTag === tag && after.brainVersion === tag.slice(1)
298
+ && JSON.stringify(generationsAfter?.stores) !== JSON.stringify(before.generations.stores),
299
+ sourceBefore: { releaseTag: before.source.releaseTag, brainVersion: before.source.brainVersion },
300
+ sourceAfter: after ? { releaseTag: after.releaseTag, brainVersion: after.brainVersion } : null,
301
+ privateByteIdentical: before.private ? JSON.stringify(privateFileDigests(kbDir, prepared.privateStore.name)) === JSON.stringify(before.private) : null,
302
+ readerModulesKept: before.modules.every((m) => installedModules(kbDir).includes(m)),
303
+ search, wallClockSec: Math.round(wallMs / 1000), peakDiskGB: Number((peakBytes / 1e9).toFixed(2)), peakGrowthGB: Number(((peakBytes - bytes0) / 1e9).toFixed(2)),
304
+ kbSiblingsAfter: siblings(kbDir), mirrorHits: mirror.hits.length,
305
+ };
306
+ }
307
+
308
+ function arg(argv, name) { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; }
309
+
310
+ async function main(argv = process.argv.slice(2)) {
311
+ const work = path.resolve(arg(argv, '--work'));
312
+ const tag = arg(argv, '--tag');
313
+ const release = readJson(arg(argv, '--release-json'));
314
+ const zip = path.resolve(arg(argv, '--zip'));
315
+ const packageRoot = path.join(path.resolve(arg(argv, '--package')), 'node_modules', 'ruvnet-brain');
316
+ const releases = JSON.parse(run('gh', ['api', `repos/${REPO}/releases?per_page=100`]).stdout);
317
+ const scenarios = deriveScenarios();
318
+ if (argv.includes('--list')) { for (const s of scenarios) console.log(s.id); return 0; }
319
+ const offset = arg(argv, '--base');
320
+ if (offset) {
321
+ const version = resolveRuntime(offset, { candidateTag: tag, releases });
322
+ const dir = path.join(work, 'bases', version);
323
+ if (fs.existsSync(path.join(dir, 'home'))) { console.log(`base ${offset} = ${version} exists`); return 0; }
324
+ // Installed AT the scenario path, then moved: every absolute path the installer wrote stays valid
325
+ // when a scenario clones it back to <work>/live.
326
+ const live = path.join(work, 'live');
327
+ fs.rmSync(live, { recursive: true, force: true });
328
+ const started = Date.now();
329
+ fs.mkdirSync(path.join(live, 'home'), { recursive: true });
330
+ installCustomer({ approvedVersion: version, work: live, home: path.join(live, 'home'),
331
+ log: (text) => fs.appendFileSync(path.join(work, `base-${version}.log`), `${text}\n`) });
332
+ fs.mkdirSync(dir, { recursive: true });
333
+ fs.renameSync(path.join(live, 'home'), path.join(dir, 'home'));
334
+ writeJson(path.join(dir, 'base.json'), { offset, version, installedSec: Math.round((Date.now() - started) / 1000) });
335
+ fs.rmSync(live, { recursive: true, force: true });
336
+ console.log(`base ${offset} = ${version} installed`);
337
+ return 0;
338
+ }
339
+ const id = arg(argv, '--scenario');
340
+ const found = scenarios.find((s) => s.id === id);
341
+ // --runtime crosses one scenario with another runtime offset (e.g. an old install started the old way).
342
+ const scenario = found && arg(argv, '--runtime') ? { id: `${found.id}+runtime=${arg(argv, '--runtime')}`, state: { ...found.state, runtime: arg(argv, '--runtime') } } : found;
343
+ if (!scenario) throw new Error(`unknown scenario ${id}; --list shows them`);
344
+ const bases = {};
345
+ for (const off of new Set(scenarios.map((s) => s.state.runtime))) {
346
+ const version = resolveRuntime(off, { candidateTag: tag, releases });
347
+ bases[off] = { version, dir: path.join(work, 'bases', version) };
348
+ }
349
+ if (!fs.existsSync(path.join(bases[scenario.state.runtime].dir, 'home'))) throw new Error(`base ${scenario.state.runtime} is not installed; run --base first`);
350
+ fs.mkdirSync(path.join(work, 'results'), { recursive: true });
351
+ // The Node axis is the interpreter itself: the same state under another node gets its own result id.
352
+ const nodeBin = arg(argv, '--node') || process.execPath;
353
+ if (arg(argv, '--node')) scenario.id = `${scenario.id}@node${spawnSync(nodeBin, ['-p', 'process.versions.node.split(".")[0]'], { encoding: 'utf8' }).stdout.trim()}`;
354
+ const result = await runScenario({ scenario, work, tag, zip, release, packageRoot, bases, nodeBin });
355
+ writeJson(path.join(work, 'results', `${scenario.id}.json`), result);
356
+ console.log(JSON.stringify(result, null, 2));
357
+ if (!argv.includes('--keep')) fs.rmSync(path.join(work, 'live'), { recursive: true, force: true });
358
+ return 0;
359
+ }
360
+
361
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
362
+ main().then((code) => { process.exitCode = code; }, (error) => { console.error(error.stack || error.message); process.exitCode = 1; });
363
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // full-suite-gate.mjs — the WHOLE vitest suite as a blocking check (docs/WORK-REGISTER.md #26).
3
+ //
4
+ // Why: release qualification runs a reviewed 26-file plan; ~550 other test files ran in no automatic
5
+ // gate, so a red test could sit on main unseen (tests/unit/corpus-canary.test.mjs went red in v4.3.40
6
+ // and nothing noticed). This gate runs every file the root vitest.config.mjs includes and FAILS on:
7
+ // 1. any failing test that is not in tests/known-red.json (the reviewed quarantine),
8
+ // 2. any quarantine entry whose test now PASSES (stale: remove it, so the list cannot rot),
9
+ // 3. any test file that errored at file level (import/collect failure: zero cases still counts),
10
+ // 4. any tests/**/*.test.mjs file that neither the root include nor the quarantine's `excludedFiles`
11
+ // accounts for (a file outside `include` is silently never run — the vacuous-green failure).
12
+ // A quarantined test that is skipped on this platform is neither red nor stale; it is reported.
13
+ // Deterministic by design: Agentic-QE's `aqe quality-gate` needs a frontier judge (inconclusive
14
+ // without a provider), which is the wrong tool for a pass/fail over executed test results.
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const key = (file, name) => `${file} :: ${name}`;
23
+
24
+ export function listTestFiles(root = ROOT, dir = 'tests') {
25
+ return fs.readdirSync(path.join(root, dir), { withFileTypes: true }).flatMap((entry) => {
26
+ const rel = `${dir}/${entry.name}`;
27
+ if (entry.isDirectory()) return listTestFiles(root, rel);
28
+ return /\.test\.[cm]?js$/.test(entry.name) ? [rel] : [];
29
+ }).sort();
30
+ }
31
+
32
+ /** Glob → RegExp for the subset vitest.config.mjs uses (`**`, `*`). */
33
+ export function globToRegExp(glob) {
34
+ const body = glob.split('**/').map((part) => part.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')).join('(?:.*/)?');
35
+ return new RegExp(`^${body}$`);
36
+ }
37
+
38
+ /** Unquarantined failing test files in a report — the only files the single isolated retry reruns. */
39
+ export function redFiles(report, quarantine, root = ROOT) {
40
+ const rel = (name) => path.relative(root, name).split(path.sep).join('/');
41
+ const known = new Set((quarantine.tests || []).map((entry) => key(entry.file, entry.test)));
42
+ return [...new Set((report?.testResults || []).filter((file) => (file.assertionResults || [])
43
+ .some((test) => test.status === 'failed' && !known.has(key(rel(file.name), test.fullName)))).map((file) => rel(file.name)))];
44
+ }
45
+
46
+ /** A failure whose MESSAGE is a timing failure, and nothing else: (1) vitest's own test/hook timeout,
47
+ * or (2) a NUMERIC bound comparison ("expected 3745 to be less than 1000") in a message that also
48
+ * carries a duration cue (an ms value, p95/p99, took/elapsed/duration/latency/budget/ceiling/timeout).
49
+ * Budget words alone never qualify: "expected 'capture budget exceeded' to be undefined" is a value
50
+ * assertion and stays RED. Matched on the failure text only, never the test title. */
51
+ const VITEST_TIMEOUT = /\b(?:Test|Hook) timed out in \d+ ?ms\b/;
52
+ const NUMERIC_BOUND = /\bexpected -?\d+(?:\.\d+)? to be (?:less|greater) than (?:or equal to )?-?\d+(?:\.\d+)?/;
53
+ const DURATION_CUE = /\d ?ms\b|\bp9[59]\b|\btook\b|\belapsed\b|\bduration\b|\blatency\b|\bbudget\b|\bceiling\b|\btimeout\b/i;
54
+ export const isTimingFailure = (message) => {
55
+ const text = String(message || '').slice(0, 2000);
56
+ return VITEST_TIMEOUT.test(text) || (NUMERIC_BOUND.test(text) && DURATION_CUE.test(text));
57
+ };
58
+ /** More flaky tests than this in one run fails the gate: load-sensitivity that wide is itself a defect. */
59
+ export const MAX_FLAKY = 3;
60
+
61
+ export function evaluateFullSuite({ report, quarantine, files, include, root = ROOT, retry = null, maxFlaky = MAX_FLAKY }) {
62
+ const problems = [];
63
+ const rel = (name) => path.relative(root, name).split(path.sep).join('/');
64
+ // One isolated retry of the red files. A test is FLAKY only when (a) its full-run failure is a
65
+ // timing/budget failure (isTimingFailure) AND (b) it passed in the isolated retry. Measured 2026-10-01:
66
+ // budget/latency tests went red in full runs at load ~100-150 and passed alone. Anything else red —
67
+ // a value assertion that passes alone may be a real shared-state bug — stays RED. Every FLAKY is
68
+ // recorded with its message, and more than maxFlaky of them fails the gate.
69
+ const retried = new Map();
70
+ for (const file of retry?.testResults || []) for (const test of file.assertionResults || []) retried.set(key(rel(file.name), test.fullName), test.status);
71
+ const flaky = [];
72
+ const tests = quarantine.tests || [];
73
+ const known = new Map(tests.map((entry) => [key(entry.file, entry.test), entry]));
74
+ if (known.size !== tests.length) problems.push('tests/known-red.json lists a test twice');
75
+ for (const entry of tests) if (!entry.reason || !entry.class || !entry.owner) problems.push(`quarantine entry lacks class/reason/owner: ${key(entry.file, entry.test)}`);
76
+ const excluded = new Map((quarantine.excludedFiles || []).map((entry) => [entry.file, entry]));
77
+ const included = include.map(globToRegExp);
78
+ const outside = files.filter((file) => !included.some((re) => re.test(file)));
79
+ for (const file of outside) if (!excluded.has(file)) problems.push(`never run: ${file} is outside vitest include and not in excludedFiles`);
80
+ for (const file of excluded.keys()) if (!outside.includes(file)) problems.push(`excludedFiles entry is not an excluded test file: ${file}`);
81
+ const expected = files.filter((file) => !outside.includes(file));
82
+ const results = report?.testResults;
83
+ if (!Array.isArray(results)) return { verdict: 'FAIL', problems: [...problems, 'no vitest JSON report'] };
84
+ const ran = new Set(results.map((file) => rel(file.name)));
85
+ for (const file of expected) if (!ran.has(file)) problems.push(`included file did not execute: ${file}`);
86
+ const seen = new Set();
87
+ const counts = { files: results.length, passed: 0, failed: 0, quarantinedRed: 0, skipped: 0, todo: 0 };
88
+ for (const file of results) {
89
+ const name = rel(file.name);
90
+ const cases = file.assertionResults || [];
91
+ if (file.status === 'failed' && !cases.some((test) => test.status === 'failed')) {
92
+ problems.push(`file-level failure: ${name}: ${String(file.message || '').split('\n')[0].slice(0, 200)}`);
93
+ }
94
+ for (const test of cases) {
95
+ const id = key(name, test.fullName);
96
+ const entry = known.get(id);
97
+ if (entry) seen.add(id);
98
+ if (test.status === 'passed') {
99
+ counts.passed += 1;
100
+ if (entry) problems.push(`stale quarantine (now passes, remove it): ${id}`);
101
+ } else if (test.status === 'failed') {
102
+ counts.failed += 1;
103
+ const message = String((test.failureMessages || [''])[0]);
104
+ if (entry) counts.quarantinedRed += 1;
105
+ else if (retried.get(id) === 'passed' && isTimingFailure(message)) {
106
+ flaky.push({ test: id, retry: 'passed', failure: message.split('\n')[0].slice(0, 240) });
107
+ } else problems.push(`RED: ${id}: ${message.split('\n')[0].slice(0, 240)}`);
108
+ } else if (test.status === 'todo') counts.todo += 1;
109
+ else counts.skipped += 1;
110
+ }
111
+ }
112
+ if (flaky.length > maxFlaky) problems.push(`too many flaky tests: ${flaky.length} > ${maxFlaky} timing failures that passed alone`);
113
+ counts.flaky = flaky.length;
114
+ const unobserved = [...known.keys()].filter((id) => !seen.has(id));
115
+ return { verdict: problems.length ? 'FAIL' : 'PASS', problems, counts, flaky, unobserved };
116
+ }
117
+
118
+ /** Markdown for the GitHub job summary: the verdict, every problem and every FLAKY, never truncated. */
119
+ export function summaryMarkdown(result) {
120
+ const c = result.counts || {};
121
+ return [`## Full suite: ${result.verdict}`, '',
122
+ `${c.files ?? '?'} files · ${c.passed ?? '?'} passed · ${c.failed ?? '?'} failed (${c.quarantinedRed ?? 0} quarantined) · ${c.flaky ?? 0} flaky · ${c.skipped ?? '?'} skipped · ${c.todo ?? '?'} todo`, '',
123
+ ...(result.problems.length ? ['### Problems', ...result.problems.map((p) => `- ${p}`), ''] : []),
124
+ ...(result.flaky?.length ? ['### Flaky (timing failure in the full run, passed alone)', ...result.flaky.map((f) => `- ${f.test} — ${f.failure}`), ''] : []),
125
+ ].join('\n');
126
+ }
127
+
128
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
129
+ const args = process.argv.slice(2);
130
+ const value = (flag) => { const i = args.indexOf(flag); return i < 0 ? null : args[i + 1]; };
131
+ const out = value('--report') || path.join(os.tmpdir(), `full-suite-${process.pid}.json`);
132
+ const vitest = (files, file) => {
133
+ const run = spawnSync(process.execPath, ['node_modules/vitest/vitest.mjs', 'run', ...files, '--reporter=dot', '--reporter=json',
134
+ `--outputFile.json=${file}`], { cwd: ROOT, stdio: 'inherit', env: { ...process.env, RUVNET_TURN_CAPTURE: 'off' } });
135
+ if (run.error) { console.error(run.error.message); process.exit(1); }
136
+ };
137
+ if (!value('--from-report')) vitest([], out);
138
+ // --root judges a report produced in another checkout of the same tree (its paths, its file list).
139
+ const root = path.resolve(value('--root') || ROOT);
140
+ const report = JSON.parse(fs.readFileSync(value('--from-report') || out, 'utf8'));
141
+ const quarantine = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests/known-red.json'), 'utf8'));
142
+ const { default: config } = await import(path.join(ROOT, 'vitest.config.mjs'));
143
+ let retry = value('--retry-report') ? JSON.parse(fs.readFileSync(value('--retry-report'), 'utf8')) : null;
144
+ const reds = redFiles(report, quarantine, root);
145
+ if (reds.length && !value('--from-report')) {
146
+ const retryOut = `${out}.retry.json`;
147
+ console.log(`retrying ${reds.length} red file(s) once, in isolation: ${reds.join(' ')}`);
148
+ vitest(reds, retryOut);
149
+ retry = JSON.parse(fs.readFileSync(retryOut, 'utf8'));
150
+ }
151
+ const result = evaluateFullSuite({ report, quarantine, files: listTestFiles(root), include: config.test.include, root, retry });
152
+ console.log(JSON.stringify(result, null, 2));
153
+ if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summaryMarkdown(result)}\n`);
154
+ process.exitCode = result.verdict === 'PASS' ? 0 : 1;
155
+ }
@@ -23,7 +23,7 @@ import path from 'node:path';
23
23
  import { transcripts, turnsOf, finalText } from './completion-claim-replay.mjs';
24
24
  import { armFor } from '../plugin/scripts/grounding-turn-mark.mjs';
25
25
  import {
26
- architectureShadow, auditAssertions, loadVocabulary, relayShadow, searchedThisTurn, turnSources,
26
+ architectureShadow, auditAssertions, loadVocabulary, relayShadow, ruvCapabilityClaims, searchedThisTurn, turnSources,
27
27
  } from '../plugin/scripts/grounding-turn-evidence.mjs';
28
28
 
29
29
  const argv = process.argv.slice(2);
@@ -83,7 +83,7 @@ if (at) {
83
83
  sources: turn.sources.map((x) => `${x.order} ${x.kind} ${x.strength} ${String(x.ref).slice(0, 60)}`) }, null, 2));
84
84
  process.exit(0);
85
85
  }
86
- const r = { root, transcripts: 0, turns: 0, armed: 0, armedGate1: 0, blocked: 0, gate1WouldFire: 0,
86
+ const r = { root, transcripts: 0, turns: 0, armed: 0, armedGate1: 0, blocked: 0, gate1WouldFire: 0, gate1ArmedUnsearched: 0,
87
87
  shadowArchitecture: 0, shadowRelay: 0, claims: 0, ms: [] };
88
88
  const samples = [];
89
89
  for (const file of [...transcripts(root, limit), ...(withSubagents ? subagentFiles() : [])]) {
@@ -98,7 +98,15 @@ for (const file of [...transcripts(root, limit), ...(withSubagents ? subagentFil
98
98
  const arm = unarmed ? { gate1: false, assert: true, architecture: false, subjects: [] }
99
99
  : armFor({ hook_event_name: 'UserPromptSubmit', session_id: 'replay', prompt: turn.prompt }, vocab);
100
100
  if (!arm) { r.ms.push(performance.now() - t0); continue; }
101
- if (arm.gate1) { r.armedGate1 += 1; if (!searchedThisTurn(turn.sources)) r.gate1WouldFire += 1; }
101
+ // gate1WouldFire = the 4.4.0 rule (an unsearched turn whose answer asserts a rUv capability);
102
+ // gate1ArmedUnsearched = what fired before 4.4.0 (every armed turn without a search).
103
+ if (arm.gate1) {
104
+ r.armedGate1 += 1;
105
+ if (!searchedThisTurn(turn.sources)) {
106
+ r.gate1ArmedUnsearched += 1;
107
+ if (ruvCapabilityClaims(message).length) r.gate1WouldFire += 1;
108
+ }
109
+ }
102
110
  let audit = { claims: [], findings: [] };
103
111
  if (arm.assert) {
104
112
  r.armed += 1;