ruvnet-brain 4.3.28 → 4.3.30

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 (73) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +90 -8
  3. package/console/app.js +7 -2
  4. package/kb/corpus-release-identity.mjs +73 -0
  5. package/package.json +4 -3
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/mcp/managed-cli-interface.mjs +83 -9
  9. package/plugin/scripts/advocacy-route.mjs +4 -1
  10. package/plugin/scripts/capacity-aware-parallel-work.mjs +4 -0
  11. package/plugin/scripts/continuation-gate.mjs +71 -0
  12. package/plugin/scripts/decision-gate.mjs +29 -57
  13. package/plugin/scripts/ground-ruvnet.sh +77 -4
  14. package/plugin/scripts/grounding-stamp.sh +34 -5
  15. package/plugin/scripts/grounding-turn-gate.mjs +8 -2
  16. package/plugin/scripts/grounding-turn-mark.mjs +6 -1
  17. package/plugin/scripts/hook-input.mjs +54 -0
  18. package/plugin/scripts/hook-shim.mjs +10 -0
  19. package/plugin/scripts/memory-doctor.mjs +43 -0
  20. package/plugin/scripts/nightly-controller.mjs +6 -1
  21. package/plugin/scripts/project-progression-contract.mjs +17 -0
  22. package/plugin/scripts/project-progression-hook.mjs +18 -5
  23. package/plugin/scripts/project-progression-producer.mjs +16 -12
  24. package/plugin/scripts/ruvnet-gate1-pattern.mjs +17 -0
  25. package/plugin/scripts/session-start-core.mjs +11 -5
  26. package/plugin/scripts/session-start-update-plane.mjs +5 -1
  27. package/plugin/scripts/unprompted-runtime.mjs +29 -8
  28. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +1 -1
  29. package/scripts/adr-072-completion.mjs +2 -1
  30. package/scripts/calibrate-router.mjs +6 -6
  31. package/scripts/corpus-reconcile.mjs +32 -3
  32. package/scripts/correction-detect.mjs +10 -11
  33. package/scripts/dispatch-receipt.mjs +2 -2
  34. package/scripts/dual-host-deliberation.mjs +3 -2
  35. package/scripts/execution-policy.mjs +10 -2
  36. package/scripts/gen-console-images.mjs +0 -1
  37. package/scripts/gen-images.mjs +0 -4
  38. package/scripts/host-install-matrix.mjs +7 -4
  39. package/scripts/independent-review-receipt.mjs +7 -8
  40. package/scripts/ingest-repo.mjs +45 -1
  41. package/scripts/learning-replay-cli.mjs +2 -1
  42. package/scripts/learning-replay-fixture.mjs +2 -1
  43. package/scripts/learnings.mjs +19 -5
  44. package/scripts/lesson-migrate-agentdb.mjs +635 -0
  45. package/scripts/loop-checkpoint.mjs +37 -1
  46. package/scripts/metaharness-receipts.mjs +3 -2
  47. package/scripts/nightly-two-run-proof.mjs +6 -6
  48. package/scripts/nightly-watchdog.mjs +10 -2
  49. package/scripts/onboarding-console.mjs +98 -49
  50. package/scripts/oracle/producer-hosts.mjs +2 -1
  51. package/scripts/private-overlay.mjs +31 -9
  52. package/scripts/public-verification-aggregate.mjs +4 -3
  53. package/scripts/publication-receipt.mjs +9 -0
  54. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  55. package/scripts/rebuild-gists-from-receipts.mjs +1 -1
  56. package/scripts/reconcile-project.mjs +0 -0
  57. package/scripts/release.mjs +34 -82
  58. package/scripts/retrieval-canary.mjs +3 -2
  59. package/scripts/review-model-defaults.mjs +26 -0
  60. package/scripts/route-cheap.mjs +20 -7
  61. package/scripts/router-utilization.mjs +5 -5
  62. package/scripts/rvf-generation.mjs +68 -2
  63. package/scripts/single-source-check.mjs +270 -0
  64. package/scripts/subscription-hosts.mjs +4 -0
  65. package/scripts/sync-version.mjs +22 -0
  66. package/scripts/trismart.mjs +3 -3
  67. package/scripts/wired-check.mjs +40 -4
  68. package/console/assets/memory.webp +0 -0
  69. package/plugin/scripts/version-bump-gate.sh +0 -124
  70. package/scripts/qe/aggregate-4.3.mjs +0 -42
  71. package/scripts/release-convergence-watchdog.mjs +0 -112
  72. package/scripts/stamp-existing-rvf-generations.mjs +0 -53
  73. package/scripts/verify-channels.mjs +0 -196
@@ -15,6 +15,9 @@
15
15
  // check → evaluate stop conditions. Exit codes are the protocol:
16
16
  // 0 = continue 3 = DONE (doneCriteria command exited 0)
17
17
  // 4 = NO-PROGRESS stop (two strikes) 2 = usage/state error
18
+ // stale → is `updatedAt` too old to trust (H3, ADR-0011 Phase 1's stale-resume gap)? Exit codes:
19
+ // 0 = fresh (safe to resume) 1 = stale (stdout: age in whole days)
20
+ // 2 = unknown (no checkpoint, or no parseable updatedAt — never treated as fresh OR stale)
18
21
  //
19
22
  // doneCriteria is a SHELL COMMAND, not prose — "machine-checkable done" means exit code 0, not a
20
23
  // model's opinion that it's finished.
@@ -52,6 +55,28 @@ export function writeCheckpoint({ iteration, doneCriteria, next, blockers }, fil
52
55
  return cp;
53
56
  }
54
57
 
