ruvnet-brain 4.3.28 → 4.3.30

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 (73) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +90 -8
  3. package/console/app.js +7 -2
  4. package/kb/corpus-release-identity.mjs +73 -0
  5. package/package.json +4 -3
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/mcp/managed-cli-interface.mjs +83 -9
  9. package/plugin/scripts/advocacy-route.mjs +4 -1
  10. package/plugin/scripts/capacity-aware-parallel-work.mjs +4 -0
  11. package/plugin/scripts/continuation-gate.mjs +71 -0
  12. package/plugin/scripts/decision-gate.mjs +29 -57
  13. package/plugin/scripts/ground-ruvnet.sh +77 -4
  14. package/plugin/scripts/grounding-stamp.sh +34 -5
  15. package/plugin/scripts/grounding-turn-gate.mjs +8 -2
  16. package/plugin/scripts/grounding-turn-mark.mjs +6 -1
  17. package/plugin/scripts/hook-input.mjs +54 -0
  18. package/plugin/scripts/hook-shim.mjs +10 -0
  19. package/plugin/scripts/memory-doctor.mjs +43 -0
  20. package/plugin/scripts/nightly-controller.mjs +6 -1
  21. package/plugin/scripts/project-progression-contract.mjs +17 -0
  22. package/plugin/scripts/project-progression-hook.mjs +18 -5
  23. package/plugin/scripts/project-progression-producer.mjs +16 -12
  24. package/plugin/scripts/ruvnet-gate1-pattern.mjs +17 -0
  25. package/plugin/scripts/session-start-core.mjs +11 -5
  26. package/plugin/scripts/session-start-update-plane.mjs +5 -1
  27. package/plugin/scripts/unprompted-runtime.mjs +29 -8
  28. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +1 -1
  29. package/scripts/adr-072-completion.mjs +2 -1
  30. package/scripts/calibrate-router.mjs +6 -6
  31. package/scripts/corpus-reconcile.mjs +32 -3
  32. package/scripts/correction-detect.mjs +10 -11
  33. package/scripts/dispatch-receipt.mjs +2 -2
  34. package/scripts/dual-host-deliberation.mjs +3 -2
  35. package/scripts/execution-policy.mjs +10 -2
  36. package/scripts/gen-console-images.mjs +0 -1
  37. package/scripts/gen-images.mjs +0 -4
  38. package/scripts/host-install-matrix.mjs +7 -4
  39. package/scripts/independent-review-receipt.mjs +7 -8
  40. package/scripts/ingest-repo.mjs +45 -1
  41. package/scripts/learning-replay-cli.mjs +2 -1
  42. package/scripts/learning-replay-fixture.mjs +2 -1
  43. package/scripts/learnings.mjs +19 -5
  44. package/scripts/lesson-migrate-agentdb.mjs +635 -0
  45. package/scripts/loop-checkpoint.mjs +37 -1
  46. package/scripts/metaharness-receipts.mjs +3 -2
  47. package/scripts/nightly-two-run-proof.mjs +6 -6
  48. package/scripts/nightly-watchdog.mjs +10 -2
  49. package/scripts/onboarding-console.mjs +98 -49
  50. package/scripts/oracle/producer-hosts.mjs +2 -1
  51. package/scripts/private-overlay.mjs +31 -9
  52. package/scripts/public-verification-aggregate.mjs +4 -3
  53. package/scripts/publication-receipt.mjs +9 -0
  54. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  55. package/scripts/rebuild-gists-from-receipts.mjs +1 -1
  56. package/scripts/reconcile-project.mjs +0 -0
  57. package/scripts/release.mjs +34 -82
  58. package/scripts/retrieval-canary.mjs +3 -2
  59. package/scripts/review-model-defaults.mjs +26 -0
  60. package/scripts/route-cheap.mjs +20 -7
  61. package/scripts/router-utilization.mjs +5 -5
  62. package/scripts/rvf-generation.mjs +68 -2
  63. package/scripts/single-source-check.mjs +270 -0
  64. package/scripts/subscription-hosts.mjs +4 -0
  65. package/scripts/sync-version.mjs +22 -0
  66. package/scripts/trismart.mjs +3 -3
  67. package/scripts/wired-check.mjs +40 -4
  68. package/console/assets/memory.webp +0 -0
  69. package/plugin/scripts/version-bump-gate.sh +0 -124
  70. package/scripts/qe/aggregate-4.3.mjs +0 -42
  71. package/scripts/release-convergence-watchdog.mjs +0 -112
  72. package/scripts/stamp-existing-rvf-generations.mjs +0 -53
  73. package/scripts/verify-channels.mjs +0 -196
