ruvnet-brain 3.5.1-dev β†’ 3.9.18-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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 3.5.1-dev β€” updated 2026-07-21 06:00 EDT](https://img.shields.io/badge/version_3.5.1--dev-updated_2026--07--21_06:00_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 3.9.18-dev β€” updated 2026-07-23 04:06 EDT](https://img.shields.io/badge/version_3.9.18--dev-updated_2026--07--23_04:06_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
 
@@ -56,7 +56,100 @@
56
56
 
57
57
  ---
58
58
 
59
- ## What's new in 3.5 β€” it stopped waiting to be asked
59
+ ## What's new in 3.9 β€” it anticipates, and it learns whether it was right
60
+
61
+ **Building toward L4/L5 (3.9.x, dev).** The mechanisms for the top two rungs of the proactivity
62
+ ladder are built and wired β€” but they are **not yet verified to 4.0's bar**, which requires all five
63
+ of ADR-028's test classes green under independent grading. Until then, [ADR-028](docs/adr/0028-what-proactive-means.md)
64
+ grades the shipped, *verified* state at **L2–L3**, and `-dev` is the honest version. 4.0 is claimed
65
+ only when L3+L4+L5 are each measured, not when the code merely exists.
66
+
67
+ - **L4 Anticipatory (mechanism built).** It infers what you're *trying to do* and names the capability
68
+ that serves that goal β€” before you hit the wall. Built to stay quiet: a match needs **two
69
+ independent cues** (the problem AND that the topic is your AI workflow), so near-misses get
70
+ silence. Measured on the negative table: **0 false positives**; fires on a genuine match. What's
71
+ still owed for L4: the full five-class verification, independently graded.
72
+ - **L5 Compounding (mechanism built, metric now live).** Advocacy records whether it was right β€”
73
+ precision = acted-on Γ· offered, ADR-028's line between advocating and nagging. The numerator is
74
+ now **derived from an observed state transition** (offered β†’ later switched on = applied) and
75
+ surfaced in the console; it reads an honest `null` until enough offers resolve, never a fabricated
76
+ score. A dismissal is evidence about **fit, not importance** β€” asymmetric budget: a suggestion dies
77
+ on one dismissal, an important finding needs three. What's still owed for L5: real precision data
78
+ from use, and the cross-project promotion test.
79
+ - **The admin page is rebuilt around deltas**, not lifetime totals β€” and the humans who wrote in
80
+ are promoted to the top, ranked by recency rather than lifetime count.
81
+
82
+ <details>
83
+ <summary><b>Earlier &#8212; what 3.8 shipped</b> &#183; the gate that fires when you stop. <i>Expand.</i></summary>
84
+
85
+ ## 3.8 β€” the gate that fires when you stop
86
+
87
+ **Shipped 2026-07-22.** Every gate in this project fires on an **action** β€” a write, a push, a
88
+ claim. That is what makes it enforceable. But **stopping is the absence of an action**, so the most
89
+ expensive failure of the night had no trigger at all, and a system built to prevent it did not.
90
+
91
+ - **`continuation-gate.mjs`** runs when a turn ends. If work you committed to is unfinished, it
92
+ makes that the last thing in context. It cannot force another turn β€” claiming otherwise would be
93
+ the fabrication this project exists to kill β€” but it removes the silence that let a stop pass
94
+ unnoticed.
95
+ - **Project-scoped ledgers.** Three simultaneous projects never see each other's commitments; a
96
+ "you didn't finish X" fired in a repo that never heard of X is a false alarm, and ADR-028 fixes
97
+ the false-alarm rate at zero.
98
+ - **Installed machine-wide**, merged into existing hooks rather than replacing them, with a backup
99
+ taken first β€” that file governs every project on the machine.
100
+
101
+ <details>
102
+ <summary><b>Earlier &#8212; what 3.7 shipped</b> &#183; your lessons can now refuse your work. <i>Expand.</i></summary>
103
+
104
+ ## 3.7 β€” your lessons can now refuse your work
105
+
106
+ **Shipped 2026-07-22.** 3.6 made the machine legible. 3.7 closes the loop: a lesson you taught is
107
+ read by a gate at the moment it applies, and β€” once you ratify it β€” **refuses the action**.
108
+
109
+ - **`lesson-gate.mjs`** β€” a gate asks the store "what applies right now?" and gets your own words
110
+ back, with the evidence and how many times you had to say it. Wired into the real pre-push gate.
111
+ - **`lesson-ratify.mjs`** β€” see every lesson, ratify it, or delete it. **Demotion is sticky**: it
112
+ survives every future mining run, because a reject the nightly quietly undoes is worse than none.
113
+ - **The model cannot ratify its own rules.** Lessons it inferred about itself are quarantined and
114
+ can never block, no matter what is asked of them β€” closing an injection path an adversarial
115
+ review found, where a hallucinated session summary could have become a blocking gate.
116
+ - Proven end to end: before ratification the ship gate informs (exit 0); after ratifying the
117
+ version-discipline lesson it **refuses** (exit 1). Same wire, no code change β€” enforcement is data.
118
+
119
+ <details>
120
+ <summary><b>Earlier &#8212; what 3.6 shipped</b> &#183; it could finally show you what you own. <i>Expand.</i></summary>
121
+
122
+ ## 3.6 β€” it can finally show you what you own
123
+
124
+ **Shipped 2026-07-22.** 3.5 made the brain speak. 3.6 makes it *legible*: a console panel that
125
+ answers the question the owner has been asking for weeks β€” **"what is actually turned on?"**
126
+
127
+ - **Eleven capabilities, each ON / OFF / UNKNOWN**, every state *derived* from a real check on your
128
+ machine at render time. `UNKNOWN` is a first-class state and is visually separated from `OFF` in a
129
+ non-colour channel, because collapsing the two is precisely the lie this release exists to kill.
130
+ - **A one-line plain-English "what it buys you"** on every row, and the evidence we observed.
131
+ - **Settings that are conservative by construction.** Each setting declares which of its *values*
132
+ escalate beyond the current project, and `default βˆ‰ escalates` is asserted as a test β€” so the rule
133
+ holds for settings added later instead of relying on a comment nobody re-reads.
134
+ - **A false alarm found and killed.** 3.5's audit reported *"26 learning hooks installed and every
135
+ one is switched off."* That was wrong: `ruflo hooks list` renders a table column keyed `enabled`
136
+ against a payload that has no such key, so it prints "No" 26 times. The learner had 457
137
+ trajectories and had adapted 106 minutes earlier. We scraped a human-readable table instead of
138
+ reading state, which is the exact mistake this project keeps writing rules about. The detector now
139
+ reads the learner's own state file and **stays silent when it cannot tell** β€” ADR-028 fixes the
140
+ false-alarm rate at zero and calls it non-negotiable.
141
+ - **Three surfaces that were built and never wired** are now connected. `capability-registry.mjs`
142
+ had zero call sites; the client referenced it only in comments. Built-tested-unwired is this
143
+ project's signature failure, and it happened three times in one night.
144
+
145
+ Honest limit: this is **3.6, not 4.0**. 4.0 requires levels 3–5 of ADR-028's proactivity ladder
146
+ (contextual, anticipatory, compounding) and all three remain unbuilt β€” see `docs/4.0-READINESS.md`,
147
+ which grades the current state at **L2** with evidence for every mark.
148
+
149
+ <details>
150
+ <summary><b>Earlier &#8212; what 3.5 shipped</b> &#183; it stopped waiting to be asked. <i>Expand for the receipts.</i></summary>
151
+
152
+ ## 3.5 β€” it stopped waiting to be asked
60
153
 
61
154
  **Shipped 2026-07-22.** For three weeks this thing had indexed rUv's entire learning stack β€” ReasoningBank, SONA, MoE, the ADR-174 distillation pipeline β€” and could have answered any question about any of it. It never once said the only sentence that mattered:
62
155
 
@@ -267,6 +360,10 @@ claude plugin install ruvnet-brain@ruvnet-brain --scope user
267
360
 
268
361
  Registers the `search_ruvnet` MCP tool, the grounding skill, and the `UserPromptSubmit` enforcement hook β€” globally, at user scope. The plugin expects the brain at `~/.cache/ruvnet-brain/kb` (or point `RUVNET_BRAIN_KB` at your own copy). The first install may show a one-time trust prompt for the hook.
269
362
 
363
+ </details>
364
+ </details>
365
+ </details>
366
+ </details>
270
367
  </details>
271
368
  </details>
272
369
 
@@ -309,7 +406,7 @@ Plus: the **β€œtake the wheel” behavioral pipeline** (below), a **4-level beha
309
406
 
310
407
  ## How it works
311
408
 
312
- 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 **149,720 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.
409
+ 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 **149,721 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.
313
410
 
314
411
  ![RuvNet Brain architecture pipeline](assets/diagrams/architecture-pipeline.svg)
315
412
 
@@ -419,7 +516,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
419
516
 
420
517
  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:
421
518
 
422
- - βœ… **The grounding brain is real and proven** β€” 54 public stores Β· 149,720 public source chunks (57 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
519
+ - βœ… **The grounding brain is real and proven** β€” 54 public stores Β· 149,721 public source chunks (57 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
423
520
  - βœ… **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).
424
521
  - βœ… **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).
425
522
  - ⚠️ **Two routing residuals** (above) β€” surfaced, not hidden.
package/bin/install.mjs CHANGED
@@ -349,11 +349,18 @@ async function obtainBundle(release) {
349
349
  );
350
350
  }
