Implementation Plan: Every post-mutation consolidate exit rolls back through one authority
Branch: claude/5385-single-rollback-authority-qqt180 | Date: 2026-09-30 | Spec: spec.md Input: Mission specification from kitty-specs/single-rollback-authority-01M3RCP4/spec.md
Planning questions were answered from the operator's triage ruling on #5385 and the two specify Decision Moments; the one planning fork (how the preflight reuses the protection authority) is recorded in research.md R-2.
Summary
spec-kitty consolidate gets one rollback door for its whole post-mutation span. The driver _run_lane_based_consolidation_locked wraps every call from _phase_merge_lanes (the first mutation) through _phase_reconcile_before_teardown (the gate) in one try. A non-zero typer.Exit, any other exception, or an interrupt calls _report_rollback, which calls the existing CAS authority rollback.rollback_to_snapshot, then re-raises the original. The revert-based per-phase rollbacks (_reset_coord_to_checkpoint, _revert_coord_done_commit, _rollback_to_pre_mutation_checkpoint, _revert_orphan_target_bake_commit) are deleted; working-tree byte restores and the strand marker stay. Separately, a pre-lock preflight asks the bookkeeping transaction's own policy gate whether the done write would be refused on the mission's status write target and refuses before any mutation; --dry-run reports the same code; a BookkeepingPolicyRefused that still escapes renders as a readable error at the command layer.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: typer, rich, git (subprocess); existing specify_cli.consolidation, specify_cli.coordination, specify_cli.git.protection_policy Storage: git refs and the consolidation record .kittify/runtime/merge/<mission_id>/state.json (unchanged schema) Testing: pytest over real temporary git repositories (tests/terminus/ real-CLI repros, tests/consolidation/ unit and AST pin); red-first regression repros per ADR 2026-07-17-1, converted to focused tests at close Target Platform: Linux, macOS, Windows (git 2.38+ for squash attribution, unchanged) Project Type: single (CLI package under src/) Performance Goals: preflight adds at most one policy evaluation (read-only git rev-parse probes) per consolidate run; rollback cost unchanged Constraints: complexity ≤ 15 per function; ruff, ruff format and mypy clean; no new suppressions; no version bump; no full heavy suites during work packages Scale/Scope: consolidation/executor.py driver and rollback helpers, coordination/transaction.py gate extraction, coordination/status_transition.py probe, consolidation/forecast.py, cli/commands/consolidate.py, the AST pin, and about ten test files re-pinned
Charter Check
- Single canonical authority: PASS by design. Rollback converges on
rollback_to_snapshot; the preflight calls the transaction's own policy gate (extracted, not copied) instead of a second protection rule. - ATDD / red-first: each work package starts with an issue-pinned
@pytest.mark.regressionrepro that fails on the base, then converts it to focused tests at wrap-up. - Architectural gate discipline: the AST pin grows a non-vacuous rule (all post-mutation phase calls inside the wrapper; no revert-based rollback helper) with self-mutation cases.
- Campsite cleaning: the retirement itself removes the executor's densest dead weight; touched functions stay ≤ 15 complexity.
- Test policy: targeted files and named architectural gates only (
NO_FULL_HEAVY_SUITES_IN_MISSION). - Pack tiers / terminology: no doctrine pack edits; Mission terminology only.
Project Structure
Documentation (this mission)
kitty-specs/single-rollback-authority-01M3RCP4/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/rollback-door.md
├── traces/ (tooling-friction, approach, design-decisions tracer files)
└── tasks.md (next phase)
Source Code (repository root)
src/specify_cli/consolidation/executor.py # driver wrapper, retirements, preflight call
src/specify_cli/consolidation/forecast.py # dry-run parity
src/specify_cli/coordination/transaction.py # extract the pre-flight policy gate
src/specify_cli/coordination/status_transition.py# status_write_refusal probe
src/specify_cli/cli/commands/consolidate.py # readable BookkeepingPolicyRefused
tests/consolidation/test_single_rollback_authority.py # AST pin grows
tests/terminus/test_repro_5385*.py # red-first real-CLI repros
docs/adr/3.x/2026-09-19-1-terminus-safety-invariant.md # amendment follow-up
Structure Decision: single package; no new modules except the repro test files.
Implementation Concern Map
IC-01 — One rollback door in the driver
- Purpose: every non-zero exit, exception or interrupt between the first mutation and the gate rolls back through the authority.
- Relevant requirements: FR-001, FR-002, FR-009
- Affected surfaces:
executor.pydriver,_report_rollback - Sequencing/depends-on: none
- Risks:
typer.Exitis anExceptionsubclass, so exit-code 0 must pass through untouched; a rollback failure must never replace the original error; the resume short-circuit path must keep its current gate-only wrapping.
IC-02 — Retire the revert-based rollbacks
- Purpose: delete the deprecated authorities so there is one way to undo branch moves.
- Relevant requirements: FR-004, FR-005
- Affected surfaces:
executor.pyhelpers and their callers;_MergeRunState.pre_bake_target_baseline_sha; tests pinning the old contract (#1826, #2367, #2711, #2786, #4764, coord reconcile) - Sequencing/depends-on: IC-01 (the door must exist before the old paths go)
- Risks:
state.pre_mutation_coord_shaand_capture_coord_checkpointstay (the authority seeds from them); test re-pins must assert the new contract, never delete coverage.
IC-03 — AST pin
- Purpose: close the class by construction.
- Relevant requirements: FR-008
- Affected surfaces:
tests/consolidation/test_single_rollback_authority.py - Sequencing/depends-on: IC-01, IC-02
- Risks: vacuity; the pin needs a concrete floor and self-mutation cases.
IC-04 — Protected-target preflight
- Purpose: refuse before any mutation when the done write would be refused.
- Relevant requirements: FR-003, FR-006, FR-007
- Affected surfaces:
coordination/transaction.py,coordination/status_transition.py,executor.pypre-lock checks,forecast.py,consolidate.py - Sequencing/depends-on: none technically; lands after IC-01 because both touch
executor.py - Risks: over-refusal (all-done resume, fallback topology, coordination recovery, single_branch landing,
--targetoverride, operator hatch). The probe must follow exactly the arm selection and gate the real write uses.
IC-05 — Docs and changelog
- Purpose: record the closed residual and the new refusal.
- Relevant requirements: all (documentation of shipped behaviour)
- Affected surfaces: ADR
2026-09-19-1,CLAUDE.mdconsolidation section,docs/changelog/CHANGELOG.md - Sequencing/depends-on: IC-01..IC-04
- Risks: docs freshness gate; terminology guard.