Recovery: Cutover Flip Fails From a Linked Worktree
spec-kitty cutover-guard --base-ref <ref> reports status_phase not flipped despite event-log runtime evidence, or cutover_mission() raises
PlacementMismatchError, even though the migration events were seeded
successfully.
Why this happens
cutover_mission() (seed → verify → flip) is run from a linked git
worktree — for example a .worktrees/pr-*-landing landing worktree.
_flip_phase (src/specify_cli/migration/runtime_state_cutover.py)
resolves the PRIMARY home via resolve_canonical_root, which maps a linked
worktree back to the primary checkout. It then compares that absolute
primary-checkout path against the (relative, worktree-scoped) write target.
They never match, so the flip fails closed before writing status_phase — a
correct, intentional refusal, not a bug. The seed events still get written
(they append to a relative path), so only the flip step is blocked.
No shipped --fix for this case
spec-kitty doctor cutover is informational only — it always exits 0 and
reports a per-mission cut-over verdict, but it has no --fix. Its
whole-corpus --dry-run spine also reads from a different root than a
single worktree expects (it can report would_flip for missions already
flipped on main), so trust the CI cutover-guard per-mission verdict over
a whole-corpus dry-run when you're inside a worktree.
Fix: run from the primary checkout (preferred)
The simplest fix is to not run the flip from the worktree at all — cd into
the primary checkout and re-run:
spec-kitty migrate backfill-runtime-state --mission <slug>
Fix: apply the flip's exact net write directly (when you can't leave the worktree)
Seed the migration events (idempotent if already seeded):
from specify_cli.migration.backfill_runtime_state import run_backfill_and_verify run_backfill_and_verify(mission_dir, dry_run=False)Apply the flip's exact net write yourself:
from specify_cli.core.paths import load_meta_fail_closed from specify_cli.mission_metadata import write_meta write_meta( mission_dir, {**load_meta_fail_closed(mission_dir), "status_phase": "1"}, validate=False, )validate=Falseis deliberate and narrow here — you're setting only the singlestatus_phasefield the flip would set; do not reuse this bypass for broadermeta.jsonedits.Confirm:
from specify_cli.status.cutover_eligibility import is_cut_over assert is_cut_over(mission_dir).cut_over is Truespec-kitty cutover-guard --base-ref upstream/mainThe guard should report 0 un-cut-over missions.
Related
- Coord-branch bookkeeping root-cause
- Recovery & Troubleshooting (agent-facing) — implementation-crash and merge recovery