ruvnet-brain 2.0.0 β†’ 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 2.0.0 β€” updated 2026-07-10 08:15 EDT](https://img.shields.io/badge/version_2.0.0-updated_2026--07--10_08:15_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 2.4.1 β€” updated 2026-07-12 20:50 EDT](https://img.shields.io/badge/version_2.4.1-updated_2026--07--12_20:50_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](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
 
@@ -36,6 +36,18 @@
36
36
 
37
37
  ---
38
38
 
39
+ ## What's new in 2.3–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 (2.3)** β€” 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 2.2's 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
+
39
51
  ## What's new in 2.0
40
52
 
41
53
  **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:
@@ -48,7 +60,7 @@
48
60
  | **Retrieval eval** | 12 frozen questions | **120 frozen, hash-pinned questions** across 5 strata; promotion gated on Wilson lower bounds, fail-closed β€” it blocked a real release this morning, which is the feature working |
49
61
  | **Token cost** | 6,183 bytes injected per hook turn Β· zero self-measurement | **684 bytes (~90% cut)** with an eval-PASS proving zero quality loss Β· a live token meter measuring real bytes/tokens per prompt class Β· ~27% faster repeat queries via the KB cache |
50
62
  | **rUv's gists** | not indexed | **437 gists** indexed with per-chunk freshness/provenance banners, refreshed nightly with cost-disciplined skip |
51
- | **Reliability** | claims were prose | **claims ledger** (5 marketing claims mechanically re-verified in CI) Β· integration tests in CI incl. Linux Β· a Windows CI job Β· an honest coverage denominator (all source files) |
63
+ | **Reliability** | claims were prose | **claims ledger** (6 marketing claims mechanically re-verified in CI) Β· integration tests in CI incl. Linux Β· a Windows CI job Β· an honest coverage denominator (all source files) |
52
64
  | **Autonomy** | hooks asked questions to an empty room β€” the #1 real-user complaint | **`/loop` contract** β€” checkpoint / resume / done-criteria; hooks detect autonomous mode and stop asking β€” 10 mutation-verified tests |
53
65
  | **Publishing** | manual npm token Β· a nightly release PATH bug | **self-renewing npm token** (launchd daemon, proven end-to-end) Β· PATH bug root-caused and cured |
54
66
  | **Memory layer** | silent SQLite corruption Β· searches returning 0 Β· invisible flywheel patterns | **3 root-caused fixes** (an ABI-mismatched binary falling back to WAL-blind whole-file overwrites; keys that were never scored; a split-store display bug) β€” plus **6 exact patches queued upstream to ruflo** |
@@ -126,6 +138,8 @@ That single command runs the whole setup, narrating _what it's doing and why_ at
126
138
 
127
139
  > 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.
128
140
 
141
+ > **Anonymous usage counts β€” opt-in, counts only.** At the end of the install you're asked once: _"Share anonymous usage counts (installs/searches β€” never your queries or code)?"_ If you say yes, the only things ever transmitted are event counts (`install` / `search` / `session`) plus the bundle version β€” batched to at most one ping per machine per day. **Never** your queries, code, repo names, or paths; the entire client is one readable file (`kb/telemetry-ping.mjs`). Your answer is a plain-text file you can read or flip any time (`~/.cache/ruvnet-brain/.telemetry-consent`), and `npx ruvnet-brain --no-telemetry` declines without being asked. Found it useful? [Star the repo](https://github.com/stuinfla/ruvnet-brain) or [leave feedback in Discussions](https://github.com/stuinfla/ruvnet-brain/discussions).
142
+
129
143
  <details><summary>Manual install (what the one-liner automates)</summary>
130
144
 
131
145
  ```bash
@@ -149,7 +163,7 @@ You install once. After that, three mechanisms keep you on the current brain wit
149
163
 
150
164
  - **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).
151
165
 
152
- - **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 v2.0.0 Β· 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.
166
+ - **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 v2.0.1 Β· 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.
153
167
 
154
168
  - **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.)
155
169
 
@@ -173,7 +187,7 @@ Plus: the **β€œtake the wheel” behavioral pipeline** (below), a **4-level beha
173
187
 
174
188
  ## How it works
175
189
 
176
- 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 **128,994 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.
190
+ 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.
177
191
 
178
192
  ![RuvNet Brain architecture pipeline](assets/diagrams/architecture-pipeline.svg)
179
193
 
@@ -283,7 +297,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
283
297
 
284
298
  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:
285
299
 
286
- - βœ… **The grounding brain is real and proven** β€” 32 repos, 128,994 chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
300
+ - βœ… **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.
287
301
  - βœ… **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).
288
302
  - βœ… **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).
289
303
  - ⚠️ **Two routing residuals** (above) β€” surfaced, not hidden.
@@ -305,6 +319,14 @@ The brain binaries ship via the [Release](https://github.com/stuinfla/ruvnet-bra
305
319
 
306
320
  ---
307
321
 
322
+ ## Community
323
+
324
+ - **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.
325
+ - **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.
326
+ - **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).
327
+
328
+ ---
329
+
308
330
  ## Links
309
331
 
310
332
  - **β–Ά Live explainer:** https://isovision.ai/ruvnet-brain/
package/bin/install.mjs CHANGED
@@ -65,11 +65,13 @@ 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)
71
72
  const FLAG_DISABLE_NIGHTLY = argv.includes('--disable-nightly'); // remove the nightly schedule
72
73
  const FLAG_NO_NIGHTLY_PROMPT = argv.includes('--no-nightly-prompt'); // don't offer nightly auto-updates at the end of an install
74
+ const FLAG_NO_TELEMETRY = argv.includes('--no-telemetry'); // decline anonymous usage counts without being asked
73
75
  // ── onboarding-experience flags (all optional; every offer is safe to decline) ──
74
76
  const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
75
77
  const FLAG_WITH_STACK = argv.includes('--with-stack'); // add missing Ruflo/RuVector without prompting
@@ -490,11 +492,9 @@ function wirePlugin() {
490
492
  }
491
493
 
492
494
  // ── step: verify the install is REAL (counts β€” never take "installed" on faith) ──────────────────
493
- function verifyInstall(cacheDir) {
494
- step(
495
- 'Verifying the brain is real and reachable',
496
- "you should never have to trust the word \"installed\" β€” here's the proof on disk",
497
- );
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) {
498
498
  let repos = 0;
499
499
  try {
500
500
  repos = fs
@@ -503,14 +503,29 @@ function verifyInstall(cacheDir) {
503
503
  } catch {
504
504
  /* ignore */
505
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);
506
523
  if (repos > 0) ok(`${repos} RuvNet repos indexed (vector stores present on disk)`);
507
524
  else warn(`no .rvf stores found in ${cacheDir} β€” the brain may be incomplete (re-run with --force)`);
508
525
 
509
- const reader = fs.existsSync(path.join(cacheDir, 'node_modules'));
510
526
  if (reader) ok('local reader installed (vector reads happen offline β€” no cloud, no API key)');
511
- else warn('reader deps missing β€” re-run the installer');
527
+ else warn(`reader deps missing β€” every search WILL fail until fixed: cd ${cacheDir} && npm i`);
512
528
 
513
- const mcp = fs.existsSync(path.join(cacheDir, 'forge-mcp-all.mjs'));
514
529
  if (mcp) ok('search_ruvnet server present (this is what Claude calls to ground answers)');
515
530
  else warn('forge-mcp-all.mjs missing β€” the brain unpacked incompletely');
516
531
 
@@ -758,6 +773,99 @@ async function doctor() {
758
773
  );
759
774
  }
760
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
+
761
869
  // ── `--update` / `--enable-nightly` / `--disable-nightly`: end-user freshness controls ────────────
762
870
  // The brain bundle SHIPS its own self-updater (forge-update.mjs, right in the KB dir): it pulls the
763
871
  // canonical Release bundle, backs the current copy up, extracts, and re-verifies with forge-guard β€”
@@ -921,6 +1029,88 @@ function disableNightly() {
921
1029
  info(`re-enable any time: ${c.bold('npx ruvnet-brain --enable-nightly')}`);
922
1030
  }
923
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
+
924
1114
  // ── step: offer nightly auto-updates at the end of a successful install (recommended, default YES) ──
925
1115
  // Requirement: a default `npx ruvnet-brain` run must never leave the user unaware of nightly
926
1116
  // auto-updates β€” it VERY CLEARLY recommends them, asks, and DEFAULTS TO YES. Before this, the
@@ -937,6 +1127,92 @@ export function parseNightlyAnswer(answer) {
937
1127
  // suppression flags) is testable in-process under RUVNET_BRAIN_IMPORT_ONLY=1 without a real install.
938
1128
  // Returns a status string; never throws (the caller also guards β€” a finished install must never
939
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
+
940
1216
  export async function offerNightly() {
941
1217
  // Suppressed outright: --no-nightly-prompt (the user said don't ask) and RUVNET_BRAIN_TEST=1
942
1218
  // (tests must stay non-interactive and must never schedule anything).
@@ -992,6 +1268,96 @@ export async function offerNightly() {
992
1268
  return 'enabled';
993
1269
  }
994
1270
 
1271
+ // ── step: offer OPT-IN anonymous usage counts (asked once ever; explicit yes required) ────────────
1272
+ // The whole contract, honestly: counts ONLY (installs / searches / sessions + version) β€” NEVER the
1273
+ // user's queries, code, repo names, or paths. Consent is a plain yes/no file the user can read and
1274
+ // flip (~/.cache/ruvnet-brain/.telemetry-consent); nothing is ever sent without the literal "yes".
1275
+ // Fail-private everywhere: no TTY β†’ not asked β†’ not enabled; TEST mode β†’ suppressed entirely.
1276
+ const telemetryStateDir = () => path.join(os.homedir(), '.cache', 'ruvnet-brain');
1277
+ const telemetryConsentPath = () => path.join(telemetryStateDir(), '.telemetry-consent');
1278
+
1279
+ // Same default-yes contract as parseNightlyAnswer, exported under its own name so the telemetry
1280
+ // tests read as telemetry tests: ENTER/y/yes accept; ONLY an explicit n/no declines.
1281
+ export const parseTelemetryAnswer = parseNightlyAnswer;
1282
+
1283
+ // Fire-and-forget install ping β€” 3s cap, all failures swallowed, payload is { event, v } and
1284
+ // nothing else. Exported (with injectable fetch) so tests can assert the payload without a network.
1285
+ export async function sendInstallPing({
1286
+ version = 'unknown',
1287
+ fetchFn = globalThis.fetch,
1288
+ pingUrl = process.env.RUVNET_BRAIN_PING_URL || 'https://ruvnet-brain.vercel.app/api/ping',
1289
+ } = {}) {
1290
+ try {
1291
+ const ctl = new AbortController();
1292
+ const timer = setTimeout(() => ctl.abort(), 3000);
1293
+ await fetchFn(pingUrl, {
1294
+ method: 'POST',
1295
+ headers: { 'Content-Type': 'application/json' },
1296
+ body: JSON.stringify({ event: 'install', v: version }),
1297
+ signal: ctl.signal,
1298
+ }).catch(() => {});
1299
+ clearTimeout(timer);
1300
+ } catch { /* a lost count is nothing; a broken install step would be everything */ }
1301
+ }
1302
+
1303
+ // Exported decision matrix (testable under RUVNET_BRAIN_IMPORT_ONLY=1, like offerNightly).
1304
+ // Returns a status string; never throws β€” a finished install must never be broken by an offer.
1305
+ export async function offerTelemetry(cacheDir) {
1306
+ if (TEST_MODE) return 'suppressed'; // tests: never prompt, never write, NEVER send
1307
+ const consentPath = telemetryConsentPath();
1308
+ if (fs.existsSync(consentPath)) return 'already-set'; // asked once ever β€” respect the answer
1309
+ if (FLAG_NO_TELEMETRY) {
1310
+ try { fs.mkdirSync(telemetryStateDir(), { recursive: true }); fs.writeFileSync(consentPath, 'no\n'); } catch { /* best-effort */ }
1311
+ return 'declined-flag';
1312
+ }
1313
+
1314
+ step(
1315
+ 'Optional: anonymous usage counts',
1316
+ 'a simple count of installs/searches tells Stuart the brain is actually helping people β€” nothing about WHAT you ask',
1317
+ );
1318
+ info(`Counts only β€” installs, searches, sessions, version. ${c.bold('Never your queries, code, repo names, or paths.')}`);
1319
+ info(c.dim(`Your answer is a plain-text file you can read or flip any time: ${consentPath}`));
1320
+
1321
+ if (!process.stdin.isTTY && !FLAG_YES) {
1322
+ // No terminal to ask on β†’ fail PRIVATE: no consent recorded, so nothing will ever be sent.
1323
+ info(`No interactive terminal here, so I won't assume β€” usage counts stay ${c.bold('OFF')}.`);
1324
+ info(`Opt in any time: echo yes > ${consentPath}`);
1325
+ return 'not-asked';
1326
+ }
1327
+
1328
+ let yes = true; // --yes accepts every optional offer, this one included
1329
+ if (!FLAG_YES) {
1330
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1331
+ const answer = await new Promise((resolve) =>
1332
+ rl.question(` ${c.cyan('?')} Share anonymous usage counts (installs/searches β€” never your queries or code)? ${c.dim('[Y/n]')} `, resolve),
1333
+ );
1334
+ rl.close();
1335
+ yes = parseTelemetryAnswer(answer);
1336
+ }
1337
+
1338
+ try {
1339
+ fs.mkdirSync(telemetryStateDir(), { recursive: true });
1340
+ fs.writeFileSync(consentPath, yes ? 'yes\n' : 'no\n');
1341
+ } catch (e) {
1342
+ warn(`couldn't record the answer (${e.message}) β€” defaulting to OFF (nothing will be sent)`);
1343
+ return 'error';
1344
+ }
1345
+ if (!yes) {
1346
+ ok('usage counts are OFF β€” nothing will ever be sent');
1347
+ return 'declined';
1348
+ }
1349
+ ok('thanks β€” anonymous counters only, batched to at most one ping a day');
1350
+ // Count this install (the one event the brain itself can't see). Version = the bundle we
1351
+ // just put on disk, read live from its own SOURCE.json β€” never guessed.
1352
+ let v = 'unknown';
1353
+ try {
1354
+ const j = JSON.parse(fs.readFileSync(path.join(cacheDir, 'SOURCE.json'), 'utf8'));
1355
+ if (typeof j.releaseTag === 'string' && /^[A-Za-z0-9._-]{1,32}$/.test(j.releaseTag)) v = j.releaseTag;
1356
+ } catch { /* unknown is honest */ }
1357
+ await sendInstallPing({ version: v });
1358
+ return 'enabled';
1359
+ }
1360
+
995
1361
  // ── tiny interactive yes/no β€” SAFE in non-TTY (returns the default; never blocks a piped install) ──
