ruvnet-brain 3.9.68-dev โ†’ 3.9.72-dev

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # ๐Ÿง  RuvNet Brain
6
6
 
7
- ### ๐Ÿง  RuvNet Brain โ€” [![RuvNet Brain version 3.9.68-dev โ€” updated 2026-07-24 16:29 EDT](https://img.shields.io/badge/version_3.9.68--dev-updated_2026--07--24_16:29_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 3.9.72-dev โ€” updated 2026-07-24 16:09 EDT](https://img.shields.io/badge/version_3.9.72--dev-updated_2026--07--24_16:09_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
 
package/bin/install.mjs CHANGED
@@ -80,6 +80,7 @@ const FLAG_UNINSTALL = argv.includes('--uninstall'); // reverse everything, in o
80
80
  const FLAG_WHAT_CHANGED = argv.includes('--what-changed'); // show our footprint on this machine
81
81
  // โ”€โ”€ onboarding-experience flags (all optional; every offer is safe to decline) โ”€โ”€
82
82
  const FLAG_YES = argv.includes('--yes') || argv.includes('-y'); // accept every optional offer non-interactively
83
+ const FLAG_PLAN = argv.includes('--plan') || argv.includes('--dry-run'); // show the interactive checklist, then exit โ€” install NOTHING
83
84
  const FLAG_WITH_STACK = argv.includes('--with-stack'); // add missing Ruflo/RuVector without prompting
84
85
  const FLAG_NO_STACK = argv.includes('--no-stack'); // skip the toolkit offer entirely
85
86
  const FLAG_ENHANCE_CLAUDE_MD = argv.includes('--enhance-claude-md'); // add the CLAUDE.md section without prompting
@@ -543,6 +544,121 @@ function wirePlugin() {
543
544
  return { wired: false, manualMarketplace, manualInstall };
544
545
  }
545
546
 
