ruvnet-brain 4.0.1 → 4.0.4

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.
Files changed (195) hide show
  1. package/.claude-plugin/marketplace.json +1 -0
  2. package/README.md +4 -4
  3. package/bin/install.mjs +303 -24
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  25. package/package.json +8 -22
  26. package/plugin/.claude-plugin/marketplace.json +1 -0
  27. package/plugin/.claude-plugin/plugin.json +2 -3
  28. package/plugin/.codex-plugin/plugin.json +1 -1
  29. package/plugin/commands/brain-console.md +2 -2
  30. package/plugin/commands/configure.md +3 -2
  31. package/plugin/commands/rvbc.md +4 -3
  32. package/plugin/commands/rvcb.md +2 -2
  33. package/plugin/commands/whats-new.md +6 -6
  34. package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
  35. package/plugin/hooks/hooks.json +1 -2
  36. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  37. package/plugin/mcp/server.mjs +90 -32
  38. package/plugin/scripts/detach.mjs +14 -0
  39. package/plugin/scripts/first-session-worker.mjs +38 -0
  40. package/plugin/scripts/ground-ruvnet.sh +16 -6
  41. package/plugin/scripts/hook-shim.mjs +34 -29
  42. package/plugin/scripts/learn-capture.sh +22 -3
  43. package/plugin/scripts/learn-flush.mjs +21 -4
  44. package/plugin/scripts/runtime-preferences.mjs +269 -0
  45. package/plugin/scripts/session-start-core.mjs +503 -0
  46. package/plugin/scripts/session-start.sh +3 -858
  47. package/plugin/scripts/whats-new.mjs +42 -0
  48. package/plugin/skills/brain-console/SKILL.md +4 -2
  49. package/plugin/skills/release-proof/SKILL.md +98 -0
  50. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  51. package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
  52. package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
  54. package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
  55. package/plugin/skills/rvbc/SKILL.md +9 -6
  56. package/plugin/skills/whats-new/SKILL.md +4 -4
  57. package/scripts/adr-backfill.mjs +107 -0
  58. package/scripts/advocacy-outcomes.mjs +808 -0
  59. package/scripts/agentdb-context.mjs +216 -0
  60. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  61. package/scripts/ascii-drift.mjs +236 -0
  62. package/scripts/behavioral-l1-l4.mjs +210 -0
  63. package/scripts/brain-capability-check.mjs +72 -0
  64. package/scripts/brain-grade-groundtruth.mjs +100 -0
  65. package/scripts/brain-latency-50.mjs +227 -0
  66. package/scripts/brain-novice-50.mjs +189 -0
  67. package/scripts/brain-stamp.mjs +94 -0
  68. package/scripts/brain-state.mjs +212 -0
  69. package/scripts/build-bundle.mjs +531 -0
  70. package/scripts/build-concepts.mjs +132 -0
  71. package/scripts/build-l2.mjs +71 -0
  72. package/scripts/build-primer.mjs +73 -0
  73. package/scripts/build-symbols.mjs +68 -0
  74. package/scripts/calibrate-router.mjs +97 -0
  75. package/scripts/capability-audit.mjs +321 -0
  76. package/scripts/capability-registry.mjs +876 -0
  77. package/scripts/check-indexation.mjs +108 -0
  78. package/scripts/check-legibility.mjs +189 -0
  79. package/scripts/ci/build-fixture-kb.mjs +67 -0
  80. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  81. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  82. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  83. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  84. package/scripts/ci/stranger-scenario.mjs +228 -0
  85. package/scripts/ci/stranger-timeout.mjs +25 -0
  86. package/scripts/ci-verdict.mjs +29 -0
  87. package/scripts/claims-verify.mjs +710 -0
  88. package/scripts/clear-claude-tmp.sh +31 -0
  89. package/scripts/console-engine.mjs +434 -0
  90. package/scripts/console-engine.test.mjs +125 -0
  91. package/scripts/corpus-qa.mjs +250 -0
  92. package/scripts/correction-detect-embed.mjs +346 -0
  93. package/scripts/correction-detect-measure.mjs +270 -0
  94. package/scripts/correction-detect.mjs +686 -0
  95. package/scripts/count-chunks.mjs +54 -0
  96. package/scripts/described-questions.json +30 -0
  97. package/scripts/design-grade.mjs +58 -0
  98. package/scripts/dev-plugin-link.sh +105 -0
  99. package/scripts/distill-project.mjs +200 -0
  100. package/scripts/doc-currency.mjs +801 -0
  101. package/scripts/eval-brain.mjs +244 -0
  102. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  103. package/scripts/fix-workstream.mjs +291 -0
  104. package/scripts/full-hints.mjs +87 -0
  105. package/scripts/gate.sh +39 -0
  106. package/scripts/gates.mjs +146 -0
  107. package/scripts/gen-console-images.mjs +54 -0
  108. package/scripts/gen-images.mjs +47 -0
  109. package/scripts/git-clone-refresh.mjs +52 -0
  110. package/scripts/git-hooks/pre-push +126 -0
  111. package/scripts/goal-match.mjs +398 -0
  112. package/scripts/goldie-research.mjs +223 -0
  113. package/scripts/goldie-weekly.sh +67 -0
  114. package/scripts/health-repair.mjs +237 -0
  115. package/scripts/helix-scenario-questions.json +10 -0
  116. package/scripts/ingest-gists.mjs +230 -0
  117. package/scripts/ingest-meeting.mjs +115 -0
  118. package/scripts/ingest-repo.mjs +79 -0
  119. package/scripts/install-npx-witness.sh +49 -0
  120. package/scripts/issue-fix.mjs +558 -0
  121. package/scripts/issue-watch.mjs +276 -0
  122. package/scripts/issue4-close-note.md +31 -0
  123. package/scripts/key-canary.mjs +91 -0
  124. package/scripts/latency-to-surface.mjs +233 -0
  125. package/scripts/learning-enable.mjs +380 -0
  126. package/scripts/learning-replay.mjs +1570 -0
  127. package/scripts/learnings.mjs +62 -0
  128. package/scripts/lesson-gate.mjs +680 -0
  129. package/scripts/lesson-lifecycle.mjs +449 -0
  130. package/scripts/lesson-promote.mjs +262 -0
  131. package/scripts/lesson-ratify.mjs +98 -0
  132. package/scripts/lesson-seed.mjs +252 -0
  133. package/scripts/lesson-store.mjs +447 -0
  134. package/scripts/loop-checkpoint.mjs +86 -0
  135. package/scripts/memdb-health.sh +14 -0
  136. package/scripts/memory-doctor.mjs +326 -0
  137. package/scripts/model-catalog.mjs +79 -0
  138. package/scripts/nightly-controller.mjs +66 -0
  139. package/scripts/nightly-gists.sh +72 -0
  140. package/scripts/nightly-wrapper.sh +172 -0
  141. package/scripts/notify.sh +12 -0
  142. package/scripts/npx-witness.sh +56 -0
  143. package/scripts/onboarding-console.mjs +2922 -0
  144. package/scripts/private-fence.mjs +69 -0
  145. package/scripts/proactivity-metrics.mjs +118 -0
  146. package/scripts/proof-questions.json +56 -0
  147. package/scripts/protected-release-invocation.mjs +76 -0
  148. package/scripts/prove.mjs +95 -0
  149. package/scripts/proxy/claude-proxied.sh +57 -0
  150. package/scripts/proxy/proxy-revert.sh +59 -0
  151. package/scripts/proxy/proxy-up.sh +60 -0
  152. package/scripts/proxy/proxy-verify.mjs +142 -0
  153. package/scripts/publication-receipt.mjs +307 -0
  154. package/scripts/published-surface-probe.mjs +241 -0
  155. package/scripts/qe/card-lane-gate.mjs +162 -0
  156. package/scripts/qe/session-start-gate.mjs +229 -0
  157. package/scripts/qe/ux-suite.mjs +323 -0
  158. package/scripts/reconcile-project.mjs +0 -0
  159. package/scripts/record-lesson.mjs +113 -0
  160. package/scripts/refresh-model-catalog.mjs +99 -0
  161. package/scripts/release-authority.mjs +93 -0
  162. package/scripts/release-proof.mjs +9 -0
  163. package/scripts/release-vector.mjs +281 -0
  164. package/scripts/release.mjs +439 -0
  165. package/scripts/remedy-registry.mjs +247 -0
  166. package/scripts/rerank-cap-eval.mjs +265 -0
  167. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  168. package/scripts/route-cheap.mjs +20 -15
  169. package/scripts/router-utilization.mjs +182 -0
  170. package/scripts/routing-flywheel.mjs +596 -0
  171. package/scripts/rvf-generation.mjs +104 -0
  172. package/scripts/rvf-index-audit.mjs +138 -0
  173. package/scripts/self-update.mjs +296 -0
  174. package/scripts/selfcheck.mjs +7 -1
  175. package/scripts/sign-bundle.mjs +69 -0
  176. package/scripts/signal-watch.mjs +171 -0
  177. package/scripts/stabilization-receipt.mjs +108 -0
  178. package/scripts/stack-sync.mjs +469 -0
  179. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  180. package/scripts/stamp-sweep.mjs +144 -0
  181. package/scripts/status-honesty.mjs +102 -0
  182. package/scripts/sync-version.mjs +217 -0
  183. package/scripts/token-report.mjs +102 -0
  184. package/scripts/top100-benchmark.mjs +479 -0
  185. package/scripts/top100-corpus.mjs +112 -0
  186. package/scripts/top100-semantic-assertions.mjs +449 -0
  187. package/scripts/update-apply.mjs +9 -0
  188. package/scripts/upgrade-notice.mjs +14 -0
  189. package/scripts/verify-bundle.mjs +51 -0
  190. package/scripts/verify-channels.mjs +184 -0
  191. package/scripts/verify-model-catalog.mjs +104 -0
  192. package/scripts/verify-nightly-close-issue4.sh +31 -0
  193. package/scripts/version.mjs +40 -0
  194. package/scripts/wired-check.mjs +867 -0
  195. package/plugin/scripts/finalize-token-meter.mjs +0 -25
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # goldie-weekly.sh — the ONLY thing launchd invokes for Goldie (weekly model-landscape research).
3
+ # Same discipline as nightly-wrapper.sh: verified outcomes, exactly one phone summary per run,
4
+ # no silent path. Built 2026-07-12 per Stuart's mandate: the router must never run on a stale
5
+ # picture of the model landscape, and no scheduled job may ever die quietly.
6
+ #
7
+ # Two layers, independent by design:
8
+ # 1. DETERMINISTIC (scripts/goldie-research.mjs): live OpenRouter pricing -> catalog refresh +
9
+ # drift flags + radar + the dated brief. Pure data, no LLM, no key needed.
10
+ # 2. JUDGMENT (headless Claude, subscription-covered): answers the brief's three standing
11
+ # questions (bucket count, best-model-per-bucket per public evals, radar adoption) with real
12
+ # web research, APPENDED to the same brief as a PROPOSAL — never auto-applied to policy.mjs.
13
+ # Skippable (GOLDIE_SKIP_JUDGMENT=1) and its failure never hides layer 1's result.
14
+ set -u
15
+ cd /Users/stuartkerr/Code/ruvnet-brain || exit 1
16
+ mkdir -p logs
17
+ LOG=logs/goldie.log
18
+ TODAY=$(date +%F)
19
+ BRIEF="$HOME/.claude/model-router/goldie/$TODAY.md"
20
+
21
+ echo "===== goldie-weekly — $(date -u +%FT%TZ) =====" >> "$LOG"
22
+
23
+ # ── Layer 1: deterministic refresh (must succeed for the run to count) ──
24
+ if ! /usr/local/bin/node scripts/goldie-research.mjs >> "$LOG" 2>&1; then
25
+ sh scripts/notify.sh "🔴 Goldie FAILED — model catalog is going stale" \
26
+ "goldie-research.mjs could not produce this week's brief (OpenRouter fetch or catalog write failed). The router is now running on last week's picture. See logs/goldie.log." \
27
+ urgent "rotating_light" || true
28
+ exit 1
29
+ fi
30
+
31
+ # ── Layer 2: judgment (best-effort, never blocks; appends to the brief) ──
32
+ JUDGMENT="skipped"
33
+ if [ "${GOLDIE_SKIP_JUDGMENT:-0}" != "1" ] && command -v claude >/dev/null 2>&1; then
34
+ PROMPT="You are Goldie, the weekly model-landscape researcher for a prompt->model router.
35
+ Read $BRIEF (this week's deterministic data) and ~/.claude/model-router/catalog.json (the candidate
36
+ catalog) and ~/.claude/model-router/policy.default.mjs (the current placeholder policy). Then use web
37
+ search on the current public evaluations (Artificial Analysis, LMArena, SWE-bench and similar) to
38
+ answer the brief's three standing questions with sources and dates:
39
+ (1) how many BUCKETS should prompts be classified into and what are they;
40
+ (2) the best model per bucket right now on capability-per-cost-per-speed, split into: covered by a
41
+ Claude Max subscription (claude-code harness), covered by a ChatGPT/Codex subscription (codex
42
+ harness), and cheapest-capable OpenRouter API model;
43
+ (3) whether any radar model in the brief deserves wiring up, and what that requires.
44
+ Rules: cite sources with dates for every claim; distinguish MEASURED numbers from vendor claims;
45
+ recommendations are PROPOSALS for catalog.json/policy.mjs — do not edit any file. End with a
46
+ '## Proposed policy changes' section in plain, reviewable prose.
47
+ Write your full answer to stdout as markdown."
48
+ # env -u ANTHROPIC_API_KEY: a stray/stale API key in the environment makes headless claude bill
49
+ # (or fail on) the API instead of riding the Claude Max login — the exact "spend where the
50
+ # subscription is free" mistake this system exists to kill. Found live 2026-07-12: an invalid
51
+ # inherited key failed the whole judgment layer with "Invalid API key". Subscription, always.
52
+ if OUT=$(timeout 900 env -u ANTHROPIC_API_KEY claude -p "$PROMPT" --model sonnet --allowed-tools "WebSearch,WebFetch,Read" 2>>"$LOG"); then
53
+ { echo ""; echo "---"; echo ""; echo "# Judgment layer (headless Claude, $(date -u +%FT%TZ))"; echo ""; echo "$OUT"; } >> "$BRIEF"
54
+ JUDGMENT="ok"
55
+ else
56
+ JUDGMENT="FAILED (see logs/goldie.log)"
57
+ { echo ""; echo "---"; echo ""; echo "# Judgment layer: FAILED this week ($(date -u +%FT%TZ)) — deterministic data above still fresh."; } >> "$BRIEF"
58
+ fi
59
+ fi
60
+
61
+ # ── Exactly one summary push per run — success included (silence is never a signal) ──
62
+ HEADLINES=$(grep -E "PRICE DRIFT|NOT FOUND" "$BRIEF" | head -3)
63
+ sh scripts/notify.sh "🧭 Goldie ran — model catalog refreshed" \
64
+ "Weekly brief: $BRIEF. Judgment layer: $JUDGMENT.${HEADLINES:+ ATTENTION: $HEADLINES}" \
65
+ default "compass" || true
66
+ echo "===== goldie-weekly done (judgment: $JUDGMENT) =====" >> "$LOG"
67
+ exit 0
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * health-repair.mjs — the EXECUTOR behind the console's health recommendations.
4
+ *
5
+ * The console used to detect a corrupt memory store, score it 49/100, render it into a card, and
6
+ * offer nothing. Stuart, 2026-07-21: "when it finds a problem, the fact that it didn't recommend a
7
+ * fix is unconscionable." This is the other half — the part that actually repairs.
8
+ *
9
+ * Three actions, each matching a recommendation id from console-engine.buildHealthRecommendations:
10
+ *
11
+ * --repair-memory REINDEX a corrupt AgentDB store (index damage, never data loss)
12
+ * --flush-learning drain the capture queue into rUv's learner
13
+ * --train-learning run one training cycle
14
+ *
15
+ * DISCIPLINE, learned the hard way tonight:
16
+ * • Back up BEFORE touching, using sqlite's own .backup — `cp` on a live WAL database silently
17
+ * truncates the newest transactions (standing lesson, proven by experiment).
18
+ * • Count rows before AND after, and refuse to report success if they differ.
19
+ * • Never hand-roll learning: the flush/train paths shell out to rUv's own `ruflo hooks`.
20
+ * • Every result is DERIVED from a re-measurement, never asserted from an exit code.
21
+ */
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import os from 'node:os';
25
+ import { execFileSync, spawnSync } from 'node:child_process';
26
+ import { findStores, diagnose } from './memory-doctor.mjs';
27
+
28
+ const HOME = os.homedir();
29
+ const argv = process.argv.slice(2);
30
+ const has = (f) => argv.includes(f);
31
+
32
+ /**
33
+ * Find ruflo HONESTLY.
34
+ *
35
+ * This was hardcoded to `~/.npm-global/bin/ruflo` — Stuart's prefix, not everyone's. On any machine
36
+ * with a different npm prefix (nvm, Homebrew, Volta, a plain `npm -g` on Linux), every learning
37
+ * action reported "ruflo not found — install it to enable learning" to a user who had ruflo
38
+ * installed and working. Telling someone their tool is missing when it is on their PATH is the
39
+ * product lying, and it is unfalsifiable from their side: they cannot see why we looked in one place.
40
+ *
41
+ * Rule 21 still holds — ONE ruflo, the global one, never `npx ruflo@latest`. This resolves WHERE
42
+ * that one global binary is rather than assuming a path.
43
+ */
44
+ function resolveRuflo() {
45
+ const preferred = path.join(HOME, '.npm-global/bin/ruflo');
46
+ if (fs.existsSync(preferred)) return preferred;
47
+ const which = spawnSync('sh', ['-lc', 'command -v ruflo'], { encoding: 'utf8', timeout: 10_000 });
48
+ const found = String(which.stdout || '').trim().split('\n')[0];
49
+ return found && fs.existsSync(found) ? found : null;
50
+ }
51
+ const RUFLO = resolveRuflo();
52
+ const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
53
+
54
+ const sqlite = (db, sql) => execFileSync('sqlite3', [db, sql], { encoding: 'utf8', timeout: 120_000 }).trim();
55
+
56
+ /** Every AgentDB store this repo knows about: the project's own, plus any passed explicitly. */
57
+ function resolveDb() {
58
+ const explicit = argv[argv.indexOf('--db') + 1];
59
+ if (argv.includes('--db') && explicit) return explicit;
60
+ return path.join(process.cwd(), '.swarm', 'memory.db');
61
+ }
62
+
63
+ /**
64
+ * REINDEX a corrupt store. Index corruption ("wrong # of entries in index X") means the indexes
65
+ * drifted from the table; the rows themselves are intact, so rebuilding indexes FROM the table is
66
+ * lossless. Verified live: 1193 rows before, 1193 after, integrity_check ok.
67
+ */
68
+ function repairMemory() {
69
+ const db = resolveDb();
70
+ if (!fs.existsSync(db)) return { ok: false, log: `no memory store at ${db.replace(HOME, '~')}` };
71
+
72
+ const before = sqlite(db, 'PRAGMA integrity_check;').split('\n')[0];
73
+ if (before === 'ok') return { ok: true, log: 'store was already clean — nothing to repair', noop: true };
74
+
75
+ const rowsBefore = Number(sqlite(db, 'SELECT COUNT(*) FROM memory_entries;'));
76
+
77
+ // Backup FIRST, via sqlite's own backup (never cp — a live WAL db loses its newest transactions).
78
+ const backup = `${db}.rescue-${new Date().toISOString().replace(/[:.]/g, '-')}`;
79
+ try { sqlite(db, `.backup '${backup}'`); }
80
+ catch (e) { return { ok: false, log: `refusing to repair — could not back up first: ${e.message}` }; }
81
+
82
+ try { sqlite(db, 'REINDEX;'); }
83
+ catch (e) { return { ok: false, log: `REINDEX failed: ${e.message}. Your backup is at ${backup.replace(HOME, '~')}`, backup }; }
84
+
85
+ // PROVE it, rather than trusting REINDEX's exit code.
86
+ const after = sqlite(db, 'PRAGMA integrity_check;').split('\n')[0];
87
+ const rowsAfter = Number(sqlite(db, 'SELECT COUNT(*) FROM memory_entries;'));
88
+
89
+ if (after !== 'ok') return { ok: false, log: `still corrupt after REINDEX: ${after}. Backup: ${backup.replace(HOME, '~')}`, backup };
90
+ if (rowsAfter !== rowsBefore) {
91
+ return { ok: false, log: `ROW COUNT CHANGED (${rowsBefore} → ${rowsAfter}) — treating as data loss. Restore: ${backup.replace(HOME, '~')}`, backup };
92
+ }
93
+ return { ok: true, log: `repaired — integrity ok, ${rowsAfter} entries intact (was ${rowsBefore}). Backup: ${backup.replace(HOME, '~')}`, backup };
94
+ }
95
+
96
+ /** Drain the capture queue into rUv's learner — his tool, not ours. */
97
+ function flushLearning() {
98
+ const flusher = path.join(HOME, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin', 'scripts', 'learn-flush.mjs');
99
+ const local = path.join(process.cwd(), 'plugin', 'scripts', 'learn-flush.mjs');
100
+ const script = fs.existsSync(flusher) ? flusher : (fs.existsSync(local) ? local : null);
101
+ if (!script) return { ok: false, log: 'learn-flush.mjs not found — cannot drain the queue' };
102
+
103
+ const queueDir = path.join(HOME, '.cache', 'ruvnet-brain', 'learn');
104
+ const depth = () => {
105
+ try {
106
+ return fs.readdirSync(queueDir).filter((f) => f.endsWith('.jsonl'))
107
+ .reduce((n, f) => n + fs.readFileSync(path.join(queueDir, f), 'utf8').split('\n').filter(Boolean).length, 0);
108
+ } catch { return 0; }
109
+ };
110
+ const before = depth();
111
+ try { execFileSync(process.execPath, [script], { stdio: 'ignore', timeout: 600_000 }); }
112
+ catch (e) { return { ok: false, log: `flush failed: ${e.message} — the queue is preserved for retry` }; }
113
+ const after = depth();
114
+ return { ok: true, log: `fed ${Math.max(0, before - after)} captured events into the learner (queue ${before} → ${after})` };
115
+ }
116
+
117
+ /** One training cycle, via rUv's own CLI, in the GLOBAL (cross-project) learner. */
118
+ function trainLearning() {
119
+ if (!RUFLO) return { ok: false, log: 'ruflo is not on this machine — install it with `npm i -g ruflo@latest` to enable learning' };
120
+ try {
121
+ execFileSync(RUFLO, ['hooks', 'intelligence', '--train'], { cwd: HOME, env: RUFLO_ENV, stdio: 'ignore', timeout: 600_000 });
122
+ } catch (e) { return { ok: false, log: `training cycle failed: ${e.message}` }; }
123
+ return { ok: true, log: 'ran one training cycle in the cross-project learner' };
124
+ }
125
+
126
+ /**
127
+ * Distill every store that is embedded but has never been mined into patterns.
128
+ *
129
+ * This is the executor for ADR-027's North Star case: 87 stores holding 154,106 memories while
130
+ * learning nothing. The fix is NOT ours — it is rUv's ADR-174 distillation pipeline
131
+ * (`ruflo memory distill run`), which memory-doctor has been printing as the remedy all along while
132
+ * the console stayed quiet. We wire it; we do not reimplement it.
133
+ *
134
+ * Discipline, same as repairMemory():
135
+ * • Snapshot each store FIRST with `ruflo memory backup` — rUv's own WAL-safe snapshotter, not cp.
136
+ * • Skip stores distillation cannot help (cover < 50%): running there would burn minutes and then
137
+ * truthfully report zero, which reads as failure. Not attempting is more honest than attempting.
138
+ * • Re-diagnose after, and report the DERIVED pattern delta — never the exit code.
139
+ * • $0: --judge defaults to 'structural' and --budget-usd defaults to 0. Nothing bills, nothing
140
+ * leaves the machine. We pass both explicitly anyway so a future default change cannot silently
141
+ * start spending a user's money.
142
+ */
143
+ function distillFleet() {
144
+ if (!RUFLO) return { ok: false, log: 'ruflo is not on this machine — install it with `npm i -g ruflo@latest` to distill' };
145
+
146
+ const scope = argv.includes('--root') ? path.resolve(argv[argv.indexOf('--root') + 1]) : null;
147
+ const targets = [];
148
+ const corrupt = [];
149
+ let found = [];
150
+ try { found = scope ? findStores(scope) : findStores(); } catch { found = []; }
151
+ for (const db of found) {
152
+ const resolved = path.resolve(db);
153
+ let d;
154
+ try { d = diagnose(resolved); } catch { continue; }
155
+ if (d.unreadable || d.schemaless || !d.total) continue;
156
+ if (d.cover < 0.5 || (d.patterns ?? 0) > 0) continue;
157
+ // A corrupt store CANNOT be distilled — ruflo refuses it outright ("memory DB reports
158
+ // corruption — run recoverMemoryDatabase first"). Attempting anyway burns minutes, writes
159
+ // nothing, and returns a zero that reads as "distillation doesn't work". The honest answer is
160
+ // that repair comes FIRST, so name these instead of silently failing on them.
161
+ if (d.integrity && d.integrity !== 'ok') { corrupt.push(d.name); continue; }
162
+ targets.push({ db: resolved, name: d.name, before: d.patterns ?? 0 });
163
+ }
164
+ const corruptNote = corrupt.length
165
+ ? ` ${corrupt.length} store${corrupt.length === 1 ? ' was' : 's were'} skipped as corrupt and must be repaired before ${corrupt.length === 1 ? 'it' : 'they'} can be distilled: ${corrupt.slice(0, 5).join(', ')}${corrupt.length > 5 ? '…' : ''}.`
166
+ : '';
167
+ if (!targets.length) {
168
+ return { ok: !corrupt.length, log: `no stores were distillable — every embedded store already has patterns.${corruptNote}`, noop: true };
169
+ }
170
+
171
+ const done = [];
172
+ const failed = [];
173
+ const receiptPath = argv.includes('--receipt') ? argv[argv.indexOf('--receipt') + 1] : null;
174
+ const receipt = [];
175
+ const writeReceipt = () => {
176
+ if (!receiptPath) return;
177
+ try {
178
+ fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
179
+ fs.writeFileSync(receiptPath, JSON.stringify({ at: new Date().toISOString(), stores: receipt }, null, 2));
180
+ } catch { /* the receipt is for undo; failing to write it must not fail the repair itself */ }
181
+ };
182
+
183
+ for (const t of targets) {
184
+ const dir = path.join(path.dirname(t.db), 'backups');
185
+ try { execFileSync(RUFLO, ['memory', 'backup', '--db', t.db, '--dir', dir], { env: RUFLO_ENV, stdio: 'ignore', timeout: 300_000 }); }
186
+ catch (e) { failed.push(`${t.name}: refused to distill — snapshot failed (${String(e.message).slice(0, 80)})`); continue; }
187
+ // Record the snapshot BEFORE distilling, and flush after every store. If the process is killed
188
+ // mid-fleet, the receipt still names every store already modified — a partial receipt is
189
+ // recoverable, a missing one is not.
190
+ receipt.push({ db: t.db, name: t.name, backupDir: dir });
191
+ writeReceipt();
192
+
193
+ try {
194
+ execFileSync(RUFLO, ['memory', 'distill', 'run', '--db', t.db, '--judge', 'structural', '--budget-usd', '0'],
195
+ { env: RUFLO_ENV, stdio: 'ignore', timeout: 1_800_000 });
196
+ } catch (e) { failed.push(`${t.name}: distill failed (${String(e.message).slice(0, 80)}); snapshot kept in ${dir.replace(HOME, '~')}`); continue; }
197
+
198
+ // PROVE it moved. An exit code of 0 is not evidence that anything was learned.
199
+ //
200
+ // Measured with a READ-WRITE sqlite3 connection, NOT memory-doctor's diagnose(). diagnose()
201
+ // opens `mode=ro`, and a read-only connection cannot build the WAL index for a database another
202
+ // process just wrote — so it read 0 patterns moments after distillation had in fact written 684.
203
+ // This executor then reported "produced no new patterns" about a run that worked perfectly.
204
+ // Caught live 2026-07-21. It is the standing lesson exactly: verify by the mechanism, not by a
205
+ // convenient instance of it — a probe that cannot see the write is not a verification.
206
+ let after = t.before;
207
+ try { after = Number(sqlite(t.db, 'SELECT count(*) FROM reasoning_patterns;')) || 0; } catch { /* unreadable — leave unchanged so we claim nothing */ }
208
+ if (after > t.before) done.push(`${t.name}: +${after - t.before} patterns`);
209
+ else failed.push(`${t.name}: ran but produced no new patterns (still ${after})`);
210
+ }
211
+
212
+ const log = [
213
+ done.length ? `distilled ${done.length} store${done.length === 1 ? '' : 's'} — ${done.join(', ')}` : 'no store gained patterns',
214
+ failed.length ? `${failed.length} did not: ${failed.join('; ')}` : '',
215
+ corruptNote.trim(),
216
+ ].filter(Boolean).join('. ');
217
+ return { ok: done.length > 0, log };
218
+ }
219
+
220
+ const action = has('--repair-memory') ? repairMemory
221
+ : has('--flush-learning') ? flushLearning
222
+ : has('--train-learning') ? trainLearning
223
+ : has('--distill-fleet') ? distillFleet
224
+ : null;
225
+
226
+ if (!action) {
227
+ console.log('health-repair — repair actions behind the console\'s health recommendations\n');
228
+ console.log(' --repair-memory [--db <path>] REINDEX a corrupt AgentDB store (backs up first, proves row count)');
229
+ console.log(' --flush-learning drain the capture queue into the learner');
230
+ console.log(' --train-learning run one training cycle');
231
+ console.log(' --distill-fleet [--root <dir>] distill embedded-but-unmined stores (snapshots each first)');
232
+ process.exit(2);
233
+ }
234
+
235
+ const res = action();
236
+ console.log(res.log);
237
+ process.exit(res.ok ? 0 : 1);
@@ -0,0 +1,10 @@
1
+ [
2
+ { "set": "helix-DESCRIBED (newcomer, no repo name)", "query": "How should I store and search a patient's medical history locally and privately using vectors?", "expectRepo": ["ruvector", "rulake", "concepts"], "minRelevance": -5 },
3
+ { "set": "helix-DESCRIBED (newcomer, no repo name)", "query": "How do I give a health agent persistent memory of past visits, labs, and medications?", "expectRepo": ["agentdb", "concepts"], "minRelevance": -5 },
4
+ { "set": "helix-DESCRIBED (newcomer, no repo name)", "query": "How can I run several agents in parallel to analyze one medical record?", "expectRepo": ["ruflo", "concepts"], "minRelevance": -5 },
5
+ { "set": "helix-DESCRIBED (newcomer, no repo name)", "query": "What methodology should I follow to plan and build the Helix data-ingestion pipeline step by step?", "expectRepo": ["sparc", "concepts", "ruflo"], "minRelevance": -5 },
6
+ { "set": "helix-NAMED (knows the RuvNet tool)", "query": "Should I use RuVector RVF with HNSW to store medical embeddings in Helix?", "expectRepo": ["ruvector", "concepts"], "minRelevance": -5 },
7
+ { "set": "helix-NAMED (knows the RuvNet tool)", "query": "Can ruflo orchestrate the diagnostic agents and swarms in Helix?", "expectRepo": ["ruflo", "concepts"], "minRelevance": -5 },
8
+ { "set": "helix-NAMED (knows the RuvNet tool)", "query": "Can AgentDB hold the patient's longitudinal health memory across sessions?", "expectRepo": ["agentdb", "concepts"], "minRelevance": -5 },
9
+ { "set": "helix-NAMED (knows the RuvNet tool)", "query": "Should I follow the SPARC methodology to build the Helix pipeline?", "expectRepo": ["sparc", "concepts", "ruflo"], "minRelevance": -5 }
10
+ ]
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // ingest-gists.mjs — pull rUv's public GitHub gists into the brain as their own store.
3
+ //
4
+ // WHY GISTS. The brain indexes rUv's REPOS, which is where an idea lands LAST. His gists are where
5
+ // it appears FIRST: `ruflo-3.24.0-flywheel.md` was published as a gist days before anything but an
6
+ // ADR existed in our corpus. Indexing them moves the brain's clock from "what rUv has shipped" to
7
+ // "what rUv is thinking" — which is the whole premise of this project.
8
+ //
9
+ // WHY THEY ARE FENCED. A gist is an announcement, not shipped source. It routinely describes work
10
+ // that is proposed, unreleased, or still moving. If the brain quotes a gist as fact, it will tell
11
+ // users about features that do not exist — the exact drift this project exists to prevent, wearing
12
+ // a new hat. So every gist passage carries a provenance banner in its own text, the same way the
13
+ // KB already stamps `ADR STATUS: PROPOSED` onto ADR passages. Retrieval then hands the model the
14
+ // claim AND its epistemic status together; they cannot be separated downstream.
15
+ //
16
+ // node scripts/ingest-gists.mjs # incremental (only re-fetch changed gists)
17
+ // node scripts/ingest-gists.mjs --full # ignore the cache, refetch everything
18
+ // node scripts/ingest-gists.mjs --owner ruvnet # default owner
19
+ // node scripts/ingest-gists.mjs --dry-run # list what would change, write nothing
20
+ //
21
+ // Then embed (the store becomes searchable with no restart — forge-ask-all discovers *.rvf at query
22
+ // time):
23
+ // node kb/forge-big.mjs both --dir kb --name ruv-gists
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { spawnSync } from 'node:child_process';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const KB = path.join(ROOT, 'kb');
32
+ const NAME = 'ruv-gists';
33
+ const CACHE = path.join(KB, `.${NAME}.cache.json`);
34
+
35
+ const argv = process.argv.slice(2);
36
+ const arg = (f, d = null) => { const i = argv.indexOf(f); return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d; };
37
+ const OWNER = arg('--owner', 'ruvnet');
38
+ const FULL = argv.includes('--full');
39
+ const DRY = argv.includes('--dry-run');
40
+ // --index-only writes the human/git-trackable index from the LIST endpoint alone (~5 API calls,
41
+ // no per-gist fetch, no embedding). That is what the nightly job runs: the KB stores are gitignored
42
+ // and ship via Release, so nightly CI can keep the INDEX fresh even though it cannot commit vectors.
43
+ const INDEX_ONLY = argv.includes('--index-only');
44
+ const INDEX_PATH = path.join(ROOT, 'docs', 'RUV-GISTS.md');
45
+
46
+ // Markdown/text only. A gist's code files are better read from the repo they land in; prose is the
47
+ // thing repos don't carry.
48
+ const TEXT_EXT = new Set(['.md', '.markdown', '.txt', '.rst']);
49
+
50
+ /** Authenticated GitHub calls via `gh` — 5000 req/hr instead of 60, and no token handling here. */
51
+ function gh(endpointArgs) {
52
+ const r = spawnSync('gh', endpointArgs, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
53
+ if (r.status !== 0) throw new Error(`gh ${endpointArgs.join(' ')} failed: ${(r.stderr || '').trim().slice(0, 200)}`);
54
+ return r.stdout;
55
+ }
56
+
57
+ async function listGists(owner) {
58
+ try {
59
+ const raw = gh(['api', `users/${owner}/gists?per_page=100`, '--paginate', '--slurp']);
60
+ const pages = JSON.parse(raw);
61
+ // --slurp yields an array of pages OR a flat array depending on gh version; flatten defensively.
62
+ return pages.flat().filter((g) => g && g.id);
63
+ } catch (err) {
64
+ // In Actions, gh can NEVER list gists: GITHUB_TOKEN is a GitHub App token and the gists API is
65
+ // closed to those ("Resource not accessible by integration", HTTP 403 — every nightly run since
66
+ // birth). Public gists need no auth at all, so fall back to the plain API (60 req/hr per IP).
67
+ console.error(` gh failed (${String(err.message).slice(0, 100)}) — falling back to unauthenticated API`);
68
+ return listGistsPublic(owner);
69
+ }
70
+ }
71
+
72
+ // Hermetic-test seam (same pattern as the router's MODEL_ROUTER_CATALOG): integration tests point
73
+ // this at an unreachable port to exercise the fallback's failure path without touching the live API.
74
+ const API_BASE = process.env.RUVNET_GISTS_API || 'https://api.github.com';
75
+
76
+ async function listGistsPublic(owner) {
77
+ const all = [];
78
+ for (let page = 1; page <= 10; page++) {
79
+ const res = await fetch(`${API_BASE}/users/${owner}/gists?per_page=100&page=${page}`, {
80
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'ruvnet-brain-gists-index' },
81
+ });
82
+ if (res.status === 403 || res.status === 429) {
83
+ const err = new Error(`unauthenticated gists list rate-limited (HTTP ${res.status})`);
84
+ err.rateLimited = true; // runner IPs share the anonymous quota — a known, transient condition
85
+ throw err;
86
+ }
87
+ if (!res.ok) throw new Error(`unauthenticated gists list failed: HTTP ${res.status}`);
88
+ const items = await res.json();
89
+ all.push(...items.filter((g) => g && g.id));
90
+ if (items.length < 100) break;
91
+ }
92
+ return all;
93
+ }
94
+
95
+ /** Raw file bodies. The list endpoint truncates `content`, so fetch each gist individually. */
96
+ function fetchGist(id) {
97
+ return JSON.parse(gh(['api', `gists/${id}`]));
98
+ }
99
+
100
+ // ~3200-char paragraph-aligned chunks — same shape build-concepts.mjs uses, so the reader's chunk
101
+ // handling and the `#N` suffix convention stay uniform across stores.
102
+ function chunk(text, size = 3200) {
103
+ const out = [];
104
+ let buf = '';
105
+ for (const para of text.split(/\n\n+/)) {
106
+ if (buf && buf.length + para.length + 2 > size) { out.push(buf); buf = ''; }
107
+ buf = buf ? `${buf}\n\n${para}` : para;
108
+ }
109
+ if (buf.trim()) out.push(buf);
110
+ return out.length ? out : [];
111
+ }
112
+
113
+ /** The banner that travels WITH the text, so a retrieval hit can never lose its provenance. */
114
+ const banner = (g, file) =>
115
+ `SOURCE: GitHub gist by @${OWNER} — "${(g.description || file).replace(/\s+/g, ' ').trim().slice(0, 160)}"\n` +
116
+ `GIST STATUS: rUv's own notes / release announcement — may describe PROPOSED or UNRELEASED work.\n` +
117
+ `Treat as intent, not as confirmed shipped behavior: verify against repo source before asserting.\n` +
118
+ `updated: ${g.updated_at?.slice(0, 10)} · https://gist.github.com/${OWNER}/${g.id}\n\n`;
119
+
120
+ /** A tiny, git-trackable feed of what rUv has published, newest first. Costs ~5 API calls. */
121
+ function writeIndex(gists) {
122
+ const rows = [...gists].sort((a, b) => b.updated_at.localeCompare(a.updated_at));
123
+ const lines = [
124
+ '# rUv\'s public gists — index',
125
+ '',
126
+ '> Auto-generated by `node scripts/ingest-gists.mjs --index-only`. Newest first.',
127
+ '>',
128
+ '> **These are announcements and notes, not shipped source.** A gist routinely describes work that is',
129
+ '> proposed, unreleased, or still moving. Verify against repo source before asserting behavior.',
130
+ '',
131
+ `_${rows.length} gists · refreshed ${new Date().toISOString().slice(0, 10)}_`,
132
+ '',
133
+ '| Updated | Gist | Description |',
134
+ '|---|---|---|',
135
+ ];
136
+ for (const g of rows) {
137
+ const file = Object.keys(g.files || {})[0] || '(no files)';
138
+ const desc = (g.description || '').replace(/\s+/g, ' ').replace(/\|/g, '\\|').trim().slice(0, 120) || '—';
139
+ lines.push(`| ${g.updated_at.slice(0, 10)} | [${file}](https://gist.github.com/${OWNER}/${g.id}) | ${desc} |`);
140
+ }
141
+ fs.mkdirSync(path.dirname(INDEX_PATH), { recursive: true });
142
+ fs.writeFileSync(INDEX_PATH, lines.join('\n') + '\n');
143
+ console.log(` wrote ${path.relative(ROOT, INDEX_PATH)} (${rows.length} rows)`);
144
+ }
145
+
146
+ async function main() {
147
+ if (!fs.existsSync(KB)) { console.error(`ingest-gists: no kb dir at ${KB}`); process.exit(2); }
148
+
149
+ const cache = !FULL && fs.existsSync(CACHE) ? JSON.parse(fs.readFileSync(CACHE, 'utf8')) : {};
150
+ console.log(`ingest-gists: listing public gists for @${OWNER}…`);
151
+ let gists;
152
+ try {
153
+ gists = await listGists(OWNER);
154
+ } catch (err) {
155
+ if (INDEX_ONLY && err.rateLimited) {
156
+ // The index is a freshness feed; one skipped night self-heals on the next run. Exit 0 so a
157
+ // transient shared-IP rate limit doesn't page anyone — real script errors still exit 1.
158
+ console.error(`ingest-gists: SKIP — ${err.message}; the index catches up on the next nightly.`);
159
+ process.exit(0);
160
+ }
161
+ throw err;
162
+ }
163
+ console.log(` ${gists.length} gists found`);
164
+
165
+ if (INDEX_ONLY) { writeIndex(gists); return; }
166
+
167
+ const changed = gists.filter((g) => cache[g.id] !== g.updated_at);
168
+ console.log(` ${changed.length} new or updated since last run${FULL ? ' (--full: cache ignored)' : ''}`);
169
+ if (DRY) {
170
+ for (const g of changed.slice(0, 20)) console.log(` ${g.updated_at.slice(0, 10)} ${Object.keys(g.files)[0]}`);
171
+ if (changed.length > 20) console.log(` … and ${changed.length - 20} more`);
172
+ return;
173
+ }
174
+ if (!changed.length && fs.existsSync(path.join(KB, `${NAME}.passages.jsonl`))) {
175
+ console.log(' nothing to do — store is current.');
176
+ return;
177
+ }
178
+
179
+ // Full rebuild of the passage file (ids must stay dense and aligned with the .rvf idmap; a
180
+ // partial append would desynchronise them — the failure mode that maps a vector to the wrong text).
181
+ const passages = [];
182
+ const entries = {};
183
+ let id = 0;
184
+ let files = 0;
185
+ let skipped = 0;
186
+
187
+ for (const [i, stub] of gists.entries()) {
188
+ if (i % 25 === 0) process.stdout.write(`\r fetching ${i}/${gists.length}…`);
189
+ let g;
190
+ try { g = fetchGist(stub.id); } catch { skipped++; continue; }
191
+ for (const [fname, f] of Object.entries(g.files || {})) {
192
+ if (!TEXT_EXT.has(path.extname(fname).toLowerCase())) continue;
193
+ const body = f.truncated && f.raw_url ? '' : (f.content || '');
194
+ if (!body.trim()) { skipped++; continue; }
195
+ const title = (g.description || fname).replace(/\s+/g, ' ').trim().slice(0, 180) || fname;
196
+ const head = banner(g, fname);
197
+ // Banner on EVERY chunk, not just the first. Retrieval returns ONE chunk — if the provenance
198
+ // lives only in chunk 0, then chunk 2 reaches the model as an unlabelled assertion, which is
199
+ // exactly the fence this file claims to build. (Caught by reading a real retrieval: the top
200
+ // hit for "enable the flywheel" was `…flywheel.md#2` and carried no status line.)
201
+ const chunks = chunk(body);
202
+ chunks.forEach((c, ci) => {
203
+ const sid = String(id++);
204
+ const p = `${g.id.slice(0, 8)}/${fname}${chunks.length > 1 ? `#${ci}` : ''}`;
205
+ const text = head + c;
206
+ passages.push({ id: sid, text, path: p, title });
207
+ entries[sid] = { path: p, kind: 'doc', title, chunk: ci, preview: text.slice(0, 200) };
208
+ });
209
+ files++;
210
+ }
211
+ cache[stub.id] = stub.updated_at;
212
+ }
213
+ process.stdout.write('\r');
214
+
215
+ fs.writeFileSync(path.join(KB, `${NAME}.passages.jsonl`), passages.map((p) => JSON.stringify(p)).join('\n') + '\n');
216
+ fs.writeFileSync(path.join(KB, `${NAME}.meta.json`), JSON.stringify({
217
+ model: NAME, dimensions: 0, metric: 'cosine', name: NAME,
218
+ generated: new Date().toISOString(), repo: `gists/${OWNER}`,
219
+ note: "rUv's public gists — announcements and thinking, PROPOSED unless confirmed in repo source.",
220
+ entries,
221
+ }, null, 2));
222
+ fs.writeFileSync(CACHE, JSON.stringify(cache, null, 2));
223
+
224
+ console.log(`ingest-gists: ${gists.length} gists · ${files} text files · ${passages.length} passages · ${skipped} skipped`);
225
+ writeIndex(gists);
226
+ console.log(` wrote kb/${NAME}.passages.jsonl + kb/${NAME}.meta.json`);
227
+ console.log(` next: node kb/forge-big.mjs both --dir kb --name ${NAME} (embed → ${NAME}.big.rvf)`);
228
+ }
229
+
230
+ await main();