ruvnet-brain 4.0.7 → 4.0.12

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 (39) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +115 -44
  3. package/console/app.js +32 -11
  4. package/console/style.css +5 -0
  5. package/package.json +1 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/hooks/codex-hooks.json +17 -17
  9. package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
  10. package/plugin/scripts/continuation-gate.mjs +41 -1
  11. package/plugin/scripts/learn-flush.mjs +37 -2
  12. package/plugin/scripts/project-identity.mjs +89 -0
  13. package/plugin/scripts/ruflo-bin.mjs +81 -0
  14. package/plugin/scripts/session-snapshot-hook.mjs +4 -1
  15. package/plugin/scripts/session-start-core.mjs +19 -2
  16. package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
  17. package/scripts/candidate-host-evidence.mjs +49 -0
  18. package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
  19. package/scripts/console-runtime-identity.mjs +74 -0
  20. package/scripts/distill-project.mjs +9 -1
  21. package/scripts/doc-currency.mjs +15 -2
  22. package/scripts/health-repair.mjs +88 -11
  23. package/scripts/host-install-matrix.mjs +155 -0
  24. package/scripts/learning-replay-contract.mjs +14 -4
  25. package/scripts/learning-replay-execution.mjs +4 -3
  26. package/scripts/learning-replay-fixture.mjs +23 -2
  27. package/scripts/memory-doctor.mjs +26 -23
  28. package/scripts/model-router-catalog.mjs +34 -0
  29. package/scripts/onboarding-console.mjs +34 -12
  30. package/scripts/publication-receipt.mjs +135 -34
  31. package/scripts/release-evidence-aggregate.mjs +55 -0
  32. package/scripts/release-payload.mjs +100 -0
  33. package/scripts/release-transaction-provider.mjs +260 -55
  34. package/scripts/release-transaction.mjs +295 -105
  35. package/scripts/release.mjs +41 -12
  36. package/scripts/rvf-generation.mjs +17 -0
  37. package/scripts/selfcheck.mjs +20 -5
  38. package/scripts/staged-host-verifier.mjs +27 -57
  39. package/scripts/sync-version.mjs +10 -10
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.0.7 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.7-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.0.12 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.12-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
10
10
 
package/bin/install.mjs CHANGED
@@ -24,7 +24,10 @@ import {
24
24
  requiredEmbedderModels,
25
25
  missingEmbedderModels,
26
26
  } from '../kb/model-requirements.mjs';
27
- import { mergeManagedCatalog } from '../scripts/model-router-catalog.mjs';
27
+ import { applyManagedCatalogUpdate } from '../scripts/model-router-catalog.mjs';
28
+ import {
29
+ CONSOLE_RUNTIME_SURFACE, CONSOLE_RUNTIME_IDENTITY_FILE, consoleRuntimeDigest,
30
+ } from '../scripts/console-runtime-identity.mjs';
28
31
 
29
32
  // SEC-0010 #6 — the Ed25519 PUBLIC key is EMBEDDED here (not a separate file) so the installer's
30
33
  // trust root travels with the installer code itself: an attacker who swaps the downloaded bundle
@@ -613,22 +616,17 @@ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT)
613
616
  fs.rmSync(prior, { recursive: true, force: true });
614
617
  fs.mkdirSync(staged, { recursive: true, mode: 0o700 });
615
618
 
