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
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
+ "description": "Source-grounded RuvNet Brain integrations for Claude Code and Codex.",
3
4
  "owner": {
4
5
  "name": "Stuart Kerr"
5
6
  },
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.0.1 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.1-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.0.4 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.4-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
10
10
 
@@ -30,7 +30,7 @@
30
30
  [![explainer](https://img.shields.io/badge/▶%20see%20it%20live-isovision.ai%2Fruvnet--brain-e8a13a?style=flat-square)](https://isovision.ai/ruvnet-brain/)
31
31
  [![license](https://img.shields.io/badge/license-MIT-8ecae6?style=flat-square)](LICENSE)
32
32
  [![grounded](https://img.shields.io/badge/answers-cited%20rUv%20source-333?style=flat-square)](#testing--proof)
33
- [![coverage](https://img.shields.io/badge/coverage-32%25%20of%20ALL%20source%20·%20honest-b58900?style=flat-square)](#testing--proof)
33
+ [![coverage](https://img.shields.io/badge/coverage-34%25%20of%20ALL%20source%20·%20honest-b58900?style=flat-square)](#testing--proof)
34
34
 
35
35
  > **One Brain generation everywhere.** npm, the GitHub tag/release, bundle manifests, source metadata, and checksum-bound RVF generations must share the same product version. Headline claims are regenerated and checked by the claims ledger (`scripts/claims-verify.mjs`); other numbers below are hand-stamped and dated:
36
36
  > - **`plugin`** (badge above) — the Claude Code plugin itself: SKILL.md, the grounding hooks, the MCP server. Read live from [`plugin/.claude-plugin/plugin.json`](plugin/.claude-plugin/plugin.json). Updates often — this is where behavior fixes land.
@@ -485,7 +485,7 @@ node plugin/test/run-tests.mjs # full plugin QA over real JSO
485
485
  | **L4 "orchestrate"** | **downgraded — measures speech, not obedience** | L4 asserts the hook's own injected prose contains required words (`must: ['take the wheel','SPARC','swarm',…]`). That proves **the brain spoke**. It cannot fail when the advice is read and ignored — which is the failure this product exists to prevent. Counterfactual replay against a brain-off control (ADR-058 §D4) is what will earn this row back |
486
486
  | **Plugin QA** | **60 / 60** | manifests, hook firing, MCP `initialize`/`tools/list`, capability battery |
487
487
  | **Clean-room install** | **3 / 3** | download the published bundle fresh → unzip → query → grounded, cited answers |
488
- | **Unit tests** | **2,327 passing, 169 todo** · 32% of ALL source covered | `npm run test:cov` regenerates both — the coverage floor fails CI if it slips (`claims:verify` re-derives the %, it is not a hand-typed badge). 32% is the honest number over every shipped file; the previous "75%" measured a hand-picked 8-file subset |
488
+ | **Unit tests** | **2,327 passing, 169 todo** · 34% of ALL source covered | `npm run test:cov` regenerates both — the coverage floor fails CI if it slips (`claims:verify` re-derives the %, it is not a hand-typed badge). 34% is the honest number over every shipped file; the previous "75%" measured a hand-picked 8-file subset |
489
489
  | **Grounding proof** | `npx ruvnet-brain --doctor` | asks a real question, then checks the cited path really exists in the on-disk store; a citation that doesn't resolve is reported as **NOT grounded** |
490
490
  | **Held-out eval** | **grounded 100/100** · routed 63/80 | `npm run eval` — 120 frozen, hash-pinned questions across 5 strata, never used for tuning, graded on ground truth, never by a model |
491
491
 
@@ -522,7 +522,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
522
522
 
523
523
  This project versions in the open (see the live badge up top for the exact plugin version; the downloadable knowledge bundle is a separate track) — we don't claim “done,” “complete,” or “zero hallucinations.” Where it stands:
524
524
 
525
- - ✅ **The grounding brain is real and proven** — 62 public stores · 151,033 public source chunks (69 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
525
+ - ✅ **The grounding brain is real and proven** — 63 public stores · 153,369 public source chunks (70 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
526
526
  - ✅ **Code-level depth** — the code-rich repos are indexed to full function bodies; “how is it implemented?” returns the implementation. Verified in the shipped bundle (clean-room 3/3).
527
527
  - ✅ **Routing holds** — named 47/48, described 26/28, scenario 7/8; behavioral L1–L3 all pass (**L4 downgraded — it measures that the brain spoke, not that anything listened**); private stores fenced out of the public bundle (zero-leak verified).
528
528
  - ⚠️ **Two routing residuals** (above) — surfaced, not hidden.
package/bin/install.mjs CHANGED
@@ -95,6 +95,7 @@ const FLAG_ENABLE_SPEND_GUARD = argv.includes('--enable-spend-guard');
95
95
  const FLAG_DISABLE_SPEND_GUARD = argv.includes('--disable-spend-guard'); // the missing undo
96
96
  const FLAG_UNINSTALL = argv.includes('--uninstall'); // reverse everything, in one command
97
97
  const FLAG_WHAT_CHANGED = argv.includes('--what-changed'); // show our footprint on this machine
98
+ const FLAG_WHATS_NEW = argv.includes('--whats-new'); // show curated major-release highlights
98
99
  // ── onboarding-experience flags (all optional; every offer is safe to decline) ──
99
100
  const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
100
101
  const FLAG_PLAN = argv.includes('--plan') || argv.includes('--dry-run'); // show the interactive checklist, then exit — install NOTHING
@@ -598,6 +599,137 @@ function installReader(cacheDir) {
598
599
  ok('reader installed');
599
600
  }
600
601
 
602
+ // The Console must survive the temporary npm/npx extraction directory. Before 4.0.2 its skills
603
+ // searched a developer checkout, so `/rvbc` worked for maintainers and failed for clean users even
604
+ // though installation was green. Persist the exact runtime shipped by the installer underneath the
605
+ // installed KB: Node then resolves optional reader dependencies from `<cacheDir>/node_modules`, and
606
+ // both Claude Code and Codex get one stable path with no source clone or second package install.
607
+ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT) {
608
+ const runtime = path.join(cacheDir, '.console-runtime');
609
+ const staged = `${runtime}.tmp-${process.pid}`;
610
+ const prior = `${runtime}.prior-${process.pid}`;
611
+ fs.rmSync(staged, { recursive: true, force: true });
612
+ fs.rmSync(prior, { recursive: true, force: true });
613
+ fs.mkdirSync(staged, { recursive: true, mode: 0o700 });
614
+
615
+ const required = [
616
+ ['console', 'console'],
617
+ ['scripts', 'scripts'],
618
+ ['plugin/scripts', 'plugin/scripts'],
619
+ ['kb/brain-profile.mjs', 'kb/brain-profile.mjs'],
620
+ ['bin/install.mjs', 'bin/install.mjs'],
621
+ ['package.json', 'package.json'],
622
+ ];
623
+ for (const [from, to] of required) {
624
+ const source = path.join(sourceRoot, from);
625
+ if (!fs.existsSync(source)) {
626
+ fs.rmSync(staged, { recursive: true, force: true });
627
+ throw new Error(`console runtime is incomplete: missing ${from}`);
628
+ }
629
+ const target = path.join(staged, to);
630
+ fs.mkdirSync(path.dirname(target), { recursive: true });
631
+ fs.cpSync(source, target, { recursive: true, force: true, preserveTimestamps: true });
632
+ }
633
+
634
+ let packageManifest;
635
+ try {
636
+ packageManifest = JSON.parse(fs.readFileSync(path.join(staged, 'package.json'), 'utf8'));
637
+ } catch (error) {
638
+ fs.rmSync(staged, { recursive: true, force: true });
639
+ throw new Error(`console runtime identity is invalid: ${error.message}`);
640
+ }
641
+ if (!packageManifest.version || packageManifest.version !== PACKAGE_VERSION) {
642
+ fs.rmSync(staged, { recursive: true, force: true });
643
+ throw new Error(`console runtime version ${packageManifest.version || '(missing)'} does not match candidate ${PACKAGE_VERSION}`);
644
+ }
645
+ const stagedEntry = path.join(staged, 'scripts', 'onboarding-console.mjs');
646
+ for (const syntaxTarget of [stagedEntry, path.join(staged, 'bin', 'install.mjs')]) {
647
+ const checked = spawnSync(process.execPath, ['--check', syntaxTarget], { encoding: 'utf8' });
648
+ if (checked.error || checked.status !== 0) {
649
+ fs.rmSync(staged, { recursive: true, force: true });
650
+ throw new Error(`console runtime failed syntax verification: ${path.relative(staged, syntaxTarget)}`);
651
+ }
652
+ }
653
+ const identity = {
654
+ product: 'ruvnet-brain-console-runtime',
655
+ schema: 1,
656
+ apiContract: 1,
657
+ runtimeVersion: packageManifest.version,
658
+ entrypoint: 'scripts/onboarding-console.mjs',
659
+ sourceSha256: crypto.createHash('sha256').update(fs.readFileSync(stagedEntry)).digest('hex'),
660
+ };
661
+ fs.writeFileSync(path.join(staged, 'runtime-identity.json'), `${JSON.stringify(identity, null, 2)}\n`, { mode: 0o600 });
662
+
663
+ let state = 'staged';
664
+ const entry = path.join(runtime, identity.entrypoint);
665
+ return {
666
+ identity,
667
+ entry,
668
+ activate() {
669
+ if (state !== 'staged') throw new Error(`console runtime transaction cannot activate from ${state}`);
670
+ if (fs.existsSync(runtime)) fs.renameSync(runtime, prior);
671
+ try {
672
+ fs.renameSync(staged, runtime);
673
+ state = 'active';
674
+ return entry;
675
+ } catch (error) {
676
+ if (fs.existsSync(prior) && !fs.existsSync(runtime)) fs.renameSync(prior, runtime);
677
+ fs.rmSync(staged, { recursive: true, force: true });
678
+ state = 'rolled-back';
679
+ throw error;
680
+ }
681
+ },
682
+ commit() {
683
+ if (state !== 'active') throw new Error(`console runtime transaction cannot commit from ${state}`);
684
+ fs.rmSync(prior, { recursive: true, force: true });
685
+ state = 'committed';
686
+ return entry;
687
+ },
688
+ rollback() {
689
+ if (state === 'staged') fs.rmSync(staged, { recursive: true, force: true });
690
+ if (state === 'active') {
691
+ fs.rmSync(runtime, { recursive: true, force: true });
692
+ if (fs.existsSync(prior)) fs.renameSync(prior, runtime);
693
+ }
694
+ fs.rmSync(staged, { recursive: true, force: true });
695
+ fs.rmSync(prior, { recursive: true, force: true });
696
+ state = 'rolled-back';
697
+ },
698
+ };
699
+ }
700
+
701
+ export function installConsoleRuntime(cacheDir, sourceRoot = REPO_ROOT) {
702
+ const transaction = beginConsoleRuntimeTransaction(cacheDir, sourceRoot);
703
+ try {
704
+ transaction.activate();
705
+ return transaction.commit();
706
+ } catch (error) {
707
+ transaction.rollback();
708
+ throw error;
709
+ }
710
+ }
711
+
712
+ export function consoleRestartState(identity, {
713
+ receiptDir = path.join(process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'), 'console-instances'),
714
+ } = {}) {
715
+ let receipts = [];
716
+ try {
717
+ receipts = fs.readdirSync(receiptDir)
718
+ .filter((name) => name.endsWith('.json'))
719
+ .map((name) => {
720
+ try { return JSON.parse(fs.readFileSync(path.join(receiptDir, name), 'utf8')); }
721
+ catch { return null; }
722
+ })
723
+ .filter((receipt) => receipt?.product === 'ruvnet-brain-console' && receipt.schema === 1);
724
+ } catch { /* no running Console receipts is the ordinary ready state */ }
725
+ const staleInstances = receipts.filter((receipt) => receipt.sourceSha256 !== identity.sourceSha256).length;
726
+ return {
727
+ state: staleInstances > 0 ? 'pending-console-restart' : 'ready',
728
+ instanceReceipts: receipts.length,
729
+ staleInstances,
730
+ };
731
+ }
732
+
601
733
  // ── plugin presence: the ONLY reliable proof the slash commands will exist ───────────────────────
602
734
  // Reported by a user on 3.4.21-dev whose install was otherwise healthy: `/rvbc` returned
603
735
  // "Unknown command: /rvbc. Did you mean /rvf?". search_ruvnet worked, the KB was current — the
@@ -753,6 +885,7 @@ function codexManagedBlock(serverPath) {
753
885
  '[mcp_servers.ruvnet-brain]',
754
886
  'command = "node"',
755
887
  `args = [${JSON.stringify(serverPath)}]`,
888
+ 'startup_timeout_sec = 30',
756
889
  CODEX_BLOCK_END,
757
890
  ].join('\n');
758
891
  }
@@ -810,7 +943,11 @@ function removeCodexWiring() {
810
943
  // What the doctor asserts from disk — never from the fact that we once ran. "Wired" means our entry
811
944
  // is in the config AND the server.mjs it points at is really there, because a registration pointing
812
945
  // at a deleted file is worse than no registration: Codex fails at spawn time with nothing to read.
813
- export function codexStatus({ configPath = codexConfigPath(), codexDir = codexHomeDir() } = {}) {
946
+ export function codexStatus({
947
+ configPath = codexConfigPath(),
948
+ codexDir = codexHomeDir(),
949
+ brainHome = path.join(path.dirname(codexDir), '.cache', 'ruvnet-brain'),
950
+ } = {}) {
814
951
  let host = false;
815
952
  try { host = fs.existsSync(codexDir); } catch { /* unreadable — treat as absent */ }
816
953
  if (!host) return { host: false, wired: false, serverPath: null, serverExists: false };
@@ -821,7 +958,63 @@ export function codexStatus({ configPath = codexConfigPath(), codexDir = codexHo
821
958
  try { serverPath = m ? JSON.parse(m[1]) : null; } catch { /* malformed entry — treat as absent */ }
822
959
  let serverExists = false;
823
960
  try { serverExists = serverPath ? fs.existsSync(serverPath) : false; } catch { /* unreadable */ }
824
- return { host: true, wired: Boolean(serverPath) && serverExists, serverPath, serverExists };
961
+ const section = m ? text.slice(m.index).split(/\n[ \t]*\[/, 1)[0] : '';
962
+ const timeoutMatch = /(?:^|\n)[ \t]*startup_timeout_sec\s*=\s*(\d+)/.exec(section);
963
+ const startupTimeoutSec = timeoutMatch ? Number(timeoutMatch[1]) : null;
964
+ const readinessReceipt = (() => {
965
+ try { return JSON.parse(fs.readFileSync(path.join(brainHome, 'mcp-readiness.json'), 'utf8')); }
966
+ catch { return null; }
967
+ })();
968
+ let live = false;
969
+ if (readinessReceipt?.state === 'ready'
970
+ && Number.isInteger(readinessReceipt.pid)
971
+ && Number.isInteger(readinessReceipt.workerPid)) {
972
+ try {
973
+ process.kill(readinessReceipt.pid, 0);
974
+ process.kill(readinessReceipt.workerPid, 0);
975
+ live = true;
976
+ } catch { /* stale process or worker receipt */ }
977
+ }
978
+ const readiness = live ? 'ready' : readinessReceipt?.state === 'degraded' ? 'degraded' : 'registered';
979
+ return {
980
+ host: true,
981
+ wired: Boolean(serverPath) && serverExists,
982
+ serverPath,
983
+ serverExists,
984
+ startupTimeoutSec,
985
+ readiness,
986
+ live,
987
+ readinessReceipt,
988
+ };
989
+ }
990
+
991
+ export function codexMcpGuidance(status) {
992
+ if (!status?.wired) {
993
+ return { healthy: false, blocking: true, summary: 'Codex MCP is not registered to a usable server.', detail: null };
994
+ }
995
+ if (status.live && status.readiness === 'ready') {
996
+ return {
997
+ healthy: true,
998
+ blocking: false,
999
+ summary: `Codex MCP ready and live; discovery uses a ${status.startupTimeoutSec ?? 'configured'}s startup deadline.`,
1000
+ detail: 'A running MCP shell completed worker initialize and warmup.',
1001
+ };
1002
+ }
1003
+ if (status.readiness === 'degraded') {
1004
+ const receipt = status.readinessReceipt || {};
1005
+ return {
1006
+ healthy: false,
1007
+ blocking: true,
1008
+ summary: 'Codex MCP is registered but degraded.',
1009
+ detail: `${receipt.phase || 'startup'}: ${receipt.error || 'worker readiness failed'}`,
1010
+ };
1011
+ }
1012
+ return {
1013
+ healthy: false,
1014
+ blocking: false,
1015
+ summary: `Codex MCP is registered with a ${status.startupTimeoutSec ?? 'missing'}s startup deadline; live readiness is not yet proven.`,
1016
+ detail: 'The first real search establishes worker readiness without removing search_ruvnet from discovery.',
1017
+ };
825
1018
  }
826
1019
 
827
1020
  // Atomic file replacement: produce the new bytes BESIDE the target, then rename() over it. Against
@@ -856,6 +1049,7 @@ export function wireCodexHost({
856
1049
  serverDir = codexServerDir(),
857
1050
  source = path.join(__dirname, '..', 'plugin', 'mcp', 'server.mjs'),
858
1051
  hookWrapperSource = path.join(__dirname, '..', 'plugin', 'scripts', 'codex-hook-wrapper.mjs'),
1052
+ runtimePreferencesSource = path.join(__dirname, '..', 'plugin', 'scripts', 'runtime-preferences.mjs'),
859
1053
  hookWrapperPath = codexHookWrapperPath(codexDir),
860
1054
  announce = true,
861
1055
  } = {}) {
@@ -876,14 +1070,21 @@ export function wireCodexHost({
876
1070
  if (announce) warn('MCP structured-interface module missing from this bundle — Codex left untouched (non-fatal)');
877
1071
  return { host: true, action: 'no-source' };
878
1072
  }
1073
+ if (!fs.existsSync(runtimePreferencesSource)) {
1074
+ if (announce) warn('MCP runtime-preferences module missing from this bundle — Codex left untouched (non-fatal)');
1075
+ return { host: true, action: 'no-source' };
1076
+ }
879
1077
  const serverPath = path.join(serverDir, 'server.mjs');
880
1078
  const managedCliPath = path.join(serverDir, 'managed-cli-interface.mjs');
1079
+ const runtimePreferencesPath = path.join(path.dirname(serverDir), 'scripts', 'runtime-preferences.mjs');
881
1080
  fs.mkdirSync(serverDir, { recursive: true });
882
1081
  // Write-beside-then-rename, both here and for the config below (issue #43): an interrupted plain
883
1082
  // copy leaves a TORN server.mjs at the exact path a prior install's config already points at, so
884
1083
  // Codex spawns half a file. rename() over the target is atomic; a failure leaves the old bytes.
885
1084
  // Copy the dependency first. If the later server swap fails, the previously registered server
886
1085
  // remains byte-intact and continues to import a backward-compatible module at the same path.
1086
+ fs.mkdirSync(path.dirname(runtimePreferencesPath), { recursive: true });
1087
+ atomicReplace(runtimePreferencesPath, (tmp) => fs.copyFileSync(runtimePreferencesSource, tmp));
887
1088
  atomicReplace(managedCliPath, (tmp) => fs.copyFileSync(managedCliSource, tmp));
888
1089
  atomicReplace(serverPath, (tmp) => fs.copyFileSync(source, tmp));
889
1090
  if (fs.existsSync(hookWrapperSource)) {
@@ -899,7 +1100,7 @@ export function wireCodexHost({
899
1100
  ok('Codex already declares ruvnet-brain in your own config — left exactly as you wrote it');
900
1101
  info(` to hand it to us instead, delete that ${c.bold('[mcp_servers.ruvnet-brain]')} block and re-run this installer`);
901
1102
  }
902
- return { host: true, action, serverPath, managedCliPath, hookWrapperPath };
1103
+ return { host: true, action, serverPath, managedCliPath, runtimePreferencesPath, hookWrapperPath };
903
1104
  }
904
1105
  if (text !== before) {
905
1106
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -910,7 +1111,7 @@ export function wireCodexHost({
910
1111
  info(` server: ${serverPath} ${c.dim('(persistent copy — the npx dir vanishes)')}`);
911
1112
  info(` ${c.dim('only our marked block is written; every other section is byte-preserved')}`);
912
1113
  }
913
- return { host: true, action, serverPath, managedCliPath, hookWrapperPath, changed: text !== before };
1114
+ return { host: true, action, serverPath, managedCliPath, runtimePreferencesPath, hookWrapperPath, changed: text !== before };
914
1115
  }
915
1116
 
916
1117
  const CODEX_PLUGIN_ID = 'ruvnet-brain@ruvnet-brain';
@@ -1553,7 +1754,9 @@ async function doctor() {
1553
1754
  if (env.ruflo) {
1554
1755
  rufloOperational = probeRufloOperationalHealth();
1555
1756
  if (rufloOperational.healthy) {
1556
- ok('Ruflo operational — runtime, memory, and learning signals agree');
1757
+ ok(rufloOperational.directMode
1758
+ ? 'Ruflo direct mode ready — daemon/swarm is stopped by design; AgentDB remains CLI-backed'
1759
+ : 'Ruflo operational — runtime, memory, and learning signals agree');
1557
1760
  } else {
1558
1761
  warn('Ruflo CLI is present, but operational learning is DEGRADED — availability is not enforcement.');
1559
1762
  if (rufloOperational.stopped) warn(' live status reports STOPPED / swarm not running');
@@ -1623,11 +1826,14 @@ async function doctor() {
1623
1826
  // from disk (our entry present AND the server.mjs it names really there), never asserted from the
1624
1827
  // fact that an install once ran.
1625
1828
  const cx = codexStatus();
1829
+ let codexMcp = null;
1626
1830
  let codexLifecycle = null;
1627
1831
  if (!cx.host) {
1628
1832
  console.log(` ${c.dim('Codex: no host detected (no ~/.codex) — nothing to wire.')}`);
1629
1833
  } else if (cx.wired) {
1630
- console.log(` ${c.green('✓ Codex: wired.')} search_ruvnet is registered in ~/.codex/config.toml and its server exists.`);
1834
+ codexMcp = codexMcpGuidance(cx);
1835
+ console.log(` ${codexMcp.healthy ? c.green('✓') : codexMcp.blocking ? c.yellow('!') : c.dim('○')} ${codexMcp.summary}`);
1836
+ if (codexMcp.detail) console.log(` ${codexMcp.detail}`);
1631
1837
  codexLifecycle = await codexLifecycleStatus();
1632
1838
  printCodexLifecycle(codexLifecycle);
1633
1839
  } else {
@@ -1696,10 +1902,12 @@ async function doctor() {
1696
1902
  && !codexLifecycleGuidance(codexLifecycle).intentional,
1697
1903
  );
1698
1904
  const codexWiringFailed = Boolean(cx.host && !cx.wired);
1905
+ const codexReadinessFailed = Boolean(codexMcp?.blocking);
1699
1906
  const failed = (hookResult ? hookResult.exitCode !== 0 : !allGreen)
1700
1907
  || groundingUnprovenPersisted
1701
1908
  || codexLifecycleFailed
1702
1909
  || codexWiringFailed
1910
+ || codexReadinessFailed
1703
1911
  || Boolean(rufloOperational && !rufloOperational.healthy);
1704
1912
  if (failed && !hookResult && !groundingUnprovenPersisted) {
1705
1913
  console.log(` ${c.red('✗ FAILING')} — the warnings above are real. Re-run ${c.bold('npx ruvnet-brain')} to repair.`);
@@ -1963,32 +2171,61 @@ function missingUpdaterHelp(kbDir) {
1963
2171
  console.error(` — the current bundle ships forge-update.mjs; then this command will work.`);
1964
2172
  }
1965
2173
 
1966
- function syncHostsAfterUpdate() {
2174
+ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
2175
+ sourceRoot = REPO_ROOT,
2176
+ brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'),
2177
+ consoleReceiptDir = path.join(brainHome, 'console-instances'),
2178
+ wireClaude = wirePlugin,
2179
+ wireCodexHost: detectCodexHost = wireCodexHost,
2180
+ wireCodexPlugin: installCodexPlugin = wireCodexPlugin,
2181
+ runStableSpine = (apply) => spawnSync(
2182
+ process.execPath,
2183
+ [apply, '--auto', '--expected-version', PACKAGE_VERSION],
2184
+ { stdio: 'inherit', env: process.env },
2185
+ ),
2186
+ } = {}) {
1967
2187
  const results = {};
1968
- results.claude = wirePlugin({ expectedVersion: PACKAGE_VERSION, requireManaged: true });
1969
- if (results.claude.host && !results.claude.wired) return { ok: false, results };
2188
+ let runtimeTransaction;
1970
2189
  try {
1971
- results.codexHost = wireCodexHost();
2190
+ runtimeTransaction = beginConsoleRuntimeTransaction(cacheDir, sourceRoot);
2191
+ } catch (error) {
2192
+ return { ok: false, results, error: `Console runtime staging failed: ${error.message}` };
2193
+ }
2194
+ const fail = (detail = {}) => {
2195
+ runtimeTransaction.rollback();
2196
+ return { ok: false, results, ...detail };
2197
+ };
2198
+
2199
+ try {
2200
+ results.claude = wireClaude === wirePlugin
2201
+ ? wirePlugin({ expectedVersion: PACKAGE_VERSION, requireManaged: true })
2202
+ : wireClaude({ expectedVersion: PACKAGE_VERSION, requireManaged: true });
2203
+ if (results.claude.host && !results.claude.wired) return fail();
2204
+ results.codexHost = detectCodexHost();
1972
2205
  if (results.codexHost.host) {
1973
- results.codex = wireCodexPlugin({ expectedVersion: PACKAGE_VERSION });
2206
+ results.codex = installCodexPlugin({ expectedVersion: PACKAGE_VERSION });
1974
2207
  if (!['unchanged', 'installed', 'updated', 'disabled'].includes(results.codex.action)) {
1975
- return { ok: false, results };
2208
+ return fail();
1976
2209
  }
1977
2210
  }
1978
2211
  } catch (error) {
1979
2212
  results.codex = { action: 'failed', error: error?.message || String(error) };
1980
- return { ok: false, results };
2213
+ return fail();
1981
2214
  }
1982
2215
 
1983
- const apply = path.join(__dirname, '..', 'plugin', 'scripts', 'update-apply.mjs');
1984
- if (!fs.existsSync(apply)) return { ok: false, results, error: 'Stable Spine updater missing from package' };
1985
- const applied = spawnSync(process.execPath, [apply, '--auto', '--expected-version', PACKAGE_VERSION], { stdio: 'inherit', env: process.env });
2216
+ const apply = path.join(sourceRoot, 'plugin', 'scripts', 'update-apply.mjs');
2217
+ if (!fs.existsSync(apply)) return fail({ error: 'Stable Spine updater missing from package' });
2218
+ const applied = runStableSpine(apply);
1986
2219
  const okApplied = !applied.error && applied.status === 0;
1987
2220
  if (okApplied) {
1988
- const home = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
1989
- const receiptPath = path.join(home, 'host-convergence.json');
2221
+ const receiptPath = path.join(brainHome, 'host-convergence.json');
1990
2222
  try {
1991
- fs.mkdirSync(home, { recursive: true });
2223
+ runtimeTransaction.activate();
2224
+ results.consoleRuntime = {
2225
+ ...runtimeTransaction.identity,
2226
+ ...consoleRestartState(runtimeTransaction.identity, { receiptDir: consoleReceiptDir }),
2227
+ };
2228
+ fs.mkdirSync(brainHome, { recursive: true });
1992
2229
  const tmp = `${receiptPath}.tmp-${process.pid}`;
1993
2230
  fs.writeFileSync(tmp, JSON.stringify({
1994
2231
  desiredVersion: PACKAGE_VERSION,
@@ -1997,13 +2234,16 @@ function syncHostsAfterUpdate() {
1997
2234
  claude: { state: results.claude.host ? 'ready' : 'absent', version: results.claude.version || null },
1998
2235
  codex: { state: results.codex?.action === 'disabled' ? 'disabled' : (results.codexHost?.host ? 'ready' : 'absent'), version: results.codex?.version || null },
1999
2236
  },
2237
+ consoleRuntime: results.consoleRuntime,
2000
2238
  }, null, 2));
2001
2239
  fs.renameSync(tmp, receiptPath);
2240
+ runtimeTransaction.commit();
2002
2241
  } catch (error) {
2003
- return { ok: false, results, applyStatus: applied.status, error: `host convergence receipt failed: ${error.message}` };
2242
+ return fail({ applyStatus: applied.status, error: `Console runtime convergence failed: ${error.message}` });
2004
2243
  }
2005
2244
  }
2006
- return { ok: okApplied, results, applyStatus: applied.status };
2245
+ if (!okApplied) return fail({ applyStatus: applied.status, error: applied.error?.message || 'Stable Spine activation failed' });
2246
+ return { ok: true, results, applyStatus: applied.status };
2007
2247
  }
2008
2248
 
2009
2249
  function runUpdate() {
@@ -2048,7 +2288,7 @@ function runUpdate() {
2048
2288
  }
2049
2289
  if (updateStatus === 0) {
2050
2290
  info(c.dim('\nsynchronizing every detected host to this exact published version…\n'));
2051
- const convergence = syncHostsAfterUpdate();
2291
+ const convergence = syncHostsAfterUpdate(kbDir);
2052
2292
  if (!convergence.ok) {
2053
2293
  warn(`host synchronization is incomplete — runtime stays on the prior verified generation${convergence.error ? ` (${convergence.error})` : ''}`);
2054
2294
  updateStatus = 1;
@@ -2421,6 +2661,17 @@ function printFootprint({ heading = 'What this put on your machine' } = {}) {
2421
2661
  return items;
2422
2662
  }
2423
2663
 
2664
+ function showWhatsNew() {
2665
+ const executable = path.join(REPO_ROOT, 'plugin', 'scripts', 'whats-new.mjs');
2666
+ const result = spawnSync(process.execPath, [executable], { stdio: 'inherit' });
2667
+ if (result.error) {
2668
+ console.error(`RuvNet Brain What's New failed: ${result.error.message}`);
2669
+ process.exitCode = 1;
2670
+ return;
2671
+ }
2672
+ process.exitCode = result.status === 0 ? 0 : 1;
2673
+ }
2674
+
2424
2675
  /**
2425
2676
  * Surgically remove ONLY our block from CLAUDE.md, leaving every other line exactly as it was.
2426
2677
  * Backed up and written atomically, same as when it was added — taking something away is at least
@@ -3070,8 +3321,15 @@ export function classifyRufloOperationalHealth({ status = '', memory = '', metri
3070
3321
  && /Total Routes\s*[|:]?\s*0\b/i.test(metrics)
3071
3322
  && /Total Executed\s*[|:]?\s*0\b/i.test(metrics);
3072
3323
  const memoryContradiction = statusSaysNoMemory && memoryEntries > 0;
3324
+ // Brain 4 is explicitly zero-daemon: `ruflo memory store/search` open the per-project
3325
+ // AgentDB directly. A stopped orchestration daemon therefore makes daemon-owned status
3326
+ // and metrics non-authoritative; treating that intentional state as degraded made Doctor
3327
+ // exit 1 immediately after proving source-grounded retrieval worked. Keep the strict
3328
+ // contradiction/learning checks when a daemon is actually running.
3329
+ const directMode = stopped;
3073
3330
  return {
3074
- healthy: !stopped && !memoryContradiction && !zeroLearning,
3331
+ healthy: directMode || (!memoryContradiction && !zeroLearning),
3332
+ directMode,
3075
3333
  stopped,
3076
3334
  zeroLearning,
3077
3335
  memoryContradiction,
@@ -3653,6 +3911,7 @@ Usage:
3653
3911
  npx ruvnet-brain --disable-nightly Remove the nightly schedule (safe to run any time)
3654
3912
  npx ruvnet-brain --what-changed Show exactly what RuvNet Brain has put on this machine,
3655
3913
  with the undo command for each piece
3914
+ npx ruvnet-brain --whats-new Show the curated v3 → v4 capabilities and honest limits
3656
3915
  npx ruvnet-brain --uninstall Remove all of it (bundle, LaunchAgents, and our CLAUDE.md
3657
3916
  block only — your own CLAUDE.md content is preserved and backed up)
3658
3917
  npx ruvnet-brain --enable-spend-guard Install the hourly runaway-agent spend alarm (alert-only)
@@ -3692,7 +3951,17 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3692
3951
 
3693
3952
  // ── main ─────────────────────────────────────────────────────────────────────────────────────────
3694
3953
  (async () => {
3695
- if (IMPORT_ONLY) return; // imported for its exports (tests) — never run the installer as a side effect
3954
+ // Importing an executable module for its exported policy helpers must never install software.
3955
+ // The old contract required every caller to remember RUVNET_BRAIN_IMPORT_ONLY=1; one missed flag
3956
+ // in a unit test ran a real install against the maintainer's HOME. Direct argv identity is the
3957
+ // authority boundary. The old IMPORT_ONLY flag may still be present in a long-lived test worker;
3958
+ // it must never suppress a genuinely direct CLI execution.
3959
+ const canonical = (value) => {
3960
+ try { return fs.realpathSync(value); } catch { return path.resolve(value); }
3961
+ };
3962
+ const invokedDirectly = Boolean(process.argv[1])
3963
+ && canonical(process.argv[1]) === canonical(fileURLToPath(import.meta.url));
3964
+ if (!invokedDirectly) return;
3696
3965
  if (FLAG_HELP) return showHelp();
3697
3966
  // `process.exitCode`, not `return` — doctor()'s verdict is the whole point of running it in a
3698
3967
  // script. A bare `return await doctor()` discarded the number, which is how "! Needs attention"
@@ -3711,6 +3980,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3711
3980
  if (FLAG_DISABLE_SPEND_GUARD) { disableSpendGuard(); return; }
3712
3981
  if (FLAG_UNINSTALL) { uninstallAll(); return; }
3713
3982
  if (FLAG_WHAT_CHANGED) { printBanner('what RuvNet Brain put on this machine'); printFootprint(); return; }
3983
+ if (FLAG_WHATS_NEW) { showWhatsNew(); return; }
3714
3984
 
3715
3985
  printBanner('installer');
3716
3986
  console.log(c.dim("I'll set up the brain for Claude Code and Codex, explaining each step as I go.\n"));
@@ -3856,6 +4126,15 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3856
4126
  }
3857
4127
 
3858
4128
  installReader(cacheDir);
4129
+ try {
4130
+ const consoleEntry = installConsoleRuntime(cacheDir);
4131
+ ok(`Brain Console installed at ${consoleEntry}`);
4132
+ } catch (error) {
4133
+ die(
4134
+ `the Brain Console runtime could not be installed (${error.message})`,
4135
+ `The knowledge base is present, but /rvbc would be broken. Re-run the installer from a complete package.`,
4136
+ );
4137
+ }
3859
4138
  const plugin = wirePlugin();
3860
4139
  // Codex hosts got nothing before this (issue #42): shipped, never registered. Non-fatal like every
3861
4140
  // other wiring step — a second host we cannot reach must never break the one we can.