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:
@@ -38,9 +38,9 @@ Exit.
|
||||
<step name="safety_gates">
|
||||
Run hard-stop checks before routing. Exit on first hit unless `--force` was passed.
|
||||
|
||||
If `--force` flag was passed, skip all gates and the consecutive guard.
|
||||
If `--force` flag was passed, skip all gates, Route 0, and the prior-phase completeness prompt.
|
||||
Print a one-line warning: `⚠ --force: skipping safety gates`
|
||||
Then proceed directly to `determine_next_action`.
|
||||
Then proceed directly to `determine_next_action`. (Route 0 and `prior_phase_completeness` are NOT reached under `--force`.)
|
||||
|
||||
**Gate 1: Unresolved checkpoint**
|
||||
Check if `.planning/.continue-here.md` exists:
|
||||
@@ -83,8 +83,67 @@ Use `--force` to bypass this check.
|
||||
```
|
||||
Exit.
|
||||
|
||||
After all three hard-stop gates pass, continue to `resume_incomplete_phase`.
|
||||
</step>
|
||||
|
||||
<step name="resume_incomplete_phase">
|
||||
**Hard invariant: any phase with PLAN.md files lacking matching SUMMARY.md files must be completed before `/gsd:progress --next` routes to any forward action.**
|
||||
|
||||
This catches the common failure mode where a session died mid-execution (hang, token exhaustion, API connection drop) and STATE.md's `current_phase` got advanced past the phase that actually has unfinished work. Without this gate, `/gsd:progress --next` would route by `current_phase` and silently skip the partially-executed phase.
|
||||
|
||||
**Skip if `--no-resume` was passed** (fall through to `prior_phase_completeness`). (`--force` already bypassed all gates and Route 0 at `safety_gates` — it never reaches this step.)
|
||||
|
||||
**Why Route 0 runs here (after Gates 1-3, before the prior-phase defer prompt):** This step is a hard invariant independent of `current_phase`'s value — it must run before any routing rule that reads `current_phase`. Gates 1-3 are cheap repo/state validity checks that must always run — skipping them on the resume path would risk advancing into a broken-state project. The prior-phase completeness-scan DEFER PROMPT, however, must NOT run in the default (no-flag) case when Route 0 is about to resume the phase automatically: that would force a double-decision (prompt first, then resume anyway), overriding the user's choice. Route 0 placed here means: default = resume silently (no defer prompt); `--no-resume` = skip Route 0 and fall through to the prior-phase defer prompt in `prior_phase_completeness`; `--force` = jump straight to `determine_next_action` at `safety_gates` (never reaches Route 0 or `prior_phase_completeness` at all).
|
||||
|
||||
Scan ALL phases in ROADMAP order (lowest-numbered to highest) for incomplete-execution state. Use `$GSD_SDK query roadmap.analyze` to get the phase list, then for each phase number `N` query `$GSD_SDK query find-phase <N>` JSON and inspect its `plans` and `summaries` arrays. A phase is **incomplete-execution** when `plans.length > summaries.length` (at least one PLAN.md has no matching SUMMARY.md).
|
||||
|
||||
Stop at the first such phase. Record its phase number as `INCOMPLETE_PHASE`. This is the lowest-numbered phase that needs continued execution.
|
||||
|
||||
Illustrative bash:
|
||||
|
||||
```bash
|
||||
INCOMPLETE_PHASE=""
|
||||
ROADMAP_JSON=$($GSD_SDK query roadmap.analyze)
|
||||
if [ $? -ne 0 ] || [ -z "$ROADMAP_JSON" ]; then
|
||||
echo "⚠ WARNING: resume-incomplete-phase scan could not run (roadmap.analyze failed)." >&2
|
||||
echo " The incomplete-phase invariant (#160) could not be verified." >&2
|
||||
echo " Proceeding to prior-phase completeness check — review project state carefully." >&2
|
||||
# Fall through to prior_phase_completeness rather than silently skipping
|
||||
else
|
||||
for PHASE_NUM in $(echo "$ROADMAP_JSON" | jq -r '.phases[] | (.number // .phase_number // empty)'); do
|
||||
PHASE_JSON=$($GSD_SDK query find-phase "$PHASE_NUM")
|
||||
if [ $? -ne 0 ] || [ -z "$PHASE_JSON" ]; then
|
||||
echo "⚠ WARNING: Could not query phase $PHASE_NUM — skipping in resume scan." >&2
|
||||
continue
|
||||
fi
|
||||
PLAN_COUNT=$(echo "$PHASE_JSON" | jq '(.plans // []) | length')
|
||||
SUMMARY_COUNT=$(echo "$PHASE_JSON" | 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:** route to `/gsd:execute-phase $INCOMPLETE_PHASE` and exit. Display a one-line notice before invoking:
|
||||
|
||||
```
|
||||
▶ Resuming incomplete Phase ${INCOMPLETE_PHASE} (plans without summaries detected)
|
||||
/gsd:execute-phase ${INCOMPLETE_PHASE}
|
||||
(use --no-resume to skip this check and defer via the prior-phase prompt)
|
||||
```
|
||||
|
||||
Then invoke via SlashCommand. Do not continue to subsequent steps.
|
||||
|
||||
**If `INCOMPLETE_PHASE` is empty:** continue to `prior_phase_completeness`.
|
||||
</step>
|
||||
|
||||
<step name="prior_phase_completeness">
|
||||
**Prior-phase completeness scan (runs when `--no-resume` was passed and Route 0 was skipped, or when Route 0 found no incomplete-execution phases in the default case). NOT reached under `--force` — that flag jumps directly to `determine_next_action` at `safety_gates`.**
|
||||
|
||||
**Prior-phase completeness scan:**
|
||||
After passing all three hard-stop gates, scan all phases that precede the current phase in ROADMAP.md order for incomplete work. For each prior phase number `N`, use `gsd-tools.cjs query find-phase <N>` JSON (plans, summaries, incomplete_plans, etc.) to inspect that phase.
|
||||
Scan all phases that precede the current phase in ROADMAP.md order for incomplete work. For each prior phase number `N`, use `$GSD_SDK query find-phase <N>` JSON (plans, summaries, incomplete_plans, etc.) to inspect that phase.
|
||||
|
||||
Detect three categories of incomplete work:
|
||||
1. **Plans without summaries** — a PLAN.md exists in a prior phase directory but no matching SUMMARY.md exists (execution started but not completed).
|
||||
@@ -227,6 +286,13 @@ Resume with: `/gsd:progress --next --auto` once resolved.
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Project state correctly detected
|
||||
- [ ] Gates 1-3 (repo/state validity) run first — always, even on the resume path
|
||||
- [ ] Route 0 (resume_incomplete_phase) runs AFTER Gates 1-3 and BEFORE the prior-phase defer prompt — no double-decision in the default (no-flag) case
|
||||
- [ ] Default (no flag): Route 0 resumes incomplete phase silently, exits — user never sees the prior-phase defer prompt
|
||||
- [ ] `--no-resume`: Route 0 skipped, prior_phase_completeness defer prompt runs as before
|
||||
- [ ] `--force`: everything skipped (Gates, Route 0, prior_phase_completeness) → straight to `determine_next_action`
|
||||
- [ ] Scan uses `$GSD_SDK` (canonical resolver form); errors are surfaced rather than suppressed
|
||||
- [ ] Predicate is plans-without-summaries (`plans.length > summaries.length`) — consistent with `determine_next_action` Route 4
|
||||
- [ ] Next action correctly determined from routing rules
|
||||
- [ ] Command invoked immediately without user confirmation
|
||||
- [ ] Clear status shown before invoking
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user