ruvnet-brain 4.0.8 β†’ 4.0.24

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 (63) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +109 -42
  3. package/console/app.js +32 -11
  4. package/console/style.css +5 -0
  5. package/package.json +1 -1
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/scripts/advocacy-outcomes.mjs +808 -0
  9. package/plugin/scripts/anticipate.sh +80 -14
  10. package/plugin/scripts/capability-registry.mjs +994 -0
  11. package/plugin/scripts/codex-hook-wrapper.mjs +1 -0
  12. package/plugin/scripts/continuation-gate.mjs +169 -1
  13. package/plugin/scripts/gates.mjs +146 -0
  14. package/plugin/scripts/goal-match.mjs +398 -0
  15. package/plugin/scripts/hijack-ruvnet.sh +69 -1
  16. package/plugin/scripts/hook-registry.mjs +616 -0
  17. package/plugin/scripts/hook-shim.mjs +13 -2
  18. package/plugin/scripts/learn-flush.mjs +37 -2
  19. package/plugin/scripts/learning-enable.mjs +382 -0
  20. package/plugin/scripts/lesson-promote.mjs +262 -0
  21. package/plugin/scripts/lesson-provenance.mjs +43 -0
  22. package/plugin/scripts/lesson-store.mjs +67 -56
  23. package/plugin/scripts/memory-doctor.mjs +345 -0
  24. package/plugin/scripts/nightly-controller.mjs +98 -0
  25. package/plugin/scripts/project-identity.mjs +89 -0
  26. package/plugin/scripts/ruflo-bin.mjs +81 -0
  27. package/plugin/scripts/runtime-preferences.mjs +18 -0
  28. package/plugin/scripts/session-snapshot-hook.mjs +4 -1
  29. package/plugin/scripts/session-start-core.mjs +19 -2
  30. package/plugin/scripts/unprompted-runtime.mjs +22 -7
  31. package/plugin/scripts/user-settings.mjs +672 -0
  32. package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
  33. package/scripts/advocacy-outcomes.mjs +4 -808
  34. package/scripts/capability-registry.mjs +4 -876
  35. package/scripts/console-runtime-identity.mjs +74 -0
  36. package/scripts/corpus-qa.mjs +44 -6
  37. package/scripts/distill-project.mjs +9 -1
  38. package/scripts/doc-currency.mjs +45 -4
  39. package/scripts/gates.mjs +4 -146
  40. package/scripts/goal-match.mjs +4 -398
  41. package/scripts/health-repair.mjs +88 -11
  42. package/scripts/hook-registry.mjs +4 -567
  43. package/scripts/host-install-matrix.mjs +155 -0
  44. package/scripts/issue-watch.mjs +108 -0
  45. package/scripts/learning-enable.mjs +4 -380
  46. package/scripts/lesson-promote.mjs +4 -262
  47. package/scripts/memory-doctor.mjs +4 -342
  48. package/scripts/model-router-catalog.mjs +34 -0
  49. package/scripts/nightly-controller.mjs +4 -66
  50. package/scripts/nightly-wrapper.sh +23 -1
  51. package/scripts/onboarding-console.mjs +34 -12
  52. package/scripts/proactivity-metrics.mjs +8 -1
  53. package/scripts/publication-receipt.mjs +10 -1
  54. package/scripts/qe/ux-suite.mjs +72 -1
  55. package/scripts/release-abort-stale.mjs +111 -0
  56. package/scripts/release-convergence-watchdog.mjs +119 -0
  57. package/scripts/release-transaction-provider.mjs +76 -8
  58. package/scripts/release-transaction.mjs +55 -17
  59. package/scripts/rvf-generation.mjs +17 -0
  60. package/scripts/self-update.mjs +63 -10
  61. package/scripts/staged-host-verifier.mjs +27 -54
  62. package/scripts/sync-version.mjs +10 -10
  63. package/scripts/user-settings.mjs +4 -640
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 4.0.8 β€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.8-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.24 β€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.0.24-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
 
@@ -445,7 +445,7 @@ The brain answers **both** kinds of questions. **Name the repo or ask something
445
445
 
446
446
  ## What it covers
447
447
 
448
- 69 of rUv's repos in the [ruvnet](https://github.com/ruvnet) org β€” the reusable **building blocks** you'd actually compose into a system β€” each deep-walked into its own canonical RVF segment. The core blocks below also carry symbol indexes and capability cards (the 8 newest repos are findable by name; their capability cards are coming).
448
+ 71 of rUv's repos in the [ruvnet](https://github.com/ruvnet) org β€” the reusable **building blocks** you'd actually compose into a system β€” each deep-walked into its own canonical RVF segment. The core blocks below also carry symbol indexes and capability cards (the 8 newest repos are findable by name; their capability cards are coming).
449
449
 