616
- const required = [
617
- ['console', 'console'],
618
- ['scripts', 'scripts'],
619
- ['plugin/scripts', 'plugin/scripts'],
620
- ['data/model-catalog.json', 'data/model-catalog.json'],
621
- ['kb/brain-profile.mjs', 'kb/brain-profile.mjs'],
622
- ['bin/install.mjs', 'bin/install.mjs'],
623
- ['package.json', 'package.json'],
624
- ];
625
- for (const [from, to] of required) {
626
- const source = path.join(sourceRoot, from);
619
+ // The copy list IS the generation's digest input (scripts/console-runtime-identity.mjs). Adding a
620
+ // file the runtime needs adds it to the runtime's identity in the same edit, so an asset can never
621
+ // again travel separately from the executable that reads it (#76) and a candidate can never again
622
+ // be indistinguishable from the one it replaces (#79).
623
+ for (const relative of CONSOLE_RUNTIME_SURFACE) {
624
+ const source = path.join(sourceRoot, relative);
627
625
  if (!fs.existsSync(source)) {
628
626
  fs.rmSync(staged, { recursive: true, force: true });
629
- throw new Error(`console runtime is incomplete: missing ${from}`);
627
+ throw new Error(`console runtime is incomplete: missing ${relative}`);
630
628
  }
631
- const target = path.join(staged, to);
629
+ const target = path.join(staged, relative);
632
630
  fs.mkdirSync(path.dirname(target), { recursive: true });
633
631
  fs.cpSync(source, target, { recursive: true, force: true, preserveTimestamps: true });
634
632
  }
@@ -661,15 +659,28 @@ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT)
661
659
  throw new Error(`console runtime failed syntax verification: ${path.relative(staged, syntaxTarget)}`);
662
660
  }
663
661
  }
662
+ // #76: prove the installed What's New boundary from the staged bytes, not from a file listing. The
663
+ // executable exits nonzero when its version manifest or curated notes are absent, so a runtime that
664
+ // would have to tell the user "the notes are unavailable" never activates in the first place.
665
+ const stagedWhatsNew = path.join(staged, 'plugin', 'scripts', 'whats-new.mjs');
666
+ const whatsNew = spawnSync(process.execPath, [stagedWhatsNew], { encoding: 'utf8' });
667
+ if (whatsNew.error || whatsNew.status !== 0) {
668
+ const detail = (whatsNew.stderr || whatsNew.error?.message || `exit ${whatsNew.status}`).trim();
669
+ fs.rmSync(staged, { recursive: true, force: true });
670
+ throw new Error(`console runtime cannot report its own release notes: ${detail}`);
671
+ }
664
672
  const identity = {
665
673
  product: 'ruvnet-brain-console-runtime',
666
674
  schema: 1,
667
675
  apiContract: 1,
668
676
  runtimeVersion: packageManifest.version,
669
677
  entrypoint: 'scripts/onboarding-console.mjs',
670
- sourceSha256: crypto.createHash('sha256').update(fs.readFileSync(stagedEntry)).digest('hex'),
678
+ // The WHOLE runtime, not just its entrypoint (#79). The launcher compares this against a live
679
+ // server's self-reported identity, so it has to change whenever anything the server executes or
680
+ // serves changes — otherwise a stale process passes as current.
681
+ sourceSha256: consoleRuntimeDigest(staged),
671
682
  };
672
- fs.writeFileSync(path.join(staged, 'runtime-identity.json'), `${JSON.stringify(identity, null, 2)}\n`, { mode: 0o600 });
683
+ fs.writeFileSync(path.join(staged, CONSOLE_RUNTIME_IDENTITY_FILE), `${JSON.stringify(identity, null, 2)}\n`, { mode: 0o600 });
673
684
 
674
685
  let state = 'staged';
675
686
  const entry = path.join(runtime, identity.entrypoint);
@@ -1962,11 +1973,15 @@ async function doctor() {
1962
1973
  && !codexLifecycleGuidance(codexLifecycle).healthy
1963
1974
  && !codexLifecycleGuidance(codexLifecycle).intentional,
1964
1975
  );
1976
+ // A staged release verifier can execute the packaged hooks under Codex's explicit automation
1977
+ // trust bypass. That proves the real hook commands without pretending a fresh interactive user
1978
+ // has already reviewed them; normal end-user doctor runs remain fail-closed on pending trust.
1979
+ const codexTrustBypassed = process.env.RUVNET_CODEX_HOOK_TRUST_MODE === 'bypass';
1965
1980
  const codexWiringFailed = Boolean(cx.host && !cx.wired);
