ruvnet-brain 3.9.134-dev → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +5 -5
  3. package/bin/install.mjs +382 -36
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/kb/zip-extract.mjs +53 -14
  25. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  26. package/package.json +14 -22
  27. package/plugin/.claude-plugin/marketplace.json +14 -0
  28. package/plugin/.claude-plugin/plugin.json +22 -0
  29. package/plugin/.codex-plugin/plugin.json +21 -0
  30. package/plugin/.mcp.json +8 -0
  31. package/plugin/commands/brain-console.md +16 -0
  32. package/plugin/commands/configure.md +33 -0
  33. package/plugin/commands/rvbc.md +79 -0
  34. package/plugin/commands/rvcb.md +16 -0
  35. package/plugin/commands/whats-new.md +57 -0
  36. package/plugin/hooks/codex-hooks.json +160 -0
  37. package/plugin/hooks/hook-contracts.json +77 -0
  38. package/plugin/hooks/hooks.json +202 -0
  39. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  40. package/plugin/mcp/server.mjs +56 -6
  41. package/plugin/scripts/anticipate.sh +534 -0
  42. package/plugin/scripts/codex-hook-adapter.mjs +96 -0
  43. package/plugin/scripts/continuation-gate.mjs +267 -0
  44. package/plugin/scripts/design-wall.sh +137 -0
  45. package/plugin/scripts/detach.mjs +182 -0
  46. package/plugin/scripts/first-session-worker.mjs +38 -0
  47. package/plugin/scripts/gate-receipt.sh +35 -0
  48. package/plugin/scripts/ground-before-write.sh +199 -0
  49. package/plugin/scripts/ground-ruvnet.sh +517 -0
  50. package/plugin/scripts/grounding-stamp.sh +113 -0
  51. package/plugin/scripts/grounding-substance.mjs +595 -0
  52. package/plugin/scripts/hijack-ruvnet.sh +81 -0
  53. package/plugin/scripts/hook-input.mjs +558 -0
  54. package/plugin/scripts/hook-shim-bash.mjs +55 -0
  55. package/plugin/scripts/hook-shim.mjs +303 -0
  56. package/plugin/scripts/host-update.mjs +58 -0
  57. package/plugin/scripts/kling-preflight.sh +146 -0
  58. package/plugin/scripts/learn-capture.sh +173 -0
  59. package/plugin/scripts/learn-flush.mjs +155 -0
  60. package/plugin/scripts/lesson-hooks.sh +213 -0
  61. package/plugin/scripts/md-stamp.mjs +219 -0
  62. package/plugin/scripts/protect-brain-state.sh +84 -0
  63. package/plugin/scripts/route-dispatch.sh +147 -0
  64. package/plugin/scripts/routing-outcome-capture.mjs +89 -0
  65. package/plugin/scripts/runtime-preferences.mjs +269 -0
  66. package/plugin/scripts/session-start-core.mjs +477 -0
  67. package/plugin/scripts/session-start.sh +13 -0
  68. package/plugin/scripts/signal-watch.mjs +193 -0
  69. package/plugin/scripts/unprompted-runtime.mjs +377 -0
  70. package/plugin/scripts/update-apply.mjs +419 -0
  71. package/plugin/scripts/verify-interface.sh +53 -0
  72. package/plugin/scripts/version-bump-gate.sh +112 -0
  73. package/plugin/skills/brain-build/SKILL.md +123 -0
  74. package/plugin/skills/brain-console/SKILL.md +22 -0
  75. package/plugin/skills/brain-prompt/SKILL.md +83 -0
  76. package/plugin/skills/brain-score/SKILL.md +101 -0
  77. package/plugin/skills/release-proof/SKILL.md +81 -0
  78. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  79. package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
  80. package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
  81. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +121 -0
  82. package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
  83. package/plugin/skills/rvbc/SKILL.md +23 -0
  84. package/plugin/skills/savings/SKILL.md +46 -0
  85. package/plugin/skills/whats-new/SKILL.md +22 -0
  86. package/scripts/adr-backfill.mjs +107 -0
  87. package/scripts/advocacy-outcomes.mjs +808 -0
  88. package/scripts/agentdb-context.mjs +216 -0
  89. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  90. package/scripts/ascii-drift.mjs +236 -0
  91. package/scripts/behavioral-l1-l4.mjs +210 -0
  92. package/scripts/brain-capability-check.mjs +72 -0
  93. package/scripts/brain-grade-groundtruth.mjs +100 -0
  94. package/scripts/brain-latency-50.mjs +227 -0
  95. package/scripts/brain-novice-50.mjs +189 -0
  96. package/scripts/brain-stamp.mjs +94 -0
  97. package/scripts/brain-state.mjs +212 -0
  98. package/scripts/build-bundle.mjs +522 -0
  99. package/scripts/build-concepts.mjs +132 -0
  100. package/scripts/build-l2.mjs +71 -0
  101. package/scripts/build-primer.mjs +73 -0
  102. package/scripts/build-symbols.mjs +68 -0
  103. package/scripts/calibrate-router.mjs +97 -0
  104. package/scripts/capability-audit.mjs +321 -0
  105. package/scripts/capability-registry.mjs +876 -0
  106. package/scripts/check-indexation.mjs +108 -0
  107. package/scripts/check-legibility.mjs +189 -0
  108. package/scripts/ci/build-fixture-kb.mjs +67 -0
  109. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  110. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  111. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  112. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  113. package/scripts/ci/stranger-scenario.mjs +228 -0
  114. package/scripts/ci/stranger-timeout.mjs +25 -0
  115. package/scripts/ci-verdict.mjs +29 -0
  116. package/scripts/claims-verify.mjs +710 -0
  117. package/scripts/clear-claude-tmp.sh +31 -0
  118. package/scripts/console-engine.mjs +434 -0
  119. package/scripts/console-engine.test.mjs +125 -0
  120. package/scripts/corpus-qa.mjs +250 -0
  121. package/scripts/correction-detect-embed.mjs +346 -0
  122. package/scripts/correction-detect-measure.mjs +270 -0
  123. package/scripts/correction-detect.mjs +686 -0
  124. package/scripts/count-chunks.mjs +54 -0
  125. package/scripts/described-questions.json +30 -0
  126. package/scripts/design-grade.mjs +58 -0
  127. package/scripts/dev-plugin-link.sh +105 -0
  128. package/scripts/distill-project.mjs +200 -0
  129. package/scripts/doc-currency.mjs +801 -0
  130. package/scripts/eval-brain.mjs +244 -0
  131. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  132. package/scripts/full-hints.mjs +87 -0
  133. package/scripts/gate.sh +39 -0
  134. package/scripts/gates.mjs +146 -0
  135. package/scripts/gen-console-images.mjs +54 -0
  136. package/scripts/gen-images.mjs +47 -0
  137. package/scripts/git-clone-refresh.mjs +52 -0
  138. package/scripts/git-hooks/pre-push +126 -0
  139. package/scripts/goal-match.mjs +398 -0
  140. package/scripts/goldie-research.mjs +223 -0
  141. package/scripts/goldie-weekly.sh +67 -0
  142. package/scripts/health-repair.mjs +250 -0
  143. package/scripts/helix-scenario-questions.json +10 -0
  144. package/scripts/ingest-gists.mjs +230 -0
  145. package/scripts/ingest-meeting.mjs +115 -0
  146. package/scripts/ingest-repo.mjs +79 -0
  147. package/scripts/install-npx-witness.sh +49 -0
  148. package/scripts/issue-fix.mjs +639 -0
  149. package/scripts/issue-watch.mjs +276 -0
  150. package/scripts/issue4-close-note.md +31 -0
  151. package/scripts/key-canary.mjs +91 -0
  152. package/scripts/latency-to-surface.mjs +233 -0
  153. package/scripts/learning-enable.mjs +380 -0
  154. package/scripts/learning-replay.mjs +1570 -0
  155. package/scripts/learnings.mjs +62 -0
  156. package/scripts/lesson-gate.mjs +680 -0
  157. package/scripts/lesson-lifecycle.mjs +449 -0
  158. package/scripts/lesson-promote.mjs +262 -0
  159. package/scripts/lesson-ratify.mjs +98 -0
  160. package/scripts/lesson-seed.mjs +252 -0
  161. package/scripts/lesson-store.mjs +447 -0
  162. package/scripts/loop-checkpoint.mjs +86 -0
  163. package/scripts/memdb-health.sh +14 -0
  164. package/scripts/memory-doctor.mjs +271 -0
  165. package/scripts/model-catalog.mjs +79 -0
  166. package/scripts/nightly-controller.mjs +66 -0
  167. package/scripts/nightly-gists.sh +72 -0
  168. package/scripts/nightly-wrapper.sh +180 -0
  169. package/scripts/notify.sh +12 -0
  170. package/scripts/npx-witness.sh +56 -0
  171. package/scripts/onboarding-console.mjs +2749 -0
  172. package/scripts/private-fence.mjs +69 -0
  173. package/scripts/proactivity-metrics.mjs +118 -0
  174. package/scripts/proof-questions.json +56 -0
  175. package/scripts/prove.mjs +95 -0
  176. package/scripts/proxy/claude-proxied.sh +57 -0
  177. package/scripts/proxy/proxy-revert.sh +59 -0
  178. package/scripts/proxy/proxy-up.sh +60 -0
  179. package/scripts/proxy/proxy-verify.mjs +142 -0
  180. package/scripts/published-surface-probe.mjs +241 -0
  181. package/scripts/qe/card-lane-gate.mjs +162 -0
  182. package/scripts/qe/session-start-gate.mjs +229 -0
  183. package/scripts/qe/ux-suite.mjs +323 -0
  184. package/scripts/reconcile-project.mjs +0 -0
  185. package/scripts/record-lesson.mjs +113 -0
  186. package/scripts/refresh-model-catalog.mjs +99 -0
  187. package/scripts/release-proof.mjs +9 -0
  188. package/scripts/release-vector.mjs +281 -0
  189. package/scripts/release.mjs +395 -0
  190. package/scripts/remedy-registry.mjs +247 -0
  191. package/scripts/rerank-cap-eval.mjs +265 -0
  192. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  193. package/scripts/route-cheap.mjs +20 -15
  194. package/scripts/router-utilization.mjs +182 -0
  195. package/scripts/routing-flywheel.mjs +596 -0
  196. package/scripts/rvf-generation.mjs +104 -0
  197. package/scripts/rvf-index-audit.mjs +138 -0
  198. package/scripts/self-update.mjs +508 -0
  199. package/scripts/selfcheck.mjs +7 -1
  200. package/scripts/sign-bundle.mjs +69 -0
  201. package/scripts/signal-watch.mjs +171 -0
  202. package/scripts/stack-sync.mjs +469 -0
  203. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  204. package/scripts/stamp-sweep.mjs +144 -0
  205. package/scripts/status-honesty.mjs +102 -0
  206. package/scripts/sync-version.mjs +217 -0
  207. package/scripts/token-report.mjs +102 -0
  208. package/scripts/top100-benchmark.mjs +479 -0
  209. package/scripts/top100-corpus.mjs +112 -0
  210. package/scripts/top100-semantic-assertions.mjs +449 -0
  211. package/scripts/update-apply.mjs +9 -0
  212. package/scripts/upgrade-notice.mjs +14 -0
  213. package/scripts/verify-bundle.mjs +51 -0
  214. package/scripts/verify-channels.mjs +184 -0
  215. package/scripts/verify-model-catalog.mjs +104 -0
  216. package/scripts/verify-nightly-close-issue4.sh +31 -0
  217. package/scripts/version.mjs +40 -0
  218. package/scripts/wired-check.mjs +864 -0
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ // scripts/ci/stranger-scenario.mjs — the shared driver behind .github/workflows/stranger-matrix.yml
3
+ // (ADR-058 §D8). ONE portable script so all five images (ubuntu, windows Git-Bash, windows
4
+ // PowerShell, macos, hostile container) run the IDENTICAL scenario logic. The `--local` contract is
5
+ // the assembled dist/ruvnet-brain/ directory, so this harness gives the packed installer that exact
6
+ // shape. Published Release ZIP extraction is a separate path with its own release/update tests.
7
+ //
8
+ // Runs against the PACKED, INSTALLED copy — the caller is expected to have already run
9
+ // `npm pack` + `npm install <tarball>` and pass --installed pointing at
10
+ // <scratch>/node_modules/ruvnet-brain. This script never touches the checkout's own bin/install.mjs
11
+ // (the whole point of D8: never install from the checkout, the checkout is the thing that always
12
+ // works and is why #42/#43 shipped broken).
13
+ //
14
+ // SCENARIOS:
15
+ // healthy — a complete fixture bundle. Asserts exit 0, then runs --doctor --hooks and
16
+ // asserts AT LEAST ONE hook fired through the INSTALLED registration, and
17
+ // that no author-local ~/.claude/settings.json exists in this virgin image.
18
+ // seeded-broken — forge-mcp-all.mjs deleted from the fixture (M-D8a). Asserts exit NON-ZERO.
19
+ // strict-ungrounded — a healthy-shaped fixture (already never ships forge-ask-all.mjs — see
20
+ // buildKbFixture below) with RUVNET_STRICT_INSTALL=1: grounding can never be
21
+ // proven in this hermetic run, and strict mode makes that FATAL. Asserts
22
+ // exit NON-ZERO — proving the strict path is real even though it is never the
23
+ // default (the SAME fixture, no strict flag, is the `healthy` scenario above
24
+ // and stays exit 0).
25
+ //
26
+ // Usage:
27
+ // node scripts/ci/stranger-scenario.mjs --scenario <name> --installed <dir> --home <dir> --plugin-src <dir>
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ import os from 'node:os';
31
+ import { spawnSync, execFileSync } from 'node:child_process';
32
+ import { fileURLToPath } from 'node:url';
33
+ import { selfCheckOuterTimeoutMs } from './stranger-timeout.mjs';
34
+ import { stageLocalBundle } from './stranger-fixture-stage.mjs';
35
+
36
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
37
+ const argv = process.argv.slice(2);
38
+ const arg = (flag, def = null) => { const i = argv.indexOf(flag); return i >= 0 && argv[i + 1] ? argv[i + 1] : def; };
39
+ const SCENARIO = arg('--scenario');
40
+ const INSTALLED = arg('--installed'); // <scratch>/node_modules/ruvnet-brain
41
+ const HOME_DIR = arg('--home'); // virgin HOME for this run
42
+ const PLUGIN_SRC = arg('--plugin-src'); // the checkout's plugin/ dir, at the candidate SHA
43
+
44
+ if (!SCENARIO || !INSTALLED || !HOME_DIR || !PLUGIN_SRC) {
45
+ console.error('usage: node stranger-scenario.mjs --scenario <healthy|seeded-broken|strict-ungrounded> --installed <dir> --home <dir> --plugin-src <dir>');
46
+ process.exit(2);
47
+ }
48
+
49
+ const hooksDoc = JSON.parse(fs.readFileSync(path.join(PLUGIN_SRC, 'hooks', 'hooks.json'), 'utf8'));
50
+ const INSTALL_TIMEOUT_MS = selfCheckOuterTimeoutMs(hooksDoc);
51
+
52
+ function log(msg) { console.log(`[stranger-scenario:${SCENARIO}] ${msg}`); }
53
+ function fail(msg) { console.error(`[stranger-scenario:${SCENARIO}] FAIL: ${msg}`); process.exit(1); }
54
+
55
+ /**
56
+ * PATH with every directory holding a `ruflo`/`claude-flow`/`claude` executable removed — same
57
+ * safety net tests/mutation/install-selfcheck-consumption-mutation.test.mjs uses (bin/install.mjs's
58
+ * OWN detectEnvironment() checks `have('ruflo') || have('claude-flow')` for the exact same tools —
59
+ * confirmed live via search_ruvnet: rUv's published orchestration CLI ships bins `claude-flow`,
60
+ * `cli`, `claude-flow-mcp` from the `claude-flow` / `@claude-flow/cli` npm packages), so a developer
61
+ * reproducing a CI failure locally does not risk their own machine's real Claude Code plugin
62
+ * marketplace or a real orchestration binary. On a hosted CI runner these binaries are already
63
+ * absent, so this is a no-op there.
64
+ */
65
+ function safePath() {
66
+ const sep = path.delimiter;
67
+ const risky = ['claude', 'ruflo', 'claude-flow'];
68
+ return (process.env.PATH || '').split(sep).filter((dir) => {
69
+ if (!dir) return true;
70
+ try { return !risky.some((exe) => fs.existsSync(path.join(dir, exe))); } catch { return true; }
71
+ }).join(sep);
72
+ }
73
+
74
+ function buildKbFixture({ dropMcp, noRvf }) {
75
+ const stageParent = fs.mkdtempSync(path.join(os.tmpdir(), 'stranger-kb-stage-'));
76
+ const root = path.join(stageParent, 'ruvnet-brain');
77
+ const args = [path.join(REPO_ROOT, 'scripts', 'ci', 'build-fixture-kb.mjs'), '--out', root];
78
+ if (dropMcp) args.push('--drop-mcp');
79
+ if (noRvf) args.push('--no-rvf');
80
+ execFileSync(process.execPath, args, { stdio: 'inherit' });
81
+ return root;
82
+ }
83
+
84
+ /**
85
+ * Marketplace-clone-shaped plugin surface, the REAL plugin/ tree at the candidate SHA — never a
86
+ * synthetic fixture — so "at least one hook fired" is a real, meaningful registration.
87
+ */
88
+ function seedPluginSurface() {
89
+ const dest = path.join(HOME_DIR, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin');
90
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
91
+ fs.cpSync(PLUGIN_SRC, dest, { recursive: true });
92
+ return dest;
93
+ }
94
+
95
+ /**
96
+ * `spawnSync().status` is null in THREE completely different situations, and reporting all three as
97
+ * the string "null" is how windows-powershell produced the least useful failure line in the matrix:
98
+ *
99
+ * [stranger-scenario:healthy] FAIL: expected exit 0 on a healthy install, got null
100
+ *
101
+ * null is not an exit code — it means no exit code was ever produced. Which of these it was decides
102
+ * what you go fix, and they have nothing to do with each other:
103
+ * · r.error set, code ENOENT -> the binary was never launched (wrong path / missing tool)
104
+ * · r.error set, code ETIMEDOUT -> the child ran and was KILLED at the timeout (a hang)
105
+ * · r.error set, code ENOBUFS -> the child was KILLED because it out-printed maxBuffer
106
+ * · r.signal set, no error -> something else killed it (OOM killer, SIGKILL)
107
+ * So describe the outcome from error.code / signal, never from `status` alone.
108
+ */
109
+ function describeExit(r) {
110
+ if (r.error) {
111
+ const code = r.error.code || r.error.name || 'unknown';
112
+ const sig = r.signal ? `, signal ${r.signal}` : '';
113
+ const why = code === 'ENOENT' ? ' — the process was never launched (binary not found)'
114
+ : code === 'ETIMEDOUT' ? ' — the process HUNG and was killed at the timeout, it did not exit'
115
+ : code === 'ENOBUFS' ? ' — the process was killed for exceeding maxBuffer on stdout/stderr, it did not exit'
116
+ : '';
117
+ return `NO EXIT CODE: spawn error ${code}${sig}${why} (${r.error.message})`;
118
+ }
119
+ if (r.status === null) {
120
+ return r.signal
121
+ ? `NO EXIT CODE: killed by signal ${r.signal} (nothing was returned by the process itself)`
122
+ : 'NO EXIT CODE and no signal — spawnSync returned neither, which should be impossible; treat as a harness bug';
123
+ }
124
+ return `exit code ${r.status}`;
125
+ }
126
+
127
+ function runInstaller(args, extraEnv = {}) {
128
+ return spawnSync(process.execPath, [path.join(INSTALLED, 'bin', 'install.mjs'), ...args], {
129
+ env: {
130
+ ...process.env,
131
+ PATH: safePath(),
132
+ HOME: HOME_DIR,
133
+ USERPROFILE: HOME_DIR,
134
+ RUVNET_BRAIN_TEST: '1',
135
+ // Preserve stage timing in the exact stranger path. The Windows cold-start miss that
136
+ // escaped PR #70 was visible only as one aggregate 4597ms number; future failures must
137
+ // identify the slow lifecycle stage in the same CI receipt.
138
+ RUVNET_SESSION_TRACE: '1',
139
+ ...extraEnv,
140
+ },
141
+ input: '',
142
+ encoding: 'utf8',
143
+ // This outer watchdog encloses the installer's post-install selfcheck. On Windows, 68 real
144
+ // shell/process fires can legitimately exceed the old fixed 120s even while every individual
145
+ // hook remains inside its own declared watchdog. Derive the ceiling from those inner budgets so
146
+ // the matrix receives the named selfcheck verdict instead of killing the healthy installer first.
147
+ timeout: INSTALL_TIMEOUT_MS,
148
+ // spawnSync's default maxBuffer is 1MB, and BLOWING IT KILLS THE CHILD and yields status null —
149
+ // indistinguishable, in the old reporting, from a hang or a missing binary. The installer prints
150
+ // a full narrated plan plus a self-check battery, so 1MB is not a comfortable margin. Raised so
151
+ // that a chatty-but-correct install cannot be mistaken for a broken one; describeExit() still
152
+ // names ENOBUFS explicitly if it is ever hit again.
153
+ maxBuffer: 64 * 1024 * 1024,
154
+ });
155
+ }
156
+
157
+ fs.mkdirSync(HOME_DIR, { recursive: true });
158
+ seedPluginSurface();
159
+
160
+ const authorSettings = path.join(HOME_DIR, '.claude', 'settings.json');
161
+ if (fs.existsSync(authorSettings)) fail(`author-local settings.json must not exist in a virgin image: ${authorSettings}`);
162
+
163
+ const dropMcp = SCENARIO === 'seeded-broken';
164
+ const fixtureDir = buildKbFixture({ dropMcp, noRvf: false });
165
+ stageLocalBundle(fixtureDir, INSTALLED);
166
+
167
+ const strictEnv = SCENARIO === 'strict-ungrounded' ? { RUVNET_STRICT_INSTALL: '1' } : {};
168
+ const install = runInstaller(
169
+ ['--local', '--no-stack', '--no-enhance', '--no-statusline', '--no-telemetry', '--no-nightly-prompt'],
170
+ strictEnv,
171
+ );
172
+ log(`install result: ${describeExit(install)}`);
173
+ console.log(install.stdout);
174
+ if (install.stderr) console.error(install.stderr);
175
+
176
+ if (SCENARIO === 'healthy') {
177
+ if (install.status !== 0) fail(`expected exit 0 on a healthy install, got ${describeExit(install)}`);
178
+
179
+ const doctor = runInstaller(['--doctor', '--hooks']);
180
+ log(`--doctor --hooks result: ${describeExit(doctor)}`);
181
+ console.log(doctor.stdout);
182
+ if (doctor.stderr) console.error(doctor.stderr);
183
+
184
+ // TWO REQUIREMENTS OF D8 CONTRADICTED EACH OTHER HERE, and the contradiction is worth stating
185
+ // rather than papering over.
186
+ //
187
+ // · The hook battery must pass on a healthy image — that is this cell's whole purpose.
188
+ // · `--doctor` DELIBERATELY exits non-zero when grounding is unproven. That is the D8 decision:
189
+ // the install-time smoke stays non-fatal (an air-gapped machine is not a broken install), but
190
+ // the verdict must stop EVAPORATING — it persists to install-state.json and --doctor gates on
191
+ // it until a real cited answer clears it.
192
+ //
193
+ // Every CI image is offline with no model weights, so grounding is ALWAYS unproven here and
194
+ // `--doctor` will ALWAYS exit 1. Asserting exit 0 asserted that D8's own design does not work.
195
+ //
196
+ // So the assertion binds to the SUBSTANCE: the battery must report zero contract violations, and
197
+ // any non-zero exit must be attributable ONLY to the grounding verdict. A doctor that fails for a
198
+ // hook violation still fails this cell — which is the property that was actually wanted.
199
+ const out = `${doctor.stdout || ''}${doctor.stderr || ''}`;
200
+ const batteryClean = /Self-check passed/.test(out);
201
+ const onlyGrounding = /Grounding UNPROVEN/.test(out) && !/contract violation/.test(out);
202
+ if (doctor.status !== 0 && !(batteryClean && onlyGrounding)) {
203
+ fail(`--doctor --hooks ended with ${describeExit(doctor)} for a reason other than unproven grounding on a healthy install`);
204
+ }
205
+ if (!batteryClean) fail('--doctor --hooks did not report a clean hook battery on a healthy install');
206
+ const firingsMatch = /registrations from marketplace-clone,\s*\d+\s*stdin regimes each\s*\((\d+)\s*firings\)/.exec(doctor.stdout || '');
207
+ if (!firingsMatch) fail('--doctor --hooks output did not name the marketplace-clone registration/firing count at all');
208
+ const firings = Number(firingsMatch[1]);
209
+ if (!(firings > 0)) fail(`expected at least one hook FIRING through the installed registration, got ${firings}`);
210
+ log(`OK — ${firings} real hook firing(s) through the installed marketplace-clone registration`);
211
+
212
+ if (fs.existsSync(authorSettings)) fail('installer must never create an author-local settings.json in a virgin image');
213
+ log('OK — no author-local ~/.claude/settings.json in this virgin image');
214
+ } else {
215
+ // seeded-broken / strict-ungrounded: the whole point is a non-zero exit.
216
+ //
217
+ // "not zero" was too weak an assertion, and weak in the direction that hides breakage: a hang, an
218
+ // ENOENT, or an ENOBUFS kill all leave status === null, which is not 0, so this cell reported PASS
219
+ // for a run in which the installer never even executed. A test that a crashed harness satisfies is
220
+ // not a test. Demand a REAL exit code the installer itself produced, and that it be non-zero.
221
+ if (install.status === null) {
222
+ fail(`scenario "${SCENARIO}" requires the installer to EXIT non-zero, but it never exited at all: ${describeExit(install)}`);
223
+ }
224
+ if (install.status === 0) fail(`expected a NON-ZERO exit for scenario "${SCENARIO}", got 0`);
225
+ log(`OK — exited non-zero (${install.status}) as required for scenario "${SCENARIO}"`);
226
+ }
227
+
228
+ log('PASS');
@@ -0,0 +1,25 @@
1
+ const DEFAULT_HOOK_TIMEOUT_SEC = 5;
2
+ const DEFAULT_REGIMES = 4;
3
+ const DEFAULT_GRACE_MS = 2_000;
4
+ const INSTALL_OVERHEAD_MS = 60_000;
5
+
6
+ /**
7
+ * The installer runs every shipped registration through selfcheck's four stdin regimes. Its outer
8
+ * watchdog must outlive those inner watchdogs; otherwise a slow but bounded Windows run is reported
9
+ * as an installer hang before selfcheck can return its named contract verdict.
10
+ */
11
+ export function selfCheckOuterTimeoutMs(
12
+ hooksDoc,
13
+ { regimes = DEFAULT_REGIMES, graceMs = DEFAULT_GRACE_MS, overheadMs = INSTALL_OVERHEAD_MS } = {},
14
+ ) {
15
+ let oneRegimeMs = 0;
16
+ for (const groups of Object.values(hooksDoc?.hooks || {})) {
17
+ for (const group of groups || []) {
18
+ for (const hook of group?.hooks || []) {
19
+ const timeoutSec = typeof hook?.timeout === 'number' ? hook.timeout : DEFAULT_HOOK_TIMEOUT_SEC;
20
+ oneRegimeMs += Math.max(1, timeoutSec * 1_000) + graceMs;
21
+ }
22
+ }
23
+ }
24
+ return overheadMs + Math.max(1, regimes) * oneRegimeMs;
25
+ }
@@ -0,0 +1,29 @@
1
+ // scripts/ci-verdict.mjs — the remote-CI ship gate's brain (ADR-053 §5), split out so the decision
2
+ // is a pure function a test can break. Between 2026-07-21 and 07-26 the `ci` workflow was red for
3
+ // ~70 consecutive runs and six releases shipped straight past it, because nothing on the ship path
4
+ // ever asked the remote verdict. Local gates prove one machine; only CI proves ubuntu and windows.
5
+
6
+ /** Latest COMPLETED ci run on origin/main → { verdict, sha } (verdict null = could not determine). */
7
+ export async function fetchLatestCiVerdict({ repo = 'stuinfla/ruvnet-brain', workflow = 'ci.yml' } = {}) {
8
+ try {
9
+ const r = await fetch(
10
+ `https://api.github.com/repos/${repo}/actions/workflows/${workflow}/runs?branch=main&status=completed&per_page=1`,
11
+ { headers: { accept: 'application/vnd.github+json' } },
12
+ );
13
+ const run = (await r.json()).workflow_runs?.[0];
14
+ return run ? { verdict: run.conclusion, sha: String(run.head_sha || '').slice(0, 7) } : { verdict: null, sha: '' };
15
+ } catch {
16
+ return { verdict: null, sha: '' }; // unknown — the gate treats this as red, never as green
17
+ }
18
+ }
19
+
20
+ /**
21
+ * The gate decision, pure: 'ship' | 'override' | 'refuse'.
22
+ * Green ships. An explicit override reason ships loudly. EVERYTHING else — failure, cancelled,
23
+ * no run found, API unreachable — refuses: an unknown main is a red main, never a green one.
24
+ */
25
+ export function assessCiGate(verdict, overrideReason = null) {
26
+ if (verdict === 'success') return 'ship';
27
+ if (overrideReason) return 'override';
28
+ return 'refuse';
29
+ }