ruvnet-brain 4.0.5 → 4.0.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.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +112 -7
  3. package/config/model-router/catalog.template.json +17 -2
  4. package/console/app.js +32 -14
  5. package/console/architecture.html +1 -1
  6. package/data/model-catalog.json +54 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/hooks/codex-hooks.json +17 -17
  11. package/plugin/hooks/hooks.json +34 -1
  12. package/plugin/scripts/codex-hook-wrapper.mjs +36 -4
  13. package/plugin/scripts/ground-ruvnet.sh +2 -1
  14. package/plugin/scripts/hook-shim.mjs +13 -6
  15. package/plugin/scripts/lesson-provenance.mjs +21 -0
  16. package/plugin/scripts/lesson-store.mjs +33 -3
  17. package/plugin/scripts/route-dispatch.sh +23 -49
  18. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  19. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  20. package/plugin/scripts/session-start-core.mjs +25 -7
  21. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  22. package/plugin/skills/release-proof/SKILL.md +4 -4
  23. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  24. package/scripts/capability-registry.mjs +3 -3
  25. package/scripts/lesson-lifecycle.mjs +2 -1
  26. package/scripts/lesson-ratify.mjs +6 -3
  27. package/scripts/lesson-seed.mjs +21 -16
  28. package/scripts/model-router-catalog.mjs +16 -0
  29. package/scripts/model-router-engine.mjs +4 -1
  30. package/scripts/onboarding-console.mjs +52 -14
  31. package/scripts/provider-availability.mjs +12 -0
  32. package/scripts/release-authority.mjs +14 -2
  33. package/scripts/release-transaction-provider.mjs +236 -0
  34. package/scripts/release-transaction.mjs +214 -0
  35. package/scripts/release.mjs +67 -279
  36. package/scripts/session-snapshot-contract.mjs +1 -0
  37. package/scripts/stabilization-receipt.mjs +1 -1
  38. package/scripts/staged-host-verifier.mjs +106 -0
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" session-start",
10
+ "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)],{input:f.readFileSync(0),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)\" 4500 session-start",
11
11
  "timeout": 5
12
12
  }
13
13
  ]
@@ -18,12 +18,12 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
21
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" ground-ruvnet",
21
+ "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)],{input:f.readFileSync(0),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)\" 9000 ground-ruvnet",
22
22
  "timeout": 10
23
23
  },
24
24
  {
25
25
  "type": "command",
26
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" unprompted-speech UserPromptSubmit",
26
+ "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)],{input:f.readFileSync(0),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)\" 9000 unprompted-speech UserPromptSubmit",
27
27
  "timeout": 10
28
28
  }
29
29
  ]
@@ -35,7 +35,7 @@
35
35
  "hooks": [
36
36
  {
37
37
  "type": "command",
38
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" hijack-ruvnet",
38
+ "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)],{input:f.readFileSync(0),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)\" 4500 hijack-ruvnet",
39
39
  "timeout": 5
40
40
  }
41
41
  ]
@@ -45,7 +45,7 @@
45
45
  "hooks": [
46
46
  {
47
47
  "type": "command",
48
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" route-dispatch",
48
+ "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)],{input:f.readFileSync(0),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)\" 4500 route-dispatch",
49
49
  "timeout": 5
50
50
  }
51
51
  ]
@@ -55,17 +55,17 @@
55
55
  "hooks": [
56
56
  {
57
57
  "type": "command",
58
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" ground-before-write",
58
+ "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)],{input:f.readFileSync(0),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)\" 4500 ground-before-write",
59
59
  "timeout": 5
60
60
  },
61
61
  {
62
62
  "type": "command",
63
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" protect-state",
63
+ "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)],{input:f.readFileSync(0),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)\" 4500 protect-state",
64
64
  "timeout": 5
65
65
  },
66
66
  {
67
67
  "type": "command",
68
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" unprompted-speech PreToolUse-write",
68
+ "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)],{input:f.readFileSync(0),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)\" 4500 unprompted-speech PreToolUse-write",
69
69
  "timeout": 5
70
70
  }
71
71
  ]
@@ -75,17 +75,17 @@
75
75
  "hooks": [
76
76
  {
77
77
  "type": "command",
78
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" verify-interface",
78
+ "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)],{input:f.readFileSync(0),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)\" 4500 verify-interface",
79
79
  "timeout": 5
80
80
  },
81
81
  {
82
82
  "type": "command",
83
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" design-wall",
83
+ "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)],{input:f.readFileSync(0),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)\" 4500 design-wall",
84
84
  "timeout": 5
85
85
  },
86
86
  {
87
87
  "type": "command",
88
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" unprompted-speech PreToolUse-bash",
88
+ "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)],{input:f.readFileSync(0),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)\" 4500 unprompted-speech PreToolUse-bash",
89
89
  "timeout": 5
90
90
  }
91
91
  ]
@@ -97,7 +97,7 @@
97
97
  "hooks": [
98
98
  {
99
99
  "type": "command",
100
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" grounding-stamp",
100
+ "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)],{input:f.readFileSync(0),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)\" 4500 grounding-stamp",
101
101
  "timeout": 5
102
102
  }
103
103
  ]
@@ -107,7 +107,7 @@
107
107
  "hooks": [
108
108
  {
109
109
  "type": "command",
110
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" learn-capture",
110
+ "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)],{input:f.readFileSync(0),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)\" 4500 learn-capture",
111
111
  "timeout": 5
112
112
  }
113
113
  ]
@@ -117,7 +117,7 @@
117
117
  "hooks": [
118
118
  {
119
119
  "type": "command",
120
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" md-stamp",
120
+ "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)],{input:f.readFileSync(0),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)\" 4500 md-stamp",
121
121
  "timeout": 5
122
122
  }
123
123
  ]
@@ -127,7 +127,7 @@
127
127
  "hooks": [
128
128
  {
129
129
  "type": "command",
130
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" signal-watch",
130
+ "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)],{input:f.readFileSync(0),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)\" 4500 signal-watch",
131
131
  "timeout": 5
132
132
  }
133
133
  ]
@@ -139,7 +139,7 @@
139
139
  "hooks": [
140
140
  {
141
141
  "type": "command",
142
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" learn-flush",
142
+ "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)],{input:f.readFileSync(0),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",
143
143
  "timeout": 3
144
144
  }
145
145
  ]
@@ -150,7 +150,7 @@
150
150
  "hooks": [
151
151
  {
152
152
  "type": "command",
153
- "command": "node \"$HOME/.cache/ruvnet-brain/codex-hook.mjs\" continuation-gate",
153
+ "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)],{input:f.readFileSync(0),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)\" 9000 continuation-gate",
154
154
  "timeout": 10
155
155
  }
156
156
  ]
@@ -23,6 +23,18 @@
23
23
  ]
24
24
  }
25
25
  ],
26
+ "PreCompact": [
27
+ {
28
+ "matcher": ".*",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-snapshot PreCompact || true",
33
+ "timeout": 5
34
+ }
35
+ ]
36
+ }
37
+ ],
26
38
  "UserPromptSubmit": [
27
39
  {
28
40
  "matcher": ".*",
@@ -61,7 +73,7 @@
61
73
  "hooks": [
62
74
  {
63
75
  "type": "command",
64
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" route-dispatch",
76
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" route-dispatch || true",
65
77
  "timeout": 5
66
78
  }
67
79
  ]
@@ -175,6 +187,16 @@
175
187
  }
176
188
  ],
177
189
  "SessionEnd": [
190
+ {
191
+ "matcher": ".*",
192
+ "hooks": [
193
+ {
194
+ "type": "command",
195
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-snapshot SessionEnd || true",
196
+ "timeout": 5
197
+ }
198
+ ]
199
+ },
178
200
  {
179
201
  "matcher": ".*",
180
202
  "hooks": [
@@ -186,6 +208,17 @@
186
208
  ]
187
209
  }
188
210
  ],
211
+ "TeammateIdle": [
212
+ {
213
+ "hooks": [
214
+ {
215
+ "type": "command",
216
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" swarm-slot-recycler",
217
+ "timeout": 5
218
+ }
219
+ ]
220
+ }
221
+ ],
189
222
  "Stop": [
190
223
  {
191
224
  "matcher": "*",
@@ -4,8 +4,27 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
 
7
- const brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
7
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
8
+ const brainHome = process.env.RUVNET_BRAIN_HOME
9
+ || path.join(path.dirname(codexHome), '.cache', 'ruvnet-brain');
8
10
  const versions = path.join(brainHome, 'versions');
11
+ const blockingHooks = new Set([
12
+ 'route-dispatch',
13
+ 'ground-before-write',
14
+ 'protect-state',
15
+ 'design-wall',
16
+ 'unprompted-speech',
17
+ ]);
18
+
19
+ function timeoutFor(hookId) {
20
+ const override = Number(process.env.RUVNET_CODEX_HOOK_TIMEOUT_MS);
21
+ if (Number.isFinite(override) && override > 0) return override;
22
+ if (hookId === 'learn-flush') return 2_250;
23
+ if (hookId === 'ground-ruvnet' || hookId === 'unprompted-speech' || hookId === 'continuation-gate') {
24
+ return 8_500;
25
+ }
26
+ return 4_000;
27
+ }
9
28
 
10
29
  function activeRoot() {
11
30
  try {
@@ -27,11 +46,24 @@ const root = activeRoot();
27
46
  const adapter = root && path.join(root, 'scripts', 'codex-hook-adapter.mjs');
28
47
  if (!adapter || !fs.existsSync(adapter)) process.exit(0);
29
48
 
49
+ const hookId = process.argv[2] || '';
30
50
  const result = spawnSync(process.execPath, [adapter, ...process.argv.slice(2)], {
31
51
  input,
32
52
  encoding: 'utf8',
33
53
  env: process.env,
54
+ timeout: timeoutFor(hookId),
55
+ killSignal: 'SIGKILL',
34
56
  });
35
- if (result.stdout) process.stdout.write(result.stdout);
36
- if (result.stderr) process.stderr.write(result.stderr);
37
- process.exit(result.status ?? 0);
57
+
58
+ // A broken optional Brain hook must never degrade the host. The only non-zero status that carries
59
+ // product meaning is an intentional exit-2 refusal from a hook whose contract is blocking.
60
+ if (result.status === 2 && blockingHooks.has(hookId)) {
61
+ if (result.stdout) process.stdout.write(result.stdout);
62
+ if (result.stderr) process.stderr.write(result.stderr);
63
+ process.exit(2);
64
+ }
65
+ if (result.status === 0) {
66
+ if (result.stdout) process.stdout.write(result.stdout);
67
+ if (result.stderr) process.stderr.write(result.stderr);
68
+ }
69
+ process.exit(0);
@@ -392,7 +392,7 @@ if [ "$DRIFT" -eq 1 ]; then
392
392
  cat <<'EOF'
393
393
  [RuvNet Brain — STOP: you're reaching for a classical default]
394
394
  You named a generic, training-prior tool. In a RuvNet workflow there's almost always a sharper rUv-native primitive — use it, don't talk the user back to the old way. Confirm the specific capability (AND any numbers) via `search_ruvnet` before you write code — do NOT assert rUv specs from memory. Direction:
395
- - vector DB / embeddings (Pinecone, pgvector, Chroma, Weaviate, FAISS, Milvus, Qdrant, hnswlib) → RuVector (.rvf single-file HNSW); branchable agent memory → agenticow; provenance cache → RuLake
395
+ - Take the wheel: vector DB / embeddings (Pinecone, pgvector, Chroma, Weaviate, FAISS, Milvus, Qdrant, hnswlib) → RuVector (.rvf single-file HNSW); branchable agent memory → agenticow; provenance cache → RuLake
396
396
  - embedding APIs (OpenAI text-embedding, Cohere, Voyage) → local ONNX MiniLM/bge via RVF (offline, free)
397
397
  - RAG / agent frameworks (LangChain, LlamaIndex, AutoGen, CrewAI, Semantic Kernel) → Ruflo + agentic-flow + FACT
398
398
  - agent memory (Redis/SQLite glue) → AgentDB · token/prompt compression → SynthLang · quantum-safe messaging → QuDAG
@@ -410,6 +410,7 @@ This is a build / change request — run THE PLAYBOOK (the standing build playbo
410
410
  - If memory looks broken, use the exact-path store → exact-key retrieve → exact SQL proof; never diagnose from semantic search, mtime, daemon startup, or a success message.
411
411
  ⛔ NO SILENT SUBSTITUTION: use the real RuvNet tool, or say out loud that you're hand-rolling and why.
412
412
  Senior partner: one plan, momentum, end with real work.
413
+ Build contract: take the wheel; use SPARC with DDD + ADR; dispatch a PARALLEL Ruflo swarm; put a QA gate between phases; for UI use frontend-design plus image generation; ask once for a missing API key; finish with a PROVEN result scored to ≥98.
413
414
  EOF
414
415
  fi
415
416
 
@@ -13,8 +13,8 @@
13
13
  // hook-shim-bash.mjs (bash-interpreter resolution, issue #38) — colocated in this same
14
14
  // boot-frozen scripts/ dir, never resolved from the spine. No imports from the hook BODY.
15
15
  // • Typed dispatch table (red-team findings 15/16/30): each hook declares its file, interpreter,
16
- // and mode. `blocking` hooks propagate their exact exit code (route-dispatch's deliberate
17
- // exit-2 wall survives by CONTRACT); `advisory` hooks can never block a turn — any failure,
16
+ // and mode. `blocking` hooks propagate their exact exit code; `advisory` hooks can never block
17
+ // a turn — any failure,
18
18
  // including a missing file, exits 0.
19
19
  // • Containment (finding 13): a codeRoot is honored ONLY if it resolves under
20
20
  // ~/.cache/ruvnet-brain/versions/ — or is the explicit dev-mode checkout declared in
@@ -71,9 +71,9 @@ catch (e) { BRAIN_OFF = !(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')); }
71
71
  //
72
72
  // 'silence' — this hook exists to advertise, ground, or learn. Off means it does not run at all
73
73
  // and writes ZERO bytes. Nothing downstream can tell it apart from not being installed.
74
- // 'run' — this is a SAFETY WALL that guards money or honesty, not retrieval. route-dispatch
75
- // stops a subagent fan-out inheriting an expensive model; design-wall stops an
76
- // ungraded surface shipping; protect-state guards the user's own consent record.
74
+ // 'run' — this protection/audit remains relevant without retrieval. route-dispatch records
75
+ // inherited-model fan-out; design-wall stops an ungraded surface shipping;
76
+ // protect-state guards the user's own consent record.
77
77
  // None becomes acceptable because retrieval is off.
78
78
  // 'partial' — the hook splits INTERNALLY. session-start-core still runs the auto-updater heartbeat, the
79
79
  // GONG health alarm and the SLA banner (an off machine must still receive fixes,
@@ -84,7 +84,9 @@ const TABLE = {
84
84
  'session-start': { file: 'session-start-core.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'partial' },
85
85
  'ground-ruvnet': { file: 'ground-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence', stdinBytes: 32768 },
86
86
  'hijack-ruvnet': { file: 'hijack-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
87
- 'route-dispatch': { file: 'route-dispatch.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
87
+ // Claude Code 2.1.220 consumes Agent/Task PreToolUse results after tool_dispatch_end (#84).
88
+ // A refusal here would be late and therefore false enforcement; retain only bounded audit.
89
+ 'route-dispatch': { file: 'route-dispatch.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'run', stdinBytes: 65536 },
88
90
  'ground-before-write': { file: 'ground-before-write.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
89
91
  'grounding-stamp': { file: 'grounding-stamp.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
90
92
  'verify-interface': { file: 'verify-interface.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
@@ -94,6 +96,7 @@ const TABLE = {
94
96
  'protect-state': { file: 'protect-brain-state.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
95
97
  'learn-capture': { file: 'learn-capture.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
96
98
  'learn-flush': { file: 'learn-flush.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
99
+ 'session-snapshot': { file: 'session-snapshot-hook.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run', stdinBytes: 65536 },
97
100
  'md-stamp': { file: 'md-stamp.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
98
101
  // THE EXTERNAL-SIGNAL WATCH PLANE, W1 OBSERVED (ADR-058 §D3; DDD-0013 Context 2). PostToolUse,
99
102
  // matcher ^Bash$ (anchored — an unanchored matcher is F3/F4). Classifies gh/vercel/netlify/npm
@@ -105,6 +108,10 @@ const TABLE = {
105
108
  // session-start-core.mjs, and that surfacing already lives under SessionStart's 'partial' contract.
106
109
  'signal-watch': { file: 'signal-watch.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
107
110
  'routing-outcome': { file: 'routing-outcome-capture.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run' },
111
+ // Claude Code's synchronous TeammateIdle boundary can prevent a worker slot from going unused
112
+ // while its host-owned shared ledger has ready work. The body is read-only and fail-open; exit 2
113
+ // is reserved for one proved, unassigned, dependency-ready task. Codex has no equivalent event.
114
+ 'swarm-slot-recycler': { file: 'swarm-slot-recycler.mjs', interpreter: 'node', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
108
115
  // The unprompted-speech chokepoint (ADR-040 / DDD-0004). ONE runtime is the sole writer of
109
116
  // user-facing bytes for every unprompted hook: it spawns the real producers (anticipate, lesson)
110
117
  // in candidate mode, applies the per-channel policy, and writes the final envelope itself. `channel`
@@ -0,0 +1,21 @@
1
+ export const SOURCE_CLASS = Object.freeze({
2
+ CURRENT_USER: 'current-user',
3
+ IMPORTED_OWNER: 'imported-owner',
4
+ MODEL_INFERRED: 'model-inferred',
5
+ DEMONSTRATION: 'demonstration',
6
+ });
7
+
8
+ export const BUNDLED_OWNER_SEED_IDS = new Set([
9
+ 'L01-verify-with-a-capable-channel',
10
+ 'L02-check-before-you-assert',
11
+ 'L03-research-before-recommending',
12
+ 'L04-never-relay-a-number',
13
+ 'L05-version-is-the-update-signal',
14
+ 'L06-use-the-real-tool',
15
+ 'L07-blast-radius-not-social-comfort',
16
+ 'L08-status-is-a-table',
17
+ 'L09-gradeable-is-not-valuable',
18
+ 'L10-under-enumeration-is-a-tell',
19
+ 'L11-retrieval-without-volition-is-broken',
20
+ 'L12-efficiency-seeking-is-the-tell',
21
+ ]);
@@ -23,6 +23,9 @@
23
23
  import fs from 'node:fs';
24
24
  import os from 'node:os';
25
25
  import path from 'node:path';
26
+ import { BUNDLED_OWNER_SEED_IDS, SOURCE_CLASS } from './lesson-provenance.mjs';
27
+
28
+ export { BUNDLED_OWNER_SEED_IDS, SOURCE_CLASS } from './lesson-provenance.mjs';
26
29
 
27
30
  /** Resolve fixture/plugin configuration without mutating the child process account HOME. */
28
31
  export function resolveConfigRoot(env = process.env, home = os.homedir()) {
@@ -99,6 +102,8 @@ export const ORIGIN = Object.freeze({
99
102
  });
100
103
  const ORIGIN_VALUES = new Set(Object.values(ORIGIN));
101
104
 
105
+ const SOURCE_CLASS_VALUES = new Set(Object.values(SOURCE_CLASS));
106
+
102
107
  /**
103
108
  * STATUS — the ratification ladder. A lesson does not become policy by existing.
104
109
  * candidate → ratified (a human agreed) → active (in force at its trigger).
@@ -128,6 +133,11 @@ export function makeLesson(spec) {
128
133
  severity = 'normal', // 'normal' | 'high' — see weightOf()
129
134
  intendedEnforcement = null, // what it should become once a human ratifies it
130
135
  ratifiedBy = null,
136
+ sourceClass = origin === ORIGIN.USER_STATED
137
+ ? SOURCE_CLASS.CURRENT_USER
138
+ : origin === ORIGIN.MODEL_INFERRED
139
+ ? SOURCE_CLASS.MODEL_INFERRED
140
+ : SOURCE_CLASS.IMPORTED_OWNER,
131
141
  } = spec;
132
142
  const err = (m) => { throw new Error(`Lesson "${id ?? '?'}" invalid: ${m}`); };
133
143
 
@@ -151,6 +161,10 @@ export function makeLesson(spec) {
151
161
  }
152
162
 
153
163
  if (!ORIGIN_VALUES.has(origin)) err(`origin must be one of: ${[...ORIGIN_VALUES].join(', ')}`);
164
+ if (!SOURCE_CLASS_VALUES.has(sourceClass)) err(`sourceClass must be one of: ${[...SOURCE_CLASS_VALUES].join(', ')}`);
165
+ if (sourceClass === SOURCE_CLASS.CURRENT_USER && origin !== ORIGIN.USER_STATED) {
166
+ err('sourceClass:current-user requires origin:user-stated');
167
+ }
154
168
  if (!STATUS_VALUES.has(status)) err(`status must be one of: ${[...STATUS_VALUES].join(', ')}`);
155
169
 
156
170
  // THE TRUST BOUNDARY. A lesson the model wrote about itself, or one imported from a repo, cannot
@@ -166,7 +180,7 @@ export function makeLesson(spec) {
166
180
 
167
181
  return Object.freeze({
168
182
  id, statement, trigger, enforcement, evidence,
169
- surface, origin, status, severity,
183
+ surface, origin, sourceClass, status, severity,
170
184
  intendedEnforcement: intendedEnforcement ?? null,
171
185
  ratifiedBy: ratifiedBy ?? null,
172
186
  projects: [...projects],
@@ -255,7 +269,12 @@ export function loadLessons(file = STORE_PATH) {
255
269
  // to edit and delete these); a malformed entry must be dropped loudly rather than acted upon.
256
270
  const out = [];
257
271
  const dropped = [];
258
- for (const l of raw.lessons || []) {
272
+ const rows = Array.isArray(raw.lessons) ? raw.lessons : [];
273
+ const ids = new Set(rows.map((lesson) => lesson?.id));
274
+ const legacyOwnerSeed = rows.length === BUNDLED_OWNER_SEED_IDS.size
275
+ && ids.size === BUNDLED_OWNER_SEED_IDS.size
276
+ && [...BUNDLED_OWNER_SEED_IDS].every((id) => ids.has(id));
277
+ for (const stored of rows) {
259
278
  // SKIP THE BAD ROW, BUT NEVER SILENTLY. An adversarial review proved that a schema change
260
279
  // (ADR-035 proposes new enforcement values the current enum rejects) would take this store
261
280
  // from 16 lessons to 0 with NO error and exit 0 — output indistinguishable from "no lessons
@@ -264,6 +283,14 @@ export function loadLessons(file = STORE_PATH) {
264
283
  //
265
284
  // A store that empties itself quietly is the worst possible failure here, because the whole
266
285
  // product promise is "you should never have to tell me twice."
286
+ const l = legacyOwnerSeed ? {
287
+ ...stored,
288
+ origin: ORIGIN.IMPORTED,
289
+ sourceClass: SOURCE_CLASS.IMPORTED_OWNER,
290
+ status: STATUS.CANDIDATE,
291
+ demoted: true,
292
+ ratifiedBy: null,
293
+ } : stored;
267
294
  try { out.push(makeLesson(l)); } catch (e) {
268
295
  dropped.push({ id: l && l.id, why: String(e && e.message || e) });
269
296
  }
@@ -435,6 +462,7 @@ export function restore(id, lessons) {
435
462
  export function ratify(id, lessons, { by = 'user' } = {}) {
436
463
  return lessons.map((l) => {
437
464
  if (l.id !== id) return l;
465
+ if (l.sourceClass === SOURCE_CLASS.IMPORTED_OWNER || l.sourceClass === SOURCE_CLASS.DEMONSTRATION) return l;
438
466
  const target = l.intendedEnforcement || l.enforcement;
439
467
  const canBlock = l.origin === ORIGIN.USER_STATED;
440
468
  return makeLesson({
@@ -448,5 +476,7 @@ export function ratify(id, lessons, { by = 'user' } = {}) {
448
476
 
449
477
  /** Lessons awaiting a human decision — what the management surface must show first. */
450
478
  export function pending(lessons) {
451
- return lessons.filter((l) => l.status === STATUS.CANDIDATE && !l.demoted);
479
+ return lessons.filter((l) => l.status === STATUS.CANDIDATE && !l.demoted
480
+ && l.sourceClass !== SOURCE_CLASS.IMPORTED_OWNER
481
+ && l.sourceClass !== SOURCE_CLASS.DEMONSTRATION);
452
482
  }
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # route-dispatch.sh — PreToolUse gate on subagent dispatch. Ends model-inheritance-by-omission.
2
+ # route-dispatch.sh — bounded PreToolUse audit of subagent model selection.
3
3
  #
4
4
  # ─────────────────────────────────────────────────────────────────────────────────────────────────
5
5
  # THE LEAK (2026-07-13). Stuart: "What happens when I'm right here in Opus 4.8 and it has 10 things
@@ -8,8 +8,12 @@
8
8
  # A SUBAGENT INHERITS THE MAIN-LOOP MODEL UNLESS `model` IS EXPLICITLY PASSED.
9
9
  #
10
10
  # Ten agents on a Fable session = ten agents at $10/$50 per Mtok, ~10x Haiku for identical mechanical
11
- # work. The router existed; the rule to use it existed; the router's ENTIRE LIFETIME OUTPUT was 3 test
12
- # pings and $0.018 saved — because the rule was ADVISORY. So this is a wall, not advice.
11
+ # work. This hook records declared and inherited dispatches so the leak remains measurable.
12
+ #
13
+ # HOST LIMITATION (#84): Claude Code 2.1.220 registers Agent/Task PreToolUse hooks asynchronously,
14
+ # completes the subagent dispatch, and only then consumes the hook result. An exit-2 refusal is
15
+ # therefore too late to block and must not be represented as enforcement. The hook is intentionally
16
+ # silent and advisory until the host provides a synchronous pre-dispatch decision boundary.
13
17
  #
14
18
  # ─────────────────────────────────────────────────────────────────────────────────────────────────
15
19
  # THREE DEFECTS IN MY OWN FIRST VERSION, caught by asking the questions Stuart would have asked
@@ -21,14 +25,10 @@
21
25
  # (a model-router profile.json exists = they answered the two subscription questions). Everyone
22
26
  # else gets NOTHING — not even a warning. Consent is the default.
23
27
  # 2. IT REQUIRED python3. The other three plugin hooks are pure bash. A hard dependency inside a
24
- # BLOCKING hook is how you brick someone's session. Now pure bash — no interpreters.
25
- # 3. IT COULD FAIL CLOSED. A blocking hook that errors must never take the session with it. Every
26
- # unparseable/ambiguous case now FAILS OPEN (exit 0). A gate that breaks your tools is worse
27
- # than the leak it prevents.
28
+ # hook is how you brick someone's session. Now pure bash — no interpreters.
29
+ # 3. IT COULD FAIL CLOSED. Every unparseable/ambiguous case fails open (exit 0).
28
30
  #
29
- # CONTRACT (verified against this machine's live hook config):
30
- # exit 0 → allow
31
- # exit 2 + stderr → BLOCK, and stderr comes back to the model as the reason (so it retries correctly)
31
+ # CONTRACT: always exit 0 and emit no user-facing bytes. Audit receipts are best-effort only.
32
32
  # ─────────────────────────────────────────────────────────────────────────────────────────────────
33
33
 
34
34
  set -uo pipefail
@@ -93,9 +93,10 @@ TOOL_USE_ID=$(field tool_use_id)
93
93
  SESSION_ID=$(field session_id)
94
94
  DESC="${DESC// /_}"; DESC="${DESC:0:40}" # builtin substitution — no `tr`, no `cut`
95
95
 
96
- if [ -n "$MODEL" ]; then
97
- # Declared. Log it so routing is AUDITABLE, not merely claimed — a growing ledger is evidence;
98
- # a promise is not. (This log is how the $0.018-lifetime failure became visible in the first place.)
96
+ log_dispatch() {
97
+ local selected_model="$1"
98
+ local enforcement="$2"
99
+ # Log so routing is AUDITABLE, not merely claimed — a growing ledger is evidence; a promise is not.
99
100
  # `date` is the ONE external command left, and only on the ALLOW path — so its absence must be
100
101
  # silent, not a stderr spew from a hook that just said "yes". (bash's printf %()T would avoid it
101
102
  # entirely, but macOS still ships bash 3.2, which does not support it.)
@@ -105,44 +106,17 @@ if [ -n "$MODEL" ]; then
105
106
  {
106
107
  TS=$(date -u +%FT%TZ) || TS="unknown" # the one external command, and only on the allow path
107
108
  mkdir -p "$HOME/.claude/metaharness"
108
- printf '{"ts":"%s","event":"dispatch","model":"%s","agent":"%s","task":"%s","toolUseId":"%s","sessionId":"%s"}\n' \
109
- "$TS" "$MODEL" "${SUBTYPE:-unknown}" "${DESC:-unlabeled}" "${TOOL_USE_ID:-}" "${SESSION_ID:-}" \
109
+ printf '{"ts":"%s","event":"dispatch","model":"%s","enforcement":"%s","agent":"%s","task":"%s","toolUseId":"%s","sessionId":"%s"}\n' \
110
+ "$TS" "$selected_model" "$enforcement" "${SUBTYPE:-unknown}" "${DESC:-unlabeled}" "${TOOL_USE_ID:-}" "${SESSION_ID:-}" \
110
111
  >> "$HOME/.claude/metaharness/dispatch-log.jsonl"
111
112
  } 2>/dev/null || true
113
+ }
114
+
115
+ if [ -n "$MODEL" ]; then
116
+ log_dispatch "$MODEL" "declared"
112
117
  exit 0
113
118
  fi
114
119
 
115
- # ── BLOCKED: no model declared → it would silently inherit the session model. ──
116
- # `read` + `printf` are BUILTINS. The original used `cat >&2 <<EOF`, which made the BLOCK path itself
117
- # depend on an external binary — the third dependency hole found in my own hook in ten minutes.
118
- read -r -d '' BLOCK_MSG <<'EOF' || true
119
- ⛔ SUBAGENT DISPATCH BLOCKED — you did not declare a `model`.
120
-
121
- An agent with no `model` INHERITS this session's model. On an Opus session that is an Opus agent;
122
- on a Fable session it is $10/$50 per Mtok — up to 10x what the same work costs on Haiku.
123
- Inheritance-by-omission is the biggest cost leak in this harness, and an advisory rule did not fix
124
- it (the router's entire first life saved $0.018). Hence a wall.
125
-
126
- Re-issue the SAME Agent call with an explicit `model`, chosen by what the task actually IS:
127
-
128
- model: "haiku" mechanical — greps, file sweeps, log triage, mechanical edits, fixture rewrites
129
- model: "sonnet" analytical — trace a bug across files, summarize a subsystem, draft tests
130
- model: "opus" judgment — architecture, root cause, security, anything user-facing
131
- (if it truly needs the main model's judgment, ask whether it should be a
132
- subagent at all, or work you should do inline)
133
-
134
- Not sure? Ask rUv's real router — it predicts each model's quality on THIS task and returns the
135
- cheapest one that clears the bar, with your subscriptions priced at $0:
136
-
137
- node ~/.claude/model-router/bin/model-router-engine.mjs --harness claude-code --prompt "<task>" --json
138
-
139
- Then log the receipt when it returns, so the saving is visible instead of asserted:
140
-
141
- node scripts/dispatch-receipt.mjs --model <m> --inherited <this session's model> \
142
- --task "<what it did>" --total-tokens <the agent's reported total>
143
-
144
- Deliberate exception (rare — and say WHY out loud): RUVNET_ALLOW_INHERITED_MODEL=1
145
- EOF
146
- bash "$(dirname "${BASH_SOURCE[0]}")/gate-receipt.sh" route-dispatch "subagent" "would inherit the session model instead of routing to a cheaper one" 2>/dev/null || true
147
- printf '%s\n' "$BLOCK_MSG" >&2
148
- exit 2
120
+ # Missing model: record the inheritance leak, but do not emit a late refusal the host cannot enforce.
121
+ log_dispatch "inherited" "advisory-host-timing"
122
+ exit 0