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 +1 -1
- package/bin/install.mjs +6 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +17 -17
- package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
- package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
- package/scripts/candidate-host-evidence.mjs +49 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
- package/scripts/learning-replay-contract.mjs +14 -4
- package/scripts/learning-replay-execution.mjs +4 -3
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/publication-receipt.mjs +126 -34
- package/scripts/release-evidence-aggregate.mjs +55 -0
- package/scripts/release-payload.mjs +100 -0
- package/scripts/release-transaction-provider.mjs +233 -56
- package/scripts/release-transaction.mjs +295 -105
- package/scripts/release.mjs +41 -12
- package/scripts/selfcheck.mjs +20 -5
- package/scripts/staged-host-verifier.mjs +3 -6
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# 🧠 RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### 🧠 RuvNet Brain — [](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.
|
|
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.
|
|
4
|
+
"version": "4.0.8",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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)],{
|
|
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
|
-
|
|
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 !==
|
|
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
|
-
['
|
|
159
|
-
['
|
|
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 ['
|
|
172
|
+
for (const hostName of ['claudeOnly', 'codexOnly', 'dual']) {
|
|
169
173
|
const host = publication?.installed?.[hostName];
|
|
170
|
-
if (host?.status !== 'PASS' || host?.
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|