351
351
  ok(`downloaded to ${tmp}`);
352
- // Best-effort fetch of the detached Ed25519 signature published alongside the asset (SEC-0010 #6).
353
- // If present we verify it before extracting; if absent (a pre-signing release) we warn but proceed
354
- // (transitional β€” see SIGNING_REQUIRED at the verify gate).
355
- try { await download(`${downloadUrl}.sig`, `${tmp}.sig`); } catch { /* no published sig yet */ }
356
- return { zipPath: tmp, tmpDir, downloaded: true };
352
+ // Fetch the detached Ed25519 signature published alongside the asset (SEC-0010 #6).
353
+ // We record WHY a fetch failed rather than discarding it. Swallowing the error made two very
354
+ // different worlds look identical: "this release genuinely has no signature" and "someone
355
+ // 404'd/reset/stripped the .sig so verification would be skipped." The second is the whole
356
+ // downgrade attack β€” strip one small file and 800MB+ of executable .mjs extracts unverified.
357
+ let sigError = null;
358
+ try {
359
+ await download(`${downloadUrl}.sig`, `${tmp}.sig`);
360
+ } catch (e) {
361
+ sigError = e && e.message ? e.message : String(e);
362
+ }
363
+ return { zipPath: tmp, tmpDir, downloaded: true, sigError };
357
364
  }
358
365
 
359
366
  // ── step: unzip into the cache dir (flattening the top-level ruvnet-brain/ folder) ───────────────
@@ -581,9 +588,13 @@ function verifyInstall(cacheDir) {
581
588
  }
582
589
 
583
590
  // ── step: warm the model + prove grounding with one real question (best-effort, never fatal) ──────