1966
1981
  const codexReadinessFailed = Boolean(codexMcp?.blocking);
1967
1982
  const failed = (hookResult ? hookResult.exitCode !== 0 : !allGreen)
1968
1983
  || groundingUnprovenPersisted
1969
- || codexLifecycleFailed
1984
+ || (codexLifecycleFailed && !codexTrustBypassed)
1970
1985
  || codexWiringFailed
1971
1986
  || codexReadinessFailed
1972
1987
  || !hostConvergence.healthy
@@ -1999,7 +2014,7 @@ async function runSelfCheck({ installState = null, quiet = false } = {}) {
1999
2014
  mod = await import(new URL('../scripts/selfcheck.mjs', import.meta.url).href);
2000
2015
  } catch (e) {
2001
2016
  warn(`self-check could not run (${e && e.message}) — this install has NOT been verified end to end`);
2002
- return { exitCode: 0, violations: [], lines: [], unavailable: true };
2017
+ return { exitCode: FLAG_DOCTOR_HOOKS ? 1 : 0, violations: [], lines: [], unavailable: true };
2003
2018
  }
2004
2019
  const result = await mod.selfCheck({ installState, security: true });
2005
2020
  if (!quiet || result.violations.length) console.log(mod.formatVerdict(result, { color: c }));
@@ -2226,6 +2241,37 @@ const xmlEscape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').
2226
2241
  const cronExample = (kbDir) =>
2227
2242
  `47 3 * * * cd ${kbDir} && ${process.execPath} forge-update.mjs --apply >> ${kbDir}/update.log 2>&1`;
2228
2243
 
