ruvnet-brain 4.5.5 → 4.5.7
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 +24 -6
- package/config/model-router/claude-terminal-mod/.claude-plugin/plugin.json +1 -0
- package/config/model-router/claude-terminal-mod/README.md +22 -0
- package/config/model-router/claude-terminal-mod/hooks/hooks.json +1 -0
- package/config/model-router/claude-terminal-mod/hooks/policy.default.mjs +2 -0
- package/config/model-router/claude-terminal-mod/hooks/register.js +66 -0
- package/config/model-router/claude-terminal-mod/hooks/routing.js +55 -0
- package/config/model-router/claude-terminal-mod/hooks/runtime.js +2 -0
- package/config/model-router/claude-terminal-mod/tests/native.test.ts +86 -0
- package/config/model-router/policy.default.mjs +4 -1
- package/kb/forge-update.mjs +50 -9
- package/package.json +4 -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/hook-contracts.json +1 -1
- package/plugin/mcp/managed-cli-generation.mjs +91 -0
- package/plugin/mcp/managed-cli-interface.mjs +45 -16
- package/plugin/mcp/server.mjs +3 -2
- package/plugin/scripts/capability-claim-evidence.mjs +9 -2
- package/plugin/scripts/continuation-gate.mjs +3 -1
- package/plugin/scripts/continuity-journal.mjs +11 -11
- package/plugin/scripts/hook-shim.mjs +2 -1
- package/plugin/scripts/kb-copy-proof.mjs +4 -3
- package/plugin/scripts/nightly-scheduler.mjs +6 -2
- package/plugin/scripts/project-capture-queue.mjs +23 -8
- package/plugin/scripts/project-progression-hook.mjs +16 -7
- package/plugin/scripts/project-progression-outbox.mjs +116 -19
- package/plugin/scripts/project-progression-producer.mjs +12 -1
- package/plugin/scripts/project-progression-session-start.mjs +10 -0
- package/plugin/scripts/project-progression-store.mjs +53 -1
- package/plugin/scripts/project-progression-suspension.mjs +60 -0
- package/plugin/scripts/project-transition-hook.mjs +44 -16
- package/plugin/scripts/session-snapshot-hook.mjs +16 -8
- package/plugin/scripts/turn-outcome-capture.mjs +7 -3
- package/scripts/claude-terminal-mod.mjs +89 -0
- package/scripts/model-routing-gateway.mjs +47 -14
- package/scripts/model-routing-launchers.mjs +36 -1
- package/scripts/model-terminal-gateway.mjs +310 -0
- package/scripts/model-terminal-launchers.mjs +300 -0
- package/scripts/release-qualification-contract.mjs +37 -6
- package/scripts/release-qualification.mjs +9 -6
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
|
@@ -80,7 +80,8 @@ import {
|
|
|
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
82
|
import { repairSecurityGuidance } from '../scripts/security-guidance-codex-compat.mjs';
|
|
83
|
-
import { installNativeLaunchers } from '../scripts/model-routing-launchers.mjs';
|
|
83
|
+
import { installNativeLaunchers, installRoutingRuntime } from '../scripts/model-routing-launchers.mjs';
|
|
84
|
+
import { installTerminalLaunchers } from '../scripts/model-terminal-launchers.mjs';
|
|
84
85
|
import { readConsoleReceipts, replaceStaleConsoles } from '../scripts/console-instances.mjs';
|
|
85
86
|
import { moveBrain, MoveRefused } from '../scripts/move-brain.mjs';
|
|
86
87
|
import { brainLocation } from '../plugin/scripts/brain-location.mjs';
|
|
@@ -1631,6 +1632,7 @@ function codexManagedBlock(serverPath) {
|
|
|
1631
1632
|
'command = "node"',
|
|
1632
1633
|
`args = [${JSON.stringify(serverPath)}]`,
|
|
1633
1634
|
'startup_timeout_sec = 30',
|
|
1635
|
+
'env = { RUVNET_HOOK_HOST = "codex" }',
|
|
1634
1636
|
CODEX_BLOCK_END,
|
|
1635
1637
|
].join('\n');
|
|
1636
1638
|
}
|
|
@@ -1837,6 +1839,10 @@ export function wireCodexHost({
|
|
|
1837
1839
|
// land where the server will look for them, and the walk is transitive because a dependency's own
|
|
1838
1840
|
// dependency is no less required.
|
|
1839
1841
|
const deps = serverDependencies(source);
|
|
1842
|
+
// Progression capture reads its adapter version from this runtime resource, not an import.
|
|
1843
|
+
// Keep it beside the copied plugin scripts, bound to the same source as the MCP shell.
|
|
1844
|
+
deps.push({ spec: '../.claude-plugin/plugin.json',
|
|
1845
|
+
from: path.resolve(path.dirname(source), '..', '.claude-plugin', 'plugin.json') });
|
|
1840
1846
|
const missing = deps.filter((d) => !fs.existsSync(d.from));
|
|
1841
1847
|
if (missing.length) {
|
|
1842
1848
|
if (announce) warn(`MCP server dependency missing from this bundle (${missing.map((d) => d.spec).join(', ')}) — Codex left untouched (non-fatal)`);
|
|
@@ -4913,18 +4919,28 @@ export function syncManagedRouterDefault({ routerDir = path.join(os.homedir(), '
|
|
|
4913
4919
|
}
|
|
4914
4920
|
|
|
4915
4921
|
export function refreshInstalledModelLaunchers({ sourceRoot = REPO_ROOT, home = os.homedir() } = {}) {
|
|
4922
|
+
const terminalFile = path.join(home, '.cache', 'ruvnet-brain', 'model-routing', 'terminal-launcher-config.json');
|
|
4923
|
+
let terminal = { action: 'not-installed' };
|
|
4924
|
+
if (fs.existsSync(terminalFile)) {
|
|
4925
|
+
const previous = JSON.parse(fs.readFileSync(terminalFile, 'utf8'));
|
|
4926
|
+
const runtime = installRoutingRuntime({ sourceRoot, home, apply: true });
|
|
4927
|
+
const receipt = installTerminalLaunchers({ home, ...runtime, nodeBinary: previous.nodeBinary,
|
|
4928
|
+
realCodex: previous.realCodex, realClaude: previous.realClaude, apply: true });
|
|
4929
|
+
terminal = { action: previous.runtimeDigest === runtime.runtimeDigest ? 'unchanged' : 'updated',
|
|
4930
|
+
runtimeDigest: runtime.runtimeDigest, shellConflicts: receipt.shellConflicts };
|
|
4931
|
+
}
|
|
4916
4932
|
const file = path.join(home, '.cache', 'ruvnet-brain', 'model-routing', 'launcher-config.json');
|
|
4917
|
-
if (!fs.existsSync(file)) return { action: 'not-installed' };
|
|
4933
|
+
if (!fs.existsSync(file)) return { action: 'not-installed', terminal };
|
|
4918
4934
|
const previous = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
4919
4935
|
const receipt = installNativeLaunchers({ sourceRoot, home, extensionsRoot: previous.extensionsRoot, nodeBinary: previous.nodeBinary, apply: true });
|
|
4920
|
-
return { action: receipt.config.runtimeDigest === previous.runtimeDigest ? 'unchanged' : 'updated', runtimeDigest: receipt.config.runtimeDigest };
|
|
4936
|
+
return { action: receipt.config.runtimeDigest === previous.runtimeDigest ? 'unchanged' : 'updated', runtimeDigest: receipt.config.runtimeDigest, terminal };
|
|
4921
4937
|
}
|
|
4922
4938
|
|
|
4923
4939
|
export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.claude', 'model-router'), packageRoot = REPO_ROOT } = {}) {
|
|
4924
4940
|
const destination = path.join(routerDir, 'bin');
|
|
4925
4941
|
fs.mkdirSync(destination, { recursive: true });
|
|
4926
4942
|
let copied = 0;
|
|
4927
|
-
for (const name of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'subscription-hosts.mjs', 'review-model-defaults.mjs', 'dual-host-deliberation.mjs', 'dual-host-suggest.mjs', 'route-cheap.mjs', 'dispatch-receipt.mjs', 'metaharness-receipts.mjs', 'metaharness-router.mjs', 'model-router-dispatch.mjs', 'model-currency.mjs', 'model-currency-evidence.mjs', 'user-model-prompt-hook.mjs', 'model-router-agent-hook.mjs', 'model-weekly-assessment.mjs', 'model-weekly-analyst.mjs', 'model-weekly-cycle.mjs', 'model-weekly-qualification.mjs', 'model-native-qualification.mjs', 'model-routing-launchers.mjs', 'model-native-catalog.mjs', 'model-analyst-sandbox.mjs', 'model-routing-policy-promotion.mjs', 'model-routing-gateway.mjs', 'native-subscription-usage.mjs', 'codex-routed.sh', 'goldie-weekly.sh']) {
|
|
4943
|
+
for (const name of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'subscription-hosts.mjs', 'review-model-defaults.mjs', 'dual-host-deliberation.mjs', 'dual-host-suggest.mjs', 'route-cheap.mjs', 'dispatch-receipt.mjs', 'metaharness-receipts.mjs', 'metaharness-router.mjs', 'model-router-dispatch.mjs', 'model-currency.mjs', 'model-currency-evidence.mjs', 'user-model-prompt-hook.mjs', 'model-router-agent-hook.mjs', 'model-weekly-assessment.mjs', 'model-weekly-analyst.mjs', 'model-weekly-cycle.mjs', 'model-weekly-qualification.mjs', 'model-native-qualification.mjs', 'model-routing-launchers.mjs', 'model-terminal-gateway.mjs', 'model-terminal-launchers.mjs', 'claude-terminal-mod.mjs', 'model-native-catalog.mjs', 'model-analyst-sandbox.mjs', 'model-routing-policy-promotion.mjs', 'model-routing-gateway.mjs', 'native-subscription-usage.mjs', 'codex-routed.sh', 'goldie-weekly.sh']) {
|
|
4928
4944
|
const source = path.join(packageRoot, 'scripts', name);
|
|
4929
4945
|
if (!fs.existsSync(source)) throw new Error(`Packaged router tool missing: ${name}`);
|
|
4930
4946
|
const target = path.join(destination, name);
|
|
@@ -4935,8 +4951,9 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
|
|
|
4935
4951
|
finally { fs.rmSync(temporary, { force: true }); }
|
|
4936
4952
|
copied++;
|
|
4937
4953
|
}
|
|
4938
|
-
// Preserve
|
|
4939
|
-
const runtimeRelative
|
|
4954
|
+
// Preserve package-relative imports without replacing user policy.mjs overrides.
|
|
4955
|
+
for (const runtimeRelative of [path.join('plugin', 'scripts', 'runtime-preferences.mjs'),
|
|
4956
|
+
path.join('config', 'model-router', 'policy.default.mjs')]) {
|
|
4940
4957
|
const runtimeTarget = path.join(routerDir, runtimeRelative);
|
|
4941
4958
|
const runtimeBytes = fs.readFileSync(path.join(packageRoot, runtimeRelative));
|
|
4942
4959
|
if (!fs.existsSync(runtimeTarget) || !fs.readFileSync(runtimeTarget).equals(runtimeBytes)) {
|
|
@@ -4946,6 +4963,7 @@ export function syncManagedRouterTools({ routerDir = path.join(os.homedir(), '.c
|
|
|
4946
4963
|
finally { fs.rmSync(temporary, { force: true }); }
|
|
4947
4964
|
copied++;
|
|
4948
4965
|
}
|
|
4966
|
+
}
|
|
4949
4967
|
const qualificationSource = path.join(packageRoot, 'config', 'model-router', 'qualification-contract.json');
|
|
4950
4968
|
const qualificationTarget = path.join(routerDir, 'qualification-contract.json');
|
|
4951
4969
|
const qualificationBytes = fs.readFileSync(qualificationSource);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"rnb-terminal-routing","version":"0.1.0","description":"Reviewed per-user native terminal routing"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Updated: 2026-10-04 16:10:00 EDT | Version 0.1.1
|
|
2
|
+
Created: 2026-10-04 16:10:00 EDT
|
|
3
|
+
|
|
4
|
+
# Native terminal routing prototype
|
|
5
|
+
|
|
6
|
+
Tested against the installed Claude Code 2.1.289 native binary and its generated mod declarations. Mods require 2.1.287 or later. This routes the native host's own `turn.step` middleware: it passes model and effort to `yield* next(...)`, preserving the native streaming result and TUI. It does not call a separate inference API or inject slash commands.
|
|
7
|
+
|
|
8
|
+
`prepareClaudeTerminalMod({ destination })` in `scripts/claude-terminal-mod.mjs` materializes a private plugin directory, generates absolute Node/helper/engine paths, and copies the approved pure classifier into the mod. The source directory is deliberately unprepared. No Node APIs run inside sandboxed mod code. `$.process.run` invokes the bridge with prompt JSON on stdin, never in arguments. The existing policy engine validates current per-user catalog, profile, reviewed model/effort allocation and subscription eligibility. Review age remains evidence for the weekly refresh and does not revoke the owner-approved allocation; it supplies no native/default/metered fallback.
|
|
9
|
+
|
|
10
|
+
A launcher must provide `RNB_CLAUDE_MOD_NONCE` (64 lowercase hexadecimal characters) and `RNB_CLAUDE_MOD_RECEIPT` (absolute receipt filename in an already prepared private directory). `session.start` calls the bridge to write an atomic, mode-0600 activation receipt. The launcher must compare nonce, exact real plugin root, native version, source digest, timestamp and live process state before accepting startup. `prepareClaudeTerminalMod` returns `modDigest`; `terminalModDigest` recomputes it. The receipt's `pid` is the bridge parent's mod worker, not an asserted main CLI PID. `--health` supports the same bounded receipt schema; it does not invent a native crash guard.
|
|
11
|
+
|
|
12
|
+
`prompt.submit` approves the prompt once. Its optional `turnId` denotes a running turn, not the upcoming idle turn. A bounded FIFO stores pending text and decisions in RAM only. `turn.start` binds the next approved prompt to the newly minted exact turn ID. If native settings hooks settled different text, the bridge reclassifies that text using the original task class as a minimum. Image-only submissions use a conservative hard-review classification while passing original attachments and text unchanged. Every backend step keeps the bound model/effort and exact turn ID/index. Completion and session end remove decisions. Queue/active maps cap at 32 entries; pending prompts expire after five minutes. Direct mid-turn deliveries (a running `turnId` without `wait`) refuse because no new authoritative `turn.start` exists; queue the prompt to start its own turn. Subagent steps and unbound continuations refuse instead of borrowing the main turn's decision.
|
|
13
|
+
|
|
14
|
+
A prompt/helper failure returns `{ drop }` without `next`. A step failure returns an immediate native refusal result without `next`, retaining turn ID/index. Every consequential hook has a `.catch` handler whose refusal does no process, filesystem or model work. Missing activation refuses subsequent prompts and steps.
|
|
15
|
+
|
|
16
|
+
## Failure boundary
|
|
17
|
+
|
|
18
|
+
Claude Code can skip all hooks if the mod fails to load, the native mod worker crashes, hooks are disabled, another guard refuses registration, or a hook and its catch handler both fail. Other mods later in the chain can rewrite model/effort. A startup receipt proves that the activation hook ran for that nonce and source digest; it is not a perpetual guarantee that routing hooks remain active. This prototype cannot claim universal fail-closed enforcement. The root CLI wrapper owns the startup guard and any further runtime monitoring. Native model inference and rendered TUI acceptance remain separate checks; plugin tests stub the engine boundary and incur no inference.
|
|
19
|
+
|
|
20
|
+
## Checks
|
|
21
|
+
|
|
22
|
+
Run `claude plugin validate config/model-router/claude-terminal-mod`, `claude plugin test config/model-router/claude-terminal-mod`, and `vitest run tests/unit/claude-terminal-mod.test.mjs`. The native tests exercise the installed mod runtime, including two different turn allocations, high coding effort, final-text class floors, missing decisions and helper exceptions. Unit tests call the actual policy engine against isolated catalog/profile/policy fixtures, including stale allocation, invalid policy, absent hard model and disabled subscription states.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"modules":["./register.js"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import runtime from './runtime.js';
|
|
2
|
+
import { createTurnCache, inspectDecision, classificationText, refusalStream, REFUSAL } from './routing.js';
|
|
3
|
+
export function register(on) {
|
|
4
|
+
const cache = createTurnCache();
|
|
5
|
+
let ready = false;
|
|
6
|
+
on('session.start', async ($, e, next) => {
|
|
7
|
+
ready = false; cache.clear();
|
|
8
|
+
const nonce = await $.env.get('RNB_CLAUDE_MOD_NONCE');
|
|
9
|
+
const receiptPath = await $.env.get('RNB_CLAUDE_MOD_RECEIPT');
|
|
10
|
+
const version = (await $.session.version()).version;
|
|
11
|
+
const sessionId = await $.session.id();
|
|
12
|
+
const pluginRoot = $.plugin.root;
|
|
13
|
+
const result = await $.process.run([runtime.nodePath, runtime.helperPath, '--ready'], {
|
|
14
|
+
stdin: JSON.stringify({ nonce, receiptPath, version, sessionId, pluginRoot }), timeoutMs: 8000,
|
|
15
|
+
});
|
|
16
|
+
if (result.exitCode !== 0) throw new Error(REFUSAL);
|
|
17
|
+
const receipt = JSON.parse(result.stdout);
|
|
18
|
+
if (receipt.nonce !== nonce || receipt.status !== 'ready') throw new Error(REFUSAL);
|
|
19
|
+
ready = true; return next(e);
|
|
20
|
+
}).catch(($, e) => { ready = false; return { cwd: e.cwd }; });
|
|
21
|
+
on('prompt.submit', async ($, e, next) => {
|
|
22
|
+
// Direct delivery into a running turn has no authoritative new turn.start boundary.
|
|
23
|
+
if (e.turnId && !e.wait) return { drop: REFUSAL };
|
|
24
|
+
if (!ready || typeof e.text !== 'string' || (!e.text.trim() && !e.attachments?.length) || e.text.length > 200000) return { drop: REFUSAL };
|
|
25
|
+
const result = await $.process.run([runtime.nodePath, runtime.helperPath, '--decision'], {
|
|
26
|
+
stdin: JSON.stringify({ prompt: classificationText(e.text, e.attachments), enginePath: runtime.enginePath, policyPath: runtime.policyPath }), timeoutMs: 8000,
|
|
27
|
+
});
|
|
28
|
+
if (result.exitCode !== 0) return { drop: REFUSAL };
|
|
29
|
+
const now = await $.clock.now();
|
|
30
|
+
const decision = inspectDecision(JSON.parse(result.stdout), classificationText(e.text, e.attachments), now);
|
|
31
|
+
const entry = cache.enqueue(e.text, decision, now);
|
|
32
|
+
try {
|
|
33
|
+
const answer = await next(e);
|
|
34
|
+
if ('drop' in answer) cache.remove(entry);
|
|
35
|
+
return answer;
|
|
36
|
+
} catch (error) { cache.remove(entry); throw error; }
|
|
37
|
+
}).catch(() => ({ drop: REFUSAL }));
|
|
38
|
+
on('turn.start', async ($, e, next) => {
|
|
39
|
+
if (!ready) throw new Error(REFUSAL);
|
|
40
|
+
const now = await $.clock.now();
|
|
41
|
+
const pending = cache.pending();
|
|
42
|
+
if (!pending) throw new Error(REFUSAL);
|
|
43
|
+
if (pending?.text === e.text) cache.bind(e, now);
|
|
44
|
+
else {
|
|
45
|
+
// Settings hooks may settle a different prompt. Reassess with the original class floor.
|
|
46
|
+
const minimumClass = pending?.decision.taskClass;
|
|
47
|
+
const text = classificationText(e.text, pending ? [{}] : undefined);
|
|
48
|
+
const result = await $.process.run([runtime.nodePath, runtime.helperPath, '--decision'], {
|
|
49
|
+
stdin: JSON.stringify({ prompt: text, minimumClass, enginePath: runtime.enginePath, policyPath: runtime.policyPath }), timeoutMs: 8000,
|
|
50
|
+
});
|
|
51
|
+
if (result.exitCode !== 0) throw new Error(REFUSAL);
|
|
52
|
+
const decision = inspectDecision(JSON.parse(result.stdout), text, now, minimumClass);
|
|
53
|
+
cache.bind(e, now, decision);
|
|
54
|
+
}
|
|
55
|
+
return next(e);
|
|
56
|
+
}).catch(($, e) => ({ turnId: e.turnId }));
|
|
57
|
+
on('turn.step', async function* ($, e, next) {
|
|
58
|
+
if (!ready) return yield* refusalStream(e);
|
|
59
|
+
const decision = cache.get(e, await $.clock.now());
|
|
60
|
+
return yield* next({ ...e, model: decision.model, effort: decision.effort });
|
|
61
|
+
}).catch(async function* ($, e) { return yield* refusalStream(e); });
|
|
62
|
+
on('turn.complete', async ($, e, next) => { cache.complete(e.turnId); return next(e); })
|
|
63
|
+
.catch(($, e) => { cache.complete(e.turnId); return { text: REFUSAL }; });
|
|
64
|
+
on('session.end', async ($, e, next) => { ready = false; cache.clear(); return next(e); })
|
|
65
|
+
.catch(($, e) => { ready = false; cache.clear(); return { sessionId: e.sessionId }; });
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { classify } from './policy.default.mjs';
|
|
2
|
+
export const REFUSAL = 'Reviewed terminal routing unavailable; request refused without model fallback.';
|
|
3
|
+
export function refusal(e) {
|
|
4
|
+
return { turnId: e.turnId, index: e.index, answer: REFUSAL, toolUses: [], stopReason: 'refusal', usage: null };
|
|
5
|
+
}
|
|
6
|
+
export async function* refusalStream(e) {
|
|
7
|
+
yield { kind: 'text', index: 0, text: REFUSAL };
|
|
8
|
+
yield { kind: 'stop', stopReason: 'refusal', usage: null };
|
|
9
|
+
return refusal(e);
|
|
10
|
+
}
|
|
11
|
+
export function classificationText(text, attachments) {
|
|
12
|
+
return text.trim() ? text : attachments?.length ? 'final substantive review of supplied attachments' : text;
|
|
13
|
+
}
|
|
14
|
+
export function inspectDecision(value, text, now, minimumClass) {
|
|
15
|
+
if (value?.schemaVersion !== 1 || value.subscriptionCovered !== true ||
|
|
16
|
+
!/^claude-[a-z0-9][a-z0-9.-]*$/.test(value.model || '') ||
|
|
17
|
+
!['low', 'medium', 'high', 'xhigh', 'max'].includes(value.effort) ||
|
|
18
|
+
!Number.isFinite(value.expiresAt) ||
|
|
19
|
+
!/^[a-f0-9]{64}$/.test(value.routeDigest || '')) throw new Error(REFUSAL);
|
|
20
|
+
text = minimumClass === 'hard' ? 'final substantive review\n' + text : minimumClass === 'medium' ? 'review task\n' + text : text;
|
|
21
|
+
const codeFences = Math.floor((text.match(/```/g) || []).length / 2);
|
|
22
|
+
const hasCode = codeFences > 0 || /\b(function|const|let|def|class|import|=>|SELECT|async)\b/.test(text) || /[{};]\s*$/m.test(text);
|
|
23
|
+
const rank = { fast: 0, medium: 1, hard: 2 };
|
|
24
|
+
const floor = classify({ taskHints: text, hasCode }, 'claude-code');
|
|
25
|
+
if (!Object.hasOwn(rank, value.taskClass) || rank[value.taskClass] < rank[floor]) throw new Error(REFUSAL);
|
|
26
|
+
return Object.freeze({ model: value.model, effort: value.effort, taskClass: value.taskClass, expiresAt: value.expiresAt });
|
|
27
|
+
}
|
|
28
|
+
// Prompt text exists only in this bounded in-memory queue; Claude mints turnId later.
|
|
29
|
+
export function createTurnCache() {
|
|
30
|
+
const pending = [];
|
|
31
|
+
const active = new Map();
|
|
32
|
+
return {
|
|
33
|
+
enqueue(text, decision, now) {
|
|
34
|
+
while (pending.length && now - pending[0].createdAt > 300000) pending.shift();
|
|
35
|
+
if (pending.length >= 32) throw new Error(REFUSAL);
|
|
36
|
+
const entry = { text, decision, createdAt: now }; pending.push(entry); return entry;
|
|
37
|
+
},
|
|
38
|
+
remove(entry) { const index = pending.indexOf(entry); if (index >= 0) pending.splice(index, 1); },
|
|
39
|
+
pending() { return pending[0]; },
|
|
40
|
+
bind(e, now, replacement) {
|
|
41
|
+
// Never substitute the most recent prompt or the model shown in the TUI.
|
|
42
|
+
const entry = pending[0];
|
|
43
|
+
if ((!entry && !replacement) || (!replacement && entry.text !== e.text) || (entry && now - entry.createdAt > 300000) ||
|
|
44
|
+
active.has(e.turnId) || active.size >= 32) throw new Error(REFUSAL);
|
|
45
|
+
if (entry) pending.shift(); active.set(e.turnId, replacement || entry.decision);
|
|
46
|
+
},
|
|
47
|
+
get(e, now) {
|
|
48
|
+
const decision = active.get(e.turnId);
|
|
49
|
+
if (!decision || e.agentId || !Number.isSafeInteger(e.index) || e.index < 0) throw new Error(REFUSAL);
|
|
50
|
+
return decision;
|
|
51
|
+
},
|
|
52
|
+
complete(turnId) { active.delete(turnId); },
|
|
53
|
+
clear() { pending.length = 0; active.clear(); },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { On, HookStream, TurnStepChunk, TurnStepResult } from 'claude-code';
|
|
2
|
+
import { expect, test } from 'claude-code/testing';
|
|
3
|
+
const nonce = 'a'.repeat(64);
|
|
4
|
+
const digest = 'b'.repeat(64);
|
|
5
|
+
const now = 1791120000000;
|
|
6
|
+
function stubs(on: On, decisions: Record<string, unknown>[], captures: Record<string, unknown>[], fail = false) {
|
|
7
|
+
on('clock.now', () => ({ value: now }));
|
|
8
|
+
on('env.get', ($, e) => ({ value: e.name === 'RNB_CLAUDE_MOD_NONCE' ? nonce : '/tmp/receipt.json' }));
|
|
9
|
+
on('session.version', () => ({ value: { version: '2.1.289' } }));
|
|
10
|
+
on('session.id', () => ({ value: 'native-fixture-session' }));
|
|
11
|
+
on('process.run', ($, e) => {
|
|
12
|
+
if (e.argv.includes('--ready')) return { value: { exitCode: 0, stdout: JSON.stringify({nonce,status:'ready'}), stderr:'',isStdoutTruncated:false,isStderrTruncated:false } };
|
|
13
|
+
captures.push(JSON.parse(e.init!.stdin!));
|
|
14
|
+
const decision = decisions.shift();
|
|
15
|
+
if (fail) throw new Error('helper failed');
|
|
16
|
+
return { value: {exitCode:0, stdout:JSON.stringify({schemaVersion:1,subscriptionCovered:true,expiresAt:now+60000,routeDigest:digest,...decision}),stderr:'',isStdoutTruncated:false,isStderrTruncated:false} };
|
|
17
|
+
});
|
|
18
|
+
on('session.start', ($, e) => ({cwd:e.cwd}));
|
|
19
|
+
on('prompt.submit', ($, e) => { if(e.attachments) captures.push({passedAttachments:e.attachments});return {text:e.text}; });
|
|
20
|
+
on('turn.start', ($, e) => ({turnId:e.turnId}));
|
|
21
|
+
on('turn.step', async function* ($, e) {
|
|
22
|
+
captures.push({model:e.model,effort:e.effort,turnId:e.turnId,index:e.index});
|
|
23
|
+
yield {kind:'text',index:0,text:'native stream preserved'};
|
|
24
|
+
return {turnId:e.turnId,index:e.index,answer:'native stream preserved',toolUses:[],stopReason:'end_turn',usage:null};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function resultOf(stream: HookStream<TurnStepChunk, TurnStepResult>) { let result; for (;;) {const item=await stream.next();if(item.done){result=item.value;break;}} return result; }
|
|
28
|
+
test('two user turns route different models and efforts while streaming native results', async ($, on) => {
|
|
29
|
+
const captures: Record<string, unknown>[]=[];
|
|
30
|
+
stubs(on,[{model:'claude-sonnet-fixture',effort:'low',taskClass:'fast'},{model:'claude-opus-fixture',effort:'high',taskClass:'hard'}],captures);
|
|
31
|
+
await $.session.start({cwd:'/tmp',surface:'terminal',isInteractive:true});
|
|
32
|
+
for (const [turnId,text] of [['one','summarize these supplied notes'],['two','perform a security audit and prove correctness']] as const) {
|
|
33
|
+
await $.prompt.submit({text});
|
|
34
|
+
await $.turn.start({text,turnId});
|
|
35
|
+
const result=await resultOf($.turn.step({turnId,index:0,model:'wrong-default',effort:'medium',messageCount:1}));
|
|
36
|
+
expect(result.answer).toBe('native stream preserved');
|
|
37
|
+
}
|
|
38
|
+
expect(captures.filter(x=>x.turnId)).toEqual([
|
|
39
|
+
{model:'claude-sonnet-fixture',effort:'low',turnId:'one',index:0},
|
|
40
|
+
{model:'claude-opus-fixture',effort:'high',turnId:'two',index:0},
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
test('unbound model step refuses without passing to core', async ($, on) => {
|
|
44
|
+
const captures: Record<string, unknown>[]=[];stubs(on,[],captures);
|
|
45
|
+
await $.session.start({cwd:'/tmp',surface:'terminal',isInteractive:true});
|
|
46
|
+
const result=await resultOf($.turn.step({turnId:'missing',index:7,model:'wrong',messageCount:1}));
|
|
47
|
+
expect(result.stopReason).toBe('refusal');expect(result.turnId).toBe('missing');expect(result.index).toBe(7);
|
|
48
|
+
expect(captures.length).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
test('helper exception drops prompt through immediate catch', async ($, on) => {
|
|
51
|
+
const captures: Record<string, unknown>[]=[];stubs(on,[],captures,true);
|
|
52
|
+
await $.session.start({cwd:'/tmp',surface:'terminal',isInteractive:true});
|
|
53
|
+
expect('drop' in await $.prompt.submit({text:'summarize notes'})).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
test('coding uses high effort and final hook rewrite keeps original hard floor', async ($, on) => {
|
|
56
|
+
const captures: Record<string, unknown>[]=[];
|
|
57
|
+
stubs(on,[{model:'claude-sonnet-fixture',effort:'high',taskClass:'medium'},
|
|
58
|
+
{model:'claude-opus-fixture',effort:'high',taskClass:'hard'},
|
|
59
|
+
{model:'claude-opus-fixture',effort:'high',taskClass:'hard'}],captures);
|
|
60
|
+
await $.session.start({cwd:'/tmp',surface:'terminal',isInteractive:true});
|
|
61
|
+
await $.prompt.submit({text:'implement this module'});await $.turn.start({text:'implement this module',turnId:'code'});
|
|
62
|
+
await resultOf($.turn.step({turnId:'code',index:1,model:'wrong',messageCount:2}));
|
|
63
|
+
await $.prompt.submit({text:'security audit'});await $.turn.start({text:'summarize notes',turnId:'changed'});
|
|
64
|
+
await resultOf($.turn.step({turnId:'changed',index:0,model:'wrong',messageCount:2}));
|
|
65
|
+
expect(captures.some(x=>x.minimumClass==='hard')).toBe(true);
|
|
66
|
+
expect(captures.filter(x=>x.turnId).map(x=>x.effort)).toEqual(['high','high']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('invalid or non-subscription bridge replies drop before prompt reaches core', async ($, on) => {
|
|
70
|
+
const captures: Record<string, unknown>[]=[];
|
|
71
|
+
stubs(on,[{model:'claude-sonnet-fixture',effort:'low',taskClass:'fast',subscriptionCovered:false},
|
|
72
|
+
{model:'claude-sonnet-fixture',effort:'low',taskClass:'fast',routeDigest:'invalid'}],captures);
|
|
73
|
+
await $.session.start({cwd:'/tmp',surface:'terminal',isInteractive:true});
|
|
74
|
+
expect('drop' in await $.prompt.submit({text:'summarize notes'})).toBe(true);
|
|
75
|
+
expect('drop' in await $.prompt.submit({text:'summarize notes'})).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
test('image-only user prompt retains attachments and binds a conservative hard allocation', async ($, on) => {
|
|
78
|
+
const captures: Record<string, unknown>[]=[];
|
|
79
|
+
stubs(on,[{model:'claude-opus-fixture',effort:'high',taskClass:'hard'}],captures);
|
|
80
|
+
await $.session.start({cwd:'/tmp',surface:'terminal',isInteractive:true});
|
|
81
|
+
await $.prompt.submit({text:'',attachments:[{type:'image',mediaType:'image/png',filename:'fixture.png'}]});
|
|
82
|
+
await $.turn.start({text:'',turnId:'image'});
|
|
83
|
+
await resultOf($.turn.step({turnId:'image',index:0,model:'wrong',messageCount:1}));
|
|
84
|
+
expect(captures.find(x=>x.passedAttachments)?.passedAttachments).toEqual([{type:'image',mediaType:'image/png',filename:'fixture.png'}]);
|
|
85
|
+
expect(captures.filter(x=>x.turnId)[0]?.model).toBe('claude-opus-fixture');
|
|
86
|
+
});
|
|
@@ -57,10 +57,13 @@ export function classify(features, harness = features.harness || 'codex') {
|
|
|
57
57
|
const text = String(features.taskHints || '');
|
|
58
58
|
const coding = features.hasCode || CODING.test(text);
|
|
59
59
|
const assessedText = assessmentText(text);
|
|
60
|
+
const securityActions = assessedText.replace(/\bdo not (?:assess|recommend)[^.;\n]*/gi, '');
|
|
61
|
+
const securityReview = /\b(security|risks?)\b/i.test(securityActions) &&
|
|
62
|
+
/\b(review|assess(?:ment)?|audit|evaluat\w*|analy[sz]\w*|identify|determine)\b/i.test(securityActions);
|
|
60
63
|
const consequential = /\b(consequential planning|substantive planning|substantive review|final substantive review|plan (?:a |the )?new system|design (?:a |the )?new architecture|ambiguous architecture|architecture ambiguity|architectur\w* tradeoff|tightly coupled uncertain implementation|uncertain tightly coupled implementation)\b/i.test(text);
|
|
61
64
|
const architectureAmbiguity = /architectur\w*/i.test(text) && /\b(ambiguous|ambiguity|unresolved|uncertain|trade[- ]?off)\b/i.test(text);
|
|
62
65
|
const coupledUncertainty = /tightly coupled/i.test(text) && /implementation|coding/i.test(text) && /uncertain|unresolved|ambiguous/i.test(text);
|
|
63
|
-
const hardText = HARD.test(assessedText) || consequenceFloor(assessedText) || consequential || architectureAmbiguity || coupledUncertainty;
|
|
66
|
+
const hardText = HARD.test(assessedText) || securityReview || consequenceFloor(assessedText) || consequential || architectureAmbiguity || coupledUncertainty;
|
|
64
67
|
const substantialText = /\b(substantial (?:implementation|coding|feature|task)|cross-module (?:implementation|feature|refactor)|multi-file (?:implementation|feature|refactor)|end-to-end implementation|broad refactor)\b/i.test(text);
|
|
65
68
|
const facts = validateTaskFacts(features.taskFacts);
|
|
66
69
|
// Partial caller metadata supplements the assessment; it cannot lower explicit high-consequence text.
|
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.7",
|
|
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": {
|
|
7
7
|
"ruvnet-brain": "bin/install.mjs"
|
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
},
|
|
147
147
|
"dependencies": {
|
|
148
148
|
"@metaharness/flywheel": "^0.1.7",
|
|
149
|
-
"@metaharness/router": "^0.4.0"
|
|
149
|
+
"@metaharness/router": "^0.4.0",
|
|
150
|
+
"ws": "^8.22.0"
|
|
150
151
|
}
|
|
151
152
|
}
|
|
@@ -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.7",
|
|
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
|