Tasks: User-content preservation for mutating flows

Mission: user-content-preservation-01M3549Q | Branch: fix/user-content-preservation → merge target fix/user-content-preservation → PR to upstream main Spec: spec.md · Plan: plan.md · Contract: contracts/preservation-contract.md

8 work packages. Model discipline: implement = sonnet (python-pedro, profile-loaded), review = opus (reviewer-renata). Every defect WP lands a RED-on-base @pytest.mark.regression repro (in tests/regressions/test_issue_NNNN_*.py) BEFORE the fix, then GREEN on the fix. Owned files are disjoint per WP so lanes do not collapse.

Subtask Index

IDDescriptionWPParallel
T001Add backup_before_overwrite(path)->Path to asset_preservation/backup.pyWP01
T002Symlink-aware capture (os.readlink, no silent deref)WP01
T003Unit tests: byte-identity, mode/mtime, O_EXCL, symlink, broken linkWP01
T004[RED] repros: remove/sync preserve user file, mixed-dir preserve, manifest byte-identical, owned-delete anchorsWP02
T005Route _remove_project_agent_surface through guard (dir-level, ManifestProver); retire 3 literalsWP02
T006Verdict-driven return + messaging (no false "Removed" on preserve)WP02
T007sync must not rewrite pinned command-skills-manifest.json; refresh behind explicit opt-in; --json enumerates mutationsWP02
T008AGENT_DIRS-table generality (not claude-special-cased)WP02
T009[RED] repro: foreign hook backed up + surfaced; signed hook replaced; no-hook unchangedWP03
T010Foreign-hook signature detection in hook_installer.installWP03
T011backup_before_overwrite + surface path; stop swallowing in install_commit_guardWP03
T012Caller wiring in implement_support surfaces the warningWP03
T013[RED] repro: brief present + no sidecar → refuse w/o --force (explicit AND --auto), byte-identicalWP04[P]
T014Drop and _source_path.exists() at intake.py :296 and :156WP04[P]
T015Message + --force path preservedWP04[P]
T016[RED] repro: mojibake, binary skip, CRLF preserve, honest "Fixed"WP05[P]
T017Classify text vs binary by content sniff, not .md extensionWP05[P]
T018Scope fallback decode to offending bytes / refuse with offsetsWP05[P]
T019Preserve line endings; report "Fixed" only on faithful repairWP05[P]
T020[RED] repro: frontmatter-only cycle/self-ref/unknown-WP → reject; valid acyclic succeedsWP06[P]
T021Validate effective persisted graph (reuse mission_finalize validator)WP06[P]
T022No status seeded on rejection; payload == persisted graphWP06[P]
T023[RED] repro: partially-staged file → index+stash byte-identical across callers; upgrade propagatesWP07[P]
T024safe_commit via temp index / commit --only (never touch operator index)WP07[P]
T025Narrow upgrade/autocommit except → propagate SafeCommitRecoveryFailed; render in upgrade.pyWP07[P]
T026Verify all callers commit exactly pathsWP07[P]
T027[RED] self-mutation: planted un-routed op fails gate; narrowed routed set failsWP08
T028Widen census: config.py in _module_set + _ROUTED_MODULES (set-equality) + :142 rmdir allowlistWP08

Work Packages

WP01 — Shared overwrite-backup helper (enabler)

Goal: One symlink-aware backup_before_overwrite(path)->Path in asset_preservation/backup.py (single backup-naming authority) for the hook flow to reuse. Priority: P2 (enabler) · Depends on: none · Subtasks: T001–T003 Independent test: unit tests over the helper (byte-identity, mode/mtime, O_EXCL collision, symlink, broken link). Prompt: tasks/WP01-overwrite-backup-helper.md

WP02 — Ownership-gate agent command-surface removal + manifest pins (#4907 P0, #2691)

Goal: Route _remove_project_agent_surface through the existing guard (dir-level) for both remove and sync orphan sweep; verdict-driven messaging; stop sync rewriting repository-pinned manifests. Priority: P1 (release-blocker) · Depends on: none · Subtasks: T004–T008 Independent test: remove/sync preserve a user command file + report it; mixed-dir preserved whole; pinned manifest byte-identical; owned-delete still occurs. Prompt: tasks/WP02-config-removal-ownership-gate.md

WP03 — Preserve a foreign .git/hooks/pre-commit before install (#4895 P0)

Goal: implement backs up a foreign hook (signature check) via the WP01 helper, installs, and surfaces the backup path; a signed hook is replaced as today. Priority: P1 (release-blocker) · Depends on: WP01 · Subtasks: T009–T012 Independent test: foreign hook backed up + path printed; user hook restorable; signed hook replaced. Prompt: tasks/WP03-pre-commit-hook-preservation.md

WP04 — intake brief overwrite gate keys on existence (#4910)

Goal: Both intake entry points refuse to overwrite an existing brief without --force; no backup. Priority: P2 · Depends on: none · Subtasks: T013–T015 Independent test: brief present + no sidecar → refuse (explicit + --auto), byte-identical. Prompt: tasks/WP04-intake-brief-overwrite-gate.md

WP05 — validate-encoding --fix repairs faithfully (#4896)

Goal: Content-sniff binaries, byte-scoped decode, line-ending preservation, honest "Fixed". Priority: P2 · Depends on: none · Subtasks: T016–T019 Independent test: mojibake avoided, binary skipped, CRLF preserved, "Fixed" only on faithful repair. Prompt: tasks/WP05-validate-encoding-faithful-fix.md

WP06 — Legacy finalize validates the persisted graph (#4890)

Goal: agent tasks finalize-tasks validates the effective persisted dependency graph (cycles/self-refs/unknown-WP), reusing the canonical validator; no status seeded on rejection. Priority: P2 · Depends on: none · Subtasks: T020–T022 Independent test: frontmatter-only cycle/self-ref/unknown-WP rejected; valid acyclic succeeds and payload == persisted graph. Prompt: tasks/WP06-legacy-finalize-effective-graph.md

WP07 — safe_commit never wipes the operator's index (#4888)

Goal: safe_commit commits via a temporary index so the operator's index/worktree is untouched; upgrade propagates SafeCommitRecoveryFailed instead of a misleading exit-0 skip. Priority: P2 (highest regression risk — hot shared helper) · Depends on: none · Subtasks: T023–T026 Independent test: with an unrelated file partially staged, index + stash byte-identical across all callers; forced-failure path surfaces stash ref + SHA. Prompt: tasks/WP07-safe-commit-index-preservation.md

WP08 — Census closure: widen the routing gate to config.py (FR-013)

Goal: Widen test_mutation_ownership_routing.py to cover cli/commands/agent/config.py (3 synchronized edits) so the removal preservation cannot silently regress. Priority: P3 (by-construction backstop) · Depends on: WP02 · Subtasks: T027–T028 Independent test: planted un-routed op fails the gate; narrowed routed set fails (self-mutation both directions). Prompt: tasks/WP08-census-widen-config.md

Dependency graph

WP01 ──► WP03
WP02 ──► WP08
WP04  WP05  WP06  WP07   (independent, parallel)

MVP / ordering

The two P0 release-blockers — WP02 (#4907) and WP03 (#4895, after the tiny WP01) — land first. The P1 lanes (WP04–WP07) run in parallel. WP08 (census closure) lands last, after WP02 is routed and literal-free.