2244
+ /**
2245
+ * kb/forge-update.mjs exit 11 = "the download completed but the bundle on disk did NOT change"
2246
+ * (its `EXIT_NOT_LANDED`). Duplicated as a literal on purpose: the updater lives in the KB BUNDLE,
2247
+ * which versions and ships independently of this installer, so there is no import to share. The
2248
+ * two are pinned together by tests/unit/update-not-landed-exit.test.mjs, which reads the constant
2249
+ * out of kb/forge-update.mjs and fails if they ever drift.
2250
+ */
2251
+ export const UPDATE_NOT_LANDED = 11;
2252
+
2253
+ /**
2254
+ * What `--update` does with the KB updater's exit code (issue #106).
2255
+ *
2256
+ * `--update` used to convert an honest failure into a reported success. The updater detected the
2257
+ * problem itself and said so — "UPDATE MISMATCH: SOURCE.json on disk is IDENTICAL to before the
2258
+ * update … REFUSING to report success" — and then the fresh-install FALLBACK below ran, succeeded
2259
+ * at re-installing the very same bytes, and its exit 0 became the exit code of the whole command.
2260
+ * A user's scheduled job read that as success while the corpus had not moved.
2261
+ *
2262
+ * The fallback exists for ONE thing: an old bundle whose canonical manifest URL 404s, where a fresh
2263
+ * install genuinely rescues the user. "Nothing landed" is not that case — it is a TRUE verdict
2264
+ * about an intact KB, and re-downloading the same bundle cannot change it. So that verdict is
2265
+ * terminal and keeps its own exit code; every other failure keeps today's fallback behaviour.
2266
+ */
2267
+ export function classifyUpdaterExit(status, { fallbackAllowed = true } = {}) {
2268
+ if (status === 0) return { verdict: 'updated', fallback: false, exitCode: 0 };
2269
+ if (status === UPDATE_NOT_LANDED) {
2270
+ return { verdict: 'not-landed', fallback: false, exitCode: UPDATE_NOT_LANDED };
2271
+ }
2272
+ return { verdict: 'failed', fallback: fallbackAllowed, exitCode: status || 1 };
2273
+ }
2274
+
2229
2275
  function missingUpdaterHelp(kbDir) {
2230
2276
  console.error(`\n${c.red('✗ can\'t update:')} ${c.bold('forge-update.mjs')} is missing from ${kbDir}.`);
2231
2277
  console.error(` Either no brain is installed there, or the bundle predates the self-updater.`);
@@ -2362,12 +2408,29 @@ function runUpdate() {
2362
2408
  // real user, Jan Lafko, hit). NEVER leave the user stranded at a 404: re-run THIS installer as a
2363
2409
  // fresh install, which pulls the latest Release DIRECTLY (releases/latest) and never touches the
2364
2410
  // manifest — so --update always succeeds and self-heals the stale SOURCE.json in one shot.
2365
- if (updateStatus !== 0 && !process.env.RUVNET_BRAIN_NO_UPDATE_FALLBACK) {
2411
+ //
2412
+ // Scoped again 2026-08-04 (issue #106): "nothing landed" is excluded, because for THAT verdict the
2413
+ // fallback re-installed identical bytes and its exit 0 became the command's exit code, turning the
2414
+ // updater's own correct refusal into a reported success.
2415
+ const outcome = classifyUpdaterExit(updateStatus, {
2416
+ fallbackAllowed: !process.env.RUVNET_BRAIN_NO_UPDATE_FALLBACK,
2417
+ });
2418
+ if (outcome.verdict === 'not-landed') {
2419
+ console.error(`\n ${c.red('✗ the knowledge bundle did not change.')} The updater downloaded a bundle and refused to`);
2420
+ console.error(` call it an update because the copy on disk is identical to the one it replaced.`);
2421
+ console.error(` Nothing is broken and nothing was lost — but this run is ${c.bold('not')} a success, so it exits`);
2422
+ console.error(` ${c.bold(String(UPDATE_NOT_LANDED))} rather than 0 and a scheduled job will see the failure (issue #106).`);
2423
+ console.error(` If you believe a newer build exists, check: ${c.bold('node forge-update.mjs --check')} in ${kbDir}`);
2424
+ process.exit(outcome.exitCode);
2425
+ }
2426
+ if (outcome.fallback) {
2366
2427
  warn("\nthe bundle's own updater couldn't complete — falling back to a fresh install of the latest Release (this always works)…\n");
2367
2428
  const self = fileURLToPath(import.meta.url);
2368
2429
  const fr = spawnSync(process.execPath, [self, '--force'], { stdio: 'inherit',
2369
2430
  env: { ...process.env, RUVNET_BRAIN_NO_UPDATE_FALLBACK: '1' } });
2370
2431
  updateStatus = fr.error ? 1 : (fr.status === null ? 1 : fr.status);
2432
+ } else {
2433
+ updateStatus = outcome.exitCode;
2371
2434
  }
2372
2435
  }
2373
2436
  if (updateStatus === 0) {
@@ -2378,6 +2441,25 @@ function runUpdate() {
2378
2441
  updateStatus = 1;
2379
2442
  }
2380
2443
  }
2444
+ // Issue #87: a normal lifecycle update must also carry MANAGED MODEL additions to an existing
2445
+ // user. This call used to live only in offerRouterProfile() — the fresh-install path — so someone
2446
+ // who already had ~/.claude/model-router/catalog.json (i.e. every existing user, the only ones who
2447
+ // can be behind) never received a newly shipped managed candidate no matter how often they
2448
+ // updated. Additive and non-interactive; their overrides win, metered rows are never auto-enabled,
2449
+ // and a failure here is reported but never fails an otherwise-good update.
2450
+ if (updateStatus === 0) {
2451
+ try {
2452
+ const managed = applyManagedCatalogUpdate({
2453
+ routerDir: path.join(os.homedir(), '.claude', 'model-router'),
2454
+ packageRoot: REPO_ROOT,
2455
+ });
2456
+ if (managed.action === 'merged' && managed.added.length) {
2457
+ ok(`model router: added ${managed.added.length} managed model(s) — ${managed.added.join(', ')} (your catalog edits were preserved)`);
2458
+ }
2459
+ } catch (e) {
2460
+ warn(`managed model additions were not merged (${e.message}); your catalog was left unchanged`);
2461
+ }
2462
+ }
2381
2463
  // `--update --auto` = update now AND enroll in Evergreen, so this is the LAST time it's ever run by
2382
2464
  // hand. Only enroll if the update itself succeeded — never promise "you're set forever" on a failed
2383
2465
  // update. enableNightly() prints its own real verification (plist path + launchctl result).
@@ -2971,31 +3053,20 @@ export async function offerRouterProfile() {
2971
3053
  );
2972
3054
 
2973
3055
  fs.mkdirSync(path.join(routerDir, 'bin'), { recursive: true });
2974
- for (const [src, dst] of [['catalog.template.json', 'catalog.json'], ['policy.default.mjs', 'policy.default.mjs']]) {
2975
- const s = path.join(pkgRoot, 'config', 'model-router', src);
2976
- const d = path.join(routerDir, dst);
2977
- if (!fs.existsSync(s)) continue;
2978
- if (!fs.existsSync(d)) {
2979
- fs.copyFileSync(s, d);
2980
- ok(`installed ${dst} (edit freely — goldie keeps prices fresh where scheduled)`);
2981
- continue;
2982
- }
2983
- if (src === 'catalog.template.json') {
2984
- try {
2985
- const existing = JSON.parse(fs.readFileSync(d, 'utf8'));
2986
- const managed = JSON.parse(fs.readFileSync(s, 'utf8'));
2987
- const merged = mergeManagedCatalog(existing, managed);
2988
- if (merged !== existing) {
2989
- fs.copyFileSync(d, `${d}.pre-managed-merge`);
2990
- const tmp = `${d}.tmp-${process.pid}`;
2991
- fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
2992
- fs.renameSync(tmp, d);
2993
- ok(`merged ${merged.candidates.length - existing.candidates.length} managed subscription model(s); your catalog overrides were preserved`);
2994
- }
2995
- } catch (error) {
2996
- warn(`managed model additions were not merged (${error.message}); your existing catalog was left unchanged`);
2997
- }
2998
- }
3056
+ const policySrc = path.join(pkgRoot, 'config', 'model-router', 'policy.default.mjs');
3057
+ const policyDst = path.join(routerDir, 'policy.default.mjs');
3058
+ if (fs.existsSync(policySrc) && !fs.existsSync(policyDst)) {
3059
+ fs.copyFileSync(policySrc, policyDst);
3060
+ ok('installed policy.default.mjs (edit freely — goldie keeps prices fresh where scheduled)');
3061
+ }
3062
+ // ONE implementation of the managed-catalog step, shared with runUpdate() (issue #87). It used to
3063
+ // live here only, which is exactly why existing users never received managed additions.
3064
+ try {
3065
+ const managed = applyManagedCatalogUpdate({ routerDir, packageRoot: pkgRoot });
3066
+ if (managed.action === 'created') ok('installed catalog.json (edit freely — goldie keeps prices fresh where scheduled)');
3067
+ if (managed.action === 'merged') ok(`merged ${managed.added.length} managed subscription model(s); your catalog overrides were preserved`);
3068
+ } catch (error) {
3069
+ warn(`managed model additions were not merged (${error.message}); your existing catalog was left unchanged`);
2999
3070
  }
3000
3071
  let copied = 0;
3001
3072
  // dispatch-receipt + metaharness-receipts added 2026-07-13: without the LOGGER, subagent routing is
package/console/app.js CHANGED
@@ -2313,6 +2313,25 @@ function renderDistribution(u) {
2313
2313
  el('p', { class: 'fineprint' }, u.note));
2314
2314
  }
2315
2315
 
2316
+ // One provider's key chip. THREE states, never two: found, not found, and NOT CHECKED. The third
2317
+ // exists because an instrument that could not run has found nothing — it has not found "no key"
2318
+ // (issue #86). Pure and total, so the not-checked branch cannot be reached by accident.
2319
+ function providerKeyChip(id, keys, keysVerified, names) {
2320
+ const name = names[id] || id;
2321
+ if (!keysVerified) {
2322
+ return el('span', {
2323
+ class: 'plan-key unknown',
2324
+ title: `Not checked: Brain could not load its provider catalog, so it cannot tell whether an API key for ${name} is set on this machine`,
2325
+ }, el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, '?'), ` ${name}`);
2326
+ }
2327
+ const found = !!keys[id];
2328
+ return el('span', {
2329
+ class: `plan-key ${found ? 'yes' : 'no'}`,
2330
+ title: found ? `An API key for ${name} is set on this machine`
2331
+ : `No API key for ${name} found on this machine`,
2332
+ }, el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, found ? '✓' : '✗'), ` ${name}`);
2333
+ }
2334
+
2316
2335
  // Plan block (issue #24, applying sparkling's #21 redesign) — three genuinely different questions
