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,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * proxy-verify.mjs — trial-specific verification for the Meta LLM Proxy.
4
+ *
5
+ * DELIBERATELY THIN. rUv already ships the health check:
6
+ *
7
+ * ruflo doctor --component proxy
8
+ *
9
+ * which covers binary signature, version, process liveness (via the proxy's
10
+ * own /status), bind address, and sponsored-consent state (ADR-307/313).
11
+ * This script SHELLS OUT to that rather than reimplementing it — an earlier
12
+ * draft of this file hand-rolled all of it before the grounding gate caught
13
+ * that the real tool existed.
14
+ *
15
+ * It adds exactly two things rUv's doctor does not do, both specific to this
16
+ * trial rather than to the proxy in general:
17
+ *
18
+ * 1. A real end-to-end completion through /v1/messages, checked against the
19
+ * PASSTHROUGH ORACLE. Per ADR-313's addendum, a genuine Anthropic
20
+ * response carries `service_tier` and `cache_creation` in usage; Cognitum's
21
+ * gateway never returns them. Their presence proves the request reached
22
+ * real Anthropic on the user's own subscription rather than being quietly
23
+ * served by a cheap-tier substitute. The doctor confirms the process is
24
+ * healthy; only this proves the round-trip is real.
25
+ *
26
+ * 2. That the trial has NOT been wired globally. This trial is opt-in per
27
+ * session by design; an ANTHROPIC_BASE_URL in ~/.claude/settings.json
28
+ * would put the proxy in the hot path of every Claude Code window on the
29
+ * machine, which is exactly what this trial promised not to do.
30
+ *
31
+ * Exit 0 only if every check passes.
32
+ */
33
+ import { execFileSync } from 'node:child_process';
34
+ import fs from 'node:fs';
35
+ import os from 'node:os';
36
+ import path from 'node:path';
37
+
38
+ const BASE = 'http://127.0.0.1:11435';
39
+ const TOKEN_PATH = path.join(os.homedir(), '.ruflo', 'proxy-token');
40
+ const PROBE_MODEL = process.env.PROXY_VERIFY_MODEL || 'claude-haiku-4-5-20251001';
41
+
42
+ const checks = [];
43
+ const record = (name, ok, detail) => {
44
+ checks.push({ name, ok });
45
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
46
+ };
47
+
48
+ console.log('Meta LLM Proxy — trial verification\n');
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // 1. rUv's shipped health check. We do not reimplement any of it.
52
+ // ---------------------------------------------------------------------------
53
+ console.log('[1] ruflo doctor --component proxy (rUv\'s own check)');
54
+ let doctorOut = '';
55
+ let doctorOk = false;
56
+ try {
57
+ doctorOut = execFileSync('ruflo', ['doctor', '--component', 'proxy'], {
58
+ encoding: 'utf8',
59
+ timeout: 60_000,
60
+ });
61
+ doctorOk = /Summary:.*passed/.test(doctorOut) && !/\b\d+ failed/.test(doctorOut);
62
+ } catch (err) {
63
+ doctorOut = err.stdout || String(err);
64
+ }
65
+ for (const line of doctorOut.split('\n')) {
66
+ if (/^[✓✗⚠]/.test(line.trim())) console.log(` ${line.trim()}`);
67
+ }
68
+ record('ruflo proxy doctor reports healthy', doctorOk);
69
+
70
+ // The doctor prints the resolved plane; surface it as its own assertion so a
71
+ // silent flip away from passthrough can never pass unnoticed.
72
+ record('data plane is passthrough', /data plane:\s*passthrough/i.test(doctorOut));
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // 2. The round-trip the doctor does not perform.
76
+ // ---------------------------------------------------------------------------
77
+ console.log('\n[2] live round-trip + passthrough oracle');
78
+ let token = '';
79
+ try {
80
+ token = fs.readFileSync(TOKEN_PATH, 'utf8').trim();
81
+ } catch {
82
+ /* handled below */
83
+ }
84
+
85
+ if (!token) {
86
+ record('live round-trip returns correct content', false, 'no proxy token');
87
+ record('response proves REAL Anthropic', false, 'skipped');
88
+ } else {
89
+ try {
90
+ const res = await fetch(`${BASE}/v1/messages`, {
91
+ method: 'POST',
92
+ headers: {
93
+ authorization: `Bearer ${token}`,
94
+ 'content-type': 'application/json',
95
+ 'anthropic-version': '2023-06-01',
96
+ },
97
+ body: JSON.stringify({
98
+ model: PROBE_MODEL,
99
+ max_tokens: 16,
100
+ messages: [{ role: 'user', content: 'Reply with exactly: PROXY_OK' }],
101
+ }),
102
+ signal: AbortSignal.timeout(60_000),
103
+ });
104
+ const body = await res.json();
105
+ const text = body?.content?.[0]?.text ?? '';
106
+ record('live round-trip returns correct content', res.ok && text.includes('PROXY_OK'), `HTTP ${res.status}`);
107
+
108
+ const usage = body?.usage ?? {};
109
+ const oracleOk = 'service_tier' in usage && 'cache_creation' in usage;
110
+ record(
111
+ 'response proves REAL Anthropic (service_tier + cache_creation)',
112
+ oracleOk,
113
+ oracleOk ? `service_tier=${usage.service_tier}` : 'oracle fields ABSENT — did not reach Anthropic',
114
+ );
115
+ } catch (err) {
116
+ record('live round-trip returns correct content', false, String(err?.message || err));
117
+ record('response proves REAL Anthropic', false, 'no response');
118
+ }
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // 3. Trial safety: opt-in per session, never global.
123
+ // ---------------------------------------------------------------------------
124
+ console.log('\n[3] trial safety');
125
+ let noGlobalWiring = true;
126
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
127
+ try {
128
+ if (fs.existsSync(settingsPath)) {
129
+ noGlobalWiring = !fs.readFileSync(settingsPath, 'utf8').includes('ANTHROPIC_BASE_URL');
130
+ }
131
+ } catch {
132
+ /* absent file is fine */
133
+ }
134
+ record('NOT wired globally (~/.claude/settings.json clean)', noGlobalWiring);
135
+
136
+ const failed = checks.filter((c) => !c.ok);
137
+ console.log(`\n${checks.length - failed.length}/${checks.length} checks passed`);
138
+ if (failed.length) {
139
+ console.log(`FAILED: ${failed.map((f) => f.name).join(', ')}`);
140
+ process.exit(1);
141
+ }
142
+ console.log('Verified: proxy healthy, passthrough, real Anthropic round-trip, not wired globally.');
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * published-surface-probe.mjs — touch the surface a REAL user touches, on a schedule.
4
+ *
5
+ * THE DEDUCTION THIS CLOSES (ADR-058 D2, grader verbatim): "Zero `scheduled-live-probe` scenarios
6
+ * (I listed all 22: 19 ci, 3 manual). Nothing ever probes the *published* surface (registry `npx`,
7
+ * real Release download) on a schedule — the exact 'dead on the surface a real user touched'
8
+ * failure ADR-053 names."
9
+ *
10
+ * Every one of the 19 `ci` scenarios runs against THE SOURCE CHECKOUT. That is a different artifact
11
+ * from the one a stranger receives. A green CI and a dead `npx ruvnet-brain@latest` are perfectly
12
+ * compatible states, and the gap between them is not hypothetical — it is produced by ordinary,
13
+ * boring events that no push triggers:
14
+ *
15
+ * · a `files:` array that omits a file the CLI requires — the checkout has it, the tarball does not
16
+ * · an `npm publish` that half-succeeded, or a version unpublished/deprecated out from under us
17
+ * · a Release asset deleted, re-uploaded truncated, or a tag moved
18
+ * · a dependency that vanished from the registry
19
+ *
20
+ * None of those change a byte in this repo, so no push-triggered workflow can ever notice. Only a
21
+ * clock can. That is why this runs nightly and NOT on push.
22
+ *
23
+ * ── WHAT MAKES IT RED (stated plainly, because a probe that cannot fail is theater) ──────────────
24
+ * A1 the npm packument for `ruvnet-brain` is not fetchable, or carries no `dist-tags.latest`
25
+ * A2 the latest version's tarball URL does not HEAD 200 with a non-zero length
26
+ * B `npx -y ruvnet-brain@latest --help` — installed FROM THE REGISTRY into a throwaway prefix —
27
+ * exits non-zero, or prints something that is not this installer's help. This is the check
28
+ * that catches a broken `files:`/`bin:` mapping, a syntax error, or a missing runtime dep:
29
+ * the published package is EXECUTED, not merely inspected.
30
+ * C1 `releases/latest` exposes no `ruvnet-brain.zip`
31
+ * C2 that asset is implausibly small (< MIN_BUNDLE_BYTES) — a truncated re-upload is a real and
32
+ * silent failure mode; "the asset exists" is not the same as "the asset is the bundle"
33
+ * C3 its browser_download_url does not resolve 200 with a matching content-length
34
+ * C4 the `.zip.sha256` sidecar is missing or is not a well-formed 64-hex digest — the installer
35
+ * verifies against it, so a malformed sidecar breaks every fresh install
36
+ *
37
+ * ── WHAT IS *NOT* RED, ON PURPOSE ───────────────────────────────────────────────────────────────
38
+ * Version equality is a product invariant: npm dist-tags.latest and GitHub releases/latest must
39
+ * name the same Brain generation. A partial publish is red even when both artifacts work alone.
40
+ *
41
+ * ── UNKNOWN IS NEVER PASS ───────────────────────────────────────────────────────────────────────
42
+ * A rate-limited or unreachable network cannot distinguish "the surface is fine" from "the surface
43
+ * is gone". That is UNKNOWN (exit 4), not green. Same discipline as scripts/learning-replay.mjs.
44
+ *
45
+ * node scripts/published-surface-probe.mjs full probe (network + a real npx install)
46
+ * node scripts/published-surface-probe.mjs --no-exec skip check B (metadata only; much faster)
47
+ * node scripts/published-surface-probe.mjs --json machine-readable result on stdout
48
+ *
49
+ * Exit: 0 PASS · 1 FAIL · 4 UNKNOWN.
50
+ */
51
+ import fs from 'node:fs';
52
+ import os from 'node:os';
53
+ import path from 'node:path';
54
+ import { spawnSync } from 'node:child_process';
55
+
56
+ export const EXIT = Object.freeze({ PASS: 0, FAIL: 1, UNKNOWN: 4 });
57
+
58
+ const PKG = 'ruvnet-brain';
59
+ const REPO = 'stuinfla/ruvnet-brain';
60
+ const ASSET = 'ruvnet-brain.zip';
61
+ const REGISTRY = `https://registry.npmjs.org/${PKG}`;
62
+ const RELEASE_API = `https://api.github.com/repos/${REPO}/releases/latest`;
63
+
64
+ /**
65
+ * The bundle is ~736MB-845MB today. The floor is set FAR below that — this is a truncation
66
+ * detector, not a size assertion; the bundle is allowed to grow or shrink substantially without
67
+ * anyone having to come edit this number. Anything under 100MB is not the brain.
68
+ */
69
+ const MIN_BUNDLE_BYTES = 100 * 1024 * 1024;
70
+ const SHA256_RE = /^[0-9a-f]{64}$/;
71
+ /** The published --help must be OUR help. A registry that serves a name-squatted package is red. */
72
+ const HELP_MARKERS = ['RuvNet Brain installer', 'npx ruvnet-brain'];
73
+
74
+ const argv = process.argv.slice(2);
75
+ const NO_EXEC = argv.includes('--no-exec');
76
+ const AS_JSON = argv.includes('--json');
77
+
78
+ const checks = [];
79
+ const record = (id, status, detail) => { checks.push({ id, status, detail }); return status; };
80
+ const say = (...a) => { if (!AS_JSON) console.log(...a); };
81
+
82
+ /** fetch + classify. A transport error is UNKNOWN; a 4xx/5xx from a reachable host is a fact. */
83
+ async function get(url, { method = 'GET', headers = {} } = {}) {
84
+ try {
85
+ const res = await fetch(url, { method, headers: { 'user-agent': `${PKG}-published-surface-probe`, ...headers }, redirect: 'follow' });
86
+ return { ok: true, status: res.status, res };
87
+ } catch (e) {
88
+ return { ok: false, error: String(e?.message || e) };
89
+ }
90
+ }
91
+
92
+ // ── A. the npm registry surface ────────────────────────────────────────────────────────────────
93
+ async function probeRegistry() {
94
+ const r = await get(REGISTRY);
95
+ if (!r.ok) return { latest: null, s: record('A1-registry', 'UNKNOWN', `registry unreachable: ${r.error}`) };
96
+ if (r.status === 404) return { latest: null, s: record('A1-registry', 'FAIL', `${PKG} is NOT PUBLISHED (registry 404) — every \`npx ${PKG}\` on earth is dead`) };
97
+ if (r.status !== 200) return { latest: null, s: record('A1-registry', 'UNKNOWN', `registry returned HTTP ${r.status}`) };
98
+
99
+ let doc;
100
+ try { doc = await r.res.json(); } catch (e) { return { latest: null, s: record('A1-registry', 'FAIL', `registry returned unparseable JSON: ${e.message}`) }; }
101
+
102
+ const latest = doc?.['dist-tags']?.latest;
103
+ if (!latest) return { latest: null, s: record('A1-registry', 'FAIL', 'no dist-tags.latest — `npx pkg@latest` cannot resolve') };
104
+ const versionDoc = doc?.versions?.[latest];
105
+ if (!versionDoc) return { latest, s: record('A1-registry', 'FAIL', `dist-tags.latest=${latest} but no such version object — the tag points at nothing`) };
106
+ const tarball = versionDoc?.dist?.tarball;
107
+ if (!tarball) return { latest, s: record('A1-registry', 'FAIL', `${latest} carries no dist.tarball`) };
108
+ record('A1-registry', 'PASS', `dist-tags.latest=${latest}`);
109
+
110
+ // PROVE BYTES, NOT HEADERS. The first version of this check asserted a non-zero content-length on
111
+ // a HEAD, and went red against a perfectly healthy registry: verified live 2026-07-28, npm answers
112
+ // HEAD on a tarball with `HTTP/2 200` and NO content-length at all. That was a harness defect
113
+ // reporting itself as a surface outage — the precise false-red that teaches people to ignore a
114
+ // nightly. So: fetch a real byte range and check it is actually a gzip (npm tarballs are .tgz).
115
+ // Stronger than a length header anyway — it catches a truncated or garbage upload, which a
116
+ // correct-looking content-length would sail straight past.
117
+ const range = await get(tarball, { headers: { range: 'bytes=0-1023' } });
118
+ if (!range.ok) return { latest, s: record('A2-tarball', 'UNKNOWN', `tarball fetch failed: ${range.error}`) };
119
+ if (range.status !== 200 && range.status !== 206) return { latest, s: record('A2-tarball', 'FAIL', `tarball HTTP ${range.status} → ${tarball}`) };
120
+ const head4 = new Uint8Array(await range.res.arrayBuffer());
121
+ if (head4.length === 0) return { latest, s: record('A2-tarball', 'FAIL', `tarball served zero bytes → ${tarball}`) };
122
+ if (head4[0] !== 0x1f || head4[1] !== 0x8b) {
123
+ return { latest, s: record('A2-tarball', 'FAIL', `tarball is not gzip (first bytes ${head4[0]?.toString(16)} ${head4[1]?.toString(16)}) → ${tarball}`) };
124
+ }
125
+ record('A2-tarball', 'PASS', `${head4.length} bytes served, gzip magic ok`);
126
+ return { latest, s: 'PASS' };
127
+ }
128
+
129
+ // ── B. EXECUTE the published package, from the registry ────────────────────────────────────────
130
+ function probeExec() {
131
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-probe-'));
132
+ try {
133
+ // --help is chosen deliberately: it is the one flag that exercises the real published entrypoint
134
+ // (npm resolve → download → unpack → bin mapping → node parses every module it imports) while
135
+ // touching NOTHING on the machine. `--doctor`/`--plan` reach the network and the brain dir; this
136
+ // probe must never be the reason a surface changes.
137
+ const r = spawnSync('npx', ['-y', `${PKG}@latest`, '--help'], {
138
+ encoding: 'utf8',
139
+ timeout: 300_000,
140
+ cwd: home,
141
+ env: { ...process.env, HOME: home, npm_config_yes: 'true', NO_COLOR: '1' },
142
+ });
143
+ if (r.error) return record('B-npx-exec', 'UNKNOWN', `could not spawn npx: ${r.error.message}`);
144
+ const out = `${r.stdout || ''}${r.stderr || ''}`;
145
+ if (r.status !== 0) {
146
+ return record('B-npx-exec', 'FAIL', `\`npx -y ${PKG}@latest --help\` exited ${r.status}. First 400 chars:\n${out.slice(0, 400)}`);
147
+ }
148
+ const missing = HELP_MARKERS.filter((m) => !out.includes(m));
149
+ if (missing.length) {
150
+ return record('B-npx-exec', 'FAIL', `published --help ran but does not look like this installer (missing: ${missing.join(', ')}). First 400 chars:\n${out.slice(0, 400)}`);
151
+ }
152
+ return record('B-npx-exec', 'PASS', `\`npx -y ${PKG}@latest --help\` exited 0 and printed this installer's help`);
153
+ } finally {
154
+ fs.rmSync(home, { recursive: true, force: true });
155
+ }
156
+ }
157
+
158
+ // ── C. the GitHub Release surface the installer actually downloads ─────────────────────────────
159
+ async function probeRelease() {
160
+ const headers = process.env.GITHUB_TOKEN ? { authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {};
161
+ const r = await get(RELEASE_API, { headers });
162
+ if (!r.ok) return { tag: null, s: record('C1-release', 'UNKNOWN', `GitHub unreachable: ${r.error}`) };
163
+ if (r.status === 403 || r.status === 429) return { tag: null, s: record('C1-release', 'UNKNOWN', `GitHub rate-limited (HTTP ${r.status}) — cannot distinguish healthy from gone`) };
164
+ if (r.status === 404) return { tag: null, s: record('C1-release', 'FAIL', `${REPO} has NO published Release — a fresh install has nothing to download`) };
165
+ if (r.status !== 200) return { tag: null, s: record('C1-release', 'UNKNOWN', `releases/latest returned HTTP ${r.status}`) };
166
+
167
+ let rel;
168
+ try { rel = await r.res.json(); } catch (e) { return { tag: null, s: record('C1-release', 'FAIL', `releases/latest unparseable: ${e.message}`) }; }
169
+ const tag = rel?.tag_name || null;
170
+ const assets = Array.isArray(rel?.assets) ? rel.assets : [];
171
+ const zip = assets.find((a) => a.name === ASSET);
172
+ if (!zip) {
173
+ return { tag, s: record('C1-release', 'FAIL', `Release ${tag} has no "${ASSET}" (has: ${assets.map((a) => a.name).join(', ') || 'nothing'})`) };
174
+ }
175
+ record('C1-release', 'PASS', `Release ${tag} carries ${ASSET}`);
176
+
177
+ if (!(zip.size >= MIN_BUNDLE_BYTES)) {
178
+ return { tag, s: record('C2-bundle-size', 'FAIL', `${ASSET} is ${zip.size} bytes — below the ${MIN_BUNDLE_BYTES}-byte truncation floor; this is not the brain`) };
179
+ }
180
+ record('C2-bundle-size', 'PASS', `${zip.size} bytes`);
181
+
182
+ const head = await get(zip.browser_download_url, { method: 'HEAD' });
183
+ if (!head.ok) return { tag, s: record('C3-bundle-download', 'UNKNOWN', `bundle HEAD failed: ${head.error}`) };
184
+ if (head.status !== 200) return { tag, s: record('C3-bundle-download', 'FAIL', `bundle download URL returned HTTP ${head.status} — the asset is listed but not fetchable`) };
185
+ const len = Number(head.res.headers.get('content-length') || 0);
186
+ if (len && Math.abs(len - zip.size) > 0) {
187
+ return { tag, s: record('C3-bundle-download', 'FAIL', `served length ${len} != API-declared size ${zip.size} — the asset on the CDN is not the asset in the Release`) };
188
+ }
189
+ record('C3-bundle-download', 'PASS', `HTTP 200, ${len || zip.size} bytes`);
190
+
191
+ const sidecar = assets.find((a) => a.name === `${ASSET}.sha256`);
192
+ if (!sidecar) return { tag, s: record('C4-sha256-sidecar', 'FAIL', `no ${ASSET}.sha256 — the installer has nothing to verify the 800MB download against`) };
193
+ const sc = await get(sidecar.browser_download_url);
194
+ if (!sc.ok) return { tag, s: record('C4-sha256-sidecar', 'UNKNOWN', `sidecar fetch failed: ${sc.error}`) };
195
+ if (sc.status !== 200) return { tag, s: record('C4-sha256-sidecar', 'FAIL', `sidecar returned HTTP ${sc.status}`) };
196
+ const digest = (await sc.res.text()).trim().split(/\s+/)[0] || '';
197
+ if (!SHA256_RE.test(digest)) {
198
+ return { tag, s: record('C4-sha256-sidecar', 'FAIL', `sidecar is not a well-formed sha256 digest (got ${JSON.stringify(digest.slice(0, 80))})`) };
199
+ }
200
+ record('C4-sha256-sidecar', 'PASS', `${digest.slice(0, 16)}…`);
201
+ return { tag, s: 'PASS' };
202
+ }
203
+
204
+ export async function main() {
205
+ say('PUBLISHED-SURFACE probe — the artifact a stranger receives, not the one in this checkout\n');
206
+
207
+ const reg = await probeRegistry();
208
+ if (!NO_EXEC) probeExec();
209
+ else record('B-npx-exec', 'SKIPPED', '--no-exec');
210
+ const rel = await probeRelease();
211
+
212
+ if (reg.latest && rel.tag) {
213
+ const githubVersion = String(rel.tag).replace(/^v/, '');
214
+ if (reg.latest === githubVersion) {
215
+ record('D-version-coherence', 'PASS', `npm ${reg.latest} == GitHub ${rel.tag}`);
216
+ } else {
217
+ record('D-version-coherence', 'FAIL', `npm ${reg.latest} != GitHub ${rel.tag} — published surfaces identify different Brain generations`);
218
+ }
219
+ } else {
220
+ record('D-version-coherence', 'UNKNOWN', 'npm or GitHub version unavailable; equality cannot be proven');
221
+ }
222
+
223
+ for (const c of checks) say(` ${c.status.padEnd(7)} ${c.id.padEnd(20)} ${c.detail}`);
224
+
225
+ say(`\n npm dist-tags.latest = ${reg.latest ?? '?'} · GitHub releases/latest = ${rel.tag ?? '?'} (must match)`);
226
+
227
+ const failed = checks.filter((c) => c.status === 'FAIL');
228
+ const unknown = checks.filter((c) => c.status === 'UNKNOWN');
229
+ const verdict = failed.length ? 'FAIL' : unknown.length ? 'UNKNOWN' : 'PASS';
230
+
231
+ if (AS_JSON) console.log(JSON.stringify({ verdict, npmLatest: reg.latest, releaseTag: rel.tag, checks }, null, 2));
232
+ else {
233
+ say(`\n PUBLISHED-SURFACE: ${verdict}`);
234
+ if (failed.length) say(` ${failed.length} check(s) FAILED — the published surface is broken for real users RIGHT NOW.`);
235
+ if (!failed.length && unknown.length) say(` ${unknown.length} check(s) UNKNOWN — a probe that could not measure is not a pass.`);
236
+ }
237
+ return EXIT[verdict];
238
+ }
239
+
240
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
241
+ if (invokedDirectly) process.exit(await main());
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // card-lane-gate.mjs — ADR-058 D6's HARD gate over kb/card-lane.mjs's decision lane.
3
+ //
4
+ // WHY THIS IS A HARD GATE WHEN THE REST OF ux-suite.mjs's TIMINGS ARE ADVISORY: ux-suite.mjs's own
5
+ // header argues, correctly, that a flaky timing gate trains people to override it — server-ready,
6
+ // console paint, etc. are all subject to real environmental noise (cold node boot, first-paint,
7
+ // disk cache state) that has nothing to do with whether the product is correct. kb/card-lane.mjs's
8
+ // decision lane is different in kind, not degree: it is MODEL-FREE, ML-FREE, keyword overlap over a
9
+ // ~20KB in-memory-cached file (measured 0.1158ms warm — see kb/card-lane-budget.json). A budget set
10
+ // at ~2,159x that baseline (250ms) and an absolute ceiling at ~8,600x it (1000ms) leaves so much
11
+ // headroom that a breach cannot be scheduler jitter — it can only be a correctness regression (an
12
+ // accidental await, a removed cache, a blocking fs call in the hot path). That is exactly the case
13
+ // this file's own anti-flake rule permits hard-gating, and is why this is a SEPARATE, narrow gate
14
+ // rather than an entry in the shared WARN table.
15
+ //
16
+ // MEASUREMENT METHOD, DELIBERATE (CI CONSTRAINT): this measures IN-PROCESS function calls only — no
17
+ // `spawnSync` per firing. A GitHub ubuntu runner has 2 vCPU against this dev machine's 16, and
18
+ // subprocess-per-firing measurement has already produced starved, silently-empty output on that
19
+ // runner twice tonight. In-process calls have no fork()/exec() cost and no OS scheduling of a new
20
+ // process per sample, so the number this file reports is the LANE's cost, not the scheduler's. If
21
+ // this ever needs to spawn a subprocess instead, the budget below MUST be re-derived and explicitly
22
+ // re-sized for a 2 vCPU runner — do not silently keep a 16-core-measured number for a 2 vCPU gate.
23
+ //
24
+ // THE THRESHOLDS ARE NOT HARDCODED HERE. They live in the checked-in manifest kb/card-lane-budget.json,
25
+ // which docs/adr/0058-the-95-contract.md `governs:` — so a silent threshold raise there shows up as
26
+ // governed-set drift under `node scripts/doc-currency.mjs --check` rather than being a free edit.
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { answerFromCards } from '../../kb/card-lane.mjs';
31
+
32
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
33
+ export const REPO_ROOT = path.resolve(HERE, '../..');
34
+ export const KB_DIR = path.join(REPO_ROOT, 'kb');
35
+ export const BUDGET_PATH = path.join(KB_DIR, 'card-lane-budget.json');
36
+
37
+ // Real, first-party questions (plugin/test/capability-questions.json) rather than an invented
38
+ // string — the lane's cost should not depend on which of these it is asked, and cycling several
39
+ // (rather than one) avoids over-fitting the measurement to a single query's token count.
40
+ const FALLBACK_QUERIES = [
41
+ 'Can ruflo orchestrate agent swarms?',
42
+ 'Does RuVector use HNSW for vector search?',
43
+ 'Can rUv building blocks run graph queries over agent memory?',
44
+ ];
45
+
46
+ function loadQueries() {
47
+ try {
48
+ const raw = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'plugin/test/capability-questions.json'), 'utf8'));
49
+ const qs = raw.map((r) => r.query).filter(Boolean);
50
+ return qs.length ? qs.slice(0, 5) : FALLBACK_QUERIES;
51
+ } catch {
52
+ return FALLBACK_QUERIES; // absence of the fixture must not sink the gate — it has its own tests
53
+ }
54
+ }
55
+
56
+ export function loadBudget(budgetPath = BUDGET_PATH) {
57
+ const raw = fs.readFileSync(budgetPath, 'utf8');
58
+ const budget = JSON.parse(raw);
59
+ for (const key of ['sampleSize', 'p95BudgetMs', 'absoluteFailMs']) {
60
+ if (typeof budget[key] !== 'number' || !(budget[key] > 0)) {
61
+ throw new Error(`card-lane-budget.json: "${key}" must be a positive number, got ${JSON.stringify(budget[key])}`);
62
+ }
63
+ }
64
+ return budget;
65
+ }
66
+
67
+ // Nearest-rank percentile over an ASCENDING-sorted array. p in [0,100].
68
+ export function percentile(sortedAsc, p) {
69
+ if (!sortedAsc.length) return null;
70
+ const idx = Math.min(sortedAsc.length - 1, Math.max(0, Math.ceil((p / 100) * sortedAsc.length) - 1));
71
+ return sortedAsc[idx];
72
+ }
73
+
74
+ /**
75
+ * Fire the decision lane `n` times, IN-PROCESS, and return each firing's wall time in ms.
76
+ * `answerFn` defaults to the real `answerFromCards` and exists as an injectable seam ONLY so
77
+ * tests/unit/card-lane-gate.test.mjs can prove the THRESHOLD LOGIC catches a slow lane using a real
78
+ * (setTimeout-based, not mocked-away) synthetic function, without needing to mutate the shipped
79
+ * kb/card-lane.mjs to do it — that file's own mutant is exercised separately, for real, per ADR-058.
80
+ * Tolerates `answerFn` returning either a plain object (today's shipped shape) or a thenable (what a
81
+ * mutant that inserts `await new Promise(...)` inside it would produce) — the timing loop must
82
+ * actually wait on the delay for the mutant to be observable at all.
83
+ */
84
+ export async function measureFirings({ dir = KB_DIR, queries = loadQueries(), n = 100, answerFn = answerFromCards } = {}) {
85
+ if (!queries.length) throw new Error('measureFirings: no queries to fire');
86
+ // Warm-up: one untimed call, matching the lane's own memoization (kb/card-lane.mjs's `_cache`) so
87
+ // the measured 100 firings reflect the WARM cost, not the one-time capability-cards.md parse.
88
+ const warm = answerFn(queries[0], dir);
89
+ if (warm && typeof warm.then === 'function') await warm;
90
+
91
+ const samplesMs = [];
92
+ for (let i = 0; i < n; i++) {
93
+ const q = queries[i % queries.length];
94
+ const t0 = process.hrtime.bigint();
95
+ const result = answerFn(q, dir);
96
+ if (result && typeof result.then === 'function') await result;
97
+ const t1 = process.hrtime.bigint();
98
+ samplesMs.push(Number(t1 - t0) / 1e6);
99
+ }
100
+ return samplesMs;
101
+ }
102
+
103
+ /**
104
+ * The gate. Returns a verdict object; never throws on a threshold breach (that is a normal result,
105
+ * not an exceptional one) — it throws only if the lane or the manifest could not run at all, which
106
+ * scripts/qe/ux-suite.mjs treats as its own hard failure ("could not measure" is never success).
107
+ */
108
+ export async function runCardLaneGate(opts = {}) {
109
+ const budget = loadBudget(opts.budgetPath);
110
+ const samplesMs = await measureFirings({ dir: opts.dir, queries: opts.queries, n: budget.sampleSize, answerFn: opts.answerFn });
111
+ const sorted = [...samplesMs].sort((a, b) => a - b);
112
+ const p50 = percentile(sorted, 50);
113
+ const p95 = percentile(sorted, 95);
114
+ const max = sorted[sorted.length - 1];
115
+
116
+ const reasons = [];
117
+ if (p95 > budget.absoluteFailMs || max > budget.absoluteFailMs) {
118
+ reasons.push(`ABSOLUTE FAIL — correctness event, not jitter: max=${max.toFixed(4)}ms p95=${p95.toFixed(4)}ms > absoluteFailMs=${budget.absoluteFailMs}ms (${budget.measuredBaseline?.warmMs != null ? `~${(budget.absoluteFailMs / budget.measuredBaseline.warmMs).toFixed(0)}x the measured ${budget.measuredBaseline.warmMs}ms warm baseline` : 'far above the measured baseline'})`);
119
+ } else if (p95 > budget.p95BudgetMs) {
120
+ reasons.push(`BUDGET BREACH: p95=${p95.toFixed(4)}ms > p95BudgetMs=${budget.p95BudgetMs}ms over ${budget.sampleSize} in-process firings`);
121
+ }
122
+
123
+ return {
124
+ pass: reasons.length === 0,
125
+ n: samplesMs.length,
126
+ p50, p95, max,
127
+ budget,
128
+ reasons,
129
+ samplesMs,
130
+ };
131
+ }
132
+
133
+ // ── CLI ─────────────────────────────────────────────────────────────────────────────────────────
134
+ function fmt(ms) { return `${ms.toFixed(4)}ms`; }
135
+
136
+ async function main() {
137
+ console.log('\n card-lane decision-lane latency gate (ADR-058 D6 — HARD gate, in-process, model-free)\n');
138
+ let result;
139
+ try {
140
+ result = await runCardLaneGate();
141
+ } catch (e) {
142
+ console.error(` ✗ could not run the gate: ${e.message}`);
143
+ process.exit(2);
144
+ }
145
+ console.log(` budget source kb/card-lane-budget.json`);
146
+ console.log(` firings ${result.n} (in-process, no subprocess per firing)`);
147
+ console.log(` p50 ${fmt(result.p50)}`);
148
+ console.log(` p95 ${fmt(result.p95)} (budget ${result.budget.p95BudgetMs}ms)`);
149
+ console.log(` max ${fmt(result.max)} (absolute fail ${result.budget.absoluteFailMs}ms)`);
150
+ console.log('');
151
+ if (result.pass) {
152
+ console.log(' PASS — decision lane inside budget.\n');
153
+ process.exit(0);
154
+ }
155
+ console.log(' FAIL (hard):');
156
+ for (const r of result.reasons) console.log(` ✗ ${r}`);
157
+ console.log('');
158
+ process.exit(1);
159
+ }
160
+
161
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
162
+ if (invokedDirectly) main();