Tasks: Close #2160 Coord-Shadows Read/Gate Arm

Mission: coord-shadows-arm-closeout-01KXAST2 | Branch: rework/ray-cluster-aggregation Plan: plan.md | Spec: spec.md

Aligns the faithful five-PR aggregate (#2503/#2505/#2511/#2514/#2515) already on this branch into one coherent slice that genuinely closes epic #2160's read/gate arm: one canonical subtask-row walk, a fully-closed emit-layer fail-open (every code path, not just the orchestrator door), a lane-recovery path that never re-leaks status files, and a live claim-liveness check. Folds in #1231 (claim friction) and closes #1862 verified-already-fixed.

6 WPs, 30 subtasks. Keystone WP01 → sequential single-owner chain WP02 → WP03 (critical path); WP04/WP05/WP06 run fully parallel to the spine and to each other.

Subtask Index

IDDescriptionWPParallel
T001Extract _walk_wp_section generator (first-WPxx-token + fence + break-on-exit)WP01
T002Rewire read counters onto _walk_wp_section (behavior parity)WP01
T003Rewrite uncheck_wp_section_subtask_rows onto _walk_wp_section (behavior change: no re-entry)WP01
T004Exactly one CHECKED/UNCHECKED pattern survives in the moduleWP01
T005NFR-005 bite battery (re-appearing heading, section-end, nested headings, depends: mention, fences, T1000+)WP01
T006Prove guard/dashboard-count/rollback-uncheck agree on the full batteryWP01
T007SPIKE: confirm the 4 production call sites + WorkspaceRootNotFound gotchaWP02
T008Reimplement _infer_subtasks_complete row logic on count_wp_section_subtask_rowsWP02
T009Remove the fail-open on missing tasks.md (absent → block, never complete)WP02
T010Thread primary surface at aggregate.py:717 via resolve_planning_read_dirWP02
T011Thread primary surface at emit.py:532/:685 with WorkspaceRootNotFound guardWP02
T012Thread primary surface at status_transition.py:444WP02
T013Regression test: native agent status --to for_review blocks via aggregate.py production routeWP02
T014Full status-suite check; confirm native move-task guard untouchedWP02
T015Remove the #2511 per-door pre-derivation block in orchestrator_api/commands.pyWP03
T016Re-point #2511 test coverage at the shared-layer behavior (judge-the-test)WP03
T017Dead-code gate green (no orphaned symbols from the removal)WP03
T018Hoist coordination_branch/short_id computation above the recovery branchWP04[P]
T019Extract _register_sparse_checkout_if_coord; call from BOTH create + recovery pathsWP04[P]
T020Tests: coord-lane recovery registers sparse-checkout; non-coord lane stays no-opWP04[P]
T021Closeout: lint/type/architectural verification; non-coord path unchangedWP04[P]
T023Create core/process_liveness.py (is_process_alive, promoted from sync/daemon)WP05[P]
T024Repoint sync/daemon.py to import from the new moduleWP05[P]
T025Wire core/stale_detection.py to suppress "stale" for a live claiming shell_pidWP05[P]
T026NFR-004 matrix tests + stale_detection live-claim testWP05[P]
T027_mt_reset_for_planned_rollback consolidation seam (no new behavior)WP06[P]
T028Test: rollback leaves the WP fully re-implementable (SC-006)WP06[P]
T029FR-009 regression guard: checkbox-insensitive freshness hashing (write + gate-check paths)WP06[P]
T030Author issue-matrix.md (terminal verdicts)WP06[P]
T031Full verification (tests, ruff, mypy, arch suite, issue-matrix schema)WP06[P]

WP01 — IC-ROWS: one canonical subtask-row walk

Summary: Collapse the two divergent section-walk loops in core/subtask_rows.py onto one private _walk_wp_section generator, consumed identically by the read counters and the uncheck writer, so guard / dashboard / rollback all agree on what counts as a subtask row. Deps: none (keystone — WP02 consumes count_wp_section_subtask_rows). Independent test: uv run pytest tests/specify_cli/core/test_subtask_rows.py tests/specify_cli/core/test_uncheck_wp_section_subtask_rows.py -q. Prompt: tasks/WP01-subtask-row-canon.md

  • ✅ T001 Extract _walk_wp_section generator (WP01)
  • ✅ T002 Rewire read counters onto _walk_wp_section (WP01)
  • ✅ T003 Rewrite uncheck writer onto _walk_wp_section — behavior change (WP01)
  • ✅ T004 Exactly one CHECKED/UNCHECKED pattern remains (WP01)
  • ✅ T005 NFR-005 bite battery (WP01)
  • ✅ T006 Guard/dashboard/rollback agreement proof (WP01)

Sketch: add _walk_wp_section(lines, wp_id) -> Iterator[tuple[int, str, bool]] encoding the first-WPxx-token heading rule, fenced-code skipping, and break-on-section-exit exactly once; rewire iter_wp_section_subtask_rows (and its count_wp_section_subtask_rows wrapper) onto it with behavior parity; rewrite uncheck_wp_section_subtask_rows onto it too — this corrects the writer's current re-enter-on-reappearing-heading bug to match the guard's canonical break semantic. Bite battery proves the divergence fixtures (re-appearing heading foremost) and that all three consumer shapes (counter/iterator/writer) now agree.

Estimated prompt size: ~430 lines.

WP02 — IC-EMIT-CORE: close the emit-layer fail-open at its callers

Summary: Reimplement _infer_subtasks_complete on WP01's canonical counter, remove the tasks.md-absent fail-open, and thread the primary planning surface through all four production callers (status/emit.py x2, status/aggregate.py:717, coordination/status_transition.py:444). The largest WP — the class-closer. Deps: WP01. Independent test: uv run pytest tests/specify_cli/status/ -q incl. test_infer_subtasks_primary.py driven through the production aggregate.py route. Prompt: tasks/WP02-emit-core.md

  • ✅ T007 SPIKE: confirm 4 call sites + WorkspaceRootNotFound gotcha (WP02)
  • ✅ T008 Reimplement row logic on count_wp_section_subtask_rows (WP02)
  • ✅ T009 Remove the fail-open on missing tasks.md (WP02)
  • ✅ T010 Thread primary surface at aggregate.py:717 (WP02)
  • ✅ T011 Thread primary surface at emit.py:532/:685 w/ guard (WP02)
  • ✅ T012 Thread primary surface at status_transition.py:444 (WP02)
  • ✅ T013 Regression test: native for_review blocked on unchecked rows (WP02)
  • ✅ T014 Full status-suite check; native guard untouched (WP02)

Sketch: delete the divergent regex/heading/fence-blind loop inside _infer_subtasks_complete in favor of core.subtask_rows.count_wp_section_subtask_rows; delete the if not tasks_path.exists(): return True fail-open, flipping the polarity to block-on-absent; resolve resolve_planning_read_dir(..., kind=TASKS_INDEX) at all four callers, guarding the two nullable-repo_root emit.py sites with a try/except WorkspaceRootNotFound fallback so non-repo unit tests don't regress. tasks_shared._check_unchecked_subtasks (the native move-task guard) is explicitly out of scope — already correct.

Estimated prompt size: ~470 lines.

WP03 — IC-EMIT-DEDUP: retire #2511's per-door pre-derivation

Summary: Remove the orchestrator-api's now-redundant per-door subtasks_complete pre-derivation (#2511), leaving the door to block solely through the shared emit layer WP02 fixed; re-point its test coverage; confirm dead-code-safe. Deps: WP02 (structural — the door's block path must be primary-correct before the per-door patch is safe to remove). Independent test: uv run pytest tests/specify_cli/orchestrator_api/test_transition_subtask_gate.py -q + the dead-symbol architectural test. Prompt: tasks/WP03-emit-dedup.md

  • □ T015 Remove the #2511 pre-derivation block + its local import (WP03)
  • □ T016 Re-point #2511 tests at shared-layer behavior (WP03)
  • □ T017 Dead-code gate green (WP03)

