Implementation Plan: Blocked-WP unblock path (alloc-failure recovery)
Branch: fix/3937-blocked-wp-unblock | Date: 2026-09-11 | Spec: spec.md Input: Feature specification from kitty-specs/blocked-wp-unblock-path-01M29093/spec.md
Summary
Fix issue #3937 (findings F-50, F-51). A tooling/environment workspace-allocation failure during spec-kitty implement must leave the work package in planned (not manufacture an unrecoverable blocked), print the conflict's actionable resolution step, and the CLI's transition refusals must state the legal recovery instead of demanding a fabricated review-feedback artifact for a non-review transition. Root cause: transition policy is keyed on a single lane rather than the typed edge (source→target + cause), so blocked is overloaded and → planned is hard-coded as "review rejection". The fix is two narrow, behavior-preserving-elsewhere edits plus honest messaging — no new lifecycle semantics.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: typer (CLI), rich (console), existing specify_cli.status FSM (wp_state.py, transitions.py, transition_pipeline.py), specify_cli.lanes.worktree_allocator Storage: append-only status.events.jsonl (event log) — sole authority for WP lane state; no schema change Testing: pytest (PWHEADLESS=1 .venv/bin/python -m pytest) — mypy --strict, ruff Target Platform: Linux/macOS/Windows CLI Project Type: single (CLI/library) Performance Goals: N/A (no hot path touched; CLI < 2s unchanged) Constraints: Zero rows of tests/status/fsm_parity_baseline.jsonl may change (NFR-002); enumeration surfaced only on the CLI/emit refusal path, never the FSM-core string; no blocked → in_progress recovery driver (C-001); F-51 fixed by source-lane-scoped early-return, not guard-tuple reorder (C-002) Scale/Scope: ~3 source files + their targeted tests; no new dependency
Design (from the pre-spec research + review squad)
Anchors verified on this branch; full trace in research.md.
F-50 — stop manufacturing a block on allocation failure (R1)
src/specify_cli/cli/commands/implement.py: theexcepthandler (~:1993-1998) calls_emit_blocked_on_alloc_failure(~:1445-1476), which emitsplanned → blockedeven thoughcreate_lane_workspace(~:1952) ran before the claim (~:1966), so the WP is stillplanned.- Change: on allocation failure, do NOT emit
blocked; leave the WPplannedand print the exception'snext_step(worktree_allocator.py:89forDependencyLaneMergeConflictError; the analogous field onPlanningCommitMergeConflictError, ~:150-181) — an actionable resolution step, not a bare "re-run". The allocator already aborts + hard-resets and does not writelanes.json, so the persisted worktree is a reentrancy vehicle, not stranded state (no disk cleanup added). - This aligns the CLI with the already-correct orchestrator-api path (
orchestrator_api/commands.py:1268-1303), which never emitsblocked.
F-51 — honest, single-stage transition refusal (R2)
src/specify_cli/cli/commands/agent/tasks_transition_core.py:_guard_planned_rollback(~:561-575) keys only ontarget == PLANNEDand demands--review-feedback-file("cannot be bypassed with --force") before any FSM legality check.- Change: early-return from that guard when the source lane (
req.old_lane, ~:109) is not a review-family lane, so ablocked → plannedrequest falls through to FSM legality (transition_pipeline.py:288/wp_state.py), which reports the illegal edge. Do NOT reorder the_GUARDStuple (would shift persist-signal prefixes at ~:716/:733) — C-002. - Surface the legal targets: enumerate the current state's
allowed_targets()on the CLI/emit refusal path only, never the machine-readable string atwp_state.py:176/181(that would change ~1500fsm_parity_baseline.jsonlrows — NFR-002). src/specify_cli/status/work_package_lifecycle.py:256: the "cannot start implementation" reject (which ablockedWP hits after the PLANNED/CLAIMED/IN_PROGRESS checks) names the legal recovery command for the current state (message-only; FR-007).
Constitution Check (charter)
GATE: passes.
- ATDD-first (C-011): each WP lands a RED-first acceptance test as its first commit, pinning observable STATE (NFR-001) — no
planned→blockedevent; refusal identity is illegal-transition; a fake feedback file cannot flip the verdict. - Single canonical authority: enumeration is sourced from the authoritative per-state
allowed_targets()— no second matrix. - Architectural gate discipline / no fixture churn (NFR-002): change kept off the FSM-core string.
- Smallest-viable-diff + locality: two guarded edits + messaging; R3/#1711 recovery driver explicitly out of scope (C-001).
- Terminology canon: no new flags; messages use canonical terms (C-004).
- No new dependency → supply-chain section N/A.
Project Structure
Documentation (this mission)
kitty-specs/blocked-wp-unblock-path-01M29093/
├── plan.md # this file
├── spec.md
├── research.md # design rationale + adversarial evidence
├── data-model.md # blocked-lane transition model
├── contracts/ # behavioral contract for the two findings
└── tasks.md # /spec-kitty.tasks output (later)
Source Code (repository root)
src/specify_cli/
├── cli/commands/
│ ├── implement.py # F-50: neuter alloc-failure blocked emit; print next_step
│ └── agent/tasks_transition_core.py # F-51: source-lane-scoped early-return; enumerate legal targets
├── status/
│ └── work_package_lifecycle.py # FR-007: start-implementation reject names recovery
└── lanes/worktree_allocator.py # (read-only) source of the actionable next_step
tests/
├── integration/test_status_emit_on_alloc_failure.py # rewrite the bug-encoding assertion as the F-50 AC test
├── status/test_transitions.py # F-51 refusal identity / legal-target enumeration
└── (targeted) tests/agent/…, tests/specify_cli/cli/commands/agent/…
Structure Decision: Single project. Edits are localized to the three named source files; the allocator is read-only (we consume its next_step).
Parallel Work Analysis
Dependency Graph
WP01 (F-50: implement.py alloc-failure) ─┐
├─→ (independent files; can be parallel lanes)
WP02 (F-51: transition refusal + reject) ┘
Work Distribution
- Sequential work: none strictly required — the two findings touch disjoint files (
implement.pyvstasks_transition_core.py/work_package_lifecycle.py). - Parallel streams: F-50 and F-51 are independent; the tasks phase may slice them as two WPs (one lane each) or, given the small size, a single WP with two ATDD commits. Decision deferred to
/spec-kitty.tasks. - Agent assignments: python-pedro for both; reviewer-renata reviews (reviewer≠implementer).
Coordination Points
- No shared file between the two findings → no lane-merge coordination needed beyond consolidation.
- Integration check: the required test surface (transitions, alloc-failure emit, move-task guards) plus
make test-fastat consolidation.