547
+ // โ”€โ”€ step: wire the Codex host (issue #42, Henrik Pettersen) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
548
+ // We shipped a .codex/ directory AND a working MCP server and never wired them together, so on a
549
+ // Codex host the brain was entirely absent โ€” no search_ruvnet at all. Every artifact was present;
550
+ // only the registration was missing. Codex reads its MCP servers from ~/.codex/config.toml, which is
551
+ // the USER's file and already carries their own settings, so this MERGES and never rewrites wholesale.
552
+ //
553
+ // Two rules make that merge safe:
554
+ // 1. Our lines live inside a comment-delimited managed block, so a reinstall rewrites exactly
555
+ // those bytes and leaves every other section byte-identical.
556
+ // 2. An [mcp_servers.ruvnet-brain] found OUTSIDE our markers is the user's, not ours โ€” we report
557
+ // it and touch nothing. Their hand-written config outranks our convenience.
558
+ //
559
+ // The registered path must also OUTLIVE the install. The npx checkout is ephemeral (the same reason
560
+ // the spend watchdog and the router tools get copied under ~/.claude), and plugin/mcp/server.mjs is
561
+ // self-contained โ€” node builtins only โ€” so it is copied to a persistent home and THAT absolute path
562
+ // is what gets registered. Registering the npx dir would rot the moment the temp dir vanished.
563
+ const CODEX_BLOCK_START = '# --- ruvnet-brain (managed block, installer-rewritten) ---';
564
+ const CODEX_BLOCK_END = '# --- end ruvnet-brain ---';
565
+ const codexHomeDir = () => path.join(os.homedir(), '.codex');
566
+ const codexConfigPath = () => path.join(codexHomeDir(), 'config.toml');
567
+ const codexServerDir = () => path.join(os.homedir(), '.claude', 'ruvnet-brain', 'mcp');
568
+
569
+ // The exact bytes we own. Kept in one place so the writer and the doctor probe can never disagree.
570
+ function codexManagedBlock(serverPath) {
571
+ return [
572
+ CODEX_BLOCK_START,
573
+ '# Written by `npx ruvnet-brain`. This block is rewritten on every reinstall;',
574
+ '# everything outside the two markers is yours and is never touched.',
575
+ '[mcp_servers.ruvnet-brain]',
576
+ 'command = "node"',
577
+ `args = [${JSON.stringify(serverPath)}]`,
578
+ CODEX_BLOCK_END,
579
+ ].join('\n');
580
+ }
581
+
582
+ // Pure, so the merge contract is testable without going near a real ~/.codex. Returns the new text
583
+ // plus which of the three things happened, and is idempotent: re-running over its own output
584
+ // reproduces it byte for byte.
585
+ export function mergeCodexConfig(existing, serverPath) {
586
+ const text = typeof existing === 'string' ? existing : '';
587
+ const block = codexManagedBlock(serverPath);
588
+ const start = text.indexOf(CODEX_BLOCK_START);
589
+ const end = text.indexOf(CODEX_BLOCK_END);
590
+ if (start !== -1 && end > start) {
591
+ return { text: text.slice(0, start) + block + text.slice(end + CODEX_BLOCK_END.length), action: 'rewritten' };
592
+ }
593
+ // Their own declaration, outside our markers. Never clobber a user's hand-written server entry.
594
+ if (/^[ \t]*\[mcp_servers\.(?:ruvnet-brain|"ruvnet-brain"|'ruvnet-brain')\]/m.test(text)) {
595
+ return { text, action: 'user-owned' };
596
+ }
597
+ const sep = text.length === 0 ? '' : text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
598
+ return { text: `${text}${sep}${block}\n`, action: 'added' };
599
+ }
600
+
601
+ // What the doctor asserts from disk โ€” never from the fact that we once ran. "Wired" means our entry
602
+ // is in the config AND the server.mjs it points at is really there, because a registration pointing
603
+ // at a deleted file is worse than no registration: Codex fails at spawn time with nothing to read.
604
+ export function codexStatus({ configPath = codexConfigPath(), codexDir = codexHomeDir() } = {}) {
605
+ let host = false;
606
+ try { host = fs.existsSync(codexDir); } catch { /* unreadable โ€” treat as absent */ }
607
+ if (!host) return { host: false, wired: false, serverPath: null, serverExists: false };
608
+ let text = '';
609
+ try { text = fs.readFileSync(configPath, 'utf8'); } catch { /* no config yet */ }
610
+ const m = /^[ \t]*\[mcp_servers\.(?:ruvnet-brain|"ruvnet-brain"|'ruvnet-brain')\][^[]*?args\s*=\s*\[\s*"([^"]+)"/m.exec(text);
611
+ const serverPath = m ? m[1] : null;
612
+ let serverExists = false;
613
+ try { serverExists = serverPath ? fs.existsSync(serverPath) : false; } catch { /* unreadable */ }
614
+ return { host: true, wired: Boolean(serverPath) && serverExists, serverPath, serverExists };
615
+ }
616
+
617
+ export function wireCodexHost({
618
+ codexDir = codexHomeDir(),
619
+ configPath = path.join(codexDir, 'config.toml'),
620
+ serverDir = codexServerDir(),
621
+ source = path.join(__dirname, '..', 'plugin', 'mcp', 'server.mjs'),
622
+ announce = true,
623
+ } = {}) {
624
+ let host = false;
625
+ try { host = fs.existsSync(codexDir); } catch { /* unreadable โ€” treat as absent */ }
626
+ // No Codex on this machine is not a warning. Say nothing and change nothing.
627
+ if (!host) return { host: false, action: 'no-host' };
628
+
629
+ if (announce) {
630
+ step('Wiring the Codex host', 'Codex reads MCP servers from ~/.codex/config.toml โ€” the brain was never registered there');
631
+ }
632
+ if (!fs.existsSync(source)) {
633
+ if (announce) warn('MCP server missing from this bundle โ€” Codex left untouched (non-fatal)');
634
+ return { host: true, action: 'no-source' };
635
+ }
636
+ const serverPath = path.join(serverDir, 'server.mjs');
637
+ fs.mkdirSync(serverDir, { recursive: true });
638
+ fs.copyFileSync(source, serverPath);
639
+
640
+ let before = '';
641
+ try { before = fs.readFileSync(configPath, 'utf8'); } catch { /* first run โ€” no config yet */ }
642
+ const { text, action } = mergeCodexConfig(before, serverPath);
643
+ if (action === 'user-owned') {
644
+ if (announce) {
645
+ ok('Codex already declares ruvnet-brain in your own config โ€” left exactly as you wrote it');
646
+ info(` to hand it to us instead, delete that ${c.bold('[mcp_servers.ruvnet-brain]')} block and re-run this installer`);
647
+ }
648
+ return { host: true, action, serverPath };
649
+ }
650
+ if (text !== before) {
651
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
652
+ fs.writeFileSync(configPath, text);
653
+ }
654
+ if (announce) {
655
+ ok(`search_ruvnet registered for Codex โ†’ ${c.bold(configPath)}`);
656
+ info(` server: ${serverPath} ${c.dim('(persistent copy โ€” the npx dir vanishes)')}`);
657
+ info(` ${c.dim('only our marked block is written; every other section is byte-preserved')}`);
658
+ }
659
+ return { host: true, action, serverPath, changed: text !== before };
660
+ }
661
+
546
662
  // โ”€โ”€ step: verify the install is REAL (counts โ€” never take "installed" on faith) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