Sketch: delete orchestrator_api/commands.py's if subtasks_complete is None and not force: block (~:1418-1430) and its function-local _infer_subtasks_complete import; subtasks_complete flows through unmodified, so the orchestrator door blocks solely via coordination/status_transition.py:444. Judge (not delete) the #2511 test coverage — re-point assertions from the removed mechanism onto the observable blocked/allowed outcome. Confirm _infer_subtasks_complete and _planning_read_dir both retain their other live callers.

Estimated prompt size: ~260 lines.

WP04 — IC-LANE: sparse-checkout on lane recovery [P]

Summary: Fix the #2514 recovery-path regression — a recovered coord-topology lane must apply the same sparse-checkout exclusion as a freshly-created one. Scoped solely to FR-006; FR-008 (an allocator-side stale-claim liveness check) has been withdrawn — the allocator has no stale-claim decision to wire it into. Deps: none — parallel to the WP01→WP02→WP03 spine. No import or build-order relationship with WP05. Independent test: uv run pytest tests/specify_cli/lanes/test_worktree_allocator_recovery.py -q. Prompt: tasks/WP04-lane-recovery.md

  • ✅ T018 Hoist coordination_branch/short_id above the recovery branch (WP04)
  • ✅ T019 Extract _register_sparse_checkout_if_coord; call from both paths (WP04)
  • ✅ T020 Tests: coord recovery registers sparse-checkout; non-coord stays no-op (WP04)
  • ✅ T021 Closeout: lint/type/architectural verification; non-coord path unchanged (WP04)

