ruvnet-brain 4.7.4 → 4.7.6
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/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/agentdb-recall-reader.mjs +3 -2
- package/plugin/scripts/ruflo-bin.mjs +78 -2
- package/plugin/scripts/ruflo-memory-hooks.mjs +7 -4
- package/scripts/release-qualification-contract.mjs +9 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
9
9
|
|
|
10
10
|
### Discover the stack. Read the source. Build with confidence.
|
|
11
11
|
|
|
12
|
-
[](plugin/.claude-plugin/plugin.json)
|
|
13
13
|
|
|
14
14
|
**A source-grounded companion for Claude Code and Codex that helps you find and use Reuven Cohen's (rUv's) RuvNet ecosystem.**
|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.6",
|
|
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 77 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 a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.7.
|
|
4
|
+
"version": "4.7.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* so a stalled CLI is retired at the recall deadline. Output contract unchanged: {ok, value}.
|
|
4
4
|
*/
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
-
import { resolveRuflo } from './ruflo-bin.mjs';
|
|
6
|
+
import { resolveRuflo, rufloInvocationOrPlain } from './ruflo-bin.mjs';
|
|
7
7
|
|
|
8
8
|
const call = (bin, args, deadline) => new Promise((resolve) => {
|
|
9
9
|
const timeout = Math.max(1, deadline - Date.now());
|
|
10
|
-
|
|
10
|
+
const inv = rufloInvocationOrPlain(bin, args, { env: process.env });
|
|
11
|
+
execFile(inv.executable, inv.args, { timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } },
|
|
11
12
|
(error, stdout) => resolve(error ? null : String(stdout || '')));
|
|
12
13
|
});
|
|
13
14
|
|
|
@@ -36,14 +36,90 @@
|
|
|
36
36
|
* Rule 21 is untouched by this: still ONE ruflo, still the global one, never `npx ruflo@latest`. This
|
|
37
37
|
* resolves WHERE that one global binary is rather than assuming everyone's prefix matches the owner's.
|
|
38
38
|
*/
|
|
39
|
+
import { spawnSync } from 'node:child_process';
|
|
39
40
|
import fs from 'node:fs';
|
|
40
41
|
import os from 'node:os';
|
|
41
42
|
import path from 'node:path';
|
|
42
43
|
|
|
44
|
+
// WHICH NODE RUNS RUFLO (measured 2026-10-14, ruflo 3.56.9 / better-sqlite3 12.11.1). `ruflo` is
|
|
45
|
+
// `#!/usr/bin/env node`, so the interpreter is whatever is FIRST on the CALLER's PATH — and an
|
|
46
|
+
// installed better-sqlite3 holds ONE native ABI (this machine's was built for node 24). A hook
|
|
47
|
+
// launched with PATH=/opt/homebrew/bin:… got node 26, and `memory store` was refused with
|
|
48
|
+
// "Native memory writer required; refusing whole-image sql.js fallback": 67 failed captures in a day,
|
|
49
|
+
// each one a lost turn record. Same command, only PATH changed: node 26 first → refused, node 24 first
|
|
50
|
+
// → stored. Upstream's guard (ruflo#2219) covers shipping prebuilds for Node 20–26, not one install
|
|
51
|
+
// being run under several nodes, so the choice belongs here, in the one resolver every caller shares.
|
|
52
|
+
//
|
|
53
|
+
// The test is the load itself — open an in-memory database through the SAME better-sqlite3 ruflo
|
|
54
|
+
// will use — never a version string. Healthy machines are untouched: when the PATH node loads the
|
|
55
|
+
// binding, this returns null and the invocation stays exactly `ruflo …`.
|
|
56
|
+
function defaultNodeCandidates(env) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const dir of String(env.PATH || '').split(path.delimiter)) if (dir) out.push(path.join(dir, 'node'));
|
|
59
|
+
out.push(process.execPath);
|
|
60
|
+
try {
|
|
61
|
+
const opt = '/opt/homebrew/opt';
|
|
62
|
+
for (const name of fs.readdirSync(opt).filter((n) => /^node(@\d+)?$/.test(n)).sort().reverse()) {
|
|
63
|
+
out.push(path.join(opt, name, 'bin', 'node'));
|
|
64
|
+
}
|
|
65
|
+
} catch { /* no Homebrew opt dir */ }
|
|
66
|
+
out.push('/opt/homebrew/bin/node', '/usr/local/bin/node');
|
|
67
|
+
return [...new Set(out)].filter((c) => { try { return fs.statSync(c).isFile(); } catch { return false; } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const nativeNodeCache = new Map();
|
|
71
|
+
const PROBE_TIMEOUT_MS = 2000;
|
|
72
|
+
const PROBE_BUDGET_MS = 3000;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* rufloInvocation for callers that must never throw: an unrecognised shim keeps the plain `bin args` they
|
|
76
|
+
* always ran, so routing a call site through node selection can only add the correct interpreter.
|
|
77
|
+
*/
|
|
78
|
+
export function rufloInvocationOrPlain(binary, args, opts) {
|
|
79
|
+
try { return rufloInvocation(binary, args, opts); } catch { return { executable: binary, args }; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The node that can load ruflo's native better-sqlite3, or null when the caller's own PATH node
|
|
84
|
+
* already can, when ruflo is not a node package we can inspect, or when no candidate can.
|
|
85
|
+
* Null always means "leave the invocation exactly as it was" — this never makes anything worse.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} binary path to the ruflo bin (symlink or script)
|
|
88
|
+
* @param {{ env?: Record<string,string|undefined>, candidates?: string[] }} [opts]
|
|
89
|
+
* @returns {{ node: string, entry: string }|null}
|
|
90
|
+
*/
|
|
91
|
+
export function nativeNodeFor(binary, { env = process.env, candidates } = {}) {
|
|
92
|
+
let entry;
|
|
93
|
+
try { entry = fs.realpathSync(binary); } catch { return null; }
|
|
94
|
+
const addon = path.join(path.dirname(path.dirname(entry)), 'node_modules', 'better-sqlite3');
|
|
95
|
+
try { if (!fs.statSync(addon).isDirectory()) return null; } catch { return null; }
|
|
96
|
+
|
|
97
|
+
const list = candidates ?? defaultNodeCandidates(env);
|
|
98
|
+
const cacheKey = `${entry}\0${list.join('\0')}`;
|
|
99
|
+
if (nativeNodeCache.has(cacheKey)) return nativeNodeCache.get(cacheKey);
|
|
100
|
+
|
|
101
|
+
const probe = `new (require(${JSON.stringify(addon)}))(':memory:').close()`;
|
|
102
|
+
let chosen = null;
|
|
103
|
+
// Bounded: this runs inside hooks that have their own deadline (SessionStart is stopped 1.5 s early), so a
|
|
104
|
+
// wedged candidate must cost seconds at most and then leave the invocation exactly as it was.
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
107
|
+
if (Date.now() - started > PROBE_BUDGET_MS) break;
|
|
108
|
+
const r = spawnSync(list[i], ['-e', probe], { stdio: 'ignore', timeout: PROBE_TIMEOUT_MS, windowsHide: true });
|
|
109
|
+
if (r.status === 0) { chosen = i === 0 ? null : { node: list[i], entry }; break; }
|
|
110
|
+
}
|
|
111
|
+
nativeNodeCache.set(cacheKey, chosen);
|
|
112
|
+
return chosen;
|
|
113
|
+
}
|
|
114
|
+
|
|
43
115
|
// npm's Windows .cmd shim cannot be spawned without a shell. Resolve the declared entry
|
|
44
116
|
// inside that same managed installation and pass argv directly to Node instead.
|
|
45
|
-
export function rufloInvocation(binary, args, { platform = process.platform } = {}) {
|
|
46
|
-
if (platform !== 'win32'
|
|
117
|
+
export function rufloInvocation(binary, args, { platform = process.platform, env = process.env, nodeCandidates } = {}) {
|
|
118
|
+
if (platform !== 'win32') {
|
|
119
|
+
const native = nativeNodeFor(binary, { env, candidates: nodeCandidates });
|
|
120
|
+
return native ? { executable: native.node, args: [native.entry, ...args] } : { executable: binary, args };
|
|
121
|
+
}
|
|
122
|
+
if (!/\.cmd$/i.test(binary)) return { executable: binary, args };
|
|
47
123
|
if (path.basename(binary).toLowerCase() !== 'ruflo.cmd') throw new Error('unsupported managed Ruflo shim');
|
|
48
124
|
const packageRoot = fs.realpathSync(path.join(path.dirname(fs.realpathSync(binary)), 'node_modules', 'ruflo'));
|
|
49
125
|
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
@@ -18,7 +18,7 @@ import crypto from 'node:crypto';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { execFile, execFileSync, spawnSync } from 'node:child_process';
|
|
20
20
|
import os from 'node:os';
|
|
21
|
-
import { resolveRuflo, rufloRunDir, rufloScratchRoot } from './ruflo-bin.mjs';
|
|
21
|
+
import { resolveRuflo, rufloInvocationOrPlain, rufloRunDir, rufloScratchRoot } from './ruflo-bin.mjs';
|
|
22
22
|
import { resolveProjectStore } from './project-store-resolver.mjs';
|
|
23
23
|
import { checkpointNamespaces, newestOfFamily } from './checkpoint-keys.mjs';
|
|
24
24
|
|
|
@@ -57,7 +57,8 @@ export function rufloMemory(args, { db, env = process.env, bin = resolveRuflo({
|
|
|
57
57
|
return new Promise((resolve) => {
|
|
58
58
|
if (!bin) return resolve({ ok: false, stdout: '', error: 'global Ruflo unavailable' });
|
|
59
59
|
const spawnAt = rufloSpawnOptions(db, env);
|
|
60
|
-
|
|
60
|
+
const inv = rufloInvocationOrPlain(bin, ['memory', ...args, '--path', db], { env: spawnAt.env });
|
|
61
|
+
execFile(inv.executable, inv.args, { cwd: spawnAt.cwd, env: spawnAt.env, timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 },
|
|
61
62
|
// The ledger gets a fixed reason, never execFile's message (it embeds the whole argv: value, db path).
|
|
62
63
|
(error, stdout) => { spawnAt.cleanup(); resolve({ ok: !error, stdout: String(stdout || ''), error: error ? (error.killed ? 'timed out' : `ruflo memory ${args[0]} exited ${error.code ?? 'abnormally'}`) : null }); });
|
|
63
64
|
});
|
|
@@ -194,11 +195,13 @@ export function storeExact({ db, namespace, key, value, env = process.env, bin =
|
|
|
194
195
|
const spawnAt = rufloSpawnOptions(db, env);
|
|
195
196
|
const options = { encoding: 'utf8', timeout: timeoutMs, killSignal: 'SIGKILL', maxBuffer: 8 * 1024 * 1024, cwd: spawnAt.cwd, env: spawnAt.env };
|
|
196
197
|
try {
|
|
197
|
-
const
|
|
198
|
+
const storeInv = rufloInvocationOrPlain(bin, ['memory', 'store', '--key', key, '--value', value, '--namespace', namespace, '--path', db], { env: spawnAt.env });
|
|
199
|
+
const stored = run(storeInv.executable, storeInv.args, options);
|
|
198
200
|
if (stored.error || stored.status !== 0) {
|
|
199
201
|
return { stored: false, verified: false, error: String(stored.stderr || stored.stdout || stored.error?.message || 'store failed').trim().slice(0, 200) };
|
|
200
202
|
}
|
|
201
|
-
const
|
|
203
|
+
const readInv = rufloInvocationOrPlain(bin, ['memory', 'retrieve', '--key', key, '--namespace', namespace, '--value-only', '--path', db], { env: spawnAt.env });
|
|
204
|
+
const read = run(readInv.executable, readInv.args, options);
|
|
202
205
|
return { stored: true, verified: !read.error && read.status === 0 && String(read.stdout || '').trim() === value, key };
|
|
203
206
|
} finally { spawnAt.cleanup(); }
|
|
204
207
|
}
|
|
@@ -220,6 +220,15 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
|
220
220
|
"tests/unit/project-store-resolver.test.mjs"
|
|
221
221
|
]
|
|
222
222
|
},
|
|
223
|
+
{
|
|
224
|
+
"id": "ruflo-runs-under-a-node-that-loads-its-binding",
|
|
225
|
+
"reason": "Hooks inherit whatever node is first on their PATH; every direct ruflo launch (turn and learning capture, the SessionEnd/PreCompact checkpoint writer, the SessionStart memory reader and the recall reader) runs ruflo under the first node that can load its own better-sqlite3 within a bounded probe (a healthy machine is untouched, and no candidate means no change), so a native memory write is not refused for a node-ABI mismatch",
|
|
226
|
+
"files": [],
|
|
227
|
+
"platformFiles": {
|
|
228
|
+
"linux": ["tests/unit/ruflo-native-node.test.mjs"],
|
|
229
|
+
"macos": ["tests/unit/ruflo-native-node.test.mjs"]
|
|
230
|
+
}
|
|
231
|
+
},
|
|
223
232
|
{
|
|
224
233
|
"id": "lossless-lifecycle-retention",
|
|
225
234
|
"reason": "Evidence compaction preserves tokens, private state and atomic transaction recovery",
|