ruvnet-brain 3.9.134-dev → 4.0.2

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 (218) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +5 -5
  3. package/bin/install.mjs +382 -36
  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/kb/zip-extract.mjs +53 -14
  25. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  26. package/package.json +14 -22
  27. package/plugin/.claude-plugin/marketplace.json +14 -0
  28. package/plugin/.claude-plugin/plugin.json +22 -0
  29. package/plugin/.codex-plugin/plugin.json +21 -0
  30. package/plugin/.mcp.json +8 -0
  31. package/plugin/commands/brain-console.md +16 -0
  32. package/plugin/commands/configure.md +33 -0
  33. package/plugin/commands/rvbc.md +79 -0
  34. package/plugin/commands/rvcb.md +16 -0
  35. package/plugin/commands/whats-new.md +57 -0
  36. package/plugin/hooks/codex-hooks.json +160 -0
  37. package/plugin/hooks/hook-contracts.json +77 -0
  38. package/plugin/hooks/hooks.json +202 -0
  39. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  40. package/plugin/mcp/server.mjs +56 -6
  41. package/plugin/scripts/anticipate.sh +534 -0
  42. package/plugin/scripts/codex-hook-adapter.mjs +96 -0
  43. package/plugin/scripts/continuation-gate.mjs +267 -0
  44. package/plugin/scripts/design-wall.sh +137 -0
  45. package/plugin/scripts/detach.mjs +182 -0
  46. package/plugin/scripts/first-session-worker.mjs +38 -0
  47. package/plugin/scripts/gate-receipt.sh +35 -0
  48. package/plugin/scripts/ground-before-write.sh +199 -0
  49. package/plugin/scripts/ground-ruvnet.sh +517 -0
  50. package/plugin/scripts/grounding-stamp.sh +113 -0
  51. package/plugin/scripts/grounding-substance.mjs +595 -0
  52. package/plugin/scripts/hijack-ruvnet.sh +81 -0
  53. package/plugin/scripts/hook-input.mjs +558 -0
  54. package/plugin/scripts/hook-shim-bash.mjs +55 -0
  55. package/plugin/scripts/hook-shim.mjs +303 -0
  56. package/plugin/scripts/host-update.mjs +58 -0
  57. package/plugin/scripts/kling-preflight.sh +146 -0
  58. package/plugin/scripts/learn-capture.sh +173 -0
  59. package/plugin/scripts/learn-flush.mjs +155 -0
  60. package/plugin/scripts/lesson-hooks.sh +213 -0
  61. package/plugin/scripts/md-stamp.mjs +219 -0
  62. package/plugin/scripts/protect-brain-state.sh +84 -0
  63. package/plugin/scripts/route-dispatch.sh +147 -0
  64. package/plugin/scripts/routing-outcome-capture.mjs +89 -0
  65. package/plugin/scripts/runtime-preferences.mjs +269 -0
  66. package/plugin/scripts/session-start-core.mjs +477 -0
  67. package/plugin/scripts/session-start.sh +13 -0
  68. package/plugin/scripts/signal-watch.mjs +193 -0
  69. package/plugin/scripts/unprompted-runtime.mjs +377 -0
  70. package/plugin/scripts/update-apply.mjs +419 -0
  71. package/plugin/scripts/verify-interface.sh +53 -0
  72. package/plugin/scripts/version-bump-gate.sh +112 -0
  73. package/plugin/skills/brain-build/SKILL.md +123 -0
  74. package/plugin/skills/brain-console/SKILL.md +22 -0
  75. package/plugin/skills/brain-prompt/SKILL.md +83 -0
  76. package/plugin/skills/brain-score/SKILL.md +101 -0
  77. package/plugin/skills/release-proof/SKILL.md +81 -0
  78. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  79. package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
  80. package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
  81. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +121 -0
  82. package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
  83. package/plugin/skills/rvbc/SKILL.md +23 -0
  84. package/plugin/skills/savings/SKILL.md +46 -0
  85. package/plugin/skills/whats-new/SKILL.md +22 -0
  86. package/scripts/adr-backfill.mjs +107 -0
  87. package/scripts/advocacy-outcomes.mjs +808 -0
  88. package/scripts/agentdb-context.mjs +216 -0
  89. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  90. package/scripts/ascii-drift.mjs +236 -0
  91. package/scripts/behavioral-l1-l4.mjs +210 -0
  92. package/scripts/brain-capability-check.mjs +72 -0
  93. package/scripts/brain-grade-groundtruth.mjs +100 -0
  94. package/scripts/brain-latency-50.mjs +227 -0
  95. package/scripts/brain-novice-50.mjs +189 -0
  96. package/scripts/brain-stamp.mjs +94 -0
  97. package/scripts/brain-state.mjs +212 -0
  98. package/scripts/build-bundle.mjs +522 -0
  99. package/scripts/build-concepts.mjs +132 -0
  100. package/scripts/build-l2.mjs +71 -0
  101. package/scripts/build-primer.mjs +73 -0
  102. package/scripts/build-symbols.mjs +68 -0
  103. package/scripts/calibrate-router.mjs +97 -0
  104. package/scripts/capability-audit.mjs +321 -0
  105. package/scripts/capability-registry.mjs +876 -0
  106. package/scripts/check-indexation.mjs +108 -0
  107. package/scripts/check-legibility.mjs +189 -0
  108. package/scripts/ci/build-fixture-kb.mjs +67 -0
  109. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  110. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  111. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  112. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  113. package/scripts/ci/stranger-scenario.mjs +228 -0
  114. package/scripts/ci/stranger-timeout.mjs +25 -0
  115. package/scripts/ci-verdict.mjs +29 -0
  116. package/scripts/claims-verify.mjs +710 -0
  117. package/scripts/clear-claude-tmp.sh +31 -0
  118. package/scripts/console-engine.mjs +434 -0
  119. package/scripts/console-engine.test.mjs +125 -0
  120. package/scripts/corpus-qa.mjs +250 -0
  121. package/scripts/correction-detect-embed.mjs +346 -0
  122. package/scripts/correction-detect-measure.mjs +270 -0
  123. package/scripts/correction-detect.mjs +686 -0
  124. package/scripts/count-chunks.mjs +54 -0
  125. package/scripts/described-questions.json +30 -0
  126. package/scripts/design-grade.mjs +58 -0
  127. package/scripts/dev-plugin-link.sh +105 -0
  128. package/scripts/distill-project.mjs +200 -0
  129. package/scripts/doc-currency.mjs +801 -0
  130. package/scripts/eval-brain.mjs +244 -0
  131. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  132. package/scripts/full-hints.mjs +87 -0
  133. package/scripts/gate.sh +39 -0
  134. package/scripts/gates.mjs +146 -0
  135. package/scripts/gen-console-images.mjs +54 -0
  136. package/scripts/gen-images.mjs +47 -0
  137. package/scripts/git-clone-refresh.mjs +52 -0
  138. package/scripts/git-hooks/pre-push +126 -0
  139. package/scripts/goal-match.mjs +398 -0
  140. package/scripts/goldie-research.mjs +223 -0
  141. package/scripts/goldie-weekly.sh +67 -0
  142. package/scripts/health-repair.mjs +250 -0
  143. package/scripts/helix-scenario-questions.json +10 -0
  144. package/scripts/ingest-gists.mjs +230 -0
  145. package/scripts/ingest-meeting.mjs +115 -0
  146. package/scripts/ingest-repo.mjs +79 -0
  147. package/scripts/install-npx-witness.sh +49 -0
  148. package/scripts/issue-fix.mjs +639 -0
  149. package/scripts/issue-watch.mjs +276 -0
  150. package/scripts/issue4-close-note.md +31 -0
  151. package/scripts/key-canary.mjs +91 -0
  152. package/scripts/latency-to-surface.mjs +233 -0
  153. package/scripts/learning-enable.mjs +380 -0
  154. package/scripts/learning-replay.mjs +1570 -0
  155. package/scripts/learnings.mjs +62 -0
  156. package/scripts/lesson-gate.mjs +680 -0
  157. package/scripts/lesson-lifecycle.mjs +449 -0
  158. package/scripts/lesson-promote.mjs +262 -0
  159. package/scripts/lesson-ratify.mjs +98 -0
  160. package/scripts/lesson-seed.mjs +252 -0
  161. package/scripts/lesson-store.mjs +447 -0
  162. package/scripts/loop-checkpoint.mjs +86 -0
  163. package/scripts/memdb-health.sh +14 -0
  164. package/scripts/memory-doctor.mjs +271 -0
  165. package/scripts/model-catalog.mjs +79 -0
  166. package/scripts/nightly-controller.mjs +66 -0
  167. package/scripts/nightly-gists.sh +72 -0
  168. package/scripts/nightly-wrapper.sh +180 -0
  169. package/scripts/notify.sh +12 -0
  170. package/scripts/npx-witness.sh +56 -0
  171. package/scripts/onboarding-console.mjs +2749 -0
  172. package/scripts/private-fence.mjs +69 -0
  173. package/scripts/proactivity-metrics.mjs +118 -0
  174. package/scripts/proof-questions.json +56 -0
  175. package/scripts/prove.mjs +95 -0
  176. package/scripts/proxy/claude-proxied.sh +57 -0
  177. package/scripts/proxy/proxy-revert.sh +59 -0
  178. package/scripts/proxy/proxy-up.sh +60 -0
  179. package/scripts/proxy/proxy-verify.mjs +142 -0
  180. package/scripts/published-surface-probe.mjs +241 -0
  181. package/scripts/qe/card-lane-gate.mjs +162 -0
  182. package/scripts/qe/session-start-gate.mjs +229 -0
  183. package/scripts/qe/ux-suite.mjs +323 -0
  184. package/scripts/reconcile-project.mjs +0 -0
  185. package/scripts/record-lesson.mjs +113 -0
  186. package/scripts/refresh-model-catalog.mjs +99 -0
  187. package/scripts/release-proof.mjs +9 -0
  188. package/scripts/release-vector.mjs +281 -0
  189. package/scripts/release.mjs +395 -0
  190. package/scripts/remedy-registry.mjs +247 -0
  191. package/scripts/rerank-cap-eval.mjs +265 -0
  192. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  193. package/scripts/route-cheap.mjs +20 -15
  194. package/scripts/router-utilization.mjs +182 -0
  195. package/scripts/routing-flywheel.mjs +596 -0
  196. package/scripts/rvf-generation.mjs +104 -0
  197. package/scripts/rvf-index-audit.mjs +138 -0
  198. package/scripts/self-update.mjs +508 -0
  199. package/scripts/selfcheck.mjs +7 -1
  200. package/scripts/sign-bundle.mjs +69 -0
  201. package/scripts/signal-watch.mjs +171 -0
  202. package/scripts/stack-sync.mjs +469 -0
  203. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  204. package/scripts/stamp-sweep.mjs +144 -0
  205. package/scripts/status-honesty.mjs +102 -0
  206. package/scripts/sync-version.mjs +217 -0
  207. package/scripts/token-report.mjs +102 -0
  208. package/scripts/top100-benchmark.mjs +479 -0
  209. package/scripts/top100-corpus.mjs +112 -0
  210. package/scripts/top100-semantic-assertions.mjs +449 -0
  211. package/scripts/update-apply.mjs +9 -0
  212. package/scripts/upgrade-notice.mjs +14 -0
  213. package/scripts/verify-bundle.mjs +51 -0
  214. package/scripts/verify-channels.mjs +184 -0
  215. package/scripts/verify-model-catalog.mjs +104 -0
  216. package/scripts/verify-nightly-close-issue4.sh +31 -0
  217. package/scripts/version.mjs +40 -0
  218. package/scripts/wired-check.mjs +864 -0
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { performance } from 'node:perf_hooks';
9
+
10
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+
12
+ export const NOVICE_QUESTIONS = [
13
+ ['orientation', 'I just installed this. What is RuvNet Brain actually for?', 'ruvnet-brain', /source|ground|actual|rUv/i],
14
+ ['orientation', 'I am lost. What should I try first?', 'ruvnet-brain', /install|doctor|console|search/i],
15
+ ['orientation', 'Is this a chatbot, a database, or something else?', 'ruvnet-brain', /knowledge|ground|MCP|RVF/i],
16
+ ['orientation', 'Why would I use this instead of asking Claude from memory?', 'ruvnet-brain', /source|stale|ground|cite/i],
17
+ ['orientation', 'Does this work in Codex too, or only Claude Code?', 'ruvnet-brain', /Codex|Claude/i],
18
+ ['tiny-command', 'How do I open the settings screen?', 'ruvnet-brain', /rvbc|console/i],
19
+ ['tiny-command', 'What command checks whether my install is healthy?', 'ruvnet-brain', /doctor/i],
20
+ ['tiny-command', 'How do I see what changed in version 4?', 'ruvnet-brain', /what.?s.new|upgrade|4\.0/i],
21
+ ['tiny-command', 'Do I install this separately in every project?', 'ruvnet-brain', /user.level|shared|every project|cache/i],
22
+ ['tiny-command', 'Can I turn the Brain off temporarily?', 'ruvnet-brain', /off|disable|sentinel/i],
23
+ ['mistake', 'I guess RVF is just a JSON file full of embeddings, right?', 'ruvector', /binary|HNSW|RVF/i],
24
+ ['mistake', 'Should I put these vectors in Pinecone instead?', 'ruvector', /RVF|HNSW|local|zero.server/i],
25
+ ['mistake', 'AgentDB and RVF are the same thing, correct?', 'agentdb', /structured|memory|vector|RVF/i],
26
+ ['mistake', 'Ruflo is only a prompt library, isn’t it?', 'ruflo', /orchestrat|agent|swarm|memory/i],
27
+ ['mistake', 'If search finds nothing, that proves the feature does not exist, yes?', 'ruvnet-brain', /thin|absence|narrow|artifact/i],
28
+ ['small-specific', 'Where does RuvNet Brain store its own knowledge?', 'ruvnet-brain', /RVF|kb|cache/i],
29
+ ['small-specific', 'What is HNSW doing inside RVF?', 'ruvector', /index|nearest|vector|search/i],
30
+ ['small-specific', 'What is a witness chain in an RVF file?', 'ruvector', /witness|integrity|chain|provenance/i],
31
+ ['small-specific', 'What does AgentDB remember that RVF does not?', 'agentdb', /structured|operational|agent|memory/i],
32
+ ['small-specific', 'What is RuLake supposed to speed up?', 'rulake', /cache|read|vector|RVF/i],
33
+ ['how-to', 'How do I ask RuvNet Brain a source question?', 'ruvnet-brain', /search_ruvnet|query|source/i],
34
+ ['how-to', 'How do I make several coding agents work together?', 'ruflo', /swarm|agent|orchestrat|hierarchical/i],
35
+ ['how-to', 'How do I keep project decisions across sessions?', 'ruflo', /memory store|memory search|AgentDB|session/i],
36
+ ['how-to', 'How do I test a repo with specialized QE agents?', 'agentic-qe', /test|fleet|agent|quality/i],
37
+ ['how-to', 'How do I find code that has weak test coverage?', 'agentic-qe', /coverage|gap|risk/i],
38
+ ['big-picture', 'Explain how RVF, AgentDB, Ruflo, and RuvNet Brain fit together.', 'ruvnet-brain', /RVF|AgentDB|Ruflo|ground/i],
39
+ ['big-picture', 'What happens from my question to a cited answer?', 'ruvnet-brain', /retriev|rerank|evidence|cite/i],
40
+ ['big-picture', 'How does the system avoid hallucinating about shipped features?', 'ruvnet-brain', /implementation|source|manifest|proposed/i],
41
+ ['big-picture', 'What is the difference between orchestration and memory here?', 'ruflo', /orchestrat|memory|agent/i],
42
+ ['big-picture', 'Why use local models and files instead of a hosted vector server?', 'ruvector', /local|offline|RVF|server/i],
43
+ ['recovery', 'My search has been spinning for minutes. What should I check?', 'ruvnet-brain', /doctor|model|cache|health|timeout/i],
44
+ ['recovery', 'The Brain says evidence is thin. What do I do next?', 'ruvnet-brain', /narrow|repo|artifact|query/i],
45
+ ['recovery', 'My project memory is not updating. Which store should I inspect?', 'ruflo', /memory\.db|ruflo memory|swarm/i],
46
+ ['recovery', 'Ruflo memory says success but nothing was written. How do I prove it?', 'ruflo', /retrieve|search|sqlite|mtime|row/i],
47
+ ['recovery', 'The native SQLite bridge has the wrong Node ABI. Is fallback good enough?', 'ruflo', /ABI|better-sqlite3|fallback|native/i],
48
+ ['architecture', 'What is the correct storage split for a new knowledge app?', 'ruvnet-brain', /RVF|AgentDB|structured|vector/i],
49
+ ['architecture', 'When should I use SPARC?', 'sparc', /specification|architecture|refinement|completion/i],
50
+ ['architecture', 'How should a release prove the exact artifact it publishes?', 'ruvnet-brain', /SHA|digest|artifact|publish/i],
51
+ ['architecture', 'How should RuvNet Brain behave when a source worker times out?', 'ruvnet-brain', /kill|health|error|restart|timeout/i],
52
+ ['architecture', 'How does query routing keep searches from touching every repo?', 'ruvnet-brain', /route|card|scope|repo/i],
53
+ ['truth', 'Is the cross-encoder cascade currently shipped on by default?', 'ruvnet-brain', /off|default|ADR-060|accepted/i],
54
+ ['truth', 'Can you prove the release-proof protocol is implemented, not merely proposed?', 'ruvnet-brain', /implementation|source|manifest|proven/i],
55
+ ['truth', 'Does a green test run prove the npm package works after install?', 'ruvnet-brain', /packed|install|artifact|public/i],
56
+ ['truth', 'If Agentic QE executes zero tests, may the gate still pass?', 'ruvnet-brain', /zero|vacuous|fail|gate/i],
57
+ ['truth', 'Is every capability described in an ADR already available?', 'ruvnet-brain', /proposed|accepted|implemented|status/i],
58
+ ['comparison', 'RuvNet Brain versus Ruflo: which one answers source questions?', 'ruvnet-brain', /source|orchestrat|Ruflo|Brain/i],
59
+ ['comparison', 'RVF versus AgentDB: where do vectors and structured records go?', 'agentdb', /RVF|vector|AgentDB|structured/i],
60
+ ['comparison', 'RuLake versus RVF: is the cache the source of truth?', 'rulake', /cache|RVF|source|truth/i],
61
+ ['advanced', 'Which code path reranks candidates after RVF retrieval?', 'ruvnet-brain', /forge-rerank|cross.encoder|rerank/i],
62
+ ['advanced', 'What exact evidence would make you call RuvNet Brain ready to ship?', 'ruvnet-brain', /exact|artifact|test|public|receipt/i],
63
+ ].map(([category, query, repo, required], index) => ({
64
+ id: index + 1, category, query, repo, required,
65
+ }));
66
+
67
+ function percentile(values, fraction) {
68
+ const sorted = [...values].sort((a, b) => a - b);
69
+ return sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)];
70
+ }
71
+
72
+ function grade(spec, text, elapsedMs, transportOk) {
73
+ const repo = spec.repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
74
+ const expectedRepoCited = new RegExp(
75
+ `(?:repo\\s*=\\s*["']?${repo}\\b|path\\s*:\\s*${repo}/|capability-cards\\.md#${repo}\\b)`,
76
+ 'i',
77
+ ).test(text);
78
+ const cited = /(?:#\d+\s+repo=[a-z0-9._-]+|path\s*:\s*[a-z0-9._-]+\/|capability-cards\.md#[a-z0-9._-]+)/i
79
+ .test(text);
80
+ const useful = spec.required.test(text);
81
+ const unsupportedAbsence = /\b(?:does not exist|must be built|you need to build it)\b/i.test(text);
82
+ const evidenceQualified = /EVIDENCE:\s*THIN|NOT PROVEN|PROPOSED|curated-capability-card|THIS QUERY found nothing/i.test(text);
83
+ const honest = !unsupportedAbsence || evidenceQualified;
84
+ const latencyPoints = elapsedMs <= 4000 ? 10 : elapsedMs <= 8000 ? 5 : 0;
85
+ const score = (transportOk ? 20 : 0) + (text.length > 200 ? 20 : 0)
86
+ + (cited ? 20 : 0) + (useful ? 20 : 0) + (honest ? 10 : 0) + latencyPoints;
87
+ const effective = transportOk && cited && useful && honest && elapsedMs <= 4000;
88
+ return { score, cited, expectedRepoCited, useful, honest, effective, latencyPoints };
89
+ }
90
+
91
+ async function main() {
92
+ const concurrency = Math.max(1, Number(process.argv[2] || 10));
93
+ const brainHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-novice-'));
94
+ const child = spawn(process.execPath, [path.join(ROOT, 'plugin/mcp/server.mjs')], {
95
+ cwd: ROOT,
96
+ env: {
97
+ ...process.env,
98
+ RUVNET_BRAIN_HOME: brainHome,
99
+ RUVNET_BRAIN_KB: path.join(ROOT, 'kb'),
100
+ RUVNET_BRAIN_CHILD_MCP: path.join(ROOT, 'kb/forge-mcp-all.mjs'),
101
+ KB_MODEL_CACHE: path.join(ROOT, 'kb/models-cache'),
102
+ RUVNET_BRAIN_CALL_TIMEOUT_MS: '30000',
103
+ RUVNET_BRAIN_METER: '0',
104
+ },
105
+ stdio: ['pipe', 'pipe', 'pipe'],
106
+ });
107
+ const rl = readline.createInterface({ input: child.stdout });
108
+ const waiting = new Map();
109
+ let id = 0;
110
+ let stderr = '';
111
+ child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
112
+ rl.on('line', (line) => {
113
+ let msg; try { msg = JSON.parse(line); } catch { return; }
114
+ const done = waiting.get(msg.id);
115
+ if (done) { waiting.delete(msg.id); done(msg); }
116
+ });
117
+ const request = (method, params, timeoutMs = 120_000) => {
118
+ const requestId = ++id;
119
+ return new Promise((resolve) => {
120
+ const timer = setTimeout(() => {
121
+ waiting.delete(requestId);
122
+ resolve({ error: { message: `${method} client timeout` } });
123
+ }, timeoutMs);
124
+ waiting.set(requestId, (msg) => { clearTimeout(timer); resolve(msg); });
125
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: requestId, method, params })}\n`);
126
+ });
127
+ };
128
+
129
+ const readyAt = performance.now();
130
+ await request('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'novice-50', version: '1' } });
131
+ const tools = await request('tools/list', {});
132
+ if (!tools.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('search_ruvnet not ready');
133
+ const readinessMs = Math.round(performance.now() - readyAt);
134
+
135
+ const results = new Array(NOVICE_QUESTIONS.length);
136
+ let next = 0;
137
+ await Promise.all(Array.from({ length: concurrency }, async () => {
138
+ for (let index = next++; index < NOVICE_QUESTIONS.length; index = next++) {
139
+ const spec = NOVICE_QUESTIONS[index];
140
+ const started = performance.now();
141
+ const msg = await request('tools/call', {
142
+ name: 'search_ruvnet',
143
+ arguments: { query: spec.query, k: 5 },
144
+ }, 35_000);
145
+ const elapsedMs = Math.round(performance.now() - started);
146
+ const text = msg.result?.content?.[0]?.text || '';
147
+ const transportOk = !msg.error && msg.result?.isError !== true;
148
+ results[index] = {
149
+ ...spec,
150
+ elapsedMs,
151
+ transportOk,
152
+ error: msg.error?.message || (msg.result?.isError ? text.slice(0, 240) : null),
153
+ answer: text,
154
+ ...grade(spec, text, elapsedMs, transportOk),
155
+ };
156
+ process.stdout.write(`${String(spec.id).padStart(2, '0')} ${String(results[index].score).padStart(3)} ${String(elapsedMs).padStart(6)}ms ${spec.category.padEnd(14)} ${spec.query}\n`);
157
+ }
158
+ }));
159
+
160
+ child.stdin.end();
161
+ await Promise.race([new Promise((resolve) => child.once('exit', resolve)), new Promise((resolve) => setTimeout(resolve, 5000))]);
162
+ if (child.exitCode === null) child.kill('SIGKILL');
163
+ rl.close();
164
+ fs.rmSync(brainHome, { recursive: true, force: true });
165
+
166
+ const times = results.map((result) => result.elapsedMs);
167
+ const report = {
168
+ generatedAt: new Date().toISOString(),
169
+ concurrency,
170
+ readinessMs,
171
+ passed: results.filter((result) => result.effective).length,
172
+ under4s: results.filter((result) => result.elapsedMs <= 4000).length,
173
+ averageScore: Math.round(results.reduce((sum, result) => sum + result.score, 0) / results.length),
174
+ averageMs: Math.round(times.reduce((sum, value) => sum + value, 0) / times.length),
175
+ medianMs: percentile(times, 0.5),
176
+ p95Ms: percentile(times, 0.95),
177
+ longestMs: Math.max(...times),
178
+ stderr: stderr.trim(),
179
+ results,
180
+ };
181
+ const output = path.join(ROOT, 'data/novice-50-report.json');
182
+ fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
183
+ console.log(`SUMMARY ${report.passed}/50 effective · ${report.under4s}/50 under 4s · score ${report.averageScore}/100 · avg ${report.averageMs}ms · p95 ${report.p95Ms}ms · max ${report.longestMs}ms`);
184
+ console.log(`REPORT ${output}`);
185
+ }
186
+
187
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
188
+ main().catch((error) => { console.error(error.stack || error.message); process.exitCode = 1; });
189
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // brain-stamp.mjs — stamp the bundle with build date + per-repo commit SHA.
3
+ // Writes data/manifest.json and injects the stamp into the primer header + each KB SOURCE.json.
4
+ // Run after a (re)build. Self-update calls this so every download says exactly what it covers.
5
+ //
6
+ // Usage: node scripts/brain-stamp.mjs [--brain-version v0.1.0-dev]
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { execFileSync } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { canonicalRvfStores, hasCanonicalRvfStore } from './rvf-generation.mjs';
12
+ import { getVersionTag, stripTag } from './version.mjs';
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
16
+ const BRAIN_VERSION = arg('--brain-version', getVersionTag()); // inherits the single source of truth
17
+ const NOW = new Date().toISOString();
18
+ const KB_DIR = path.join(ROOT, 'kb');
19
+
20
+ // Known local clones (extend as repos are added). Self-update resolves remote SHAs for the rest.
21
+ const CLONES = JSON.parse(process.env.RUVNET_KNOWN_CLONES || '{}');
22
+ const builtName = (n) => hasCanonicalRvfStore(KB_DIR, n);
23
+ const shaOf = (dir) => { try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir }).toString().trim(); } catch { return null; } };
24
+ // no shell: slug is placed in a URL arg, never interpreted by a shell
25
+ const remoteSha = (slug) => { try { return execFileSync('git', ['ls-remote', `https://github.com/ruvnet/${slug}`, 'HEAD']).toString().split(/\s/)[0] || null; } catch { return null; } };
26
+
27
+ const tiers = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/registry.tiers.json'), 'utf8'));
28
+ const repos = [];
29
+ for (const t of ['T0', 'T1', 'T2', 'T3']) {
30
+ for (const r of tiers.tiers[t].repos) {
31
+ // Default clone location is ROOT/clones/<name> (where self-update.mjs puts them) — the env
32
+ // override is the exception, not the only source. Stamping 'unknown' when a real clone sits
33
+ // on disk made unknown-SHA repos permanently invisible to the freshness loop (ruflo bug).
34
+ const cloneDir = CLONES[r.name] || path.join(ROOT, 'clones', r.name);
35
+ const localSha = fs.existsSync(path.join(cloneDir, '.git')) ? shaOf(cloneDir) : null;
36
+ repos.push({
37
+ name: r.name, tier: t, stars: r.stars,
38
+ builtFromSha: builtName(r.name) ? (localSha || 'unknown') : null,
39
+ latestRemoteSha: null, // filled by --check-remote (network) in self-update
40
+ status: builtName(r.name) ? 'built' : 'pending',
41
+ });
42
+ }
43
+ }
44
+
45
+ // DURABILITY FIX (nail-the-nightly): the nightly is now --fresh-window / live-scan driven, but this
46
+ // stamper was tiers-file driven. A repo discovered by --fresh-window that isn't in the static
47
+ // registry.tiers.json would build every night yet never receive a builtFromSha, so the freshness loop
48
+ // would REBUILD it forever. Stamp EVERY built store on disk (tiers-listed or not) so "unchanged =>
49
+ // skipped" holds for fresh-window repos too. Clone dir resolved case-insensitively (store names are
50
+ // lowercased; clone dirs keep the repo's own casing, e.g. kb/photonlayer.rvf <- clones/PhotonLayer).
51
+ const clonesDir = path.join(ROOT, 'clones');
52
+ const cloneEntries = fs.existsSync(clonesDir) ? fs.readdirSync(clonesDir) : [];
53
+ const findClone = (name) => {
54
+ if (CLONES[name] && fs.existsSync(path.join(CLONES[name], '.git'))) return CLONES[name];
55
+ const exact = path.join(clonesDir, name);
56
+ if (fs.existsSync(path.join(exact, '.git'))) return exact;
57
+ const ci = cloneEntries.find((d) => d.toLowerCase() === name.toLowerCase());
58
+ return ci && fs.existsSync(path.join(clonesDir, ci, '.git')) ? path.join(clonesDir, ci) : null;
59
+ };
60
+ const seen = new Set(repos.map((r) => r.name.toLowerCase()));
61
+ for (const store of canonicalRvfStores(KB_DIR)) {
62
+ if (seen.has(store.toLowerCase())) continue;
63
+ seen.add(store.toLowerCase());
64
+ const cloneDir = findClone(store);
65
+ repos.push({
66
+ name: store, tier: 'fresh', stars: undefined,
67
+ builtFromSha: cloneDir ? (shaOf(cloneDir) || 'unknown') : 'unknown',
68
+ latestRemoteSha: null,
69
+ status: 'built',
70
+ });
71
+ }
72
+ const built = repos.filter((r) => r.status === 'built');
73
+ const manifest = {
74
+ brainVersion: stripTag(BRAIN_VERSION), // FIELD = bare literal; the "v" tag is display-only (see version.mjs)
75
+ generated: NOW,
76
+ generatedHuman: new Date(NOW).toUTCString(),
77
+ coverage: { built: built.length, catalogued: repos.length, orgTotalApprox: 248, pending: repos.length - built.length },
78
+ builtRepos: built,
79
+ pendingRepos: repos.filter((r) => r.status === 'pending').map((r) => ({ name: r.name, tier: r.tier })),
80
+ };
81
+ fs.mkdirSync(path.join(ROOT, 'data'), { recursive: true });
82
+ fs.writeFileSync(path.join(ROOT, 'data/manifest.json'), JSON.stringify(manifest, null, 2));
83
+
84
+ // Stamp the primer header line.
85
+ const primerPath = path.join(ROOT, 'primer/ruvnet-primer.md');
86
+ if (fs.existsSync(primerPath)) {
87
+ let p = fs.readFileSync(primerPath, 'utf8');
88
+ const stamp = `\`Brain version: ${BRAIN_VERSION} · Built: ${new Date(NOW).toISOString().slice(0, 10)} · Covers: ${built.length}/${repos.length} repos built @ pinned SHAs (see data/manifest.json)\``;
89
+ p = p.replace(/`Brain version:[^`]*`/, stamp);
90
+ fs.writeFileSync(primerPath, p);
91
+ }
92
+ console.log(`Stamped ${BRAIN_VERSION} @ ${NOW}`);
93
+ console.log(`Coverage: ${built.length} built / ${repos.length} total`);
94
+ for (const b of built) console.log(` ${b.tier} ${b.name} @ ${b.builtFromSha?.slice(0, 12)}`);
@@ -0,0 +1,212 @@
1
+ // brain-state.mjs — is the brain ON or OFF? One question, one file, no third state. (ADR-054 §2)
2
+ //
3
+ // THE ONE IDEA: THE SENTINEL *IS* THE SWITCH.
4
+ //
5
+ // `~/.config/ruvnet-brain/brain-off` present = off. Absent = on. There is deliberately no "enabled"
6
+ // value anywhere that a reader has to parse, interpret, version-negotiate, or repair — because every
7
+ // one of those verbs is a place where the user's stated choice can be silently lost, and the
8
+ // 2026-07-26 adversarial duel found all of them in the JSON-settings design this replaced:
9
+ //
10
+ // • SKEW. An older release's validate() DROPS unknown keys ("Dropped, not preserved" —
11
+ // user-settings.mjs). A user turns the brain off on v4, opens a project still running v3, v3
12
+ // saves any unrelated setting, and `brainEnabled` is gone. The next session the brain is back on
13
+ // and nothing anywhere reports it. Gate test 1 reproduces exactly that sequence.
14
+ // • CORRUPTION AND FAIL POLARITY. A truncated write, an EACCES, or a file written by a future
15
+ // version all make loadSettings() fall back to DEFAULTS — and the default is ON. Every failure
16
+ // mode in a settings file therefore fails toward "enabled", which is the wrong direction for a
17
+ // consent record. `[ -f ]` has no failure mode that resolves to "the user must have meant on".
18
+ // • READERS. Three bash gates and a bundled MCP child have to answer this question. `[ -f x ]`
19
+ // works in bash 3.2 with no jq, no node and no parsing; `fs.existsSync` works in any node
20
+ // vintage. A JSON schema does not.
21
+ //
22
+ // settings.json keeps a MIRROR key (`brainEnabled`) so the console has a UI record and so the choice
23
+ // shows up where a user goes looking for their choices. The mirror is a record, never the switch:
24
+ // when the two disagree the sentinel wins and `disagreement()` hands the console the fact to show,
25
+ // rather than letting one surface quietly pick a winner.
26
+ //
27
+ // DELIBERATELY DEPENDENCY-FREE. node builtins only. This module is imported by the console and by
28
+ // the installer's siblings; the boot-frozen hook shim and the bundled KB child re-derive the same
29
+ // path inline instead of importing it, because neither can reach outside its own frozen tree. Those
30
+ // three copies are held together by tests, not by hope (tests/unit/brain-off.test.mjs asserts the
31
+ // real path from bash, from the shim and from the bundle).
32
+
33
+ import fs from 'node:fs';
34
+ import os from 'node:os';
35
+ import path from 'node:path';
36
+
37
+ /**
38
+ * WHERE IT LIVES. Same directory, and the same argument, as user-settings.mjs's STORE_PATH: grepping
39
+ * bin/install.mjs for `.config/ruvnet-brain` shows the installer has no code path that writes there
40
+ * during `--update`, so a preference stored here survives every release. `--uninstall` is the ONE
41
+ * exception and it is deliberate (ADR-054 §5): a removed product must not leave an invisible OFF
42
+ * behind for its own reinstall to inherit.
43
+ *
44
+ * Resolved per call, not once at module load, so a test (or a second machine profile) can point it
45
+ * somewhere else with RUVNET_BRAIN_STATE_DIR and have every function in this file agree immediately.
46
+ */
47
+ export function stateDir() {
48
+ return process.env.RUVNET_BRAIN_STATE_DIR || path.join(os.homedir(), '.config', 'ruvnet-brain');
49
+ }
50
+
51
+ /** The switch. Its EXISTENCE is the entire protocol; its contents are metadata for humans. */
52
+ export function sentinelPath() {
53
+ return path.join(stateDir(), 'brain-off');
54
+ }
55
+
56
+ /**
57
+ * IS THE BRAIN OFF? The only question this module exists to answer.
58
+ *
59
+ * Note what it does NOT do: read the file, parse it, validate it, or consult settings.json. A
60
+ * `touch`ed, empty, corrupt or hand-written sentinel all mean the same thing, because a user who put
61
+ * a file there meant it. Anything cleverer would reintroduce a parse step, and a parse step is a
62
+ * failure mode, and a failure mode on a consent record resolves to "we ignored what you asked for".
63
+ */
64
+ export function isBrainOff(file = sentinelPath()) {
65
+ try { fs.statSync(file); return true; }
66
+ catch (e) { return !isAbsent(e); }
67
+ }
68
+
69
+ /**
70
+ * ONLY "the file is genuinely not there" counts as ON. Everything else counts as OFF.
71
+ *
72
+ * This distinction was NOT in the first draft and gate test 5 caught it, which is the whole reason
73
+ * that test exists. `fs.existsSync` does not throw on EACCES — it returns FALSE. So a state
74
+ * directory the process cannot read reported "no sentinel" and the brain came back on, silently,
75
+ * for a user who had switched it off: the exact fail-open polarity ADR-054 §2 chose the sentinel to
76
+ * dissolve, reproduced inside the sentinel's own reader.
77
+ *
78
+ * The tradeoff is stated rather than hidden. An unreadable state dir now reads as OFF even for a
79
+ * user who never switched it off — and that is the direction to be wrong in. Wrong-toward-off is
80
+ * VISIBLE (session-start prints its one state line every session, the console shows the switch,
81
+ * both name the file) and recoverable in one `chmod`. Wrong-toward-on is INVISIBLE and overrides a
82
+ * choice the user already made, which is a consent violation rather than a bug.
83
+ */
84
+ function isAbsent(e) {
85
+ return !!e && (e.code === 'ENOENT' || e.code === 'ENOTDIR');
86
+ }
87
+
88
+ /**
89
+ * READ THE STATE, including the parts we might not know.
90
+ *
91
+ * `since` falls back to the file's own mtime when the contents are unparseable, so the session-start
92
+ * line can still name a date instead of printing "since undefined". `reason` does NOT get a fallback:
93
+ * we either know why or we say null. Inventing a plausible reason on a consent record is precisely
94
+ * the fabrication this repo has a standing order against.
95
+ */
96
+ export function readOffState(file = sentinelPath()) {
97
+ let stat;
98
+ try { stat = fs.statSync(file); }
99
+ catch (e) {
100
+ // Same polarity as isBrainOff(): only genuine absence is ON. An unreadable state dir reports OFF
101
+ // with everything it does not know set to null, rather than inventing an "on" it cannot see.
102
+ if (isAbsent(e)) return { off: false, since: null, reason: null, host: null, path: file };
103
+ return { off: true, since: null, reason: null, host: null, path: file, unreadable: e.code || 'unknown' };
104
+ }
105
+
106
+ let parsed = null;
107
+ try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); }
108
+ catch { /* touched / truncated / hand-written — existence still decides */ }
109
+
110
+ const valid = parsed && typeof parsed === 'object' && !Array.isArray(parsed);
111
+ return {
112
+ off: true,
113
+ since: (valid && typeof parsed.since === 'string' && !Number.isNaN(Date.parse(parsed.since)))
114
+ ? parsed.since
115
+ : new Date(stat.mtimeMs).toISOString(),
116
+ reason: valid && typeof parsed.reason === 'string' && parsed.reason ? parsed.reason : null,
117
+ host: valid && typeof parsed.host === 'string' ? parsed.host : null,
118
+ path: file,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * TURN IT OFF — atomically, so no reader can ever observe a half-written sentinel.
124
+ *
125
+ * temp-file + rename, the same primitive and the same reason as user-settings.mjs's writeAtomic():
126
+ * two console tabs, two Claude Code windows and a CLI can all be writing this at once, and a reader
127
+ * that catches a zero-byte file mid-write must still read "off" rather than crash or, far worse,
128
+ * read "on". The temp name carries the pid so concurrent writers cannot collide on it.
129
+ *
130
+ * The write is best-effort about its CONTENT and strict about its EXISTENCE: if the JSON body cannot
131
+ * be written we still report the failure honestly rather than claiming the brain is off when it is
132
+ * not. A switch that lies about having been flipped is worse than a switch that refuses.
133
+ */
134
+ export function setBrainOff(reason = null, { file = sentinelPath() } = {}) {
135
+ const dir = path.dirname(file);
136
+ try { fs.mkdirSync(dir, { recursive: true }); }
137
+ catch (e) { return { ok: false, path: file, log: `could not create ${dir}: ${e.message}` }; }
138
+
139
+ const body = `${JSON.stringify({
140
+ off: true,
141
+ since: new Date().toISOString(),
142
+ reason: reason ? String(reason) : null,
143
+ host: os.hostname(),
144
+ // Legible to a human who finds this file and has no idea what it is. Support reads it in one go.
145
+ note: 'RuvNet Brain is switched OFF. This file IS the switch — deleting it turns the brain back on.',
146
+ }, null, 2)}\n`;
147
+
148
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
149
+ try {
150
+ fs.writeFileSync(tmp, body, { flag: 'wx' });
151
+ fs.renameSync(tmp, file);
152
+ } catch (e) {
153
+ try { fs.rmSync(tmp, { force: true }); } catch { /* leave the temp rather than throw over it */ }
154
+ return { ok: false, path: file, log: `could not write the switch: ${e.message}` };
155
+ }
156
+ return { ok: true, path: file, off: true, log: 'the brain is off' };
157
+ }
158
+
159
+ /**
160
+ * TURN IT ON — remove the sentinel. Idempotent by construction: "already on" is a success, not an
161
+ * error, because the caller asked for a STATE and got it. The installer's uninstall path relies on
162
+ * exactly this shape.
163
+ */
164
+ export function setBrainOn({ file = sentinelPath() } = {}) {
165
+ try {
166
+ if (!fs.existsSync(file)) return { ok: true, path: file, off: false, log: 'the brain was already on' };
167
+ fs.rmSync(file);
168
+ return { ok: true, path: file, off: false, log: 'the brain is on' };
169
+ } catch (e) {
170
+ return { ok: false, path: file, log: `could not remove the switch (${file}): ${e.message}` };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * DO THE SWITCH AND ITS MIRROR AGREE? Returns null when they do, and the FACT when they do not.
176
+ *
177
+ * The console renders this rather than resolving it silently. The two can legitimately drift — an
178
+ * older release drops the mirror key (gate test 1), a user hand-edits one of the two files, a
179
+ * `--update` from a different line lands in between — and in every one of those cases the honest
180
+ * surface says "these disagree, here is which one is in force" instead of showing a toggle whose
181
+ * position is a guess.
182
+ */
183
+ export function disagreement(mirrorEnabled, { file = sentinelPath() } = {}) {
184
+ const off = isBrainOff(file);
185
+ const mirrorSaysOff = mirrorEnabled === false;
186
+ if (off === mirrorSaysOff) return null;
187
+ return {
188
+ sentinelWins: true,
189
+ inForce: off ? 'off' : 'on',
190
+ sentinel: off ? 'off' : 'on',
191
+ mirror: mirrorSaysOff ? 'off' : 'on',
192
+ note: off
193
+ ? 'The switch file says OFF and your saved settings say ON. The switch is what every part of the brain actually reads, so the brain is OFF.'
194
+ : 'Your saved settings say OFF but the switch file is gone, so the brain is ON. Set it again here if you meant it to stay off.',
195
+ };
196
+ }
197
+
198
+ // ── CLI ──────────────────────────────────────────────────────────────────────────────────────────
199
+ // Read-only. Turning the brain on or off is the console's job (it takes a backup of the mirror and
200
+ // records the change); this prints what is true right now so support can diagnose in one command.
201
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]).endsWith('brain-state.mjs');
202
+ if (invokedDirectly) {
203
+ const state = readOffState();
204
+ if (process.argv.includes('--json')) {
205
+ console.log(JSON.stringify(state, null, 2));
206
+ } else if (state.off) {
207
+ console.log(`\n RuvNet Brain: OFF since ${state.since.slice(0, 10)}${state.reason ? ` — ${state.reason}` : ''}`);
208
+ console.log(` Switch file : ${state.path.replace(os.homedir(), '~')}\n`);
209
+ } else {
210
+ console.log(`\n RuvNet Brain: ON (no switch file at ${state.path.replace(os.homedir(), '~')})\n`);
211
+ }
212
+ }