ruvnet-brain 4.4.1 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/install.mjs +612 -113
- package/console/app.js +178 -81
- package/console/index.html +1 -1
- package/console/install-architecture.html +1 -0
- package/console/scope.css +4 -1
- package/console/style.css +13 -0
- package/console/tips.html +4 -4
- package/kb/brain-profile.mjs +1 -0
- package/kb/corpus-release-identity.mjs +1 -1
- package/kb/forge-update.mjs +41 -17
- package/kb/model-requirements.mjs +4 -1
- package/kb/update-storage-transaction.mjs +79 -0
- package/kb/zip-extract.mjs +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +5 -4
- package/plugin/commands/configure.md +5 -4
- package/plugin/commands/rnb-brief.md +41 -0
- package/plugin/commands/rnb.md +80 -0
- package/plugin/commands/rnbc.md +80 -0
- package/plugin/commands/rvbc.md +5 -4
- package/plugin/commands/rvcb.md +5 -4
- package/plugin/commands/whats-new.md +4 -4
- package/plugin/mcp/server.mjs +10 -2
- package/plugin/scripts/advocacy-route.mjs +59 -23
- package/plugin/scripts/anticipate.sh +4 -0
- package/plugin/scripts/brain-confirmation.mjs +258 -0
- package/plugin/scripts/brain-footprint.mjs +494 -0
- package/plugin/scripts/brain-location.mjs +47 -0
- package/plugin/scripts/capability-registry.mjs +11 -1
- package/plugin/scripts/continuity-brief.mjs +324 -0
- package/plugin/scripts/continuity-events.mjs +327 -0
- package/plugin/scripts/continuity-journal.mjs +500 -0
- package/plugin/scripts/decision-gate.mjs +56 -3
- package/plugin/scripts/footprint-io.mjs +186 -0
- package/plugin/scripts/ground-before-write.sh +8 -1
- package/plugin/scripts/ground-ruvnet.sh +103 -12
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-stamp.sh +3 -0
- package/plugin/scripts/grounding-substance.mjs +1 -1
- package/plugin/scripts/grounding-turn-evidence.mjs +17 -2
- package/plugin/scripts/hook-input.mjs +78 -4
- package/plugin/scripts/kb-copy-proof.mjs +148 -0
- package/plugin/scripts/lesson-bridge.mjs +6 -2
- package/plugin/scripts/nightly-controller.mjs +8 -1
- package/plugin/scripts/node-sqlite.mjs +41 -0
- package/plugin/scripts/package-cards.json +797 -0
- package/plugin/scripts/package-cards.rvf +0 -0
- package/plugin/scripts/package-cards.rvf.idmap.json +1 -0
- package/plugin/scripts/package-cards.rvf.meta.json +1 -0
- package/plugin/scripts/package-recommender-client.mjs +138 -0
- package/plugin/scripts/package-recommender-flag.mjs +30 -0
- package/plugin/scripts/package-recommender.mjs +391 -0
- package/plugin/scripts/project-progression-outbox.mjs +26 -8
- package/plugin/scripts/project-progression-reader.mjs +4 -2
- package/plugin/scripts/protect-brain-state.sh +4 -1
- package/plugin/scripts/session-snapshot-hook.mjs +27 -3
- package/plugin/scripts/session-start-budget.mjs +1 -0
- package/plugin/scripts/session-start-core.mjs +34 -4
- package/plugin/scripts/session-start-health.mjs +7 -1
- package/plugin/scripts/session-start-update-plane.mjs +35 -0
- package/plugin/scripts/turn-outcome-capture.mjs +12 -1
- package/plugin/scripts/unprompted-runtime.mjs +2 -2
- package/plugin/skills/brain-console/SKILL.md +3 -3
- package/plugin/skills/rnbc/SKILL.md +24 -0
- package/plugin/skills/rvbc/SKILL.md +2 -2
- package/scripts/approved-runtime.mjs +2 -2
- package/scripts/ci/warm-brain-models.mjs +28 -0
- package/scripts/codex-hook-trust.mjs +94 -0
- package/scripts/console-runtime-identity.mjs +5 -0
- package/scripts/corpus-canary.mjs +46 -6
- package/scripts/corpus-dispatch-decision.mjs +2 -2
- package/scripts/corpus-promotion.mjs +1 -1
- package/scripts/hook-qualify-hosts.mjs +15 -3
- package/scripts/host-install-matrix.mjs +63 -2
- package/scripts/human-approval-phrases.mjs +46 -0
- package/scripts/installed-brain-health.mjs +53 -0
- package/scripts/move-brain.mjs +310 -0
- package/scripts/onboarding-console.mjs +93 -10
- package/scripts/oracle/abstain-threshold-sweep.mjs +62 -0
- package/scripts/oracle/abstain-trace.mjs +139 -0
- package/scripts/oracle/doc2query-generate.mjs +162 -0
- package/scripts/oracle/doc2query-reach.mjs +110 -0
- package/scripts/oracle/judge-train.mjs +158 -0
- package/scripts/oracle/need-set-split.mjs +48 -0
- package/scripts/oracle/sona-query-adapter-eval.mjs +139 -0
- package/scripts/package-cards.mjs +374 -0
- package/scripts/publication-receipt.mjs +37 -9
- package/scripts/recommendation-e2e.mjs +110 -0
- package/scripts/recommendation-eval.mjs +105 -0
- package/scripts/recommendation-floor.mjs +56 -0
- package/scripts/recommendation-judge-score.mjs +74 -0
- package/scripts/recommendation-latency.mjs +95 -0
- package/scripts/recommendation-real-host-score.mjs +76 -0
- package/scripts/recommendation-real-host.mjs +137 -0
- package/scripts/release-channel-kind.mjs +1 -1
- package/scripts/release-environment-policy.mjs +33 -0
- package/scripts/single-source-check.mjs +15 -10
- package/scripts/sync-commands.mjs +5 -2
- package/scripts/wired-check.mjs +17 -2
|
@@ -26,14 +26,15 @@ import fs from 'node:fs';
|
|
|
26
26
|
import os from 'node:os';
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
-
import {
|
|
29
|
+
import { restoreWithBrief } from './continuity-brief.mjs';
|
|
30
30
|
import {
|
|
31
31
|
read, json, exists, mkdir, write, runNode,
|
|
32
32
|
} from './session-start-fsutil.mjs';
|
|
33
33
|
import { maintainerIssueEntitlement, surfaceIssuePointer } from './session-start-issue-alert.mjs';
|
|
34
34
|
import { surfaceSignals } from './session-start-signals.mjs';
|
|
35
35
|
import { brainState, health, knowledgeCurrency, mcpReadiness, KNOWLEDGE_LINE_PREFIX } from './session-start-health.mjs';
|
|
36
|
-
import { stableSpine, heartbeat, knowledgeAutoUpdate } from './session-start-update-plane.mjs';
|
|
36
|
+
import { stableSpine, heartbeat, knowledgeAutoUpdate, footprintCheck } from './session-start-update-plane.mjs';
|
|
37
|
+
import { FOOTPRINT_LINE_PREFIX } from './brain-confirmation.mjs';
|
|
37
38
|
import { describeLifecycleHooks, readHookContracts } from './session-start-hook-description.mjs';
|
|
38
39
|
import { createStageTracer } from './session-start-trace.mjs';
|
|
39
40
|
|
|
@@ -59,13 +60,33 @@ const meter = ({ env, cwd, stateDir, output }) => {
|
|
|
59
60
|
} catch { /* metering never blocks */ }
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* ground-ruvnet.sh sends each long block once per session and a one-line form after that (4.5). A
|
|
65
|
+
* SessionStart — startup, resume, clear, or compact — is exactly when the earlier full text may have
|
|
66
|
+
* left the model's context, so touching injected/.reset makes every block go out in full once more.
|
|
67
|
+
* No session id reaches this hook, so the reset is global: another window re-receives its blocks once,
|
|
68
|
+
* which errs toward delivering. Per-session marker dirs older than 3 days are pruned. Never throws.
|
|
69
|
+
*/
|
|
70
|
+
export function resetInjectionDedupe(stateDir, now = Date.now()) {
|
|
71
|
+
const base = path.join(stateDir, 'injected');
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(base, { recursive: true });
|
|
74
|
+
fs.writeFileSync(path.join(base, '.reset'), `${new Date(now).toISOString()}\n`);
|
|
75
|
+
for (const name of fs.readdirSync(base)) {
|
|
76
|
+
if (name.startsWith('.')) continue;
|
|
77
|
+
const dir = path.join(base, name);
|
|
78
|
+
try { if (now - fs.statSync(dir).mtimeMs > 3 * 86_400_000) fs.rmSync(dir, { recursive: true, force: true }); } catch { /* raced */ }
|
|
79
|
+
}
|
|
80
|
+
} catch { /* an unwritable cache only costs dedupe, never the session */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
export async function runSessionStart({
|
|
63
84
|
env = process.env,
|
|
64
85
|
cwd = process.cwd(),
|
|
65
86
|
stdout = process.stdout,
|
|
66
87
|
stderr = process.stderr,
|
|
67
88
|
platform = process.platform,
|
|
68
|
-
restoreContinuity =
|
|
89
|
+
restoreContinuity = restoreWithBrief,
|
|
69
90
|
runHeartbeat = true,
|
|
70
91
|
} = {}) {
|
|
71
92
|
const lines = [];
|
|
@@ -80,6 +101,7 @@ export async function runSessionStart({
|
|
|
80
101
|
|| s.startsWith('[RuvNet Brain — INSTALL ALARM')
|
|
81
102
|
|| s.startsWith('[RuvNet Brain — NIGHTLY FAILED')
|
|
82
103
|
|| s.startsWith(KNOWLEDGE_LINE_PREFIX)
|
|
104
|
+
|| s.startsWith(FOOTPRINT_LINE_PREFIX)
|
|
83
105
|
|| s.startsWith('[RuvNet Brain — OPEN ISSUES')
|
|
84
106
|
|| /\bopen issue\(s\)/i.test(s)
|
|
85
107
|
|| /^\[RuvNet Brain — external signal/i.test(s)
|
|
@@ -100,6 +122,7 @@ export async function runSessionStart({
|
|
|
100
122
|
|| s.startsWith('[ASCII→SVG]')
|
|
101
123
|
|| s.startsWith('[RuvNet Brain — PROJECT CONTINUITY UNKNOWN]')
|
|
102
124
|
|| s.startsWith('[RuvNet Brain — PROJECT CONTINUITY RESTORED]')
|
|
125
|
+
|| s.startsWith('[RuvNet Brain — COME UP TO SPEED')
|
|
103
126
|
|| s.startsWith('[RuvNet Brain — MAINTAINER ONLY:');
|
|
104
127
|
};
|
|
105
128
|
// An alarm's HEADER line always matches isSafeStatus on its own dedicated prefix (above); its
|
|
@@ -132,9 +155,10 @@ export async function runSessionStart({
|
|
|
132
155
|
};
|
|
133
156
|
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
134
157
|
const stateDir = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
|
|
158
|
+
resetInjectionDedupe(stateDir, Date.now());
|
|
135
159
|
const hookDir = path.dirname(fileURLToPath(import.meta.url));
|
|
136
160
|
const now = Date.now();
|
|
137
|
-
const consoleInvoke = env.RUVNET_HOOK_HOST === 'codex' ? '$ruvnet-brain:
|
|
161
|
+
const consoleInvoke = env.RUVNET_HOOK_HOST === 'codex' ? '$ruvnet-brain:rnbc' : '/rnbc';
|
|
138
162
|
const brain = brainState(env, home);
|
|
139
163
|
const pluginRoot = env.CLAUDE_PLUGIN_ROOT || path.resolve(hookDir, '..');
|
|
140
164
|
const manifest = json(path.join(pluginRoot, '.claude-plugin', 'plugin.json'), {});
|
|
@@ -218,6 +242,12 @@ export async function runSessionStart({
|
|
|
218
242
|
const line = knowledgeCurrency({ env, home, now });
|
|
219
243
|
if (line) emit(line);
|
|
220
244
|
});
|
|
245
|
+
// ONE line only when the footprint is wrong (a second KB copy, cruft, a worker on another copy);
|
|
246
|
+
// a bounded detached sweep follows at most every 6h (ADR-0098). Silent on a clean machine.
|
|
247
|
+
tracer.stage('footprint', () => {
|
|
248
|
+
if (brain.off || state.problem) return;
|
|
249
|
+
footprintCheck({ env, home, now, hookDir, emit });
|
|
250
|
+
});
|
|
221
251
|
|
|
222
252
|
tracer.stage('misc', () => {
|
|
223
253
|
const consoleOffered = path.join(stateDir, '.console-offered');
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { json, exists, mtimeMs, read } from './session-start-fsutil.mjs';
|
|
14
|
+
import { unmountedNotice } from './brain-location.mjs';
|
|
14
15
|
import {
|
|
15
16
|
describeFailedRefreshRun, readNightlyRegistration, refreshHistory, updateOwnedByAgenticKit,
|
|
16
17
|
} from './nightly-scheduler.mjs';
|
|
@@ -33,9 +34,14 @@ export const brainState = (env, home) => {
|
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
export const health = (home, off) => {
|
|
37
|
+
// Moved to another disk that is not plugged in: say exactly that — not "MISSING, reinstall", which
|
|
38
|
+
// would re-create a fresh brain in ~/.cache over the link. A problem also stands the self-heal down.
|
|
39
|
+
const unmounted = unmountedNotice({ home });
|
|
40
|
+
if (unmounted) return { problem: unmounted, absentByChoice: false };
|
|
36
41
|
const kb = path.join(home, '.cache', 'ruvnet-brain', 'kb');
|
|
37
42
|
let rvf = false;
|
|
38
|
-
|
|
43
|
+
// `._x.rvf` is macOS AppleDouble from an exFAT/FAT volume, not a store.
|
|
44
|
+
try { rvf = fs.readdirSync(kb).some((name) => name.endsWith('.rvf') && !name.startsWith('._') && exists(path.join(kb, name))); }
|
|
39
45
|
catch { /* absent */ }
|
|
40
46
|
const absentByChoice = off && (!exists(kb) || !rvf);
|
|
41
47
|
if (absentByChoice) return { problem: '', absentByChoice };
|
|
@@ -9,6 +9,8 @@ import path from 'node:path';
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { read, write, exists, json, firstVersion, dispatchDetached, mtimeMs } from './session-start-fsutil.mjs';
|
|
11
11
|
import { KNOWLEDGE_LINE_PREFIX, AUTO_UPDATE_LOCK_STALE_MS, autoUpdateOptOut, knowledgeFacts } from './session-start-health.mjs';
|
|
12
|
+
import { inventoryFootprint } from './brain-footprint.mjs';
|
|
13
|
+
import { confirm, footprintAlarm } from './brain-confirmation.mjs';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* KNOWLEDGE SELF-HEAL (owner invariant 2026-09-30: no brain may ever be more than 48h old). The
|
|
@@ -77,6 +79,39 @@ export const knowledgeAutoUpdate = ({ env, home, now, hookDir, emit = () => {},
|
|
|
77
79
|
return { launched: true, why: 'stale' };
|
|
78
80
|
};
|
|
79
81
|
|
|
82
|
+
/**
|
|
83
|
+
* FOOTPRINT (ADR-0098): a cheap, read-only classification (names and sizes of single files only, no
|
|
84
|
+
* hashing, no tree walks) on every session. Silent when the machine is clean. When it is not: ONE line,
|
|
85
|
+
* and — at most once per 6h, never in test mode, never while a refresh holds the lock — a detached
|
|
86
|
+
* `brain-footprint.mjs --sweep --apply` that removes only what the proof allows (private-unique copies are
|
|
87
|
+
* kept). Self-heal installs and updates enforce the same sweep inside bin/install.mjs.
|
|
88
|
+
*/
|
|
89
|
+
export const FOOTPRINT_SWEEP_HOURS = 6;
|
|
90
|
+
export const footprintCheck = ({ env, home, now, hookDir, emit = () => {}, dispatch = dispatchDetached }) => {
|
|
91
|
+
const footprint = inventoryFootprint({ env, home, now, measure: false });
|
|
92
|
+
const line = footprintAlarm(confirm({ footprint, env, home, now }));
|
|
93
|
+
if (!line) return { clean: true, dispatched: false };
|
|
94
|
+
const stamp = path.join(footprint.roots.brainHome, '.footprint-sweep');
|
|
95
|
+
const removable = footprint.cruft.some((i) => ['remove', 'remove-if-proven', 'rotate', 'truncate'].includes(i.action));
|
|
96
|
+
// Nothing a sweep could remove (e.g. a copy kept for the private data it holds — review S7): say it ONCE,
|
|
97
|
+
// until the line changes, instead of at every session; and dispatch no sweep that could only keep it again.
|
|
98
|
+
if (!removable && !footprint.roots.dangling) { // an unplugged brain disk is told every session, and nothing is written beside it
|
|
99
|
+
const notice = path.join(footprint.roots.brainHome, '.footprint-kept-notice');
|
|
100
|
+
if (read(notice) === line) return { clean: false, dispatched: false, repeated: true };
|
|
101
|
+
write(notice, line);
|
|
102
|
+
}
|
|
103
|
+
const due = !(now - mtimeMs(stamp) < FOOTPRINT_SWEEP_HOURS * 3_600_000);
|
|
104
|
+
const testMode = env.RUVNET_BRAIN_TEST === '1' && String(env.RUVNET_FOOTPRINT_SWEEP || '').toLowerCase() !== 'on';
|
|
105
|
+
let dispatched = false;
|
|
106
|
+
if (removable && due && !testMode && !footprint.lockHeld) {
|
|
107
|
+
write(stamp, `${new Date(now).toISOString()}\n`);
|
|
108
|
+
dispatched = dispatch(hookDir, 600, path.join(footprint.roots.brainHome, '.last-footprint-sweep.log'),
|
|
109
|
+
process.execPath, [path.join(hookDir, 'brain-footprint.mjs'), '--sweep', '--apply', '--json'], env);
|
|
110
|
+
}
|
|
111
|
+
emit(dispatched ? line.replace(/\. Fix: /, ' — cleaning it up in the background now. Fix: ') : line);
|
|
112
|
+
return { clean: false, dispatched };
|
|
113
|
+
};
|
|
114
|
+
|
|
80
115
|
export const compareVersions = (a, b) => {
|
|
81
116
|
const left = String(a).split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
82
117
|
const right = String(b).split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
@@ -43,6 +43,7 @@ import crypto from 'node:crypto';
|
|
|
43
43
|
import { spawn, spawnSync } from 'node:child_process';
|
|
44
44
|
import { fileURLToPath } from 'node:url';
|
|
45
45
|
import { resolveRuflo, rufloInvocation } from './ruflo-bin.mjs';
|
|
46
|
+
import { userLevelAgentdbHooks } from './continuity-events.mjs';
|
|
46
47
|
|
|
47
48
|
export const TURN_NAMESPACE = 'turns';
|
|
48
49
|
export const MIN_OUTCOME_CHARS = 200;
|
|
@@ -201,7 +202,17 @@ export function captureTurnOutcome({
|
|
|
201
202
|
const project = projectName(projectDir);
|
|
202
203
|
const receipts = path.join(brainHome, 'turn-capture', 'receipts.jsonl');
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
// ONE WRITER PER TURN (ADR-100 §3). Measured 2026-10-01 on this repo's real store: 624 `turns` rows
|
|
206
|
+
// in five days for 323 distinct outcomes — this writer (host-tagged) and the owner's user-level
|
|
207
|
+
// ~/.claude/hooks/agentdb-turn-capture.mjs both recorded every Claude turn. The user-level hook is
|
|
208
|
+
// the owner's and is never edited by the product, so where it is registered the product DEFERS
|
|
209
|
+
// (RUVNET_TURN_CAPTURE=force keeps both). Codex turns are not seen by that Claude-only hook.
|
|
210
|
+
const deferTo = event === 'Stop' && host === 'claude' && String(env.RUVNET_TURN_CAPTURE || '').toLowerCase() !== 'force'
|
|
211
|
+
&& userLevelAgentdbHooks({ home }).turnCapture;
|
|
212
|
+
if (deferTo) {
|
|
213
|
+
report.skipped = 'deferred: the user-level ~/.claude/hooks/agentdb-turn-capture.mjs records this turn (one writer per turn)';
|
|
214
|
+
report.deferredToUserLevel = true;
|
|
215
|
+
} else if (event === 'Stop') {
|
|
205
216
|
const sessionKey = String(payload.session_id || payload.transcript_path || '');
|
|
206
217
|
const message = typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message.trim() : '';
|
|
207
218
|
let turn = { finalText: '', files: [], actions: [] };
|
|
@@ -112,8 +112,8 @@ const SCRIPTS_DIR = path.dirname(SELF); // the payload's scr
|
|
|
112
112
|
// The CC event name the shim forwarded. No event → nothing to run; stay silent.
|
|
113
113
|
const EVENT = process.argv[2] || '';
|
|
114
114
|
|
|
115
|
-
// Bound the whole runtime well under this hook's DECLARED timeout (hooks.json
|
|
116
|
-
//
|
|
115
|
+
// Bound the whole runtime well under this hook's DECLARED timeout (hooks.json: 'unprompted-speech' is
|
|
116
|
+
// 9s; codex-hooks.json: 10s host, 9000ms outer — it was 3s when this budget was first set) — producers run sequentially and
|
|
117
117
|
// each has its own internal watchdog, but a backstop timeout here means a wedged producer can never
|
|
118
118
|
// hang the turn. Found live 2026-09-27: the old 4000ms default left NO real margin under a 3000ms
|
|
119
119
|
// declared timeout once Node startup + per-producer spawnSync overhead is counted, and slower
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: brain-console
|
|
3
|
-
description: Open the RuvNet Brain Console for "/rvbc", "/rvcb", "/brain-console", or "/ruvnet-brain:configure". Use when the user asks to open, configure, inspect, or view the Brain Console. It opens the live local page in the background; the page is read-only until the user clicks a clearly explained, reversible action.
|
|
4
|
-
updated: 2026-
|
|
3
|
+
description: Open the RuvNet Brain Console (RNBC) for "/rnbc", "/rnb", "/rvbc", "/rvcb", "/brain-console", or "/ruvnet-brain:configure". Use when the user asks to open, configure, inspect, or view the Brain Console. It opens the live local page in the background; the page is read-only until the user clicks a clearly explained, reversible action.
|
|
4
|
+
updated: 2026-10-01
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Brain Console
|
|
8
8
|
|
|
9
|
-
Treat `/rvbc`, `/rvcb`, `/brain-console`, and `/ruvnet-brain:configure` as equally valid names.
|
|
9
|
+
Treat `/rnbc` (the name), `/rnb`, `/rvbc`, `/rvcb`, `/brain-console`, and `/ruvnet-brain:configure` as equally valid names.
|
|
10
10
|
Never correct the user's spelling.
|
|
11
11
|
|
|
12
12
|
1. Say one short sentence: "Opening it now; it scans live while you watch."
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rnbc
|
|
3
|
+
description: Open the RNBC — RuvNet Brain Console — in Claude Code and Codex when the user says "rnbc", "rnb", "Configure RuvNet Brain", asks for the Brain Console, or wants to configure or inspect RuvNet Brain. Claude Code supports /rnbc (and /rnb, /rvbc); Codex invokes the native $ruvnet-brain:rnbc skill.
|
|
4
|
+
updated: 2026-10-01
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# RNBC — RuvNet Brain Console
|
|
8
|
+
|
|
9
|
+
RuvNet Brain abbreviates as RNB, so its console is the RNBC. Claude Code opens it with `/rnbc`
|
|
10
|
+
(also `/rnb`, `/rvbc`, `/rvcb`, `/brain-console`, `/ruvnet-brain:configure`); Codex invokes this
|
|
11
|
+
skill as `$ruvnet-brain:rnbc` because custom plugin slash commands are not part of the Codex CLI
|
|
12
|
+
slash-command surface. Never correct the user's spelling.
|
|
13
|
+
|
|
14
|
+
1. Say one short sentence: "Opening it now; it scans live while you watch."
|
|
15
|
+
2. Resolve the installed runtime at
|
|
16
|
+
`${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
17
|
+
A current-repository `scripts/onboarding-console.mjs` is allowed only for an explicit developer
|
|
18
|
+
checkout. Never fall back to a guessed `~/Code` path.
|
|
19
|
+
3. Run `node <resolved-script> --serve --open` in the background.
|
|
20
|
+
4. Give the URL immediately. Do not promise a duration; the page reports its own scan progress.
|
|
21
|
+
|
|
22
|
+
An already-running server is success. The Console is read-only until the user chooses an action;
|
|
23
|
+
every change must be explained and reversible. If the script cannot be located or the server fails,
|
|
24
|
+
report the exact failure plainly instead of claiming the Console opened.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rvbc
|
|
3
|
-
description: Open the RuvNet Brain Console in Claude Code and Codex when the user says "Configure RuvNet Brain", mentions "rvbc", asks for the Brain Console, or wants to configure or inspect RuvNet Brain. Claude Code supports /rvbc; Codex invokes the native $ruvnet-brain:rvbc skill.
|
|
4
|
-
updated: 2026-
|
|
3
|
+
description: Open the RuvNet Brain Console in Claude Code and Codex when the user says "Configure RuvNet Brain", mentions "rvbc" or "rnbc", asks for the Brain Console, or wants to configure or inspect RuvNet Brain. Claude Code supports /rnbc and /rvbc; Codex invokes the native $ruvnet-brain:rvbc skill (or $ruvnet-brain:rnbc).
|
|
4
|
+
updated: 2026-10-01
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# RuvNet Brain Console
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// "Pinning survives only through enforced equality to the approved shipped runtime and its
|
|
15
15
|
// executable hashes. Copying current-main package.json or preserving a version string alone is
|
|
16
16
|
// insufficient." So this compares every executable/runtime file's sha256 AND byte length against a
|
|
17
|
-
// inventory produced from the
|
|
17
|
+
// inventory produced from the install-verified shipped code artifact — and, in the other
|
|
18
18
|
// direction, refuses any executable-shaped file in the archive that the inventory does not cover, so
|
|
19
19
|
// a NEW unpinned executable cannot ride along.
|
|
20
20
|
//
|
|
@@ -405,7 +405,7 @@ async function main() {
|
|
|
405
405
|
catch (error) { console.error(`[approved-runtime] ${error.message}`); return 1; }
|
|
406
406
|
const result = verifyApprovedRuntime({ manifest, pin });
|
|
407
407
|
if (result.verdict !== 'PASS') {
|
|
408
|
-
console.error('[approved-runtime] FAIL: archive runtime is not the
|
|
408
|
+
console.error('[approved-runtime] FAIL: archive runtime is not the install-verified shipped code artifact');
|
|
409
409
|
for (const failure of result.failures) console.error(` - ${failure}`);
|
|
410
410
|
return 1;
|
|
411
411
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/ci/warm-brain-models.mjs — download every embedder the Brain at $RUVNET_BRAIN_KB requires into
|
|
3
|
+
// $KB_MODEL_CACHE, resolving @xenova/transformers from that Brain's own node_modules (the reader that
|
|
4
|
+
// will run). Used by ci.yml's warm-brain job (seed corpus) and release-qe's capability battery (the
|
|
5
|
+
// exact assembled candidate), so both warm the cache the same way.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
+
import { requiredEmbedderModels } from '../../plugin/test/model-cache.mjs';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const modelCache = process.env.KB_MODEL_CACHE;
|
|
14
|
+
const kb = process.env.RUVNET_BRAIN_KB;
|
|
15
|
+
if (!modelCache || !kb) throw new Error('RUVNET_BRAIN_KB and KB_MODEL_CACHE are required');
|
|
16
|
+
fs.mkdirSync(modelCache, { recursive: true });
|
|
17
|
+
const requireFromKb = createRequire(path.join(kb, 'package.json'));
|
|
18
|
+
const T = await import(pathToFileURL(requireFromKb.resolve('@xenova/transformers')).href);
|
|
19
|
+
T.env.localModelPath = modelCache;
|
|
20
|
+
T.env.cacheDir = modelCache;
|
|
21
|
+
T.env.allowRemoteModels = true;
|
|
22
|
+
for (const model of requiredEmbedderModels(kb)) {
|
|
23
|
+
await T.pipeline('feature-extraction', model, { quantized: true });
|
|
24
|
+
console.log(`[warm-brain-models] ${model}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// codex-hook-trust.mjs — which Brain hooks will Codex refuse to run after this update until the user
|
|
2
|
+
// re-reviews them? Answered OFFLINE, from the two hooks files, with Codex's own trust identity.
|
|
3
|
+
//
|
|
4
|
+
// THE MECHANISM (source, not inference — openai/codex at tag rust-v0.159.3, the installed codex-cli):
|
|
5
|
+
// codex-rs/hooks/src/engine/discovery.rs
|
|
6
|
+
// · key = hook_key("<plugin_id>:<relative hooks file>", event, group_index, handler_index)
|
|
7
|
+
// e.g. "ruvnet-brain@ruvnet-brain:hooks/codex-hooks.json:user_prompt_submit:0:1"
|
|
8
|
+
// · hash = hook_hash(): sha256 over the canonical JSON of the NORMALISED identity
|
|
9
|
+
// { event_name, matcher (tool/session events only), hooks: [normalised handler] } —
|
|
10
|
+
// command, timeout (normalised), async, statusMessage; "so equivalent hooks from config
|
|
11
|
+
// TOML and hooks.json converge on the same trust identity".
|
|
12
|
+
// · hook_trust_status(): trusted_hash == current hash -> Trusted; a different stored hash ->
|
|
13
|
+
// Modified; none -> Untrusted. Only Managed|Trusted handlers are pushed to run (unless
|
|
14
|
+
// bypass_hook_trust). So a release that edits a command's text, its timeout or its
|
|
15
|
+
// matcher, or that shifts a hook to another group/handler index, silently stops that
|
|
16
|
+
// hook on every machine until the user trusts it again.
|
|
17
|
+
// codex-rs/config/src/fingerprint.rs version_for_toml(): "sha256:" + hex(sha256(serde_json::to_vec(
|
|
18
|
+
// canonical_json(value)))) — keys sorted recursively, compact.
|
|
19
|
+
// codex-rs/tui/src/startup_hooks_review.rs: the interactive prompt "Hooks need review" offers
|
|
20
|
+
// "Review hooks" / "Trust all and continue" / "Continue without trusting (hooks won't run)".
|
|
21
|
+
// MEASURED 2026-10-01 in an isolated CODEX_HOME (codex-cli 0.159.3, no credentials): fresh install ->
|
|
22
|
+
// all 11 Brain hooks `untrusted`; trusted_hash written -> `trusted`; one command's text changed ->
|
|
23
|
+
// that one hook `modified`, the other ten still `trusted`. currentHash from `hooks/list` equals
|
|
24
|
+
// codexHookHash() below for every hook (tests/unit/codex-hook-trust.test.mjs pins the real values).
|
|
25
|
+
import crypto from 'node:crypto';
|
|
26
|
+
|
|
27
|
+
const EVENT_LABEL = Object.freeze({
|
|
28
|
+
PreToolUse: 'pre_tool_use', PermissionRequest: 'permission_request', PostToolUse: 'post_tool_use',
|
|
29
|
+
PreCompact: 'pre_compact', PostCompact: 'post_compact', SessionStart: 'session_start', SessionEnd: 'session_end',
|
|
30
|
+
UserPromptSubmit: 'user_prompt_submit', SubagentStart: 'subagent_start', SubagentStop: 'subagent_stop',
|
|
31
|
+
Stop: 'stop', Interrupt: 'interrupt',
|
|
32
|
+
});
|
|
33
|
+
const NO_MATCHER = new Set(['UserPromptSubmit', 'Stop', 'Interrupt']);
|
|
34
|
+
const CONTEXT_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'SessionStart', 'UserPromptSubmit', 'SubagentStart']);
|
|
35
|
+
const DEFAULT_CONTEXT_LIMIT = 2500;
|
|
36
|
+
|
|
37
|
+
function canonical(v) {
|
|
38
|
+
if (Array.isArray(v)) return v.map(canonical);
|
|
39
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).sort().map((k) => [k, canonical(v[k])]));
|
|
40
|
+
return v;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** discovery.rs normalize_command_hook(): SessionEnd/Interrupt default 1s, clamped 1..3; others default 600, min 1. */
|
|
44
|
+
function normalisedTimeout(event, t) {
|
|
45
|
+
const n = Number.isInteger(t) && t >= 0 ? t : null;
|
|
46
|
+
if (event === 'SessionEnd' || event === 'Interrupt') return Math.min(3, Math.max(1, n ?? 1));
|
|
47
|
+
return Math.max(1, n ?? 600);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The exact `currentHash` Codex computes for one command handler, or null for a handler Codex would skip. */
|
|
51
|
+
export function codexHookHash(event, group, handler) {
|
|
52
|
+
if (!EVENT_LABEL[event] || handler?.type !== 'command' || typeof handler.command !== 'string' || !handler.command.trim()) return null;
|
|
53
|
+
const h = { type: 'command', command: handler.command, timeout: normalisedTimeout(event, handler.timeout), async: handler.async === true };
|
|
54
|
+
if (typeof handler.statusMessage === 'string') h.statusMessage = handler.statusMessage;
|
|
55
|
+
const limit = handler.additionalContextLimit;
|
|
56
|
+
if (CONTEXT_EVENTS.has(event) && Number.isInteger(limit) && limit !== DEFAULT_CONTEXT_LIMIT) h.additionalContextLimit = limit;
|
|
57
|
+
const identity = { event_name: EVENT_LABEL[event], hooks: [h] };
|
|
58
|
+
if (!NO_MATCHER.has(event) && typeof group?.matcher === 'string') identity.matcher = group.matcher;
|
|
59
|
+
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(canonical(identity))).digest('hex')}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** key -> hash for every handler in a Codex hooks file ({ hooks: { Event: [groups] } }). */
|
|
63
|
+
export function codexHookIdentities(hooksFile, keySource = 'ruvnet-brain@ruvnet-brain:hooks/codex-hooks.json') {
|
|
64
|
+
const out = new Map();
|
|
65
|
+
for (const [event, groups] of Object.entries(hooksFile?.hooks || {})) {
|
|
66
|
+
if (!EVENT_LABEL[event] || !Array.isArray(groups)) continue;
|
|
67
|
+
groups.forEach((group, gi) => (group?.hooks || []).forEach((handler, hi) => {
|
|
68
|
+
const hash = codexHookHash(event, group, handler);
|
|
69
|
+
if (hash) out.set(`${keySource}:${EVENT_LABEL[event]}:${gi}:${hi}`, hash);
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The Brain hooks an update leaves needing review: every key of the NEW file whose hash differs from the
|
|
77
|
+
* OLD file's hash at the same key ('modified') or that did not exist before ('untrusted'). A hook the user
|
|
78
|
+
* had already trusted at the old hash stops running until reviewed; that is the whole point of the list.
|
|
79
|
+
*/
|
|
80
|
+
export function codexTrustChanges(prevHooksFile, nextHooksFile, keySource) {
|
|
81
|
+
const prev = codexHookIdentities(prevHooksFile, keySource);
|
|
82
|
+
const next = codexHookIdentities(nextHooksFile, keySource);
|
|
83
|
+
const changes = [];
|
|
84
|
+
for (const [key, hash] of next) {
|
|
85
|
+
if (!prev.has(key)) changes.push({ key, status: 'untrusted' });
|
|
86
|
+
else if (prev.get(key) !== hash) changes.push({ key, status: 'modified' });
|
|
87
|
+
}
|
|
88
|
+
return changes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The exact instruction for a user whose Brain hooks Codex is holding back. */
|
|
92
|
+
export const CODEX_TRUST_ACTION = 'Start Codex (interactive, in any project). At the "Hooks need review" prompt choose'
|
|
93
|
+
+ ' "Trust all and continue" — or type /hooks and trust each RuvNet Brain hook. Until then Codex skips them'
|
|
94
|
+
+ ' (codex exec never asks). Then re-run npx ruvnet-brain --doctor.';
|
|
@@ -51,6 +51,11 @@ export const CONSOLE_RUNTIME_SURFACE = Object.freeze([
|
|
|
51
51
|
// ERR_MODULE_NOT_FOUND on import, before a single line runs.
|
|
52
52
|
'kb/corpus-release-identity.mjs',
|
|
53
53
|
'bin/install.mjs',
|
|
54
|
+
// The Console's Settings → Nightly switch runs the copied installer's --enable-nightly, which installs
|
|
55
|
+
// THIS file (beside it, REPO_ROOT/bin) as the scheduler's immutable runner. Without it every installed
|
|
56
|
+
// customer got "nightly runner source is missing" (RNBC review 2026-10-01). It imports only node:
|
|
57
|
+
// built-ins, so nothing further travels with it.
|
|
58
|
+
'bin/nightly-refresh.mjs',
|
|
54
59
|
'package.json',
|
|
55
60
|
]);
|
|
56
61
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// `codex` host is wired — this is the knowledge channel, not the host matrix.
|
|
36
36
|
//
|
|
37
37
|
// node scripts/corpus-canary.mjs --repo owner/name --tag corpus-sha256-<64hex> --approved-version X.Y.Z
|
|
38
|
-
// --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>]
|
|
38
|
+
// --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>] [--footprint-updates]
|
|
39
39
|
// exit 0 = PASS, 1 = FAIL (verdict written either way), 2 = usage.
|
|
40
40
|
|
|
41
41
|
import crypto from 'node:crypto';
|
|
@@ -45,6 +45,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
45
45
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
46
|
import { addPrivateStore, resolveRuntime, resultFromRefreshReceipt, runInstallerDoor } from './customer-seams.mjs';
|
|
47
47
|
import { CANARY_VERDICT_KIND, CORPUS_TAG_PATTERN, REQUIRED_CANARY_CHECKS } from './corpus-promotion.mjs';
|
|
48
|
+
import { inventoryFootprint } from '../plugin/scripts/brain-footprint.mjs';
|
|
48
49
|
|
|
49
50
|
export const PUBLIC_API = 'https://api.github.com';
|
|
50
51
|
export const FRESHNESS_LIMIT_MS = 48 * 3_600_000;
|
|
@@ -331,8 +332,30 @@ async function fetchReleaseList({ apiBase, repo }) {
|
|
|
331
332
|
return response.json();
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
/** The footprint of a customer HOME after one update (plugin/scripts/brain-footprint.mjs, never restated). */
|
|
336
|
+
export function footprintSnapshot({ home, kbDir }) {
|
|
337
|
+
const fp = inventoryFootprint({ env: { HOME: home, RUVNET_BRAIN_KB: kbDir, RUVNET_BRAIN_HOME: path.dirname(kbDir) }, home });
|
|
338
|
+
const receipts = fp.breakdown.receipts || 0;
|
|
339
|
+
return { kbCopies: fp.kbCopies, netBytes: fp.totalBytes - receipts, receipts, budgetBytes: fp.budgetBytes,
|
|
340
|
+
withinBudget: fp.withinBudget, copies: fp.cruft.filter((i) => /kb-copy|quarantine|install-/.test(i.kind)).map((i) => i.path) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Three updates, one KB: exactly one copy after each, nothing that must not exist, no net growth. */
|
|
344
|
+
export function judgeFootprintStability(inventories, { growthAllowanceBytes = 1024 * 1024 } = {}) {
|
|
345
|
+
const problems = [];
|
|
346
|
+
inventories.forEach((inv, index) => {
|
|
347
|
+
if (inv.exitCode !== undefined && inv.exitCode !== 0) problems.push(`update ${index + 1} exited ${inv.exitCode}`);
|
|
348
|
+
if (inv.kbCopies !== 1) problems.push(`update ${index + 1}: ${inv.kbCopies} KB copies (${inv.copies.join(', ')})`);
|
|
349
|
+
if (!inv.withinBudget) problems.push(`update ${index + 1}: over the footprint budget`);
|
|
350
|
+
});
|
|
351
|
+
const growth = inventories.at(-1).netBytes - inventories[0].netBytes;
|
|
352
|
+
if (growth > growthAllowanceBytes) problems.push(`grew ${growth} bytes across ${inventories.length} updates (receipts excluded)`);
|
|
353
|
+
return problems.length ? { ok: false, detail: problems.join('; ') }
|
|
354
|
+
: { ok: true, detail: `${inventories.length} updates: 1 KB copy each time, within budget, net growth ${growth} bytes` };
|
|
355
|
+
}
|
|
356
|
+
|
|
334
357
|
async function runCase({ name, spec, repo, tag, approvedVersion, release, work, home, apiBase, install, now,
|
|
335
|
-
retryDelayMs, maxAttempts, log, writerRoot, approvedInstaller }) {
|
|
358
|
+
retryDelayMs, maxAttempts, log, writerRoot, approvedInstaller, footprintUpdates = false }) {
|
|
336
359
|
fs.mkdirSync(home, { recursive: true });
|
|
337
360
|
let runtime = approvedVersion;
|
|
338
361
|
if (spec.runtime !== 'approved') runtime = resolveRuntime(spec.runtime, { candidateTag: `v${approvedVersion}`, releases: await fetchReleaseList({ apiBase, repo }) });
|
|
@@ -362,6 +385,22 @@ async function runCase({ name, spec, repo, tag, approvedVersion, release, work,
|
|
|
362
385
|
: (fs.existsSync(resultFile) ? readJson(resultFile) : null);
|
|
363
386
|
const judged = await judgeCanary({ tag, approvedVersion, release, updater, before, beforeModules, beforeCoverage,
|
|
364
387
|
kbDir: fs.realpathSync(kbDir), home: fs.realpathSync(home), now: now() });
|
|
388
|
+
// ADR-0098 footprint check, OPT-IN (--footprint-updates): two more updates of the same customer through
|
|
389
|
+
// the same door, each followed by the footprint inventory. Off by default because its CI runtime on the
|
|
390
|
+
// real ~1.4 GB brain (two more full-tree validations) has not been measured yet; the offline fixture
|
|
391
|
+
// run is in tests/unit/corpus-canary.test.mjs.
|
|
392
|
+
if (footprintUpdates && name === 'clean' && updater.exitCode === 0) {
|
|
393
|
+
const inventories = [footprintSnapshot({ home: fs.realpathSync(home), kbDir })];
|
|
394
|
+
for (let extra = 0; extra < 2; extra += 1) {
|
|
395
|
+
fs.rmSync(resultFile, { force: true });
|
|
396
|
+
const again = spec.door === 'installer'
|
|
397
|
+
? await runInstallerDoor({ installer: await approvedInstaller(), env: childEnv, cwd: home })
|
|
398
|
+
: await runUpdater({ kbDir, env: childEnv, resultFile });
|
|
399
|
+
log(`--- [${name}] footprint update ${extra + 2} exit ${again.exitCode}\n${again.output}`);
|
|
400
|
+
inventories.push({ ...footprintSnapshot({ home: fs.realpathSync(home), kbDir }), exitCode: again.exitCode });
|
|
401
|
+
}
|
|
402
|
+
judged.checks.push({ name: 'footprint-three-updates', ...judgeFootprintStability(inventories) });
|
|
403
|
+
}
|
|
365
404
|
if (overlay) {
|
|
366
405
|
const after = Object.fromEntries(Object.keys(overlay.digests).map((f) => [f,
|
|
367
406
|
fs.existsSync(path.join(kbDir, f)) ? sha256File(path.join(kbDir, f)) : null]));
|
|
@@ -403,7 +442,7 @@ export async function runCanary({
|
|
|
403
442
|
repo, tag, approvedVersion, work, apiBase = PUBLIC_API, install = installCustomer, home = path.join(work, 'home'),
|
|
404
443
|
env = process.env, now = () => Date.now(), retryDelayMs = 30_000, maxAttempts = 3, log = () => {},
|
|
405
444
|
cases = ['clean'], writerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
|
|
406
|
-
approvedInstaller = null,
|
|
445
|
+
approvedInstaller = null, footprintUpdates = false,
|
|
407
446
|
}) {
|
|
408
447
|
if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
|
|
409
448
|
if (!CORPUS_TAG_PATTERN.test(String(tag || ''))) throw new Error('--tag must be corpus-sha256-<64 hex>');
|
|
@@ -426,7 +465,7 @@ export async function runCanary({
|
|
|
426
465
|
return installerPath;
|
|
427
466
|
});
|
|
428
467
|
const common = { repo, tag, approvedVersion, release, apiBase, install, now, retryDelayMs, maxAttempts, log, writerRoot,
|
|
429
|
-
approvedInstaller: resolveInstaller };
|
|
468
|
+
approvedInstaller: resolveInstaller, footprintUpdates };
|
|
430
469
|
let cleanKb = null;
|
|
431
470
|
const clean = await runCase({ ...common, name: 'clean', spec: CANARY_CASES.clean, work, home,
|
|
432
471
|
install: async (args) => { const installed = await install(args); cleanKb = physical(installed.kbDir); return installed; } });
|
|
@@ -476,7 +515,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
476
515
|
const work = opt('--work') && path.resolve(opt('--work'));
|
|
477
516
|
const verdictOut = opt('--verdict-out') && path.resolve(opt('--verdict-out'));
|
|
478
517
|
if (!work || !verdictOut) {
|
|
479
|
-
process.stderr.write('usage: corpus-canary.mjs --repo o/n --tag corpus-sha256-<hex> --approved-version X.Y.Z --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>] [--cases clean,private-overlay,older-runtime]\n');
|
|
518
|
+
process.stderr.write('usage: corpus-canary.mjs --repo o/n --tag corpus-sha256-<hex> --approved-version X.Y.Z --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>] [--cases clean,private-overlay,older-runtime] [--footprint-updates]\n');
|
|
480
519
|
return 2;
|
|
481
520
|
}
|
|
482
521
|
fs.mkdirSync(work, { recursive: true });
|
|
@@ -488,7 +527,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
488
527
|
let record;
|
|
489
528
|
try {
|
|
490
529
|
record = await runCanary({ repo: opt('--repo'), tag: opt('--tag'), approvedVersion: opt('--approved-version'), work,
|
|
491
|
-
apiBase: opt('--api-base') || PUBLIC_API, install, home: path.resolve(opt('--home') || path.join(work, 'home')), log, cases
|
|
530
|
+
apiBase: opt('--api-base') || PUBLIC_API, install, home: path.resolve(opt('--home') || path.join(work, 'home')), log, cases,
|
|
531
|
+
footprintUpdates: argv.includes('--footprint-updates') });
|
|
492
532
|
} catch (error) {
|
|
493
533
|
// Could not even reach a judgement = the consumer did not accept. Still a written verdict.
|
|
494
534
|
record = { schemaVersion: 1, kind: CANARY_VERDICT_KIND, verdict: 'FAIL', repo: opt('--repo') || null, tag: opt('--tag') || null,
|
|
@@ -48,8 +48,8 @@ export function killSwitchOff(nightlyVar) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* @param nightlyVar repository variable CORPUS_NIGHTLY (armed unless it reads `off`, any case; the
|
|
52
|
-
*
|
|
51
|
+
* @param nightlyVar repository variable CORPUS_NIGHTLY (armed unless it reads `off`, any case; the newest
|
|
52
|
+
* install-verified code release is the consent — no person approves it)
|
|
53
53
|
* @param resolution { status: 'resolved', release: {tag, version, sourceSha} }
|
|
54
54
|
* | { status: 'not-yet-verified' } | { status: 'invalid', reason }
|
|
55
55
|
*/
|
|
@@ -33,7 +33,7 @@ export function evaluateCorpusPromotion({ tag, generation, currentLatest } = {})
|
|
|
33
33
|
if (!CORPUS_TAG_PATTERN.test(String(currentLatest.tagName || ''))) {
|
|
34
34
|
// A code release holds latest. Corpus promotion does not regress the runtime, because
|
|
35
35
|
// scripts/approved-runtime.mjs has already proved this archive's executables ARE the
|
|
36
|
-
//
|
|
36
|
+
// install-verified shipped runtime, byte for byte.
|
|
37
37
|
return {
|
|
38
38
|
allowed: true,
|
|
39
39
|
reason: `current latest ${currentLatest.tagName} is a code release; runtime equality is enforced by the approved runtime pin`,
|
|
@@ -58,7 +58,15 @@ export function scanCodex({ stdout, stderr }) {
|
|
|
58
58
|
for (const l of lines(stderr)) if (/hook/i.test(l)) f.push(`stderr: ${l.slice(0, 200)}`);
|
|
59
59
|
return { findings: [...new Set(f)], unrelatedStderr: lines(stderr).filter((l) => !/hook/i.test(l)).slice(0, 3) };
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Pure: scan a real `grok -p --debug-file` run.
|
|
63
|
+
*
|
|
64
|
+
* MEASURED 2026-10-01 (grok 1.0.13, docs/research/grok-hooks-2026-10-01.md): `grok -p` discovers the Brain
|
|
65
|
+
* from Claude Code's plugin cache ("plugin discovered name=ruvnet-brain … has_hooks=true") and attaches its
|
|
66
|
+
* MCP server, but the session's "loaded hooks hook_count=8" was exactly the 4 hooks in ~/.claude/settings.json
|
|
67
|
+
* plus the 4 in ~/.grok/hooks/ — no plugin's hooks at all — and no Brain hook ever completed. A discovered
|
|
68
|
+
* plugin whose hooks never dispatch is therefore a FINDING here, never a silent pass.
|
|
69
|
+
*/
|
|
62
70
|
export function scanGrok({ stdout, stderr, debug = '' }) {
|
|
63
71
|
const f = []; let loaded = null; const pluginHooks = /plugin discovered name=ruvnet-brain .*has_hooks=true/.test(debug);
|
|
64
72
|
for (const l of lines(debug)) {
|
|
@@ -68,7 +76,11 @@ export function scanGrok({ stdout, stderr, debug = '' }) {
|
|
|
68
76
|
}
|
|
69
77
|
if (stderr.trim()) f.push(`process stderr: ${JSON.stringify(stderr.trim().slice(0, 300))}`);
|
|
70
78
|
if (/hook/i.test(stdout) && /(error|failed)/i.test(stdout.match(/.{0,80}hook.{0,120}/i)?.[0] || '')) f.push('hook error text in the output stream');
|
|
71
|
-
|
|
79
|
+
const brainHooksRan = /hook completed hook_name=\S*ruvnet/.test(debug);
|
|
80
|
+
if (pluginHooks && !brainHooksRan) {
|
|
81
|
+
f.push(`Brain plugin hooks discovered but never ran (session loaded ${loaded ?? '?'} hooks): grok -p loads only global hooks — see docs/research/grok-hooks-2026-10-01.md`);
|
|
82
|
+
}
|
|
83
|
+
return { findings: [...new Set(f)], hooksLoaded: loaded, pluginHasHooks: pluginHooks, pluginHooksRan: brainHooksRan };
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
async function runClaude(root, dbgDir) {
|
|
@@ -98,7 +110,7 @@ async function runGrok(root, dbgDir) {
|
|
|
98
110
|
const debug = fs.existsSync(debugFile) ? fs.readFileSync(debugFile, 'utf8') : '';
|
|
99
111
|
const s = scanGrok({ stdout: r.stdout || '', stderr: r.stderr || '', debug });
|
|
100
112
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
101
|
-
return { status: r.status === 0 && !s.findings.length ? 'PASS' : 'FAIL', exit: r.status, ...s, note: '
|
|
113
|
+
return { status: r.status === 0 && !s.findings.length ? 'PASS' : 'FAIL', exit: r.status, ...s, note: 'real HOME: the Brain is discovered from Claude Code\'s plugin cache; whether its hooks actually RUN in grok -p is what is measured (they did not on grok 1.0.13)' };
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
export async function layer2(hosts, { maxLoad = 40, waitMs = 15 * 60_000, log = () => {}, root = REPO } = {}) {
|