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

@@ -1201,6 +1201,10 @@ Execute: `/gsd-execute-phase {phase} --gaps-only`
Follow templates in checkpoints and revision_mode sections respectively.
## Chunked Mode Returns
See @~/.claude/get-shit-done/references/planner-chunked.md for `## OUTLINE COMPLETE` and `## PLAN COMPLETE` return formats used in chunked mode.
</structured_returns>
<critical_rules>

View File

@@ -43,6 +43,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
"plan_bounce": false,
"plan_bounce_script": null,
"plan_bounce_passes": 2,
"plan_chunked": false,
"code_review_command": null,
"cross_ai_execution": false,
"cross_ai_command": null,
@@ -150,6 +151,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| `workflow.plan_bounce` | boolean | `false` | Run external validation script against generated plans. When enabled, the plan-phase orchestrator pipes each PLAN.md through the script specified by `plan_bounce_script` and blocks on non-zero exit. Added in v1.36 |
| `workflow.plan_bounce_script` | string | (none) | Path to the external script invoked for plan bounce validation. Receives the PLAN.md path as its first argument. Required when `plan_bounce` is `true`. Added in v1.36 |
| `workflow.plan_bounce_passes` | number | `2` | Number of sequential bounce passes to run. Each pass feeds the previous pass's output back into the validator. Higher values increase rigor at the cost of latency. Added in v1.36 |
| `workflow.plan_chunked` | boolean | `false` | Enable chunked planning mode. When `true` (or when `--chunked` flag is passed to `/gsd-plan-phase`), the orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3-5 min each). Each plan is committed individually for crash resilience. If a Task hangs and the terminal is force-killed, rerunning with `--chunked` resumes from the last completed plan. Particularly useful on Windows where long-lived Tasks may hang on stdio. Added in v1.38 |
| `workflow.code_review_command` | string | (none) | Shell command for external code review integration in `/gsd-ship`. Receives changed file paths via stdin. Non-zero exit blocks the ship workflow. Added in v1.36 |
| `workflow.tdd_mode` | boolean | `false` | Enable TDD pipeline as a first-class execution mode. When `true`, the planner aggressively applies `type: tdd` to eligible tasks (business logic, APIs, validations, algorithms) and the executor enforces RED/GREEN/REFACTOR gate sequence. An end-of-phase collaborative review checkpoint verifies gate compliance. Added in v1.36 |
| `workflow.cross_ai_execution` | boolean | `false` | Delegate phase execution to an external AI CLI instead of spawning local executor agents. Useful for leveraging a different model's strengths for specific phases. Added in v1.36 |

View File

@@ -229,6 +229,7 @@
"model-profiles.md",
"phase-argument-parsing.md",
"planner-antipatterns.md",
"planner-chunked.md",
"planner-gap-closure.md",
"planner-reviews.md",
"planner-revision.md",

View File

@@ -265,7 +265,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
---
## References (49 shipped)
## References (50 shipped)
Full roster at `get-shit-done/references/*.md`. References are shared knowledge documents that workflows and agents `@-reference`. The groupings below match [`docs/ARCHITECTURE.md`](ARCHITECTURE.md#references-get-shit-donereferencesmd) — core, workflow, thinking-model clusters, and the modular planner decomposition.
@@ -344,12 +344,13 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
| Reference | Role |
|-----------|------|
| `planner-antipatterns.md` | Planner anti-patterns and specificity examples. |
| `planner-chunked.md` | Chunked mode return formats (`## OUTLINE COMPLETE`, `## PLAN COMPLETE`) for Windows stdio hang mitigation. |
| `planner-gap-closure.md` | Gap-closure mode behavior (reads VERIFICATION.md, targeted replanning). |
| `planner-reviews.md` | Cross-AI review integration (reads REVIEWS.md from `/gsd-review`). |
| `planner-revision.md` | Plan revision patterns for iterative refinement. |
| `planner-source-audit.md` | Planner source-audit and authority-limit rules. |
> **Subdirectory:** `get-shit-done/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 49 top-level references.
> **Subdirectory:** `get-shit-done/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 50 top-level references.
---

View File

@@ -34,6 +34,7 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.plan_bounce',
'workflow.plan_bounce_script',
'workflow.plan_bounce_passes',
'workflow.plan_chunked',
'workflow.security_enforcement',
'workflow.security_asvs_level',
'workflow.security_block_on',

View File

@@ -0,0 +1,49 @@
# Chunked Mode Return Formats
Used when `plan-phase` spawns `gsd-planner` with `CHUNKED_MODE=true` (triggered by `--chunked`
flag or `workflow.plan_chunked: true` config). Splits the single long-lived planner Task into
shorter-lived Tasks to bound the blast radius of Windows stdio hangs.
## Modes
### outline-only
Write **only** `{PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md`. Do not write any PLAN.md files.
Return:
```markdown
## OUTLINE COMPLETE
**Phase:** {phase-name}
**Plans:** {N} plan(s) in {M} wave(s)
| Plan ID | Objective | Wave | Depends On | Requirements |
|---------|-----------|------|-----------|-------------|
| {padded_phase}-01 | [brief objective] | 1 | none | REQ-001, REQ-002 |
| {padded_phase}-02 | [brief objective] | 1 | none | REQ-003 |
```
The orchestrator reads this table, then spawns one single-plan Task per row.
### single-plan
Write **exactly one** `{PHASE_DIR}/{plan_id}-PLAN.md`. Do not write any other plan files.
Return:
```markdown
## PLAN COMPLETE
**Plan:** {plan-id}
**Objective:** {brief}
**File:** {PHASE_DIR}/{plan-id}-PLAN.md
**Tasks:** {N}
```
The orchestrator verifies the file exists on disk after each return, commits it, then moves
to the next plan entry from the outline.
## Resume Behaviour
If the orchestrator detects that `PLAN-OUTLINE.md` already exists (from a prior interrupted
run), it skips the outline-only Task and goes directly to single-plan Tasks, skipping any
`{plan_id}-PLAN.md` files that already exist on disk.

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 ~35 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).

View File

@@ -0,0 +1,236 @@
'use strict';
/**
* Tests for #2310: plan-phase chunked mode + filesystem fallback.
*
* Context: on Windows (and occasionally other platforms), gsd-planner's
* Task() call may never return even though the subagent finished writing all
* PLAN.md files to disk. The orchestrator hangs indefinitely. Two mitigations:
*
* 1. Filesystem fallback (steps 9a, 11a): if the Task() return lacks the
* expected marker but PLAN.md files exist on disk, surface a recoverable
* prompt instead of hanging/failing silently.
*
* 2. Chunked mode (step 8.5): --chunked flag / workflow.plan_chunked config
* splits the single long planner Task into (a) a short outline Task and
* (b) N short single-plan Tasks. Each Task is shorter-lived, the
* orchestrator can commit work incrementally, and a hang loses only one
* plan instead of the entire phase.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const PLAN_PHASE = path.join(
__dirname, '..', 'get-shit-done', 'workflows', 'plan-phase.md'
);
const PLANNER_AGENT = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
const PLANNER_CHUNKED_REF = path.join(__dirname, '..', 'get-shit-done', 'references', 'planner-chunked.md');
const CONFIG_SCHEMA = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'config-schema.cjs');
const CONFIGURATION_MD = path.join(__dirname, '..', 'docs', 'CONFIGURATION.md');
describe('plan-phase.md — filesystem fallback (#2310)', () => {
const content = fs.readFileSync(PLAN_PHASE, 'utf-8');
test('step 9 checks PLAN.md count on disk when planner return lacks completion marker', () => {
assert.ok(
content.includes('DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md'),
'step 9a must check disk for PLAN.md files via DISK_PLANS variable'
);
});
test('step 9a fallback section exists', () => {
assert.ok(
content.includes('## 9a. Filesystem Fallback'),
'plan-phase.md must have a ## 9a. Filesystem Fallback section for planner hang recovery'
);
});
test('step 9a fallback offers Accept plans option', () => {
assert.ok(
content.includes('Accept plans'),
'step 9a must offer "Accept plans" as a recovery option'
);
});
test('step 9a fallback offers Retry planner option', () => {
assert.ok(
content.includes('Retry planner'),
'step 9a must offer "Retry planner" as a recovery option'
);
});
test('step 11 has filesystem fallback section', () => {
assert.ok(
content.includes('## 11a. Filesystem Fallback'),
'plan-phase.md must have a ## 11a. Filesystem Fallback section for checker hang recovery'
);
});
test('step 11a fallback offers Accept verification option', () => {
assert.ok(
content.includes('Accept verification'),
'step 11a must offer "Accept verification" as a recovery option'
);
});
test('step 11a fallback offers Retry checker option', () => {
assert.ok(
content.includes('Retry checker'),
'step 11a must offer "Retry checker" as a recovery option'
);
});
test('step 9 routes to step 9a when no recognized marker found', () => {
assert.ok(
content.includes('step 9a') || content.includes('9a.'),
'step 9 handle-return must reference the filesystem fallback path (step 9a)'
);
});
test('step 11 routes to step 11a when no recognized marker found', () => {
assert.ok(
content.includes('step 11a') || content.includes('11a.'),
'step 11 handle-return must reference the filesystem fallback path (step 11a)'
);
});
});
describe('plan-phase.md — chunked mode flag and config (#2310)', () => {
const content = fs.readFileSync(PLAN_PHASE, 'utf-8');
test('step 2 parses --chunked flag', () => {
assert.ok(
content.includes('--chunked'),
'step 2 must parse --chunked flag from $ARGUMENTS'
);
});
test('step 2 reads workflow.plan_chunked config', () => {
assert.ok(
content.includes('workflow.plan_chunked'),
'step 2 must read workflow.plan_chunked config key'
);
});
test('step 2 sets CHUNKED_MODE variable', () => {
assert.ok(
content.includes('CHUNKED_MODE'),
'step 2 must set CHUNKED_MODE from flag or config'
);
});
});
describe('plan-phase.md — chunked mode implementation (#2310)', () => {
const content = fs.readFileSync(PLAN_PHASE, 'utf-8');
test('step 8.5 chunked planning section exists', () => {
assert.ok(
content.includes('## 8.5.'),
'plan-phase.md must have a step 8.5 section for chunked planning mode'
);
});
test('chunked mode produces PLAN-OUTLINE.md', () => {
assert.ok(
content.includes('PLAN-OUTLINE.md'),
'chunked mode outline step must produce a *-PLAN-OUTLINE.md file'
);
});
test('chunked outline step uses outline-only mode', () => {
assert.ok(
content.includes('outline-only'),
'chunked step 8.5.1 must spawn the planner in outline-only mode'
);
});
test('chunked per-plan step uses single-plan mode', () => {
assert.ok(
content.includes('single-plan'),
'chunked step 8.5.2 must spawn the planner in single-plan mode for each plan'
);
});
test('chunked mode checks for existing outline to enable resume', () => {
// The resume check skips the outline Task if PLAN-OUTLINE.md already exists
assert.ok(
content.includes('PLAN-OUTLINE.md') && content.includes('already exists'),
'chunked mode must detect existing PLAN-OUTLINE.md and skip outline generation (resume safety)'
);
});
test('chunked mode commits each plan individually', () => {
assert.ok(
content.includes('chunked'),
'chunked mode must commit each individual plan for crash resilience'
);
});
test('step 8 routes to chunked path when CHUNKED_MODE is true', () => {
assert.ok(
content.includes('CHUNKED_MODE') && content.includes('8.5'),
'step 8 must route to step 8.5 when CHUNKED_MODE is true'
);
});
});
describe('gsd-planner.md — references planner-chunked.md (#2310)', () => {
const plannerContent = fs.readFileSync(PLANNER_AGENT, 'utf-8');
test('gsd-planner.md references planner-chunked.md for chunked return formats', () => {
assert.ok(
plannerContent.includes('planner-chunked.md'),
'gsd-planner.md must reference planner-chunked.md for ## OUTLINE COMPLETE / ## PLAN COMPLETE formats'
);
});
});
describe('planner-chunked.md — chunked return formats (#2310)', () => {
const content = fs.readFileSync(PLANNER_CHUNKED_REF, 'utf-8');
test('planner-chunked.md defines OUTLINE COMPLETE structured return', () => {
assert.ok(
content.includes('## OUTLINE COMPLETE'),
'planner-chunked.md must define ## OUTLINE COMPLETE return format for outline-only mode'
);
});
test('planner-chunked.md defines PLAN COMPLETE structured return for single-plan mode', () => {
assert.ok(
content.includes('## PLAN COMPLETE'),
'planner-chunked.md must define ## PLAN COMPLETE return format for single-plan mode'
);
});
test('planner-chunked.md describes resume behaviour', () => {
assert.ok(
content.includes('Resume') || content.includes('resume'),
'planner-chunked.md must describe resume behaviour for interrupted chunked runs'
);
});
});
describe('config-schema.cjs — workflow.plan_chunked key (#2310)', () => {
test('VALID_CONFIG_KEYS includes workflow.plan_chunked', () => {
const { VALID_CONFIG_KEYS } = require(CONFIG_SCHEMA);
assert.ok(
VALID_CONFIG_KEYS.has('workflow.plan_chunked'),
'config-schema.cjs VALID_CONFIG_KEYS must include workflow.plan_chunked'
);
});
});
describe('docs/CONFIGURATION.md — workflow.plan_chunked documented (#2310)', () => {
const content = fs.readFileSync(CONFIGURATION_MD, 'utf-8');
test('CONFIGURATION.md documents workflow.plan_chunked', () => {
assert.ok(
content.includes('`workflow.plan_chunked`'),
'docs/CONFIGURATION.md must document workflow.plan_chunked'
);
});
});