450
450
  ![The RuvNet stack the brain covers](primer/assets/diagrams/ruvnet-stack.svg)
451
451
 
@@ -522,7 +522,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
522
522
 
523
523
  This project versions in the open (see the live badge up top for the exact plugin version; the downloadable knowledge bundle is a separate track) β€” we don't claim β€œdone,” β€œcomplete,” or β€œzero hallucinations.” Where it stands:
524
524
 
525
- - βœ… **The grounding brain is real and proven** β€” 63 public stores Β· 153,369 public source chunks (70 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
525
+ - βœ… **The grounding brain is real and proven** β€” 64 public stores Β· 138,140 public source chunks (71 built stores incl. private), dual embeddings, cross-encoder rerank, plugin (MCP tool + enforcement hook + skill), all re-runnable.
526
526
  - βœ… **Code-level depth** β€” the code-rich repos are indexed to full function bodies; β€œhow is it implemented?” returns the implementation. Verified in the shipped bundle (clean-room 3/3).
527
527
  - βœ… **Routing holds** β€” named 47/48, described 26/28, scenario 7/8; behavioral L1–L3 all pass (**L4 downgraded β€” it measures that the brain spoke, not that anything listened**); private stores fenced out of the public bundle (zero-leak verified).
528
528
  - ⚠️ **Two routing residuals** (above) β€” surfaced, not hidden.
package/bin/install.mjs CHANGED
@@ -24,7 +24,10 @@ import {
24
24
  requiredEmbedderModels,
25
25
  missingEmbedderModels,
26
26
  } from '../kb/model-requirements.mjs';
27
- import { mergeManagedCatalog } from '../scripts/model-router-catalog.mjs';
27
+ import { applyManagedCatalogUpdate } from '../scripts/model-router-catalog.mjs';
28
+ import {
29
+ CONSOLE_RUNTIME_SURFACE, CONSOLE_RUNTIME_IDENTITY_FILE, consoleRuntimeDigest,
30
+ } from '../scripts/console-runtime-identity.mjs';
28
31
 
29
32
  // SEC-0010 #6 β€” the Ed25519 PUBLIC key is EMBEDDED here (not a separate file) so the installer's
30
33
  // trust root travels with the installer code itself: an attacker who swaps the downloaded bundle
@@ -613,22 +616,17 @@ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT)
613
616
  fs.rmSync(prior, { recursive: true, force: true });
614
617
  fs.mkdirSync(staged, { recursive: true, mode: 0o700 });
615
618
 
616
- const required = [
617
- ['console', 'console'],
618
- ['scripts', 'scripts'],
619
- ['plugin/scripts', 'plugin/scripts'],
620
- ['data/model-catalog.json', 'data/model-catalog.json'],
621
- ['kb/brain-profile.mjs', 'kb/brain-profile.mjs'],
622
- ['bin/install.mjs', 'bin/install.mjs'],
623
- ['package.json', 'package.json'],
624
- ];
625
- for (const [from, to] of required) {
626
- const source = path.join(sourceRoot, from);
619
+ // The copy list IS the generation's digest input (scripts/console-runtime-identity.mjs). Adding a
620
+ // file the runtime needs adds it to the runtime's identity in the same edit, so an asset can never
621
+ // again travel separately from the executable that reads it (#76) and a candidate can never again
622
+ // be indistinguishable from the one it replaces (#79).
623
+ for (const relative of CONSOLE_RUNTIME_SURFACE) {
624
+ const source = path.join(sourceRoot, relative);
627
625
  if (!fs.existsSync(source)) {
628
626
  fs.rmSync(staged, { recursive: true, force: true });
629
- throw new Error(`console runtime is incomplete: missing ${from}`);
627
+ throw new Error(`console runtime is incomplete: missing ${relative}`);
630
628
  }
631
- const target = path.join(staged, to);
629
+ const target = path.join(staged, relative);
632
630
  fs.mkdirSync(path.dirname(target), { recursive: true });
633
631
  fs.cpSync(source, target, { recursive: true, force: true, preserveTimestamps: true });
634
632
  }
@@ -661,15 +659,28 @@ export function beginConsoleRuntimeTransaction(cacheDir, sourceRoot = REPO_ROOT)
661
659
  throw new Error(`console runtime failed syntax verification: ${path.relative(staged, syntaxTarget)}`);
662
660
  }
