ruvnet-brain 3.9.134-dev → 4.0.1

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.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/README.md +2 -2
  3. package/bin/install.mjs +284 -33
  4. package/kb/zip-extract.mjs +53 -14
  5. package/package.json +7 -1
  6. package/plugin/.claude-plugin/marketplace.json +13 -0
  7. package/plugin/.claude-plugin/plugin.json +23 -0
  8. package/plugin/.codex-plugin/plugin.json +21 -0
  9. package/plugin/.mcp.json +8 -0
  10. package/plugin/commands/brain-console.md +16 -0
  11. package/plugin/commands/configure.md +32 -0
  12. package/plugin/commands/rvbc.md +78 -0
  13. package/plugin/commands/rvcb.md +16 -0
  14. package/plugin/commands/whats-new.md +57 -0
  15. package/plugin/hooks/codex-hooks.json +160 -0
  16. package/plugin/hooks/hook-contracts.json +77 -0
  17. package/plugin/hooks/hooks.json +203 -0
  18. package/plugin/mcp/server.mjs +35 -6
  19. package/plugin/scripts/anticipate.sh +534 -0
  20. package/plugin/scripts/codex-hook-adapter.mjs +96 -0
  21. package/plugin/scripts/continuation-gate.mjs +267 -0
  22. package/plugin/scripts/design-wall.sh +137 -0
  23. package/plugin/scripts/detach.mjs +168 -0
  24. package/plugin/scripts/finalize-token-meter.mjs +25 -0
  25. package/plugin/scripts/gate-receipt.sh +35 -0
  26. package/plugin/scripts/ground-before-write.sh +199 -0
  27. package/plugin/scripts/ground-ruvnet.sh +507 -0
  28. package/plugin/scripts/grounding-stamp.sh +113 -0
  29. package/plugin/scripts/grounding-substance.mjs +595 -0
  30. package/plugin/scripts/hijack-ruvnet.sh +81 -0
  31. package/plugin/scripts/hook-input.mjs +558 -0
  32. package/plugin/scripts/hook-shim-bash.mjs +55 -0
  33. package/plugin/scripts/hook-shim.mjs +303 -0
  34. package/plugin/scripts/host-update.mjs +58 -0
  35. package/plugin/scripts/kling-preflight.sh +146 -0
  36. package/plugin/scripts/learn-capture.sh +154 -0
  37. package/plugin/scripts/learn-flush.mjs +138 -0
  38. package/plugin/scripts/lesson-hooks.sh +213 -0
  39. package/plugin/scripts/md-stamp.mjs +219 -0
  40. package/plugin/scripts/protect-brain-state.sh +84 -0
  41. package/plugin/scripts/route-dispatch.sh +147 -0
  42. package/plugin/scripts/routing-outcome-capture.mjs +89 -0
  43. package/plugin/scripts/session-start.sh +868 -0
  44. package/plugin/scripts/signal-watch.mjs +193 -0
  45. package/plugin/scripts/unprompted-runtime.mjs +377 -0
  46. package/plugin/scripts/update-apply.mjs +419 -0
  47. package/plugin/scripts/verify-interface.sh +53 -0
  48. package/plugin/scripts/version-bump-gate.sh +112 -0
  49. package/plugin/skills/brain-build/SKILL.md +123 -0
  50. package/plugin/skills/brain-console/SKILL.md +20 -0
  51. package/plugin/skills/brain-prompt/SKILL.md +83 -0
  52. package/plugin/skills/brain-score/SKILL.md +101 -0
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +117 -0
  54. package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
  55. package/plugin/skills/rvbc/SKILL.md +20 -0
  56. package/plugin/skills/savings/SKILL.md +46 -0
  57. package/plugin/skills/whats-new/SKILL.md +22 -0
