Implementation Plan: Terminus / Merge-Coord Integrity

Branch: fix/terminus-merge-integrity | Date: 2026-09-23 | Spec: spec.md Input: Feature specification from kitty-specs/terminus-merge-integrity-01M380R6/spec.md Grounding: work/epic-5001-research/DEBRIEF.md (+ 4 per-lens research deliverables)

Summary

Make Epic #5001's invariant executable and closed by construction: no terminus command (merge / merge --resume / merge --abort / upgrade / agent issue-verdict / doctor coordination --fix) may exit 0 while the target tree diverges from the claimed WP set or while committed work is destroyed. The root cause is the absence of a transaction boundary around advance-ref → project-status → teardown plus the absence of a post-condition comparing the target tree to the approved-WP commit set. The technical approach introduces one shared terminus transaction seam every terminus path routes through — CAS ref-advance → full post-checkpoint projection → fail-closed reachability verifier → gated teardown — backed by four single-authority consolidations (target, lock, residue-topology, lane-identity) and a surface-authority write gate.

Technical Context

Language/Version: Python 3.11+ (repo standard; pyproject.toml requires-python) Primary Dependencies: git (porcelain/plumbing via subprocess), spec_kitty_events (read-only consumer — NOT modified here), no new runtime dependencies Storage: git refs/worktrees; status.events.jsonl (append-only log); .kittify/merge-state.json (MergeState); meta.json / lanes.json (mission manifests) Testing: pytest (tests/), with per-child red-first reproductions + one Tier-0 property test; ruff + mypy zero-issue gates Target Platform: Linux/macOS CLI (developer tooling) Project Type: single project (src/specify_cli/ layered adapter) Performance Goals: reconciliation gate is O(#approved-WP-commits), not O(repo history); ≤15% merge wall-clock overhead on the reference mission fixture (NFR-003) Constraints: fail-closed, no auto-mutation on divergence (C-003); changes confined to the true seam set (C-004, widened per post-plan squad): src/specify_cli/{merge,coordination,git,lanes} plus src/specify_cli/core/paths.py, src/specify_cli/tasks/issue_matrix.py, src/specify_cli/cli/commands/agent/issue_verdict.py, src/specify_cli/status/reducer.py (wrapper routing only), src/mission_runtime/…write_target_degrade, and CLAUDE.md/docs; no shared-package-boundary crossing into spec_kitty_events (C-002) — the Lamport routing fix stays inside specify_cli's wrapper; non-vacuous call-site gate over all 6 terminus entry points (NFR-005) Scale/Scope: 12 in-scope defects; ~7 module surfaces; ~10–12 work packages

Supply-chain note

No dependencies are added, upgraded, or removed. DIRECTIVE_051 supply-chain posture: N/A for this mission (no install-time surface changes). Recorded here so silence is not mistaken for an unexamined default.

Constitution / Charter Check

GATE: must pass before Phase 0; re-checked after design.

Charter ruleApplicationStatus
DIRECTIVE_043 close-by-construction / non-vacuous gateThe reconciliation gate ships with a concrete-floor call-site gate + self-mutation test + shrink-only allowlist so no future terminus path bypasses itPLANNED (NFR-005)
DIRECTIVE_044 single canonical authority / no split-brainEach of the 4 dual-authority pairs (target, lock, surface, lane-id) collapses to one owner; verifier reads one authoritative surfacePLANNED (C-1..4, S-C)
DIRECTIVE_010 spec fidelityDocs corrected in-band so CLAUDE.md/docstrings stop asserting guarantees the code lacksPLANNED (FR-013)
DIRECTIVE_041/034 test-remediation, red-firstEvery child reproduced red-first through its documented entry point before the fixPLANNED (FR-014, NFR-001)
DIRECTIVE_024 locality / DIRECTIVE_025 boy-scoutEdits confined to the merge/coord/git/lanes seams; campsite-clean only touched surfacesPLANNED (C-004)
red-main-release-disciplineThese are honest P0s; land failing repros, work them green, never green-washACTIVE
Terminology canon (primary/merge/routing footgun)Name the sense wherever these terms appear in code/docsPLANNED (C-005)

No unjustified violations. No complexity-tracking entries required (the seam reduces net divergence rather than adding a layer).

Architecture — the terminus transaction seam

Decision (resolves the spec's deferred design question): one SurfaceAuthority object with two entry points, and one TerminusTransaction composing the ordered steps. Details and alternatives in research.md; entity/contract shapes in data-model.md and contracts/.

flowchart TD
    subgraph TT[TerminusTransaction — every terminus path routes through this]
      A["RefAdvance.advance(ref, new, expected_old)<br/>compare-and-swap · fail closed"]
      P["CoordProjection.project_since(checkpoint)<br/>ALL post-checkpoint commits → target"]
      V["MergeOutcomeVerifier.verify(target, approved_wp_set)<br/>reachability: every approved commit in, no excluded commit in"]
      T["Teardown.run()<br/>gated: only after V passes"]
      A --> P --> V -->|pass| T
      V -->|fail| R["refuse · non-zero · recovery guidance<br/>NO teardown, NO mutation"]
    end
    SA["SurfaceAuthority<br/>resolve_for_read (loud-primary-fallback OK)<br/>resolve_for_write (fail-closed gate)"]
    MT["MergeTarget (single persisted authority)"]
    LK["MergeLock (owner-token keyed)"]
    RC["ResidueClassifier (topology-parameterized)"]
    LI["LaneIdentity (branch-bound, origin-aware)"]
    SA -.writes route through.-> P
    MT -.target for.-> A
    LK -.guards.-> TT
    RC -.gates dirty-preflight of.-> TT
    LI -.feeds approved_wp_set of.-> V

Module mapping (where each piece lives)

Corrected after the post-plan squad (see research.md dispositions; real call sites verified in-tree at HEAD).

SeamFile(s)Change
S-A CAS advancesrc/specify_cli/git/ref_advance.py:410advance_branch_ref 2-arg → 3-arg CAS (update-ref ref new old), fail closed; drop lock-dependence in docstring
S-D verifier + gatesrc/specify_cli/merge/reconciliation.py (NEW), wired in merge/executor.py between _phase_commit_and_assert (:1587) and cleanup; assigns merge/git_probes.py (ancestry→tree-equality integration check)reachability tree-vs-claim; claim fail-closed + Lamport-sourced + tree-tip SHAs (D3+); refuse-before-teardown; excluded-commit check by patch-id
S-B projection + coord CAS + strand healmerge/bookkeeping_projection.py:282, coordination/teardown.py:125, coordination/coherence.py (heal — sole owner of this file)project ALL post-checkpoint commits; SHA-scoped revert; teardown gated on reachability
S-C surface write gatecoordination/write_seam.write_artifact + src/mission_runtime/…write_target_degrade.resolve_write_target_or_degrade (the real degrade point); consumers tasks/issue_matrix.py:328, cli/commands/agent/issue_verdict.py:129, implement.py:_validate_base_ref; coordination/surface_resolver.resolve_for_write = thin helperrefuse degrade-to-primary on WRITE (read path unchanged)
C-1 single targetcore/paths.py resolve_merge_target_branch, merge/resolve.py:284, merge/state.py, merge/executor.py (reseed manifest from state.target_branch right after _load_or_create_merge_state @ :2280-2288)persist resolved target; the 28 lanes_manifest.target_branch read-sites see it; --target beats stale meta on resume
C-2 owned lockmerge/executor.py, merge/state.py:370 acquire_merge_lock, cli/commands/merge.py:462owner_token = merge-state-id (stable across resume); abort frees only own lock
C-3 residue topologycoordination/coherence.py:155-158, merge/executor.py dirty gate (:2178-2210)thread stored topology into is_coord_residue_churn
C-4 lane identitylanes/compute.py:515-537 (mint stable id once, read back on finalize — never re-letter over a bound id), workspace/context.pybranch-bound stable id; consult origin/<lane>
FR-011 behind-HEADmerge/preflight.py / resume remedy classifier; merge/git_probes.py:31-54 (ancestry-only integration check)distinguish behind-HEAD from local changes
FR-012 legacyreconciliation entrydetect+refuse pre-fix in-flight state
FR-013 docsCLAUDE.md, git/ref_advance.py:18-24 docstring, ADR 2026-02-09-3 reconciliation note (keep #4990 named-open)correct false guarantees

Project Structure

Documentation (this mission)

kitty-specs/terminus-merge-integrity-01M380R6/
├── plan.md · research.md · data-model.md · quickstart.md · contracts/
└── tasks.md            # created by /spec-kitty.tasks

Source Code (repository root)

src/specify_cli/
├── git/ref_advance.py                    # S-A CAS advance
├── merge/
│   ├── reconciliation.py   (NEW)         # S-D verifier + gate
│   ├── executor.py                       # wire gate; owned lock; single target
│   ├── state.py · resolve.py             # C-1 persisted target
│   ├── bookkeeping_projection.py         # S-B projection
│   └── preflight.py                      # FR-011 behind-HEAD
├── coordination/
│   ├── surface_resolver.py               # S-C write gate
│   ├── teardown.py · coherence.py        # S-B gating; C-3 topology
├── lanes/compute.py · workspace/context.py  # C-4 lane identity
└── tasks/issue_matrix.py                 # S-C write consumer

tests/
├── merge/ · coordination/ · git/ · lanes/   # per-child repros + unit
└── <property>/ terminus reconciliation Tier-0 property test

Structure Decision: single-project layered adapter (existing). The one new file is merge/reconciliation.py (the verifier + gate); everything else is a change to an existing seam, honoring locality (C-004).

Parallel Work Analysis

Corrected after the post-plan squad. merge/executor.py (2663 lines) is a god-file already decomposed into ~50 _phase_* functions over one _MergeRunState dataclass — it is NOT decomposed further (out-of-domain, DIRECTIVE_024, would rewrite every hunk the fix needs). Instead: a scaffold WP lands all new _MergeRunState fields + the linear-caller phase slot + a reconciliation.py stub in ONE change, then a single serialized executor lane carries every edit to executor.py / coherence.py. Everything file-isolated runs in a true parallel fan.

WP DAG (nodes + dependency edges; /spec-kitty.tasks materializes this)

Parallel fan (file-isolated, start immediately):
  WP01  F1  Tier-0 property + per-child repro harness (red-first, real CLI entry, no git mocks)
  WP02  F2  S-A CAS ref-advance          → git/ref_advance.py
  WP03      S-C surface WRITE gate         → write_seam, mission_runtime/write_target_degrade,
                                             issue_matrix.py, issue_verdict.py, implement.py, surface_resolver.py
  WP04      C-4 stable/origin-aware lane id→ lanes/compute.py, workspace/context.py
  WP05      FR-011 behind-HEAD remedy      → merge/preflight.py, merge/git_probes.py(read)
Serialized executor lane (one owner; each ← previous):
  WP06  scaffold: _MergeRunState fields + phase slot + reconciliation.py stub  ← WP01
  WP07  S-D verifier + reconciliation gate (claim fail-closed+Lamport+tree-tips; patch-id excluded; git_probes tree-eq) ← WP06, WP02
  WP08  S-B projection + coord-CAS + SHA-scoped strand heal (coherence.py heal) ← WP07
  WP09  C-1 single persisted target (core/paths.py + manifest reseed)          ← WP08
  WP10  C-2 owned lock (owner_token=merge-state-id)                            ← WP09
  WP11  C-3 topology-aware residue (coherence.py classifier)                   ← WP10
Integration:
  WP12  FR-012 legacy refuse + FR-013 docs + final green (12 repros + blast radius) ← all

Ownership map (file globs per WP — guarantees no two concurrent WPs share a writer)

to the single serial lane (WP06–WP11). No parallel-fan WP touches them.

tasks/issue_matrix.py, cli/commands/agent/issue_verdict.py, implement.py, coordination/surface_resolver.py. WP04 owns lanes/compute.py, workspace/context.py. WP05 owns merge/preflight.py (+ read-only git_probes.py). WP02 owns git/ref_advance.py. Disjoint — full parallelism.

  • The only multi-writer files — merge/executor.py and coordination/coherence.py — are confined
  • WP03 owns coordination/write_seam.py, src/mission_runtime/*/write_target_degrade,

Work distribution

fan but MUST merge before WP07 starts (S-D pairs with atomic advance).

lane is one implementer end-to-end to keep _MergeRunState coherent.

  • Critical path: WP06 scaffold → WP07 S-D → WP08 S-B (→WP09→WP10→WP11). WP02 (CAS) is in the
  • Agent assignments: python-pedro implements; reviewer-renata reviews each WP. The serial

Coordination points

serial lane + companions land. Per-child repros drive the real CLI entry point (no _run_git mocking) so they exercise ref_advance.py:410's CAS for real.

concurrent, so _MergeRunState never has two simultaneous writers.

  • Integration test: WP01's Tier-0 property test is the shared gate — red before, green after the
  • Sequencing guard: WP09/WP10/WP11 edit executor.py; they are strictly ordered, never

Complexity Tracking

No Constitution violations to justify — the seam consolidates authority and removes divergence.