ruvnet-brain 4.0.5 โ†’ 4.0.7

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 (38) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +112 -7
  3. package/config/model-router/catalog.template.json +17 -2
  4. package/console/app.js +32 -14
  5. package/console/architecture.html +1 -1
  6. package/data/model-catalog.json +54 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/hooks/codex-hooks.json +17 -17
  11. package/plugin/hooks/hooks.json +34 -1
  12. package/plugin/scripts/codex-hook-wrapper.mjs +36 -4
  13. package/plugin/scripts/ground-ruvnet.sh +2 -1
  14. package/plugin/scripts/hook-shim.mjs +13 -6
  15. package/plugin/scripts/lesson-provenance.mjs +21 -0
  16. package/plugin/scripts/lesson-store.mjs +33 -3
  17. package/plugin/scripts/route-dispatch.sh +23 -49
  18. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  19. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  20. package/plugin/scripts/session-start-core.mjs +25 -7
  21. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  22. package/plugin/skills/release-proof/SKILL.md +4 -4
  23. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  24. package/scripts/capability-registry.mjs +3 -3
  25. package/scripts/lesson-lifecycle.mjs +2 -1
  26. package/scripts/lesson-ratify.mjs +6 -3
  27. package/scripts/lesson-seed.mjs +21 -16
  28. package/scripts/model-router-catalog.mjs +16 -0
  29. package/scripts/model-router-engine.mjs +4 -1
  30. package/scripts/onboarding-console.mjs +52 -14
  31. package/scripts/provider-availability.mjs +12 -0
  32. package/scripts/release-authority.mjs +14 -2
  33. package/scripts/release-transaction-provider.mjs +236 -0
  34. package/scripts/release-transaction.mjs +214 -0
  35. package/scripts/release.mjs +67 -279
  36. package/scripts/session-snapshot-contract.mjs +1 -0
  37. package/scripts/stabilization-receipt.mjs +1 -1
  38. package/scripts/staged-host-verifier.mjs +106 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # ๐Ÿง  RuvNet Brain
6
6
 
7
- ### ๐Ÿง  RuvNet Brain โ€” [![RuvNet Brain version 4.0.5 โ€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.5-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### ๐Ÿง  RuvNet Brain โ€” [![RuvNet Brain version 4.0.7 โ€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.7-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack โ€” delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
10
10
 
@@ -247,11 +247,11 @@ Our code now does **one honest job**: a price transform. A model your subscripti
247
247
 
248
248
  Result: **11 jobs supervised, every one producing a fresh successful receipt.** One had been *totally blind* โ€” writing zero bytes on a healthy day, so "ran fine" and "never ran" were indistinguishable. Cured without changing a line of its logic.
249
249
 
250
- ### 3. A subagent can no longer inherit your expensive model by accident
250
+ ### 3. Inherited subagent models are now auditable
251
251
 
252
252
  **A subagent inherits your session's model unless something says otherwise.** Ten agents on an Opus session are ten Opus agents; on a Fable session that's `$10/$50` per Mtok โ€” up to **10ร— what the same mechanical work costs on Haiku**. That single default was the biggest cost leak in the harness, and an advisory rule did not fix it (the router's entire first life saved **$0.018**).
253
253
 
254
- So 2.5.1 makes it a **wall, not advice**: a `PreToolUse` gate that **blocks any subagent dispatch that doesn't declare a `model`**, tells you which tier the task actually needs, and logs every allowed dispatch so routing is *auditable* rather than merely claimed. Forks still inherit โ€” that's what a fork is.
254
+ The hook records both explicit-model and inherited-model dispatches so routing is *auditable* rather than merely claimed. It does not claim to block Agent/Task calls: Claude Code 2.1.220 consumes those `PreToolUse` results only after `tool_dispatch_end` ([issue #84](https://github.com/stuinfla/ruvnet-brain/issues/84), [upstream #83195](https://github.com/anthropics/claude-code/issues/83195)). Forks still inherit โ€” that's what a fork is.
255
255
 
256
256
  > **`npm run falsify`** โ€” the adversary. Every question that had to be asked of this project ("is the nightly *actually* running?", "is that really rUv's code?", "why is my quota still burning?", "is CI *actually* green?") is now a check that fails on an **unproven claim**, not merely on broken code. Because tests you wrote yourself passing is circular evidence.
257
257
 
package/bin/install.mjs CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  requiredEmbedderModels,
25
25
  missingEmbedderModels,
26
26
  } from '../kb/model-requirements.mjs';
27
+ import { mergeManagedCatalog } from '../scripts/model-router-catalog.mjs';
27
28
 
28
29
  // SEC-0010 #6 โ€” the Ed25519 PUBLIC key is EMBEDDED here (not a separate file) so the installer's
29
30
  // trust root travels with the installer code itself: an attacker who swaps the downloaded bundle
@@ -616,6 +617,7 @@ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT)
616
617
  ['console', 'console'],
617
618
  ['scripts', 'scripts'],
618
619
  ['plugin/scripts', 'plugin/scripts'],
620
+ ['data/model-catalog.json', 'data/model-catalog.json'],
619
621
  ['kb/brain-profile.mjs', 'kb/brain-profile.mjs'],
620
622
  ['bin/install.mjs', 'bin/install.mjs'],
621
623
  ['package.json', 'package.json'],
@@ -642,6 +644,15 @@ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT)
642
644
  fs.rmSync(staged, { recursive: true, force: true });
