ruvnet-brain 3.6.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 +70 -4
- package/bin/install.mjs +129 -24
- package/kb/verify-citation.mjs +114 -0
- package/package.json +7 -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?"**
|
|
@@ -297,6 +360,9 @@ claude plugin install ruvnet-brain@ruvnet-brain --scope user
|
|
|
297
360
|
|
|
298
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.
|
|
299
362
|
|
|
363
|
+
</details>
|
|
364
|
+
</details>
|
|
365
|
+
</details>
|
|
300
366
|
</details>
|
|
301
367
|
</details>
|
|
302
368
|
</details>
|
|
@@ -340,7 +406,7 @@ Plus: the **βtake the wheelβ behavioral pipeline** (below), a **4-level beha
|
|
|
340
406
|
|
|
341
407
|
## How it works
|
|
342
408
|
|
|
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,
|
|
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.
|
|
344
410
|
|
|
345
411
|

|
|
346
412
|
|
|
@@ -450,7 +516,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
450
516
|
|
|
451
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:
|
|
452
518
|
|
|
453
|
-
- β
**The grounding brain is real and proven** β 54 public stores Β· 149,
|
|
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.
|
|
454
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).
|
|
455
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).
|
|
456
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
|
-
//
|
|
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>
|
|
@@ -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 =
|
|
2512
|
-
//
|
|
2513
|
-
//
|
|
2514
|
-
|
|
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
|
|
2519
|
-
|
|
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.
|
|
4
|
-
"description": "One-command installer for RuvNet Brain
|
|
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
|
-
}
|