ruvnet-brain 3.6.1-dev β 3.9.50-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 +76 -7
- package/bin/install.mjs +296 -31
- package/kb/verify-citation.mjs +114 -0
- package/package.json +8 -6
- package/config/ruvnet-autoupdate.sh.snapshot +0 -109
- package/config/scheduled-jobs.json +0 -119
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# π§ RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### π§ RuvNet Brain β [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
8
8
|
|
|
9
9
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack β delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
10
10
|
|
|
@@ -56,7 +56,70 @@
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
## What's new in 3.
|
|
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 — what 3.8 shipped</b> · 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 — what 3.7 shipped</b> · 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 — what 3.6 shipped</b> · it could finally show you what you own. <i>Expand.</i></summary>
|
|
121
|
+
|
|
122
|
+
## 3.6 β it can finally show you what you own
|
|
60
123
|
|
|
61
124
|
**Shipped 2026-07-22.** 3.5 made the brain speak. 3.6 makes it *legible*: a console panel that
|
|
62
125
|
answers the question the owner has been asking for weeks β **"what is actually turned on?"**
|
|
@@ -79,9 +142,12 @@ answers the question the owner has been asking for weeks β **"what is actually
|
|
|
79
142
|
had zero call sites; the client referenced it only in comments. Built-tested-unwired is this
|
|
80
143
|
project's signature failure, and it happened three times in one night.
|
|
81
144
|
|
|
82
|
-
Honest limit: this is
|
|
83
|
-
(contextual, anticipatory, compounding) and
|
|
84
|
-
|
|
145
|
+
Honest limit: this is still a dev release, **not 4.0**. 4.0 requires levels 3β5 of ADR-028's ladder
|
|
146
|
+
(contextual, anticipatory, compounding) **shipped and independently graded β₯95**. The mechanisms now
|
|
147
|
+
exist β L3's delivery seam (the chokepoint), an L4 goal surface, and L5 cross-project promotion (lessons
|
|
148
|
+
promoted to your global brain, survival proven by isolation) β but the ladder still grades **L2βL3**,
|
|
149
|
+
because L3 is built rather than live (deploy-gated) and the five acceptance metrics aren't all measured.
|
|
150
|
+
See `docs/4.0-READINESS.md` and `docs/4.0-EXECUTIVE-BRIEFING.md` (last independent grade: 70/100 overall).
|
|
85
151
|
|
|
86
152
|
<details>
|
|
87
153
|
<summary><b>Earlier — what 3.5 shipped</b> · it stopped waiting to be asked. <i>Expand for the receipts.</i></summary>
|
|
@@ -297,6 +363,9 @@ claude plugin install ruvnet-brain@ruvnet-brain --scope user
|
|
|
297
363
|
|
|
298
364
|
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.
|
|
299
365
|
|
|
366
|
+
</details>
|
|
367
|
+
</details>
|
|
368
|
+
</details>
|
|
300
369
|
</details>
|
|
301
370
|
</details>
|
|
302
371
|
</details>
|
|
@@ -340,7 +409,7 @@ Plus: the **βtake the wheelβ behavioral pipeline** (below), a **4-level beha
|
|
|
340
409
|
|
|
341
410
|
## How it works
|
|
342
411
|
|
|
343
|
-
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,
|
|
412
|
+
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,729 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.
|
|
344
413
|
|
|
345
414
|

