ruvnet-brain 4.3.39 β†’ 4.3.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
7
7
 
8
8
  # 🧠 RuvNet Brain
9
9
 
10
- ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 4.3.39 β€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.39-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)
10
+ ### 🧠 RuvNet Brain β€” [![RuvNet Brain version 4.3.40 β€” updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.40-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)
11
11
 
12
12
  **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.**
13
13
 
@@ -562,7 +562,7 @@ node forge-ask-all.mjs --dir . --q "How does RuVector implement HNSW vector sear
562
562
 
563
563
  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:
564
564
 
565
- - βœ… **The grounding brain is real and proven** β€” 199 public stores Β· 161,322 public source chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + explicit skills; automatic hooks retired), all re-runnable.
565
+ - βœ… **The grounding brain is real and proven** β€” 199 public stores Β· 161,365 public source chunks, dual embeddings, cross-encoder rerank, plugin (MCP tool + explicit skills; automatic hooks retired), all re-runnable.
566
566
  - βœ… **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).
567
567
  - βœ… **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).
568
568
  - ⚠️ **Two routing residuals** (above) β€” surfaced, not hidden.
package/bin/install.mjs CHANGED
@@ -102,8 +102,9 @@ const PACKAGE_VERSION = (() => {
102
102
  const REPO = 'stuinfla/ruvnet-brain';
103
103
  const RELEASE_API = `https://api.github.com/repos/${REPO}/releases/latest`;
104
104
  const ASSET_NAME = 'ruvnet-brain.zip';
105
- // Known-good BUNDLE tag, used when we can't reach GitHub (offline / rate-limited / no releases),
106
- // and by --pin. Default behavior is "get the latest Release"; this is only the safety net.
105
+ // Known-good BUNDLE tag, used ONLY by --pin. It is no longer a silent fallback for a failed
106
+ // latest-release lookup: that bundle predates ReleaseCoverage and cannot pass validation, so a lookup
107
+ // failure now stops with its real cause (resolveRelease / releaseLookupFailure).
107
108
  //
108
109
  // This MUST NOT be derived from this package's own version. The installer and the brain bundle are
109
110
  // two independent version streams (README: "Three independent things version separately here β€” by
@@ -299,7 +300,9 @@ function fetchJson(url, redirects = 0) {
299
300
  }
300
301
  if (statusCode !== 200) {
301
302
  res.resume();
302
- return reject(new Error(`GitHub API returned HTTP ${statusCode}`));
303
+ const reset = headers['x-ratelimit-remaining'] === '0' && Number(headers['x-ratelimit-reset']);
304
+ return reject(new Error(`GitHub API returned HTTP ${statusCode}${reset
305
+ ? ` (anonymous rate limit used up; it resets at ${new Date(reset * 1000).toISOString()})` : ''}`));
303
306
  }
304
307
  let body = '';
305
308
  res.setEncoding('utf8');
@@ -321,8 +324,8 @@ function fetchJson(url, redirects = 0) {
321
324
  // ── step: resolve which Release to download (latest by default; safe fallback) ───────────────────
322
325
  // Default behavior: ask GitHub for the LATEST Release and use its ruvnet-brain.zip asset.
323
326
  // --version <tag> forces a tag; --pin skips the network check and uses the bundled known-good tag.
324
- // Any failure (offline / rate-limited / no releases) FALLS BACK to the pinned known-good Release,
325
- // narrated clearly so the user knows exactly what happened.
327
+ // Any failure (offline / rate-limited / no releases) THROWS with the HTTP status or network error and
328
+ // a retry hint; callers that must download stop on it, the staleness check reports "could not check".
326
329
  /**
327
330
  * Which asset of a Release actually holds the brain bundle.
328
331
  *
@@ -387,12 +390,31 @@ async function resolveRelease() {
387
390
  ok(`latest Release is ${c.bold(tag)}`);
388
391
  return { tag, url, source: 'latest' };
389
392
  } catch (e) {
390
- warn(`couldn't check for the latest version (${e.message})`);
391
- info(`using the known-good ${c.bold(RELEASE_VERSION)} instead β€” the install is still safe and complete`);
392
- return { tag: RELEASE_VERSION, url: fallbackUrl(RELEASE_VERSION), source: 'fallback' };
393
+ // FAIL LOUD. This used to return the hardcoded RELEASE_VERSION and promise "the install is still
394
+ // safe and complete" β€” but that bundle predates ReleaseCoverage, so two steps later it failed
395
+ // validation with "COVERAGE.json is missing", and the reason (a rate limit, a network blip) was
396
+ // gone from the screen. Measured 2026-09-30 on the owner's recovery rail. Only --pin and
397
+ // --version choose a tag the lookup did not return.
398
+ const failure = releaseLookupFailure(e);
399
+ throw Object.assign(new Error(failure.message), { hint: failure.hint });
393
400
  }
394
401
  }
395
402
 
403
+ /** Why the latest-release lookup failed, and what to do about it. Pure, for testing. */
404
+ export function releaseLookupFailure(error) {
405
+ const detail = (error && error.message) || String(error);
406
+ const limited = /HTTP (?:403|429)\b/.test(detail);
407
+ return {
408
+ message: `couldn't look up the latest RuvNet Brain release (${detail}).`,
409
+ hint: [
410
+ limited
411
+ ? 'GitHub allows a limited number of anonymous release checks per hour from one network address. Wait until the reset time above, then re-run the same command.'
412
+ : 'Check your connection, then re-run the same command in a minute.',
413
+ `Nothing was downloaded or installed. To install one specific release instead: add --version <tag> (tags: https://github.com/${REPO}/releases).`,
414
+ ].join('\n'),
415
+ };
416
+ }
417
+
396
418
  // ── step: resolve the cache dir ──────────────────────────────────────────────────────────────────
397
419
  function resolveCacheDir() {
398
420
  const custom = process.env.RUVNET_BRAIN_KB;
@@ -3650,7 +3672,7 @@ async function runUpdate() {
3650
3672
  let fr;
3651
3673
  if (hasPrivateOverlay) {
3652
3674
  warn("\nthe installed updater failed; using authenticated staged recovery to preserve private stores…\n");
3653
- const release = await resolveRelease();
3675
+ const release = await resolveRelease().catch((error) => die(error.message, error.hint));
3654
3676
  const bundle = await obtainBundle(release);
3655
3677
  if (!bundle.zipPath) throw new Error('private-overlay recovery requires a downloadable signed bundle');
3656
3678
  const sigPath = `${bundle.zipPath}.sig`;
@@ -5333,8 +5355,9 @@ function showHelp() {
5333
5355
  console.log(`
5334
5356
  RuvNet Brain installer
5335
5357
 
5336
- By default this installs the LATEST published Release (it asks GitHub which one that is),
5337
- and falls back to a known-good version if GitHub can't be reached.
5358
+ By default this installs the LATEST published Release (it asks GitHub which one that is).
5359
+ If GitHub can't be reached or rate-limits the check, it STOPS with the reason and downloads
5360
+ nothing; re-run later, or pick a release yourself with --version <tag>.
5338
5361
 
5339
5362
  Usage:
5340
5363
  npx ruvnet-brain Install the brain + Claude Code plugin (recommended, npm)
@@ -5476,9 +5499,9 @@ the installer reports that boot-level declarations changed.
5476
5499
  installedTag = installedBrainVersion(cacheDir); // 'unknown' when SOURCE.json has no releaseTag
5477
5500
  try {
5478
5501
  resolvedRelease = await resolveRelease();
5479
- // ONLY a genuine `latest` lookup counts as "what current means". resolveRelease() does NOT
5480
- // throw when the GitHub API fails β€” it returns the hardcoded known-good pin with
5481
- // source:'fallback'. Treating that as latest inverts this whole fix: a rate-limited lookup
5502
+ // ONLY a genuine `latest` lookup counts as "what current means". A failed lookup now THROWS
5503
+ // (caught below: "could not check"); it used to return the hardcoded known-good pin with
5504
+ // source:'fallback'. Treating that as latest inverted this whole fix: a rate-limited lookup
5482
5505
  // would report installed v3.4.21-dev "β†’ latest v2.9.0" and DOWNGRADE a perfectly current
5483
5506
  // machine. (Caught by exercising the failure path against a 404 repo β€” the first version of
5484
5507
  // this fix did exactly that.) A pinned/forced resolution is likewise the operator's explicit
@@ -5541,7 +5564,8 @@ the installer reports that boot-level declarations changed.
5541
5564
  const localZipPresent =
5542
5565
  FLAG_LOCAL || fs.existsSync(path.join(REPO_ROOT, 'dist', 'ruvnet-brain.zip'));
5543
5566
  // Reuse the staleness check's resolution when it already ran β€” one network round-trip, not two.
5544
- const release = localZipPresent ? null : (resolvedRelease || await resolveRelease());
5567
+ const release = localZipPresent ? null
5568
+ : (resolvedRelease || await resolveRelease().catch((error) => die(error.message, error.hint)));
5545
5569
  const { zipPath, sourceDir, tmpDir, downloaded, sigError } = await obtainBundle(release);
5546
5570
  // Verify the Ed25519 signature BEFORE extracting a downloaded bundle into the user's config
5547
5571
  // (SEC-0010 #6 β€” trust root = the pubkey EMBEDDED in this file, so an attacker who swaps the
@@ -420,9 +420,13 @@ export async function applyVerifiedStagedRelease({
420
420
  if (runtime.brainVersion !== expectedRuntimeVersion) throw new Error('installed runtime identity differs from the approved recovery runtime');
421
421
  fs.copyFileSync(assertNoFollowPath(liveDir, runtimeIdentity),
422
422
  assertNoFollowPath(candidateDir, path.join(candidateDir, 'RUNTIME-IDENTITY.json')));
423
- const privateFence = path.join(liveDir, 'PRIVATE-STORES.json');
424
- if (fs.existsSync(privateFence)) fs.copyFileSync(privateFence,
425
- assertNoFollowPath(candidateDir, path.join(candidateDir, 'PRIVATE-STORES.json')));
423
+ // The candidate keeps the BUNDLE's fence; restorePrivateOverlayState adds the restored private
424
+ // names to it. Copying the LIVE fence over it imported every stale entry: measured 2026-09-30, the
425
+ // owner's live fence listed 108 stores, 100 of them public in 4.3.39, so the rail could only fail
426
+ // with "private/public store collision".
427
+ // Same carry main() does: without it the exact-tree swap deleted the live node_modules, and the
428
+ // recovered brain could no longer load its embedder (measured 2026-09-30 on the owner's copy).
429
+ carryLiveNodeModules({ candidateDir, liveDir });
426
430
  // S1: ONE APPLY PATH β€” the same helper main()'s normal apply uses, not a second, duplicated
427
431
  // copy-loop. It also adds the collision refusal main() previously lacked.
428
432
  restorePrivateFilesIntoCandidate({ candidateDir, sourceDir: liveDir, overlay });
@@ -658,23 +662,48 @@ export function restorePrivateOverlayState({ kbDir, overlay }) {
658
662
  }
659
663
  }
660
664
 
665
+ // The published capability-cards.md is a sealed input of the derived `concepts` store: its digest
666
+ // is in the release's concepts receipt, and the trusted validator re-hashes it on every candidate and
667
+ // live tree. So the public bytes are kept EXACTLY as extracted and private cards are APPENDED as
668
+ // whole sections after them β€” never re-serialized in between. Rejoining every section used to put
669
+ // private cards inside the hashed bytes, and every overlay install refused its own update with
670
+ // "derived concepts input receipt differs from capability-cards.md" (measured 2026-09-30).
661
671
  const publicCardsText = fs.existsSync(cardsFile) ? fs.readFileSync(cardsFile, 'utf8') : '';
662
672
  const publicCards = cardSections(publicCardsText);
673
+ const appendedCards = [];
674
+ // Case-folded, like the trusted validator (coverage-integrity derivedInputIdentity): an appended
675
+ // card whose heading folds onto a published one would fail the sealed-input check, so refuse it here
676
+ // with the collision named instead of writing a tree that cannot validate.
677
+ const publicFolded = new Set([...publicCards.keys()].map((name) => name.toLowerCase()));
663
678
  for (const [name, section] of Object.entries(overlay.cards || {})) {
664
- if (publicCards.has(name) && publicCards.get(name) !== section) {
665
- throw new Error(`capability-cards.md collision for private store ${name}`);
679
+ if (publicCards.has(name)) {
680
+ if (publicCards.get(name) !== section) throw new Error(`capability-cards.md collision for private store ${name}`);
681
+ continue; // already published verbatim
666
682
  }
667
- publicCards.set(name, section);
683
+ if (publicFolded.has(name.toLowerCase())) throw new Error(`capability-cards.md collision for private store ${name}`);
684
+ appendedCards.push(section);
668
685
  }
669
- const preambleEnd = publicCardsText.search(/^## /m);
670
- const preamble = preambleEnd >= 0 ? publicCardsText.slice(0, preambleEnd).trimEnd() : publicCardsText.trimEnd();
671
- const mergedCards = `${preamble}${preamble ? '\n\n' : ''}${[...publicCards.values()].join('\n\n')}\n`;
686
+ const separator = !publicCardsText ? '' : publicCardsText.endsWith('\n') ? '\n' : '\n\n';
687
+ const mergedCards = appendedCards.length
688
+ ? `${publicCardsText}${separator}${appendedCards.join('\n\n')}\n` : publicCardsText;
689
+
690
+ // The candidate's PRIVATE-STORES.json is the PUBLIC bundle's fence. A restored private store the
691
+ // bundle does not fence (a local ingest, or a store the bundle never knew) would leave the runtime
692
+ // ledger with "unclassified stores", so the restored names are added β€” never removed, and the file
693
+ // is untouched when the bundle already fences them all (a byte-identical re-apply stays a no-op).
694
+ const fenceFile = path.join(kbDir, 'PRIVATE-STORES.json');
695
+ const fence = fs.existsSync(fenceFile) ? JSON.parse(fs.readFileSync(fenceFile, 'utf8')) : { privateStores: [] };
696
+ const fenced = new Set((Array.isArray(fence.privateStores) ? fence.privateStores : []).map((name) => String(name).toLowerCase()));
697
+ const unfenced = Object.keys(overlay.sourceStores || {}).filter((name) => !fenced.has(name.toLowerCase()));
672
698
 
673
699
  atomicJson(sourceFile, { ...source, stores: mergedSource });
674
700
  atomicJson(generationsFile, { ...generations, stores: mergedGenerations });
675
701
  atomicJson(aliasesFile, mergedAliases);
676
- fs.writeFileSync(`${cardsFile}.tmp-${process.pid}`, mergedCards);
677
- fs.renameSync(`${cardsFile}.tmp-${process.pid}`, cardsFile);
702
+ if (unfenced.length) atomicJson(fenceFile, { ...fence, privateStores: [...(fence.privateStores || []), ...unfenced] });
703
+ if (mergedCards !== publicCardsText) {
704
+ fs.writeFileSync(`${cardsFile}.tmp-${process.pid}`, mergedCards);
705
+ fs.renameSync(`${cardsFile}.tmp-${process.pid}`, cardsFile);
706
+ }
678
707
  return { restored: Object.keys(overlay.sourceStores).length };
679
708
  }
680
709
 
@@ -702,6 +731,20 @@ export function restorePrivateOverlayState({ kbDir, overlay }) {
702
731
  * copying anything is the assertion `applyPublicBundlePreservingPrivate` had and the real path did
703
732
  * not; it is preserved here rather than dropped.
704
733
  */
734
+ /**
735
+ * node_modules (the ONNX embedder and RVF readers) is installer-placed and never ships inside a
736
+ * bundle. The candidate is validated in a SIBLING directory with no parent node_modules, and the
737
+ * exact-tree swap would delete it from the live KB, so both apply paths carry the LIVE copy across.
738
+ * Reflink clone where the filesystem supports it (APFS/btrfs), plain copy otherwise.
739
+ */
740
+ export function carryLiveNodeModules({ candidateDir, liveDir }) {
741
+ const liveModules = path.join(liveDir, 'node_modules');
742
+ if (!fs.existsSync(liveModules) || fs.existsSync(path.join(candidateDir, 'node_modules'))) return false;
743
+ fs.cpSync(assertNoFollowPath(liveDir, liveModules), path.join(candidateDir, 'node_modules'),
744
+ { recursive: true, verbatimSymlinks: true, mode: fs.constants.COPYFILE_FICLONE });
745
+ return true;
746
+ }
747
+
705
748
  export function restorePrivateFilesIntoCandidate({ candidateDir, sourceDir, overlay }) {
706
749
  if (!overlay) return { restored: 0 };
707
750
  for (const relative of Object.keys(overlay.files || {})) {
@@ -720,7 +763,9 @@ export function restorePrivateFilesIntoCandidate({ candidateDir, sourceDir, over
720
763
  }
721
764
 
722
765
  const manifestUrl = source.canonicalManifestUrl || stores.find((s) => s.canonicalManifestUrl)?.canonicalManifestUrl;
723
- if (!manifestUrl) {
766
+ // The recovery rail runs from the npm package's kb/forge-update.mjs, and the package ships no
767
+ // kb/SOURCE.json (verified in ruvnet-brain-4.3.39.tgz), so this die() stopped the rail before it began.
768
+ if (!manifestUrl && !STAGED_RELEASE_FILE) {
724
769
  die(`self-update not configured for this build β€” SOURCE.json has no canonicalManifestUrl ` +
725
770
  `(forge-build.mjs was run without --canonical-url). Provenance is still in SOURCE.json.`);
726
771
  }
@@ -1692,6 +1737,28 @@ async function main() {
1692
1737
  die(`staged ReleaseCoverage failed integrity: ${stagedCoverage.failures.join('; ')} β€” local files untouched.`);
1693
1738
  }
1694
1739
 
1740
+ // A release may RETIRE a store this brain still lists. Measured 2026-09-30: 4.3.39 no longer ships
1741
+ // agentic-flows/agentic-music (excluded-no-corpus) or cogs/support under those names, and guarding
1742
+ // them could only fail ("[FAIL] MISSING file: agentic-flows.rvf"), so no brain listing them could
1743
+ // ever update. The release coverage validated above is the authority on what ships; only the stores
1744
+ // the staged bundle declares are guarded and read back, and the retired ones are named, not dropped
1745
+ // silently. (A legacy flat SOURCE.json has no store name to compare and is always guarded.)
1746
+ let stagedStoreNames;
1747
+ try {
1748
+ const staged = JSON.parse(fs.readFileSync(path.join(extractDir, 'SOURCE.json'), 'utf8')).stores || {};
1749
+ stagedStoreNames = new Set(Array.isArray(staged) ? staged.map((s) => s?.kbName) : Object.keys(staged));
1750
+ } catch (error) {
1751
+ fs.rmSync(tmp, { recursive: true, force: true });
1752
+ die(`staged SOURCE.json is unreadable: ${error.message} β€” local files untouched.`);
1753
+ }
1754
+ const landingTargets = resolvedTargets.filter(({ local }) => local.kbName == null || stagedStoreNames.has(local.kbName));
1755
+ const retiredStores = resolvedTargets.filter((target) => !landingTargets.includes(target)).map(({ local }) => local.kbName);
1756
+ if (!landingTargets.length) {
1757
+ fs.rmSync(tmp, { recursive: true, force: true });
1758
+ die(`release ${canonTag || canonLabel} ships none of the selected stores (${retiredStores.join(', ')}) β€” local files untouched.`);
1759
+ }
1760
+ if (retiredStores.length) console.log(`\n retired by this release (no longer shipped): ${retiredStores.join(', ')}`);
1761
+
1695
1762
  const privateNames = Object.keys(privateOverlay?.sourceStores || {});
1696
1763
  let profileResult = null;
1697
1764
  const finalVerificationByStore = new Map();
@@ -1703,7 +1770,7 @@ async function main() {
1703
1770
  const guard = path.join(dir, 'forge-guard.mjs');
1704
1771
  if (!fs.existsSync(guard)) return { valid: false, failures: ['forge-guard.mjs is missing'] };
1705
1772
  try {
1706
- for (const { local, resolved: storeResolution } of resolvedTargets) {
1773
+ for (const { local, resolved: storeResolution } of landingTargets) {
1707
1774
  execFileSync(process.execPath, [guard, '--dir', dir, '--name', local.kbName], { cwd: dir, stdio: 'pipe' });
1708
1775
  const verified = verifyLanded({ kbDir: dir, kbName: local.kbName, before: local, beforeBundle: source,
1709
1776
  expectedDigest: storeResolution.digest, downloadedBuffer: buf });
@@ -1747,16 +1814,8 @@ async function main() {
1747
1814
  fs.copyFileSync(assertNoFollowPath(liveDir, liveRuntimeIdentity),
1748
1815
  assertNoFollowPath(candidateDir, path.join(candidateDir, 'RUNTIME-IDENTITY.json')));
1749
1816
  }
1750
- // node_modules (the ONNX embedder and RVF readers) is installer-placed and never ships inside a
1751
- // bundle, the same class as the two files above. The candidate is validated by forge-guard in a
1752
- // SIBLING directory with no parent node_modules, so without it the guard cannot load the
1753
- // embedder and every apply fails; and the exact-tree swap would delete it from the live KB.
1754
- // Reflink clone where the filesystem supports it (APFS/btrfs), plain copy otherwise.
1755
- const liveModules = path.join(liveDir, 'node_modules');
1756
- if (fs.existsSync(liveModules) && !fs.existsSync(path.join(candidateDir, 'node_modules'))) {
1757
- fs.cpSync(assertNoFollowPath(liveDir, liveModules), path.join(candidateDir, 'node_modules'),
1758
- { recursive: true, verbatimSymlinks: true, mode: fs.constants.COPYFILE_FICLONE });
1759
- }
1817
+ // node_modules: the same installer-owned class as the two files above (see carryLiveNodeModules).
1818
+ carryLiveNodeModules({ candidateDir, liveDir });
1760
1819
  restorePrivateFilesIntoCandidate({ candidateDir, sourceDir: liveDir, overlay: privateOverlay });
1761
1820
  // ATOMIC WITH INSTALLATION, not after it. The transport identity is written INTO the
1762
1821
  // candidate, so the storage transaction's single rename either promotes the bytes AND the
@@ -1796,7 +1855,7 @@ async function main() {
1796
1855
  process.exitCode = 12;
1797
1856
  return;
1798
1857
  }
1799
- for (const { local, resolved: storeResolution } of resolvedTargets) {
1858
+ for (const { local, resolved: storeResolution } of landingTargets) {
1800
1859
  const verified = finalVerificationByStore.get(local.kbName)
1801
1860
  || verifyLanded({ kbDir: KB_DIR, kbName: local.kbName, before: local, beforeBundle: source,
1802
1861
  expectedDigest: storeResolution.digest, downloadedBuffer: buf });
@@ -1842,7 +1901,8 @@ async function main() {
1842
1901
  ? `\n=== DONE β€” exact no-op; installed bytes already equal the validated candidate ===`
1843
1902
  : `\n=== DONE β€” ${behindStores.length} store(s) updated ===`);
1844
1903
  console.log(`resolved target (live manifest, checked BEFORE downloading): ${canonLabel}`);
1845
- for (const { local } of behindStores) {
1904
+ if (retiredStores.length) console.log(`retired by this release (no longer shipped): ${retiredStores.join(', ')}`);
1905
+ for (const { local } of landingTargets) {
1846
1906
  const r = landedByStore.get(local.kbName);
1847
1907
  const l = r.landed;
1848
1908
  console.log(`[${local.kbName}] SOURCE.json on disk now reads: built ${l.builtUtc || '?'} from ${short(l.sourceCommit)}${l.sourceDescribe ? ` (${l.sourceDescribe})` : ''}`);
@@ -1855,7 +1915,7 @@ async function main() {
1855
1915
  console.log(`\n${unchangedStores.length} of ${behindStores.length} store(s) were already at the canonical build and did not change: ${unchangedStores.join(', ')}`);
1856
1916
  console.log(` (stores are forged independently β€” an unchanged store means its upstream repo did not move, not a failed update.)`);
1857
1917
  }
1858
- const finalOutcome = writeUpdateOutcome({ terminalVerdict: transaction.terminalVerdict, storeCount: behindStores.length,
1918
+ const finalOutcome = writeUpdateOutcome({ terminalVerdict: transaction.terminalVerdict, storeCount: behindStores.length, retiredStores,
1859
1919
  currencyVerdict: verdict.verdict, currencyReason: verdict.reason, candidateKind: candidateIdentity.kind,
1860
1920
  storageDelta: transaction.storageDelta,
1861
1921
  transactionReceipts: transaction.paths.receipts,
@@ -96,21 +96,27 @@ export function managedStorageInventory(liveDir, { measuredAt = new Date().toISO
96
96
  const basename = path.basename(live);
97
97
  const fullCorpusCopies = [];
98
98
  const evidence = [];
99
+ const evidenceNames = [`.${basename}.update-transactions`, 'refresh-runs'];
99
100
  for (const name of fs.readdirSync(parent).sort()) {
100
101
  const file = path.join(parent, name);
101
- const stat = fs.lstatSync(file);
102
102
  const installerKind = name.startsWith(`${basename}.install-preserved-`) ? 'installer-preserved'
103
103
  : name.startsWith(`${basename}.install-prior-`) ? 'installer-prior'
104
104
  : name.startsWith(`.${basename}.install-stage-`) ? 'installer-stage' : null;
105
- if (installerKind) {
106
- fullCorpusCopies.push(observedInstallerSummary(file, installerKind));
107
- continue;
108
- }
109
105
  const kind = name === basename ? 'active'
110
106
  : name.startsWith(`${basename}.next-`) ? 'candidate'
111
107
  : name.startsWith(`${basename}.rollback-`) ? 'rollback'
112
108
  : name.startsWith(`${basename}.failed-`) ? 'failed'
113
109
  : name.startsWith(`${basename}.bak-`) ? 'backup' : null;
110
+ // Classify by NAME first and lstat only managed entries. The parent (e.g. ~/.cache/ruvnet-brain)
111
+ // also holds hook stamps and logs created and deleted constantly; one vanishing between readdir
112
+ // and lstat threw ENOENT and aborted the whole update. A managed entry that vanished is absent.
113
+ if (!installerKind && !kind && !evidenceNames.includes(name)) continue;
114
+ let stat;
115
+ try { stat = fs.lstatSync(file); } catch (error) { if (error?.code === 'ENOENT') continue; throw error; }
116
+ if (installerKind) {
117
+ fullCorpusCopies.push(observedInstallerSummary(file, installerKind));
118
+ continue;
119
+ }
114
120
  if (kind) {
115
121
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`managed ${kind} entry is not a trusted directory: ${file}`);
116
122
  fullCorpusCopies.push(trustedTreeSummary(file, kind));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.39",
3
+ "version": "4.3.40",
4
4
  "description": "One-command installer for RuvNet Brain \u2014 a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 77 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 a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
4
- "version": "4.3.39",
4
+ "version": "4.3.40",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.39",
3
+ "version": "4.3.40",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -182,6 +182,54 @@ function evidenceIdentity(root, file, kind) {
182
182
  };
183
183
  }
184
184
 
185
+ /**
186
+ * The ONE derived input a private overlay legitimately extends on an installed brain.
187
+ * kb/forge-update.mjs's restorePrivateOverlayState keeps the published capability-cards.md bytes
188
+ * verbatim and APPENDS the user's private `## <store>` cards after them, so card-lane and every other
189
+ * reader keep a single file. Hashing the whole file made every overlay install fail this check
190
+ * ("derived concepts input receipt differs from capability-cards.md", measured 2026-09-30 on the
191
+ * owner's 4.3.39 update) β€” the published digest can never cover bytes the user added locally.
192
+ */
193
+ export const OVERLAY_EXTENSIBLE_DERIVED_INPUTS = Object.freeze(['capability-cards.md']);
194
+
195
+ /**
196
+ * Identity of a derived input as the release sealed it. Exact file match first. For an
197
+ * overlay-extensible input, accept ONLY a file whose leading bytes, cut at a line boundary, hash to
198
+ * the sealed digest AND whose remainder is nothing but whole `## ` card sections β€” so every published
199
+ * card still parses to exactly its published body, and nothing local can be glued onto one. The
200
+ * evidence row records the sealed prefix (digest + byte count), which is what the release's partition
201
+ * digest was computed over. Returns null when the sealed bytes are not present.
202
+ */
203
+ function derivedInputIdentity(root, file, input) {
204
+ const relative = path.relative(root, file).split(path.sep).join('/');
205
+ const bytes = fs.readFileSync(file);
206
+ const whole = crypto.createHash('sha256').update(bytes).digest('hex');
207
+ if (whole === input.sha256) return { kind: 'derived-input', path: relative, sha256: whole, bytes: bytes.length };
208
+ if (!OVERLAY_EXTENSIBLE_DERIVED_INPUTS.includes(relative)) return null;
209
+ const hash = crypto.createHash('sha256');
210
+ let consumed = 0;
211
+ // An appended section may only ADD a card. One that repeats a sealed `## ` heading (another copy,
212
+ // a case variant, or an empty one) would be served by the card lane as curated evidence for that
213
+ // PUBLIC product, so it is refused, compared trimmed and case-insensitively.
214
+ const headings = (text) => [...text.matchAll(/^## ([^\n]*)$/gm)].map((m) => m[1].trim().toLowerCase());
215
+ const matchesAt = (end) => {
216
+ hash.update(bytes.subarray(consumed, end));
217
+ consumed = end;
218
+ if (hash.copy().digest('hex') !== input.sha256) return false;
219
+ const rest = bytes.subarray(end).toString('utf8');
220
+ if (!/^\n*## \S/.test(rest)) return false;
221
+ const sealed = new Set(headings(bytes.subarray(0, end).toString('utf8')));
222
+ return !headings(rest).some((heading) => sealed.has(heading));
223
+ };
224
+ for (let index = bytes.indexOf(0x0a); index >= 0; index = bytes.indexOf(0x0a, index + 1)) {
225
+ // Both sides of every newline: a published file that ends without one is still a clean prefix.
226
+ for (const end of [index, index + 1]) {
227
+ if (matchesAt(end)) return { kind: 'derived-input', path: relative, sha256: input.sha256, bytes: end };
228
+ }
229
+ }
230
+ return null;
231
+ }
232
+
185
233
  const canonicalStores = (root) => {
186
234
  const stores = [];
187
235
  for (const name of fs.readdirSync(root).filter((entry) => !entry.startsWith('._'))) {
@@ -341,10 +389,9 @@ export function validatePublicInventory({ assetsDir, coverage, ledger, installed
341
389
  for (const input of receipt.inputs) {
342
390
  let inputFile = null;
343
391
  try { inputFile = containedRegular(root, input?.path, `derived ${store} input`); } catch { /* handled below */ }
344
- if (!inputFile || !HEX64.test(String(input.sha256 || '')) || input.sha256 !== sha256File(inputFile)) {
345
- throw new Error(`derived ${store} input receipt differs from ${input?.path || '(missing)'}`);
346
- }
347
- evidenceFiles.push(evidenceIdentity(root, inputFile, 'derived-input'));
392
+ const identity = inputFile && HEX64.test(String(input.sha256 || '')) ? derivedInputIdentity(root, inputFile, input) : null;
393
+ if (!identity) throw new Error(`derived ${store} input receipt differs from ${input?.path || '(missing)'}`);
394
+ evidenceFiles.push(identity);
348
395
  }
349
396
  evidenceFiles.push(evidenceIdentity(root, passages, 'derived-passages'));
350
397
  evidenceFiles.push(evidenceIdentity(root, receiptFile, 'derived-receipt'));