547
663
  // Shared state-gatherer behind verifyInstall / --doctor / --feedback: what is REALLY on disk.
548
664
  // Pure read, never prints โ€” callers decide how to narrate (or, for --feedback, how to report) it.
@@ -865,13 +981,14 @@ async function doctor() {
865
981
  have('git')
866
982
  ? ok('git present (not required by this installer, but handy)')
867
983
  : info('git not found โ€” that\'s fine, this installer never needs it');
984
+ cleanupStrayRuvectorDb(); // issue #39: sweep a leftover empty scaffold from before this fix, if one's here
868
985
  const env = detectEnvironment();
869
986
  env.ruflo
870
987
  ? ok('Ruflo present โ€” orchestration / swarms / SPARC available')
871
988
  : warn('Ruflo not found โ€” answers still work. To build: npm install -g claude-flow@alpha (or /plugin add ruvnet/claude-flow)');
872
989
  env.ruvector
873
990
  ? ok('RuVector present โ€” vector CLI / MCP available')
874
- : warn('RuVector not found โ€” answers still work. To add: claude mcp add ruvector --scope user -- npx -y ruvector mcp start');
991
+ : warn(`RuVector not found โ€” answers still work. To add: ${buildRuvectorMcpAddCommand(env).say}`);
875
992
  // Network probe (issue #27, Jan Lafko): in a network-restricted sandbox the cold-cache embedder
876
993
  // pull used to hang FOREVER. Diagnose the condition here, explicitly and in 3 seconds flat: if the
877
994
  // model host is unreachable AND no local model cache exists, the first query needs the network and
@@ -911,11 +1028,34 @@ async function doctor() {
911
1028
  console.log(` ${c.yellow('! Grounding not verifiable')} on this bundle โ€” it predates the citation verifier.`);
912
1029
  console.log(' Re-run npx ruvnet-brain to refresh, then --doctor will prove it.');
913
1030
  }
1031
+ // Codex is a SECOND host, and until issue #42 the doctor was silent about it โ€” so a machine where
1032
+ // the brain was completely unavailable in Codex still read "Healthy โ€ฆ Grounding PROVEN". Probed
1033
+ // from disk (our entry present AND the server.mjs it names really there), never asserted from the
1034
+ // fact that an install once ran.
1035
+ const cx = codexStatus();
1036
+ if (!cx.host) {
1037
+ console.log(` ${c.dim('Codex: no host detected (no ~/.codex) โ€” nothing to wire.')}`);
1038
+ } else if (cx.wired) {
1039
+ console.log(` ${c.green('โœ“ Codex: wired.')} search_ruvnet is registered in ~/.codex/config.toml and its server exists.`);
1040
+ } else {
1041
+ console.log(` ${c.yellow('! Codex: host detected but NOT wired')}${
1042
+ cx.serverPath && !cx.serverExists ? ` โ€” registered server is missing (${cx.serverPath})` : ' โ€” no [mcp_servers.ruvnet-brain] entry'
1043
+ }.`);
1044
+ console.log(` Codex cannot reach the brain until it is. Fix: re-run ${c.bold('npx ruvnet-brain')}`);
1045
+ }
914
1046
  console.log(` ${c.dim(meterSummaryLine())}`);
915
1047
  if (allGreen) {
916
1048
  console.log(`\n ${c.bold('What this means for you:')}`);
917
- console.log(` โ€ข ${c.bold('It works in EVERY project')} โ€” user-level (global). Open Claude Code in any repo or VS Code`);
918
- console.log(` window and it's there. ${c.bold('No reinstall per project. No second download.')} One brain, shared.`);
1049
+ // "EVERY project" is a claim about Claude Code. On a machine with an unwired Codex host it would
1050
+ // be read as "every editor", which is exactly the invisible gap issue #42 reported โ€” so scope the
1051
+ // sentence honestly instead of quietly overclaiming.
1052
+ if (cx.host && !cx.wired) {
1053
+ console.log(` โ€ข ${c.bold('It works in EVERY project in Claude Code')} โ€” user-level (global). Open Claude Code in any`);
1054
+ console.log(` repo or VS Code window and it's there. ${c.bold('No reinstall per project.')} ${c.yellow('Codex is NOT wired yet')} (above).`);
1055
+ } else {
1056
+ console.log(` โ€ข ${c.bold('It works in EVERY project')} โ€” user-level (global). Open Claude Code in any repo or VS Code`);
1057
+ console.log(` window and it's there. ${c.bold('No reinstall per project. No second download.')} One brain, shared.`);
1058
+ }
919
1059
  console.log(` โ€ข ${c.bold('Nothing to git-ignore')} in your projects โ€” it drops zero files into your working repos.`);
920
1060
  console.log(` โ€ข ${c.bold('To use it:')} just ask Claude about rUv's stack (RuVector, Ruflo, AgentDB, SPARCโ€ฆ) โ€” it`);
921
1061
  console.log(` grounds the answer automatically and takes the lead on builds. You don't invoke anything.`);
@@ -1682,7 +1822,10 @@ export async function offerSpendGuard() {
1682
1822
  // NOT gated on FLAG_YES โ€” second launchd job, same rule as the nightly updater above.
1683
1823
  if (!process.stdin.isTTY && !FLAG_ENABLE_SPEND_GUARD) { info(`No terminal to prompt on โ€” install it any time with ${c.bold('npx ruvnet-brain --enable-spend-guard')}`); return 'recommended'; }
1684
1824
  let yes = true;
1685
- if (!FLAG_ENABLE_SPEND_GUARD) {
1825
+ const plannedSpend = plannedChoice('spend');
1826
+ if (plannedSpend !== undefined) {
1827
+ yes = plannedSpend; // answered in the up-front checklist โ€” never re-ask
1828
+ } else if (!FLAG_ENABLE_SPEND_GUARD) {
1686
1829
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1687
1830
  const answer = await new Promise((resolve) => rl.question(` ${c.cyan('?')} Install the spend watchdog? ${c.dim('[Y/n]')} `, resolve));
1688
1831
  rl.close();
@@ -1848,7 +1991,11 @@ export async function offerNightly() {
1848
1991
  }
1849
1992
 
1850
1993
  let yes = true;
1851
- if (!FLAG_ENABLE_NIGHTLY) {
1994
+ const planned = plannedChoice('nightly');
1995
+ if (planned !== undefined) {
1996
+ // Already answered in the up-front checklist โ€” don't ask twice (the owner's complaint).
1997
+ yes = planned;
1998
+ } else if (!FLAG_ENABLE_NIGHTLY) {
1852
1999
  // Not ask(): its parser treats anything but y/yes as no. Here the DEFAULT is yes โ€” only an
1853
2000
  // explicit n/no declines (parseNightlyAnswer holds that contract, and the tests hold it there).
1854
2001
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -1925,7 +2072,10 @@ export async function offerTelemetry(cacheDir) {
1925
2072
  }
1926
2073
 
1927
2074
  let yes = true; // --yes accepts every optional offer, this one included
1928
- if (!FLAG_YES) {
2075
+ const plannedTelemetry = plannedChoice('telemetry');
2076
+ if (plannedTelemetry !== undefined) {
2077
+ yes = plannedTelemetry; // answered in the up-front checklist โ€” never re-ask
2078
+ } else if (!FLAG_YES) {
1929
2079
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1930
2080
  const answer = await new Promise((resolve) =>
1931
2081
  rl.question(` ${c.cyan('?')} Share anonymous usage counts (installs/searches โ€” never your queries or code)? ${c.dim('[Y/n]')} `, resolve),
@@ -2004,66 +2154,96 @@ async function printPlanAndConfirm() {
2004
2154
  const H = os.homedir();
2005
2155
  const short = (p) => p.replace(H, '~');
2006
2156
 
2007
- const steps = [
2008
- { auto: true, name: 'Download the knowledge base',
2009
- // short() already renders $HOME as "~"; prefixing another one produced "~~/.cache/โ€ฆ".
2157
+ // The REQUIRED core โ€” always runs, shown for transparency, never a question (nothing works without
2158
+ // these, so asking would be theatre).
2159
+ const core = [
2160
+ { name: 'Download the knowledge base',
2010
2161
  cost: short(resolveCacheDir().cacheDir),
2011
- what: 'Real rUv source, on your disk, so answers cite files instead of guessing.',
2012
- undo: 'npx ruvnet-brain --uninstall' },
2013
- { auto: true, name: 'Register the Claude Code plugin + MCP server',
2162
+ what: 'Real rUv source, on your disk, so answers cite files instead of guessing.' },
2163
+ { name: 'Register the Claude Code plugin + MCP server',
2014
2164
  cost: `an entry in ${short(path.join(H, '.claude'))}`,
2015
- what: 'Gives Claude a search_ruvnet tool. Adds no hooks you have not agreed to.',
2016
- undo: 'npx ruvnet-brain --uninstall' },
2017
- { ask: true, name: 'Add rUv tools you are missing',
2018
- cost: 'npm installs, only the ones you pick',
2019
- what: 'So the brain can build with them, not just answer questions about them.',
2020
- undo: 'npm uninstall -g <tool>' },
2021
- { ask: true, name: 'Nightly auto-updates',
2022
- cost: 'one LaunchAgent',
2023
- what: 'Keeps the KB and plugin current. Recommended โ€” rUv ships fast, and a stale brain is the main way this stops being useful.',
2024
- undo: 'npx ruvnet-brain --disable-nightly' },
2025
- { ask: true, name: 'Spend watchdog',
2026
- cost: 'one LaunchAgent',
2165
+ what: 'Gives Claude a search_ruvnet tool. Adds no hooks you have not agreed to.' },
2166
+ ];
2167
+ // The OPTIONAL steps โ€” each asked ONCE here, stored in PLAN_CHOICES, and consumed by its offer
2168
+ // later (no second prompt). `def` is the pre-check: nightly + spend are recommended (default yes),
2169
+ // the rest default off. `rec` tags the two recommended ones for the display grouping.
2170
+ const optional = [
2171
+ { key: 'nightly', rec: true, name: 'Evergreen auto-updates',
2172
+ what: 'Keeps the KB and plugin current โ€” declining makes updates manual forever. rUv ships fast, and a stale brain is the main way this stops being useful.',
2173
+ undo: 'npx ruvnet-brain --disable-nightly', def: true },
2174
+ { key: 'spend', rec: true, name: 'Spend watchdog',
2027
2175
  what: 'Warns you if an agent fleet starts burning API credit unexpectedly.',
2028
- undo: 'npx ruvnet-brain --disable-spend-guard' },
2029
- { ask: true, name: 'Status-bar version segment',
2030
- cost: 'one line in settings.json',
2176
+ undo: 'npx ruvnet-brain --disable-spend-guard', def: true },
2177
+ { key: 'stack', name: 'Add rUv tools you are missing',
2178
+ what: 'So the brain can build with them, not just answer questions about them. The brain answers grounded questions fine without them.',
2179
+ undo: 'npm uninstall -g <tool>', def: false },
2180
+ { key: 'statusline', name: 'Status-bar version segment',
2031
2181
  what: 'Shows which brain version is live while you work.',
2032
- undo: 'npx ruvnet-brain --no-statusline, or delete the statusLine entry' },
2033
- { ask: true, name: 'Anonymous usage counts',
2034
- cost: 'a counter ping',
2182
+ undo: 'npx ruvnet-brain --no-statusline', def: false },
2183
+ { key: 'telemetry', name: 'Anonymous usage counts',
2035
2184
  what: 'Installs and searches only โ€” never your queries, your code, or your paths.',
2036
- undo: 'npx ruvnet-brain --no-telemetry' },
2185
+ undo: 'npx ruvnet-brain --no-telemetry', def: true },
2037
2186
  ];
2038
2187
 
2039
2188
  console.log(` ${c.bold('Here is everything this will do.')} Nothing has happened yet.\n`);
2040
- for (const s of steps) {
2041
- const mark = s.auto ? c.green('โœ“') : c.cyan('?');
2042
- console.log(` ${mark} ${c.bold(s.name)} ${c.dim('ยท ' + s.cost)}`);
2043
- console.log(` ${s.what}`);
2044
- console.log(` ${c.dim('undo: ' + s.undo)}\n`);
2189
+ console.log(` ${c.bold('The core')} ${c.dim('โ€” required; happens automatically')}`);
2190
+ for (const s of core) {
2191
+ console.log(` ${c.green('โœ“')} ${c.bold(s.name)} ${c.dim('ยท ' + s.cost)}`);
2192
+ console.log(` ${s.what}`);
2045
2193
  }
2046
- console.log(` ${c.green('โœ“')} happens automatically. ${c.cyan('?')} is asked first โ€” and "no" is a complete answer.`);
2047
- console.log(` ${c.dim('Every step is reversible, and `npx ruvnet-brain --what-changed` lists everything it touched.')}\n`);
2194
+ console.log('');
2048
2195
 
2049
- // FLAG_YES means the caller already decided; a plan screen that blocks automation would break
2050
- // agentic-kit and every scripted install. Print it, then proceed โ€” the information is the point,
2051
- // the pause is a courtesy to humans.
2196
+ // FLAG_YES / non-TTY: the caller already decided (agentic-kit, scripted installs). Print the plan,
2197
+ // leave PLAN_CHOICES empty, and let each offer honor its own flags/defaults as before. Never block.
2052
2198
  if (FLAG_YES || FLAG_AUTO || !process.stdin.isTTY) {
2199
+ console.log(` ${c.dim('The rest (auto-update, spend watchdog, extras) is handled non-interactively by each step.')}`);
2200
+ if (FLAG_PLAN) {
2201
+ console.log(` ${c.dim('--plan: preview only. Nothing was installed.')}\n`);
2202
+ process.exit(0);
2203
+ }
2053
2204
  console.log(c.dim(' (non-interactive โ€” continuing)\n'));
2054
2205
  return;
2055
2206
  }
2207
+
2208
+ // INTERACTIVE CHECKLIST โ€” decide each optional ONCE, here, instead of being re-asked one by one.
2209
+ console.log(` ${c.bold('Choose the optional pieces')} ${c.dim('โ€” press Enter to accept the [default]; every one is reversible')}\n`);
2056
2210
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2057
- const a = await new Promise((r) => rl.question(` ${c.cyan('?')} Continue? ${c.dim('[Y/n]')} `, r));
2211
+ const askOne = (q, def) => new Promise((resolve) => {
2212
+ const suffix = def ? c.dim('[Y/n]') : c.dim('[y/N]');
2213
+ rl.question(` ${def ? c.yellow('โ—') : c.dim('โ—‹')} ${c.bold(q)} ${suffix} `, (a) => {
2214
+ const s = String(a).trim().toLowerCase();
2215
+ resolve(s === '' ? def : (s === 'y' || s === 'yes'));
2216
+ });
2217
+ });
2218
+ for (const s of optional) {
2219
+ console.log(` ${s.what}`);
2220
+ console.log(` ${c.dim('undo: ' + s.undo)}`);
2221
+ // eslint-disable-next-line no-await-in-loop
2222
+ PLAN_CHOICES[s.key] = await askOne(s.name + (s.rec ? c.dim(' (recommended)') : ''), s.def);
2223
+ console.log('');
2224
+ }
2058
2225
  rl.close();
2059
- if (/^n/i.test(String(a).trim())) {
2060
- console.log(`\n Stopped. Nothing was changed.\n`);
2226
+
2227
+ const on = optional.filter((s) => PLAN_CHOICES[s.key]).map((s) => s.name);
2228
+ console.log(` ${c.green('โœ“')} Plan set โ€” the core, plus: ${on.length ? c.bold(on.join(', ')) : c.dim('none of the optional pieces')}.`);
2229
+ if (FLAG_PLAN) {
2230
+ console.log(` ${c.dim('--plan: preview only. Nothing was installed. Re-run without --plan to apply this.')}\n`);
2061
2231
  process.exit(0);
2062
2232
  }
2063
- console.log('');
2233
+ console.log(` ${c.dim('`npx ruvnet-brain --what-changed` lists everything it touched. Installingโ€ฆ')}\n`);
2064
2234
  }
2065
2235
 
2066
- function ask(question, def = false, { blanketYes = true } = {}) {
2236
+ // THE CHECKLIST'S ANSWERS, collected ONCE up front in printPlanAndConfirm() and consumed by each
2237
+ // offer instead of a second serial prompt. The owner's complaint (2026-07-24): the plan screen
2238
+ // showed everything, then every offer asked again, one at a time. Now you decide once. Empty in the
2239
+ // non-interactive / --yes / CI paths (printPlanAndConfirm returns before populating it), so every
2240
+ // offer falls through to its existing flag/default behavior unchanged.
2241
+ export const PLAN_CHOICES = Object.create(null);
2242
+ /** The checklist's answer for `key`, or undefined if it wasn't collected (so the offer prompts as before). */
2243
+ export function plannedChoice(key) { return key in PLAN_CHOICES ? PLAN_CHOICES[key] : undefined; }
2244
+
2245
+ export function ask(question, def = false, { blanketYes = true, planKey = null } = {}) {
2246
+ if (planKey != null) { const p = plannedChoice(planKey); if (p !== undefined) return Promise.resolve(p); }
2067
2247
  if (FLAG_YES && blanketYes) return Promise.resolve(true);
2068
2248
  if (!process.stdin.isTTY) return Promise.resolve(def);
2069
2249
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -2116,12 +2296,79 @@ function detectEnvironment() {
2116
2296
  return env;
2117
2297
  }
2118
2298
 
2299
+ // โ”€โ”€ issue #39: the ruvector MCP server's own native VectorDb defaults ITS storage to
2300
+ // "./ruvector.db" relative to whatever cwd it happens to be launched in โ€” and Claude Code
2301
+ // always launches an MCP server with cwd = the current project, so every consumer project got
2302
+ // an empty ~1.5MB scaffold dropped in its own root. Traced live (2026-07-24) to
2303
+ // ruvector/dist/core/intelligence-engine.js `initVectorDb()`, which constructs the native
2304
+ // VectorDb with `{ dimensions, distanceMetric: 'Cosine' }` and NO storagePath at all โ€” that
2305
+ // constructor runs unconditionally from a module-level `new Intelligence()` the instant
2306
+ // `ruvector mcp start` boots. `claude mcp add` has no --cwd flag, and `ruvector mcp start
2307
+ // --help` documents only `-h` (verified live) โ€” no flag or env var lets us set the storage
2308
+ // path directly. So the fix has to live in the command we REGISTER: a tiny `node -e` launcher
2309
+ // that uses child_process's own `cwd` option (no shell, so it behaves the same on Windows) to
2310
+ // relocate into a fixed cache dir before delegating to the real `npx -y ruvector mcp start`.
2311
+ // Verified live: this reproduces the exact same empty scaffold, just under
2312
+ // ~/.cache/ruvnet-brain/ruvector-mcp instead of whatever project happens to be open.
2313
+ const RUVECTOR_MCP_CWD = path.join(os.homedir(), '.cache', 'ruvnet-brain', 'ruvector-mcp');
2314
+ function buildRuvectorMcpLauncher(cwd) {
2315
+ return (
2316
+ 'const fs=require("fs"),os=require("os"),path=require("path"),cp=require("child_process");' +
2317
+ `const d=${JSON.stringify(cwd)};` +
2318
+ 'fs.mkdirSync(d,{recursive:true});' +
2319
+ 'const r=cp.spawnSync("npx",["-y","ruvector","mcp","start"],{stdio:"inherit",cwd:d});' +
2320
+ 'process.exit(r.status==null?1:r.status);'
2321
+ );
2322
+ }
2323
+ export function buildRuvectorMcpAddCommand(env, cwd = RUVECTOR_MCP_CWD) {
2324
+ const launcher = buildRuvectorMcpLauncher(cwd);
2325
+ return {
2326
+ shell: env.claude ? ['claude', ['mcp', 'add', 'ruvector', '--scope', 'user', '--', 'node', '-e', launcher]] : null,
2327
+ say: `claude mcp add ruvector --scope user -- node -e '${launcher}'`,
2328
+ };
2329
+ }
2330
+
2331
+ // โ”€โ”€ issue #39 mitigation: clean up the empty ruvector.db this bug already left behind in
2332
+ // projects that installed before this fix landed. Matches ONLY the exact empty-scaffold
2333
+ // signature (dimensions 384, Cosine, storage_path "./ruvector.db" โ€” literally the string the
2334
+ // buggy native default writes, verified live byte-for-byte) under a conservative size cap, so
2335
+ // a real store that just happens to share the filename is never at risk: a populated store
2336
+ // either exceeds the cap or lacks this exact header.
2337
+ const RUVECTOR_EMPTY_DB_SIGNATURE =
2338
+ '{"dimensions":384,"distance_metric":"Cosine","storage_path":"./ruvector.db","hnsw_config":null,"quantization":null}';
2339
+ const RUVECTOR_EMPTY_DB_MAX_BYTES = 3 * 1024 * 1024; // observed empty scaffold: 1,589,248 bytes; headroom, still far below any real dataset
2340
+ export function cleanupStrayRuvectorDb(dir = process.cwd()) {
2341
+ const p = path.join(dir, 'ruvector.db');
2342
+ let st;
2343
+ try {
2344
+ st = fs.statSync(p);
2345
+ } catch {
2346
+ return false; // doesn't exist โ€” nothing to do
2347
+ }
2348
+ if (!st.isFile() || st.size === 0 || st.size > RUVECTOR_EMPTY_DB_MAX_BYTES) return false;
2349
+ let buf;
2350
+ try {
2351
+ buf = fs.readFileSync(p);
2352
+ } catch {
2353
+ return false; // unreadable โ€” never guess, just leave it alone
2354
+ }
2355
+ if (!buf.includes(RUVECTOR_EMPTY_DB_SIGNATURE)) return false;
2356
+ try {
2357
+ fs.unlinkSync(p);
2358
+ } catch {
2359
+ return false;
2360
+ }
2361
+ warn(`removed ${p} โ€” an empty container the ruvector MCP server left in this project before issue #39 was fixed (matched the known empty signature; nothing was stored in it)`);
2362
+ return true;
2363
+ }
2364
+
2119
2365
  // โ”€โ”€ step: is the rUv toolkit here? the brain ANSWERS alone; it BUILDS best with Ruflo + RuVector โ”€โ”€
2120
2366
  async function offerStack(env) {
2121
2367
  step(
2122
2368
  'Checking your rUv toolkit',
2123
2369
  "the brain answers on its own with zero setup โ€” but it can also BUILD, and that shines when the tools it recommends are here",
2124
2370
  );
2371
+ cleanupStrayRuvectorDb(); // issue #39: sweep a leftover scaffold from before this fix, if one's here
2125
2372
  const mark = (ok) => (ok ? c.green('โœ“ present') : c.yellow('โ€” not found'));
2126
2373
  info(`platform: ${c.bold(`${env.platform}/${env.arch}`)} ยท node ${c.bold(env.node)}`);
2127
2374
  info(`Ruflo (claude-flow ยท swarms / SPARC / orchestration): ${mark(env.ruflo)}`);
@@ -2147,9 +2394,9 @@ async function offerStack(env) {
2147
2394
  what: 'RuVector (vectors / RVF)',
2148
2395
  // --scope user: same reason the plugin itself installs at user scope โ€” the brain is "one
2149
2396
  // toolkit, every project", and the default "local" scope ties the server to whatever
2150
- // directory happens to be the cwd when this runs.
2151
- shell: env.claude ? ['claude', ['mcp', 'add', 'ruvector', '--scope', 'user', '--', 'npx', '-y', 'ruvector', 'mcp', 'start']] : null,
2152
- say: 'claude mcp add ruvector --scope user -- npx -y ruvector mcp start',
2397
+ // directory happens to be the cwd when this runs. The registered command itself also
2398
+ // pins ITS OWN cwd (issue #39) โ€” see buildRuvectorMcpAddCommand above.
2399
+ ...buildRuvectorMcpAddCommand(env),
2153
2400
  plugin: null,
2154
2401
  verify: () => hasUserScopeMcpServer('ruvector'),
2155
2402
  });
@@ -2169,7 +2416,7 @@ async function offerStack(env) {
2169
2416
  return;
2170
2417
  }
2171
2418
 
2172
- const yes = FLAG_WITH_STACK || (await ask('Add the missing rUv tools now so the brain can build, not just answer?', false));
2419
+ const yes = FLAG_WITH_STACK || (await ask('Add the missing rUv tools now so the brain can build, not just answer?', false, { planKey: 'stack' }));
2173
2420
  if (!yes) {
2174
2421
  info("No problem โ€” nothing's missing for answers. To add the build tools any time:");
2175
2422
  printCmds();
@@ -2457,7 +2704,7 @@ export async function offerStatusline() {
2457
2704
  return 'not-asked';
2458
2705
  }
2459
2706
 
2460
- const yes = FLAG_STATUSLINE || (await ask('Add a RuvNet Brain version segment to your Claude Code status bar?', false, { blanketYes: false }));
2707
+ const yes = FLAG_STATUSLINE || (await ask('Add a RuvNet Brain version segment to your Claude Code status bar?', false, { blanketYes: false, planKey: 'statusline' }));
2461
2708
 
2462
2709
  try {
2463
2710
  fs.mkdirSync(telemetryStateDir(), { recursive: true });
@@ -2768,6 +3015,9 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
2768
3015
 
2769
3016
  installReader(cacheDir);
2770
3017
  const plugin = wirePlugin();
3018
+ // Codex hosts got nothing before this (issue #42): shipped, never registered. Non-fatal like every
3019
+ // other wiring step โ€” a second host we cannot reach must never break the one we can.
3020
+ try { wireCodexHost(); } catch (e) { warn(`(Codex wiring skipped: ${e && e.message})`); }
2771
3021
  if (!FLAG_NO_VERIFY) {
2772
3022
  verifyInstall(cacheDir);
2773
3023
  await smokeQuery(cacheDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "3.9.68-dev",
3
+ "version": "3.9.72-dev",
4
4
  "description": "One-command installer for RuvNet Brain \u2014 a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {