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
package/bin/install.mjs CHANGED
@@ -84,6 +84,7 @@ const FLAG_DEMO = argv.includes('--demo'); // guided, real (non-fabricated) walk
84
84
  const FLAG_FEEDBACK = argv.includes('--feedback'); // prefill a GitHub Discussion (version + health, nothing private) and open it
85
85
  // ── freshness flags — invoke/schedule the SELF-UPDATER the bundle already ships (kb/forge-update.mjs) ──
86
86
  const FLAG_UPDATE = argv.includes('--update'); // one-shot: pull the latest Release bundle into the installed brain now
87
+ const FLAG_HOST_SYNC_ONLY = argv.includes('--host-sync-only'); // internal: repair host shells even when KB updater is absent
87
88
  const FLAG_AUTO = argv.includes('--auto'); // with --update: also enroll in Evergreen auto-update, so it's never run by hand again
88
89
  const FLAG_ENABLE_NIGHTLY = argv.includes('--enable-nightly'); // schedule that update nightly (macOS LaunchAgent)
89
90
  const FLAG_DISABLE_NIGHTLY = argv.includes('--disable-nightly'); // remove the nightly schedule
@@ -94,6 +95,7 @@ const FLAG_ENABLE_SPEND_GUARD = argv.includes('--enable-spend-guard');
94
95
  const FLAG_DISABLE_SPEND_GUARD = argv.includes('--disable-spend-guard'); // the missing undo
95
96
  const FLAG_UNINSTALL = argv.includes('--uninstall'); // reverse everything, in one command
96
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
97
99
  // ── onboarding-experience flags (all optional; every offer is safe to decline) ──
98
100
  const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
99
101
  const FLAG_PLAN = argv.includes('--plan') || argv.includes('--dry-run'); // show the interactive checklist, then exit — install NOTHING
@@ -597,6 +599,50 @@ function installReader(cacheDir) {
597
599
  ok('reader installed');
598
600
  }
599
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 installConsoleRuntime(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
+ if (fs.existsSync(runtime)) fs.renameSync(runtime, prior);
635
+ try {
636
+ fs.renameSync(staged, runtime);
637
+ fs.rmSync(prior, { recursive: true, force: true });
638
+ } catch (error) {
639
+ if (fs.existsSync(prior) && !fs.existsSync(runtime)) fs.renameSync(prior, runtime);
640
+ fs.rmSync(staged, { recursive: true, force: true });
641
+ throw error;
642
+ }
643
+ return path.join(runtime, 'scripts', 'onboarding-console.mjs');
644
+ }
645
+
600
646
  // ── plugin presence: the ONLY reliable proof the slash commands will exist ───────────────────────
601
647
  // Reported by a user on 3.4.21-dev whose install was otherwise healthy: `/rvbc` returned
602
648
  // "Unknown command: /rvbc. Did you mean /rvf?". search_ruvnet worked, the KB was current — the
@@ -620,7 +666,40 @@ function pluginCommandsDir() {
620
666
  }
621
667
 
622
668
  // ── step: wire the Claude Code plugin ────────────────────────────────────────────────────────────
