Implementation Plan: Accept fails closed on a stale or pending acceptance matrix
Branch: claude/project-thread-zj01ct | Date: 2026-09-27 | Spec: spec.md Input: Mission specification from kitty-specs/accept-fails-closed-01M3HS4V/spec.md
Summary
acceptance-matrix.json has one locked read-modify-write today, in the verdict command (#4858). Accept's gate writes back an unlocked pre-check snapshot (#4974), and orchestrator-api accept-mission never looks at the readiness verdict (#4934). The plan:
1. Lift the verdict command's locked re-read/splice/write into one shared seam in acceptance/matrix.py (#4887). 2. Route the verdict command and the accept gate through it. The gate splices only the rows it owns, per the spec's row ownership rule, and judges the fresh matrix. 3. Add a locked pre-stamp verdict re-check around the acceptance record, shared by host accept and accept-mission. 4. Make accept-mission refuse on a not-ok summary. 5. Add a call-site gate so no production writer bypasses the seam.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: typer, rich, ruamel.yaml (existing); no new dependencies, so the supply-chain checks have nothing to assess. Storage: files: kitty-specs/<mission>/acceptance-matrix.json and meta.json, on the placement surface the gate context resolves (primary or coord) Testing: pytest. Issue-pinned @pytest.mark.regression tests are written RED first through the CLI entry points (spec-kitty accept, spec-kitty orchestrator-api accept-mission, spec-kitty agent mission acceptance-verdict), then kept as focused tests. The architectural call-site gate carries a self-mutation test. Target Platform: Linux, macOS, Windows (file lock via the existing machine_file_lock) Project Type: single (the src/specify_cli CLI package) Performance Goals: The locked span covers re-read, splice and write only; checks run outside it (NFR-001). The bounded 10 s lock wait is reused (NFR-002). Constraints: C-001 through C-006 in the spec. Complexity ≤ 15 per function. ruff, ruff format and mypy clean. Scale/Scope: about 6 production modules, 1 architectural gate, 2 contract docs and 1 skill reference.
Charter Check
- Single canonical authority: one matrix RMW seam replaces the command-local copy. accept-mission reuses
collect_feature_summary/summary.ok(C-001) and the shared guarded record (FR-010). Pass. - ATDD-first / red-first: every [build] FR gets a regression test RED on the planning base first (SC-004). Pass.
- Close defect class by construction (DIRECTIVE_043): FR-006 is a non-vacuous call-site gate with a self-mutation test and a shrink-only allowlist. Pass.
- Locality / smallest viable diff:
issue_verdict.pyconvergence, post-consolidation routing and matrix authoring are deferred (C-005). Pass. - Terminology canon: Mission, never feature, in all new prose and messages. The orchestrator skill is touched, so run
tests/architectural/test_no_legacy_terminology.py. Pass. - No full heavy suites in mission work: targeted test files plus the named architectural gate files only. Pass.
Project Structure
Documentation (this mission)
kitty-specs/accept-fails-closed-01M3HS4V/
├── spec.md
├── plan.md # this file
├── research.md # design decisions + squad dispositions
├── tasks.md # /spec-kitty.tasks
└── tasks/ # WP prompt files
Source Code (repository root)
src/specify_cli/acceptance/
├── matrix.py # NEW seam: locked_reread_splice_and_write(); NEW owned-row splice helper; NEW locked pre-stamp guard
├── gates_core.py # accept gate routes its write through the seam; judges the fresh matrix; records the matrix dir
└── __init__.py # AcceptanceSummary carries the matrix dir; _commit_acceptance_meta records under the guard
src/specify_cli/cli/commands/
├── accept.py # lock-timeout diagnostic (fail closed)
└── agent/acceptance_verdict.py # private copy removed; calls the shared seam
src/specify_cli/orchestrator_api/
├── commands.py # accept_mission: summary.ok guard + guarded record
└── envelope.py # CONTRACT_VERSION 1.7.0 + ledger
docs/api/orchestrator-api.md
src/charter/offering/skills/spec-kitty-orchestrator-api-operator/{SKILL.md,references/orchestrator-api-contract.md}
tests/architectural/test_acceptance_matrix_write_seam.py # NEW call-site gate
tests/specify_cli/acceptance/ ... # seam, splice, guard tests + #4974 regressions
tests/orchestrator_api/test_issue_4934_accept_mission_readiness.py
tests/specify_cli/cli/commands/ (accept CLI #4891 pin)
Structure Decision: Everything stays inside specify_cli (top layer). The seam lives next to the writers it guards, in acceptance/matrix.py, because putting an acceptance type in status/ would invert ownership. Lock primitives are imported from specify_cli.status.
Implementation Concern Map
IC-01 — Shared locked matrix write seam
- Purpose: Make the locked re-read, splice, write and optional commit the one way to read-modify-write the acceptance matrix.
- Relevant requirements: FR-001, FR-002, NFR-001, NFR-002, C-004
- Affected surfaces:
acceptance/matrix.py,cli/commands/agent/acceptance_verdict.py - Sequencing/depends-on: none
- Risks:
- The #4858 tests spy on
feature_status_lockat the verdict module's import site. Keep the spy targets working, or re-point them at the seam module. commit=Truekeeps committing under the lock. That is existing behaviour, not a regression.
IC-02 — Accept gate owns only the rows it judged
- Purpose: Stop accept from erasing concurrent verdicts and judge the fresh matrix.
- Relevant requirements: FR-003, FR-004, FR-005, C-003
- Affected surfaces:
acceptance/gates_core.py(_evaluate_acceptance_matrix),acceptance/matrix.py(owned-row splice helper),cli/commands/accept.py(lock-timeout mapping) - Sequencing/depends-on: IC-01
- Risks:
--no-commitmust stay commit-free, so the seam is called withcommit=False.- Diagnose mode writes nothing and takes no lock.
_evaluate_acceptance_matrixis near the complexity ceiling; extract helpers.
IC-03 — Locked pre-stamp verdict re-check
- Purpose: Close the window between the gate's write and
record_acceptance. - Relevant requirements: FR-010, SC-005
- Affected surfaces:
AcceptanceSummary: a new optionalacceptance_matrix_dirfield, set by the gate.acceptance/matrix.py: a new guard that is a context manager. It takes the lock, re-reads, refuses unless the verdict ispassorpass_pending_consolidation, and yields._commit_acceptance_meta:record_acceptanceis called inside the guard; the git commit stays outside it.- Sequencing/depends-on: IC-02 (the gate populates the dir)
- Risks:
- Nothing inside the guard may take the lock from a subprocess. Only the in-process
record_acceptance/write_metarun inside it. - A summary with no matrix dir (gate not run) skips the guard. That is only reachable when
summary.okalready required the gate, so assert it.
IC-04 — accept-mission applies the host readiness verdict
- Purpose: One readiness authority for host accept and orchestrator accept.
- Relevant requirements: FR-007, FR-009, SC-003, C-001, C-002
- Affected surfaces:
orchestrator_api/commands.py(accept_mission),orchestrator_api/envelope.py,docs/api/orchestrator-api.md, the orchestrator-api operator skill and its contract reference,tests/specify_cli/orchestrator_api/test_contract_version.py - Sequencing/depends-on: IC-03 (the guarded record)
- Risks:
- Two positive fixtures (
test_all_done_accepted,test_all_approved_accepted) need a real acceptable mission: a lanes manifest, spec and plan, claim events with an agent, convention dirs, a passing matrix and a real git repo.
IC-05 — Call-site gate and #4891 CLI pin
- Purpose: Make the lock structurally unavoidable, and pin #4891 at the CLI.
- Relevant requirements: FR-006, FR-008, SC-002
- Affected surfaces:
- New
tests/architectural/test_acceptance_matrix_write_seam.py. It is an AST scan ofsrc/for Name, Attribute and aliased calls towrite_acceptance_matrixandwrite_and_commit_acceptance_matrix. - Per-function allowlist: the seam itself,
matrix.py::scaffold_acceptance_matrix(and its commit wrapper, if separate), andpost_consolidation.py::verify_deferred_invariants, which has no production caller and is deferred. - The accept CLI regression test.
- Sequencing/depends-on: IC-02 (the gate's raw call must be gone first)
- Risks:
- Copy the census style of
tests/architectural/test_guard_capability_call_sites.py. - The self-mutation test must plant both an alias and an attribute call.