# Scratchpad — #791 Upgrade config protection (ms-791 worker lane) **Lane:** web1:ms-791 → reports to MS-LEAD (web1:mosaic-100). Do NOT contact Jason/Mos directly. **Worktree:** `/home/hermes/agent-work/stack-agents-dir-791`, branch `feat/791-upgrade-config-protection` off `origin/main` `9745bc3f` (verified exact head). ## Mission prompt (verbatim intent) Protect operator-owned config under `~/.config/mosaic` from framework-upgrade wipes. Ratified combination (Mos-approved, do NOT re-litigate): (b) strict ownership separation [PRIMARY] + (a) transactional pre-update snapshot [safety net] + (d) regeneration-from-SSOT [recovery]. (c) periodic timer DEFERRED. HARD GATE: unit test that an upgrade run touches NO path outside the manifest. Design-first: write design doc, send to MS-LEAD, WAIT for confirmation before impl. ## Session 1 (2026-07-16) — Phase 1 design ### Evidence gathered (wipe mechanism, file/line) - `mosaic update` → `update-checker.ts:509` `buildReseedCommand` → `bash install.sh` (`MOSAIC_SYNC_ONLY=1`, `MOSAIC_INSTALL_MODE=keep`). - Wipe = `packages/mosaic/framework/install.sh:199` `rsync -a --delete` + `PRESERVE_PATHS` denylist (`install.sh:47`). cp-fallback `install.sh:223` `find ... -exec rm -rf`. - Denylist gaps → WIPED: `agents/*.conf`, `policy/*.md`, `*.local.md`, harvester/SOP, `tools/_lib/credentials.json`. - Stale comment `update-checker.ts:492` claims `*.local` preserved — PRESERVE_PATHS has no such entry. - TS path `file-adapter.ts:157` → `file-ops.ts:66` `syncDirectory` = non-destructive copy-overlay, BUT its preserve list (`file-adapter.ts:164`) already DRIFTED from install.sh (missing `fleet/backlog`, `fleet/roles.local`). Evidence for single shared manifest SSOT. - Existing snapshot (`install.sh:76`) = /tmp, crash-trap only, deleted on success → inadequate; no `mosaic restore`. - `fleet-reconciler.ts:93,234` already has `regenerate-projections-from-roster` phase separate from lifecycle → `mosaic fleet regen` = thin projection-only wrapper (no restart), no FCM-M4/M5 preemption. ### Design decisions - **(b)** Invert to allow-list: shared `framework/framework-manifest.json` (framework globs + operatorReserved carve-outs); resolve per-path, deny-wins; **UNKNOWN ⇒ operator (fail-safe)**. Mechanism: drop `--delete`; non-deleting bulk copy + explicit manifest-scoped prune pass (iterate framework globs only → operator/unknown structurally unreachable). Pure prune-planner fn for tests. - **(a)** Snapshot to `~/.local/state/mosaic/backups/pre-update-/` 0700/0600, retention N=5, post-sync verify+restore, `mosaic restore --list/--from`. No secret values in output. - **(d)** `mosaic fleet regen` projection-only, preview-first, never restart. - HARD GATE test includes a deliberately-unanticipated operator path to prove fail-safe default. - **PR split:** PR1 manifest+guard (root fix, ships alone) → PR2 snapshot/restore (secrev) → PR3 regen+docs. PR2/PR3 depend on PR1. ### Status Design doc written: `docs/design/791-upgrade-config-protection.md`. Sent to MS-LEAD. ## Session 1 (cont.) — MS-LEAD CONFIRMED → Phase 2 GO All 4 asks approved. Binding conditions: - TDD tests-first, red-first proof per PR; ≥85% new-code; co-located `*.spec.ts`; never `--no-verify`. - HARD GATE test (§2.4, unanticipated sentinel survives byte-identical + mtime unchanged) = MERGE-BLOCKING for PR1. - Manifest-completeness test (§6.2) required. - Bash+TS read ONE shared `framework-manifest.json`; parity test (§6.1) required (closes #631 drift class). - UNKNOWN⇒operator (rule 3) non-negotiable. Keep prune-planner PURE. - `fleet regen`: NEVER restart; dry-run default, `--write` to apply; "never issues restart" test mandatory. - Independent review every PR; PR2 dedicated secrev. - One PR at a time through DAG. Report PR1 exact head + red→green evidence for review commission. ### Now: implementing PR1 (manifest + resolver + non-deleting sync + scoped prune + guard tests). ## Session 2 (2026-07-16) — PR1 built, tests-first, red→green proven Deviation noted to MS-LEAD in PR: manifest is `framework-manifest.txt` (line-oriented), NOT `.json`. Rationale: keep the bash installer free of a python3/jq dependency. The "ONE shared file, parity- tested" requirement is honored — `manifest-parity.spec.ts` drives the bash resolver as a subprocess and asserts byte-identical ownership vs the TS resolver over 34 probe paths spanning every class. ### PR1 artifacts - SSOT: `packages/mosaic/framework/framework-manifest.txt` ([framework]/[operator], deny-wins, fail-safe). - TS resolver: `src/framework/manifest.ts` (pure: parse/matchGlob/resolveOwnership/frameworkSubtreeRoots/ planPrune) + `manifest.spec.ts` (18 tests incl. planPrune property test + §6.2 completeness). - Bash resolver: `framework/tools/_lib/manifest.sh` (compiled globs → fork-free `manifest_is_framework`; CLI `resolve|subtree-roots|classify`). Sourced by install.sh. - HARD GATE (§2.4): `framework/tools/quality/scripts/test-upgrade-manifest-guard.sh` — keep-mode reseed, 10 operator sentinels (incl. unanticipated `unknown-operator-dir/x`, `harvester/sop.md`, `fleet/my-fleet.yaml`) survive byte-identical + mtime-unchanged; retired framework file pruned; secret value absent from output. RED=31 fail (orig install.sh) → GREEN=48 pass (fixed). - install.sh: keep mode now manifest-driven (`sync_framework_keep`, no `--delete`); overwrite unchanged. PRESERVE_PATHS denylist deleted. - TS sync: `file-ops.syncDirectory` gains `isOperatorOwned` guard; `file-adapter.syncFramework` derives it from `loadManifest` — hardcoded (drifted) preservePaths deleted. Fixture uses the REAL manifest. - Parity: `manifest-parity.spec.ts` (§6.1) — bash↔TS agree on 34 paths + subtree roots. - Migration matrix `test-install-migration.sh`: F6 flipped — `my-fleet.yaml` now MUST survive (fail-safe). - CI: new merge-blocking `upgrade-guard` step (`.woodpecker/ci.yml`) runs both bash suites (adds rsync). - update-checker.ts reseed comment corrected to the manifest model. ### Gates (all green) - `pnpm typecheck` ✓ · `pnpm lint` ✓ · `pnpm format:check` ✓ - Full mosaic vitest: 1062 passed (cli-smoke needs `pnpm build` first — build-artifact dep, not this change). - HARD GATE 48/48 · migration 21/21 · parity 3/3 · manifest 18/18 · file-adapter 8/8.