ruvnet-brain 4.0.1 → 4.0.4

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 (195) hide show
  1. package/.claude-plugin/marketplace.json +1 -0
  2. package/README.md +4 -4
  3. package/bin/install.mjs +303 -24
  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/keys/ruvnet-brain-signing.pub.pem +3 -0
  25. package/package.json +8 -22
  26. package/plugin/.claude-plugin/marketplace.json +1 -0
  27. package/plugin/.claude-plugin/plugin.json +2 -3
  28. package/plugin/.codex-plugin/plugin.json +1 -1
  29. package/plugin/commands/brain-console.md +2 -2
  30. package/plugin/commands/configure.md +3 -2
  31. package/plugin/commands/rvbc.md +4 -3
  32. package/plugin/commands/rvcb.md +2 -2
  33. package/plugin/commands/whats-new.md +6 -6
  34. package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
  35. package/plugin/hooks/hooks.json +1 -2
  36. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  37. package/plugin/mcp/server.mjs +90 -32
  38. package/plugin/scripts/detach.mjs +14 -0
  39. package/plugin/scripts/first-session-worker.mjs +38 -0
  40. package/plugin/scripts/ground-ruvnet.sh +16 -6
  41. package/plugin/scripts/hook-shim.mjs +34 -29
  42. package/plugin/scripts/learn-capture.sh +22 -3
  43. package/plugin/scripts/learn-flush.mjs +21 -4
  44. package/plugin/scripts/runtime-preferences.mjs +269 -0
  45. package/plugin/scripts/session-start-core.mjs +503 -0
  46. package/plugin/scripts/session-start.sh +3 -858
  47. package/plugin/scripts/whats-new.mjs +42 -0
  48. package/plugin/skills/brain-console/SKILL.md +4 -2
  49. package/plugin/skills/release-proof/SKILL.md +98 -0
  50. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  51. package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
  52. package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
  54. package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
  55. package/plugin/skills/rvbc/SKILL.md +9 -6
  56. package/plugin/skills/whats-new/SKILL.md +4 -4
  57. package/scripts/adr-backfill.mjs +107 -0
  58. package/scripts/advocacy-outcomes.mjs +808 -0
  59. package/scripts/agentdb-context.mjs +216 -0
  60. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  61. package/scripts/ascii-drift.mjs +236 -0
  62. package/scripts/behavioral-l1-l4.mjs +210 -0
  63. package/scripts/brain-capability-check.mjs +72 -0
  64. package/scripts/brain-grade-groundtruth.mjs +100 -0
  65. package/scripts/brain-latency-50.mjs +227 -0
  66. package/scripts/brain-novice-50.mjs +189 -0
  67. package/scripts/brain-stamp.mjs +94 -0
  68. package/scripts/brain-state.mjs +212 -0
  69. package/scripts/build-bundle.mjs +531 -0
  70. package/scripts/build-concepts.mjs +132 -0
  71. package/scripts/build-l2.mjs +71 -0
  72. package/scripts/build-primer.mjs +73 -0
  73. package/scripts/build-symbols.mjs +68 -0
  74. package/scripts/calibrate-router.mjs +97 -0
  75. package/scripts/capability-audit.mjs +321 -0
  76. package/scripts/capability-registry.mjs +876 -0
  77. package/scripts/check-indexation.mjs +108 -0
  78. package/scripts/check-legibility.mjs +189 -0
  79. package/scripts/ci/build-fixture-kb.mjs +67 -0
  80. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  81. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  82. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  83. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  84. package/scripts/ci/stranger-scenario.mjs +228 -0
  85. package/scripts/ci/stranger-timeout.mjs +25 -0
  86. package/scripts/ci-verdict.mjs +29 -0
  87. package/scripts/claims-verify.mjs +710 -0
  88. package/scripts/clear-claude-tmp.sh +31 -0
  89. package/scripts/console-engine.mjs +434 -0
  90. package/scripts/console-engine.test.mjs +125 -0
  91. package/scripts/corpus-qa.mjs +250 -0
  92. package/scripts/correction-detect-embed.mjs +346 -0
  93. package/scripts/correction-detect-measure.mjs +270 -0
  94. package/scripts/correction-detect.mjs +686 -0
  95. package/scripts/count-chunks.mjs +54 -0
  96. package/scripts/described-questions.json +30 -0
  97. package/scripts/design-grade.mjs +58 -0
  98. package/scripts/dev-plugin-link.sh +105 -0
  99. package/scripts/distill-project.mjs +200 -0
  100. package/scripts/doc-currency.mjs +801 -0
  101. package/scripts/eval-brain.mjs +244 -0
  102. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  103. package/scripts/fix-workstream.mjs +291 -0
  104. package/scripts/full-hints.mjs +87 -0
  105. package/scripts/gate.sh +39 -0
  106. package/scripts/gates.mjs +146 -0
  107. package/scripts/gen-console-images.mjs +54 -0
  108. package/scripts/gen-images.mjs +47 -0
  109. package/scripts/git-clone-refresh.mjs +52 -0
  110. package/scripts/git-hooks/pre-push +126 -0
  111. package/scripts/goal-match.mjs +398 -0
  112. package/scripts/goldie-research.mjs +223 -0
  113. package/scripts/goldie-weekly.sh +67 -0
  114. package/scripts/health-repair.mjs +237 -0
  115. package/scripts/helix-scenario-questions.json +10 -0
  116. package/scripts/ingest-gists.mjs +230 -0
  117. package/scripts/ingest-meeting.mjs +115 -0
  118. package/scripts/ingest-repo.mjs +79 -0
  119. package/scripts/install-npx-witness.sh +49 -0
  120. package/scripts/issue-fix.mjs +558 -0
  121. package/scripts/issue-watch.mjs +276 -0
  122. package/scripts/issue4-close-note.md +31 -0
  123. package/scripts/key-canary.mjs +91 -0
  124. package/scripts/latency-to-surface.mjs +233 -0
  125. package/scripts/learning-enable.mjs +380 -0
  126. package/scripts/learning-replay.mjs +1570 -0
  127. package/scripts/learnings.mjs +62 -0
  128. package/scripts/lesson-gate.mjs +680 -0
  129. package/scripts/lesson-lifecycle.mjs +449 -0
  130. package/scripts/lesson-promote.mjs +262 -0
  131. package/scripts/lesson-ratify.mjs +98 -0
  132. package/scripts/lesson-seed.mjs +252 -0
  133. package/scripts/lesson-store.mjs +447 -0
  134. package/scripts/loop-checkpoint.mjs +86 -0
  135. package/scripts/memdb-health.sh +14 -0
  136. package/scripts/memory-doctor.mjs +326 -0
  137. package/scripts/model-catalog.mjs +79 -0
  138. package/scripts/nightly-controller.mjs +66 -0
  139. package/scripts/nightly-gists.sh +72 -0
  140. package/scripts/nightly-wrapper.sh +172 -0
  141. package/scripts/notify.sh +12 -0
  142. package/scripts/npx-witness.sh +56 -0
  143. package/scripts/onboarding-console.mjs +2922 -0
  144. package/scripts/private-fence.mjs +69 -0
  145. package/scripts/proactivity-metrics.mjs +118 -0
  146. package/scripts/proof-questions.json +56 -0
  147. package/scripts/protected-release-invocation.mjs +76 -0
  148. package/scripts/prove.mjs +95 -0
  149. package/scripts/proxy/claude-proxied.sh +57 -0
  150. package/scripts/proxy/proxy-revert.sh +59 -0
  151. package/scripts/proxy/proxy-up.sh +60 -0
  152. package/scripts/proxy/proxy-verify.mjs +142 -0
  153. package/scripts/publication-receipt.mjs +307 -0
  154. package/scripts/published-surface-probe.mjs +241 -0
  155. package/scripts/qe/card-lane-gate.mjs +162 -0
  156. package/scripts/qe/session-start-gate.mjs +229 -0
  157. package/scripts/qe/ux-suite.mjs +323 -0
  158. package/scripts/reconcile-project.mjs +0 -0
  159. package/scripts/record-lesson.mjs +113 -0
  160. package/scripts/refresh-model-catalog.mjs +99 -0
  161. package/scripts/release-authority.mjs +93 -0
  162. package/scripts/release-proof.mjs +9 -0
  163. package/scripts/release-vector.mjs +281 -0
  164. package/scripts/release.mjs +439 -0
  165. package/scripts/remedy-registry.mjs +247 -0
  166. package/scripts/rerank-cap-eval.mjs +265 -0
  167. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  168. package/scripts/route-cheap.mjs +20 -15
  169. package/scripts/router-utilization.mjs +182 -0
  170. package/scripts/routing-flywheel.mjs +596 -0
  171. package/scripts/rvf-generation.mjs +104 -0
  172. package/scripts/rvf-index-audit.mjs +138 -0
  173. package/scripts/self-update.mjs +296 -0
  174. package/scripts/selfcheck.mjs +7 -1
  175. package/scripts/sign-bundle.mjs +69 -0
  176. package/scripts/signal-watch.mjs +171 -0
  177. package/scripts/stabilization-receipt.mjs +108 -0
  178. package/scripts/stack-sync.mjs +469 -0
  179. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  180. package/scripts/stamp-sweep.mjs +144 -0
  181. package/scripts/status-honesty.mjs +102 -0
  182. package/scripts/sync-version.mjs +217 -0
  183. package/scripts/token-report.mjs +102 -0
  184. package/scripts/top100-benchmark.mjs +479 -0
  185. package/scripts/top100-corpus.mjs +112 -0
  186. package/scripts/top100-semantic-assertions.mjs +449 -0
  187. package/scripts/update-apply.mjs +9 -0
  188. package/scripts/upgrade-notice.mjs +14 -0
  189. package/scripts/verify-bundle.mjs +51 -0
  190. package/scripts/verify-channels.mjs +184 -0
  191. package/scripts/verify-model-catalog.mjs +104 -0
  192. package/scripts/verify-nightly-close-issue4.sh +31 -0
  193. package/scripts/version.mjs +40 -0
  194. package/scripts/wired-check.mjs +867 -0
  195. package/plugin/scripts/finalize-token-meter.mjs +0 -25
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ // scripts/release-vector.mjs — the release gate as a CRITICAL-INVARIANT VECTOR, never an average.
3
+ //
4
+ // ADR-058 §"The release gate", DDD-0013 aggregate CandidateVerdict. Eight named invariants, each
5
+ // PASS | FAIL | UNKNOWN **on the exact candidate SHA**. The verdict is the vector MINIMUM.
6
+ //
7
+ // WHY A VECTOR AND NOT A SCORE — the failure this file exists to make impossible:
8
+ // on 2026-07-27 this product scored 18/100 on a stranger's machine while README.md advertised
9
+ // "L1–L4 behavioral harness — all pass" on the same commit. Both statements were derived from real
10
+ // checks. A composite absorbed the 18: average enough green cells and the worst one disappears.
11
+ // So there is deliberately NO averaging operation on this aggregate. Not "we don't call it" — no
12
+ // method computes one, and tests/unit/release-vector.test.mjs asserts the module exposes none.
13
+ // If a future reader wants a percentage, they have to add the capability first, and the mutation
14
+ // test will make them look at this comment while they do it.
15
+ //
16
+ // THE OTHER LAW — UNKNOWN IS NEVER PASS. `behavioral-l1-l4.mjs --levels L5` once selected zero
17
+ // checks and printed `OVERALL: PASS`, exit 0: a run that measured nothing certified itself. Here a
18
+ // detector that cannot tell returns UNKNOWN, UNKNOWN ranks BELOW pass, and the minimum carries it
19
+ // all the way to the verdict. Silence is not consent.
20
+ import { spawnSync } from 'node:child_process';
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+
27
+ // Ordered worst -> best. The verdict is the minimum, so the ORDER of this array IS the policy.
28
+ export const RANK = ['FAIL', 'UNKNOWN', 'PASS'];
29
+
30
+ /** The vector minimum. Deliberately the only aggregation in this module. */
31
+ export function verdictOf(results) {
32
+ if (!results.length) return 'UNKNOWN'; // an empty vector measured nothing — see the L5 incident
33
+ return results.reduce((worst, r) => (RANK.indexOf(r.state) < RANK.indexOf(worst) ? r.state : worst), 'PASS');
34
+ }
35
+
36
+ export function candidateLineage() {
37
+ const git = (args) => {
38
+ const r = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8' });
39
+ return r.status === 0 ? r.stdout.trim() : '';
40
+ };
41
+ return {
42
+ sha: git(['rev-parse', 'HEAD']) || 'UNKNOWN-SHA',
43
+ tree: git(['rev-parse', 'HEAD^{tree}']) || 'UNKNOWN-TREE',
44
+ dirty: Boolean(git(['status', '--porcelain'])),
45
+ };
46
+ }
47
+
48
+ export function verdictWithLineage(results, lineage) {
49
+ return lineage?.dirty ? 'FAIL' : verdictOf(results);
50
+ }
51
+
52
+ const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
53
+ const read = (rel) => { try { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { return null; } };
54
+
55
+ /** Run a command; UNKNOWN (not FAIL) when the runner itself could not execute. */
56
+ function run(cmd, args, timeoutMs = 120_000, env = process.env) {
57
+ // npm installs `npm.cmd` / `npx.cmd` command shims on Windows. Direct spawn does not resolve the
58
+ // extension through PATHEXT, and .cmd files require cmd.exe, so a healthy detector otherwise
59
+ // reads UNKNOWN with ENOENT. Invoke the command interpreter explicitly rather than `shell:true`;
60
+ // the latter concatenates arguments into a shell string and is deprecated for that reason.
61
+ const windowsCommandShim = process.platform === 'win32' && /^(?:npm|npx)$/i.test(cmd);
62
+ const executable = windowsCommandShim ? (process.env.ComSpec || 'cmd.exe') : cmd;
63
+ const spawnArgs = windowsCommandShim ? ['/d', '/c', `${cmd}.cmd`, ...args] : args;
64
+ const r = spawnSync(executable, spawnArgs, {
65
+ cwd: ROOT,
66
+ encoding: 'utf8',
67
+ timeout: timeoutMs,
68
+ env,
69
+ });
70
+ if (r.error || r.status === null) return { state: 'UNKNOWN', why: `runner did not complete: ${r.error?.code || 'killed/timeout'}` };
71
+ return { state: r.status === 0 ? 'PASS' : 'FAIL', why: r.status === 0 ? 'exit 0' : `exit ${r.status}`, out: r.stdout };
72
+ }
73
+
74
+ // ─── the eight invariants ────────────────────────────────────────────────────────────────────
75
+ // Each names the dimension it gates and the dated failure it descends from. A detector with no
76
+ // incident behind it is a checkbox; every one of these was bought with a real defect.
77
+
78
+ export const INVARIANTS = [
79
+ {
80
+ name: 'INSTALL-FAILS-LOUD',
81
+ dimension: 'D8',
82
+ incident: 'verifyInstall() warned and continued; a stranger scored 18/100 on an install that could not fail',
83
+ detect() {
84
+ if (!exists('.github/workflows/stranger-matrix.yml')) return { state: 'FAIL', why: 'no stranger matrix' };
85
+ const inst = read('bin/install.mjs');
86
+ if (inst === null) return { state: 'UNKNOWN', why: 'bin/install.mjs unreadable' };
87
+ // The substance, not the ceremony: the selfcheck verdict must reach the PROCESS EXIT CODE.
88
+ // A workflow that runs a check whose result evaporates is the 40/100 defect with extra YAML.
89
+ return /process\.exitCode\s*=\s*selfcheck/.test(inst)
90
+ ? { state: 'PASS', why: 'matrix present and selfcheck verdict reaches process.exitCode' }
91
+ : { state: 'FAIL', why: 'selfcheck verdict never reaches process.exitCode' };
92
+ },
93
+ },
94
+ {
95
+ name: 'INTERFACE-CORPUS',
96
+ dimension: 'D7',
97
+ incident: 'shell-parsing defects recurred across issues #12/#13/#41/#44, then three stale ADRs reached main because document currency was outside every release gate',
98
+ detect() {
99
+ if (!exists('tests/regression/interface-gate-corpus.test.mjs')) return { state: 'FAIL', why: 'no incident corpus' };
100
+ if (!exists('scripts/doc-currency.mjs')) return { state: 'FAIL', why: 'no document-currency gate' };
101
+ const corpus = run('npx', ['vitest', 'run', 'tests/regression/interface-gate-corpus.test.mjs', '--reporter=dot']);
102
+ if (corpus.state !== 'PASS') return { ...corpus, why: `interface corpus: ${corpus.why}` };
103
+ const currency = run('node', ['scripts/doc-currency.mjs', '--check'], 180_000);
104
+ return currency.state === 'PASS'
105
+ ? { state: 'PASS', why: 'interface incident corpus and document currency both exit 0' }
106
+ : { ...currency, why: `document currency: ${currency.why}` };
107
+ },
108
+ },
109
+ {
110
+ name: 'LATENCY-DECISION-LANE',
111
+ dimension: 'D6',
112
+ incident: 'latency breaches only warned; timing regressions did not block shipping',
113
+ detect: () => (exists('scripts/qe/card-lane-gate.mjs')
114
+ ? run('node', ['scripts/qe/card-lane-gate.mjs'])
115
+ : { state: 'FAIL', why: 'no decision-lane gate' }),
116
+ },
117
+ {
118
+ name: 'COEXIST-BYTE-EQUAL',
119
+ dimension: 'D5',
120
+ incident: 'the merged-registry lint found 63 findings across 42 registrations; the prior suite saw 15',
121
+ detect: () => (exists('tests/mesh/coexistence.test.mjs')
122
+ ? run('npx', ['vitest', 'run', 'tests/mesh', '--reporter=dot'])
123
+ : { state: 'FAIL', why: 'no coexistence suite' }),
124
+ },
125
+ {
126
+ name: 'LEARNING-REPLAY',
127
+ dimension: 'D4',
128
+ incident: 'L4 asserted the hook\'s own prose contained words — it proved the brain SPOKE, never that anything LISTENED',
129
+ async detect() {
130
+ // DELEGATED to scripts/learning-replay.mjs's own checkArtifact(), not reimplemented here.
131
+ //
132
+ // This detector originally required `artifact.sha === HEAD`, which is WRONG in a way worth
133
+ // recording: committing the artifact necessarily changes HEAD, so a strict-equality rule can
134
+ // never be satisfied by its own commit and would read UNKNOWN forever. The trap's own rule
135
+ // binds to SUBSTANCE instead — `git diff <artifactSha>..HEAD -- LOAD_BEARING` — so a verdict
136
+ // stays valid until code that could actually change the outcome moves. Same ADR-055 design
137
+ // law as everywhere else: bind to the thing that can invalidate the claim, not to a
138
+ // convenient token that merely correlates with it. Two gates disagreeing about what "current"
139
+ // means is how a product ends up with two truths.
140
+ let mod;
141
+ try { mod = await import(new URL('./learning-replay.mjs', import.meta.url).href); }
142
+ catch { return { state: 'UNKNOWN', why: 'counterfactual replay harness absent' }; }
143
+ let r;
144
+ try { r = mod.checkPortfolio(); } catch (e) { return { state: 'UNKNOWN', why: `replay portfolio unreadable: ${e.message}` }; }
145
+ // `.status` is the field checkArtifact() actually returns — read, not assumed. Reaching for
146
+ // `.verdict` (the name used INSIDE the nested artifact) silently yielded undefined and made a
147
+ // real PASS read UNKNOWN: the delegation "worked" while reporting the opposite of the truth.
148
+ const v = String(r?.status ?? r?.verdict ?? 'UNKNOWN').toUpperCase();
149
+ // INCONCLUSIVE never rounds up (DDD-0013 invariant 6): a trap whose CONTROL arm also produced
150
+ // the token measured nothing about the brain, and measuring nothing is not passing.
151
+ if (v === 'INCONCLUSIVE' || v === 'SKIP') return { state: 'UNKNOWN', why: r?.why || 'trap invalid — control also succeeded' };
152
+ if (v !== 'PASS' && v !== 'FAIL') return { state: 'UNKNOWN', why: r?.why || `unrecognised verdict ${v}` };
153
+ return { state: v, why: r?.why || v };
154
+ },
155
+ },
156
+ {
157
+ name: 'SIGNAL-WATCH-FIRES',
158
+ dimension: 'D3',
159
+ incident: 'the OWNER had to report that CI was red — every local surface stayed calm',
160
+ detect() {
161
+ if (!exists('scripts/signal-watch.mjs')) return { state: 'FAIL', why: 'no signal watcher' };
162
+ const raw = read('plugin/hooks/hooks.json');
163
+ if (raw === null) return { state: 'UNKNOWN', why: 'plugin/hooks/hooks.json unreadable' };
164
+ let doc;
165
+ try { doc = JSON.parse(raw); } catch { return { state: 'UNKNOWN', why: 'hooks.json unparseable' }; }
166
+ // Built-but-unregistered is the F5 class: the capability exists on disk and never fires. So
167
+ // check the REGISTRATION — the door it actually walks through.
168
+ //
169
+ // AND CHECK IT BY PARSING, NOT BY SUBSTRING. The first version of this detector asked
170
+ // `raw.includes('signal-watch')`, and its own mutant renamed the handler to
171
+ // `signal-watch-DISABLED-BY-MUTANT` — which still CONTAINS 'signal-watch', so the gate stayed
172
+ // green on a registration that could no longer fire. A substring is not a registration; it
173
+ // would equally match a mention inside a comment, a `_note` field, or a disabled entry.
174
+ const commands = Object.values(doc.hooks || {})
175
+ .flatMap((groups) => (Array.isArray(groups) ? groups : []))
176
+ .flatMap((g) => (Array.isArray(g?.hooks) ? g.hooks : []))
177
+ .map((h) => String(h?.command || ''));
178
+ // the shim dispatches by a bare handler id, so match the id as a WHOLE argument token
179
+ const fires = commands.some((c) => /(^|[\s"'/])signal-watch($|[\s"'|;])/.test(c));
180
+ if (!fires) {
181
+ return { state: 'FAIL', why: 'watcher exists but is not registered as a dispatchable handler — it will never fire' };
182
+ }
183
+
184
+ // Registration is necessary but not sufficient. The first release gate stopped here and
185
+ // certified a named handler without proving that a real red verdict reached the maintainer.
186
+ // Execute the whole shipped path: push debt → poller → red surface → dedupe → green close →
187
+ // silence. The paired mutation suite deletes the consumer, breaks dedupe, converts UNKNOWN
188
+ // to green, and blinds the observer; every mutant must remain killed for D3 to pass.
189
+ const behavior = run('npx', [
190
+ 'vitest', 'run',
191
+ 'tests/unit/signal-lifecycle.test.mjs',
192
+ 'tests/mutation/signal-watch-mutation.test.mjs',
193
+ '--reporter=dot',
194
+ ]);
195
+ return behavior.state === 'PASS'
196
+ ? {
197
+ state: 'PASS',
198
+ why: `registered in ${commands.length} shipped registration(s); executable red→surface→green→silence lifecycle and 4 mutants pass`,
199
+ }
200
+ : {
201
+ state: behavior.state,
202
+ why: `signal-watch behavioral lifecycle or mutant proof failed (${behavior.why})`,
203
+ };
204
+ },
205
+ },
206
+ {
207
+ name: 'SCENARIOS-CURRENT',
208
+ dimension: 'D2',
209
+ incident: 'ADR-053 specified a mental-model scenario list; it was simply absent',
210
+ detect: () => (exists('tests/experience/report.mjs')
211
+ ? run('node', ['tests/experience/report.mjs'])
212
+ : { state: 'FAIL', why: 'no scenario report' }),
213
+ },
214
+ {
215
+ name: 'GUARANTEE-RUNS',
216
+ dimension: 'D1',
217
+ incident: 'the product guarantee skipped on every CI runner; REQUIRE_BRAIN=1 was set nowhere in the repo',
218
+ detect() {
219
+ const ci = read('.github/workflows/ci.yml');
220
+ if (ci === null) return { state: 'UNKNOWN', why: 'ci.yml unreadable' };
221
+ if (!/REQUIRE_BRAIN/.test(ci)) {
222
+ return { state: 'FAIL', why: 'REQUIRE_BRAIN set nowhere; the guarantee skips' };
223
+ }
224
+ const live = run(
225
+ 'npx',
226
+ [
227
+ 'vitest', 'run',
228
+ '--config', 'tests/qe/gpt56/vitest.config.mjs',
229
+ 'tests/qe/gpt56/live-brain-search.test.mjs',
230
+ '--reporter=dot',
231
+ ],
232
+ 90_000,
233
+ { ...process.env, REQUIRE_BRAIN: '1', RUVNET_QE_LIVE: '1' },
234
+ );
235
+ return live.state === 'PASS'
236
+ ? { state: 'PASS', why: 'REQUIRE_BRAIN is wired and the real search_ruvnet MCP returns cited source under concurrency' }
237
+ : { ...live, why: `live search_ruvnet guarantee: ${live.why}` };
238
+ },
239
+ },
240
+ ];
241
+
242
+ export function headSha() {
243
+ const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
244
+ return r.status === 0 ? r.stdout.trim() : 'UNKNOWN-SHA';
245
+ }
246
+
247
+ /** Strings a non-PASS verdict mechanically bans from release surfaces (ADR-058). */
248
+ export const BANNED_WHEN_DEGRADED = ['healthy', 'proven', 'all pass'];
249
+
250
+ export async function evaluate(invariants = INVARIANTS) {
251
+ const lineage = candidateLineage();
252
+ const sha = lineage.sha;
253
+ const results = [];
254
+ for (const inv of invariants) {
255
+ const r = await inv.detect();
256
+ results.push({ name: inv.name, dimension: inv.dimension, state: r.state, why: r.why, sha });
257
+ }
258
+ return { sha, lineage, results, verdict: verdictWithLineage(results, lineage) };
259
+ }
260
+
261
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
262
+ const { sha, lineage, results, verdict } = await evaluate();
263
+ const json = process.argv.includes('--json');
264
+ if (json) {
265
+ console.log(JSON.stringify({ sha, lineage, verdict, results }, null, 2));
266
+ } else {
267
+ const mark = { PASS: '✓', FAIL: '✗', UNKNOWN: '?' };
268
+ console.log(`\n release vector @ ${sha.slice(0, 7)}\n`);
269
+ for (const r of results) {
270
+ console.log(` ${mark[r.state]} ${r.state.padEnd(7)} ${r.dimension.padEnd(3)} ${r.name.padEnd(22)} ${r.why}`);
271
+ }
272
+ console.log(` lineage: tree ${lineage.tree.slice(0, 12)} · ${lineage.dirty ? 'DIRTY (release-blocking)' : 'clean'}`);
273
+ console.log(`\n verdict: ${verdict} (vector MINIMUM over ${results.length} invariants — never an average)`);
274
+ if (verdict !== 'PASS') {
275
+ console.log(` release metadata must read DEGRADED; these strings are banned: ${BANNED_WHEN_DEGRADED.map((s) => `"${s}"`).join(', ')}\n`);
276
+ } else {
277
+ console.log('');
278
+ }
279
+ }
280
+ process.exitCode = verdict === 'PASS' ? 0 : 1;
281
+ }