584
- // ── BEGIN GENERATED: verify-citation.mjs (node scripts/embed-verifier.mjs) ──
585
- const VERIFY_CITATION_B64 = 'IyEvdXNyL2Jpbi9lbnYgbm9kZQovLyB2ZXJpZnktY2l0YXRpb24ubWpzIOKAlCBkZWNpZGUgd2hldGhlciBhbiBhbnN3ZXIgaXMgR1JPVU5ERUQsIGJ5IGdyb3VuZCB0cnV0aCByYXRoZXIgdGhhbiBieSB2aWJlcy4KLy8KLy8gV0hZIFRISVMgRVhJU1RTCi8vIC0tLS0tLS0tLS0tLS0tLQovLyBUaGUgb2xkIGdyb3VuZGluZyBjaGVjayBhc2tlZDogZG9lcyB0aGUgYW5zd2VyIGNvbnRhaW4gdGhlIHN0cmluZyAicnZmIiBvciAicnV2ZWN0b3IiPyBBIG1vZGVsCi8vIHRoYXQgaGFsbHVjaW5hdGVkICJqdXN0IHVzZSBSVkYhIiB3aXRoIHplcm8gc291cmNlcyBwYXNzZWQuIFNvIGRpZCBhbiBhbnN3ZXIgY2l0aW5nIGEgZmlsZSB0aGF0Ci8vIGRvZXMgbm90IGV4aXN0LiBLZXl3b3JkIHByZXNlbmNlIGlzIG5vdCBldmlkZW5jZSDigJQgYW4gTExNIHBhbmVsIG9uY2Ugc2NvcmVkIGEgemVyby1jaXRhdGlvbgovLyBhbnN3ZXIgOTgvMTAwIG9uIHRoaXMgcmVwby4KLy8KLy8gQSBjaXRhdGlvbiBpcyBvbmx5IHJlYWwgaWYgaXQgUkVTT0xWRVM6IHRoZSByZXBvIG11c3QgYmUgYW4gaW5kZXhlZCBzdG9yZSBvbiBkaXNrLCBhbmQgdGhlIGNpdGVkCi8vIGRvY3VtZW50IHBhdGggbXVzdCBhcHBlYXIgYXMgdGhlIGBwYXRoYCBvZiBhbiBhY3R1YWwgcGFzc2FnZSBpbnNpZGUgdGhhdCBzdG9yZSdzIHBhc3NhZ2VzIGZpbGUuCi8vIFRoYXQgaXMgY2hlY2thYmxlIHdpdGhvdXQgYSBtb2RlbCwgd2l0aG91dCB0aGUgbmV0d29yaywgYW5kIHdpdGhvdXQgdHJ1c3RpbmcgYW55dGhpbmcgdGhlIG1vZGVsCi8vIHNhaWQuIFRoaXMgbW9kdWxlIGRvZXMgZXhhY3RseSB0aGF0IGFuZCBub3RoaW5nIGVsc2UuCi8vCi8vIFRoZSByZWFkZXIgKGBmb3JnZS1hc2stYWxsLm1qc2ApIHByaW50cyBlYWNoIGhpdCBhczoKLy8gICAgICMxICByZXBvPWNvbmNlcHRzICBjZT0wLjIwMSAgdmVjPTAuODY4NiAga2luZD1kb2MKLy8gICAgIHBhdGggOiBjb25jZXB0cy9ydXZlY3Rvci9DQVJEL3J1dmVjdG9yLWNhcmQKLy8gICAgIHRpdGxlOiBydXZlY3RvciDigJQgQ2FwYWJpbGl0eQovLyBOb3RlIHRoZSBwcmludGVkIHBhdGggaXMgYDxyZXBvPi88ZG9jUGF0aD5gOyBpbnNpZGUgYGNvbmNlcHRzLnBhc3NhZ2VzLmpzb25sYCB0aGUgc3RvcmVkIGBwYXRoYAovLyBpcyBqdXN0IGBydXZlY3Rvci9DQVJEL3J1dmVjdG9yLWNhcmRgIChvcHRpb25hbGx5IHN1ZmZpeGVkIGAjMGAsIGAjMWAsIOKApiB3aGVuIGNodW5rZWQpLgoKaW1wb3J0IGZzIGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgcGF0aCBmcm9tICdub2RlOnBhdGgnOwppbXBvcnQgcmVhZGxpbmUgZnJvbSAnbm9kZTpyZWFkbGluZSc7CgovKiogUGFyc2UgdGhlIHJlYWRlcidzIHN0ZG91dCBpbnRvIHN0cnVjdHVyZWQgY2l0YXRpb25zLiBOZXZlciB0aHJvd3M7IHVucGFyc2VhYmxlIGlucHV0IOKGkiBbXS4gKi8KZXhwb3J0IGZ1bmN0aW9uIHBhcnNlQ2l0YXRpb25zKHN0ZG91dCkgewogIGNvbnN0IG91dCA9IFtdOwogIGNvbnN0IHRleHQgPSBTdHJpbmcoc3Rkb3V0ID8/ICcnKTsKICBjb25zdCBibG9ja1JlID0gL14jKFxkKylccytyZXBvPShcUyspKD86XHMrY2U9KC0/W1xkLl0rKSk/KD86XHMrdmVjPSgtP1tcZC5dKykpPyg/OlxzK2tpbmQ9KFxTKykpPy9nbTsKICBsZXQgbTsKICB3aGlsZSAoKG0gPSBibG9ja1JlLmV4ZWModGV4dCkpICE9PSBudWxsKSB7CiAgICBjb25zdCByZXN0ID0gdGV4dC5zbGljZShtLmluZGV4KTsKICAgIGNvbnN0IHBhdGhNID0gL15wYXRoXHMqOlxzKiguKykkL20uZXhlYyhyZXN0KTsKICAgIGNvbnN0IHRpdGxlTSA9IC9edGl0bGVccyo6XHMqKC4rKSQvbS5leGVjKHJlc3QpOwogICAgaWYgKCFwYXRoTSkgY29udGludWU7CiAgICBjb25zdCByZXBvID0gbVsyXTsKICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aE1bMV0udHJpbSgpOwogICAgLy8gU3RyaXAgdGhlIHJlcG8gcHJlZml4IHRoZSByZWFkZXIgYWRkcywgc28gdGhlIHJlbWFpbmRlciBjYW4gYmUgbWF0Y2hlZCBhZ2FpbnN0IHRoZSBzdG9yZS4KICAgIGNvbnN0IGRvY1BhdGggPSBmdWxsUGF0aC5zdGFydHNXaXRoKGAke3JlcG99L2ApID8gZnVsbFBhdGguc2xpY2UocmVwby5sZW5ndGggKyAxKSA6IGZ1bGxQYXRoOwogICAgb3V0LnB1c2goewogICAgICByYW5rOiBOdW1iZXIobVsxXSksCiAgICAgIHJlcG8sCiAgICAgIGNlOiBtWzNdICE9PSB1bmRlZmluZWQgPyBOdW1iZXIobVszXSkgOiBudWxsLAogICAgICB2ZWM6IG1bNF0gIT09IHVuZGVmaW5lZCA/IE51bWJlcihtWzRdKSA6IG51bGwsCiAgICAgIGtpbmQ6IG1bNV0gPz8gbnVsbCwKICAgICAgZnVsbFBhdGgsCiAgICAgIGRvY1BhdGgsCiAgICAgIHRpdGxlOiB0aXRsZU0gPyB0aXRsZU1bMV0udHJpbSgpIDogbnVsbCwKICAgIH0pOwogIH0KICByZXR1cm4gb3V0Owp9CgovKiogVGhlIHBhc3NhZ2VzIGZpbGVzIHRoYXQgY291bGQgaG9sZCBhIHJlcG8ncyBkb2N1bWVudHMg4oCUIHRoZSBzbGltIHN0b3JlIGFuZCB0aGUgZGVlcCBgLmJpZ2Agb25lLiAqLwpleHBvcnQgZnVuY3Rpb24gcGFzc2FnZXNGaWxlc0ZvcihyZXBvLCBrYkRpcikgewogIHJldHVybiBbcGF0aC5qb2luKGtiRGlyLCBgJHtyZXBvfS5wYXNzYWdlcy5qc29ubGApLCBwYXRoLmpvaW4oa2JEaXIsIGAke3JlcG99LmJpZy5wYXNzYWdlcy5qc29ubGApXQogICAgLmZpbHRlcigocCkgPT4gZnMuZXhpc3RzU3luYyhwKSk7Cn0KCi8qKiBUcnVlIHdoZW4gYHN0b3JlZGAgaXMgdGhlIGNpdGVkIGRvYywgYWxsb3dpbmcgZm9yIHRoZSBgI05gIGNodW5rIHN1ZmZpeCB0aGUgYnVpbGRlciBhcHBlbmRzLiAqLwpmdW5jdGlvbiBzYW1lUGF0aChzdG9yZWQsIGRvY1BhdGgpIHsKICByZXR1cm4gc3RvcmVkID09PSBkb2NQYXRoIHx8IHN0b3JlZC5zdGFydHNXaXRoKGAke2RvY1BhdGh9I2ApOwp9CgovKioKICogRG9lcyB0aGlzIGNpdGF0aW9uIHBvaW50IGF0IGEgcGFzc2FnZSB0aGF0IHJlYWxseSBleGlzdHMgb24gZGlzaz8KICogU3RyZWFtcyB0aGUgZmlsZSBhbmQgc3RvcHMgYXQgdGhlIGZpcnN0IG1hdGNoLCBzbyBhIDUwME1CIGAuYmlnYCBzdG9yZSBjb3N0cyBvbmx5IGFzIG11Y2ggYXMgaXQKICogdGFrZXMgdG8gcmVhY2ggdGhlIGhpdC4gQSBtYWxmb3JtZWQgSlNPTiBsaW5lIGlzIHNraXBwZWQsIG5ldmVyIGZhdGFsLgogKi8KZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNpdGF0aW9uUmVzb2x2ZXMoY2l0YXRpb24sIGtiRGlyKSB7CiAgY29uc3QgZmlsZXMgPSBwYXNzYWdlc0ZpbGVzRm9yKGNpdGF0aW9uLnJlcG8sIGtiRGlyKTsKICBpZiAoIWZpbGVzLmxlbmd0aCkgcmV0dXJuIHsgcmVzb2x2ZWQ6IGZhbHNlLCByZWFzb246ICduby1zdG9yZScsIGZpbGU6IG51bGwsIHN0b3JlZFBhdGg6IG51bGwgfTsKICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHsKICAgIGNvbnN0IHJsID0gcmVhZGxpbmUuY3JlYXRlSW50ZXJmYWNlKHsgaW5wdXQ6IGZzLmNyZWF0ZVJlYWRTdHJlYW0oZmlsZSksIGNybGZEZWxheTogSW5maW5pdHkgfSk7CiAgICB0cnkgewogICAgICBmb3IgYXdhaXQgKGNvbnN0IGxpbmUgb2YgcmwpIHsKICAgICAgICBpZiAoIWxpbmUpIGNvbnRpbnVlOwogICAgICAgIGxldCByZWM7CiAgICAgICAgdHJ5IHsgcmVjID0gSlNPTi5wYXJzZShsaW5lKTsgfSBjYXRjaCB7IGNvbnRpbnVlOyB9CiAgICAgICAgaWYgKHR5cGVvZiByZWM/LnBhdGggPT09ICdzdHJpbmcnICYmIHNhbWVQYXRoKHJlYy5wYXRoLCBjaXRhdGlvbi5kb2NQYXRoKSkgewogICAgICAgICAgcmV0dXJuIHsgcmVzb2x2ZWQ6IHRydWUsIHJlYXNvbjogJ29rJywgZmlsZTogcGF0aC5iYXNlbmFtZShmaWxlKSwgc3RvcmVkUGF0aDogcmVjLnBhdGggfTsKICAgICAgICB9CiAgICAgIH0KICAgIH0gZmluYWxseSB7CiAgICAgIHJsLmNsb3NlKCk7CiAgICB9CiAgfQogIHJldHVybiB7IHJlc29sdmVkOiBmYWxzZSwgcmVhc29uOiAncGF0aC1ub3QtaW4tc3RvcmUnLCBmaWxlOiBudWxsLCBzdG9yZWRQYXRoOiBudWxsIH07Cn0KCi8qKgogKiBUaGUgZ2F0ZS4gQW4gYW5zd2VyIGlzIGdyb3VuZGVkIG9ubHkgd2hlbiBpdCBjaXRlcyBhdCBsZWFzdCBvbmUgcGFzc2FnZSB0aGF0IHJlc29sdmVzIG9uIGRpc2suCiAqIFJldHVybnMgdGhlIHJlY2VpcHQgc28gYSBjYWxsZXIgY2FuIFBSSU5UIHRoZSBldmlkZW5jZSBpbnN0ZWFkIG9mIGFzc2VydGluZyBhIGNvbmNsdXNpb24uCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gdmVyaWZ5R3JvdW5kaW5nKHN0ZG91dCwga2JEaXIpIHsKICBjb25zdCBjaXRhdGlvbnMgPSBwYXJzZUNpdGF0aW9ucyhzdGRvdXQpOwogIGlmICghY2l0YXRpb25zLmxlbmd0aCkgewogICAgcmV0dXJuIHsgZ3JvdW5kZWQ6IGZhbHNlLCByZWFzb246ICduby1jaXRhdGlvbnMnLCBjaXRhdGlvbnM6IFtdLCByZWNlaXB0OiBudWxsIH07CiAgfQogIGZvciAoY29uc3QgY2l0YXRpb24gb2YgY2l0YXRpb25zKSB7CiAgICBjb25zdCByID0gYXdhaXQgY2l0YXRpb25SZXNvbHZlcyhjaXRhdGlvbiwga2JEaXIpOwogICAgaWYgKHIucmVzb2x2ZWQpIHsKICAgICAgcmV0dXJuIHsKICAgICAgICBncm91bmRlZDogdHJ1ZSwKICAgICAgICByZWFzb246ICdvaycsCiAgICAgICAgY2l0YXRpb25zLAogICAgICAgIHJlY2VpcHQ6IHsgcmVwbzogY2l0YXRpb24ucmVwbywgcGF0aDogY2l0YXRpb24uZnVsbFBhdGgsIHRpdGxlOiBjaXRhdGlvbi50aXRsZSwgZmlsZTogci5maWxlLCBzdG9yZWRQYXRoOiByLnN0b3JlZFBhdGggfSwKICAgICAgfTsKICAgIH0KICB9CiAgcmV0dXJuIHsgZ3JvdW5kZWQ6IGZhbHNlLCByZWFzb246ICdjaXRhdGlvbnMtZG8tbm90LXJlc29sdmUnLCBjaXRhdGlvbnMsIHJlY2VpcHQ6IG51bGwgfTsKfQo=';
586
- // ── END GENERATED ──
591
+ // The verifier ships as a real file (kb/verify-citation.mjs, in package.json files[]).
592
+ // It used to live here as a ~4KB base64 literal that was decoded at runtime, written to disk as
593
+ // .mjs, and then dynamically imported. That is the canonical staged-payload chain, and EDR
594
+ // behavioural engines score write-then-execute on a just-created file as high-confidence dropper
595
+ // activity β€” a benign explanation that arrives only AFTER the alarm. Copying a file we shipped is
596
+ // the same capability with none of the signature. (base64 was originally chosen to dodge escaping
597
+ // bugs β€” the module contains backticks, ${...} and backslashes β€” which shipping a file also solves.)
587
598
 