623
- function wirePlugin() {
669
+ export function claudePluginStatus({ home = os.homedir() } = {}) {
670
+ const registry = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
671
+ let sawUserRecord = false;
672
+ try {
673
+ const doc = JSON.parse(fs.readFileSync(registry, 'utf8'));
674
+ const rows = doc?.plugins?.['ruvnet-brain@ruvnet-brain'];
675
+ const row = Array.isArray(rows) ? rows.find((candidate) => candidate?.scope === 'user') : null;
676
+ if (!row?.installPath) return { managed: false, installed: false, version: null };
677
+ sawUserRecord = true;
678
+ const realHome = fs.realpathSync(home);
679
+ const cacheRoot = path.join(realHome, '.claude', 'plugins', 'cache', 'ruvnet-brain', 'ruvnet-brain');
680
+ const realInstall = fs.realpathSync(row.installPath);
681
+ if (realInstall !== cacheRoot && !realInstall.startsWith(`${cacheRoot}${path.sep}`)) {
682
+ return { managed: true, installed: false, version: null, error: 'user plugin installPath escapes the managed cache' };
683
+ }
684
+ const manifestPath = path.join(realInstall, '.claude-plugin', 'plugin.json');
685
+ const commandPath = path.join(realInstall, 'commands', 'rvbc.md');
686
+ if (!fs.lstatSync(manifestPath).isFile() || fs.lstatSync(manifestPath).isSymbolicLink()
687
+ || !fs.lstatSync(commandPath).isFile() || fs.lstatSync(commandPath).isSymbolicLink()) {
688
+ return { managed: true, installed: false, version: null, error: 'managed payload files are not regular files' };
689
+ }
690
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
691
+ return {
692
+ managed: true,
693
+ installed: true,
694
+ version: manifest?.version || row.version || null,
695
+ installPath: realInstall,
696
+ };
697
+ } catch {
698
+ return { managed: sawUserRecord, installed: false, version: null };
699
+ }
700
+ }
701
+
702
+ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false } = {}) {
624
703
  step(
625
704
  'Wiring the Claude Code plugin',
626
705
  'this registers search_ruvnet + the grounding hook so Claude uses the brain automatically',
@@ -636,37 +715,54 @@ function wirePlugin() {
636
715
  info(` ${c.bold(manualMarketplace)}`);
637
716
  info(` ${c.bold(manualInstall)}`);
638
717
  info(`Then reopen Claude Code once. ${c.dim('Or simply open Claude Code and ask: “finish setting up the RuvNet Brain.”')}`);
639
- return { wired: false, manualMarketplace, manualInstall };
718
+ return { host: false, wired: false, manualMarketplace, manualInstall };
640
719
  }
641
720
 
642
- const addedMarket = tryRun('claude', ['plugin', 'marketplace', 'add', 'stuinfla/ruvnet-brain']);
721
+ const before = claudePluginStatus();
722
+ if (requireManaged && !before.managed) return { host: false, wired: false, action: 'unmanaged' };
723
+ const addedMarket = before.managed
724
+ ? tryRun('claude', ['plugin', 'marketplace', 'update', 'ruvnet-brain'])
725
+ : tryRun('claude', ['plugin', 'marketplace', 'add', 'stuinfla/ruvnet-brain']);
643
726
  // Deliberately NOT reassuring here. This used to say "it may already be added — that's fine",
644
727
  // which is a GUESS about someone else's machine, and when it was wrong the user finished the
645
728
  // install with a working search_ruvnet, no slash commands, and a message telling them all was
646
729
  // well. The real state is checked below; nothing is declared fine until it has been looked at.
647
730
  if (!addedMarket) info(`marketplace add didn't report success — checking what actually landed…`);
648
731
 
649
- tryRun('claude', ['plugin', 'install', 'ruvnet-brain@ruvnet-brain', '--scope', 'user']);
732
+ tryRun('claude', before.installed
733
+ ? ['plugin', 'update', 'ruvnet-brain@ruvnet-brain', '--scope', 'user']
734
+ : ['plugin', 'install', 'ruvnet-brain@ruvnet-brain', '--scope', 'user']);
650
735
 
651
736
  // NEVER take "installed" on faith — same discipline verifyInstall() applies to the KB. An exit
652
737
  // code says the command ran, not that the plugin is usable; the commands either exist on disk or
653
738
  // they do not. This is the difference between `/rvbc` working and "Unknown command: /rvbc".
654
- const commandsDir = pluginCommandsDir();
655
- if (commandsDir) {
656
- ok('plugin installed at user scope (global, alongside Ruflo / RuVector)');
739
+ const installed = claudePluginStatus();
740
+ if (installed.installed && (!expectedVersion || installed.version === expectedVersion)) {
741
+ ok(`plugin installed at user scope (global, alongside Ruflo / RuVector) — exact version ${installed.version}`);
657
742
  info(` commands available after a restart: ${c.bold('/rvbc')}, ${c.bold('/ruvnet-brain:configure')}`);
658
- return { wired: true, manualMarketplace, manualInstall };
743
+ return { host: true, wired: true, version: installed.version, manualMarketplace, manualInstall };
659
744
  }
660
745
 
661
746
  // The honest failure. The brain still WORKS — this is the difference between a broken install and
662
747
  // a partial one, and the user is told exactly which they have instead of being congratulated.
663
- warn(`the plugin did NOT land — so slash commands like ${c.bold('/rvbc')} will not exist yet.`);
748
+ const mismatch = installed.installed && expectedVersion && installed.version !== expectedVersion;
749
+ warn(mismatch
750
+ ? `Claude installed ${installed.version || 'an unknown version'}, not required ${expectedVersion}; refusing to call this host converged.`
751
+ : `the plugin did NOT land — so slash commands like ${c.bold('/rvbc')} will not exist yet.`);
664
752
  info(`${c.green('Your brain still works')}: search_ruvnet is wired and Claude will ground answers with it.`);
665
753
  info(`Only the plugin extras (slash commands, the Console, the grounding hook) are missing.`);
666
754
  info(`Run these two yourself to finish:`);
667
755
  info(` ${c.bold(manualMarketplace)}`);
668
756
  info(` ${c.bold(manualInstall)}`);
669
- return { wired: false, manualMarketplace, manualInstall };
757
+ return {
758
+ host: true,
759
+ wired: false,
760
+ action: mismatch ? 'verification-failed' : 'install-failed',
761
+ expectedVersion,
762
+ version: installed.version,
763
+ manualMarketplace,
764
+ manualInstall,
765
+ };
670
766
  }
671
767
 
672
768
  // ── step: wire the Codex host (issue #42, Henrik Pettersen) ──────────────────────────────────────
@@ -725,6 +821,37 @@ export function mergeCodexConfig(existing, serverPath) {
725
821
  return { text: `${text}${sep}${block}\n`, action: 'added' };
726
822
  }
727
823
 
824
+ export function removeCodexManagedBlock(existing) {
825
+ const text = typeof existing === 'string' ? existing : '';
826
+ const start = text.indexOf(CODEX_BLOCK_START);
827
+ const end = text.indexOf(CODEX_BLOCK_END, start === -1 ? 0 : start);
828
+ if (start === -1 || end === -1 || end < start) return { text, action: 'absent' };
829
+ const blockEnd = end + CODEX_BLOCK_END.length;
830
+ let before = text.slice(0, start);
831
+ let after = text.slice(blockEnd);
832
+ // mergeCodexConfig inserts one separator newline before the block and the block carries one
833
+ // trailing newline. Remove exactly those two installer-owned seam bytes when present.
834
+ if (before.endsWith('\n\n')) before = before.slice(0, -1);
835
+ if (after.startsWith('\n')) after = after.slice(1);
836
+ return { text: `${before}${after}`, action: 'removed' };
837
+ }
838
+
839
+ function removeCodexWiring() {
840
+ const configPath = codexConfigPath();
841
+ let before = '';
842
+ try { before = fs.readFileSync(configPath, 'utf8'); } catch { return 'absent'; }
843
+ const result = removeCodexManagedBlock(before);
844
+ if (result.action !== 'removed') return result.action;
845
+ try {
846
+ const backup = `${configPath}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`;
847
+ fs.copyFileSync(configPath, backup);
848
+ atomicReplace(configPath, (tmp) => fs.writeFileSync(tmp, result.text));
849
+ return 'removed';
850
+ } catch {
851
+ return 'failed';
852
+ }
853
+ }
854
+
728
855
  // What the doctor asserts from disk — never from the fact that we once ran. "Wired" means our entry
729
856
  // is in the config AND the server.mjs it points at is really there, because a registration pointing
730
857
  // at a deleted file is worse than no registration: Codex fails at spawn time with nothing to read.
@@ -774,6 +901,7 @@ export function wireCodexHost({
774
901
  serverDir = codexServerDir(),
775
902
  source = path.join(__dirname, '..', 'plugin', 'mcp', 'server.mjs'),
776
903
  hookWrapperSource = path.join(__dirname, '..', 'plugin', 'scripts', 'codex-hook-wrapper.mjs'),
904
+ runtimePreferencesSource = path.join(__dirname, '..', 'plugin', 'scripts', 'runtime-preferences.mjs'),
777
905
  hookWrapperPath = codexHookWrapperPath(codexDir),
778
906
  announce = true,
779
907
  } = {}) {
@@ -794,14 +922,21 @@ export function wireCodexHost({
794
922
  if (announce) warn('MCP structured-interface module missing from this bundle — Codex left untouched (non-fatal)');
795
923
  return { host: true, action: 'no-source' };
796
924
  }
925
+ if (!fs.existsSync(runtimePreferencesSource)) {
926
+ if (announce) warn('MCP runtime-preferences module missing from this bundle — Codex left untouched (non-fatal)');
927
+ return { host: true, action: 'no-source' };
928
+ }
797
929
  const serverPath = path.join(serverDir, 'server.mjs');
798
930
  const managedCliPath = path.join(serverDir, 'managed-cli-interface.mjs');
931
+ const runtimePreferencesPath = path.join(path.dirname(serverDir), 'scripts', 'runtime-preferences.mjs');
799
932
  fs.mkdirSync(serverDir, { recursive: true });
800
933
  // Write-beside-then-rename, both here and for the config below (issue #43): an interrupted plain
801
934
  // copy leaves a TORN server.mjs at the exact path a prior install's config already points at, so
802
935
  // Codex spawns half a file. rename() over the target is atomic; a failure leaves the old bytes.
803
936
  // Copy the dependency first. If the later server swap fails, the previously registered server
804
937
  // remains byte-intact and continues to import a backward-compatible module at the same path.
938
+ fs.mkdirSync(path.dirname(runtimePreferencesPath), { recursive: true });
939
+ atomicReplace(runtimePreferencesPath, (tmp) => fs.copyFileSync(runtimePreferencesSource, tmp));
805
940
  atomicReplace(managedCliPath, (tmp) => fs.copyFileSync(managedCliSource, tmp));
806
941
  atomicReplace(serverPath, (tmp) => fs.copyFileSync(source, tmp));
807
942
  if (fs.existsSync(hookWrapperSource)) {
@@ -817,7 +952,7 @@ export function wireCodexHost({
817
952
  ok('Codex already declares ruvnet-brain in your own config — left exactly as you wrote it');
818
953
  info(` to hand it to us instead, delete that ${c.bold('[mcp_servers.ruvnet-brain]')} block and re-run this installer`);
819
954
  }
820
- return { host: true, action, serverPath, managedCliPath, hookWrapperPath };
955
+ return { host: true, action, serverPath, managedCliPath, runtimePreferencesPath, hookWrapperPath };
821
956
  }
822
957
  if (text !== before) {
823
958
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -828,13 +963,39 @@ export function wireCodexHost({
828
963
  info(` server: ${serverPath} ${c.dim('(persistent copy — the npx dir vanishes)')}`);
829
964
  info(` ${c.dim('only our marked block is written; every other section is byte-preserved')}`);
830
965
  }
831
- return { host: true, action, serverPath, managedCliPath, hookWrapperPath, changed: text !== before };
966
+ return { host: true, action, serverPath, managedCliPath, runtimePreferencesPath, hookWrapperPath, changed: text !== before };
832
967
  }
833
968
 
834
969
  const CODEX_PLUGIN_ID = 'ruvnet-brain@ruvnet-brain';
835
970
  const CODEX_MARKETPLACE = 'ruvnet-brain';
836
971
  const CODEX_MARKETPLACE_SOURCE = 'stuinfla/ruvnet-brain';
837
972
 
973
+ function prepareCodexMarketplace() {
974
+ const target = path.join(
975
+ process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'),
976
+ 'codex-marketplace',
977
+ );
978
+ const staged = `${target}.tmp-${process.pid}`;
979
+ fs.rmSync(staged, { recursive: true, force: true });
980
+ fs.mkdirSync(path.join(staged, '.claude-plugin'), { recursive: true });
981
+ fs.copyFileSync(
982
+ path.join(REPO_ROOT, '.claude-plugin', 'marketplace.json'),
983
+ path.join(staged, '.claude-plugin', 'marketplace.json'),
984
+ );
985
+ fs.cpSync(path.join(REPO_ROOT, 'plugin'), path.join(staged, 'plugin'), { recursive: true });
986
+ const prior = `${target}.prior-${process.pid}`;
987
+ fs.rmSync(prior, { recursive: true, force: true });
988
+ if (fs.existsSync(target)) fs.renameSync(target, prior);
989
+ try {
990
+ fs.renameSync(staged, target);
991
+ fs.rmSync(prior, { recursive: true, force: true });
992
+ } catch (error) {
993
+ if (fs.existsSync(prior) && !fs.existsSync(target)) fs.renameSync(prior, target);
994
+ throw error;
995
+ }
996
+ return target;
997
+ }
998
+
838
999
  // Codex 0.145 returns `{ marketplaces: [...] }`; early preview builds returned the array itself.
839
1000
  // Keep both shapes so an installer update never turns a harmless host-version difference into a
840
1001
  // repeated "marketplace already exists" failure.
@@ -904,6 +1065,7 @@ export function wireCodexPlugin({
904
1065
  return { host: true, action: 'unchanged', ...before };
905
1066
  }
906
1067
 
1068
+ const localMarketplace = runJson === runCodexJson ? prepareCodexMarketplace() : null;
907
1069
  const markets = runJson(['plugin', 'marketplace', 'list', '--json'], options);
908
1070
  if (!markets.ok) {
909
1071
  if (announce) warn(`Codex marketplace check failed — ${markets.error}`);
@@ -911,9 +1073,15 @@ export function wireCodexPlugin({
911
1073
  }
912
1074
  const marketRows = codexMarketplaceRows(markets.value);
913
1075
  const known = marketRows.some((market) => market?.name === CODEX_MARKETPLACE);
914
- const marketAction = known
915
- ? runJson(['plugin', 'marketplace', 'upgrade', CODEX_MARKETPLACE, '--json'], options)
916
- : runJson(['plugin', 'marketplace', 'add', marketplaceSource, '--json'], options);
1076
+ let marketAction;
1077
+ if (localMarketplace) {
1078
+ if (known) runJson(['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE, '--json'], options);
1079
+ marketAction = runJson(['plugin', 'marketplace', 'add', localMarketplace, '--json'], options);
1080
+ } else {
1081
+ marketAction = known
1082
+ ? runJson(['plugin', 'marketplace', 'upgrade', CODEX_MARKETPLACE, '--json'], options)
1083
+ : runJson(['plugin', 'marketplace', 'add', marketplaceSource, '--json'], options);
1084
+ }
917
1085
  if (!marketAction.ok) {
918
1086
  if (announce) warn(`Codex marketplace ${known ? 'upgrade' : 'add'} failed — ${marketAction.error}`);
919
1087
  return { host: true, action: 'marketplace-failed', error: marketAction.error };
@@ -924,9 +1092,9 @@ export function wireCodexPlugin({
924
1092
  return { host: true, action: 'plugin-failed', error: added.error };
925
1093
  }
926
1094
  const after = codexPluginStatus({ ...options, runJson });
927
- if (!after.installed || !after.enabled) {
1095
+ if (!after.installed || !after.enabled || (expectedVersion && after.version !== expectedVersion)) {
928
1096
  if (announce) warn('Codex accepted the install command but the Brain plugin is not installed and enabled.');
929
- return { host: true, action: 'verification-failed', ...after };
1097
+ return { host: true, action: 'verification-failed', expectedVersion, ...after };
930
1098
  }
931
1099
  if (announce) {
932
1100
  ok(`Codex Brain plugin installed and enabled (${after.version || 'version unknown'}).`);
@@ -1434,9 +1602,22 @@ async function doctor() {
1434
1602
  : info('git not found — that\'s fine, this installer never needs it');
1435
1603
  cleanupStrayRuvectorDb(); // issue #39: sweep a leftover empty scaffold from before this fix, if one's here
1436
1604
  const env = detectEnvironment();
1437
- env.ruflo
1438
- ? ok('Ruflo present — orchestration / swarms / SPARC available')
1439
- : warn('Ruflo not found — answers still work. To build: npm install -g claude-flow@alpha (or /plugin add ruvnet/claude-flow)');
1605
+ let rufloOperational = null;
1606
+ if (env.ruflo) {
1607
+ rufloOperational = probeRufloOperationalHealth();
1608
+ if (rufloOperational.healthy) {
1609
+ ok(rufloOperational.directMode
1610
+ ? 'Ruflo direct mode ready — daemon/swarm is stopped by design; AgentDB remains CLI-backed'
1611
+ : 'Ruflo operational — runtime, memory, and learning signals agree');
1612
+ } else {
1613
+ warn('Ruflo CLI is present, but operational learning is DEGRADED — availability is not enforcement.');
1614
+ if (rufloOperational.stopped) warn(' live status reports STOPPED / swarm not running');
1615
+ if (rufloOperational.memoryContradiction) warn(` status reports no memory while status memory reports ${rufloOperational.memoryEntries} entries`);
1616
+ if (rufloOperational.zeroLearning) warn(' learning dashboard is all zero: patterns=0, routes=0, commands=0');
1617
+ }
1618
+ } else {
1619
+ warn('Ruflo not found — answers still work. To build: npm install -g claude-flow@alpha (or /plugin add ruvnet/claude-flow)');
1620
+ }
1440
1621
  env.ruvector
1441
1622
  ? ok('RuVector present — vector CLI / MCP available')
1442
1623
  : warn(`RuVector not found — answers still work. To add: ${buildRuvectorMcpAddCommand(env).say}`);
@@ -1569,9 +1750,12 @@ async function doctor() {
1569
1750
  && !codexLifecycleGuidance(codexLifecycle).healthy
1570
1751
  && !codexLifecycleGuidance(codexLifecycle).intentional,
1571
1752
  );
1753
+ const codexWiringFailed = Boolean(cx.host && !cx.wired);
1572
1754
  const failed = (hookResult ? hookResult.exitCode !== 0 : !allGreen)
1573
1755
  || groundingUnprovenPersisted
1574
- || codexLifecycleFailed;
1756
+ || codexLifecycleFailed
1757
+ || codexWiringFailed
1758
+ || Boolean(rufloOperational && !rufloOperational.healthy);
1575
1759
  if (failed && !hookResult && !groundingUnprovenPersisted) {
1576
1760
  console.log(` ${c.red('✗ FAILING')} — the warnings above are real. Re-run ${c.bold('npx ruvnet-brain')} to repair.`);
1577
1761
  }
@@ -1834,6 +2018,49 @@ function missingUpdaterHelp(kbDir) {
1834
2018
  console.error(` — the current bundle ships forge-update.mjs; then this command will work.`);
1835
2019
  }
1836
2020
 
2021
+ function syncHostsAfterUpdate() {
2022
+ const results = {};
2023
+ results.claude = wirePlugin({ expectedVersion: PACKAGE_VERSION, requireManaged: true });
2024
+ if (results.claude.host && !results.claude.wired) return { ok: false, results };
2025
+ try {
2026
+ results.codexHost = wireCodexHost();
2027
+ if (results.codexHost.host) {
2028
+ results.codex = wireCodexPlugin({ expectedVersion: PACKAGE_VERSION });
2029
+ if (!['unchanged', 'installed', 'updated', 'disabled'].includes(results.codex.action)) {
2030
+ return { ok: false, results };
2031
+ }
2032
+ }
2033
+ } catch (error) {
2034
+ results.codex = { action: 'failed', error: error?.message || String(error) };
2035
+ return { ok: false, results };
2036
+ }
2037
+
2038
+ const apply = path.join(__dirname, '..', 'plugin', 'scripts', 'update-apply.mjs');
2039
+ if (!fs.existsSync(apply)) return { ok: false, results, error: 'Stable Spine updater missing from package' };
2040
+ const applied = spawnSync(process.execPath, [apply, '--auto', '--expected-version', PACKAGE_VERSION], { stdio: 'inherit', env: process.env });
2041
+ const okApplied = !applied.error && applied.status === 0;
2042
+ if (okApplied) {
2043
+ const home = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
2044
+ const receiptPath = path.join(home, 'host-convergence.json');
2045
+ try {
2046
+ fs.mkdirSync(home, { recursive: true });
2047
+ const tmp = `${receiptPath}.tmp-${process.pid}`;
2048
+ fs.writeFileSync(tmp, JSON.stringify({
2049
+ desiredVersion: PACKAGE_VERSION,
2050
+ verifiedAt: new Date().toISOString(),
2051
+ hosts: {
2052
+ claude: { state: results.claude.host ? 'ready' : 'absent', version: results.claude.version || null },
2053
+ codex: { state: results.codex?.action === 'disabled' ? 'disabled' : (results.codexHost?.host ? 'ready' : 'absent'), version: results.codex?.version || null },
2054
+ },
2055
+ }, null, 2));
2056
+ fs.renameSync(tmp, receiptPath);
2057
+ } catch (error) {
2058
+ return { ok: false, results, applyStatus: applied.status, error: `host convergence receipt failed: ${error.message}` };
2059
+ }
2060
+ }
2061
+ return { ok: okApplied, results, applyStatus: applied.status };
2062
+ }
2063
+
1837
2064
  function runUpdate() {
1838
2065
  printBanner('update');
1839
2066
  const kbDir = resolvedKbDir();
@@ -1848,10 +2075,14 @@ function runUpdate() {
1848
2075
  // private KB stores a surprise fresh PUBLIC install is exactly the store-stripping hazard the
1849
2076
  // project docs warn about. The fallback's own comment scopes it to an updater that EXISTS but
1850
2077
  // is broken — this branch enforces that scope.)
1851
- if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
2078
+ if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs')) && !FLAG_HOST_SYNC_ONLY) {
1852
2079
  missingUpdaterHelp(kbDir);
1853
2080
  process.exit(1);
1854
2081
  }
2082
+ if (FLAG_HOST_SYNC_ONLY && !fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
2083
+ warn('KB updater is absent — continuing with host-shell repair only; the knowledge bundle remains unchanged.');
2084
+ updateStatus = 0;
2085
+ } else {
1855
2086
  info(c.dim("running the bundle's own self-updater (backs up first, re-verifies, never half-applies)…\n"));
1856
2087
  // Relative filename + matching cwd — same launch convention as smokeQuery(); stdio:'inherit'
1857
2088
  // streams the updater's narration live and unedited.
@@ -1869,6 +2100,15 @@ function runUpdate() {
1869
2100
  env: { ...process.env, RUVNET_BRAIN_NO_UPDATE_FALLBACK: '1' } });
1870
2101
  updateStatus = fr.error ? 1 : (fr.status === null ? 1 : fr.status);
1871
2102
  }
2103
+ }
2104
+ if (updateStatus === 0) {
2105
+ info(c.dim('\nsynchronizing every detected host to this exact published version…\n'));
2106
+ const convergence = syncHostsAfterUpdate();
2107
+ if (!convergence.ok) {
2108
+ warn(`host synchronization is incomplete — runtime stays on the prior verified generation${convergence.error ? ` (${convergence.error})` : ''}`);
2109
+ updateStatus = 1;
2110
+ }
2111
+ }
1872
2112
  // `--update --auto` = update now AND enroll in Evergreen, so this is the LAST time it's ever run by
1873
2113
  // hand. Only enroll if the update itself succeeded — never promise "you're set forever" on a failed
1874
2114
  // update. enableNightly() prints its own real verification (plist path + launchctl result).
@@ -2185,6 +2425,27 @@ export function machineFootprint() {
2185
2425
  items.push({ label: 'The search_ruvnet MCP server registration', path: claudeJson, undo: 'claude mcp remove ruvnet-brain --scope user' });
2186
2426
  }
2187
2427
  } catch { /* unreadable — do not claim it */ }
2428
+ try {
2429
+ const codex = codexStatus();
2430
+ if (codex.host && codex.serverPath) {
2431
+ items.push({
2432
+ label: 'The Codex search_ruvnet MCP registration',
2433
+ path: codexConfigPath(),
2434
+ undo: 'npx ruvnet-brain --uninstall (removes only the managed block)',
2435
+ });
2436
+ }
2437
+ add('Codex lifecycle hook wrapper', codexHookWrapperPath(),
2438
+ 'npx ruvnet-brain --uninstall');
2439
+ add('Codex MCP server files', codexServerDir(), 'npx ruvnet-brain --uninstall');
2440
+ const codexPlugin = codexPluginStatus();
2441
+ if (codexPlugin.installed) {
2442
+ items.push({
2443
+ label: 'Codex plugin',
2444
+ path: codexHomeDir(),
2445
+ undo: 'codex plugin remove ruvnet-brain@ruvnet-brain && codex plugin marketplace remove ruvnet-brain',
2446
+ });
2447
+ }
2448
+ } catch { /* Codex absent or unavailable — do not claim it */ }
2188
2449
  // GAP FIX: recordNotified() (scripts/upgrade-notice.mjs, invoked from main() and runDemo() below)
2189
2450
  // writes this file the first time a "what's new" notice is shown — a real disk artifact this
2190
2451
  // installer's own code path creates, that was never listed here and never removed on --uninstall.
@@ -2215,6 +2476,18 @@ function printFootprint({ heading = 'What this put on your machine' } = {}) {
2215
2476
  return items;
2216
2477
  }
2217
2478
 
2479
+ function showWhatsNew() {
2480
+ const notes = path.join(REPO_ROOT, 'docs', 'RELEASE-NOTES-4.0.md');
2481
+ if (!fs.existsSync(notes)) {
2482
+ console.error('RuvNet Brain release notes are missing from this artifact.');
2483
+ process.exitCode = 1;
2484
+ return;
2485
+ }
2486
+ const text = fs.readFileSync(notes, 'utf8');
2487
+ process.stdout.write(text);
2488
+ if (!text.endsWith('\n')) process.stdout.write('\n');
2489
+ }
2490
+
2218
2491
  /**
2219
2492
  * Surgically remove ONLY our block from CLAUDE.md, leaving every other line exactly as it was.
2220
2493
  * Backed up and written atomically, same as when it was added — taking something away is at least
@@ -2276,7 +2549,9 @@ function uninstallAll() {
2276
2549
  'Model-router files', 'Status-bar version script', 'Status-bar preference', 'Usage-counts preference',
2277
2550
  // Two gaps closed here: the statusLine KEY is now removable in place (we know exactly what we
2278
2551
  // wrote — see removeSettingsStatusLine()), and the upgrade-notice tracker is a file we fully own.
2279
- 'The statusLine entry in settings.json', 'Upgrade-notice state (release-notice tracking)']);
2552
+ 'The statusLine entry in settings.json', 'Upgrade-notice state (release-notice tracking)',
2553
+ 'The Codex search_ruvnet MCP registration', 'Codex lifecycle hook wrapper',
2554
+ 'Codex MCP server files']);
2280
2555
  const willRemove = before.filter((it) => AUTO.has(it.label));
2281
2556
  const manual = before.filter((it) => !AUTO.has(it.label));
2282
2557
 
@@ -2302,6 +2577,8 @@ function uninstallAll() {
2302
2577
  // customized — removeSettingsStatusLine() checks the live command before touching anything).
2303
2578
  const statusLine = removeSettingsStatusLine();
2304
2579
  if (statusLine === 'removed') ok('removed the statusLine entry from ~/.claude/settings.json (your other settings untouched, backup saved)');
2580
+ const codexWiring = removeCodexWiring();
2581
+ if (codexWiring === 'removed') ok('removed the managed RuvNet Brain block from Codex config (your other config untouched, backup saved)');
2305
2582
 
2306
2583
  // NEVER rm -rf A PATH WE HAVE NOT PROVEN IS OURS. resolvedKbDir() honours $RUVNET_BRAIN_KB, which
2307
2584
  // the docs encourage for custom install locations — so `RUVNET_BRAIN_KB=$HOME npx ruvnet-brain
@@ -2340,6 +2617,8 @@ function uninstallAll() {
2340
2617
  ['upgrade-notice state', upgradeNoticeStatePath()],
2341
2618
  // ADR-054 §5: a reinstall must never inherit an invisible OFF. See brainOffSentinelPath().
2342
2619
  ['brain on/off switch', brainOffSentinelPath()],
2620
+ ['Codex lifecycle hook wrapper', codexHookWrapperPath()],
2621
+ ['Codex MCP server files', codexServerDir()],
2343
2622
  ]) {
2344
2623
  if (!fs.existsSync(target)) continue;
2345
2624
  try { fs.rmSync(target, { recursive: true, force: true }); ok(`removed the ${label}`); }
@@ -2714,9 +2993,9 @@ async function printPlanAndConfirm() {
2714
2993
  { name: 'Download the knowledge base',
2715
2994
  cost: short(resolveCacheDir().cacheDir),
2716
2995
  what: 'Real rUv source, on your disk, so answers cite files instead of guessing.' },
2717
- { name: 'Register the Claude Code plugin + MCP server',
2718
- cost: `an entry in ${short(path.join(H, '.claude'))}`,
2719
- what: 'Gives Claude a search_ruvnet tool. Adds no hooks you have not agreed to.' },
2996
+ { name: 'Register host plugins + MCP server',
2997
+ cost: `managed entries in ${short(path.join(H, '.claude'))} and, when detected, ${short(path.join(H, '.codex'))}`,
2998
+ what: 'Gives Claude Code and Codex the same search_ruvnet tool and native Brain workflows.' },
2720
2999
  ];
2721
3000
  // The OPTIONAL steps — each asked ONCE here, stored in PLAN_CHOICES, and consumed by its offer
2722
3001
  // later (no second prompt). `def` is the pre-check: nightly + spend are recommended (default yes),
@@ -2850,6 +3129,42 @@ function detectEnvironment() {
2850
3129
  return env;
2851
3130
  }
2852
3131
 
3132
+ export function classifyRufloOperationalHealth({ status = '', memory = '', metrics = '' } = {}) {
3133
+ const stopped = /\b(?:RuFlo V3 \[STOPPED\]|Swarm not running|MCP Server\s*\\n.*Not running)/i.test(status);
3134
+ const statusSaysNoMemory = /Backend\s*[|:]?\s*none|Entries\s*[|:]?\s*0\b/i.test(status);
3135
+ const memoryEntries = Number((memory.match(/Total Entries\s*[|:]?\s*([\d,]+)/i)?.[1] || '0').replace(/,/g, ''));
3136
+ const zeroLearning = /Total Patterns\s*[|:]?\s*0\b/i.test(metrics)
3137
+ && /Total Routes\s*[|:]?\s*0\b/i.test(metrics)
3138
+ && /Total Executed\s*[|:]?\s*0\b/i.test(metrics);
3139
+ const memoryContradiction = statusSaysNoMemory && memoryEntries > 0;
3140
+ // Brain 4 is explicitly zero-daemon: `ruflo memory store/search` open the per-project
3141
+ // AgentDB directly. A stopped orchestration daemon therefore makes daemon-owned status
3142
+ // and metrics non-authoritative; treating that intentional state as degraded made Doctor
3143
+ // exit 1 immediately after proving source-grounded retrieval worked. Keep the strict
3144
+ // contradiction/learning checks when a daemon is actually running.
3145
+ const directMode = stopped;
3146
+ return {
3147
+ healthy: directMode || (!memoryContradiction && !zeroLearning),
3148
+ directMode,
3149
+ stopped,
3150
+ zeroLearning,
3151
+ memoryContradiction,
3152
+ memoryEntries,
3153
+ };
3154
+ }
3155
+
3156
+ function probeRufloOperationalHealth() {
3157
+ const run = (args) => {
3158
+ const result = spawnSync('ruflo', args, { cwd: process.cwd(), encoding: 'utf8', timeout: 10_000 });
3159
+ return `${result.stdout || ''}\n${result.stderr || ''}`;
3160
+ };
3161
+ return classifyRufloOperationalHealth({
3162
+ status: run(['status']),
3163
+ memory: run(['status', 'memory']),
3164
+ metrics: run(['hooks', 'metrics', '--v3-dashboard']),
3165
+ });
3166
+ }
3167
+
2853
3168
  // ── issue #39: the ruvector MCP server's own native VectorDb defaults ITS storage to
2854
3169
  // "./ruvector.db" relative to whatever cwd it happens to be launched in — and Claude Code
2855
3170
  // always launches an MCP server with cwd = the current project, so every consumer project got
@@ -3295,7 +3610,7 @@ function installedRepoCount(cacheDir) {
3295
3610
  }
3296
3611
  }
3297
3612
 
3298
- function success({ cacheDir, isCustom, plugin, env, nightly }) {
3613
+ function success({ cacheDir, isCustom, plugin, codexHost, codexPlugin, env, nightly }) {
3299
3614
  const line = '─'.repeat(64);
3300
3615
  console.log(`\n${c.green(line)}`);
3301
3616
  console.log(`${c.green(c.bold(' RuvNet Brain is installed.'))}`);
@@ -3306,6 +3621,13 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
3306
3621
  console.log(
3307
3622
  ` • the Claude Code plugin ${plugin.wired ? c.green('wired at user scope') : c.yellow('(finish the 2 commands above)')} — search_ruvnet + grounding hook`,
3308
3623
  );
3624
+ if (codexHost?.host) {
3625
+ const hostReady = ['added', 'rewritten', 'user-owned'].includes(codexHost.action);
3626
+ const pluginReady = Boolean(codexPlugin?.installed && codexPlugin?.enabled);
3627
+ console.log(
3628
+ ` • the Codex host ${hostReady && pluginReady ? c.green('wired at user scope') : c.yellow('needs attention')} — search_ruvnet + native Brain skills`,
3629
+ );
3630
+ }
3309
3631
  if (env) {
3310
3632
  const t = (okv) => (okv ? c.green('✓ present') : c.yellow('not added (optional)'));
3311
3633
  console.log(` • rUv build toolkit — Ruflo: ${t(env.ruflo)} · RuVector: ${t(env.ruvector)} ${c.dim('answers work without them')}`);
@@ -3316,7 +3638,8 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
3316
3638
  console.log(` ${c.bold(`export RUVNET_BRAIN_KB="${cacheDir}"`)}`);
3317
3639
  }
3318
3640
  // ── where + how it runs — the confidence answers, stated up front ──
3319
- console.log(`\n ${c.bold('Where it runs:')} ${c.bold('everywhere you use Claude Code')} — CLI, VS Code, JetBrains, the desktop app.`);
3641
+ const hostLabel = codexHost?.host ? 'Claude Code and Codex' : 'Claude Code';
3642
+ console.log(`\n ${c.bold('Where it runs:')} ${c.bold(`everywhere you use ${hostLabel}`)} — CLI and supported editor/desktop surfaces.`);
3320
3643
  console.log(` It's ${c.bold('user-level (global)')}: open ANY repo or folder and it's already there. Nothing per project,`);
3321
3644
  console.log(` nothing to copy in, nothing to git-ignore. ${c.dim('(Runs locally — it is not active in the claude.ai web app.)')}`);
3322
3645
  console.log(`\n ${c.bold('How it runs:')} ${c.bold('automatically')} — you never call or configure anything. Ask normally; on rUv-stack work it`);
@@ -3325,12 +3648,12 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
3325
3648
  console.log(` Release regardless (that part isn't cached). For the bleeding-edge installer too, use ${c.bold('npx github:stuinfla/ruvnet-brain')}.`);
3326
3649
 
3327
3650
  // ── one important expectation: the hook activates on the NEXT session ──
3328
- console.log(`\n ${c.yellow(c.bold('One thing to know:'))} the grounding hook turns on at your ${c.bold('next')} Claude Code session.`);
3329
- console.log(` ${c.dim('If Claude Code is open right now, quit and reopen it — then the brain is live on every prompt.')}`);
3651
+ console.log(`\n ${c.yellow(c.bold('One thing to know:'))} lifecycle hooks turn on in your ${c.bold('next')} host session.`);
3652
+ console.log(` ${c.dim(`If ${hostLabel} is open right now, quit and reopen it so the new plugin snapshot loads.`)}`);
3330
3653
 
3331
3654
  // ── what to do now ──
3332
3655
  console.log(`\n ${c.bold('What to do now:')}`);
3333
- console.log(` ${c.cyan('1.')} Open Claude Code in ${c.bold('any')} project (your own, or a fresh repo — nothing to copy in).`);
3656
+ console.log(` ${c.cyan('1.')} Open ${hostLabel} in ${c.bold('any')} project (your own, or a fresh repo — nothing to copy in).`);
3334
3657
  console.log(` ${c.cyan('2.')} Just ask, normally. Try: ${c.bold('"Set up vector search here the way rUv would."')}`);
3335
3658
  console.log(` ${c.cyan('3.')} Watch what changes (below).`);
3336
3659
 
@@ -3404,6 +3727,7 @@ Usage:
3404
3727
  npx ruvnet-brain --disable-nightly Remove the nightly schedule (safe to run any time)
3405
3728
  npx ruvnet-brain --what-changed Show exactly what RuvNet Brain has put on this machine,
3406
3729
  with the undo command for each piece
3730
+ npx ruvnet-brain --whats-new Show the curated v3 → v4 capabilities and honest limits
3407
3731
  npx ruvnet-brain --uninstall Remove all of it (bundle, LaunchAgents, and our CLAUDE.md
3408
3732
  block only — your own CLAUDE.md content is preserved and backed up)
3409
3733
  npx ruvnet-brain --enable-spend-guard Install the hourly runaway-agent spend alarm (alert-only)
@@ -3443,7 +3767,17 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3443
3767
 
3444
3768
  // ── main ─────────────────────────────────────────────────────────────────────────────────────────
3445
3769
  (async () => {
3446
- if (IMPORT_ONLY) return; // imported for its exports (tests) — never run the installer as a side effect
3770
+ // Importing an executable module for its exported policy helpers must never install software.
3771
+ // The old contract required every caller to remember RUVNET_BRAIN_IMPORT_ONLY=1; one missed flag
3772
+ // in a unit test ran a real install against the maintainer's HOME. Direct argv identity is the
3773
+ // authority boundary. The old IMPORT_ONLY flag may still be present in a long-lived test worker;
3774
+ // it must never suppress a genuinely direct CLI execution.
3775
+ const canonical = (value) => {
3776
+ try { return fs.realpathSync(value); } catch { return path.resolve(value); }
3777
+ };
3778
+ const invokedDirectly = Boolean(process.argv[1])
3779
+ && canonical(process.argv[1]) === canonical(fileURLToPath(import.meta.url));
3780
+ if (!invokedDirectly) return;
3447
3781
  if (FLAG_HELP) return showHelp();
3448
3782
  // `process.exitCode`, not `return` — doctor()'s verdict is the whole point of running it in a
3449
3783
  // script. A bare `return await doctor()` discarded the number, which is how "! Needs attention"
@@ -3462,9 +3796,10 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3462
3796
  if (FLAG_DISABLE_SPEND_GUARD) { disableSpendGuard(); return; }
3463
3797
  if (FLAG_UNINSTALL) { uninstallAll(); return; }
3464
3798
  if (FLAG_WHAT_CHANGED) { printBanner('what RuvNet Brain put on this machine'); printFootprint(); return; }
3799
+ if (FLAG_WHATS_NEW) { showWhatsNew(); return; }
3465
3800
 
3466
3801
  printBanner('installer');
3467
- console.log(c.dim("I'll set up the brain and the Claude Code plugin, explaining each step as I go.\n"));
3802
+ console.log(c.dim("I'll set up the brain for Claude Code and Codex, explaining each step as I go.\n"));
3468
3803
 
3469
3804
  // ── environment guard: fail early and CLEARLY on unsupported Node, not cryptically mid-install ──
3470
3805
  // Node itself can't be silently auto-upgraded from inside a script it's currently running as —
@@ -3607,13 +3942,24 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3607
3942
  }
3608
3943
 
3609
3944
  installReader(cacheDir);
3945
+ try {
3946
+ const consoleEntry = installConsoleRuntime(cacheDir);
3947
+ ok(`Brain Console installed at ${consoleEntry}`);
3948
+ } catch (error) {
3949
+ die(
3950
+ `the Brain Console runtime could not be installed (${error.message})`,
3951
+ `The knowledge base is present, but /rvbc would be broken. Re-run the installer from a complete package.`,
3952
+ );
3953
+ }
3610
3954
  const plugin = wirePlugin();
3611
3955
  // Codex hosts got nothing before this (issue #42): shipped, never registered. Non-fatal like every
3612
3956
  // other wiring step — a second host we cannot reach must never break the one we can.
3957
+ let codexHost = { host: false, action: 'no-host' };
3958
+ let codexPlugin = null;
3613
3959
  try {
3614
- const codexHost = wireCodexHost();
3960
+ codexHost = wireCodexHost();
3615
3961
  if (codexHost.host) {
3616
- const codexPlugin = wireCodexPlugin();
3962
+ codexPlugin = wireCodexPlugin();
3617
3963
  if (codexPlugin.available) printCodexLifecycle(await codexLifecycleStatus());
3618
3964
  }
3619
3965
  } catch (e) {
@@ -3657,7 +4003,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
3657
4003
  // Same rule: an optional offer can never break a finished install.
3658
4004
  try { await offerStatusline(); } catch { /* non-fatal — a status-bar nicety must never break the install */ }
3659
4005
 
3660
- success({ cacheDir, isCustom, plugin, env, nightly });
4006
+ success({ cacheDir, isCustom, plugin, codexHost, codexPlugin, env, nightly });
3661
4007
 
3662
4008
  // Close every install by stating, in one place, exactly what is now on their machine and how to
3663
4009
  // take each piece back off. Someone reading this should never have to file a bug report to find