58
+ // H3 (GitHub issue: AUTONOMOUS MODE injected into attended sessions + a stale checkpoint resumed as
59
+ // if it were live). ground-ruvnet.sh used to inject `.ruvnet-brain/checkpoint.json`'s raw content on
60
+ // every autonomous-flagged turn with no age check at all — a checkpoint from a loop that ended days
61
+ // or weeks ago would be resumed as though it were the current session's own state. This is the ONE
62
+ // place "how old is too old" is decided; ground-ruvnet.sh (via the `stale` CLI verb below) and
63
+ // scripts/single-source-check.mjs's E1 audit both judge staleness against this exact threshold and
64
+ // this exact field, so the two can never quietly disagree about what "stale" means.
65
+ export const CHECKPOINT_STALE_MS = 24 * 60 * 60 * 1000; // 24h
66
+
67
+ /**
68
+ * Age verdict for a checkpoint object, judged by its OWN `updatedAt` claim — never by the file's
69
+ * mtime, which a copy/rsync/restore can reset without the checkpoint's content actually changing.
70
+ * `known: false` means there is nothing to judge (no checkpoint, or no parseable `updatedAt`); a
71
+ * caller must treat "unknown" as neither proven-fresh nor proven-stale.
72
+ */
73
+ export function checkpointStaleness(cp, now = Date.now()) {
74
+ const updatedAtMs = cp && typeof cp.updatedAt === 'string' ? Date.parse(cp.updatedAt) : NaN;
75
+ if (!Number.isFinite(updatedAtMs)) return { known: false, stale: false, ageMs: null, ageDays: null };
76
+ const ageMs = now - updatedAtMs;
77
+ return { known: true, stale: ageMs >= CHECKPOINT_STALE_MS, ageMs, ageDays: ageMs / 86_400_000 };
78
+ }
79
+
55
80
  /** Stop-condition verdict. Pure decision from state + one real doneCriteria execution. */
56
81
  export function checkCheckpoint(file = FILE, runner = (c) => spawnSync('sh', ['-c', c], { stdio: 'ignore' }).status) {
57
82
  const cp = readCheckpoint(file);
@@ -79,8 +104,19 @@ if (invokedDirectly) {
79
104
  const r = checkCheckpoint();
80
105
  console.log(r.verdict);
81
106
  process.exit(r.code);
107
+ } else if (cmd === 'stale') {
108
+ // H3: the ONE place a caller (ground-ruvnet.sh, via `node loop-checkpoint.mjs stale`) asks "is
109
+ // this checkpoint too old to resume from?" without re-deriving the 24h rule itself. Exit codes
110
+ // are the protocol, same style as `check`: 0 = fresh (safe to inject/resume), 1 = stale (stdout
111
+ // carries the whole age in days, floored, for a human-readable message), 2 = unknown — no
112
+ // checkpoint, unparseable JSON, or no usable `updatedAt` to judge by. A caller must not treat 2
113
+ // as either fresh or stale.
114
+ const info = checkpointStaleness(readCheckpoint());
115
+ if (!info.known) process.exit(2);
116
+ if (info.stale) { console.log(String(Math.floor(info.ageDays))); process.exit(1); }
117
+ process.exit(0);
82
118
  } else {
83
- console.error('usage: loop-checkpoint.mjs <read|write|check> [--dir D] [--iteration N --done-criteria CMD --next "…" --blockers "…"]');
119
+ console.error('usage: loop-checkpoint.mjs <read|write|check|stale> [--dir D] [--iteration N --done-criteria CMD --next "…" --blockers "…"]');
84
120
  process.exit(2);
85
121
  }
86
122
  }
@@ -11,6 +11,7 @@ import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import os from 'node:os';
13
13
  import { pathToFileURL } from 'node:url';
14
+ import { CLAUDE_MODEL_IDS } from './route-cheap.mjs';
14
15
 
