v4.11.0
Warning
ADVISORY (added 2026-08-20) — read before upgrading.
If audit/managed-content-manifest.json is missing from your install, do not run
hooks/local/upgrade.sh. It half-applies: new files arrive, the engine that calls them does not,
VERSION advances anyway, and the run writes upstream's manifest in as your base — after which
your stale files are reported as your own edits and the tree freezes.
If you have already run it from that state: do not re-run it, do not delete or
re-stamp the manifest, and do not bootstrap blindly — each recreates the same wrong base.
Preserve your *.pre-upgrade-* backups and wait for the recovery path.
Full guidance: https://github.com/fusebase-dev/fusebase-flow/blob/main/docs/ADVISORY-2026-08-20-missing-base-upgrade.md
v4.11.0 — an upgrade that cannot deliver no longer claims it did
Full entry: CHANGELOG.md § [4.11.0]. This file records release evidence and the upgrade note.
Reported by a consumer (finding N5, HIGH) against v4.10.1, with a mechanical reproduction.
Why this is a minor, not a patch
An upgrade that previously reported success and exited 0 can now refuse and exit 4. Automation
reading the exit code will see a new value, so it belongs in the version number rather than being
discovered.
The defect
With audit/managed-content-manifest.json absent, every pre-existing managed file classified
unknown-base → keep → not refreshed, while VERSION advanced and the run reported success. The
dry run showed no conflicts. The consumer's reproduction, same trees, one variable:
--base |
upstream-only (refreshed) | unknown-base (silently kept) | consumer-only |
|---|---|---|---|
| absent | 0 | 26 | 0 |
| present | 24 | 0 | 2 |
24 upstream changes silently dropped, and the 2 genuinely consumer-edited files lost their
consumer-only protection into the same bucket. It was caught only by hand-grepping for three fixes
known to be missing.
The exposed population is precisely the one that most needed the release: installs whose last upgrade
ran a pre-4.7.0 engine, which never delivered a base manifest.
The fix
This completes a locked decision rather than amending one. K13a already states the invariant
unconditionally — a consumer with no base manifest does not get a tree full of unknown-base — and
ff_synthesize_base already existed. It lived only in bootstrap-upgrade.sh; the ordinary
upgrade.sh path never called it.
- Base synthesis is extracted to
hooks/local/lib/synthesize-base.shand now runs on both engines. - A run-level delivery guard refuses to advance VERSION when all three hold: VERSION would change, and nothing was applied or removed, and at least one path classified
unknown-base. That third clause separates "nothing to do" from "couldn't tell what to do", so a current tree and a docs-only release never trip it. - The refusal exits 4, distinct from both success and the
changed-by-bothabort, and names the recovery. The dry run surfaces the same condition.
Aborting on unknown-base was rejected, not overlooked: it is K9's Option A, already rejected as
"unusable first adoption", and it would strand the forked consumer for whom no tag can ever resolve
and who has the least recovery available.
Recovery advice now branches on why synthesis failed. Previously it could tell an operator to run
the command that had just failed. For a plain-directory source it names a source carrying history; for
an unresolvable tag it says plainly that there is no automatic recovery for this shape and gives
the fingerprint-matching route instead of inventing advice.
Also fixed
policies/module-size-baseline.txt was written with CRLF and stored as LF, so the manifest recorded a
digest of bytes that never ship. Locally the stamper and verifier both read the same wrong copy and
agreed; only a clean checkout disagrees. Fourth occurrence of that class — evidence for the open
stamper-hashes-worktree-not-artifact ticket, which is deliberately undecided because hashing
committed bytes would trade away local-tamper detection.
A test was passing because of the bug
Worth stating plainly: an assertion requiring the repair remediation to exit 0 had been green
because it reproduced the defect — it certified an upgrade that advanced VERSION while delivering
nothing. Its measurand is now the contract (the remediation runs and restores the artifact),
accepting exit 4 only when the delivery refusal was genuinely reported.
Release evidence
| Item | Value |
|---|---|
| Tag verification | the v4.11.0 run is the authoritative gate; a local run is never release evidence |
| Pre-tag verification | f1b4d5e green on both platforms |
| Scoped upgrade phases | 80/80 (upgrade-classify 15, upgrade-boundary 23, preboundary-consumed 18, upgrade-repair 18, n5-delivery 6) |
Known residuals
upgrade.shsits at its FR-25 module-size baseline. The next change touching it must decompose it first — recorded with a named seam indocs/backlog/upgrade-sh-at-module-ceiling.run_shell_phase's crash guard only fires when a phase exits non-zero with no recorded failure, so a phase that dies mid-way after recording one is indistinguishable from a completed one. Found when a phase reported 15/16 while two cases had never executed. Recorded, not fixed.- The command gate still denies honest prose quoting a destructive pattern (K21/M8).
- Tag immutability is enforced by operator confirmation, not a repository ruleset.