ruvnet-brain 4.3.21 → 4.3.26

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 (143) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +275 -60
  3. package/console/app.js +189 -9
  4. package/console/index.html +70 -24
  5. package/console/scope.css +137 -0
  6. package/console/scope.html +144 -0
  7. package/console/scope.js +209 -0
  8. package/console/style.css +26 -0
  9. package/console/tips.html +1 -0
  10. package/kb/corpus-release-identity.mjs +239 -0
  11. package/kb/update-storage-transaction.mjs +20 -3
  12. package/package.json +9 -2
  13. package/plugin/.claude-plugin/plugin.json +2 -2
  14. package/plugin/.codex-plugin/plugin.json +1 -1
  15. package/plugin/commands/checkpoint.md +61 -0
  16. package/plugin/hooks/codex-hooks.json +64 -1
  17. package/plugin/hooks/hook-contracts.json +299 -6
  18. package/plugin/hooks/hooks.json +81 -1
  19. package/plugin/mcp/server.mjs +23 -0
  20. package/plugin/scripts/advocacy-catalog.mjs +245 -0
  21. package/plugin/scripts/advocacy-route.mjs +460 -0
  22. package/plugin/scripts/continuation-gate.mjs +25 -2
  23. package/plugin/scripts/continuation-objective.mjs +7 -1
  24. package/plugin/scripts/continuity-hook-policy.mjs +190 -15
  25. package/plugin/scripts/coverage-integrity.mjs +7 -0
  26. package/plugin/scripts/gates.mjs +113 -10
  27. package/plugin/scripts/grounding-turn-gate.mjs +167 -0
  28. package/plugin/scripts/grounding-turn-mark.mjs +91 -0
  29. package/plugin/scripts/hook-shim.mjs +14 -0
  30. package/plugin/scripts/nightly-scheduler.mjs +37 -4
  31. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  32. package/plugin/scripts/project-progression-contract.mjs +16 -0
  33. package/plugin/scripts/project-progression-hook.mjs +3 -0
  34. package/plugin/scripts/project-progression-producer.mjs +252 -0
  35. package/plugin/scripts/project-progression-reader.mjs +271 -0
  36. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  37. package/plugin/scripts/project-progression-sources.mjs +220 -0
  38. package/plugin/scripts/project-progression-store.mjs +106 -13
  39. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  40. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  41. package/plugin/scripts/session-start-budget.mjs +59 -0
  42. package/plugin/scripts/session-start-core.mjs +234 -457
  43. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  44. package/plugin/scripts/session-start-health.mjs +64 -0
  45. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  46. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  47. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  48. package/plugin/scripts/session-start-signals.mjs +73 -0
  49. package/plugin/scripts/session-start-trace.mjs +86 -0
  50. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  51. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  52. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  53. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  54. package/scripts/adr-072-completion.mjs +1 -1
  55. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  56. package/scripts/approved-runtime.mjs +197 -0
  57. package/scripts/brain-novice-50.mjs +16 -1
  58. package/scripts/brain-score.mjs +23 -5
  59. package/scripts/build-bundle.mjs +971 -530
  60. package/scripts/build-concepts.mjs +36 -116
  61. package/scripts/console-engine.test.mjs +8 -7
  62. package/scripts/console-runtime-identity.mjs +4 -0
  63. package/scripts/corpus-aggregates.mjs +94 -77
  64. package/scripts/corpus-candidate.mjs +475 -222
  65. package/scripts/corpus-next-seed.mjs +225 -0
  66. package/scripts/corpus-promotion.mjs +58 -0
  67. package/scripts/corpus-reconcile.mjs +411 -105
  68. package/scripts/doc-currency.mjs +16 -1
  69. package/scripts/dual-host-deliberation.mjs +25 -2
  70. package/scripts/dual-host-suggest.mjs +17 -1
  71. package/scripts/falsify.mjs +13 -3
  72. package/scripts/gist-receipts.mjs +482 -87
  73. package/scripts/github-health-watch.mjs +12 -2
  74. package/scripts/handoff-asset.mjs +34 -0
  75. package/scripts/hook-retirement-check.mjs +8 -1
  76. package/scripts/host-registry.mjs +1 -1
  77. package/scripts/ingest-gists.mjs +74 -101
  78. package/scripts/job-heartbeat.sh +77 -14
  79. package/scripts/learning-replay-execution.mjs +10 -4
  80. package/scripts/nightly-gists.sh +27 -13
  81. package/scripts/nightly-two-run-proof.mjs +1 -1
  82. package/scripts/nightly-watchdog.mjs +61 -4
  83. package/scripts/onboarding-console.mjs +364 -28
  84. package/scripts/oracle/produce-questions.mjs +293 -0
  85. package/scripts/oracle/producer-hosts.mjs +235 -0
  86. package/scripts/oracle/repo-recall.mjs +448 -0
  87. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  88. package/scripts/oracle/source-tree.mjs +165 -0
  89. package/scripts/oracle/source-units.mjs +391 -0
  90. package/scripts/oracle/spike-run.mjs +98 -0
  91. package/scripts/oracle/unit-inventory.mjs +141 -0
  92. package/scripts/oracle/unit-sampling.mjs +128 -0
  93. package/scripts/oracle/validate-labels.mjs +250 -0
  94. package/scripts/private-overlay.mjs +248 -0
  95. package/scripts/product-integrity-contract.mjs +1 -1
  96. package/scripts/proxy/claude-proxied.sh +6 -0
  97. package/scripts/proxy/proxy-revert.sh +5 -0
  98. package/scripts/proxy/proxy-up.sh +6 -0
  99. package/scripts/proxy/proxy-verify.mjs +4 -0
  100. package/scripts/public-inputs.mjs +409 -0
  101. package/scripts/public-verification-inputs.mjs +112 -26
  102. package/scripts/public-verification-lane.mjs +1 -1
  103. package/scripts/published-surface-probe.mjs +34 -4
  104. package/scripts/qe/card-lane-gate.mjs +16 -1
  105. package/scripts/qe/session-start-gate.mjs +16 -1
  106. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  107. package/scripts/record-lesson.mjs +4 -1
  108. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  109. package/scripts/release-abort-stale.mjs +5 -1
  110. package/scripts/release-authority.mjs +104 -12
  111. package/scripts/release-channel-kind.mjs +86 -0
  112. package/scripts/release-convergence-watchdog.mjs +7 -2
  113. package/scripts/release-projection.mjs +177 -72
  114. package/scripts/release-transaction-provider.mjs +47 -10
  115. package/scripts/release-transaction.mjs +40 -11
  116. package/scripts/release.mjs +252 -17
  117. package/scripts/retrieval-canary.mjs +87 -0
  118. package/scripts/rvf-index-audit.mjs +573 -13
  119. package/scripts/rvf-wire.mjs +269 -0
  120. package/scripts/seal-gist-receipt.mjs +65 -0
  121. package/scripts/selfcheck.mjs +42 -21
  122. package/scripts/source-coverage.mjs +253 -24
  123. package/scripts/status-honesty.mjs +25 -0
  124. package/scripts/sync-census.mjs +0 -0
  125. package/scripts/sync-version.mjs +2 -0
  126. package/scripts/trismart.mjs +42 -0
  127. package/scripts/updater-manifest.mjs +162 -0
  128. package/scripts/verify-channels.mjs +17 -5
  129. package/scripts/wired-check.mjs +48 -10
  130. package/tri-smart-skill/QUICKSTART.md +37 -0
  131. package/tri-smart-skill/README.md +92 -0
  132. package/tri-smart-skill/install.cmd +14 -0
  133. package/tri-smart-skill/install.command +13 -0
  134. package/tri-smart-skill/install.mjs +51 -0
  135. package/tri-smart-skill/install.sh +9 -0
  136. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  137. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  138. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  139. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  140. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  141. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  142. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  143. package/scripts/corpus-seed-publish.mjs +0 -110