2317
2336
  // used to render as one flat row of identical "chips": which subscription this runs on ("house" — a
2318
2337
  // word nobody outside the source knows), whether cheap-task routing is on, and which OTHER API keys
@@ -2339,19 +2358,21 @@ function renderProviders(sv) {
2339
2358
 
2340
2359
  // BOX 1 — YOUR PLAN. The one choice here that changes cost: which subscription MetaHarness treats
2341
2360
  // as $0. Footer checklist: which OTHER providers have a real API key on this machine — honest now.
2361
+ //
2362
+ // Issue #86: `keys` is only a MEASUREMENT while the verified provider catalog actually loaded. When
2363
+ // that asset was missing from the packaged runtime, the server fell back to native detections and
2364
+ // said so in providerCatalog — and this checklist ignored it, printing a confident "✗ … No API key
2365
+ // for Gemini found on this machine" straight off `keys`. That turned an internal packaging failure
2366
+ // into a stated fact about the user's credentials. Not-checked is now its own third state.
2367
+ const catalogHealth = re.providerCatalog || null;
2368
+ const keysVerified = !catalogHealth
2369
+ || (catalogHealth.keysVerified !== false && catalogHealth.status !== 'degraded');
2342
2370
  const others = ['anthropic', 'openai', 'google', 'xai'].filter((id) => id !== house.provider);
2343
2371
  const checklist = el('div', { class: 'plan-keys' },
2344
- el('span', { class: 'plan-keys-lab' }, 'Other keys found:'),
2345
- ...others.map((id) => {
2346
- const ok = !!keys[id];
2347
- return el('span', {
2348
- class: `plan-key ${ok ? 'yes' : 'no'}`,
2349
- title: ok ? `An API key for ${KEY_NAME[id]} is set on this machine`
2350
- : `No API key for ${KEY_NAME[id]} found on this machine`,
2351
- },
2352
- el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, ok ? '✓' : '✗'),
2353
- ` ${KEY_NAME[id]}`);
2354
- }));
2372
+ el('span', { class: 'plan-keys-lab' }, keysVerified ? 'Other keys found:' : 'Other keys:'),
2373
+ ...others.map((id) => providerKeyChip(id, keys, keysVerified, KEY_NAME)),
2374
+ ...(keysVerified ? [] : [el('span', { class: 'plan-keys-note' },
2375
+ `Not checked — Brain could not load its provider catalog${catalogHealth && catalogHealth.detail ? ` (${catalogHealth.detail})` : ''}.`)]));
2355
2376
  const planBox = el('div', { class: 'plan-box' },
2356
2377
  el('span', { class: 'plan-label' }, 'Your plan', infoBtn('Your plan and OpenRouter', PROVIDERS_INFO)),
2357
2378
  head('is-house', houseName, action('Change', 'provider')),
package/console/style.css CHANGED
@@ -1236,6 +1236,11 @@ details.sub .sub-body > *:first-child { margin-top: 4px; }
1236
1236
  .plan-key-mark { font-weight: 700; }
1237
1237
  .plan-key.yes .plan-key-mark { color: var(--green-text); }
1238
1238
  .plan-key.no, .plan-key.no .plan-key-mark { color: var(--faint); }
1239
+ /* Not checked (issue #86): visually distinct from "no key found" so a degraded catalog never reads
1240
+ as a finding about the user's machine. */
1241
+ .plan-key.unknown { color: var(--muted); }
1242
+ .plan-key.unknown .plan-key-mark { color: var(--amber); }
1243
+ .plan-keys-note { flex-basis: 100%; color: var(--muted); font-size: 11.5px; line-height: 1.5; }
1239
1244
 
1240
1245
  /* WP2b — first-run empty state: confident, designed, not an apology */
1241
1246
  .mh-empty {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.7",
3
+ "version": "4.0.12",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 69 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.0.7",
4
+ "version": "4.0.12",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.7",
3
+ "version": "4.0.12",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 session-start",
10
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 session-start",
11
11
  "timeout": 5
12
12
  }
13
13
  ]
@@ -18,12 +18,12 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
21
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 ground-ruvnet",
21
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 ground-ruvnet",
22
22
  "timeout": 10
23
23
  },
24
24
  {
25
25
  "type": "command",
26
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 unprompted-speech UserPromptSubmit",
26
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 unprompted-speech UserPromptSubmit",
27
27
  "timeout": 10
28
28
  }
29
29
  ]
@@ -35,7 +35,7 @@
35
35
  "hooks": [
36
36
  {
37
37
  "type": "command",
38
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 hijack-ruvnet",
38
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 hijack-ruvnet",
39
39
  "timeout": 5
40
40
  }
41
41
  ]
@@ -45,7 +45,7 @@
45
45
  "hooks": [
46
46
  {
47
47
  "type": "command",
48
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 route-dispatch",
48
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 route-dispatch",
49
49
  "timeout": 5
50
50
  }
51
51
  ]
@@ -55,17 +55,17 @@
55
55
  "hooks": [
56
56
  {
57
57
  "type": "command",
58
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 ground-before-write",
58
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 ground-before-write",
59
59
  "timeout": 5
60
60
  },
61
61
  {
62
62
  "type": "command",
63
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 protect-state",
63
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 protect-state",
64
64
  "timeout": 5
65
65
  },
66
66
  {
67
67
  "type": "command",
68
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 unprompted-speech PreToolUse-write",
68
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 unprompted-speech PreToolUse-write",
69
69
  "timeout": 5
70
70
  }
71
71
  ]