@@ -1,112 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * RELEASE CONVERGENCE WATCHDOG — finishes issue #77 unattended, or does nothing at all.
4
- *
5
- * WHY THIS EXISTS. On 2026-08-06 the published surfaces named different generations (npm 4.0.12,
6
- * GitHub releases/latest v4.0.7). The cause was not drift: the release rail was dead three
7
- * independent ways, each fatal alone, and with the rail dead every release was made by hand — which
8
- * is how the surfaces came apart in the first place. All three are fixed. What remained was a
9
- * GitHub Actions MAJOR OUTAGE ("workflow runs are still failing or delayed in starting"), and the
10
- * publisher is an Actions workflow, so the last step could not run and the maintainer was leaving
11
- * for a month.
12
- *
13
- * WHAT IT WILL NOT DO. It does not publish. `scripts/self-update.mjs:56` refuses `--publish` and
14
- * `release-candidate-preflight.yml` owns the one-time long lanes and `protected-release.yml` is the
15
- * sole publication controller. This watchdog is report-only: it never dispatches either phase and
16
- * cannot manufacture typed candidate evidence.
17
- *
18
- * IT IS A NO-OP UNLESS EVERY PRECONDITION HOLDS. It runs from the nightly, unattended, for weeks.
19
- * A watchdog that acts on a partial picture is worse than no watchdog, so it refuses on anything
20
- * unexpected and says why. The default outcome is "nothing happened, here is the reason".
21
- *
22
- * node scripts/release-convergence-watchdog.mjs # report only, never acts
23
- * node scripts/release-convergence-watchdog.mjs --dispatch # compatibility flag; still reports only
24
- */
25
- import { execFileSync } from 'node:child_process';
26
- import fs from 'node:fs';
27
- import path from 'node:path';
28
- import { latestCodeReleaseTag } from './release-channel-kind.mjs';
29
-
30
- const ROOT = path.dirname(path.dirname(new URL(import.meta.url).pathname));
31
- const DISPATCH = process.argv.includes('--dispatch');
32
- const REPO = 'stuinfla/ruvnet-brain';
33
-
34
- const log = (s) => process.stdout.write(`[release-watchdog] ${s}\n`);
35
- const sh = (cmd, args, opts = {}) =>
36
- execFileSync(cmd, args, { cwd: ROOT, encoding: 'utf8', timeout: 120_000, ...opts }).trim();
37
- const tryShy = (fn, fallback = null) => { try { return fn(); } catch { return fallback; } };
38
-
39
- /** Refuse loudly and exit 0 — a watchdog that exits non-zero would page someone every night. */
40
- function stand_down(reason) {
41
- log(`STAND DOWN: ${reason}`);
42
- log('no action taken. This is the expected outcome on most nights.');
43
- process.exit(0);
44
- }
45
-
46
- // ── 1. Is there anything to converge? ────────────────────────────────────────────────────────────
47
- const npmLatest = tryShy(() => sh('npm', ['view', 'ruvnet-brain', 'dist-tags.latest']));
48
- // ADR-086 S1: `gh release view` with no tag resolves `releases/latest`, which is a corpus
49
- // generation on any night a corpus round shipped. Comparing npm against a corpus digest would read
50
- // "never converged" forever and keep dispatching the publisher — a guard that silently disarms into
51
- // a loop is worse than one that breaks loudly.
52
- const ghLatest = tryShy(() => latestCodeReleaseTag(JSON.parse(sh('gh', ['api', `repos/${REPO}/releases?per_page=30`]))));
53
- if (!npmLatest || !ghLatest) stand_down(`could not read published surfaces (npm=${npmLatest} github=${ghLatest})`);
54
-
55
- const ghVersion = ghLatest.replace(/^v/, '');
56
- log(`npm dist-tags.latest = ${npmLatest} · GitHub latest code release = ${ghLatest}`);
57
- if (npmLatest === ghVersion) {
58
- log(`CONVERGED — both surfaces name ${npmLatest}. Issue #77's invariant holds; nothing to do.`);
59
- process.exit(0);
60
- }
61
-
62
- // ── 2. Can the publisher even run? ───────────────────────────────────────────────────────────────
63
- // The whole reason this script exists. Dispatching into a broken Actions plane burns a candidate and
64
- // leaves a confusing failed run behind for a maintainer who is not here to read it.
65
- const running = tryShy(() => sh('gh', ['run', 'list', '--repo', REPO, '--limit', '20', '--json', 'status',
66
- '-q', '[.[]|select(.status=="in_progress")]|length']), '0');
67
- const queued = tryShy(() => sh('gh', ['run', 'list', '--repo', REPO, '--limit', '40', '--json', 'status',
68
- '-q', '[.[]|select(.status=="queued")]|length']), '0');
69
- if (Number(running) === 0 && Number(queued) > 3) {
70
- stand_down(`Actions appears stalled (${running} running, ${queued} queued) — likely still the outage. Will retry tomorrow.`);
71
- }
72
-
73
- // ── 3. Everything else must already be resolved. ─────────────────────────────────────────────────
74
- const openPrs = tryShy(() => sh('gh', ['pr', 'list', '--repo', REPO, '--state', 'open', '--json', 'number', '-q', 'length']), '?');
75
- if (openPrs !== '0') stand_down(`${openPrs} open PR(s) — merge them before cutting a release, so the release contains them`);
76
-
77
- const dirty = tryShy(() => sh('git', ['status', '--porcelain']), 'unknown');
78
- if (dirty === 'unknown' || dirty.split('\n').filter((l) => l && !l.startsWith('??')).length) {
79
- stand_down('working tree is dirty — release candidates must come from a clean worktree');
80
- }
81
-
82
- sh('git', ['fetch', 'origin', '--quiet']);
83
- const head = sh('git', ['rev-parse', 'HEAD']);
84
- const originMain = sh('git', ['rev-parse', 'origin/main']);
85
- if (head !== originMain) stand_down(`local HEAD ${head.slice(0, 7)} != origin/main ${originMain.slice(0, 7)}`);
86
-
87
- // ── 4. The candidate must BE a release candidate. ────────────────────────────────────────────────
88
- const version = JSON.parse(fs.readFileSync(path.join(ROOT, 'plugin/.claude-plugin/plugin.json'), 'utf8')).version;
89
- if (/-dev$/.test(version)) {
90
- // Deliberate: promoting -dev to clean means writing a release commit, and that is an authoring
91
- // decision (which generation ships, with what narrative) — not something a nightly should invent.
92
- stand_down(`main carries ${version}; a release needs a clean version in a release(<version>) commit. Authoring that is a human/session decision, not a watchdog's.`);
93
- }
94
- const subjects = sh('git', ['log', '-25', '--pretty=%s']).split('\n');
95
- if (!subjects.some((s) => /^release\s*\(/i.test(s) && s.includes(version))) {
96
- stand_down(`clean version ${version} present but no release(${version}) commit in recent history`);
97
- }
98
-
99
- // ── 5. Exact-SHA evidence must exist and be green. ───────────────────────────────────────────────
100
- const runsFor = (wf) => JSON.parse(tryShy(() => sh('gh', ['run', 'list', '--repo', REPO, '--workflow', wf,
101
- '--limit', '20', '--json', 'databaseId,status,conclusion,headSha']), '[]'));
102
- const greenAt = (wf) => runsFor(wf).find((r) => r.headSha === originMain && r.conclusion === 'success');
103
-
104
- const ci = greenAt('ci');
105
- if (!ci) stand_down(`no successful exact-SHA \`ci\` run at ${originMain.slice(0, 7)} yet`);
106
-
107
- log(`PRECONDITIONS VISIBLE — candidate=${originMain.slice(0, 7)} version=${version} ci=${ci.databaseId}`);
108
- // ── 6. Never dispatch a publisher from the watchdog. ───────────────────────────────────────────
109
- // Only protected-release may derive the complete typed evidence chain and cross Production.
110
- // This unattended observer cannot turn visible preconditions into release authority.
111
- log(`PRECONDITIONS ONLY — no candidate artifact inferred and no action taken for ${version} at ${originMain}`);
112
- log('Run release-candidate-preflight on release/** once; fast-forward that exact SHA to main; then protected-release imports release-candidate-<SHA> without a run ID.');
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
- // Bind every existing canonical RVF to the current Brain release without re-embedding. Optional
3
- // legacy pruning is fail-closed: the canonical passages/meta and big RVF sidecars must exist first.
4
-
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
- import { writeRvfGeneration } from './rvf-generation.mjs';
9
-
10
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
- const KB = path.join(ROOT, 'kb');
12
- const PRUNE = process.argv.includes('--prune-legacy');
13
- const stores = fs.readdirSync(KB)
14
- .filter((file) => file.endsWith('.big.rvf'))
15
- .map((file) => file.slice(0, -'.big.rvf'.length))
16
- .sort();
17
-
18
- let pruned = 0;
19
- for (const store of stores) {
20
- const embedFile = path.join(KB, `${store}.big.rvf.embed.json`);
21
- const idmapFile = path.join(KB, `${store}.big.rvf.idmap.json`);
22
- const passagesFile = path.join(KB, `${store}.passages.jsonl`);
23
- const metaFile = path.join(KB, `${store}.meta.json`);
24
- for (const required of [embedFile, idmapFile, passagesFile, metaFile]) {
25
- if (!fs.existsSync(required)) throw new Error(`${store}: missing ${path.basename(required)}; refusing to stamp/prune`);
26
- }
27
- const embed = JSON.parse(fs.readFileSync(embedFile, 'utf8'));
28
- const generation = writeRvfGeneration({
29
- dir: KB,
30
- store,
31
- model: embed.model,
32
- dimensions: embed.dimensions,
33
- sourceCommit: null,
34
- });
35
- console.log(`[generation] ${store}: ${generation.sha256.slice(0, 16)}… ${generation.bytes} bytes`);
36
-
37
- if (PRUNE) {
38
- for (const legacy of [
39
- `${store}.rvf`,
40
- `${store}.rvf.idmap.json`,
41
- `${store}.rvf.embed.json`,
42
- `${store}.big.passages.jsonl`,
43
- `${store}.big.meta.json`,
44
- ]) {
45
- const file = path.join(KB, legacy);
46
- if (fs.existsSync(file)) {
47
- fs.rmSync(file);
48
- pruned++;
49
- }
50
- }
51
- }
52
- }
53
- console.log(`[generation] stamped ${stores.length} canonical RVFs${PRUNE ? `; pruned ${pruned} legacy files` : ''}`);
@@ -1,196 +0,0 @@
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
- import { describeLatestPointer, latestCodeReleaseTag } from './release-channel-kind.mjs';
33
-
34
- const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
35
- const JSON_OUT = process.argv.includes('--json');
36
- const PRE_PUSH = process.argv.includes('--pre-push'); // git pre-push hook mode: block only on what is KNOWABLE before the push
37
- const REPO = 'stuinfla/ruvnet-brain';
38
- const NPM_PKG = 'ruvnet-brain';
39
- const EXPLAINER = 'https://isovision.ai/ruvnet-brain/';
40
-
41
- 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` };
42
- const pluginVersion = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'plugin/.claude-plugin/plugin.json'), 'utf8')).version;
43
- const mm = (v) => (String(v).match(/^(\d+\.\d+)/) || [, '?'])[1];
44
-
45
- async function head(url, timeout = 20000) {
46
- const ctl = AbortSignal.timeout(timeout);
47
- 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 }; }
48
- catch (e) { return { ok: false, status: 0, error: e.message }; }
49
- }
50
-
51
- const results = [];
52
- const check = (name, pass, detail) => { results.push({ name, pass: !!pass, detail }); };
53
-
54
- async function run() {
55
- const V = pluginVersion();
56
-
57
- // ── PRE-PUSH MODE ──────────────────────────────────────────────────────────
58
- // Runs from the git pre-push hook. It can ONLY block on things knowable BEFORE the push —
59
- // i.e. what I am about to ship. npm-latest and the live explainer are DOWNSTREAM of a push
60
- // (publish + Vercel deploy happen after), so blocking on them here would refuse every honest
61
- // push. The two things that stranded Jan and are 100% knowable now: (a) the version surfaces
62
- // disagree, (b) the self-update manifest in the SOURCE.json I'm shipping 404s. Block on those.
63
- if (PRE_PUSH) {
64
- // a) version single-source-of-truth
65
- const sv = execFileSync(process.execPath, [path.join(ROOT, 'scripts/sync-version.mjs'), '--check'], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
66
- check('version surfaces agree', /agree|✓/i.test(sv), sv.trim().split('\n').pop());
67
- // b) the self-update manifest URL in the bundle SOURCE.json resolves (the exact 404 that broke --update)
68
- let manifestUrl = null;
69
- try { const sj = JSON.parse(fs.readFileSync(path.join(ROOT, 'kb/SOURCE.json'), 'utf8'));
70
- 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; };
71
- manifestUrl = find(sj); } catch { /* */ }
72
- if (!manifestUrl) check('self-update manifest URL present', false, 'no canonicalManifestUrl in kb/SOURCE.json');
73
- 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'}`); }
74
-
75
- const failed = results.filter((r) => !r.pass);
76
- console.log(`\n ${c.b('pre-push gate')} ${c.dim('· shipping ' + V + ' · (npm/explainer are verified post-publish by `release.mjs --publish`)')}`);
77
- for (const r of results) console.log(` ${r.pass ? c.g('✓') : c.r('✗')} ${r.name} ${c.dim(r.detail || '')}`);
78
- 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); }
79
- console.log(`\n ${c.g('✓ version/channel checks passed — document currency runs next.')}\n`);
80
- process.exit(0);
81
- }
82
-
83
- // 1) npm registry latest == shipping version. npm's read replicas lag a few seconds behind a fresh
84
- // publish, so a single immediate `npm view` right after `release.mjs` publishes can still return the
85
- // PRIOR version — a false STALE that fails an otherwise-perfect ship (hit exactly this 2026-07-18:
86
- // publish succeeded, `+ ruvnet-brain@3.4.9-dev`, yet the immediate view read 3.4.8-dev). Retry with a
87
- // short backoff so propagation lag is absorbed, while a publish that genuinely never landed still
88
- // fails after the window. A gate that false-fails teaches you to distrust the gate — the worst outcome.
89
- {
90
- let latest = '', ok = false, lastErr = '';
91
- const ATTEMPTS = 6; // up to ~6×5s = 30s of propagation tolerance
92
- for (let i = 0; i < ATTEMPTS; i++) {
93
- try {
94
- latest = execFileSync('npm', ['view', NPM_PKG, 'dist-tags.latest'], { encoding: 'utf8', timeout: 30000 }).trim();
95
- if (latest === V) { ok = true; break; }
96
- } catch (e) { lastErr = e.message.split('\n')[0]; }
97
- if (i < ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 5000));
98
- }
99
- check('npm latest matches shipping version', ok,
100
- ok ? `npm latest=${latest} · shipping=${V}`
101
- : `npm latest=${latest || '(view failed: ' + lastErr + ')'} · shipping=${V}${latest && latest !== V ? ' ← STILL STALE after ~' + (ATTEMPTS * 5) + 's: dispatch the protected-release workflow for the exact candidate SHA' : ''}`);
102
- }
103
-
104
- // 2) the self-update manifest URL actually resolves (the 404 that stranded users)
105
- let manifestUrl = null;
106
- try {
107
- const sj = JSON.parse(fs.readFileSync(path.join(ROOT, 'kb/SOURCE.json'), 'utf8'));
108
- 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; };
109
- manifestUrl = find(sj);
110
- } catch { /* */ }
111
- if (!manifestUrl) check('self-update manifest URL present', false, 'no canonicalManifestUrl in kb/SOURCE.json');
112
- 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'}`); }
113
-
114
- // 3) release bundle downloadable
115
- const bundleUrl = `https://github.com/${REPO}/releases/latest/download/ruvnet-brain.zip`;
116
- { const h = await head(bundleUrl); check('release bundle downloads', h.ok, `releases/latest/…/ruvnet-brain.zip → HTTP ${h.status}`); }
117
-
118
- // 3b) …AND IT IS THE VERSION WE ARE SHIPPING. Reachability is not currency.
119
- //
120
- // MEASURED 2026-07-24, and this gate reported SHIPPED while it was false. npm was moved to
121
- // 3.9.50-dev by `release.mjs --publish`; the GitHub Release stayed at v3.9.18-dev from 2026-07-23,
122
- // THIRTY-TWO versions behind. Every check above passed — the manifest resolved, the bundle
123
- // downloaded, the signature was present — because they all ask "does the latest release respond?"
124
- // and none asked "is the latest release the one we just built?" A 32-version-old bundle answers
125
- // HTTP 200 perfectly well.
126
- //
127
- // WHY THE GAP EXISTED AT ALL: the ship path is SPLIT and nothing said so. `release.mjs --publish`
128
- // publishes to npm and contains zero `gh release` calls; GitHub Releases are cut only by
129
- // `self-update.mjs`, which runs from the NIGHTLY. The nightly had been aborting since 2026-07-23 on
130
- // its (correct) main-only branch guard, so releases silently froze while npm advanced. Two channels,
131
- // two owners, one of them broken, and a verifier that could not tell.
132
- //
133
- // This is the same defect shape this repo has now fixed four times in one day: a proxy measured in
134
- // place of the thing (receipt-count for "is routing running", backlog-remaining for "is promotion in
135
- // force", cache-exists for "is this measurement current"). Reachability is the proxy; version is the
136
- // thing. Users updating via the release bundle get whatever THIS says, not whatever npm says.
137
- // ADR-086 S1: `releases/latest` is the CUSTOMER download pointer and may legitimately be a corpus
138
- // generation (`corpus-sha256-<digest>`), which can never equal a semver. Asking that pointer "are
139
- // you the shipping version?" therefore stopped being the right question the moment corpus
140
- // promotion existed — and because release.mjs runs this script as `runOrDie` in step E, the wrong
141
- // question would have failed the OWNER'S OWN preflight every time a corpus night succeeded. The
142
- // code generation is a separate fact, so it gets asked separately.
143
- {
144
- let releases = null, tag = null, latestPointer = null, err = null;
145
- try {
146
- const r = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=30`, {
147
- headers: { 'User-Agent': 'ruvnet-brain-verify-channels' },
148
- });
149
- if (r.ok) {
150
- releases = await r.json();
151
- tag = latestCodeReleaseTag(releases);
152
- latestPointer = (Array.isArray(releases) ? releases.find((row) => row && !row.draft) : null)?.tag_name || null;
153
- } else err = `HTTP ${r.status}`;
154
- } catch (e) { err = String(e && e.message || e); }
155
- // Tags carry a leading `v`; the version does not. Compare the bare versions.
156
- const bare = (s) => String(s || '').replace(/^v/, '');
157
- const pointerNote = latestPointer ? ` · ${describeLatestPointer(latestPointer)}` : '';
158
- check('GitHub Release IS the shipping version', tag != null && bare(tag) === bare(V),
159
- tag == null
160
- ? `could not read the latest code release (${err || 'no semver-tagged release found'}) — release currency NOT verified`
161
- : `code release=${tag} · shipping=${V}${pointerNote}${bare(tag) === bare(V) ? '' : ' ← STALE: the release channel is behind npm. Cut the release, or users on --update stay on ' + tag}`);
162
- }
163
-
164
- // 4) release signature present (auto-apply verifies against it)
165
- { const h = await head(`${bundleUrl}.sig`); check('release signature present', h.ok, `ruvnet-brain.zip.sig → HTTP ${h.status}`); }
166
-
167
- // 5) explainer live shows current major.minor AND the update command
168
- { const h = await head(EXPLAINER);
169
- if (!h.ok) check('explainer live + current', false, `${EXPLAINER} → HTTP ${h.status}`);
170
- else { const html = await h.r.text().catch(() => '');
171
- const bare = html.replace(/<[^>]+>/g, '').replace(/&(?:rsquo|apos|#8217|#39);/gi, "'");
172
- const newIn = [...bare.matchAll(/what[’']?s new in (\d+\.\d+)/gi)].map((m) => m[1]);
173
- const stale = newIn.filter((x) => x !== mm(V));
174
- const hasCmd = /ruvnet-brain@latest --update --auto/.test(bare);
175
- 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)}" ✓`);
176
- check('explainer live shows the update command', hasCmd, hasCmd ? 'update one-liner present' : 'the --update --auto command is MISSING from the live page');
177
- }
178
- }
179
-
180
- // 6) local HEAD pushed to origin/main (no unpushed shipped code)
181
- try {
182
- const local = execFileSync('git', ['-C', ROOT, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
183
- const remote = execFileSync('git', ['-C', ROOT, 'ls-remote', 'origin', 'main'], { encoding: 'utf8' }).trim().split(/\s+/)[0];
184
- 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`);
185
- } catch (e) { check('local HEAD pushed to origin/main', false, `git check failed: ${e.message.split('\n')[0]}`); }
186
-
187
- const failed = results.filter((r) => !r.pass);
188
- if (JSON_OUT) { console.log(JSON.stringify({ version: V, ok: failed.length === 0, results }, null, 2)); process.exit(failed.length ? 1 : 0); }
189
-
190
- console.log(`\n ${c.b('RuvNet Brain — channel verification')} ${c.dim('· shipping ' + V)}\n`);
191
- for (const r of results) console.log(` ${r.pass ? c.g('✓') : c.r('✗')} ${r.name}\n ${c.dim(r.detail || '')}`);
192
- 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); }
193
- console.log(`\n ${c.g('✓ every channel is current and reachable — a user on any path gets the working, current build.')}\n`);
194
- process.exit(0);
195
- }
196
- run();