ruvnet-brain 4.3.18 → 4.3.20
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 +95 -25
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/session-start-core.mjs +20 -1
- package/plugin/scripts/update-apply.mjs +33 -8
- package/scripts/eval-brain.mjs +32 -4
- package/scripts/host-install-matrix.mjs +6 -3
- package/scripts/publication-receipt.mjs +48 -14
- package/scripts/wired-check.mjs +1 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# 🧠 RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**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.**
|
|
13
13
|
|
package/bin/install.mjs
CHANGED
|
@@ -673,8 +673,8 @@ export async function unzipInto(zipPath, cacheDir, sourceDir = null) {
|
|
|
673
673
|
* Registry reachability identifies candidates; it is not deletion proof. Claude freezes
|
|
674
674
|
* CLAUDE_PLUGIN_ROOT at session start, so an unregistered generation can remain live until that
|
|
675
675
|
* session exits. Modern roots expose `.in_use` PID-incarnation leases. Live or ambiguous leases
|
|
676
|
-
* retain the complete root; dead leases are collected. Legacy roots without the protocol
|
|
677
|
-
* a
|
|
676
|
+
* retain the complete root; dead leases are collected. Legacy roots without the protocol are
|
|
677
|
+
* retained indefinitely because a frozen host session is not observable without a lease.
|
|
678
678
|
*
|
|
679
679
|
* WHAT MAKES DELETION SAFE HERE, since this removes directories from someone's machine:
|
|
680
680
|
* • The registry is the ONLY authority. Unreadable, missing, or naming no ruvnet-brain install →
|
|
@@ -786,7 +786,11 @@ export function prunePluginGenerations({
|
|
|
786
786
|
continue;
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
if (!leaseCapable
|
|
789
|
+
if (!leaseCapable) {
|
|
790
|
+
if (now() - orphanedAt < graceMs) continue;
|
|
791
|
+
cleanupBlocked.push({ version: candidate.version, reason: 'legacy generation has no liveness lease; refusing destructive cleanup' });
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
790
794
|
let safeToReap = true;
|
|
791
795
|
for (const leaseName of leaseNames) {
|
|
792
796
|
const leasePath = path.join(leaseDir, leaseName);
|
|
@@ -2915,29 +2919,82 @@ function missingUpdaterHelp(kbDir) {
|
|
|
2915
2919
|
console.error(` — the current bundle ships forge-update.mjs; then this command will work.`);
|
|
2916
2920
|
}
|
|
2917
2921
|
|
|
2918
|
-
function acquireHostConvergenceLock(lockPath
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2922
|
+
export function acquireHostConvergenceLock(lockPath, {
|
|
2923
|
+
staleMs = 5 * 60 * 1000,
|
|
2924
|
+
now = () => Date.now(),
|
|
2925
|
+
processAlive = (pid, owner) => {
|
|
2926
|
+
if (owner?.host && owner.host !== os.hostname()) return true;
|
|
2927
|
+
try { process.kill(pid, 0); return true; }
|
|
2928
|
+
catch (error) { return error?.code !== 'ESRCH'; }
|
|
2929
|
+
},
|
|
2930
|
+
} = {}) {
|
|
2931
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
2932
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
2933
|
+
const owner = {
|
|
2934
|
+
pid: process.pid,
|
|
2935
|
+
host: os.hostname(),
|
|
2936
|
+
token,
|
|
2937
|
+
startedAt: new Date(now()).toISOString(),
|
|
2938
|
+
};
|
|
2939
|
+
const publish = () => {
|
|
2940
|
+
try {
|
|
2941
|
+
// mkdir is the exclusive operation. The owner write follows it, but an incomplete
|
|
2942
|
+
// directory is treated as busy until the stale threshold; no contender may reclaim it in
|
|
2943
|
+
// that publication window. This avoids rename-over-empty-directory replacement on macOS.
|
|
2944
|
+
fs.mkdirSync(lockPath, { recursive: false });
|
|
2945
|
+
fs.writeFileSync(path.join(lockPath, 'owner.json'), JSON.stringify(owner), { flag: 'wx', mode: 0o600 });
|
|
2946
|
+
return true;
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
const release = () => {
|
|
2953
|
+
const quarantine = `${lockPath}.release-${process.pid}-${token}`;
|
|
2925
2954
|
try {
|
|
2926
|
-
const
|
|
2927
|
-
if (
|
|
2928
|
-
|
|
2929
|
-
|
|
2955
|
+
const current = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
2956
|
+
if (current?.token !== token) return false;
|
|
2957
|
+
fs.renameSync(lockPath, quarantine);
|
|
2958
|
+
const moved = JSON.parse(fs.readFileSync(path.join(quarantine, 'owner.json'), 'utf8'));
|
|
2959
|
+
if (moved?.token !== token) {
|
|
2960
|
+
try { fs.renameSync(quarantine, lockPath); } catch { /* preserve evidence if another writer won */ }
|
|
2961
|
+
return false;
|
|
2930
2962
|
}
|
|
2931
|
-
|
|
2932
|
-
|
|
2963
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
2964
|
+
return true;
|
|
2965
|
+
} catch { return false; }
|
|
2966
|
+
};
|
|
2967
|
+
|
|
2968
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2969
|
+
try {
|
|
2970
|
+
if (publish()) return { acquired: true, release };
|
|
2971
|
+
} catch (error) {
|
|
2972
|
+
return { acquired: false, error: error.message };
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
let existing;
|
|
2976
|
+
let lockAge = 0;
|
|
2977
|
+
try {
|
|
2978
|
+
const stat = fs.statSync(lockPath);
|
|
2979
|
+
lockAge = Math.max(0, now() - stat.mtimeMs);
|
|
2980
|
+
existing = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
2981
|
+
} catch { /* an incomplete lock is handled below, with a grace period */ }
|
|
2982
|
+
if (existing && Number.isInteger(existing.pid) && processAlive(existing.pid, existing)) {
|
|
2983
|
+
return { acquired: false, error: `another host synchronization is running (pid ${existing.pid})` };
|
|
2984
|
+
}
|
|
2985
|
+
if (!existing && lockAge < staleMs) {
|
|
2986
|
+
return { acquired: false, error: 'another host synchronization is publishing its lock' };
|
|
2987
|
+
}
|
|
2988
|
+
const quarantine = `${lockPath}.reclaim-${process.pid}-${token}-${attempt}`;
|
|
2933
2989
|
try {
|
|
2934
|
-
fs.
|
|
2935
|
-
fs.
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
return { acquired: false, error:
|
|
2990
|
+
fs.renameSync(lockPath, quarantine);
|
|
2991
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
if (error?.code === 'ENOENT') continue;
|
|
2994
|
+
return { acquired: false, error: `host synchronization lock changed concurrently: ${error.message}` };
|
|
2939
2995
|
}
|
|
2940
2996
|
}
|
|
2997
|
+
return { acquired: false, error: 'host synchronization lock changed concurrently' };
|
|
2941
2998
|
}
|
|
2942
2999
|
|
|
2943
3000
|
export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
@@ -2995,9 +3052,18 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
2995
3052
|
if (!fs.existsSync(apply)) return fail({ error: 'Stable Spine updater missing from package' });
|
|
2996
3053
|
const applied = runStableSpine(apply);
|
|
2997
3054
|
const okApplied = !applied.error && applied.status === 0;
|
|
3055
|
+
const codexReceipt = {
|
|
3056
|
+
state: results.codex?.action === 'disabled' ? 'disabled' : (results.codexHost?.host ? 'ready' : 'absent'),
|
|
3057
|
+
version: results.codex?.version || null,
|
|
3058
|
+
};
|
|
3059
|
+
if (results.codex?.restartRequired === true) {
|
|
3060
|
+
codexReceipt.restartRequired = true;
|
|
3061
|
+
codexReceipt.sessionSafety = results.codex.sessionSafety || null;
|
|
3062
|
+
codexReceipt.sessionSafetyReason = results.codex.sessionSafetyReason || null;
|
|
3063
|
+
}
|
|
2998
3064
|
if (okApplied) {
|
|
2999
3065
|
// ISSUE #153 — a running host may freeze an old plugin root. Reclaim only generations whose
|
|
3000
|
-
// modern leases prove no live consumer
|
|
3066
|
+
// modern leases prove no live consumer; legacy roots without that proof stay on disk.
|
|
3001
3067
|
try {
|
|
3002
3068
|
const gens = prunePluginGenerations({ apply: true });
|
|
3003
3069
|
if (gens.retired?.length) {
|
|
@@ -3028,7 +3094,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
3028
3094
|
verifiedAt: new Date().toISOString(),
|
|
3029
3095
|
hosts: {
|
|
3030
3096
|
claude: { state: results.claude.host ? 'ready' : 'absent', version: results.claude.version || null },
|
|
3031
|
-
codex:
|
|
3097
|
+
codex: codexReceipt,
|
|
3032
3098
|
},
|
|
3033
3099
|
consoleRuntime: results.consoleRuntime,
|
|
3034
3100
|
}, null, 2));
|
|
@@ -3043,7 +3109,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
3043
3109
|
desiredVersion: PACKAGE_VERSION,
|
|
3044
3110
|
hosts: {
|
|
3045
3111
|
claude: { state: results.claude.host ? 'ready' : 'absent', version: results.claude.version || null },
|
|
3046
|
-
codex:
|
|
3112
|
+
codex: codexReceipt,
|
|
3047
3113
|
},
|
|
3048
3114
|
consoleRuntime: results.consoleRuntime,
|
|
3049
3115
|
});
|
|
@@ -3061,7 +3127,11 @@ export function classifyHostConvergence(receipt, expectedVersion = PACKAGE_VERSI
|
|
|
3061
3127
|
}
|
|
3062
3128
|
const hostStates = Object.values(receipt.hosts || {});
|
|
3063
3129
|
const badHost = hostStates.find((host) => !['ready', 'disabled', 'absent'].includes(host?.state)
|
|
3064
|
-
|| (host.state === 'ready' && !versionSatisfies(host.version, expectedVersion))
|
|
3130
|
+
|| (host.state === 'ready' && !versionSatisfies(host.version, expectedVersion))
|
|
3131
|
+
|| (host.state === 'ready' && host.restartRequired === true));
|
|
3132
|
+
if (badHost?.restartRequired === true) {
|
|
3133
|
+
return { healthy: false, state: 'host-restart-required', action: badHost.sessionSafetyReason || 'restart the host, then re-run --doctor' };
|
|
3134
|
+
}
|
|
3065
3135
|
if (badHost) return { healthy: false, state: 'host-pending', action: 're-run host synchronization' };
|
|
3066
3136
|
if (receipt.consoleRuntime?.state !== 'ready') {
|
|
3067
3137
|
return { healthy: false, state: receipt.consoleRuntime?.state || 'console-unproven', action: 'restart Console, then re-run --doctor' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.20",
|
|
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 77 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.3.
|
|
4
|
+
"version": "4.3.20",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -248,6 +248,19 @@ const announceVersion = ({ running, off, stateDir, consoleInvoke, emit }) => {
|
|
|
248
248
|
write(announced, `${running}\n`);
|
|
249
249
|
};
|
|
250
250
|
|
|
251
|
+
const compareVersions = (a, b) => {
|
|
252
|
+
const left = String(a).split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
253
|
+
const right = String(b).split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
254
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
255
|
+
const x = left[i] ?? 0;
|
|
256
|
+
const y = right[i] ?? 0;
|
|
257
|
+
if (x === y) continue;
|
|
258
|
+
if (typeof x === 'number' && typeof y === 'number') return x - y;
|
|
259
|
+
return String(x) < String(y) ? -1 : 1;
|
|
260
|
+
}
|
|
261
|
+
return 0;
|
|
262
|
+
};
|
|
263
|
+
|
|
251
264
|
const stableSpine = ({ env, hookDir, stateDir, home, pluginVersion, emit, now }) => {
|
|
252
265
|
const activeFile = path.join(stateDir, 'active.json');
|
|
253
266
|
const stamp = path.join(stateDir, '.last-update-check');
|
|
@@ -270,7 +283,11 @@ const stableSpine = ({ env, hookDir, stateDir, home, pluginVersion, emit, now })
|
|
|
270
283
|
}
|
|
271
284
|
} else {
|
|
272
285
|
const active = json(activeFile);
|
|
273
|
-
|
|
286
|
+
const shellBoundary = active?.shellChangedAtVersion;
|
|
287
|
+
const shellChangedBeforeHost = shellBoundary && pluginVersion
|
|
288
|
+
? compareVersions(pluginVersion, shellBoundary) < 0
|
|
289
|
+
: false;
|
|
290
|
+
if ((active?.shellChanged || shellChangedBeforeHost) && pluginVersion && active.version && active.version !== pluginVersion) {
|
|
274
291
|
emit(`[RuvNet Brain — v${active.version} changed boot-level declarations (the rare case); this session booted v${pluginVersion}'s]`);
|
|
275
292
|
const host = env.RUVNET_HOOK_HOST || 'claude';
|
|
276
293
|
const convergence = json(path.join(stateDir, 'host-convergence.json'));
|
|
@@ -371,6 +388,8 @@ export async function runSessionStart({
|
|
|
371
388
|
|| s.startsWith('[RuvNet Brain — grounding not yet PROVEN')
|
|
372
389
|
|| s.startsWith('The last check (')
|
|
373
390
|
|| s.startsWith('[RuvNet Brain — update available')
|
|
391
|
+
|| s.startsWith('[RuvNet Brain — v')
|
|
392
|
+
|| s.startsWith('Tell the user ONE line: "🧠 RuvNet Brain v')
|
|
374
393
|
|| s.startsWith('[RuvNet Brain — brain OFF by your setting')
|
|
375
394
|
|| s.startsWith('[RuvNet Brain — new in v')
|
|
376
395
|
|| s.startsWith('[RuvNet Brain — first session initialized]')
|
|
@@ -212,18 +212,34 @@ function promote(staging, version) {
|
|
|
212
212
|
// the update is fully live with no restart and session-start stays silent; if any did, the ONE
|
|
213
213
|
// honest nag fires (the release classifier computes the same thing publish-side; this is the
|
|
214
214
|
// client-side truth for locally-applied generations). Red-team finding 18's client half.
|
|
215
|
-
const SHELL_PATHS = [
|
|
216
|
-
|
|
215
|
+
export const SHELL_PATHS = [
|
|
216
|
+
'hooks/hooks.json',
|
|
217
|
+
'scripts/hook-shim.mjs',
|
|
218
|
+
'scripts/hook-shim-bash.mjs',
|
|
219
|
+
'scripts/development-maintenance.mjs',
|
|
220
|
+
'mcp/server.mjs',
|
|
221
|
+
'.mcp.json',
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
function treeDigest(root, relative) {
|
|
225
|
+
const target = path.join(root, relative);
|
|
226
|
+
let stat;
|
|
227
|
+
try { stat = fs.lstatSync(target); } catch { return '<missing>'; }
|
|
228
|
+
if (stat.isSymbolicLink()) return '<symlink>';
|
|
229
|
+
if (stat.isFile()) return `file:${crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex')}`;
|
|
230
|
+
if (!stat.isDirectory()) return `<special:${stat.mode}>`;
|
|
231
|
+
const entries = fs.readdirSync(target, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
232
|
+
return `dir:${entries.map((entry) => `${entry.name}:${treeDigest(target, entry.name)}`).join('|')}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function shellDiff(prevRootAbs, nextRootAbs) {
|
|
217
236
|
const changed = [];
|
|
218
237
|
for (const rel of SHELL_PATHS) {
|
|
219
|
-
|
|
220
|
-
const b = (() => { try { return fs.readFileSync(path.join(nextRootAbs, rel), 'utf8'); } catch { return null; } })();
|
|
221
|
-
if (a !== b) changed.push(rel);
|
|
238
|
+
if (treeDigest(prevRootAbs, rel) !== treeDigest(nextRootAbs, rel)) changed.push(rel);
|
|
222
239
|
}
|
|
223
240
|
// skills/ and commands/ are boot-loaded markdown: any file-set or content difference counts.
|
|
224
241
|
for (const dir of ['skills', 'commands']) {
|
|
225
|
-
|
|
226
|
-
if (list(prevRootAbs) !== list(nextRootAbs)) changed.push(`${dir}/`);
|
|
242
|
+
if (treeDigest(prevRootAbs, dir) !== treeDigest(nextRootAbs, dir)) changed.push(`${dir}/`);
|
|
227
243
|
}
|
|
228
244
|
return changed;
|
|
229
245
|
}
|
|
@@ -239,6 +255,15 @@ function flip(version, codeRootAbs, why) {
|
|
|
239
255
|
previous: prev ? { generation: prev.generation, version: prev.version, codeRoot: prev.codeRoot } : null,
|
|
240
256
|
shellChanged: shellChanged.length > 0,
|
|
241
257
|
shellChangedPaths: shellChanged,
|
|
258
|
+
// Keep the first boot-level change attached to subsequent body-only generations. A host that
|
|
259
|
+
// still booted the pre-change generation must not lose the restart signal merely because a
|
|
260
|
+
// later update changed only implementation code.
|
|
261
|
+
shellChangedSinceVersion: shellChanged.length > 0
|
|
262
|
+
? (prev?.version || version)
|
|
263
|
+
: (prev?.shellChangedSinceVersion || null),
|
|
264
|
+
shellChangedAtVersion: shellChanged.length > 0
|
|
265
|
+
? version
|
|
266
|
+
: (prev?.shellChangedAtVersion || null),
|
|
242
267
|
flippedAt: new Date().toISOString(),
|
|
243
268
|
};
|
|
244
269
|
writeAtomic(TXN, JSON.stringify({ state: 'flipping', from: prev?.version ?? null, to: version }));
|
|
@@ -446,4 +471,4 @@ function main() {
|
|
|
446
471
|
}
|
|
447
472
|
}
|
|
448
473
|
|
|
449
|
-
process.exit(main());
|
|
474
|
+
if (process.env.RUVNET_BRAIN_IMPORT_ONLY !== 'update-shell-test') process.exit(main());
|
package/scripts/eval-brain.mjs
CHANGED
|
@@ -80,10 +80,11 @@ export function wilson(k, n, z = 1.96) {
|
|
|
80
80
|
* answers "does the query reach the store that actually holds the answer" (brain-score.mjs's own
|
|
81
81
|
* wording for this metric) — that store is the one named in `receipt`, when one was verified.
|
|
82
82
|
*/
|
|
83
|
-
export function gradeQuestion(q, { grounded, citations, bannerPresent, receipt }) {
|
|
83
|
+
export function gradeQuestion(q, { grounded, citations, bannerPresent, receipt, repoAliases = {} }) {
|
|
84
84
|
const top = citations?.[0] ?? null;
|
|
85
85
|
const routedRepo = receipt?.repo ?? top?.repo ?? null;
|
|
86
|
-
const routed = !!(grounded && q.expectRepo?.length && routedRepo
|
|
86
|
+
const routed = !!(grounded && q.expectRepo?.length && routedRepo
|
|
87
|
+
&& repoMatchesExpected(routedRepo, q.expectRepo, repoAliases));
|
|
87
88
|
const abstained = !top || (typeof top.ce === 'number' && top.ce < ABSTAIN_CE);
|
|
88
89
|
switch (q.stratum) {
|
|
89
90
|
case 'adversarial':
|
|
@@ -97,6 +98,16 @@ export function gradeQuestion(q, { grounded, citations, bannerPresent, receipt }
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Ground truth names the logical repository, while the deployed corpus may use a documented
|
|
102
|
+
// store alias (for example agent-harness-generator -> metaharness). Credit only aliases present
|
|
103
|
+
// in the shipped registry; never turn an arbitrary similarly named store into a pass.
|
|
104
|
+
export function repoMatchesExpected(actual, expected, aliases = {}) {
|
|
105
|
+
if (!actual || !Array.isArray(expected)) return false;
|
|
106
|
+
if (expected.includes(actual)) return true;
|
|
107
|
+
return expected.some((canonical) => Array.isArray(aliases?.[canonical])
|
|
108
|
+
&& aliases[canonical].includes(actual));
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
/** Aggregate graded rows into the four gated metrics, each with its Wilson interval. */
|
|
101
112
|
export function aggregate(rows) {
|
|
102
113
|
const by = (pred) => rows.filter(pred);
|
|
@@ -145,6 +156,11 @@ async function main() {
|
|
|
145
156
|
const die = (msg) => { console.error(`eval-brain: ${msg}`); process.exit(2); };
|
|
146
157
|
|
|
147
158
|
if (!fs.existsSync(path.join(KB, 'forge-ask-all.mjs'))) die(`no brain at ${KB} — run: npx ruvnet-brain`);
|
|
159
|
+
let repoAliases = {};
|
|
160
|
+
try {
|
|
161
|
+
const aliasesFile = path.join(KB, 'repo-aliases.json');
|
|
162
|
+
if (fs.existsSync(aliasesFile)) repoAliases = JSON.parse(fs.readFileSync(aliasesFile, 'utf8'));
|
|
163
|
+
} catch { /* a legacy bundle simply has no alias normalization to apply */ }
|
|
148
164
|
const verifierPath = path.join(KB, 'verify-citation.mjs');
|
|
149
165
|
if (!fs.existsSync(verifierPath)) die('this bundle predates verify-citation.mjs — refusing to score grounding without a way to check it');
|
|
150
166
|
const { verifyGrounding } = await import(pathToFileURL(verifierPath).href);
|
|
@@ -161,7 +177,13 @@ async function main() {
|
|
|
161
177
|
// serial cost was ~13.5s/question ≈ 27 min for 120; at concurrency 6 the wall drops ~6×.
|
|
162
178
|
const { execFile } = await import('node:child_process');
|
|
163
179
|
const ask = (query) => new Promise((resolve) => {
|
|
164
|
-
|
|
180
|
+
// The evaluator must exercise the same bounded source-routing path used by the production
|
|
181
|
+
// MCP host. The old CLI default widened every routed miss to the entire corpus, which turned
|
|
182
|
+
// a 120-question quality check into a multi-hour cross-encoder run and measured a path no user
|
|
183
|
+
// actually takes. Set EVAL_FULL_CORPUS=1 only for an explicit historical fan-out experiment.
|
|
184
|
+
const args = ['forge-ask-all.mjs', '--dir', KB, '--q', query, '--k', '3'];
|
|
185
|
+
if (process.env.EVAL_FULL_CORPUS !== '1') args.push('--bounded');
|
|
186
|
+
execFile('node', args,
|
|
165
187
|
{ cwd: KB, timeout: 240000, env: process.env, maxBuffer: 64 * 1024 * 1024 },
|
|
166
188
|
(err, stdout) => resolve(err ? '' : String(stdout || '')));
|
|
167
189
|
});
|
|
@@ -190,7 +212,13 @@ async function main() {
|
|
|
190
212
|
continue;
|
|
191
213
|
}
|
|
192
214
|
const v = await verifyGrounding(out, KB);
|
|
193
|
-
const graded = gradeQuestion(q, {
|
|
215
|
+
const graded = gradeQuestion(q, {
|
|
216
|
+
grounded: v.grounded,
|
|
217
|
+
citations: v.citations,
|
|
218
|
+
receipt: v.receipt,
|
|
219
|
+
bannerPresent: /GIST STATUS/.test(out),
|
|
220
|
+
repoAliases,
|
|
221
|
+
});
|
|
194
222
|
const top = v.citations?.[0] ?? null;
|
|
195
223
|
rows[i] = {
|
|
196
224
|
id: q.id, stratum: q.stratum, query: q.query,
|
|
@@ -395,12 +395,13 @@ export function createInstalledMcpSession({ serverPath, env, timeout = 300_000,
|
|
|
395
395
|
pending.set(id, { resolve, reject });
|
|
396
396
|
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
397
397
|
});
|
|
398
|
-
const execute = async ({ query = 'How does RuvNet Brain prove a public release artifact?', k = 5
|
|
398
|
+
const execute = async ({ query = 'How does RuvNet Brain prove a public release artifact?', k = 5,
|
|
399
|
+
timeoutMs = timeout } = {}) => {
|
|
399
400
|
if (terminalError) return { status: null, error: terminalError, signal: exitSignal, stdout: '', stderr };
|
|
400
401
|
stderr = '';
|
|
401
402
|
const timer = setTimeout(() => {
|
|
402
403
|
void close(Object.assign(new Error('MCP search timed out'), { code: 'ETIMEDOUT' }));
|
|
403
|
-
},
|
|
404
|
+
}, timeoutMs);
|
|
404
405
|
try {
|
|
405
406
|
if (!initialized) {
|
|
406
407
|
const ready = await call('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'release-host-matrix', version: '1' } });
|
|
@@ -409,10 +410,12 @@ export function createInstalledMcpSession({ serverPath, env, timeout = 300_000,
|
|
|
409
410
|
if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
|
|
410
411
|
initialized = true;
|
|
411
412
|
}
|
|
413
|
+
const started = performance.now();
|
|
412
414
|
const searched = await call('tools/call', { name: 'search_ruvnet', arguments: { query, k } });
|
|
415
|
+
const broadMs = Math.round(performance.now() - started);
|
|
413
416
|
const stdout = (searched.result?.content || []).map((item) => item.text || '').join('\n');
|
|
414
417
|
if (searched.error || searched.result?.isError) throw new Error(`installed Brain search failed: ${stdout.slice(0, 400)}`);
|
|
415
|
-
return { status: 0, signal: null, error: null, stdout, stderr, mcpResult: searched.result };
|
|
418
|
+
return { status: 0, signal: null, error: null, stdout, stderr, broadMs, mcpResult: searched.result };
|
|
416
419
|
} catch (error) {
|
|
417
420
|
await close(error);
|
|
418
421
|
return { status: null, signal: exitSignal, error: terminalError, stdout: '', stderr };
|
|
@@ -31,6 +31,7 @@ import { parseRetrievalResult } from '../kb/retrieval-result.mjs';
|
|
|
31
31
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
32
32
|
const PACKAGE = 'ruvnet-brain';
|
|
33
33
|
const DEADLINE_MS = 30_000;
|
|
34
|
+
const WARMUP_TIMEOUT_MS = 300_000;
|
|
34
35
|
|
|
35
36
|
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
36
37
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -453,8 +454,10 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
453
454
|
// Derived from HOST_MODES, so a fourth host shape is added in ONE place and this matrix
|
|
454
455
|
// cannot fall behind the staged-side check the way it did. Each host owns its HOME and
|
|
455
456
|
// mutable state, so installation runs concurrently rather than adding three cold-start
|
|
456
|
-
// durations together on slower runners.
|
|
457
|
-
//
|
|
457
|
+
// durations together on slower runners. The MCP workers are opened once after installation
|
|
458
|
+
// and retained through the complete proof. Reusing those workers keeps the model warm for
|
|
459
|
+
// probeBrain and every retrieval canary; starting a fresh process there caused a real
|
|
460
|
+
// macOS false negative when cold model startup crossed the 30-second public deadline.
|
|
458
461
|
const hostResults = await Promise.all(HOST_MODES.map(async (hostMode) => {
|
|
459
462
|
const mode = RECEIPT_MODE_NAMES[hostMode];
|
|
460
463
|
console.log(`Public verification: installing ${mode} on ${process.platform}`);
|
|
@@ -521,16 +524,39 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
521
524
|
};
|
|
522
525
|
}));
|
|
523
526
|
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
+
// Keep one MCP worker per host. The first search on each worker may load the local model,
|
|
528
|
+
// but all later proofs reuse the initialized process instead of paying that startup cost
|
|
529
|
+
// again. The fixed deadline remains strict for each actual search operation.
|
|
530
|
+
for (const { mode, context } of hostResults) {
|
|
531
|
+
const publicMode = MODE_FROM_RECEIPT_NAME[mode];
|
|
532
|
+
mcpSessions.set(publicMode, createInstalledMcpSession({
|
|
533
|
+
serverPath: findMcpServer(context.home), env: context.env, timeout: WARMUP_TIMEOUT_MS,
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
527
536
|
const searched = new Map();
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
537
|
+
const searchInstalledHost = async ({ mode }, timeoutMs) => {
|
|
538
|
+
const publicMode = MODE_FROM_RECEIPT_NAME[mode];
|
|
539
|
+
const session = mcpSessions.get(publicMode);
|
|
540
|
+
const result = await session.search({
|
|
541
|
+
query: 'How does RuvNet Brain prove a public release artifact?', k: 5,
|
|
542
|
+
timeoutMs,
|
|
543
|
+
});
|
|
544
|
+
if (result.error || !result.mcpResult || (Object.hasOwn(result, 'status') && result.status !== 0)) {
|
|
545
|
+
throw new Error(`installed Brain search failed for ${mode}: ${result.error?.message || 'no MCP result'}`);
|
|
546
|
+
}
|
|
547
|
+
if (!/repo=/i.test(result.stdout) || !/path\s*:/i.test(result.stdout)) {
|
|
548
|
+
throw new Error(`installed Brain search returned no source citation for ${mode}`);
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
};
|
|
552
|
+
// Warm each worker serially. The model cache is shared, but simultaneous first-loads can
|
|
553
|
+
// contend on slower runners (the macOS Codex-only timeout that motivated this path). The
|
|
554
|
+
// warm-up is bounded generously and is never used as the release latency measurement.
|
|
555
|
+
for (const host of hostResults) await searchInstalledHost(host, WARMUP_TIMEOUT_MS);
|
|
556
|
+
// Once initialized, the three steady-state checks can run in parallel and are each held to
|
|
557
|
+
// the strict public deadline that the receipt and aggregate validators enforce.
|
|
558
|
+
await Promise.all(hostResults.map(async (host) => {
|
|
559
|
+
searched.set(host.mode, await searchInstalledHost(host, DEADLINE_MS));
|
|
534
560
|
}));
|
|
535
561
|
await Promise.all(hostResults.map(async ({ context, installer }) => {
|
|
536
562
|
await commandAsync(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
@@ -560,8 +586,16 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
560
586
|
if (rvfs.length === 0) throw new Error('installed public Brain has no ruvnet-brain self RVF');
|
|
561
587
|
const server = path.join(installContext.home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
|
|
562
588
|
if (!fs.existsSync(server)) throw new Error('installed persistent MCP server is missing');
|
|
563
|
-
|
|
564
|
-
|
|
589
|
+
// installHosts keeps this mode's worker alive, so this probe exercises the same warmed
|
|
590
|
+
// installed process that passed the host canary instead of starting a cold verifier child.
|
|
591
|
+
const session = mcpSessions.get(mode);
|
|
592
|
+
const result = session
|
|
593
|
+
? await session.search({ query: 'How does RuvNet Brain prove a public release artifact?', k: 5, timeoutMs: DEADLINE_MS })
|
|
594
|
+
: await rpcSearch(server, installContext.env,
|
|
595
|
+
'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
|
|
596
|
+
if (result.error || !result.mcpResult || (Object.hasOwn(result, 'status') && result.status !== 0)) {
|
|
597
|
+
throw new Error(`installed Brain search failed for ${mode}: ${result.error?.message || 'no MCP result'}`);
|
|
598
|
+
}
|
|
565
599
|
const readiness = readJson(path.join(installContext.brainHome, 'mcp-readiness.json'));
|
|
566
600
|
if (readiness.state !== 'ready') throw new Error(`installed Brain readiness is ${readiness.state || 'missing'}`);
|
|
567
601
|
return { status: 'PASS', selfStore: true, broadMs: result.broadMs, deadlineMs: DEADLINE_MS };
|
|
@@ -578,7 +612,7 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
578
612
|
session = createInstalledMcpSession({ serverPath: findMcpServer(context.home), env: context.env, timeout: DEADLINE_MS });
|
|
579
613
|
mcpSessions.set(mode, session);
|
|
580
614
|
}
|
|
581
|
-
const result = await session.search({ query, k });
|
|
615
|
+
const result = await session.search({ query, k, timeoutMs: DEADLINE_MS });
|
|
582
616
|
return parseRetrievalResult(result.mcpResult, { query, k });
|
|
583
617
|
},
|
|
584
618
|
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -70,7 +70,7 @@ const argv = process.argv.slice(2);
|
|
|
70
70
|
* every entry on every run, below, so they cannot rot unseen.
|
|
71
71
|
*/
|
|
72
72
|
const STANDALONE = [
|
|
73
|
-
['gate', 'retired automatic-hook helper retained for explicit use
|
|
73
|
+
['gate', 'retired automatic-hook helper and manual benchmark retained for explicit human use; no workflow or scheduler invokes this expensive command'],
|
|
74
74
|
['dream-issue-gate', 'pure Dream Cycle disposition policy; invoked by the external issue adapter, never a GitHub writer'],
|
|
75
75
|
['sync-census', 'explicit maintainer census writer; a destructive source-to-surface refresh is never scheduled'],
|
|
76
76
|
['sync-commands', 'explicit maintainer alias synchronizer; run deliberately before release, never from a lifecycle hook'],
|
|
@@ -105,8 +105,6 @@ const STANDALONE = [
|
|
|
105
105
|
+ 'stops at sealed preparation with contents:read. This entry validates receipt/archive inputs '
|
|
106
106
|
+ 'and delegates mutation to release.mjs protected authority. No operational caller is registered; '
|
|
107
107
|
+ 'classification does not claim publication works or has occurred'],
|
|
108
|
-
['gate', 'manual benchmark harness: rebuilds concepts and runs three routing proof batteries; '
|
|
109
|
-
+ 'no workflow or scheduler invokes this expensive command'],
|
|
110
108
|
['execution-preflight', 'external orchestration boundary — invoked by the host before consequential Ruflo/Codex execution; no in-repo caller exists because the host supplies the live Brain and AgentDB receipts'],
|
|
111
109
|
['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
|
|
112
110
|
+ 'isolated writing agent to start and hand off a fix lane. Scheduling it would violate its safety '
|