@@ -33,6 +33,7 @@ import { planFor } from './remedy-registry.mjs';
33
33
  import { auditAll as capabilityAuditAll } from './capability-registry.mjs';
34
34
  import { getVersion } from './version.mjs';
35
35
  import { consoleRuntimeDigest } from './console-runtime-identity.mjs';
36
+ import { isCorpusReleaseTag } from '../kb/corpus-release-identity.mjs';
36
37
  // L5 (ADR-028): the audit is the one place that observes live capability state, so it is where an
37
38
  // OFFERED-then-now-`on` transition becomes an APPLIED — the numerator of the precision metric that
38
39
  // tells the owner whether advocacy is landing or nagging. Both are pure reads/appends and never throw.
@@ -422,6 +423,27 @@ function wiringSurvey() {
422
423
  if (mech) sites.push({ scope: 'project', project: projName, file: '.mcp.json', event: 'MCP', matcher: name, spec: full.slice(0, 160), mechanism: mech });
423
424
  }
424
425
  }
426
+ // THE MACHINE-WIDE FILE (console audit 2026-09-11). Plugins are switched on in ~/.claude/settings.json
427
+ // and nowhere else, and this survey never read that file — so the card said `plugin: 0` on a machine
428
+ // with 14 enabled, ruvnet-brain among them. Each enabled plugin is one global PLUGIN site. The file's
429
+ // own hooks go through the same classifier the project scan uses, so a machine-wide npx or
430
+ // global-binary hook is counted in the same words as a project one.
431
+ const machine = readJSON(path.join(CONSOLE_ROOT, '.claude', 'settings.json'));
432
+ if (machine) {
433
+ for (const [event, groups] of Object.entries(machine.hooks || {})) {
434
+ const list = Array.isArray(groups) ? groups : [groups];
435
+ for (const g of list) {
436
+ const hookArr = Array.isArray(g?.hooks) ? g.hooks : (g?.command ? [g] : []);
437
+ for (const h of hookArr) {
438
+ const mech = classifyCommand(h?.command);
439
+ if (mech) sites.push({ scope: 'global', file: '~/.claude/settings.json', event, matcher: g?.matcher ?? '*', spec: String(h.command).slice(0, 160), mechanism: mech });
440
+ }
441
+ }
442
+ }
443
+ for (const [name, on] of Object.entries(machine.enabledPlugins || {})) {
444
+ if (on === true) sites.push({ scope: 'global', file: '~/.claude/settings.json', event: 'plugin', matcher: name, spec: name, mechanism: 'PLUGIN' });
445
+ }
446
+ }
425
447
  return { sites, summary: summarizeWiring(sites) };
426
448
  }
427
449
 
