You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
npx ruvnet-brain@4.3.28 --update runs the kb/forge-update.mjs that is already installed in the KB. Before spawning it, ensureUpdaterPrerequisites() replaces coverage-integrity.mjs with the package copy, but it never replaces the updater. The npm package does not ship kb/forge-update.mjs, so there is nothing to replace it from.
A KB installed at 4.3.22 therefore keeps running the 4.3.22 updater. That updater's private-overlay preflight walks the KB in strict mode and rejects npm's ordinary node_modules/.bin/semver symlink: private overlay preflight failed: symbolic link is not a governed regular file: node_modules/.bin/semver — refusing to update.
The walker fix already exists (9fda9e1, 2026-09-12, first released in v4.3.25), but this path can never deliver it. If the fallback is allowed, the installer re-runs itself with --force. That fresh install downloads and extracts the full bundle, then refuses activation because the KB has a private overlay, and tells the user to run --update. A private-overlay KB installed at any v4.3.x release through 4.3.22 has no supported way to upgrade. Every such tag carries the strict walk, and there are no v4.3.23 or v4.3.24 tags.
The same failure has been described on a real installation in pacphi/agentic-kit#237, together with a hand-applied local workaround. This issue isolates the install-contract mechanism and gives a disposable repro.
~/.claude/plugins/installed_plugins.json; ~/.claude/plugins/cache/ruvnet-brain/ruvnet-brain/4.3.28/.claude-plugin/plugin.json (older cache dirs 4.3.21 and 4.3.26 also present)
This machine's own KB is at 4.3.28, so the live install is not affected. The repro below builds a minimal stuck KB in a disposable HOME.
Steps to reproduce
This was run as shown. It builds a synthetic KB in a disposable HOME, using the real v4.3.22 updater modules taken from the tag. The KB has:
one public store that is behind releases/latest
one private overlay store (updateManaged: false)
the npm .bin symlink that every KB with node_modules has
Then it runs the published 4.3.28 installer's --update with the fallback disabled, so no bundle is downloaded. The only network traffic is the release-manifest JSON and the npm package.
Output (installer banner lines omitted; the temp directory is shown as <tmp>):
before: forge-update.mjs d565985c98fd4dda, coverage-integrity.mjs absent
brain dir: <tmp>/brain-br3.VmkHTo/home/.cache/ruvnet-brain/kb
trusted coverage validator placed beside the updater
running the bundle's own self-updater (backs up first, re-verifies, never half-applies)…
=== rvf-kb-forge evergreen check ===
canonical manifest: https://api.github.com/repos/stuinfla/ruvnet-brain/releases/latest
canonical built: v4.3.28 (published 2026-09-21T04:17:53Z)
[ruvector] BEHIND
canonical: built 2026-09-21T04:17:53Z from (none) (v4.3.28)
yours: built 2026-09-12T00:00:00Z from (none)
[forge-update] ERROR: private overlay preflight failed: symbolic link is not a governed regular file: node_modules/.bin/semver — refusing to update.
exit=1
after: forge-update.mjs d565985c98fd4dda, coverage-integrity.mjs 77e24ef89cb93994
preflight throws <- updater sha d565985c98fd4dda: symbolic link is not a governed regular file: node_modules/.bin/semver
preflight OK <- updater sha 40ee42ab9572c1e6
What the hashes show:
d565985c… is kb/forge-update.mjs at tag v4.3.22. It is unchanged after --update.
77e24ef8… is plugin/scripts/coverage-integrity.mjs from the 4.3.28 npm package. It was placed during --update.
40ee42ab… is kb/forge-update.mjs at tag v4.3.28, byte-identical to this machine's live 4.3.28 KB. Its preflight passes on the same KB.
The npm package installed by npx in the sandbox contains no kb/forge-update.mjs.
After the run, the KB also holds a new RUNTIME-IDENTITY.json. It is stamped "brainVersion": "4.3.28" and lists only coverage-integrity.mjs under executables. SOURCE.json still says 4.3.22.
Not run: the default path without RUVNET_BRAIN_NO_UPDATE_FALLBACK. It downloads and extracts the full release bundle before refusing. That behaviour is described from the code under Evidence.
Expected vs actual
Expected. The 4.3.28 installer's --update runs an updater at least as new as the installer, or refreshes or verifies it first, as it already does for the coverage validator. The 4.3.25+ walker fix then reaches the KB, the private store is preserved, and the KB updates.
Actual. The stale 4.3.22 updater runs unmodified and dies at the private-overlay preflight with exit 1. The only file refreshed from the package is coverage-integrity.mjs, which is recorded in a new RUNTIME-IDENTITY.json stamped 4.3.28. The same preflight in the 4.3.28 updater passes on the same KB. With the fallback allowed, the next step is a fresh install that downloads the full bundle, then refuses, and points back at --update.
Evidence
Installed installer, ruvnet-brain 4.3.28 (bin/install.mjs from the npx cache; byte-identical to main at e89ea1b and to release/4.3.29):
:520-528: the reason the validator must come from the package: "The bundle cannot be the source: the validator vets the bundle, so it must come from the signed npm package." The comment also records that on 2026-09-12 "every 4.3.21 --update — the nightly included — died in the updater and fell back to a fresh install, which a private-overlay brain refuses."
:559-562: ensureUpdaterPrerequisites() places only coverage-integrity.mjs. The updater's sibling modules are not placed either: after the run, the KB still had the v4.3.22 update-storage-transaction.mjs, which differs from the package copy.
package.jsonfiles (same on main) ships the updater's sibling modules under kb/: zip-extract.mjs, brain-profile.mjs, refresh-run.mjs, update-storage-transaction.mjs, lifecycle-evidence-retention.mjs, corpus-release-identity.mjs and others. It does not ship kb/forge-update.mjs.
:3509-3536: runUpdate() checks that kbDir/forge-update.mjs exists, then spawns it from the KB with --apply.
:3566-3571: the fallback runs spawnSync(process.execPath, [self, '--force']). It passes only --force, so --no-telemetry and --no-nightly-prompt given to --update are not carried into the fresh install.
:675-682: the fresh-install refusal sits inside unzipInto(), after the bundle has been downloaded and extracted to a staging directory. The message is fresh-install activation refused because this brain contains a private overlay / Run npx ruvnet-brain --update so the bundle updater preserves those private stores.
Updater at tag v4.3.22 (kb/forge-update.mjs):
:338-353: relativeFiles() throws on any symlink when strict is on, which is the default.
:356: capturePrivateOverlayState().
:409: relativeFiles(kbDir), which uses the strict default.
:1173-1174: the preflight die('private overlay preflight failed: …').
Updater at tag v4.3.28 (kb/forge-update.mjs), identical to the live KB here:
:435-438: relativeFiles(kbDir, '', { strict: false }). The comment dates the incident to 2026-09-12.
History:
9fda9e1 (2026-09-12), "forge-update: private-overlay capture walks the KB root as an inventory, not a governed payload". gh api repos/stuinfla/ruvnet-brain/compare/9fda9e1b6a80...<tag> shows that v4.3.21 and v4.3.22 do not contain it, and that v4.3.25, v4.3.26 and v4.3.28 do.
5f919c5 (2026-09-12), "install: place the trusted coverage validator beside the updater; forge-update carries it forward". It added the validator placement, but not an updater refresh.
RuvNet Brain corpus: kb/forge-update.mjs and bin/install.mjs were not surfaced by search_ruvnet (receipt c6d43d08016b). The paths above come from the installed package and GitHub tags.
Impact
A KB installed at any v4.3.x release through 4.3.22 that has one or more private stores (updateManaged: false) cannot upgrade through any supported command:
--update, which the nightly also runs, dies in the stale preflight every time.
--force, run directly or through the fallback, pays a full bundle download and extraction before it refuses.
The only way out today is to patch the installed updater by hand. That is what happened on the affected installation.
Suggested direction
Refresh the updater the way the validator is refreshed. Ship kb/forge-update.mjs in the npm package; its sibling modules already ship under kb/. Before spawning the updater, have ensureUpdaterPrerequisites() place or verify them whenever the installed copy is older than the package. The argument in the validator comment at :520-528 applies equally to the updater.
Or, when the installed updater fails on a KB with private stores, run the package's own updater logic against the KB. Falling back to a fresh install does not work here: a private-overlay KB always refuses it.
Move the fresh-install private-overlay check before the download. Stop pointing its refusal back at --update when --update is the step that just failed.
Carry the caller's opt-out flags into the --force fallback.
A duplicate search found no existing report of the stale-updater mechanism. It covered open and closed issues and PRs for forge-update, private overlay, private overlay preflight, governed regular file symlink, fresh-install activation refused, stale install trap, installed updater older, update stale updater, self-updater stale refresh updater and semver.
Summary
npx ruvnet-brain@4.3.28 --updateruns thekb/forge-update.mjsthat is already installed in the KB. Before spawning it,ensureUpdaterPrerequisites()replacescoverage-integrity.mjswith the package copy, but it never replaces the updater. The npm package does not shipkb/forge-update.mjs, so there is nothing to replace it from.A KB installed at 4.3.22 therefore keeps running the 4.3.22 updater. That updater's private-overlay preflight walks the KB in strict mode and rejects npm's ordinary
node_modules/.bin/semversymlink:private overlay preflight failed: symbolic link is not a governed regular file: node_modules/.bin/semver — refusing to update.The walker fix already exists (9fda9e1, 2026-09-12, first released in v4.3.25), but this path can never deliver it. If the fallback is allowed, the installer re-runs itself with
--force. That fresh install downloads and extracts the full bundle, then refuses activation because the KB has a private overlay, and tells the user to run--update. A private-overlay KB installed at any v4.3.x release through 4.3.22 has no supported way to upgrade. Every such tag carries the strict walk, and there are no v4.3.23 or v4.3.24 tags.The same failure has been described on a real installation in pacphi/agentic-kit#237, together with a hand-applied local workaround. This issue isolates the install-contract mechanism and gives a disposable repro.
Environment
sw_vers,uname -runame -mnode --versionnpm --versionpnpm --versionlatest3.45.0)<npm-root-g>/ruflo/package.json;ruflo --version→ruflo v3.45.0latest3.45.0)<npm-root-g>/ruflo/node_modules/@claude-flow/cli/package.json(not installed globally on its own)latest3.0.0-alpha.26)<npm-root-g>/ruflo/node_modules/@claude-flow/memory/package.json; nested copy under@claude-flow/cliis also 3.0.0-alpha.25latest3.0.0-alpha.20)<npm-root-g>/ruflo/node_modules/agentdb/package.json; nested copies under@claude-flow/cliand@claude-flow/memoryare also 3.0.0-alpha.20<npm-root-g>/agentdb/package.json;agentdb --version→agentdb v3.0.0-alpha.17<npm-root-g>/ruflo/node_modules/ruvector/package.json(same version nested under@claude-flow/cli,@claude-flow/memory,agentdb)latest0.3.3)<npm-root-g>/ruvector/package.json;ruvector --version→0.3.3latest3.14.3)<npm-root-g>/agentic-qe/package.json;aqe --version→3.14.3~/.claude/plugins/installed_plugins.json;~/.claude/plugins/cache/ruvnet-brain/ruvnet-brain/4.3.28/.claude-plugin/plugin.json(older cache dirs 4.3.21 and 4.3.26 also present)~/.cache/ruvnet-brain/kb/SOURCE.jsonclaude --version;~/.local/bin/claude→~/.local/share/claude/versions/2.1.283codex --version;<npm-root-g>/@openai/codex/package.json<npm-root-g>/@pacphi/agentic-kit/package.json;ak --versiongit rev-parse HEAD<npm-root-g>=~/.local/share/mise/installs/node/26.4.0/lib/node_modules(npm root -g). npmlatestdist-tags read withnpm view <pkg> dist-tags. Captured 2026-09-26.This machine's own KB is at 4.3.28, so the live install is not affected. The repro below builds a minimal stuck KB in a disposable HOME.
Steps to reproduce
This was run as shown. It builds a synthetic KB in a disposable HOME, using the real v4.3.22 updater modules taken from the tag. The KB has:
releases/latestupdateManaged: false).binsymlink that every KB withnode_moduleshasThen it runs the published 4.3.28 installer's
--updatewith the fallback disabled, so no bundle is downloaded. The only network traffic is the release-manifest JSON and the npm package.Output (installer banner lines omitted; the temp directory is shown as
<tmp>):What the hashes show:
d565985c…iskb/forge-update.mjsat tag v4.3.22. It is unchanged after--update.77e24ef8…isplugin/scripts/coverage-integrity.mjsfrom the 4.3.28 npm package. It was placed during--update.40ee42ab…iskb/forge-update.mjsat tag v4.3.28, byte-identical to this machine's live 4.3.28 KB. Its preflight passes on the same KB.npxin the sandbox contains nokb/forge-update.mjs.RUNTIME-IDENTITY.json. It is stamped"brainVersion": "4.3.28"and lists onlycoverage-integrity.mjsunderexecutables.SOURCE.jsonstill says 4.3.22.Not run: the default path without
RUVNET_BRAIN_NO_UPDATE_FALLBACK. It downloads and extracts the full release bundle before refusing. That behaviour is described from the code under Evidence.Expected vs actual
Expected. The 4.3.28 installer's
--updateruns an updater at least as new as the installer, or refreshes or verifies it first, as it already does for the coverage validator. The 4.3.25+ walker fix then reaches the KB, the private store is preserved, and the KB updates.Actual. The stale 4.3.22 updater runs unmodified and dies at the private-overlay preflight with exit 1. The only file refreshed from the package is
coverage-integrity.mjs, which is recorded in a newRUNTIME-IDENTITY.jsonstamped 4.3.28. The same preflight in the 4.3.28 updater passes on the same KB. With the fallback allowed, the next step is a fresh install that downloads the full bundle, then refuses, and points back at--update.Evidence
Installed installer, ruvnet-brain 4.3.28 (
bin/install.mjsfrom the npx cache; byte-identical tomainat e89ea1b and torelease/4.3.29):--update— the nightly included — died in the updater and fell back to a fresh install, which a private-overlay brain refuses."ensureUpdaterPrerequisites()places onlycoverage-integrity.mjs. The updater's sibling modules are not placed either: after the run, the KB still had the v4.3.22update-storage-transaction.mjs, which differs from the package copy.package.jsonfiles(same onmain) ships the updater's sibling modules underkb/:zip-extract.mjs,brain-profile.mjs,refresh-run.mjs,update-storage-transaction.mjs,lifecycle-evidence-retention.mjs,corpus-release-identity.mjsand others. It does not shipkb/forge-update.mjs.runUpdate()checks thatkbDir/forge-update.mjsexists, then spawns it from the KB with--apply.spawnSync(process.execPath, [self, '--force']). It passes only--force, so--no-telemetryand--no-nightly-promptgiven to--updateare not carried into the fresh install.unzipInto(), after the bundle has been downloaded and extracted to a staging directory. The message isfresh-install activation refused because this brain contains a private overlay/Run npx ruvnet-brain --update so the bundle updater preserves those private stores.Updater at tag v4.3.22 (
kb/forge-update.mjs):relativeFiles()throws on any symlink whenstrictis on, which is the default.capturePrivateOverlayState().relativeFiles(kbDir), which uses the strict default.die('private overlay preflight failed: …').Updater at tag v4.3.28 (
kb/forge-update.mjs), identical to the live KB here:relativeFiles(kbDir, '', { strict: false }). The comment dates the incident to 2026-09-12.History:
gh api repos/stuinfla/ruvnet-brain/compare/9fda9e1b6a80...<tag>shows that v4.3.21 and v4.3.22 do not contain it, and that v4.3.25, v4.3.26 and v4.3.28 do.RuvNet Brain corpus:
kb/forge-update.mjsandbin/install.mjswere not surfaced bysearch_ruvnet(receiptc6d43d08016b). The paths above come from the installed package and GitHub tags.Impact
updateManaged: false) cannot upgrade through any supported command:--update, which the nightly also runs, dies in the stale preflight every time.--force, run directly or through the fallback, pays a full bundle download and extraction before it refuses.--updatewould stop at this stale preflight instead.Suggested direction
kb/forge-update.mjsin the npm package; its sibling modules already ship underkb/. Before spawning the updater, haveensureUpdaterPrerequisites()place or verify them whenever the installed copy is older than the package. The argument in the validator comment at :520-528 applies equally to the updater.--updatewhen--updateis the step that just failed.--forcefallback.Related
node_modules/.bin/semversymlink brokereclaimBackups(). Commit ab3fec9 added the non-strict walk for reclaim only. The private-overlay capture kept the strict walk until 9fda9e1.--updateand stale-install traps.forge-update,private overlay,private overlay preflight,governed regular file symlink,fresh-install activation refused,stale install trap,installed updater older,update stale updater,self-updater stale refresh updaterandsemver.