Phase 0 Research — Merge/Git Destructive-Operation Safety

Consolidated from the pre-spec grounding squad (research/alignment + similar-surface lenses) and the post-spec adversarial lens. Every claim traces to a file:line at current main ba4e590142.

Decision 1 — Unify one guard, do not patch three sites

route the three destructive sites through it.

~9 independent predicates* (git/ref_advance._dirty_entries, lanes/worktree_allocator._validate_worktree_clean, review/dirty_classifier.classify_dirty_paths, charter_runtime/preflight/runner._detect_dirty_artifacts, git/sparse_checkout_remediation._is_dirty, cli/commands/agent/mission_repair._is_worktree_dirty, cli/commands/accept.__dirty_paths, acceptance._porcelain_dirty_path, merge/git_probes._paths_have_status_changes), 41+ files parsing git status --porcelain, with a documented history (#2795/FR-012) of three of them giving opposite verdicts on the same file. No canonical owner exists — a file-lock-authority-gap-shaped defect. Patch-3 would mint a 10th predicate and guarantee the next whack-a-field.

fork); overloading advance_branch_ref (rejected — it only inspects target-branch worktrees, ref_advance.py:366, not the primary checkout #4752 resets).

  • Decision: Introduce a single pre-mutation refuse-before-destroy guard and
  • Rationale: The alignment lens found dirty-state/branch safety **forked across
  • Alternatives rejected: Per-site bespoke checks (rejected — perpetuates the

Decision 2 — Reuse _dirty_entries; new typed refusal; inject the churn classifier

as the residue-aware dirty predicate and raises a NEW typed refusal modeled on RefAdvanceDirtyWorktreeError (ref_advance.py:82, error_code + resume-remediation text). Do NOT overload SafeCommitHeadMismatch.

the meta-lock exemption, and an injectable is_residue classifier (coordination.coherence.is_toolchain_generated_churn) that the merge paths already pass at merge/ordering.py. SafeCommitHeadMismatch (commit_helpers.py:138) has ≥6 downstream catchers (cli/commands/implement.py, core/mission_creation.py, cli/commands/agent/workflow_executor.py, git/bookkeeping_commit.py, merge/executor.py) whose semantics must not shift.

_validate_worktree_clean (rejected — no residue exemption → false-positive blocks on toolchain churn, the #2795/FR-012 trap in reverse). Two classifier layers (rejected — C-005: inject one classifier as the mechanism).

  • Decision: The guard reuses git/ref_advance._dirty_entries (ref_advance.py:240)
  • Rationale: _dirty_entries already handles --ignored, tree-obstruction,
  • Alternatives rejected: A second, blanket porcelain check like

Decision 3 — remove_workspace is the worktree-removal chokepoint

core/vcs/git.py remove_workspace (git.py:222) and add the guard there.

check" defect at merge/executor.py:1607 (#4753), coordination/workspace.py:300 (coord teardown), and orchestrator_api/commands.py:869 (cleanup mirror). A patch-N approach WILL miss the orchestrator mirror (not named in the issues). One chokepoint covers all three + future callers.

cleanup_merge_workspace) — detached, no user work, always unconditional (C-006); all detached temp worktrees (ordering.py, lanes/merge.py, review/baseline.py); mission close --discard (intentional destruction); doctrine git-source reset.

  • Decision: Route lane + coord worktree force-removal through
  • Rationale: The surface lens found the same "force-remove worktree, no dirty
  • Out of scope (explicitly): scratch merge workspace (merge/workspace.py,

Decision 4 — Pre-mutation preflight (governing atomicity model)

before _phase_mission_to_target advances the ref (merge/executor.py phase 6, lanes/merge.py:899/946); on any unsafe condition the merge refuses fail-closed before mutating anything.

pre-invocation" is unsatisfiable if the lane-worktree guard fires at the cleanup phase (phase 13, after the ref advance and bookkeeping commit). Moving detection to preflight makes atomicity uniform across US1/US2/US3, removes the coord half-tear risk, and lets NFR-001 be enforced by asserting the destructive call site is unreachable on the refusal path (a spy/ordering test, not a post-hoc-restore observable).

  • Decision: All dirty/branch detection happens in the merge preflight,
  • Rationale: The post-spec lens proved NFR-001 "byte-identical to

Pipeline anchors (for the implementers)

Lane-based merge phase order (merge/executor.py:1815-1828): gates → merge lanes → baseline/surface → bake+pre-target-done → capture-pre-target → _phase_mission_to_target (REF ADVANCE) → _phase_capture_and_baseline (#4752 reset at :920) → record-done → porcelain-invariant → commit-and-assert → dossier/stale → push → _phase_cleanup_worktrees_and_branches (#4753 remove --force at :1606) → finalize. #4754 is on the separate _dispatch_abort path (cli/commands/merge.py:420 → merge/state.py:455).

Red-first harness

tests/integration/test_merge_lane_planning_data_loss.py Layer 2 (_real_merge_external_mocks, ~:346) drives real git and mocks only out-of-git side effects — the template for #4752 (do not mock _refresh_primary_checkout_after_merge; seed the primary checkout dirty/off-target) and #4753 (do not mock the cleanup loop; seed a dirty lane worktree). #4754 needs a new _dispatch_abort harness (seed MERGE_HEAD in repo_root, no spec-kitty merge state).

Adversarial evidence (post-spec squad dispositions)

Per contracts/adversarial-evidence-contract.md — every contested finding's disposition (no finding silently dropped):

FindingSeverityDisposition
Coord-triple coupled teardown unspecifiedBLOCKINGchanged — FR-004 + US2 AC4 (atomic triple retention)
NFR-001 atomicity unsatisfiable on cleanup pathBLOCKINGchanged — adopted pre-mutation preflight model (Decision 4); NFR-001 rewritten
--resume guard honoring prose-onlySHOULD-FIXchanged — US1 AC4 + NFR-002 resume parity
Scratch workspace not excludedSHOULD-FIXchanged — C-006 + Edge Case
SC-004/FR-007 unification not testedSHOULD-FIXchanged — NFR-006 arch gate + IC-5
US2 AC1 disjunction exit-code undecidableSHOULD-FIXchanged — US2 AC1/AC2 exit + retain-path success-line contract, FR-006
NFR-001 mutate-then-restore readingSHOULD-FIXchanged — NFR-001 now asserts call-site unreachable (ordering spy)
C-001/C-005 classifier reconciliationSHOULD-FIXchanged — C-005 reworded: injection is the mechanism; NFR-003 fixture = meta.json VCS-lock stamp
US priority headings off-by-oneSHOULD-FIXchanged — corrected to P0/P0/P1
C-004 could license unguarded coord branch -Dcaveatchanged — C-004 reworded (coupled coord-branch deletion in scope)
mission close --discard over-guard riskNICEaccepted — Edge Case "explicit discard excluded"
Deferring standalone branch -DOKaccepted — kept as C-004 follow-up

No supply-chain dependency decision in this mission → DIRECTIVE_051 N/A.