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.
- package/README.md +1 -1
- package/bin/install.mjs +115 -44
- package/console/app.js +32 -11
- package/console/style.css +5 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +17 -17
- package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
- package/plugin/scripts/continuation-gate.mjs +41 -1
- package/plugin/scripts/learn-flush.mjs +37 -2
- package/plugin/scripts/project-identity.mjs +89 -0
- package/plugin/scripts/ruflo-bin.mjs +81 -0
- package/plugin/scripts/session-snapshot-hook.mjs +4 -1
- package/plugin/scripts/session-start-core.mjs +19 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
- package/scripts/candidate-host-evidence.mjs +49 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
- package/scripts/console-runtime-identity.mjs +74 -0
- package/scripts/distill-project.mjs +9 -1
- package/scripts/doc-currency.mjs +15 -2
- package/scripts/health-repair.mjs +88 -11
- package/scripts/host-install-matrix.mjs +155 -0
- package/scripts/learning-replay-contract.mjs +14 -4
- package/scripts/learning-replay-execution.mjs +4 -3
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/memory-doctor.mjs +26 -23
- package/scripts/model-router-catalog.mjs +34 -0
- package/scripts/onboarding-console.mjs +34 -12
- package/scripts/publication-receipt.mjs +135 -34
- package/scripts/release-evidence-aggregate.mjs +55 -0
- package/scripts/release-payload.mjs +100 -0
- package/scripts/release-transaction-provider.mjs +260 -55
- package/scripts/release-transaction.mjs +295 -105
- package/scripts/release.mjs +41 -12
- package/scripts/rvf-generation.mjs +17 -0
- package/scripts/selfcheck.mjs +20 -5
- package/scripts/staged-host-verifier.mjs +27 -57
- 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 — [](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 {
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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 ${
|
|
627
|
+
throw new Error(`console runtime is incomplete: missing ${relative}`);
|
|
630
628
|
}
|
|
631
|
-
const target = path.join(staged,
|
|
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
|
-
|
|
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,
|
|
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, '&').replace(/</g, '<').
|
|
|
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
|
-
|
|
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
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
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.
|
|
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.
|
|
4
|
+
"version": "4.0.12",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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
|
-
|
|
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);
|