663
661
  }
662
+ // #76: prove the installed What's New boundary from the staged bytes, not from a file listing. The
663
+ // executable exits nonzero when its version manifest or curated notes are absent, so a runtime that
664
+ // would have to tell the user "the notes are unavailable" never activates in the first place.
665
+ const stagedWhatsNew = path.join(staged, 'plugin', 'scripts', 'whats-new.mjs');
666
+ const whatsNew = spawnSync(process.execPath, [stagedWhatsNew], { encoding: 'utf8' });
667
+ if (whatsNew.error || whatsNew.status !== 0) {
668
+ const detail = (whatsNew.stderr || whatsNew.error?.message || `exit ${whatsNew.status}`).trim();
669
+ fs.rmSync(staged, { recursive: true, force: true });
670
+ throw new Error(`console runtime cannot report its own release notes: ${detail}`);
671
+ }
664
672
  const identity = {
665
673
  product: 'ruvnet-brain-console-runtime',
666
674
  schema: 1,
667
675
  apiContract: 1,
668
676
  runtimeVersion: packageManifest.version,
669
677
  entrypoint: 'scripts/onboarding-console.mjs',
670
- sourceSha256: crypto.createHash('sha256').update(fs.readFileSync(stagedEntry)).digest('hex'),
678
+ // The WHOLE runtime, not just its entrypoint (#79). The launcher compares this against a live
679
+ // server's self-reported identity, so it has to change whenever anything the server executes or
680
+ // serves changes β€” otherwise a stale process passes as current.
681
+ sourceSha256: consoleRuntimeDigest(staged),
671
682
  };
672
- fs.writeFileSync(path.join(staged, 'runtime-identity.json'), `${JSON.stringify(identity, null, 2)}\n`, { mode: 0o600 });
683
+ fs.writeFileSync(path.join(staged, CONSOLE_RUNTIME_IDENTITY_FILE), `${JSON.stringify(identity, null, 2)}\n`, { mode: 0o600 });
673
684
 
674
685
  let state = 'staged';
675
686
  const entry = path.join(runtime, identity.entrypoint);
@@ -2230,6 +2241,37 @@ const xmlEscape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').
2230
2241
  const cronExample = (kbDir) =>
2231
2242
  `47 3 * * * cd ${kbDir} && ${process.execPath} forge-update.mjs --apply >> ${kbDir}/update.log 2>&1`;
2232
2243
 
