Research — Terminus / Merge-Coord Integrity

Phase 0 output. Design decisions that resolve the spec's open questions, each with rationale + alternatives, grounded in work/epic-5001-research/DEBRIEF.md and the four per-lens deliverables.

D1 — Seam shape: one SurfaceAuthority object, two entry points

loud-primary-fallback, which is correct for reads) and resolve_for_write() (fail-closed: refuse when the authoritative coord worktree/branch is unresolved/unmaterialized). Not two separate classes.

already correct; only writes lack a gate. One object with two entry points keeps the authority decision in one place (DIRECTIVE_044) while letting reads and writes differ in failure posture.

resolution logic, reintroduces drift risk; (b) gate at each call site — rejected: that is exactly the per-site open-coding that caused the class.

  • Decision: introduce a single SurfaceAuthority with resolve_for_read() (keeps today's
  • Rationale: the split-brain lens showed the read side (coordination/surface_resolver.py) is
  • Alternatives: (a) two classes (write-fence + read-resolver) — rejected: duplicates the

D2 — CAS on the forward ref advance

<old> read at the start of the transaction; on mismatch, fail closed (raise), never retry-write. The rollback path already uses 3-arg CAS — bring the advance to the same discipline.

(the mechanism behind #4996's rewind and the abort-frees-live-lock amplification). Removes the docstring's admission that safety rests on the global lock.

the git-native atomic primitive already in the file.

  • Decision: advance_branch_ref uses git update-ref <ref> <new> <old> (3-arg CAS) with the
  • Rationale: eliminates the TOCTOU between the merge-base --is-ancestor precheck and the write
  • Alternatives: keep lock-based safety — rejected: the lock is itself unsound (#4996) and CAS is

D3 — Reconciliation gate: reachability, not envelope

approved WP's approved commits are reachable from target, and (b) no excluded (canceled/removed) WP commit is reachable. It runs before any teardown; failure ⇒ refuse + non-zero + guidance, no teardown, no mutation.

reachability is the only authority that cannot be faked by the bookkeeping that is itself wrong. Reuses ref_advance ancestry helpers + _durable_done_wps_on_coordination_ref.

full tree diff of every file — rejected: O(repo) and unnecessary (commit-set reachability suffices and is O(#approved commits), satisfying NFR-003).

  • Decision: MergeOutcomeVerifier.verify(target, approved_wp_commit_set) asserts (a) every
  • Rationale: today's asserts check derived rows/meta the same run wrote (circular). Tree
  • Alternatives: strengthen the row asserts — rejected: still trusts a self-written envelope;

D4 — Projection before teardown (all commits, not just status files)

commit added to the coord ref after the checkpoint onto the target (not only status.events.jsonl/status.json), then gate teardown on the reachability check. Coord-ref advance is CAS (tip unchanged since checkpoint) or teardown aborts.

teardown. Content-scoped strand heal (revert only recorded SHAs) replaces the range revert that erased third-party events.

complement (reduces the race window) but not a replacement; projection + coord-CAS is the correctness guarantee, the fence is an optimization deferred unless tests show contention.

  • Decision: capture the coord tip at a bookkeeping checkpoint; before teardown, project every
  • Rationale: closes #4981/#4970/#4973 — concurrent status-emit/verdict commits currently die in
  • Alternatives: hold a merge writer-fence lock over the whole window — considered as a

D5 — Single persisted merge target (authority order)

MergeState target > meta.json), persist it into MergeState, and make that the sole read authority for all phases and every --resume. --resume never re-derives from meta.

target is write-only. One persisted authority ends the split-brain.

is exactly when they can't.

  • Decision: resolve the landing branch once (precedence: explicit --target > persisted
  • Rationale: closes #4985/#4991 — four copies today; executor reads the manifest, MergeState's
  • Alternatives: pass --target again on resume — rejected: operators don't, and the crash path

D6 — Forward-only legacy handling (operator decision)

terminus command refuses with a recovery instruction; the new guarantees are not retro-applied to auto-heal it.

auto-mutation this epic condemns; refuse-and-guide is honest and safe.

state).

  • Decision: pre-fix in-flight MergeState/coord state is detected (schema/marker absent) and the
  • Rationale: operator-confirmed. Auto-healing pre-fix corrupted state is the exact risky
  • Alternatives: retroactive recovery — rejected by operator (higher blast radius over corrupted

D7 — Owned merge lock

releases only a lock whose owner matches the aborting invocation. A stale/dead-owner lock is reclaimable via an explicit liveness check, never by blanket unlink.

running merge.

  • Decision: key the merge lock with an owner token (pid + start time / merge-state id); --abort
  • Rationale: closes #4996 second half — today --abort unlinks the single global lock of any

D8 — Topology-parameterized residue classifier

gate classifies using actual topology, so lanes/single_branch planning artifacts are never treated as coord residue and reset --harded.

COORD.

  • Decision: thread the mission's stored topology into is_coord_residue_churn; the merge dirty
  • Rationale: closes #4978; the classifier's own docstring already says it is only valid under

D9 — Stable, origin-aware lane identity

letter recomputed on WP removal); lane base resolution consults origin/<lane> before cutting a fresh branch from local main.

  • Decision: bind a lane's identity to its git branch at creation (stable id, not positional
  • Rationale: closes #4945 (re-lettering) and #4969 (origin shadowing).

Adversarial evidence (planning)

No dependency/security-impacting decision was made (no install-surface change), so the supply-chain adversarial pass is not applicable in v1 terms. The design-level adversarial challenge is the post-plan brownfield squad run at this pointcut; its contested findings and dispositions (accepted / changed / deferred_with_rationale) are appended below after that squad reports.

Post-plan squad dispositions (3 lenses: paula split-brain-closure, renata coverage-honesty, priti decomposition)

Per contracts/adversarial-evidence-contract.md, every contested finding gets a disposition: accepted (design changed), deferred_with_rationale, or rejected. Full lens reports: work/epic-5001-research/postplan-{paula,renata,priti}.md.

#FindingDispositionChange
PP-F3 / RN-Q4Verifier claim sourced via resolve_for_read degrades to empty primary → gate passes vacuously; and membership flows through LWW reduce_parsed (#4990)ACCEPTEDD3+ below: claim sourced fail-closed (refuse on empty/degraded, cross-check against manifest all_wp_ids) AND through the Lamport wrapper (status/reducer.py:371 materialize/reduce_shared_state), never reduce_parsed. In-specify_cli routing only — does NOT modify spec_kitty_events, does NOT cross C-002, does NOT fix #4990's reducer itself.
RN-F1#4970 real write-degrade is coordination/write_seam.write_artifact → mission_runtime.write_target_degrade.resolve_write_target_or_degrade; chain cli/commands/agent/issue_verdict.py:129 → tasks/issue_matrix.py:328 → write_seam — NOT surface_resolver.pyACCEPTEDS-C write gate moves to write_seam / resolve_write_target_or_degrade; surface_resolver.resolve_for_write becomes a thin helper the real write chain calls, not a redundant guard.
RN-F3 / PP-F1C-1 real authority is core/paths.py resolve_merge_target_branch (via merge/resolve.py:284); executor reads run.lanes_manifest.target_branch (28 sites) seeded from meta at executor.py:2536-2538 BEFORE state load (:2280-2288) and never reconciled to state.target_branch (the live #4991 mechanism)ACCEPTEDC-1 adds core/paths.py + a single manifest-reseed-from-state point right after _load_or_create_merge_state, so the 28 read-sites see the persisted target.
RN-F3Plan's C-004 locality {merge,coordination,git,lanes} cannot even wire the fix — real sites include core/paths.py, tasks/issue_matrix.py, cli/commands/agent/issue_verdict.py, src/mission_runtime/…write_target_degrade, status/reducer.py (routing), CLAUDE.mdACCEPTEDC-004 widened to the true seam set (below). Layer note: mission_runtime is a deeper layer; edits there respect the enforced chain …{mission_runtime}<-specify_cli.
PP-F2Lock owner_token = "pid OR state-id" re-splits durable/ephemeral identity — a pid can't survive the crash the lock protects (--resume = new pid)ACCEPTEDPin owner_token = merge-state-id (stable across resume). acquire_merge_lock (state.py:370) already writes a body — clean change.
RN-F4S-D "no excluded commit reachable" uses WP-IDs/SHAs; misses cherry-picked/rebased/re-lettered canceled code (lanes/compute.py:536 positional)ACCEPTEDExcluded-commit detection uses patch-id equivalence (not just SHA) so cherry-picked/re-lettered copies of canceled code are caught; canceled set sourced from acceptably_canceled_wp_ids mapped to lane tips.
RN-Q3Fakeable DoD: nothing forbids deriving approved SHAs from status rows (the vacuous _assert_merged_wps_done_on_target pattern) or mocking _run_git in reprosACCEPTEDContract pins: approved SHAs come from lane-branch git tips (not status rows); per-child repros drive the real CLI entry point, no _run_git/subprocess mocking; excluded-check is non-vacuous even when canceled set is empty (planted-canceled-commit assertion).
RN-F5 / PPNFR-005 non-vacuous gate must enumerate all 6 terminus entry points incl. upgradeACCEPTEDGate allowlist enumerates merge/resume/abort/upgrade/issue-verdict/doctor-coordination-fix; self-mutation test proves a 7th unrouted path fails.
PR-priti"Parallel companions" mislabeled — S-D/S-B/C-1/C-2 all mutate executor.py _MergeRunState (2663 lines, already function-decomposed into ~50 _phase_*)ACCEPTEDAdd a scaffold WP (all new dataclass fields + linear-caller phase slot + reconciliation.py stub in ONE change), then a single serialized executor lane {scaffold→S-D→S-B→C-1→C-2→C-3}. NO god-module decomposition (out-of-domain, DIRECTIVE_024).
PR-priticoherence.py double-owned (S-B heal ∧ C-3 classifier); git_probes.py unassigned; S-C base = implement.py:_validate_base_ref not workspace/context.pyACCEPTEDcoherence.py → one owner in the serial lane; git_probes.py → S-D lane; S-C corrected to implement.py:_validate_base_ref.
PP-F4 / RN-Q4Green Tier-0 could imply verdict-integrity it doesn't provide while #4990 openACCEPTEDGate success message scoped to "approved-WP commit reachability"; #4990 stays named-open in FR-013 docs.
PP-F5lanes/compute.py:515-537 re-runs positional lettering every finalize — confirm it READS BACK the minted id, not also-mintsACCEPTEDC-4 mints stable id once at creation and reads it back on finalize; positional lettering never overwrites a bound id.

No finding rejected or silently dropped. The spine (S-A/S-D/S-B insertion points) was confirmed correct by all three lenses; the changes above are localization + claim-integrity + sequencing corrections, not a re-architecture.

D3+ (amended) — verifier claim integrity

The verifier's approved_wp_set is sourced (a) fail-closed — refuse when the coord surface is unresolved/unmaterialized or the derived claim is empty while the manifest lists WPs; (b) through the Lamport reduction wrapper, so a wall-clock-later approval cannot override a committed rejection in the claim the gate trusts; (c) approved commit SHAs come from lane-branch git tips, never status rows. This makes the gate non-vacuous on both axes (tree AND claim).