fix(#2868): resume a phase stranded between its last plan and verification (#3041)

* fix(#2868): resume a phase stranded between its last plan and verification

discover_and_group_plans exited unconditionally once every plan was filtered
out, conflating "no plan work left" with "phase fully done". Those differ once a
run can be interrupted between the final wave's SUMMARY and the verify step --
most often by a checkpoint plan that is retired but still writes a SUMMARY. The
result was a phase that looked healthy from every index yet had no
VERIFICATION.md, and whose recommended recovery command provably no-opped,
because the only step that produces the artifact sits ten steps past that exit.

The exit is now conditional. When the verification report is genuinely missing
and no filter is active, the run reports the situation by name and continues at
the tail gates instead of stopping.

Two guards keep the normal paths untouched:

- A filtered run (--gaps-only, or an explicit wave) finding nothing left in its
  own slice says nothing about whether the phase as a whole is done, so it exits
  exactly as before. Without this, --wave 1 on a finished first wave would jump
  to verification with later waves still outstanding.
- A phase that already has its report exits as before too.

The recovered path deliberately keeps the code-review and regression gates. The
manual workaround this replaces skipped both, and that gap is the reason a real
route exists rather than telling users to spawn the verifier by hand.

Also acknowledges the emitted growth of the workflow file. As with #2830 it is
appended to the fragment that already owns that path, since the linter hard-fails
when two acknowledgment sources name the same one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2868): never treat a blocked-and-incomplete phase as finished

Three findings from adversarial review, all fixed.

BLOCKER -- the trigger conflated two different zero-runnable states. This step
now has two skip rules: has_summary (the #2868 target) and, from #2830, a skip
for plans whose blocked_by is non-empty. "All filtered" was therefore reachable
with plans that never ran: plan A halts and is summarized, plan B is blocked by
A and has no summary. The resume path fired, announced "All N plans are
summarized" -- false -- skipped the wave steps so B was never dispatched, and
jumped to the gates. B was silently abandoned, which is the same class of
disappearance #2830 exists to prevent, reintroduced one layer up.

The decision is now an explicit ordered three-way: a filtered run exits
unchanged; any blocked-plan skip reports the phase as stuck on a halt and exits,
routing to resolving the halt rather than to verification; only an
all-summarized, unfiltered phase with a missing report resumes.

MAJOR -- RESUME_TAIL_ONLY was set and never read anywhere in the workflow or its
step fragments. Dead state implying enforcement that did not exist. Removed; the
imperative at the decision point is what actually carries the control flow, so it
now says so plainly.

MAJOR -- the resume path skipped aggregate_results, which is the only step that
runs the secure-phase threats-open gate. A phase with open threats would have
advanced with no warning where a normal run always shows one. The path now
enters at aggregate_results, verified to read only on-disk phase artifacts and
independent queries, so it tolerates having executed no plans this run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#2868): backfill changeset pr number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-08-04 08:38:31 -04:00
committed by GitHub
parent ef823ca9d9
commit b146b82e3c
5 changed files with 243 additions and 2 deletions

View File

@@ -340,7 +340,48 @@ Parse JSON for: `phase`, `plans[]` (each with `id`, `wave`, `autonomous`, `objec
**Wave safety check:** If `WAVE_FILTER` is set and there are still incomplete plans in any lower wave that match the current execution mode, STOP and tell the user to finish earlier waves first. Do not let Wave 2+ execute while prerequisite earlier-wave plans remain incomplete.
If all filtered: "No matching incomplete plans" → exit.
**If all filtered — do NOT exit unconditionally (#2868).** "No plan work left" and "phase fully
done" are different conditions: a run can be interrupted between the final wave's SUMMARY and
`verify_phase_goal` (most commonly by a checkpoint plan that is retired but still writes a SUMMARY),
leaving a phase that looks complete from every index yet never produced `*-VERIFICATION.md`. A
third condition looks identical to the first two by plan_count alone but is neither: some filtered
plans were filtered because they are **blocked** (non-empty `blocked_by`, #2830), not because they
are done. Blocked-and-incomplete must never be reported as finished.
```bash
VERIFY_STATUS=$(gsd_run query verification status "${PHASE_DIR}" --pick status)
```
Evaluate in this exact order — the first matching condition decides the outcome; do not evaluate
later conditions once one matches:
1. **A filter is active** (`--gaps-only`, or `WAVE_FILTER` set): report "No matching incomplete
plans" → exit, unchanged. A filtered run finding nothing left in ITS slice says nothing about
whether the phase as a whole is done, and must never jump to verification.
2. **No filter is active, and at least one filtered plan was skipped because of a non-empty
`blocked_by`** (irrespective of `VERIFY_STATUS`): the phase is NOT finished — it is **stuck on a
halt**. A plan with no SUMMARY and no dispatched work must never be treated as done merely
because nothing was left to filter. Report:
`"Phase stuck: {blocked plan ids} blocked by halted {their blocked_by ids} — resolve the halt, do not resume verification."`
→ exit. Do not fall through to condition 3; this is not a completion state.
3. **No filter is active, and every filtered plan was filtered by `has_summary` alone** (no
blocked-plan skip occurred):
- **`VERIFY_STATUS` is anything other than `missing`**: the phase genuinely finished. Report
"No matching incomplete plans" → exit, unchanged.
- **`VERIFY_STATUS == missing`**: the plans are all summarized but the run never reached the
tail gates. Report:
`"All {plan_count} plans are summarized but no VERIFICATION.md exists — resuming at the phase gates (#2868)."`
SKIP `cross_ai_delegation`, `execute_waves` and `checkpoint_handling` — there is no wave work
to do — and continue directly at `aggregate_results`, NOT `code_review_gate`. `aggregate_results`
is the only step that runs the `SECURITY_FILE` / secure-phase threats-open gate, and it reads
exclusively from on-disk `${PHASE_DIR}` artifacts (`*-SUMMARY.md`, `*-SECURITY.md` via `ls`) and
independent `gsd_run` calls — nothing it reads is produced only by `execute_waves` or
`checkpoint_handling` — so it tolerates having executed no plans in this run. From there the
run proceeds exactly as a normal one: `aggregate_results` → `code_review_gate` →
`close_parent_artifacts` → `regression_gate` → `verify_phase_goal` → `update_roadmap`. Never
skip `aggregate_results`, `code_review_gate` or `regression_gate` on this path — the manual
workaround this replaces skipped all three, and that gap is the reason this route exists
rather than telling users to spawn the verifier by hand.
Report:
```