|
|
346
415
|
|
|
@@ -450,7 +519,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
450
519
|
|
|
451
520
|
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:
|
|
452
521
|
|
|
453
|
-
- β
**The grounding brain is real and proven** β 54 public stores Β· 149,
|
|
522
|
+
- β
**The grounding brain is real and proven** β 54 public stores Β· 149,729 public source chunks (57 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
|
|
454
523
|
- β
**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).
|
|
455
524
|
- β
**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).
|
|
456
525
|
- β οΈ **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
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
//
|
|
585
|
-
|
|
586
|
-
//
|
|
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
|
-
|
|
607
|
+
const shipped = path.join(REPO_ROOT, 'kb', 'verify-citation.mjs');
|
|
608
|
+
if (!fs.existsSync(shipped)) return 'unavailable';
|
|
597
609
|
try {
|
|
598
|
-
fs.
|
|
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.
|
|
1170
|
-
//
|
|
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
|
|
1182
|
-
<string
|
|
1183
|
-
<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>
|
|
@@ -1348,6 +1412,12 @@ function disableSpendGuard() {
|
|
|
1348
1412
|
else ok('spend watchdog was already off β nothing to remove (safe to run any time)');
|
|
1349
1413
|
}
|
|
1350
1414
|
|
|
1415
|
+
// Default STATE_PATH from scripts/upgrade-notice.mjs, duplicated (not imported β see the cmpTag()
|
|
1416
|
+
// comment above this file's own rule on why) so machineFootprint()/uninstallAll() can find and
|
|
1417
|
+
// remove it without a module this file may not statically depend on.
|
|
1418
|
+
const upgradeNoticeStatePath = () =>
|
|
1419
|
+
process.env.RUVNET_UPGRADE_NOTICE_FILE || path.join(os.homedir(), '.config', 'ruvnet-brain', 'upgrade-notice.json');
|
|
1420
|
+
|
|
1351
1421
|
/**
|
|
1352
1422
|
* Everything this installer can leave on a machine, DERIVED from disk β never asserted.
|
|
1353
1423
|
*
|
|
@@ -1371,7 +1441,14 @@ export function machineFootprint() {
|
|
|
1371
1441
|
if (cmds) items.push({
|
|
1372
1442
|
label: 'Claude Code plugin',
|
|
1373
1443
|
path: path.dirname(cmds),
|
|
1374
|
-
|
|
1444
|
+
// `claude plugin uninstall` removes the INSTALLED plugin only β it leaves the marketplace itself
|
|
1445
|
+
// registered (a separate git clone + registry entry: `claude plugin marketplace list` still
|
|
1446
|
+
// shows it, and wirePlugin() above is the thing that added it via `claude plugin marketplace add
|
|
1447
|
+
// stuinfla/ruvnet-brain`). Verified live against `claude plugin marketplace --help` (marketplace
|
|
1448
|
+
// name = "ruvnet-brain", from this repo's own .claude-plugin/marketplace.json) that `remove` takes
|
|
1449
|
+
// that same short name. Without this second command someone who ran every undo we printed would
|
|
1450
|
+
// still have us registered as a marketplace.
|
|
1451
|
+
undo: 'claude plugin uninstall ruvnet-brain@ruvnet-brain && claude plugin marketplace remove ruvnet-brain',
|
|
1375
1452
|
});
|
|
1376
1453
|
const cmdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
1377
1454
|
try {
|
|
@@ -1393,12 +1470,21 @@ export function machineFootprint() {
|
|
|
1393
1470
|
add('Status-bar preference', path.join(telemetryStateDir(), '.statusline-pref'), 'delete this file');
|
|
1394
1471
|
add('Model-router files', path.join(os.homedir(), '.claude', 'model-router'),
|
|
1395
1472
|
'rm -rf ~/.claude/model-router');
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1473
|
+
// GAP FIX: this used to be a loose `.includes('ruvnet-brain')` substring test against the WHOLE
|
|
1474
|
+
// settings.json file β which (a) could also fire on something unrelated (e.g. a marketplace
|
|
1475
|
+
// autoUpdate setting that merely mentions our name) and get mislabeled as "the statusLine entry",
|
|
1476
|
+
// and (b) meant this was ALWAYS reported as a manual edit, never auto-removed, even though this
|
|
1477
|
+
// installer is the one that wrote the key and knows exactly what it wrote. detectStatusLine() now
|
|
1478
|
+
// checks the actual parsed statusLine.command against the exact string writeSettingsStatusLine()
|
|
1479
|
+
// writes β precise enough that removeSettingsStatusLine() (called from uninstallAll()) can safely
|
|
1480
|
+
// reverse just this key, the same way removeClaudeMdBlock() reverses just its own block. A status
|
|
1481
|
+
// line a user has since edited or folded into their own script no longer matches and is correctly
|
|
1482
|
+
// left off this list entirely (nothing to claim, nothing to undo).
|
|
1398
1483
|
try {
|
|
1399
|
-
const settings =
|
|
1400
|
-
|
|
1401
|
-
|
|
1484
|
+
const settings = settingsJsonPath();
|
|
1485
|
+
const detected = detectStatusLine(settings);
|
|
1486
|
+
if (detected.hasStatusLine && !detected.parseError && detected.command === `node "${statuslineHelperPath()}"`) {
|
|
1487
|
+
items.push({ label: 'The statusLine entry in settings.json', path: settings, undo: 'npx ruvnet-brain --uninstall (removes just this key; settings.json is backed up first)' });
|
|
1402
1488
|
}
|
|
1403
1489
|
} catch { /* unreadable β do not claim it */ }
|
|
1404
1490
|
try {
|
|
@@ -1407,6 +1493,14 @@ export function machineFootprint() {
|
|
|
1407
1493
|
items.push({ label: 'The search_ruvnet MCP server registration', path: claudeJson, undo: 'claude mcp remove ruvnet-brain --scope user' });
|
|
1408
1494
|
}
|
|
1409
1495
|
} catch { /* unreadable β do not claim it */ }
|
|
1496
|
+
// GAP FIX: recordNotified() (scripts/upgrade-notice.mjs, invoked from main() and runDemo() below)
|
|
1497
|
+
// writes this file the first time a "what's new" notice is shown β a real disk artifact this
|
|
1498
|
+
// installer's own code path creates, that was never listed here and never removed on --uninstall.
|
|
1499
|
+
// Path duplicated rather than imported (see the cmpTag() comment above: this file must not import
|
|
1500
|
+
// from scripts/ at module scope, since scripts/upgrade-notice.mjs isn't in the npm `files` list β
|
|
1501
|
+
// it only reaches a user via a repo clone or `npx github:...`, never a plain npm publish; harmless
|
|
1502
|
+
// no-op existence check either way). Env override name matches STATE_PATH there exactly.
|
|
1503
|
+
add('Upgrade-notice state (release-notice tracking)', upgradeNoticeStatePath(), 'delete this file');
|
|
1410
1504
|
|
|
1411
1505
|
return items;
|
|
1412
1506
|
}
|
|
@@ -1487,7 +1581,10 @@ function uninstallAll() {
|
|
|
1487
1581
|
// ours to delete, so they are handed over as commands.
|
|
1488
1582
|
const AUTO = new Set(['Brain bundle (knowledge base)', 'Nightly updater (LaunchAgent)',
|
|
1489
1583
|
'Spend watchdog (LaunchAgent)', 'Spend watchdog script', 'CLAUDE.md block (6 lines, between markers)',
|
|
1490
|
-
'Model-router files', 'Status-bar version script', 'Status-bar preference', 'Usage-counts preference'
|
|
1584
|
+
'Model-router files', 'Status-bar version script', 'Status-bar preference', 'Usage-counts preference',
|
|
1585
|
+
// Two gaps closed here: the statusLine KEY is now removable in place (we know exactly what we
|
|
1586
|
+
// wrote β see removeSettingsStatusLine()), and the upgrade-notice tracker is a file we fully own.
|
|
1587
|
+
'The statusLine entry in settings.json', 'Upgrade-notice state (release-notice tracking)']);
|
|
1491
1588
|
const willRemove = before.filter((it) => AUTO.has(it.label));
|
|
1492
1589
|
const manual = before.filter((it) => !AUTO.has(it.label));
|
|
1493
1590
|
|
|
@@ -1506,6 +1603,14 @@ function uninstallAll() {
|
|
|
1506
1603
|
const claudeMd = removeClaudeMdBlock();
|
|
1507
1604
|
if (claudeMd === 'removed') ok('removed our block from ~/.claude/CLAUDE.md (your content untouched, backup saved)');
|
|
1508
1605
|
|
|
1606
|
+
// GAP FIX: this used to be permanently "manual" β machineFootprint() treated ANY settings.json edit
|
|
1607
|
+
// as something only the user could safely touch. That blanket rule was right for edits we cannot
|
|
1608
|
+
// attribute with confidence, but wrong for this one specific key: we wrote it ourselves and know
|
|
1609
|
+
// the exact string we wrote, so we can reverse exactly that (never a status line the user has since
|
|
1610
|
+
// customized β removeSettingsStatusLine() checks the live command before touching anything).
|
|
1611
|
+
const statusLine = removeSettingsStatusLine();
|
|
1612
|
+
if (statusLine === 'removed') ok('removed the statusLine entry from ~/.claude/settings.json (your other settings untouched, backup saved)');
|
|
1613
|
+
|
|
1509
1614
|
// NEVER rm -rf A PATH WE HAVE NOT PROVEN IS OURS. resolvedKbDir() honours $RUVNET_BRAIN_KB, which
|
|
1510
1615
|
// the docs encourage for custom install locations β so `RUVNET_BRAIN_KB=$HOME npx ruvnet-brain
|
|
1511
1616
|
// --uninstall` would have recursively deleted the user's home directory. Found by adversarial
|
|
@@ -1536,6 +1641,11 @@ function uninstallAll() {
|
|
|
1536
1641
|
['status-bar script', path.join(os.homedir(), '.cache', 'ruvnet-brain', 'ruvnet-brain-statusline.cjs')],
|
|
1537
1642
|
['status-bar preference', path.join(telemetryStateDir(), '.statusline-pref')],
|
|
1538
1643
|
['usage-counts preference', telemetryConsentPath()],
|
|
1644
|
+
// GAP FIX: written by recordNotified() (scripts/upgrade-notice.mjs) whenever the "what's new"
|
|
1645
|
+
// notice fires from main()/runDemo() below β a single-purpose file under a directory this
|
|
1646
|
+
// installer alone writes to, safe to remove outright (not the whole ~/.config/ruvnet-brain/ dir,
|
|
1647
|
+
// which can also hold lessons.json/settings.json this installer never creates and must not touch).
|
|
1648
|
+
['upgrade-notice state', upgradeNoticeStatePath()],
|
|
1539
1649
|
]) {
|
|
1540
1650
|
if (!fs.existsSync(target)) continue;
|
|
1541
1651
|
try { fs.rmSync(target, { recursive: true, force: true }); ok(`removed the ${label}`); }
|
|
@@ -1868,6 +1978,91 @@ export async function offerTelemetry(cacheDir) {
|
|
|
1868
1978
|
* So: persistent background jobs and global-config edits require their OWN explicit flag. There is
|
|
1869
1979
|
* no combination of `-y` alone that installs a daemon.
|
|
1870
1980
|
*/
|
|
1981
|
+
/**
|
|
1982
|
+
* THE PLAN β everything this run may do, stated BEFORE the first thing is done.
|
|
1983
|
+
*
|
|
1984
|
+
* WHY THIS EXISTS (real user feedback, 2026-07-24, relayed by the owner): people were not running
|
|
1985
|
+
* `npx ruvnet-brain` *because they could not tell what it would do.* One of them, a sophisticated
|
|
1986
|
+
* user, put the general objection precisely: he dislikes "the virus/plugin approach⦠it all works in
|
|
1987
|
+
* memory, with invisible hooks and all."
|
|
1988
|
+
*
|
|
1989
|
+
* The installer already asked consent for every high-impact step β watchdog, nightly updates,
|
|
1990
|
+
* telemetry, stack tools, statusline. That was necessary and NOT sufficient: consent granted one
|
|
1991
|
+
* question at a time, after the run has already started, never tells you the SHAPE of what you
|
|
1992
|
+
* agreed to. You cannot decline a thing you have not yet been told is coming, and a person deciding
|
|
1993
|
+
* whether to paste a command into their terminal is deciding about the whole run, not about step 4.
|
|
1994
|
+
*
|
|
1995
|
+
* So: the whole list, up front, with what each one costs you and how to undo it. Steps marked [?] are
|
|
1996
|
+
* asked individually as before β this screen does not replace those prompts, it makes them
|
|
1997
|
+
* predictable. Nothing here mutates anything; it prints and waits.
|
|
1998
|
+
*
|
|
1999
|
+
* TRUTHFULNESS RULE: every line below names a real step this file performs and a real reversal
|
|
2000
|
+
* command. If a step is added to the installer and not to this list, the list becomes a lie about
|
|
2001
|
+
* the installer β which is worse than having no list. Keep them together.
|
|
2002
|
+
*/
|
|
2003
|
+
async function printPlanAndConfirm() {
|
|
2004
|
+
const H = os.homedir();
|
|
2005
|
+
const short = (p) => p.replace(H, '~');
|
|
2006
|
+
|
|
2007
|
+
const steps = [
|
|
2008
|
+
{ auto: true, name: 'Download the knowledge base',
|
|
2009
|
+
// short() already renders $HOME as "~"; prefixing another one produced "~~/.cache/β¦".
|
|
2010
|
+
cost: short(resolveCacheDir().cacheDir),
|
|
2011
|
+
what: 'Real rUv source, on your disk, so answers cite files instead of guessing.',
|
|
2012
|
+
undo: 'npx ruvnet-brain --uninstall' },
|
|
2013
|
+
{ auto: true, name: 'Register the Claude Code plugin + MCP server',
|
|
2014
|
+
cost: `an entry in ${short(path.join(H, '.claude'))}`,
|
|
2015
|
+
what: 'Gives Claude a search_ruvnet tool. Adds no hooks you have not agreed to.',
|
|
2016
|
+
undo: 'npx ruvnet-brain --uninstall' },
|
|
2017
|
+
{ ask: true, name: 'Add rUv tools you are missing',
|
|
2018
|
+
cost: 'npm installs, only the ones you pick',
|
|
2019
|
+
what: 'So the brain can build with them, not just answer questions about them.',
|
|
2020
|
+
undo: 'npm uninstall -g <tool>' },
|
|
2021
|
+
{ ask: true, name: 'Nightly auto-updates',
|
|
2022
|
+
cost: 'one LaunchAgent',
|
|
2023
|
+
what: 'Keeps the KB and plugin current. Recommended β rUv ships fast, and a stale brain is the main way this stops being useful.',
|
|
2024
|
+
undo: 'npx ruvnet-brain --disable-nightly' },
|
|
2025
|
+
{ ask: true, name: 'Spend watchdog',
|
|
2026
|
+
cost: 'one LaunchAgent',
|
|
2027
|
+
what: 'Warns you if an agent fleet starts burning API credit unexpectedly.',
|
|
2028
|
+
undo: 'npx ruvnet-brain --disable-spend-guard' },
|
|
2029
|
+
{ ask: true, name: 'Status-bar version segment',
|
|
2030
|
+
cost: 'one line in settings.json',
|
|
2031
|
+
what: 'Shows which brain version is live while you work.',
|
|
2032
|
+
undo: 'npx ruvnet-brain --no-statusline, or delete the statusLine entry' },
|
|
2033
|
+
{ ask: true, name: 'Anonymous usage counts',
|
|
2034
|
+
cost: 'a counter ping',
|
|
2035
|
+
what: 'Installs and searches only β never your queries, your code, or your paths.',
|
|
2036
|
+
undo: 'npx ruvnet-brain --no-telemetry' },
|
|
2037
|
+
];
|
|
2038
|
+
|
|
2039
|
+
console.log(` ${c.bold('Here is everything this will do.')} Nothing has happened yet.\n`);
|
|
2040
|
+
for (const s of steps) {
|
|
2041
|
+
const mark = s.auto ? c.green('β') : c.cyan('?');
|
|
2042
|
+
console.log(` ${mark} ${c.bold(s.name)} ${c.dim('Β· ' + s.cost)}`);
|
|
2043
|
+
console.log(` ${s.what}`);
|
|
2044
|
+
console.log(` ${c.dim('undo: ' + s.undo)}\n`);
|
|
2045
|
+
}
|
|
2046
|
+
console.log(` ${c.green('β')} happens automatically. ${c.cyan('?')} is asked first β and "no" is a complete answer.`);
|
|
2047
|
+
console.log(` ${c.dim('Every step is reversible, and `npx ruvnet-brain --what-changed` lists everything it touched.')}\n`);
|
|
2048
|
+
|
|
2049
|
+
// FLAG_YES means the caller already decided; a plan screen that blocks automation would break
|
|
2050
|
+
// agentic-kit and every scripted install. Print it, then proceed β the information is the point,
|
|
2051
|
+
// the pause is a courtesy to humans.
|
|
2052
|
+
if (FLAG_YES || FLAG_AUTO || !process.stdin.isTTY) {
|
|
2053
|
+
console.log(c.dim(' (non-interactive β continuing)\n'));
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2057
|
+
const a = await new Promise((r) => rl.question(` ${c.cyan('?')} Continue? ${c.dim('[Y/n]')} `, r));
|
|
2058
|
+
rl.close();
|
|
2059
|
+
if (/^n/i.test(String(a).trim())) {
|
|
2060
|
+
console.log(`\n Stopped. Nothing was changed.\n`);
|
|
2061
|
+
process.exit(0);
|
|
2062
|
+
}
|
|
2063
|
+
console.log('');
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1871
2066
|
function ask(question, def = false, { blanketYes = true } = {}) {
|
|
1872
2067
|
if (FLAG_YES && blanketYes) return Promise.resolve(true);
|
|
1873
2068
|
if (!process.stdin.isTTY) return Promise.resolve(def);
|
|
@@ -2160,6 +2355,33 @@ function writeSettingsStatusLine(detected, command) {
|
|
|
2160
2355
|
return backup;
|
|
2161
2356
|
}
|
|
2162
2357
|
|
|
2358
|
+
// The uninstall-side mirror of writeSettingsStatusLine() above β this installer is the ONLY writer
|
|
2359
|
+
// that can safely reverse this specific edit, because it is the only one that knows the EXACT string
|
|
2360
|
+
// it wrote. Match on that exact command (never a loose "mentions ruvnet-brain" guess β see the
|
|
2361
|
+
// machineFootprint() comment this replaces) so a status line the user has since folded their own
|
|
2362
|
+
// script into, or edited by hand, is left completely alone. Same "refuse rather than guess"
|
|
2363
|
+
// discipline removeClaudeMdBlock() already applies to CLAUDE.md, and the same backup-first courtesy.
|
|
2364
|
+
function removeSettingsStatusLine() {
|
|
2365
|
+
const settingsPath = settingsJsonPath();
|
|
2366
|
+
const detected = detectStatusLine(settingsPath);
|
|
2367
|
+
if (!detected.exists || detected.parseError || !detected.hasStatusLine) return 'absent';
|
|
2368
|
+
const ours = `node "${statuslineHelperPath()}"`;
|
|
2369
|
+
if (detected.command !== ours) return 'not-ours'; // never touch a status line we didn't write
|
|
2370
|
+
try {
|
|
2371
|
+
const backup = backupSettingsJson(settingsPath);
|
|
2372
|
+
const next = { ...(detected.json || {}) };
|
|
2373
|
+
delete next.statusLine;
|
|
2374
|
+
const tmp = `${settingsPath}.ruvnet-tmp`;
|
|
2375
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n');
|
|
2376
|
+
fs.renameSync(tmp, settingsPath);
|
|
2377
|
+
info(c.dim(` your original is saved at ${backup}`));
|
|
2378
|
+
return 'removed';
|
|
2379
|
+
} catch (e) {
|
|
2380
|
+
warn(`couldn't remove the statusLine entry (${e.message}) β remove it yourself from ${settingsPath}`);
|
|
2381
|
+
return 'error';
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2163
2385
|
// Only called after explicit consent. NEVER overwrites an existing statusLine β detectStatusLine()
|
|
2164
2386
|
// is the single source of truth for "is one already there", checked fresh right before any write.
|
|
2165
2387
|
function applyStatusline() {
|
|
@@ -2427,6 +2649,8 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
2427
2649
|
}
|
|
2428
2650
|
}
|
|
2429
2651
|
|
|
2652
|
+
await printPlanAndConfirm();
|
|
2653
|
+
|
|
2430
2654
|
const { cacheDir, isCustom } = resolveCacheDir();
|
|
2431
2655
|
|
|
2432
2656
|
// ββ "ALREADY PRESENT" IS THE WRONG QUESTION β ask "already CURRENT" ββββββββββββββββββββββββββ
|
|
@@ -2506,17 +2730,25 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
2506
2730
|
FLAG_LOCAL || fs.existsSync(path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip'));
|
|
2507
2731
|
// Reuse the staleness check's resolution when it already ran β one network round-trip, not two.
|
|
2508
2732
|
const release = localZipPresent ? null : (resolvedRelease || await resolveRelease());
|
|
2509
|
-
const { zipPath, tmpDir, downloaded } = await obtainBundle(release);
|
|
2733
|
+
const { zipPath, tmpDir, downloaded, sigError } = await obtainBundle(release);
|
|
2510
2734
|
// Verify the Ed25519 signature BEFORE extracting a downloaded bundle into the user's config
|
|
2511
|
-
// (SEC-0010 #6 β trust root =
|
|
2512
|
-
//
|
|
2513
|
-
//
|
|
2514
|
-
|
|
2735
|
+
// (SEC-0010 #6 β trust root = the pubkey EMBEDDED in this file, so an attacker who swaps the
|
|
2736
|
+
// bundle cannot also swap the key it is checked against).
|
|
2737
|
+
//
|
|
2738
|
+
// SIGNING_REQUIRED was `false` transitionally, for releases that predated signing. That is over:
|
|
2739
|
+
// every release from v2.0.0 on is signed, including the pinned offline fallback (RELEASE_VERSION).
|
|
2740
|
+
// Leaving it false left a real downgrade path β strip or 404 the small .sig file and the missing-
|
|
2741
|
+
// signature branch printed a warning and extracted 800MB+ of executable .mjs anyway. No alarm
|
|
2742
|
+
// fired, because no signature was ever obtained. Now a missing signature fails closed like an
|
|
2743
|
+
// invalid one, and --no-verify remains the single explicit, user-chosen override.
|
|
2744
|
+
const SIGNING_REQUIRED = true;
|
|
2515
2745
|
if (downloaded && !FLAG_NO_VERIFY) {
|
|
2516
2746
|
const sigPath = `${zipPath}.sig`;
|
|
2517
2747
|
const hasSig = fs.existsSync(sigPath);
|
|
2518
|
-
if (!hasSig
|
|
2519
|
-
|
|
2748
|
+
if (!hasSig) {
|
|
2749
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
2750
|
+
die(`no signature published alongside this release β refusing to extract an unverified bundle${sigError ? `\n (signature fetch failed: ${sigError})` : ''}`,
|
|
2751
|
+
`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
2752
|
} else {
|
|
2521
2753
|
step('Verifying the bundle signature', 'so a tampered or MITM-swapped download can never be extracted');
|
|
2522
2754
|
const { ok: valid, reason } = verifyBundle(zipPath, sigPath);
|
|
@@ -2570,6 +2802,39 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
2570
2802
|
// out what we did β which is precisely the position the 2026-07-20 corporate-machine reporter was
|
|
2571
2803
|
// left in. Derived from disk, so it can only ever describe what is actually there.
|
|
2572
2804
|
try { printFootprint(); } catch { /* a summary must never break a finished install */ }
|
|
2805
|
+
|
|
2806
|
+
// ββ SCOPE + UPGRADE, on the path a real user actually takes βββββββββββββββββββββββββββββββββ
|
|
2807
|
+
//
|
|
2808
|
+
// These two blocks were first added inside runDemo(), which only runs with --demo β so almost
|
|
2809
|
+
// nobody would have seen them. Caught by running the install path instead of reasoning about it
|
|
2810
|
+
// (P1: verify through the USER'S path, never your own). printFootprint() is the right neighbour:
|
|
2811
|
+
// it already exists to tell someone exactly what is now on their machine.
|
|
2812
|
+
//
|
|
2813
|
+
// Read-only and fail-silent. This INFORMS; it never changes scope on its own. P3 (nudge, never
|
|
2814
|
+
// force) and P4 (the user is the arbiter of their own machine).
|
|
2815
|
+
try {
|
|
2816
|
+
const { detectCurrentScope, explainChoice, RECOMMENDED } = await import(new URL('../scripts/install-scope.mjs', import.meta.url).href);
|
|
2817
|
+
const current = detectCurrentScope();
|
|
2818
|
+
if (current && current.scope !== RECOMMENDED) {
|
|
2819
|
+
console.log(`\n${c.dim(' ββ how this is set up on your machine ββ')}`);
|
|
2820
|
+
for (const line of String(explainChoice({ current: current.scope })).split('\n').slice(0, 12)) console.log(` ${line}`);
|
|
2821
|
+
}
|
|
2822
|
+
} catch { /* informational only */ }
|
|
2823
|
+
|
|
2824
|
+
try {
|
|
2825
|
+
const { shouldNotify, noticeFor, recordNotified } = await import(new URL('../scripts/upgrade-notice.mjs', import.meta.url).href);
|
|
2826
|
+
const v = wrapperVersion();
|
|
2827
|
+
if (v && shouldNotify(v)) {
|
|
2828
|
+
const notice = noticeFor(v);
|
|
2829
|
+
if (notice) {
|
|
2830
|
+
console.log('');
|
|
2831
|
+
for (const line of String(notice).split('\n').slice(0, 14)) console.log(` ${line}`);
|
|
2832
|
+
// Record it so this fires at most once per minor version β the anti-nag rule is only real
|
|
2833
|
+
// if the "already told them" state is actually written.
|
|
2834
|
+
try { recordNotified(v); } catch { /* best effort */ }
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
} catch { /* informational only */ }
|
|
2573
2838
|
})().catch((e) => {
|
|
2574
2839
|
die(e && e.message ? e.message : String(e));
|
|
2575
2840
|
});
|
|
@@ -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.
|
|
4
|
-
"description": "One-command installer for RuvNet Brain
|
|
3
|
+
"version": "3.9.50-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,17 @@
|
|
|
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
|
+
"status:check": "node scripts/status-honesty.mjs"
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"bin/install.mjs",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE",
|
|
40
|
-
"config/",
|
|
41
|
+
"config/model-router/",
|
|
42
|
+
"kb/verify-citation.mjs",
|
|
41
43
|
"scripts/model-router-engine.mjs",
|
|
42
44
|
"scripts/model-router-setup.mjs",
|
|
43
45
|
"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
|
-
}
|