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
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ // scripts/single-source-check.mjs — the single-source contract, as executable checks.
3
+ //
4
+ // Every entry is one defect found in the 2026-09-26 audit: two or more versions of the same rule,
5
+ // process, code path or fact. Each check's exit is the verdict — nobody's word is. `repo` checks run
6
+ // in CI (canonical-qa) so a contradiction that is removed can never silently come back; `machine`
7
+ // checks describe this maintainer Mac and run with --machine.
8
+ //
9
+ // node scripts/single-source-check.mjs # repo checks (CI)
10
+ // node scripts/single-source-check.mjs --machine # repo + machine checks
11
+ // node scripts/single-source-check.mjs --json
12
+ import { execFileSync, spawnSync } from 'node:child_process';
13
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { readCheckpoint, checkpointStaleness } from './loop-checkpoint.mjs';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const HOME = os.homedir();
21
+ const MACHINE = process.argv.includes('--machine');
22
+ const JSON_OUT = process.argv.includes('--json');
23
+
24
+ const git = (...a) => execFileSync('git', a, { cwd: ROOT, encoding: 'utf8', maxBuffer: 1 << 26 }).trim();
25
+ const tracked = git('ls-files').split('\n').filter(Boolean);
26
+ const read = (f) => { try { return readFileSync(path.join(ROOT, f), 'utf8'); } catch { return ''; } };
27
+ const readAbs = (f) => { try { return readFileSync(f, 'utf8'); } catch { return ''; } };
28
+ const json = (s) => { try { return JSON.parse(s); } catch { return {}; } };
29
+
30
+ // Instruction surfaces: files that tell a person or an agent HOW to operate this project.
31
+ // Decision records (ADR/DDD), research notes and append-only logs are history and are excluded.
32
+ const HISTORY = /^(docs\/(adr|ddd|research|audits|reviews|qe)\/|CHANGELOG\.md$|PROGRESS\.md$|plugin\/docs\/RELEASE-NOTES|\.release-evidence\/|tests\/|evals\/)/;
33
+ // kb/ is corpus content (primers/cards describing OTHER projects' own commands) and docs/issues/
34
+ // are upstream bug reports quoting other configs — both are data, not instructions to this project.
35
+ const instructions = tracked.filter((f) => /\.md$/.test(f) && !HISTORY.test(f) && !/^(kb|docs\/issues)\//.test(f));
36
+ const grepIn = (files, re) => files.flatMap((f) => read(f).split('\n')
37
+ .map((l, i) => (re.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 140)}` : null)).filter(Boolean));
38
+ const none = (hits) => ({ ok: hits.length === 0, detail: hits.slice(0, 8).join('\n') || 'none' });
39
+
40
+ const pkg = json(read('package.json'));
41
+ const workflows = tracked.filter((f) => /^\.github\/workflows\/.+\.ya?ml$/.test(f));
42
+
43
+ const checks = [
44
+ // A — one copy of the code
45
+ { id: 'A1', area: 'code', scope: 'repo', title: 'Every module is wired or has a stated reason (the repo\'s own wired-check audit)',
46
+ run: () => { const r = spawnSync(process.execPath, [path.join(ROOT, 'scripts/wired-check.mjs'), '--check'], { cwd: ROOT, encoding: 'utf8' });
47
+ return { ok: r.status === 0, detail: `${r.stdout}${r.stderr}`.split('\n').filter((l) => /UNWIRED|✗/.test(l)).slice(0, 6).join('\n') }; } },
48
+ { id: 'A2', area: 'code', scope: 'repo', title: 'No byte-identical duplicate files (outside test fixtures)',
49
+ run: () => {
50
+ const seen = new Map(); const dup = [];
51
+ for (const f of tracked) {
52
+ if (!/\.(mjs|js|cjs|sh|md|yml|json)$/.test(f) || /^tests\/|fixtures?\//.test(f)) continue;
53
+ if (f.endsWith('RELEASE-NOTES-4.0.md')) continue; // intentional pair, equality enforced by tests/integration/whats-new-installed.test.mjs
54
+ const t = read(f); if (t.length < 200) continue;
55
+ if (seen.has(t)) dup.push(`${f} == ${seen.get(t)}`); else seen.set(t, f);
56
+ }
57
+ return none(dup);
58
+ } },
59
+
60
+ // B — one version of the rules
61
+ { id: 'B1', area: 'rules', scope: 'repo', title: 'CONTRIBUTING.md is the only operating rulebook (no parallel process docs)',
62
+ run: () => none(['docs/QA-RELEASE-PROCESS.md', 'docs/NIGHTLY-REFRESH.md'].filter((f) => tracked.includes(f))) },
63
+ { id: 'B2', area: 'rules', scope: 'repo', title: 'No instruction tells anyone to publish locally',
64
+ run: () => none(grepIn(instructions.filter((f) => f !== 'CONTRIBUTING.md'), /release\.mjs --publish|\bnpm publish\b|gh release create/)
65
+ .filter((l) => !/\b(never|not|unusable|refus|intentionally|blocked|cannot|forbid|only through|provenance|ahead of the last)/i.test(l))) },
66
+ { id: 'B3', area: 'rules', scope: 'repo', title: 'No instruction uses npx for ruflo / claude-flow (one global ruflo)',
67
+ run: () => none(grepIn(instructions, /npx (-y )?(@claude-flow|claude-flow|ruflo)\b/)
68
+ .filter((l) => !/\bnever\b|console\/CONTRACT\.md:\d+: +"event"/i.test(l))) },
69
+ { id: 'B4', area: 'rules', scope: 'repo', title: 'No live file cites a workflow that does not exist',
70
+ run: () => {
71
+ const exists = new Set(workflows.map((w) => path.basename(w)));
72
+ const live = [...instructions, ...workflows, ...tracked.filter((f) => /^(scripts|plugin|bin|kb)\/.+\.(mjs|sh)$/.test(f))];
73
+ return none(live.flatMap((f) => [...read(f).matchAll(/\b([a-z0-9-]+\.ya?ml)\b/g)].map((m) => m[1])
74
+ .filter((n) => /release|corpus|publish|nightly|preflight|grading/.test(n) && !exists.has(n))
75
+ .map((n) => `${f}: ${n}`)));
76
+ } },
77
+ { id: 'B5', area: 'rules', scope: 'repo', title: 'Every `npm run X` in an instruction exists in package.json',
78
+ run: () => {
79
+ const scripts = new Set([...Object.keys(pkg.scripts || {}), ...Object.keys(json(read('kb/package.json')).scripts || {})]);
80
+ return none(instructions.flatMap((f) => [...read(f).matchAll(/npm run ([a-z0-9:_-]+)/g)]
81
+ .filter((m) => !scripts.has(m[1])).map((m) => `${f}: npm run ${m[1]}`)));
82
+ } },
83
+ { id: 'B6', area: 'rules', scope: 'repo', title: 'No instruction grants standing authority to publish without the owner',
84
+ run: () => none(grepIn(instructions, /standing authori[sz]ation permits|no separate human reviewer click/i)) },
85
+ { id: 'B7', area: 'rules', scope: 'repo', title: 'Model IDs are selected only in their owner modules',
86
+ run: () => {
87
+ const re = /['"`](claude-(fable|opus|sonnet|haiku)-[0-9][a-z0-9.-]*|gpt-[0-9][a-z0-9.-]*)['"`]/;
88
+ // Owners (the single definition sites) and non-selecting mentions, each with its reason.
89
+ const allowed = new Map([
90
+ ['scripts/review-model-defaults.mjs', 'owner: review / dual-host defaults'],
91
+ ['scripts/route-cheap.mjs', 'owner: Claude cost ladder'],
92
+ ['plugin/scripts/identifier-preflight.mjs', 'doc comment quoting a historical incident'],
93
+ ['scripts/goldie-research.mjs', 'family prefix match against the live catalog'],
94
+ ['scripts/routing-flywheel.mjs', 'comment example of id normalisation'],
95
+ ['scripts/proxy/proxy-verify.mjs', 'standalone ADR-0026 proxy trial tool (wired-check exempt)'],
96
+ ]);
97
+ const files = tracked.filter((f) => /^(scripts|plugin|bin|kb)\/.+\.mjs$/.test(f) && !allowed.has(f) && re.test(read(f)));
98
+ return none(files);
99
+ } },
100
+ { id: 'B8', area: 'rules', scope: 'repo', title: 'ADRs governing the live corpus publisher are not left "Proposed"',
101
+ run: () => none(tracked.filter((f) => /^docs\/adr\/008[56]-/.test(f)).filter((f) => /^status:\s*Proposed/im.test(read(f)))) },
102
+ { id: 'B9', area: 'rules', scope: 'repo', title: 'All version surfaces carry one version',
103
+ run: () => {
104
+ const v = [['package.json', pkg.version], ['kb/package.json', json(read('kb/package.json')).version],
105
+ ['plugin/.claude-plugin/plugin.json', json(read('plugin/.claude-plugin/plugin.json')).version],
106
+ ['plugin/.codex-plugin/plugin.json', json(read('plugin/.codex-plugin/plugin.json')).version]].filter(([, x]) => x);
107
+ return { ok: new Set(v.map(([, x]) => x)).size === 1, detail: v.map(([f, x]) => `${f}=${x}`).join(' ') };
108
+ } },
109
+ { id: 'B10', area: 'rules', scope: 'repo', title: 'Public pages show exactly one current version (no candidate/public split)',
110
+ run: () => none(grepIn(['README.md', 'explainer/index.html'], /candidate preview|public npm v\d/i)) },
111
+ { id: 'B11', area: 'rules', scope: 'repo', title: 'The ADR index lists every ADR, with the status the ADR itself declares',
112
+ run: () => {
113
+ const idx = read('docs/adr/README.md');
114
+ const adrs = tracked.filter((f) => /^docs\/adr\/\d{4}-.+\.md$/.test(f));
115
+ const missing = adrs.filter((f) => !idx.includes(path.basename(f))).map((f) => `not indexed: ${f}`);
116
+ const drift = adrs.flatMap((f) => {
117
+ const own = (read(f).match(/^status:\s*([A-Za-z]+)/m) || [])[1];
118
+ const statusTable = idx.slice(Math.max(0, idx.indexOf('| ADR | Title | Status |')));
119
+ const row = statusTable.split('\n').find((l) => l.includes(`(${path.basename(f)})`));
120
+ const listed = row && (row.match(/\|\s*([A-Za-z]+)[^|]*\|\s*$/) || [])[1];
121
+ return own && listed && own.toLowerCase() !== listed.toLowerCase() ? [`${path.basename(f)}: ADR says ${own}, index says ${listed}`] : [];
122
+ });
123
+ return none([...missing, ...drift]);
124
+ } },
125
+
126
+ { id: 'B12', area: 'rules', scope: 'machine', title: 'The local (untracked) CLAUDE.md / AGENTS.md carry no contradicting instruction',
127
+ run: () => {
128
+ const main = path.join(HOME, 'Code/ruvnet-brain');
129
+ const bad = /npx (-y )?(@claude-flow|claude-flow|ruflo)\b|standing authori[sz]ation permits|no separate human reviewer click|npm run (build|dev|test:integration|test:coverage|test:security)\b|not direct Agent tool/i;
130
+ return none(['CLAUDE.md', 'AGENTS.md'].flatMap((f) => readAbs(path.join(main, f)).split('\n')
131
+ .map((l, i) => (bad.test(l) ? `${f}:${i + 1}: ${l.trim().slice(0, 120)}` : null)).filter(Boolean)));
132
+ } },
133
+
134
+ // C — one release path
135
+ { id: 'C1', area: 'release', scope: 'repo', title: 'Unattended corpus promotion stays disarmed until a code release is install-verified',
136
+ run: () => ({ ok: !tracked.includes('data/approved-runtime.json'), detail: tracked.includes('data/approved-runtime.json') ? 'data/approved-runtime.json present (arms the 07:17 UTC nightly)' : 'absent' }) },
137
+ { id: 'C2', area: 'release', scope: 'repo', title: 'Every scheduled workflow pages the phone on failure',
138
+ run: () => {
139
+ const alerts = read('.github/workflows/ntfy-alerts.yml');
140
+ return none(workflows.filter((w) => /^\s*schedule:/m.test(read(w)) && !w.endsWith('ntfy-alerts.yml'))
141
+ .map((w) => ((read(w).match(/^name:\s*(.+)$/m) || [])[1] || w).trim().replace(/['"]/g, ''))
142
+ .filter((n) => !alerts.includes(n)));
143
+ } },
144
+ { id: 'C4', area: 'release', scope: 'machine', title: 'The corpus gist job has its authenticated token (RUVNET_GISTS_TOKEN)',
145
+ run: () => { const r = spawnSync('gh', ['secret', 'list', '-R', 'stuinfla/ruvnet-brain'], { encoding: 'utf8' }); return { ok: /^RUVNET_GISTS_TOKEN\b/m.test(r.stdout), detail: r.stdout.split('\n').map((l) => l.split(/\s/)[0]).filter(Boolean).join(', ') }; } },
146
+ { id: 'C3', area: 'release', scope: 'machine', title: 'Publishing requires the owner: Production environment has a required reviewer',
147
+ run: () => {
148
+ const r = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/environments', '-q',
149
+ '.environments[]|select(.name=="Production – ruvnet-brain")|[.protection_rules[]?|select(.type=="required_reviewers")]|length'], { encoding: 'utf8' });
150
+ return { ok: r.status === 0 && Number(r.stdout.trim()) > 0, detail: `required_reviewers rules: ${r.stdout.trim() || r.stderr.trim()}` };
151
+ } },
152
+
153
+ // D — one corpus / update path
154
+ { id: 'D1', area: 'corpus', scope: 'repo', title: 'The end-user updater label is defined once; the one necessary copy (standalone runner) matches it',
155
+ run: () => {
156
+ const owner = (read('plugin/scripts/nightly-scheduler.mjs').match(/NIGHTLY_LABEL\s*=\s*'([^']+)'/) || [])[1];
157
+ const copies = tracked.filter((f) => /\.(mjs|sh)$/.test(f) && !/^tests\//.test(f) && f !== 'plugin/scripts/nightly-scheduler.mjs')
158
+ .filter((f) => /\^com\\\.ruvnet\\\.brain-update|'com\.ruvnet\.brain-update'/.test(read(f)));
159
+ // bin/nightly-refresh.mjs is copied to a standalone content-addressed path and executed outside the package, so it cannot import.
160
+ const extra = copies.filter((f) => f !== 'bin/nightly-refresh.mjs');
161
+ // The runner MUST carry the owner's exact label (it validates its own registration against it);
162
+ // testing only files that already contain the label would miss exactly the drifted case.
163
+ const runner = read('bin/nightly-refresh.mjs');
164
+ const esc = owner.replace(/\./g, '\\.');
165
+ const drift = !owner ? ['owner label not found in plugin/scripts/nightly-scheduler.mjs']
166
+ : (!runner.includes(`'${owner}'`) || !runner.includes(`/^${esc}\\.proof-`)) ? [`bin/nightly-refresh.mjs does not carry owner label ${owner}`] : [];
167
+ return none([...extra, ...drift]);
168
+ } },
169
+
170
+ { id: 'D2', area: 'corpus', scope: 'machine', title: 'Installed knowledge matches the installed plugin version',
171
+ run: () => {
172
+ const tag = String(json(readAbs(path.join(HOME, '.cache/ruvnet-brain/kb/SOURCE.json'))).releaseTag || '').replace(/^v/, '');
173
+ const active = json(readAbs(path.join(HOME, '.cache/ruvnet-brain/active.json'))).version;
174
+ return { ok: !!tag && tag === active, detail: `knowledge ${tag || 'unknown'} vs plugin ${active || 'unknown'}` };
175
+ } },
176
+ { id: 'D3', area: 'corpus', scope: 'machine', title: 'Exactly one update owner exists, and its last refresh run passed within 26h',
177
+ run: () => {
178
+ const brainJob = spawnSync('launchctl', ['print', `gui/${process.getuid()}/com.ruvnet.brain-update`], { encoding: 'utf8' }).status === 0;
179
+ const akJob = spawnSync('launchctl', ['print', `gui/${process.getuid()}/com.stuartkerr.ak-sync`], { encoding: 'utf8' }).status === 0;
180
+ const dir = path.join(HOME, '.cache/ruvnet-brain/refresh-runs');
181
+ const runs = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.json')).sort() : [];
182
+ const last = runs.length ? json(readAbs(path.join(dir, runs.at(-1)))) : {};
183
+ const fresh = last.startedAt && (Date.now() - Date.parse(last.startedAt)) < 26 * 3600e3;
184
+ const owners = [brainJob && 'brain-update', akJob && 'ak-sync'].filter(Boolean);
185
+ return { ok: owners.length === 1 && fresh && last.terminalVerdict !== 'failed', detail: `owners=[${owners.join(',') || 'none'}] last run ${last.startedAt || 'none'} verdict ${last.terminalVerdict || last.status || 'none'}` };
186
+ } },
187
+
188
+ // E — one hook / context plane
189
+ { id: 'E1', area: 'hooks', scope: 'machine', title: 'No stale autonomous-loop checkpoint is injected into sessions',
190
+ // H3: judged by the checkpoint's OWN `updatedAt` claim (loop-checkpoint.mjs's checkpointStaleness
191
+ // — the SAME rule ground-ruvnet.sh's `stale` verb uses), never by file mtime. A copy/rsync/restore
192
+ // resets mtime without the checkpoint's content changing, which is exactly the kind of second,
193
+ // drifting definition of "stale" this audit exists to catch — including, previously, in itself.
194
+ run: () => {
195
+ const f = path.join(ROOT, '.ruvnet-brain/checkpoint.json');
196
+ const main = path.join(HOME, 'Code/ruvnet-brain/.ruvnet-brain/checkpoint.json');
197
+ const stale = [f, main].filter((p) => existsSync(p) && checkpointStaleness(readCheckpoint(p)).stale);
198
+ return none(stale);
199
+ } },
200
+ { id: 'E2', area: 'hooks', scope: 'machine', title: 'Only one hook writes session snapshots (no global + plugin double writer)',
201
+ run: () => { const s = readAbs(path.join(HOME, '.claude/settings.json')); return { ok: !/agentdb-autocapture/.test(s), detail: /agentdb-autocapture/.test(s) ? 'global agentdb-autocapture still registered alongside the plugin session-snapshot' : 'single writer' }; } },
202
+
203
+ { id: 'E3', area: 'hooks', scope: 'machine', title: 'Lessons live in one store (no .swarm lesson-* rows outside the plugin lesson store)',
204
+ run: () => { const r = spawnSync('ruflo', ['memory', 'list', '--path', path.join(HOME, 'Code/ruvnet-brain/.swarm/memory.db'), '--limit', '500'], { encoding: 'utf8', timeout: 60000 });
205
+ const n = (r.stdout.match(/lesson-/g) || []).length; return { ok: n === 0, detail: `${n} lesson-* rows in project .swarm (plugin store: ~/.config/ruvnet-brain/lessons.json)` }; } },
206
+ { id: 'E4', area: 'hooks', scope: 'machine', title: 'One session-start continuity restorer (global hook stands down when the plugin is enabled)',
207
+ run: () => { const s = readAbs(path.join(HOME, '.claude/hooks/agentdb-ensure.sh')); return { ok: s.includes('SINGLE CONTINUITY OWNER'), detail: 'agentdb-ensure.sh must stand down its project-state recall' }; } },
208
+
209
+ // F — this Mac
210
+ { id: 'F1', area: 'machine', scope: 'machine', title: 'No plaintext API keys in ~/.claude/settings.json',
211
+ run: () => none(readAbs(path.join(HOME, '.claude/settings.json')).split('\n')
212
+ .filter((l) => /(api-key-|sk-[A-Za-z0-9]{20,}|_TOKEN"\s*:\s*"[^"$]{16,})/.test(l)).map((l) => l.replace(/:\s*".*"/, ': "<redacted>"').trim())) },
213
+ { id: 'F2', area: 'machine', scope: 'machine', title: 'Every required scheduled job proves it ran (repo watchdog)',
214
+ run: () => { const r = spawnSync(process.execPath, [path.join(ROOT, 'scripts/nightly-watchdog.mjs')], { cwd: ROOT, encoding: 'utf8', timeout: 60000 }); return { ok: r.status === 0, detail: `${r.stdout}${r.stderr}`.trim().split('\n').slice(-6).join('\n') }; } },
215
+ { id: 'F3', area: 'machine', scope: 'machine', title: 'Scheduled jobs use the one Node the shell uses (no /usr/local/bin/node v22)',
216
+ run: () => {
217
+ const dir = path.join(HOME, 'Library/LaunchAgents');
218
+ const plists = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.plist')) : [];
219
+ return none(plists.filter((f) => readAbs(path.join(dir, f)).includes('/usr/local/bin/node')));
220
+ } },
221
+ { id: 'F4', area: 'machine', scope: 'machine', title: 'Permission allow-list names the real Ruflo MCP server',
222
+ run: () => { const s = readAbs(path.join(HOME, '.claude/settings.json')); return { ok: !s.includes('"mcp__ruflo__*"') || s.includes('"mcp__claude-flow__*"'), detail: 'allow list names mcp__ruflo__* but the server is claude-flow' }; } },
223
+
224
+ // G — one release state
225
+ { id: 'G1', area: 'state', scope: 'machine', title: 'Local main is exactly origin/main',
226
+ run: () => { const m = git('rev-parse', 'main'); const o = git('rev-parse', 'origin/main'); return { ok: m === o, detail: `main ${m.slice(0, 8)} origin ${o.slice(0, 8)}` }; } },
227
+ { id: 'G2', area: 'state', scope: 'machine', title: 'npm latest, GitHub latest release and main carry the same version',
228
+ run: () => {
229
+ const npm = spawnSync('npm', ['view', 'ruvnet-brain', 'dist-tags.latest'], { encoding: 'utf8' }).stdout.trim();
230
+ const gh = spawnSync('gh', ['api', 'repos/stuinfla/ruvnet-brain/releases/latest', '-q', '.tag_name'], { encoding: 'utf8' }).stdout.trim().replace(/^v/, '');
231
+ const main = json(git('show', 'origin/main:package.json')).version;
232
+ return { ok: npm === gh && gh === main, detail: `npm ${npm} · github ${gh} · main ${main}` };
233
+ } },
234
+ ];
235
+
236
+ const selected = checks.filter((c) => c.scope === 'repo' || MACHINE);
237
+ const results = selected.map((c) => { let r; try { r = c.run(); } catch (e) { r = { ok: false, detail: `check crashed: ${e.message}` }; } return { ...c, ...r }; });
238
+
239
+ // R1 — the register may not claim more than the evidence shows. A row in docs/WORK-REGISTER.md whose
240
+ // State is LIVE or STAGED must have every check it names passing (machine checks only in --machine).
241
+ {
242
+ const byId = new Map(results.map((r) => [r.id, r]));
243
+ const scopeOf = new Map(checks.map((c) => [c.id, c.scope]));
244
+ const bad = [];
245
+ for (const line of read('docs/WORK-REGISTER.md').split('\n')) {
246
+ const cells = line.split('|').map((x) => x.trim());
247
+ if (cells.length < 7 || !/^\d+$/.test(cells[1])) continue;
248
+ const [, num, family, , state, checkList] = cells;
249
+ if (!/^(LIVE|STAGED)$/.test(state)) continue;
250
+ const ids = checkList.split(/[ ,]+/).filter(Boolean);
251
+ if (!ids.length) bad.push(`#${num} ${family}: claims ${state} but names no check`);
252
+ for (const id of ids) {
253
+ if (!scopeOf.has(id)) { bad.push(`#${num}: unknown check ${id}`); continue; }
254
+ if (scopeOf.get(id) === 'machine' && !MACHINE) continue;
255
+ const r = byId.get(id);
256
+ if (!r?.ok) bad.push(`#${num} ${family}: claims ${state} but ${id} fails`);
257
+ }
258
+ }
259
+ results.push({ id: 'R1', area: 'register', scope: 'repo', title: 'The work register claims nothing its checks do not prove', ok: bad.length === 0, detail: bad.join('\n') || 'none' });
260
+ }
261
+ if (JSON_OUT) console.log(JSON.stringify(results.map(({ run, ...r }) => r), null, 1));
262
+ else {
263
+ for (const r of results) {
264
+ console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.id.padEnd(4)} [${r.area}] ${r.title}`);
265
+ if (!r.ok) for (const l of String(r.detail).split('\n')) console.log(` ${l}`);
266
+ }
267
+ const failed = results.filter((r) => !r.ok).length;
268
+ console.log(`\n${results.length - failed}/${results.length} pass${MACHINE ? ' (repo + machine)' : ' (repo)'}`);
269
+ }
270
+ process.exitCode = results.every((r) => r.ok) ? 0 : 1;
@@ -14,10 +14,14 @@ export const API_BILLING_ENV = Object.freeze([
14
14
  'GEMINI_API_KEY',
15
15
  'XAI_API_KEY',
16
16
  ]);
17
+ export const PRIVATE_SIGNING_ENV = Object.freeze([
18
+ 'RUVNET_SIGNING_KEY', 'RUVNET_FABLE_REVIEW_SIGNING_KEY', 'RUVNET_ASTRA_REVIEW_SIGNING_KEY',
19
+ ]);
17
20
 
18
21
  export function subscriptionOnlyEnv(parent = process.env) {
19
22
  const child = { ...parent, RUVNET_SUBSCRIPTION_ONLY: '1' };
20
23
  for (const name of API_BILLING_ENV) delete child[name];
24
+ for (const name of PRIVATE_SIGNING_ENV) delete child[name];
21
25
  return child;
22
26
  }
23
27
 
@@ -41,6 +41,14 @@ export function writeExplainerBadgeVersion(s, version) {
41
41
  return s.replace(/(&middot;\s*v)\d+\.\d+\.\d+(?:-[\w.-]+)?/, `$1${version}`);
42
42
  }
43
43
 
44
+ // The explainer once showed a second, competing version next to the product version
45
+ // ("candidate preview" ... "public npm vX.Y.Z") — two versions on one public page, exactly the
46
+ // defect single-source-check.mjs check B10 exists to catch (fixed 2026-09-26). This regression
47
+ // guard keeps it from silently coming back.
48
+ export function hasSplitVersionLabel(s) {
49
+ return /candidate preview|public npm v\d/i.test(s);
50
+ }
51
+
44
52
  // Each target: a file, a regex to find the version-bearing line, and the corrected line.
45
53
  const targets = [
46
54
  { // Codex plugin manifest — same product, same release train as the Claude manifest
@@ -160,6 +168,20 @@ if (CHECK && fs.existsSync(path.join(ROOT, 'kb/RVF-GENERATIONS.json'))) {
160
168
  }
161
169
  }
162
170
 
171
+ // Guard: the explainer must never again show a second, competing version next to the product
172
+ // version (the "candidate preview" / "public npm vX.Y.Z" split fixed 2026-09-26 — see
173
+ // hasSplitVersionLabel() above and single-source-check.mjs check B10). This is a content
174
+ // regression, not a mechanical one, so --check only reports it; there is no auto-fix.
175
+ {
176
+ const p = path.join(ROOT, 'explainer/index.html');
177
+ if (fs.existsSync(p) && hasSplitVersionLabel(fs.readFileSync(p, 'utf8'))) {
178
+ if (CHECK) {
179
+ console.error('[version] DRIFT: explainer/index.html shows a second/candidate version label ("candidate preview" / "public npm vX") — the page must show exactly one current version');
180
+ drift++;
181
+ }
182
+ }
183
+ }
184
+
163
185
  // Guard: no stray hardcoded product-version literal anywhere in the source tree — every code path must
164
186
  // read getVersion() (or inherit a synced literal via the `targets` above / the README badge). Rather than
165
187
  // a fixed 3-file allowlist (which silently missed strays in every other file), walk the WHOLE tree and
@@ -3,6 +3,7 @@
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import path from 'node:path';
6
+ import { TRI_HOST_MODEL_IDS } from './review-model-defaults.mjs';
6
7
 
7
8
  const ROOT = path.dirname(fileURLToPath(import.meta.url));
8
9
  const DUAL = path.join(ROOT, 'dual-host-deliberation.mjs');
@@ -24,9 +25,8 @@ if (!task) { usage(); process.exit(2); }
24
25
 
25
26
  if (dryRun) {
26
27
  const providers = mode === 'dual' ? ['claude', 'codex'] : ['claude', 'codex', 'grok'];
27
- console.log(JSON.stringify({ status: 'ready', mode, providers, models: {
28
- claude: 'claude-fable-5-1', codex: 'gpt-6-astra', grok: 'grok-4.6',
29
- }, billingPath: 'native subscription/OAuth CLI; API-key variables unset', taskHash: task.length }, null, 2));
28
+ console.log(JSON.stringify({ status: 'ready', mode, providers, models: TRI_HOST_MODEL_IDS,
29
+ billingPath: 'native subscription/OAuth CLI; API-key variables unset', taskHash: task.length }, null, 2));
30
30
  process.exit(0);
31
31
  }
32
32
 
@@ -78,7 +78,6 @@ const STANDALONE = [
78
78
  ['dream-issue-gate', 'pure Dream Cycle disposition policy; invoked by the external issue adapter, never a GitHub writer'],
79
79
  ['sync-census', 'explicit maintainer census writer; a destructive source-to-surface refresh is never scheduled'],
80
80
  ['sync-commands', 'explicit maintainer alias synchronizer; run deliberately before release, never from a lifecycle hook'],
81
- ['version-bump-gate', 'retired automatic interceptor; explicit version checks own release validation'],
82
81
  ['project-progression-checkpoint', 'the body of the shipped `/ruvnet-brain:checkpoint` command '
83
82
  + '(plugin/commands/checkpoint.md:39; added a7167b6b 2026-09-11). The command host executes that '
84
83
  + 'instruction, and this check does not scan command markdown — the same shape as '
@@ -89,6 +88,10 @@ const STANDALONE = [
89
88
  + '.claude/worktrees/ — their package.json copies had been counted as callers all day.'],
90
89
  ['lesson-seed', 'one-shot seeding, run deliberately by a human'],
91
90
  ['lesson-ratify', 'the human control surface — a CLI is its entire purpose'],
91
+ ['lesson-migrate-agentdb', 'one-time reconciliation between the two AgentDB lesson stores and the '
92
+ + 'one plugin lesson store, run deliberately by a human/model, never scheduled. Completion is '
93
+ + 'proven and kept honest by tests/unit/lesson-migrate-agentdb.test.mjs (0 pending rows), a drift '
94
+ + 'canary rather than automated invocation — the same shape as lesson-seed/lesson-ratify above.'],
92
95
  ['stamp-sweep', 'ADR-056 §2 — the ONE-TIME backfill half of the stamp rule. A human runs it once '
93
96
  + '(--apply) to reach the files nobody is editing; the ongoing half is the md-stamp PostToolUse '
94
97
  + 'hook, which IS wired. Deliberately not in a gate: it WRITES to documents, and a writer that '
@@ -109,9 +112,6 @@ const STANDALONE = [
109
112
  ['fix-metaharness-memretrieve', 'one-shot historical repair; kept for the record'],
110
113
  ['gen-console-images', 'build-time asset generation, run by hand'],
111
114
  ['adr-backfill', 'one-shot backfill by a human; its result is enforced by adr-format.test.mjs'],
112
- ['stamp-existing-rvf-generations', 'one-shot maintainer migration that binds already-built canonical '
113
- + 'RVFs to RVF-GENERATIONS.json and optionally prunes legacy sidecars; build-bundle.mjs consumes '
114
- + 'and validates the resulting manifest, so scheduling this destructive migration would be wrong'],
115
115
  ['release', 'the ship path, run by a human'],
116
116
  ['execution-preflight', 'external orchestration boundary — invoked by the host before consequential Ruflo/Codex execution; no in-repo caller exists because the host supplies the live Brain and AgentDB receipts'],
117
117
  ['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
@@ -230,6 +230,27 @@ const STANDALONE = [
230
230
  + 'ground-before-write. Per ADR-0014 ownership moved to the Kling skill (confirmed live: a copy '
231
231
  + 'ships at ~/.claude/skills/klingai/scripts/kling-preflight.sh); NOT currently wired into any '
232
232
  + 'settings.json there either — honestly dormant until a user opts in, not a silent gap'],
233
+
234
+ // H5 (2026-09-26 dead-code audit): decision-gate.mjs's REGISTRY['bash'] was the only production
235
+ // caller of these three — never registered in plugin/hooks/hooks.json or codex-hooks.json
236
+ // (continuity-hook-policy.mjs's own header: "remains reachable through hook-shim's dispatch table
237
+ // by explicit invocation" only). Removing that dead route makes each of these genuinely uncalled
238
+ // by any product path. Not deleted: each exports pure, independently-tested logic with its own
239
+ // passing suite — identifier-preflight.test.mjs / spend-guard.test.mjs import and exercise these
240
+ // functions directly, and tests/unit/lesson-gate.test.mjs imports degradation-watch.mjs's
241
+ // `dependentEvent` to cross-check lesson-hooks.sh's own pattern. A test importing an export is not
242
+ // a product caller (same distinction this file already draws for correction-detect-measure above),
243
+ // so these are honestly STANDALONE rather than silently left to read as still-wired.
244
+ ['identifier-preflight', 'H5: orphaned by decision-gate.mjs\'s dead bash route (REGISTRY[\'bash\'] '
245
+ + 'removed 2026-09-26). Pure `check`/`identifierIn` logic remains directly imported and exercised '
246
+ + 'by its own tests/unit/identifier-preflight.test.mjs; no product path calls it'],
247
+ ['spend-guard', 'H5: orphaned by decision-gate.mjs\'s dead bash route (REGISTRY[\'bash\'] removed '
248
+ + '2026-09-26). Its logic remains directly imported and exercised by its own '
249
+ + 'tests/unit/spend-guard.test.mjs; no product path calls it'],
250
+ ['degradation-watch', 'H5: orphaned by decision-gate.mjs\'s dead bash route (REGISTRY[\'bash\'] '
251
+ + 'removed 2026-09-26). Its `dependentEvent` export remains directly imported by '
252
+ + 'tests/unit/lesson-gate.test.mjs (cross-checked against lesson-hooks.sh\'s own pattern) and by '
253
+ + 'its own test file; no product path calls it'],
233
254
  ];
234
255
  // REMOVED 2026-07-22, each verified before removal:
235
256
  // check-legibility / check-indexation / status-honesty — claimed "invoked from the workflow";
@@ -889,9 +910,24 @@ export function hookWiringAudit({
889
910
  const scriptsDir = path.join(repo, 'plugin/scripts');
890
911
  let all = [];
891
912
  try { all = fs.readdirSync(scriptsDir).filter((f) => /\.(mjs|sh)$/.test(f)); } catch { /* no dir */ }
913
+ // H6 (2026-09-26 dead-code audit): hook-shim.mjs is a DISPATCH TABLE, not a script that
914
+ // unconditionally executes everything it mentions. `scanConfig()` above already proves reachability
915
+ // for a TABLE entry CORRECTLY — only when its id is genuinely dispatched by a real command string in
916
+ // a manifest. Letting hook-shim.mjs ALSO participate as a "from" in the generic fixed-point pass
917
+ // below double-counts every OTHER entry in its TABLE as "spawned", because the pass's own predicate
918
+ // (callerPattern) matches any quoted filename — and every TABLE entry is, by construction, a quoted
919
+ // filename in hook-shim.mjs's source, dispatched or not. Measured: before this exclusion,
920
+ // design-wall.sh, route-dispatch.sh, verify-interface.sh, learn-capture.sh, learn-flush.mjs,
921
+ // md-stamp.mjs, signal-watch.mjs, routing-outcome-capture.mjs and swarm-slot-recycler.mjs all read
922
+ // "wired · via spawned by plugin/scripts/hook-shim.mjs" while zero manifest (hooks.json,
923
+ // codex-hooks.json, this repo's or the user's settings.json) ever dispatches most of their ids —
924
+ // exactly the false positive this check exists to catch. Excluding it here does not remove any
925
+ // GENUINE reachability: every id hook-shim.mjs's TABLE actually dispatches was already added by
926
+ // scanConfig() above, before this loop runs at all.
892
927
  for (let i = 0; i < 10; i++) {
893
928
  let changed = false;
894
929
  for (const from of [...reached.keys()]) {
930
+ if (from === 'hook-shim.mjs') continue;
895
931
  const src = sourceCache.commentFree(path.join(scriptsDir, from));
896
932
  if (src === null) continue;
897
933
  for (const cand of all) {
Binary file
@@ -1,124 +0,0 @@
1
- #!/bin/bash
2
- # version-bump-gate.sh — PreToolUse gate on Bash. EVERY PUSH CARRIES A VERSION INCREMENT.
3
- #
4
- # ─────────────────────────────────────────────────────────────────────────────────────────────
5
- # WHY (2026-07-13). Stuart, after a push with no bump served him stale code: "Every single
6
- # commit to GitHub should come with a version increment: major.minor.bugfix. It's the only way
7
- # we're gonna know beyond a shadow of a doubt what's going on. When other things are looking to
8
- # read a change in version to know that there's something they need to be aware of, that's not
9
- # negotiable."
10
- #
11
- # THE INCIDENT: gates/fleet-doctor//savings were pushed under an unchanged 2.5.2. The plugin
12
- # cache compared 2.5.2 == 2.5.2, correctly served the STALE copy, and a restarted session
13
- # loaded without /savings. THE VERSION NUMBER IS THE UPDATE SIGNAL — a push without a bump is
14
- # an update nothing can see.
15
- #
16
- # Prior art is rUv's own: agent-harness-generator preflight.mjs refuses to greenlight a release
17
- # when package versions drift, and version-bump.mjs bumps every manifest in lockstep — the gate
18
- # is the product. This is that discipline applied at the push boundary.
19
- #
20
- # CONTRACT: exit 0 = allow · exit 2 + stderr = BLOCK. FAILS OPEN on anything unparseable.
21
- # Opt-in (router profile.json); local Git checks and a Node maintenance-state check.
22
- # ─────────────────────────────────────────────────────────────────────────────────────────────
23
-
24
- set -uo pipefail
25
-
26
- # Reversible, repository-scoped development maintenance; no profile, ledger, or Git work first.
27
- MAINTENANCE_HELPER="${BASH_SOURCE[0]%/*}/development-maintenance.mjs"
28
- if [ -f "$MAINTENANCE_HELPER" ] && node --input-type=module -e '
29
- import { pathToFileURL } from "node:url";
30
- const { developmentHooksSuspended } = await import(pathToFileURL(process.argv[1]));
31
- process.exit(developmentHooksSuspended() ? 0 : 1);
32
- ' "$MAINTENANCE_HELPER" >/dev/null 2>&1; then
33
- exit 0
34
- fi
35
-
36
- INPUT=""
37
- # BOUNDED READ (2026-07-27, ADR-055 F20): an unqualified `read` never returns on a stdin that is
38
- # opened and never closed — measured across the mesh, 18 of 37 registered commands sat until the
39
- # harness killed them. Real Claude Code writes and closes, so this costs no normal turn; that is
40
- # exactly why a hook that CAN hang forever survives unnoticed. -t bounds the wait, and the string
41
- # is truncated AFTER the loop because a hook payload is one line with no newline, so `read` hands
42
- # the whole thing back at once and a per-iteration cap never fires.
43
- while IFS= read -r -t 2 _l; do
44
- INPUT+="$_l"
45
- [ ${#INPUT} -ge 65536 ] && break
46
- done
47
- [ -n "$_l" ] && INPUT+="$_l"
48
- INPUT="${INPUT:0:65536}"
49
- [ -n "$INPUT" ] || exit 0
50
-
51
- PROFILE="${MODEL_ROUTER_PROFILE:-$HOME/.claude/model-router/profile.json}"
52
- [ -f "$PROFILE" ] || exit 0
53
- [ "${RUVNET_SKIP_VERSION_GATE:-0}" = "1" ] && exit 0
54
-
55
- # Consume JSON escape pairs so quoted commit messages do not truncate the command before a
56
- # following git push. This retains the existing bounded Bash parser's matching behavior.
57
- field() { local re="\"$1\"[[:space:]]*:[[:space:]]*\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\""; [[ $INPUT =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"; }
58
- [ "$(field tool_name)" = "Bash" ] || exit 0
59
- CMD=$(field command)
60
- [[ $CMD == *"git push"* ]] || exit 0
61
-
62
- ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
63
-
64
- # The version source of truth, in precedence order. A repo with neither is not version-managed
65
- # by this convention — pass untouched.
66
- SRC=""
67
- for c in plugin/.claude-plugin/plugin.json package.json; do
68
- [ -f "$ROOT/$c" ] && { SRC="$c"; break; }
69
- done
70
- [ -n "$SRC" ] || exit 0
71
-
72
- UP=$(git -C "$ROOT" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || UP=origin/main
73
- git -C "$ROOT" rev-parse --verify "$UP" >/dev/null 2>&1 || exit 0
74
-
75
- AHEAD=$(git -C "$ROOT" rev-list --count "$UP..HEAD" 2>/dev/null) || exit 0
76
- [ "$AHEAD" -gt 0 ] || exit 0 # nothing outgoing → nothing to gate
77
-
78
- ver_at() { # ver_at <rev> — version string in $SRC at that rev, empty on any failure
79
- local j re='"version"[[:space:]]*:[[:space:]]*"([^"]*)"'
80
- j=$(git -C "$ROOT" show "$1:$SRC" 2>/dev/null) || return 0
81
- [[ $j =~ $re ]] && printf '%s' "${BASH_REMATCH[1]}"
82
- }
83
- V_HEAD=$(ver_at HEAD); V_UP=$(ver_at "$UP")
84
- [ -n "$V_HEAD" ] && [ -n "$V_UP" ] || exit 0 # can't read either side → fail open
85
- [ "$V_HEAD" != "$V_UP" ] && exit 0 # bumped → pass
86
-
87
- # ── LESSON STORE CONSULTATION (ADR-030 L3, wired 2026-07-22) ────────────────────────────────────
88
- # This gate hardcoded its own message for months while lesson L05 — the identical rule, recorded 52
89
- # times across 4 projects — sat in a store that nothing read. A grep for `lessonsFor` across every
90
- # gate returned zero: lessons were mined, weighted, trust-boundaried, and consumed by nobody.
91
- #
92
- # Now the store speaks here. Today every lesson is an unratified candidate, so this ADDS the lesson's
93
- # own words and evidence to the refusal below rather than changing whether it fires. The day the
94
- # owner ratifies L05, the same wire starts refusing on the lesson's authority instead of on this
95
- # script's hardcoded copy — with no change to this file. Enforcement is data, not a rewrite.
96
- #
97
- # Fails open by construction: any error, missing store, or absent node exits this block silently. A
98
- # gate that blocks a push because it could not read a config file is a gate people switch off.
99
- LESSON_GATE="$(cd "$(dirname "$0")/../.." 2>/dev/null && pwd)/scripts/lesson-gate.mjs"
100
- if [ -f "$LESSON_GATE" ] && command -v node >/dev/null 2>&1; then
101
- LESSON_OUT="$(node "$LESSON_GATE" --trigger ship 2>/dev/null || true)"
102
- fi
103
-
104
- read -r -d '' MSG <<EOF || true
105
- ⛔ BLOCKED — this push carries $AHEAD commit(s) but NO version increment ($SRC still $V_UP).
106
-
107
- THE VERSION NUMBER IS THE UPDATE SIGNAL. Plugin caches, update checks, and other sessions decide
108
- whether to pull fresh code by comparing versions — a push without a bump is an update nothing can
109
- see. This exact miss served a restarted session stale 2.5.2 without /savings on 2026-07-13.
110
-
111
- Before pushing:
112
- 1. bump "version" in $SRC (major.minor.bugfix)
113
- 2. node scripts/sync-version.mjs # syncs every surface, --check must pass
114
- 3. include the bump in the outgoing commits, then push
115
-
116
- (Stuart, 2026-07-13: "that's not negotiable." Deliberate override, say why out loud:
117
- RUVNET_SKIP_VERSION_GATE=1)
118
- ${LESSON_OUT:+
119
- ── from your own lesson store ─────────────────────────────────────────────
120
- $LESSON_OUT}
121
- EOF
122
- bash "$(dirname "${BASH_SOURCE[0]}")/gate-receipt.sh" version-bump-gate "push" "commits carried no version bump — an update nothing can see" 2>/dev/null || true
123
- printf '%s\n' "$MSG" >&2
124
- exit 2
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
-
5
- const ROOT = path.resolve(import.meta.dirname, '../..');
6
- const dir = path.resolve(ROOT, process.env.QE_RECEIPT_DIR || '.qe/receipts');
7
- const required = ['preflight', 'check', 'integration', 'release', 'resources', 'windows-unit'];
8
- const failures = [];
9
- const receipts = required.map((lane) => {
10
- const file = path.join(dir, `${lane}.json`);
11
- if (!fs.existsSync(file)) { failures.push(`${lane}: receipt missing`); return null; }
12
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) {
13
- failures.push(`${lane}: invalid receipt (${error.message})`); return null;
14
- }
15
- });
16
- const shas = new Set(receipts.filter(Boolean).map((receipt) => receipt.sha));
17
- if (shas.size !== 1) failures.push(`receipts are not bound to one SHA: ${[...shas].join(', ') || '<none>'}`);
18
- const runIds = new Set(receipts.filter(Boolean).map((receipt) => receipt.runId));
19
- if (runIds.size !== 1) failures.push(`receipts are not bound to one run: ${[...runIds].join(', ') || '<none>'}`);
20
- for (const [index, receipt] of receipts.entries()) {
21
- const lane = required[index];
22
- if (!receipt) continue;
23
- if (receipt.schema !== 'ruvnet-brain.agentic-qe.receipt' || receipt.contract !== 'agentic-qe-4.3') {
24
- failures.push(`${lane}: wrong receipt contract`);
25
- }
26
- if (receipt.lane !== lane || receipt.status !== 'PASS') failures.push(`${lane}: status ${receipt.status || '<missing>'}`);
27
- for (const step of receipt.steps || []) {
28
- if (step.status !== 'PASS') failures.push(`${lane} step ${step.index}: ${step.status}`);
29
- if (step.tests && (step.tests.total === 0 || step.tests.skipped > 0)) {
30
- failures.push(`${lane} step ${step.index}: skipped or empty test result`);
31
- }
32
- }
33
- }
34
- const result = {
35
- schema: 'ruvnet-brain.agentic-qe.aggregate', contract: 'agentic-qe-4.3',
36
- sha: shas.size === 1 ? [...shas][0] : null, required,
37
- status: failures.length ? 'FAIL' : 'PASS', failures, receipts,
38
- };
39
- fs.mkdirSync(dir, { recursive: true });
40
- fs.writeFileSync(path.join(dir, 'aggregate.json'), `${JSON.stringify(result, null, 2)}\n`);
41
- console.log(JSON.stringify({ status: result.status, sha: result.sha, failures }));
42
- process.exitCode = failures.length ? 1 : 0;