Implementation Plan: Read-Path / Error-Fidelity Adoption
Branch: feat/read-path-error-fidelity (stacked on feat/naming-rider-3-2-1 / PR #2012) | Date: 2026-06-16 | Spec: kitty-specs/read-path-error-fidelity-adoption-01KV8NPC/spec.md Input: Feature specification from /kitty-specs/read-path-error-fidelity-adoption-01KV8NPC/spec.md
Summary
Adoption/routing refactor onto the EXISTING canonical read-path authority (resolve_action_context → ExecutionContext / ActionContextError, mission_runtime/resolution.py:689). Six commands fail in Robert's #2007 run because they flatten the resolver's typed error or hold a second authority wrapping it. This mission finishes the adoption (route the bypassers, preserve typed errors end-to-end), makes the two root resolvers agree on the submodule case, adds the missing primary-target-branch leg to the committed-check, and freezes the ExecutionContext composite with a build-time invariant. C-001: adopt, do not build — no new resolver, root authority, or error type. Phase 0 (research.md + research/call-site-inventory.md + research/live-repro.md) verified every call-site and live-repro'd every fix against HEAD 87697e5e4.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: typer, rich, ruamel.yaml, subprocess/pygit2 git, pytest, mypy, ruff (existing spec-kitty stack) Storage: filesystem — kitty-specs/<mission>/ planning artifacts, status.events.jsonl, git worktrees/coord branches Testing: pytest; function-over-form behavioral tests + verification-by-deletion; topology-true fixtures (real 26-char ULID, real coord-worktree + real submodule); TDD-first for behavioral fixes; the naming architectural ratchet is OUT of scope Target Platform: Linux/macOS developer + CI (the spec-kitty CLI) Project Type: single (CLI tool — src/specify_cli/ + src/mission_runtime/ + src/runtime/) Performance Goals: N/A (correctness/fidelity mission); resolution stays O(existing) Constraints: behavioral equivalence across primary/coord/submodule input classes (NFR-001); bounded conflict surface — named files only (NFR-005); idempotency-preserving (no on-disk worktree/coord churn); ruff+mypy clean, complexity ≤15, no suppressions (NFR-004) Scale/Scope: ~17 call-sites across 8 owning files; 7 implementation concerns; no schema/data migration
Charter Check
GATE: software-dev-default charter (compact mode). Directives DIR-001..DIR-013. Re-checked post-design — clean.
exactly one IC; agent/mission.py (4 call-sites) is owned solely by IC-03.
D-2..D-5 documented below.
fix are pure boundary translations — no new error type (C-001).
- DIR-001 (one owning module per concern): satisfied — the IC partition assigns each shared file to
- DIR-003 (decisions documented): D-1 recorded as decision
01KV8Q49WEG9RRKCEZ3XYN5DWP(resolved); - DIR-031 (bounded-context translation): the typed-error pass-through + the
decisionescape-check - No charter conflicts. Loopback/HTTP, security-hotspot, and terminology gates are not implicated.
Decisions (locked)
topology, ~2094 LOC, not required for read-path equivalence, would violate C-001 + explode NFR-005). CARRY #1993 minimal (~20 LOC resolve_lanes_dir seam, owned by IC-05 with #1832).
reject-on-mismatch (CONTEXT_INVARIANT_VIOLATION) + freeze the ExecutionContext composite. Do NOT normalize; do NOT retire the flat substrate (larger #1619 grain, deferred). The spec's branch_name == branch_ref.target_branch wording is superseded — branch_name is the WP lane branch and is expected to differ.
regression test only, NO code fix (debbie live-repro PASSED on HEAD).
as a raw traceback; keep _SAFE_SLUG_RE traversal-rejection on the raw operator token only.
commit hash + distinguish genuine-unchanged from no-op-against-wrong-surface".
IC-01 is re-scoped from "freeze + invariant" to "establish the single named context factory (build_execution_context) + freeze + invariant + declare the write-projection boundary contract." Construction is already single-sited (one ExecutionContext( call, resolution.py:739); the factory names that site, freezes the product, and is the sole construction door. branch_naming stays a collaborator (not absorbed — #2012 bounded context). The factory encapsulates identity/topology resolution so the deferred write-side (#1716/#1878, Mission B) adopts against a frozen seam — not a rewrite. D-1 stays. Boundary contract: write surfaces compose names/paths/identity from a factory-projected IdentityFragment + BranchRefFragment (+ workspace/surface); they MUST NOT re-derive mission_id/mid8/primary_root independently. Fragment retirement limited to prompt_source + the dead StatusSurfaceFragment surface= read-param. > ⚠️ The "factory boundary" is a documented docstring CONTRACT (a MUST-NOT-re-derive-identity > rule), NOT an importable callable API. WP01 ships a package-PRIVATE factory + the contract > docstring; it does NOT add an identity-projection symbol to mission_runtime.__all__ (which > exposes only resolve_placement_only / CommitTarget, no mid8 door). Consumers (IC-02b/M3, > IC-03, IC-04, IC-05) honor the contract via the primitive pattern — read the real > mission_id from meta and pass it to resolve_mid8(slug, mission_id=<real>) (the decision.py:421 > / context.py:73 shape) — never by importing a projection callable (none exists) and never by > seeding empty identity.
flatten (context/resolver.py:164 — corrected from context/mission_resolver.py, which has no flatten) and M2 orchestrator-api flatten across 8 endpoints (orchestrator_api/commands.py:263-266) extend IC-02; M3 orchestrator empty-mid8 suppressing the coord-aware fail-closed guard is a read-path SAFETY fix tied to the IC-01 identity boundary. Robert's merge.py routing (#1956/#1972) is verify-don't-redo (no IC owns it).
- D-1 (C-005 scope — decision
01KV8Q49WEG9RRKCEZ3XYN5DWP): DEFER #1716 entirely (write-side - D-2 (FR-009 rule): enforce
context.target_branch == branch_ref.target_branchat build via - D-3 (#1827 — FR-012): verified-already-fixed; deliver a full-sequence (incl. resume)
- D-4 (#8 — FR-003): delete the escape-walk for resolved paths and stop the typed error surfacing
- D-5 (FR-006): narrowed — the hard-failure swallow is already fixed; deliver "report the real
- D-6 (read/write-symmetry seam — investigation-2/3, synthesis
docs/engineering_notes/context-factory-readwrite-symmetry/00-SYNTHESIS.md): - D-7 (net-new surfaces — debbie, fold into this mission): M1
context mission-resolvetyped-error
Project Structure
Documentation (this mission)
kitty-specs/read-path-error-fidelity-adoption-01KV8NPC/
├── plan.md # This file
├── research.md # Phase 0 synthesis (+ research/call-site-inventory.md, research/live-repro.md, research/priti-related-issues-sweep.md)
├── contracts/ # Phase 1 behavioral contracts
└── tasks.md # Phase 2 (/spec-kitty.tasks — NOT created here)
Source Code (repository root)
src/
├── mission_runtime/ # IC-01: resolution.py, context.py (the SSOT)
├── specify_cli/
│ ├── runtime/next/ # IC-02: runtime_bridge.py
│ ├── cli/commands/ # IC-02 next_cmd.py; IC-03 agent/mission.py; IC-04 decision.py; IC-05 agent/workflow.py; IC-07 charter/_status_collectors.py
│ ├── missions/ # IC-03: _substantive.py
│ ├── core/ # IC-06: paths.py
│ ├── lanes/ # IC-05: persistence.py (#1993 seam)
│ └── workspace/ # IC-05: context.py
tests/ # behavioral + topology-true fixtures, mirroring src layout
Structure Decision: Single-project CLI. No new packages — every change lands in an existing module owned by exactly one IC.
Implementation Concern Map
> Concerns are NOT work packages. /spec-kitty.tasks translates these into executable WPs. Seven > concerns, zero file-overlap (NFR-005). branch_naming.py is OUT (prior mission #2012).
IC-01 — Single context factory + freeze + build-invariant + write-projection boundary (the SSOT spine)
- Purpose: Name the single context factory (
build_execution_context, funneling the existing soleExecutionContext(site atresolution.py:739+ resolving the:800-808post-build mutation), freeze the composite, assertcontext.target_branch == branch_ref.target_branchat build (reject-on-mismatch →CONTEXT_INVARIANT_VIOLATION), and declare the write-projection boundary contract so the deferred write-side adopts against a frozen seam (D-6).branch_namingstays a collaborator the factory calls — not absorbed. - Relevant requirements: FR-009
- Affected surfaces:
src/mission_runtime/context.py,src/mission_runtime/resolution.py - Sequencing/depends-on: none (precondition for IC-02..IC-05).
resolve_action_contextdelegates to the factory. - Risks: freezing surfaces the
:800-808WP-bearing mutation — resolve by constructing once through the factory (function-over-form), not by keeping mutability. Keep the factory package-private (no new public symbol — C-001, ADR-06-07-1 lean API). Est. ~15–60 LOC, ≤7 subtasks (pedro feasibility).
IC-02 — Typed-error pass-through (next family + M1/M2 surfaces)
- Purpose: Preserve
ActionContextError.code+ checked-paths across thenext-family catch-sites AND the two net-new flatten surfaces, instead of collapsing intoMISSION_NOT_FOUND; the cheapest cut, closes #12/#14/#15 (+M1/M2) with no resolver change. - Relevant requirements: FR-001, FR-002 (folds #1911 richer
next_step) - Affected surfaces:
src/runtime/next/runtime_bridge.py(:3128-3130,:3265-3274),src/specify_cli/cli/commands/next_cmd.py(_resolve_mission_slugcollapse:361, emitter:374-408); M1src/specify_cli/context/resolver.py(:164); M2src/specify_cli/orchestrator_api/commands.py(:263-266, 8 endpoints). _(M1 cite corrected: the flatten lives incontext/resolver.py:164, NOTcontext/mission_resolver.py— that file has no flatten.)_ - Sequencing/depends-on: IC-01
- Risks: mirror the existing
QueryModeValidationErrorbranch (next_cmd.py:474-491) so the typed code+paths reach the JSON envelope; copy theagent context resolvereference (context.py:158). M2 touches the external orchestrator-api contract — preserve the envelope shape while surfacing the real code.
IC-02b — Orchestrator-api typed-error + fail-closed identity (M2 + M3, read-path SAFETY)
- Purpose: One WP owning
orchestrator_api/commands.py: M2 — stop flatteningStatusReadPathNotFound→MISSION_NOT_FOUNDacross the 8 endpoints (:263-266); M3 — stop seedingresolve_mid8(mission_slug, mission_id=None)→ empty mid8 (:261), which suppresses the coord-aware fail-closed guard (external automation reads stale primary status on a coord topology). Honor the factory boundary contract (D-6) — read the realmission_idfrom meta and pass it toresolve_mid8(slug, mission_id=<real>)(thedecision.py:421primitive, NOT an importable projection API) — not a primary-only pre-read seeding empty identity; on a coord-only topology where metamission_idis absent, fail closed (do not silently seed empty). - Relevant requirements: FR-001 (M2 typed-error), FR-011 (M3 single-authority / read-path safety)
- Affected surfaces:
src/specify_cli/orchestrator_api/commands.py(M2:263-266+ M3:261) — distinct owned file from IC-02 (next family), so no overlap. - Sequencing/depends-on: IC-01 (consumes the factory identity boundary)
- Risks: must not regress the external orchestrator-api envelope shape; topology-true coord fixture required to prove the fail-closed guard fires. NOTE:
commands.py:484/787also passmission_id=Nonefor the legacy{slug}-{lane}grammar — those are intentional, NOT the M3 bug; touch only the status-read identity seed.
IC-03 — mission.py planning-entry adoption
- Purpose: setup-plan exact-one auto-select (in
setup_plan, NOT the shared helper); finalize-tasks read anchored on primary root (fix #11 fail-closed-pre-read);is_committedprimary-target-branch leg;_commit_to_branchreports the real hash + distinguishes no-op-against-wrong-surface. - Relevant requirements: FR-004, FR-005, FR-006, (#11)
- Affected surfaces:
src/specify_cli/cli/commands/agent/mission.py(SOLE owner — C8/C10/C11/C15),src/specify_cli/missions/_substantive.py(C9) - Sequencing/depends-on: IC-01
- Risks:
agent/mission.pyis the one true collision surface — keep it a single IC; do NOT add auto-select inside the shared_find_feature_directory(would change behavior for all callers).
IC-04 — decision single authority
- Purpose: Delete the primary-anchored escape-walk for resolved paths; derive
repo_rootfrom the canonical root authority; render the typed error as a structured message, not a traceback.cmd_verifyshares the helper — fix lands once. - Relevant requirements: FR-003 (#8)
- Affected surfaces:
src/specify_cli/cli/commands/decision.py(_resolve_repo_root_and_slug:57-119) - Sequencing/depends-on: IC-01
- Risks: keep
_SAFE_SLUG_REtraversal-rejection on the raw operator token (DIR-031); the live symptom is an uncaughtActionContextErrortraceback, so catch+structure it too.
IC-05 — implement single-resolution + #1993 lanes-dir seam
- Purpose:
agent action implementconsumes the claim's already-resolved context (single resolution path, kills "no workspace resolved"); extractresolve_lanes_dir(feature_dir)and route the 2-3 ad-hocfeature_dir/lanes.jsonderivations (#1993 carry, satisfies must-not-land-alone). - Relevant requirements: FR-008 (#1832), FR-011, (#1993)
- Affected surfaces:
src/specify_cli/cli/commands/agent/workflow.py(:1341,:1377-1381),src/specify_cli/lanes/persistence.py,src/specify_cli/workspace/context.py - Sequencing/depends-on: IC-01
- Risks: #1993 must NOT land alone — it rides with #1832 here; pure path composition, LOW risk.
IC-06 — Root-resolver unification (submodule boundary)
- Purpose: Make
resolve_canonical_rootstop at the submodule /.kittify/kitty-specsboundary, mirroringlocate_project_root(:122-131), so the two root authorities AGREE — the liveassert_initializedguard uses the broken one, so #1944/#1965 never covered #6. - Relevant requirements: FR-007 (#6/#2011)
- Affected surfaces:
src/specify_cli/core/paths.py(:284-288) - Sequencing/depends-on: none
- Risks: #1971 is a separate 3-way
locate_project_rootconsolidation — do NOT conflate; #2011 pins THIS resolver.
IC-07 — Charter status side-effect-free + JSON-safe
- Purpose: Make charter status collectors side-effect-free (no
generate_all/ensure_..._freshwrite inside read-only status) and emit one normalized, JSON-safe hash. - Relevant requirements: FR-010 (#1914 no-op slice)
- Affected surfaces:
src/specify_cli/cli/commands/charter/_status_collectors.py(:36-42) - Sequencing/depends-on: none
- Risks: scope to the read-path/status-read no-op slice only; broader #1914 umbrella stays on its own track.
Cross-cutting: FR-011 (single-authority adoption) is satisfied by verification-by-deletion across IC-02/03/04/05. FR-012 (#1827 regression test, test-only per D-3) is placed at tasks time (rides with IC-03's merge/baseline surface or its own thin WP).
> NIT (verify-or-document follow-up): there is a latent 4th empty-mid8 seed > resolve_mid8(mission_slug, mission_id=None) at agent/tasks.py:4047 — the same empty-identity > shape as M3, not in any WP/inventory. Likely benign (tasks-finalize context), but it should be > verified-or-documented so the D-6 "callers MUST NOT seed empty identity" boundary contract is > enforced consistently, not just at M3. No WP owns it; track as a follow-up.
Sequencing (DIR-003)
IC-01 (invariant — trustworthy-context precondition)
├─> IC-02 (error pass-through; closes #12/#14/#15) ┐
├─> IC-03 (mission.py planning entry) ├─ parallel (disjoint files)
├─> IC-04 (decision single authority) │
└─> IC-05 (implement + #1993) ┘
IC-06 (root resolver) and IC-07 (charter no-op) — independent, any time.
IC-01 + IC-02 are the safe lead. IC-06/IC-07 have no dependency and can start immediately.
Post-planning brownfield check (2026-06-16)
Standing discipline run before tasks. Outcome:
net-new related issues → 6 folded in (matrix in spec.md), 16 cross-ref'd, #1827 re-test (→ D-3), provenance flag (#1888≠#1886) logged.
two root resolvers (→ IC-06), decision dual authority (→ IC-04), ExecutionContext mutability/ post-freeze write (→ IC-01), is_committed surface-blindness (→ IC-03). No NEW split introduced; edits are surgical against large god-modules (agent/mission.py 3942 LOC — touched in-place, owned solely by IC-03; resolution.py 823; workflow.py 2737).
--feature hidden alias (next_cmd.py:73), --mission/--require-tasks (agent/mission.py:1427,1727), legacy feedback:// warnings (agent/workflow.py:1733,1818). NONE belongs to this mission's read-path/error-fidelity concern; no due-deprecation removal is owed here — bundling them would be scope creep (the --feature retirement is the separate #1060-A track).
corrected in research/call-site-inventory.md §6.
- Foldable issues: the planner-priti sweep (
research/priti-related-issues-sweep.md) surfaced 22 - Split-brain / duplication: mapped by
research/call-site-inventory.mdand assigned to ICs — - Due deprecations: scan of the IC-owned files found only orthogonal deprecation shims —
- Path drift: none — every IC-owned file exists at the cited path on HEAD; line-number drift
Complexity Tracking
No Charter Check violations — table intentionally empty.
Out of Scope
- #1716 write-side topology (D-1, stays on #1878); flat-substrate retirement (larger #1619 grain).
- The naming/identity AST ratchet +
branch_naming.py(prior mission #2012). - #2008/#2009/#1890/#1891 sibling clusters; patch-version assignment (C-004).