ruvnet-brain 3.4.11-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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 3.4.11-dev β€” updated 2026-07-18 03:29 EDT](https://img.shields.io/badge/version_3.4.11--dev-updated_2026--07--18_03:29_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 3.4.16-dev β€” updated 2026-07-18 11:07 EDT](https://img.shields.io/badge/version_3.4.16--dev-updated_2026--07--18_11:07_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack β€” delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
10
10
 
@@ -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,135 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.
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
  ![RuvNet Brain architecture pipeline](assets/diagrams/architecture-pipeline.svg)
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,135 chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
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
- if (fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
912
- info(c.dim("running the bundle's own self-updater (backs up first, re-verifies, never half-applies)…\n"));
913
- // Relative filename + matching cwd β€” same launch convention as smokeQuery(); stdio:'inherit'
914
- // streams the updater's narration live and unedited.
915
- const r = spawnSync(process.execPath, ['forge-update.mjs', '--apply'], { cwd: kbDir, stdio: 'inherit' });
916
- updateStatus = r.error ? 1 : (r.status === null ? 1 : r.status);
917
- }
918
- // FALLBACK (2026-07-17). The bundle's self-updater is missing OR failed β€” e.g. an OLDER bundle whose
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 bounded live flywheel over routing policy (candidate+receipt only, never live routing)"
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.11-dev",
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": {
@@ -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
- if (c.state === OK) await push(`βœ… ${c.label} is healthy again`, c.detail, 'default');
170
- else await push(`πŸ”΄ ${c.state}: ${c.label}`, `${c.what}\n\n${c.detail}\n\nschedule: ${c.schedule}`, 'urgent');
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) {