996
1362
  function ask(question, def = false) {
997
1363
  if (FLAG_YES) return Promise.resolve(true);
@@ -1232,6 +1598,11 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
1232
1598
  console.log(` ${c.dim('Either way, your copy only advances when a new Release is actually published.')}`);
1233
1599
  }
1234
1600
 
1601
+ // One tasteful ask, at the moment the value was just delivered β€” never repeated by the plugin
1602
+ // more than once ever (see session-start.sh's stamped one-liner).
1603
+ console.log(`\n ${c.bold('If it earns it:')} a GitHub star helps other people find the brain β€”`);
1604
+ console.log(` ${c.bold('https://github.com/stuinfla/ruvnet-brain')} ${c.dim('Β· feedback in one command: npx ruvnet-brain --feedback')}`);
1605
+
1235
1606
  console.log(`\n ${c.dim('You can\'t break anything β€” the plugin is disable-able and only acts on RuvNet-shaped work.')}`);
1236
1607
  console.log('');
1237
1608
  }
@@ -1248,6 +1619,9 @@ Usage:
1248
1619
  npx github:stuinfla/ruvnet-brain Same, but from the bleeding-edge GitHub commit
1249
1620
  npx ruvnet-brain --doctor Health-check an existing install (green/red per part)
1250
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
1251
1625
  npx ruvnet-brain --update One-shot: pull the latest Release bundle into your installed brain
