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,54 @@
1
+ #!/usr/bin/env node
2
+ // scripts/count-chunks.mjs — recount the REAL passage total across the local PUBLIC kb stores and
3
+ // restamp it onto every public surface that advertises it. Rule: no number beats a wrong number —
4
+ // the hand-typed "129,685 source chunks" rotted (SOURCE.json advertised a corpus that had moved on)
5
+ // and was pulled from every surface in v3.2.5. This script is how it comes back honestly: computed
6
+ // fresh from disk every time, never hand-typed again.
7
+ //
8
+ // NOTHING is duplicated here — neither the counting nor the restamping. Both live in
9
+ // claims-verify.mjs (`applyFix`), the same module whose `verifyChunkCountSurfaces` gates the result,
10
+ // and this file is a thin CLI over it. That is deliberate: a second writer with its own regexes is
11
+ // how four surfaces drift four ways in the first place. Private stores (kb/PRIVATE-STORES.json) are
12
+ // fenced out by the shared counter, so the public number can never include private content.
13
+ //
14
+ // A surface with NO existing "N chunks" claim (e.g. explainer/index.html before this was restored)
15
+ // is reported, not silently improvised into someone else's prose — the first stamp on a new surface
16
+ // is a deliberate one-time copy edit, done by hand once; after that this script keeps it fresh.
17
+ //
18
+ // Usage: node scripts/count-chunks.mjs # recount + write the fresh census into every surface
19
+ // node scripts/count-chunks.mjs --check # report only; exit 1 if any surface would change
20
+ //
21
+ // `npm run claims:fix` does this AND the coverage badge in one pass; prefer it. This entry point
22
+ // stays because the nightly brain rebuild wants the census restamped without touching coverage.
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { applyFix } from './claims-verify.mjs';
26
+
27
+ const CHECK = process.argv.includes('--check');
28
+
29
+ async function main() {
30
+ // summaryFile deliberately points nowhere: this entry point restamps the CENSUS only, and the
31
+ // coverage half then reports itself as "not stamped" rather than silently doing half a job.
32
+ const report = await applyFix({ write: !CHECK, summaryFile: path.join('/nonexistent', 'coverage-summary.json') });
33
+ if (!report.census) {
34
+ console.error('[count-chunks] no public kb stores found under kb/*.big.rvf.idmap.json — is the local brain built? Nothing to stamp.');
35
+ process.exit(1);
36
+ }
37
+ const { chunks, publicStores, builtStores } = report.census;
38
+ const want = chunks.toLocaleString('en-US');
39
+ console.log(`[count-chunks] recounted ${want} chunks across ${publicStores} public stores of ${builtStores} built (private stores excluded per kb/PRIVATE-STORES.json)`);
40
+ for (const n of report.notes) console.warn(`[count-chunks] ${n}`);
41
+
42
+ const drifted = report.changed;
43
+ if (CHECK && drifted.length) {
44
+ for (const rel of drifted) console.error(`[count-chunks] DRIFT: ${rel} would restamp -> ${want}`);
45
+ console.error(`\n[count-chunks] --check: ${drifted.length} surface(s) drifted`);
46
+ process.exit(1);
47
+ }
48
+ for (const rel of drifted) console.log(`[count-chunks] ${rel}: restamped -> ${want}`);
49
+ console.log(`\n[count-chunks] done — ${want} chunks is the live count; ${drifted.length} surface(s) rewritten.`);
50
+ }
51
+
52
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
53
+ await main();
54
+ }
@@ -0,0 +1,30 @@
1
+ [
2
+ { "set": "described", "query": "What should I use to coordinate a swarm of agents working in parallel?", "expectRepo": ["ruflo", "concepts"], "minRelevance": -6 },
3
+ { "set": "described", "query": "How do I run several coding agents at once and have them share state and memory?", "expectRepo": ["ruflo", "concepts"], "minRelevance": -6 },
4
+ { "set": "described", "query": "What is the best way to store and search embeddings on disk locally and privately?", "expectRepo": ["ruvector", "concepts"], "minRelevance": -6 },
5
+ { "set": "described", "query": "I need on-disk approximate nearest neighbor vector search with an HNSW graph. What should I use?", "expectRepo": ["ruvector", "concepts"], "minRelevance": -6 },
6
+ { "set": "described", "query": "How do I give an agent persistent structured memory that survives across sessions?", "expectRepo": ["agentdb", "concepts"], "minRelevance": -6 },
7
+ { "set": "described", "query": "What stores agent memory and lets me run graph queries over it?", "expectRepo": ["agentdb", "concepts"], "minRelevance": -6 },
8
+ { "set": "described", "query": "How do I make repeated vector queries return in under a millisecond with a cache?", "expectRepo": ["rulake", "concepts"], "minRelevance": -6 },
9
+ { "set": "described", "query": "Is there a caching layer that sits in front of a vector store to speed up recall?", "expectRepo": ["rulake", "concepts"], "minRelevance": -6 },
10
+ { "set": "described", "query": "How can I detect people, occupancy, and falls through walls using only WiFi signals and no camera?", "expectRepo": ["ruview", "concepts"], "minRelevance": -6 },
11
+ { "set": "described", "query": "How do I read breathing rate and heart rate contactlessly from radio signals, with no wearable?", "expectRepo": ["ruview", "concepts"], "minRelevance": -6 },
12
+ { "set": "described", "query": "How do I route to cheaper models automatically to cut my AI bill in Claude Code?", "expectRepo": ["agentic-flow", "concepts"], "minRelevance": -6 },
13
+ { "set": "described", "query": "What picks the cheapest good-enough model for each task automatically?", "expectRepo": ["agentic-flow", "concepts"], "minRelevance": -6 },
14
+ { "set": "described", "query": "What structured methodology should I follow to plan and build a non-trivial feature step by step?", "expectRepo": ["sparc", "concepts", "ruflo"], "minRelevance": -6 },
15
+ { "set": "described", "query": "Is there a phased build process with quality gates between each stage?", "expectRepo": ["sparc", "concepts", "ruflo"], "minRelevance": -6 },
16
+ { "set": "described", "query": "How do I get quantum-resistant secure messaging between agents?", "expectRepo": ["qudag", "concepts"], "minRelevance": -6 },
17
+ { "set": "described", "query": "What provides anonymous DAG-based communication that resists quantum attacks?", "expectRepo": ["qudag", "concepts"], "minRelevance": -6 },
18
+ { "set": "described", "query": "What gives an agent a self-aware feedback loop so it improves itself over time?", "expectRepo": ["safla", "concepts"], "minRelevance": -6 },
19
+ { "set": "described", "query": "Is there a fast neural network library I can run in Rust or compile to WASM?", "expectRepo": ["ruv-fann", "concepts"], "minRelevance": -6 },
20
+ { "set": "described", "query": "How do I compress prompts to cut token cost without losing meaning?", "expectRepo": ["synthlang", "concepts"], "minRelevance": -6 },
21
+ { "set": "described", "query": "How do I retrieve documents using visual or page-image embeddings instead of text?", "expectRepo": ["rupixel", "concepts"], "minRelevance": -6 },
22
+ { "set": "described", "query": "What uses copy-on-write to fork and branch agent memory cheaply?", "expectRepo": ["agenticow", "concepts"], "minRelevance": -6 },
23
+ { "set": "described", "query": "How do I benchmark whether an agent can actually fix real security vulnerabilities?", "expectRepo": ["cve-bench", "concepts"], "minRelevance": -6 },
24
+ { "set": "described", "query": "What framework supports decentralized autonomous agents and applications?", "expectRepo": ["daa", "concepts"], "minRelevance": -6 },
25
+ { "set": "described", "query": "Is there a TypeScript way to program and optimize LLM pipelines the way DSPy does in Python?", "expectRepo": ["dspy.ts", "concepts"], "minRelevance": -6 },
26
+ { "set": "described", "query": "What gives fast cached tool results for reliable, low-latency AI tool integration?", "expectRepo": ["fact", "concepts"], "minRelevance": -6 },
27
+ { "set": "described", "query": "How do I scaffold, A/B test, and score different agent harnesses?", "expectRepo": ["metaharness", "concepts"], "minRelevance": -6 },
28
+ { "set": "described", "query": "What is the building block for orchestrating parallel work across many specialized agents?", "expectRepo": ["ruflo", "concepts"], "minRelevance": -6 },
29
+ { "set": "described", "query": "I want private local semantic search over my own documents. Which engine should I build on?", "expectRepo": ["ruvector", "concepts"], "minRelevance": -6 }
30
+ ]
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // design-grade.mjs — the ONLY key to the design wall (plugin/scripts/design-wall.sh).
3
+ //
4
+ // WHY (2026-07-16). Stuart: "You are always supposed to look at a page as an end user would: take
5
+ // pictures of it, review it, analyze it, grade it, see if it gets a 95 or better, and if it doesn't,
6
+ // tweak it until it does — BEFORE you ever tell me something is ready." A memory-file rule was not
7
+ // enough ("suggestions mean bullshit"); this repo's own history says only walls hold. This tool
8
+ // enforces the RITUAL mechanically: it refuses to record a grade unless >=2 fresh screenshots at
9
+ // distinct widths exist and the deductions are written down. The grade itself remains judgment —
10
+ // but the receipt (shots + deductions + number) is auditable, and the wall only opens at >=95.
11
+ //
12
+ // Usage:
13
+ // node scripts/design-grade.mjs --surface explainer --grade 96 \
14
+ // --shot /path/a-1440.png --shot /path/b-1920.png \
15
+ // --deductions "caption widowed at 1440 (-2); card border too faint in light (-2)"
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import os from 'node:os';
19
+ import { execFileSync } from 'node:child_process';
20
+
21
+ const args = process.argv.slice(2);
22
+ const get = (k) => { const i = args.indexOf('--' + k); return i >= 0 ? args[i + 1] : null; };
23
+ const shots = []; args.forEach((a, i) => { if (a === '--shot') shots.push(args[i + 1]); });
24
+ const surface = get('surface');
25
+ const grade = Number(get('grade'));
26
+ const deductions = get('deductions') || '';
27
+ const fail = (m) => { console.error('✗ ' + m); process.exit(1); };
28
+
29
+ if (!surface || !/^[a-z0-9-]+$/.test(surface)) fail('need --surface (kebab-case: explainer, console, readme, …)');
30
+ if (!Number.isFinite(grade) || grade < 0 || grade > 100) fail('need a numeric --grade 0-100');
31
+ if (shots.length < 2) fail('the ritual requires >=2 screenshots (two widths) — take them and LOOK at them first');
32
+ if (!deductions.trim()) fail('write the deductions down — a grade with no deductions is a vibe, not a grade');
33
+
34
+ const widths = new Set();
35
+ for (const s of shots) {
36
+ if (!s || !fs.existsSync(s)) fail('missing screenshot: ' + s);
37
+ const ageMin = (Date.now() - fs.statSync(s).mtimeMs) / 60000;
38
+ if (ageMin > 45) fail(`stale screenshot (${ageMin | 0} min old): ${s} — the page may have changed; re-shoot`);
39
+ try {
40
+ const out = String(execFileSync('sips', ['-g', 'pixelWidth', s], { stdio: ['ignore', 'pipe', 'ignore'] }));
41
+ const w = Number(out.match(/pixelWidth: (\d+)/)?.[1] || 0);
42
+ if (w) widths.add(w);
43
+ } catch { /* sips unavailable → widths check degrades below */ }
44
+ }
45
+ if (widths.size > 0 && widths.size < 2) fail('shots must cover >=2 distinct widths (e.g. 1440 and 1920) — got: ' + [...widths].join(', '));
46
+
47
+ const rec = {
48
+ surface, grade, passing: grade >= 95,
49
+ widths: [...widths].sort((a, b) => a - b),
50
+ shots: shots.map((s) => path.resolve(s)),
51
+ deductions, at: new Date().toISOString(),
52
+ };
53
+ const dir = path.join(os.homedir(), '.cache/ruvnet-brain');
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ fs.appendFileSync(path.join(dir, 'design-grades.jsonl'), JSON.stringify(rec) + '\n');
56
+ fs.writeFileSync(path.join(dir, `design-stamp-${surface}.json`), JSON.stringify(rec, null, 2) + '\n');
57
+ console.log(`${rec.passing ? '✓ PASSING' : '✗ RECORDED — NOT PASSING'} · ${surface} graded ${grade}/100 · widths ${rec.widths.join('/') || 'unverified'} · ${shots.length} shots`);
58
+ if (!rec.passing) { console.log(' fix the deductions and re-run — the wall stays closed below 95.'); process.exit(3); }
@@ -0,0 +1,105 @@
1
+ #!/bin/sh
2
+ # dev-plugin-link.sh — stop having to exit and restart Claude Code after every hook edit.
3
+ #
4
+ # THE PROBLEM. Claude Code COPIES a plugin into ~/.claude/plugins/cache/<name>/<version>/ and a
5
+ # session pins one version directory at startup. The hook the harness executes is that copy, never
6
+ # your working tree. So "I fixed the hook" and "the hook is fixed" are two different statements
7
+ # until you restart. (Observed 2026-07-09: the running session's cached hook was 242 lines while
8
+ # the repo's was 268 — a whole day of hook changes were inert.)
9
+ #
10
+ # WHAT DOESN'T NEED THIS. Data is already live: forge-ask-all discovers *.rvf at query time, so a
11
+ # rebuilt KB is searchable immediately, no restart. Only CODE is pinned.
12
+ #
13
+ # THE FIX. Replace the cached hook scripts with a two-line shim that `exec`s the working tree's
14
+ # copy, keeping the original as <name>.frozen.sh. The next prompt runs your edits. Reversible.
15
+ #
16
+ # sh scripts/dev-plugin-link.sh --link # working tree becomes the live hook
17
+ # sh scripts/dev-plugin-link.sh --status # what is linked, what is frozen
18
+ # sh scripts/dev-plugin-link.sh --unlink # restore the pristine cached copies
19
+ #
20
+ # SCOPE + SAFETY
21
+ # · Only touches ~/.claude/plugins/cache/ruvnet-brain/**/scripts/*.sh — never the repo, never
22
+ # settings.json, never a LaunchAgent.
23
+ # · The shim falls back to the frozen copy if the repo is missing, so deleting the repo cannot
24
+ # silently disable grounding. The harness invokes the hook with `|| true`, meaning a broken
25
+ # hook fails SILENTLY — hence the fallback, and hence --status.
26
+ # · Set RUVNET_BRAIN_DEV=0 in the environment to bypass the shim without unlinking.
27
+ # · This is a DEVELOPER convenience for the person who edits the plugin. Users never run it.
28
+ #
29
+ # The MCP server (mcp/) is a long-lived process spawned at session start; its CODE still needs that
30
+ # process restarted. Hooks are re-exec'd per prompt, which is exactly why they can hot-reload.
31
+
32
+ set -eu
33
+
34
+ REPO="$(cd "$(dirname "$0")/.." && pwd)"
35
+ SRC_DIR="$REPO/plugin/scripts"
36
+ CACHE_ROOT="$HOME/.claude/plugins/cache/ruvnet-brain/ruvnet-brain"
37
+ MODE="${1:---status}"
38
+
39
+ [ -d "$SRC_DIR" ] || { echo "dev-plugin-link: no $SRC_DIR"; exit 2; }
40
+ [ -d "$CACHE_ROOT" ] || { echo "dev-plugin-link: plugin not installed at $CACHE_ROOT"; exit 2; }
41
+
42
+ # Every cached version dir — a running session may have pinned any of them, and we do not know which.
43
+ versions() { find "$CACHE_ROOT" -mindepth 1 -maxdepth 1 -type d | sort; }
44
+
45
+ is_shim() { grep -q 'RUVNET_BRAIN_DEV_SHIM' "$1" 2>/dev/null; }
46
+
47
+ write_shim() {
48
+ target="$1"; name="$(basename "$target")"; frozen="$(dirname "$target")/${name%.sh}.frozen.sh"
49
+ # Freeze the pristine copy exactly once; re-linking must never overwrite it with a shim.
50
+ if [ ! -f "$frozen" ]; then cp "$target" "$frozen"; fi
51
+ cat >"$target" <<SHIM
52
+ #!/bin/sh
53
+ # RUVNET_BRAIN_DEV_SHIM — generated by scripts/dev-plugin-link.sh. Do not edit.
54
+ # Delegates to the working tree so hook edits take effect on the NEXT prompt, no restart.
55
+ DEV="$SRC_DIR/$name"
56
+ if [ "\${RUVNET_BRAIN_DEV:-1}" = "1" ] && [ -r "\$DEV" ]; then
57
+ # Point CLAUDE_PLUGIN_ROOT at the repo too, so version/skills resolve from the working tree.
58
+ CLAUDE_PLUGIN_ROOT="$REPO/plugin"; export CLAUDE_PLUGIN_ROOT
59
+ exec /bin/sh "\$DEV" "\$@"
60
+ fi
61
+ exec /bin/sh "$(dirname "$target")/${name%.sh}.frozen.sh" "\$@"
62
+ SHIM
63
+ chmod +x "$target"
64
+ }
65
+
66
+ case "$MODE" in
67
+ --link)
68
+ n=0
69
+ for v in $(versions); do
70
+ [ -d "$v/scripts" ] || continue
71
+ for f in "$v"/scripts/*.sh; do
72
+ [ -f "$f" ] || continue
73
+ case "$(basename "$f")" in *.frozen.sh) continue ;; esac
74
+ [ -f "$SRC_DIR/$(basename "$f")" ] || continue # only shim scripts the repo actually has
75
+ write_shim "$f"; n=$((n + 1))
76
+ done
77
+ done
78
+ echo "dev-plugin-link: linked $n script(s) across $(versions | wc -l | tr -d ' ') cached version(s)"
79
+ echo " live source: $SRC_DIR"
80
+ echo " hook edits now apply on the NEXT prompt — no exit/restart."
81
+ ;;
82
+ --unlink)
83
+ n=0
84
+ for v in $(versions); do
85
+ for f in "$v"/scripts/*.frozen.sh; do
86
+ [ -f "$f" ] || continue
87
+ orig="${f%.frozen.sh}.sh"
88
+ mv "$f" "$orig"; n=$((n + 1))
89
+ done
90
+ done
91
+ echo "dev-plugin-link: restored $n pristine script(s)"
92
+ ;;
93
+ --status)
94
+ for v in $(versions); do
95
+ [ -d "$v/scripts" ] || continue
96
+ shimmed=0; total=0
97
+ for f in "$v"/scripts/*.sh; do
98
+ case "$(basename "$f")" in *.frozen.sh) continue ;; esac
99
+ total=$((total + 1)); is_shim "$f" && shimmed=$((shimmed + 1))
100
+ done
101
+ printf ' %-14s %s/%s shimmed\n' "$(basename "$v")" "$shimmed" "$total"
102
+ done
103
+ ;;
104
+ *) echo "usage: dev-plugin-link.sh [--link|--unlink|--status]"; exit 2 ;;
105
+ esac
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * distill-project.mjs — the ONE genuinely offerable capability, made genuinely reversible.
4
+ *
5
+ * WHY THIS FILE EXISTS. ADR-047 proposed a system for offering dormant capabilities to the user
6
+ * unprompted. Both duelists rejected it, and GPT-5.6-Sol's arithmetic is the reason: the registry has
7
+ * five `turnOn` commands and ZERO verified inverses, so the honest launch surface was **zero**. Its
8
+ * one claimed exception — memory distillation — sourced its undo from `remedy-registry.mjs`'s
9
+ * RESTORE_STORE_BACKUPS, which restores snapshots that `health-repair.mjs --distill-fleet` takes,
10
+ * while the OFFER handed the user raw `ruflo memory distill run`. Verified against `--help`: that
11
+ * command has no backup option at all. **The offered action and the promised inverse were two
12
+ * different execution paths.**
13
+ *
14
+ * An undo promise with no branch behind the action actually offered is this repo's origin sin. So
15
+ * before any delivery system gets built, one capability has to be safe end to end. This is that one.
16
+ *
17
+ * WHAT IT IS NOT. It is not a reimplementation of distillation, and it does not hand-roll a backup.
18
+ * `ruflo memory backup` is rUv's own snapshot and its `--help` states it is "WAL-safe, rotated" —
19
+ * which matters, because `cp` on a live WAL database silently amputates the newest transactions (this
20
+ * project has already lost data that way once). This file is a WRAPPER that sequences rUv's real
21
+ * commands in the order that makes the operation reversible, and refuses to proceed when it cannot.
22
+ *
23
+ * THE SEQUENCE, and every step is there because skipping it breaks a guarantee:
24
+ *
25
+ * 1. status BEFORE — the delta is meaningless without it, and "it worked" is unfalsifiable.
26
+ * 2. WAL-safe snapshot — rUv's `memory backup --db --dir`. This is the undo. No snapshot, no run.
27
+ * 3. receipt, FAIL-CLOSED — written and fsync'd BEFORE the mutation. `health-repair.mjs:185`
28
+ * catches its own receipt-write failure and distills anyway; that is how a mutation happens with
29
+ * no durable record of where its undo lives. Here a receipt that cannot be written ABORTS.
30
+ * 4. distill run --db — project-scoped, `--budget-usd 0` (the default), so nothing paid can fire.
31
+ * 5. status AFTER + delta — verified, and reported as a measurement rather than a verdict.
32
+ * 6. receipt completed — the same file, updated with the outcome and the exact restore command.
33
+ *
34
+ * `--restore <snapshot>` is the inverse, and it is tested. An undo nobody has run is a promise.
35
+ */
36
+ import fs from 'node:fs';
37
+ import os from 'node:os';
38
+ import path from 'node:path';
39
+ import { execFileSync, spawnSync } from 'node:child_process';
40
+
41
+ const HOME = os.homedir();
42
+ const RUFLO = process.env.RUFLO_BIN || path.join(HOME, '.npm-global/bin/ruflo');
43
+ const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
44
+
45
+ const argv = process.argv.slice(2);
46
+ const arg = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
47
+ const has = (f) => argv.includes(f);
48
+
49
+ const PROJECT = path.resolve(arg('--project', process.cwd()));
50
+ const DB = path.resolve(arg('--db', path.join(PROJECT, '.swarm', 'memory.db')));
51
+ const BACKUP_DIR = path.resolve(arg('--backup-dir', path.join(PROJECT, '.swarm', 'backups')));
52
+ const RECEIPTS = path.join(HOME, '.cache', 'ruvnet-brain', 'distill-receipts.jsonl');
53
+ const DRY = has('--dry-run');
54
+
55
+ const say = (s) => process.stdout.write(s + '\n');
56
+ const die = (s, code = 1) => { process.stderr.write(s + '\n'); process.exit(code); };
57
+
58
+ /** Run a ruflo subcommand, returning {ok, out, err}. Never throws — callers decide what a failure means. */
59
+ function ruflo(args, { timeout = 600_000 } = {}) {
60
+ const r = spawnSync(RUFLO, args, { encoding: 'utf8', timeout, env: RUFLO_ENV, shell: process.platform === 'win32' }); // a global npm ruflo is ruflo.cmd on Windows; Node refuses .cmd without a shell (CVE-2024-27980)
61
+ return { ok: !r.error && r.status === 0, out: r.stdout || '', err: (r.stderr || '') + (r.error ? String(r.error.message) : '') };
62
+ }
63
+
64
+ /**
65
+ * Read the distilled-table counts. Parsed from `memory distill status`, and a parse failure is
66
+ * reported as UNKNOWN rather than 0 — a fabricated zero here would turn "we could not read it" into
67
+ * "there was nothing", which is the exact lie this repo's registry rule exists to kill.
68
+ */
69
+ function distillCounts() {
70
+ const r = ruflo(['memory', 'distill', 'status', '--db', DB], { timeout: 120_000 });
71
+ if (!r.ok) return { readable: false, why: r.err.trim().split('\n')[0] || 'distill status failed', raw: r.out };
72
+ const num = (label) => {
73
+ const m = new RegExp(`${label}[^0-9]{0,40}(\\d[\\d,]*)`, 'i').exec(r.out);
74
+ return m ? Number(m[1].replace(/,/g, '')) : null;
75
+ };
76
+ return {
77
+ readable: true,
78
+ patterns: num('reasoning_patterns') ?? num('patterns'),
79
+ episodes: num('episodes'),
80
+ raw: r.out.trim(),
81
+ };
82
+ }
83
+
84
+ /** Append a receipt line, fsync'd. Returns false if it cannot be durably written — the caller ABORTS. */
85
+ function writeReceipt(rec) {
86
+ try {
87
+ fs.mkdirSync(path.dirname(RECEIPTS), { recursive: true });
88
+ const fd = fs.openSync(RECEIPTS, 'a');
89
+ try { fs.writeSync(fd, JSON.stringify(rec) + '\n'); fs.fsyncSync(fd); }
90
+ finally { fs.closeSync(fd); }
91
+ return true;
92
+ } catch { return false; }
93
+ }
94
+
95
+ /** The newest snapshot this project has, or null. Used to tell the user what `--restore` would take. */
96
+ function newestSnapshot() {
97
+ try {
98
+ const files = fs.readdirSync(BACKUP_DIR)
99
+ .filter((f) => /\.(db|sqlite|bak)$/i.test(f) || /memory.*\d/.test(f))
100
+ .map((f) => ({ f, p: path.join(BACKUP_DIR, f), t: fs.statSync(path.join(BACKUP_DIR, f)).mtimeMs }))
101
+ .sort((a, b) => b.t - a.t);
102
+ return files.length ? files[0].p : null;
103
+ } catch { return null; }
104
+ }
105
+
106
+ // ── RESTORE — the inverse. Deliberately first, because an undo you cannot reach is not an undo. ────
107
+ if (has('--restore')) {
108
+ const snap = arg('--restore', null) || newestSnapshot();
109
+ if (!snap) die(`no snapshot to restore from — looked in ${BACKUP_DIR.replace(HOME, '~')}`);
110
+ if (!fs.existsSync(snap)) die(`snapshot not found: ${snap}`);
111
+ if (DRY) { say(`[dry-run] would restore ${snap.replace(HOME, '~')} → ${DB.replace(HOME, '~')}`); process.exit(0); }
112
+
113
+ // Snapshot the CURRENT state before overwriting it. Restoring is itself a mutation, and an undo
114
+ // that destroys the thing it replaces leaves the user with no way back if they restored by mistake.
115
+ const pre = path.join(BACKUP_DIR, `pre-restore-${Date.now()}.db`);
116
+ try { fs.mkdirSync(BACKUP_DIR, { recursive: true }); fs.copyFileSync(DB, pre); }
117
+ catch (e) { die(`refusing to restore — could not snapshot the current DB first (${e.message})`); }
118
+
119
+ try { fs.copyFileSync(snap, DB); }
120
+ catch (e) { die(`restore FAILED (${e.message}). Your pre-restore copy is at ${pre.replace(HOME, '~')}`); }
121
+
122
+ writeReceipt({ at: new Date().toISOString(), action: 'restore', db: DB, from: snap, preRestore: pre });
123
+ say(`restored ${snap.replace(HOME, '~')} → ${DB.replace(HOME, '~')}`);
124
+ say(`the state you replaced is at ${pre.replace(HOME, '~')} if you need it back`);
125
+ process.exit(0);
126
+ }
127
+
128
+ // ── PRE-FLIGHT ─────────────────────────────────────────────────────────────────────────────────────
129
+ if (!fs.existsSync(RUFLO)) die(`ruflo is not at ${RUFLO.replace(HOME, '~')} — install it with \`npm i -g ruflo@latest\``);
130
+ if (!fs.existsSync(DB)) die(`no memory store at ${DB.replace(HOME, '~')} — nothing to distill for this project`);
131
+
132
+ say(`project : ${PROJECT.replace(HOME, '~')}`);
133
+ say(`store : ${DB.replace(HOME, '~')}`);
134
+
135
+ const before = distillCounts();
136
+ if (!before.readable) {
137
+ // Refuse rather than guess. A delta computed from an unreadable baseline is a fabricated number.
138
+ die(`cannot read distill status for this store (${before.why}) — refusing to run, because without a baseline the result would be unverifiable`);
139
+ }
140
+ say(`before : ${before.patterns ?? '?'} patterns, ${before.episodes ?? '?'} episodes`);
141
+
142
+ if (DRY) {
143
+ const r = ruflo(['memory', 'distill', 'run', '--db', DB, '--dry-run']);
144
+ say('');
145
+ say(r.out.trim() || '(no dry-run output)');
146
+ say('');
147
+ say('[dry-run] nothing was written, and no snapshot was taken.');
148
+ process.exit(r.ok ? 0 : 1);
149
+ }
150
+
151
+ // ── 1. SNAPSHOT FIRST. This is the undo; without it the operation is not offerable. ────────────────
152
+ say('snapshot: taking a WAL-safe copy via `ruflo memory backup` before touching anything…');
153
+ const bk = ruflo(['memory', 'backup', '--db', DB, '--dir', BACKUP_DIR]);
154
+ if (!bk.ok) {
155
+ die(`REFUSING TO DISTILL — the snapshot failed (${bk.err.trim().split('\n')[0] || 'unknown error'}).\n`
156
+ + 'This is deliberate: distillation without a verified undo is exactly the unsafe offer this wrapper exists to prevent.');
157
+ }
158
+ const snapshot = newestSnapshot();
159
+ if (!snapshot) die('REFUSING TO DISTILL — `memory backup` reported success but no snapshot file is present, so the undo cannot be located.');
160
+ say(`snapshot: ${snapshot.replace(HOME, '~')}`);
161
+
162
+ // ── 2. RECEIPT, FAIL-CLOSED, BEFORE THE MUTATION. ─────────────────────────────────────────────────
163
+ // health-repair.mjs:185 catches its own receipt-write failure and distills anyway. That is how a
164
+ // mutation happens with no durable record of where its undo lives. Here, no receipt means no run.
165
+ const startedAt = new Date().toISOString();
166
+ if (!writeReceipt({ at: startedAt, action: 'distill:start', db: DB, snapshot, before: { patterns: before.patterns, episodes: before.episodes } })) {
167
+ die(`REFUSING TO DISTILL — could not durably write a receipt to ${RECEIPTS.replace(HOME, '~')}.\n`
168
+ + 'A mutation with no record of its undo is worse than no mutation. Fix the path and re-run.');
169
+ }
170
+
171
+ // ── 3. THE ACTUAL WORK — rUv's command, project-scoped, $0 by default. ────────────────────────────
172
+ say('distill : running `ruflo memory distill run` against this project only…');
173
+ const run = ruflo(['memory', 'distill', 'run', '--db', DB]);
174
+ if (run.out.trim()) say(run.out.trim());
175
+
176
+ // ── 4. VERIFY THE DELTA. A measurement, not a verdict. ────────────────────────────────────────────
177
+ const after = distillCounts();
178
+ const delta = (before.readable && after.readable && before.patterns != null && after.patterns != null)
179
+ ? after.patterns - before.patterns : null;
180
+
181
+ const outcome = {
182
+ at: new Date().toISOString(), action: 'distill:done', db: DB, snapshot, startedAt,
183
+ ok: run.ok,
184
+ before: { patterns: before.patterns, episodes: before.episodes },
185
+ after: after.readable ? { patterns: after.patterns, episodes: after.episodes } : { unreadable: after.why },
186
+ deltaPatterns: delta,
187
+ restore: `node ${path.join(path.dirname(new URL(import.meta.url).pathname))}/distill-project.mjs --restore ${snapshot}`,
188
+ };
189
+ writeReceipt(outcome);
190
+
191
+ say('');
192
+ if (!run.ok) {
193
+ say(`distill did NOT complete cleanly: ${run.err.trim().split('\n')[0] || 'unknown error'}`);
194
+ say(`your store is unchanged or partially changed; restore with:\n ${outcome.restore}`);
195
+ process.exit(1);
196
+ }
197
+ if (delta === null) say('after : counts could not be re-read, so the change is UNVERIFIED — the snapshot above is still your undo.');
198
+ else if (delta > 0) say(`after : ${after.patterns} patterns (+${delta}) — measured, not asserted.`);
199
+ else say(`after : ${after.patterns} patterns (no change). Distillation ran and found nothing new to mine; that is a real answer, not a failure.`);
200
+ say(`undo : ${outcome.restore}`);