feat(plan-phase): chunked mode + filesystem fallback for Windows stdio hang (#2499)
* feat(plan-phase): chunked mode + filesystem fallback for Windows stdio hang (#2310) Addresses the 2026-04-16 Windows incident where gsd-planner wrote all 5 PLAN.md files to disk but Task() never returned, hanging the orchestrator for 30+ minutes. Two mitigations: 1. Filesystem fallback (steps 9a, 11a): when Task() returns with an empty/truncated response but PLAN.md files exist on disk, surface a recoverable prompt (Accept plans / Retry planner / Stop) instead of silently failing. Directly addresses the post-restart recovery path. 2. Chunked mode (--chunked flag / workflow.plan_chunked config): splits the single long-lived planner Task into a short outline Task (~2 min) followed by N short per-plan Tasks (~3-5 min each). Each plan is committed individually for crash resilience. A hang loses one plan, not all of them. Resume detection skips plans already on disk on re-run. RCA confirmed: task state mtime 14:29 vs PLAN.md writes 14:32-14:52 = subagent completed normally, IPC return was dropped by Windows stdio deadlock. Neither mitigation fixes the root cause (requires upstream Task() timeout support); both bound damage and enable recovery. New reference file planner-chunked.md keeps OUTLINE COMPLETE / PLAN COMPLETE return formats out of gsd-planner.md (which sits at 46K near its size limit). Closes #2310 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plan-phase): address CodeRabbit review comments on #2499 - docs/CONFIGURATION.md: add workflow.plan_chunked to full JSON schema example - plan-phase.md step 8.5.1: validate PLAN-OUTLINE.md with grep for OUTLINE COMPLETE marker before reusing (not just file existence) - plan-phase.md step 8.5.2: validate per-plan PLAN.md has YAML frontmatter (head -1 grep for ---) before skipping in resume path - plan-phase.md: add language tags (text/javascript/bash) to bare fenced code blocks in steps 8.5, 9a, 11a (markdownlint MD040) - Rejected: commit_docs gate on per-plan commits (gsd-sdk query commit already respects commit_docs internally — comment was a false positive) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plan-phase): route Accept-plans through step 9 PLANNING COMPLETE handling Honors --skip-verify / plan_checker_enabled=false in 9a fallback path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_
|
||||
|
||||
## 2. Parse and Normalize Arguments
|
||||
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`).
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`).
|
||||
|
||||
Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from init JSON is `true`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for Claude Code remote sessions (`/rc` mode) where TUI menus don't work through the Claude App.
|
||||
|
||||
@@ -69,6 +69,15 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}"
|
||||
|
||||
**Existing artifacts from init:** `has_research`, `has_plans`, `plan_count`.
|
||||
|
||||
Set `CHUNKED_MODE` from flag or config:
|
||||
```bash
|
||||
CHUNKED_CFG=$(gsd-sdk query config-get workflow.plan_chunked 2>/dev/null || echo "false")
|
||||
CHUNKED_MODE=false
|
||||
if [[ "$ARGUMENTS" =~ --chunked ]] || [[ "$CHUNKED_CFG" == "true" ]]; then
|
||||
CHUNKED_MODE=true
|
||||
fi
|
||||
```
|
||||
|
||||
## 2.5. Validate `--reviews` Prerequisite
|
||||
|
||||
**Skip if:** No `--reviews` flag.
|
||||
@@ -795,6 +804,8 @@ Every task MUST include these fields — they are NOT optional:
|
||||
</quality_gate>
|
||||
```
|
||||
|
||||
**If `CHUNKED_MODE` is `false` (default):** Spawn the planner as a single long-lived Task:
|
||||
|
||||
```
|
||||
Task(
|
||||
prompt=filled_prompt,
|
||||
@@ -804,6 +815,112 @@ Task(
|
||||
)
|
||||
```
|
||||
|
||||
**If `CHUNKED_MODE` is `true`:** Skip the Task() call above — proceed to step 8.5 instead.
|
||||
|
||||
## 8.5. Chunked Planning Mode
|
||||
|
||||
**Skip if `CHUNKED_MODE` is `false`.**
|
||||
|
||||
Chunked mode splits the single long-lived planner Task into a short outline Task followed by
|
||||
N short per-plan Tasks. Each Task is bounded to ~3–5 min; each plan is committed individually
|
||||
for crash resilience. If any Task hangs and the terminal is force-killed, rerunning
|
||||
`/gsd-plan-phase {N} --chunked` resumes from the last successfully committed plan.
|
||||
|
||||
**Intended for new or in-progress chunked runs.** To recover plans already written by a prior
|
||||
*non-chunked* run, use step 6's "Add more plans" or proceed directly to `/gsd-execute-phase`
|
||||
— don't start a fresh chunked run over existing non-chunked plans.
|
||||
|
||||
### 8.5.1 Outline Phase (outline-only mode, ~2 min)
|
||||
|
||||
**Resume detection:** If `${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md` already exists **and
|
||||
is valid** (contains the `## OUTLINE COMPLETE` marker), skip this sub-step — the outline
|
||||
already exists from a previous run. Proceed directly to 8.5.2.
|
||||
|
||||
```bash
|
||||
OUTLINE_FILE="${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md"
|
||||
if [[ -f "$OUTLINE_FILE" ]] && grep -q "^## OUTLINE COMPLETE" "$OUTLINE_FILE"; then
|
||||
# reuse existing outline — skip to 8.5.2
|
||||
fi
|
||||
```
|
||||
|
||||
Display:
|
||||
```text
|
||||
◆ Chunked mode: spawning outline planner...
|
||||
```
|
||||
|
||||
Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
|
||||
PLAN.md files:
|
||||
|
||||
```javascript
|
||||
Task(
|
||||
prompt="{same planning_context as step 8, plus:}
|
||||
|
||||
**Chunked mode: outline-only.**
|
||||
Do NOT write any PLAN.md files in this Task.
|
||||
Write only: {PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md
|
||||
|
||||
The outline must be a markdown table with columns:
|
||||
Plan ID | Objective | Wave | Depends On | Requirements
|
||||
|
||||
Return: ## OUTLINE COMPLETE with plan count.",
|
||||
subagent_type="gsd-planner",
|
||||
model="{planner_model}",
|
||||
description="Outline Phase {phase} (chunked)"
|
||||
)
|
||||
```
|
||||
|
||||
Handle return:
|
||||
- **`## OUTLINE COMPLETE`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
|
||||
- **Any other return or empty:** Display error. Offer: 1) Retry outline, 2) Stop.
|
||||
|
||||
### 8.5.2 Per-Plan Tasks (single-plan mode, ~3-5 min each)
|
||||
|
||||
For each plan entry extracted from `PLAN-OUTLINE.md`:
|
||||
|
||||
1. **Resume check:** If `${PHASE_DIR}/{plan_id}-PLAN.md` already exists on disk **and has
|
||||
valid YAML frontmatter** (opening `---` delimiter present), skip this plan (do not
|
||||
overwrite completed work — resume safety).
|
||||
|
||||
```bash
|
||||
PLAN_FILE="${PHASE_DIR}/${plan_id}-PLAN.md"
|
||||
if [[ -f "$PLAN_FILE" ]] && head -1 "$PLAN_FILE" | grep -q '^---'; then
|
||||
continue # plan already written, skip
|
||||
fi
|
||||
```
|
||||
|
||||
2. Display:
|
||||
```text
|
||||
◆ Chunked mode: planning {plan_id} ({k}/{N})...
|
||||
```
|
||||
|
||||
3. Spawn the planner in **single-plan** mode — it must write exactly one PLAN.md file:
|
||||
```javascript
|
||||
Task(
|
||||
prompt="{same planning_context as step 8, plus:}
|
||||
|
||||
**Chunked mode: single-plan.**
|
||||
Write exactly ONE plan file: {PHASE_DIR}/{plan_id}-PLAN.md
|
||||
Plan to write: {plan_id} — {objective}
|
||||
Wave: {wave} | Depends on: {depends_on}
|
||||
Phase requirement IDs to cover in this plan: {plan_requirements}
|
||||
|
||||
Return: ## PLAN COMPLETE with the plan ID.",
|
||||
subagent_type="gsd-planner",
|
||||
model="{planner_model}",
|
||||
description="Plan {plan_id} (chunked {k}/{N})"
|
||||
)
|
||||
```
|
||||
|
||||
4. **Verify disk:** Check `${PHASE_DIR}/{plan_id}-PLAN.md` exists. If missing: offer 1) Retry, 2) Stop.
|
||||
|
||||
5. **Commit per-plan:**
|
||||
```bash
|
||||
gsd-sdk query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" "${PHASE_DIR}/${plan_id}-PLAN.md"
|
||||
```
|
||||
|
||||
After all N plans are written and committed, treat this as `## PLANNING COMPLETE` and continue
|
||||
to step 9.
|
||||
|
||||
## 9. Handle Planner Return
|
||||
|
||||
- **`## PLANNING COMPLETE`:** Display plan count. If `--skip-verify` or `plan_checker_enabled` is false (from init): skip to step 13. Otherwise: step 10.
|
||||
@@ -811,6 +928,35 @@ Task(
|
||||
- **`## ⚠ Source Audit: Unplanned Items Found`:** The planner's multi-source coverage audit found items from REQUIREMENTS.md, RESEARCH.md, ROADMAP goal, or CONTEXT.md decisions that are not covered by any plan. Handle in step 9c.
|
||||
- **`## CHECKPOINT REACHED`:** Present to user, get response, spawn continuation (step 12)
|
||||
- **`## PLANNING INCONCLUSIVE`:** Show attempts, offer: Add context / Retry / Manual
|
||||
- **Empty / truncated / no recognized marker:** → Filesystem fallback (step 9a).
|
||||
|
||||
## 9a. Filesystem Fallback (Planner)
|
||||
|
||||
**Triggered when:** Task() returns but the return contains no recognized marker (`## PLANNING COMPLETE`, `## PHASE SPLIT RECOMMENDED`, `## ⚠ Source Audit`, `## CHECKPOINT REACHED`, `## PLANNING INCONCLUSIVE`).
|
||||
|
||||
```bash
|
||||
DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l | tr -d ' ')
|
||||
```
|
||||
|
||||
**If `DISK_PLANS` > 0:** The planner wrote plans to disk but the Task() return was empty or
|
||||
truncated (the Windows stdio hang pattern — the subagent finished but the return never
|
||||
arrived). Display:
|
||||
|
||||
```text
|
||||
◆ Planner wrote {DISK_PLANS} plan(s) to disk but did not emit a PLANNING COMPLETE marker.
|
||||
This is a known Windows stdio hang pattern — work is likely recoverable.
|
||||
|
||||
Plans found on disk:
|
||||
{ls output of *-PLAN.md}
|
||||
```
|
||||
|
||||
Offer 3 options:
|
||||
1. **Accept plans** — treat as `## PLANNING COMPLETE` and continue through step 9 `## PLANNING COMPLETE` handling (so `--skip-verify` / `plan_checker_enabled=false` are honored — may skip to step 13 rather than step 10)
|
||||
2. **Retry planner** — re-spawn the planner with the same prompt (return to step 8)
|
||||
3. **Stop** — exit; user can re-run `/gsd-plan-phase {N}` to resume
|
||||
|
||||
**If `DISK_PLANS` is 0 and no marker:** The planner produced no output. Treat as
|
||||
`## PLANNING INCONCLUSIVE` and handle accordingly.
|
||||
|
||||
## 9b. Handle Phase Split Recommendation
|
||||
|
||||
@@ -925,6 +1071,7 @@ Task(
|
||||
|
||||
- **`## VERIFICATION PASSED`:** Display confirmation, proceed to step 13.
|
||||
- **`## ISSUES FOUND`:** Display issues, check iteration count, proceed to step 12.
|
||||
- **Empty / truncated / no recognized marker:** → Filesystem fallback (step 11a).
|
||||
|
||||
**Thinking partner for architectural tradeoffs (conditional):**
|
||||
If `features.thinking_partner` is enabled, scan the checker's issues for architectural tradeoff keywords
|
||||
@@ -945,6 +1092,29 @@ Apply this to the revision? [Yes] / [No, I'll decide]
|
||||
If yes: include the recommendation in the revision prompt. If no: proceed to revision loop as normal.
|
||||
If thinking_partner disabled: skip this block entirely.
|
||||
|
||||
## 11a. Filesystem Fallback (Checker)
|
||||
|
||||
**Triggered when:** Checker Task() returns but the return contains neither `## VERIFICATION PASSED` nor `## ISSUES FOUND`.
|
||||
|
||||
```bash
|
||||
DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l | tr -d ' ')
|
||||
```
|
||||
|
||||
**If `DISK_PLANS` > 0:** Plans exist on disk; the checker return was empty or truncated (the
|
||||
Windows stdio hang pattern — the subagent finished but the return never arrived). Display:
|
||||
|
||||
```text
|
||||
◆ Checker return was empty or truncated. {DISK_PLANS} plan(s) exist on disk.
|
||||
This is a known Windows stdio hang pattern — checker may have completed without returning.
|
||||
```
|
||||
|
||||
Offer 3 options:
|
||||
1. **Accept verification** — treat as `## VERIFICATION PASSED` and continue to step 13
|
||||
2. **Retry checker** — re-spawn the checker with the same prompt (return to step 10)
|
||||
3. **Stop** — exit; user can re-run `/gsd-plan-phase {N}` to resume
|
||||
|
||||
**If `DISK_PLANS` is 0:** No plans on disk — something is seriously wrong. Display error and stop.
|
||||
|
||||
## 12. Revision Loop (Max 3 Iterations)
|
||||
|
||||
Track `iteration_count` (starts at 1 after initial plan + check).
|
||||
|
||||
Reference in New Issue
Block a user