643
645
  throw new Error(`console runtime version ${packageManifest.version || '(missing)'} does not match candidate ${PACKAGE_VERSION}`);
644
646
  }
647
+ try {
648
+ const providerCatalog = JSON.parse(fs.readFileSync(path.join(staged, 'data', 'model-catalog.json'), 'utf8'));
649
+ if (!providerCatalog.providers || typeof providerCatalog.providers !== 'object' || !Object.keys(providerCatalog.providers).length) {
650
+ throw new Error('providers object is missing or empty');
651
+ }
652
+ } catch (error) {
653
+ fs.rmSync(staged, { recursive: true, force: true });
654
+ throw new Error(`console runtime model-catalog is invalid: ${error.message}`);
655
+ }
645
656
  const stagedEntry = path.join(staged, 'scripts', 'onboarding-console.mjs');
646
657
  for (const syntaxTarget of [stagedEntry, path.join(staged, 'bin', 'install.mjs')]) {
647
658
  const checked = spawnSync(process.execPath, ['--check', syntaxTarget], { encoding: 'utf8' });
@@ -791,7 +802,8 @@ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false
791
802
  'Wiring the Claude Code plugin',
792
803
  'this registers search_ruvnet + the grounding hook so Claude uses the brain automatically',
793
804
  );
794
- const manualMarketplace = 'claude plugin marketplace add stuinfla/ruvnet-brain';
805
+ const marketplaceSource = process.env.RUVNET_CLAUDE_MARKETPLACE_SOURCE || 'stuinfla/ruvnet-brain';
806
+ const manualMarketplace = `claude plugin marketplace add ${marketplaceSource}`;
795
807
  const manualInstall = 'claude plugin install ruvnet-brain@ruvnet-brain --scope user';
796
808
 
