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,146 @@
1
+ // gates.mjs — what stands between the model and your machine, and what it has caught.
2
+ //
3
+ // ─────────────────────────────────────────────────────────────────────────────────────────────────
4
+ // WHY (2026-07-17). Stuart, looking at the console's wiring card: "I have no idea what message it's
5
+ // supposed to tell me… it seems to be facts without purpose." The card counted launch sites. Nobody
6
+ // wants a census. The question worth answering is the one the harness exists for: WHAT STOPS CLAUDE
7
+ // FROM BEING WRONG, AND HAS IT EVER ACTUALLY STOPPED IT?
8
+ //
9
+ // Two things had to be true before that card could be honest, and neither was:
10
+ //
11
+ // 1. The console never read the gates. wiringSurvey() walks ~/Code project settings only, so the
12
+ // 12 machine-wide hooks in ~/.claude/settings.json and the 9 in the plugin's own hooks.json —
13
+ // including the design wall that blocks commits — were invisible to the page bragging about them.
14
+ //
15
+ // 2. The gates never recorded a block. Only successes were logged, so "the harness caught Claude"
16
+ // had no receipt. gate-receipt.sh now writes one at the moment of refusal.
17
+ //
18
+ // THE HONEST DISTINCTION this module exists to draw: a hook wired with `|| true` CANNOT block —
19
+ // it injects context and the tool call proceeds regardless. Only a gate that can exit non-zero
20
+ // stops anything. Counting all hooks as "protection" would be the same inflation as counting
21
+ // cloned upstream repos as your own wiring. Advisory and blocking are different claims.
22
+ //
23
+ // Reads only. Never asserts a count it cannot source from a file on this machine.
24
+ // ─────────────────────────────────────────────────────────────────────────────────────────────────
25
+
26
+ import fs from 'node:fs';
27
+ import os from 'node:os';
28
+ import path from 'node:path';
29
+
30
+ const HOME = os.homedir();
31
+ const BLOCKS = path.join(HOME, '.cache/ruvnet-brain/gate-blocks.jsonl');
32
+
33
+ function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } }
34
+
35
+ // Two independent things must BOTH be true for a hook to stop anything, and conflating them is how
36
+ // a census gets sold as protection:
37
+ // 1. The EVENT must be one that runs before the thing it would stop. PreToolUse gates a tool call;
38
+ // UserPromptSubmit gates a prompt. SessionStart has nothing to refuse yet, and PostToolUse /
39
+ // PreCompact / SessionEnd arrive after the fact — those inject or record, they never block.
40
+ // 2. The COMMAND must not end in `|| true`, which swallows the exit code the gate would refuse with.
41
+ const BLOCKING_EVENTS = new Set(['PreToolUse', 'UserPromptSubmit']);
42
+ const swallowsExit = (cmd) => /\|\|\s*true\s*$/.test(String(cmd || '').trim());
43
+ const canBlock = (event, cmd) => BLOCKING_EVENTS.has(event) && !swallowsExit(cmd);
44
+
45
+ const NAME = (cmd) => {
46
+ const m = String(cmd || '').match(/([\w-]+)\.(sh|mjs|js)/);
47
+ return m ? m[1] : String(cmd || '').split(/\s+/).filter((t) => !t.startsWith('-')).pop()?.slice(0, 28) || 'hook';
48
+ };
49
+
50
+ function collect(hooksObj, source) {
51
+ const out = [];
52
+ for (const [event, groups] of Object.entries(hooksObj || {})) {
53
+ const list = Array.isArray(groups) ? groups : [groups];
54
+ for (const g of list) {
55
+ const hooks = Array.isArray(g?.hooks) ? g.hooks : (g?.command ? [g] : []);
56
+ for (const h of hooks) {
57
+ if (!h?.command) continue;
58
+ out.push({ event, matcher: g?.matcher ?? '*', name: NAME(h.command), blocking: canBlock(event, h.command), source });
59
+ }
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ // Every catch the gates have recorded. This file only exists once a gate has actually refused
66
+ // something — an empty ledger is an honest "nothing caught yet", never a failure.
67
+ export function gateBlocks() {
68
+ try {
69
+ return fs.readFileSync(BLOCKS, 'utf8').trim().split('\n')
70
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
71
+ .filter(Boolean);
72
+ } catch { return []; }
73
+ }
74
+
75
+ export function gatesSurvey({ repo } = {}) {
76
+ const machine = collect(readJSON(path.join(HOME, '.claude/settings.json'))?.hooks, 'machine');
77
+ const pluginCfg = repo ? readJSON(path.join(repo, 'plugin/hooks/hooks.json')) : null;
78
+ const plugin = collect(pluginCfg?.hooks || pluginCfg, 'plugin');
79
+
80
+ const all = [...machine, ...plugin];
81
+ const blocking = all.filter((g) => g.blocking);
82
+
83
+ // THE LEDGER IS MACHINE-WIDE; THIS SURVEY IS ABOUT ONE PROJECT. Every catch ever recorded on the
84
+ // machine used to be counted here, so standing in an empty folder produced "203 refusals have been
85
+ // recorded" — this repo's history, attributed to a project that has never run a gate. Each record
86
+ // carries the `cwd` it was caught in, so when a project is named, only its own catches count.
87
+ //
88
+ // KNOWN LIMIT, stated rather than hidden: `cwd` is a basename, so two projects sharing a folder
89
+ // name share a count. That is a real ambiguity and it is narrow; attributing the whole machine's
90
+ // history to whichever directory you happen to be standing in was neither.
91
+ const here = repo ? path.basename(path.resolve(repo)) : null;
92
+ const allBlocks = gateBlocks();
93
+ const blocks = here ? allBlocks.filter((b) => b.cwd === here) : allBlocks;
94
+
95
+ // Same gate, same event, wired both machine-wide AND by the plugin — it runs twice on every
96
+ // matching call. Harmless to correctness (these gates are idempotent) but it is real duplicated
97
+ // work, and counting it as two protections would inflate the only number on the card that matters.
98
+ const seen = new Map();
99
+ const duplicated = [];
100
+ for (const g of all) {
101
+ const k = `${g.event}:${g.name}`;
102
+ if (seen.has(k) && seen.get(k) !== g.source) { if (!duplicated.includes(g.name)) duplicated.push(g.name); }
103
+ seen.set(k, g.source);
104
+ }
105
+ const uniqueBlocking = new Set(blocking.map((g) => `${g.event}:${g.name}`)).size;
106
+
107
+ // Group the catches by gate so the card can say WHAT was caught, not just how many times.
108
+ const byGate = {};
109
+ for (const b of blocks) (byGate[b.gate] ||= []).push(b);
110
+
111
+ const weekAgo = Date.now() - 7 * 864e5;
112
+ const recent = blocks.filter((b) => Date.parse(b.at || 0) >= weekAgo);
113
+
114
+ // THREE NUMBERS, ONE UNIT. This block used to mix two: `blocking` was DEDUPLICATED
115
+ // (distinct event:name) while `advisory` was `all.length - blocking.length`, computed from the
116
+ // RAW array. So `blocking + advisory` fell short of `armed` by exactly the number of duplicated
117
+ // blocking wirings, and the console printed "N gates armed — B can stop a call. The other A add
118
+ // context" where B + A ≠ N. On a machine with 4 duplicate blocking gates the sentence silently
119
+ // lost four of them — in the one sentence whose entire job is to account for all of them.
120
+ //
121
+ // It summed correctly on any machine with no duplicates, which is why it survived: the defect was
122
+ // invisible exactly where it was most often looked at. Found by Fable 5, 2026-07-24, by adding up
123
+ // the numbers on the owner's own console.
124
+ //
125
+ // Fixed by reporting all three in WIRED-ENTRY units, so armed = blocking + advisory holds by
126
+ // construction. The distinct-gate count is still exported — it is genuinely the more meaningful
127
+ // number for "how many different things can refuse" — but under its own name, where it cannot be
128
+ // mistaken for a term in that sum.
129
+ const blockingWired = blocking.length;
130
+ return {
131
+ summary: {
132
+ armed: all.length,
133
+ blocking: blockingWired, // wired entries that can refuse — same unit as `armed`
134
+ advisory: all.length - blockingWired,
135
+ blockingDistinct: uniqueBlocking, // distinct gates that can refuse; ≤ blocking when wired twice
136
+ duplicated, // wired twice; runs twice
137
+ caughtTotal: blocks.length,
138
+ caughtThisWeek: recent.length,
139
+ everRecorded: blocks.length > 0,
140
+ },
141
+ gates: all.sort((a, b) => Number(b.blocking) - Number(a.blocking)),
142
+ // Newest first — the most recent catch is the one worth reading.
143
+ catches: blocks.slice(-12).reverse(),
144
+ byGate: Object.fromEntries(Object.entries(byGate).map(([k, v]) => [k, v.length])),
145
+ };
146
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // gen-console-images.mjs — imagery for the Onboarding Console. Same proven path as gen-images.mjs
3
+ // (OpenAI gpt-image-1, fallback dall-e-3), same brand style, output into console/assets/.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const OUT = path.join(ROOT, 'console/assets');
10
+ fs.mkdirSync(OUT, { recursive: true });
11
+
12
+ // Load the key the same way gen-images.mjs does, but tolerate a few env-file locations.
13
+ function loadKey() {
14
+ if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY;
15
+ const candidates = [process.env.RUVNET_ENV_FILE, path.join(ROOT, '.env'), path.join(process.env.HOME, 'Code/Ask-Ruvnet/.env')].filter(Boolean);
16
+ for (const f of candidates) { try { const m = fs.readFileSync(f, 'utf8').match(/^OPENAI_API_KEY=(.+)$/m); if (m) return m[1]; } catch { /* next */ } }
17
+ return '';
18
+ }
19
+ const KEY = (loadKey().match(/sk-[A-Za-z0-9_\-]+/) || [''])[0];
20
+ if (!KEY) { console.error('no OPENAI_API_KEY (env or .env)'); process.exit(2); }
21
+ const redact = (s) => String(s).replace(/sk-[A-Za-z0-9_\-]+/g, 'sk-***');
22
+
23
+ const STYLE = ' — Style: deep near-black background (#0a0c10), sophisticated premium editorial-tech aesthetic, restrained warm amber (#f0a830) and warm gold (#ffce6a) glow with cool cyan (#5ad6ff) and calm green (#5fd38a) accents only, cinematic soft volumetric light, generous negative space, refined, calming, screenshot-worthy, high craft. Absolutely NO text, NO words, NO letters, NO numbers, NO UI chrome, NO logos, NO circuit-board cliché.';
24
+
25
+ const IMAGES = [
26
+ { slug: 'hero', size: '1536x1024', p: 'A warm amber intelligence gently understanding a computer: soft glowing amber and gold neural filaments and threads of light weaving and resolving out of a faint tangle on the left into an elegant, orderly, translucent crystalline lattice of floating glass panels and cards on the right — the feeling of messy machine settings being calmly brought into clear, beautiful order. Lots of soft dark negative space on the right for text.' },
27
+ { slug: 'memory', size: '1024x1024', p: 'A single luminous softly-glowing sphere of warm amber and cyan light, made of countless fine interwoven filaments, holding its shape calmly in dark space — an abstract emblem of a mind that remembers; serene, alive, precise.' },
28
+ ];
29
+
30
+ async function gen(model, prompt, size) {
31
+ const body = model === 'gpt-image-1'
32
+ ? { model, prompt, size, quality: 'high', n: 1 }
33
+ : { model, prompt, size: size === '1536x1024' ? '1792x1024' : '1024x1024', response_format: 'b64_json', n: 1 };
34
+ const r = await fetch('https://api.openai.com/v1/images/generations', {
35
+ method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body),
36
+ });
37
+ if (!r.ok) throw new Error(`${model} HTTP ${r.status}: ${(await r.text()).slice(0, 160)}`);
38
+ return (await r.json()).data[0].b64_json;
39
+ }
40
+
41
+ const only = process.argv.slice(2).filter((a) => !a.startsWith('-'));
42
+ for (const im of IMAGES) {
43
+ if (only.length && !only.includes(im.slug)) continue;
44
+ const prompt = im.p + STYLE;
45
+ let b64;
46
+ try { b64 = await gen('gpt-image-1', prompt, im.size); console.log(`✓ ${im.slug} (gpt-image-1)`); }
47
+ catch (e) {
48
+ console.log(` gpt-image-1 failed for ${im.slug}: ${redact(e.message)} — trying dall-e-3`);
49
+ try { b64 = await gen('dall-e-3', prompt, im.size); console.log(`✓ ${im.slug} (dall-e-3)`); }
50
+ catch (e2) { console.error(`✗ ${im.slug}: ${redact(e2.message)}`); continue; }
51
+ }
52
+ fs.writeFileSync(path.join(OUT, `${im.slug}.png`), Buffer.from(b64, 'base64'));
53
+ }
54
+ console.log('done →', OUT);
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // gen-images.mjs — generate a cohesive set of explainer images (OpenAI gpt-image-1, fallback dall-e-3).
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
+ const OUT = path.join(ROOT, 'explainer/assets/img');
8
+ fs.mkdirSync(OUT, { recursive: true });
9
+ const RAWKEY = process.env.OPENAI_API_KEY || (fs.readFileSync((process.env.RUVNET_ENV_FILE || '.env'), 'utf8').match(/^OPENAI_API_KEY=(.+)$/m) || [])[1] || '';
10
+ const KEY = (RAWKEY.match(/sk-[A-Za-z0-9_\-]+/) || [''])[0]; // first valid token only — handles dup lines / whitespace
11
+ if (!KEY) { console.error('no OPENAI_API_KEY'); process.exit(2); }
12
+ const redact = (s) => String(s).replace(/sk-[A-Za-z0-9_\-]+/g, 'sk-***'); // never let a key reach logs
13
+
14
+ const STYLE = ' — Style: dark near-black background (#0b0d0f) with a faint blueprint grid, sophisticated premium editorial-tech aesthetic, restrained warm amber (#f0a830) and cool cyan (#5ad6ff) accent light only, cinematic volumetric glow, fine detail, high craft, elegant and confident. Absolutely NO text, NO words, NO letters, NO numbers, NO UI chrome, NO logos.';
15
+
16
+ const IMAGES = [
17
+ { slug: 'hero', size: '1536x1024', p: 'A luminous intricate three-dimensional structure resembling a brain fused with a vast interconnected codebase: thousands of glowing amber and cyan filaments forming one elegant organized sphere of intelligence, floating in dark space, a sense of all knowledge made orderly and alive.' },
18
+ { slug: 'problem-skim', size: '1536x1024', p: 'A vast deep canyon made of densely stacked layers of code and documents descending far into darkness; a single small fragile light hovers at the very top only grazing the surface, never reaching the immense depth below. The feeling of skimming and missing everything underneath.' },
19
+ { slug: 'point-deeper', size: '1536x1024', p: 'One precise clean beam of warm amber light cutting straight down through many deep translucent strata of a vast structure to perfectly illuminate a single exact point far below; surgical precision locating the one true answer in the depths.' },
20
+ { slug: 'architecture', size: '1536x1024', p: 'An elegant isometric exploded view of five translucent glass layers floating one above another in dark space, each a slightly different luminous tone, joined by thin vertical conduits of light; a refined premium product render of a clean layered system.' },
21
+ { slug: 'proof', size: '1536x1024', p: 'Three distinct elegant luminous measuring instruments aim converging beams of light onto a single crystalline object at center that glows confident green, while one beam exposes a hidden flaw glowing warning red; independent rigorous verification against a single source of truth.' },
22
+ ];
23
+
24
+ async function gen(model, prompt, size) {
25
+ const body = model === 'gpt-image-1'
26
+ ? { model, prompt, size, quality: 'high', n: 1 }
27
+ : { model, prompt, size: size === '1536x1024' ? '1792x1024' : '1024x1024', response_format: 'b64_json', n: 1 };
28
+ const r = await fetch('https://api.openai.com/v1/images/generations', {
29
+ method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body),
30
+ });
31
+ if (!r.ok) throw new Error(`${model} HTTP ${r.status}: ${(await r.text()).slice(0, 160)}`);
32
+ const j = await r.json();
33
+ return j.data[0].b64_json;
34
+ }
35
+
36
+ for (const im of IMAGES) {
37
+ const prompt = im.p + STYLE;
38
+ let b64;
39
+ try { b64 = await gen('gpt-image-1', prompt, im.size); console.log(`✓ ${im.slug} (gpt-image-1)`); }
40
+ catch (e) {
41
+ console.log(` gpt-image-1 failed for ${im.slug}: ${redact(e.message)} — trying dall-e-3`);
42
+ try { b64 = await gen('dall-e-3', prompt, im.size); console.log(`✓ ${im.slug} (dall-e-3)`); }
43
+ catch (e2) { console.error(`✗ ${im.slug}: ${redact(e2.message)}`); continue; }
44
+ }
45
+ fs.writeFileSync(path.join(OUT, `${im.slug}.png`), Buffer.from(b64, 'base64'));
46
+ }
47
+ console.log('done →', OUT);
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ function submodulePaths(repoDir) {
6
+ const gitmodules = path.join(repoDir, '.gitmodules');
7
+ if (!fs.existsSync(gitmodules)) return [];
8
+ let output = '';
9
+ try {
10
+ output = execFileSync(
11
+ 'git',
12
+ ['config', '-f', gitmodules, '--get-regexp', '^submodule\\..*\\.path$'],
13
+ { encoding: 'utf8' },
14
+ );
15
+ } catch (error) {
16
+ if (error.status !== 1) throw error;
17
+ }
18
+ return output.trim().split('\n').filter(Boolean).map((line) => line.replace(/^\S+\s+/, ''));
19
+ }
20
+
21
+ export function withSubmoduleSymlinksDetached(repoDir, operation) {
22
+ const detached = [];
23
+ for (const relativePath of submodulePaths(repoDir)) {
24
+ const absolutePath = path.join(repoDir, relativePath);
25
+ let stat;
26
+ try {
27
+ stat = fs.lstatSync(absolutePath);
28
+ } catch (error) {
29
+ if (error.code === 'ENOENT') continue;
30
+ throw error;
31
+ }
32
+ if (!stat.isSymbolicLink()) continue;
33
+ detached.push({ absolutePath, target: fs.readlinkSync(absolutePath) });
34
+ fs.unlinkSync(absolutePath);
35
+ }
36
+
37
+ try {
38
+ return operation();
39
+ } finally {
40
+ for (const { absolutePath, target } of detached) {
41
+ if (fs.existsSync(absolutePath)) {
42
+ const stat = fs.lstatSync(absolutePath);
43
+ if (!stat.isDirectory() || fs.readdirSync(absolutePath).length > 0) {
44
+ throw new Error(`cannot restore submodule symlink over non-empty path: ${absolutePath}`);
45
+ }
46
+ fs.rmdirSync(absolutePath);
47
+ }
48
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
49
+ fs.symlinkSync(target, absolutePath);
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,126 @@
1
+ #!/bin/sh
2
+ # RuvNet Brain — pre-push gate. NON-OPTIONAL. Runs on every `git push`.
3
+ #
4
+ # WHY (2026-07-17): a real client (Jan) hit npx ruvnet-brain@latest with a stale package whose
5
+ # self-update manifest 404'd. The root cause wasn't the bug — it was that the CHECK was optional
6
+ # and I skipped it. This hook removes the discretion: it refuses the push if the version surfaces
7
+ # disagree or the self-update manifest 404s. It does not care whether I remembered, or was in a
8
+ # hurry, or "was pretty sure it was fine." Pass or the push does not happen.
9
+ #
10
+ # It blocks ONLY on what is knowable before a push. npm-latest and the live explainer are verified
11
+ # post-publish by `node scripts/release.mjs --publish` (the only sanctioned ship path).
12
+ #
13
+ # Escape hatch is `git push --no-verify` — but if you use it, you are consciously overriding a gate
14
+ # that exists because of a specific failure that cost real user trust. Own that out loud.
15
+ #
16
+ # SECRET SCAN (2026-07-21): a buggy diagnostic echoed live OPENAI and OPENROUTER keys into a
17
+ # terminal, and AgentDB's session autocapture wrote them to .swarm/. Nothing reached git — but the
18
+ # only thing that stopped it was a GLOBAL gitignore that does not travel to a clone or another
19
+ # machine. Relying on "the file happened to be ignored" is not a control. This is: a real key
20
+ # pattern in anything being pushed refuses the push. Placeholders (your-key, abc123, xxxx,
21
+ # deadbeef, example) are excluded, because rUv's own docs are full of them and a gate that cries
22
+ # wolf on documentation is a gate people learn to bypass.
23
+ #
24
+ # SCOPE FIX (2026-07-22): this scanned `git diff --cached HEAD` — the STAGED INDEX. But a push ships
25
+ # COMMITS, and at push time the index is normally clean, so the scan read nothing. Proven both ways
26
+ # in a scratch repo: a key COMMITTED then pushed sailed through, while the same key merely STAGED was
27
+ # blocked. The gate could only catch the one case that never happens. It is the same defect class the
28
+ # 7e715bb commit fixed in the *test* — a check that cannot fail in the situation it was written for —
29
+ # which survived here in the *scan*. Now it reads the actual push range from the hook's stdin.
30
+ RANGE_DIFF=""
31
+ CURRENCY_BASE=""
32
+ while read -r _local_ref local_sha _remote_ref remote_sha; do
33
+ [ -z "$local_sha" ] && continue
34
+ case "$local_sha" in *[!0]*) ;; *) continue ;; esac # all-zeros = branch deletion, nothing to scan
35
+ case "$remote_sha" in
36
+ ""|*[!0-9a-f]*|0000000000000000000000000000000000000000) # new branch: scan its whole history vs other refs
37
+ RANGE_DIFF="$RANGE_DIFF$(git diff --no-color -U0 "$(git merge-base HEAD origin/main 2>/dev/null || echo '')" "$local_sha" 2>/dev/null)"
38
+ # ADR-056 §5: a new branch has no remote counterpart, so the merge-base with main is the only
39
+ # honest base. If even that cannot resolve, CURRENCY_BASE stays empty and the currency check is
40
+ # SKIPPED rather than run against a guess — never a scope invented from nothing.
41
+ [ -z "$CURRENCY_BASE" ] && CURRENCY_BASE="$(git merge-base HEAD origin/main 2>/dev/null || true)" ;;
42
+ *)
43
+ RANGE_DIFF="$RANGE_DIFF$(git diff --no-color -U0 "$remote_sha" "$local_sha" 2>/dev/null)"
44
+ # ADR-056: the remote tip is only a valid "what did I change" base when it is an ANCESTOR of
45
+ # what we are pushing. After a rebase or squash it is NOT — the range then spans the entire
46
+ # rewrite and drags in every document the upstream branch already carried, red ones included.
47
+ # Measured 2026-07-27: a force-push after rebasing onto main was refused for ADR-054 and
48
+ # ADR-055 — two documents on main that this branch never touched. GPT-5.6 predicted exactly
49
+ # this in the ADR-056 duel ("rebase or squash changes the touched-file set without changing
50
+ # whether a violation is genuinely new"). So: ancestor => use it; otherwise fall back to the
51
+ # merge-base with main, which is the honest answer to "what is mine".
52
+ if [ -z "$CURRENCY_BASE" ]; then
53
+ if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
54
+ CURRENCY_BASE="$remote_sha"
55
+ else
56
+ CURRENCY_BASE="$(git merge-base "$local_sha" origin/main 2>/dev/null || true)"
57
+ fi
58
+ fi ;;
59
+ esac
60
+ done
61
+ # Fall back to the staged index too, so a pre-commit-style invocation still gets covered.
62
+ RANGE_DIFF="$RANGE_DIFF
63
+ $(git diff --cached --no-color -U0 HEAD 2>/dev/null)"
64
+
65
+ SECRET_HITS=$(printf '%s' "$RANGE_DIFF" | grep -E "^\+" \
66
+ | grep -oE "sk-(proj|ant|or-v1)-[A-Za-z0-9_-]{20,}" \
67
+ | grep -vEi "your-key|abc123|xxxx+|deadbeef|example|placeholder|REDACTED" | head -3)
68
+ if [ -n "$SECRET_HITS" ]; then
69
+ echo ""
70
+ echo " ⛔ PUSH BLOCKED — what looks like a REAL API key is in this push."
71
+ echo " matched (prefix only): $(echo "$SECRET_HITS" | cut -c1-14 | tr '\n' ' ')"
72
+ echo " Remove it, rotate the key, and push again. Never --no-verify past this one:"
73
+ echo " a key that reaches a remote is compromised the moment it lands."
74
+ echo ""
75
+ exit 1
76
+ fi
77
+
78
+ # Hooks may be shared by several git worktrees. Resolving from this script's own path is wrong when
79
+ # core.hooksPath names another checkout: the hook then validates that checkout while pushing the
80
+ # current one (measured 2026-07-28: announced 3.9.128, pushed 3.9.129). Git already knows the
81
+ # worktree whose refs are being pushed, so derive the shipping root from that live repository.
82
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
83
+ if [ -z "$ROOT" ] || [ ! -f "$ROOT/scripts/verify-channels.mjs" ]; then
84
+ echo " ⛔ pre-push gate cannot resolve the worktree being pushed."
85
+ exit 1
86
+ fi
87
+ node "$ROOT/scripts/verify-channels.mjs" --pre-push
88
+ status=$?
89
+ if [ "$status" -ne 0 ]; then
90
+ echo ""
91
+ echo " ⛔ pre-push gate refused this push. A user on some path would get a broken build."
92
+ echo " Fix the channel(s) above, then push again. Do not route around this."
93
+ exit 1
94
+ fi
95
+
96
+ # ── DOCUMENT CURRENCY (ADR-056 §5) ─────────────────────────────────────────────────────────────────
97
+ # The tool was written and tested on 2026-07-22 and then invoked by NOTHING for five days — absent
98
+ # from this hook, gate.sh, gates.mjs and every workflow, while `wired-check` reported it "wired"
99
+ # because package.json DEFINED an npm alias for it. This line is the whole fix.
100
+ #
101
+ # NO RATCHET, DELIBERATELY. The first draft of ADR-056 proposed a stateful new-vs-pre-existing
102
+ # violation tracker so this gate could coexist with 32 pre-existing BLOCKs forever. Two independent
103
+ # adversarial reviews killed it: a committed baseline is gamed by the same commit that adds the
104
+ # violation, re-deriving at the remote SHA needs a second worktree, and "report the rest forever"
105
+ # is the wolf-cry doc-currency.mjs's own header names as its designed failure mode. The debt was
106
+ # PAID instead (32 BLOCKs -> 4, all four genuine re-reads), which makes the plain gate safe.
107
+ #
108
+ # SCOPED to what this push touches. A pre-existing red document must not block someone who merely
109
+ # walked past it — that teaches people never to touch the red documents, preserving exactly the drift
110
+ # this exists to end. Scope is empty => nothing to check => pass; never a scope invented from nothing.
111
+ if [ -n "$CURRENCY_BASE" ]; then
112
+ node "$ROOT/scripts/doc-currency.mjs" --check --changed "$CURRENCY_BASE"
113
+ dc_status=$?
114
+ if [ "$dc_status" -ne 0 ]; then
115
+ echo ""
116
+ echo " ⛔ pre-push gate refused this push — a document you touched disagrees with its code."
117
+ echo " An ADR that describes a world the code left is worse than no ADR."
118
+ echo " \`node scripts/doc-currency.mjs --fix\` backfills every date git can prove;"
119
+ echo " everything else is a claim only you can make."
120
+ exit 1
121
+ fi
122
+ fi
123
+ echo ""
124
+ echo " ✓ pre-push gate passed — version, channels, secrets, and document currency are clean."
125
+ echo ""
126
+ exit 0