ruvnet-brain 1.6.2-dev β 1.16.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 +46 -12
- package/bin/install.mjs +341 -26
- package/package.json +27 -3
package/README.md
CHANGED
|
@@ -4,29 +4,33 @@
|
|
|
4
4
|
|
|
5
5
|
# π§ RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### RuvNet Brain version 1.
|
|
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
|
|
|
11
11
|
[](plugin/.claude-plugin/plugin.json)
|
|
12
12
|
[](https://www.npmjs.com/package/ruvnet-brain)
|
|
13
13
|
[](https://github.com/stuinfla/ruvnet-brain/releases/latest)
|
|
14
|
-
[](https://isovision.ai/ruvnet-brain/)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
[](#testing--proof)
|
|
17
|
+
[](#testing--proof)
|
|
17
18
|
|
|
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:**
|
|
19
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.
|
|
20
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.
|
|
21
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.
|
|
23
|
+
> - **Update an installed brain once:** `npx ruvnet-brain --update` β runs the bundle's own self-updater (backs up first, re-verifies, fails loud instead of half-applying).
|
|
24
|
+
> - **Nightly auto-update is OFF by default:** `npx ruvnet-brain --enable-nightly` schedules it (macOS LaunchAgent, 03:47); `npx ruvnet-brain --disable-nightly` removes it; Linux/Windows get the cron line documented in the bundle's `forge-update.mjs`.
|
|
25
|
+
> - **Either way, your copy only advances when a new Release is published** β the updater pulls `releases/latest`, so running it between releases is a safe no-op.
|
|
22
26
|
|
|
23
27
|
<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>
|
|
24
28
|
|
|
25
|
-
### [βΆ Come see it visually explained](https://ruvnet-brain
|
|
29
|
+
### [βΆ Come see it visually explained](https://isovision.ai/ruvnet-brain/)
|
|
26
30
|
|
|
27
31
|
<sub>An interactive, animated walkthrough of what you can actually build β **click the preview** to open it.</sub>
|
|
28
32
|
|
|
29
|
-
[](https://ruvnet-brain
|
|
33
|
+
[](https://isovision.ai/ruvnet-brain/)
|
|
30
34
|
|
|
31
35
|
</div>
|
|
32
36
|
|
|
@@ -40,7 +44,7 @@ But **Claude was trained on _classical_ software development.** Point it at rUv'
|
|
|
40
44
|
|
|
41
45
|
> **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.
|
|
42
46
|
|
|
43
|
-
The novelty is **
|
|
47
|
+
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.**
|
|
44
48
|
|
|
45
49
|
---
|
|
46
50
|
|
|
@@ -79,7 +83,7 @@ WITHOUT the brain β drift | WITH RuvNet Brain β grounded
|
|
|
79
83
|
npx ruvnet-brain
|
|
80
84
|
```
|
|
81
85
|
|
|
82
|
-
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.
|
|
86
|
+
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.
|
|
83
87
|
|
|
84
88
|
> 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.
|
|
85
89
|
|
|
@@ -100,9 +104,21 @@ Registers the `search_ruvnet` MCP tool, the grounding skill, and the `UserPrompt
|
|
|
100
104
|
|
|
101
105
|
---
|
|
102
106
|
|
|
103
|
-
##
|
|
107
|
+
## Staying current β how updates work
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
You install once. After that, three mechanisms keep you on the current brain without you having to remember an update command.
|
|
110
|
+
|
|
111
|
+
- **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).
|
|
112
|
+
|
|
113
|
+
- **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.
|
|
114
|
+
|
|
115
|
+
- **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.)
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## β¨ What the knowledge bundle knows β the stack _down to the code_
|
|
120
|
+
|
|
121
|
+
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:
|
|
106
122
|
|
|
107
123
|
| Repo | Full-body code passages | |
|
|
108
124
|
|---|---:|---|
|
|
@@ -130,7 +146,7 @@ The expensive work happens **once, at build time**: every covered repo is deep-w
|
|
|
130
146
|
|
|
131
147
|
## How it changes Claude's behavior β it takes the wheel
|
|
132
148
|
|
|
133
|
-
Grounding is **
|
|
149
|
+
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.)
|
|
134
150
|
|
|
135
151
|

|
|
136
152
|
|
|
@@ -191,9 +207,27 @@ node plugin/test/run-tests.mjs # full plugin QA over real JSO
|
|
|
191
207
|
| **L1βL4 behavioral harness** | **all pass** | route Β· deep-recall (returns _code_) Β· implement (cites the API) Β· orchestrate (the hook drives the full pipeline) |
|
|
192
208
|
| **Plugin QA** | **26 / 26** | manifests, hook firing, MCP `initialize`/`tools/list`, capability battery |
|
|
193
209
|
| **Clean-room install** | **3 / 3** | download the published 512 MB bundle fresh β unzip β query β grounded, cited answers |
|
|
210
|
+
| **Unit tests** | **208 passing** Β· 10% of ALL source covered | `npm run test:cov` β the floor fails CI if it slips. 10% is the honest number over every shipped file; the previous "75%" measured a hand-picked 8-file subset |
|
|
211
|
+
| **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** |
|
|
212
|
+
| **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 |
|
|
213
|
+
|
|
214
|
+
<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>
|
|
194
215
|
|
|
195
216
|
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).
|
|
196
217
|
|
|
218
|
+
### The eval flywheel
|
|
219
|
+
|
|
220
|
+
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:
|
|
221
|
+
|
|
222
|
+
- **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.
|
|
223
|
+
- **routed** β the citation came from a repo that actually owns the capability.
|
|
224
|
+
|
|
225
|
+
`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.
|
|
226
|
+
|
|
227
|
+
**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.
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
197
231
|
**Query it directly (CLI):**
|
|
198
232
|
|
|
199
233
|
```bash
|
|
@@ -208,7 +242,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
208
242
|
|
|
209
243
|
## Honest status
|
|
210
244
|
|
|
211
|
-
This is
|
|
245
|
+
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:
|
|
212
246
|
|
|
213
247
|
- β
**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.
|
|
214
248
|
- β
**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).
|
|
@@ -225,7 +259,7 @@ This is **`v0.5.0-dev`** β we don't claim βdone,β βcomplete,β or βze
|
|
|
225
259
|
- `plugin/` β the Claude Code plugin (MCP server, grounding skill, `UserPromptSubmit` enforcement hook, marketplace manifest, test suite).
|
|
226
260
|
- `scripts/` β `gate.sh` (routing gate), `behavioral-l1-l4.mjs` (behavioral harness), `prove.mjs`, `build-bundle.mjs`, `brain-stamp.mjs`.
|
|
227
261
|
- `docs/` β [`VISION.md`](docs/VISION.md) (the why), [`adr/`](docs/adr/) (locked decisions incl. ADR-0008), [`DDD.md`](docs/DDD.md).
|
|
228
|
-
- `explainer/` β the source of the [live explainer](https://ruvnet-brain
|
|
262
|
+
- `explainer/` β the source of the [live explainer](https://isovision.ai/ruvnet-brain/).
|
|
229
263
|
- `SPEC.md` Β· `PROGRESS.md` β the master spec and the living, timestamped build log.
|
|
230
264
|
|
|
231
265
|
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.
|
|
@@ -234,7 +268,7 @@ The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-bra
|
|
|
234
268
|
|
|
235
269
|
## Links
|
|
236
270
|
|
|
237
|
-
- **βΆ Live explainer:** https://ruvnet-brain
|
|
271
|
+
- **βΆ Live explainer:** https://isovision.ai/ruvnet-brain/
|
|
238
272
|
- **Download / Release:** https://github.com/stuinfla/ruvnet-brain/releases/tag/v0.5.0-dev
|
|
239
273
|
- **rUv's RuvNet org:** https://github.com/ruvnet
|
|
240
274
|
- **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
|
|
|
@@ -39,6 +65,10 @@ const FLAG_DOCTOR = argv.includes('--doctor');
|
|
|
39
65
|
const FLAG_NO_VERIFY = argv.includes('--no-verify');
|
|
40
66
|
const FLAG_PIN = argv.includes('--pin'); // skip the latest-check, use the bundled default
|
|
41
67
|
const FLAG_DEMO = argv.includes('--demo'); // guided, real (non-fabricated) walkthrough of the brain in action
|
|
68
|
+
// ββ freshness flags β invoke/schedule the SELF-UPDATER the bundle already ships (kb/forge-update.mjs) ββ
|
|
69
|
+
const FLAG_UPDATE = argv.includes('--update'); // one-shot: pull the latest Release bundle into the installed brain now
|
|
70
|
+
const FLAG_ENABLE_NIGHTLY = argv.includes('--enable-nightly'); // schedule that update nightly (macOS LaunchAgent)
|
|
71
|
+
const FLAG_DISABLE_NIGHTLY = argv.includes('--disable-nightly'); // remove the nightly schedule
|
|
42
72
|
// ββ onboarding-experience flags (all optional; every offer is safe to decline) ββ
|
|
43
73
|
const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
|
|
44
74
|
const FLAG_WITH_STACK = argv.includes('--with-stack'); // add missing Ruflo/RuVector without prompting
|
|
@@ -288,19 +318,31 @@ async function obtainBundle(release) {
|
|
|
288
318
|
);
|
|
289
319
|
info(`version: ${c.bold((release && release.tag) || RELEASE_VERSION)}`);
|
|
290
320
|
info(`from: ${downloadUrl}`);
|
|
291
|
-
|
|
321
|
+
// Download into a PRIVATE, per-run temp DIR β never a predictable os.tmpdir()/ruvnet-brain-<pid>.zip
|
|
322
|
+
// filename (CWE-377: a guessable path invites a pre-created or symlinked file at that location to be
|
|
323
|
+
// clobbered, or the extraction target to be hijacked). mkdtempSync creates a fresh, unguessable,
|
|
324
|
+
// owner-only (0700) directory; we write the zip inside it. The exit handler guarantees the whole dir
|
|
325
|
+
// is removed on ANY exit β success, thrown error, or die()βprocess.exit β and the caller also removes
|
|
326
|
+
// it immediately on success so ~512MB isn't held for the rest of the install.
|
|
327
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-'));
|
|
328
|
+
process.on('exit', () => { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } });
|
|
329
|
+
const tmp = path.join(tmpDir, ASSET_NAME);
|
|
292
330
|
try {
|
|
293
331
|
console.log(` downloading the brain (${APPROX_SIZE})β¦`);
|
|
294
332
|
await download(downloadUrl, tmp);
|
|
295
333
|
} catch (e) {
|
|
296
|
-
try { fs.rmSync(
|
|
334
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
297
335
|
die(
|
|
298
336
|
`couldn't download the brain (${e.message}).`,
|
|
299
337
|
`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
338
|
);
|
|
301
339
|
}
|
|
302
340
|
ok(`downloaded to ${tmp}`);
|
|
303
|
-
|
|
341
|
+
// Best-effort fetch of the detached Ed25519 signature published alongside the asset (SEC-0010 #6).
|
|
342
|
+
// If present we verify it before extracting; if absent (a pre-signing release) we warn but proceed
|
|
343
|
+
// (transitional β see SIGNING_REQUIRED at the verify gate).
|
|
344
|
+
try { await download(`${downloadUrl}.sig`, `${tmp}.sig`); } catch { /* no published sig yet */ }
|
|
345
|
+
return { zipPath: tmp, tmpDir, downloaded: true };
|
|
304
346
|
}
|
|
305
347
|
|
|
306
348
|
// ββ step: unzip into the cache dir (flattening the top-level ruvnet-brain/ folder) βββββββββββββββ
|
|
@@ -379,14 +421,34 @@ function installReader(cacheDir) {
|
|
|
379
421
|
die(`\`npm\` isn't available, but the brain needs it for its reader.`, `Install Node.js (which includes npm) and re-run.`);
|
|
380
422
|
}
|
|
381
423
|
info('installing the local readerβ¦');
|
|
424
|
+
// Prefer `npm ci` for a PINNED, reproducible install when the bundle shipped its lockfile
|
|
425
|
+
// (SEC-0010 #8 β otherwise every install did a fully unpinned resolve). Fall back to `npm i`
|
|
426
|
+
// for older bundles that predate the shipped lockfile.
|
|
427
|
+
const hasLock = fs.existsSync(path.join(cacheDir, 'package-lock.json'));
|
|
428
|
+
const npmArgs = hasLock
|
|
429
|
+
? ['ci', '--no-audit', '--no-fund', '--loglevel=error']
|
|
430
|
+
: ['i', '--no-audit', '--no-fund', '--loglevel=error'];
|
|
382
431
|
try {
|
|
383
|
-
run('npm',
|
|
432
|
+
run('npm', npmArgs, {
|
|
384
433
|
cwd: cacheDir,
|
|
385
434
|
// silence npm's "new version available" update-notifier so the narration stays clean
|
|
386
435
|
env: { ...process.env, npm_config_update_notifier: 'false', npm_config_fund: 'false' },
|
|
387
436
|
});
|
|
388
437
|
} catch (e) {
|
|
389
|
-
|
|
438
|
+
// `npm ci` is strict (fails if lock and package.json disagree); fall back to `npm i` once
|
|
439
|
+
// rather than hard-failing a user's install on a lockfile mismatch.
|
|
440
|
+
if (hasLock) {
|
|
441
|
+
warn(`pinned install (npm ci) failed (${e.message}); retrying with npm i`);
|
|
442
|
+
try {
|
|
443
|
+
run('npm', ['i', '--no-audit', '--no-fund', '--loglevel=error'], {
|
|
444
|
+
cwd: cacheDir, env: { ...process.env, npm_config_update_notifier: 'false', npm_config_fund: 'false' },
|
|
445
|
+
});
|
|
446
|
+
} catch (e2) {
|
|
447
|
+
die(`the reader install failed (${e2.message}).`, `Re-run after checking your network / npm setup.`);
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
die(`the reader install failed (${e.message}).`, `Re-run after checking your network / npm setup.`);
|
|
451
|
+
}
|
|
390
452
|
}
|
|
391
453
|
ok('reader installed');
|
|
392
454
|
}
|
|
@@ -455,7 +517,37 @@ function verifyInstall(cacheDir) {
|
|
|
455
517
|
}
|
|
456
518
|
|
|
457
519
|
// ββ step: warm the model + prove grounding with one real question (best-effort, never fatal) ββββββ
|
|
458
|
-
|
|
520
|
+
// ββ BEGIN GENERATED: verify-citation.mjs (node scripts/embed-verifier.mjs) ββ
|
|
521
|
+
const VERIFY_CITATION_B64 = 'IyEvdXNyL2Jpbi9lbnYgbm9kZQovLyB2ZXJpZnktY2l0YXRpb24ubWpzIOKAlCBkZWNpZGUgd2hldGhlciBhbiBhbnN3ZXIgaXMgR1JPVU5ERUQsIGJ5IGdyb3VuZCB0cnV0aCByYXRoZXIgdGhhbiBieSB2aWJlcy4KLy8KLy8gV0hZIFRISVMgRVhJU1RTCi8vIC0tLS0tLS0tLS0tLS0tLQovLyBUaGUgb2xkIGdyb3VuZGluZyBjaGVjayBhc2tlZDogZG9lcyB0aGUgYW5zd2VyIGNvbnRhaW4gdGhlIHN0cmluZyAicnZmIiBvciAicnV2ZWN0b3IiPyBBIG1vZGVsCi8vIHRoYXQgaGFsbHVjaW5hdGVkICJqdXN0IHVzZSBSVkYhIiB3aXRoIHplcm8gc291cmNlcyBwYXNzZWQuIFNvIGRpZCBhbiBhbnN3ZXIgY2l0aW5nIGEgZmlsZSB0aGF0Ci8vIGRvZXMgbm90IGV4aXN0LiBLZXl3b3JkIHByZXNlbmNlIGlzIG5vdCBldmlkZW5jZSDigJQgYW4gTExNIHBhbmVsIG9uY2Ugc2NvcmVkIGEgemVyby1jaXRhdGlvbgovLyBhbnN3ZXIgOTgvMTAwIG9uIHRoaXMgcmVwby4KLy8KLy8gQSBjaXRhdGlvbiBpcyBvbmx5IHJlYWwgaWYgaXQgUkVTT0xWRVM6IHRoZSByZXBvIG11c3QgYmUgYW4gaW5kZXhlZCBzdG9yZSBvbiBkaXNrLCBhbmQgdGhlIGNpdGVkCi8vIGRvY3VtZW50IHBhdGggbXVzdCBhcHBlYXIgYXMgdGhlIGBwYXRoYCBvZiBhbiBhY3R1YWwgcGFzc2FnZSBpbnNpZGUgdGhhdCBzdG9yZSdzIHBhc3NhZ2VzIGZpbGUuCi8vIFRoYXQgaXMgY2hlY2thYmxlIHdpdGhvdXQgYSBtb2RlbCwgd2l0aG91dCB0aGUgbmV0d29yaywgYW5kIHdpdGhvdXQgdHJ1c3RpbmcgYW55dGhpbmcgdGhlIG1vZGVsCi8vIHNhaWQuIFRoaXMgbW9kdWxlIGRvZXMgZXhhY3RseSB0aGF0IGFuZCBub3RoaW5nIGVsc2UuCi8vCi8vIFRoZSByZWFkZXIgKGBmb3JnZS1hc2stYWxsLm1qc2ApIHByaW50cyBlYWNoIGhpdCBhczoKLy8gICAgICMxICByZXBvPWNvbmNlcHRzICBjZT0wLjIwMSAgdmVjPTAuODY4NiAga2luZD1kb2MKLy8gICAgIHBhdGggOiBjb25jZXB0cy9ydXZlY3Rvci9DQVJEL3J1dmVjdG9yLWNhcmQKLy8gICAgIHRpdGxlOiBydXZlY3RvciDigJQgQ2FwYWJpbGl0eQovLyBOb3RlIHRoZSBwcmludGVkIHBhdGggaXMgYDxyZXBvPi88ZG9jUGF0aD5gOyBpbnNpZGUgYGNvbmNlcHRzLnBhc3NhZ2VzLmpzb25sYCB0aGUgc3RvcmVkIGBwYXRoYAovLyBpcyBqdXN0IGBydXZlY3Rvci9DQVJEL3J1dmVjdG9yLWNhcmRgIChvcHRpb25hbGx5IHN1ZmZpeGVkIGAjMGAsIGAjMWAsIOKApiB3aGVuIGNodW5rZWQpLgoKaW1wb3J0IGZzIGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgcGF0aCBmcm9tICdub2RlOnBhdGgnOwppbXBvcnQgcmVhZGxpbmUgZnJvbSAnbm9kZTpyZWFkbGluZSc7CgovKiogUGFyc2UgdGhlIHJlYWRlcidzIHN0ZG91dCBpbnRvIHN0cnVjdHVyZWQgY2l0YXRpb25zLiBOZXZlciB0aHJvd3M7IHVucGFyc2VhYmxlIGlucHV0IOKGkiBbXS4gKi8KZXhwb3J0IGZ1bmN0aW9uIHBhcnNlQ2l0YXRpb25zKHN0ZG91dCkgewogIGNvbnN0IG91dCA9IFtdOwogIGNvbnN0IHRleHQgPSBTdHJpbmcoc3Rkb3V0ID8/ICcnKTsKICBjb25zdCBibG9ja1JlID0gL14jKFxkKylccytyZXBvPShcUyspKD86XHMrY2U9KC0/W1xkLl0rKSk/KD86XHMrdmVjPSgtP1tcZC5dKykpPyg/OlxzK2tpbmQ9KFxTKykpPy9nbTsKICBsZXQgbTsKICB3aGlsZSAoKG0gPSBibG9ja1JlLmV4ZWModGV4dCkpICE9PSBudWxsKSB7CiAgICBjb25zdCByZXN0ID0gdGV4dC5zbGljZShtLmluZGV4KTsKICAgIGNvbnN0IHBhdGhNID0gL15wYXRoXHMqOlxzKiguKykkL20uZXhlYyhyZXN0KTsKICAgIGNvbnN0IHRpdGxlTSA9IC9edGl0bGVccyo6XHMqKC4rKSQvbS5leGVjKHJlc3QpOwogICAgaWYgKCFwYXRoTSkgY29udGludWU7CiAgICBjb25zdCByZXBvID0gbVsyXTsKICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aE1bMV0udHJpbSgpOwogICAgLy8gU3RyaXAgdGhlIHJlcG8gcHJlZml4IHRoZSByZWFkZXIgYWRkcywgc28gdGhlIHJlbWFpbmRlciBjYW4gYmUgbWF0Y2hlZCBhZ2FpbnN0IHRoZSBzdG9yZS4KICAgIGNvbnN0IGRvY1BhdGggPSBmdWxsUGF0aC5zdGFydHNXaXRoKGAke3JlcG99L2ApID8gZnVsbFBhdGguc2xpY2UocmVwby5sZW5ndGggKyAxKSA6IGZ1bGxQYXRoOwogICAgb3V0LnB1c2goewogICAgICByYW5rOiBOdW1iZXIobVsxXSksCiAgICAgIHJlcG8sCiAgICAgIGNlOiBtWzNdICE9PSB1bmRlZmluZWQgPyBOdW1iZXIobVszXSkgOiBudWxsLAogICAgICB2ZWM6IG1bNF0gIT09IHVuZGVmaW5lZCA/IE51bWJlcihtWzRdKSA6IG51bGwsCiAgICAgIGtpbmQ6IG1bNV0gPz8gbnVsbCwKICAgICAgZnVsbFBhdGgsCiAgICAgIGRvY1BhdGgsCiAgICAgIHRpdGxlOiB0aXRsZU0gPyB0aXRsZU1bMV0udHJpbSgpIDogbnVsbCwKICAgIH0pOwogIH0KICByZXR1cm4gb3V0Owp9CgovKiogVGhlIHBhc3NhZ2VzIGZpbGVzIHRoYXQgY291bGQgaG9sZCBhIHJlcG8ncyBkb2N1bWVudHMg4oCUIHRoZSBzbGltIHN0b3JlIGFuZCB0aGUgZGVlcCBgLmJpZ2Agb25lLiAqLwpleHBvcnQgZnVuY3Rpb24gcGFzc2FnZXNGaWxlc0ZvcihyZXBvLCBrYkRpcikgewogIHJldHVybiBbcGF0aC5qb2luKGtiRGlyLCBgJHtyZXBvfS5wYXNzYWdlcy5qc29ubGApLCBwYXRoLmpvaW4oa2JEaXIsIGAke3JlcG99LmJpZy5wYXNzYWdlcy5qc29ubGApXQogICAgLmZpbHRlcigocCkgPT4gZnMuZXhpc3RzU3luYyhwKSk7Cn0KCi8qKiBUcnVlIHdoZW4gYHN0b3JlZGAgaXMgdGhlIGNpdGVkIGRvYywgYWxsb3dpbmcgZm9yIHRoZSBgI05gIGNodW5rIHN1ZmZpeCB0aGUgYnVpbGRlciBhcHBlbmRzLiAqLwpmdW5jdGlvbiBzYW1lUGF0aChzdG9yZWQsIGRvY1BhdGgpIHsKICByZXR1cm4gc3RvcmVkID09PSBkb2NQYXRoIHx8IHN0b3JlZC5zdGFydHNXaXRoKGAke2RvY1BhdGh9I2ApOwp9CgovKioKICogRG9lcyB0aGlzIGNpdGF0aW9uIHBvaW50IGF0IGEgcGFzc2FnZSB0aGF0IHJlYWxseSBleGlzdHMgb24gZGlzaz8KICogU3RyZWFtcyB0aGUgZmlsZSBhbmQgc3RvcHMgYXQgdGhlIGZpcnN0IG1hdGNoLCBzbyBhIDUwME1CIGAuYmlnYCBzdG9yZSBjb3N0cyBvbmx5IGFzIG11Y2ggYXMgaXQKICogdGFrZXMgdG8gcmVhY2ggdGhlIGhpdC4gQSBtYWxmb3JtZWQgSlNPTiBsaW5lIGlzIHNraXBwZWQsIG5ldmVyIGZhdGFsLgogKi8KZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNpdGF0aW9uUmVzb2x2ZXMoY2l0YXRpb24sIGtiRGlyKSB7CiAgY29uc3QgZmlsZXMgPSBwYXNzYWdlc0ZpbGVzRm9yKGNpdGF0aW9uLnJlcG8sIGtiRGlyKTsKICBpZiAoIWZpbGVzLmxlbmd0aCkgcmV0dXJuIHsgcmVzb2x2ZWQ6IGZhbHNlLCByZWFzb246ICduby1zdG9yZScsIGZpbGU6IG51bGwsIHN0b3JlZFBhdGg6IG51bGwgfTsKICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHsKICAgIGNvbnN0IHJsID0gcmVhZGxpbmUuY3JlYXRlSW50ZXJmYWNlKHsgaW5wdXQ6IGZzLmNyZWF0ZVJlYWRTdHJlYW0oZmlsZSksIGNybGZEZWxheTogSW5maW5pdHkgfSk7CiAgICB0cnkgewogICAgICBmb3IgYXdhaXQgKGNvbnN0IGxpbmUgb2YgcmwpIHsKICAgICAgICBpZiAoIWxpbmUpIGNvbnRpbnVlOwogICAgICAgIGxldCByZWM7CiAgICAgICAgdHJ5IHsgcmVjID0gSlNPTi5wYXJzZShsaW5lKTsgfSBjYXRjaCB7IGNvbnRpbnVlOyB9CiAgICAgICAgaWYgKHR5cGVvZiByZWM/LnBhdGggPT09ICdzdHJpbmcnICYmIHNhbWVQYXRoKHJlYy5wYXRoLCBjaXRhdGlvbi5kb2NQYXRoKSkgewogICAgICAgICAgcmV0dXJuIHsgcmVzb2x2ZWQ6IHRydWUsIHJlYXNvbjogJ29rJywgZmlsZTogcGF0aC5iYXNlbmFtZShmaWxlKSwgc3RvcmVkUGF0aDogcmVjLnBhdGggfTsKICAgICAgICB9CiAgICAgIH0KICAgIH0gZmluYWxseSB7CiAgICAgIHJsLmNsb3NlKCk7CiAgICB9CiAgfQogIHJldHVybiB7IHJlc29sdmVkOiBmYWxzZSwgcmVhc29uOiAncGF0aC1ub3QtaW4tc3RvcmUnLCBmaWxlOiBudWxsLCBzdG9yZWRQYXRoOiBudWxsIH07Cn0KCi8qKgogKiBUaGUgZ2F0ZS4gQW4gYW5zd2VyIGlzIGdyb3VuZGVkIG9ubHkgd2hlbiBpdCBjaXRlcyBhdCBsZWFzdCBvbmUgcGFzc2FnZSB0aGF0IHJlc29sdmVzIG9uIGRpc2suCiAqIFJldHVybnMgdGhlIHJlY2VpcHQgc28gYSBjYWxsZXIgY2FuIFBSSU5UIHRoZSBldmlkZW5jZSBpbnN0ZWFkIG9mIGFzc2VydGluZyBhIGNvbmNsdXNpb24uCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gdmVyaWZ5R3JvdW5kaW5nKHN0ZG91dCwga2JEaXIpIHsKICBjb25zdCBjaXRhdGlvbnMgPSBwYXJzZUNpdGF0aW9ucyhzdGRvdXQpOwogIGlmICghY2l0YXRpb25zLmxlbmd0aCkgewogICAgcmV0dXJuIHsgZ3JvdW5kZWQ6IGZhbHNlLCByZWFzb246ICduby1jaXRhdGlvbnMnLCBjaXRhdGlvbnM6IFtdLCByZWNlaXB0OiBudWxsIH07CiAgfQogIGZvciAoY29uc3QgY2l0YXRpb24gb2YgY2l0YXRpb25zKSB7CiAgICBjb25zdCByID0gYXdhaXQgY2l0YXRpb25SZXNvbHZlcyhjaXRhdGlvbiwga2JEaXIpOwogICAgaWYgKHIucmVzb2x2ZWQpIHsKICAgICAgcmV0dXJuIHsKICAgICAgICBncm91bmRlZDogdHJ1ZSwKICAgICAgICByZWFzb246ICdvaycsCiAgICAgICAgY2l0YXRpb25zLAogICAgICAgIHJlY2VpcHQ6IHsgcmVwbzogY2l0YXRpb24ucmVwbywgcGF0aDogY2l0YXRpb24uZnVsbFBhdGgsIHRpdGxlOiBjaXRhdGlvbi50aXRsZSwgZmlsZTogci5maWxlLCBzdG9yZWRQYXRoOiByLnN0b3JlZFBhdGggfSwKICAgICAgfTsKICAgIH0KICB9CiAgcmV0dXJuIHsgZ3JvdW5kZWQ6IGZhbHNlLCByZWFzb246ICdjaXRhdGlvbnMtZG8tbm90LXJlc29sdmUnLCBjaXRhdGlvbnMsIHJlY2VpcHQ6IG51bGwgfTsKfQo=';
|
|
522
|
+
// ββ END GENERATED ββ
|
|
523
|
+
|
|
524
|
+
// The verifier belongs next to the data it verifies, so it lives in the KB. But every bundle
|
|
525
|
+
// published before 2026-07-09 predates it, and telling those users "grounding not verifiable β
|
|
526
|
+
// re-run the installer" would send them in a circle, because re-running fetches the same bundle.
|
|
527
|
+
// So the installer CARRIES the verifier and writes it in when it's missing. A newer bundle's copy
|
|
528
|
+
// always wins: we never overwrite a file the bundle shipped.
|
|
529
|
+
function ensureVerifier(cacheDir) {
|
|
530
|
+
const p = path.join(cacheDir, 'verify-citation.mjs');
|
|
531
|
+
if (fs.existsSync(p)) return 'from-bundle';
|
|
532
|
+
if (!VERIFY_CITATION_B64) return 'unavailable';
|
|
533
|
+
try {
|
|
534
|
+
fs.writeFileSync(p, Buffer.from(VERIFY_CITATION_B64, 'base64').toString('utf8'), 'utf8');
|
|
535
|
+
return 'installed';
|
|
536
|
+
} catch { return 'unavailable'; }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function loadCitationVerifier(cacheDir) {
|
|
540
|
+
ensureVerifier(cacheDir);
|
|
541
|
+
const p = path.join(cacheDir, 'verify-citation.mjs');
|
|
542
|
+
if (!fs.existsSync(p)) return null;
|
|
543
|
+
try { return await import(pathToFileURL(p).href); } catch { return null; }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Proving grounding means proving the answer's CITATION RESOLVES β that the file it points at is a
|
|
547
|
+
// real, indexed passage on this disk. The old check here tested `/rvf|ruvector|hnsw/` against the
|
|
548
|
+
// answer text, which a hallucinated "just use RVF!" passes with zero sources. Keyword presence is
|
|
549
|
+
// not evidence. We now print the cited path as a receipt, so you can go look at it yourself.
|
|
550
|
+
async function smokeQuery(cacheDir) {
|
|
459
551
|
const ask = path.join(cacheDir, 'forge-ask-all.mjs');
|
|
460
552
|
if (!fs.existsSync(ask)) return { ran: false };
|
|
461
553
|
step(
|
|
@@ -465,6 +557,7 @@ function smokeQuery(cacheDir) {
|
|
|
465
557
|
const Q = 'How should I store embeddings in this project without running a server?';
|
|
466
558
|
info(`Q: ${c.cyan(`"${Q}"`)}`);
|
|
467
559
|
info(c.dim('(first run downloads a small local model once β this can take a minute)'));
|
|
560
|
+
const started = Date.now();
|
|
468
561
|
let r;
|
|
469
562
|
try {
|
|
470
563
|
// Relative filename + matching cwd (NOT the absolute `ask` path) β forge-ask-all.mjs only runs
|
|
@@ -472,7 +565,7 @@ function smokeQuery(cacheDir) {
|
|
|
472
565
|
// absolute path via spawnSync (no shell involved) that identity check silently fails on this
|
|
473
566
|
// machine, so main() never runs β exit 0, zero stdout, zero stderr, no exception. Looks like a
|
|
474
567
|
// 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', '
|
|
568
|
+
r = spawnSync('node', ['forge-ask-all.mjs', '--dir', cacheDir, '--q', Q, '--k', '3'], {
|
|
476
569
|
cwd: cacheDir,
|
|
477
570
|
encoding: 'utf8',
|
|
478
571
|
timeout: 240000,
|
|
@@ -482,15 +575,34 @@ function smokeQuery(cacheDir) {
|
|
|
482
575
|
warn("skipped the live test (couldn't launch the reader) β it'll warm on your first real question");
|
|
483
576
|
return { ran: false };
|
|
484
577
|
}
|
|
578
|
+
const secs = ((Date.now() - started) / 1000).toFixed(1);
|
|
485
579
|
const out = `${r.stdout || ''}`;
|
|
486
|
-
if (r.status
|
|
487
|
-
|
|
488
|
-
return { ran: true, grounded:
|
|
580
|
+
if (r.status !== 0 || !out.trim()) {
|
|
581
|
+
warn('no answer came back (first-run model download or offline) β the brain is installed; it\'ll warm on your first real question');
|
|
582
|
+
return { ran: true, grounded: false, reason: 'no-answer' };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const verifier = await loadCitationVerifier(cacheDir);
|
|
586
|
+
if (!verifier) {
|
|
587
|
+
info(`the brain answered in ${secs}s, but this bundle predates the citation verifier β`);
|
|
588
|
+
info(c.dim(' re-run `npx ruvnet-brain` to refresh it, and grounding will be PROVEN, not assumed'));
|
|
589
|
+
return { ran: true, grounded: null, reason: 'verifier-missing' };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const v = await verifier.verifyGrounding(out, cacheDir);
|
|
593
|
+
if (v.grounded) {
|
|
594
|
+
ok(`grounded in rUv's real source β verified in ${secs}s, not guessed β¦`);
|
|
595
|
+
console.log(` ${c.dim('cited:')} ${c.bold(v.receipt.path)}`);
|
|
596
|
+
if (v.receipt.title) console.log(` ${c.dim('title:')} ${v.receipt.title}`);
|
|
597
|
+
console.log(` ${c.dim('verified:')} that passage really exists in ${c.bold(v.receipt.file)}`);
|
|
598
|
+
return { ran: true, grounded: true, receipt: v.receipt, secs };
|
|
489
599
|
}
|
|
490
600
|
warn(
|
|
491
|
-
|
|
601
|
+
v.reason === 'no-citations'
|
|
602
|
+
? 'the answer cited no source at all β NOT grounded (re-run the installer to repair the KB)'
|
|
603
|
+
: "the answer's citations don't resolve to any indexed passage β NOT grounded (KB may be corrupt; re-run the installer)",
|
|
492
604
|
);
|
|
493
|
-
return { ran: true, grounded: false };
|
|
605
|
+
return { ran: true, grounded: false, reason: v.reason };
|
|
494
606
|
}
|
|
495
607
|
|
|
496
608
|
// ββ `--demo`: a guided, REAL walkthrough β proves grounding live, never fabricates output βββββββββ
|
|
@@ -556,7 +668,7 @@ function runDemo() {
|
|
|
556
668
|
}
|
|
557
669
|
|
|
558
670
|
// ββ `--doctor`: a standalone health check the user can run any time βββββββββββββββββββββββββββββββ
|
|
559
|
-
function doctor() {
|
|
671
|
+
async function doctor() {
|
|
560
672
|
printBanner('doctor');
|
|
561
673
|
console.log(c.dim('Checking every part of the install and reporting green/red.\n'));
|
|
562
674
|
const cacheDir = process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
@@ -583,13 +695,25 @@ function doctor() {
|
|
|
583
695
|
? ok('RuVector present β vector CLI / MCP available')
|
|
584
696
|
: warn('RuVector not found β answers still work. To add: claude mcp add ruvector --scope user -- npx -y ruvector mcp start');
|
|
585
697
|
const v = verifyInstall(cacheDir);
|
|
586
|
-
smokeQuery(cacheDir);
|
|
698
|
+
const smoke = await smokeQuery(cacheDir);
|
|
587
699
|
const allGreen = v.repos > 0 && v.reader && v.mcp;
|
|
588
700
|
console.log(
|
|
589
701
|
`\n ${allGreen ? c.green('β Healthy.') : c.yellow('! Needs attention.')} ${
|
|
590
702
|
allGreen ? 'The brain is installed and reachable.' : 'Re-run the installer to fix the warnings above.'
|
|
591
703
|
}`,
|
|
592
704
|
);
|
|
705
|
+
// Installed-and-reachable and actually-grounded are different claims. Keep them separate, so a
|
|
706
|
+
// healthy install can never be mistaken for proven grounding.
|
|
707
|
+
if (smoke.grounded === true) {
|
|
708
|
+
console.log(` ${c.green('β Grounding PROVEN.')} It answered from ${c.bold(smoke.receipt.path)} β a passage that`);
|
|
709
|
+
console.log(` really exists in your local KB. Checked in ${smoke.secs}s, no cloud, no API key.`);
|
|
710
|
+
} else if (smoke.grounded === false) {
|
|
711
|
+
console.log(` ${c.yellow('! Grounding NOT proven')} (${smoke.reason}). The install is present but the brain did not`);
|
|
712
|
+
console.log(' answer from a verifiable source. Re-run npx ruvnet-brain to repair the KB.');
|
|
713
|
+
} else if (smoke.grounded === null) {
|
|
714
|
+
console.log(` ${c.yellow('! Grounding not verifiable')} on this bundle β it predates the citation verifier.`);
|
|
715
|
+
console.log(' Re-run npx ruvnet-brain to refresh, then --doctor will prove it.');
|
|
716
|
+
}
|
|
593
717
|
if (allGreen) {
|
|
594
718
|
console.log(`\n ${c.bold('What this means for you:')}`);
|
|
595
719
|
console.log(` β’ ${c.bold('It works in EVERY project')} β user-level (global). Open Claude Code in any repo or VS Code`);
|
|
@@ -605,6 +729,163 @@ function doctor() {
|
|
|
605
729
|
);
|
|
606
730
|
}
|
|
607
731
|
|
|
732
|
+
// ββ `--update` / `--enable-nightly` / `--disable-nightly`: end-user freshness controls ββββββββββββ
|
|
733
|
+
// The brain bundle SHIPS its own self-updater (forge-update.mjs, right in the KB dir): it pulls the
|
|
734
|
+
// canonical Release bundle, backs the current copy up, extracts, and re-verifies with forge-guard β
|
|
735
|
+
// failing loud with no partial clobber. These flags never reimplement any of that; they only INVOKE
|
|
736
|
+
// it once (--update) or SCHEDULE it per-user (--enable-nightly). Nothing here ever publishes.
|
|
737
|
+
const NIGHTLY_LABEL = 'com.ruvnet.brain-update';
|
|
738
|
+
const resolvedKbDir = () =>
|
|
739
|
+
process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
740
|
+
const nightlyPlistPath = () =>
|
|
741
|
+
path.join(os.homedir(), 'Library', 'LaunchAgents', `${NIGHTLY_LABEL}.plist`);
|
|
742
|
+
// RUVNET_BRAIN_TEST=1 β write/remove the plist but NEVER call launchctl. Tests point HOME at a temp
|
|
743
|
+
// dir; bootstrapping a temp-dir plist into the user's real gui domain would mutate exactly the
|
|
744
|
+
// system state the tests promise not to touch.
|
|
745
|
+
const TEST_MODE = process.env.RUVNET_BRAIN_TEST === '1';
|
|
746
|
+
const xmlEscape = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
747
|
+
|
|
748
|
+
// The same nightly command, cron-flavored β the pattern forge-update.mjs documents in its header.
|
|
749
|
+
// 03:47 on purpose: an off-hour minute, so it never piles onto the :00 cron rush.
|
|
750
|
+
const cronExample = (kbDir) =>
|
|
751
|
+
`47 3 * * * cd ${kbDir} && ${process.execPath} forge-update.mjs --apply >> ${kbDir}/update.log 2>&1`;
|
|
752
|
+
|
|
753
|
+
function missingUpdaterHelp(kbDir) {
|
|
754
|
+
console.error(`\n${c.red('β can\'t update:')} ${c.bold('forge-update.mjs')} is missing from ${kbDir}.`);
|
|
755
|
+
console.error(` Either no brain is installed there, or the bundle predates the self-updater.`);
|
|
756
|
+
console.error(` Fix: re-run the installer β ${c.bold('npx ruvnet-brain')} ${c.dim('(add --force if a brain is already present)')}`);
|
|
757
|
+
console.error(` β the current bundle ships forge-update.mjs; then this command will work.`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function runUpdate() {
|
|
761
|
+
printBanner('update');
|
|
762
|
+
const kbDir = resolvedKbDir();
|
|
763
|
+
info(`brain dir: ${c.bold(kbDir)}`);
|
|
764
|
+
if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
|
|
765
|
+
missingUpdaterHelp(kbDir);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
info(c.dim("running the bundle's own self-updater (backs up first, re-verifies, never half-applies)β¦\n"));
|
|
769
|
+
// Relative filename + matching cwd β same launch convention as smokeQuery(); stdio:'inherit'
|
|
770
|
+
// streams the updater's narration live and unedited.
|
|
771
|
+
const r = spawnSync(process.execPath, ['forge-update.mjs', '--apply'], { cwd: kbDir, stdio: 'inherit' });
|
|
772
|
+
if (r.error) {
|
|
773
|
+
console.error(`\n${c.red('β update failed to launch:')} ${r.error.message}`);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
process.exit(r.status === null ? 1 : r.status); // exit with the updater's own verdict
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function enableNightly() {
|
|
780
|
+
printBanner('enable nightly updates');
|
|
781
|
+
const kbDir = resolvedKbDir();
|
|
782
|
+
|
|
783
|
+
if (process.platform !== 'darwin') {
|
|
784
|
+
info('The LaunchAgent scheduler is macOS-only (for now).');
|
|
785
|
+
info("On this platform, schedule the bundle's self-updater with cron β the pattern");
|
|
786
|
+
info('forge-update.mjs itself documents:');
|
|
787
|
+
console.log(`\n ${c.bold(cronExample(kbDir))}\n`);
|
|
788
|
+
info(`(${c.bold('crontab -e')}, paste the line, save. Remove the line to disable.)`);
|
|
789
|
+
return; // exit 0 β the user got the working recipe
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
info(`brain dir: ${c.bold(kbDir)}`);
|
|
793
|
+
if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
|
|
794
|
+
// Refuse to schedule a job that is guaranteed to fail every night β fail loud NOW instead.
|
|
795
|
+
missingUpdaterHelp(kbDir);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Template the plist to THIS user's kb dir + node binary. Quotes guard paths with spaces;
|
|
800
|
+
// xmlEscape guards the XML (>> and && must survive as shell operators after plist parsing).
|
|
801
|
+
const logPath = path.join(kbDir, 'update.log');
|
|
802
|
+
const shellCmd = `cd "${kbDir}" && "${process.execPath}" forge-update.mjs --apply >> "${logPath}" 2>&1`;
|
|
803
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
804
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
805
|
+
<plist version="1.0">
|
|
806
|
+
<dict>
|
|
807
|
+
<key>Label</key>
|
|
808
|
+
<string>${NIGHTLY_LABEL}</string>
|
|
809
|
+
<key>ProgramArguments</key>
|
|
810
|
+
<array>
|
|
811
|
+
<string>/bin/sh</string>
|
|
812
|
+
<string>-c</string>
|
|
813
|
+
<string>${xmlEscape(shellCmd)}</string>
|
|
814
|
+
</array>
|
|
815
|
+
<key>StartCalendarInterval</key>
|
|
816
|
+
<dict>
|
|
817
|
+
<key>Hour</key>
|
|
818
|
+
<integer>3</integer>
|
|
819
|
+
<key>Minute</key>
|
|
820
|
+
<integer>47</integer>
|
|
821
|
+
</dict>
|
|
822
|
+
<key>RunAtLoad</key>
|
|
823
|
+
<false/>
|
|
824
|
+
</dict>
|
|
825
|
+
</plist>
|
|
826
|
+
`;
|
|
827
|
+
const plistPath = nightlyPlistPath();
|
|
828
|
+
try {
|
|
829
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
830
|
+
fs.writeFileSync(plistPath, plist);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
console.error(`\n${c.red('β couldn\'t write the LaunchAgent:')} ${e.message}`);
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
ok(`wrote ${c.bold(plistPath)}`);
|
|
836
|
+
info(c.dim('runs nightly at 03:47 β an off-hour minute, so it never lands on the :00 rush'));
|
|
837
|
+
|
|
838
|
+
if (TEST_MODE) {
|
|
839
|
+
warn('RUVNET_BRAIN_TEST=1 β skipping launchctl bootout/bootstrap (plist written only)');
|
|
840
|
+
} else {
|
|
841
|
+
const uid = process.getuid();
|
|
842
|
+
// bootout first so re-running replaces the loaded job cleanly; failure just means "wasn't loaded".
|
|
843
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}/${NIGHTLY_LABEL}`], { stdio: 'ignore' });
|
|
844
|
+
const boot = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, plistPath], { encoding: 'utf8' });
|
|
845
|
+
if (boot.status === 0) ok('LaunchAgent loaded β your brain now updates while you sleep');
|
|
846
|
+
else {
|
|
847
|
+
warn(`launchctl bootstrap failed (${(boot.stderr || '').trim() || `exit ${boot.status}`}) β the plist is in place;`);
|
|
848
|
+
info(`load it yourself: ${c.bold(`launchctl bootstrap gui/${uid} ${plistPath}`)}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
console.log(`\n ${c.bold('Verify it:')} launchctl list | grep ${NIGHTLY_LABEL}`);
|
|
853
|
+
console.log(` ${c.bold('Watch it:')} tail ${logPath} ${c.dim('(appears after the first nightly run)')}`);
|
|
854
|
+
console.log(` ${c.bold('Disable it:')} npx ruvnet-brain --disable-nightly`);
|
|
855
|
+
console.log(`\n ${c.dim('It only ever PULLS the published Release bundle (backup + re-verify built in) β it never publishes,')}`);
|
|
856
|
+
console.log(` ${c.dim('and a night with no new Release is a clean no-op.')}\n`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function disableNightly() {
|
|
860
|
+
printBanner('disable nightly updates');
|
|
861
|
+
if (process.platform !== 'darwin') {
|
|
862
|
+
info('The LaunchAgent nightly is macOS-only, so nothing was scheduled here by this tool.');
|
|
863
|
+
info(`If you added the cron line yourself, remove it with: ${c.bold('crontab -e')}`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const plistPath = nightlyPlistPath();
|
|
867
|
+
const existed = fs.existsSync(plistPath);
|
|
868
|
+
if (TEST_MODE) {
|
|
869
|
+
warn('RUVNET_BRAIN_TEST=1 β skipping launchctl bootout (plist removal only)');
|
|
870
|
+
} else {
|
|
871
|
+
// Ignore failure: "not loaded" is exactly the state we want anyway.
|
|
872
|
+
spawnSync('launchctl', ['bootout', `gui/${process.getuid()}/${NIGHTLY_LABEL}`], { stdio: 'ignore' });
|
|
873
|
+
}
|
|
874
|
+
if (existed) {
|
|
875
|
+
try {
|
|
876
|
+
fs.rmSync(plistPath);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
console.error(`\n${c.red('β couldn\'t remove the LaunchAgent:')} ${e.message}`);
|
|
879
|
+
console.error(` Remove it yourself: rm ${plistPath}`);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
ok(`nightly updates disabled β removed ${plistPath}`);
|
|
883
|
+
} else {
|
|
884
|
+
ok('nightly updates were already off β nothing to remove (safe to run any time)');
|
|
885
|
+
}
|
|
886
|
+
info(`re-enable any time: ${c.bold('npx ruvnet-brain --enable-nightly')}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
608
889
|
// ββ tiny interactive yes/no β SAFE in non-TTY (returns the default; never blocks a piped install) ββ
|
|
609
890
|
function ask(question, def = false) {
|
|
610
891
|
if (FLAG_YES) return Promise.resolve(true);
|
|
@@ -829,9 +1110,14 @@ function success({ cacheDir, isCustom, plugin, env }) {
|
|
|
829
1110
|
β’ Want to see it answer, live, right now? ${c.bold('npx ruvnet-brain --demo')} β 2 real questions, real cited answers.`);
|
|
830
1111
|
|
|
831
1112
|
console.log(`\n ${c.bold('Set it up your way:')} this default is ${c.bold('global')} β live in every VS Code project automatically,`);
|
|
832
|
-
console.log(` which is what most people want. Want it different (project-only, moved, with the build stack
|
|
833
|
-
console.log(`
|
|
834
|
-
|
|
1113
|
+
console.log(` which is what most people want. Want it different (project-only, moved, with the build stack`);
|
|
1114
|
+
console.log(` added)? ${c.bold('Just tell Claude')} once it's on. You never have to learn its internals.`);
|
|
1115
|
+
|
|
1116
|
+
console.log(`\n ${c.bold('Staying current:')} nightly updates are ${c.bold('OFF by default')} β nothing runs on your machine unasked.`);
|
|
1117
|
+
console.log(` β’ one-shot check + update now: ${c.bold('npx ruvnet-brain --update')}`);
|
|
1118
|
+
console.log(` β’ nightly at 03:47 (macOS): ${c.bold('npx ruvnet-brain --enable-nightly')} ${c.dim('Β· off again: --disable-nightly')}`);
|
|
1119
|
+
console.log(` β’ Linux/Windows: the cron line documented in the brain's own ${c.bold('forge-update.mjs')}`);
|
|
1120
|
+
console.log(` ${c.dim('Either way, your copy only advances when a new Release is actually published.')}`);
|
|
835
1121
|
|
|
836
1122
|
console.log(`\n ${c.dim('You can\'t break anything β the plugin is disable-able and only acts on RuvNet-shaped work.')}`);
|
|
837
1123
|
console.log('');
|
|
@@ -849,6 +1135,11 @@ Usage:
|
|
|
849
1135
|
npx github:stuinfla/ruvnet-brain Same, but from the bleeding-edge GitHub commit
|
|
850
1136
|
npx ruvnet-brain --doctor Health-check an existing install (green/red per part)
|
|
851
1137
|
npx ruvnet-brain --demo Guided walkthrough β 2 real questions, real cited answers
|
|
1138
|
+
npx ruvnet-brain --update One-shot: pull the latest Release bundle into your installed brain
|
|
1139
|
+
(runs the bundle's own forge-update.mjs --apply: backup + re-verify)
|
|
1140
|
+
npx ruvnet-brain --enable-nightly Schedule that update nightly at 03:47 β macOS LaunchAgent;
|
|
1141
|
+
other platforms get the documented cron line. OFF by default.
|
|
1142
|
+
npx ruvnet-brain --disable-nightly Remove the nightly schedule (safe to run any time)
|
|
852
1143
|
node bin/install.mjs --version <tag> Install a specific Release tag (e.g. --version v0.4.0-dev)
|
|
853
1144
|
node bin/install.mjs --pin Skip the latest-check; use the bundled known-good version
|
|
854
1145
|
node bin/install.mjs --local Install from a repo clone's dist/ruvnet-brain.zip
|
|
@@ -870,8 +1161,11 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
870
1161
|
// ββ main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
871
1162
|
(async () => {
|
|
872
1163
|
if (FLAG_HELP) return showHelp();
|
|
873
|
-
if (FLAG_DOCTOR) return doctor();
|
|
1164
|
+
if (FLAG_DOCTOR) return await doctor();
|
|
874
1165
|
if (FLAG_DEMO) return runDemo();
|
|
1166
|
+
if (FLAG_UPDATE) return runUpdate();
|
|
1167
|
+
if (FLAG_ENABLE_NIGHTLY) return enableNightly();
|
|
1168
|
+
if (FLAG_DISABLE_NIGHTLY) return disableNightly();
|
|
875
1169
|
|
|
876
1170
|
printBanner('installer');
|
|
877
1171
|
console.log(c.dim("I'll set up the brain and the Claude Code plugin, explaining each step as I go.\n"));
|
|
@@ -909,10 +1203,31 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
909
1203
|
const localZipPresent =
|
|
910
1204
|
FLAG_LOCAL || fs.existsSync(path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip'));
|
|
911
1205
|
const release = localZipPresent ? null : await resolveRelease();
|
|
912
|
-
const { zipPath, downloaded } = await obtainBundle(release);
|
|
1206
|
+
const { zipPath, tmpDir, downloaded } = await obtainBundle(release);
|
|
1207
|
+
// Verify the Ed25519 signature BEFORE extracting a downloaded bundle into the user's config
|
|
1208
|
+
// (SEC-0010 #6 β trust root = keys/ruvnet-brain-signing.pub.pem shipped inside this package).
|
|
1209
|
+
// SIGNING_REQUIRED is transitional: while releases predate signing, a MISSING sig warns-and-proceeds
|
|
1210
|
+
// but a PRESENT-but-INVALID sig ALWAYS fails closed. Flip to true once every release is signed.
|
|
1211
|
+
const SIGNING_REQUIRED = false;
|
|
1212
|
+
if (downloaded && !FLAG_NO_VERIFY) {
|
|
1213
|
+
const sigPath = `${zipPath}.sig`;
|
|
1214
|
+
const hasSig = fs.existsSync(sigPath);
|
|
1215
|
+
if (!hasSig && !SIGNING_REQUIRED) {
|
|
1216
|
+
warn('this release is not signed yet β proceeding (bundle integrity not cryptographically verified)');
|
|
1217
|
+
} else {
|
|
1218
|
+
step('Verifying the bundle signature', 'so a tampered or MITM-swapped download can never be extracted');
|
|
1219
|
+
const { ok: valid, reason } = verifyBundle(zipPath, sigPath);
|
|
1220
|
+
if (!valid) {
|
|
1221
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
1222
|
+
die(`bundle signature check FAILED β ${reason}`,
|
|
1223
|
+
`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')}.)`);
|
|
1224
|
+
}
|
|
1225
|
+
ok(reason);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
913
1228
|
unzipInto(zipPath, cacheDir);
|
|
914
|
-
if (downloaded) {
|
|
915
|
-
try { fs.rmSync(
|
|
1229
|
+
if (downloaded && tmpDir) {
|
|
1230
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* leave temp behind, not fatal */ }
|
|
916
1231
|
}
|
|
917
1232
|
}
|
|
918
1233
|
|
|
@@ -920,7 +1235,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
920
1235
|
const plugin = wirePlugin();
|
|
921
1236
|
if (!FLAG_NO_VERIFY) {
|
|
922
1237
|
verifyInstall(cacheDir);
|
|
923
|
-
smokeQuery(cacheDir);
|
|
1238
|
+
await smokeQuery(cacheDir);
|
|
924
1239
|
}
|
|
925
1240
|
|
|
926
1241
|
// ββ onboarding: detect the toolkit + make offers (all optional, all non-fatal) ββ
|
package/package.json
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "One-command installer for RuvNet Brain
|
|
3
|
+
"version": "1.16.0-dev",
|
|
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
|
+
"claims:verify": "node scripts/claims-verify.mjs",
|
|
13
|
+
"version:sync": "node scripts/sync-version.mjs",
|
|
14
|
+
"test:unit": "vitest run tests/unit",
|
|
15
|
+
"test:cov": "vitest run tests/unit --coverage",
|
|
16
|
+
"test:all": "npm run test:unit && npm run test:integration && npm test",
|
|
17
|
+
"metaharness:fix": "node scripts/fix-metaharness-memretrieve.mjs --apply",
|
|
18
|
+
"metaharness:check": "node scripts/fix-metaharness-memretrieve.mjs --check",
|
|
19
|
+
"eval": "node scripts/eval-brain.mjs",
|
|
20
|
+
"eval:gate": "node scripts/eval-brain.mjs --gate",
|
|
21
|
+
"eval:record": "node scripts/eval-brain.mjs --record",
|
|
22
|
+
"embed:verifier": "node scripts/embed-verifier.mjs",
|
|
23
|
+
"embed:check": "node scripts/embed-verifier.mjs --check",
|
|
24
|
+
"gists:index": "node scripts/ingest-gists.mjs --index-only",
|
|
25
|
+
"gists:sync": "node scripts/ingest-gists.mjs && node kb/forge-big.mjs both --dir kb --name ruv-gists",
|
|
26
|
+
"test:integration": "vitest run tests/integration"
|
|
27
|
+
},
|
|
9
28
|
"files": [
|
|
10
29
|
"bin/install.mjs",
|
|
11
30
|
"README.md",
|
|
@@ -34,5 +53,10 @@
|
|
|
34
53
|
},
|
|
35
54
|
"homepage": "https://github.com/stuinfla/ruvnet-brain#readme",
|
|
36
55
|
"bugs": "https://github.com/stuinfla/ruvnet-brain/issues",
|
|
37
|
-
"author": "Stuart Kerr"
|
|
56
|
+
"author": "Stuart Kerr",
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@metaharness/darwin": "~0.8.0",
|
|
59
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
60
|
+
"vitest": "^4.1.10"
|
|
61
|
+
}
|
|
38
62
|
}
|