ruvnet-brain 4.3.39 → 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 (49) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +150 -42
  3. package/kb/brain-profile.mjs +17 -2
  4. package/kb/forge-update.mjs +92 -28
  5. package/kb/lifecycle-evidence-retention.mjs +12 -9
  6. package/kb/refresh-run.mjs +17 -1
  7. package/kb/update-storage-transaction.mjs +44 -10
  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/coverage-integrity.mjs +51 -4
  16. package/plugin/scripts/design-wall.sh +1 -0
  17. package/plugin/scripts/ground-before-write.sh +1 -0
  18. package/plugin/scripts/ground-ruvnet.sh +3 -3
  19. package/plugin/scripts/grounding-answer.mjs +129 -0
  20. package/plugin/scripts/grounding-stamp.sh +32 -31
  21. package/plugin/scripts/grounding-turn-evidence.mjs +99 -5
  22. package/plugin/scripts/grounding-turn-gate.mjs +25 -6
  23. package/plugin/scripts/hook-shim.mjs +3 -0
  24. package/plugin/scripts/kling-preflight.sh +1 -0
  25. package/plugin/scripts/learn-capture.sh +1 -0
  26. package/plugin/scripts/project-progression-sources.mjs +16 -4
  27. package/plugin/scripts/project-progression-store.mjs +49 -1
  28. package/plugin/scripts/protect-brain-state.sh +1 -0
  29. package/plugin/scripts/route-dispatch.sh +1 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +224 -38
  31. package/plugin/scripts/session-start-health.mjs +24 -3
  32. package/plugin/scripts/session-start-update-plane.mjs +1 -1
  33. package/plugin/scripts/update-apply.mjs +22 -2
  34. package/scripts/console-instances.mjs +145 -0
  35. package/scripts/console-runtime-identity.mjs +2 -0
  36. package/scripts/corpus-canary.mjs +130 -18
  37. package/scripts/customer-seams.mjs +84 -0
  38. package/scripts/customer-state-matrix.mjs +363 -0
  39. package/scripts/full-suite-gate.mjs +155 -0
  40. package/scripts/grounding-turn-replay.mjs +11 -3
  41. package/scripts/hook-qualify-core.mjs +346 -0
  42. package/scripts/hook-qualify-hosts.mjs +115 -0
  43. package/scripts/hook-qualify.mjs +101 -0
  44. package/scripts/host-cli.mjs +115 -0
  45. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  46. package/scripts/route-gold-rank.mjs +156 -0
  47. package/scripts/route-index-memory.mjs +51 -0
  48. package/scripts/route-latency-warm.mjs +123 -0
  49. package/scripts/wired-check.mjs +17 -3
@@ -42,7 +42,8 @@ import crypto from 'node:crypto';
42
42
  import fs from 'node:fs';
43
43
  import path from 'node:path';
44
44
  import { spawn, spawnSync } from 'node:child_process';
45
- import { pathToFileURL } from 'node:url';
45
+ import { fileURLToPath, pathToFileURL } from 'node:url';
46
+ import { addPrivateStore, resolveRuntime, resultFromRefreshReceipt, runInstallerDoor } from './customer-seams.mjs';
46
47
  import { CANARY_VERDICT_KIND, CORPUS_TAG_PATTERN, REQUIRED_CANARY_CHECKS } from './corpus-promotion.mjs';
47
48
 
48
49
  export const PUBLIC_API = 'https://api.github.com';
@@ -284,7 +285,7 @@ export function installCustomer({ approvedVersion, work, home, log = () => {} })
284
285
  runLogged(process.execPath, [installer, '--yes', '--force', '--version', `v${approvedVersion}`, '--no-nightly-prompt',
285
286
  '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck'],
286
287
  { env, cwd: home, timeoutMs: 1_800_000, log });
287
- return { kbDir: env.RUVNET_BRAIN_KB };
288
+ return { kbDir: env.RUVNET_BRAIN_KB, home };
288
289
  }
289
290
 