2244
+ /**
2245
+ * kb/forge-update.mjs exit 11 = "the download completed but the bundle on disk did NOT change"
2246
+ * (its `EXIT_NOT_LANDED`). Duplicated as a literal on purpose: the updater lives in the KB BUNDLE,
2247
+ * which versions and ships independently of this installer, so there is no import to share. The
2248
+ * two are pinned together by tests/unit/update-not-landed-exit.test.mjs, which reads the constant
2249
+ * out of kb/forge-update.mjs and fails if they ever drift.
2250
+ */
2251
+ export const UPDATE_NOT_LANDED = 11;
2252
+
2253
+ /**
2254
+ * What `--update` does with the KB updater's exit code (issue #106).
2255
+ *
2256
+ * `--update` used to convert an honest failure into a reported success. The updater detected the
2257
+ * problem itself and said so β€” "UPDATE MISMATCH: SOURCE.json on disk is IDENTICAL to before the
2258
+ * update … REFUSING to report success" β€” and then the fresh-install FALLBACK below ran, succeeded
2259
+ * at re-installing the very same bytes, and its exit 0 became the exit code of the whole command.
2260
+ * A user's scheduled job read that as success while the corpus had not moved.
2261
+ *
2262
+ * The fallback exists for ONE thing: an old bundle whose canonical manifest URL 404s, where a fresh
2263
+ * install genuinely rescues the user. "Nothing landed" is not that case β€” it is a TRUE verdict
2264
+ * about an intact KB, and re-downloading the same bundle cannot change it. So that verdict is
2265
+ * terminal and keeps its own exit code; every other failure keeps today's fallback behaviour.
2266
+ */
2267
+ export function classifyUpdaterExit(status, { fallbackAllowed = true } = {}) {
2268
+ if (status === 0) return { verdict: 'updated', fallback: false, exitCode: 0 };
2269
+ if (status === UPDATE_NOT_LANDED) {
2270
+ return { verdict: 'not-landed', fallback: false, exitCode: UPDATE_NOT_LANDED };
2271
+ }
2272
+ return { verdict: 'failed', fallback: fallbackAllowed, exitCode: status || 1 };
2273
+ }
2274
+
2233
2275
  function missingUpdaterHelp(kbDir) {
2234
2276
  console.error(`\n${c.red('βœ— can\'t update:')} ${c.bold('forge-update.mjs')} is missing from ${kbDir}.`);
2235
2277
  console.error(` Either no brain is installed there, or the bundle predates the self-updater.`);
@@ -2366,12 +2408,29 @@ function runUpdate() {
2366
2408
  // real user, Jan Lafko, hit). NEVER leave the user stranded at a 404: re-run THIS installer as a
2367
2409
  // fresh install, which pulls the latest Release DIRECTLY (releases/latest) and never touches the
2368
2410
  // manifest β€” so --update always succeeds and self-heals the stale SOURCE.json in one shot.
2369
- if (updateStatus !== 0 && !process.env.RUVNET_BRAIN_NO_UPDATE_FALLBACK) {
2411
+ //
2412
+ // Scoped again 2026-08-04 (issue #106): "nothing landed" is excluded, because for THAT verdict the
2413
+ // fallback re-installed identical bytes and its exit 0 became the command's exit code, turning the
2414
+ // updater's own correct refusal into a reported success.
2415
+ const outcome = classifyUpdaterExit(updateStatus, {
2416
+ fallbackAllowed: !process.env.RUVNET_BRAIN_NO_UPDATE_FALLBACK,
2417
+ });
2418
+ if (outcome.verdict === 'not-landed') {
2419
+ console.error(`\n ${c.red('βœ— the knowledge bundle did not change.')} The updater downloaded a bundle and refused to`);
2420
+ console.error(` call it an update because the copy on disk is identical to the one it replaced.`);
2421
+ console.error(` Nothing is broken and nothing was lost β€” but this run is ${c.bold('not')} a success, so it exits`);
2422
+ console.error(` ${c.bold(String(UPDATE_NOT_LANDED))} rather than 0 and a scheduled job will see the failure (issue #106).`);
2423
+ console.error(` If you believe a newer build exists, check: ${c.bold('node forge-update.mjs --check')} in ${kbDir}`);
2424
+ process.exit(outcome.exitCode);
2425
+ }
2426
+ if (outcome.fallback) {
2370
2427
  warn("\nthe bundle's own updater couldn't complete β€” falling back to a fresh install of the latest Release (this always works)…\n");
2371
2428
  const self = fileURLToPath(import.meta.url);
2372
2429
  const fr = spawnSync(process.execPath, [self, '--force'], { stdio: 'inherit',
2373
2430
  env: { ...process.env, RUVNET_BRAIN_NO_UPDATE_FALLBACK: '1' } });
2374
2431
  updateStatus = fr.error ? 1 : (fr.status === null ? 1 : fr.status);
2432
+ } else {
2433
+ updateStatus = outcome.exitCode;
2375
2434
  }
2376
2435
  }
2377
2436
  if (updateStatus === 0) {
@@ -2382,6 +2441,25 @@ function runUpdate() {
2382
2441
  updateStatus = 1;
2383
2442
  }
2384
2443
  }
2444
+ // Issue #87: a normal lifecycle update must also carry MANAGED MODEL additions to an existing
2445
+ // user. This call used to live only in offerRouterProfile() β€” the fresh-install path β€” so someone
2446
+ // who already had ~/.claude/model-router/catalog.json (i.e. every existing user, the only ones who
2447
+ // can be behind) never received a newly shipped managed candidate no matter how often they
2448
+ // updated. Additive and non-interactive; their overrides win, metered rows are never auto-enabled,
2449
+ // and a failure here is reported but never fails an otherwise-good update.
2450
+ if (updateStatus === 0) {
2451
+ try {
2452
+ const managed = applyManagedCatalogUpdate({
2453
+ routerDir: path.join(os.homedir(), '.claude', 'model-router'),
2454
+ packageRoot: REPO_ROOT,
2455
+ });
2456
+ if (managed.action === 'merged' && managed.added.length) {
2457
+ ok(`model router: added ${managed.added.length} managed model(s) β€” ${managed.added.join(', ')} (your catalog edits were preserved)`);
2458
+ }
2459
+ } catch (e) {
2460
+ warn(`managed model additions were not merged (${e.message}); your catalog was left unchanged`);
2461
+ }
2462
+ }
2385
2463
  // `--update --auto` = update now AND enroll in Evergreen, so this is the LAST time it's ever run by
2386
2464
  // hand. Only enroll if the update itself succeeded β€” never promise "you're set forever" on a failed
2387
2465
  // update. enableNightly() prints its own real verification (plist path + launchctl result).
@@ -2975,31 +3053,20 @@ export async function offerRouterProfile() {
2975
3053
  );
2976
3054
 
2977
3055
  fs.mkdirSync(path.join(routerDir, 'bin'), { recursive: true });
2978
- for (const [src, dst] of [['catalog.template.json', 'catalog.json'], ['policy.default.mjs', 'policy.default.mjs']]) {
2979
- const s = path.join(pkgRoot, 'config', 'model-router', src);
2980
- const d = path.join(routerDir, dst);
2981
- if (!fs.existsSync(s)) continue;
2982
- if (!fs.existsSync(d)) {
2983
- fs.copyFileSync(s, d);
2984
- ok(`installed ${dst} (edit freely β€” goldie keeps prices fresh where scheduled)`);
2985
- continue;
2986
- }
2987
- if (src === 'catalog.template.json') {
2988
- try {
2989
- const existing = JSON.parse(fs.readFileSync(d, 'utf8'));
2990
- const managed = JSON.parse(fs.readFileSync(s, 'utf8'));
2991
- const merged = mergeManagedCatalog(existing, managed);
2992
- if (merged !== existing) {
2993
- fs.copyFileSync(d, `${d}.pre-managed-merge`);
2994
- const tmp = `${d}.tmp-${process.pid}`;
2995
- fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
2996
- fs.renameSync(tmp, d);
2997
- ok(`merged ${merged.candidates.length - existing.candidates.length} managed subscription model(s); your catalog overrides were preserved`);
2998
- }
2999
- } catch (error) {
3000
- warn(`managed model additions were not merged (${error.message}); your existing catalog was left unchanged`);
3001
- }
3002
- }
3056
+ const policySrc = path.join(pkgRoot, 'config', 'model-router', 'policy.default.mjs');
3057
+ const policyDst = path.join(routerDir, 'policy.default.mjs');
3058
+ if (fs.existsSync(policySrc) && !fs.existsSync(policyDst)) {
3059
+ fs.copyFileSync(policySrc, policyDst);
3060
+ ok('installed policy.default.mjs (edit freely β€” goldie keeps prices fresh where scheduled)');
3061
+ }
3062
+ // ONE implementation of the managed-catalog step, shared with runUpdate() (issue #87). It used to
3063
+ // live here only, which is exactly why existing users never received managed additions.
3064
+ try {
3065
+ const managed = applyManagedCatalogUpdate({ routerDir, packageRoot: pkgRoot });
3066
+ if (managed.action === 'created') ok('installed catalog.json (edit freely β€” goldie keeps prices fresh where scheduled)');
3067
+ if (managed.action === 'merged') ok(`merged ${managed.added.length} managed subscription model(s); your catalog overrides were preserved`);
3068
+ } catch (error) {
3069
+ warn(`managed model additions were not merged (${error.message}); your existing catalog was left unchanged`);
3003
3070
  }
3004
3071
  let copied = 0;
3005
3072
  // dispatch-receipt + metaharness-receipts added 2026-07-13: without the LOGGER, subagent routing is
package/console/app.js CHANGED
@@ -2313,6 +2313,25 @@ function renderDistribution(u) {
2313
2313
  el('p', { class: 'fineprint' }, u.note));
2314
2314
  }
2315
2315
 
2316
+ // One provider's key chip. THREE states, never two: found, not found, and NOT CHECKED. The third
2317
+ // exists because an instrument that could not run has found nothing β€” it has not found "no key"
2318
+ // (issue #86). Pure and total, so the not-checked branch cannot be reached by accident.
2319
+ function providerKeyChip(id, keys, keysVerified, names) {
2320
+ const name = names[id] || id;
2321
+ if (!keysVerified) {
2322
+ return el('span', {
2323
+ class: 'plan-key unknown',
2324
+ title: `Not checked: Brain could not load its provider catalog, so it cannot tell whether an API key for ${name} is set on this machine`,
2325
+ }, el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, '?'), ` ${name}`);
2326
+ }
2327
+ const found = !!keys[id];
2328
+ return el('span', {
2329
+ class: `plan-key ${found ? 'yes' : 'no'}`,
2330
+ title: found ? `An API key for ${name} is set on this machine`
2331
+ : `No API key for ${name} found on this machine`,
2332
+ }, el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, found ? 'βœ“' : 'βœ—'), ` ${name}`);
2333
+ }
2334
+
2316
2335
  // Plan block (issue #24, applying sparkling's #21 redesign) β€” three genuinely different questions