@@ -75,17 +75,17 @@
75
75
  "hooks": [
76
76
  {
77
77
  "type": "command",
78
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 verify-interface",
78
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 verify-interface",
79
79
  "timeout": 5
80
80
  },
81
81
  {
82
82
  "type": "command",
83
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 design-wall",
83
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 design-wall",
84
84
  "timeout": 5
85
85
  },
86
86
  {
87
87
  "type": "command",
88
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 unprompted-speech PreToolUse-bash",
88
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 unprompted-speech PreToolUse-bash",
89
89
  "timeout": 5
90
90
  }
91
91
  ]
@@ -97,7 +97,7 @@
97
97
  "hooks": [
98
98
  {
99
99
  "type": "command",
100
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 grounding-stamp",
100
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 grounding-stamp",
101
101
  "timeout": 5
102
102
  }
103
103
  ]
@@ -107,7 +107,7 @@
107
107
  "hooks": [
108
108
  {
109
109
  "type": "command",
110
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 learn-capture",
110
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 learn-capture",
111
111
  "timeout": 5
112
112
  }
113
113
  ]
@@ -117,7 +117,7 @@
117
117
  "hooks": [
118
118
  {
119
119
  "type": "command",
120
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 md-stamp",
120
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 md-stamp",
121
121
  "timeout": 5
122
122
  }
123
123
  ]
@@ -127,7 +127,7 @@
127
127
  "hooks": [
128
128
  {
129
129
  "type": "command",
130
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 signal-watch",
130
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 signal-watch",
131
131
  "timeout": 5
132
132
  }
133
133
  ]
