Implementation Plan: Verdict-matrix RMW preservation (#4858 + #4868)
Branch: fix/verdict-matrix-rmw-preservation | Date: 2026-09-21 | Spec: spec.md Input: Feature specification from kitty-specs/verdict-matrix-rmw-preservation-01M32M9G/spec.md
Summary
Close the verdict-matrix RMW-preservation defect class — a verdict-recording command silently dropping previously-committed rows from its coordination-resident matrix and exiting success — across both verdict matrices:
different entries lose a committed row because the write is an unlocked read-modify-write that overwrites the whole matrix from a pre-check snapshot; overall_verdict flips fail→pass. Fix (Option A): run the slow custom check outside the lock, then inside status/locking.py::feature_status_lock(repo_root, matrix_dir.name) re-read the on-disk matrix, splice in only the owned entry, and write+commit; route the shared write_acceptance_matrix through kernel.atomic.atomic_write.
wrong source directory on first JSON write (issue_verdict.py::_migrate_if_needed hands the primary feature_dir to migrate_issue_matrix_to_json instead of the coord-aware read_dir), so existing issue verdicts vanish. Fix: give migrate_issue_matrix_to_json an optional read_dir and read the legacy matrix from read_dir or feature_dir; keep the write staged on primary so the write-seam still materializes coord and cleans residue.
- #4858 (P0, acceptance / concurrency): two concurrent
acceptance-verdictwriters for - #4868 (P1, issue / serial migration):
issue-verdicton a coord mission migrates the
Both fixes are grounded (research/grounding-4858.md, research/grounding-4868.md) and their test contracts hardened by two post-spec adversarial squads.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: typer, rich, ruamel.yaml (existing); no new dependencies added Storage: JSON matrix files on disk (acceptance-matrix.json, issue-matrix.json) resolved via the placement seam (primary / coordination surface); git commits via the write-seam Testing: pytest (unit + integration); .venv/bin/python -m pytest (editable install → exercises the fix directly) Target Platform: Linux/macOS/Windows CLI Project Type: single (CLI/library) Performance Goals: verdict command stays well within existing CLI budgets; the #4858 held critical section is only re-read+merge+write+commit (slow check runs outside the lock) Constraints: kernel.locks only (no raw fcntl/msvcrt/filelock — test_lock_primitive_ban.py); atomic write via kernel.atomic.atomic_write; no new --feature CLI surface; no changes to src/specify_cli/__init__.py; ATDD red-first per root Scale/Scope: two disjoint fix loci, ~2 source files each; no schema/API contract change; no dependency change
Constitution Check (Charter Check)
GATE: Must pass before Phase 0 research. Re-checked after Phase 1 design.
(feature_status_lock, kernel.atomic.atomic_write, the placement-seam read_dir resolver); introduces no second locking/atomic authority.
delegate to kernel (direction specify_cli → kernel, allowed). No layer violation (validated by the post-spec architecture lens).
a distinct commit before implementation for EACH root; reviewer verifies red-on-base → green-on-fix per root.
matrices are separate bounded contexts kept separate.
feature_dir/feature_slug/read_dir are the tolerated exception.
activate the deferred write_if_changed (confirmed absent), avoiding a dead-symbol red.
version-bump/CHANGELOG coupling is forced (a CHANGELOG entry is still added for the fix).
- Single canonical authority ✅ — reuses the existing sanctioned primitives
- Architectural alignment ✅ — judgement stays in
specify_cli; only mechanical write/lock - ATDD-first (C-011 charter / Standing Order #4) ✅ — a failing reproduction is committed as
- DDD + tiered rigour ✅ — core concurrency/preservation logic gets focused tests; the two
- Terminology canon ✅ — no new
feature/--featuresurface; internal - Supply-chain safety ✅ N/A — no dependency add/upgrade/remove; nothing to vet.
- Locking-primitive ban gate ✅ — #4858 uses
kernel.locksviafeature_status_lockonly. - Dead-symbol discipline (C-007) ✅ — uses the live
kernel.atomic.atomic_write; does NOT - No
__init__.pycoupling (C-009) ✅ — changes avoidsrc/specify_cli/__init__.py, so no
No violations → Complexity Tracking empty.
Project Structure
Documentation (this mission)
kitty-specs/verdict-matrix-rmw-preservation-01M32M9G/
├── plan.md # This file
├── spec.md # Hardened spec
├── research.md # Phase 0 output (this command)
├── data-model.md # Phase 1 output (this command)
├── quickstart.md # Phase 1 output (this command)
├── contracts/ # Phase 1 output (this command)
├── checklists/requirements.md
├── research/ # grounding-4858.md, grounding-4868.md, hardened-4858-spec-draft.md
└── tracer-*.md # design-decisions, approach, tooling-friction, squad-findings
Source Code (repository root) — files this mission touches
src/specify_cli/
├── cli/commands/agent/
│ ├── acceptance_verdict.py # #4858: lock + re-read + single-row splice; slow check outside lock
│ └── issue_verdict.py # #4868: pass read_dir into _migrate_if_needed's migrate call
├── acceptance/
│ └── matrix.py # #4858: write_acceptance_matrix → kernel.atomic.atomic_write
└── tasks/
└── issue_matrix_migration.py # #4868: migrate_issue_matrix_to_json gains optional read_dir
# Reused unchanged:
src/specify_cli/status/locking.py # feature_status_lock (git-common-dir keyed)
src/kernel/atomic.py # atomic_write
tests/
├── specify_cli/acceptance/test_acceptance_verdict_command.py # #4858 flat + criterion concurrency
├── integration/test_accept_matrix_coord_partition.py # #4858 coord (reuse _build_coord_mission_for_matrix)
├── integration/test_issue_verdict_coord_legacy_md_preservation.py # #4868 NEW integration repro
└── specify_cli/cli/commands/agent/test_issue_verdict_command.py # #4868 flat control (cite, don't rewrite)
Structure Decision: Single-project CLI. Two disjoint fix loci (acceptance vs issue), no new modules, no new package boundaries.
Complexity Tracking
No Constitution Check violations — none.
Implementation Concerns → (translated to Work Packages by /spec-kitty.tasks)
Two independent concerns, disjoint file sets → parallelizable, each ATDD red-first:
1. Red-first: deterministic serialized read1→run2→finish1 harness (no threads) with the command-module-bound one-shot seam wrapper; disk-reload + fail→pass assertions; lock spy (key == matrix_dir.name, path under common dir); strict order lock.enter → read → write → lock.exit (re-read inside the lock); call-order (check before lock); atomic-write spy; fail-closed-on-timeout (patch lock to raise → no write, non-zero exit); reported overall_verdict == disk; flat + coord (two worktree roots) + criterion-mode variants. (Site the coord concurrency test in test_acceptance_verdict_command.py, which already imports _build_coord_mission_for_matrix; do not edit the shared fixture file.) 2. Fix in acceptance_verdict.py: run the slow check outside the lock; materialize the coord worktree, then acquire feature_status_lock with a bounded timeout (fail closed on timeout); re-read the on-disk matrix (surface == commit surface); recompute criterion index / unknown-criterion from the re-read; splice the single owned row — criterion by id, NI via a replace-or-append-judged-row helper (NOT _register_negative_invariant); write+commit; emit overall_verdict from the re-read+ spliced matrix. In matrix.py: write_acceptance_matrix → atomic_write (tempfile in matrix_dir, no residue).
1. Red-first: integration repro (real write-seam) via _build_coord_mission_for_matrix (new file tests/integration/test_issue_verdict_coord_legacy_md_preservation.py); seed coord legacy .md with #A; record #B; assert both survive + migrated is True; spy that the migration's write_issue_matrix received feature_dir == primary (mutation-tested RED vs the read_dir-as-write variant — a residue-only check is insufficient); 2nd-call idempotency (migrated=False, rows preserved); malformed-.md → structured IssueVerdictError. 2. Fix: migrate_issue_matrix_to_json(feature_dir, *, read_dir=None, ...) reads legacy from read_dir or feature_dir; _migrate_if_needed passes read_dir=read_dir; write staging on primary unchanged (C-011); translate a malformed-.md validation failure into a structured IssueVerdictError.
- Concern A (#4858, P0) — acceptance-verdict concurrency lost-update.
- Concern B (#4868, P1) — issue-verdict wrong-source migration.
Parallel Work Analysis
Dependency Graph
Concern A (#4858) ─┐
├─► (independent, disjoint files) ─► aggregate review + CHANGELOG/docs
Concern B (#4868) ─┘
Work Distribution
(acceptance_verdict.py/matrix.py vs issue_verdict.py/issue_matrix_migration.py).
- Sequential work: none between the two concerns — they touch disjoint files
- Parallel streams: Concern A and Concern B can be implemented in parallel lanes.
- Agent assignments: one lane per concern; no file overlap → no coordination conflict.
Coordination Points
PR to skupstream/main).
reports' blast radii, union).
- Sync:
spec-kitty mergeconsolidates both lanes into localmain(then a topic branch + - Integration tests: run the full blast-radius surface after both lanes land (both grounding
Supply-Chain / Adversarial Evidence
coherence); all contested findings folded (accepted/changed), none dropped. A post-plan squad runs next per the mission's point-cut cadence, and a pre-merge squad before the PR.
- Supply-chain: N/A — no dependency add/upgrade/remove.
- Adversarial evidence: two post-spec squads ran (3 lenses on #4858, 2 on #4868 + fold