ruvnet-brain 4.5.6 → 4.5.8
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 +50 -1
- package/kb/forge-update.mjs +50 -9
- package/package.json +5 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/configure.md +13 -1
- package/plugin/hooks/codex-hooks.json +31 -1
- package/plugin/hooks/hook-contracts.json +82 -2
- package/plugin/hooks/hooks.json +31 -1
- package/plugin/mcp/managed-cli-generation.mjs +91 -0
- package/plugin/mcp/managed-cli-interface.mjs +36 -12
- package/plugin/mcp/server.mjs +3 -2
- package/plugin/scripts/agentdb-recall.mjs +38 -10
- package/plugin/scripts/continuation-gate.mjs +3 -1
- package/plugin/scripts/continuity-hook-policy.mjs +3 -0
- package/plugin/scripts/continuity-journal.mjs +11 -11
- package/plugin/scripts/hook-shim.mjs +3 -2
- package/plugin/scripts/kb-copy-proof.mjs +84 -22
- package/plugin/scripts/learn-capture.mjs +44 -0
- package/plugin/scripts/learn-capture.sh +2 -241
- package/plugin/scripts/learn-flush.mjs +86 -191
- package/plugin/scripts/learning-observation.mjs +51 -0
- package/plugin/scripts/learning-queue.mjs +135 -0
- package/plugin/scripts/learning-store.mjs +105 -0
- package/plugin/scripts/learning-worker-supervisor.mjs +79 -0
- package/plugin/scripts/nightly-scheduler.mjs +6 -2
- package/plugin/scripts/project-capture-queue.mjs +16 -7
- package/plugin/scripts/project-progression-contract.mjs +58 -4
- package/plugin/scripts/project-progression-outbox.mjs +22 -6
- package/plugin/scripts/project-progression-session-start.mjs +10 -0
- package/plugin/scripts/project-progression-suspension.mjs +60 -0
- package/plugin/scripts/project-transition-hook.mjs +25 -8
- package/plugin/scripts/runtime-preferences.mjs +45 -4
- package/plugin/scripts/session-snapshot-hook.mjs +14 -7
- package/plugin/scripts/session-start-core.mjs +8 -0
- package/plugin/scripts/session-start-proof.mjs +49 -0
- package/plugin/scripts/turn-outcome-capture.mjs +7 -3
- package/scripts/codex-fresh-host-proof.mjs +179 -0
- package/scripts/codex-host-execution-proof.mjs +246 -0
- package/scripts/codex-host-proof-runtime.mjs +104 -0
- package/scripts/console-engine.mjs +29 -16
- package/scripts/health-repair.mjs +47 -68
- package/scripts/onboarding-console.mjs +25 -54
- package/scripts/qa/progression-validation-benchmark.mjs +66 -0
- package/scripts/release-qualification-contract.mjs +76 -7
- package/scripts/remedy-registry.mjs +9 -2
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
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
} from '../scripts/console-runtime-identity.mjs';
|
|
80
80
|
import { shellDiff as pluginShellDiff } from '../plugin/scripts/host-shell-boundary.mjs';
|
|
81
81
|
import { codexTrustChanges, CODEX_TRUST_ACTION } from '../scripts/codex-hook-trust.mjs';
|
|
82
|
+
import { probeFreshCodexDeclarations } from '../scripts/codex-fresh-host-proof.mjs';
|
|
82
83
|
import { repairSecurityGuidance } from '../scripts/security-guidance-codex-compat.mjs';
|
|
83
84
|
import { installNativeLaunchers, installRoutingRuntime } from '../scripts/model-routing-launchers.mjs';
|
|
84
85
|
import { installTerminalLaunchers } from '../scripts/model-terminal-launchers.mjs';
|
|
@@ -3045,6 +3046,21 @@ async function doctorRun({ json }) {
|
|
|
3045
3046
|
const recorded = withLiveConsoleState(JSON.parse(fs.readFileSync(convergencePath, 'utf8')),
|
|
3046
3047
|
{ receiptDir: path.join(path.dirname(convergencePath), 'console-instances') });
|
|
3047
3048
|
hostConvergence = classifyHostConvergence(recorded);
|
|
3049
|
+
const codex = recorded.hosts?.codex;
|
|
3050
|
+
if (codex?.state === 'ready' && codex.restartRequired === true && codex.restartScope === 'unproven') {
|
|
3051
|
+
let binary = process.env.CODEX_BIN || 'codex';
|
|
3052
|
+
if (!process.env.CODEX_BIN) {
|
|
3053
|
+
try {
|
|
3054
|
+
const configured = JSON.parse(fs.readFileSync(path.join(path.dirname(convergencePath), 'model-routing', 'terminal-launcher-config.json'), 'utf8'));
|
|
3055
|
+
if (typeof configured.realCodex === 'string' && path.isAbsolute(configured.realCodex)) binary = configured.realCodex;
|
|
3056
|
+
} catch { /* native PATH resolution remains bounded and fail closed */ }
|
|
3057
|
+
}
|
|
3058
|
+
const fresh = await probeFreshCodexDeclarations({ binary, codexHome: codexHomeDir(), cwd: process.cwd(),
|
|
3059
|
+
releasedPluginRoot: path.join(REPO_ROOT, 'plugin'), expectedVersion: PACKAGE_VERSION });
|
|
3060
|
+
hostConvergence = reconcileFreshCodexDeclarations(recorded, fresh);
|
|
3061
|
+
if (fresh.ok) info('Codex fresh-declarations-ready: current trusted hook declarations only; hook bodies and MCP execution were not tested. Existing open windows remain unproven.');
|
|
3062
|
+
else warn(`Codex fresh declarations remain unproven: ${fresh.reason}`);
|
|
3063
|
+
}
|
|
3048
3064
|
if (hostConvergence.healthy) {
|
|
3049
3065
|
ok(`host convergence receipt: ${hostConvergence.state}`);
|
|
3050
3066
|
if (hostConvergence.notice) info(hostConvergence.notice);
|
|
@@ -3922,6 +3938,36 @@ export function classifyHostConvergence(receipt, expectedVersion = PACKAGE_VERSI
|
|
|
3922
3938
|
return { healthy: true, state: 'channels-converged' };
|
|
3923
3939
|
}
|
|
3924
3940
|
|
|
3941
|
+
/** Activation and host readiness are independent; never claim a rollback from a host refusal. */
|
|
3942
|
+
export function hostSynchronizationFailureMessage(convergence, active, expectedVersion = PACKAGE_VERSION) {
|
|
3943
|
+
const verified = active?.version === expectedVersion && Number.isSafeInteger(active.generation) && active.generation > 0
|
|
3944
|
+
&& active.codeRoot === `versions/${expectedVersion}`;
|
|
3945
|
+
const state = verified ? `runtime ${expectedVersion} generation ${active.generation} is active`
|
|
3946
|
+
: 'the active runtime generation could not be verified';
|
|
3947
|
+
return `host synchronization is incomplete — ${state}${convergence?.error ? ` (${convergence.error})` : ''}`;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
/** Doctor projection only. Fresh hook metadata cannot clear an executable or MCP boot gap. */
|
|
3951
|
+
export function reconcileFreshCodexDeclarations(recorded, proof, expectedVersion = PACKAGE_VERSION) {
|
|
3952
|
+
const original = classifyHostConvergence(recorded, expectedVersion);
|
|
3953
|
+
const codex = recorded?.hosts?.codex;
|
|
3954
|
+
if (!proof?.ok || proof.state !== 'fresh-declarations-ready' || proof.expectedVersion !== expectedVersion
|
|
3955
|
+
|| recorded?.desiredVersion !== expectedVersion || codex?.version !== expectedVersion
|
|
3956
|
+
|| codex.state !== 'ready' || codex.restartRequired !== true || codex.restartScope !== 'unproven') return original;
|
|
3957
|
+
// Older receipts carry the paths only in this machine-generated reason. An unknown reason,
|
|
3958
|
+
// body file, skill/command surface, or MCP declaration retains the historical restart refusal.
|
|
3959
|
+
const prefix = 'boot-level declarations changed: ';
|
|
3960
|
+
const reason = codex.sessionSafetyReason;
|
|
3961
|
+
const changed = typeof reason === 'string' && reason.startsWith(prefix) ? reason.slice(prefix.length).split(', ') : [];
|
|
3962
|
+
if (!changed.length || changed.some((file) => !['hooks/codex-hooks.json', 'hooks/hooks.json'].includes(file))) {
|
|
3963
|
+
return { ...original, freshDeclarations: proof, notice: 'Fresh Codex hook declarations are ready; executable/MCP boot readiness and existing open windows remain unproven.' };
|
|
3964
|
+
}
|
|
3965
|
+
const projected = classifyHostConvergence({ ...recorded, hosts: { ...recorded.hosts,
|
|
3966
|
+
codex: { ...codex, restartScope: 'open-sessions' } } }, expectedVersion);
|
|
3967
|
+
return { ...projected, ...(projected.healthy ? { state: 'fresh-declarations-ready' } : {}), freshDeclarations: proof,
|
|
3968
|
+
notice: 'Fresh Codex hook declarations are ready; hook bodies and MCP execution were not tested. Existing open windows remain unproven.' };
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3925
3971
|
const HOST_LABELS = { claude: 'Claude Code', codex: 'Codex' };
|
|
3926
3972
|
/** The one accurate line for converged hosts whose already-open windows booted the old declarations. */
|
|
3927
3973
|
export function openSessionsNotice(hosts, version) {
|
|
@@ -4233,7 +4279,9 @@ async function runUpdate() {
|
|
|
4233
4279
|
info(c.dim('\nsynchronizing every detected host to this exact published version…\n'));
|
|
4234
4280
|
const convergence = syncHostsAfterUpdate(kbDir);
|
|
4235
4281
|
if (!convergence.ok) {
|
|
4236
|
-
|
|
4282
|
+
let active;
|
|
4283
|
+
try { active = JSON.parse(fs.readFileSync(path.join(footprintRoots().brainHome, 'active.json'), 'utf8')); } catch { /* report unknown activation */ }
|
|
4284
|
+
warn(hostSynchronizationFailureMessage(convergence, active));
|
|
4237
4285
|
updateStatus = 1;
|
|
4238
4286
|
} else if (convergence.convergence?.notice) info(convergence.convergence.notice);
|
|
4239
4287
|
try { reportLegacyRufloDebris(); } catch (error) { warn(`legacy ruflo debris cleanup failed: ${error.message}`); }
|
|
@@ -4953,6 +5001,7 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
|
|
|
4953
5001
|
}
|
|
4954
5002
|
// Preserve package-relative imports without replacing user policy.mjs overrides.
|
|
4955
5003
|
for (const runtimeRelative of [path.join('plugin', 'scripts', 'runtime-preferences.mjs'),
|
|
5004
|
+
path.join('plugin', 'scripts', 'project-identity.mjs'),
|
|
4956
5005
|
path.join('config', 'model-router', 'policy.default.mjs')]) {
|
|
4957
5006
|
const runtimeTarget = path.join(routerDir, runtimeRelative);
|
|
4958
5007
|
const runtimeBytes = fs.readFileSync(path.join(packageRoot, runtimeRelative));
|
package/kb/forge-update.mjs
CHANGED
|
@@ -1405,6 +1405,35 @@ export function bundleIdentity(src) {
|
|
|
1405
1405
|
return parts.some(Boolean) ? parts.join('|') : null;
|
|
1406
1406
|
}
|
|
1407
1407
|
|
|
1408
|
+
const downloadedArchiveVerifiers = new WeakSet();
|
|
1409
|
+
|
|
1410
|
+
function archiveVerifierFromDigests(actualDigests, bytes) {
|
|
1411
|
+
const verifier = Object.freeze({
|
|
1412
|
+
bytes,
|
|
1413
|
+
digest(algorithm = 'sha256') {
|
|
1414
|
+
if (!Object.hasOwn(actualDigests, algorithm)) throw new Error(`archive algorithm ${algorithm} was not captured`);
|
|
1415
|
+
return actualDigests[algorithm];
|
|
1416
|
+
},
|
|
1417
|
+
});
|
|
1418
|
+
downloadedArchiveVerifiers.add(verifier);
|
|
1419
|
+
return verifier;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Capture the closed transaction algorithm inventory synchronously. Never retain mutable bytes:
|
|
1423
|
+
// the capability attests only to this creation-time snapshot, not a later caller-owned Buffer.
|
|
1424
|
+
export function createDownloadedArchiveVerifier(downloadedBuffer, expectedDigests = []) {
|
|
1425
|
+
if (!Buffer.isBuffer(downloadedBuffer) || !Array.isArray(expectedDigests)) throw new TypeError('archive bytes and digest inventory required');
|
|
1426
|
+
const algorithms = new Set(['sha256']);
|
|
1427
|
+
for (const expected of expectedDigests) {
|
|
1428
|
+
if (expected == null) continue;
|
|
1429
|
+
if (typeof expected !== 'string' || !expected) throw new TypeError('invalid archive digest');
|
|
1430
|
+
algorithms.add(expected.includes(':') ? expected.split(':')[0] : 'sha256');
|
|
1431
|
+
}
|
|
1432
|
+
const actualDigests = Object.freeze(Object.fromEntries([...algorithms].map(algorithm =>
|
|
1433
|
+
[algorithm, createHash(algorithm).update(downloadedBuffer).digest('hex')])));
|
|
1434
|
+
return archiveVerifierFromDigests(actualDigests, downloadedBuffer.length);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1408
1437
|
/**
|
|
1409
1438
|
* Confirm the download+extraction actually changed what is on disk (issue #35 item 2, Dr. Mark
|
|
1410
1439
|
* Allen / @mamd69).
|
|
@@ -1448,8 +1477,11 @@ export function bundleIdentity(src) {
|
|
|
1448
1477
|
* @returns {{ok: boolean, reason: string|null, landed: object|null, kind: 'noop'|'damaged'|null,
|
|
1449
1478
|
* storeUnchanged: boolean, bundleChanged: boolean|null}}
|
|
1450
1479
|
*/
|
|
1451
|
-
export function verifyLanded({ kbDir, kbName, before, beforeBundle = null, expectedDigest = null, downloadedBuffer = null }) {
|
|
1480
|
+
export function verifyLanded({ kbDir, kbName, before, beforeBundle = null, expectedDigest = null, downloadedBuffer = null, archiveVerifier = null }) {
|
|
1452
1481
|
const damaged = (reason, landed = null) => ({ ok: false, kind: 'damaged', reason, landed, storeUnchanged: false, bundleChanged: null });
|
|
1482
|
+
if (archiveVerifier !== null && (!downloadedArchiveVerifiers.has(archiveVerifier) || downloadedBuffer !== null)) {
|
|
1483
|
+
return damaged('archive verification capability is invalid or mixed with mutable archive bytes');
|
|
1484
|
+
}
|
|
1453
1485
|
const p = path.join(kbDir, 'SOURCE.json');
|
|
1454
1486
|
if (!fs.existsSync(p)) {
|
|
1455
1487
|
return damaged(`no SOURCE.json found at ${p} after extraction — cannot confirm what actually landed`);
|
|
@@ -1487,9 +1519,11 @@ export function verifyLanded({ kbDir, kbName, before, beforeBundle = null, expec
|
|
|
1487
1519
|
|
|
1488
1520
|
// Bytes that do not match what the release declared are a HARD failure whatever the identities
|
|
1489
1521
|
// say, and the copy now in place is suspect — so this is checked before anything else.
|
|
1490
|
-
if (expectedDigest && downloadedBuffer) {
|
|
1522
|
+
if (expectedDigest && (downloadedBuffer || archiveVerifier)) {
|
|
1491
1523
|
const algo = expectedDigest.includes(':') ? expectedDigest.split(':')[0] : 'sha256';
|
|
1492
|
-
|
|
1524
|
+
let actual;
|
|
1525
|
+
try { actual = `${algo}:${archiveVerifier ? archiveVerifier.digest(algo) : createHash(algo).update(downloadedBuffer).digest('hex')}`; }
|
|
1526
|
+
catch (error) { return damaged(`downloaded bundle digest verification failed: ${error.message}`, landed); }
|
|
1493
1527
|
if (actual !== expectedDigest) {
|
|
1494
1528
|
return damaged(`downloaded bundle digest ${actual} does not match the release-declared digest ${expectedDigest}`, landed);
|
|
1495
1529
|
}
|
|
@@ -1705,7 +1739,7 @@ async function main() {
|
|
|
1705
1739
|
const originLabel = resolved.origin === 'latest-release-asset' ? `live release asset "${resolved.assetName}"`
|
|
1706
1740
|
: resolved.origin === 'live-manifest' ? 'live manifest' : 'PINNED FALLBACK (see warning above)';
|
|
1707
1741
|
console.log(`\n[${behindStores.length} store(s)] downloading ${resolved.url}\n (source: ${originLabel}) ...`);
|
|
1708
|
-
|
|
1742
|
+
let buf = await fetchBuffer(resolved.url);
|
|
1709
1743
|
const sigBuf = await fetchBuffer(`${resolved.url}.sig`, { failureCode: 3, kind: 'signature' });
|
|
1710
1744
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-update-release-'));
|
|
1711
1745
|
const zipPath = path.join(tmp, 'bundle.zip');
|
|
@@ -1718,6 +1752,14 @@ async function main() {
|
|
|
1718
1752
|
const signature = verifyDownloadedBundle(zipPath, sigPath);
|
|
1719
1753
|
if (!signature.ok) { fs.rmSync(tmp, { recursive: true, force: true }); die(`✗ SIGNATURE VERIFICATION FAILED: ${signature.reason}`, 4); }
|
|
1720
1754
|
console.log(` ✓ signature verified — ${signature.reason}`);
|
|
1755
|
+
let archiveVerifier;
|
|
1756
|
+
try { archiveVerifier = createDownloadedArchiveVerifier(buf, resolvedTargets.map(({ resolved: target }) => target.digest)); }
|
|
1757
|
+
catch (error) {
|
|
1758
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1759
|
+
die(`archive digest snapshot failed: ${error.message} — local files untouched.`, 4);
|
|
1760
|
+
}
|
|
1761
|
+
const bundleSha256 = archiveVerifier.digest();
|
|
1762
|
+
buf = null; // Snapshot owns only immutable digest strings; release the downloaded bytes before validation.
|
|
1721
1763
|
// DISK-SPACE PREFLIGHT, before a single byte is unpacked: the bundle unpacks in temp, then a whole
|
|
1722
1764
|
// candidate generation is built beside the live one: the bundle, plus what prepareCandidate carries into
|
|
1723
1765
|
// it from live — node_modules, and every private/local-ingest store file (restorePrivateFilesIntoCandidate
|
|
@@ -1801,7 +1843,7 @@ async function main() {
|
|
|
1801
1843
|
for (const { local, resolved: storeResolution } of landingTargets) {
|
|
1802
1844
|
execFileSync(process.execPath, [guard, '--dir', dir, '--name', local.kbName], { cwd: dir, stdio: 'pipe' });
|
|
1803
1845
|
const verified = verifyLanded({ kbDir: dir, kbName: local.kbName, before: local, beforeBundle: source,
|
|
1804
|
-
expectedDigest: storeResolution.digest,
|
|
1846
|
+
expectedDigest: storeResolution.digest, archiveVerifier });
|
|
1805
1847
|
if (!verified.ok && verified.kind !== 'noop') return { valid: false, failures: [verified.reason] };
|
|
1806
1848
|
if (phase === 'live') finalVerificationByStore.set(local.kbName, verified);
|
|
1807
1849
|
}
|
|
@@ -1872,7 +1914,6 @@ async function main() {
|
|
|
1872
1914
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1873
1915
|
console.log(` storage transaction: ${transaction.terminalVerdict}`);
|
|
1874
1916
|
if (transaction.terminalVerdict === 'cleanup-pending') {
|
|
1875
|
-
const bundleSha256 = createHash('sha256').update(buf).digest('hex');
|
|
1876
1917
|
const cleanupOutcome = writeUpdateOutcome({ terminalVerdict: 'cleanup-pending', storageDelta: transaction.storageDelta,
|
|
1877
1918
|
transactionReceipts: transaction.paths.receipts, bundleSha256,
|
|
1878
1919
|
phaseEvidence: phaseEvidenceFor({ root: KB_DIR, terminalVerdict: 'cleanup-pending', bundleSha256,
|
|
@@ -1886,7 +1927,7 @@ async function main() {
|
|
|
1886
1927
|
for (const { local, resolved: storeResolution } of landingTargets) {
|
|
1887
1928
|
const verified = finalVerificationByStore.get(local.kbName)
|
|
1888
1929
|
|| verifyLanded({ kbDir: KB_DIR, kbName: local.kbName, before: local, beforeBundle: source,
|
|
1889
|
-
expectedDigest: storeResolution.digest,
|
|
1930
|
+
expectedDigest: storeResolution.digest, archiveVerifier });
|
|
1890
1931
|
if (verified.storeUnchanged || verified.kind === 'noop') unchangedStores.push(local.kbName);
|
|
1891
1932
|
landedByStore.set(local.kbName, { landed: verified.landed, origin: storeResolution.origin, assetName: storeResolution.assetName });
|
|
1892
1933
|
}
|
|
@@ -1947,10 +1988,10 @@ async function main() {
|
|
|
1947
1988
|
currencyVerdict: verdict.verdict, currencyReason: verdict.reason, candidateKind: candidateIdentity.kind,
|
|
1948
1989
|
storageDelta: transaction.storageDelta,
|
|
1949
1990
|
transactionReceipts: transaction.paths.receipts,
|
|
1950
|
-
bundleSha256
|
|
1991
|
+
bundleSha256,
|
|
1951
1992
|
coverageSha256: sha256File(path.join(KB_DIR, 'COVERAGE.json')),
|
|
1952
1993
|
phaseEvidence: phaseEvidenceFor({ root: KB_DIR, terminalVerdict: transaction.terminalVerdict,
|
|
1953
|
-
bundleSha256
|
|
1994
|
+
bundleSha256,
|
|
1954
1995
|
transactionReceipts: transaction.paths.receipts, overlay: privateOverlay,
|
|
1955
1996
|
storageDelta: transaction.storageDelta }) });
|
|
1956
1997
|
if (finalOutcome?.terminalVerdict === 'recovery-required') die(finalOutcome.reason);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.5.
|
|
4
|
-
"description": "One-command installer for RuvNet Brain
|
|
3
|
+
"version": "4.5.8",
|
|
4
|
+
"description": "One-command installer for RuvNet Brain \u2014 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": {
|
|
7
7
|
"ruvnet-brain": "bin/install.mjs"
|
|
@@ -74,7 +74,9 @@
|
|
|
74
74
|
"hooks:check": "node scripts/hook-retirement-check.mjs",
|
|
75
75
|
"release:qualify": "node scripts/release-qualification.mjs",
|
|
76
76
|
"rehearse:corpus": "node scripts/rehearse-corpus-pipeline.mjs",
|
|
77
|
-
"single-source:check": "node scripts/single-source-check.mjs"
|
|
77
|
+
"single-source:check": "node scripts/single-source-check.mjs",
|
|
78
|
+
"benchmark:continuity": "node scripts/qa/progression-validation-benchmark.mjs",
|
|
79
|
+
"host:codex:execution-proof": "node scripts/codex-host-execution-proof.mjs"
|
|
78
80
|
},
|
|
79
81
|
"files": [
|
|
80
82
|
".claude-plugin/marketplace.json",
|
|
@@ -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 a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.5.
|
|
4
|
+
"version": "4.5.8",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Open the RuvNet Brain Console (/rnbc) — a local web page that mirrors YOUR machine's RuvNet setup, explains it in plain English, and lets you safely configure and fix it. Read-only until you click; every machine change is explained first and is reversible.
|
|
3
|
-
updated: 2026-10-
|
|
3
|
+
updated: 2026-10-04
|
|
4
|
+
version: 1.0.1
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
@@ -64,6 +65,17 @@ header chip says how fresh the reading is and re-measures if they click it.
|
|
|
64
65
|
Report it plainly — the real error, not a guess — and offer to fix it. Never leave them staring at
|
|
65
66
|
a dead tab wondering whether it is still loading.
|
|
66
67
|
|
|
68
|
+
## Promise capture preference
|
|
69
|
+
|
|
70
|
+
If the user asks to stop automatic promise capture, explain the supported setting:
|
|
71
|
+
`RUVNET_PROMISE_CAPTURE=off` in the hook process environment. It can be supplied for the machine or
|
|
72
|
+
one project through the host's environment configuration. The exact value `off` stops capturing new
|
|
73
|
+
"I'll …" promises; unset it or set `on` to resume capture. This preference is separate from the Brain
|
|
74
|
+
on/off switch and is not currently a Console toggle. Existing promises remain visible and forceable,
|
|
75
|
+
and close only on verified evidence. Capability and completion integrity checks keep running. Do not
|
|
76
|
+
disable the Stop hook or edit the work ledger to apply this preference. Reopen the host if its current
|
|
77
|
+
session has not picked up the changed environment.
|
|
78
|
+
|
|
67
79
|
---
|
|
68
80
|
|
|
69
81
|
## Things NOT to do before the page is open
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "RuvNet Brain continuity plane for Codex. Broad legacy automatic gates remain retired. Registrations here are MEASURED, not assumed: a probe hook was registered on all twelve event names the installed codex-cli 0.154.0 binary declares and a real `codex exec` run was observed (2026-09-11). SessionStart, UserPromptSubmit and SessionEnd FIRED and carry handlers. Stop keeps the pre-existing, project-scoped continuation gate; session-snapshot was added at Stop on 2026-09-29 to record each turn's outcome (turn-outcome-capture.mjs) \u2014 Codex SessionEnd carries no last_assistant_message, so Stop is the only boundary that can. Its delivery rests on codex-cli 0.158.0's declared stop.command.input schema and the other Codex Stop handlers; it has NOT been live-observed and fails open. PreCompact is DECLARED ABSENT here: the binary declares the event but the probe never observed it, and a capture registered on an unobserved event would look symmetrical while capturing nothing. All handlers are bounded and fail open. Amended 2026-09-11: UserPromptSubmit also runs ground-ruvnet (grounding injection, ADR-040 \u00a7Amendment). Amended 2026-09-12: the 2026-09-11 PreToolUse/PostToolUse \"not observed\" note was measured with a prompt (`codex exec \"reply OK\"`) that never invoked a tool, so neither event had anything to fire on \u2014 that was an untested path, not a failing one. Re-probed with prompts that actually call a tool: a real apply_patch write fired PreToolUse/PostToolUse with tool_name \"apply_patch\", and a real MCP call to this repo's own search_ruvnet server fired both with tool_name \"mcp__ruvnet_brain__search_ruvnet\". Both are now registered below: decision-gate's write route (matcher includes apply_patch, Codex's raw write-tool name) and grounding-stamp (unchanged matcher already recognizes the mcp__..__search_ruvnet shape). The bash route (exec_command) remains unregistered \u2014 today's measurement covered a write and an MCP call, not exec_command, and extending on that evidence would be the same unproven leap this note replaces. Also added 2026-09-12: grounding-turn-mark (UserPromptSubmit) and grounding-turn-gate (Stop), the \"answered without searching\" pair \u2014 a prompt-level grounding directive is advisory, so this records whether it fired and forces continuation at Stop if no search_ruvnet call was recorded since (reusing grounding-stamp's own stamp evidence). Both are registered on Codex identically to Claude: the Stop-block contract (hookSpecificOutput.additionalContext) already has a proven Codex translation via codex-hook-adapter.mjs's Stop branch (see tests/unit/codex-lifecycle-hooks.test.mjs), the same path continuation-gate already uses. Capacity-aware parallel-work guidance is also registered at UserPromptSubmit: it advises the coordinator to launch only real independent workers within sampled resource headroom and the live runtime/tool cap; it never spawns workers or claims execution. Automatic normalized project observations are captured at prompt, pre-tool, post-tool and child completion; Claude also observes PostToolUseFailure. Pending and degraded memory are disclosed; this does not establish synchronous ADR-073 conformance or native Grok recall. SessionStart has an 8-second measured replay/restore envelope.",
|
|
2
|
+
"description": "RuvNet Brain continuity plane for Codex. Broad legacy automatic gates remain retired. Registrations here are MEASURED, not assumed: a probe hook was registered on all twelve event names the installed codex-cli 0.154.0 binary declares and a real `codex exec` run was observed (2026-09-11). SessionStart, UserPromptSubmit and SessionEnd FIRED and carry handlers. Stop keeps the pre-existing, project-scoped continuation gate; session-snapshot was added at Stop on 2026-09-29 to record each turn's outcome (turn-outcome-capture.mjs) \u2014 Codex SessionEnd carries no last_assistant_message, so Stop is the only boundary that can. Its delivery rests on codex-cli 0.158.0's declared stop.command.input schema and the other Codex Stop handlers; it has NOT been live-observed and fails open. PreCompact is DECLARED ABSENT here: the binary declares the event but the probe never observed it, and a capture registered on an unobserved event would look symmetrical while capturing nothing. All handlers are bounded and fail open. Amended 2026-09-11: UserPromptSubmit also runs ground-ruvnet (grounding injection, ADR-040 \u00a7Amendment). Amended 2026-09-12: the 2026-09-11 PreToolUse/PostToolUse \"not observed\" note was measured with a prompt (`codex exec \"reply OK\"`) that never invoked a tool, so neither event had anything to fire on \u2014 that was an untested path, not a failing one. Re-probed with prompts that actually call a tool: a real apply_patch write fired PreToolUse/PostToolUse with tool_name \"apply_patch\", and a real MCP call to this repo's own search_ruvnet server fired both with tool_name \"mcp__ruvnet_brain__search_ruvnet\". Both are now registered below: decision-gate's write route (matcher includes apply_patch, Codex's raw write-tool name) and grounding-stamp (unchanged matcher already recognizes the mcp__..__search_ruvnet shape). The bash route (exec_command) remains unregistered \u2014 today's measurement covered a write and an MCP call, not exec_command, and extending on that evidence would be the same unproven leap this note replaces. Also added 2026-09-12: grounding-turn-mark (UserPromptSubmit) and grounding-turn-gate (Stop), the \"answered without searching\" pair \u2014 a prompt-level grounding directive is advisory, so this records whether it fired and forces continuation at Stop if no search_ruvnet call was recorded since (reusing grounding-stamp's own stamp evidence). Both are registered on Codex identically to Claude: the Stop-block contract (hookSpecificOutput.additionalContext) already has a proven Codex translation via codex-hook-adapter.mjs's Stop branch (see tests/unit/codex-lifecycle-hooks.test.mjs), the same path continuation-gate already uses. Capacity-aware parallel-work guidance is also registered at UserPromptSubmit: it advises the coordinator to launch only real independent workers within sampled resource headroom and the live runtime/tool cap; it never spawns workers or claims execution. Automatic normalized project observations are captured at prompt, pre-tool, post-tool and child completion; Claude also observes PostToolUseFailure. Pending and degraded memory are disclosed; this does not establish synchronous ADR-073 conformance or native Grok recall. SessionStart has an 8-second measured replay/restore envelope. Issue377: consent-eligible learning captures only fixed workflow metadata; lifecycle recovery schedules one finite worker. Original records remain retained with hash-bound acknowledgments. Native delivery must be separately evidenced; registration is not proof.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
"timeout": 8
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"matcher": "startup|resume|clear|compact|fork",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"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",
|
|
21
|
+
"timeout": 3
|
|
22
|
+
}
|
|
23
|
+
]
|
|
14
24
|
}
|
|
15
25
|
],
|
|
16
26
|
"Stop": [
|
|
@@ -77,6 +87,16 @@
|
|
|
77
87
|
"timeout": 5
|
|
78
88
|
}
|
|
79
89
|
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"matcher": "^(?:Bash|Write|Edit|MultiEdit|exec_command|apply_patch|functions(?:\\.|__)(?:exec_command|apply_patch))$",
|
|
93
|
+
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"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-capture",
|
|
97
|
+
"timeout": 3
|
|
98
|
+
}
|
|
99
|
+
]
|
|
80
100
|
}
|
|
81
101
|
],
|
|
82
102
|
"UserPromptSubmit": [
|
|
@@ -121,6 +141,16 @@
|
|
|
121
141
|
"timeout": 3
|
|
122
142
|
}
|
|
123
143
|
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"matcher": "*",
|
|
147
|
+
"hooks": [
|
|
148
|
+
{
|
|
149
|
+
"type": "command",
|
|
150
|
+
"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",
|
|
151
|
+
"timeout": 3
|
|
152
|
+
}
|
|
153
|
+
]
|
|
124
154
|
}
|
|
125
155
|
],
|
|
126
156
|
"SubagentStop": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "The legacy automatic gate collection remains retired. What is permitted is the continuity plane below and nothing else: SessionStart restores the canonical project checkpoint; UserPromptSubmit runs the single unprompted-speech chokepoint; Stop may nudge one explicitly authorized project-scoped objective AND capture a project snapshot; PreCompact and SessionEnd capture a project snapshot. Version 3 of this file permitted exactly two handlers, which was not a safety property but a contradiction: it demanded a restore while forbidding any event that could WRITE the journal the restore reads, so the canonical store held zero progression rows. Adding to this list is still a deliberate act that must pass `npm run hooks:check`; what changed is that the shape can now express the plane that actually works. Amended 2026-09-11 (Stuart): the continuity-only charter had retired the ONLY enforcement of ADR-0012 \u2014 never write rUv-product code the brain has not seen \u2014 and the failure it exists to prevent recurred the day it was measured absent. The plane now also carries three grounding registrations: ground-ruvnet (UserPromptSubmit, grounding injection \u2014 a second owner of that event, scoped by ADR-040 \u00a7Amendment 2026-09-11 to directives rather than speech), decision-gate\u2019s write route (PreToolUse, the one refuser, ADR-067), and grounding-stamp (PostToolUse on a successful search_ruvnet, the receipt that opens the write gate). Amended 2026-09-12: the 2026-09-11 claim that Codex PreToolUse/PostToolUse delivery had never been observed was measured with a prompt that never invoked a tool (`codex exec \"reply OK\"`), so it was an untested path, not a failing one. Re-measured with prompts that actually call a tool (a real apply_patch write, a real MCP search_ruvnet call against this repo's own server): both events FIRED on codex-cli 0.154.0 with real payloads. decision-gate's write route and grounding-stamp are now dual-host (see _codexCapture and contracts below); the bash route (exec_command) remains Claude-only \u2014 today's measurement did not exercise it. Also amended 2026-09-12: added grounding-turn-mark (UserPromptSubmit) and grounding-turn-gate (Stop), the \"answered without searching\" pair \u2014 ground-ruvnet's Gate 1 directive is advisory, so nothing previously checked whether the model complied before the turn ended. grounding-turn-mark records that Gate 1 fired for a turn; grounding-turn-gate forces continuation at Stop if grounding-stamp's own evidence shows no search_ruvnet call happened since. Both dual-host from the start (the Stop-block contract is already proven on Codex via continuation-gate). Added capacity-aware-parallel-work at UserPromptSubmit on both measured hosts: it is context-only, resource-bounded, and requires the coordinator to check actual tool/runtime slots; it does not spawn or claim workers. Amended 2026-09-30 (ADR-0030 decision point #1, no new registration): grounding-turn-mark also arms a turn when the prompt asks a capability/feasibility/architecture question about any tool or platform, and grounding-turn-gate then requires every capability claim in the final answer to be bound to a relevant STRONG source read this turn after the last weak one (a WebFetch body is a small model's summary: weak); on Claude it reads the transcript instead of stamp mtimes, which removed the measured 'no search recorded' false alarms. ADR-0030 #2/#3 run in shadow only. Amendment 2026-10-03: normalized prompt/tool/failure/child observations are automatic; startup replays before restoring. Queued and unavailable evidence remain distinct from exact committed readback. Native Grok prompt recall is unsupported.",
|
|
3
|
-
"_version":
|
|
3
|
+
"_version": 10,
|
|
4
4
|
"_eventOwners": [
|
|
5
5
|
{
|
|
6
6
|
"event": "SessionStart",
|
|
@@ -169,6 +169,36 @@
|
|
|
169
169
|
"codex"
|
|
170
170
|
],
|
|
171
171
|
"responsibility": "Capture bounded redacted observable intent or outcome through the canonical progression bridge; retain pending/failed status and do not infer success from pre-tool intent."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"event": "PostToolUse",
|
|
175
|
+
"owner": "learn-capture",
|
|
176
|
+
"class": "workflow learning",
|
|
177
|
+
"hosts": [
|
|
178
|
+
"claude",
|
|
179
|
+
"codex"
|
|
180
|
+
],
|
|
181
|
+
"responsibility": "Fixed vocabulary capture or finite selected-scope orphan recovery; no cross-scope legacy drain; registration alone does not prove native delivery."
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"event": "SessionEnd",
|
|
185
|
+
"owner": "learn-flush",
|
|
186
|
+
"class": "workflow learning",
|
|
187
|
+
"hosts": [
|
|
188
|
+
"claude",
|
|
189
|
+
"codex"
|
|
190
|
+
],
|
|
191
|
+
"responsibility": "Fixed vocabulary capture or finite selected-scope orphan recovery; no cross-scope legacy drain; registration alone does not prove native delivery."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"event": "SessionStart",
|
|
195
|
+
"owner": "learn-flush",
|
|
196
|
+
"class": "workflow learning",
|
|
197
|
+
"hosts": [
|
|
198
|
+
"claude",
|
|
199
|
+
"codex"
|
|
200
|
+
],
|
|
201
|
+
"responsibility": "Fixed vocabulary capture or finite selected-scope orphan recovery; no cross-scope legacy drain; registration alone does not prove native delivery."
|
|
172
202
|
}
|
|
173
203
|
],
|
|
174
204
|
"_notInThisPlane": [
|
|
@@ -297,7 +327,7 @@
|
|
|
297
327
|
"codex"
|
|
298
328
|
],
|
|
299
329
|
"mode": "advisory",
|
|
300
|
-
"offBehavior": "
|
|
330
|
+
"offBehavior": "silence",
|
|
301
331
|
"matcher": "*",
|
|
302
332
|
"timeout": 6
|
|
303
333
|
},
|
|
@@ -407,6 +437,42 @@
|
|
|
407
437
|
"offBehavior": "run",
|
|
408
438
|
"matcher": "*",
|
|
409
439
|
"timeout": 10
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"id": "learn-capture",
|
|
443
|
+
"event": "PostToolUse",
|
|
444
|
+
"hosts": [
|
|
445
|
+
"claude",
|
|
446
|
+
"codex"
|
|
447
|
+
],
|
|
448
|
+
"mode": "advisory",
|
|
449
|
+
"offBehavior": "silence",
|
|
450
|
+
"matcher": "^(?:Bash|Write|Edit|MultiEdit|exec_command|apply_patch|functions(?:\\.|__)(?:exec_command|apply_patch))$",
|
|
451
|
+
"timeout": 3
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"id": "learn-flush",
|
|
455
|
+
"event": "SessionEnd",
|
|
456
|
+
"hosts": [
|
|
457
|
+
"claude",
|
|
458
|
+
"codex"
|
|
459
|
+
],
|
|
460
|
+
"mode": "advisory",
|
|
461
|
+
"offBehavior": "silence",
|
|
462
|
+
"matcher": "*",
|
|
463
|
+
"timeout": 3
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"id": "learn-flush",
|
|
467
|
+
"event": "SessionStart",
|
|
468
|
+
"hosts": [
|
|
469
|
+
"claude",
|
|
470
|
+
"codex"
|
|
471
|
+
],
|
|
472
|
+
"mode": "advisory",
|
|
473
|
+
"offBehavior": "silence",
|
|
474
|
+
"matcher": "startup|resume|clear|compact|fork",
|
|
475
|
+
"timeout": 3
|
|
410
476
|
}
|
|
411
477
|
],
|
|
412
478
|
"matcherAllowlist": [
|
|
@@ -549,6 +615,20 @@
|
|
|
549
615
|
"matcher": "*",
|
|
550
616
|
"reason": "Explicit supported Codex lifecycle registration, corresponding to the host-filtered continuity policy; its enforcing handler and live registration are checked independently.",
|
|
551
617
|
"retiredBy": "not retired - canonical continuity host parity"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
"layer": "plugin",
|
|
621
|
+
"event": "PostToolUse",
|
|
622
|
+
"matcher": "^(?:Bash|Write|Edit|MultiEdit|exec_command|apply_patch|functions(?:\\.|__)(?:exec_command|apply_patch))$",
|
|
623
|
+
"reason": "Consent-eligible normalized fixed workflow vocabulary, privacy-minimized before write.",
|
|
624
|
+
"retiredBy": "reviewed issue377 learning repair"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
"layer": "codex",
|
|
628
|
+
"event": "PostToolUse",
|
|
629
|
+
"matcher": "^(?:Bash|Write|Edit|MultiEdit|exec_command|apply_patch|functions(?:\\.|__)(?:exec_command|apply_patch))$",
|
|
630
|
+
"reason": "Consent-eligible normalized fixed workflow vocabulary, privacy-minimized before write.",
|
|
631
|
+
"retiredBy": "reviewed issue377 learning repair"
|
|
552
632
|
}
|
|
553
633
|
]
|
|
554
634
|
}
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "RuvNet Brain lifecycle plane. The broad legacy routing, learning, and release interceptors remain retired. What is automatic is exactly: SessionStart restores the canonical project checkpoint; UserPromptSubmit runs the single unprompted-speech chokepoint, ground-ruvnet grounding injection, and a capacity-aware context hint for clearly large independent work. The capacity hook uses bounded CPU/memory-pressure evidence, never starts agents, and tells the coordinator to check live tools and clamp to the runtime cap; it is not user-facing speech. ground-ruvnet remains a directive to the model, not speech \u2014 ADR-040 \u00a7Amendment 2026-09-11. PreToolUse runs decision-gate's write route, the ONE process that may refuse a Write/Edit/MultiEdit/NotebookEdit/apply_patch (ADR-067, composing ground-before-write per ADR-0012); PostToolUse on a successful search_ruvnet mints the grounding stamp that opens that gate. grounding-turn-mark (UserPromptSubmit) records that the grounding directive fired this turn, and grounding-turn-gate (Stop) forces continuation if the final answer asserts a rUv capability and no search_ruvnet call was recorded since. Stop also runs the ledger-scoped continuation handler and captures a project snapshot; PreCompact and SessionEnd capture a project snapshot. Every capture is bounded and fails open; only decision-gate may block on exit code, and it fails open on its own errors \u2014 the Stop stdout envelopes are advisory at the shim boundary but keep the agent working. Automatic normalized project observations are captured at prompt, pre-tool, post-tool and child completion; Claude also observes PostToolUseFailure. Pending and degraded memory are disclosed; this does not establish synchronous ADR-073 conformance or native Grok recall. SessionStart has an 8-second measured replay/restore envelope.",
|
|
2
|
+
"description": "RuvNet Brain lifecycle plane. The broad legacy routing, learning, and release interceptors remain retired. What is automatic is exactly: SessionStart restores the canonical project checkpoint; UserPromptSubmit runs the single unprompted-speech chokepoint, ground-ruvnet grounding injection, and a capacity-aware context hint for clearly large independent work. The capacity hook uses bounded CPU/memory-pressure evidence, never starts agents, and tells the coordinator to check live tools and clamp to the runtime cap; it is not user-facing speech. ground-ruvnet remains a directive to the model, not speech \u2014 ADR-040 \u00a7Amendment 2026-09-11. PreToolUse runs decision-gate's write route, the ONE process that may refuse a Write/Edit/MultiEdit/NotebookEdit/apply_patch (ADR-067, composing ground-before-write per ADR-0012); PostToolUse on a successful search_ruvnet mints the grounding stamp that opens that gate. grounding-turn-mark (UserPromptSubmit) records that the grounding directive fired this turn, and grounding-turn-gate (Stop) forces continuation if the final answer asserts a rUv capability and no search_ruvnet call was recorded since. Stop also runs the ledger-scoped continuation handler and captures a project snapshot; PreCompact and SessionEnd capture a project snapshot. Every capture is bounded and fails open; only decision-gate may block on exit code, and it fails open on its own errors \u2014 the Stop stdout envelopes are advisory at the shim boundary but keep the agent working. Automatic normalized project observations are captured at prompt, pre-tool, post-tool and child completion; Claude also observes PostToolUseFailure. Pending and degraded memory are disclosed; this does not establish synchronous ADR-073 conformance or native Grok recall. SessionStart has an 8-second measured replay/restore envelope. Issue377: consent-eligible learning captures only fixed workflow metadata; lifecycle recovery schedules one finite worker. Original records remain retained with hash-bound acknowledgments. Native delivery must be separately evidenced; registration is not proof.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
"timeout": 8
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"matcher": "startup|resume|clear|compact|fork",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-flush || true",
|
|
21
|
+
"timeout": 3
|
|
22
|
+
}
|
|
23
|
+
]
|
|
14
24
|
}
|
|
15
25
|
],
|
|
16
26
|
"UserPromptSubmit": [
|
|
@@ -87,6 +97,16 @@
|
|
|
87
97
|
"timeout": 5
|
|
88
98
|
}
|
|
89
99
|
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"matcher": "^(?:Bash|Write|Edit|MultiEdit|exec_command|apply_patch|functions(?:\\.|__)(?:exec_command|apply_patch))$",
|
|
103
|
+
"hooks": [
|
|
104
|
+
{
|
|
105
|
+
"type": "command",
|
|
106
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-capture || true",
|
|
107
|
+
"timeout": 3
|
|
108
|
+
}
|
|
109
|
+
]
|
|
90
110
|
}
|
|
91
111
|
],
|
|
92
112
|
"Stop": [
|
|
@@ -133,6 +153,16 @@
|
|
|
133
153
|
"timeout": 10
|
|
134
154
|
}
|
|
135
155
|
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"matcher": "*",
|
|
159
|
+
"hooks": [
|
|
160
|
+
{
|
|
161
|
+
"type": "command",
|
|
162
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-flush || true",
|
|
163
|
+
"timeout": 3
|
|
164
|
+
}
|
|
165
|
+
]
|
|
136
166
|
}
|
|
137
167
|
],
|
|
138
168
|
"SubagentStop": [
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// DISTINCT-FROM hook-shim.mjs: managed execution fails closed; advisory hook fallback cannot
|
|
2
|
+
// authorize a command. The protocol shell owns declarations, each request owns one generation.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const LOCAL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
|
+
const contained = (root, file) => file.startsWith(`${root}${path.sep}`);
|
|
12
|
+
|
|
13
|
+
export function managedGenerationIdentity(root, { version, generation = 'source' } = {}) {
|
|
14
|
+
root = fs.realpathSync(root);
|
|
15
|
+
const manifestPath = path.join(root, '.claude-plugin', 'plugin.json');
|
|
16
|
+
const handlerPath = path.join(root, 'mcp', 'managed-cli-interface.mjs');
|
|
17
|
+
for (const file of [manifestPath, handlerPath]) {
|
|
18
|
+
if (!fs.lstatSync(file).isFile() || !contained(root, fs.realpathSync(file))) {
|
|
19
|
+
throw new Error('managed generation resource is not a contained regular file');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const manifestBytes = fs.readFileSync(manifestPath);
|
|
23
|
+
const manifest = JSON.parse(manifestBytes);
|
|
24
|
+
if (manifest.name !== 'ruvnet-brain' || typeof manifest.version !== 'string'
|
|
25
|
+
|| !/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.+-]+)?$/.test(manifest.version)
|
|
26
|
+
|| (version !== undefined && manifest.version !== version)) {
|
|
27
|
+
throw new Error('managed generation manifest name/version mismatch');
|
|
28
|
+
}
|
|
29
|
+
const sourceDigest = digest(Buffer.concat([manifestBytes, fs.readFileSync(handlerPath)]));
|
|
30
|
+
return Object.freeze({ root, handlerPath, version: manifest.version, generation, sourceDigest,
|
|
31
|
+
binding: digest(JSON.stringify([root, manifest.version, generation, sourceDigest])) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function selectGeneration(env, fallbackRoot) {
|
|
35
|
+
const home = env.HOME || os.homedir();
|
|
36
|
+
const brainHome = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
|
|
37
|
+
let activeBytes;
|
|
38
|
+
try { activeBytes = fs.readFileSync(path.join(brainHome, 'active.json'), 'utf8'); }
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== 'ENOENT') throw error;
|
|
41
|
+
// A copied persistent shell must never manufacture an active generation from its resources.
|
|
42
|
+
if (path.resolve(fallbackRoot) === path.join(home, '.claude', 'ruvnet-brain')) {
|
|
43
|
+
throw new Error('managed active generation unavailable');
|
|
44
|
+
}
|
|
45
|
+
return { ...managedGenerationIdentity(fallbackRoot), brainHome, leased: false };
|
|
46
|
+
}
|
|
47
|
+
const active = JSON.parse(activeBytes);
|
|
48
|
+
if (typeof active.codeRoot !== 'string' || typeof active.version !== 'string'
|
|
49
|
+
|| !Number.isSafeInteger(active.generation) || active.generation < 0) {
|
|
50
|
+
throw new Error('managed active generation descriptor invalid');
|
|
51
|
+
}
|
|
52
|
+
const versions = fs.realpathSync(path.join(brainHome, 'versions'));
|
|
53
|
+
const root = fs.realpathSync(path.resolve(brainHome, active.codeRoot));
|
|
54
|
+
if (!contained(versions, root) || path.basename(root) !== active.version) {
|
|
55
|
+
throw new Error('managed active generation escapes its version tree');
|
|
56
|
+
}
|
|
57
|
+
return { ...managedGenerationIdentity(root, active), brainHome, leased: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createManagedCliDispatcher({ fallbackRoot = LOCAL_ROOT,
|
|
61
|
+
importModule = (url) => import(url) } = {}) {
|
|
62
|
+
const pins = new Map();
|
|
63
|
+
return async function dispatchManagedCli(toolName, args, env = process.env,
|
|
64
|
+
fetchImpl = globalThis.fetch, lifecycle = {}) {
|
|
65
|
+
let lease;
|
|
66
|
+
try {
|
|
67
|
+
const selected = selectGeneration(env, fallbackRoot);
|
|
68
|
+
// ESM caches by URL. A changed file at the same immutable path must never be blessed as
|
|
69
|
+
// new code while execution still uses the old cached module (nor can query strings fix dependencies).
|
|
70
|
+
const pinned = pins.get(selected.root);
|
|
71
|
+
if (pinned && pinned !== selected.sourceDigest) throw new Error('managed immutable generation changed; restart required');
|
|
72
|
+
pins.set(selected.root, selected.sourceDigest);
|
|
73
|
+
if (selected.leased) {
|
|
74
|
+
const dir = path.join(selected.brainHome, 'leases');
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
lease = path.join(dir, `mcp-managed-${process.pid}-${randomUUID()}.json`);
|
|
77
|
+
fs.writeFileSync(lease, JSON.stringify({ pid: process.pid, version: selected.version, at: new Date().toISOString() }), { flag: 'wx', mode: 0o600 });
|
|
78
|
+
}
|
|
79
|
+
const handler = await importModule(pathToFileURL(selected.handlerPath).href);
|
|
80
|
+
if (typeof handler.callManagedCli !== 'function') throw new Error('managed generation handler unavailable');
|
|
81
|
+
return await handler.callManagedCli(toolName, args, env, fetchImpl,
|
|
82
|
+
{ ...lifecycle, generationBinding: selected.binding });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { content: [{ type: 'text', text: `managed generation unavailable: ${error.message}` }], isError: true };
|
|
85
|
+
} finally {
|
|
86
|
+
if (lease) { try { fs.unlinkSync(lease); } catch { /* lease expiry preserves interrupted calls */ } }
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const dispatchManagedCli = createManagedCliDispatcher();
|