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,184 @@
1
+ #!/usr/bin/env node
2
+ // scripts/verify-channels.mjs — the release-channel protocol. WALK EVERY PATH A USER WALKS.
3
+ //
4
+ // WHY (2026-07-17, Stuart, after a real client hit a broken install): "whenever you push it, you are
5
+ // verifying all the aspects — the explainer page, the GitHub repo, the npm, the npx — all are working
6
+ // flawlessly. Having people find out the experience is lousy because you didn't track it is 100%
7
+ // unacceptable." He is right. The root failure was assuming the repo state equals the user experience
8
+ // (see AgentDB lesson: YOUR ASSUMPTIONS = FAILURE). CI green != it works; committed != shipped;
9
+ // pushed != the npm package is current.
10
+ //
11
+ // This script asserts, against the LIVE endpoints (never the repo, never a guess), that every channel
12
+ // is current AND reachable. Exit 0 = all channels good. Exit 1 = at least one is broken/stale — and
13
+ // the word "shipped" MUST NOT be uttered until this passes.
14
+ //
15
+ // Usage:
16
+ // node scripts/verify-channels.mjs # check all channels against the local plugin version
17
+ // node scripts/verify-channels.mjs --json # machine-readable
18
+ //
19
+ // Checks (each hits the real thing):
20
+ // 1. npm registry — `ruvnet-brain` latest dist-tag == the shipping plugin version (not stale)
21
+ // 2. self-update path — the bundle's canonicalManifestUrl actually resolves (no 404 — Jan's bug)
22
+ // 3. release bundle — releases/latest/download/ruvnet-brain.zip is downloadable (HTTP 200)
23
+ // 4. release signature— the .sig asset exists next to the bundle (auto-apply needs it)
24
+ // 5. explainer (live) — isovision.ai serves the current major.minor AND the update one-liner
25
+ // 6. git remote — local HEAD is pushed to origin/main (no unpushed shipped code)
26
+
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import os from 'node:os';
30
+ import { fileURLToPath } from 'node:url';
31
+ import { execFileSync } from 'node:child_process';
32
+
33
+ const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
34
+ const JSON_OUT = process.argv.includes('--json');
35
+ const PRE_PUSH = process.argv.includes('--pre-push'); // git pre-push hook mode: block only on what is KNOWABLE before the push
36
+ const REPO = 'stuinfla/ruvnet-brain';
37
+ const NPM_PKG = 'ruvnet-brain';
38
+ const EXPLAINER = 'https://isovision.ai/ruvnet-brain/';
39
+
40
+ const c = { g: (s) => `\x1b[32m${s}\x1b[0m`, r: (s) => `\x1b[31m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, b: (s) => `\x1b[1m${s}\x1b[0m` };
41
+ const pluginVersion = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'plugin/.claude-plugin/plugin.json'), 'utf8')).version;
42
+ const mm = (v) => (String(v).match(/^(\d+\.\d+)/) || [, '?'])[1];
43
+
44
+ async function head(url, timeout = 20000) {
45
+ const ctl = AbortSignal.timeout(timeout);
46
+ try { const r = await fetch(url, { method: 'GET', redirect: 'follow', signal: ctl, headers: { 'user-agent': 'ruvnet-brain-verify' } }); return { ok: r.ok, status: r.status, r }; }
47
+ catch (e) { return { ok: false, status: 0, error: e.message }; }
48
+ }
49
+
50
+ const results = [];
51
+ const check = (name, pass, detail) => { results.push({ name, pass: !!pass, detail }); };
52
+
53
+ async function run() {
54
+ const V = pluginVersion();
55
+
56
+ // ── PRE-PUSH MODE ──────────────────────────────────────────────────────────
57
+ // Runs from the git pre-push hook. It can ONLY block on things knowable BEFORE the push —
58
+ // i.e. what I am about to ship. npm-latest and the live explainer are DOWNSTREAM of a push
59
+ // (publish + Vercel deploy happen after), so blocking on them here would refuse every honest
60
+ // push. The two things that stranded Jan and are 100% knowable now: (a) the version surfaces
61
+ // disagree, (b) the self-update manifest in the SOURCE.json I'm shipping 404s. Block on those.
62
+ if (PRE_PUSH) {
63
+ // a) version single-source-of-truth
64
+ const sv = execFileSync(process.execPath, [path.join(ROOT, 'scripts/sync-version.mjs'), '--check'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
65
+ check('version surfaces agree', /agree|✓/i.test(sv), sv.trim().split('\n').pop());
66
+ // b) the self-update manifest URL in the bundle SOURCE.json resolves (the exact 404 that broke --update)
67
+ let manifestUrl = null;
68
+ try { const sj = JSON.parse(fs.readFileSync(path.join(ROOT, 'kb/SOURCE.json'), 'utf8'));
69
+ const find = (o) => { if (!o || typeof o !== 'object') return null; if (o.canonicalManifestUrl) return o.canonicalManifestUrl; for (const k of Object.keys(o)) { const r = find(o[k]); if (r) return r; } return null; };
70
+ manifestUrl = find(sj); } catch { /* */ }
71
+ if (!manifestUrl) check('self-update manifest URL present', false, 'no canonicalManifestUrl in kb/SOURCE.json');
72
+ else { const h = await head(manifestUrl); check('self-update manifest resolves (no 404)', h.ok, `${manifestUrl} → HTTP ${h.status}${h.ok ? '' : ' ← would strand every --update user, exactly like Jan'}`); }
73
+
74
+ const failed = results.filter((r) => !r.pass);
75
+ console.log(`\n ${c.b('pre-push gate')} ${c.dim('· shipping ' + V + ' · (npm/explainer are verified post-publish by `release.mjs --publish`)')}`);
76
+ for (const r of results) console.log(` ${r.pass ? c.g('✓') : c.r('✗')} ${r.name} ${c.dim(r.detail || '')}`);
77
+ if (failed.length) { console.log(`\n ${c.r('✗ pre-push gate FAILED — push refused. Fix the above; do not --no-verify around it.')}\n`); process.exit(1); }
78
+ console.log(`\n ${c.g('✓ version/channel checks passed — document currency runs next.')}\n`);
79
+ process.exit(0);
80
+ }
81
+
82
+ // 1) npm registry latest == shipping version. npm's read replicas lag a few seconds behind a fresh
83
+ // publish, so a single immediate `npm view` right after `release.mjs` publishes can still return the
84
+ // PRIOR version — a false STALE that fails an otherwise-perfect ship (hit exactly this 2026-07-18:
85
+ // publish succeeded, `+ ruvnet-brain@3.4.9-dev`, yet the immediate view read 3.4.8-dev). Retry with a
86
+ // short backoff so propagation lag is absorbed, while a publish that genuinely never landed still
87
+ // fails after the window. A gate that false-fails teaches you to distrust the gate — the worst outcome.
88
+ {
89
+ let latest = '', ok = false, lastErr = '';
90
+ const ATTEMPTS = 6; // up to ~6×5s = 30s of propagation tolerance
91
+ for (let i = 0; i < ATTEMPTS; i++) {
92
+ try {
93
+ latest = execFileSync('npm', ['view', NPM_PKG, 'dist-tags.latest'], { encoding: 'utf8', timeout: 30000 }).trim();
94
+ if (latest === V) { ok = true; break; }
95
+ } catch (e) { lastErr = e.message.split('\n')[0]; }
96
+ if (i < ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 5000));
97
+ }
98
+ check('npm latest matches shipping version', ok,
99
+ ok ? `npm latest=${latest} · shipping=${V}`
100
+ : `npm latest=${latest || '(view failed: ' + lastErr + ')'} · shipping=${V}${latest && latest !== V ? ' ← STILL STALE after ~' + (ATTEMPTS * 5) + 's: run `npm publish` + `npm dist-tag add ' + NPM_PKG + '@' + V + ' latest`' : ''}`);
101
+ }
102
+
103
+ // 2) the self-update manifest URL actually resolves (the 404 that stranded users)
104
+ let manifestUrl = null;
105
+ try {
106
+ const sj = JSON.parse(fs.readFileSync(path.join(ROOT, 'kb/SOURCE.json'), 'utf8'));
107
+ const find = (o) => { if (!o || typeof o !== 'object') return null; if (o.canonicalManifestUrl) return o.canonicalManifestUrl; for (const k of Object.keys(o)) { const r = find(o[k]); if (r) return r; } return null; };
108
+ manifestUrl = find(sj);
109
+ } catch { /* */ }
110
+ if (!manifestUrl) check('self-update manifest URL present', false, 'no canonicalManifestUrl in kb/SOURCE.json');
111
+ else { const h = await head(manifestUrl); check('self-update manifest resolves (no 404)', h.ok, `${manifestUrl} → HTTP ${h.status}${h.ok ? '' : ' ← the exact 404 that broke --update'}`); }
112
+
113
+ // 3) release bundle downloadable
114
+ const bundleUrl = `https://github.com/${REPO}/releases/latest/download/ruvnet-brain.zip`;
115
+ { const h = await head(bundleUrl); check('release bundle downloads', h.ok, `releases/latest/…/ruvnet-brain.zip → HTTP ${h.status}`); }
116
+
117
+ // 3b) …AND IT IS THE VERSION WE ARE SHIPPING. Reachability is not currency.
118
+ //
119
+ // MEASURED 2026-07-24, and this gate reported SHIPPED while it was false. npm was moved to
120
+ // 3.9.50-dev by `release.mjs --publish`; the GitHub Release stayed at v3.9.18-dev from 2026-07-23,
121
+ // THIRTY-TWO versions behind. Every check above passed — the manifest resolved, the bundle
122
+ // downloaded, the signature was present — because they all ask "does the latest release respond?"
123
+ // and none asked "is the latest release the one we just built?" A 32-version-old bundle answers
124
+ // HTTP 200 perfectly well.
125
+ //
126
+ // WHY THE GAP EXISTED AT ALL: the ship path is SPLIT and nothing said so. `release.mjs --publish`
127
+ // publishes to npm and contains zero `gh release` calls; GitHub Releases are cut only by
128
+ // `self-update.mjs`, which runs from the NIGHTLY. The nightly had been aborting since 2026-07-23 on
129
+ // its (correct) main-only branch guard, so releases silently froze while npm advanced. Two channels,
130
+ // two owners, one of them broken, and a verifier that could not tell.
131
+ //
132
+ // This is the same defect shape this repo has now fixed four times in one day: a proxy measured in
133
+ // place of the thing (receipt-count for "is routing running", backlog-remaining for "is promotion in
134
+ // force", cache-exists for "is this measurement current"). Reachability is the proxy; version is the
135
+ // thing. Users updating via the release bundle get whatever THIS says, not whatever npm says.
136
+ {
137
+ let tag = null, err = null;
138
+ try {
139
+ const r = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
140
+ headers: { 'User-Agent': 'ruvnet-brain-verify-channels' },
141
+ });
142
+ if (r.ok) tag = (await r.json()).tag_name || null; else err = `HTTP ${r.status}`;
143
+ } catch (e) { err = String(e && e.message || e); }
144
+ // Tags carry a leading `v`; the version does not. Compare the bare versions.
145
+ const bare = (s) => String(s || '').replace(/^v/, '');
146
+ check('GitHub Release IS the shipping version', tag != null && bare(tag) === bare(V),
147
+ tag == null
148
+ ? `could not read releases/latest (${err}) — release currency NOT verified`
149
+ : `release=${tag} · shipping=${V}${bare(tag) === bare(V) ? '' : ` ← ${bare(tag) !== bare(V) ? 'STALE: the release channel is behind npm. Cut the release, or users on --update stay on ' + tag : ''}`}`);
150
+ }
151
+
152
+ // 4) release signature present (auto-apply verifies against it)
153
+ { const h = await head(`${bundleUrl}.sig`); check('release signature present', h.ok, `ruvnet-brain.zip.sig → HTTP ${h.status}`); }
154
+
155
+ // 5) explainer live shows current major.minor AND the update command
156
+ { const h = await head(EXPLAINER);
157
+ if (!h.ok) check('explainer live + current', false, `${EXPLAINER} → HTTP ${h.status}`);
158
+ else { const html = await h.r.text().catch(() => '');
159
+ const bare = html.replace(/<[^>]+>/g, '').replace(/&(?:rsquo|apos|#8217|#39);/gi, "'");
160
+ const newIn = [...bare.matchAll(/what[’']?s new in (\d+\.\d+)/gi)].map((m) => m[1]);
161
+ const stale = newIn.filter((x) => x !== mm(V));
162
+ const hasCmd = /ruvnet-brain@latest --update --auto/.test(bare);
163
+ check('explainer live shows current version', stale.length === 0, stale.length ? `live still says "What's new in ${stale.join(', ')}" while ${mm(V)} ships` : `"What's new in ${mm(V)}" ✓`);
164
+ check('explainer live shows the update command', hasCmd, hasCmd ? 'update one-liner present' : 'the --update --auto command is MISSING from the live page');
165
+ }
166
+ }
167
+
168
+ // 6) local HEAD pushed to origin/main (no unpushed shipped code)
169
+ try {
170
+ const local = execFileSync('git', ['-C', ROOT, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
171
+ const remote = execFileSync('git', ['-C', ROOT, 'ls-remote', 'origin', 'main'], { encoding: 'utf8' }).trim().split(/\s+/)[0];
172
+ check('local HEAD pushed to origin/main', local === remote, local === remote ? 'in sync' : `local ${local.slice(0, 7)} != origin ${(remote || '').slice(0, 7)} — unpushed commits`);
173
+ } catch (e) { check('local HEAD pushed to origin/main', false, `git check failed: ${e.message.split('\n')[0]}`); }
174
+
175
+ const failed = results.filter((r) => !r.pass);
176
+ if (JSON_OUT) { console.log(JSON.stringify({ version: V, ok: failed.length === 0, results }, null, 2)); process.exit(failed.length ? 1 : 0); }
177
+
178
+ console.log(`\n ${c.b('RuvNet Brain — channel verification')} ${c.dim('· shipping ' + V)}\n`);
179
+ for (const r of results) console.log(` ${r.pass ? c.g('✓') : c.r('✗')} ${r.name}\n ${c.dim(r.detail || '')}`);
180
+ if (failed.length) { console.log(`\n ${c.r('✗ ' + failed.length + ' channel(s) BROKEN or STALE — this is NOT shipped. Fix, then re-run.')}\n`); process.exit(1); }
181
+ console.log(`\n ${c.g('✓ every channel is current and reachable — a user on any path gets the working, current build.')}\n`);
182
+ process.exit(0);
183
+ }
184
+ run();
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ // scripts/verify-model-catalog.mjs — THE WALL for model facts.
3
+ //
4
+ // Every model in data/model-catalog.json must EXIST in the live OpenRouter catalog and be priced
5
+ // correctly. A missing model, a wrong price, or a stale snapshot is a HARD FAILURE (exit 1). This is the
6
+ // enforcement of Rule 0 for the one place it kept getting skipped: model/version claims. The fact no
7
+ // longer lives in anyone's memory where it can be asserted lazily — it lives in verified data, and this
8
+ // gate refuses to let a wrong or stale one through. ADR-0016.
9
+ //
10
+ // CI runs it offline against the committed snapshot (data/openrouter-catalog-snapshot.json, refreshed
11
+ // nightly by refresh-model-catalog.mjs). Run with --live to check against OpenRouter directly.
12
+ //
13
+ // Usage: node scripts/verify-model-catalog.mjs [--live]
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = path.resolve(__dirname, '..');
21
+ const CATALOG = path.join(ROOT, 'data/model-catalog.json');
22
+ const SNAPSHOT = path.join(ROOT, 'data/openrouter-catalog-snapshot.json');
23
+
24
+ /** Resolve a catalog model id against the live-catalog map, tolerating bare Claude ids. */
25
+ export function findModel(models, id) {
26
+ if (models[id]) return models[id];
27
+ if (!id.includes('/') && models['anthropic/' + id]) return models['anthropic/' + id];
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Pure verifier: check every catalog model against a {pulledAt, models} source. Returns
33
+ * { ok, checked, ageDays, fails[] }. No I/O — fully unit-testable, which a wall must be.
34
+ */
35
+ export function verifyCatalog(catalog, source, { now = Date.now(), maxAgeDays = 14, priceTol = 0.05 } = {}) {
36
+ const models = source.models || {};
37
+ const fails = [];
38
+ const ageDays = (now - new Date(source.pulledAt).getTime()) / 86400000;
39
+ if (!(ageDays <= maxAgeDays)) fails.push(`STALE: source pulled ${source.pulledAt} (${ageDays.toFixed(1)}d old > ${maxAgeDays}d) — run refresh-model-catalog.mjs`);
40
+
41
+ let checked = 0;
42
+ for (const [pid, p] of Object.entries(catalog.providers || {})) {
43
+ if (p.aliasOf) continue;
44
+ // Every provider MUST have a verified frontier (the personalized escalation + savings baseline);
45
+ // mid/cheap are optional because real provider lineups vary in depth (e.g. xAI has no budget SKU).
46
+ if (!p.frontier) fails.push(`INCOMPLETE: ${pid} has no frontier tier`);
47
+ for (const tier of ['frontier', 'mid', 'cheap']) {
48
+ const e = p[tier];
49
+ if (!e) continue;
50
+ checked++;
51
+ const live = findModel(models, e.model);
52
+ if (!live) { fails.push(`MISSING: ${pid}.${tier} "${e.model}" is NOT in the live OpenRouter catalog`); continue; }
53
+ for (const k of ['in', 'out']) {
54
+ const rel = live[k] === 0 ? (e[k] === 0 ? 0 : 1) : Math.abs(e[k] - live[k]) / live[k];
55
+ if (rel > priceTol) fails.push(`PRICE: ${pid}.${tier} "${e.model}" ${k} = catalog $${e[k]} vs live $${live[k]} (${(rel * 100).toFixed(0)}% off)`);
56
+ }
57
+ }
58
+ }
59
+ return { ok: fails.length === 0, checked, ageDays, fails };
60
+ }
61
+
62
+ function loadKey() {
63
+ if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
64
+ try {
65
+ const env = fs.readFileSync(path.join(ROOT, '.env'), 'utf8');
66
+ const m = env.match(/^OPENROUTER_API_KEY=(.+)$/m);
67
+ if (m) return m[1].trim().replace(/^["']|["']$/g, '');
68
+ } catch { /* none */ }
69
+ return null;
70
+ }
71
+
72
+ async function liveSource() {
73
+ const key = loadKey();
74
+ const res = await fetch('https://openrouter.ai/api/v1/models', { headers: key ? { Authorization: `Bearer ${key}` } : {} });
75
+ if (!res.ok) throw new Error(`OpenRouter /models HTTP ${res.status}`);
76
+ const data = (await res.json()).data || [];
77
+ const models = {};
78
+ for (const m of data) { const p = m.pricing || {}; models[m.id] = { in: +(+p.prompt * 1e6).toFixed(4), out: +(+p.completion * 1e6).toFixed(4) }; }
79
+ return { pulledAt: new Date().toISOString(), models };
80
+ }
81
+
82
+ export async function main(argv = process.argv) {
83
+ const live = argv.includes('--live');
84
+ const catalog = JSON.parse(fs.readFileSync(CATALOG, 'utf8'));
85
+ let source;
86
+ if (live) source = await liveSource();
87
+ else {
88
+ if (!fs.existsSync(SNAPSHOT)) { console.error('✗ no snapshot — run: node scripts/refresh-model-catalog.mjs'); process.exit(1); }
89
+ const s = JSON.parse(fs.readFileSync(SNAPSHOT, 'utf8'));
90
+ source = { pulledAt: s._meta.pulledAt, models: s.models };
91
+ }
92
+ const r = verifyCatalog(catalog, source);
93
+ if (!r.ok) {
94
+ console.error(`\n✗ model-catalog verification FAILED — ${r.fails.length} issue(s) [source: ${live ? 'LIVE OpenRouter' : 'snapshot'} @ ${source.pulledAt}]:`);
95
+ for (const f of r.fails) console.error(' - ' + f);
96
+ console.error('\nA model fact cannot ship unless it is verified live. Fix data/model-catalog.json (or refresh the snapshot).');
97
+ process.exit(1);
98
+ }
99
+ console.log(`✓ model-catalog VERIFIED: ${r.checked} model facts match the ${live ? 'LIVE OpenRouter' : 'committed snapshot'} catalog (pulled ${source.pulledAt}, ${r.ageDays.toFixed(1)}d old ≤ 14d).`);
100
+ }
101
+
102
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
103
+ main().catch((e) => { console.error('verify-model-catalog error:', e.message); process.exit(1); });
104
+ }
@@ -0,0 +1,31 @@
1
+ #!/bin/sh
2
+ # verify-nightly-close-issue4.sh — one-shot morning check for issue #4 (runs 07:17, then removes itself).
3
+ # Closes the issue ONLY on proof: releases/latest must have ADVANCED past v0.5.0-dev within the last
4
+ # 24h (i.e., tonight's 3:15 publish worked). Otherwise it posts the honest failure and stays open.
5
+ set -u
6
+ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
7
+ cd /Users/stuartkerr/Code/ruvnet-brain || exit 1
8
+ LOG=logs/issue4-verify.log
9
+ {
10
+ echo "[$(date '+%Y-%m-%dT%H:%M:%S')] checking releases/latest"
11
+ TAG=$(gh release view --json tagName -q .tagName 2>/dev/null)
12
+ PUB=$(gh release view --json publishedAt -q .publishedAt 2>/dev/null)
13
+ echo " latest: $TAG published $PUB"
14
+ FRESH=0
15
+ if [ -n "$PUB" ] && [ "$TAG" != "v0.5.0-dev" ]; then # sync-version-ignore: the stale BUNDLE tag being compared against, not a product version
16
+ AGE=$(( $(date +%s) - $(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$PUB" +%s 2>/dev/null || echo 0) ))
17
+ [ "$AGE" -gt 0 ] && [ "$AGE" -lt 86400 ] && FRESH=1
18
+ fi
19
+ if [ "$FRESH" = "1" ]; then
20
+ # The full professional close note, with the real receipt substituted in at close time.
21
+ NOTE=$(sed -e "s/__TAG__/${TAG}/g" -e "s|__PUB__|${PUB}|g" scripts/issue4-close-note.md)
22
+ gh issue close 4 --comment "$NOTE"
23
+ echo " CLOSED issue #4 with the full note (tag=$TAG)"
24
+ else
25
+ gh issue comment 4 --body "Honest status: the overnight publish did **not** advance releases/latest (still $TAG). The PATH fix is in but something else is failing — investigating; leaving this open. Log: logs/nightly.log"
26
+ echo " release did NOT advance — commented, left open"
27
+ fi
28
+ launchctl bootout "gui/$(id -u)/com.ruvnet.issue4-verify" 2>/dev/null
29
+ rm -f "$HOME/Library/LaunchAgents/com.ruvnet.issue4-verify.plist"
30
+ echo " one-shot agent removed"
31
+ } >> "$LOG" 2>&1
@@ -0,0 +1,40 @@
1
+ // version.mjs — THE single source of truth for the product version (SEC-0010 #2 / ADR-0009 decision 1).
2
+ // plugin/.claude-plugin/plugin.json `version` is the one hand-edited number. EVERY other surface
3
+ // (installer, stamp, bundle, README, manifest, npm package) inherits it — at runtime via getVersion()
4
+ // for code paths, or written once by sync-version.mjs for files that must carry a literal.
5
+ // Never hardcode a version string anywhere else; scripts/sync-version.mjs --check fails the build if you do.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ export const PLUGIN_JSON = path.join(ROOT, 'plugin', '.claude-plugin', 'plugin.json');
12
+
13
+ /** The product version, e.g. "1.9.1-dev" — read live from the single source of truth. */
14
+ export function getVersion() {
15
+ const v = JSON.parse(fs.readFileSync(PLUGIN_JSON, 'utf8')).version;
16
+ if (!v || typeof v !== 'string') throw new Error(`no version in ${PLUGIN_JSON}`);
17
+ return v;
18
+ }
19
+
20
+ /** The product version with a leading "v", e.g. "v1.9.1-dev" (Release-tag form). */
21
+ export function getVersionTag() {
22
+ return `v${getVersion()}`;
23
+ }
24
+
25
+ /**
26
+ * Strip the Release-tag "v" so a value is safe to write into a version FIELD.
27
+ *
28
+ * There are two legitimate forms of the same number: the Release tag (`v1.14.1-dev`, used for git
29
+ * tags, Release URLs and human stamps) and the bare literal (`1.14.1-dev`, used in package.json and
30
+ * every other version field). sync-version.mjs owns the fields and compares them bare. brain-stamp
31
+ * and build-bundle both carried the TAG into `data/manifest.json.brainVersion`, so `--check` went
32
+ * red on a clean tree, every time either ran — two writers, two formats, a contradiction no amount
33
+ * of re-syncing could settle. Cross the boundary once, here.
34
+ */
35
+ export const stripTag = (v) => String(v).replace(/^v/, '');
36
+
37
+ // CLI: `node scripts/version.mjs` prints the version (handy for shell scripts).
38
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
39
+ process.stdout.write(getVersion() + '\n');
40
+ }