588
599
  // The verifier belongs next to the data it verifies, so it lives in the KB. But every bundle
589
600
  // published before 2026-07-09 predates it, and telling those users "grounding not verifiable β€”
@@ -593,9 +604,10 @@ const VERIFY_CITATION_B64 = 'IyEvdXNyL2Jpbi9lbnYgbm9kZQovLyB2ZXJpZnktY2l0YXRpb24
593
604
  function ensureVerifier(cacheDir) {
594
605
  const p = path.join(cacheDir, 'verify-citation.mjs');
595
606
  if (fs.existsSync(p)) return 'from-bundle';
596
- if (!VERIFY_CITATION_B64) return 'unavailable';
607
+ const shipped = path.join(REPO_ROOT, 'kb', 'verify-citation.mjs');
608
+ if (!fs.existsSync(shipped)) return 'unavailable';
597
609
  try {
598
- fs.writeFileSync(p, Buffer.from(VERIFY_CITATION_B64, 'base64').toString('utf8'), 'utf8');
610
+ fs.copyFileSync(shipped, p);
599
611
  return 'installed';
600
612
  } catch { return 'unavailable'; }
601
613
  }
@@ -699,7 +711,7 @@ const DEMO_QUESTIONS = [
699
711
  why: 'shows it grounding in Ruflo (the real orchestration engine) instead of guessing at a generic pattern',
700
712
  },
701
713
  ];
702
- function runDemo() {
714
+ async function runDemo() {
703
715
  printBanner('demo');
704
716
  const cacheDir = process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
705
717
  const ask = path.join(cacheDir, 'forge-ask-all.mjs');
@@ -750,6 +762,47 @@ function runDemo() {
750
762
  console.log(`\n${c.green('─'.repeat(64))}`);
751
763
  console.log(` ${c.bold('That\'s it β€” no cloud calls, no API key, just your local brain.')}`);
752
764
  console.log(`${c.green('─'.repeat(64))}`);
765
+ // ── SCOPE + UPGRADE CONVERSATION ────────────────────────────────────────────────────────────
766
+ //
767
+ // The owner, 2026-07-22: "Normally this happens on a per-user basis, which lets learning,
768
+ // intelligence, access and software versions stay updated across ALL your projects. Only choose
769
+ // per-project if this is something you absolutely only use in one place. Our strong
770
+ // recommendation is per-user β€” but we always want YOU to be the arbiter of how things run on
771
+ // your machine."
772
+ //
773
+ // And, critically, for people who already have it installed: "That's only going to help people
774
+ // newly installing. It needs to be smart enough when it comes up to say version 4 is here, here
775
+ // are your choices."
776
+ //
777
+ // Both modules were BUILT AND WIRED TO NOTHING until this was added β€” the seventh instance of
778
+ // built-tested-unwired in one session, which is why scripts/wired-check.mjs now gates the release.
779
+ //
780
+ // Read-only here by design: this INFORMS and never changes scope on its own. P3 (nudge, never
781
+ // force) and P4 (the user is the arbiter). Fail-silent, because an informational block must never
782
+ // break an install that otherwise succeeded.
783
+ try {
784
+ const { detectCurrentScope, explainChoice, RECOMMENDED } = await import(new URL('../scripts/install-scope.mjs', import.meta.url).href);
785
+ const current = detectCurrentScope();
786
+ if (current && current.scope !== RECOMMENDED) {
787
+ console.log(`\n${c.dim(' ── how this is set up on your machine ──')}`);
788
+ for (const line of String(explainChoice({ current: current.scope })).split('\n').slice(0, 12)) {
789
+ console.log(` ${line}`);
790
+ }
791
+ }
792
+ } catch { /* informational only β€” never break an install */ }
793
+
794
+ try {
795
+ const { shouldNotify, noticeFor } = await import(new URL('../scripts/upgrade-notice.mjs', import.meta.url).href);
796
+ const v = wrapperVersion();
797
+ if (v && shouldNotify(v)) {
798
+ const notice = noticeFor(v);
799
+ if (notice) {
800
+ console.log('');
801
+ for (const line of String(notice).split('\n').slice(0, 14)) console.log(` ${line}`);
802
+ }
803
+ }
804
+ } catch { /* informational only */ }
805
+
753
806
  console.log(`\n Now try it for real: open Claude Code in any project and ask it something about`);
754
807
  console.log(` RuVector, Ruflo, AgentDB, or SPARC β€” it'll ground the same way, automatically.`);
755
808
  console.log(` Run this demo again any time: ${c.bold('npx ruvnet-brain --demo')}`);
@@ -1166,10 +1219,15 @@ function enableNightly() {
1166
1219
  process.exit(1);
1167
1220
  }
1168
1221
 
1169
- // Template the plist to THIS user's kb dir + node binary. Quotes guard paths with spaces;
1170
- // xmlEscape guards the XML (>> and && must survive as shell operators after plist parsing).
1222
+ // Template the plist to THIS user's kb dir + node binary.
1223
+ //
1224
+ // No `/bin/sh -c` (ADR-038): a LaunchAgent whose ProgramArguments invoke a shell is the standard
1225
+ // macOS persistence pattern, and EDR persistence monitors score it well above a plist that execs a
1226
+ // binary directly. launchd provides everything the shell was doing here natively β€”
1227
+ // WorkingDirectory replaces `cd`, StandardOutPath/StandardErrorPath replace `>>` and `2>&1` β€” so
1228
+ // dropping the shell costs nothing and removes both a shell parse of interpolated paths and the
1229
+ // signature. Same schedule, same command, same log.
1171
1230
  const logPath = path.join(kbDir, 'update.log');
1172
- const shellCmd = `cd "${kbDir}" && "${process.execPath}" forge-update.mjs --apply >> "${logPath}" 2>&1`;
1173
1231
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
1174
1232
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1175
1233
  <plist version="1.0">
@@ -1178,10 +1236,16 @@ function enableNightly() {
1178
1236
  <string>${NIGHTLY_LABEL}</string>
1179
1237
  <key>ProgramArguments</key>
1180
1238
  <array>
1181
- <string>/bin/sh</string>
1182
- <string>-c</string>
1183
- <string>${xmlEscape(shellCmd)}</string>
1239
+ <string>${xmlEscape(process.execPath)}</string>
1240
+ <string>forge-update.mjs</string>
1241
+ <string>--apply</string>
1184
1242
  </array>
1243
+ <key>WorkingDirectory</key>
1244
+ <string>${xmlEscape(kbDir)}</string>
1245
+ <key>StandardOutPath</key>
1246
+ <string>${xmlEscape(logPath)}</string>
1247
+ <key>StandardErrorPath</key>
1248
+ <string>${xmlEscape(logPath)}</string>
1185
1249
  <key>StartCalendarInterval</key>
1186
1250
  <dict>
1187
1251
  <key>Hour</key>
@@ -2506,17 +2570,25 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
2506
2570
  FLAG_LOCAL || fs.existsSync(path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip'));
2507
2571
  // Reuse the staleness check's resolution when it already ran β€” one network round-trip, not two.
2508
2572
  const release = localZipPresent ? null : (resolvedRelease || await resolveRelease());
2509
- const { zipPath, tmpDir, downloaded } = await obtainBundle(release);
2573
+ const { zipPath, tmpDir, downloaded, sigError } = await obtainBundle(release);
2510
2574
  // Verify the Ed25519 signature BEFORE extracting a downloaded bundle into the user's config
2511
- // (SEC-0010 #6 β€” trust root = keys/ruvnet-brain-signing.pub.pem shipped inside this package).
2512
- // SIGNING_REQUIRED is transitional: while releases predate signing, a MISSING sig warns-and-proceeds
2513
- // but a PRESENT-but-INVALID sig ALWAYS fails closed. Flip to true once every release is signed.
2514
- const SIGNING_REQUIRED = false;
2575
+ // (SEC-0010 #6 β€” trust root = the pubkey EMBEDDED in this file, so an attacker who swaps the
2576
+ // bundle cannot also swap the key it is checked against).
2577
+ //
2578
+ // SIGNING_REQUIRED was `false` transitionally, for releases that predated signing. That is over:
2579
+ // every release from v2.0.0 on is signed, including the pinned offline fallback (RELEASE_VERSION).
2580
+ // Leaving it false left a real downgrade path β€” strip or 404 the small .sig file and the missing-
2581
+ // signature branch printed a warning and extracted 800MB+ of executable .mjs anyway. No alarm
2582
+ // fired, because no signature was ever obtained. Now a missing signature fails closed like an
2583
+ // invalid one, and --no-verify remains the single explicit, user-chosen override.
2584
+ const SIGNING_REQUIRED = true;
2515
2585
  if (downloaded && !FLAG_NO_VERIFY) {
2516
2586
  const sigPath = `${zipPath}.sig`;
2517
2587
  const hasSig = fs.existsSync(sigPath);
2518
- if (!hasSig && !SIGNING_REQUIRED) {
2519
- warn('this release is not signed yet β€” proceeding (bundle integrity not cryptographically verified)');
2588
+ if (!hasSig) {
2589
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
2590
+ die(`no signature published alongside this release β€” refusing to extract an unverified bundle${sigError ? `\n (signature fetch failed: ${sigError})` : ''}`,
2591
+ `Every release from v2.0.0 on is signed, so a missing signature means the download was\nincomplete, blocked, or tampered with. Re-run to fetch a fresh copy. If it persists, report it.\n(Override at your own risk with ${c.bold('--no-verify')}.)`);
2520
2592
  } else {
2521
2593
  step('Verifying the bundle signature', 'so a tampered or MITM-swapped download can never be extracted');
2522
2594
  const { ok: valid, reason } = verifyBundle(zipPath, sigPath);
@@ -2570,6 +2642,39 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
2570
2642
  // out what we did β€” which is precisely the position the 2026-07-20 corporate-machine reporter was
2571
2643
  // left in. Derived from disk, so it can only ever describe what is actually there.
2572
2644
  try { printFootprint(); } catch { /* a summary must never break a finished install */ }
2645
+
2646
+ // ── SCOPE + UPGRADE, on the path a real user actually takes ─────────────────────────────────
2647
+ //
2648
+ // These two blocks were first added inside runDemo(), which only runs with --demo β€” so almost
2649
+ // nobody would have seen them. Caught by running the install path instead of reasoning about it
2650
+ // (P1: verify through the USER'S path, never your own). printFootprint() is the right neighbour:
2651
+ // it already exists to tell someone exactly what is now on their machine.
2652
+ //
2653
+ // Read-only and fail-silent. This INFORMS; it never changes scope on its own. P3 (nudge, never
2654
+ // force) and P4 (the user is the arbiter of their own machine).
2655
+ try {
2656
+ const { detectCurrentScope, explainChoice, RECOMMENDED } = await import(new URL('../scripts/install-scope.mjs', import.meta.url).href);
2657
+ const current = detectCurrentScope();
2658
+ if (current && current.scope !== RECOMMENDED) {
2659
+ console.log(`\n${c.dim(' ── how this is set up on your machine ──')}`);
2660
+ for (const line of String(explainChoice({ current: current.scope })).split('\n').slice(0, 12)) console.log(` ${line}`);
2661
+ }
2662
+ } catch { /* informational only */ }
2663
+
2664
+ try {
2665
+ const { shouldNotify, noticeFor, recordNotified } = await import(new URL('../scripts/upgrade-notice.mjs', import.meta.url).href);
2666
+ const v = wrapperVersion();
2667
+ if (v && shouldNotify(v)) {
2668
+ const notice = noticeFor(v);
2669
+ if (notice) {
2670
+ console.log('');
2671
+ for (const line of String(notice).split('\n').slice(0, 14)) console.log(` ${line}`);
2672
+ // Record it so this fires at most once per minor version β€” the anti-nag rule is only real
2673
+ // if the "already told them" state is actually written.
2674
+ try { recordNotified(v); } catch { /* best effort */ }
2675
+ }
2676
+ }
2677
+ } catch { /* informational only */ }
2573
2678
  })().catch((e) => {
2574
2679
  die(e && e.message ? e.message : String(e));
2575
2680
  });
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // verify-citation.mjs β€” decide whether an answer is GROUNDED, by ground truth rather than by vibes.
3
+ //
4
+ // WHY THIS EXISTS
5
+ // ---------------
6
+ // The old grounding check asked: does the answer contain the string "rvf" or "ruvector"? A model
7
+ // that hallucinated "just use RVF!" with zero sources passed. So did an answer citing a file that
8
+ // does not exist. Keyword presence is not evidence β€” an LLM panel once scored a zero-citation
9
+ // answer 98/100 on this repo.
10
+ //
11
+ // A citation is only real if it RESOLVES: the repo must be an indexed store on disk, and the cited
12
+ // document path must appear as the `path` of an actual passage inside that store's passages file.
13
+ // That is checkable without a model, without the network, and without trusting anything the model
14
+ // said. This module does exactly that and nothing else.
15
+ //
16
+ // The reader (`forge-ask-all.mjs`) prints each hit as:
17
+ // #1 repo=concepts ce=0.201 vec=0.8686 kind=doc
18
+ // path : concepts/ruvector/CARD/ruvector-card
19
+ // title: ruvector β€” Capability
20
+ // Note the printed path is `<repo>/<docPath>`; inside `concepts.passages.jsonl` the stored `path`
21
+ // is just `ruvector/CARD/ruvector-card` (optionally suffixed `#0`, `#1`, … when chunked).
22
+
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import readline from 'node:readline';
26
+
27
+ /** Parse the reader's stdout into structured citations. Never throws; unparseable input β†’ []. */
28
+ export function parseCitations(stdout) {
29
+ const out = [];
30
+ const text = String(stdout ?? '');
31
+ const blockRe = /^#(\d+)\s+repo=(\S+)(?:\s+ce=(-?[\d.]+))?(?:\s+vec=(-?[\d.]+))?(?:\s+kind=(\S+))?/gm;
32
+ let m;
33
+ while ((m = blockRe.exec(text)) !== null) {
34
+ const rest = text.slice(m.index);
35
+ const pathM = /^path\s*:\s*(.+)$/m.exec(rest);
36
+ const titleM = /^title\s*:\s*(.+)$/m.exec(rest);
37
+ if (!pathM) continue;
38
+ const repo = m[2];
39
+ const fullPath = pathM[1].trim();
40
+ // Strip the repo prefix the reader adds, so the remainder can be matched against the store.
41
+ const docPath = fullPath.startsWith(`${repo}/`) ? fullPath.slice(repo.length + 1) : fullPath;
42
+ out.push({
43
+ rank: Number(m[1]),
44
+ repo,
45
+ ce: m[3] !== undefined ? Number(m[3]) : null,
46
+ vec: m[4] !== undefined ? Number(m[4]) : null,
47
+ kind: m[5] ?? null,
48
+ fullPath,
49
+ docPath,
50
+ title: titleM ? titleM[1].trim() : null,
51
+ });
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /** The passages files that could hold a repo's documents β€” the slim store and the deep `.big` one. */
57
+ export function passagesFilesFor(repo, kbDir) {
58
+ return [path.join(kbDir, `${repo}.passages.jsonl`), path.join(kbDir, `${repo}.big.passages.jsonl`)]
59
+ .filter((p) => fs.existsSync(p));
60
+ }
61
+
62
+ /** True when `stored` is the cited doc, allowing for the `#N` chunk suffix the builder appends. */
63
+ function samePath(stored, docPath) {
64
+ return stored === docPath || stored.startsWith(`${docPath}#`);
65
+ }
66
+
67
+ /**
68
+ * Does this citation point at a passage that really exists on disk?
69
+ * Streams the file and stops at the first match, so a 500MB `.big` store costs only as much as it
70
+ * takes to reach the hit. A malformed JSON line is skipped, never fatal.
71
+ */
72
+ export async function citationResolves(citation, kbDir) {
73
+ const files = passagesFilesFor(citation.repo, kbDir);
74
+ if (!files.length) return { resolved: false, reason: 'no-store', file: null, storedPath: null };
75
+ for (const file of files) {
76
+ const rl = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
77
+ try {
78
+ for await (const line of rl) {
79
+ if (!line) continue;
80
+ let rec;
81
+ try { rec = JSON.parse(line); } catch { continue; }
82
+ if (typeof rec?.path === 'string' && samePath(rec.path, citation.docPath)) {
83
+ return { resolved: true, reason: 'ok', file: path.basename(file), storedPath: rec.path };
84
+ }
85
+ }
86
+ } finally {
87
+ rl.close();
88
+ }
89
+ }
90
+ return { resolved: false, reason: 'path-not-in-store', file: null, storedPath: null };
91
+ }
92
+
93
+ /**
94
+ * The gate. An answer is grounded only when it cites at least one passage that resolves on disk.
95
+ * Returns the receipt so a caller can PRINT the evidence instead of asserting a conclusion.
96
+ */
97
+ export async function verifyGrounding(stdout, kbDir) {
98
+ const citations = parseCitations(stdout);
99
+ if (!citations.length) {
100
+ return { grounded: false, reason: 'no-citations', citations: [], receipt: null };
101
+ }
102
+ for (const citation of citations) {
103
+ const r = await citationResolves(citation, kbDir);
104
+ if (r.resolved) {
105
+ return {
106
+ grounded: true,
107
+ reason: 'ok',
108
+ citations,
109
+ receipt: { repo: citation.repo, path: citation.fullPath, title: citation.title, file: r.file, storedPath: r.storedPath },
110
+ };
111
+ }
112
+ }
113
+ return { grounded: false, reason: 'citations-do-not-resolve', citations, receipt: null };
114
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "3.5.1-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.",
3
+ "version": "3.9.18-dev",
4
+ "description": "One-command installer for RuvNet Brain \u2014 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"
@@ -22,8 +22,6 @@
22
22
  "eval": "node scripts/eval-brain.mjs",
23
23
  "eval:gate": "node scripts/eval-brain.mjs --gate",
24
24
  "eval:record": "node scripts/eval-brain.mjs --record",
25
- "embed:verifier": "node scripts/embed-verifier.mjs",
26
- "embed:check": "node scripts/embed-verifier.mjs --check",
27
25
  "gists:index": "node scripts/ingest-gists.mjs --index-only",
28
26
  "gists:sync": "node scripts/ingest-gists.mjs && node kb/forge-big.mjs both --dir kb --name ruv-gists",
29
27
  "test:integration": "vitest run tests/integration",
@@ -31,13 +29,16 @@
31
29
  "catalog:verify": "node scripts/verify-model-catalog.mjs",
32
30
  "catalog:refresh": "node scripts/refresh-model-catalog.mjs",
33
31
  "falsify": "node scripts/falsify.mjs",
34
- "sbom": "npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-file sbom/ruvnet-brain.cdx.json --mc-type application --validate"
32
+ "sbom": "npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-file sbom/ruvnet-brain.cdx.json --mc-type application --validate",
33
+ "wired:check": "node scripts/wired-check.mjs --check",
34
+ "doc:currency": "node scripts/doc-currency.mjs --check"
35
35
  },
36
36
  "files": [
37
37
  "bin/install.mjs",
38
38
  "README.md",
39
39
  "LICENSE",
40
- "config/",
40
+ "config/model-router/",
41
+ "kb/verify-citation.mjs",
41
42
  "scripts/model-router-engine.mjs",
42
43
  "scripts/model-router-setup.mjs",
43
44
  "scripts/model-router-status.mjs",
@@ -1,109 +0,0 @@
1
- #!/bin/bash
2
- # Ruvnet Ecosystem Auto-Update
3
- # Updated: 2026-05-07
4
- #
5
- # Runs every night at 03:30 via ~/Library/LaunchAgents/com.stuartkerr.ruflo-autoupdate.plist
6
- # Discovers all currently-installed Ruvnet ecosystem packages and refreshes them.
7
- #
8
- # Tag policy:
9
- # - ruflo, @claude-flow/cli β†’ @alpha (Stuart runs alpha track)
10
- # - agentdb β†’ RETIRED 2026-05-30: AgentDB is bundled inside
11
- # @claude-flow/memory (Ruflo); no standalone install.
12
- # - everything else (@ruvector/*, ruvector, @claude-flow/aidefence,
13
- # agent-browser, agentic-flow, flow-nexus)
14
- # β†’ @latest
15
-
16
- set -uo pipefail
17
-
18
- LOG="$HOME/Library/Logs/ruflo-autoupdate.log"
19
- PATH="$HOME/.npm-global/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
20
- export PATH
21
-
22
- ts() { date -u +"[%Y-%m-%dT%H:%M:%SZ]"; }
23
-
24
- echo "$(ts) ──── Ruvnet ecosystem update starting ────" | tee -a "$LOG"
25
-
26
- # Packages known to fail on this platform β€” skipped from auto-update.
27
- # @ruvector/edge-net pulls wrtc which needs a darwin-arm64 prebuilt binary
28
- # from S3 that returns 404. Bug in upstream wrtc package, not actionable here.
29
- SKIP_PACKAGES="@ruvector/edge-net"
30
-
31
- is_skipped() {
32
- local pkg="$1"
33
- for sk in $SKIP_PACKAGES; do
34
- [ "$pkg" = "$sk" ] && return 0
35
- done
36
- return 1
37
- }
38
-
39
- # Discover all currently-installed packages of interest
40
- # Discover all currently-installed RuvNet packages.
41
- # 2026-07-13: the old filter listed only 7 names/prefixes, so agentic-qe, @metaharness/*, qudag,
42
- # ruv-swarm, ruvbot, ruvi, ruvector-extensions and agentic-robotics were installed but NEVER updated
43
- # β€” silently frozen at whatever version they landed on. Live drift found that day: agentic-qe
44
- # 3.11.5 vs 3.12.0, ruvbot 0.3.1 vs 0.3.2, ruvector-extensions 0.1.1 vs 0.1.2.
45
- # Principle: if it is an installed RuvNet package, it gets kept current. No arbitrary allowlist.
46
- # (@marketing/ai-swarms is a LOCAL npm link β€” never npm-install over it; it is excluded by name.)
47
- PACKAGES=$(npm ls -g --depth=0 --json 2>/dev/null | jq -r '
48
- .dependencies // {} | keys[] | select(
49
- startswith("@ruvector/") or
50
- startswith("@claude-flow/") or
51
- startswith("@metaharness/") or
52
- startswith("@agentic-robotics/") or
53
- . == "ruflo" or
54
- . == "ruvector" or
55
- . == "ruvector-extensions" or
56
- . == "agent-browser" or
57
- . == "agentic-flow" or
58
- . == "agentic-qe" or
59
- . == "agentic-robotics" or
60
- . == "flow-nexus" or
61
- . == "qudag" or
62
- . == "ruv-swarm" or
63
- . == "ruvbot" or
64
- . == "ruvi"
65
- ) | select(startswith("@marketing/") | not)
66
- ')
67
-
68
- if [ -z "$PACKAGES" ]; then
69
- echo "$(ts) ERROR: no Ruvnet packages found via 'npm ls -g'" | tee -a "$LOG"
70
- exit 1
71
- fi
72
-
73
- # Build install args with the right tag per package
74
- INSTALL_ARGS=""
75
- SKIPPED=""
76
- ALPHA_PACKAGES="ruflo @claude-flow/cli"
77
- for pkg in $PACKAGES; do
78
- if is_skipped "$pkg"; then
79
- SKIPPED="$SKIPPED $pkg"
80
- continue
81
- fi
82
- TAG="@latest"
83
- for alpha_pkg in $ALPHA_PACKAGES; do
84
- if [ "$pkg" = "$alpha_pkg" ]; then
85
- TAG="@alpha"
86
- break
87
- fi
88
- done
89
- INSTALL_ARGS="$INSTALL_ARGS ${pkg}${TAG}"
90
- done
91
-
92
- if [ -n "$SKIPPED" ]; then
93
- echo "$(ts) Skipping known-broken packages:$SKIPPED" | tee -a "$LOG"
94
- fi
95
-
96
- echo "$(ts) Updating: $INSTALL_ARGS" | tee -a "$LOG"
97
-
98
- # Run the install β€” keep going even if one package fails
99
- npm install -g $INSTALL_ARGS 2>&1 | tee -a "$LOG"
100
-
101
- EXIT_CODE=$?
102
- if [ $EXIT_CODE -eq 0 ]; then
103
- echo "$(ts) βœ“ auto-update finished cleanly" | tee -a "$LOG"
104
- else
105
- echo "$(ts) ⚠ auto-update finished with exit code $EXIT_CODE β€” check log for failed packages" | tee -a "$LOG"
106
- fi
107
-
108
- echo "" | tee -a "$LOG"
109
- exit $EXIT_CODE
@@ -1,119 +0,0 @@
1
- {
2
- "_why": "THE REGISTRY OF WHAT MUST BE RUNNING. Created 2026-07-13 after a failure that must never repeat: com.ruvnet.brain-nightly's launchd trigger had NEVER fired, and nothing noticed, because 'is it running?' was only ever answered by looking at a job's own exit code \u2014 and launchd reports exit 0 for a job that has never run, which is indistinguishable from success. Silence was being read as health.",
3
- "_how_it_works": "scripts/job-heartbeat.sh wraps every job and writes start/end/exit receipts that a dying job cannot forge or skip (trap-protected). scripts/nightly-watchdog.mjs compares THIS registry against reality: a job listed here that is not loaded, or loaded but has no fresh receipt, or has a receipt with a non-zero exit, is a VIOLATION and gongs the phone. Absence of evidence is failure, never 'probably fine'.",
4
- "_adding_a_job": "Add it here FIRST, then wrap its plist command in job-heartbeat.sh. A job not in this registry is unwatched by definition \u2014 that is the whole point of a registry rather than per-job good intentions.",
5
- "heartbeatDir": "~/.cache/ruvnet-brain/heartbeats",
6
- "jobs": [
7
- {
8
- "label": "com.ruvnet.brain-nightly",
9
- "what": "Refreshes every RuvNet repo, rebuilds the brain, publishes the GitHub Release",
10
- "schedule": "daily 03:15",
11
- "maxAgeHours": 26,
12
- "required": true,
13
- "legacyLog": "logs/nightly.log"
14
- },
15
- {
16
- "label": "com.ruvnet.brain-gists",
17
- "what": "Pulls rUv's new gists into the brain and re-embeds when they change",
18
- "schedule": "daily 21:47",
19
- "maxAgeHours": 26,
20
- "required": true,
21
- "legacyLog": "logs/gists-nightly.log"
22
- },
23
- {
24
- "label": "com.ruvnet.goldie-weekly",
25
- "what": "Re-researches the cheapest/best models and refreshes the router catalog",
26
- "schedule": "Mondays 07:30",
27
- "maxAgeHours": 192,
28
- "required": true,
29
- "legacyLog": "logs/goldie.log"
30
- },
31
- {
32
- "label": "com.ruvnet.model-refresh",
33
- "what": "Refreshes the live model list (~/.claude/models.json) that the router prices against",
34
- "schedule": "daily 09:00",
35
- "maxAgeHours": 26,
36
- "required": true
37
- },
38
- {
39
- "label": "com.ruvnet.npm-token-renew",
40
- "what": "Renews the npm publish token before it expires (silently lets publishing die if it stops)",
41
- "schedule": "daily 03:33",
42
- "maxAgeHours": 26,
43
- "required": true,
44
- "_was_blind": "THE PUREST CASE OF THE BUG. On a healthy day this script returns early and writes ZERO BYTES anywhere \u2014 its log and state file sat frozen 2+ days stale while it fired on schedule every night. 'Ran and did nothing (healthy)' was literally indistinguishable from 'never ran', ~76 days out of every 90. The heartbeat wrapper fixes this for free: the RECEIPT is written by the wrapper, so the job no longer has to remember to report."
45
- },
46
- {
47
- "label": "com.stuartkerr.api-spend-watchdog",
48
- "what": "Hourly guard against runaway API spend and agent bursts \u2014 the one that actually pages the phone",
49
- "schedule": "hourly",
50
- "maxAgeHours": 3,
51
- "required": true,
52
- "_note": "It has been pushing '4 scheduled jobs failing silently' every hour, correctly \u2014 but off launchctl's exit-code field, which CANNOT see a job that never ran. It alerts; it just can't see the hole. Now it is itself watched: nothing supervised the supervisor."
53
- },
54
- {
55
- "label": "com.stuartkerr.ruflo-autoupdate",
56
- "what": "Keeps the whole RuvNet/Ruflo npm stack current (npm i -g ~26 packages on @latest/@alpha)",
57
- "schedule": "daily 03:30",
58
- "maxAgeHours": 26,
59
- "required": true,
60
- "_note": "This is THE job that keeps rUv's stack fresh on this machine. It was unwatched until 2026-07-13 \u2014 the single most important update job here had no supervision at all."
61
- },
62
- {
63
- "label": "com.cognitum.ruvector-autoupdate",
64
- "what": "Every 6h: git pull of ~/RuVector_Clean (the RuVector source the Rust crates build from)",
65
- "schedule": "every 6h",
66
- "maxAgeHours": 8,
67
- "required": true,
68
- "_note": "Its git stash pop has silently failed once (orphaned stash@{0} from 2026-06-28 \u2014 it swallowed local edits and nothing said so). The heartbeat now catches a non-zero exit; the orphaned stash still needs a human decision."
69
- },
70
- {
71
- "label": "com.stuartkerr.clear-claude-tmp",
72
- "what": "Every 3h: purges Claude Code task-output temp files older than 2 days (the dir hits ~800MB)",
73
- "schedule": "every 3h",
74
- "maxAgeHours": 5,
75
- "required": true,
76
- "_was_lying": "Its log line had the date BAKED IN at plist-write time \u2014 all 43 entries since 2026-04-06 were byte-identical. It worked; its log was a lie. Now a real script with a real $(date) and a real delete count."
77
- },
78
- {
79
- "label": "com.ruvnet.nightly-watchdog",
80
- "what": "Watches all of the above. Listed here so that IT is watched too \u2014 a watchdog nobody watches is the same blind spot one level up",
81
- "schedule": "daily 09:00",
82
- "maxAgeHours": 26,
83
- "required": true
84
- },
85
- {
86
- "label": "com.ruvnet.issue-watch",
87
- "what": "Hourly GitHub-issues SLA watcher (stuinfla/ruvnet-brain) \u2014 pages ntfy when an open issue sits >4h with no comment from the repo owner",
88
- "schedule": "hourly",
89
- "maxAgeHours": 3,
90
- "required": true
91
- },
92
- {
93
- "label": "com.ruvnet.issue-fix",
94
- "what": "Every 30 min (was 10 \u2014 a real run takes ~13 min, so the 10-min schedule SIGTERMed its own overlapping runs, fixed 2026-07-18): auto-fixes newly opened GitHub issues (stuinfla/ruvnet-brain) \u2014 bounded headless `claude -p` per issue in a disposable git worktree, pushes an issue-fix/<N> branch + comment or posts an honest triage comment, never touches main, never closes an issue",
95
- "schedule": "every 10 min",
96
- "maxAgeHours": 1,
97
- "required": true
98
- },
99
- {
100
- "label": "com.ruvnet.routing-flywheel",
101
- "schedule": "nightly 04:45",
102
- "maxAgeHours": 26,
103
- "what": "nightly data-readiness DRY-RUN of the routing flywheel (the plist runs --dry-run: reads receipts + prints readiness, runs NO loop and writes NO receipt β€” honest label per F13 2026-07-18; switch the plist to --synthetic to run a real $0 loop)"
104
- },
105
- {
106
- "label": "com.ruvnet.npx-witness",
107
- "schedule": "WatchPaths on ~/.npm/_npx (event-driven, no cadence)",
108
- "maxAgeHours": 2160,
109
- "what": "records every npx-cache mutation to the witness log (pure observer). Event-driven: weeks of silence are NORMAL when nothing touches _npx β€” maxAgeHours is a 90-day tripwire against the job being unloaded, not a freshness SLA. Registered per F2 2026-07-18: it was loaded+wrapped but absent here, i.e. unwatched by the registry's own definition."
110
- }
111
- ],
112
- "_retired": [
113
- {
114
- "label": "io.ruv.auto-subscribe",
115
- "retired": "2026-07-14",
116
- "why": "archived as cruft (archive-cruft-2026-07-14/); hourly RuvNet package auto-install is superseded by com.stuartkerr.ruflo-autoupdate (daily) + com.cognitum.ruvector-autoupdate. Removed from watch registry 2026-07-18 so nightly-watchdog stops flagging a deliberately-killed job."
117
- }
118
- ]
119
- }