797
809
  if (!have('claude')) {
@@ -809,7 +821,7 @@ function wirePlugin({ expectedVersion = PACKAGE_VERSION, requireManaged = false
809
821
  if (requireManaged && !before.managed) return { host: false, wired: false, action: 'unmanaged' };
810
822
  const addedMarket = before.managed
811
823
  ? tryRun('claude', ['plugin', 'marketplace', 'update', 'ruvnet-brain'])
812
- : tryRun('claude', ['plugin', 'marketplace', 'add', 'stuinfla/ruvnet-brain']);
824
+ : tryRun('claude', ['plugin', 'marketplace', 'add', marketplaceSource]);
813
825
  // Deliberately NOT reassuring here. This used to say "it may already be added โ€” that's fine",
814
826
  // which is a GUESS about someone else's machine, and when it was wrong the user finished the
815
827
  // install with a working search_ruvnet, no slash commands, and a message telling them all was
@@ -1118,11 +1130,27 @@ const CODEX_PLUGIN_ID = 'ruvnet-brain@ruvnet-brain';
1118
1130
  const CODEX_MARKETPLACE = 'ruvnet-brain';
1119
1131
  const CODEX_MARKETPLACE_SOURCE = 'stuinfla/ruvnet-brain';
1120
1132
 
1121
- function prepareCodexMarketplace() {
1122
- const target = path.join(
1133
+ function codexMarketplaceTarget() {
1134
+ return path.join(
1123
1135
  process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'),
1124
1136
  'codex-marketplace',
1125
1137
  );
1138
+ }
1139
+
1140
+ function codexMarketplaceReady(target = codexMarketplaceTarget()) {
1141
+ try {
1142
+ const manifest = JSON.parse(fs.readFileSync(path.join(target, '.claude-plugin', 'marketplace.json'), 'utf8'));
1143
+ return manifest?.name === CODEX_MARKETPLACE
1144
+ && Array.isArray(manifest.plugins)
1145
+ && manifest.plugins.some((plugin) => plugin?.name === 'ruvnet-brain' && plugin?.source === './plugin')
1146
+ && fs.existsSync(path.join(target, 'plugin', '.codex-plugin', 'plugin.json'));
1147
+ } catch {
1148
+ return false;
1149
+ }
1150
+ }
1151
+
1152
+ function prepareCodexMarketplace() {
1153
+ const target = codexMarketplaceTarget();
1126
1154
  const staged = `${target}.tmp-${process.pid}`;
1127
1155
  fs.rmSync(staged, { recursive: true, force: true });
1128
1156
  fs.mkdirSync(path.join(staged, '.claude-plugin'), { recursive: true });
@@ -1199,6 +1227,18 @@ export function wireCodexPlugin({
1199
1227
  } = {}) {
1200
1228
  if (!fs.existsSync(codexDir)) return { host: false, action: 'no-host' };
1201
1229
  const options = { codexBin, codexHome, cwd };
1230
+ // Codex loads every configured marketplace before it can report plugin state. Repair only an
1231
+ // absent/malformed Brain-owned snapshot before that probe, or the probe can fail before its own
1232
+ // repair path is reachable. A healthy current cache stays byte-untouched, and plugin enablement
1233
+ // is still read and preserved below.
1234
+ let localMarketplace = null;
1235
+ if (runJson === runCodexJson && !codexMarketplaceReady()) {
1236
+ try { localMarketplace = prepareCodexMarketplace(); }
1237
+ catch (error) {
1238
+ if (announce) warn(`Codex marketplace preparation failed โ€” ${error.message}`);
1239
+ return { host: true, action: 'marketplace-prepare-failed', error: error.message };
1240
+ }
1241
+ }
1202
1242
  const before = codexPluginStatus({ ...options, runJson });
1203
1243
  if (!before.available) {
1204
1244
  if (announce) warn(`Codex plugin lifecycle not installed โ€” ${before.error}`);
@@ -1213,7 +1253,13 @@ export function wireCodexPlugin({
1213
1253
  return { host: true, action: 'unchanged', ...before };
1214
1254
  }
1215
1255
 
1216
- const localMarketplace = runJson === runCodexJson ? prepareCodexMarketplace() : null;
1256
+ if (runJson === runCodexJson && !localMarketplace) {
1257
+ try { localMarketplace = prepareCodexMarketplace(); }
1258
+ catch (error) {
1259
+ if (announce) warn(`Codex marketplace preparation failed โ€” ${error.message}`);
1260
+ return { host: true, action: 'marketplace-prepare-failed', error: error.message };
1261
+ }
1262
+ }
1217
1263
  const markets = runJson(['plugin', 'marketplace', 'list', '--json'], options);
1218
1264
  if (!markets.ok) {
1219
1265
  if (announce) warn(`Codex marketplace check failed โ€” ${markets.error}`);
@@ -1728,6 +1774,21 @@ async function doctor() {
1728
1774
  warn('brain not found here โ€” run the installer first: npx ruvnet-brain');
1729
1775
  return 1; // "not installed" is a FAILING doctor, not a neutral one
1730
1776
  }
1777
+ const convergencePath = path.join(process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'), 'host-convergence.json');
1778
+ let hostConvergence = { healthy: true, state: 'not-recorded' };
1779
+ if (fs.existsSync(convergencePath)) {
1780
+ try {
1781
+ hostConvergence = classifyHostConvergence(JSON.parse(fs.readFileSync(convergencePath, 'utf8')));
1782
+ if (hostConvergence.healthy) ok(`host convergence receipt: ${hostConvergence.state}`);
1783
+ else {
1784
+ warn(`host convergence incomplete: ${hostConvergence.state}`);
1785
+ info(`Retry the same generation: ${c.bold('npx ruvnet-brain --update')}${hostConvergence.action ? `; ${hostConvergence.action}` : ''}`);
1786
+ }
1787
+ } catch (error) {
1788
+ hostConvergence = { healthy: false, state: 'invalid-receipt', error: error.message };
1789
+ warn(`host convergence receipt is invalid: ${error.message}`);
1790
+ }
1791
+ }
1731
1792
  have('node') ? ok('node present') : warn('node missing');
1732
1793
  have('npm') ? ok('npm present') : warn('npm missing');
1733
1794
  have('claude') ? ok('claude CLI present') : warn('claude CLI missing (plugin wiring needs it)');
@@ -1908,6 +1969,7 @@ async function doctor() {
1908
1969
  || codexLifecycleFailed
1909
1970
  || codexWiringFailed
1910
1971
  || codexReadinessFailed
1972
+ || !hostConvergence.healthy
1911
1973
  || Boolean(rufloOperational && !rufloOperational.healthy);
1912
1974
  if (failed && !hookResult && !groundingUnprovenPersisted) {
1913
1975
  console.log(` ${c.red('โœ— FAILING')} โ€” the warnings above are real. Re-run ${c.bold('npx ruvnet-brain')} to repair.`);
@@ -2243,7 +2305,29 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
2243
2305
  }
2244
2306
  }
2245
2307
  if (!okApplied) return fail({ applyStatus: applied.status, error: applied.error?.message || 'Stable Spine activation failed' });
2246
- return { ok: true, results, applyStatus: applied.status };
2308
+ const convergence = classifyHostConvergence({
2309
+ desiredVersion: PACKAGE_VERSION,
2310
+ hosts: {
2311
+ claude: { state: results.claude.host ? 'ready' : 'absent', version: results.claude.version || null },
2312
+ codex: { state: results.codex?.action === 'disabled' ? 'disabled' : (results.codexHost?.host ? 'ready' : 'absent'), version: results.codex?.version || null },
2313
+ },
2314
+ consoleRuntime: results.consoleRuntime,
2315
+ });
2316
+ return { ok: true, convergence, results, applyStatus: applied.status };
2317
+ }
2318
+
2319
+ export function classifyHostConvergence(receipt, expectedVersion = PACKAGE_VERSION) {
2320
+ if (!receipt || receipt.desiredVersion !== expectedVersion) {
2321
+ return { healthy: false, state: 'version-mismatch', action: `required version ${expectedVersion}` };
2322
+ }
2323
+ const hostStates = Object.values(receipt.hosts || {});
2324
+ const badHost = hostStates.find((host) => !['ready', 'disabled', 'absent'].includes(host?.state)
2325
+ || (host.state === 'ready' && host.version !== expectedVersion));
2326
+ if (badHost) return { healthy: false, state: 'host-pending', action: 're-run host synchronization' };
2327
+ if (receipt.consoleRuntime?.state !== 'ready') {
2328
+ return { healthy: false, state: receipt.consoleRuntime?.state || 'console-unproven', action: 'restart Console, then re-run --doctor' };
2329
+ }
2330
+ return { healthy: true, state: 'channels-converged' };
2247
2331
  }
2248
2332
 
2249
2333
  function runUpdate() {
@@ -2890,7 +2974,28 @@ export async function offerRouterProfile() {
2890
2974
  for (const [src, dst] of [['catalog.template.json', 'catalog.json'], ['policy.default.mjs', 'policy.default.mjs']]) {
2891
2975
  const s = path.join(pkgRoot, 'config', 'model-router', src);
2892
2976
  const d = path.join(routerDir, dst);
2893
- if (fs.existsSync(s) && !fs.existsSync(d)) { fs.copyFileSync(s, d); ok(`installed ${dst} (edit freely โ€” goldie keeps prices fresh where scheduled)`); }
2977
+ if (!fs.existsSync(s)) continue;
2978
+ if (!fs.existsSync(d)) {
2979
+ fs.copyFileSync(s, d);
2980
+ ok(`installed ${dst} (edit freely โ€” goldie keeps prices fresh where scheduled)`);
2981
+ continue;
2982
+ }
2983
+ if (src === 'catalog.template.json') {
2984
+ try {
2985
+ const existing = JSON.parse(fs.readFileSync(d, 'utf8'));
2986
+ const managed = JSON.parse(fs.readFileSync(s, 'utf8'));
2987
+ const merged = mergeManagedCatalog(existing, managed);
2988
+ if (merged !== existing) {
2989
+ fs.copyFileSync(d, `${d}.pre-managed-merge`);
2990
+ const tmp = `${d}.tmp-${process.pid}`;
2991
+ fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
2992
+ fs.renameSync(tmp, d);
2993
+ ok(`merged ${merged.candidates.length - existing.candidates.length} managed subscription model(s); your catalog overrides were preserved`);
2994
+ }
2995
+ } catch (error) {
2996
+ warn(`managed model additions were not merged (${error.message}); your existing catalog was left unchanged`);
2997
+ }
2998
+ }
2894
2999
  }
2895
3000
  let copied = 0;
2896
3001
  // dispatch-receipt + metaharness-receipts added 2026-07-13: without the LOGGER, subagent routing is
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "_comment": "Model candidate catalog for model-router-engine.mjs. EDIT ME freely. Pricing is $/Mtok. 'verified' names the source+date a price was confirmed live; null pricing means UNKNOWN \u2014 the engine will NOT invent a cost for it (same honesty rule as route-cheap.mjs). 'harness' = which agent(s) can launch each model \u2014 EMPTY [] means landscape-only: known to exist, but no wired execution path yet, so the engine will never select it (see model-router-engine.mjs pool filter). Do not add a real execution path (an entry to route-cheap.mjs PRICING or a harness value) until dispatch is actually implemented and tested \u2014 an engine that 'chooses' a model it can't run is worse than one that doesn't know the model exists. 'subscription' = harness(es) under which this model is covered by a flat subscription (Claude Max, Codex/ChatGPT), so its marginal cost to the user is ~$0 \u2014 the default policy treats those as free and will NOT prefer a billed model over them (never spend where the subscription is free).",
3
- "updated": "2026-07-12 (template)",
3
+ "managedVersion": 2,
4
+ "updated": "2026-08-02 (managed template)",
4
5
  "candidates": [
5
6
  {
6
7
  "id": "claude-haiku-4-5-20251001",
@@ -45,6 +46,20 @@
45
46
  },
46
47
  "verified": "2026-07-07 route-cheap.mjs FRONTIER (API price; $0 under Max)"
47
48
  },
49
+ {
50
+ "id": "claude-opus-5",
51
+ "provider": "anthropic",
52
+ "harness": [
53
+ "claude-code"
54
+ ],
55
+ "subscription": [
56
+ "claude-code"
57
+ ],
58
+ "tier": "frontier",
59
+ "costPerMTok": null,
60
+ "verified": "2026-08-02 Claude Code 2.1.220 subscription launch (canonicalModel claude-opus-5)",
61
+ "note": "Launchability is host-scoped. The managed row adds the reviewed candidate; the user's subscription profile still decides whether it is available and covered."
62
+ },
48
63
  {
49
64
  "id": "claude-fable-5",
50
65
  "provider": "anthropic",
@@ -217,4 +232,4 @@
217
232
  "note": "LANDSCAPE-ONLY: 295B MoE Apache-2.0, strong generalist, second-tier coder (SWE-bench V 78.0 vs GLM-5.2 84.2, vendor numbers). IMPORTANT: this is the PAID slug on purpose \u2014 the tencent/hy3:free promo window closes ~2026-07-20 and would silently die/bill; never wire the :free slug. See goldie/2026-07-12.md Q3."
218
233
  }
219
234
  ]
220
- }
235
+ }
package/console/app.js CHANGED
@@ -2099,17 +2099,24 @@ function renderRouterEngine(re) {
2099
2099
  const pool = Array.isArray(re.pool) ? re.pool : [];
2100
2100
  const TIER_ORDER = { mechanical: 0, cheap: 1, mid: 2, frontier: 3 };
2101
2101
  const byTier = (a, b) => (TIER_ORDER[a.tier] ?? 9) - (TIER_ORDER[b.tier] ?? 9);
2102
- const lensTable = (rows, costOf, costHead) => el('div', { class: 'scroll-x' },
2102
+ const recommended = (Array.isArray(re.decisions) ? re.decisions : []).find((decision) => decision && decision.model);
2103
+ const lensTable = (rows, costOf, costHead, { showRouting = false } = {}) => el('div', { class: 'scroll-x' },
2103
2104
  el('table', { class: 'tb rp-tb' },
2104
2105
  el('thead', {}, el('tr', {},
2105
2106
  el('th', { scope: 'col' }, 'Bucket'), el('th', { scope: 'col' }, 'Model'),
2106
- el('th', { scope: 'col' }, costHead))),
2107
+ el('th', { scope: 'col' }, costHead),
2108
+ showRouting ? el('th', { scope: 'col' }, 'Routing') : null)),
2107
2109
  el('tbody', {}, rows.map((p) => el('tr', {},
2108
2110
  el('td', { class: 'rp-band' }, p.tier || 'โ€”'),
2109
2111
  el('td', {}, el('div', { class: 'rp-model' }, prettyModel(p.id))),
2110
2112
  el('td', { class: 'cell-mono num' }, costOf(p)),
2113
+ showRouting ? el('td', {}, recommended?.model === p.id
2114
+ ? chip('last selected', 'green', recommended.reason || 'The latest router receipt selected this model.')
2115
+ : 'available') : null,
2111
2116
  )))));
2112
- // Development: only what this machine's harness can launch; best (cheapest-marginal) per bucket.
2117
+ // Development is an inventory, not a recommendation filter. The previous one-row-per-tier
2118
+ // projection hid equal-cost Fable behind Opus based solely on array order. Every launchable row
2119
+ // remains visible; the latest actual routing receipt is a separate marker.
2113
2120
  const bestPerTier = (rows, price) => {
2114
2121
  const seen = {};
2115
2122
  for (const p of rows) {
@@ -2118,9 +2125,9 @@ function renderRouterEngine(re) {
2118
2125
  }
2119
2126
  return Object.values(seen).sort(byTier);
2120
2127
  };
2121
- const devRows = bestPerTier(
2122
- pool.filter((p) => (p.harness || []).includes('claude-code')),
2123
- (p) => (p.subscriptionCovered ? -1 : p.marginalPerMTok ?? Infinity));
2128
+ const devRows = pool
2129
+ .filter((p) => (p.harness || []).includes('claude-code'))
2130
+ .sort((a, b) => byTier(a, b) || String(a.id).localeCompare(String(b.id)));
2124
2131
  const prodRows = bestPerTier(
2125
2132
  pool.filter((p) => p.listPerMTok != null && p.provider !== 'local'),
2126
2133
  (p) => p.listPerMTok ?? Infinity);
@@ -2130,7 +2137,7 @@ function renderRouterEngine(re) {
2130
2137
  el('span', { class: 'rp-obj' }, 'you, in Claude Code โ€” models your plan covers win at $0 marginal')),
2131
2138
  lensTable(devRows,
2132
2139
  (p) => (p.subscriptionCovered ? el('b', { title: 'covered by your subscription โ€” zero marginal cost' }, '$0 ยท yours') : money(p.marginalPerMTok)),
2133
- 'Your cost'));
2140
+ 'Your cost', { showRouting: true }));
2134
2141
  const prodBlock = el('div', { class: 'rp-profile' },
2135
2142
  el('div', { class: 'rp-head' },
2136
2143
  el('span', { class: 'rp-name' }, 'Production'),
@@ -2140,7 +2147,7 @@ function renderRouterEngine(re) {
2140
2147
  const poolFoot = el('p', { class: 'fineprint' },
2141
2148
  re.catalogSource === 'built-in-fallback'
2142
2149
  ? `No personal catalog found โ€” showing a minimal built-in set of ${pool.length}. Run \`node scripts/model-router-setup.mjs\` to build your real catalog, then the engine weighs yours on every call.`
2143
- : `Best pick per bucket shown; the engine weighs all ${pool.length} candidates in its catalog on every call โ€” nothing is retired by being off this summary.`);
2150
+ : `All ${devRows.length} launchable Claude Code models are shown. The engine weighs all ${pool.length} candidates; the routing marker comes from its latest receipt, never catalog order.`);
2144
2151
 
2145
2152
  // Decisions: dedupe consecutive identical picks, keep 3, humanize the reason head. The full
2146
2153
  // append-only log stays on disk โ€” this is a pulse, not a table of record.
@@ -4011,6 +4018,7 @@ function renderLessons(data) {
4011
4018
  if (c.awaitingYou) chips.push(chip(`${c.awaitingYou} awaiting you`, 'amber', 'Recorded, but not yet agreed to by you. Until you decide, it does not enforce at full strength.'));
4012
4019
  if (c.active) chips.push(chip(`${c.active} on`, 'green'));
4013
4020
  if (c.off) chips.push(chip(`${c.off} off`, 'grey', 'Switched off by you. The record of where you taught it is kept.'));
4021
+ if (c.quarantined) chips.push(chip(`${c.quarantined} imported`, 'nt', 'Maintainer or demonstration history. It is visible for audit but cannot become your personal policy.'));
4014
4022
  if (c.blocking) chips.push(chip(`${c.blocking} can stop me`, 'cyan', 'These interrupt me at their moment and I cannot continue until the check passes.'));
4015
4023
  setChips('chips-lessons', chips);
4016
4024
 
@@ -4022,6 +4030,7 @@ function renderLessons(data) {
4022
4030
  const box = el('input', {
4023
4031
  type: 'checkbox', class: 'lesson-switch', id: `lsw-${r.id}`,
4024
4032
  'aria-label': `${isOn ? 'Turn off' : 'Turn on'}: ${r.statement.slice(0, 60)}`,
4033
+ disabled: r.quarantined || null,
4025
4034
  });
4026
4035
  box.checked = isOn;
4027
4036
 
@@ -4073,7 +4082,9 @@ function renderLessons(data) {
4073
4082
  chip(r.origin, r.userStated ? 'green' : 'nt',
4074
4083
  r.userStated
4075
4084
  ? 'You said this. Only lessons you stated yourself are allowed to reach the strongest level.'
4076
- : 'I inferred this from what happened. A lesson I inferred can never be raised to "Stops me", however often it fires โ€” the model does not get to ratify its own rules.'),
4085
+ : r.quarantined
4086
+ ? 'This came from bundled maintainer or demonstration history. It is not your statement and cannot be turned into your personal policy.'
4087
+ : 'I inferred this from what happened. A lesson I inferred can never be raised to "Stops me", however often it fires โ€” the model does not get to ratify its own rules.'),
4077
4088
  r.taughtCount ? chip(`taught ${r.taughtCount}ร—`, 'grey') : null,
4078
4089
  r.awaitingYou ? chip('awaiting your decision', 'amber', 'Recorded, but you have not agreed to it yet.') : null,
4079
4090
  ].filter(Boolean);
@@ -4097,9 +4108,9 @@ function renderLessons(data) {
4097
4108
  : null,
4098
4109
  r.projects && r.projects.length
4099
4110
  ? el('p', null, el('strong', null, 'Learned in: '), r.projects.join(', ')) : null,
4100
- el('p', { class: 'muted' },
4101
- 'Turning this off hides the rule without deleting the record of where you taught it โ€” ',
4102
- 'you can switch it back on here at any time.')));
4111
+ el('p', { class: 'muted' }, r.quarantined
4112
+ ? 'This imported record is quarantined for audit. It cannot be switched on or ratified as your policy.'
4113
+ : 'Turning this off hides the rule without deleting the record of where you taught it โ€” you can switch it back on here at any time.')));
4103
4114
 
4104
4115
  list.append(el('div', { class: `cap-row lesson-row${r.demoted ? ' is-off' : ''}` },
4105
4116
  // NO `for=` here. The label WRAPS its checkbox, which is already an implicit association; a
@@ -4142,9 +4153,10 @@ function partitionLessons(list, rows) {
4142
4153
  // the card whose job is to be the truth about it, and printed BECAUSE they used the control we
4143
4154
  // gave them. Found by GPT-5.6-Sol, 2026-07-24. A group's heading must be derivable from the state
4144
4155
  // of the rows inside it, which is why the counts below are computed from the split, never passed in.
4145
- const asks = [], inForce = [], off = [];
4156
+ const asks = [], inForce = [], off = [], quarantined = [];
4146
4157
  rows.forEach((r, i) => {
4147
- if (r.demoted) off.push(kids[i]);
4158
+ if (r.quarantined) quarantined.push(kids[i]);
4159
+ else if (r.demoted) off.push(kids[i]);
4148
4160
  else if (r.awaitingYou) asks.push(kids[i]);
4149
4161
  else inForce.push(kids[i]);
4150
4162
  });
@@ -4171,6 +4183,12 @@ function partitionLessons(list, rows) {
4171
4183
  `${off.length} ${off.length === 1 ? 'rule you switched off' : 'rules you switched off'} โ€” not in force; switch back on any time`),
4172
4184
  el('div', { class: 'cap-list' }, ...off)));
4173
4185
  }
4186
+ if (quarantined.length) {
4187
+ out.push(el('details', { class: 'lessons-more' },
4188
+ el('summary', null,
4189
+ `${quarantined.length} imported maintainer or demonstration ${quarantined.length === 1 ? 'record' : 'records'} โ€” quarantined, not your policy`),
4190
+ el('div', { class: 'cap-list' }, ...quarantined)));
4191
+ }
4174
4192
  return out;
4175
4193
  }
4176
4194
 
@@ -749,7 +749,7 @@ table.tb { width: 100%; border-collapse: collapse; font-size: var(--fs--1); min-
749
749
  <tr><td class="cell-mono">SessionStart</td><td class="cell-name">session-start.sh</td><td><span class="part">nudge</span></td><td>Tells you the brain is on. Runs 4 pure-filesystem health checks in under 5 ms โ€” no cache dir? no <code>.rvf</code> at all? reader deps gone? last real search failed? Injects the capability playbook once per session.</td></tr>
750
750
  <tr><td class="cell-mono">UserPromptSubmit</td><td class="cell-name">ground-ruvnet.sh</td><td><span class="part">nudge</span></td><td>Three gates on your prompt: <b>RuvNet</b> (you named the stack โ†’ ground before asserting), <b>drift</b> (you reached for a classical default โ†’ name the rUv replacement), <b>build</b> (a build request โ†’ apply the playbook).</td></tr>
751
751
  <tr><td class="cell-mono">PreToolUse<small>Write ยท Edit ยท Bash</small></td><td class="cell-name">hijack-ruvnet.sh</td><td><span class="part">nudge</span></td><td>Scans the payload for four categories of classical default โ€” generic vector stores, paid embedding APIs, generic RAG frameworks, hand-rolled memory glue โ€” and names the rUv replacement. <b>Never blocks.</b> One word on line 12 would make it block; it ships as advisory because a false-positive refusal bricks real work.</td></tr>
752
- <tr><td class="cell-mono">PreToolUse<small>Task</small></td><td class="cell-name">route-dispatch.sh</td><td><span class="no">blocks</span></td><td>Refuses a subagent dispatch that would silently inherit your expensive main-loop model. <b>Opt-in only</b> โ€” with no router profile on disk it does nothing, not even warn.</td></tr>
752
+ <tr><td class="cell-mono">PreToolUse<small>Task ยท Agent</small></td><td class="cell-name">route-dispatch.sh</td><td><span class="part">audit</span></td><td>Records declared versus inherited model use. Claude Code 2.1.220 consumes this result after dispatch, so it never claims a late refusal. <b>Opt-in only</b> โ€” with no router profile on disk it does nothing.</td></tr>
753
753
  <tr><td class="cell-mono">PreToolUse<small>Bash</small></td><td class="cell-name">verify-interface.sh</td><td><span class="no">blocks</span></td><td>Refuses an ecosystem CLI subcommand whose <code>--help</code> you have not read in 24 h. Born from reporting AgentDB broken three times when the real defect was passing a query positionally instead of with <code>-q</code>.</td></tr>
754
754
  <tr><td class="cell-mono">PreToolUse<small>Bash</small></td><td class="cell-name">design-wall.sh</td><td><span class="no">blocks</span></td><td>Refuses shipping or opening a visual surface with no fresh design grade. <b>Scoped to this repo</b> since issue&nbsp;#17, after a plain <code>git commit</code> in an unrelated project got blocked demanding someone else's ritual.</td></tr>
755
755
  <tr><td class="cell-mono">PreToolUse ยท Stop</td><td class="cell-name">lesson-hooks.sh</td><td><span class="part">nudge</span></td><td>Surfaces your own recorded corrections at the moment they apply โ€” writing code, changing the machine, asserting a fact, claiming done.</td></tr>
@@ -0,0 +1,54 @@
1
+ {
2
+ "_meta": {
3
+ "purpose": "Per-provider (house) tier ladders. The FRONTIER โ€” the escalation target AND the savings baseline โ€” is personalized to the user's own house: a Claude shop's frontier is Fable 5, a ChatGPT shop's is GPT-5.6 Sol, a Codex shop's is Sol, a Gemini shop's is Gemini 3.1 Pro, a Grok shop's is Grok 4.5. Modeled on ruflo ADR-148 (assets/model-router/openrouter-alts.json), extended with a provider-house axis rUv's registry does not carry.",
4
+ "generated": "2026-07-15",
5
+ "schema_version": 1,
6
+ "sources": {
7
+ "prices": "OpenRouter /api/v1/models live catalog, pulled 2026-07-15 (in/out USD per Mtok).",
8
+ "rankings": "Artificial Analysis Intelligence Index (artificialanalysis.ai) + Arena/LMArena (arena.ai) โ€” the ONLY independent evaluators carrying current-generation models as of 2026-07-15; each figure cross-verified twice.",
9
+ "provenance_rule": "rUv ADR-206: vendor-reported scores are optimistic and harness-confounded โ€” trust independent (AA/Arena) numbers, treat vendor self-scaffold SWE-bench/LiveCodeBench figures as noisy features, never as truth.",
10
+ "benchmark_lag": "The canonical hard coding benchmarks (SWE-bench Verified standardized harness, LiveCodeBench, Aider polyglot) were ALL months stale on 2026-07-15 and carry NONE of these models. The '88.6% / 95% SWE-bench' figures in the press are vendor self-scaffold scores, not the standardized harness โ€” excluded here.",
11
+ "release_refs": "OpenAI GPT-5.6 GA 2026-07-09 (openai.com/index/gpt-5-6); Anthropic Fable 5 2026-06-09 (anthropic.com/news/claude-fable-5-mythos-5); Google Gemini 3.1 Pro 2026-02-19; xAI/SpaceXAI Grok 4.5 2026-07-08."
12
+ },
13
+ "caveat": "Prices are live-verified; tier placements are sensible, independent-benchmark-grounded starters, NOT this user's measured results. Override per-installation via $RUVNET_MODEL_CATALOG or the console. The automated always-current path is rUv's ADR-206 (BenchPress predictor + hourly OpenRouter watcher) โ€” see scripts/refresh-model-catalog.mjs for the live re-pull.",
14
+ "effort_note": "Frontier effort defaults to 'xhigh' (the max reasoning a shop reaches for on the hardest tasks); this is a principled default, not a per-effort measurement."
15
+ },
16
+ "default_provider": "anthropic",
17
+ "default_provider_reason": "This is a Claude Code plugin, so DEVELOPMENT genuinely runs on Anthropic โ€” that is a detected fact, not an arbitrary house preference. Set your PRODUCTION house in the console (or $RUVNET_PROVIDER) if your app runs on a different provider; it is never assumed silently.",
18
+ "providers": {
19
+ "anthropic": {
20
+ "label": "Claude (Anthropic)",
21
+ "detect_env": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_ENTRYPOINT", "CLAUDECODE"],
22
+ "frontier": { "model": "claude-fable-5", "in": 10, "out": 50, "released": "2026-06-09", "rank": "AA Intelligence #1 (60) ยท Arena #1 (1508 Elo)", "source": "independent (AA + Arena)" },
23
+ "mid": { "model": "claude-sonnet-5", "in": 2, "out": 10, "released": "2026-06-30", "rank": "AA 53; intro price $2/$10 โ†’ $3/$15 after 2026-08-31", "source": "independent (AA)" },
24
+ "cheap": { "model": "claude-haiku-4.5", "in": 1, "out": 5, "released": "2025-10-15", "rank": "cheap tier (AA index low; cost-cascade prefers a cross-provider value pick when an OpenRouter key is present)", "source": "catalog" }
25
+ },
26
+ "openai": {
27
+ "label": "ChatGPT (OpenAI)",
28
+ "detect_env": ["OPENAI_API_KEY"],
29
+ "frontier": { "model": "openai/gpt-5.6-sol", "in": 5, "out": 30, "released": "2026-07-09", "rank": "AA Intelligence #2 (59) ยท AA Coding Index leader (80) ยท Terminal-Bench 2.1 SOTA ยท ~1/3 Fable 5's cost/task", "source": "independent (AA)" },
30
+ "mid": { "model": "openai/gpt-5.6-terra", "in": 1, "out": 6, "released": "2026-07-09", "rank": "AA Coding 77.4 โ€” beats prev-gen flagship GPT-5.5 (76.4) at lower live catalog pricing", "source": "independent (AA)" },
31
+ "cheap": { "model": "openai/gpt-5.6-luna", "in": 0.1, "out": 0.6, "released": "2026-07-09", "rank": "AA 51 ยท Coding 74.6 โ€” beats most last-gen mid-tier", "source": "independent (AA)" }
32
+ },
33
+ "codex": {
34
+ "label": "Codex (OpenAI)",
35
+ "detect_env": [],
36
+ "aliasOf": "openai",
37
+ "note": "Codex folded into the GPT-5.6 Sol/Terra/Luna tiers on 2026-07-09 โ€” there is NO separate '-codex' SKU this generation; gpt-5.3-codex (2026-02-05) was the last dedicated one and is superseded. A Codex shop's frontier IS Sol."
38
+ },
39
+ "google": {
40
+ "label": "Gemini (Google)",
41
+ "detect_env": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
42
+ "frontier": { "model": "google/gemini-3.1-pro-preview", "in": 2, "out": 12, "released": "2026-02-19", "rank": "AA Intelligence 46 โ€” BELOW the Anthropic/OpenAI/xAI frontier cluster (54โ€“60); still preview-named. Honest: Gemini is not frontier-competitive on independent indices right now.", "source": "independent (AA) โ€” vendor's GPQA-D 94.3% / ARC-AGI-2 77.1% are Google's own, uncorroborated" },
43
+ "mid": { "model": "google/gemini-3.5-flash", "in": 1.5, "out": 9, "released": "2026", "rank": "AA 50 โ€” out-scores Google's own 3.1 Pro on this index (real anomaly, not a typo)", "source": "independent (AA)" },
44
+ "cheap": { "model": "google/gemini-3.1-flash-lite", "in": 0.25, "out": 1.5, "released": "2026", "rank": "budget", "source": "catalog" }
45
+ },
46
+ "xai": {
47
+ "label": "Grok (SpaceXAI)",
48
+ "detect_env": ["XAI_API_KEY", "GROK_API_KEY"],
49
+ "note": "xAI's live lineup has no distinct budget SKU below Grok 4.3 as of 2026-07-15 (the 'grok-4.1-fast' I first wrote does not exist in the live catalog โ€” the verify gate caught it). A Grok shop's cheap tasks route to Grok 4.3 or a cross-provider value pick.",
50
+ "frontier": { "model": "x-ai/grok-4.5", "in": 2, "out": 6, "released": "2026-07-08", "rank": "AA Intelligence 54 (#8) at ~1/3 Opus 4.7's blended price โ€” the frontier VALUE standout; 'Opus-class, faster, cheaper' (Musk)", "source": "independent (AA)" },
51
+ "mid": { "model": "x-ai/grok-4.3", "in": 1.25, "out": 2.5, "released": "2026-04-30", "rank": "AA 38 โ€” also xAI's cheapest verified stable tier", "source": "independent (AA)" }
52
+ }
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "description": "One-command installer for RuvNet Brain โ€” 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": {
@@ -68,6 +68,7 @@
68
68
  "LICENSE",
69
69
  "console/",
70
70
  "config/model-router/",
71
+ "data/model-catalog.json",
71
72
  "scripts/",
72
73
  "kb/verify-citation.mjs",
73
74
  "kb/zip-extract.mjs",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code โ€” grounds every RuvNet decision in real source across 69 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.0.5",
4
+ "version": "4.0.7",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"