ruvnet-brain 4.3.9 → 4.3.11

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 (87) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +12 -3
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +494 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/release-proof/SKILL.md +6 -2
  36. package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
  37. package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
  38. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  39. package/scripts/build-bundle.mjs +8 -0
  40. package/scripts/build-primer.mjs +2 -3
  41. package/scripts/candidate-host-evidence.mjs +59 -41
  42. package/scripts/ci/build-fixture-kb.mjs +59 -2
  43. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  44. package/scripts/ci/stranger-scenario.mjs +13 -28
  45. package/scripts/claims-verify.mjs +219 -21
  46. package/scripts/console-engine.mjs +4 -18
  47. package/scripts/console-runtime-identity.mjs +6 -0
  48. package/scripts/development-maintenance.mjs +47 -0
  49. package/scripts/development-push-check.mjs +35 -0
  50. package/scripts/distill-project.mjs +24 -47
  51. package/scripts/doc-currency.mjs +91 -19
  52. package/scripts/git-hooks/pre-push +4 -162
  53. package/scripts/health-repair.mjs +22 -4
  54. package/scripts/hook-retirement-check.mjs +22 -0
  55. package/scripts/host-install-matrix.mjs +125 -61
  56. package/scripts/integration-evidence.mjs +11 -6
  57. package/scripts/learning-replay-fixture.mjs +23 -2
  58. package/scripts/nightly-package-observation.mjs +67 -0
  59. package/scripts/nightly-two-run-proof.mjs +489 -0
  60. package/scripts/nightly-watchdog.mjs +17 -2
  61. package/scripts/npm-invocation.mjs +20 -0
  62. package/scripts/prepublication-evidence.mjs +42 -6
  63. package/scripts/primer-grounding.mjs +20 -0
  64. package/scripts/product-integrity-contract.mjs +22 -2
  65. package/scripts/public-verification-abandon.mjs +101 -0
  66. package/scripts/public-verification-aggregate.mjs +61 -19
  67. package/scripts/public-verification-finalizer.mjs +8 -2
  68. package/scripts/public-verification-lane.mjs +64 -8
  69. package/scripts/publication-receipt.mjs +156 -50
  70. package/scripts/published-surface-probe.mjs +10 -2
  71. package/scripts/qa-contract.mjs +57 -0
  72. package/scripts/qa-lanes.mjs +40 -0
  73. package/scripts/qa-runner.mjs +54 -64
  74. package/scripts/qe/ux-suite.mjs +10 -34
  75. package/scripts/qualified-candidate-check.mjs +124 -0
  76. package/scripts/release-abort-stale.mjs +2 -2
  77. package/scripts/release-projection.mjs +20 -1
  78. package/scripts/release-qualification-contract.mjs +87 -0
  79. package/scripts/release-qualification.mjs +135 -0
  80. package/scripts/release-transaction.mjs +81 -1
  81. package/scripts/remedy-registry.mjs +6 -13
  82. package/scripts/retrieval-canary.mjs +91 -18
  83. package/scripts/selfcheck.mjs +12 -4
  84. package/scripts/snapshot-freshness.mjs +58 -0
  85. package/scripts/stack-sync.mjs +41 -43
  86. package/scripts/staged-host-verifier.mjs +63 -14
  87. package/scripts/wired-check.mjs +31 -2
@@ -1,167 +1,4 @@
1
1
  {
2
- "description": "RuvNet Brain lifecycle hooks for Codex. Every command enters through a generation-independent wrapper and adapts the shared Brain hook bodies to Codex contracts.\n\nSCHEMA WARNING, MEASURED 2026-08-14 ON A LIVE CODEX 0.147.0 SESSION: this file accepts EXACTLY two top-level keys, `description` and `hooks`. Anything else and Codex logs `warning: failed to parse hooks config ...: unknown field `X`, expected `description` or `hooks`` and DROPS THE WHOLE FILE — all 16 hooks silently stop, on a warning line nobody reads. A structured `hostParity` object was written here first and did exactly that; the record below lives in this string because this string is the only place it can live.\n\nHOST PARITY (ADR-067). PreToolUse refusal routes through decision-gate.mjs on BOTH hosts, so decision-gate.mjs REFUSAL_POLICIES is the single list of policies and neither host can gain or lose one silently. This file deliberately does NOT restate that list: it used to name policies one by one, which is exactly why identifier-preflight, degradation-watch and adr-currency (added 2026-08-13) went live on Claude Code and were unreachable on Codex — a new policy had to be remembered twice, and nothing failed when it was not.\n\nDECLARED ABSENT — swarm-slot-recycler (Claude Code event TeammateIdle). Codex 0.147.0 has no TeammateIdle hook event. Its complete event set, read from the binary rather than assumed, is pre_tool_use, permission_request, post_tool_use, pre_compact, post_compact, session_start, session_end, user_prompt_submit, subagent_start, subagent_stop, stop. There is nothing to register against.\n\nOUTPUT CONTRACT. PreToolUse/PostToolUse/SessionStart/UserPromptSubmit stdout is PARSED by Codex (\"hook returned invalid post-tool-use JSON output\"); SessionEnd has no output schema at all and PreCompact carries no additionalContext. codex-hook-adapter.mjs enforces this per event.\n\nSESSIONEND IS HARD-CAPPED AT 3s BY THE HOST, measured on a live Codex 0.147.0 session which says so itself: `warning: clamping SessionEnd hook timeout to 3s`. A larger number here is not a longer budget, it is a lie the host silently corrects. learn-flush needs 18s (measured 18342/18347/18384 ms with a real queue and real ruflo), so codex-hook-wrapper.mjs DETACHES it: the cap governs how long the host WAITS, not how long work may take.",
3
- "hooks": {
4
- "SessionStart": [
5
- {
6
- "matcher": "startup|resume|clear|compact|fork",
7
- "hooks": [
8
- {
9
- "type": "command",
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)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 session-start",
11
- "timeout": 5
12
- }
13
- ]
14
- }
15
- ],
16
- "PreCompact": [
17
- {
18
- "matcher": ".*",
19
- "hooks": [
20
- {
21
- "type": "command",
22
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 session-snapshot PreCompact",
23
- "timeout": 5
24
- }
25
- ]
26
- }
27
- ],
28
- "UserPromptSubmit": [
29
- {
30
- "hooks": [
31
- {
32
- "type": "command",
33
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 ground-ruvnet",
34
- "timeout": 10
35
- },
36
- {
37
- "type": "command",
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)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 unprompted-speech UserPromptSubmit",
39
- "timeout": 10
40
- }
41
- ]
42
- }
43
- ],
44
- "PreToolUse": [
45
- {
46
- "matcher": "^(Edit|Write|MultiEdit|apply_patch)$",
47
- "hooks": [
48
- {
49
- "type": "command",
50
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 6500 decision-gate write",
51
- "timeout": 8
52
- }
53
- ]
54
- },
55
- {
56
- "matcher": "^(Bash|exec_command|functions\\.exec_command|functions__exec_command)$",
57
- "hooks": [
58
- {
59
- "type": "command",
60
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 6500 decision-gate bash",
61
- "timeout": 8
62
- }
63
- ]
64
- },
65
- {
66
- "matcher": "^(Agent|spawn_agent)$",
67
- "hooks": [
68
- {
69
- "type": "command",
70
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 route-dispatch",
71
- "timeout": 5
72
- }
73
- ]
74
- },
75
- {
76
- "matcher": "^(Bash|exec_command|functions\\.exec_command|functions__exec_command)$",
77
- "hooks": [
78
- {
79
- "type": "command",
80
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 verify-interface",
81
- "timeout": 5
82
- }
83
- ]
84
- }
85
- ],
86
- "PostToolUse": [
87
- {
88
- "matcher": "^(?:.*__)?search_ruvnet$",
89
- "hooks": [
90
- {
91
- "type": "command",
92
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 grounding-stamp",
93
- "timeout": 5
94
- }
95
- ]
96
- },
97
- {
98
- "matcher": "^(Bash|Edit|Write|MultiEdit|exec_command|functions\\.exec_command|functions__exec_command|apply_patch)$",
99
- "hooks": [
100
- {
101
- "type": "command",
102
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 learn-capture",
103
- "timeout": 5
104
- }
105
- ]
106
- },
107
- {
108
- "matcher": "^(Edit|Write|MultiEdit|apply_patch)$",
109
- "hooks": [
110
- {
111
- "type": "command",
112
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 md-stamp",
113
- "timeout": 5
114
- }
115
- ]
116
- },
117
- {
118
- "matcher": "^(Bash|exec_command|functions\\.exec_command|functions__exec_command)$",
119
- "hooks": [
120
- {
121
- "type": "command",
122
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 signal-watch",
123
- "timeout": 5
124
- }
125
- ]
126
- },
127
- {
128
- "matcher": "^(Agent|spawn_agent)$",
129
- "hooks": [
130
- {
131
- "type": "command",
132
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 4500 routing-outcome",
133
- "timeout": 5
134
- }
135
- ]
136
- }
137
- ],
138
- "SessionEnd": [
139
- {
140
- "matcher": ".*",
141
- "hooks": [
142
- {
143
- "type": "command",
144
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 2800 session-snapshot SessionEnd",
145
- "timeout": 3
146
- },
147
- {
148
- "type": "command",
149
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 2800 learn-flush",
150
- "timeout": 3
151
- }
152
- ]
153
- }
154
- ],
155
- "Stop": [
156
- {
157
- "hooks": [
158
- {
159
- "type": "command",
160
- "command": "node -e \"const f=require('node:fs'),o=require('node:os'),p=require('node:path'),c=require('node:child_process'),d=process.env.CODEX_HOME||p.join(o.homedir(),'.codex'),b=process.env.RUVNET_BRAIN_HOME||p.join(p.dirname(d),'.cache','ruvnet-brain'),w=p.join(b,'codex-hook.mjs');let s;try{s=f.statSync(w)}catch{}if(!s?.isFile())process.exit(0);const r=c.spawnSync(process.execPath,[w,...process.argv.slice(2)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 9000 continuation-gate",
161
- "timeout": 10
162
- }
163
- ]
164
- }
165
- ]
166
- }
2
+ "description": "RuvNet Brain automatic host hooks are intentionally retired. This schema-valid empty registry is shipped so install and update converge old hook-bearing generations to zero implicit lifecycle handlers.",
3
+ "hooks": {}
167
4
  }
@@ -1,67 +1,6 @@
1
1
  {
2
- "_note": "OUT-OF-SHIM HOOK CONTRACTS (ADR-055 \u00a76). The per-hook contract \u2014 event, matcher, layer, codeRoot, mode, offBehavior, failureBehavior, timeout, budgets, state, dependencies, reachesStrangers, owner \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 a crash or unparseable payload permits the command and records degraded health; malfunction is never a decision (ADR-055 \u00a71.2)",
15
- "timeoutSeconds": 5,
16
- "warmP95BudgetMs": 50,
17
- "coldBudgetMs": 500,
18
- "stdoutCapBytes": 4096,
19
- "stateRead": [
20
- "git HEAD",
21
- "plugin/.claude-plugin/plugin.json"
22
- ],
23
- "stateWritten": [],
24
- "dependencies": [
25
- "bash",
26
- "git"
27
- ],
28
- "reachesStrangers": false,
29
- "owner": "ruvnet-brain (this repo only \u2014 ADR-053; it enforces a rule that is true here and nowhere else)",
30
- "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."
31
- }
32
- ],
33
- "matcherAllowlist": [
34
- {
35
- "layer": "plugin",
36
- "event": "PreToolUse",
37
- "matcher": "Bash",
38
- "handler": "verify-interface.sh",
39
- "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 \u2014 the parse-once execution-guard is the real fix, not six anchor characters.",
40
- "retiredBy": "ADR-055 build item 3 (execution-guard.mjs \u2014 ONE registration, ONE parse)"
41
- },
42
- {
43
- "layer": "plugin",
44
- "event": "PostToolUse",
45
- "matcher": "Write|Edit|MultiEdit|Bash",
46
- "handler": "learn-capture.sh",
47
- "reason": "UNANCHORED, known (ADR-055 F4): NotebookEdit reaches the learning pipeline by substring accident, never by decision. Advisory.",
48
- "retiredBy": "ADR-055 build item 3"
49
- },
50
- {
51
- "layer": "plugin",
52
- "event": "PostToolUse",
53
- "matcher": "Write|Edit|MultiEdit",
54
- "handler": "md-stamp.mjs",
55
- "reason": "UNANCHORED, known (ADR-055 F4): same NotebookEdit substring accident. Advisory.",
56
- "retiredBy": "ADR-055 build item 3"
57
- },
58
- {
59
- "layer": "project",
60
- "event": "PreToolUse",
61
- "matcher": "Bash",
62
- "handler": "version-bump-gate.sh",
63
- "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.",
64
- "retiredBy": "ADR-055 build item 3, together with the plugin-layer Bash walls it sits beside"
65
- }
66
- ]
2
+ "_note": "No Brain-owned automatic hook contract remains. Dormant hook bodies are retained as historical implementation material and may only be exposed through explicit, separately tested commands.",
3
+ "_version": 2,
4
+ "contracts": [],
5
+ "matcherAllowlist": []
67
6
  }
@@ -1,210 +1,4 @@
1
1
  {
2
- "description": "POSIX Stable Spine (ADR-023): every hook routes through hook-shim.mjs, which resolves the active generation per invocation. Advisory hooks fail open. ADR-067: exactly ONE hook may refuse a given tool call \u2014 decision-gate.mjs composes every policy into one decision. TIMEOUTS ARE A CONTRACT, NOT A GUESS: both decision-gate PreToolUse entries declare 5s, and the gate self-caps at DEFAULT_BUDGET_MS=2000 + MIN_HEADROOM_MS=3000, which is EXACTLY 5000ms \u2014 the relationship is the point, not the numbers. They ran at 10s for six days and that was earned at the time: an audit on 2026-08-13 timed 14 runs in unrelated projects and SIX exceeded 5s (up to 5700ms, including a plain `ls -la` at 5109ms), and because the host KILLS an over-timeout hook and renders it as a FAILED PreToolUse HOOK, the gate was producing the \"ton of hook errors\" this plugin was reported for while refusing nothing. RE-MEASURED 2026-08-19, after the adr-currency two-pass fix landed: the same full shim+gate path now runs 405-438ms in a stranger project and 412-438ms in this repo \u2014 about 10x faster \u2014 so the 10s ceiling was no longer earned by any measurement, and a ceiling nobody needs is charged to the USER, who waits behind it on every single Write and Bash. A budget is widened by evidence and must be narrowed again when the evidence goes away. The number is asserted, not asserted-to: decision-gate.mjs exports DEFAULT_BUDGET_MS + MIN_HEADROOM_MS and tests/unit/decision-gate.test.mjs reads the timeout out of THIS file and goes red if the two stop fitting. Comment keys (_note and friends) must NOT be added to this file: ADR-055 F17 records that Codex's plugin parser rejects them.",
3
- "hooks": {
4
- "SessionStart": [
5
- {
6
- "matcher": "startup",
7
- "hooks": [
8
- {
9
- "type": "command",
10
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-start || true",
11
- "timeout": 5
12
- }
13
- ]
14
- },
15
- {
16
- "matcher": "resume",
17
- "hooks": [
18
- {
19
- "type": "command",
20
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-start || true",
21
- "timeout": 5
22
- }
23
- ]
24
- },
25
- {
26
- "matcher": "clear|compact|fork",
27
- "hooks": [
28
- {
29
- "type": "command",
30
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-start || true",
31
- "timeout": 5
32
- }
33
- ]
34
- }
35
- ],
36
- "PreCompact": [
37
- {
38
- "matcher": ".*",
39
- "hooks": [
40
- {
41
- "type": "command",
42
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-snapshot PreCompact || true",
43
- "timeout": 5
44
- }
45
- ]
46
- }
47
- ],
48
- "UserPromptSubmit": [
49
- {
50
- "matcher": ".*",
51
- "hooks": [
52
- {
53
- "type": "command",
54
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" ground-ruvnet || true",
55
- "timeout": 10
56
- }
57
- ]
58
- },
59
- {
60
- "matcher": "*",
61
- "hooks": [
62
- {
63
- "type": "command",
64
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" unprompted-speech UserPromptSubmit",
65
- "timeout": 10
66
- }
67
- ]
68
- }
69
- ],
70
- "PreToolUse": [
71
- {
72
- "matcher": "^(Write|Edit|MultiEdit|NotebookEdit)$",
73
- "hooks": [
74
- {
75
- "type": "command",
76
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" decision-gate write",
77
- "timeout": 5
78
- }
79
- ]
80
- },
81
- {
82
- "matcher": "^Bash$",
83
- "hooks": [
84
- {
85
- "type": "command",
86
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" decision-gate bash",
87
- "timeout": 5
88
- }
89
- ]
90
- },
91
- {
92
- "matcher": "^(Task|Agent)$",
93
- "hooks": [
94
- {
95
- "type": "command",
96
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" route-dispatch || true",
97
- "timeout": 5
98
- }
99
- ]
100
- },
101
- {
102
- "matcher": "Bash",
103
- "hooks": [
104
- {
105
- "type": "command",
106
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" verify-interface || true",
107
- "timeout": 5
108
- }
109
- ]
110
- }
111
- ],
112
- "PostToolUse": [
113
- {
114
- "matcher": "^(Task|Agent)$",
115
- "hooks": [
116
- {
117
- "type": "command",
118
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" routing-outcome || true",
119
- "timeout": 5
120
- }
121
- ]
122
- },
123
- {
124
- "matcher": "^(?:.*__)?search_ruvnet$",
125
- "hooks": [
126
- {
127
- "type": "command",
128
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" grounding-stamp || true",
129
- "timeout": 5
130
- }
131
- ]
132
- },
133
- {
134
- "matcher": "Write|Edit|MultiEdit|Bash",
135
- "hooks": [
136
- {
137
- "type": "command",
138
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-capture || true",
139
- "timeout": 5
140
- }
141
- ]
142
- },
143
- {
144
- "matcher": "Write|Edit|MultiEdit",
145
- "hooks": [
146
- {
147
- "type": "command",
148
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" md-stamp || true",
149
- "timeout": 5
150
- }
151
- ]
152
- },
153
- {
154
- "matcher": "^Bash$",
155
- "hooks": [
156
- {
157
- "type": "command",
158
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" signal-watch || true",
159
- "timeout": 5
160
- }
161
- ]
162
- }
163
- ],
164
- "SessionEnd": [
165
- {
166
- "matcher": ".*",
167
- "hooks": [
168
- {
169
- "type": "command",
170
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" session-snapshot SessionEnd || true",
171
- "timeout": 5
172
- }
173
- ]
174
- },
175
- {
176
- "matcher": ".*",
177
- "hooks": [
178
- {
179
- "type": "command",
180
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" learn-flush || true",
181
- "timeout": 30
182
- }
183
- ]
184
- }
185
- ],
186
- "TeammateIdle": [
187
- {
188
- "hooks": [
189
- {
190
- "type": "command",
191
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" swarm-slot-recycler",
192
- "timeout": 5
193
- }
194
- ]
195
- }
196
- ],
197
- "Stop": [
198
- {
199
- "matcher": "*",
200
- "hooks": [
201
- {
202
- "type": "command",
203
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-shim.mjs\" continuation-gate || true",
204
- "timeout": 10
205
- }
206
- ]
207
- }
208
- ]
209
- }
2
+ "description": "RuvNet Brain automatic host hooks are intentionally retired. Knowledge access remains available through MCP, skills, explicit CLI commands, CI, and release proof; no lifecycle event runs Brain code implicitly.",
3
+ "hooks": {}
210
4
  }
@@ -36,7 +36,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
36
36
  const HERE = path.dirname(fileURLToPath(import.meta.url));
37
37
  const REPO = path.resolve(HERE, '..', '..');
38
38
 
39
- /** States that mean "the code moved and nobody has reconciled the document since". */
39
+ /** Finding codes within this edit gate's narrow stale-debt policy. */
40
40
  const STALE = new Set(['presumed-stale']);
41
41
 
42
42
  /**
@@ -66,7 +66,7 @@ async function loadDocCurrency(root) {
66
66
  export async function staleGovernorsOf(relPath, { root = REPO, docCurrency = null, readFile = null } = {}) {
67
67
  const mod = docCurrency ?? await loadDocCurrency(root);
68
68
  if (!mod) return [];
69
- const { listDocs, evaluateDoc, parseFrontmatter, DEFAULT_DIRS, isGitRepo } = mod;
69
+ const { listDocs, evaluateDoc, parseFrontmatter, resolveGoverned, blockingFindings, DEFAULT_DIRS, isGitRepo } = mod;
70
70
  // `readFile` is injectable for one reason, and it is not tidiness: with `fs.readFileSync` hardcoded
71
71
  // here, a test that injects `listDocs`/`parseFrontmatter` never reaches them — the read throws on a
72
72
  // fixture path that does not exist, the loop `continue`s, and NO CANDIDATE IS EVER FOUND. The first
@@ -74,38 +74,48 @@ export async function staleGovernorsOf(relPath, { root = REPO, docCurrency = nul
74
74
  // passed. A suite of only allow-cases would have shipped this green and unfireable, which is the
75
75
  // exact defect class this repo has now hit four times in one day.
76
76
  const read = readFile ?? ((p) => fs.readFileSync(p, 'utf8'));
77
- if (!isGitRepo(root)) return [];
77
+ let docs;
78
+ try {
79
+ if (!isGitRepo(root)) return [];
80
+ docs = listDocs(root, DEFAULT_DIRS);
81
+ } catch { return []; }
78
82
 
79
- // TWO PASSES, AND THE ORDER IS THE WHOLE DESIGN. Measured before writing it, not after:
80
- // `evaluateDoc` costs ~190-240ms because it shells out to git, and there are 83 documents — a
81
- // naive loop is ~19 SECONDS on the Write path, which would make this the gate everybody disables.
82
- // Reading frontmatter for all 83 costs 40ms total. So: cheap pass to find WHICH documents govern
83
- // this file (usually one), expensive pass on only those. ~250ms typical, and exactly 40ms for the
84
- // overwhelmingly common case of a file no ADR governs.
83
+ // Resolve declarations together, then evaluate only the documents governing this exact file.
84
+ // The shared resolver owns scalar/list/glob semantics; directories are not recursive governors.
85
85
  const candidates = [];
86
- for (const docRel of listDocs(root, DEFAULT_DIRS)) {
86
+ const entries = new Set();
87
+ for (const docRel of docs) {
87
88
  let fm;
88
89
  try { fm = parseFrontmatter(read(path.join(root, docRel))); } catch { continue; }
89
90
  const governs = fm?.keys?.governs;
90
- if (!Array.isArray(governs)) continue;
91
- // Entries are exact paths or directory prefixes, read the way the pre-push gate reads them.
92
- const governsThis = governs.some((p) => typeof p === 'string'
93
- && (relPath === p || relPath.startsWith(p.endsWith('/') ? p : `${p}/`)));
94
- if (governsThis) candidates.push({ docRel, id: fm?.keys?.id ?? path.basename(docRel) });
91
+ const declared = (Array.isArray(governs) ? governs : (governs ? [String(governs)] : []))
92
+ .map((entry) => String(entry).trim()).filter(Boolean);
93
+ if (!declared.length) continue;
94
+ declared.forEach((entry) => entries.add(entry));
95
+ candidates.push({ docRel, id: fm?.keys?.id ?? path.basename(docRel), declared });
95
96
  }
96
97
  if (!candidates.length) return [];
98
+ let matching;
99
+ try {
100
+ const target = relPath.split(path.sep).join('/');
101
+ matching = new Set(resolveGoverned(root, [...entries])
102
+ .filter((g) => g.resolved && g.path === target).map((g) => g.from));
103
+ } catch { return []; }
97
104
 
98
105
  const out = [];
99
106
  for (const c of candidates) {
100
- let doc;
101
- try { doc = evaluateDoc(root, c.docRel); } catch { continue; }
102
- if (STALE.has(doc?.drift?.state)) out.push({ doc: c.docRel, id: c.id, why: doc?.drift?.why ?? '' });
107
+ if (!c.declared.some((entry) => matching.has(entry))) continue;
108
+ try {
109
+ const doc = evaluateDoc(root, c.docRel);
110
+ const finding = blockingFindings([doc]).find((f) => STALE.has(f.code));
111
+ if (finding) out.push({ doc: c.docRel, id: c.id, why: finding.message });
112
+ } catch { continue; }
103
113
  }
104
114
  return out;
105
115
  }
106
116
 
107
117
  export function refusalText(relPath, stale) {
108
- const names = stale.map((s) => `${s.id} (${s.doc})`).join('\n ');
118
+ const names = stale.map((s) => `${s.id} (${s.doc})${s.why ? ` — ${s.why}` : ''}`).join('\n ');
109
119
  return `⛔ BLOCKED — ${relPath} is governed by a document that is ALREADY stale.
110
120
 
111
121
  stale: ${names}