diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index e96c004d4..dc7e702a4 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -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. + diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 287557a7a..1a041d644 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 | diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index d9aa053df..3af04df8b 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -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", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 1a8bf0391..7ec26381c 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -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. --- diff --git a/get-shit-done/bin/lib/config-schema.cjs b/get-shit-done/bin/lib/config-schema.cjs index b474d5a20..503b4c3f4 100644 --- a/get-shit-done/bin/lib/config-schema.cjs +++ b/get-shit-done/bin/lib/config-schema.cjs @@ -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', diff --git a/get-shit-done/references/planner-chunked.md b/get-shit-done/references/planner-chunked.md new file mode 100644 index 000000000..1b227a3b5 --- /dev/null +++ b/get-shit-done/references/planner-chunked.md @@ -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. diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 1c4551f1e..27d5e2410 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -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 `, `--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 `, `--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: ``` +**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). diff --git a/tests/enh-2310-chunked-plan-phase.test.cjs b/tests/enh-2310-chunked-plan-phase.test.cjs new file mode 100644 index 000000000..d4592310d --- /dev/null +++ b/tests/enh-2310-chunked-plan-phase.test.cjs @@ -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' + ); + }); +});