@@ -444,25 +466,41 @@ function sessionHookExists() {
444
466
  //
445
467
  // So the probe stops guessing and asks the filesystem: consider both known names, and use whichever
446
468
  // actually holds rows. That is correct on their machine and on mine without knowing why they differ.
469
+ //
470
+ // THE SECOND HALF OF THAT LESSON (console audit 2026-09-11). "Whichever holds rows" was implemented
471
+ // as "whichever is LARGER", and on a machine where BOTH hold rows that picked the wrong one: the
472
+ // card scored ruvnet-brain on `.swarm/agentdb-memory.db` — 15,824 rows, the MCP coordination store
473
+ // the global CLAUDE.md warns is a DIFFERENT container — and rendered "15824 entries, 100% embedded"
474
+ // while the canonical `.swarm/memory.db`, the file `ruflo memory store` writes, held 5. The #127
475
+ // reporter's cases (canonical absent, or present but empty) still resolve to the coordination store;
476
+ // a canonical store that holds rows is now always the one scored, and BOTH files are reported so the
477
+ // page can say which it scored and which it did not.
447
478
  const MEMORY_DB_NAMES = ['memory.db', 'agentdb-memory.db'];
479
+ // Row count that never throws and never hangs on a live store: null means "unreadable this instant".
480
+ function memoryRows(file) {
481
+ if (!fs.existsSync(file)) return null;
482
+ const r = robustRead(file, 'SELECT count(*) FROM memory_entries;');
483
+ return r.ok ? (r.value === null ? 0 : parseInt(r.value, 10)) : null;
484
+ }
485
+ export function memoryStores(projectDir) {
486
+ const [canonicalPath, coordinationPath] = MEMORY_DB_NAMES.map((name) => path.join(projectDir, '.swarm', name));
487
+ const canonical = { path: canonicalPath, exists: fs.existsSync(canonicalPath), rows: memoryRows(canonicalPath) };
488
+ const coordination = { path: coordinationPath, exists: fs.existsSync(coordinationPath), rows: memoryRows(coordinationPath) };
489
+ // A canonical store that holds rows — or is busy being written this instant (rows null) — is the
490
+ // one scored. Only an absent or EMPTY canonical store hands the score to the coordination file.
491
+ const scored = canonical.exists && (canonical.rows === null || canonical.rows > 0) ? canonical.path
492
+ : coordination.exists ? coordination.path
493
+ : canonical.path; // canonical name for the "absent" message
494
+ return { canonical, coordination, scored };
495
+ }
448
496
  export function resolveMemoryDb(projectDir) {
449
- const candidates = MEMORY_DB_NAMES
450
- .map((name) => path.join(projectDir, '.swarm', name))
451
- .filter((file) => fs.existsSync(file));
452
- if (!candidates.length) return path.join(projectDir, '.swarm/memory.db'); // canonical name for the "absent" message
453
- if (candidates.length === 1) return candidates[0];
454
- // Both present: prefer the one with real content. Size is a proxy for rows that costs no query and
455
- // cannot throw on a locked or WAL-mode database — this runs inside a UI probe that must never hang.
456
- return candidates.sort((a, b) => {
457
- const sa = (() => { try { return fs.statSync(a).size; } catch { return 0; } })();
458
- const sb = (() => { try { return fs.statSync(b).size; } catch { return 0; } })();
459
- return sb - sa;
460
- })[0];
497
+ return memoryStores(projectDir).scored;
461
498
  }
462
499
 
463
500
  function probeMemory(projectDir, { now = Date.now() } = {}) {
464
- const db = resolveMemoryDb(projectDir);
465
- const probes = {};
501
+ const stores = memoryStores(projectDir);
502
+ const db = stores.scored;
503
+ const probes = { stores };
466
504
  // compaction survival + session surfacing are filesystem facts, always checkable
467
505
  const snapshot = inspectSessionSnapshots(projectDir, { now });
468
506
  const snapshotDetail = snapshot.kind === 'canonical'
@@ -503,8 +541,10 @@ function probeMemory(projectDir, { now = Date.now() } = {}) {
503
541
  const liveNote = integ.mode === 'immutable=1' ? ' and in active use' : '';
504
542
  if (integrity !== 'ok') probes.liveness = { status: 'fail', detail: `store is corrupt (integrity_check: ${integrity})` };
505
543
  else if (total === null) probes.liveness = { status: 'notTested', detail: 'store opened but counts were unavailable this instant' };
506
- else if (total > 0) probes.liveness = { status: 'ok', detail: `store is live${liveNote}, integrity ok, ${total} entries${embedded != null && total ? `, ${Math.round((embedded / total) * 100)}% embedded` : ''} (read-only)` };
507
- else probes.liveness = { status: 'warn', detail: 'store exists but is empty' };
544
+ // The file is NAMED in the sentence, so "15824 entries" can never again be read as the canonical
545
+ // store's count when it was the coordination store's.
546
+ else if (total > 0) probes.liveness = { status: 'ok', detail: `${path.basename(db)} is live${liveNote}, integrity ok, ${total} entries${embedded != null && total ? `, ${Math.round((embedded / total) * 100)}% embedded` : ''} (read-only)` };
547
+ else probes.liveness = { status: 'warn', detail: `${path.basename(db)} exists but is empty` };
508
548
 
509
549
  const cp = robustRead(db, "SELECT max(updated_at) FROM memory_entries WHERE key LIKE 'project-state-current%';");
510
550
  if (cp.ok && cp.value) {
@@ -542,12 +582,20 @@ function gatherMemory(cwd, { fleet = true } = {}) {
542
582
  const scope = projectDirectory({ cwd });
543
583
  const project = fs.existsSync(path.join(scope, '.swarm/memory.db')) ? scope : REPO;
544
584
  const projName = project.replace(CONSOLE_ROOT + '/Code/', '').replace(CONSOLE_ROOT + '/', '~/');
545
- const health = scoreMemoryHealth({ project: projName, probes: probeMemory(project) });
585
+ const probes = probeMemory(project);
586
+ const health = scoreMemoryHealth({ project: projName, probes });
587
+ // Which file the score is about, and which file it deliberately is not — both with row counts.
588
+ const short = (p) => String(p).replace(CONSOLE_ROOT, '~');
589
+ health.stores = probes.stores ? {
590
+ scored: short(probes.stores.scored),
591
+ canonical: { ...probes.stores.canonical, path: short(probes.stores.canonical.path) },
592
+ coordination: { ...probes.stores.coordination, path: short(probes.stores.coordination.path) },
593
+ } : null;
546
594
  return { fleet: fleet ? scanFleet() : null, health };
547
595
  }
548
596
 
549
597
  // ── Savings ledger (receipts only) ────────────────────────────────────────────────────────────────
550
- function gatherSavings() {
598
+ function gatherSavings({ repo = REPO } = {}) {
551
599
  // Primary source is the real routing-receipts ledger written by scripts/route-cheap.mjs.
552
600
  const files = [
553
601
  path.join(SYSTEM_HOME, '.claude/metaharness/routing-receipts.jsonl'),
@@ -555,7 +603,9 @@ function gatherSavings() {
555
603
  // Canonical user-level ledger (issue #36 — the hooks no longer scatter per-CWD copies).
556
604
  path.join(SYSTEM_HOME, '.cache/ruvnet-brain/token-ledger.jsonl'),
557
605
  // Legacy location, still read so an existing user's history is not orphaned by the move.
558
- path.join(REPO, 'plugin/scripts/.ruvnet-brain/token-ledger.jsonl'),
606
+ // `repo` is injectable because on a developer checkout this file is REAL history (2 rows from
607
+ // 2026-07-14 on the owner's machine) and HOME isolation alone cannot keep it out of a fixture.
608
+ path.join(repo, 'plugin/scripts/.ruvnet-brain/token-ledger.jsonl'),
559
609
  ];
560
610
  const receipts = [];
561
611
  let baselineUsd = 0;
@@ -591,10 +641,16 @@ function gatherSavings() {
591
641
  }
592
642
  }
593
643
  const usdSaved = +receipts.reduce((a, r) => a + (r.measuredUsd || 0), 0).toFixed(4);
644
+ const timed = receipts.filter((r) => r.measuredMs !== null);
594
645
  const totals = receipts.length ? {
595
646
  count: receipts.length,
596
647
  usdSaved,
648
+ // SIGNED, and it stays signed. A net-slower ledger sums negative and is REPORTED negative — the
649
+ // page once turned that into a dash (console audit 2026-09-11: −46,737 ms across 25 timed
650
+ // routes, 23 of them slower than baseline, rendered as "—"). A loss is a finding about the
651
+ // router; `timedCount` says how many routes the number is even about.
597
652
  msSaved: receipts.reduce((a, r) => a + (r.measuredMs || 0), 0),
653
+ timedCount: timed.length,
598
654
  baselineUsd: +baselineUsd.toFixed(4),
599
655
  pctSaved: baselineUsd > 0 ? Math.round((usdSaved / baselineUsd) * 100) : null,
600
656
  } : null;
@@ -643,11 +699,86 @@ function unsupportedConfigControls() {
643
699
  * off, and the Settings form shows the shipped default beside it as a recommendation rather than as
644
700
  * a fact about their machine.
645
701
  */
702
+ /**
703
+ * NIGHTLY REFRESH IS THREE FACTS, NOT ONE BOOLEAN (console audit 2026-09-11). The toggle rendered
704
+ * `nightly: true` because the LaunchAgent was loaded, while ~/.claude/ruvnet-brain/config.json said
705
+ * `nightly: false` and the job had never completed a run. Three independently measured things —
706
+ * the CHOICE the user saved (config.json), the ENFORCEMENT the scheduler adapter reports (loaded,
707
+ * off, degraded), and the OUTCOME of the last completed run (its receipt) — had been collapsed into
708
+ * the one the user did not set. A single boolean is emitted only when choice and enforcement agree;
709
+ * otherwise the field is null (never chosen, or in disagreement) and `facts` carries all three so
710
+ * the page can print them side by side. Pure, so the rule is testable without a scheduler.
711
+ */
712
+ export function reconcileNightly({ choice, schedule }) {
713
+ const enforcement = schedule?.state ?? 'unknown';
714
+ const run = schedule?.runHealth ?? null;
715
+ const enforced = enforcement === 'on' ? true : enforcement === 'off' ? false : null;
716
+ const chosen = choice === true || choice === false ? choice : null;
717
+ const agree = chosen === null ? null : (enforced !== null && chosen === enforced);
718
+ return {
719
+ value: agree === true ? chosen : null,
720
+ facts: {
721
+ choice: chosen,
722
+ enforcement,
723
+ enforcementEvidence: schedule?.evidence ?? null,
724
+ lastRun: run?.state ?? 'unknown',
725
+ lastRunEvidence: run?.evidence ?? null,
726
+ lastRunAt: run?.receipt?.finishedAt ?? run?.receipt?.startedAt ?? null,
727
+ agree,
728
+ },
729
+ };
730
+ }
731
+
732
+ // The one thing the console never showed: WHICH repositories are in the brain, by name. It has
733
+ // always carried the count (206) and never the inventory — the owner asked for this list more than
734
+ // a dozen times. RVF-GENERATIONS.json is the manifest that matches disk exactly (verified 206 = 206);
735
+ // SOURCE.json is the provenance subset (104) that carries the upstream URL. Join on store name,
736
+ // never restate either. Ranked by build recency — never alphabetical (relevance-ordering rule).
737
+ function gatherInventory() {
738
+ const generations = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
739
+ const source = readJSON(path.join(INSTALLED_KB, 'SOURCE.json'));
740
+ const stores = generations?.stores && typeof generations.stores === 'object' ? generations.stores : null;
741
+ if (!stores) return { available: false, reason: 'RVF-GENERATIONS.json not readable in the installed brain', count: 0, stores: [] };
742
+ // SOURCE.json has carried `stores` as an array in some generations and a name-keyed object in
743
+ // others; accept both rather than silently joining nothing (measured 2026-09-16: object form,
744
+ // and the array-only read reported withProvenance=0 for a brain with 104 provenance rows).
745
+ const provenance = new Map();
746
+ const sourceRows = Array.isArray(source?.stores) ? source.stores
747
+ : source?.stores && typeof source.stores === 'object'
748
+ ? Object.entries(source.stores).map(([name, row]) => ({ name, ...row })) : [];
749
+ for (const row of sourceRows) {
750
+ if (row?.name) provenance.set(String(row.name).toLowerCase(), row);
751
+ }
752
+ const rows = Object.entries(stores).map(([name, gen]) => {
753
+ const prov = provenance.get(name.toLowerCase());
754
+ return {
755
+ name,
756
+ sourceRepo: prov?.sourceRepo || null,
757
+ sourceCommit: gen?.sourceCommit ? String(gen.sourceCommit).slice(0, 7) : null,
758
+ builtUtc: gen?.builtUtc || null,
759
+ bytes: Number(gen?.bytes) || 0,
760
+ model: gen?.model || null,
761
+ private: prov?.updateManaged === false,
762
+ };
763
+ }).sort((a, b) => String(b.builtUtc || '').localeCompare(String(a.builtUtc || '')) || a.name.localeCompare(b.name));
764
+ return {
765
+ available: true,
766
+ count: rows.length,
767
+ withProvenance: rows.filter((r) => r.sourceRepo).length,
768
+ privateCount: rows.filter((r) => r.private).length,
769
+ brainVersion: generations.brainVersion || null,
770
+ releaseTag: generations.releaseTag || null,
771
+ totalBytes: rows.reduce((sum, r) => sum + r.bytes, 0),
772
+ stores: rows,
773
+ };
774
+ }
775
+
646
776
  function gatherConfig() {
647
777
  const cfg = readJSON(CONFIG_PATH) || {};
648
778
  const credential = openRouterCredentialStatus({ cwd: process.cwd() });
649
779
  const schedule = nightlyStatus();
650
780
  const bool = (v) => (v === true ? true : v === false ? false : null);
781
+ const nightly = reconcileNightly({ choice: bool(cfg.nightly), schedule });
651
782
  const unavailable = unsupportedConfigControls();
652
783
  if (!schedule.artifact.supported) {
653
784
  unavailable.push({
@@ -662,7 +793,7 @@ function gatherConfig() {
662
793
  values: {
663
794
  openrouterKey: credential.configured, // boolean only — never the secret itself
664
795
  provider: typeof cfg.provider === 'string' && cfg.provider ? cfg.provider : null,
665
- nightly: schedule.state === 'on' ? true : schedule.state === 'off' ? false : null,
796
+ nightly: nightly.value,
666
797
  routing: cfg.routing === 'off' ? 'off' : cfg.routing === 'auto' ? 'auto' : null,
667
798
  qeFleet: bool(cfg.qeFleet),
668
799
  },
@@ -675,7 +806,7 @@ function gatherConfig() {
675
806
  unavailable,
676
807
  runtime: {
677
808
  openrouterKey: credential,
678
- nightly: schedule,
809
+ nightly: { ...schedule, facts: nightly.facts },
679
810
  },
680
811
  };
681
812
  }
@@ -791,8 +922,21 @@ function gatherBrainProfile() {
791
922
  : installed.stores.length === 1
792
923
  ? PROFILE_RUVECTOR
793
924
  : PROFILE_COMPLETE;
925
+ // THE "— stores · 0 MB" LIE (console audit 2026-09-11). `choices.complete` was rendered from
926
+ // `source` — the restore BUNDLE at COMPLETE_BRAIN_SOURCE, which on an installed host resolves to
927
+ // <runtime>/dist/ruvnet-brain and never exists after install — while `installed`, measured three
928
+ // lines up (184 stores, 979 MB on the owner's machine), went unused. discoverStoreFamilies()
929
+ // swallows ENOENT into [], so a complete brain rendered as zero and wore an `active` badge over it.
930
+ // Two different questions are now answered separately: what is INSTALLED here, and by which path
931
+ // Apply→complete could RESTORE it here. The path order mirrors saveBrainProfile()'s own branches
932
+ // (local bundle, then the signed forge-update download, then nothing), so the card describes the
933
+ // exact mechanism Apply would take — never a hopeful one, never a number nobody measured.
794
934
  const source = measureBrainProfile(COMPLETE_BRAIN_SOURCE);
935
+ const bundlePresent = source.stores.includes(PROFILE_RUVECTOR) && source.storeCount > 1;
795
936
  const updaterAvailable = fs.existsSync(path.join(INSTALLED_KB, 'forge-update.mjs'));
937
+ const restoreVia = bundlePresent ? 'local-bundle' : updaterAvailable ? 'signed-download' : null;
938
+ const completeInstalled = actual === PROFILE_COMPLETE;
939
+ const restorePath = COMPLETE_BRAIN_SOURCE.replace(CONSOLE_ROOT, '~');
796
940
  return {
797
941
  path: INSTALLED_KB.replace(CONSOLE_ROOT, '~'),
798
942
  values: { brainProfile: actual },
@@ -803,10 +947,20 @@ function gatherBrainProfile() {
803
947
  installed,
804
948
  choices: {
805
949
  complete: {
806
- available: (source.stores.includes(PROFILE_RUVECTOR) && source.storeCount > 1)
807
- || updaterAvailable,
808
- storeCount: source.storeCount,
809
- bytes: source.bytes,
950
+ available: restoreVia !== null,
951
+ // From the INSTALLED brain when complete is what is installed; from the local bundle when one
952
+ // exists to restore from; otherwise null — never a 0 that nothing measured.
953
+ storeCount: completeInstalled ? installed.storeCount : bundlePresent ? source.storeCount : null,
954
+ bytes: completeInstalled ? installed.bytes : bundlePresent ? source.bytes : null,
955
+ installed: completeInstalled ? { storeCount: installed.storeCount, bytes: installed.bytes } : null,
956
+ restoreBundle: {
957
+ present: bundlePresent,
958
+ path: restorePath,
959
+ storeCount: bundlePresent ? source.storeCount : null,
960
+ bytes: bundlePresent ? source.bytes : null,
961
+ },
962
+ updaterAvailable,
963
+ restoreVia,
810
964
  },
811
965
  ruvector: {
812
966
  available: installed.stores.includes(PROFILE_RUVECTOR)
@@ -1960,6 +2114,34 @@ function readInstallChannel() {
1960
2114
  * practice, because the check that did exist only ever triggered a blocking recompute rather than an
1961
2115
  * honest stale-serve.
1962
2116
  */
2117
+ /**
2118
+ * THREE VERSIONS ON ONE PAGE (console audit 2026-09-11): the header chip said v4.3.21 (running
2119
+ * brain), the Install-channel row said v4.3.22 (plugin cache), the installed KB's RVF-GENERATIONS.json
2120
+ * said 4.3.10 — and nothing said they differed. Each is read from its own file and LABELED. `agree`
2121
+ * is true only when every measured one is identical, null when fewer than two could be read. A
2122
+ * version that cannot be read is null, never guessed.
2123
+ */
2124
+ export function versionFacts({ release = null } = {}) {
2125
+ const gen = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
2126
+ const kbGeneration = typeof gen?.brainVersion === 'string' && gen.brainVersion ? gen.brainVersion.replace(/^v/, '') : null;
2127
+ const runningBrain = brainVersionOnDisk();
2128
+ let installedPlugin = null;
2129
+ try { installedPlugin = readInstallChannel().version || null; } catch { installedPlugin = null; }
2130
+ // A CORPUS RELEASE IS NOT A VERSION (ADR-086 step 16). `releases/latest` can now carry a
2131
+ // `corpus-sha256-<64 hex>` content address, and stripping a leading "v" off one yields a 78-char
2132
+ // string that this panel would have printed in the "latest release" slot beside `4.3.22`-shaped
2133
+ // numbers — in the one panel whose whole job is to make version disagreement visible. That is not
2134
+ // a version disagreement; it is a category error, and showing it as one would be the panel lying.
2135
+ // So the two identities are reported in two fields, each labeled for what it actually is.
2136
+ const rawTag = typeof release?.tag === 'string' && release.tag ? release.tag : null;
2137
+ const corpusTag = isCorpusReleaseTag(rawTag) ? rawTag : null;
2138
+ const latestRelease = rawTag && !corpusTag ? rawTag.replace(/^v/, '') : null;
2139
+ const known = [kbGeneration, runningBrain, installedPlugin].filter(Boolean);
2140
+ const agree = known.length < 2 ? null : known.every((x) => x === known[0]);
2141
+ return { kbGeneration, runningBrain, installedPlugin, latestRelease, latestCorpusRelease: corpusTag,
2142
+ agree, known: known.length };
2143
+ }
2144
+
1963
2145
  async function gatherTrust() {
1964
2146
  // COLD ONLY: no successful release read has ever landed, so there is nothing to withhold or serve
1965
2147
  // stale — the one exception serveCached() itself carves out for its own caches.
@@ -1971,7 +2153,7 @@ async function gatherTrust() {
1971
2153
  const generatedAt = new Date().toISOString();
1972
2154
  const data = { generatedAt, release };
1973
2155
  if (release.ok) { TRUST_CACHE = { at: Date.parse(generatedAt), data }; saveConsoleCache(); }
1974
- return { ...data, channel: readInstallChannel(), sbom: readSbom(), ...freshnessOf(generatedAt) };
2156
+ return { ...data, channel: readInstallChannel(), sbom: readSbom(), versions: versionFacts({ release }), ...freshnessOf(generatedAt) };
1975
2157
  }
1976
2158
 
1977
2159
  // WARM — including over-ceiling. Never await the network here; hand back what we measured, say
@@ -1980,7 +2162,7 @@ async function gatherTrust() {
1980
2162
  if (fresh.stale) kickTrustRefresh();
1981
2163
  // Disk facts stay live even when the release read is served from cache — the SBOM file and install
1982
2164
  // channel can change (a fresh `npm run sbom`, a plugin update) between two calls inside the ceiling.
1983
- return { ...TRUST_CACHE.data, channel: readInstallChannel(), sbom: readSbom(), ...fresh };
2165
+ return { ...TRUST_CACHE.data, channel: readInstallChannel(), sbom: readSbom(), versions: versionFacts({ release: TRUST_CACHE.data.release }), ...fresh };
1984
2166
  }
1985
2167
 
1986
2168
  // ── Assemble the read-models ─────────────────────────────────────────────────────────────────────
@@ -2130,7 +2312,7 @@ function gatherState(cwd, { fleet = true } = {}) {
2130
2312
  generatedAt: new Date().toISOString(),
2131
2313
  preStateHash,
2132
2314
  host: { user: os.userInfo().username, platform: process.platform, node: process.version, npmPrefix: NPM_PREFIX.replace(SYSTEM_HOME, '~'), brainVersion: brainVersionOnDisk() },
2133
- sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations },
2315
+ sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations, inventory: gatherInventory() },
2134
2316
  };
2135
2317
  // Cache the last good state so repeat page-loads paint instantly, same as the stack audit does.
2136
2318
  // TOKEN is per-server-run and must never touch disk — ?fast=1 splices the live one back in.
@@ -2864,6 +3046,10 @@ function startServer({ port = Number(process.env.CONSOLE_PORT) || 7411, open = f
2864
3046
  if (req.method === 'GET' && url === '/api/lessons') return sendJSON(res, 200, gatherLessons());
2865
3047
  if (req.method === 'GET' && url === '/api/trust') return sendJSON(res, 200, await gatherTrust());
2866
3048
  if (req.method === 'GET' && url === '/tips') { req.url = '/tips.html'; return serveStatic(req, res); }
3049
+ // What's in the brain: derived from the installed root's own receipts on every request (read-only,
3050
+ // no network, ~ms) — a coverage listing served from a stale cache would defeat its one purpose.
3051
+ if (req.method === 'GET' && url === '/api/scope') return sendJSON(res, 200, gatherScope());
3052
+ if (req.method === 'GET' && url === '/scope') { req.url = '/scope.html'; return serveStatic(req, res); }
2867
3053
  if (req.method === 'POST') {
2868
3054
  const body = await readBody(req);
2869
3055
  if (body.token !== TOKEN) return sendJSON(res, 403, { error: 'bad or missing token' });
@@ -3046,7 +3232,155 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith('onboarding-consol
3046
3232
  else { console.log(`\n onboarding-console — the RuvNet Brain configure page\n\n --serve [--open] start or safely replace the scoped local server\n --runtime-status print candidate, receipt, and live runtime status\n --print-state print the read-only state JSON and exit (for tests)\n --print-stack print the stack audit JSON and exit\n`); }
3047
3233
  }
3048
3234
 
3235
+ // ── "What's in the brain" (the scope page) ─────────────────────────────────────────────────────────
3236
+ // Every verdict comes from the installed brain's OWN receipts, never from the shipped `status` word.
3237
+ // Measured 2026-09-11 (KB audit): release-projection.mjs:80 stamps every seeded row CURRENT — 63 real
3238
+ // rows had sourceCommit ≠ upstream.sha. GitHub `updatedAt` is metadata (222/227 repos moved on it in
3239
+ // one week) and `pushedAt` leads the default-branch HEAD on 39/227; only `committedAt` dates the commit
3240
+ // the brain ingests. Gists have no branches, so their `updatedAt` IS the content date — and the shipped
3241
+ // COVERAGE row carries no sourceCommit for them (479/479 null); the gist receipt set
3242
+ // (ruv-gists.sources.json → versionSha) is the brain's record of which revision it holds.
3243
+ //
3244
+ // The truth per row is `brainSha === upstreamSha`. A store with no recorded source commit is
3245
+ // "unverified" — never promoted to current on the strength of a date or a label. A store whose
3246
+ // .big.rvf is not at the installed root is "not in the brain", whatever the record says.
3247
+ const SCOPE_SENTENCE = "If rUv's last change to a repo is on or before the date the brain read it, everything in that repo is in the brain. If it's after, the brain is behind on that repo by the difference.";
3248
+ const SCOPE_DAY_MS = 86_400_000;
3249
+ const SCOPE_STALE_DAYS = 7;
3250
+ // A row's `desc` is what the thing DOES, so the page's search answers "is the stuff on Federation in
3251
+ // Ruflo?" — a description question — not only name lookups. Bounded so a README pasted into a card
3252
+ // cannot bloat 700 rows.
3253
+ const SCOPE_DESC_MAX = 200;
3254
+ const scopeBounded = (s) => (s.length > SCOPE_DESC_MAX ? `${s.slice(0, SCOPE_DESC_MAX - 1)}…` : s);
3255
+
3256
+ /**
3257
+ * Pure: the installed brain's capability-cards.md → Map(lowercased repo/store name → first line of
3258
+ * its card, bounded). The card file is the ONLY per-repo description that ships to a customer
3259
+ * machine (data/ruvnet-registry.json is repo-side and never installs — measured 2026-09-12). Format,
3260
+ * measured on 182 sections: `## <name>`, one line saying what it does, one auto-derivation note.
3261
+ */
3262
+ function parseCapabilityCards(text) {
3263
+ const cards = new Map();
3264
+ if (typeof text !== 'string') return cards;
3265
+ const lines = text.split('\n');
3266
+ for (let i = 0; i < lines.length; i++) {
3267
+ const h = /^##\s+(\S.*?)\s*$/.exec(lines[i]);
3268
+ if (!h) continue;
3269
+ let j = i + 1;
3270
+ while (j < lines.length && !lines[j].trim()) j += 1;
3271
+ const first = j < lines.length && !lines[j].startsWith('#') ? lines[j].trim() : '';
3272
+ if (first) cards.set(h[1].toLowerCase(), scopeBounded(first));
3273
+ }
3274
+ return cards;
3275
+ }
3276
+
3277
+ function scopeDesc(row, { isGist, store, gistId, gistSources, cards }) {
3278
+ if (isGist) {
3279
+ // A gist's only words are its file names (COVERAGE upstream.files; the receipt's files as fallback).
3280
+ const files = row.upstream?.files ?? gistSources?.gists?.[gistId]?.files ?? null;
3281
+ const names = Array.isArray(files) ? files.map((f) => (typeof f === 'string' ? f : f?.filename)).filter(Boolean) : [];
3282
+ return names.length ? scopeBounded(names.join(' · ')) : null;
3283
+ }
3284
+ const byName = cards?.get(String(row.name ?? '').toLowerCase());
3285
+ if (byName) return byName;
3286
+ return (store && cards?.get(String(store).toLowerCase())) || null;
3287
+ }
3288
+
3289
+ function scopeRow(row, { generations, gistSources, installedStores, cards }) {
3290
+ const isGist = row.kind === 'gist';
3291
+ const store = row.artifact?.store ?? null;
3292
+ const ledger = store ? (generations?.stores?.[store] ?? null) : null;
3293
+ const gistId = isGist ? String(row.key ?? '').replace(/^gist:/, '') : null;
3294
+ const upstreamSha = row.upstream?.sha ?? null;
3295
+ const desc = scopeDesc(row, { isGist, store, gistId, gistSources, cards });
3296
+
3297
+ let brainSha = row.artifact?.sourceCommit ?? null;
3298
+ if (brainSha == null) brainSha = isGist ? (gistSources?.gists?.[gistId]?.versionSha ?? null) : (ledger?.sourceCommit ?? null);
3299
+
3300
+ const ruvChangedAt = (isGist ? row.upstream?.updatedAt : row.upstream?.committedAt) ?? null;
3301
+ const brainReadAt = row.artifact?.ingestedAt ?? ledger?.builtUtc ?? null;
3302
+
3303
+ let installed = Boolean(store) && installedStores.has(store);
3304
+ // With a gist receipt set present, a gist the set does not enumerate is not in the store.
3305
+ if (installed && isGist && gistSources?.gists && !(gistId in gistSources.gists)) installed = false;
3306
+
3307
+ let bucket;
3308
+ if (!installed) bucket = 'not-in-brain';
3309
+ else if (brainSha && upstreamSha) bucket = brainSha === upstreamSha ? 'current' : 'behind';
3310
+ else bucket = 'unverified';
3311
+
3312
+ let behindDays = null;
3313
+ if (bucket === 'behind' && ruvChangedAt && brainReadAt) {
3314
+ const d = (Date.parse(ruvChangedAt) - Date.parse(brainReadAt)) / SCOPE_DAY_MS;
3315
+ behindDays = Number.isFinite(d) ? Math.max(0, Math.round(d)) : null;
3316
+ }
3317
+ return { kind: row.kind, name: row.name, url: row.url ?? null, store, desc, bucket, ruvChangedAt, brainReadAt, behindDays, brainSha, upstreamSha };
3318
+ }
3319
+
3320
+ const scopeNewestFirst = (a, b) => {
3321
+ if (a.ruvChangedAt == null && b.ruvChangedAt == null) return String(a.name).localeCompare(String(b.name));
3322
+ if (a.ruvChangedAt == null) return 1;
3323
+ if (b.ruvChangedAt == null) return -1;
3324
+ return a.ruvChangedAt < b.ruvChangedAt ? 1 : a.ruvChangedAt > b.ruvChangedAt ? -1 : String(a.name).localeCompare(String(b.name));
3325
+ };
3326
+
3327
+ /** Pure: coverage + ledger + gist receipts + the set of installed store names → the page's payload. */
3328
+ function computeScope({ coverage, generations, gistSources, installedStores, root, cards = new Map(), now = Date.now() }) {
3329
+ const empty = () => ({ total: 0, current: 0, behind: 0, unverified: 0, notInBrain: 0, ineligible: 0 });
3330
+ const counts = { repos: empty(), gists: empty() };
3331
+ const out = { repos: [], gists: [] };
3332
+ const covered = new Set();
3333
+ for (const row of coverage.rows ?? []) {
3334
+ const kind = row.kind === 'gist' ? 'gists' : 'repos';
3335
+ if (row.artifact?.store) covered.add(row.artifact.store);
3336
+ counts[kind].total += 1;
3337
+ if (row.disposition !== 'eligible') { counts[kind].ineligible += 1; continue; }
3338
+ const r = scopeRow(row, { generations, gistSources, installedStores, cards });
3339
+ counts[kind][r.bucket === 'not-in-brain' ? 'notInBrain' : r.bucket] += 1;
3340
+ out[kind].push(r);
3341
+ }
3342
+ out.repos.sort(scopeNewestFirst);
3343
+ out.gists.sort(scopeNewestFirst);
3344
+ const observedAt = typeof coverage.observedAt === 'string' ? coverage.observedAt : null;
3345
+ const ageMs = observedAt ? now - Date.parse(observedAt) : NaN;
3346
+ const ageDays = Number.isFinite(ageMs) ? Math.round((ageMs / SCOPE_DAY_MS) * 10) / 10 : null;
3347
+ return {
3348
+ available: true,
3349
+ root,
3350
+ observedAt,
3351
+ ageDays,
3352
+ stale: ageDays == null || ageDays > SCOPE_STALE_DAYS,
3353
+ brainVersion: generations?.brainVersion ?? null,
3354
+ releaseTag: generations?.releaseTag ?? null,
3355
+ sentence: SCOPE_SENTENCE,
3356
+ counts,
3357
+ repos: out.repos,
3358
+ gists: out.gists,
3359
+ installedStoreCount: installedStores.size,
3360
+ installedOutsideCoverage: [...installedStores].filter((s) => !covered.has(s)).sort(),
3361
+ };
3362
+ }
3363
+
3364
+ /** Read the installed root and compute. Read-only; no network; returns `available: false` rather than a guess. */
3365
+ function gatherScope() {
3366
+ const readJson = (rel) => { try { return JSON.parse(fs.readFileSync(path.join(INSTALLED_KB, rel), 'utf8')); } catch { return null; } };
3367
+ const coverage = readJson('COVERAGE.json');
3368
+ if (!coverage || !Array.isArray(coverage.rows)) {
3369
+ return { available: false, reason: `no readable COVERAGE.json at ${INSTALLED_KB}`, root: INSTALLED_KB, sentence: SCOPE_SENTENCE, repos: [], gists: [], counts: {}, installedOutsideCoverage: [], installedStoreCount: 0 };
3370
+ }
3371
+ let installedStores = new Set();
3372
+ try { installedStores = new Set(fs.readdirSync(INSTALLED_KB).filter((f) => f.endsWith('.big.rvf')).map((f) => f.slice(0, -'.big.rvf'.length))); }
3373
+ catch { /* unreadable root reads as nothing installed — every row then says "not in the brain" */ }
3374
+ let cardsText = null;
3375
+ try { cardsText = fs.readFileSync(path.join(INSTALLED_KB, 'capability-cards.md'), 'utf8'); } catch { /* no cards → every desc is null, never invented */ }
3376
+ return computeScope({ coverage, generations: readJson('RVF-GENERATIONS.json'), gistSources: readJson('ruv-gists.sources.json'),
3377
+ installedStores, root: INSTALLED_KB, cards: parseCapabilityCards(cardsText) });
3378
+ }
3379
+
3049
3380
  export {
3381
+ gatherScope,
3382
+ computeScope,
3383
+ parseCapabilityCards,
3050
3384
  gatherState,
3051
3385
  gatherStack,
3052
3386
  gatherTrust,
@@ -3063,6 +3397,8 @@ export {
3063
3397
  saveBrainProfile,
3064
3398
  gatherRouterEngine,
3065
3399
  autoEligibleIds,
3400
+ gatherConfig,
3401
+ gatherSavings,
3066
3402
  };
3067
3403
  // Exported for the cross-project cache-isolation test (console-cache-scope.test.mjs). serveCached's
3068
3404
  // scopeKey is the guard that stops one project's cached state being served for another.