Closes the root cause of #791. Inverts the framework updater from a denylist
("framework owns everything unless preserved") to an explicit allow-list manifest
("operator owns everything unless framework"). A path the manifest never anticipated
resolves to operator-owned by the fail-safe default, so it is structurally
unreachable by any write or prune.
This is PR1 of the ratified 3-PR split (b→a→d). PR2 (snapshot/restore, secrev) and
PR3 (fleet regen + docs) depend on this landing.
Root cause
mosaic update re-seeds via install.sh keep-mode, whose rsync -a --delete +
hand-maintained PRESERVE_PATHS denylist wiped operator paths the denylist forgot: agents/*.conf, policy/*.md, *.local.md, harvester SOP, tools/_lib/credentials.json, and any unanticipated fleet file.
Change
framework-manifest.txt — single SSOT ([framework]/[operator], deny-wins, UNKNOWN ⇒ operator fail-safe), consumed by both installers.
src/framework/manifest.ts — pure resolver (parse / matchGlob / resolveOwnership /
frameworkSubtreeRoots / planPrune) — the testable seam.
tools/_lib/manifest.sh — bash resolver (globs compiled once → fork-free manifest_is_framework), sourced by install.sh; parity-tested vs the TS resolver.
install.sh keep mode is now manifest-driven (no --delete): overlay-copy
framework files, scoped-prune only retired framework files inside shipped subtrees.
Operator + unknown paths are never written or deleted. Overwrite mode unchanged.
file-ops.syncDirectory gains an isOperatorOwned guard; file-adapter derives
it from the shared manifest, deleting the drifted hardcoded preservePaths.
Note on the manifest format
The design doc referenced framework-manifest.json; this ships framework-manifest.txt
(line-oriented) to keep the bash installer free of a python3/jq dependency. The
"ONE shared file, parity-tested" requirement is honored regardless of extension — see manifest-parity.spec.ts.
HARD GATE (§2.4) — merge-blocking
test-upgrade-manifest-guard.sh: 10 operator sentinels — including an unanticipated unknown-operator-dir/x, harvester/sop.md, fleet/my-fleet.yaml — survive a keep-mode
reseed byte-identical + mtime-unchanged; a retired framework file is pruned; the operator
secret value is absent from installer output. Runs both rsync + reduced-PATH environments.
Red → green:31 failed against the old installer → 48 passed after the fix.
Wired into CI as the merge-blocking upgrade-guard step.
Tests
Suite
Result
test-upgrade-manifest-guard.sh (HARD GATE)
48/48
test-install-migration.sh (F6 flipped: unanticipated operator file MUST survive)
21/21
manifest-parity.spec.ts (§6.1, bash↔TS over 34 paths + subtree roots)
3/3
manifest.spec.ts (incl. planPrune property test + §6.2 shipped-tree completeness)
## #791 PR1 — Manifest-owned upgrade guard (root-cause fix)
**Closes the root cause of #791.** Inverts the framework updater from a denylist
("framework owns everything unless preserved") to an explicit **allow-list manifest**
("operator owns everything unless framework"). A path the manifest never anticipated
resolves to **operator-owned by the fail-safe default**, so it is *structurally
unreachable* by any write or prune.
This is PR1 of the ratified 3-PR split (b→a→d). PR2 (snapshot/restore, secrev) and
PR3 (`fleet regen` + docs) depend on this landing.
### Root cause
`mosaic update` re-seeds via `install.sh` keep-mode, whose `rsync -a --delete` +
hand-maintained `PRESERVE_PATHS` denylist wiped operator paths the denylist forgot:
`agents/*.conf`, `policy/*.md`, `*.local.md`, harvester SOP,
`tools/_lib/credentials.json`, and any unanticipated fleet file.
### Change
- **`framework-manifest.txt`** — single SSOT (`[framework]`/`[operator]`, deny-wins,
`UNKNOWN ⇒ operator` fail-safe), consumed by **both** installers.
- **`src/framework/manifest.ts`** — pure resolver (parse / matchGlob / resolveOwnership /
frameworkSubtreeRoots / **planPrune**) — the testable seam.
- **`tools/_lib/manifest.sh`** — bash resolver (globs compiled once → fork-free
`manifest_is_framework`), sourced by `install.sh`; parity-tested vs the TS resolver.
- **`install.sh`** keep mode is now manifest-driven (**no `--delete`**): overlay-copy
framework files, scoped-prune only *retired framework files inside shipped subtrees*.
Operator + unknown paths are never written or deleted. Overwrite mode unchanged.
- **`file-ops.syncDirectory`** gains an `isOperatorOwned` guard; **`file-adapter`** derives
it from the shared manifest, deleting the drifted hardcoded `preservePaths`.
### Note on the manifest format
The design doc referenced `framework-manifest.json`; this ships **`framework-manifest.txt`**
(line-oriented) to keep the bash installer free of a `python3`/`jq` dependency. The
"ONE shared file, parity-tested" requirement is honored regardless of extension — see
`manifest-parity.spec.ts`.
### HARD GATE (§2.4) — merge-blocking
`test-upgrade-manifest-guard.sh`: 10 operator sentinels — **including an unanticipated
`unknown-operator-dir/x`, `harvester/sop.md`, `fleet/my-fleet.yaml`** — survive a keep-mode
reseed **byte-identical + mtime-unchanged**; a retired framework file is pruned; the operator
secret value is **absent** from installer output. Runs both rsync + reduced-PATH environments.
**Red → green:** `31 failed` against the old installer → `48 passed` after the fix.
Wired into CI as the merge-blocking **`upgrade-guard`** step.
### Tests
| Suite | Result |
|---|---|
| `test-upgrade-manifest-guard.sh` (HARD GATE) | 48/48 |
| `test-install-migration.sh` (F6 flipped: unanticipated operator file MUST survive) | 21/21 |
| `manifest-parity.spec.ts` (§6.1, bash↔TS over 34 paths + subtree roots) | 3/3 |
| `manifest.spec.ts` (incl. planPrune property test + §6.2 shipped-tree completeness) | 18/18 |
| `file-adapter.test.ts` (real manifest fixture) | 8/8 |
| Full mosaic vitest | 1062 passed |
### Gates
`pnpm typecheck` ✓ · `pnpm lint` ✓ · `pnpm format:check` ✓ · no `--no-verify`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Part of #791
Design-only planning artifact for #791. Traces the wipe mechanism
(install.sh:199 rsync --delete gated by PRESERVE_PATHS denylist) and
specifies the ratified (b)+(a)+(d) fix: framework-owned manifest allow-list
with fail-safe unknown=>operator default and manifest-scoped prune, a
transactional pre-update snapshot with mosaic restore, and a projection-only
mosaic fleet regen recovery path. No implementation changes.
Refs #791
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Invert the framework updater from a denylist ("framework owns everything unless
preserved") to an explicit allow-list manifest ("operator owns everything unless
framework"). A path the manifest never anticipated resolves to operator-owned by
the fail-safe default, so it is structurally unreachable by any write or prune.
Root cause (#791): `mosaic update` re-seeds via `install.sh` keep-mode, whose
`rsync -a --delete` + hand-maintained PRESERVE_PATHS denylist wiped operator
paths the denylist forgot (agents/*.conf, policy/*.md, *.local.md, harvester
SOP, tools/_lib/credentials.json, unanticipated fleet files).
- framework-manifest.txt: single SSOT ([framework]/[operator], deny-wins,
UNKNOWN=>operator fail-safe), read by BOTH installers.
- src/framework/manifest.ts: pure resolver (parse/matchGlob/resolveOwnership/
frameworkSubtreeRoots/planPrune) — the testable seam.
- tools/_lib/manifest.sh: bash resolver (compiled globs, fork-free hot path),
sourced by install.sh; parity-tested against the TS resolver.
- install.sh keep mode is now manifest-driven (no --delete): overlay-copy
framework files, scoped-prune only retired framework files inside shipped
subtrees. Operator + unknown paths are never written or deleted.
- file-ops.syncDirectory gains an isOperatorOwned guard; file-adapter derives it
from the shared manifest, replacing the drifted hardcoded preservePaths.
Tests (TDD, red->green):
- HARD GATE test-upgrade-manifest-guard.sh: 10 operator sentinels (incl. an
unanticipated one) survive a keep-mode reseed byte-identical + mtime-unchanged;
retired framework file pruned; secret value absent from output. RED 31 fail on
the old installer -> GREEN 48 pass. Wired merge-blocking into CI.
- manifest-parity.spec.ts (§6.1): bash<->TS agree on 34 paths + subtree roots.
- manifest.spec.ts: 18 tests incl. planPrune property test + shipped-tree
completeness (§6.2).
- test-install-migration.sh F6 flipped: an unanticipated operator fleet file now
MUST survive keep-mode reseed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold the Mos-elevated #797 Runtime-Ledger survival sentinel into #791 PR1 and
harden the .txt-format parity test per the accepted-format conditions.
- framework-manifest.txt: annotate the existing fleet/run/** operator carve-out
to name the #797 ledger (fleet/run/sessions/) so it reads as load-bearing.
The glob already matches the #797 spec exactly — no location divergence.
- HARD GATE (test-upgrade-manifest-guard.sh): seed a populated ledger
(events.ndjson journal + ledger.json projection, 0600 under 0700) as an
operator sentinel; assert byte-identical + mtime-unchanged + dir-perms
unchanged after a keep-mode upgrade. Relabel the prune check as the explicit
negative control. 48 -> 58 checks.
- Parity (manifest-parity.spec.ts): add format-edge fixtures driven through
BOTH resolvers via MANIFEST_FILE — comments/blanks/whitespace, duplicate and
overlapping globs (deny-wins), section/glob-ordering independence, and an
explicit UNKNOWN->operator negative probe; add ledger probe paths.
- manifest.spec.ts: isolate the carve-out's load-bearing value with a resolver
red->green — under a hypothetical fleet/** framework glob, the ledger is
pruned WITHOUT the fleet/run/** carve-out and protected WITH it (deny-wins).
Gates: typecheck, lint, format:check green; mosaic vitest 1069 passed;
HARD GATE 58/58; migration 21/21. Commits forward on 34e55d4a (no rebase).
Part of #791
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Second- and third-round independent-review reliability fixes on the keep-mode
upgrade rollback path, plus accurate abort messaging. All fixed red-first with
self-verifying controls in the rollback gate.
Round 2 (blockers A/B, should-fix C):
- install.sh: `trap 'restore_snapshot; exit 1' ERR INT TERM` so an INT/TERM
mid-sync terminates instead of resuming past the interrupt and reporting
success (a bash signal handler that only returns does not terminate).
- manifest.{ts,sh}: reject a degenerate [framework] section whose entries are
all empty or bare-dot (`/`, `./`, `.`, `..`) — it passed the non-empty guard
yet yielded zero usable globs, silently resolving everything to operator.
Parity via a shared `[^/.]` usable-glob test; TS throws ManifestError.
- finalize.ts: classify the sync-abort message — a ManifestError is a pre-sync
validation abort ("no files were changed"); any other error may be partial.
Round 3 (blockers D1, D2):
- install.sh: enumerate framework files with a checked temp file (_scan_or_die)
instead of `< <(find …)` — process substitution discards find's exit status,
so an EACCES/I/O failure mid-scan would truncate the file list yet leave the
loop exiting 0, committing a partial upgrade as success (ERR trap never fires).
- install.sh: guard the `rm -rf; mkdir -p` target reset inside restore_snapshot
— a bare reset failing under set -e exits silently after partial deletion,
never printing the snapshot-recovery pointer. Now checked like the cp -a
restore: on failure it preserves the snapshot and tells the operator where.
Tests: rollback gate 14→28 (Parts C/D/E with disabled-guard controls);
new finalize-sync-abort.spec.ts (3). No secret value is ever emitted; snapshots
stay 0700. Gates green: typecheck, lint, format:check, full mosaic vitest 1094,
HARD GATE 193, rollback 28, migration 21.
Refs #791
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Exact head reviewed:af627e758399bdbe5b07ad50ee73f32440fe36df
Head tree:c1a198df062a5aabc0f45f39a0de3707023d89d3 · parent 0a5e703a7012f27b18da35513ecceff5bcfa920e
Reviewer: independent adversarial/security review (Opus), author≠reviewer (author = worker lane ms-791; reviewer is not the author)
Exact-head CI: Woodpecker #1877 @ af627e75 = success (all 8 steps incl the new upgrade-guard gate)
Verdict per concern (all FIXED / verified)
Signal/trap integrity (blocker A):trap 'restore_snapshot; exit 1' ERR INT TERM — ; exit 1 is sequenced so it always terminates non-zero; restore_snapshot disarms the trap first (no re-entrancy). set -Eeuo pipefail propagates ERR into the per-file loop and sync_framework_keep(). No resume-to-success path.
Find exit status (blocker D1):_scan_or_die writes find -print0 to a temp and checks status, return 1 on failure → ERR trap → restore, for BOTH copy and prune scans. No < <(find …) process-substitution status loss.
Restore rm-fail + secret non-emission (blocker D2 + invariant): reset guard short-circuits on rm failure, preserves the snapshot, and surfaces the recovery pointer (path only). Every emitting path prints directory paths only — never file contents. Snapshot is mktemp -d 0700. No secret value reaches stdout/stderr on any path.
Degenerate manifest (blocker B): shared [^/.] usable-glob test in bash manifest_load and TS isUsableFrameworkGlob; hand-traced norm/regex parity — degenerate set (/,./,.,..) rejected; legit globs (**, tools/**, .hidden) accepted. No false-accept/reject.
Finalize classification (blocker C):ManifestError is provably pre-write (loadManifest before first write); finalize maps it to "no files were changed", every other error to "may be partially applied". Cannot mislabel a partial apply.
Test quality: all controls are genuine RED-first and non-tautological (-E strip, ; exit 1 strip, D1-guard neuter, D2-message delete, carve-out RED/GREEN). HARD GATE proves operator sentinels survive byte-identical + mtime/inode-stable.
CI config: new upgrade-guard step runs manifest-guard + rollback + migration suites with no failure: ignore; typecheck depends_on it. No --no-verify, no required check made non-blocking.
Scope/hygiene: clean; SUPER-SECRET-* is a test sentinel asserted absent from output; no committed snapshots/secrets/session.lock.
Execution evidence (lead first-hand, at exact head af627e75)
The static reviewer had no shell tool; MS Team Lead executed the suites at the exact head to close that caveat:
migration 21/21 + mosaic vitest 1094 covered green by CI #1877's upgrade-guard step.
Disposition: APPROVE at exact head af627e758399bdbe5b07ad50ee73f32440fe36df. Merge to proceed via queue guard + squash (MS Team Lead), per Mosaic gates.
## Independent Reviewer-of-Record — exact-head verdict: **APPROVE**
**PR #802 (issue #791) — manifest-owned upgrade config protection**
- **Exact head reviewed:** `af627e758399bdbe5b07ad50ee73f32440fe36df`
- **Head tree:** `c1a198df062a5aabc0f45f39a0de3707023d89d3` · parent `0a5e703a7012f27b18da35513ecceff5bcfa920e`
- **Reviewer:** independent adversarial/security review (Opus), author≠reviewer (author = worker lane ms-791; reviewer is not the author)
- **Exact-head CI:** Woodpecker `#1877` @ `af627e75` = **success** (all 8 steps incl the new `upgrade-guard` gate)
### Verdict per concern (all FIXED / verified)
1. **Signal/trap integrity (blocker A):** `trap 'restore_snapshot; exit 1' ERR INT TERM` — `; exit 1` is sequenced so it always terminates non-zero; `restore_snapshot` disarms the trap first (no re-entrancy). `set -Eeuo pipefail` propagates ERR into the per-file loop and `sync_framework_keep()`. No resume-to-success path.
2. **Find exit status (blocker D1):** `_scan_or_die` writes `find -print0` to a temp and checks status, `return 1` on failure → ERR trap → restore, for BOTH copy and prune scans. No `< <(find …)` process-substitution status loss.
3. **Restore rm-fail + secret non-emission (blocker D2 + invariant):** reset guard short-circuits on `rm` failure, preserves the snapshot, and surfaces the recovery pointer (path only). Every emitting path prints directory paths only — never file contents. Snapshot is `mktemp -d` 0700. No secret value reaches stdout/stderr on any path.
4. **Degenerate manifest (blocker B):** shared `[^/.]` usable-glob test in bash `manifest_load` and TS `isUsableFrameworkGlob`; hand-traced norm/regex parity — degenerate set (`/`,`./`,`.`,`..`) rejected; legit globs (`**`, `tools/**`, `.hidden`) accepted. No false-accept/reject.
5. **Finalize classification (blocker C):** `ManifestError` is provably pre-write (loadManifest before first write); finalize maps it to "no files were changed", every other error to "may be partially applied". Cannot mislabel a partial apply.
6. **Test quality:** all controls are genuine RED-first and non-tautological (`-E` strip, `; exit 1` strip, D1-guard neuter, D2-message delete, carve-out RED/GREEN). HARD GATE proves operator sentinels survive byte-identical + mtime/inode-stable.
7. **CI config:** new `upgrade-guard` step runs manifest-guard + rollback + migration suites with no `failure: ignore`; `typecheck` depends_on it. No `--no-verify`, no required check made non-blocking.
8. **Scope/hygiene:** clean; `SUPER-SECRET-*` is a test sentinel asserted absent from output; no committed snapshots/secrets/session.lock.
### Execution evidence (lead first-hand, at exact head `af627e75`)
The static reviewer had no shell tool; MS Team Lead executed the suites at the exact head to close that caveat:
- `test-upgrade-rollback.sh` → **28 passed, 0 failed** (exit 0)
- `test-upgrade-manifest-guard.sh` (HARD GATE) → **193 passed, 0 failed** (exit 0)
- migration 21/21 + mosaic vitest 1094 covered green by CI `#1877`'s `upgrade-guard` step.
**Disposition:** APPROVE at exact head `af627e758399bdbe5b07ad50ee73f32440fe36df`. Merge to proceed via queue guard + squash (MS Team Lead), per Mosaic gates.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
#791 PR1 — Manifest-owned upgrade guard (root-cause fix)
Closes the root cause of #791. Inverts the framework updater from a denylist
("framework owns everything unless preserved") to an explicit allow-list manifest
("operator owns everything unless framework"). A path the manifest never anticipated
resolves to operator-owned by the fail-safe default, so it is structurally
unreachable by any write or prune.
This is PR1 of the ratified 3-PR split (b→a→d). PR2 (snapshot/restore, secrev) and
PR3 (
fleet regen+ docs) depend on this landing.Root cause
mosaic updatere-seeds viainstall.shkeep-mode, whosersync -a --delete+hand-maintained
PRESERVE_PATHSdenylist wiped operator paths the denylist forgot:agents/*.conf,policy/*.md,*.local.md, harvester SOP,tools/_lib/credentials.json, and any unanticipated fleet file.Change
framework-manifest.txt— single SSOT ([framework]/[operator], deny-wins,UNKNOWN ⇒ operatorfail-safe), consumed by both installers.src/framework/manifest.ts— pure resolver (parse / matchGlob / resolveOwnership /frameworkSubtreeRoots / planPrune) — the testable seam.
tools/_lib/manifest.sh— bash resolver (globs compiled once → fork-freemanifest_is_framework), sourced byinstall.sh; parity-tested vs the TS resolver.install.shkeep mode is now manifest-driven (no--delete): overlay-copyframework files, scoped-prune only retired framework files inside shipped subtrees.
Operator + unknown paths are never written or deleted. Overwrite mode unchanged.
file-ops.syncDirectorygains anisOperatorOwnedguard;file-adapterderivesit from the shared manifest, deleting the drifted hardcoded
preservePaths.Note on the manifest format
The design doc referenced
framework-manifest.json; this shipsframework-manifest.txt(line-oriented) to keep the bash installer free of a
python3/jqdependency. The"ONE shared file, parity-tested" requirement is honored regardless of extension — see
manifest-parity.spec.ts.HARD GATE (§2.4) — merge-blocking
test-upgrade-manifest-guard.sh: 10 operator sentinels — including an unanticipatedunknown-operator-dir/x,harvester/sop.md,fleet/my-fleet.yaml— survive a keep-modereseed byte-identical + mtime-unchanged; a retired framework file is pruned; the operator
secret value is absent from installer output. Runs both rsync + reduced-PATH environments.
Red → green:
31 failedagainst the old installer →48 passedafter the fix.Wired into CI as the merge-blocking
upgrade-guardstep.Tests
test-upgrade-manifest-guard.sh(HARD GATE)test-install-migration.sh(F6 flipped: unanticipated operator file MUST survive)manifest-parity.spec.ts(§6.1, bash↔TS over 34 paths + subtree roots)manifest.spec.ts(incl. planPrune property test + §6.2 shipped-tree completeness)file-adapter.test.ts(real manifest fixture)Gates
pnpm typecheck✓ ·pnpm lint✓ ·pnpm format:check✓ · no--no-verify.🤖 Generated with Claude Code
Part of #791
Invert the framework updater from a denylist ("framework owns everything unless preserved") to an explicit allow-list manifest ("operator owns everything unless framework"). A path the manifest never anticipated resolves to operator-owned by the fail-safe default, so it is structurally unreachable by any write or prune. Root cause (#791): `mosaic update` re-seeds via `install.sh` keep-mode, whose `rsync -a --delete` + hand-maintained PRESERVE_PATHS denylist wiped operator paths the denylist forgot (agents/*.conf, policy/*.md, *.local.md, harvester SOP, tools/_lib/credentials.json, unanticipated fleet files). - framework-manifest.txt: single SSOT ([framework]/[operator], deny-wins, UNKNOWN=>operator fail-safe), read by BOTH installers. - src/framework/manifest.ts: pure resolver (parse/matchGlob/resolveOwnership/ frameworkSubtreeRoots/planPrune) — the testable seam. - tools/_lib/manifest.sh: bash resolver (compiled globs, fork-free hot path), sourced by install.sh; parity-tested against the TS resolver. - install.sh keep mode is now manifest-driven (no --delete): overlay-copy framework files, scoped-prune only retired framework files inside shipped subtrees. Operator + unknown paths are never written or deleted. - file-ops.syncDirectory gains an isOperatorOwned guard; file-adapter derives it from the shared manifest, replacing the drifted hardcoded preservePaths. Tests (TDD, red->green): - HARD GATE test-upgrade-manifest-guard.sh: 10 operator sentinels (incl. an unanticipated one) survive a keep-mode reseed byte-identical + mtime-unchanged; retired framework file pruned; secret value absent from output. RED 31 fail on the old installer -> GREEN 48 pass. Wired merge-blocking into CI. - manifest-parity.spec.ts (§6.1): bash<->TS agree on 34 paths + subtree roots. - manifest.spec.ts: 18 tests incl. planPrune property test + shipped-tree completeness (§6.2). - test-install-migration.sh F6 flipped: an unanticipated operator fleet file now MUST survive keep-mode reseed. Co-Authored-By: Claude Opus 4.8 <[email protected]>Second- and third-round independent-review reliability fixes on the keep-mode upgrade rollback path, plus accurate abort messaging. All fixed red-first with self-verifying controls in the rollback gate. Round 2 (blockers A/B, should-fix C): - install.sh: `trap 'restore_snapshot; exit 1' ERR INT TERM` so an INT/TERM mid-sync terminates instead of resuming past the interrupt and reporting success (a bash signal handler that only returns does not terminate). - manifest.{ts,sh}: reject a degenerate [framework] section whose entries are all empty or bare-dot (`/`, `./`, `.`, `..`) — it passed the non-empty guard yet yielded zero usable globs, silently resolving everything to operator. Parity via a shared `[^/.]` usable-glob test; TS throws ManifestError. - finalize.ts: classify the sync-abort message — a ManifestError is a pre-sync validation abort ("no files were changed"); any other error may be partial. Round 3 (blockers D1, D2): - install.sh: enumerate framework files with a checked temp file (_scan_or_die) instead of `< <(find …)` — process substitution discards find's exit status, so an EACCES/I/O failure mid-scan would truncate the file list yet leave the loop exiting 0, committing a partial upgrade as success (ERR trap never fires). - install.sh: guard the `rm -rf; mkdir -p` target reset inside restore_snapshot — a bare reset failing under set -e exits silently after partial deletion, never printing the snapshot-recovery pointer. Now checked like the cp -a restore: on failure it preserves the snapshot and tells the operator where. Tests: rollback gate 14→28 (Parts C/D/E with disabled-guard controls); new finalize-sync-abort.spec.ts (3). No secret value is ever emitted; snapshots stay 0700. Gates green: typecheck, lint, format:check, full mosaic vitest 1094, HARD GATE 193, rollback 28, migration 21. Refs #791 Co-Authored-By: Claude Opus 4.8 <[email protected]>Independent Reviewer-of-Record — exact-head verdict: APPROVE
PR #802 (issue #791) — manifest-owned upgrade config protection
af627e758399bdbe5b07ad50ee73f32440fe36dfc1a198df062a5aabc0f45f39a0de3707023d89d3· parent0a5e703a7012f27b18da35513ecceff5bcfa920e#1877@af627e75= success (all 8 steps incl the newupgrade-guardgate)Verdict per concern (all FIXED / verified)
trap 'restore_snapshot; exit 1' ERR INT TERM—; exit 1is sequenced so it always terminates non-zero;restore_snapshotdisarms the trap first (no re-entrancy).set -Eeuo pipefailpropagates ERR into the per-file loop andsync_framework_keep(). No resume-to-success path._scan_or_diewritesfind -print0to a temp and checks status,return 1on failure → ERR trap → restore, for BOTH copy and prune scans. No< <(find …)process-substitution status loss.rmfailure, preserves the snapshot, and surfaces the recovery pointer (path only). Every emitting path prints directory paths only — never file contents. Snapshot ismktemp -d0700. No secret value reaches stdout/stderr on any path.[^/.]usable-glob test in bashmanifest_loadand TSisUsableFrameworkGlob; hand-traced norm/regex parity — degenerate set (/,./,.,..) rejected; legit globs (**,tools/**,.hidden) accepted. No false-accept/reject.ManifestErroris provably pre-write (loadManifest before first write); finalize maps it to "no files were changed", every other error to "may be partially applied". Cannot mislabel a partial apply.-Estrip,; exit 1strip, D1-guard neuter, D2-message delete, carve-out RED/GREEN). HARD GATE proves operator sentinels survive byte-identical + mtime/inode-stable.upgrade-guardstep runs manifest-guard + rollback + migration suites with nofailure: ignore;typecheckdepends_on it. No--no-verify, no required check made non-blocking.SUPER-SECRET-*is a test sentinel asserted absent from output; no committed snapshots/secrets/session.lock.Execution evidence (lead first-hand, at exact head
af627e75)The static reviewer had no shell tool; MS Team Lead executed the suites at the exact head to close that caveat:
test-upgrade-rollback.sh→ 28 passed, 0 failed (exit 0)test-upgrade-manifest-guard.sh(HARD GATE) → 193 passed, 0 failed (exit 0)#1877'supgrade-guardstep.Disposition: APPROVE at exact head
af627e758399bdbe5b07ad50ee73f32440fe36df. Merge to proceed via queue guard + squash (MS Team Lead), per Mosaic gates.