@@ -0,0 +1,77 @@
1
+ {
2
+ "_note": "OUT-OF-SHIM HOOK CONTRACTS (ADR-055 §6). The per-hook contract — event, matcher, layer, codeRoot, mode, offBehavior, failureBehavior, timeout, budgets, state, dependencies, reachesStrangers, owner — is carried NATIVELY by plugin/scripts/hook-shim.mjs's dispatch table for every shim-routed registration. Everything registered outside that table has nowhere to carry it, which is how the whole non-plugin two thirds of this machine's mesh ended up with no declared answer to 'what happens when the brain is off' (ADR-055 F14). This file is the other half: a checked-in contract for every registration this REPO owns but does not route through the shim. scripts/hook-registry.mjs reads both, and tests/unit/hook-registry-lint.test.mjs fails any repo-owned registration absent from both — so a new one cannot be added silently. SCOPE, deliberately: this file declares only registrations THIS repo owns (plugin/hooks/hooks.json and this project's .claude/settings.json). It does NOT declare contracts for the machine owner's ~/.claude/settings.json or for third-party plugins — those are not ours to specify, and inventing an offBehavior for someone else's hook would be exactly the fiction the lint exists to prevent. They stay visible as documented machine-local findings in the lint's expected-red block.",
3
+ "_version": 1,
4
+ "contracts": [
5
+ {
6
+ "id": "project:version-bump-gate",
7
+ "layer": "project",
8
+ "event": "PreToolUse",
9
+ "matcher": "Bash",
10
+ "commandIncludes": "version-bump-gate.sh",
11
+ "mode": "blocking",
12
+ "offBehavior": "run",
13
+ "offReason": "ADR-054's discriminator, applied literally: this is an HONESTY wall, not a retrieval feature. It refuses a commit that changes behaviour without bumping the version — the signal every user's updater reads. Nothing about that becomes acceptable because the owner switched the retrieval brain off, and the gate reads git state only: no corpus, no network, no brain.",
14
+ "failureBehavior": "fail-open — a crash or unparseable payload permits the command and records degraded health; malfunction is never a decision (ADR-055 §1.2)",
15
+ "timeoutSeconds": 5,
16
+ "warmP95BudgetMs": 50,
17
+ "coldBudgetMs": 500,
18
+ "stdoutCapBytes": 4096,
19
+ "stateRead": ["git HEAD", "plugin/.claude-plugin/plugin.json"],
20
+ "stateWritten": [],
21
+ "dependencies": ["bash", "git"],
22
+ "reachesStrangers": false,
23
+ "owner": "ruvnet-brain (this repo only — ADR-053; it enforces a rule that is true here and nowhere else)",
24
+ "note": "Project-scoped on purpose: it lived in the GLOBAL settings until 2026-07-14, taxing every Bash call in 34+ projects with a rule true in one."
25
+ }
26
+ ],
27
+ "matcherAllowlist": [
28
+ {
29
+ "layer": "plugin",
30
+ "event": "PreToolUse",
31
+ "matcher": "Write|Edit|Bash",
32
+ "handler": "hijack-ruvnet.sh",
33
+ "reason": "UNANCHORED, known (ADR-055 F4). Claude Code SEARCHES the tool name, so this also selects MultiEdit, NotebookEdit, TodoWrite and BashOutput — an advisory hook firing on four tools nobody chose. Advisory, so the blast radius is wasted latency rather than a wrong refusal.",
34
+ "retiredBy": "ADR-055 build item 3 (four plane dispatchers) — registration changes are SHELL changes and item 2 (battery v2) must land first, so this is recorded, not fixed out of order."
35
+ },
36
+ {
37
+ "layer": "plugin",
38
+ "event": "PreToolUse",
39
+ "matcher": "Bash",
40
+ "handler": "verify-interface.sh",
41
+ "reason": "UNANCHORED, known (ADR-055 F4/F12): also selects BashOutput. One of three blocking walls on the same event that each re-parse the same payload — the parse-once execution-guard is the real fix, not six anchor characters.",
42
+ "retiredBy": "ADR-055 build item 3 (execution-guard.mjs — ONE registration, ONE parse)"
43
+ },
44
+ {
45
+ "layer": "plugin",
46
+ "event": "PreToolUse",
47
+ "matcher": "Bash",
48
+ "handler": "design-wall.sh",
49
+ "reason": "UNANCHORED, known (ADR-055 F4/F12) — same pair as verify-interface above, same fix.",
50
+ "retiredBy": "ADR-055 build item 3 (execution-guard.mjs)"
51
+ },
52
+ {
53
+ "layer": "plugin",
54
+ "event": "PostToolUse",
55
+ "matcher": "Write|Edit|MultiEdit|Bash",
56
+ "handler": "learn-capture.sh",
57
+ "reason": "UNANCHORED, known (ADR-055 F4): NotebookEdit reaches the learning pipeline by substring accident, never by decision. Advisory.",
58
+ "retiredBy": "ADR-055 build item 3"
59
+ },
60
+ {
61
+ "layer": "plugin",
62
+ "event": "PostToolUse",
63
+ "matcher": "Write|Edit|MultiEdit",
64
+ "handler": "md-stamp.mjs",
65
+ "reason": "UNANCHORED, known (ADR-055 F4): same NotebookEdit substring accident. Advisory.",
66
+ "retiredBy": "ADR-055 build item 3"
67
+ },
68
+ {
69
+ "layer": "project",
70
+ "event": "PreToolUse",
71
+ "matcher": "Bash",
72
+ "handler": "version-bump-gate.sh",
73
+ "reason": "UNANCHORED, known: also selects BashOutput, which carries no command to gate, so the gate reads an empty payload and permits. Wasted spawn, not a wrong refusal.",
74
+ "retiredBy": "ADR-055 build item 3, together with the plugin-layer Bash walls it sits beside"
75
+ }
76
+ ]
77
+ }
@@ -0,0 +1,203 @@
1
+ {
2
+ "_note": "STABLE SPINE (ADR-023): every hook routes through hook-shim.mjs, which resolves the ACTIVE generation from ~/.cache/ruvnet-brain/active.json per invocation. This file is boot-frozen, so matcher, command, and dispatch-mode changes require restart classification. Advisory hooks keep `|| true` and can never block a turn. FOUR blocking hooks remain unguarded because their exit codes are their contract: route-dispatch, design-wall, unprompted-speech, and protect-state. Issue #48 demoted verify-interface to advisory-only: raw shell reconstruction may suggest the structured ruvnet_cli_help/ruvnet_cli_run boundary but can never authorize or deny execution. protect-state remains a blocking offBehavior:'run' consent guard. continuation-gate remains advisory with offBehavior:'run'.",
3
+ "_platform": "posix",
4
+ "hooks": {
5
+ "SessionStart": [
6
+ {
7
+ "matcher": "startup",
8
+ "hooks": [
9
+ {
10
+ "type": "command",
11
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-start || true",
12
+ "timeout": 5
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "matcher": "resume",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-start || true",
22
+ "timeout": 5
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "UserPromptSubmit": [
28
+ {
29
+ "matcher": ".*",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" ground-ruvnet || true",
34
+ "timeout": 10
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "matcher": "*",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" unprompted-speech UserPromptSubmit",
44
+ "timeout": 10
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "PreToolUse": [
50
+ {
51
+ "matcher": "Write|Edit|Bash",
52
+ "hooks": [
53
+ {
54
+ "type": "command",
55
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" hijack-ruvnet || true",
56
+ "timeout": 5
57
+ }
58
+ ]
59
+ },
60
+ {
61
+ "matcher": "^(Task|Agent)$",
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" route-dispatch",
66
+ "timeout": 5
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "matcher": "^(Write|Edit|MultiEdit|NotebookEdit)$",
72
+ "hooks": [
73
+ {
74
+ "type": "command",
75
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" ground-before-write || true",
76
+ "timeout": 5
77
+ },
78
+ {
79
+ "type": "command",
80
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" protect-state",
81
+ "timeout": 5
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ "matcher": "Bash",
87
+ "hooks": [
88
+ {
89
+ "type": "command",
90
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" verify-interface || true",
91
+ "timeout": 5
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "matcher": "Bash",
97
+ "hooks": [
98
+ {
99
+ "type": "command",
100
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" design-wall",
101
+ "timeout": 5
102
+ }
103
+ ]
104
+ },
105
+ {
106
+ "matcher": "^(Write|Edit|MultiEdit)$",
107
+ "hooks": [
108
+ {
109
+ "type": "command",
110
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" unprompted-speech PreToolUse-write",
111
+ "timeout": 5
112
+ }
113
+ ]
114
+ },
115
+ {
116
+ "matcher": "^Bash$",
117
+ "hooks": [
118
+ {
119
+ "type": "command",
120
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" unprompted-speech PreToolUse-bash",
121
+ "timeout": 5
122
+ }
123
+ ]
124
+ }
125
+ ],
126
+ "PostToolUse": [
127
+ {
128
+ "matcher": "^(Task|Agent)$",
129
+ "hooks": [
130
+ {
131
+ "type": "command",
132
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" routing-outcome || true",
133
+ "timeout": 5
134
+ }
135
+ ]
136
+ },
137
+ {
138
+ "matcher": "^(?:.*__)?search_ruvnet$",
139
+ "hooks": [
140
+ {
141
+ "type": "command",
142
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" grounding-stamp || true",
143
+ "timeout": 5
144
+ }
145
+ ]
146
+ },
147
+ {
148
+ "matcher": "Write|Edit|MultiEdit|Bash",
149
+ "hooks": [
150
+ {
151
+ "type": "command",
152
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-capture || true",
153
+ "timeout": 5
154
+ }
155
+ ]
156
+ },
157
+ {
158
+ "matcher": "Write|Edit|MultiEdit",
159
+ "hooks": [
160
+ {
161
+ "type": "command",
162
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" md-stamp || true",
163
+ "timeout": 5
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ "matcher": "^Bash$",
169
+ "hooks": [
170
+ {
171
+ "type": "command",
172
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" signal-watch || true",
173
+ "timeout": 5
174
+ }
175
+ ]
176
+ }
177
+ ],
178
+ "SessionEnd": [
179
+ {
180
+ "matcher": ".*",
181
+ "hooks": [
182
+ {
183
+ "type": "command",
184
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-flush || true",
185
+ "timeout": 30
186
+ }
187
+ ]
188
+ }
189
+ ],
190
+ "Stop": [
191
+ {
192
+ "matcher": "*",
193
+ "hooks": [
194
+ {
195
+ "type": "command",
196
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" continuation-gate || true",
197
+ "timeout": 10
198
+ }
199
+ ]
200
+ }
201
+ ]
202
+ }
203
+ }
@@ -37,7 +37,7 @@ import { callManagedCli, MANAGED_CLI_TOOLS } from './managed-cli-interface.mjs';
37
37
 
38
38
  const BRAIN_HOME = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
39
39
  const KB = process.env.RUVNET_BRAIN_KB || path.join(BRAIN_HOME, 'kb');
40
- const CHILD_MCP = path.join(KB, 'forge-mcp-all.mjs');
40
+ const CHILD_MCP = process.env.RUVNET_BRAIN_CHILD_MCP || path.join(KB, 'forge-mcp-all.mjs');
41
41
  const ACTIVE = path.join(BRAIN_HOME, 'active.json');
42
42
  const LEASES = path.join(BRAIN_HOME, 'leases');
43
43
  const LEASE = path.join(LEASES, `mcp-${process.pid}.json`);
@@ -95,10 +95,33 @@ let child = null; // { proc, generation, nextId, pending: Map<childId
95
95
  let pendingCount = 0; // client tools/call requests currently in flight (drain gate for swaps)
96
96
  let childRetirement = Promise.resolve();
97
97
  let childStartup = null;
98
+ let childIdleTimer = null;
98
99
  const CHILD_TERM_GRACE_MS = 3_000;
99
100
  const CHILD_INIT_TIMEOUT_MS = Number(process.env.RUVNET_BRAIN_INIT_TIMEOUT_MS) || 60_000;
101
+ const CHILD_IDLE_MS = Number(process.env.RUVNET_BRAIN_CHILD_IDLE_MS) || 30 * 60_000;
102
+
103
+ function clearChildIdleTimer() {
104
+ if (childIdleTimer) clearTimeout(childIdleTimer);
105
+ childIdleTimer = null;
106
+ }
107
+
108
+ function armChildIdleTimer(c = child) {
109
+ clearChildIdleTimer();
110
+ if (!c || CHILD_IDLE_MS <= 0) return;
111
+ childIdleTimer = setTimeout(() => {
112
+ childIdleTimer = null;
113
+ if (child !== c) return;
114
+ if (pendingCount > 0 || c.pending.size > 0 || childStartup) {
115
+ armChildIdleTimer(c);
116
+ return;
117
+ }
118
+ void killChild(`idle for ${CHILD_IDLE_MS / 1000}s`);
119
+ }, CHILD_IDLE_MS);
120
+ childIdleTimer.unref?.();
121
+ }
100
122
 
101
123
  function killChild(reason) {
124
+ clearChildIdleTimer();
102
125
  if (!child) return childRetirement;
103
126
  const c = child; child = null;
104
127
  for (const [, p] of c.pending) p.reject(new Error(`brain worker ${reason}`));
@@ -158,6 +181,7 @@ async function ensureChild() {
158
181
  await killChild('failed initialize');
159
182
  throw new Error(`brain worker failed to initialize: ${e.message}`);
160
183
  }
184
+ armChildIdleTimer(c);
161
185
  return c;
162
186
  })();
163
187
  }
@@ -191,10 +215,11 @@ async function onChildTimeout(method, timeoutMs) {
191
215
  }
192
216
  }
193
217
 
194
- // Overridable so the outage path above can actually be TESTED — a 120s default is untestable, and
195
- // an untestable failure path is how this one went four days reporting "ok" while it was down. Also
196
- // genuinely useful in the field: a slow machine can raise it, and a CI runner can lower it.
197
- const CALL_TIMEOUT_MS = Number(process.env.RUVNET_BRAIN_CALL_TIMEOUT_MS) || 120_000;
218
+ // Overridable so the outage path above can actually be TESTED. The default matches the installer's
219
+ // real cold-start smoke budget: measured 153s after a cache repair on 2026-07-29. A 120s parent
220
+ // deadline killed the model just before it became usable and forced every retry to start cold.
221
+ // Steady-state latency remains a separate release gate; this budget prevents a permanent cold loop.
222
+ const CALL_TIMEOUT_MS = Number(process.env.RUVNET_BRAIN_CALL_TIMEOUT_MS) || 240_000;
198
223
 
199
224
  function childRequest(c, method, params, timeoutMs = CALL_TIMEOUT_MS) {
200
225
  return new Promise((resolve, reject) => {
@@ -225,7 +250,10 @@ async function handleClient(msg) {
225
250
  if (c) {
226
251
  try {
227
252
  const r = await childRequest(c, 'tools/list', {}, 15_000);
228
- if (r.result?.tools?.length) return clientOk(id, { ...r.result, tools: withLocalTools(r.result.tools) });
253
+ if (r.result?.tools?.length) {
254
+ armChildIdleTimer(c);
255
+ return clientOk(id, { ...r.result, tools: withLocalTools(r.result.tools) });
256
+ }
229
257
  }
230
258
  catch { /* fall through to the static declaration */ }
231
259
  }
@@ -259,6 +287,7 @@ async function handleClient(msg) {
259
287
  return clientOk(id, { content: [{ type: 'text', text: `search_ruvnet error: ${e.message}` }], isError: true });
260
288
  } finally {
261
289
  pendingCount--;
290
+ armChildIdleTimer(c);
262
291
  }
263
292
  }
264
293
  default: