ruvnet-brain 4.0.7 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.0.7 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.7-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.0.8 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.8-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
10
10
 
package/bin/install.mjs CHANGED
@@ -1962,11 +1962,15 @@ async function doctor() {
1962
1962
  && !codexLifecycleGuidance(codexLifecycle).healthy
1963
1963
  && !codexLifecycleGuidance(codexLifecycle).intentional,
1964
1964
  );
1965
+ // A staged release verifier can execute the packaged hooks under Codex's explicit automation
1966
+ // trust bypass. That proves the real hook commands without pretending a fresh interactive user
1967
+ // has already reviewed them; normal end-user doctor runs remain fail-closed on pending trust.
1968
+ const codexTrustBypassed = process.env.RUVNET_CODEX_HOOK_TRUST_MODE === 'bypass';
1965
1969
  const codexWiringFailed = Boolean(cx.host && !cx.wired);
1966
1970
  const codexReadinessFailed = Boolean(codexMcp?.blocking);
1967
1971
  const failed = (hookResult ? hookResult.exitCode !== 0 : !allGreen)
1968
1972
  || groundingUnprovenPersisted
1969
- || codexLifecycleFailed
1973
+ || (codexLifecycleFailed && !codexTrustBypassed)
1970
1974
  || codexWiringFailed
1971
1975
  || codexReadinessFailed
1972
1976
  || !hostConvergence.healthy
@@ -1999,7 +2003,7 @@ async function runSelfCheck({ installState = null, quiet = false } = {}) {
1999
2003
  mod = await import(new URL('../scripts/selfcheck.mjs', import.meta.url).href);
2000
2004
  } catch (e) {
2001
2005
  warn(`self-check could not run (${e && e.message}) — this install has NOT been verified end to end`);
2002
- return { exitCode: 0, violations: [], lines: [], unavailable: true };
2006
+ return { exitCode: FLAG_DOCTOR_HOOKS ? 1 : 0, violations: [], lines: [], unavailable: true };
2003
2007
  }
2004
2008
  const result = await mod.selfCheck({ installState, security: true });
2005
2009
  if (!quiet || result.violations.length) console.log(mod.formatVerdict(result, { color: c }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 69 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.0.7",
4
+ "version": "4.0.8",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
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)],{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",
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
11
  "timeout": 5
12
12
  }
13
13
  ]
@@ -18,12 +18,12 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
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",
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)],{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",
22
22
  "timeout": 10
23
23
  },
24
24
  {
25
25
  "type": "command",
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",
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)],{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",
27
27
  "timeout": 10
28
28
  }
29
29
  ]
@@ -35,7 +35,7 @@
35
35
  "hooks": [
36
36
  {
37
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)],{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",
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)\" 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 -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",
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)],{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",
49
49
  "timeout": 5
50
50
  }
51
51
  ]
@@ -55,17 +55,17 @@
55
55
  "hooks": [
56
56
  {
57
57
  "type": "command",
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",
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)],{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 ground-before-write",
59
59
  "timeout": 5
60
60
  },
61
61
  {
62
62
  "type": "command",
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",
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)],{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 protect-state",
64
64
  "timeout": 5
65
65
  },
66
66
  {
67
67
  "type": "command",
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",
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)],{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 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 -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",
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)],{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",
79
79
  "timeout": 5
80
80
  },
81
81
  {
82
82
  "type": "command",
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",
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)],{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 design-wall",
84
84
  "timeout": 5
85
85
  },
86
86
  {
87
87
  "type": "command",
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",
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)],{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 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 -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",
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)],{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",
101
101
  "timeout": 5
102
102
  }
103
103
  ]
@@ -107,7 +107,7 @@
107
107
  "hooks": [
108
108
  {
109
109
  "type": "command",
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",
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)],{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",
111
111
  "timeout": 5
112
112
  }
113
113
  ]
@@ -117,7 +117,7 @@
117
117
  "hooks": [
118
118
  {
119
119
  "type": "command",
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",
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)],{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",
121
121
  "timeout": 5
122
122
  }
123
123
  ]
@@ -127,7 +127,7 @@
127
127
  "hooks": [
128
128
  {
129
129
  "type": "command",
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",
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)],{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",
131
131
  "timeout": 5
132
132
  }
133
133
  ]
@@ -139,7 +139,7 @@
139
139
  "hooks": [
140
140
  {
141
141
  "type": "command",
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",
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)],{stdio:['inherit','pipe','pipe'],encoding:'utf8',env:process.env,timeout:Number(process.argv[1]),killSignal:'SIGKILL'});if(r.status===0||r.status===2){if(r.stdout)process.stdout.write(r.stdout);if(r.stderr)process.stderr.write(r.stderr)}process.exit(r.status===2?2:0)\" 2500 learn-flush",
143
143
  "timeout": 3
144
144
  }
145
145
  ]
@@ -150,7 +150,7 @@
150
150
  "hooks": [
151
151
  {
152
152
  "type": "command",
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",
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)],{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",
154
154
  "timeout": 10
155
155
  }
156
156
  ]
@@ -41,7 +41,78 @@ function activeRoot() {
41
41
  }
42
42
  }
43
43
 
44
- const input = fs.readFileSync(0);
44
+ function readHookInput(limit = 1024 * 1024) {
45
+ return new Promise((resolve) => {
46
+ const chunks = [];
47
+ let bytes = 0;
48
+ let settled = false;
49
+ let idle;
50
+ let hardDeadline;
51
+ let poll;
52
+
53
+ const cleanup = () => {
54
+ clearTimeout(idle);
55
+ clearTimeout(hardDeadline);
56
+ clearInterval(poll);
57
+ process.stdin.off('data', onData);
58
+ process.stdin.off('readable', onReadable);
59
+ process.stdin.off('end', finish);
60
+ process.stdin.off('close', finish);
61
+ process.stdin.off('error', finish);
62
+ process.stdin.pause();
63
+ };
64
+ const finish = () => {
65
+ if (settled) return;
66
+ settled = true;
67
+ cleanup();
68
+ resolve(Buffer.concat(chunks));
69
+ };
70
+ const armIdle = () => {
71
+ clearTimeout(idle);
72
+ idle = setTimeout(finish, 100);
73
+ };
74
+ const onData = (chunk) => {
75
+ if (bytes < limit) {
76
+ const kept = chunk.subarray(0, limit - bytes);
77
+ chunks.push(kept);
78
+ bytes += kept.length;
79
+ }
80
+ if (bytes >= limit) return finish();
81
+ try {
82
+ JSON.parse(Buffer.concat(chunks).toString('utf8'));
83
+ return finish();
84
+ } catch {
85
+ armIdle();
86
+ }
87
+ };
88
+ // Windows inherited pipes can expose the first value through the readable interface without
89
+ // emitting a data event until the writer closes. Drain both interfaces; POSIX keeps the fast
90
+ // data path and Windows gets the same complete-value contract without waiting for EOF.
91
+ const onReadable = () => {
92
+ let chunk;
93
+ while ((chunk = process.stdin.read()) !== null) onData(chunk);
94
+ };
95
+
96
+ process.stdin.on('data', onData);
97
+ process.stdin.on('readable', onReadable);
98
+ process.stdin.once('end', finish);
99
+ process.stdin.once('close', finish);
100
+ process.stdin.once('error', finish);
101
+ // Some Windows pipe handles can deliver repeated readable notifications while the writer
102
+ // remains open, continually resetting an idle timer. The host contract is one JSON value, not
103
+ // EOF; an absolute cap makes the wrapper deterministic under that regime without weakening
104
+ // the normal complete-value fast path.
105
+ // Some Windows inherited pipes do not emit a readable/data notification until the writer
106
+ // closes. Poll the non-blocking stream read instead of using fs.readSync, which can block the
107
+ // event loop before the first pipe chunk is surfaced.
108
+ poll = setInterval(onReadable, 10);
109
+ hardDeadline = setTimeout(finish, 250);
110
+ armIdle();
111
+ process.stdin.resume();
112
+ });
113
+ }
114
+
115
+ const input = await readHookInput();
45
116
  const root = activeRoot();
