src/runtime/next/runtime_bridge.py, src/specify_cli/core/mission_creation.py,
src/specify_cli/missions/_read_path_resolver.py
Design driver: #2069
Predecessor: ADR 2026-06-03-2 — ExecutionContext Owner and CommitTarget,
ADR 2026-06-19-1 — Coord-Empty Surface Policy
Context
A Spec Kitty mission can take one of four shapes across the orthogonal
coordination × lanes grid: no-coord/no-lanes, no-coord/lanes,
coord/no-lanes, coord/lanes. That shape decides where a mission's planning
artifacts and status surface live — the primary checkout
(kitty-specs/<slug>[-mid8]/) versus the coordination worktree
(.worktrees/<slug>-<mid8>-coord/...).
Today no shape is a first-class stored value. Each consumer re-infers a slice of it, ad-hoc, from scattered on-disk and git signals:
CommitTargetKindis classified per ref at construction time, never read back as a mission shape.- The
coordination_branch is None ⇒ FLATTENEDderivation is hand-rolled in two independent places: behind the canonical construction door atresolution.py:705-718(_resolve_coordination_branch), and a second, parallel disk-statladder atruntime_bridge.py:144-211(_mission_declares_coordination_branchplus a_coord_path.exists() ⇒ COORDINATIONbranch). - The read path keys on
CoordState.MATERIALIZED— a diskstatof the-coordworktree — in_read_path_resolver._resolve_existing_for_slug. read_lanes_json(...)re-derives the lanes axis independently.
Because the shape is re-inferred at every seam, the slices drift: an
artifact written through one inference is read back through another. That drift
is the coord/primary read/write desync class —
#2062 (a flattened
mission with a stale -coord husk reads the husk and mis-reports a planned
lane), #2063
(spec.md/tasks written to one surface, read from another →
"not found" divergence), and
#2064
(map-requirements and finalize-tasks disagree about where WP
requirement_refs live).
Prior fixes in this family were symptomatic: they band-aided one read leg
(e.g. threading a declares_coordination signal into the disk-stat
heuristic) while leaving the parallel inferences alive. The operator's binding
principle for this mission: "if storing topology re-opens #2062, that proves
our prior #2062 fix was non-structural."
Decision
Land the #2069 structural design: name the mission shape, store it, and resolve it once through a pure projection over the single existing construction door.
Name the shape. Add a mission-level enum
MissionTopology {SINGLE_BRANCH, LANES, COORD, LANES_WITH_COORD}inmission_runtime/context.py, naming the coordination × lanes 2×2 grid as one value.FLATTENEDis NOT an enum member — it is a separate historical/metadata provenance flag. A mission that was coord and had itscoordination_branchdropped is nowSINGLE_BRANCH/LANEScarrying aflattenedmark; the shape value never encodes history. This is the single place the lanes-vs-coord cross-product is named.Store it, do not guess it.
topologyis minted intometa.jsonatmission createand read thereafter — never re-inferred from disk or fromcoordination_branch is Noneat resolve time. Legacy missions are backfilled once viaspec-kitty migrate backfill-topology(mirroring thebackfill-identityprecedent), with aspec-kitty doctor topology --jsonaudit. Until a mission is backfilled, the imperative shell falls back to the legacy derivation exactly once — to compute and persist the topology — then reads the stored value.One resolver, pure. Add
resolve_context_for_mission(mission_id: str, topology: MissionTopology) -> ExecutionContextas a pure projection over the existing single construction doorbuild_execution_context(functional core / imperative shell). It performs no filesystem or git I/O; the shell parses/persistsmeta.jsonand passesid + topology.topologyis an authoritative input (optional input-assertion: fail-closed on a supplied-vs-resolved mismatch). This is a second projection of the same door — "one authority, two projections", the wayresolve_placement_onlyalready is — not a new parallel resolver (C-003).Retire BOTH live derivations. The
coordination_branch is None ⇒ FLATTENEDinference is removed from both sites: (a)resolution.py:705-718behind the door, and (b) the independentruntime_bridge.py:144-211disk-statladder. Leaving either alive is the parallel-inference death-spiral; both route through the stored topology under the same live convergence proof.CommitTargetKindbecomes a topology-derived predicate. Introduce aMissionTopology-derived per-ref predicateroutes_through_coordination(target)and re-express the 9.kind is COORDINATIONbranch-decision sites against it, so no site re-infers the per-ref topology. TheCommitTargetKindtype itself is NOT deleted here — its ~143 value-literal references (≈63 constructions + ≈24 imports + ≈56 test refs across 41 files) are behavior-neutral and carved to Mission B (#2070). This mission stops reading.kindfor decisions; the constructor field stays vestigial until Mission B eradicates the type.Adopt structurally on the read and write paths. The read path (
_read_path_resolver._resolve_existing_for_slugand the legs it feeds) resolves the surface from the stored topology, soCoordState.MATERIALIZED(a diskstat) is no longer the deciding signal (FR-006). The write path — every planning-phase commit and everystatus.emit.emit_status_transitioncall site — resolves its destination through the seam, not from the currentHEADbranch (FR-007/FR-009).safe-commit's two responsibilities are separated: mission-aware planning commits resolve via the seam; generic operator-file commits keep their existing behavior (NFR-002).
Binding principle — structural, not symptomatic (C-004)
The fix is structural: the read path consults the STORED topology, never
re-inferring the shape from on-disk worktree existence. A flattened mission
resolves PRIMARY because its stored topology says so — not because a band-aid
out-voted an on-disk husk. By construction, the orphaned -coord husk is never
consulted, so #2062/#2063/#2064 cannot re-open. If storing the topology could
re-open #2062, that would prove the prior fix was a symptom patch; the
resolution is to stop the read path inferring from disk, never to re-add a
band-aid.
Consequences
Positive
- The mission shape is one named, stored, authoritative value. It is
parseable after an interruption or an agent tool-switch — no caller has to
recompute it from
coordination_branch is None,lanes.jsonpresence, or a worktreestat. - The resolver is pure and isolated-testable (NFR-005): feed
(mission_id, topology), assert the returnedExecutionContextsurface fields — zero filesystem/git fixtures. All FS/git access lives in the imperative shell. - Both hand-rolled derivations are dead (SC-001): a
grepfor thecoordination_branch is None/_coord_path.exists()inference pattern finds zero live decision sites. - The #2062/#2063/#2064 desync class is closed at the root — structurally, not by close-on-static (witnessed live per NFR-001).
Negative / risks
- Backfill sequencing is a dogfooding landmine. This mission's own
meta.jsonmust be topology-backfilled before any caller reads the stored field (FR-003); flatten/coord friction is expected during implement, carried under the live-evidence rule. - Transient on-disk×git states are NOT subsumed by the enum (C-006). The
create→first-write window (#1718:
topology = COORD but the worktree is not yet materialized) and the
coord-deleted state (#1848:
declared branch deleted from git →
CoordinationBranchDeleteddata-loss carve-out) are orthogonal to the four enum cells and stay discriminated byprobe_coord_state(with the branch signal). ALANES_WITH_COORDmission can be MATERIALIZED/EMPTY/UNMATERIALIZED/DELETED at any instant; the stored topology does not encode that and must not try to. - The
CommitTargetKindtype lives vestigially until Mission B (#2070). The 9 decision sites no longer read it, but the constructor field and its ~143 value-literal references remain until the behavior-neutral eradication lands.
Alternatives considered
- Derive-and-carry per call (the #2069 ticket's original lean). Rejected: recomputing the shape per call is itself the drift vector this mission exists to remove — every recomputation is another place the slices can diverge.
- A new unified parallel resolver. Rejected (C-003):
build_execution_contextis already the single, verified construction door (per missions01KVGCE8/01KVN754).resolve_context_for_missionprojects it; introducing a second resolver that re-readsmeta.json/lanes.json/git independently would re-create the very split-brain under repair. - The original adopt-
resolve_placement_only+ band-aid-the-read-path plan. Superseded as symptomatic (D-2/C-004): threading adeclares_coordinationsignal into the disk-statheuristic treats the symptom; it leaves the diskstatas a topology signal and is re-openable. - Delete the whole
CommitTargetKindtype in-mission. Rejected: 41-file churn balloons a focused seam mission with zero correctness gain. Carved to Mission B (#2070) as principled cleanup over an already-correct door — the #2065 read-side strangler pattern.
References
- Mission spec:
kitty-specs/single-planning-surface-authority-01KVPR00/spec.md - Phase-0 decisions (D-1..D-8):
kitty-specs/single-planning-surface-authority-01KVPR00/research.md - Design driver: #2069 (MissionTopology SSOT seam)
- Closed structurally: #2062, #2063, #2064
- Mission B (behavior-neutral follow-on, blocked-by this mission): #2070 —
CommitTargetKindtype eradication + richer-API adoption at the 14resolve_placement_only/resolve_action_contextcall sites - Write-side twin (kind-aware placement over the stored topology): ADR 2026-06-24-1 — Kind- and topology-aware artifact placement (#2090 / #2101)
- Epics: #1716 (single surface authority), #2007, #1619 (execution-context)
- Transient-state carve-outs preserved: #1718 (create-window), #1848 (coord-deleted)
- Canonical seams:
src/mission_runtime/context.py(ExecutionContext,CommitTargetKind),src/mission_runtime/resolution.py(build_execution_context, retired derivation),src/runtime/next/runtime_bridge.py(retired second derivation)