15
16
  export function receiptsPath() {
16
17
  return (
@@ -61,7 +62,7 @@ export function formatTable(rows) {
61
62
  r.source === 'claude-subagent' ? 'subagent' : r.source === 'calibration' ? 'calibrate' : 'openrouter',
62
63
  r.task_class || '?',
63
64
  r.model,
64
- r.frontier_ref || 'claude-opus-4.8',
65
+ r.frontier_ref || CLAUDE_MODEL_IDS.opus,
65
66
  fmt$(r.est_cost ?? 0),
66
67
  fmt$(r.est_frontier_cost ?? 0),
67
68
  fmt$(r.saved),
@@ -79,7 +80,7 @@ export function formatTable(rows) {
79
80
  const pct = totalFrontier > 0 ? Math.round((totalSaved / totalFrontier) * 100) : 0;
80
81
 
81
82
  // Baselines now vary per row; name them all rather than picking one and implying it covers everything.
82
- const baselines = [...new Set(rows.map((r) => r.frontier_ref || 'claude-opus-4.8'))].join(', ');
83
+ const baselines = [...new Set(rows.map((r) => r.frontier_ref || CLAUDE_MODEL_IDS.opus))].join(', ');
83
84
  const timedTotal = rows.reduce((s, r) => s + (typeof r.duration_ms === 'number' && r.duration_ms > 0 ? r.duration_ms : 0), 0);
84
85
  const timedRows = rows.filter((r) => typeof r.duration_ms === 'number' && r.duration_ms > 0).length;
85
86
  // TIME as a percentage, like cost — "20 seconds" is trivia; "~40% faster" is the message
@@ -11,7 +11,7 @@ import { managedStorageInventory } from '../kb/update-storage-transaction.mjs';
11
11
  import { REQUIRED_REFRESH_PHASES, validateCurrentRunPhaseExecution, inspectRefreshOwner, refreshLockPath } from '../kb/refresh-run.mjs';
12
12
  import { npmInvocation } from './npm-invocation.mjs';
13
13
  import { packageTreeObservation, observeNpxPackage, validateNpxObservations } from './nightly-package-observation.mjs';
14
- import { validateRefreshReceiptEnvelope } from '../plugin/scripts/nightly-scheduler.mjs';
14
+ import { NIGHTLY_LABEL, NIGHTLY_PROOF_LABEL, validateRefreshReceiptEnvelope } from '../plugin/scripts/nightly-scheduler.mjs';
15
15
  import { validateCoverageDirectory, validateCoverageLedger } from '../plugin/scripts/coverage-integrity.mjs';
16
16
 
17
17
  const PUBLIC_KEY = fs.readFileSync(new URL('../keys/ruvnet-brain-signing.pub.pem', import.meta.url), 'utf8');
@@ -117,7 +117,7 @@ function refreshExecutionStopped(receipt, kbDir) {
117
117
  export async function triggerNativeRun({ scheduler, registration, platform, env, brainHome, kbDir,
118
118
  timeoutMs, command = run, receipts = nightlyReceipts, pause = sleep, now = Date.now, stopped = refreshExecutionStopped }) {
119
119
  const identity = registration.identity;
120
- if (!/^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/.test(identity || '')) {
120
+ if (!NIGHTLY_PROOF_LABEL.test(identity || '')) {
121
121
  throw new Error('native proof requires a unique proof scheduler identity');
122
122
  }
123
123
  if (!['darwin', 'linux', 'win32'].includes(platform)) throw new Error(`unsupported native scheduler platform: ${platform}`);
@@ -209,7 +209,7 @@ export async function runNativeSchedulerSmoke({ packageRoot, packagePath, bundle
209
209
  || typeof scheduler.installScheduler !== 'function' || typeof scheduler.schedulerStatus !== 'function'
210
210
  || typeof scheduler.removeScheduler !== 'function') throw new Error('scheduler smoke adapter is incomplete');
211
211
 
212
- const identity = `com.ruvnet.brain-update.proof-smoke-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
212
+ const identity = `${NIGHTLY_LABEL}.proof-smoke-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
213
213
  const packageSha256 = sha256(fs.readFileSync(packageArchive));
214
214
  const bundleSha256 = sha256(fs.readFileSync(bundle));
215
215
  const registration = scheduler.installNightlyRunner({ brainHome,
@@ -281,7 +281,7 @@ export function validateNativeSchedulerSmoke(smoke, { platform, sourceSha, workf
281
281
  || smoke.scope !== 'public-release-scheduler-boundary') failures.push('scheduler smoke envelope is invalid');
282
282
  if (smoke.platform !== platform || smoke.sourceSha !== sourceSha
283
283
  || String(smoke.workflowRunId || '') !== String(workflowRunId || '')) failures.push('scheduler smoke identity differs');
284
- if (!/^com\.ruvnet\.brain-update\.proof-smoke-[A-Za-z0-9._-]+$/.test(smoke.identity || '')) failures.push('scheduler smoke identity is not isolated');
284
+ if (!NIGHTLY_PROOF_LABEL.test(smoke.identity || '') || !String(smoke.identity || '').includes('.proof-smoke-')) failures.push('scheduler smoke identity is not isolated');
285
285
  if (smoke.loaded !== true || smoke.cleaned !== true || smoke.trigger?.identity !== smoke.identity) failures.push('scheduler smoke did not prove load, trigger, and cleanup');
286
286
  const expectedTrigger = { darwin: 'launchctl-kickstart', linux: 'cron-registration', win32: 'schtasks-run' }[platform];
287
287
  if (smoke.trigger?.kind !== expectedTrigger) failures.push('scheduler smoke trigger differs');
@@ -424,7 +424,7 @@ export function validateNightlyProofReceipt(receipt, { platform, version, packag
424
424
  if (!/^[a-f0-9]{64}$/.test(bundleSha256 || '') || receipt.candidate?.bundle?.sha256 !== bundleSha256) failures.push('native proof bundle digest differs');
425
425
  if (!/^[a-f0-9]{40}$/.test(sourceSha || '') || receipt.sourceSha !== sourceSha) failures.push('native proof source SHA differs');
426
426
  if (!/^\d+$/.test(String(workflowRunId || '')) || String(receipt.workflowRunId || '') !== String(workflowRunId)) failures.push('native proof workflow run differs');
427
- if (!/^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/.test(receipt.identity || '')) failures.push('native proof identity is not isolated');
427
+ if (!NIGHTLY_PROOF_LABEL.test(receipt.identity || '')) failures.push('native proof identity is not isolated');
428
428
  if (receipt.packageExecution !== undefined || receipt.registration?.packageTarget?.spec === 'ruvnet-brain@latest') {
429
429
  failures.push(...validateNpxObservations(receipt));
430
430
  }
@@ -480,7 +480,7 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
480
480
  const prefix = path.join(root, 'prefix');
481
481
  const npmCache = path.join(root, 'npm-cache');
482
482
  const logPath = path.join(kbDir, 'update.log');
483
- const identity = `com.ruvnet.brain-update.proof-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
483
+ const identity = `${NIGHTLY_LABEL}.proof-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
484
484
  const tempDir = path.join(root, 'tmp');
485
485
  const env = {
486
486
  ...Object.fromEntries(['SystemRoot', 'SYSTEMROOT', 'ComSpec', 'COMSPEC', 'PATHEXT']
@@ -227,6 +227,14 @@ export function productSchedulerVerdict(status) {
227
227
  return { ...base, state: mapped, detail: run.evidence };
228
228
  }
229
229
 
230
+ /** ONE update owner per machine (CONTRIBUTING.md § The knowledge corpus). agentic-kit removes the
231
+ * Brain's own scheduler by design and updates through `ak sync`, which the registry watches as
232
+ * com.stuartkerr.ak-sync — so on such a machine the Brain scheduler is not expected to exist. */
233
+ export function brainUpdateOwnedByAgenticKit(home = os.homedir()) {
234
+ try { return JSON.parse(fs.readFileSync(path.join(home, '.config', 'agentic-kit', 'kit.json'), 'utf8')).ruvnetBrain === true; }
235
+ catch { return false; }
236
+ }
237
+
230
238
  const loadState = () => { try { return JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch { return {}; } };
231
239
  const saveState = (s) => { fs.mkdirSync(path.dirname(STATE), { recursive: true }); fs.writeFileSync(STATE, JSON.stringify(s, null, 2)); };
232
240
 
@@ -253,9 +261,9 @@ async function main() {
253
261
  const json = process.argv.includes('--json');
254
262
  const quiet = process.argv.includes('--quiet');
255
263
  const brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
256
- const results = [...checkAll(new Date()), productSchedulerVerdict(schedulerStatus({
264
+ const results = [...checkAll(new Date()), ...(brainUpdateOwnedByAgenticKit() ? [] : [productSchedulerVerdict(schedulerStatus({
257
265
  brainHome, kbDir: process.env.RUVNET_BRAIN_KB || path.join(brainHome, 'kb'),
258
- }))];
266
+ }))])];
259
267
  const bad = results.filter((r) => r.state !== OK);
260
268
 
261
269
  const prev = loadState();
@@ -27,7 +27,7 @@ import { fileURLToPath } from 'node:url';
27
27
  import { spawnSync, execFileSync, spawn } from 'node:child_process';
28
28
 
29
29
  import { auditModel, installedVersion } from './stack-sync.mjs';
30
- import { candidateRoots, findStores, diagnose } from './memory-doctor.mjs';
30
+ import { candidateRoots, findStores, findProjects, diagnose } from './memory-doctor.mjs';
31
31
  import { buildStackRecommendations, buildWiringRecommendations, summarizeWiring, scoreMemoryHealth, buildHealthRecommendations, buildCapabilityRecommendations } from './console-engine.mjs';
32
32
  import { planFor } from './remedy-registry.mjs';
33
33
  import { auditAll as capabilityAuditAll } from './capability-registry.mjs';
@@ -73,6 +73,7 @@ import { applyNightlyChoice, nightlyStatus } from './nightly-controller.mjs';
73
73
  // One canonical answer to "which directory is this, and have I counted it already?" — shared with
74
74
  // the PreCompact snapshot producer (#85) and with memory-doctor's root scan (#107).
75
75
  import { canonicalPath, pathIdentity, projectDirectory } from '../plugin/scripts/project-identity.mjs';
76
+ import { resolveProjectStore } from '../plugin/scripts/project-store-resolver.mjs';
76
77
 
77
78
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
78
79
  const REPO = path.dirname(__dirname);
@@ -350,27 +351,6 @@ function robustReadJSON(db, sql) {
350
351
  const VENDOR = ['/clones/', '/node_modules/', '/vendor/', '/upstream/', '.claude-backup', '_snapshots',
351
352
  '/ruvnet-repos/', '/ruvnet_repos/'];
352
353
 
353
- // memory-doctor.mjs owns candidate-root policy for the standalone CLI, Console, and other callers.
354
- // Keeping one exported implementation prevents a new project-root convention from fixing one
355
- // surface while another continues to print a confident but incomplete machine-wide count (#81).
356
- function findProjects(root) {
357
- const out = new Set();
358
- const walk = (dir, depth) => {
359
- if (depth > 4) return;
360
- let ents; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
361
- for (const e of ents) {
362
- const p = path.join(dir, e.name);
363
- if (VENDOR.some((m) => (p + '/').includes(m))) continue;
364
- if (e.isDirectory()) {
365
- if (e.name === '.claude') { out.add(dir); continue; }
366
- if (e.name.startsWith('.') || e.name === 'node_modules') continue;
367
- walk(p, depth + 1);
368
- } else if (e.name === '.mcp.json') out.add(dir);
369
- }
370
- };
371
- walk(root, 0);
372
- return [...out].sort();
373
- }
374
354
  // Text that PRINTS the word npx is not an npx call site. Two of the sites this card warned about were
375
355
  // `echo "Session ended. Run: npx aqe learn status"` — advice being displayed to the user, matched as
376
356
  // though the machine were executing it. Strip quoted echo/printf payloads before classifying.
@@ -483,15 +463,25 @@ function memoryRows(file) {
483
463
  return r.ok ? (r.value === null ? 0 : parseInt(r.value, 10)) : null;
484
464
  }
485
465
  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 };
466
+ let canonicalPath;
467
+ let resolutionError = null;
468
+ try { canonicalPath = resolveProjectStore({ projectDir }).canonicalAgentDbPath; }
469
+ catch (error) {
470
+ // A resolver refusal (foreign store, broken symlink, or ambiguous project) is an
471
+ // authoritative UNKNOWN. Never probe a guessed path after the resolver declines it.
472
+ canonicalPath = path.join(projectDir, '.swarm', MEMORY_DB_NAMES[0]);
473
+ resolutionError = String(error?.message || error);
474
+ }
475
+ const coordinationPath = path.join(path.dirname(canonicalPath), MEMORY_DB_NAMES[1]);
476
+ const canonical = resolutionError
477
+ ? { path: canonicalPath, exists: null, rows: null, status: 'unknown', error: resolutionError }
478
+ : { path: canonicalPath, exists: fs.existsSync(canonicalPath), rows: memoryRows(canonicalPath) };
479
+ const coordination = resolutionError
480
+ ? { path: coordinationPath, exists: null, rows: null, status: 'unknown', error: 'canonical project store could not be resolved' }
481
+ : { path: coordinationPath, exists: fs.existsSync(coordinationPath), rows: memoryRows(coordinationPath) };
482
+ // Coordination storage is a separate diagnostic container. It can never become project memory
483
+ // merely because the canonical file is absent, empty, busy, or larger.
484
+ return { canonical, coordination, scored: canonical.path, resolutionError };
495
485
  }
496
486
  export function resolveMemoryDb(projectDir) {
497
487
  return memoryStores(projectDir).scored;
@@ -519,6 +509,12 @@ function probeMemory(projectDir, { now = Date.now() } = {}) {
519
509
  // recall quality: honestly NOT probed at render (a true probe needs an embedding query; left for an explicit deep test)
520
510
  probes.recallQuality = { status: 'notTested', detail: 'not checked this session — a real recall probe needs an embedding round-trip, which render deliberately avoids' };
521
511
 
512
+ if (stores.resolutionError) {
513
+ probes.liveness = { status: 'unknown', detail: `canonical project store could not be resolved: ${stores.resolutionError}` };
514
+ probes.coverage = { status: 'unknown', detail: 'project checkpoint cannot be assessed until the canonical store resolves' };
515
+ return probes;
516
+ }
517
+
522
518
  if (!fs.existsSync(db)) {
523
519
  probes.liveness = { status: 'fail', detail: 'this project has no memory store (.swarm/memory.db) yet' };
524
520
  probes.coverage = { status: 'warn', detail: 'no checkpoint — no store has been created here' };
@@ -580,7 +576,8 @@ function gatherMemory(cwd, { fleet = true } = {}) {
580
576
  // launched from a subdirectory probes the project root the hook actually wrote to, instead of
581
577
  // warning that a snapshot it can see on disk does not exist (#85).
582
578
  const scope = projectDirectory({ cwd });
583
- const project = fs.existsSync(path.join(scope, '.swarm/memory.db')) ? scope : REPO;
579
+ let project = scope;
580
+ try { project = resolveProjectStore({ projectDir: scope }).projectRoot; } catch { /* probe reports unavailable */ }
584
581
  const projName = project.replace(CONSOLE_ROOT + '/Code/', '').replace(CONSOLE_ROOT + '/', '~/');
585
582
  const probes = probeMemory(project);
586
583
  const health = scoreMemoryHealth({ project: projName, probes });
@@ -1534,11 +1531,33 @@ function writeCache(file, at, data, scope = null) {
1534
1531
  */
1535
1532
  let REFRESH_CHILD = null;
1536
1533
  const REFRESH_WEDGED_MS = 5 * 60 * 1000;
1537
- function kickRefresh({ force = false } = {}) {
1538
- if (process.env.RUVNET_CONSOLE_DISABLE_BACKGROUND_REFRESH === '1') return false;
1534
+ let REFRESH_STATE = Object.freeze({ status: 'idle', runId: null, error: null });
1535
+ export function refreshState() { return REFRESH_STATE; }
1536
+ export function classifyRefreshState({ disabled = false, running = false, debounced = false, failed = null, runId = null } = {}) {
1537
+ if (disabled) return Object.freeze({ status: 'disabled', runId: null, error: null });
1538
+ if (failed) return Object.freeze({ status: 'failed', runId, error: String(failed) });
1539
+ if (running || debounced) return Object.freeze({ status: 'already-running', runId, error: debounced ? 'debounced' : null });
1540
+ return Object.freeze({ status: 'started', runId, error: null });
1541
+ }
1542
+ export function settleRefreshState({ currentRunId, runId, code = 0, signal = null } = {}) {
1543
+ if (currentRunId !== runId) return null;
1544
+ if (code !== 0 || signal) return classifyRefreshState({ failed: signal ? `signal ${signal}` : `exit ${code}`, runId });
1545
+ return Object.freeze({ status: 'settled', runId, error: null });
1546
+ }
1547
+ function kickRefreshDetailed({ force = false } = {}) {
1548
+ if (process.env.RUVNET_CONSOLE_DISABLE_BACKGROUND_REFRESH === '1') {
1549
+ REFRESH_STATE = classifyRefreshState({ disabled: true });
1550
+ return REFRESH_STATE;
1551
+ }
1539
1552
  const now = Date.now();
1540
- if (REFRESH_CHILD && now - LAST_REFRESH_KICK < REFRESH_WEDGED_MS) return false; // one at a time
1541
- if (!force && now - LAST_REFRESH_KICK < 15000) return false; // debounce: at most one background refresh / 15s
1553
+ if (REFRESH_CHILD && now - LAST_REFRESH_KICK < REFRESH_WEDGED_MS) {
1554
+ REFRESH_STATE = classifyRefreshState({ running: true, runId: REFRESH_STATE.runId });
1555
+ return REFRESH_STATE;
1556
+ }
1557
+ if (!force && now - LAST_REFRESH_KICK < 15000) {
1558
+ REFRESH_STATE = classifyRefreshState({ debounced: true, runId: REFRESH_STATE.runId });
1559
+ return REFRESH_STATE;
1560
+ }
1542
1561
  LAST_REFRESH_KICK = now;
1543
1562
  try {
1544
1563
  // cwd = the SERVED project, NOT REPO. This was `cwd: REPO` and it was a real console-honesty bug
@@ -1553,12 +1572,32 @@ function kickRefresh({ force = false } = {}) {
1553
1572
  // contract (the 2026-07-17 outage) — only to which project the background compute is about.
1554
1573
  const child = spawn(process.execPath, [SELF, '--refresh-cache'], { detached: true, stdio: 'ignore', cwd: process.cwd() });
1555
1574
  REFRESH_CHILD = child;
1575
+ const runId = `${process.pid}-${now}`;
1576
+ REFRESH_STATE = classifyRefreshState({ runId });
1556
1577
  // unref() only releases the event-loop hold; these listeners still fire while the server lives.
1557
- child.on('exit', () => { REFRESH_CHILD = null; });
1558
- child.on('error', () => { REFRESH_CHILD = null; });
1578
+ child.on('exit', (code, signal) => {
1579
+ // An older child can exit after a newer run has started. It may only settle its own run.
1580
+ const settled = settleRefreshState({ currentRunId: REFRESH_STATE.runId, runId, code, signal });
1581
+ if (!settled) return;
1582
+ REFRESH_CHILD = null;
1583
+ REFRESH_STATE = settled;
1584
+ });
1585
+ child.on('error', (error) => {
1586
+ const settled = settleRefreshState({ currentRunId: REFRESH_STATE.runId, runId, code: 1, signal: error?.message || error });
1587
+ if (!settled) return;
1588
+ REFRESH_CHILD = null;
1589
+ REFRESH_STATE = settled;
1590
+ });
1559
1591
  child.unref(); // let it outlive this request; it writes the caches and exits on its own
1560
- return true;
1561
- } catch { REFRESH_CHILD = null; return false; /* a failed spawn just means the cache ages until the next kick */ }
1592
+ return REFRESH_STATE;
1593
+ } catch (error) {
1594
+ REFRESH_CHILD = null;
1595
+ REFRESH_STATE = classifyRefreshState({ failed: error?.message || error });
1596
+ return REFRESH_STATE;
1597
+ }
1598
+ }
1599
+ function kickRefresh(options = {}) {
1600
+ return kickRefreshDetailed(options).status === 'started';
1562
1601
  }
1563
1602
 
1564
1603
  /**
@@ -1819,10 +1858,12 @@ function findMemoryStores(root) {
1819
1858
  const p = path.join(dir, e.name);
1820
1859
  if (VENDOR.some((m) => (p + '/').includes(m))) continue;
1821
1860
  if (e.name === '.swarm') {
1822
- // Same resolution as probeMemory — the fleet walk had the identical hardcoded assumption (#127).
1823
- const resolved = ['memory.db', 'agentdb-memory.db'].map((n) => path.join(p, n)).filter((f) => fs.existsSync(f))
1824
- .sort((a, b) => { const sz = (f) => { try { return fs.statSync(f).size; } catch { return 0; } }; return sz(b) - sz(a); })[0];
1825
- if (resolved) out.push({ project: dir, db: resolved });
1861
+ // Fleet activity is project memory, so resolve the canonical store rather than selecting a
1862
+ // coordination database by size. The latter is diagnostic-only and can contain unrelated rows.
1863
+ try {
1864
+ const resolved = resolveProjectStore({ projectDir: dir }).canonicalAgentDbPath;
1865
+ if (fs.existsSync(resolved)) out.push({ project: dir, db: resolved });
1866
+ } catch { /* an unresolvable directory is not a project store */ }
1826
1867
  continue;
1827
1868
  }
1828
1869
  if (e.name.startsWith('.') || e.name === 'node_modules') continue;
@@ -1833,8 +1874,16 @@ function findMemoryStores(root) {
1833
1874
  return out;
1834
1875
  }
1835
1876
  function gatherActivity(cwd) {
1836
- const project = fs.existsSync(path.join(cwd, '.swarm/memory.db')) ? cwd : REPO;
1837
- const db = path.join(project, '.swarm/memory.db');
1877
+ let project;
1878
+ let db;
1879
+ try {
1880
+ const resolved = resolveProjectStore({ projectDir: projectDirectory({ cwd }) });
1881
+ project = resolved.projectRoot;
1882
+ db = resolved.canonicalAgentDbPath;
1883
+ } catch {
1884
+ const measuredAt = new Date().toISOString();
1885
+ return { generatedAt: measuredAt, project: path.basename(cwd), hasStore: false, continuity: 'unavailable', ...freshnessOf(measuredAt) };
1886
+ }
1838
1887
  if (!fs.existsSync(db)) {
1839
1888
  // No store to read — the existence check above IS the entire measurement, so it IS the
1840
1889
  // observation instant. Stamped here, not with a value taken before the check ran.
@@ -2254,7 +2303,7 @@ export function preferredSeat(subs) {
2254
2303
  function gatherState(cwd, { fleet = true } = {}) {
2255
2304
  const wiring = wiringSurvey();
2256
2305
  const memory = gatherMemory(cwd, { fleet });
2257
- try { memory.learnings = learnings(); } catch { memory.learnings = null; }
2306
+ try { memory.learnings = learnings({ cwd }); } catch { memory.learnings = null; }
2258
2307
  const savings = gatherSavings();
2259
2308
  const cfgNow = readJSON(CONFIG_PATH) || {};
2260
2309
  // issue #20: the Savings card's "Turn on smart routing" CTA must reflect what was actually saved —
@@ -3078,8 +3127,8 @@ function startServer({ port = Number(process.env.CONSOLE_PORT) || 7411, open = f
3078
3127
  // refresh that did not start must not report that it did, so `started` is the child's
3079
3128
  // real answer, not a constant.
3080
3129
  expireCachesEmbedding([STATE_CACHE, STACK_CACHE, MEMORY_CACHE, CAPABILITY_CACHE]);
3081
- const started = kickRefresh({ force: true });
3082
- return sendJSON(res, 200, { ok: true, refreshing: true, started });
3130
+ const refresh = kickRefreshDetailed({ force: true });
3131
+ return sendJSON(res, 200, { ok: refresh.status !== 'failed', started: refresh.status === 'started', refreshing: refresh.status === 'started' || refresh.status === 'already-running', refresh });
3083
3132
  }
3084
3133
  if (url === '/api/undo') return sendJSON(res, 200, undo(body.undoToken));
3085
3134
  if (url === '/api/set-lesson') return sendJSON(res, 200, setLesson(body));
@@ -18,9 +18,10 @@
18
18
  * with them.
19
19
  */
20
20
  import { spawn } from 'node:child_process';
21
+ import { DUAL_HOST_MODEL_IDS } from '../review-model-defaults.mjs';
21
22
 
22
23
  // Pinned on 2026-09-13 against the native hosts (same ids scripts/dual-host-deliberation.mjs pins).
23
- export const PRODUCER_MODELS = Object.freeze({ claude: 'claude-fable-5-1', codex: 'gpt-6-astra' });
24
+ export const PRODUCER_MODELS = DUAL_HOST_MODEL_IDS;
24
25
  export const PRODUCER_HOSTS = Object.freeze(['claude', 'codex']);
25
26
  export const DEFAULT_ROLES = Object.freeze({ generator: 'claude', judge: 'codex' });
26
27
  export const CLAUDE_TIMEOUT_MS = 900_000;
@@ -4,13 +4,23 @@
4
4
  //
5
5
  // node scripts/private-overlay.mjs --root <kbDir> --from <sidecarDir> --store <name> [--store ...]
6
6
  // [--dry-run] [--force] [--alias <name>=<nick,nick>] [--card <name>=<file>]
7
+ // [--origin <label>]
8
+ //
9
+ // `--origin` is stamped into SOURCE.json alongside updateManaged:false (omitted when not passed, so
10
+ // every existing caller's output is byte-identical). scripts/ingest-repo.mjs passes `local-ingest`
11
+ // (S3: explicit local ownership) so a repo pulled in on demand is distinguishable from a genuinely
12
+ // private pre-built sidecar; `--from` may equal `--root` for that case (the bytes are already in
13
+ // place — this writer degenerates to registry stamping only, since every file compares byte-equal
14
+ // to itself).
7
15
  //
8
16
  // WHY THIS FILE EXISTS (measured 2026-09-11/12). kb/forge-update.mjs preserves exactly one thing
9
17
  // across a public bundle apply: SOURCE.json store entries flagged `updateManaged:false`
10
18
  // (capturePrivateOverlayState, kb/forge-update.mjs:356-358) — the capture then needs a matching
11
19
  // RVF-GENERATIONS.json row with a `file` (:369-372) and picks up `## <name>` cards and alias rows
12
- // (:402-408). restoreTreeExact (:570-591) deletes every name the bundle lacks. Nothing anywhere
13
- // WROTE that flag: forge-refresh's writeSourceManifest records public repos only, and
20
+ // (:402-408). The apply path builds its candidate fresh from the extracted bundle
21
+ // (`restorePrivateFilesIntoCandidate`, forge-update.mjs — the one apply path, S1), so every name the
22
+ // bundle lacks is simply absent from that candidate. Nothing anywhere
23
+ // WROTE that flag: forge-refresh's writeSourceManifestFromLedger records public repos only, and
14
24
  // ingest-repo.mjs builds from a git checkout with a canonical URL. So private stores that arrive as
15
25
  // pre-built sidecars had no writer, sat in the root unflagged, and 0 of 8 survived the 2026-09-10
16
26
  // tree replacement. This is that writer. It ADDS entries and never rebuilds top-level identity:
@@ -20,7 +30,7 @@
20
30
  import fs from 'node:fs';
21
31
  import path from 'node:path';
22
32
  import { pathToFileURL } from 'node:url';
23
- import { sha256File } from './rvf-generation.mjs';
33
+ import { sha256File, projectSourceStore } from './rvf-generation.mjs';
24
34
 
25
35
  export const REQUIRED_SIDECARS = ['.big.rvf', '.big.rvf.embed.json', '.big.rvf.idmap.json', '.meta.json', '.passages.jsonl'];
26
36
  export const OPTIONAL_SIDECARS = ['.symbols.json', '-primer.md'];
@@ -94,7 +104,7 @@ function cardFor({ from, name, meta, cardFile }) {
94
104
  throw new Error(`${name}: no card source — add ${name}-primer.md to ${from}, or pass --card ${name}=<file>`);
95
105
  }
96
106
 
97
- function planStore({ root, from, name, force, aliases, cardFile }) {
107
+ function planStore({ root, from, name, force, aliases, cardFile, origin }) {
98
108
  if (!NAME_RE.test(name)) throw new Error(`invalid store name: ${name}`);
99
109
  const missing = REQUIRED_SIDECARS.filter((suffix) => !isRegular(path.join(from, name + suffix)));
100
110
  if (missing.length) throw new Error(`${name}: missing sidecar(s) in ${from}: ${missing.map((suffix) => name + suffix).join(', ')}`);
@@ -121,10 +131,21 @@ function planStore({ root, from, name, force, aliases, cardFile }) {
121
131
  }
122
132
  const rvf = path.join(from, `${name}.big.rvf`);
123
133
  const card = cardFor({ from, name, meta, cardFile });
134
+ const generation = { file: `${name}.big.rvf`, sha256: sha256File(rvf), bytes: fs.statSync(rvf).size, model: embed.model, dimensions: embed.dimensions, sourceCommit: null, builtUtc };
124
135
  return {
125
- name, files, copies, card, aliases,
126
- generation: { file: `${name}.big.rvf`, sha256: sha256File(rvf), bytes: fs.statSync(rvf).size, model: embed.model, dimensions: embed.dimensions, sourceCommit: null, builtUtc },
127
- source: { kbName: name, updateManaged: false, builtUtc, sourceCommit: null, sourceRepo: 'private', canonicalManifestUrl: null },
136
+ name, files, copies, card, aliases, generation,
137
+ // S4 (ONE PROVENANCE RECORD): source.sourceCommit/builtUtc are projected from `generation`
138
+ // above (projectSourceStore, scripts/rvf-generation.mjs) rather than a second, independent
139
+ // copy of the same facts. A private store has no selfUpdate/canonicalBundleUrl/builder —
140
+ // those are public-bundle-only fields, so they are simply never passed in the updater here.
141
+ // `origin` is opt-in (omitted entirely when not supplied, never written as null/undefined) so a
142
+ // caller that does not pass it gets the exact byte-identical stamp this writer has always
143
+ // produced. S3 (explicit local ownership): scripts/ingest-repo.mjs passes origin:'local-ingest'
144
+ // so a store this brain pulled in on demand is distinguishable, in SOURCE.json itself, from one
145
+ // that arrived as a genuinely private pre-built sidecar (a bare "private" sourceRepo alone conflated
146
+ // the two before this).
147
+ source: projectSourceStore(name, generation, { updateManaged: false, sourceRepo: 'private',
148
+ canonicalManifestUrl: null, ...(origin ? { origin } : {}) }),
128
149
  };
129
150
  }
130
151
 
@@ -132,7 +153,7 @@ function planStore({ root, from, name, force, aliases, cardFile }) {
132
153
  * Add private stores to a live brain root. Idempotent; refuses before writing anything; snapshots
133
154
  * every registry it is about to change to `<file>.pre-overlay-<epoch>` first; temp+rename writes.
134
155
  */
135
- export function applyPrivateOverlay({ root, from, stores, dryRun = false, force = false, aliases = {}, cards = {}, now = Date.now }) {
156
+ export function applyPrivateOverlay({ root, from, stores, dryRun = false, force = false, aliases = {}, cards = {}, now = Date.now, origin = null }) {
136
157
  root = path.resolve(String(root || ''));
137
158
  from = path.resolve(String(from || ''));
138
159
  if (!Array.isArray(stores) || !stores.length) throw new Error('at least one --store <name> is required');
@@ -158,7 +179,7 @@ export function applyPrivateOverlay({ root, from, stores, dryRun = false, force
158
179
  schemaVersion: 1, kind: 'ruvnet-brain-private-overlay-receipt', at: new Date(now()).toISOString(),
159
180
  root, from, dryRun, force, stores: [],
160
181
  };
161
- const plans = stores.map((name) => planStore({ root, from, name, force, aliases: aliases[name], cardFile: cards[name] }));
182
+ const plans = stores.map((name) => planStore({ root, from, name, force, aliases: aliases[name], cardFile: cards[name], origin }));
162
183
  for (const plan of plans) {
163
184
  const { name } = plan;
164
185
  if (!fence.has(name.toLowerCase())) {
@@ -226,6 +247,7 @@ function parseArgs(argv) {
226
247
  else if (flag === '--force') options.force = true;
227
248
  else if (flag === '--alias') { const [name, list] = pair(next(), flag); options.aliases[name] = list.split(',').map((s) => s.trim()).filter(Boolean); }
228
249
  else if (flag === '--card') { const [name, file] = pair(next(), flag); options.cards[name] = path.resolve(file); }
250
+ else if (flag === '--origin') options.origin = next();
229
251
  else throw new Error(`unknown argument: ${flag}`);
230
252
  }
231
253
  if (!options.root || !options.from || !options.stores.length) {
@@ -7,10 +7,11 @@ import { canonicalJson, digest } from './coverage-integrity.mjs';
7
7
  import { retrievalOracleExpectationFromPlan, validateRetrievalOracleReview } from './independent-review-receipt.mjs';
8
8
  import { validateRetrievalCanaryPlan, validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
9
9
  import { validateNightlyProofReceipt, validateNativeSchedulerSmoke } from './nightly-two-run-proof.mjs';
10
+ import { CLAUDE_FABLE_5_ID, GPT_5_6_SOL_ID, LEGACY_REVIEW_MODEL_IDS } from './review-model-defaults.mjs';
10
11
 
11
12
  export const PUBLIC_VERIFICATION_OS = Object.freeze(['linux', 'macos', 'windows']);
12
13
  export const PUBLIC_VERIFICATION_MODES = Object.freeze(['claude', 'codex', 'dual']);
13
- export const REQUIRED_REVIEW_MODELS = Object.freeze(['claude-fable-5', 'gpt-5.6-sol']);
14
+ export const REQUIRED_REVIEW_MODELS = LEGACY_REVIEW_MODEL_IDS;
14
15
 
15
16
  const HEX40 = /^[a-f0-9]{40}$/;
16
17
  const HEX64 = /^[a-f0-9]{64}$/;
@@ -110,10 +111,10 @@ export function validateIndependentReviewReceipt(review) {
110
111
  || typeof review.execution?.invocationDigest !== 'string' || !HEX64.test(review.execution.invocationDigest)) {
111
112
  throw new Error('independent review receipt is malformed, below 95, or incomplete');
112
113
  }
113
- if (review.model === 'claude-fable-5' && review.provider !== 'firstParty') {
114
+ if (review.model === CLAUDE_FABLE_5_ID && review.provider !== 'firstParty') {
114
115
  throw new Error('Fable 5 review did not use the verified first-party subscription path');
115
116
  }
116
- if (review.model === 'gpt-5.6-sol' && (review.provider !== 'openai' || typeof review.execution.threadId !== 'string'
117
+ if (review.model === GPT_5_6_SOL_ID && (review.provider !== 'openai' || typeof review.execution.threadId !== 'string'
117
118
  || !review.execution.threadId || !HEX64.test(String(review.execution.catalogRowSha256 || '')))) {
118
119
  throw new Error('GPT-5.6-Sol review lacks live catalog and thread evidence');
119
120
  }
@@ -654,6 +654,15 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
654
654
  }
655
655
  }
656
656
  const result = await session.search({ query, k, timeoutMs: DEADLINE_MS });
657
+ if (result.error || !result.mcpResult || (Object.hasOwn(result, 'status') && result.status !== 0)) {
658
+ // Same guard as warmupInstalled() above and host-install-matrix.mjs's equivalent canary
659
+ // search: without it, a real timeout on this full-corpus search reaches
660
+ // parseRetrievalResult() with no mcpResult, which throws the generic "UNKNOWN:
661
+ // incompatible MCP response lacks structured retrieval results" — masking a timeout as a
662
+ // data-shape error. This is the release blocker traced from v4.3.28's public verification
663
+ // failure ("retrieval canary acceptance failed for claude", run 35559726522).
664
+ throw new Error(`installed Brain search failed for ${mode} (query="${query}"): ${result.error?.message || `no MCP result within ${DEADLINE_MS}ms`}`);
665
+ }
657
666
  return parseRetrievalResult(result.mcpResult, { query, k });
658
667
  },
659
668
 
@@ -26,7 +26,6 @@ const LANES = Object.freeze({
26
26
  // Fail design/provenance issues before any long behavioral or platform lane starts.
27
27
  preflight: [
28
28
  { kind: 'command', command: process.execPath, args: ['--check', 'scripts/qe/agentic-qe-4.3.mjs'], timeoutMs: 10_000 },
29
- { kind: 'command', command: process.execPath, args: ['--check', 'scripts/qe/aggregate-4.3.mjs'], timeoutMs: 10_000 },
30
29
  { kind: 'command', command: npm, args: ['run', 'version:check'], timeoutMs: 60_000 },
31
30
  vitest(['tests/qe/gpt56/critical-risk-map.test.mjs'], 60_000),
32
31
  ],
@@ -149,7 +149,7 @@ export async function reconstructGists(source, { fetchFn = globalThis.fetch, con
149
149
  const digest = sha256(bytes);
150
150
  if (digest !== file.sha256) fail(`${gistId}/${file.filename} sha256 ${digest} differs from receipt ${file.sha256}`);
151
151
  try {
152
- return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
152
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
153
153
  } catch {
154
154
  fail(`${gistId}/${file.filename} is not valid UTF-8 text`);
155
155
  return undefined; // unreachable; keeps linters happy about a missing return on every path
Binary file