Sketch: close-by-construction, not mirrored guards — hoist the two values both paths need above the recovery branch, extract one _register_sparse_checkout_if_coord helper, call it from both the fresh-create and recovery branches so they cannot drift again.

Estimated prompt size: ~300 lines.

WP05 — IC-LIVENESS: one claim-liveness helper (indicator + daemon) [P]

Summary: Promote the existing sync/daemon._is_process_alive psutil helper into a new low-level core/process_liveness.py (no core → sync layering inversion), repoint its consumers — core/stale_detection.py (the sole new consumer) and sync/daemon.py (via a thin re-export alias, since dashboard/lifecycle.py imports _is_process_alive from that module) — and wire core/stale_detection.py to suppress "stale" for a live claiming shell_pid. Deps: none — parallel to the spine and to WP04/WP06. WP04 does not consume this WP's output (FR-008 withdrawn). Independent test: uv run pytest tests/specify_cli/core/test_process_liveness.py -q + stale_detection live-claim test. Prompt: tasks/WP05-claim-liveness.md

  • ✅ T023 Create core/process_liveness.py (promoted is_process_alive) (WP05)
  • ✅ T024 Repoint sync/daemon.py to the new module (WP05)
  • ✅ T025 Wire stale_detection.py liveness suppression via task_utils frontmatter reader (WP05)
  • ✅ T026 NFR-004 matrix + live-claim stale_detection test (WP05)

Sketch: move _is_process_alive's body verbatim (psutil-only, NoSuchProcessFalse, AccessDeniedTrue, bare except ExceptionFalse, never raises) into core/process_liveness.is_process_alive, importing only psutil + stdlib; repoint sync/daemon.py's three call sites; thread the claiming shell_pid (read via the existing task_utils.support.WorkPackage.shell_pid reader — no new frontmatter parse) into check_wp_staleness's decision, suppressing "stale" when the process is live and falling back to the existing timestamp heuristic otherwise.

Estimated prompt size: ~300 lines.

WP06 — IC-TIDY: rollback-reset seam, freshness regression guard, issue-matrix [P]

Summary: Route the existing rollback-to-planned resets through one named seam (no new behavior); pin #1764's checkbox-insensitive freshness hashing with a regression guard only (no new logic); author issue-matrix.md. Deps: none — parallel to everything; owns tasks_move_task.py exclusively (C-003). Independent test: uv run pytest tests/specify_cli/cli/commands/agent/test_freshness_checkbox_insensitive.py -q + a production-path move-task --to planned rollback test. Prompt: tasks/WP06-tidy.md

  • ✅ T027 _mt_reset_for_planned_rollback consolidation seam (WP06)
  • ✅ T028 Test: rollback leaves the WP fully re-implementable (WP06)
  • ✅ T029 FR-009 regression guard: checkbox-insensitive freshness hashing (WP06)
  • ✅ T030 Author issue-matrix.md (WP06)
  • ✅ T031 Full verification (WP06)

Sketch: consolidate the existing agent/shell_pid frontmatter clear (#2514) and subtask uncheck (#2513, via _mt_uncheck_rollback_subtasks) onto one clearly-named seam without changing the status-lock boundary either reset currently runs relative to; pin (do not re-implement) analysis_report._normalize_tasks_md's checkbox-stripping on both write_analysis_report and check_analysis_report_current; author the mission's terminal-verdict issue-matrix using the canonical Issue | Verdict | Evidence ref | Scope schema.

Estimated prompt size: ~330 lines.


Sequencing

WP01 (IC-ROWS) ──▶ WP02 (IC-EMIT-CORE) ──▶ WP03 (IC-EMIT-DEDUP)   critical path
WP04 (IC-LANE)    ┐
WP05 (IC-LIVENESS) ├─ fully parallel with the WP01→WP02→WP03 spine
WP06 (IC-TIDY)     ┘

through coordination/status_transition.py:444 once the per-door pre-derivation is gone; that line must already be primary-correct (WP02) before WP03 removes the patch.

liveness consult that previously coupled them) has been withdrawn; WP04 no longer imports anything from core.process_liveness.

(WP02) / orchestrator_api/commands.py (WP03) / worktree_allocator.py (WP04) / process_liveness.py+stale_detection.py+sync/daemon.py-repoint (WP05) / tasks_move_task.py+issue-matrix.md (WP06) — no overlaps.

  • Keystone: WP01 before WP02 — WP02 consumes count_wp_section_subtask_rows.
  • Structural (not just preferred): WP03 after WP02 — the orchestrator door blocks solely
  • No cross-lane coupling: WP04 and WP05 are fully independent — FR-008 (the allocator-side
  • Disjoint ownership (C-003): subtask_rows.py (WP01) / emit.py+aggregate.py+status_transition.py