ruvnet-brain 3.9.134-dev → 4.0.1
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/.claude-plugin/marketplace.json +13 -0
- package/README.md +2 -2
- package/bin/install.mjs +284 -33
- package/kb/zip-extract.mjs +53 -14
- package/package.json +7 -1
- package/plugin/.claude-plugin/marketplace.json +13 -0
- package/plugin/.claude-plugin/plugin.json +23 -0
- package/plugin/.codex-plugin/plugin.json +21 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/brain-console.md +16 -0
- package/plugin/commands/configure.md +32 -0
- package/plugin/commands/rvbc.md +78 -0
- package/plugin/commands/rvcb.md +16 -0
- package/plugin/commands/whats-new.md +57 -0
- package/plugin/hooks/codex-hooks.json +160 -0
- package/plugin/hooks/hook-contracts.json +77 -0
- package/plugin/hooks/hooks.json +203 -0
- package/plugin/mcp/server.mjs +35 -6
- package/plugin/scripts/anticipate.sh +534 -0
- package/plugin/scripts/codex-hook-adapter.mjs +96 -0
- package/plugin/scripts/continuation-gate.mjs +267 -0
- package/plugin/scripts/design-wall.sh +137 -0
- package/plugin/scripts/detach.mjs +168 -0
- package/plugin/scripts/finalize-token-meter.mjs +25 -0
- package/plugin/scripts/gate-receipt.sh +35 -0
- package/plugin/scripts/ground-before-write.sh +199 -0
- package/plugin/scripts/ground-ruvnet.sh +507 -0
- package/plugin/scripts/grounding-stamp.sh +113 -0
- package/plugin/scripts/grounding-substance.mjs +595 -0
- package/plugin/scripts/hijack-ruvnet.sh +81 -0
- package/plugin/scripts/hook-input.mjs +558 -0
- package/plugin/scripts/hook-shim-bash.mjs +55 -0
- package/plugin/scripts/hook-shim.mjs +303 -0
- package/plugin/scripts/host-update.mjs +58 -0
- package/plugin/scripts/kling-preflight.sh +146 -0
- package/plugin/scripts/learn-capture.sh +154 -0
- package/plugin/scripts/learn-flush.mjs +138 -0
- package/plugin/scripts/lesson-hooks.sh +213 -0
- package/plugin/scripts/md-stamp.mjs +219 -0
- package/plugin/scripts/protect-brain-state.sh +84 -0
- package/plugin/scripts/route-dispatch.sh +147 -0
- package/plugin/scripts/routing-outcome-capture.mjs +89 -0
- package/plugin/scripts/session-start.sh +868 -0
- package/plugin/scripts/signal-watch.mjs +193 -0
- package/plugin/scripts/unprompted-runtime.mjs +377 -0
- package/plugin/scripts/update-apply.mjs +419 -0
- package/plugin/scripts/verify-interface.sh +53 -0
- package/plugin/scripts/version-bump-gate.sh +112 -0
- package/plugin/skills/brain-build/SKILL.md +123 -0
- package/plugin/skills/brain-console/SKILL.md +20 -0
- package/plugin/skills/brain-prompt/SKILL.md +83 -0
- package/plugin/skills/brain-score/SKILL.md +101 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +117 -0
- package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
- package/plugin/skills/rvbc/SKILL.md +20 -0
- package/plugin/skills/savings/SKILL.md +46 -0
- package/plugin/skills/whats-new/SKILL.md +22 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ruvnet-brain",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Stuart Kerr"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "ruvnet-brain",
|
|
9
|
+
"source": "./plugin",
|
|
10
|
+
"description": "RuvNet brain for Claude Code: source-grounded knowledge of rUv's RuvNet building blocks (RuVector, Ruflo, AgentDB, RuLake, SPARC and ~18 more) + an enforced UserPromptSubmit grounding hook so Claude answers from real source instead of drifting, and works like Ruv (assess → SPARC → ADR/DDD → QA → score → revise). Pulls in any RuvNet repo on demand."
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# 🧠 RuvNet Brain
|
|
6
6
|
|
|
7
|
-
### 🧠 RuvNet Brain — [](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
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
## What's new in
|
|
59
|
+
## What's new in 4.0 — it anticipates, and it learns whether it was right
|
|
60
60
|
|
|
61
61
|
**Building toward L4/L5 (3.9.x, dev).** The mechanisms for the top two rungs of the proactivity
|
|
62
62
|
ladder are built and wired — but they are **not yet verified to 4.0's bar**, which requires all five
|
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
|
|
@@ -620,7 +621,40 @@ function pluginCommandsDir() {
|
|
|
620
621
|
}
|
|
621
622
|
|
|
622
623
|
// ── step: wire the Claude Code plugin ────────────────────────────────────────────────────────────
|
|
623
|
-
function
|
|
624
|
+
export function claudePluginStatus({ home = os.homedir() } = {}) {
|
|
625
|
+
const registry = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
626
|
+
let sawUserRecord = false;
|
|
627
|
+
try {
|
|
628
|
+
const doc = JSON.parse(fs.readFileSync(registry, 'utf8'));
|
|
629
|
+
const rows = doc?.plugins?.['ruvnet-brain@ruvnet-brain'];
|
|
630
|
+
const row = Array.isArray(rows) ? rows.find((candidate) => candidate?.scope === 'user') : null;
|
|
631
|
+
if (!row?.installPath) return { managed: false, installed: false, version: null };
|
|
632
|
+
sawUserRecord = true;
|
|
633
|
+
const realHome = fs.realpathSync(home);
|
|
634
|
+
const cacheRoot = path.join(realHome, '.claude', 'plugins', 'cache', 'ruvnet-brain', 'ruvnet-brain');
|
|
635
|
+
const realInstall = fs.realpathSync(row.installPath);
|
|
636
|
+
if (realInstall !== cacheRoot && !realInstall.startsWith(`${cacheRoot}${path.sep}`)) {
|
|
637
|
+
return { managed: true, installed: false, version: null, error: 'user plugin installPath escapes the managed cache' };
|
|
638
|
+
}
|
|
639
|
+
const manifestPath = path.join(realInstall, '.claude-plugin', 'plugin.json');
|
|
640
|
+
const commandPath = path.join(realInstall, 'commands', 'rvbc.md');
|
|
641
|
+
if (!fs.lstatSync(manifestPath).isFile() || fs.lstatSync(manifestPath).isSymbolicLink()
|
|
642
|
+
|| !fs.lstatSync(commandPath).isFile() || fs.lstatSync(commandPath).isSymbolicLink()) {
|
|
643
|
+
return { managed: true, installed: false, version: null, error: 'managed payload files are not regular files' };
|
|
644
|
+
}
|
|
645
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
646
|
+
return {
|
|
647
|
+
managed: true,
|
|
648
|
+
installed: true,
|
|
649
|
+
version: manifest?.version || row.version || null,
|
|
650
|
+
installPath: realInstall,
|
|
651
|
+
};
|
|
652
|
+
} catch {
|
|
653
|
+
return { managed: sawUserRecord, installed: false, version: null };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false } = {}) {
|
|
624
658
|
step(
|
|
625
659
|
'Wiring the Claude Code plugin',
|
|
626
660
|
'this registers search_ruvnet + the grounding hook so Claude uses the brain automatically',
|
|
@@ -636,37 +670,54 @@ function wirePlugin() {
|
|
|
636
670
|
info(` ${c.bold(manualMarketplace)}`);
|
|
637
671
|
info(` ${c.bold(manualInstall)}`);
|
|
638
672
|
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 };
|
|
673
|
+
return { host: false, wired: false, manualMarketplace, manualInstall };
|
|
640
674
|
}
|
|
641
675
|
|
|
642
|
-
const
|
|
676
|
+
const before = claudePluginStatus();
|
|
677
|
+
if (requireManaged && !before.managed) return { host: false, wired: false, action: 'unmanaged' };
|
|
678
|
+
const addedMarket = before.managed
|
|
679
|
+
? tryRun('claude', ['plugin', 'marketplace', 'update', 'ruvnet-brain'])
|
|
680
|
+
: tryRun('claude', ['plugin', 'marketplace', 'add', 'stuinfla/ruvnet-brain']);
|
|
643
681
|
// Deliberately NOT reassuring here. This used to say "it may already be added — that's fine",
|
|
644
682
|
// which is a GUESS about someone else's machine, and when it was wrong the user finished the
|
|
645
683
|
// install with a working search_ruvnet, no slash commands, and a message telling them all was
|
|
646
684
|
// well. The real state is checked below; nothing is declared fine until it has been looked at.
|
|
647
685
|
if (!addedMarket) info(`marketplace add didn't report success — checking what actually landed…`);
|
|
648
686
|
|
|
649
|
-
tryRun('claude',
|
|
687
|
+
tryRun('claude', before.installed
|
|
688
|
+
? ['plugin', 'update', 'ruvnet-brain@ruvnet-brain', '--scope', 'user']
|
|
689
|
+
: ['plugin', 'install', 'ruvnet-brain@ruvnet-brain', '--scope', 'user']);
|
|
650
690
|
|
|
651
691
|
// NEVER take "installed" on faith — same discipline verifyInstall() applies to the KB. An exit
|
|
652
692
|
// code says the command ran, not that the plugin is usable; the commands either exist on disk or
|
|
653
693
|
// they do not. This is the difference between `/rvbc` working and "Unknown command: /rvbc".
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
656
|
-
ok(
|
|
694
|
+
const installed = claudePluginStatus();
|
|
695
|
+
if (installed.installed && (!expectedVersion || installed.version === expectedVersion)) {
|
|
696
|
+
ok(`plugin installed at user scope (global, alongside Ruflo / RuVector) — exact version ${installed.version}`);
|
|
657
697
|
info(` commands available after a restart: ${c.bold('/rvbc')}, ${c.bold('/ruvnet-brain:configure')}`);
|
|
658
|
-
return { wired: true, manualMarketplace, manualInstall };
|
|
698
|
+
return { host: true, wired: true, version: installed.version, manualMarketplace, manualInstall };
|
|
659
699
|
}
|
|
660
700
|
|
|
661
701
|
// The honest failure. The brain still WORKS — this is the difference between a broken install and
|
|
662
702
|
// a partial one, and the user is told exactly which they have instead of being congratulated.
|
|
663
|
-
|
|
703
|
+
const mismatch = installed.installed && expectedVersion && installed.version !== expectedVersion;
|
|
704
|
+
warn(mismatch
|
|
705
|
+
? `Claude installed ${installed.version || 'an unknown version'}, not required ${expectedVersion}; refusing to call this host converged.`
|
|
706
|
+
: `the plugin did NOT land — so slash commands like ${c.bold('/rvbc')} will not exist yet.`);
|
|
664
707
|
info(`${c.green('Your brain still works')}: search_ruvnet is wired and Claude will ground answers with it.`);
|
|
665
708
|
info(`Only the plugin extras (slash commands, the Console, the grounding hook) are missing.`);
|
|
666
709
|
info(`Run these two yourself to finish:`);
|
|
667
710
|
info(` ${c.bold(manualMarketplace)}`);
|
|
668
711
|
info(` ${c.bold(manualInstall)}`);
|
|
669
|
-
return {
|
|
712
|
+
return {
|
|
713
|
+
host: true,
|
|
714
|
+
wired: false,
|
|
715
|
+
action: mismatch ? 'verification-failed' : 'install-failed',
|
|
716
|
+
expectedVersion,
|
|
717
|
+
version: installed.version,
|
|
718
|
+
manualMarketplace,
|
|
719
|
+
manualInstall,
|
|
720
|
+
};
|
|
670
721
|
}
|
|
671
722
|
|
|
672
723
|
// ── step: wire the Codex host (issue #42, Henrik Pettersen) ──────────────────────────────────────
|
|
@@ -725,6 +776,37 @@ export function mergeCodexConfig(existing, serverPath) {
|
|
|
725
776
|
return { text: `${text}${sep}${block}\n`, action: 'added' };
|
|
726
777
|
}
|
|
727
778
|
|
|
779
|
+
export function removeCodexManagedBlock(existing) {
|
|
780
|
+
const text = typeof existing === 'string' ? existing : '';
|
|
781
|
+
const start = text.indexOf(CODEX_BLOCK_START);
|
|
782
|
+
const end = text.indexOf(CODEX_BLOCK_END, start === -1 ? 0 : start);
|
|
783
|
+
if (start === -1 || end === -1 || end < start) return { text, action: 'absent' };
|
|
784
|
+
const blockEnd = end + CODEX_BLOCK_END.length;
|
|
785
|
+
let before = text.slice(0, start);
|
|
786
|
+
let after = text.slice(blockEnd);
|
|
787
|
+
// mergeCodexConfig inserts one separator newline before the block and the block carries one
|
|
788
|
+
// trailing newline. Remove exactly those two installer-owned seam bytes when present.
|
|
789
|
+
if (before.endsWith('\n\n')) before = before.slice(0, -1);
|
|
790
|
+
if (after.startsWith('\n')) after = after.slice(1);
|
|
791
|
+
return { text: `${before}${after}`, action: 'removed' };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function removeCodexWiring() {
|
|
795
|
+
const configPath = codexConfigPath();
|
|
796
|
+
let before = '';
|
|
797
|
+
try { before = fs.readFileSync(configPath, 'utf8'); } catch { return 'absent'; }
|
|
798
|
+
const result = removeCodexManagedBlock(before);
|
|
799
|
+
if (result.action !== 'removed') return result.action;
|
|
800
|
+
try {
|
|
801
|
+
const backup = `${configPath}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
802
|
+
fs.copyFileSync(configPath, backup);
|
|
803
|
+
atomicReplace(configPath, (tmp) => fs.writeFileSync(tmp, result.text));
|
|
804
|
+
return 'removed';
|
|
805
|
+
} catch {
|
|
806
|
+
return 'failed';
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
728
810
|
// What the doctor asserts from disk — never from the fact that we once ran. "Wired" means our entry
|
|
729
811
|
// is in the config AND the server.mjs it points at is really there, because a registration pointing
|
|
730
812
|
// at a deleted file is worse than no registration: Codex fails at spawn time with nothing to read.
|
|
@@ -835,6 +917,32 @@ const CODEX_PLUGIN_ID = 'ruvnet-brain@ruvnet-brain';
|
|
|
835
917
|
const CODEX_MARKETPLACE = 'ruvnet-brain';
|
|
836
918
|
const CODEX_MARKETPLACE_SOURCE = 'stuinfla/ruvnet-brain';
|
|
837
919
|
|
|
920
|
+
function prepareCodexMarketplace() {
|
|
921
|
+
const target = path.join(
|
|
922
|
+
process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'),
|
|
923
|
+
'codex-marketplace',
|
|
924
|
+
);
|
|
925
|
+
const staged = `${target}.tmp-${process.pid}`;
|
|
926
|
+
fs.rmSync(staged, { recursive: true, force: true });
|
|
927
|
+
fs.mkdirSync(path.join(staged, '.claude-plugin'), { recursive: true });
|
|
928
|
+
fs.copyFileSync(
|
|
929
|
+
path.join(REPO_ROOT, '.claude-plugin', 'marketplace.json'),
|
|
930
|
+
path.join(staged, '.claude-plugin', 'marketplace.json'),
|
|
931
|
+
);
|
|
932
|
+
fs.cpSync(path.join(REPO_ROOT, 'plugin'), path.join(staged, 'plugin'), { recursive: true });
|
|
933
|
+
const prior = `${target}.prior-${process.pid}`;
|
|
934
|
+
fs.rmSync(prior, { recursive: true, force: true });
|
|
935
|
+
if (fs.existsSync(target)) fs.renameSync(target, prior);
|
|
936
|
+
try {
|
|
937
|
+
fs.renameSync(staged, target);
|
|
938
|
+
fs.rmSync(prior, { recursive: true, force: true });
|
|
939
|
+
} catch (error) {
|
|
940
|
+
if (fs.existsSync(prior) && !fs.existsSync(target)) fs.renameSync(prior, target);
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
return target;
|
|
944
|
+
}
|
|
945
|
+
|
|
838
946
|
// Codex 0.145 returns `{ marketplaces: [...] }`; early preview builds returned the array itself.
|
|
839
947
|
// Keep both shapes so an installer update never turns a harmless host-version difference into a
|
|
840
948
|
// repeated "marketplace already exists" failure.
|
|
@@ -904,6 +1012,7 @@ export function wireCodexPlugin({
|
|
|
904
1012
|
return { host: true, action: 'unchanged', ...before };
|
|
905
1013
|
}
|
|
906
1014
|
|
|
1015
|
+
const localMarketplace = runJson === runCodexJson ? prepareCodexMarketplace() : null;
|
|
907
1016
|
const markets = runJson(['plugin', 'marketplace', 'list', '--json'], options);
|
|
908
1017
|
if (!markets.ok) {
|
|
909
1018
|
if (announce) warn(`Codex marketplace check failed — ${markets.error}`);
|
|
@@ -911,9 +1020,15 @@ export function wireCodexPlugin({
|
|
|
911
1020
|
}
|
|
912
1021
|
const marketRows = codexMarketplaceRows(markets.value);
|
|
913
1022
|
const known = marketRows.some((market) => market?.name === CODEX_MARKETPLACE);
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1023
|
+
let marketAction;
|
|
1024
|
+
if (localMarketplace) {
|
|
1025
|
+
if (known) runJson(['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE, '--json'], options);
|
|
1026
|
+
marketAction = runJson(['plugin', 'marketplace', 'add', localMarketplace, '--json'], options);
|
|
1027
|
+
} else {
|
|
1028
|
+
marketAction = known
|
|
1029
|
+
? runJson(['plugin', 'marketplace', 'upgrade', CODEX_MARKETPLACE, '--json'], options)
|
|
1030
|
+
: runJson(['plugin', 'marketplace', 'add', marketplaceSource, '--json'], options);
|
|
1031
|
+
}
|
|
917
1032
|
if (!marketAction.ok) {
|
|
918
1033
|
if (announce) warn(`Codex marketplace ${known ? 'upgrade' : 'add'} failed — ${marketAction.error}`);
|
|
919
1034
|
return { host: true, action: 'marketplace-failed', error: marketAction.error };
|
|
@@ -924,9 +1039,9 @@ export function wireCodexPlugin({
|
|
|
924
1039
|
return { host: true, action: 'plugin-failed', error: added.error };
|
|
925
1040
|
}
|
|
926
1041
|
const after = codexPluginStatus({ ...options, runJson });
|
|
927
|
-
if (!after.installed || !after.enabled) {
|
|
1042
|
+
if (!after.installed || !after.enabled || (expectedVersion && after.version !== expectedVersion)) {
|
|
928
1043
|
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 };
|
|
1044
|
+
return { host: true, action: 'verification-failed', expectedVersion, ...after };
|
|
930
1045
|
}
|
|
931
1046
|
if (announce) {
|
|
932
1047
|
ok(`Codex Brain plugin installed and enabled (${after.version || 'version unknown'}).`);
|
|
@@ -1434,9 +1549,20 @@ async function doctor() {
|
|
|
1434
1549
|
: info('git not found — that\'s fine, this installer never needs it');
|
|
1435
1550
|
cleanupStrayRuvectorDb(); // issue #39: sweep a leftover empty scaffold from before this fix, if one's here
|
|
1436
1551
|
const env = detectEnvironment();
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1552
|
+
let rufloOperational = null;
|
|
1553
|
+
if (env.ruflo) {
|
|
1554
|
+
rufloOperational = probeRufloOperationalHealth();
|
|
1555
|
+
if (rufloOperational.healthy) {
|
|
1556
|
+
ok('Ruflo operational — runtime, memory, and learning signals agree');
|
|
1557
|
+
} else {
|
|
1558
|
+
warn('Ruflo CLI is present, but operational learning is DEGRADED — availability is not enforcement.');
|
|
1559
|
+
if (rufloOperational.stopped) warn(' live status reports STOPPED / swarm not running');
|
|
1560
|
+
if (rufloOperational.memoryContradiction) warn(` status reports no memory while status memory reports ${rufloOperational.memoryEntries} entries`);
|
|
1561
|
+
if (rufloOperational.zeroLearning) warn(' learning dashboard is all zero: patterns=0, routes=0, commands=0');
|
|
1562
|
+
}
|
|
1563
|
+
} else {
|
|
1564
|
+
warn('Ruflo not found — answers still work. To build: npm install -g claude-flow@alpha (or /plugin add ruvnet/claude-flow)');
|
|
1565
|
+
}
|
|
1440
1566
|
env.ruvector
|
|
1441
1567
|
? ok('RuVector present — vector CLI / MCP available')
|
|
1442
1568
|
: warn(`RuVector not found — answers still work. To add: ${buildRuvectorMcpAddCommand(env).say}`);
|
|
@@ -1569,9 +1695,12 @@ async function doctor() {
|
|
|
1569
1695
|
&& !codexLifecycleGuidance(codexLifecycle).healthy
|
|
1570
1696
|
&& !codexLifecycleGuidance(codexLifecycle).intentional,
|
|
1571
1697
|
);
|
|
1698
|
+
const codexWiringFailed = Boolean(cx.host && !cx.wired);
|
|
1572
1699
|
const failed = (hookResult ? hookResult.exitCode !== 0 : !allGreen)
|
|
1573
1700
|
|| groundingUnprovenPersisted
|
|
1574
|
-
|| codexLifecycleFailed
|
|
1701
|
+
|| codexLifecycleFailed
|
|
1702
|
+
|| codexWiringFailed
|
|
1703
|
+
|| Boolean(rufloOperational && !rufloOperational.healthy);
|
|
1575
1704
|
if (failed && !hookResult && !groundingUnprovenPersisted) {
|
|
1576
1705
|
console.log(` ${c.red('✗ FAILING')} — the warnings above are real. Re-run ${c.bold('npx ruvnet-brain')} to repair.`);
|
|
1577
1706
|
}
|
|
@@ -1834,6 +1963,49 @@ function missingUpdaterHelp(kbDir) {
|
|
|
1834
1963
|
console.error(` — the current bundle ships forge-update.mjs; then this command will work.`);
|
|
1835
1964
|
}
|
|
1836
1965
|
|
|
1966
|
+
function syncHostsAfterUpdate() {
|
|
1967
|
+
const results = {};
|
|
1968
|
+
results.claude = wirePlugin({ expectedVersion: PACKAGE_VERSION, requireManaged: true });
|
|
1969
|
+
if (results.claude.host && !results.claude.wired) return { ok: false, results };
|
|
1970
|
+
try {
|
|
1971
|
+
results.codexHost = wireCodexHost();
|
|
1972
|
+
if (results.codexHost.host) {
|
|
1973
|
+
results.codex = wireCodexPlugin({ expectedVersion: PACKAGE_VERSION });
|
|
1974
|
+
if (!['unchanged', 'installed', 'updated', 'disabled'].includes(results.codex.action)) {
|
|
1975
|
+
return { ok: false, results };
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
results.codex = { action: 'failed', error: error?.message || String(error) };
|
|
1980
|
+
return { ok: false, results };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
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 });
|
|
1986
|
+
const okApplied = !applied.error && applied.status === 0;
|
|
1987
|
+
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');
|
|
1990
|
+
try {
|
|
1991
|
+
fs.mkdirSync(home, { recursive: true });
|
|
1992
|
+
const tmp = `${receiptPath}.tmp-${process.pid}`;
|
|
1993
|
+
fs.writeFileSync(tmp, JSON.stringify({
|
|
1994
|
+
desiredVersion: PACKAGE_VERSION,
|
|
1995
|
+
verifiedAt: new Date().toISOString(),
|
|
1996
|
+
hosts: {
|
|
1997
|
+
claude: { state: results.claude.host ? 'ready' : 'absent', version: results.claude.version || null },
|
|
1998
|
+
codex: { state: results.codex?.action === 'disabled' ? 'disabled' : (results.codexHost?.host ? 'ready' : 'absent'), version: results.codex?.version || null },
|
|
1999
|
+
},
|
|
2000
|
+
}, null, 2));
|
|
2001
|
+
fs.renameSync(tmp, receiptPath);
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
return { ok: false, results, applyStatus: applied.status, error: `host convergence receipt failed: ${error.message}` };
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return { ok: okApplied, results, applyStatus: applied.status };
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1837
2009
|
function runUpdate() {
|
|
1838
2010
|
printBanner('update');
|
|
1839
2011
|
const kbDir = resolvedKbDir();
|
|
@@ -1848,10 +2020,14 @@ function runUpdate() {
|
|
|
1848
2020
|
// private KB stores a surprise fresh PUBLIC install is exactly the store-stripping hazard the
|
|
1849
2021
|
// project docs warn about. The fallback's own comment scopes it to an updater that EXISTS but
|
|
1850
2022
|
// is broken — this branch enforces that scope.)
|
|
1851
|
-
if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
|
|
2023
|
+
if (!fs.existsSync(path.join(kbDir, 'forge-update.mjs')) && !FLAG_HOST_SYNC_ONLY) {
|
|
1852
2024
|
missingUpdaterHelp(kbDir);
|
|
1853
2025
|
process.exit(1);
|
|
1854
2026
|
}
|
|
2027
|
+
if (FLAG_HOST_SYNC_ONLY && !fs.existsSync(path.join(kbDir, 'forge-update.mjs'))) {
|
|
2028
|
+
warn('KB updater is absent — continuing with host-shell repair only; the knowledge bundle remains unchanged.');
|
|
2029
|
+
updateStatus = 0;
|
|
2030
|
+
} else {
|
|
1855
2031
|
info(c.dim("running the bundle's own self-updater (backs up first, re-verifies, never half-applies)…\n"));
|
|
1856
2032
|
// Relative filename + matching cwd — same launch convention as smokeQuery(); stdio:'inherit'
|
|
1857
2033
|
// streams the updater's narration live and unedited.
|
|
@@ -1869,6 +2045,15 @@ function runUpdate() {
|
|
|
1869
2045
|
env: { ...process.env, RUVNET_BRAIN_NO_UPDATE_FALLBACK: '1' } });
|
|
1870
2046
|
updateStatus = fr.error ? 1 : (fr.status === null ? 1 : fr.status);
|
|
1871
2047
|
}
|
|
2048
|
+
}
|
|
2049
|
+
if (updateStatus === 0) {
|
|
2050
|
+
info(c.dim('\nsynchronizing every detected host to this exact published version…\n'));
|
|
2051
|
+
const convergence = syncHostsAfterUpdate();
|
|
2052
|
+
if (!convergence.ok) {
|
|
2053
|
+
warn(`host synchronization is incomplete — runtime stays on the prior verified generation${convergence.error ? ` (${convergence.error})` : ''}`);
|
|
2054
|
+
updateStatus = 1;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1872
2057
|
// `--update --auto` = update now AND enroll in Evergreen, so this is the LAST time it's ever run by
|
|
1873
2058
|
// hand. Only enroll if the update itself succeeded — never promise "you're set forever" on a failed
|
|
1874
2059
|
// update. enableNightly() prints its own real verification (plist path + launchctl result).
|
|
@@ -2185,6 +2370,27 @@ export function machineFootprint() {
|
|
|
2185
2370
|
items.push({ label: 'The search_ruvnet MCP server registration', path: claudeJson, undo: 'claude mcp remove ruvnet-brain --scope user' });
|
|
2186
2371
|
}
|
|
2187
2372
|
} catch { /* unreadable — do not claim it */ }
|
|
2373
|
+
try {
|
|
2374
|
+
const codex = codexStatus();
|
|
2375
|
+
if (codex.host && codex.serverPath) {
|
|
2376
|
+
items.push({
|
|
2377
|
+
label: 'The Codex search_ruvnet MCP registration',
|
|
2378
|
+
path: codexConfigPath(),
|
|
2379
|
+
undo: 'npx ruvnet-brain --uninstall (removes only the managed block)',
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
add('Codex lifecycle hook wrapper', codexHookWrapperPath(),
|
|
2383
|
+
'npx ruvnet-brain --uninstall');
|
|
2384
|
+
add('Codex MCP server files', codexServerDir(), 'npx ruvnet-brain --uninstall');
|
|
2385
|
+
const codexPlugin = codexPluginStatus();
|
|
2386
|
+
if (codexPlugin.installed) {
|
|
2387
|
+
items.push({
|
|
2388
|
+
label: 'Codex plugin',
|
|
2389
|
+
path: codexHomeDir(),
|
|
2390
|
+
undo: 'codex plugin remove ruvnet-brain@ruvnet-brain && codex plugin marketplace remove ruvnet-brain',
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
} catch { /* Codex absent or unavailable — do not claim it */ }
|
|
2188
2394
|
// GAP FIX: recordNotified() (scripts/upgrade-notice.mjs, invoked from main() and runDemo() below)
|
|
2189
2395
|
// writes this file the first time a "what's new" notice is shown — a real disk artifact this
|
|
2190
2396
|
// installer's own code path creates, that was never listed here and never removed on --uninstall.
|
|
@@ -2276,7 +2482,9 @@ function uninstallAll() {
|
|
|
2276
2482
|
'Model-router files', 'Status-bar version script', 'Status-bar preference', 'Usage-counts preference',
|
|
2277
2483
|
// Two gaps closed here: the statusLine KEY is now removable in place (we know exactly what we
|
|
2278
2484
|
// 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)'
|
|
2485
|
+
'The statusLine entry in settings.json', 'Upgrade-notice state (release-notice tracking)',
|
|
2486
|
+
'The Codex search_ruvnet MCP registration', 'Codex lifecycle hook wrapper',
|
|
2487
|
+
'Codex MCP server files']);
|
|
2280
2488
|
const willRemove = before.filter((it) => AUTO.has(it.label));
|
|
2281
2489
|
const manual = before.filter((it) => !AUTO.has(it.label));
|
|
2282
2490
|
|
|
@@ -2302,6 +2510,8 @@ function uninstallAll() {
|
|
|
2302
2510
|
// customized — removeSettingsStatusLine() checks the live command before touching anything).
|
|
2303
2511
|
const statusLine = removeSettingsStatusLine();
|
|
2304
2512
|
if (statusLine === 'removed') ok('removed the statusLine entry from ~/.claude/settings.json (your other settings untouched, backup saved)');
|
|
2513
|
+
const codexWiring = removeCodexWiring();
|
|
2514
|
+
if (codexWiring === 'removed') ok('removed the managed RuvNet Brain block from Codex config (your other config untouched, backup saved)');
|
|
2305
2515
|
|
|
2306
2516
|
// NEVER rm -rf A PATH WE HAVE NOT PROVEN IS OURS. resolvedKbDir() honours $RUVNET_BRAIN_KB, which
|
|
2307
2517
|
// the docs encourage for custom install locations — so `RUVNET_BRAIN_KB=$HOME npx ruvnet-brain
|
|
@@ -2340,6 +2550,8 @@ function uninstallAll() {
|
|
|
2340
2550
|
['upgrade-notice state', upgradeNoticeStatePath()],
|
|
2341
2551
|
// ADR-054 §5: a reinstall must never inherit an invisible OFF. See brainOffSentinelPath().
|
|
2342
2552
|
['brain on/off switch', brainOffSentinelPath()],
|
|
2553
|
+
['Codex lifecycle hook wrapper', codexHookWrapperPath()],
|
|
2554
|
+
['Codex MCP server files', codexServerDir()],
|
|
2343
2555
|
]) {
|
|
2344
2556
|
if (!fs.existsSync(target)) continue;
|
|
2345
2557
|
try { fs.rmSync(target, { recursive: true, force: true }); ok(`removed the ${label}`); }
|
|
@@ -2714,9 +2926,9 @@ async function printPlanAndConfirm() {
|
|
|
2714
2926
|
{ name: 'Download the knowledge base',
|
|
2715
2927
|
cost: short(resolveCacheDir().cacheDir),
|
|
2716
2928
|
what: 'Real rUv source, on your disk, so answers cite files instead of guessing.' },
|
|
2717
|
-
{ name: 'Register
|
|
2718
|
-
cost: `
|
|
2719
|
-
what: 'Gives Claude
|
|
2929
|
+
{ name: 'Register host plugins + MCP server',
|
|
2930
|
+
cost: `managed entries in ${short(path.join(H, '.claude'))} and, when detected, ${short(path.join(H, '.codex'))}`,
|
|
2931
|
+
what: 'Gives Claude Code and Codex the same search_ruvnet tool and native Brain workflows.' },
|
|
2720
2932
|
];
|
|
2721
2933
|
// The OPTIONAL steps — each asked ONCE here, stored in PLAN_CHOICES, and consumed by its offer
|
|
2722
2934
|
// later (no second prompt). `def` is the pre-check: nightly + spend are recommended (default yes),
|
|
@@ -2850,6 +3062,35 @@ function detectEnvironment() {
|
|
|
2850
3062
|
return env;
|
|
2851
3063
|
}
|
|
2852
3064
|
|
|
3065
|
+
export function classifyRufloOperationalHealth({ status = '', memory = '', metrics = '' } = {}) {
|
|
3066
|
+
const stopped = /\b(?:RuFlo V3 \[STOPPED\]|Swarm not running|MCP Server\s*\\n.*Not running)/i.test(status);
|
|
3067
|
+
const statusSaysNoMemory = /Backend\s*[|:]?\s*none|Entries\s*[|:]?\s*0\b/i.test(status);
|
|
3068
|
+
const memoryEntries = Number((memory.match(/Total Entries\s*[|:]?\s*([\d,]+)/i)?.[1] || '0').replace(/,/g, ''));
|
|
3069
|
+
const zeroLearning = /Total Patterns\s*[|:]?\s*0\b/i.test(metrics)
|
|
3070
|
+
&& /Total Routes\s*[|:]?\s*0\b/i.test(metrics)
|
|
3071
|
+
&& /Total Executed\s*[|:]?\s*0\b/i.test(metrics);
|
|
3072
|
+
const memoryContradiction = statusSaysNoMemory && memoryEntries > 0;
|
|
3073
|
+
return {
|
|
3074
|
+
healthy: !stopped && !memoryContradiction && !zeroLearning,
|
|
3075
|
+
stopped,
|
|
3076
|
+
zeroLearning,
|
|
3077
|
+
memoryContradiction,
|
|
3078
|
+
memoryEntries,
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
function probeRufloOperationalHealth() {
|
|
3083
|
+
const run = (args) => {
|
|
3084
|
+
const result = spawnSync('ruflo', args, { cwd: process.cwd(), encoding: 'utf8', timeout: 10_000 });
|
|
3085
|
+
return `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
3086
|
+
};
|
|
3087
|
+
return classifyRufloOperationalHealth({
|
|
3088
|
+
status: run(['status']),
|
|
3089
|
+
memory: run(['status', 'memory']),
|
|
3090
|
+
metrics: run(['hooks', 'metrics', '--v3-dashboard']),
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2853
3094
|
// ── issue #39: the ruvector MCP server's own native VectorDb defaults ITS storage to
|
|
2854
3095
|
// "./ruvector.db" relative to whatever cwd it happens to be launched in — and Claude Code
|
|
2855
3096
|
// always launches an MCP server with cwd = the current project, so every consumer project got
|
|
@@ -3295,7 +3536,7 @@ function installedRepoCount(cacheDir) {
|
|
|
3295
3536
|
}
|
|
3296
3537
|
}
|
|
3297
3538
|
|
|
3298
|
-
function success({ cacheDir, isCustom, plugin, env, nightly }) {
|
|
3539
|
+
function success({ cacheDir, isCustom, plugin, codexHost, codexPlugin, env, nightly }) {
|
|
3299
3540
|
const line = '─'.repeat(64);
|
|
3300
3541
|
console.log(`\n${c.green(line)}`);
|
|
3301
3542
|
console.log(`${c.green(c.bold(' RuvNet Brain is installed.'))}`);
|
|
@@ -3306,6 +3547,13 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
|
|
|
3306
3547
|
console.log(
|
|
3307
3548
|
` • the Claude Code plugin ${plugin.wired ? c.green('wired at user scope') : c.yellow('(finish the 2 commands above)')} — search_ruvnet + grounding hook`,
|
|
3308
3549
|
);
|
|
3550
|
+
if (codexHost?.host) {
|
|
3551
|
+
const hostReady = ['added', 'rewritten', 'user-owned'].includes(codexHost.action);
|
|
3552
|
+
const pluginReady = Boolean(codexPlugin?.installed && codexPlugin?.enabled);
|
|
3553
|
+
console.log(
|
|
3554
|
+
` • the Codex host ${hostReady && pluginReady ? c.green('wired at user scope') : c.yellow('needs attention')} — search_ruvnet + native Brain skills`,
|
|
3555
|
+
);
|
|
3556
|
+
}
|
|
3309
3557
|
if (env) {
|
|
3310
3558
|
const t = (okv) => (okv ? c.green('✓ present') : c.yellow('not added (optional)'));
|
|
3311
3559
|
console.log(` • rUv build toolkit — Ruflo: ${t(env.ruflo)} · RuVector: ${t(env.ruvector)} ${c.dim('answers work without them')}`);
|
|
@@ -3316,7 +3564,8 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
|
|
|
3316
3564
|
console.log(` ${c.bold(`export RUVNET_BRAIN_KB="${cacheDir}"`)}`);
|
|
3317
3565
|
}
|
|
3318
3566
|
// ── where + how it runs — the confidence answers, stated up front ──
|
|
3319
|
-
|
|
3567
|
+
const hostLabel = codexHost?.host ? 'Claude Code and Codex' : 'Claude Code';
|
|
3568
|
+
console.log(`\n ${c.bold('Where it runs:')} ${c.bold(`everywhere you use ${hostLabel}`)} — CLI and supported editor/desktop surfaces.`);
|
|
3320
3569
|
console.log(` It's ${c.bold('user-level (global)')}: open ANY repo or folder and it's already there. Nothing per project,`);
|
|
3321
3570
|
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
3571
|
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 +3574,12 @@ function success({ cacheDir, isCustom, plugin, env, nightly }) {
|
|
|
3325
3574
|
console.log(` Release regardless (that part isn't cached). For the bleeding-edge installer too, use ${c.bold('npx github:stuinfla/ruvnet-brain')}.`);
|
|
3326
3575
|
|
|
3327
3576
|
// ── one important expectation: the hook activates on the NEXT session ──
|
|
3328
|
-
console.log(`\n ${c.yellow(c.bold('One thing to know:'))}
|
|
3329
|
-
console.log(` ${c.dim(
|
|
3577
|
+
console.log(`\n ${c.yellow(c.bold('One thing to know:'))} lifecycle hooks turn on in your ${c.bold('next')} host session.`);
|
|
3578
|
+
console.log(` ${c.dim(`If ${hostLabel} is open right now, quit and reopen it so the new plugin snapshot loads.`)}`);
|
|
3330
3579
|
|
|
3331
3580
|
// ── what to do now ──
|
|
3332
3581
|
console.log(`\n ${c.bold('What to do now:')}`);
|
|
3333
|
-
console.log(` ${c.cyan('1.')} Open
|
|
3582
|
+
console.log(` ${c.cyan('1.')} Open ${hostLabel} in ${c.bold('any')} project (your own, or a fresh repo — nothing to copy in).`);
|
|
3334
3583
|
console.log(` ${c.cyan('2.')} Just ask, normally. Try: ${c.bold('"Set up vector search here the way rUv would."')}`);
|
|
3335
3584
|
console.log(` ${c.cyan('3.')} Watch what changes (below).`);
|
|
3336
3585
|
|
|
@@ -3464,7 +3713,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
3464
3713
|
if (FLAG_WHAT_CHANGED) { printBanner('what RuvNet Brain put on this machine'); printFootprint(); return; }
|
|
3465
3714
|
|
|
3466
3715
|
printBanner('installer');
|
|
3467
|
-
console.log(c.dim("I'll set up the brain
|
|
3716
|
+
console.log(c.dim("I'll set up the brain for Claude Code and Codex, explaining each step as I go.\n"));
|
|
3468
3717
|
|
|
3469
3718
|
// ── environment guard: fail early and CLEARLY on unsupported Node, not cryptically mid-install ──
|
|
3470
3719
|
// Node itself can't be silently auto-upgraded from inside a script it's currently running as —
|
|
@@ -3610,10 +3859,12 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
3610
3859
|
const plugin = wirePlugin();
|
|
3611
3860
|
// Codex hosts got nothing before this (issue #42): shipped, never registered. Non-fatal like every
|
|
3612
3861
|
// other wiring step — a second host we cannot reach must never break the one we can.
|
|
3862
|
+
let codexHost = { host: false, action: 'no-host' };
|
|
3863
|
+
let codexPlugin = null;
|
|
3613
3864
|
try {
|
|
3614
|
-
|
|
3865
|
+
codexHost = wireCodexHost();
|
|
3615
3866
|
if (codexHost.host) {
|
|
3616
|
-
|
|
3867
|
+
codexPlugin = wireCodexPlugin();
|
|
3617
3868
|
if (codexPlugin.available) printCodexLifecycle(await codexLifecycleStatus());
|
|
3618
3869
|
}
|
|
3619
3870
|
} catch (e) {
|
|
@@ -3657,7 +3908,7 @@ It is safe to re-run at any time. After installing, restart Claude Code so the g
|
|
|
3657
3908
|
// Same rule: an optional offer can never break a finished install.
|
|
3658
3909
|
try { await offerStatusline(); } catch { /* non-fatal — a status-bar nicety must never break the install */ }
|
|
3659
3910
|
|
|
3660
|
-
success({ cacheDir, isCustom, plugin, env, nightly });
|
|
3911
|
+
success({ cacheDir, isCustom, plugin, codexHost, codexPlugin, env, nightly });
|
|
3661
3912
|
|
|
3662
3913
|
// Close every install by stating, in one place, exactly what is now on their machine and how to
|
|
3663
3914
|
// take each piece back off. Someone reading this should never have to file a bug report to find
|