ruvnet-brain 0.5.1-dev β 1.14.0-dev
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 +52 -13
- package/bin/install.mjs +164 -23
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -4,21 +4,30 @@
|
|
|
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
|
+
|
|
7
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.**
|
|
8
10
|
|
|
9
|
-
[](plugin/.claude-plugin/plugin.json)
|
|
12
|
+
[](https://www.npmjs.com/package/ruvnet-brain)
|
|
13
|
+
[](https://github.com/stuinfla/ruvnet-brain/releases/latest)
|
|
14
|
+
[](https://isovision.ai/ruvnet-brain/)
|
|
12
15
|
[](LICENSE)
|
|
13
16
|
[](#testing--proof)
|
|
17
|
+
[](#testing--proof)
|
|
18
|
+
|
|
19
|
+
> **Three independent things version separately here β by design, not drift. Every number below is live (read straight from its real source, never hand-typed), so none of them can go stale:**
|
|
20
|
+
> - **`plugin`** (badge above) β the Claude Code plugin itself: SKILL.md, the grounding hooks, the MCP server. Read live from [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json). Updates often β this is where behavior fixes land.
|
|
21
|
+
> - **`installer (npm)`** (badge above) β the `npx ruvnet-brain` setup script. Read live from the [npm registry](https://www.npmjs.com/package/ruvnet-brain). Only moves when the installer script itself changes β rare.
|
|
22
|
+
> - **Brain Release** (the downloadable 512MB knowledge bundle, linked from the "download" badge above) β always resolves to [`releases/latest`](https://github.com/stuinfla/ruvnet-brain/releases/latest), currently `v0.5.0-dev`. Only moves when the underlying knowledge base is rebuilt β separate again from the two above.
|
|
14
23
|
|
|
15
24
|
<sub>Built by **[Stuart Kerr](https://isovision.ai)** at [Isovision.ai](https://isovision.ai) Β· free & fair use, to help everyone leverage the high end of agentic coding.</sub>
|
|
16
25
|
|
|
17
|
-
### [βΆ Come see it visually explained](https://ruvnet-brain
|
|
26
|
+
### [βΆ Come see it visually explained](https://isovision.ai/ruvnet-brain/)
|
|
18
27
|
|
|
19
28
|
<sub>An interactive, animated walkthrough of what you can actually build β **click the preview** to open it.</sub>
|
|
20
29
|
|
|
21
|
-
[](https://ruvnet-brain
|
|
30
|
+
[](https://isovision.ai/ruvnet-brain/)
|
|
22
31
|
|
|
23
32
|
</div>
|
|
24
33
|
|
|
@@ -32,7 +41,7 @@ But **Claude was trained on _classical_ software development.** Point it at rUv'
|
|
|
32
41
|
|
|
33
42
|
> **RuvNet Brain is the missing instruction manual.** It reads rUv's real source, hands Claude the _answer key_, and removes Claude's permission to make things up about the stack. Install it once, aim it at any repo, and a newcomer can build ~9 months ahead β without being rUv.
|
|
34
43
|
|
|
35
|
-
The novelty is **
|
|
44
|
+
The novelty is **structural grounding, not plain retrieval.** Plain RAG only decides what to _add_ to context. This ships a `UserPromptSubmit` hook that injects a grounding directive on **every** RuvNet-relevant turn β the harness consumes that stdout structurally, so the directive is _always present_, not a decline-able suggestion. It's a **strong, always-on nudge** β Claude is pointed at the real source and told to ground before asserting on every relevant turn β not a hard block on the model's output. **RAG decides what to add; this makes grounding the default the model has to actively argue its way out of.**
|
|
36
45
|
|
|
37
46
|
---
|
|
38
47
|
|
|
@@ -71,7 +80,7 @@ WITHOUT the brain β drift | WITH RuvNet Brain β grounded
|
|
|
71
80
|
npx ruvnet-brain
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
That single command runs the whole setup, narrating _what it's doing and why_ at each step: it downloads the brain (~512 MB) from the [latest GitHub Release](https://github.com/stuinfla/ruvnet-brain/releases/latest), unpacks it to `~/.cache/ruvnet-brain/kb`, installs its local reader (no cloud calls, no API keys), and wires the Claude Code plugin β the `search_ruvnet` MCP tool + the `UserPromptSubmit` grounding hook β at user scope.
|
|
83
|
+
That single command runs the whole setup, narrating _what it's doing and why_ at each step: it downloads the brain (~512 MB) from the [latest GitHub Release](https://github.com/stuinfla/ruvnet-brain/releases/latest), unpacks it to `~/.cache/ruvnet-brain/kb`, installs its local reader (no cloud calls, no API keys), and wires the Claude Code plugin β the `search_ruvnet` MCP tool + the `UserPromptSubmit` grounding hook β at user scope. The installer and the `search_ruvnet` tool run on **macOS, Linux, and Windows**; the grounding/enforcement **hooks are POSIX shell**, so they fire on macOS, Linux, and Windows-via-WSL/Git-Bash (on native Windows without WSL the search tool still works, but the auto-grounding hooks don't fire β a Node port of the hooks is on the roadmap). It's safe to re-run, and the brain itself always fetches the current Release regardless of which install path you use β you install once; you don't keep re-downloading.
|
|
75
84
|
|
|
76
85
|
> Want the bleeding-edge installer, even ahead of the last npm publish? `npx github:stuinfla/ruvnet-brain` always runs straight off the latest GitHub commit.
|
|
77
86
|
|
|
@@ -92,9 +101,21 @@ Registers the `search_ruvnet` MCP tool, the grounding skill, and the `UserPrompt
|
|
|
92
101
|
|
|
93
102
|
---
|
|
94
103
|
|
|
95
|
-
##
|
|
104
|
+
## Staying current β how updates work
|
|
105
|
+
|
|
106
|
+
You install once. After that, three mechanisms keep you on the current brain without you having to remember an update command.
|
|
107
|
+
|
|
108
|
+
- **Consent-gated auto-update heartbeat** (the `SessionStart` hook, `plugin/scripts/session-start.sh`). The **first** time the plugin runs on a machine it asks you **once** whether it may keep itself updated in the background β a security-conscious opt-in, because self-update can change the model's own instructions. Your answer is remembered (`~/.cache/ruvnet-brain/.auto-update-pref`) and never asked again. On each session start it does a rate-limited (~15 min) 3s-capped check of the live GitHub `plugin.json`. If a newer plugin version exists **and** you opted in, it downloads it in the background through Claude Code's own trusted marketplace path β but the new version is **staged, not active**: Claude Code only loads plugins at process start, so **this session keeps running the version it started with** until you restart (`claude --continue` brings your conversation right back on the new version). If you declined, it just tells you the command to run. The 512 MB knowledge bundle is handled more conservatively β **detect + notify only**, never auto-applied, because the bundle isn't cryptographically signed yet and applying it would overwrite executable tool files (SEC-0010 #6).
|
|
109
|
+
|
|
110
|
+
- **Stack watchdog status footer** (the `UserPromptSubmit` hook's always-on Gate 0, `plugin/scripts/ground-ruvnet.sh`). Every response ends with one dim status line β e.g. `π§ RuvNet Brain v1.11.0-dev Β· Ruflo: yes Β· AgentDB memory: on` β read from **filesystem ground truth**, not impressions. The version shown is always the one **actually loaded in memory** for this session; if a newer version is staged awaiting a restart, the line says so plainly (`β¦ vX staged, restart to load`). So you never have to wonder whether the brain is on, which version is acting, or whether project memory is wired.
|
|
111
|
+
|
|
112
|
+
- **Nightly publish β `releases/latest` chain** (`scripts/self-update.mjs --publish`, run by the `deploy/com.ruvnet.brain-nightly.plist` LaunchAgent at 03:15). The nightly rebuilds only the repos whose upstream changed, and **if anything was rebuilt** it bumps the product version, cuts a GitHub Release, and advances [`releases/latest`](https://github.com/stuinfla/ruvnet-brain/releases/latest). Plugin and knowledge bundle move under **one** version number, so the heartbeat above picks up both automatically. (The LaunchAgent is not auto-installed β enabling a system scheduler needs explicit owner approval.)
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## β¨ What the knowledge bundle knows β the stack _down to the code_
|
|
96
117
|
|
|
97
|
-
Earlier
|
|
118
|
+
Earlier bundles knew the **docs and architecture**. The current bundle re-indexes the code-rich repos to **full function bodies**, against each repo's real source layout β so βhow is this actually implemented?β returns the implementation, not a summary:
|
|
98
119
|
|
|
99
120
|
| Repo | Full-body code passages | |
|
|
100
121
|
|---|---:|---|
|
|
@@ -122,7 +143,7 @@ The expensive work happens **once, at build time**: every covered repo is deep-w
|
|
|
122
143
|
|
|
123
144
|
## How it changes Claude's behavior β it takes the wheel
|
|
124
145
|
|
|
125
|
-
Grounding is **
|
|
146
|
+
Grounding is **injected every turn, not left to chance.** On a RuvNet-relevant prompt the `UserPromptSubmit` hook injects a directive into context; Claude calls `search_ruvnet`, gets whole source files labeled by repo and path, and answers _from_ them. Because the hook's stdout is consumed by the harness every turn, the directive is _always there_ β a strong grounding nudge on every relevant turn. (It's a nudge, not a hard gate: the hook can't rewrite Claude's output, so it steers rather than blocks β see [ADR-0005](docs/adr/0005-behavioral-grounding-not-lock.md) for exactly what ships.)
|
|
126
147
|
|
|
127
148
|

|
|
128
149
|
|
|
@@ -183,9 +204,27 @@ node plugin/test/run-tests.mjs # full plugin QA over real JSO
|
|
|
183
204
|
| **L1βL4 behavioral harness** | **all pass** | route Β· deep-recall (returns _code_) Β· implement (cites the API) Β· orchestrate (the hook drives the full pipeline) |
|
|
184
205
|
| **Plugin QA** | **26 / 26** | manifests, hook firing, MCP `initialize`/`tools/list`, capability battery |
|
|
185
206
|
| **Clean-room install** | **3 / 3** | download the published 512 MB bundle fresh β unzip β query β grounded, cited answers |
|
|
207
|
+
| **Unit tests** | **154 passing** Β· 75% lines | `npm run test:cov` β a CI floor fails the build if coverage slips |
|
|
208
|
+
| **Grounding proof** | `npx ruvnet-brain --doctor` | asks a real question, then checks the cited path really exists in the on-disk store; a citation that doesn't resolve is reported as **NOT grounded** |
|
|
209
|
+
| **Held-out eval** | **grounded 12/12** Β· routed 10/12 | `npm run eval` β 12 frozen questions never used for tuning, graded on ground truth, never by a model |
|
|
210
|
+
|
|
211
|
+
<sub>The suite also carries **181 `it.todo` stubs** β a written backlog, each naming an untested behavior and what it would take to cover. They are deliberately **not** counted as tests: a stub proves nothing, and a number that flatters is worse than no number.</sub>
|
|
186
212
|
|
|
187
213
|
Two honest residuals, not hidden: one described question (_βroute to cheaper models to cut costβ_) still leans `ruflo` over `agentic-flow` (orchestration/cost overlap); one unnamed _βmethodologyβ_ question routes to `synthlang` instead of `sparc`. Proof reports land in [`PROOF.md`](PROOF.md), [`DESCRIBED-PROOF.md`](DESCRIBED-PROOF.md), and [`HELIX-DEMO-NOHELIX.md`](HELIX-DEMO-NOHELIX.md).
|
|
188
214
|
|
|
215
|
+
### The eval flywheel
|
|
216
|
+
|
|
217
|
+
Most of the numbers above were tuned against. [`evals/held-out.json`](evals/held-out.json) was not: twelve questions phrased the way a newcomer would ask, each with the owning repo chosen **from first principles before the brain ever saw them**. `npm run eval` scores two things, both without asking a model's opinion:
|
|
218
|
+
|
|
219
|
+
- **grounded** β the cited passage genuinely exists in the on-disk store. A path that doesn't resolve is a *fabricated* citation, and is counted as a failure however plausible it reads.
|
|
220
|
+
- **routed** β the citation came from a repo that actually owns the capability.
|
|
221
|
+
|
|
222
|
+
`npm run eval:gate` fails the build (exit 1) if either score drops below [`evals/baseline.json`](evals/baseline.json) β and also if there is **no baseline at all**, since you cannot promote against nothing. Baselines are only ever written deliberately, with `npm run eval:record`; a baseline that silently follows the code is a ratchet with no teeth.
|
|
223
|
+
|
|
224
|
+
**Why not let an LLM grade it?** Because on this very repo an LLM panel scored a **zero-citation answer 98/100**. Model-as-judge is blind to the one failure that matters here.
|
|
225
|
+
|
|
226
|
+
Current: **grounded 12/12, routed 10/12.** The two routing misses are recorded, not tuned away β `"generate tests and find coverage gaps"` cites agentic-qe's real test generator, which lives *vendored inside the ruflo repo*, so the answer is right and the repo label is a corpus-attribution artifact; `"spend less money on model calls"` cites a genuine `agentic-qe/docs/guides/cheaper-model-eval-lanes.md`, the same cost/orchestration overlap noted above.
|
|
227
|
+
|
|
189
228
|
**Query it directly (CLI):**
|
|
190
229
|
|
|
191
230
|
```bash
|
|
@@ -200,7 +239,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
200
239
|
|
|
201
240
|
## Honest status
|
|
202
241
|
|
|
203
|
-
This is
|
|
242
|
+
This is a **`-dev`** project (see the live version badge up top for the exact plugin version; the downloadable knowledge bundle is a separate track) β we don't claim βdone,β βcomplete,β or βzero hallucinations.β Where it stands:
|
|
204
243
|
|
|
205
244
|
- β
**The grounding brain is real and proven** β ~21 building-block repos, 90,842 chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
|
|
206
245
|
- β
**Code-level depth** β the code-rich repos are indexed to full function bodies; βhow is it implemented?β returns the implementation. Verified in the shipped bundle (clean-room 3/3).
|
|
@@ -217,7 +256,7 @@ This is **`v0.5.0-dev`** β we don't claim βdone,β βcomplete,β or βze
|
|
|
217
256
|
- `plugin/` β the Claude Code plugin (MCP server, grounding skill, `UserPromptSubmit` enforcement hook, marketplace manifest, test suite).
|
|
218
257
|
- `scripts/` β `gate.sh` (routing gate), `behavioral-l1-l4.mjs` (behavioral harness), `prove.mjs`, `build-bundle.mjs`, `brain-stamp.mjs`.
|
|
219
258
|
- `docs/` β [`VISION.md`](docs/VISION.md) (the why), [`adr/`](docs/adr/) (locked decisions incl. ADR-0008), [`DDD.md`](docs/DDD.md).
|
|
220
|
-
- `explainer/` β the source of the [live explainer](https://ruvnet-brain
|
|
259
|
+
- `explainer/` β the source of the [live explainer](https://isovision.ai/ruvnet-brain/).
|
|
221
260
|
- `SPEC.md` Β· `PROGRESS.md` β the master spec and the living, timestamped build log.
|
|
222
261
|
|
|
223
262
|
The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev), not git β a fresh clone is lightweight; `npx` fetches the 512 MB bundle.
|
|
@@ -226,7 +265,7 @@ The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-bra
|
|
|
226
265
|
|
|
227
266
|
## Links
|
|
228
267
|
|
|
229
|
-
- **βΆ Live explainer:** https://ruvnet-brain
|
|
268
|
+
- **βΆ Live explainer:** https://isovision.ai/ruvnet-brain/
|
|
230
269
|
- **Download / Release:** https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev
|
|
231
270
|
- **rUv's RuvNet org:** https://github.com/ruvnet
|
|
232
271
|
- **Built by:** [Stuart Kerr β Isovision.ai](https://isovision.ai)
|
package/bin/install.mjs
CHANGED
|
@@ -16,8 +16,27 @@ import fs from 'node:fs';
|
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
import readline from 'node:readline';
|
|
21
|
+
import crypto from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
// SEC-0010 #6 β the Ed25519 PUBLIC key is EMBEDDED here (not a separate file) so the installer's
|
|
24
|
+
// trust root travels with the installer code itself: an attacker who swaps the downloaded bundle
|
|
25
|
+
// cannot also swap the key the installer checks it against. Rotate via `node scripts/sign-bundle.mjs
|
|
26
|
+
// --gen-key` and paste the new keys/β¦pub.pem here. Verify logic mirrors scripts/verify-bundle.mjs.
|
|
27
|
+
const SIGNING_PUBKEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
28
|
+
MCowBQYDK2VwAyEAgse9TAtehXUvUfTrJFY2CCHiCbmelR8yCgS//sen5/w=
|
|
29
|
+
-----END PUBLIC KEY-----`;
|
|
30
|
+
function verifyBundle(bundlePath, sigPath) {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(bundlePath)) return { ok: false, reason: `bundle not found: ${bundlePath}` };
|
|
33
|
+
if (!fs.existsSync(sigPath)) return { ok: false, reason: `signature missing (fail-closed)` };
|
|
34
|
+
const digest = crypto.createHash('sha256').update(fs.readFileSync(bundlePath)).digest('hex');
|
|
35
|
+
const pub = crypto.createPublicKey(SIGNING_PUBKEY_PEM);
|
|
36
|
+
const ok = crypto.verify(null, Buffer.from(digest, 'hex'), pub, fs.readFileSync(sigPath));
|
|
37
|
+
return ok ? { ok: true, reason: `signature valid (sha256 ${digest.slice(0, 12)}β¦)` } : { ok: false, reason: 'signature does NOT match β bundle may be tampered' };
|
|
38
|
+
} catch (e) { return { ok: false, reason: `verify error: ${e.message}` }; }
|
|
39
|
+
}
|
|
21
40
|
|
|
22
41
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
42
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
@@ -25,9 +44,16 @@ const REPO_ROOT = path.resolve(__dirname, '..');
|
|
|
25
44
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
26
45
|
const RELEASE_API = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
27
46
|
const ASSET_NAME = 'ruvnet-brain.zip';
|
|
28
|
-
// Known-good
|
|
29
|
-
// Default behavior is "get the latest"; this is only the safety net.
|
|
30
|
-
|
|
47
|
+
// Known-good BUNDLE tag, used when we can't reach GitHub (offline / rate-limited / no releases),
|
|
48
|
+
// and by --pin. Default behavior is "get the latest Release"; this is only the safety net.
|
|
49
|
+
//
|
|
50
|
+
// This MUST NOT be derived from this package's own version. The installer and the brain bundle are
|
|
51
|
+
// two independent version streams (README: "Three independent things version separately here β by
|
|
52
|
+
// design"). Reading it from package.json produced a tag that has never existed β installer 1.14.0-dev
|
|
53
|
+
// asking for releases/download/v1.14.0-dev/ruvnet-brain.zip, which 404s, while the newest bundle
|
|
54
|
+
// Release is v0.5.0-dev. Verified live: v1.14.0-dev β HTTP 404, v0.5.0-dev β HTTP 200. The safety net
|
|
55
|
+
// was broken in exactly the situation it exists for. Bump this by hand when a new bundle ships.
|
|
56
|
+
const RELEASE_VERSION = 'v0.5.0-dev'; // sync-version-ignore: the BUNDLE Release tag, not this package's version
|
|
31
57
|
const fallbackUrl = (tag) => `https://github.com/${REPO}/releases/download/${tag}/${ASSET_NAME}`;
|
|
32
58
|
const APPROX_SIZE = '~512MB';
|
|
33
59
|
|
|
@@ -288,19 +314,31 @@ async function obtainBundle(release) {
|
|
|
288
314
|
);
|
|
289
315
|
info(`version: ${c.bold((release && release.tag) || RELEASE_VERSION)}`);
|
|
290
316
|
info(`from: ${downloadUrl}`);
|
|
291
|
-
|
|
317
|
+
// Download into a PRIVATE, per-run temp DIR β never a predictable os.tmpdir()/ruvnet-brain-<pid>.zip
|
|
318
|
+
// filename (CWE-377: a guessable path invites a pre-created or symlinked file at that location to be
|
|
319
|
+
// clobbered, or the extraction target to be hijacked). mkdtempSync creates a fresh, unguessable,
|
|
320
|
+
// owner-only (0700) directory; we write the zip inside it. The exit handler guarantees the whole dir
|
|
321
|
+
// is removed on ANY exit β success, thrown error, or die()βprocess.exit β and the caller also removes
|
|
322
|
+
// it immediately on success so ~512MB isn't held for the rest of the install.
|
|
323
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-'));
|
|
324
|
+
process.on('exit', () => { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } });
|
|
325
|
+
const tmp = path.join(tmpDir, ASSET_NAME);
|
|
292
326
|
try {
|
|
293
327
|
console.log(` downloading the brain (${APPROX_SIZE})β¦`);
|
|
294
328
|
await download(downloadUrl, tmp);
|
|
295
329
|
} catch (e) {
|
|
296
|
-
try { fs.rmSync(
|
|
330
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
297
331
|
die(
|
|
298
332
|
`couldn't download the brain (${e.message}).`,
|
|
299
333
|
`Check your connection, then re-run. Or, if you have a repo clone, build the bundle locally\n(${c.bold('node scripts/build-bundle.mjs')}) and run ${c.bold('node bin/install.mjs --local')}.`,
|
|
300
334
|
);
|
|
301
335
|
}
|
|
302
336
|
ok(`downloaded to ${tmp}`);
|
|
303
|
-
|
|
337
|
+
// Best-effort fetch of the detached Ed25519 signature published alongside the asset (SEC-0010 #6).
|
|
338
|
+
// If present we verify it before extracting; if absent (a pre-signing release) we warn but proceed
|
|
339
|
+
// (transitional β see SIGNING_REQUIRED at the verify gate).
|
|
340
|
+
try { await download(`${downloadUrl}.sig`, `${tmp}.sig`); } catch { /* no published sig yet */ }
|
|
341
|
+
return { zipPath: tmp, tmpDir, downloaded: true };
|
|
304
342
|
}
|
|
305
343
|
|
|
306
344
|
// ββ step: unzip into the cache dir (flattening the top-level ruvnet-brain/ folder) βββββββββββββββ
|
|
@@ -379,14 +417,34 @@ function installReader(cacheDir) {
|
|
|
379
417
|
die(`\`npm\` isn't available, but the brain needs it for its reader.`, `Install Node.js (which includes npm) and re-run.`);
|
|
380
418
|
}
|
|
381
419
|
info('installing the local readerβ¦');
|
|
420
|
+
// Prefer `npm ci` for a PINNED, reproducible install when the bundle shipped its lockfile
|
|
421
|
+
// (SEC-0010 #8 β otherwise every install did a fully unpinned resolve). Fall back to `npm i`
|
|
422
|
+
// for older bundles that predate the shipped lockfile.
|
|
423
|
+
const hasLock = fs.existsSync(path.join(cacheDir, 'package-lock.json'));
|
|
424
|
+
const npmArgs = hasLock
|
|
425
|
+
? ['ci', '--no-audit', '--no-fund', '--loglevel=error']
|
|
426
|
+
: ['i', '--no-audit', '--no-fund', '--loglevel=error'];
|
|
382
427
|
try {
|
|
383
|
-
run('npm',
|
|
428
|
+
run('npm', npmArgs, {
|
|
384
429
|
cwd: cacheDir,
|
|
385
430
|
// silence npm's "new version available" update-notifier so the narration stays clean
|
|
386
431
|
env: { ...process.env, npm_config_update_notifier: 'false', npm_config_fund: 'false' },
|
|
387
432
|
});
|
|
388
433
|
} catch (e) {
|
|
389
|
-
|
|
434
|
+
// `npm ci` is strict (fails if lock and package.json disagree); fall back to `npm i` once
|
|
435
|
+
// rather than hard-failing a user's install on a lockfile mismatch.
|
|
436
|
+
if (hasLock) {
|
|
437
|
+
warn(`pinned install (npm ci) failed (${e.message}); retrying with npm i`);
|
|
438
|
+
try {
|
|
439
|
+
run('npm', ['i', '--no-audit', '--no-fund', '--loglevel=error'], {
|
|
440
|
+
cwd: cacheDir, env: { ...process.env, npm_config_update_notifier: 'false', npm_config_fund: 'false' },
|
|
441
|
+
});
|
|
442
|
+
} catch (e2) {
|
|
443
|
+
die(`the reader install failed (${e2.message}).`, `Re-run after checking your network / npm setup.`);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
die(`the reader install failed (${e.message}).`, `Re-run after checking your network / npm setup.`);
|
|
447
|
+
}
|
|
390
448
|
}
|
|
391
449
|
ok('reader installed');
|
|
392
450
|
}
|
|
@@ -455,7 +513,37 @@ function verifyInstall(cacheDir) {
|
|
|
455
513
|
}
|
|
456
514
|
|
|
457
515
|
// ββ step: warm the model + prove grounding with one real question (best-effort, never fatal) ββββββ
|
|
458
|
-
|
|
516
|
+
// ββ BEGIN GENERATED: verify-citation.mjs (node scripts/embed-verifier.mjs) ββ
|
|
517
|
+
const VERIFY_CITATION_B64 = 'IyEvdXNyL2Jpbi9lbnYgbm9kZQovLyB2ZXJpZnktY2l0YXRpb24ubWpzIOKAlCBkZWNpZGUgd2hldGhlciBhbiBhbnN3ZXIgaXMgR1JPVU5ERUQsIGJ5IGdyb3VuZCB0cnV0aCByYXRoZXIgdGhhbiBieSB2aWJlcy4KLy8KLy8gV0hZIFRISVMgRVhJU1RTCi8vIC0tLS0tLS0tLS0tLS0tLQovLyBUaGUgb2xkIGdyb3VuZGluZyBjaGVjayBhc2tlZDogZG9lcyB0aGUgYW5zd2VyIGNvbnRhaW4gdGhlIHN0cmluZyAicnZmIiBvciAicnV2ZWN0b3IiPyBBIG1vZGVsCi8vIHRoYXQgaGFsbHVjaW5hdGVkICJqdXN0IHVzZSBSVkYhIiB3aXRoIHplcm8gc291cmNlcyBwYXNzZWQuIFNvIGRpZCBhbiBhbnN3ZXIgY2l0aW5nIGEgZmlsZSB0aGF0Ci8vIGRvZXMgbm90IGV4aXN0LiBLZXl3b3JkIHByZXNlbmNlIGlzIG5vdCBldmlkZW5jZSDigJQgYW4gTExNIHBhbmVsIG9uY2Ugc2NvcmVkIGEgemVyby1jaXRhdGlvbgovLyBhbnN3ZXIgOTgvMTAwIG9uIHRoaXMgcmVwby4KLy8KLy8gQSBjaXRhdGlvbiBpcyBvbmx5IHJlYWwgaWYgaXQgUkVTT0xWRVM6IHRoZSByZXBvIG11c3QgYmUgYW4gaW5kZXhlZCBzdG9yZSBvbiBkaXNrLCBhbmQgdGhlIGNpdGVkCi8vIGRvY3VtZW50IHBhdGggbXVzdCBhcHBlYXIgYXMgdGhlIGBwYXRoYCBvZiBhbiBhY3R1YWwgcGFzc2FnZSBpbnNpZGUgdGhhdCBzdG9yZSdzIHBhc3NhZ2VzIGZpbGUuCi8vIFRoYXQgaXMgY2hlY2thYmxlIHdpdGhvdXQgYSBtb2RlbCwgd2l0aG91dCB0aGUgbmV0d29yaywgYW5kIHdpdGhvdXQgdHJ1c3RpbmcgYW55dGhpbmcgdGhlIG1vZGVsCi8vIHNhaWQuIFRoaXMgbW9kdWxlIGRvZXMgZXhhY3RseSB0aGF0IGFuZCBub3RoaW5nIGVsc2UuCi8vCi8vIFRoZSByZWFkZXIgKGBmb3JnZS1hc2stYWxsLm1qc2ApIHByaW50cyBlYWNoIGhpdCBhczoKLy8gICAgICMxICByZXBvPWNvbmNlcHRzICBjZT0wLjIwMSAgdmVjPTAuODY4NiAga2luZD1kb2MKLy8gICAgIHBhdGggOiBjb25jZXB0cy9ydXZlY3Rvci9DQVJEL3J1dmVjdG9yLWNhcmQKLy8gICAgIHRpdGxlOiBydXZlY3RvciDigJQgQ2FwYWJpbGl0eQovLyBOb3RlIHRoZSBwcmludGVkIHBhdGggaXMgYDxyZXBvPi88ZG9jUGF0aD5gOyBpbnNpZGUgYGNvbmNlcHRzLnBhc3NhZ2VzLmpzb25sYCB0aGUgc3RvcmVkIGBwYXRoYAovLyBpcyBqdXN0IGBydXZlY3Rvci9DQVJEL3J1dmVjdG9yLWNhcmRgIChvcHRpb25hbGx5IHN1ZmZpeGVkIGAjMGAsIGAjMWAsIOKApiB3aGVuIGNodW5rZWQpLgoKaW1wb3J0IGZzIGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgcGF0aCBmcm9tICdub2RlOnBhdGgnOwppbXBvcnQgcmVhZGxpbmUgZnJvbSAnbm9kZTpyZWFkbGluZSc7CgovKiogUGFyc2UgdGhlIHJlYWRlcidzIHN0ZG91dCBpbnRvIHN0cnVjdHVyZWQgY2l0YXRpb25zLiBOZXZlciB0aHJvd3M7IHVucGFyc2VhYmxlIGlucHV0IOKGkiBbXS4gKi8KZXhwb3J0IGZ1bmN0aW9uIHBhcnNlQ2l0YXRpb25zKHN0ZG91dCkgewogIGNvbnN0IG91dCA9IFtdOwogIGNvbnN0IHRleHQgPSBTdHJpbmcoc3Rkb3V0ID8/ICcnKTsKICBjb25zdCBibG9ja1JlID0gL14jKFxkKylccytyZXBvPShcUyspKD86XHMrY2U9KC0/W1xkLl0rKSk/KD86XHMrdmVjPSgtP1tcZC5dKykpPyg/OlxzK2tpbmQ9KFxTKykpPy9nbTsKICBsZXQgbTsKICB3aGlsZSAoKG0gPSBibG9ja1JlLmV4ZWModGV4dCkpICE9PSBudWxsKSB7CiAgICBjb25zdCByZXN0ID0gdGV4dC5zbGljZShtLmluZGV4KTsKICAgIGNvbnN0IHBhdGhNID0gL15wYXRoXHMqOlxzKiguKykkL20uZXhlYyhyZXN0KTsKICAgIGNvbnN0IHRpdGxlTSA9IC9edGl0bGVccyo6XHMqKC4rKSQvbS5leGVjKHJlc3QpOwogICAgaWYgKCFwYXRoTSkgY29udGludWU7CiAgICBjb25zdCByZXBvID0gbVsyXTsKICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aE1bMV0udHJpbSgpOwogICAgLy8gU3RyaXAgdGhlIHJlcG8gcHJlZml4IHRoZSByZWFkZXIgYWRkcywgc28gdGhlIHJlbWFpbmRlciBjYW4gYmUgbWF0Y2hlZCBhZ2FpbnN0IHRoZSBzdG9yZS4KICAgIGNvbnN0IGRvY1BhdGggPSBmdWxsUGF0aC5zdGFydHNXaXRoKGAke3JlcG99L2ApID8gZnVsbFBhdGguc2xpY2UocmVwby5sZW5ndGggKyAxKSA6IGZ1bGxQYXRoOwogICAgb3V0LnB1c2goewogICAgICByYW5rOiBOdW1iZXIobVsxXSksCiAgICAgIHJlcG8sCiAgICAgIGNlOiBtWzNdICE9PSB1bmRlZmluZWQgPyBOdW1iZXIobVszXSkgOiBudWxsLAogICAgICB2ZWM6IG1bNF0gIT09IHVuZGVmaW5lZCA/IE51bWJlcihtWzRdKSA6IG51bGwsCiAgICAgIGtpbmQ6IG1bNV0gPz8gbnVsbCwKICAgICAgZnVsbFBhdGgsCiAgICAgIGRvY1BhdGgsCiAgICAgIHRpdGxlOiB0aXRsZU0gPyB0aXRsZU1bMV0udHJpbSgpIDogbnVsbCwKICAgIH0pOwogIH0KICByZXR1cm4gb3V0Owp9CgovKiogVGhlIHBhc3NhZ2VzIGZpbGVzIHRoYXQgY291bGQgaG9sZCBhIHJlcG8ncyBkb2N1bWVudHMg4oCUIHRoZSBzbGltIHN0b3JlIGFuZCB0aGUgZGVlcCBgLmJpZ2Agb25lLiAqLwpleHBvcnQgZnVuY3Rpb24gcGFzc2FnZXNGaWxlc0ZvcihyZXBvLCBrYkRpcikgewogIHJldHVybiBbcGF0aC5qb2luKGtiRGlyLCBgJHtyZXBvfS5wYXNzYWdlcy5qc29ubGApLCBwYXRoLmpvaW4oa2JEaXIsIGAke3JlcG99LmJpZy5wYXNzYWdlcy5qc29ubGApXQogICAgLmZpbHRlcigocCkgPT4gZnMuZXhpc3RzU3luYyhwKSk7Cn0KCi8qKiBUcnVlIHdoZW4gYHN0b3JlZGAgaXMgdGhlIGNpdGVkIGRvYywgYWxsb3dpbmcgZm9yIHRoZSBgI05gIGNodW5rIHN1ZmZpeCB0aGUgYnVpbGRlciBhcHBlbmRzLiAqLwpmdW5jdGlvbiBzYW1lUGF0aChzdG9yZWQsIGRvY1BhdGgpIHsKICByZXR1cm4gc3RvcmVkID09PSBkb2NQYXRoIHx8IHN0b3JlZC5zdGFydHNXaXRoKGAke2RvY1BhdGh9I2ApOwp9CgovKioKICogRG9lcyB0aGlzIGNpdGF0aW9uIHBvaW50IGF0IGEgcGFzc2FnZSB0aGF0IHJlYWxseSBleGlzdHMgb24gZGlzaz8KICogU3RyZWFtcyB0aGUgZmlsZSBhbmQgc3RvcHMgYXQgdGhlIGZpcnN0IG1hdGNoLCBzbyBhIDUwME1CIGAuYmlnYCBzdG9yZSBjb3N0cyBvbmx5IGFzIG11Y2ggYXMgaXQKICogdGFrZXMgdG8gcmVhY2ggdGhlIGhpdC4gQSBtYWxmb3JtZWQgSlNPTiBsaW5lIGlzIHNraXBwZWQsIG5ldmVyIGZhdGFsLgogKi8KZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNpdGF0aW9uUmVzb2x2ZXMoY2l0YXRpb24sIGtiRGlyKSB7CiAgY29uc3QgZmlsZXMgPSBwYXNzYWdlc0ZpbGVzRm9yKGNpdGF0aW9uLnJlcG8sIGtiRGlyKTsKICBpZiAoIWZpbGVzLmxlbmd0aCkgcmV0dXJuIHsgcmVzb2x2ZWQ6IGZhbHNlLCByZWFzb246ICduby1zdG9yZScsIGZpbGU6IG51bGwsIHN0b3JlZFBhdGg6IG51bGwgfTsKICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHsKICAgIGNvbnN0IHJsID0gcmVhZGxpbmUuY3JlYXRlSW50ZXJmYWNlKHsgaW5wdXQ6IGZzLmNyZWF0ZVJlYWRTdHJlYW0oZmlsZSksIGNybGZEZWxheTogSW5maW5pdHkgfSk7CiAgICB0cnkgewogICAgICBmb3IgYXdhaXQgKGNvbnN0IGxpbmUgb2YgcmwpIHsKICAgICAgICBpZiAoIWxpbmUpIGNvbnRpbnVlOwogICAgICAgIGxldCByZWM7CiAgICAgICAgdHJ5IHsgcmVjID0gSlNPTi5wYXJzZShsaW5lKTsgfSBjYXRjaCB7IGNvbnRpbnVlOyB9CiAgICAgICAgaWYgKHR5cGVvZiByZWM/LnBhdGggPT09ICdzdHJpbmcnICYmIHNhbWVQYXRoKHJlYy5wYXRoLCBjaXRhdGlvbi5kb2NQYXRoKSkgewogICAgICAgICAgcmV0dXJuIHsgcmVzb2x2ZWQ6IHRydWUsIHJlYXNvbjogJ29rJywgZmlsZTogcGF0aC5iYXNlbmFtZShmaWxlKSwgc3RvcmVkUGF0aDogcmVjLnBhdGggfTsKICAgICAgICB9CiAgICAgIH0KICAgIH0gZmluYWxseSB7CiAgICAgIHJsLmNsb3NlKCk7CiAgICB9CiAgfQogIHJldHVybiB7IHJlc29sdmVkOiBmYWxzZSwgcmVhc29uOiAncGF0aC1ub3QtaW4tc3RvcmUnLCBmaWxlOiBudWxsLCBzdG9yZWRQYXRoOiBudWxsIH07Cn0KCi8qKgogKiBUaGUgZ2F0ZS4gQW4gYW5zd2VyIGlzIGdyb3VuZGVkIG9ubHkgd2hlbiBpdCBjaXRlcyBhdCBsZWFzdCBvbmUgcGFzc2FnZSB0aGF0IHJlc29sdmVzIG9uIGRpc2suCiAqIFJldHVybnMgdGhlIHJlY2VpcHQgc28gYSBjYWxsZXIgY2FuIFBSSU5UIHRoZSBldmlkZW5jZSBpbnN0ZWFkIG9mIGFzc2VydGluZyBhIGNvbmNsdXNpb24uCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gdmVyaWZ5R3JvdW5kaW5nKHN0ZG91dCwga2JEaXIpIHsKICBjb25zdCBjaXRhdGlvbnMgPSBwYXJzZUNpdGF0aW9ucyhzdGRvdXQpOwogIGlmICghY2l0YXRpb25zLmxlbmd0aCkgewogICAgcmV0dXJuIHsgZ3JvdW5kZWQ6IGZhbHNlLCByZWFzb246ICduby1jaXRhdGlvbnMnLCBjaXRhdGlvbnM6IFtdLCByZWNlaXB0OiBudWxsIH07CiAgfQogIGZvciAoY29uc3QgY2l0YXRpb24gb2YgY2l0YXRpb25zKSB7CiAgICBjb25zdCByID0gYXdhaXQgY2l0YXRpb25SZXNvbHZlcyhjaXRhdGlvbiwga2JEaXIpOwogICAgaWYgKHIucmVzb2x2ZWQpIHsKICAgICAgcmV0dXJuIHsKICAgICAgICBncm91bmRlZDogdHJ1ZSwKICAgICAgICByZWFzb246ICdvaycsCiAgICAgICAgY2l0YXRpb25zLAogICAgICAgIHJlY2VpcHQ6IHsgcmVwbzogY2l0YXRpb24ucmVwbywgcGF0aDogY2l0YXRpb24uZnVsbFBhdGgsIHRpdGxlOiBjaXRhdGlvbi50aXRsZSwgZmlsZTogci5maWxlLCBzdG9yZWRQYXRoOiByLnN0b3JlZFBhdGggfSwKICAgICAgfTsKICAgIH0KICB9CiAgcmV0dXJuIHsgZ3JvdW5kZWQ6IGZhbHNlLCByZWFzb246ICdjaXRhdGlvbnMtZG8tbm90LXJlc29sdmUnLCBjaXRhdGlvbnMsIHJlY2VpcHQ6IG51bGwgfTsKfQo=';
|
|
518
|
+
// ββ END GENERATED ββ
|
|
519
|
+
|
|
520
|
+
// The verifier belongs next to the data it verifies, so it lives in the KB. But every bundle
|
|
521
|
+
// published before 2026-07-09 predates it, and telling those users "grounding not verifiable β
|
|
522
|
+
// re-run the installer" would send them in a circle, because re-running fetches the same bundle.
|
|
523
|
+
// So the installer CARRIES the verifier and writes it in when it's missing. A newer bundle's copy
|
|
524
|
+
// always wins: we never overwrite a file the bundle shipped.
|
|
525
|
+
function ensureVerifier(cacheDir) {
|
|
526
|
+
const p = path.join(cacheDir, 'verify-citation.mjs');
|
|
527
|
+
if (fs.existsSync(p)) return 'from-bundle';
|
|
528
|
+
if (!VERIFY_CITATION_B64) return 'unavailable';
|
|
529
|
+
try {
|
|
530
|
+
fs.writeFileSync(p, Buffer.from(VERIFY_CITATION_B64, 'base64').toString('utf8'), 'utf8');
|
|
531
|
+
return 'installed';
|
|
532
|
+
} catch { return 'unavailable'; }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function loadCitationVerifier(cacheDir) {
|
|
536
|
+
ensureVerifier(cacheDir);
|
|
537
|
+
const p = path.join(cacheDir, 'verify-citation.mjs');
|
|
538
|
+
if (!fs.existsSync(p)) return null;
|
|
539
|
+
try { return await import(pathToFileURL(p).href); } catch { return null; }
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Proving grounding means proving the answer's CITATION RESOLVES β that the file it points at is a
|
|
543
|
+
// real, indexed passage on this disk. The old check here tested `/rvf|ruvector|hnsw/` against the
|
|
544
|
+
// answer text, which a hallucinated "just use RVF!" passes with zero sources. Keyword presence is
|
|
545
|
+
// not evidence. We now print the cited path as a receipt, so you can go look at it yourself.
|
|
546
|
+
async function smokeQuery(cacheDir) {
|
|
459
547
|
const ask = path.join(cacheDir, 'forge-ask-all.mjs');
|
|
460
548
|
if (!fs.existsSync(ask)) return { ran: false };
|
|
461
549
|
step(
|
|
@@ -465,6 +553,7 @@ function smokeQuery(cacheDir) {
|
|
|
465
553
|
const Q = 'How should I store embeddings in this project without running a server?';
|
|
466
554
|
info(`Q: ${c.cyan(`"${Q}"`)}`);
|
|
467
555
|
info(c.dim('(first run downloads a small local model once β this can take a minute)'));
|
|
556
|
+
const started = Date.now();
|
|
468
557
|
let r;
|
|
469
558
|
try {
|
|
470
559
|
// Relative filename + matching cwd (NOT the absolute `ask` path) β forge-ask-all.mjs only runs
|
|
@@ -472,7 +561,7 @@ function smokeQuery(cacheDir) {
|
|
|
472
561
|
// absolute path via spawnSync (no shell involved) that identity check silently fails on this
|
|
473
562
|
// machine, so main() never runs β exit 0, zero stdout, zero stderr, no exception. Looks like a
|
|
474
563
|
// clean success; is actually a total no-op. Verified: switching to a relative name + cwd fixes it.
|
|
475
|
-
r = spawnSync('node', ['forge-ask-all.mjs', '--dir', cacheDir, '--q', Q, '--k', '
|
|
564
|
+
r = spawnSync('node', ['forge-ask-all.mjs', '--dir', cacheDir, '--q', Q, '--k', '3'], {
|
|
476
565
|
cwd: cacheDir,
|
|
477
566
|
encoding: 'utf8',
|
|
478
567
|
timeout: 240000,
|
|
@@ -482,15 +571,34 @@ function smokeQuery(cacheDir) {
|
|
|
482
571
|
warn("skipped the live test (couldn't launch the reader) β it'll warm on your first real question");
|
|
483
572
|
return { ran: false };
|
|
484
573
|
}
|
|
574
|
+
const secs = ((Date.now() - started) / 1000).toFixed(1);
|
|
485
575
|
const out = `${r.stdout || ''}`;
|
|
486
|
-
if (r.status
|
|
487
|
-
|
|
488
|
-
return { ran: true, grounded:
|
|
576
|
+
if (r.status !== 0 || !out.trim()) {
|
|
577
|
+
warn('no answer came back (first-run model download or offline) β the brain is installed; it\'ll warm on your first real question');
|
|
578
|
+
return { ran: true, grounded: false, reason: 'no-answer' };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const verifier = await loadCitationVerifier(cacheDir);
|
|
582
|
+
if (!verifier) {
|
|
583
|
+
info(`the brain answered in ${secs}s, but this bundle predates the citation verifier β`);
|
|
584
|
+
info(c.dim(' re-run `npx ruvnet-brain` to refresh it, and grounding will be PROVEN, not assumed'));
|
|
585
|
+
return { ran: true, grounded: null, reason: 'verifier-missing' };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const v = await verifier.verifyGrounding(out, cacheDir);
|
|
589
|
+
if (v.grounded) {
|
|
590
|
+
ok(`grounded in rUv's real source β verified in ${secs}s, not guessed β¦`);
|
|
591
|
+
console.log(` ${c.dim('cited:')} ${c.bold(v.receipt.path)}`);
|
|
592
|
+
if (v.receipt.title) console.log(` ${c.dim('title:')} ${v.receipt.title}`);
|
|
593
|
+
console.log(` ${c.dim('verified:')} that passage really exists in ${c.bold(v.receipt.file)}`);
|
|
594
|
+
return { ran: true, grounded: true, receipt: v.receipt, secs };
|
|
489
595
|
}
|
|
490
596
|
warn(
|
|
491
|
-
|
|
597
|
+
v.reason === 'no-citations'
|
|
598
|
+
? 'the answer cited no source at all β NOT grounded (re-run the installer to repair the KB)'
|
|
599
|
+
: "the answer's citations don't resolve to any indexed passage β NOT grounded (KB may be corrupt; re-run the installer)",
|
|
492
600
|
);
|
|
493
|
-
return { ran: true, grounded: false };
|
|
601
|
+
return { ran: true, grounded: false, reason: v.reason };
|
|
494
602
|
}
|
|
495
603
|
|
|
496
604
|
// ββ `--demo`: a guided, REAL walkthrough β proves grounding live, never fabricates output βββββββββ
|
|
@@ -556,7 +664,7 @@ function runDemo() {
|
|
|
556
664
|
}
|
|
557
665
|
|
|
558
666
|
// ββ `--doctor`: a standalone health check the user can run any time βββββββββββββββββββββββββββββββ
|
|
559
|
-
function doctor() {
|
|
667
|
+
async function doctor() {
|
|
560
668
|
printBanner('doctor');
|
|
561
669
|
console.log(c.dim('Checking every part of the install and reporting green/red.\n'));
|
|
562
670
|
const cacheDir = process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
@@ -583,13 +691,25 @@ function doctor() {
|
|
|
583
691
|
? ok('RuVector present β vector CLI / MCP available')
|
|
584
692
|
: warn('RuVector not found β answers still work. To add: claude mcp add ruvector --scope user -- npx -y ruvector mcp start');
|
|
585
693
|
const v = verifyInstall(cacheDir);
|
|
586
|
-
smokeQuery(cacheDir);
|
|
694
|
+
const smoke = await smokeQuery(cacheDir);
|
|
587
695
|
const allGreen = v.repos > 0 && v.reader && v.mcp;
|
|
588
696
|
console.log(
|
|
589
697
|
`\n ${allGreen ? c.green('β Healthy.') : c.yellow('! Needs attention.')} ${
|
|
590
698
|
allGreen ? 'The brain is installed and reachable.' : 'Re-run the installer to fix the warnings above.'
|
|
591
699
|
}`,
|
|
592
700
|
);
|
|
701
|
+
// Installed-and-reachable and actually-grounded are different claims. Keep them separate, so a
|
|
702
|
+
// healthy install can never be mistaken for proven grounding.
|
|
703
|
+
if (smoke.grounded === true) {
|
|
704
|
+
console.log(` ${c.green('β Grounding PROVEN.')} It answered from ${c.bold(smoke.receipt.path)} β a passage that`);
|
|
705
|
+
console.log(` really exists in your local KB. Checked in ${smoke.secs}s, no cloud, no API key.`);
|
|
706
|
+
} else if (smoke.grounded === false) {
|
|
707
|
+
console.log(` ${c.yellow('! Grounding NOT proven')} (${smoke.reason}). The install is present but the brain did not`);
|
|
708
|
+
console.log(' answer from a verifiable source. Re-run npx ruvnet-brain to repair the KB.');
|
|
709
|
+
} else if (smoke.grounded === null) {
|
|
710
|
+
console.log(` ${c.yellow('! Grounding not verifiable')} on this bundle β it predates the citation verifier.`);
|
|
711
|
+
console.log(' Re-run npx ruvnet-brain to refresh, then --doctor will prove it.');
|
|
712
|
+
}
|
|
593
713
|
if (allGreen) {
|
|
594
714
|
console.log(`\n ${c.bold('What this means for you:')}`);
|
|
595
715
|
console.log(` β’ ${c.bold('It works in EVERY project')} β user-level (global). Open Claude Code in any repo or VS Code`);
|
|
@@ -870,7 +990,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
870
990
|
// ββ main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
871
991
|
(async () => {
|
|
872
992
|
if (FLAG_HELP) return showHelp();
|
|
873
|
-
if (FLAG_DOCTOR) return doctor();
|
|
993
|
+
if (FLAG_DOCTOR) return await doctor();
|
|
874
994
|
if (FLAG_DEMO) return runDemo();
|
|
875
995
|
|
|
876
996
|
printBanner('installer');
|
|
@@ -909,10 +1029,31 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
909
1029
|
const localZipPresent =
|
|
910
1030
|
FLAG_LOCAL || fs.existsSync(path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip'));
|
|
911
1031
|
const release = localZipPresent ? null : await resolveRelease();
|
|
912
|
-
const { zipPath, downloaded } = await obtainBundle(release);
|
|
1032
|
+
const { zipPath, tmpDir, downloaded } = await obtainBundle(release);
|
|
1033
|
+
// Verify the Ed25519 signature BEFORE extracting a downloaded bundle into the user's config
|
|
1034
|
+
// (SEC-0010 #6 β trust root = keys/ruvnet-brain-signing.pub.pem shipped inside this package).
|
|
1035
|
+
// SIGNING_REQUIRED is transitional: while releases predate signing, a MISSING sig warns-and-proceeds
|
|
1036
|
+
// but a PRESENT-but-INVALID sig ALWAYS fails closed. Flip to true once every release is signed.
|
|
1037
|
+
const SIGNING_REQUIRED = false;
|
|
1038
|
+
if (downloaded && !FLAG_NO_VERIFY) {
|
|
1039
|
+
const sigPath = `${zipPath}.sig`;
|
|
1040
|
+
const hasSig = fs.existsSync(sigPath);
|
|
1041
|
+
if (!hasSig && !SIGNING_REQUIRED) {
|
|
1042
|
+
warn('this release is not signed yet β proceeding (bundle integrity not cryptographically verified)');
|
|
1043
|
+
} else {
|
|
1044
|
+
step('Verifying the bundle signature', 'so a tampered or MITM-swapped download can never be extracted');
|
|
1045
|
+
const { ok: valid, reason } = verifyBundle(zipPath, sigPath);
|
|
1046
|
+
if (!valid) {
|
|
1047
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
1048
|
+
die(`bundle signature check FAILED β ${reason}`,
|
|
1049
|
+
`Refusing to extract an unverified bundle. Re-run to fetch a fresh copy; if it persists, the\nrelease may be tampered β report it. (Override at your own risk with ${c.bold('--no-verify')}.)`);
|
|
1050
|
+
}
|
|
1051
|
+
ok(reason);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
913
1054
|
unzipInto(zipPath, cacheDir);
|
|
914
|
-
if (downloaded) {
|
|
915
|
-
try { fs.rmSync(
|
|
1055
|
+
if (downloaded && tmpDir) {
|
|
1056
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* leave temp behind, not fatal */ }
|
|
916
1057
|
}
|
|
917
1058
|
}
|
|
918
1059
|
|
|
@@ -920,7 +1061,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
920
1061
|
const plugin = wirePlugin();
|
|
921
1062
|
if (!FLAG_NO_VERIFY) {
|
|
922
1063
|
verifyInstall(cacheDir);
|
|
923
|
-
smokeQuery(cacheDir);
|
|
1064
|
+
await smokeQuery(cacheDir);
|
|
924
1065
|
}
|
|
925
1066
|
|
|
926
1067
|
// ββ onboarding: detect the toolkit + make offers (all optional, all non-fatal) ββ
|
package/package.json
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.14.0-dev",
|
|
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": {
|
|
7
7
|
"ruvnet-brain": "bin/install.mjs"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node plugin/test/run-tests.mjs",
|
|
11
|
+
"version:check": "node scripts/sync-version.mjs --check",
|
|
12
|
+
"version:sync": "node scripts/sync-version.mjs",
|
|
13
|
+
"test:unit": "vitest run",
|
|
14
|
+
"test:cov": "vitest run --coverage",
|
|
15
|
+
"metaharness:fix": "node scripts/fix-metaharness-memretrieve.mjs --apply",
|
|
16
|
+
"metaharness:check": "node scripts/fix-metaharness-memretrieve.mjs --check",
|
|
17
|
+
"eval": "node scripts/eval-brain.mjs",
|
|
18
|
+
"eval:gate": "node scripts/eval-brain.mjs --gate",
|
|
19
|
+
"eval:record": "node scripts/eval-brain.mjs --record",
|
|
20
|
+
"embed:verifier": "node scripts/embed-verifier.mjs",
|
|
21
|
+
"embed:check": "node scripts/embed-verifier.mjs --check"
|
|
22
|
+
},
|
|
9
23
|
"files": [
|
|
10
24
|
"bin/install.mjs",
|
|
11
25
|
"README.md",
|
|
@@ -34,5 +48,9 @@
|
|
|
34
48
|
},
|
|
35
49
|
"homepage": "https://github.com/stuinfla/ruvnet-brain#readme",
|
|
36
50
|
"bugs": "https://github.com/stuinfla/ruvnet-brain/issues",
|
|
37
|
-
"author": "Stuart Kerr"
|
|
51
|
+
"author": "Stuart Kerr",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
54
|
+
"vitest": "^4.1.10"
|
|
55
|
+
}
|
|
38
56
|
}
|