@@ -139,7 +139,7 @@
139
139
  "hooks": [
140
140
  {
141
141
  "type": "command",
142
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 2500 learn-flush",
142
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 2500 learn-flush",
143
143
  "timeout": 3
144
144
  }
145
145
  ]
@@ -150,7 +150,7 @@
150
150
  "hooks": [
151
151
  {
152
152
  "type": "command",
153
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{input:f.readFileSync(0),encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 continuation-gate",
153
+ "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 continuation-gate",
154
154
  "timeout": 10
155
155
  }
156
156
  ]
@@ -41,7 +41,78 @@ function activeRoot() {
41
41
  }
42
42
  }
43
43
 
44
- const input = fs.readFileSync(0);
44
+ function readHookInput(limit = 1024 * 1024) {
45
+ return new Promise((resolve) => {
46
+ const chunks = [];
47
+ let bytes = 0;
48
+ let settled = false;
49
+ let idle;
50
+ let hardDeadline;
51
+ let poll;
52
+
53
+ const cleanup = () => {
54
+ clearTimeout(idle);
55
+ clearTimeout(hardDeadline);
56
+ clearInterval(poll);
57
+ process.stdin.off('data', onData);
58
+ process.stdin.off('readable', onReadable);
59
+ process.stdin.off('end', finish);
60
+ process.stdin.off('close', finish);
61
+ process.stdin.off('error', finish);
62
+ process.stdin.pause();
63
+ };
64
+ const finish = () => {
65
+ if (settled) return;
66
+ settled = true;
67
+ cleanup();
68
+ resolve(Buffer.concat(chunks));
69
+ };
70
+ const armIdle = () => {
71
+ clearTimeout(idle);
72
+ idle = setTimeout(finish, 100);
73
+ };
74
+ const onData = (chunk) => {
75
+ if (bytes < limit) {
76
+ const kept = chunk.subarray(0, limit - bytes);
77
+ chunks.push(kept);
78
+ bytes += kept.length;
79
+ }
80
+ if (bytes >= limit) return finish();
81
+ try {
82
+ JSON.parse(Buffer.concat(chunks).toString('utf8'));
83
+ return finish();
84
+ } catch {
85
+ armIdle();
86
+ }
87
+ };
88
+ // Windows inherited pipes can expose the first value through the readable interface without
89
+ // emitting a data event until the writer closes. Drain both interfaces; POSIX keeps the fast
90
+ // data path and Windows gets the same complete-value contract without waiting for EOF.
91
+ const onReadable = () => {
92
+ let chunk;
93
+ while ((chunk = process.stdin.read()) !== null) onData(chunk);
94
+ };
95
+
96
+ process.stdin.on('data', onData);
97
+ process.stdin.on('readable', onReadable);
98
+ process.stdin.once('end', finish);
99
+ process.stdin.once('close', finish);
100
+ process.stdin.once('error', finish);
101
+ // Some Windows pipe handles can deliver repeated readable notifications while the writer
102
+ // remains open, continually resetting an idle timer. The host contract is one JSON value, not
103
+ // EOF; an absolute cap makes the wrapper deterministic under that regime without weakening
104
+ // the normal complete-value fast path.
105
+ // Some Windows inherited pipes do not emit a readable/data notification until the writer
106
+ // closes. Poll the non-blocking stream read instead of using fs.readSync, which can block the
107
+ // event loop before the first pipe chunk is surfaced.
108
+ poll = setInterval(onReadable, 10);
109
+ hardDeadline = setTimeout(finish, 250);
110
+ armIdle();
111
+ process.stdin.resume();
112
+ });
113
+ }
114
+
115
+ const input = await readHookInput();
45
116
  const root = activeRoot();
46
117
  const adapter = root && path.join(root, 'scripts', 'codex-hook-adapter.mjs');
47
118
  if (!adapter || !fs.existsSync(adapter)) process.exit(0);