ruvnet-brain 4.7.4 → 4.7.5

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
@@ -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
- [![RuvNet Brain version 4.6.2 — README updated 2026-10-10 EDT](https://img.shields.io/badge/version_4.7.4-updated_2026--10--10_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](plugin/.claude-plugin/plugin.json)
12
+ [![RuvNet Brain version 4.6.2 — README updated 2026-10-10 EDT](https://img.shields.io/badge/version_4.7.5-updated_2026--10--10_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](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.4",
3
+ "version": "4.7.5",
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",
4
+ "version": "4.7.5",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.7.4",
3
+ "version": "4.7.5",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -36,14 +36,76 @@
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
+
72
+ /**
73
+ * The node that can load ruflo's native better-sqlite3, or null when the caller's own PATH node
74
+ * already can, when ruflo is not a node package we can inspect, or when no candidate can.
75
+ * Null always means "leave the invocation exactly as it was" — this never makes anything worse.
76
+ *
77
+ * @param {string} binary path to the ruflo bin (symlink or script)
78
+ * @param {{ env?: Record<string,string|undefined>, candidates?: string[] }} [opts]
79
+ * @returns {{ node: string, entry: string }|null}
80
+ */
81
+ export function nativeNodeFor(binary, { env = process.env, candidates } = {}) {
82
+ let entry;
83
+ try { entry = fs.realpathSync(binary); } catch { return null; }
84
+ const addon = path.join(path.dirname(path.dirname(entry)), 'node_modules', 'better-sqlite3');
85
+ try { if (!fs.statSync(addon).isDirectory()) return null; } catch { return null; }
86
+
87
+ const list = candidates ?? defaultNodeCandidates(env);
88
+ const cacheKey = `${entry}\0${list.join('\0')}`;
89
+ if (nativeNodeCache.has(cacheKey)) return nativeNodeCache.get(cacheKey);
90
+
91
+ const probe = `new (require(${JSON.stringify(addon)}))(':memory:').close()`;
92
+ let chosen = null;
93
+ for (let i = 0; i < list.length; i += 1) {
94
+ const r = spawnSync(list[i], ['-e', probe], { stdio: 'ignore', timeout: 8000, windowsHide: true });
95
+ if (r.status === 0) { chosen = i === 0 ? null : { node: list[i], entry }; break; }
96
+ }
97
+ nativeNodeCache.set(cacheKey, chosen);
98
+ return chosen;
99
+ }
100
+
43
101
  // npm's Windows .cmd shim cannot be spawned without a shell. Resolve the declared entry
44
102
  // 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' || !/\.cmd$/i.test(binary)) return { executable: binary, args };
103
+ export function rufloInvocation(binary, args, { platform = process.platform, env = process.env, nodeCandidates } = {}) {
104
+ if (platform !== 'win32') {
105
+ const native = nativeNodeFor(binary, { env, candidates: nodeCandidates });
106
+ return native ? { executable: native.node, args: [native.entry, ...args] } : { executable: binary, args };
107
+ }
108
+ if (!/\.cmd$/i.test(binary)) return { executable: binary, args };
47
109
  if (path.basename(binary).toLowerCase() !== 'ruflo.cmd') throw new Error('unsupported managed Ruflo shim');
48
110
  const packageRoot = fs.realpathSync(path.join(path.dirname(fs.realpathSync(binary)), 'node_modules', 'ruflo'));
49
111
  const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
@@ -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; ruflo is run under the first node that can load its own better-sqlite3 (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",