ruvnet-brain 2.1.0 β 2.4.3
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 +36 -9
- package/bin/install.mjs +295 -9
- package/config/model-router/catalog.template.json +220 -0
- package/config/model-router/policy.default.mjs +83 -0
- package/package.json +10 -3
- package/scripts/codex-routed.sh +38 -0
- package/scripts/model-router-engine.mjs +214 -0
- package/scripts/model-router-outcome.mjs +98 -0
- package/scripts/model-router-setup.mjs +141 -0
- package/scripts/model-router-status.mjs +160 -0
- package/scripts/route-cheap.mjs +152 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# π§ RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### π§ RuvNet Brain β [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
8
8
|
|
|
9
9
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack β delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
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
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
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)
|
|
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) (the nightly publishes fresh bundles as the corpus grows). Only moves when the underlying knowledge base is rebuilt β separate again from the two above.
|
|
23
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
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
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.
|
|
@@ -36,7 +36,24 @@
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## What's new in 2.
|
|
39
|
+
## What's new in 2.4 β it routes your money, and it can never break silently
|
|
40
|
+
|
|
41
|
+
**Shipped 2026-07-12, every piece proven live before it was written down:**
|
|
42
|
+
|
|
43
|
+
- **MetaHarness model router** β reviews each task and sends it to the cheapest model that can do the job, **your subscriptions first ($0)**: a harness-neutral decision engine (Claude Code *and* Codex), a verified-pricing catalog, and a cross-tier floor that never pays a metered API while a subscription model can do the work.
|
|
44
|
+
- **Per-user subscription profiles** β setup detects what it can *prove* (Codex's ChatGPT login, from the auth file's shape, never its secrets), **asks** what it can't ("Claude Pro or Max?"), and records every answer with its evidence basis (`verified` / `user-attested` / `assumed`). Your $0 is never assumed from someone else's machine.
|
|
45
|
+
- **The offer + the path** β one line ("want me to set up cost-optimal routing? yes/no"), two questions, then a live-derived display of your zero-cost options per harness and the cheapest verified paid-API fallbacks (DeepSeek V4 Flash at $0.077/MTok in β dispatch-proven, not just priced).
|
|
46
|
+
- **Goldie** β a weekly scheduled research job: refreshes verified OpenRouter pricing, flags >20% drift, cross-checks the model registries, auto-reinstates models the moment live evidence appears, and runs a headless research pass on the standing questions (how many buckets, best model per bucket). Its **first run caught a false "verified" stamp in its own catalog**.
|
|
47
|
+
- **The gong system** β a dark brain can never read as "(no results)" again: real-time screaming errors + an urgent phone push, a red banner in every new session, and a nightly canary β each layer proven by deliberately breaking the brain and watching it ring, then watching recovery clear it.
|
|
48
|
+
- **Key + spend canaries** β every provider API key live-probed nightly through the real shell chain (a dead key pages you within a day; it found and healed one on day one), alongside the API-spend watchdog.
|
|
49
|
+
- **An outcome log** β every routing decision and every human override is recorded as labeled data; the current documented-placeholder policy gets replaced by a learned one (per rUv's ADR-040/DRACO and ruflo ADR-149 evidence) once the labels accumulate.
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary><b>Earlier — what 2.0 proved</b> · the release where the brain stopped taking its own word for anything: 32 verified repos, a 120-question fail-closed eval gate, ~90% cheaper per-turn injection, and an 8-dimension evidence-backed scorecard (55 → 83 in two days). <i>Expand for the receipts.</i></summary>
|
|
55
|
+
|
|
56
|
+
### What's new in 2.0
|
|
40
57
|
|
|
41
58
|
**2.0 is the release where the brain got bigger β and, more importantly, stopped taking its own word for anything.** Every number below regenerates from an artifact on disk; the claims ledger (`node scripts/claims-verify.mjs`) re-checks the advertised ones in CI:
|
|
42
59
|
|
|
@@ -57,7 +74,7 @@ The depth jump wasn't tuning β it was two pipeline root-causes fixed for good:
|
|
|
57
74
|
|
|
58
75
|
### The scorecard β 8 dimensions, self-scored /100, every deduction evidenced β 2026-07-10 (evening re-score)
|
|
59
76
|
|
|
60
|
-
| Dimension | v1 (2026-07-09) | v2.0 (
|
|
77
|
+
| Dimension | v1 (2026-07-09) | v2.0 (2026-07-10) | Ξ | What moved it |
|
|
61
78
|
|---|---:|---:|---:|---|
|
|
62
79
|
| End-user experience | 54 | 83 | +29 | One-command install now offers nightly self-updates (default yes); the page lives on isovision.ai; publishing renews itself |
|
|
63
80
|
| Knowledge corpus | 71 | 88 | +17 | 24β32 verified repos; a 72/72 embeds-and-reads QA gate; full source depth restored β flagship went 0β2,996 source bodies |
|
|
@@ -71,6 +88,8 @@ The depth jump wasn't tuning β it was two pipeline root-causes fixed for good:
|
|
|
71
88
|
|
|
72
89
|
These are self-scores under a hard rule: **every deduction requires specific evidence, and a known architectural flaw caps a dimension at β€70 until the flaw is fixed** β *acting like rUv* spent the morning capped for its memory-layer flaw; the flaw was root-caused and fixed with proofs, the cap lifted, and it re-scored 72. Overall **55 β 83 in two days**, each number regenerating from a stored receipt β and the same scoring that produced these blocked a release mid-day. **That's why they're credible: scores you can trust beat scores that flatter.**
|
|
73
90
|
|
|
91
|
+
</details>
|
|
92
|
+
|
|
74
93
|
**The honest small print, kept visible:** warm query is ~21 s on the large corpus (it grew with depth; candidate dedup/pruning is the next optimization) Β· the 8 newest repos are findable by name but don't yet have primers/capability cards for described-need routing (coming) Β· the Windows CI job is new and unproven until its first green run.
|
|
75
94
|
|
|
76
95
|
---
|
|
@@ -159,7 +178,7 @@ You install once. After that, three mechanisms keep you on the current brain wit
|
|
|
159
178
|
|
|
160
179
|
## β¨ What the knowledge bundle knows β the stack _down to the code_
|
|
161
180
|
|
|
162
|
-
Earlier bundles knew only the **docs and architecture**. v0.5 began re-indexing 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. The table below is that v0.5 depth jump, kept as the before/after receipt; **2.0 went further still** β flagship `ruvector` alone now carries 28,018 passages and 2,996 full bodies (see
|
|
181
|
+
Earlier bundles knew only the **docs and architecture**. v0.5 began re-indexing 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. The table below is that v0.5 depth jump, kept as the before/after receipt; **2.0 went further still** β flagship `ruvector` alone now carries 28,018 passages and 2,996 full bodies (see the "what 2.0 proved" foldout near the top):
|
|
163
182
|
|
|
164
183
|
| Repo | Full-body code passages (v0.5) | |
|
|
165
184
|
|---|---:|---|
|
|
@@ -175,7 +194,7 @@ Plus: the **βtake the wheelβ behavioral pipeline** (below), a **4-level beha
|
|
|
175
194
|
|
|
176
195
|
## How it works
|
|
177
196
|
|
|
178
|
-
The expensive work happens **once, at build time**: every covered repo is deep-walked (whole files, full function bodies, plus a symbol index), embedded into **two** vector variants (MiniLM-384 for edge/portability, bge-768 for depth) stored on-disk in **RVF / HNSW**, and distilled into a concepts + capability layer of per-repo primers and cards. That's **129,
|
|
197
|
+
The expensive work happens **once, at build time**: every covered repo is deep-walked (whole files, full function bodies, plus a symbol index), embedded into **two** vector variants (MiniLM-384 for edge/portability, bge-768 for depth) stored on-disk in **RVF / HNSW**, and distilled into a concepts + capability layer of per-repo primers and cards. That's **129,011 source chunks**. At **query time**, `search_ruvnet` searches every repo's store at once, pools the hits, and runs them through **one cross-encoder rerank** on a common scale β so the truly relevant file wins regardless of which repo it lives in β then returns whole source files, each labeled by repo and path.
|
|
179
198
|
|
|
180
199
|

|
|
181
200
|
|
|
@@ -285,7 +304,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
285
304
|
|
|
286
305
|
This project versions in the open (see the live 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:
|
|
287
306
|
|
|
288
|
-
- β
**The grounding brain is real and proven** β 32 repos, 129,
|
|
307
|
+
- β
**The grounding brain is real and proven** β 32 repos, 129,011 chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
|
|
289
308
|
- β
**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).
|
|
290
309
|
- β
**Routing holds** β named 47/48, described 26/28, scenario 7/8; behavioral L1βL4 all pass; private stores fenced out of the public bundle (zero-leak verified).
|
|
291
310
|
- β οΈ **Two routing residuals** (above) β surfaced, not hidden.
|
|
@@ -303,14 +322,22 @@ This project versions in the open (see the live badge up top for the exact plugi
|
|
|
303
322
|
- `explainer/` β the source of the [live explainer](https://isovision.ai/ruvnet-brain/).
|
|
304
323
|
- `SPEC.md` Β· `PROGRESS.md` β the master spec and the living, timestamped build log.
|
|
305
324
|
|
|
306
|
-
The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-brain/releases/
|
|
325
|
+
The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-brain/releases/latest), not git β a fresh clone is lightweight; `npx` fetches the 512 MB bundle.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Community
|
|
330
|
+
|
|
331
|
+
- **Tell us how it went β one command:** `npx ruvnet-brain --feedback` prefills a [Discussion](https://github.com/stuinfla/ruvnet-brain/discussions) with your version + a 3-line health summary (you see exactly what's in it; never your queries, code, or paths) and opens it in your browser.
|
|
332
|
+
- **Questions, ideas, show-and-tell:** [Discussions](https://github.com/stuinfla/ruvnet-brain/discussions) Β· bugs go to [issues](https://github.com/stuinfla/ruvnet-brain/issues) β new issues and PRs page the maintainer's phone in real time, so first response is typically within 1 business day.
|
|
333
|
+
- **Contributors get credited:** merged PRs land in [`CONTRIBUTORS.md`](CONTRIBUTORS.md) β the two-signal hook gate and the `/brain-build` contract both started as user field reports. House rules: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md); build/test map: [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
307
334
|
|
|
308
335
|
---
|
|
309
336
|
|
|
310
337
|
## Links
|
|
311
338
|
|
|
312
339
|
- **βΆ Live explainer:** https://isovision.ai/ruvnet-brain/
|
|
313
|
-
- **Download / Release:** https://github.com/stuinfla/ruvnet-brain/releases/
|
|
340
|
+
- **Download / Release:** https://github.com/stuinfla/ruvnet-brain/releases/latest
|
|
314
341
|
- **rUv's RuvNet org:** https://github.com/ruvnet
|
|
315
342
|
- **Built by:** [Stuart Kerr β Isovision.ai](https://isovision.ai)
|
|
316
343
|
|
package/bin/install.mjs
CHANGED
|
@@ -65,6 +65,7 @@ const FLAG_DOCTOR = argv.includes('--doctor');
|
|
|
65
65
|
const FLAG_NO_VERIFY = argv.includes('--no-verify');
|
|
66
66
|
const FLAG_PIN = argv.includes('--pin'); // skip the latest-check, use the bundled default
|
|
67
67
|
const FLAG_DEMO = argv.includes('--demo'); // guided, real (non-fabricated) walkthrough of the brain in action
|
|
68
|
+
const FLAG_FEEDBACK = argv.includes('--feedback'); // prefill a GitHub Discussion (version + health, nothing private) and open it
|
|
68
69
|
// ββ freshness flags β invoke/schedule the SELF-UPDATER the bundle already ships (kb/forge-update.mjs) ββ
|
|
69
70
|
const FLAG_UPDATE = argv.includes('--update'); // one-shot: pull the latest Release bundle into the installed brain now
|
|
70
71
|
const FLAG_ENABLE_NIGHTLY = argv.includes('--enable-nightly'); // schedule that update nightly (macOS LaunchAgent)
|
|
@@ -491,11 +492,9 @@ function wirePlugin() {
|
|
|
491
492
|
}
|
|
492
493
|
|
|
493
494
|
// ββ step: verify the install is REAL (counts β never take "installed" on faith) ββββββββββββββββββ
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
"you should never have to trust the word \"installed\" β here's the proof on disk",
|
|
498
|
-
);
|
|
495
|
+
// Shared state-gatherer behind verifyInstall / --doctor / --feedback: what is REALLY on disk.
|
|
496
|
+
// Pure read, never prints β callers decide how to narrate (or, for --feedback, how to report) it.
|
|
497
|
+
function gatherInstallState(cacheDir) {
|
|
499
498
|
let repos = 0;
|
|
500
499
|
try {
|
|
501
500
|
repos = fs
|
|
@@ -504,14 +503,29 @@ function verifyInstall(cacheDir) {
|
|
|
504
503
|
} catch {
|
|
505
504
|
/* ignore */
|
|
506
505
|
}
|
|
506
|
+
return {
|
|
507
|
+
repos,
|
|
508
|
+
// A bare node_modules dir is not enough β on 2026-07-12 the dir test passed conceptually while
|
|
509
|
+
// the embedder was gone and every search failed. Check the two load-bearing packages directly.
|
|
510
|
+
reader:
|
|
511
|
+
fs.existsSync(path.join(cacheDir, 'node_modules', '@xenova', 'transformers', 'package.json'))
|
|
512
|
+
&& fs.existsSync(path.join(cacheDir, 'node_modules', '@ruvector')),
|
|
513
|
+
mcp: fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs')),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function verifyInstall(cacheDir) {
|
|
518
|
+
step(
|
|
519
|
+
'Verifying the brain is real and reachable',
|
|
520
|
+
"you should never have to trust the word \"installed\" β here's the proof on disk",
|
|
521
|
+
);
|
|
522
|
+
const { repos, reader, mcp } = gatherInstallState(cacheDir);
|
|
507
523
|
if (repos > 0) ok(`${repos} RuvNet repos indexed (vector stores present on disk)`);
|
|
508
524
|
else warn(`no .rvf stores found in ${cacheDir} β the brain may be incomplete (re-run with --force)`);
|
|
509
525
|
|
|
510
|
-
const reader = fs.existsSync(path.join(cacheDir, 'node_modules'));
|
|
511
526
|
if (reader) ok('local reader installed (vector reads happen offline β no cloud, no API key)');
|
|
512
|
-
else warn(
|
|
527
|
+
else warn(`reader deps missing β every search WILL fail until fixed: cd ${cacheDir} && npm i`);
|
|
513
528
|
|
|
514
|
-
const mcp = fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs'));
|
|
515
529
|
if (mcp) ok('search_ruvnet server present (this is what Claude calls to ground answers)');
|
|
516
530
|
else warn('forge-mcp-all.mjs missing β the brain unpacked incompletely');
|
|
517
531
|
|
|
@@ -759,6 +773,99 @@ async function doctor() {
|
|
|
759
773
|
);
|
|
760
774
|
}
|
|
761
775
|
|
|
776
|
+
// ββ `--feedback`: the easiest possible way to tell us how it went ββββββββββββββββββββββββββββββββ
|
|
777
|
+
// Composes a prefilled GitHub Discussion β brain version, platform, install age, and a 3-line
|
|
778
|
+
// --doctor-style health summary β SHOWS the user exactly what's in it (that's all there is), prints
|
|
779
|
+
// the URL, and opens the browser. Deliberately boring on privacy: no queries, no code, no paths,
|
|
780
|
+
// no env β every field is generic. The user still writes and posts the actual feedback themselves.
|
|
781
|
+
const DISCUSSIONS_URL = `https://github.com/${REPO}/discussions`;
|
|
782
|
+
|
|
783
|
+
function installedBrainVersion(cacheDir) {
|
|
784
|
+
// Same read the telemetry ping uses: the bundle stamps its Release tag into SOURCE.json.
|
|
785
|
+
// "unknown" is honest for a locally-built or pre-stamping bundle β never guess a tag.
|
|
786
|
+
try {
|
|
787
|
+
const j = JSON.parse(fs.readFileSync(path.join(cacheDir, 'SOURCE.json'), 'utf8'));
|
|
788
|
+
const v = String(j.releaseTag || '');
|
|
789
|
+
if (/^[A-Za-z0-9._-]{1,32}$/.test(v)) return v;
|
|
790
|
+
} catch { /* fall through */ }
|
|
791
|
+
return 'unknown';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function installAgeLine(cacheDir) {
|
|
795
|
+
// SOURCE.json's mtime is when the bundle last landed here (install or self-update) β say which.
|
|
796
|
+
for (const f of ['SOURCE.json', 'forge-mcp-all.mjs']) {
|
|
797
|
+
try {
|
|
798
|
+
const days = Math.floor((Date.now() - fs.statSync(path.join(cacheDir, f)).mtimeMs) / 86400000);
|
|
799
|
+
return days === 0 ? 'installed/updated today' : `installed/updated ${days} day${days === 1 ? '' : 's'} ago`;
|
|
800
|
+
} catch { /* try the next anchor file */ }
|
|
801
|
+
}
|
|
802
|
+
return 'not installed here';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// The last-3-lines-of---doctor health summary, from the SAME state --doctor reads (gatherInstallState
|
|
806
|
+
// + detectEnvironment) β counts and presence only, never a path.
|
|
807
|
+
function feedbackHealthLines(cacheDir) {
|
|
808
|
+
const s = gatherInstallState(cacheDir);
|
|
809
|
+
const env = detectEnvironment();
|
|
810
|
+
const allGreen = s.repos > 0 && s.reader && s.mcp;
|
|
811
|
+
return [
|
|
812
|
+
`${s.repos} repo stores on disk Β· reader ${s.reader ? 'ok' : 'MISSING'} Β· search_ruvnet ${s.mcp ? 'ok' : 'MISSING'}`,
|
|
813
|
+
`toolkit: Ruflo ${env.ruflo ? 'present' : 'not found'} Β· RuVector ${env.ruvector ? 'present' : 'not found'} Β· claude CLI ${env.claude ? 'present' : 'not found'}`,
|
|
814
|
+
allGreen ? 'verdict: Healthy β installed and reachable' : 'verdict: Needs attention β re-run npx ruvnet-brain',
|
|
815
|
+
];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function openInBrowser(url) {
|
|
819
|
+
if (TEST_MODE) return false; // tests: print the URL, never open anything
|
|
820
|
+
try {
|
|
821
|
+
// rundll32 on Windows (not `cmd /c start`): the URL's & would need cmd-metachar escaping there.
|
|
822
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
823
|
+
: IS_WIN ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
|
|
824
|
+
: ['xdg-open', [url]];
|
|
825
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore', shell: false });
|
|
826
|
+
return !r.error && r.status === 0;
|
|
827
|
+
} catch { return false; }
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function runFeedback() {
|
|
831
|
+
printBanner('feedback');
|
|
832
|
+
const cacheDir = resolvedKbDir();
|
|
833
|
+
const brainV = installedBrainVersion(cacheDir);
|
|
834
|
+
let installerV = 'unknown';
|
|
835
|
+
try { installerV = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')).version || 'unknown'; } catch { /* honest */ }
|
|
836
|
+
const health = feedbackHealthLines(cacheDir);
|
|
837
|
+
|
|
838
|
+
const title = `Feedback: RuvNet Brain ${brainV} on ${process.platform}`;
|
|
839
|
+
const body = [
|
|
840
|
+
`**Brain version:** ${brainV} Β· installer ${installerV}`,
|
|
841
|
+
`**Platform:** ${process.platform}/${process.arch} Β· node ${process.version}`,
|
|
842
|
+
`**Install age:** ${installAgeLine(cacheDir)}`,
|
|
843
|
+
`**Health (--doctor, last 3 lines):**`,
|
|
844
|
+
'```',
|
|
845
|
+
...health,
|
|
846
|
+
'```',
|
|
847
|
+
'',
|
|
848
|
+
'**What happened / what you\'d like:**',
|
|
849
|
+
'_(your words here β what you asked, what you got, what you wish it did)_',
|
|
850
|
+
'',
|
|
851
|
+
].join('\n');
|
|
852
|
+
|
|
853
|
+
console.log(c.dim('\nThis prefills a public GitHub Discussion with the block below β and NOTHING else.'));
|
|
854
|
+
console.log(c.dim('No queries, no code, no paths. You review it on GitHub and post it yourself.\n'));
|
|
855
|
+
console.log(body.split('\n').map((l) => ` ${c.dim('β')} ${l}`).join('\n'));
|
|
856
|
+
|
|
857
|
+
const url = `${DISCUSSIONS_URL}/new?category=general&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
|
858
|
+
console.log(`\n ${c.bold('Prefilled Discussion URL')} ${c.dim('(open it anywhere if no browser pops up):')}`);
|
|
859
|
+
console.log(` ${url}\n`);
|
|
860
|
+
|
|
861
|
+
if (TEST_MODE) {
|
|
862
|
+
warn('RUVNET_BRAIN_TEST=1 β not opening a browser (URL printed above)');
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (openInBrowser(url)) ok('opened in your browser β say anything, even one line helps');
|
|
866
|
+
else info(`couldn't open a browser here β copy the URL above into any browser to post`);
|
|
867
|
+
}
|
|
868
|
+
|
|
762
869
|
// ββ `--update` / `--enable-nightly` / `--disable-nightly`: end-user freshness controls ββββββββββββ
|
|
763
870
|
// The brain bundle SHIPS its own self-updater (forge-update.mjs, right in the KB dir): it pulls the
|
|
764
871
|
// canonical Release bundle, backs the current copy up, extracts, and re-verifies with forge-guard β
|
|
@@ -922,6 +1029,88 @@ function disableNightly() {
|
|
|
922
1029
|
info(`re-enable any time: ${c.bold('npx ruvnet-brain --enable-nightly')}`);
|
|
923
1030
|
}
|
|
924
1031
|
|
|
1032
|
+
// ββ spend guard: the alarm that catches a runaway agentic fleet BEFORE it drains a card βββββββββββ
|
|
1033
|
+
// WHY THIS SHIPS: on 2026-07-09 an automated QE fleet spawned 374+ headless agents, each billing the
|
|
1034
|
+
// Anthropic API on Sonnet, and burned ~$1,600 SILENTLY while a paid Max plan sat unused β nothing
|
|
1035
|
+
// alerted. That is the exact failure this guard makes impossible: a tiny hourly watchdog that trips
|
|
1036
|
+
// the moment automated agents flood a project (burst detector, no key needed) or β with
|
|
1037
|
+
// ANTHROPIC_ADMIN_KEY β daily API spend crosses a threshold. Alert-only; it NEVER spends. Same
|
|
1038
|
+
// non-fatal, TEST_MODE-aware, default-yes contract as the nightly updater above.
|
|
1039
|
+
const SPEND_GUARD_LABEL = 'com.ruvnet.spend-watchdog';
|
|
1040
|
+
const spendGuardScriptPath = () => path.join(os.homedir(), '.claude', 'scripts', 'api-spend-watchdog.mjs');
|
|
1041
|
+
const spendGuardPlistPath = () => path.join(os.homedir(), 'Library', 'LaunchAgents', `${SPEND_GUARD_LABEL}.plist`);
|
|
1042
|
+
|
|
1043
|
+
function enableSpendGuard() {
|
|
1044
|
+
// The npx checkout is ephemeral, so copy the bundled watchdog to a persistent home the launchd
|
|
1045
|
+
// job can point at for good.
|
|
1046
|
+
const src = path.join(__dirname, 'api-spend-watchdog.mjs');
|
|
1047
|
+
const dst = spendGuardScriptPath();
|
|
1048
|
+
if (!fs.existsSync(src)) { warn('spend-watchdog source missing from this bundle β skipping (non-fatal)'); return 'no-source'; }
|
|
1049
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
1050
|
+
fs.copyFileSync(src, dst);
|
|
1051
|
+
ok(`installed the spend watchdog β ${c.bold(dst)}`);
|
|
1052
|
+
|
|
1053
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1054
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1055
|
+
<plist version="1.0">
|
|
1056
|
+
<dict>
|
|
1057
|
+
<key>Label</key><string>${SPEND_GUARD_LABEL}</string>
|
|
1058
|
+
<key>ProgramArguments</key>
|
|
1059
|
+
<array>
|
|
1060
|
+
<string>${process.execPath}</string>
|
|
1061
|
+
<string>${dst}</string>
|
|
1062
|
+
</array>
|
|
1063
|
+
<key>StartInterval</key><integer>3600</integer>
|
|
1064
|
+
<key>RunAtLoad</key><true/>
|
|
1065
|
+
<key>EnvironmentVariables</key>
|
|
1066
|
+
<dict>
|
|
1067
|
+
<key>SPEND_ALERT_USD</key><string>50</string>
|
|
1068
|
+
<key>SPEND_BURST_AGENTS</key><string>20</string>
|
|
1069
|
+
</dict>
|
|
1070
|
+
</dict>
|
|
1071
|
+
</plist>
|
|
1072
|
+
`;
|
|
1073
|
+
const plistPath = spendGuardPlistPath();
|
|
1074
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
1075
|
+
fs.writeFileSync(plistPath, plist);
|
|
1076
|
+
ok(`wrote ${c.bold(plistPath)} β runs hourly, alert-only`);
|
|
1077
|
+
|
|
1078
|
+
if (TEST_MODE) { warn('RUVNET_BRAIN_TEST=1 β skipping launchctl (plist written only)'); return 'test'; }
|
|
1079
|
+
const uid = process.getuid();
|
|
1080
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}/${SPEND_GUARD_LABEL}`], { stdio: 'ignore' });
|
|
1081
|
+
const boot = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, plistPath], { encoding: 'utf8' });
|
|
1082
|
+
if (boot.status === 0) ok('spend watchdog is live β it warns you the moment a fleet runs away');
|
|
1083
|
+
else { warn(`launchctl bootstrap failed (${(boot.stderr || '').trim() || `exit ${boot.status}`}) β the plist is in place;`); info(`load it: ${c.bold(`launchctl bootstrap gui/${uid} ${plistPath}`)}`); }
|
|
1084
|
+
return 'enabled';
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Exported (testable under RUVNET_BRAIN_IMPORT_ONLY=1, like offerNightly). Never throws β the caller
|
|
1088
|
+
// also guards, because a finished install must never be broken by an optional safety offer.
|
|
1089
|
+
export async function offerSpendGuard() {
|
|
1090
|
+
if (FLAG_NO_NIGHTLY_PROMPT || TEST_MODE) return 'suppressed';
|
|
1091
|
+
if (process.platform !== 'darwin') return 'unsupported';
|
|
1092
|
+
if (fs.existsSync(spendGuardPlistPath())) { ok('spend watchdog already installed β runaway API spend will alert you'); return 'already-on'; }
|
|
1093
|
+
|
|
1094
|
+
step(
|
|
1095
|
+
'One more safety net β a spend watchdog',
|
|
1096
|
+
'agentic tools can bill the paid API in the background; this alarm catches a runaway before it drains your card',
|
|
1097
|
+
);
|
|
1098
|
+
info(`${c.bold('Strongly recommended:')} an hourly check that alerts you the moment an automated agent`);
|
|
1099
|
+
info('fleet floods a project β the pattern that has quietly burned real money. Alert-only, never spends.');
|
|
1100
|
+
|
|
1101
|
+
if (!process.stdin.isTTY && !FLAG_YES) { info(`No terminal to prompt on β install it any time by re-running ${c.bold('npx ruvnet-brain')}`); return 'recommended'; }
|
|
1102
|
+
let yes = true;
|
|
1103
|
+
if (!FLAG_YES) {
|
|
1104
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1105
|
+
const answer = await new Promise((resolve) => rl.question(` ${c.cyan('?')} Install the spend watchdog? ${c.dim('[Y/n]')} `, resolve));
|
|
1106
|
+
rl.close();
|
|
1107
|
+
yes = parseNightlyAnswer(answer);
|
|
1108
|
+
}
|
|
1109
|
+
if (!yes) { info(`No problem β install it any time by re-running ${c.bold('npx ruvnet-brain')}`); return 'declined'; }
|
|
1110
|
+
try { enableSpendGuard(); } catch (e) { warn(`spend guard install skipped: ${e.message}`); return 'error'; }
|
|
1111
|
+
return 'enabled';
|
|
1112
|
+
}
|
|
1113
|
+
|
|
925
1114
|
// ββ step: offer nightly auto-updates at the end of a successful install (recommended, default YES) ββ
|
|
926
1115
|
// Requirement: a default `npx ruvnet-brain` run must never leave the user unaware of nightly
|
|
927
1116
|
// auto-updates β it VERY CLEARLY recommends them, asks, and DEFAULTS TO YES. Before this, the
|
|
@@ -938,6 +1127,92 @@ export function parseNightlyAnswer(answer) {
|
|
|
938
1127
|
// suppression flags) is testable in-process under RUVNET_BRAIN_IMPORT_ONLY=1 without a real install.
|
|
939
1128
|
// Returns a status string; never throws (the caller also guards β a finished install must never
|
|
940
1129
|
// be broken by an optional offer).
|
|
1130
|
+
// ββ MetaHarness router: config materialization + THIS user's subscription profile (2026-07-12) ββ
|
|
1131
|
+
// Stuart's mandate: subscription-awareness must be per-user. Detect what the machine can PROVE
|
|
1132
|
+
// (Codex auth mode from ~/.codex/auth.json's SHAPE β never its secrets), ASK what it can't (Claude
|
|
1133
|
+
// plan tiers aren't probeable from disk), and RECORD both with their basis, so the router's
|
|
1134
|
+
// $0-floor never assumes a plan this user doesn't have (billing them) or misses one they do
|
|
1135
|
+
// (wasting it). Config templates ship in the npm package's config/; router tools are copied to
|
|
1136
|
+
// ~/.claude/model-router/bin/ because the npx run dir vanishes after install. Never overwrites
|
|
1137
|
+
// user-edited files. Non-fatal like every offer.
|
|
1138
|
+
export async function offerRouterProfile() {
|
|
1139
|
+
if (TEST_MODE) return 'suppressed';
|
|
1140
|
+
const routerDir = path.join(os.homedir(), '.claude', 'model-router');
|
|
1141
|
+
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
1142
|
+
step(
|
|
1143
|
+
'MetaHarness model router β the right model for each task, cheapest first',
|
|
1144
|
+
"your subscription models are $0 marginal; the router just needs to know which ones YOU have",
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
fs.mkdirSync(path.join(routerDir, 'bin'), { recursive: true });
|
|
1148
|
+
for (const [src, dst] of [['catalog.template.json', 'catalog.json'], ['policy.default.mjs', 'policy.default.mjs']]) {
|
|
1149
|
+
const s = path.join(pkgRoot, 'config', 'model-router', src);
|
|
1150
|
+
const d = path.join(routerDir, dst);
|
|
1151
|
+
if (fs.existsSync(s) && !fs.existsSync(d)) { fs.copyFileSync(s, d); ok(`installed ${dst} (edit freely β goldie keeps prices fresh where scheduled)`); }
|
|
1152
|
+
}
|
|
1153
|
+
let copied = 0;
|
|
1154
|
+
for (const t of ['model-router-engine.mjs', 'model-router-setup.mjs', 'model-router-status.mjs', 'model-router-outcome.mjs', 'route-cheap.mjs', 'codex-routed.sh']) {
|
|
1155
|
+
const s = path.join(pkgRoot, 'scripts', t);
|
|
1156
|
+
if (fs.existsSync(s)) { fs.copyFileSync(s, path.join(routerDir, 'bin', t)); copied++; }
|
|
1157
|
+
}
|
|
1158
|
+
if (copied) {
|
|
1159
|
+
try { fs.chmodSync(path.join(routerDir, 'bin', 'codex-routed.sh'), 0o755); } catch { /* not fatal */ }
|
|
1160
|
+
ok(`${copied} router tools at ~/.claude/model-router/bin/ (stable path β the npx dir vanishes)`);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const profilePath = path.join(routerDir, 'profile.json');
|
|
1164
|
+
if (fs.existsSync(profilePath)) { ok('subscription profile already exists β routing already uses it'); return 'already'; }
|
|
1165
|
+
|
|
1166
|
+
// Codex is the one subscription we can PROVE: OAuth tokens in auth.json = signed in with ChatGPT
|
|
1167
|
+
// (Plus/Pro/Business all include Codex). An API key instead = metered per-token.
|
|
1168
|
+
let codexAuth = null;
|
|
1169
|
+
try {
|
|
1170
|
+
const a = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.codex', 'auth.json'), 'utf8'));
|
|
1171
|
+
codexAuth = a.tokens ? 'chatgpt' : a.OPENAI_API_KEY ? 'api-key' : null;
|
|
1172
|
+
} catch { /* codex absent or not authed */ }
|
|
1173
|
+
|
|
1174
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1175
|
+
// This installer's audience is Claude Code users β claude-code is available by definition.
|
|
1176
|
+
let claudeSub = true;
|
|
1177
|
+
let claudeBasis = `assumed: installing the Claude Code brain (${today}); confirm with model-router-setup.mjs --show`;
|
|
1178
|
+
let codexSub = codexAuth === 'chatgpt';
|
|
1179
|
+
let codexBasis =
|
|
1180
|
+
codexAuth === 'chatgpt' ? `verified: ~/.codex/auth.json ChatGPT OAuth tokens (${today})`
|
|
1181
|
+
: codexAuth === 'api-key' ? `verified: ~/.codex/auth.json API key β METERED, not subscription (${today})`
|
|
1182
|
+
: `detected: codex not authed on this machine (${today})`;
|
|
1183
|
+
|
|
1184
|
+
if (process.stdin.isTTY && !FLAG_YES) {
|
|
1185
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1186
|
+
const q = (text) => new Promise((resolve) => rl.question(text, resolve));
|
|
1187
|
+
const a1 = await q(` ${c.cyan('?')} Do you have a Claude subscription (Pro or Max) covering your Claude Code use? ${c.dim('[Y/n]')} `);
|
|
1188
|
+
claudeSub = !/^n/i.test((a1 || '').trim());
|
|
1189
|
+
claudeBasis = `user-attested ${today}`;
|
|
1190
|
+
if (codexAuth === 'chatgpt') {
|
|
1191
|
+
info(`Codex: verified signed in with ChatGPT β your ChatGPT plan covers it ($0). Nothing to ask.`);
|
|
1192
|
+
} else {
|
|
1193
|
+
const a2 = await q(` ${c.cyan('?')} Do you also use OpenAI's Codex CLI signed in with a ChatGPT subscription? ${c.dim('[y/N]')} `);
|
|
1194
|
+
codexSub = /^y/i.test((a2 || '').trim());
|
|
1195
|
+
codexBasis = `user-attested ${today}${codexSub && codexAuth !== 'chatgpt' ? ' (auth.json does not show ChatGPT login yet β run `codex login`)' : ''}`;
|
|
1196
|
+
}
|
|
1197
|
+
rl.close();
|
|
1198
|
+
} else {
|
|
1199
|
+
info('No interactive terminal β recording detections with labeled assumptions. Refine any time:');
|
|
1200
|
+
info(` ${c.bold('node ~/.claude/model-router/bin/model-router-setup.mjs')}`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const profile = {
|
|
1204
|
+
updated: today,
|
|
1205
|
+
harnesses: {
|
|
1206
|
+
'claude-code': { available: true, subscription: claudeSub, plan: claudeSub ? 'pro-or-max' : 'api-billed', basis: claudeBasis },
|
|
1207
|
+
codex: { available: codexAuth !== null, subscription: codexSub, plan: codexAuth, basis: codexBasis },
|
|
1208
|
+
},
|
|
1209
|
+
keys: Object.fromEntries(['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_API_KEY', 'XAI_API_KEY'].map((k) => [k, !!process.env[k]])),
|
|
1210
|
+
};
|
|
1211
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2) + '\n');
|
|
1212
|
+
ok(`subscription profile saved β Claude Code: ${claudeSub ? 'subscription ($0)' : 'API-billed'}; Codex: ${codexSub ? 'subscription ($0)' : codexAuth === 'api-key' ? 'METERED' : 'not in use'}`);
|
|
1213
|
+
return 'created';
|
|
1214
|
+
}
|
|
1215
|
+
|
|
941
1216
|
export async function offerNightly() {
|
|
942
1217
|
// Suppressed outright: --no-nightly-prompt (the user said don't ask) and RUVNET_BRAIN_TEST=1
|
|
943
1218
|
// (tests must stay non-interactive and must never schedule anything).
|
|
@@ -1326,7 +1601,7 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
|
|
|
1326
1601
|
// One tasteful ask, at the moment the value was just delivered β never repeated by the plugin
|
|
1327
1602
|
// more than once ever (see session-start.sh's stamped one-liner).
|
|
1328
1603
|
console.log(`\n ${c.bold('If it earns it:')} a GitHub star helps other people find the brain β`);
|
|
1329
|
-
console.log(` ${c.bold('https://github.com/stuinfla/ruvnet-brain')} ${c.dim('Β· feedback
|
|
1604
|
+
console.log(` ${c.bold('https://github.com/stuinfla/ruvnet-brain')} ${c.dim('Β· feedback in one command: npx ruvnet-brain --feedback')}`);
|
|
1330
1605
|
|
|
1331
1606
|
console.log(`\n ${c.dim('You can\'t break anything β the plugin is disable-able and only acts on RuvNet-shaped work.')}`);
|
|
1332
1607
|
console.log('');
|
|
@@ -1344,6 +1619,9 @@ Usage:
|
|
|
1344
1619
|
npx github:stuinfla/ruvnet-brain Same, but from the bleeding-edge GitHub commit
|
|
1345
1620
|
npx ruvnet-brain --doctor Health-check an existing install (green/red per part)
|
|
1346
1621
|
npx ruvnet-brain --demo Guided walkthrough β 2 real questions, real cited answers
|
|
1622
|
+
npx ruvnet-brain --feedback Tell us how it went β prefills a GitHub Discussion with your brain
|
|
1623
|
+
version, platform, and a 3-line health summary (you see exactly
|
|
1624
|
+
what's in it; never your queries, code, or paths), then opens it
|
|
1347
1625
|
npx ruvnet-brain --update One-shot: pull the latest Release bundle into your installed brain
|
|
1348
1626
|
(runs the bundle's own forge-update.mjs --apply: backup + re-verify)
|
|
1349
1627
|
npx ruvnet-brain --enable-nightly Schedule that update nightly at 03:47 β macOS LaunchAgent;
|
|
@@ -1379,6 +1657,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
1379
1657
|
if (FLAG_HELP) return showHelp();
|
|
1380
1658
|
if (FLAG_DOCTOR) return await doctor();
|
|
1381
1659
|
if (FLAG_DEMO) return runDemo();
|
|
1660
|
+
if (FLAG_FEEDBACK) return runFeedback();
|
|
1382
1661
|
if (FLAG_UPDATE) return runUpdate();
|
|
1383
1662
|
if (FLAG_ENABLE_NIGHTLY) return enableNightly();
|
|
1384
1663
|
if (FLAG_DISABLE_NIGHTLY) return disableNightly();
|
|
@@ -1462,6 +1741,13 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
1462
1741
|
// and asking, DEFAULTING TO YES (TTY + macOS + not already on). Non-fatal like every other offer.
|
|
1463
1742
|
let nightly = 'skipped';
|
|
1464
1743
|
try { nightly = await offerNightly(); } catch { /* never let the offer break a finished install */ }
|
|
1744
|
+
// A spend watchdog, offered right after the updater: agentic tools can bill the paid API in the
|
|
1745
|
+
// background (a real 2026-07-09 incident burned ~$1,600 silently). This alarm makes that loud.
|
|
1746
|
+
// Non-fatal like every other offer β a safety net can never break a finished install.
|
|
1747
|
+
try { await offerSpendGuard(); } catch { /* a safety offer must never break a finished install */ }
|
|
1748
|
+
// Per-user subscription profile + router config (Stuart's mandate 2026-07-12: detect, ASK,
|
|
1749
|
+
// verify, record β never assume this user's subscriptions match anyone else's).
|
|
1750
|
+
try { await offerRouterProfile(); } catch { /* router setup must never break a finished install */ }
|
|
1465
1751
|
// Anonymous usage counts β OPT-IN, asked once ever, right after the nightly offer. Same rule:
|
|
1466
1752
|
// an optional offer can never break a finished install.
|
|
1467
1753
|
try { await offerTelemetry(cacheDir); } catch { /* fail-private: unanswered = OFF */ }
|