2317
2336
  // used to render as one flat row of identical "chips": which subscription this runs on ("house" β€” a
2318
2337
  // word nobody outside the source knows), whether cheap-task routing is on, and which OTHER API keys
@@ -2339,19 +2358,21 @@ function renderProviders(sv) {
2339
2358
 
2340
2359
  // BOX 1 β€” YOUR PLAN. The one choice here that changes cost: which subscription MetaHarness treats
2341
2360
  // as $0. Footer checklist: which OTHER providers have a real API key on this machine β€” honest now.
2361
+ //
2362
+ // Issue #86: `keys` is only a MEASUREMENT while the verified provider catalog actually loaded. When
2363
+ // that asset was missing from the packaged runtime, the server fell back to native detections and
2364
+ // said so in providerCatalog β€” and this checklist ignored it, printing a confident "βœ— … No API key
2365
+ // for Gemini found on this machine" straight off `keys`. That turned an internal packaging failure
2366
+ // into a stated fact about the user's credentials. Not-checked is now its own third state.
2367
+ const catalogHealth = re.providerCatalog || null;
2368
+ const keysVerified = !catalogHealth
2369
+ || (catalogHealth.keysVerified !== false && catalogHealth.status !== 'degraded');
2342
2370
  const others = ['anthropic', 'openai', 'google', 'xai'].filter((id) => id !== house.provider);
2343
2371
  const checklist = el('div', { class: 'plan-keys' },
2344
- el('span', { class: 'plan-keys-lab' }, 'Other keys found:'),
2345
- ...others.map((id) => {
2346
- const ok = !!keys[id];
2347
- return el('span', {
2348
- class: `plan-key ${ok ? 'yes' : 'no'}`,
2349
- title: ok ? `An API key for ${KEY_NAME[id]} is set on this machine`
2350
- : `No API key for ${KEY_NAME[id]} found on this machine`,
2351
- },
2352
- el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, ok ? 'βœ“' : 'βœ—'),
2353
- ` ${KEY_NAME[id]}`);
2354
- }));
2372
+ el('span', { class: 'plan-keys-lab' }, keysVerified ? 'Other keys found:' : 'Other keys:'),
2373
+ ...others.map((id) => providerKeyChip(id, keys, keysVerified, KEY_NAME)),
2374
+ ...(keysVerified ? [] : [el('span', { class: 'plan-keys-note' },
2375
+ `Not checked β€” Brain could not load its provider catalog${catalogHealth && catalogHealth.detail ? ` (${catalogHealth.detail})` : ''}.`)]));
2355
2376
  const planBox = el('div', { class: 'plan-box' },
2356
2377
  el('span', { class: 'plan-label' }, 'Your plan', infoBtn('Your plan and OpenRouter', PROVIDERS_INFO)),
2357
2378
  head('is-house', houseName, action('Change', 'provider')),
package/console/style.css CHANGED
@@ -1236,6 +1236,11 @@ details.sub .sub-body > *:first-child { margin-top: 4px; }
1236
1236
  .plan-key-mark { font-weight: 700; }
1237
1237
  .plan-key.yes .plan-key-mark { color: var(--green-text); }
1238
1238
  .plan-key.no, .plan-key.no .plan-key-mark { color: var(--faint); }
1239
+ /* Not checked (issue #86): visually distinct from "no key found" so a degraded catalog never reads
1240
+ as a finding about the user's machine. */
1241
+ .plan-key.unknown { color: var(--muted); }
1242
+ .plan-key.unknown .plan-key-mark { color: var(--amber); }
1243
+ .plan-keys-note { flex-basis: 100%; color: var(--muted); font-size: 11.5px; line-height: 1.5; }
1239
1244
 
1240
1245
  /* WP2b β€” first-run empty state: confident, designed, not an apology */
1241
1246
  .mh-empty {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.0.8",
3
+ "version": "4.0.24",
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": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
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.8",
3
+ "description": "RuvNet brain transplant for Claude Code β€” grounds every RuvNet decision in real source across 71 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.24",
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.8",
3
+ "version": "4.0.24",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"