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:
Tom Boucher
2026-04-21 08:40:39 -04:00
committed by GitHub
parent d1b56febcb
commit b2534e8a05
8 changed files with 467 additions and 3 deletions

View File

@@ -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).