Context and Problem Statement
mission_runtime.resolution._classify_artifact_surface is the single classifier that
turns a mission's four-state coordination probe (specify_cli.missions._read_path_resolver .CoordState) into a read decision for a coord-partition artifact kind (STATUS_STATE,
ISSUE_MATRIX, ACCEPTANCE_MATRIX, DECISION_LOG, TRACER_FILE, REVIEW_CYCLE,
DECISION_LEDGER). Before this ADR, the classifier's tail read:
if coord_state is CoordState.DELETED:
raise CoordinationBranchDeleted.for_mission(...)
if coord_state is CoordState.MATERIALIZED:
return TopologySurface.COORD, coord_feature_dir(...)
# EMPTY / UNMATERIALIZED / NONE -> primary + PRIMARY stamp
return TopologySurface.PRIMARY, None
CoordState.UNMATERIALIZED names the state where meta.json declares a
coordination_branch that still exists in git, but the coordination worktree itself has
never been created on disk — the fresh-clone / CI-runner / removed-worktree window between
mission create and the mission's first coordination-branch write. In that state the
classifier fell through to the same TopologySurface.PRIMARY, None answer as the
genuinely-benign EMPTY (coord root present, mission dir absent) and NONE (no
coordination topology at all) states — an empty-PRIMARY substitution: a coord-partition
read resolves to the primary checkout, which typically has no copy of the coord-owned
artifact, so the read comes back empty.
A caller with no special handling treats that empty result as "this document has never been
written" rather than "the authoritative surface for this document could not be reached
right now." The consequence is not merely a None/empty return silently absorbed
somewhere: retrospective.tracer_writer._read_current_coord_content catches the resulting
StatusReadPathNotFound-family exceptions (via _NO_EXISTING_CONTENT_EXCEPTIONS), maps
them to "", and the tracer writer then clobbers a real coord-side trace file's header
from that empty baseline the next time it writes — a destructive read-then-write, not
merely a stale read. Roughly two dozen other coord STATUS_STATE readers with no catch of
their own would, after this fix, propagate the new raise instead of quietly reading empty;
each of those is the intended "desired fail-loud" outcome this ADR names (see Consequences).
The sibling CoordState.DELETED state already fails closed (CoordinationBranchDeleted,
#4403/#1848) precisely because a deleted coord branch carrying unmerged status is data
loss. UNMATERIALIZED is a different failure shape — the branch is not lost, only not yet
checked out — but it shares the same defect class as DELETED once compared against what
EMPTY/NONE actually mean: EMPTY and NONE are declared, expected steady states (a
mission that never took the coordination branch, or one whose coord worktree the operator
legitimately flattened); UNMATERIALIZED is a transitional window where the
authoritative surface unambiguously exists but is not yet reachable from this checkout, and
substituting empty-PRIMARY silently discards that distinction.
Decision
The placement read seam raises a new typed error, CoordinationWorktreeUnmaterialized,
for a coord-partition read on CoordState.UNMATERIALIZED, instead of returning the
empty-PRIMARY substitution.
CoordinationWorktreeUnmaterializedis added tosrc/specify_cli/coordination/surface_resolver.py, as aStatusReadPathNotFoundsubclass sitting beside the existingCoordinationBranchDeleted— mirroring its shape (error_code,next_step,for_mission(...)factory) so every existingexcept StatusReadPathNotFoundhandler (the sanctioned read-only degraders inmission_runtime.read_dir_degradeandreview.cycle, plus every other absorbing catcher) keeps catching it unchanged.- Its
error_codeisCOORDINATION_WORKTREE_UNMATERIALIZED(distinct fromCOORDINATION_BRANCH_DELETED) and itsnext_stepnames the truthful recovery — materialize the coordination worktree (it self-materializes on the mission's first coordination-branch write, or an operator can force it viaspec-kitty doctor workspaces --fix) — and explicitly does not suggest flattening the mission, which would be a false and destructive recovery for a branch that still exists. mission_runtime.resolution._classify_artifact_surfacegains one new branch:if coord_state is CoordState.UNMATERIALIZED: raise CoordinationWorktreeUnmaterialized.for_mission(...), inserted between the existingDELETEDandMATERIALIZED/fall-through arms.DELETED->CoordinationBranchDeletedandMATERIALIZED->TopologySurface.COORDare unchanged.- The scope is coord-partition reads only. A PRIMARY-partition kind (for example
PRIMARY_METADATA) short-circuits toTopologySurface.PRIMARYindeclared_read_surfacebefore any coordination probe runs, so this change is structurally invisible to it — no new raise, no behavior change, for every non-coord-routing topology and every PRIMARY-partition artifact. EMPTYandNONEare explicitly out of scope and keep returning the declaredPRIMARYsurface unchanged. They are not the reported defect (#4959):NONEis a coord-less mission that never had a coordination branch to fail to reach, andEMPTYis a materialized-but-vacant coord root the project already treats as an operator-decided, loud-warned degrade (docs/adr/3.x/2026-06-19-1-coord-empty-surface-fallback.md). Folding either into this raise would widen the mission beyond the audited blast radius.
Consequences
- Coord readers now fail loud on the transitional window. Any coord-partition
read_dir(viamission_runtime.PlacementSeam.read_dir/resolve_artifact_surface) issued while the coordination worktree has not yet been materialized now raisesCoordinationWorktreeUnmaterializedinstead of silently handing back an empty PRIMARY path. Callers that already catchStatusReadPathNotFound(or its known subclasses) absorb this unchanged; the roughly two dozen coordSTATUS_STATEreaders with no catch of their own now propagate the raise to a caller boundary, which is the intended fail-loud outcome — each was audited (research.md) to land at a sane boundary rather than a raw traceback. - Sanctioned read-only degraders keep degrading.
mission_runtime.read_dir_degradeandreview.cycle's existingexcept StatusReadPathNotFoundblocks absorb the new sibling exception exactly as they already absorbCoordinationBranchDeleted— no regression for non-destructive reads (NFR-002). - The tracer-writer clobber this mission set out to close is a separate, dependent fix.
retrospective.tracer_writer._read_current_coord_content's narrowed catch (so a coord-topology unresolved/unmaterialized read propagates instead of degrading to"") is scoped to a later work package in this mission, not this ADR's change alone — this ADR only makes the underlying seam raise; the writer must still choose to let that raise through rather than catch-and-clobber. _coord_branch_exists(surface_resolver.py) is untouched. The new exception class is a pure addition besideCoordinationBranchDeleted; no existing function insurface_resolver.py, including the#4979/#4950_coord_branch_existssurface, was modified.- A future contributor extending the UNMATERIALIZED raise to
EMPTY/NONEmust revisit the coord-empty loud-fallback ADR (2026-06-19-1) first — those states carry their own, separately adjudicated policy and are not silently absorbed into this decision.
Alternatives Considered
- Reuse
CoordinationBranchDeletedforUNMATERIALIZED. Rejected — itsnext_steprecommends flattening the mission, which is false and destructive when the coordination branch is fully intact and merely not yet checked out. - Fix at the reader level only (e.g. only
tracer_writer). Rejected by operator decision (DM-01M38VWD) — a reader-local fix closes one symptom but leaves every other coord-partition reader silently substituting empty-PRIMARY on the same transitional window, reopening the same defect class the next time a new reader is added. - Widen the raise to cover
EMPTYandNONEas well. Rejected as out of scope — both are declared, expected steady states with their own existing policy; treating them as errors would regress the coord-empty loud-fallback decision and the coord-less topology path for no reported defect.
References
- Mission
kitty-specs/coord-read-fail-closed-01M38VVH/:research.md(Finding A),contracts/seam-fail-closed-contract.md,data-model.md. src/mission_runtime/resolution.py::_classify_artifact_surface,declared_read_surface,resolve_artifact_surface,PlacementSeam.read_dir.src/specify_cli/coordination/surface_resolver.py::CoordinationBranchDeleted,CoordinationWorktreeUnmaterialized.src/specify_cli/missions/_read_path_resolver.py::CoordState,probe_coord_state.- Sibling ADR:
docs/adr/3.x/2026-06-19-1-coord-empty-surface-fallback.md(theEMPTYpolicy this ADR leaves unchanged). - Prior fail-closed precedent:
#4403/#1848(CoordinationBranchDeleted,DELETED). - This mission:
#4959. - Regression coverage:
tests/mission_runtime/test_coord_read_seam.py,tests/mission_runtime/test_resolution_typed_errors.py.