46
117
  const adapter = root && path.join(root, 'scripts', 'codex-hook-adapter.mjs');
47
118
  if (!adapter || !fs.existsSync(adapter)) process.exit(0);
@@ -145,8 +145,11 @@ export function evaluatePublicationReceipt(candidate, publication) {
145
145
  const failures = [...candidateResult.failures];
146
146
  const sha = candidateResult.sha;
147
147
  const digest = candidateResult.artifactSha256;
148
- if (publication?.schemaVersion !== 1 || publication?.phase !== 'publication') failures.push(fail('INVALID_PUBLICATION_RECEIPT', 'schemaVersion=1 and phase=publication are required'));
148
+ if (publication?.schemaVersion !== 2 || publication?.phase !== 'publication') failures.push(fail('INVALID_PUBLICATION_RECEIPT', 'schemaVersion=2 and phase=publication are required'));
149
149
  if (publication?.sha !== sha || publication?.artifactSha256 !== digest) failures.push(fail('PUBLICATION_SEAL_MISMATCH', 'publication does not reference candidate seal'));
150
+ if (!cleanHex(publication?.payloadId, 64) || !cleanHex(publication?.bundleArtifactSha256, 64)) {
151
+ failures.push(fail('PUBLIC_PAYLOAD_BINDING_MISSING', 'signed payload and public bundle digests are required'));
152
+ }
150
153
  const version = versionField(candidate?.version);
151
154
  const tag = version ? `v${version}` : null;
152
155
  const identityMismatches = versionIdentityFailures(version, candidate?.tag, [
@@ -155,8 +158,9 @@ export function evaluatePublicationReceipt(candidate, publication) {
155
158
  ['GitHub release tag', publication?.githubRelease?.tag, tag],
156
159
  ['bundle brainVersion', publication?.bundle?.brainVersion],
157
160
  ['bundle releaseTag', publication?.bundle?.releaseTag, tag],
158
- ['installed Claude host', publication?.installed?.claude?.version],
159
- ['installed Codex host', publication?.installed?.codex?.version],
161
+ ['Claude-only host', publication?.installed?.claudeOnly?.version],
162
+ ['Codex-only host', publication?.installed?.codexOnly?.version],
163
+ ['dual host', publication?.installed?.dual?.version],
160
164
  ]);
161
165
  if (identityMismatches.length > 0) {
162
166
  failures.push(fail('PUBLIC_VERSION_IDENTITY_MISMATCH', `public surfaces must identify candidate ${version ?? 'UNKNOWN'}: ${identityMismatches.join(', ')}`));
@@ -165,9 +169,12 @@ export function evaluatePublicationReceipt(candidate, publication) {
165
169
  const item = publication?.[surface];
166
170
  if (item?.sha !== sha || item?.artifactSha256 !== digest) failures.push(fail('PUBLIC_ARTIFACT_MISMATCH', `${surface} differs from candidate seal`));
167
171
  }
168
- for (const hostName of ['claude', 'codex']) {
172
+ for (const hostName of ['claudeOnly', 'codexOnly', 'dual']) {
169
173
  const host = publication?.installed?.[hostName];
170
- if (host?.status !== 'PASS' || host?.artifactSha256 !== digest) failures.push(fail('PUBLIC_HOST_NOT_PASS', `${hostName} is not running sealed public artifact`));
174
+ if (host?.status !== 'PASS' || host?.doctorExit !== 0 || host?.artifactSha256 !== digest
175
+ || host?.functionalSearch !== true || !(host?.searchMs <= Number(publication?.brain?.deadlineMs) * 0.8)) {
176
+ failures.push(fail('PUBLIC_HOST_NOT_PASS', `${hostName} is not running a doctor-clean functional sealed public artifact`));
177
+ }
171
178
  }
172
179
  const brain = publication?.brain || {};
173
180
  if (brain.status !== 'PASS' || brain.selfStore !== true || !(brain.broadMs <= Number(brain.deadlineMs) * 0.8)) failures.push(fail('PUBLIC_BRAIN_NOT_PASS', 'public installed Brain acceptance failed'));
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { stagedHostVerifier } from './staged-host-verifier.mjs';
6
+ import { payloadIdFor } from './release-payload.mjs';
7
+
8
+ const arg = (name) => {
9
+ const index = process.argv.indexOf(name);
10
+ return index >= 0 ? process.argv[index + 1] : null;
11
+ };
12
+ const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
13
+
14
+ const manifestPath = path.resolve(arg('--manifest'));
15
+ const packagePath = path.resolve(arg('--package'));
16
+ const bundlePath = path.resolve(arg('--bundle'));
17
+ const out = path.resolve(arg('--out'));
18
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
19
+ const payloadId = payloadIdFor(manifest);
20
+ const identity = { version: manifest.version, candidateSha: manifest.candidateSha, payloadId };
21
+ const result = await stagedHostVerifier({ assets: { packagePath, bundlePath }, identity })
22
+ .verify({ source: 'candidate', assets: { packagePath, bundlePath } });
23
+ if (result.verdict !== 'PASS') throw new Error(`candidate host matrix failed: ${result.error || 'unknown'}`);
24
+
25
+ const modeNames = { claude: 'claude-only', codex: 'codex-only', dual: 'dual-host' };
26
+ const leaves = Object.entries(modeNames).map(([mode, name]) => {
27
+ const fixture = result.fixtures?.[mode];
28
+ if (fixture?.status !== 'PASS' || fixture?.doctorExit !== 0) throw new Error(`${name} did not produce a clean doctor receipt`);
29
+ return {
30
+ name,
31
+ sha: manifest.candidateSha,
32
+ payloadId,
33
+ status: fixture.status === 'PASS' ? 'completed' : 'failed',
34
+ conclusion: 'success',
35
+ verdict: 'PASS',
36
+ source: 'candidate-host-evidence',
37
+ mode,
38
+ doctorExit: fixture.doctorExit,
39
+ artifactSha256: sha256(packagePath),
40
+ };
41
+ });
42
+ fs.writeFileSync(out, `${JSON.stringify({
43
+ schemaVersion: 1,
44
+ sha: manifest.candidateSha,
45
+ payloadId,
46
+ artifactSha256: sha256(packagePath),
47
+ leaves,
48
+ }, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
49
+ console.log(JSON.stringify({ verdict: 'PASS', payloadId, leaves: leaves.map(({ name }) => name) }));
@@ -23,8 +23,20 @@ const probe = process.env.RUVNET_REPLAY_LESSON_PROBE || '';
23
23
  if (event === 'PreToolUse') {
24
24
  const recorder = process.env.RUVNET_REPLAY_RECORDER || '';
25
25
  if (!recorder) process.exit(2);
26
+ // Codex emits exec_command with tool_input.cmd; the shared Claude/Ruflo boundary consumes
27
+ // Bash with tool_input.command. Normalize the real host envelope before the recorder parses it.
28
+ const recorderInput = Buffer.from(JSON.stringify({
29
+ ...input,
30
+ tool_name: /^(?:functions[._]{1,2})?exec_command$/.test(String(input.tool_name || ''))
31
+ ? 'Bash'
32
+ : input.tool_name,
33
+ tool_input: {
34
+ ...(input.tool_input || {}),
35
+ command: input.tool_input?.command ?? input.tool_input?.cmd ?? input.command ?? '',
36
+ },
37
+ }));
26
38
  const result = spawnSync(process.execPath, [recorder, attemptsFile, sequenceFile], {
27
- input: raw,
39
+ input: recorderInput,
28
40
  encoding: 'utf8',
29
41
  env: process.env,
30
42
  });
@@ -59,6 +59,16 @@ export function trapSpec(id = TRAP.MEMORY_SEARCH) {
59
59
  };
60
60
  }
61
61
 
62
+ // Codex commonly prints the global binary as $HOME/.npm-global/bin/ruflo. That is a real shell
63
+ // executable path, not prose, but the shared command parser intentionally does not expand shell
64
+ // variables. Canonicalize only an executable-position prefix; never rewrite quoted mentions.
65
+ export function normalizeRufloExecutable(command) {
66
+ return String(command || '').replace(
67
+ /(^|(?:&&|\|\||[;|()\n])\s*)\$(?:HOME|\{HOME\})\/\.npm-global\/bin\/(ruflo|claude-flow)(?=\s|$)/g,
68
+ '$1$2',
69
+ );
70
+ }
71
+
62
72
  export const LOAD_BEARING = Object.freeze([
63
73
  'scripts/learning-replay.mjs',
64
74
  'scripts/learning-replay-contract.mjs',
@@ -80,7 +90,7 @@ export const LOAD_BEARING = Object.freeze([
80
90
  ]);
81
91
 
82
92
  export function classifyCommand(cmd) {
83
- const invocations = findInvocations(String(cmd || ''), ['ruflo', 'claude-flow']);
93
+ const invocations = findInvocations(normalizeRufloExecutable(cmd), ['ruflo', 'claude-flow']);
84
94
  if (!invocations.length) return 'none';
85
95
  let sawPositional = false;
86
96
  for (const inv of invocations) {
@@ -102,7 +112,7 @@ export function classifyCommand(cmd) {
102
112
  }
103
113
 
104
114
  export function subcommandCorrect(cmd) {
105
- for (const inv of findInvocations(String(cmd || ''), ['ruflo', 'claude-flow'])) {
115
+ for (const inv of findInvocations(normalizeRufloExecutable(cmd), ['ruflo', 'claude-flow'])) {
106
116
  const words = inv.args.filter((a) => a !== '' && !a.startsWith('-'));
107
117
  const memory = words.indexOf('memory');
108
118
  if (memory !== -1 && words[memory + 1] === 'search') return true;
@@ -124,7 +134,7 @@ export function optionValue(args, short, long) {
124
134
  }
125
135
 
126
136
  export function classifyPostTaskCommand(cmd) {
127
- const invocations = findInvocations(String(cmd || ''), ['ruflo', 'claude-flow']);
137
+ const invocations = findInvocations(normalizeRufloExecutable(cmd), ['ruflo', 'claude-flow']);
128
138
  if (!invocations.length) return 'none';
129
139
  let sawPostTask = false;
130
140
  for (const inv of invocations) {
@@ -142,7 +152,7 @@ export function classifyPostTaskCommand(cmd) {
142
152
  }
143
153
 
144
154
  export function postTaskSubcommandCorrect(cmd) {
145
- return findInvocations(String(cmd || ''), ['ruflo', 'claude-flow']).some((inv) => {
155
+ return findInvocations(normalizeRufloExecutable(cmd), ['ruflo', 'claude-flow']).some((inv) => {
146
156
  const words = inv.args.filter((a) => a !== '' && !a.startsWith('-'));
147
157
  const hooks = words.indexOf('hooks');
148
158
  return hooks !== -1 && words[hooks + 1] === 'post-task';
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { commandNodes, findInvocations } from '../plugin/scripts/hook-input.mjs';
6
- import { optionValue, TRAP } from './learning-replay-contract.mjs';
6
+ import { normalizeRufloExecutable, optionValue, TRAP } from './learning-replay-contract.mjs';
7
7
 
8
8
  export const PROJECT_B_MEMORY_KEY = 'note-caching-strategy';
9
9
  export const PROJECT_B_MEMORY_VALUE =
@@ -90,9 +90,10 @@ export function executeProducedCommand(cmd, {
90
90
  output: '',
91
91
  ...extra,
92
92
  });
93
- const invocations = findInvocations(String(cmd || ''), ['ruflo', 'claude-flow']);
93
+ const normalized = normalizeRufloExecutable(cmd);
94
+ const invocations = findInvocations(normalized, ['ruflo', 'claude-flow']);
94
95
  if (!invocations.length) return reject('no Ruflo invocation to execute');
95
- const firstExecutable = path.basename(commandNodes(String(cmd || ''))[0]?.exe || '').split('@')[0];
96
+ const firstExecutable = path.basename(commandNodes(normalized)[0]?.exe || '').split('@')[0];
96
97
  if (invocations.length !== 1 || firstExecutable !== 'ruflo') {
97
98
  return reject('corrective command was not the first and only Ruflo invocation');
98
99
  }
@@ -48,6 +48,16 @@ function initMemoryDb(ruflo, db, cwd) {
48
48
  return sh(ruflo, ['memory', 'init', '--path', db, '--backend', 'hybrid'], { cwd });
49
49
  }
50
50
 
51
+ function retrieveExact(ruflo, db, cwd, key, expected) {
52
+ const result = sh(ruflo, [
53
+ 'memory', 'retrieve', '-k', key, '-n', 'default', '--value-only', '--path', db,
54
+ ], { cwd });
55
+ return {
56
+ exit: result.status,
57
+ matched: result.status === 0 && String(result.stdout || '').includes(expected),
58
+ };
59
+ }
60
+
51
61
  export function buildFixtures(baseDir) {
52
62
  fs.mkdirSync(baseDir, { recursive: true });
53
63
  const dirs = {
@@ -90,6 +100,7 @@ export function recordInProjectA(dirs, {
90
100
  'memory', 'search', '-q', spec.recordQuery,
91
101
  '-n', 'default', '--path', db, '-t', 'keyword',
92
102
  ], { cwd: project });
103
+ const exact = retrieveExact(ruflo, db, project, key, spec.lesson);
93
104
  return {
94
105
  project: path.basename(project),
95
106
  db,
@@ -97,7 +108,8 @@ export function recordInProjectA(dirs, {
97
108
  initExit: init.status,
98
109
  storeExit: store.status,
99
110
  readBackExit: readBack.status,
100
- recorded: fs.existsSync(db),
111
+ exactReadExit: exact.exit,
112
+ recorded: exact.matched,
101
113
  };
102
114
  });
103
115
  const lesson = makeLesson({
@@ -138,12 +150,14 @@ export function seedProjectBMemory(dirs, { ruflo = RUFLO_BIN } = {}) {
138
150
  'memory', 'store', '-k', PROJECT_B_MEMORY_KEY,
139
151
  '--value', PROJECT_B_MEMORY_VALUE, '-n', 'default', '--path', db,
140
152
  ], { cwd: dirs.projectB });
153
+ const exact = retrieveExact(ruflo, db, dirs.projectB, PROJECT_B_MEMORY_KEY, PROJECT_B_MEMORY_VALUE);
141
154
  return {
142
155
  db,
143
156
  key: PROJECT_B_MEMORY_KEY,
144
157
  initExit: init.status,
145
158
  storeExit: store.status,
146
- ok: init.status === 0 && store.status === 0 && fs.existsSync(db),
159
+ exactReadExit: exact.exit,
160
+ ok: init.status === 0 && store.status === 0 && exact.matched,
147
161
  };
148
162
  }
149
163
 
@@ -156,6 +170,13 @@ export function nightlyRefresh(dirs, { ruflo = RUFLO_BIN } = {}) {
156
170
  path.join(ROOT, 'scripts', 'ci', 'learning-replay-codex-adapter.mjs'),
157
171
  path.join(versionDir, 'scripts', 'codex-hook-adapter.mjs'),
158
172
  );
173
+ // Codex registrations call this stable, generation-independent wrapper. A fixture that only
174
+ // creates active.json silently falls back to the user's real Brain home and never records the
175
+ // trap's PreToolUse command, making an expensive replay unmeasurable.
176
+ fs.copyFileSync(
177
+ path.join(ROOT, 'plugin', 'scripts', 'codex-hook-wrapper.mjs'),
178
+ path.join(dirs.brainHome, 'codex-hook.mjs'),
179
+ );
159
180
  fs.writeFileSync(path.join(dirs.brainHome, 'active.json'), JSON.stringify({
160
181
  codeRoot: versionDir,
161
182
  generation,