ruvnet-brain 3.4.9-dev β 3.4.16-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 +3 -3
- package/bin/install.mjs +38 -8
- package/config/scheduled-jobs.json +8 -2
- package/package.json +1 -1
- package/scripts/job-heartbeat.sh +23 -0
- package/scripts/nightly-watchdog.mjs +22 -3
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
|
|
|
@@ -271,7 +271,7 @@ Plus: the **βtake the wheelβ behavioral pipeline** (below), a **4-level beha
|
|
|
271
271
|
|
|
272
272
|
## How it works
|
|
273
273
|
|
|
274
|
-
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 **132,
|
|
274
|
+
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 **132,216 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.
|
|
275
275
|
|
|
276
276
|

|
|
277
277
|
|
|
@@ -381,7 +381,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
|
|
|
381
381
|
|
|
382
382
|
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:
|
|
383
383
|
|
|
384
|
-
- β
**The grounding brain is real and proven** β 57 repos, 132,
|
|
384
|
+
- β
**The grounding brain is real and proven** β 57 repos, 132,216 chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
|
|
385
385
|
- β
**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).
|
|
386
386
|
- β
**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).
|
|
387
387
|
- β οΈ **Two routing residuals** (above) β surfaced, not hidden.
|
package/bin/install.mjs
CHANGED
|
@@ -740,6 +740,25 @@ async function doctor() {
|
|
|
740
740
|
env.ruvector
|
|
741
741
|
? ok('RuVector present β vector CLI / MCP available')
|
|
742
742
|
: warn('RuVector not found β answers still work. To add: claude mcp add ruvector --scope user -- npx -y ruvector mcp start');
|
|
743
|
+
// Network probe (issue #27, Jan Lafko): in a network-restricted sandbox the cold-cache embedder
|
|
744
|
+
// pull used to hang FOREVER. Diagnose the condition here, explicitly and in 3 seconds flat: if the
|
|
745
|
+
// model host is unreachable AND no local model cache exists, the first query needs the network and
|
|
746
|
+
// will fail loud (bounded by RUVNET_BRAIN_FETCH_TIMEOUT_MS) β tell the user BEFORE they hit it.
|
|
747
|
+
const modelCacheDir = process.env.KB_MODEL_CACHE || path.join(cacheDir, 'models-cache');
|
|
748
|
+
const haveLocalModel = fs.existsSync(path.join(modelCacheDir, 'Xenova', 'all-MiniLM-L6-v2'))
|
|
749
|
+
|| fs.existsSync(path.join(modelCacheDir, 'Xenova/all-MiniLM-L6-v2'));
|
|
750
|
+
try {
|
|
751
|
+
await fetch('https://huggingface.co', { method: 'HEAD', signal: AbortSignal.timeout(3000) });
|
|
752
|
+
ok('model host reachable (huggingface.co) β cold-cache model download would work');
|
|
753
|
+
} catch {
|
|
754
|
+
if (haveLocalModel) {
|
|
755
|
+
ok(`model host UNREACHABLE, but the embedder is already cached locally (${modelCacheDir}) β queries work offline`);
|
|
756
|
+
} else {
|
|
757
|
+
warn('network-restricted environment detected: huggingface.co unreachable (3s probe) AND no local model cache.');
|
|
758
|
+
warn(` The first query needs the embedder model once. Fix: on a networked machine run one query, then copy`);
|
|
759
|
+
warn(` its model cache to this machine and set KB_MODEL_CACHE to that path. (Queries fail loud, not hang.)`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
743
762
|
const v = verifyInstall(cacheDir);
|
|
744
763
|
const smoke = await smokeQuery(cacheDir);
|
|
745
764
|
const allGreen = v.repos > 0 && v.reader && v.mcp;
|
|
@@ -908,14 +927,25 @@ function runUpdate() {
|
|
|
908
927
|
const kbDir = resolvedKbDir();
|
|
909
928
|
info(`brain dir: ${c.bold(kbDir)}`);
|
|
910
929
|
let updateStatus = 1;
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
//
|
|
930
|
+
// NO updater at all = no brain installed here (or a pre-self-updater bundle). That is a USER
|
|
931
|
+
// message, not a fallback trigger: fail LOUD with the re-run-installer help and exit β never
|
|
932
|
+
// surprise the user with a full fresh install as a side effect of `--update` on an empty dir.
|
|
933
|
+
// (Restored 2026-07-18: the 2026-07-17 fallback below accidentally swallowed this branch too,
|
|
934
|
+
// turning `--update` on an empty dir into a silent 512MB re-install β it hung CI's 60s smoke
|
|
935
|
+
// test on both platforms, mutated a dir the contract promises untouched, and on a machine with
|
|
936
|
+
// private KB stores a surprise fresh PUBLIC install is exactly the store-stripping hazard the
|
|
937
|
+
// project docs warn about. The fallback's own comment scopes it to an updater that EXISTS but
|
|
938
|
+
// is broken β this branch enforces that scope.)
|
|
939
|
+
if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
|
|
940
|
+
missingUpdaterHelp(kbDir);
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
info(c.dim("running the bundle's own self-updater (backs up first, re-verifies, never half-applies)β¦\n"));
|
|
944
|
+
// Relative filename + matching cwd β same launch convention as smokeQuery(); stdio:'inherit'
|
|
945
|
+
// streams the updater's narration live and unedited.
|
|
946
|
+
const r = spawnSync(process.execPath, ['forge-update.mjs', '--apply'], { cwd: kbDir, stdio: 'inherit' });
|
|
947
|
+
updateStatus = r.error ? 1 : (r.status === null ? 1 : r.status);
|
|
948
|
+
// FALLBACK (2026-07-17), scoped 2026-07-18 to exists-but-FAILED only. An OLDER bundle whose
|
|
919
949
|
// canonicalManifestUrl points at the dead main/kb/.last-built.json path and 404s (the exact break a
|
|
920
950
|
// real user, Jan Lafko, hit). NEVER leave the user stranded at a 404: re-run THIS installer as a
|
|
921
951
|
// fresh install, which pulls the latest Release DIRECTLY (releases/latest) and never touches the
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"label": "com.ruvnet.issue-fix",
|
|
94
|
-
"what": "Every 10 min: 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",
|
|
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
95
|
"schedule": "every 10 min",
|
|
96
96
|
"maxAgeHours": 1,
|
|
97
97
|
"required": true
|
|
@@ -100,7 +100,13 @@
|
|
|
100
100
|
"label": "com.ruvnet.routing-flywheel",
|
|
101
101
|
"schedule": "nightly 04:45",
|
|
102
102
|
"maxAgeHours": 26,
|
|
103
|
-
"what": "nightly
|
|
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."
|
|
104
110
|
}
|
|
105
111
|
],
|
|
106
112
|
"_retired": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.16-dev",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain β a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/job-heartbeat.sh
CHANGED
|
@@ -32,6 +32,14 @@ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
|
32
32
|
STARTED="$(ts)"
|
|
33
33
|
START_EPOCH="$(date +%s)"
|
|
34
34
|
|
|
35
|
+
# F3 (2026-07-18): remember the receipt as it was BEFORE this fire. A skip-fire (exit 75, the
|
|
36
|
+
# reserved "another instance is already running" code) must not destroy the live run's evidence β
|
|
37
|
+
# its finish() RESTORES this snapshot instead of overwriting the receipt with a meaningless "ok/0s".
|
|
38
|
+
# Without this, a skip stamped state:"ok" over a real run's "running", and if that real run was then
|
|
39
|
+
# SIGKILLed, the watchdog's started-and-never-finished detection had nothing left to see.
|
|
40
|
+
PREV_HB=""
|
|
41
|
+
[ -f "$HB" ] && PREV_HB="$(cat "$HB" 2>/dev/null)"
|
|
42
|
+
|
|
35
43
|
# Start receipt. If the job vanishes without ever writing an end receipt, THIS is the evidence that
|
|
36
44
|
# it started and never finished β a state the watchdog reports as FAILING, not as silence.
|
|
37
45
|
cat > "$HB" <<EOF
|
|
@@ -39,6 +47,12 @@ cat > "$HB" <<EOF
|
|
|
39
47
|
EOF
|
|
40
48
|
|
|
41
49
|
notify() { # notify <title> <body> <priority>
|
|
50
|
+
# NTFY_TOPIC set-but-EMPTY is an explicit opt-out (2026-07-18): unit tests wrap this script around
|
|
51
|
+
# deliberately-failing fixture commands (t-fail, t.bad, exit 7), and the topic-file fallback below
|
|
52
|
+
# meant every test run PAGED Stuart's real phone β 16+ fixture pages in one morning, indistinguishable
|
|
53
|
+
# from real job failures. Tests set NTFY_TOPIC="" and stay silent; production (unset) still falls
|
|
54
|
+
# through to the topic file.
|
|
55
|
+
if [ "${NTFY_TOPIC+set}" = "set" ] && [ -z "$NTFY_TOPIC" ]; then return 0; fi
|
|
42
56
|
topic="${NTFY_TOPIC:-}"
|
|
43
57
|
[ -z "$topic" ] && [ -f "$HOME/.cache/ruvnet-brain/ntfy-topic" ] && topic="$(cat "$HOME/.cache/ruvnet-brain/ntfy-topic")"
|
|
44
58
|
[ -z "$topic" ] && return 0
|
|
@@ -49,6 +63,15 @@ finish() {
|
|
|
49
63
|
code=${FORCED_CODE:-$?}
|
|
50
64
|
ended="$(ts)"
|
|
51
65
|
dur=$(( $(date +%s) - START_EPOCH ))
|
|
66
|
+
# Exit 75 = SKIP (lock held by a live run). Restore the pre-fire receipt so the live run's evidence
|
|
67
|
+
# survives; report 0 to launchd (a skip is not a failure). If no receipt ever existed, record an
|
|
68
|
+
# honest "skipped" β which the watchdog treats as NOT proof of a real run.
|
|
69
|
+
if [ "$code" -eq 75 ]; then
|
|
70
|
+
if [ -n "$PREV_HB" ]; then printf '%s' "$PREV_HB" > "$HB"; else
|
|
71
|
+
printf '{"label":"%s","started_at":"%s","ended_at":"%s","state":"skipped","duration_sec":%s}' "$LABEL" "$STARTED" "$ended" "$dur" > "$HB"
|
|
72
|
+
fi
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
52
75
|
if [ "$code" -eq 0 ]; then state="ok"; else state="failed"; fi
|
|
53
76
|
cat > "$HB" <<EOF
|
|
54
77
|
{"label":"$LABEL","started_at":"$STARTED","ended_at":"$ended","state":"$state","exit_code":$code,"duration_sec":$dur}
|
|
@@ -87,6 +87,12 @@ export function judge(job, hb, loaded, now) {
|
|
|
87
87
|
if (ageHours > job.maxAgeHours) {
|
|
88
88
|
return { state: STALE, ageHours, detail: `last ran ${ageHours.toFixed(1)}h ago β its schedule allows ${job.maxAgeHours}h. It stopped.` };
|
|
89
89
|
}
|
|
90
|
+
// A "skipped" receipt (lock-skip fired but no real run has EVER recorded a result β job-heartbeat
|
|
91
|
+
// restores the real receipt when one exists, so this only survives when there was none) proves the
|
|
92
|
+
// schedule fires, not that the work runs. Never count it as OK. (F3, 2026-07-18)
|
|
93
|
+
if (hb.state === 'skipped') {
|
|
94
|
+
return { state: NEVER_RAN, ageHours, detail: 'only a lock-skip receipt exists β no real run has ever recorded a result' };
|
|
95
|
+
}
|
|
90
96
|
if (hb.state === 'failed' || (typeof hb.exit_code === 'number' && hb.exit_code !== 0)) {
|
|
91
97
|
return { state: FAILING, ageHours, detail: `last run FAILED with exit ${hb.exit_code} (${ageHours.toFixed(1)}h ago)` };
|
|
92
98
|
}
|
|
@@ -165,11 +171,24 @@ async function main() {
|
|
|
165
171
|
const bad = results.filter((r) => r.state !== OK);
|
|
166
172
|
|
|
167
173
|
const prev = loadState();
|
|
174
|
+
// DERIVED, not asserted (F1, 2026-07-18): a transition is only recorded as handled when its page
|
|
175
|
+
// was actually DELIVERED (push() returned true). The old code saved every new state unconditionally
|
|
176
|
+
// β so a FAILING job whose urgent page failed (ntfy down for a minute) was marked "alerted" and,
|
|
177
|
+
// because alerts are transition-only, would NEVER page again. Now an undelivered transition keeps
|
|
178
|
+
// its PREVIOUS state in the snapshot, so the same transition re-fires (and re-pages) on the next
|
|
179
|
+
// run until a page genuinely lands. The ledger can no longer claim a page that didn't happen.
|
|
180
|
+
const undelivered = new Set();
|
|
168
181
|
for (const c of transitions(results, prev)) {
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
const delivered = c.state === OK
|
|
183
|
+
? await push(`β
${c.label} is healthy again`, c.detail, 'default')
|
|
184
|
+
: await push(`π΄ ${c.state}: ${c.label}`, `${c.what}\n\n${c.detail}\n\nschedule: ${c.schedule}`, 'urgent');
|
|
185
|
+
if (!delivered) undelivered.add(c.label);
|
|
171
186
|
}
|
|
172
|
-
saveState(Object.fromEntries(results.map((r) => [r.label, r.state])));
|
|
187
|
+
saveState(Object.fromEntries(results.map((r) => [r.label, undelivered.has(r.label) ? (prev[r.label] ?? OK) : r.state])));
|
|
188
|
+
// Sol amendment to F1: an undelivered page also FAILS this run (exitCode, not exit β the report
|
|
189
|
+
// below still prints). The watchdog's own heartbeat then records the degradation, so "the pager is
|
|
190
|
+
// broken" is itself a paged, visible condition instead of a silent one.
|
|
191
|
+
if (undelivered.size > 0) process.exitCode = 1;
|
|
173
192
|
|
|
174
193
|
if (json) console.log(JSON.stringify({ results, checkedAt: new Date().toISOString() }, null, 2));
|
|
175
194
|
else if (!quiet || bad.length) {
|