290
291
  function runUpdater({ kbDir, env, resultFile }) {
@@ -310,17 +311,35 @@ async function fetchRelease({ apiBase, repo, tag }) {
310
311
  * installer); the unit suite hands in a customer tree built offline so the updater, the judge and the
311
312
  * verdict run for real without a 555 MB download.
312
313
  */
313
- export async function runCanary({
314
- repo, tag, approvedVersion, work, apiBase = PUBLIC_API, install = installCustomer, home = path.join(work, 'home'),
315
- env = process.env, now = () => Date.now(), retryDelayMs = 30_000, maxAttempts = 3, log = () => {},
316
- }) {
317
- if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
318
- if (!CORPUS_TAG_PATTERN.test(String(tag || ''))) throw new Error('--tag must be corpus-sha256-<64 hex>');
319
- if (!SEMVER.test(String(approvedVersion || ''))) throw new Error('--approved-version must be X.Y.Z');
314
+ /**
315
+ * THE CUSTOMER STATES THE CANARY ASKS ABOUT. `clean` is the original (and default) case. The others are
316
+ * the two states 2026-09-30 showed are not covered by it (scripts/customer-state-matrix.mjs measured
317
+ * both on real releases): a machine carrying a private overlay, and a machine installed at an OLDER
318
+ * runtime that reaches the candidate through `npx ruvnet-brain@latest --update`. Each extra case is a
319
+ * fresh install in its own directory, run AFTER the clean case and deleted before the next, so peak disk
320
+ * stays one install + one apply. Its checks are the same judge's, prefixed `<case>:`; any failure is a FAIL.
321
+ */
322
+ export const CANARY_CASES = Object.freeze({
323
+ clean: Object.freeze({ runtime: 'approved', door: 'updater', overlay: false }),
324
+ 'private-overlay': Object.freeze({ runtime: 'approved', door: 'updater', overlay: true }),
325
+ 'older-runtime': Object.freeze({ runtime: 'N-1', door: 'installer', overlay: false }),
326
+ });
327
+
328
+ async function fetchReleaseList({ apiBase, repo }) {
329
+ const response = await fetch(`${apiBase}/repos/${repo}/releases?per_page=100`, { headers: { accept: 'application/vnd.github+json' } });
330
+ if (!response.ok) throw new Error(`release list returned HTTP ${response.status}`);
331
+ return response.json();
332
+ }
333
+
334
+ async function runCase({ name, spec, repo, tag, approvedVersion, release, work, home, apiBase, install, now,
335
+ retryDelayMs, maxAttempts, log, writerRoot, approvedInstaller }) {
320
336
  fs.mkdirSync(home, { recursive: true });
321
- const started = now();
322
- const { kbDir } = await install({ approvedVersion, work, home, log });
323
- const release = await fetchRelease({ apiBase, repo, tag });
337
+ let runtime = approvedVersion;
338
+ if (spec.runtime !== 'approved') runtime = resolveRuntime(spec.runtime, { candidateTag: `v${approvedVersion}`, releases: await fetchReleaseList({ apiBase, repo }) });
339
+ const installed = await install({ approvedVersion: runtime, work, home, log });
340
+ const { kbDir } = installed;
341
+ home = installed.home || home;
342
+ const overlay = spec.overlay ? await addPrivateStore({ kbDir, scratch: work, writerRoot }) : null;
324
343
  const beforeModules = installedModules(kbDir);
325
344
  const beforeCoverage = coverageRows(path.join(kbDir, 'CORPUS-COVERAGE.json'));
326
345
  const { before } = pointUpdaterAtCandidate({ kbDir, apiBase, repo, tag });
@@ -331,15 +350,107 @@ export async function runCanary({
331
350
  let updater = { exitCode: null, output: '', attempts: 0 };
332
351
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
333
352
  fs.rmSync(resultFile, { force: true });
334
- const run = await runUpdater({ kbDir, env: childEnv, resultFile });
353
+ const run = spec.door === 'installer'
354
+ ? await runInstallerDoor({ installer: await approvedInstaller(), env: childEnv, cwd: home })
355
+ : await runUpdater({ kbDir, env: childEnv, resultFile });
335
356
  updater = { ...run, attempts: attempt };
336
- log(`--- forge-update.mjs --apply (attempt ${attempt}) exit ${run.exitCode}\n${run.output}`);
357
+ log(`--- [${name}] ${spec.door === 'installer' ? 'install.mjs --update' : 'forge-update.mjs --apply'} (attempt ${attempt}) exit ${run.exitCode}\n${run.output}`);
337
358
  if (run.exitCode !== NETWORK_EXIT || attempt === maxAttempts) break;
338
359
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
339
360
  }
340
- updater.result = fs.existsSync(resultFile) ? readJson(resultFile) : null;
361
+ updater.result = spec.door === 'installer' ? resultFromRefreshReceipt(childEnv.RUVNET_BRAIN_HOME)
362
+ : (fs.existsSync(resultFile) ? readJson(resultFile) : null);
341
363
  const judged = await judgeCanary({ tag, approvedVersion, release, updater, before, beforeModules, beforeCoverage,
342
364
  kbDir: fs.realpathSync(kbDir), home: fs.realpathSync(home), now: now() });
365
+ if (overlay) {
366
+ const after = Object.fromEntries(Object.keys(overlay.digests).map((f) => [f,
367
+ fs.existsSync(path.join(kbDir, f)) ? sha256File(path.join(kbDir, f)) : null]));
368
+ const changed = Object.keys(after).filter((f) => after[f] !== overlay.digests[f]);
369
+ judged.checks.push({ name: 'private-store-preserved', ok: changed.length === 0,
370
+ detail: changed.length ? `private files changed or lost: ${changed.join(', ')}` : `${Object.keys(after).length} private file(s) byte-identical` });
371
+ }
372
+ return { updater, checks: judged.checks, runtime };
373
+ }
374
+
375
+ const physical = (dir) => { try { return fs.realpathSync(dir); } catch { return path.resolve(dir); } };
376
+
377
+ /**
378
+ * `--installed-kb`: the clean case runs on the supplied KB (as before); every extra case gets its OWN copy
379
+ * of that KB as it was BEFORE the clean case touched it, under the case's work dir, so the supplied brain
380
+ * never gains the overlay case's private store or a rewritten PRIVATE-STORES.json.
381
+ *
382
+ * DISK: copies are reflink clones (COPYFILE_FICLONE) where the filesystem supports it (APFS, btrfs, XFS
383
+ * with reflink) and cost almost nothing; elsewhere (ext4, most CI runners) they are full copies. Then the
384
+ * peak is the supplied KB + one snapshot + one case copy (each case's copy is deleted before the next),
385
+ * i.e. about 3x the KB (~4 GB for a 1.3 GB brain). Hardlinks are not used: an update that rewrote a
386
+ * file in place would write through into the caller's brain.
387
+ */
388
+ export function suppliedKbInstaller({ installedKb, work, cases }) {
389
+ const supplied = path.resolve(installedKb);
390
+ const snapshot = path.join(work, 'supplied-kb-snapshot');
391
+ const copy = (from, to) => fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, mode: fs.constants.COPYFILE_FICLONE });
392
+ if (cases.length > 1) { fs.rmSync(snapshot, { recursive: true, force: true }); copy(supplied, snapshot); }
393
+ return ({ work: caseWork, home }) => {
394
+ if (path.resolve(caseWork) === path.resolve(work)) return { kbDir: supplied };
395
+ const kbDir = path.join(home, '.cache', 'ruvnet-brain', 'kb');
396
+ fs.mkdirSync(path.dirname(kbDir), { recursive: true });
397
+ copy(snapshot, kbDir);
398
+ return { kbDir, home };
399
+ };
400
+ }
401
+
402
+ export async function runCanary({
403
+ repo, tag, approvedVersion, work, apiBase = PUBLIC_API, install = installCustomer, home = path.join(work, 'home'),
404
+ env = process.env, now = () => Date.now(), retryDelayMs = 30_000, maxAttempts = 3, log = () => {},
405
+ cases = ['clean'], writerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
406
+ approvedInstaller = null,
407
+ }) {
408
+ if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
409
+ if (!CORPUS_TAG_PATTERN.test(String(tag || ''))) throw new Error('--tag must be corpus-sha256-<64 hex>');
410
+ if (!SEMVER.test(String(approvedVersion || ''))) throw new Error('--approved-version must be X.Y.Z');
411
+ if (cases[0] !== 'clean' || cases.some((name) => !CANARY_CASES[name])) {
412
+ throw new Error(`--cases must start with clean and name only: ${Object.keys(CANARY_CASES).join(', ')}`);
413
+ }
414
+ const started = now();
415
+ const release = await fetchRelease({ apiBase, repo, tag });
416
+ // The approved package's installer — what `npx ruvnet-brain@latest` runs — fetched once, on demand.
417
+ let installerPath = null;
418
+ const resolveInstaller = approvedInstaller || (async () => {
419
+ if (installerPath) return installerPath;
420
+ const prefix = path.join(work, 'approved-npx');
421
+ fs.mkdirSync(prefix, { recursive: true });
422
+ const npm = path.join(path.dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
423
+ runLogged(npm, ['install', '--prefix', prefix, '--no-audit', '--no-fund', '--no-save', `ruvnet-brain@${approvedVersion}`],
424
+ { env: customerEnv({ home, work }), cwd: prefix, timeoutMs: 600_000, log });
425
+ installerPath = path.join(prefix, 'node_modules', 'ruvnet-brain', 'bin', 'install.mjs');
426
+ return installerPath;
427
+ });
428
+ const common = { repo, tag, approvedVersion, release, apiBase, install, now, retryDelayMs, maxAttempts, log, writerRoot,
429
+ approvedInstaller: resolveInstaller };
430
+ let cleanKb = null;
431
+ const clean = await runCase({ ...common, name: 'clean', spec: CANARY_CASES.clean, work, home,
432
+ install: async (args) => { const installed = await install(args); cleanKb = physical(installed.kbDir); return installed; } });
433
+ const { updater } = clean;
434
+ const judged = { checks: [...clean.checks] };
435
+ for (const name of cases.slice(1)) {
436
+ const caseWork = path.join(work, `case-${name}`);
437
+ let outcome;
438
+ try {
439
+ // An extra case adds a private store and rewrites PRIVATE-STORES.json: it must never do that to the
440
+ // clean case's KB (with --installed-kb, the caller's own brain). Each case gets its own tree.
441
+ const caseInstall = async (args) => {
442
+ const installed = await install(args);
443
+ if (physical(installed.kbDir) === cleanKb) throw new Error(`case ${name} was handed the clean case's KB (${installed.kbDir}); refusing to mutate it`);
444
+ return installed;
445
+ };
446
+ outcome = await runCase({ ...common, install: caseInstall, name, spec: CANARY_CASES[name], work: caseWork, home: path.join(caseWork, 'home') });
447
+ } catch (error) {
448
+ outcome = { checks: [{ name: 'case-ran', ok: false, detail: error.message }] };
449
+ }
450
+ for (const entry of outcome.checks) judged.checks.push({ ...entry, name: `${name}:${entry.name}` });
451
+ fs.rmSync(caseWork, { recursive: true, force: true });
452
+ }
453
+ judged.verdict = judged.checks.every((entry) => entry.ok) ? 'PASS' : 'FAIL';
343
454
  return {
344
455
  schemaVersion: 1,
345
456
  kind: CANARY_VERDICT_KIND,
@@ -365,18 +476,19 @@ async function main(argv = process.argv.slice(2)) {
365
476
  const work = opt('--work') && path.resolve(opt('--work'));
366
477
  const verdictOut = opt('--verdict-out') && path.resolve(opt('--verdict-out'));
367
478
  if (!work || !verdictOut) {
368
- process.stderr.write('usage: corpus-canary.mjs --repo o/n --tag corpus-sha256-<hex> --approved-version X.Y.Z --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>]\n');
479
+ process.stderr.write('usage: corpus-canary.mjs --repo o/n --tag corpus-sha256-<hex> --approved-version X.Y.Z --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>] [--cases clean,private-overlay,older-runtime]\n');
369
480
  return 2;
370
481
  }
371
482
  fs.mkdirSync(work, { recursive: true });
372
483
  const logFile = path.join(work, 'canary.log');
373
484
  const log = (text) => { fs.appendFileSync(logFile, `${text}\n`); };
374
485
  const installedKb = opt('--installed-kb');
375
- const install = installedKb ? () => ({ kbDir: path.resolve(installedKb) }) : installCustomer;
486
+ const cases = String(opt('--cases') || 'clean').split(',').map((name) => name.trim()).filter(Boolean);
487
+ const install = installedKb ? suppliedKbInstaller({ installedKb, work, cases }) : installCustomer;
376
488
  let record;
377
489
  try {
378
490
  record = await runCanary({ repo: opt('--repo'), tag: opt('--tag'), approvedVersion: opt('--approved-version'), work,
379
- apiBase: opt('--api-base') || PUBLIC_API, install, home: path.resolve(opt('--home') || path.join(work, 'home')), log });
491
+ apiBase: opt('--api-base') || PUBLIC_API, install, home: path.resolve(opt('--home') || path.join(work, 'home')), log, cases });
380
492
  } catch (error) {
381
493
  // Could not even reach a judgement = the consumer did not accept. Still a written verdict.
382
494
  record = { schemaVersion: 1, kind: CANARY_VERDICT_KIND, verdict: 'FAIL', repo: opt('--repo') || null, tag: opt('--tag') || null,
@@ -0,0 +1,84 @@
1
+ // scripts/customer-seams.mjs — the customer-side seams shared by scripts/corpus-canary.mjs (the nightly
2
+ // consumer gate) and scripts/customer-state-matrix.mjs (the same seams across many machine states). One copy.
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { pathToFileURL } from 'node:url';
8
+
9
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
10
+ const sha256File = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
11
+
12
+ const SEMVER_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
13
+ /** `N-k`: the k-th published (non-draft) semver code release behind `candidateTag`, from a live release list. */
14
+ export function resolveRuntime(offset, { candidateTag, releases }) {
15
+ const match = /^N-(\d+)$/.exec(offset);
16
+ if (!match) throw new Error(`runtime offset must be N-<k>: ${offset}`);
17
+ const key = (tag) => SEMVER_TAG.exec(tag).slice(1).map(Number);
18
+ const cmp = (a, b) => { const [x, y] = [key(a), key(b)]; return y[0] - x[0] || y[1] - x[1] || y[2] - x[2]; };
19
+ const tags = releases.filter((r) => !r.draft && SEMVER_TAG.test(r.tag_name || '')).map((r) => r.tag_name).sort(cmp);
20
+ const at = tags.indexOf(candidateTag);
21
+ if (at < 0) throw new Error(`candidate ${candidateTag} is not a published code release`);
22
+ const tag = tags[at + Number(match[1])];
23
+ if (!tag) throw new Error(`no published release ${offset} behind ${candidateTag}`);
24
+ return tag.slice(1);
25
+ }
26
+
27
+ /**
28
+ * A customer's PRIVATE store: the smallest real public store family, renamed, stamped by the product's
29
+ * own writer (scripts/private-overlay.mjs) with an alias and a capability card — the same shape the
30
+ * owner's 108 private stores have. Exercises capture -> candidate restore -> coverage convergence.
31
+ */
32
+ export async function addPrivateStore({ kbDir, scratch, writerRoot, name = 'acme-private-notes' }) {
33
+ const ledger = readJson(path.join(kbDir, 'RVF-GENERATIONS.json')).stores;
34
+ const donor = Object.entries(ledger).filter(([, g]) => /\.big\.rvf$/.test(g.file || ''))
35
+ .sort(([, a], [, b]) => a.bytes - b.bytes)[0][0];
36
+ const from = path.join(scratch, 'private-sidecars');
37
+ fs.mkdirSync(from, { recursive: true });
38
+ for (const file of fs.readdirSync(kbDir).filter((f) => f.startsWith(`${donor}.`) || f === `${donor}-primer.md`)) {
39
+ fs.copyFileSync(path.join(kbDir, file), path.join(from, file.replace(donor, name)));
40
+ }
41
+ const { applyPrivateOverlay } = await import(pathToFileURL(path.join(writerRoot, 'scripts', 'private-overlay.mjs')).href);
42
+ const card = path.join(scratch, `${name}.card.md`);
43
+ fs.writeFileSync(card, `Private notes (a renamed copy of ${donor}). Reach for it only for this customer's own material.\n`);
44
+ // The fence comes first, exactly as the writer demands of a customer (PRIVATE-STORES.json).
45
+ const fenceFile = path.join(kbDir, 'PRIVATE-STORES.json');
46
+ const fence = fs.existsSync(fenceFile) ? readJson(fenceFile) : { privateStores: [] };
47
+ fs.writeFileSync(fenceFile, `${JSON.stringify({ ...fence, privateStores: [...new Set([...(fence.privateStores || []), name])] }, null, 2)}\n`);
48
+ applyPrivateOverlay({ root: kbDir, from, stores: [name], aliases: { [name]: [`${name}-alias`] }, cards: { [name]: card } });
49
+ const digests = Object.fromEntries(fs.readdirSync(kbDir).filter((f) => f.startsWith(`${name}.`) || f.startsWith(`${name}-`))
50
+ .sort().map((f) => [f, sha256File(path.join(kbDir, f))]));
51
+ return { name, donor, digests };
52
+ }
53
+
54
+ /**
55
+ * The `npx ruvnet-brain@latest --update` door: the APPROVED package's own installer, which self-upgrades
56
+ * the installed updater, re-stamps the runtime, takes the refresh lock and runs the updater. Fallback is
57
+ * disabled for the same reason the clean case never uses this door: a refused candidate must not turn
58
+ * into a green fresh install. The updater's receipt is reconstructed from the refresh-run receipt the
59
+ * installer settles (the installer deletes its private result file).
60
+ */
61
+ export function runInstallerDoor({ installer, env, cwd }) {
62
+ return new Promise((resolve) => {
63
+ const child = spawn(process.execPath, [installer, '--update', '--no-nightly-prompt'],
64
+ { cwd, env: { ...env, RUVNET_BRAIN_NO_UPDATE_FALLBACK: '1' }, stdio: ['ignore', 'pipe', 'pipe'] });
65
+ let output = '';
66
+ child.stdout.on('data', (chunk) => { output += chunk; });
67
+ child.stderr.on('data', (chunk) => { output += chunk; });
68
+ child.on('error', (error) => resolve({ exitCode: null, output: `${output}\n${error.message}` }));
69
+ child.on('close', (exitCode) => resolve({ exitCode, output }));
70
+ });
71
+ }
72
+
73
+ export function resultFromRefreshReceipt(brainHome) {
74
+ const dir = path.join(brainHome, 'refresh-runs');
75
+ if (!fs.existsSync(dir)) return null;
76
+ const newest = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => path.join(dir, f))
77
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
78
+ if (!newest) return null;
79
+ const receipt = readJson(newest);
80
+ const phase = (name) => receipt.phases?.find((p) => p.phase === name)?.evidence || {};
81
+ return { terminalVerdict: receipt.status === 'SUCCEEDED' ? phase('update').terminalVerdict || null : receipt.terminalVerdict,
82
+ bundleSha256: phase('bundle-assembly').bundleSha256 || null, coverageSha256: phase('coverage-generation').coverageSha256 || null,
83
+ refreshStatus: receipt.status };
84
+ }