1252
1626
  (runs the bundle's own forge-update.mjs --apply: backup + re-verify)
1253
1627
  npx ruvnet-brain --enable-nightly Schedule that update nightly at 03:47 β€” macOS LaunchAgent;
@@ -1255,6 +1629,10 @@ Usage:
1255
1629
  npx ruvnet-brain --disable-nightly Remove the nightly schedule (safe to run any time)
1256
1630
  (a default install RECOMMENDS nightly and asks, defaulting to yes)
1257
1631
  node bin/install.mjs --no-nightly-prompt Don't offer nightly auto-updates at the end of the install
1632
+ node bin/install.mjs --no-telemetry Decline anonymous usage counts without being asked
1633
+ (counts of installs/searches ONLY β€” never queries, code, or paths;
1634
+ opt-in prompt appears once at install; answer lives in a plain file:
1635
+ ~/.cache/ruvnet-brain/.telemetry-consent)
1258
1636
  node bin/install.mjs --version <tag> Install a specific Release tag (e.g. --version v0.5.0-dev)
1259
1637
  node bin/install.mjs --pin Skip the latest-check; use the bundled known-good version
1260
1638
  node bin/install.mjs --local Install from a repo clone's dist/ruvnet-brain.zip
@@ -1279,6 +1657,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
1279
1657
  if (FLAG_HELP) return showHelp();
1280
1658
  if (FLAG_DOCTOR) return await doctor();
1281
1659
  if (FLAG_DEMO) return runDemo();
1660
+ if (FLAG_FEEDBACK) return runFeedback();
1282
1661
  if (FLAG_UPDATE) return runUpdate();
1283
1662
  if (FLAG_ENABLE_NIGHTLY) return enableNightly();
1284
1663
  if (FLAG_DISABLE_NIGHTLY) return disableNightly();
@@ -1362,6 +1741,16 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
1362
1741
  // and asking, DEFAULTING TO YES (TTY + macOS + not already on). Non-fatal like every other offer.
1363
1742
  let nightly = 'skipped';
1364
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 */ }
1751
+ // Anonymous usage counts β€” OPT-IN, asked once ever, right after the nightly offer. Same rule:
1752
+ // an optional offer can never break a finished install.
1753
+ try { await offerTelemetry(cacheDir); } catch { /* fail-private: unanswered = OFF */ }
1365
1754
 
1366
1755
  success({ cacheDir, isCustom, plugin, env, nightly });
1367
1756
  })().catch((e) => {