fix(#160): resume partially-executed phases before current_phase routing (#406)

When a session dies mid-execution (hang, token exhaustion, API drop),
STATE.md's current_phase can be advanced past a phase that still has
PLAN.md files without matching SUMMARY.md files. Without this fix,
/gsd-next and /gsd-progress would route by current_phase and silently
skip the partially-executed phase, producing a data-loss-shape outcome.

Adds a Route 0 cross-phase incomplete-execution scan to both
next.md and progress.md. Before any current_phase-based routing,
the scan finds the lowest-numbered phase where plans outnumber
summaries and routes to /gsd-execute-phase <that-phase> to resume it.
Opt out with --no-resume to fall back to the prior-phase defer prompt;
--force bypasses all gates as before.

Rework (codex review):
- Route 0 now ordered AFTER Gates 1-3 (repo/state validity always run)
  but BEFORE the prior-phase completeness-scan defer prompt — eliminating
  the double-decision where the default path would both prompt the user
  (C/S/F) AND resume the phase anyway. Prior-phase defer prompt moved to
  a new prior_phase_completeness step; only reached via --no-resume.
- --force flow made coherent across all three steps: safety_gates jumps
  directly to determine_next_action, skipping Gates, Route 0, AND
  prior_phase_completeness. resume_incomplete_phase and prior_phase_completeness
  now correctly state --force never reaches them. success_criteria entry
  updated to reflect --force → determine_next_action (not prior_phase_completeness).
- Scan uses $GSD_SDK (canonical resolver form) throughout next.md, matching
  the file's existing convention. progress.md uses $ROADMAP already loaded
  by analyze_roadmap. Neither file uses bare gsd-sdk.
- Errors are surfaced rather than suppressed: removed 2>/dev/null on the
  main roadmap.analyze call; added explicit WARNING emission when the scan
  cannot run, so the invariant fails closed instead of failing open.
- Predicate aligned to plans-without-summaries (plans.length > summaries.length)
  in both files, consistent with determine_next_action Route 4.
- command references use canonical /gsd: namespace form

Fixes #160

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-27 20:22:14 -04:00
committed by GitHub
parent fd061fccc5
commit af36416d14
4 changed files with 547 additions and 3 deletions

View File

@@ -169,6 +169,55 @@ When `MVP_MODE=false` (mode is null, absent, or the phase has no `**Mode:**` lin
<step name="route">
**Determine next action based on verified counts.**
**Step 0: Resume-incomplete-phase invariant (Route 0)**
Before any current-phase-scoped counting, scan ALL phases for incomplete execution. This catches the case where STATE.md's `current_phase` was advanced past the phase that actually has unfinished work (common after a mid-execution session death from hang, token exhaustion, or API disruption). Without this guard, the current-phase-scoped count in Step 1 would inspect the wrong phase and the routing would skip the unfinished work.
**Skip if `--no-resume` or `--force` is present in `$ARGUMENTS`.**
Scan all phases via the `$ROADMAP` JSON already loaded in `analyze_roadmap`. For each phase entry, compare `plans` length to `summaries` length using the same plans-without-summaries predicate as `determine_next_action` Route 4 (`plans.length > summaries.length`). Stop at the first (lowest-numbered) phase where the predicate is true. Record its phase number as `INCOMPLETE_PHASE`.
If `$ROADMAP` is empty or the query failed, surface a warning rather than silently proceeding:
```bash
INCOMPLETE_PHASE=""
if [ -z "$ROADMAP" ]; then
echo "⚠ WARNING: resume-incomplete-phase scan could not run (\$ROADMAP is empty)." >&2
echo " The incomplete-phase invariant (#160) could not be verified." >&2
echo " Review project state carefully before continuing." >&2
else
for PHASE_NUM in $(echo "$ROADMAP" | jq -r '.phases[] | (.number // .phase_number)'); do
PHASE_DATA=$(echo "$ROADMAP" | jq --arg n "$PHASE_NUM" '.phases[] | select((.number // .phase_number) == ($n | tonumber))')
PLAN_COUNT=$(echo "$PHASE_DATA" | jq '(.plans // []) | length')
SUMMARY_COUNT=$(echo "$PHASE_DATA" | jq '(.summaries // []) | length')
if [ "${PLAN_COUNT:-0}" -gt "${SUMMARY_COUNT:-0}" ]; then
INCOMPLETE_PHASE="$PHASE_NUM"
break
fi
done
fi
```
**If `INCOMPLETE_PHASE` is non-empty:** emit a one-line resume notice in the routing output and route to `/gsd:execute-phase ${INCOMPLETE_PHASE}` instead of running Step 1's current-phase routing. The progress report (already displayed by the `report` step above) gives the user full project status before this routing decision is shown.
```
---
## ▶ Next Up — Resuming incomplete Phase ${INCOMPLETE_PHASE}
`/clear` then:
`/gsd:execute-phase ${INCOMPLETE_PHASE} ${GSD_WS}`
(plans without summaries detected; use --no-resume to skip this check and route by current_phase instead; --force to skip all gates)
---
```
Then exit the route step. Do NOT run Steps 1 through Routes A-F.
**If `INCOMPLETE_PHASE` is empty:** continue to Step 1.
**Step 1: Count plans, summaries, and issues in current phase**
List files in the current phase directory: