diff --git a/gsd-core/workflows/plan-phase.md b/gsd-core/workflows/plan-phase.md index 6b6060eb2..88a62ccfd 100644 --- a/gsd-core/workflows/plan-phase.md +++ b/gsd-core/workflows/plan-phase.md @@ -591,7 +591,8 @@ map is refreshed first. (`drift_action: auto-remap` stays at `execute:wave:post` ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null || true ``` -**If exists AND no `--reviews` flag:** Offer: 1) Add more plans, 2) View existing, 3) Replan from scratch. +**If exists AND no `--reviews` flag:** Offer: 1) Verify existing plans (resume an interrupted run; +proceed to step 10), 2) Add more plans, 3) View existing, 4) Replan from scratch. ## 7. Use Context Paths from INIT @@ -729,8 +730,15 @@ Read+execute `gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md` (d `gsd_stall_should_recover`/`gsd_stall_watch`, and how `{outputFile}` below is bound; independent of the teams-status guard above, AC2). +## 7.995. Agent Recovery and Stage Timing + +Read+execute `gsd-core/workflows/plan-phase/steps/agent-recovery-and-timing.md`. Its retry guard, +quota classifier, and stage timers apply to planner, checker, and revision dispatches. + ## 8. Spawn gsd-planner Agent +Run `gsd_plan_stage_start planner` immediately before dispatch. + Display banner: ``` ### GSD ► PLANNING PHASE {X} @@ -966,6 +974,8 @@ If `section_manifest` is `null` or `"chunked-planning-mode"` is in its `included ## 9. Handle Planner Return +Call `gsd_plan_stage_end` with the outcome. Before fallback or retry, apply step 7.995. + - **`## PLANNING COMPLETE`:** Display plan count. If `--skip-verify` or `plan_checker_enabled` is false (from init): skip to step 13. Otherwise: step 10. - **`## PHASE SPLIT RECOMMENDED`:** The planner determined the phase exceeds the context budget for full-fidelity implementation of all source items. Handle in step 9b. - **`## ⚠ 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. @@ -978,7 +988,7 @@ If `section_manifest` is `null` or `"chunked-planning-mode"` is in its `included **Triggered when:** Agent() returns but the return contains no recognized marker (`## PLANNING COMPLETE`, `## PHASE SPLIT RECOMMENDED`, `## ⚠ Source Audit`, `## CHECKPOINT REACHED`, `## PLANNING INCONCLUSIVE`). ```bash -DISK_PLANS=$(gsd_run query find-phase "${PHASE_NUMBER}" | jq -r '.plan_count_all // 0') +DISK_PLANS=$(gsd_run query find-phase "${phase_number}" | jq -r '.plan_count_all // 0') ``` If `DISK_PLANS` is greater than 0 (a known Windows stdio hang pattern — the planner wrote plans to disk but the return never arrived), offer: 1) Accept plans (treat as `## PLANNING COMPLETE`), 2) Retry planner (return to step 8), 3) Stop. If it is 0 and no marker, treat as `## PLANNING INCONCLUSIVE`. Full banner text: `gsd-core/workflows/plan-phase/detail/elaboration.md` § 9a. @@ -993,6 +1003,8 @@ When the planner returns `## ⚠ Source Audit: Unplanned Items Found`, items fro ## 10. Spawn gsd-plan-checker Agent +Run `gsd_plan_stage_start checker` immediately before dispatch. + Display banner: ``` ### GSD ► VERIFYING PLANS @@ -1094,6 +1106,8 @@ Agent( ## 11. Handle Checker Return +Call `gsd_plan_stage_end` with the outcome. Before fallback or retry, apply step 7.995. + - **`marker_received` + `## VERIFICATION PASSED`:** Display confirmation, proceed to step 13. - **`marker_received` + `## ISSUES FOUND`:** Display issues, check iteration count, proceed to step 12. - **`stalled`:** Automatically surface 11a's recovery choice (Accept verification / Retry checker / Stop) — no manual interrupt needed. @@ -1106,7 +1120,7 @@ Agent( **Triggered when:** Checker Agent() returns but the return contains neither `## VERIFICATION PASSED` nor `## ISSUES FOUND`. ```bash -DISK_PLANS=$(gsd_run query find-phase "${PHASE_NUMBER}" | jq -r '.plan_count_all // 0') +DISK_PLANS=$(gsd_run query find-phase "${phase_number}" | jq -r '.plan_count_all // 0') ``` If `DISK_PLANS` is greater than 0 (plans exist on disk; a known Windows stdio hang pattern), offer: 1) Accept verification (treat as `## VERIFICATION PASSED`, continue to step 13), 2) Retry checker (return to step 10), 3) Stop. If it is 0, something is seriously wrong — display error and stop. Full banner text: `gsd-core/workflows/plan-phase/detail/elaboration.md` § 11a. @@ -1145,6 +1159,9 @@ Set `prev_issue_count = issue_count`. Revision prompt: +Before dispatch, run the stale-run guard from step 7.995, then `gsd_plan_stage_start revision`. +On return, call `gsd_plan_stage_end` and apply step 7.995 before any retry. + ```markdown **Phase:** {phase_number} diff --git a/gsd-core/workflows/plan-phase/steps/agent-recovery-and-timing.md b/gsd-core/workflows/plan-phase/steps/agent-recovery-and-timing.md new file mode 100644 index 000000000..e3575cebd --- /dev/null +++ b/gsd-core/workflows/plan-phase/steps/agent-recovery-and-timing.md @@ -0,0 +1,51 @@ +# Agent Recovery and Stage Timing + +Initialize once before the first planning agent is dispatched: + +```bash +PLAN_PHASE_STARTED_AT=$(date +%s) +PLAN_PHASE_STAGE_STARTED_AT=$PLAN_PHASE_STARTED_AT +PLAN_PHASE_BASELINE_SUMMARIES=$(gsd_run query find-phase "${phase_number}" | jq -r '.summary_count // 0') + +gsd_plan_stage_start() { + PLAN_PHASE_STAGE="$1" + PLAN_PHASE_STAGE_STARTED_AT=$(date +%s) + echo "◆ Stage ${PLAN_PHASE_STAGE} started" +} + +gsd_plan_stage_end() { + _now=$(date +%s) + echo "◆ Stage ${PLAN_PHASE_STAGE:-unknown}: $((_now - PLAN_PHASE_STAGE_STARTED_AT))s (${1:-complete}); plan-phase total: $((_now - PLAN_PHASE_STARTED_AT))s" +} + +gsd_plan_external_execution_started() { + _current_summaries=$(gsd_run query find-phase "${phase_number}" | jq -r '.summary_count // 0') + [ "$_current_summaries" -gt "$PLAN_PHASE_BASELINE_SUMMARIES" ] +} + +gsd_plan_classify_return() { + gsd_run query agent.classify-failure -- "$1" +} +``` + +Call `gsd_plan_stage_start ` immediately before each corresponding +Agent dispatch. Call `gsd_plan_stage_end ` exactly +once when that dispatch ends, before retrying, stopping, or moving on. This exposes elapsed time +even when an agent fails or exhausts quota. + +Before every retry or revision dispatch, call `gsd_plan_external_execution_started`. If it +succeeds, stop this stale planning run immediately: another session produced a new SUMMARY.md +since this run started. Report baseline/current counts and direct the user to `/gsd:progress`. + +For every empty, truncated, errored, or unrecognized Agent return, read the complete return body +(for a background agent, read its bound `{outputFile}`), then run: + +```bash +PLAN_AGENT_FAILURE=$(gsd_plan_classify_return "$AGENT_RETURN_BODY") +PLAN_AGENT_FAILURE_CLASS=$(printf '%s' "$PLAN_AGENT_FAILURE" | jq -r '.class // "unknown-failure"') +``` + +If the class is `quota-exceeded`, call `gsd_plan_stage_end quota-exceeded`, show `sentinel` and +`retryAfterSeconds` when present, and stop cleanly. Never offer or perform an immediate retry. +Preserve PLAN.md files and tell the user to rerun `/gsd:plan-phase {PHASE}` after reset; step 6 +offers verification of existing plans. Non-quota failures continue to the existing fallback. diff --git a/tests/plan-phase-agent-recovery.test.cjs b/tests/plan-phase-agent-recovery.test.cjs new file mode 100644 index 000000000..b64f33b2d --- /dev/null +++ b/tests/plan-phase-agent-recovery.test.cjs @@ -0,0 +1,32 @@ +// allow-test-rule: source-text-is-the-product — workflow markdown is the runtime contract. +'use strict'; +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const root = path.join(__dirname, '..'); +const workflow = fs.readFileSync(path.join(root, 'gsd-core/workflows/plan-phase.md'), 'utf8'); +const recovery = fs.readFileSync(path.join(root, 'gsd-core/workflows/plan-phase/steps/agent-recovery-and-timing.md'), 'utf8'); + +describe('plan-phase agent recovery and timing', () => { + test('classifies quota failures and forbids immediate retry', () => { + assert.match(recovery, /agent\.classify-failure/); + assert.match(recovery, /class is `quota-exceeded`/); + assert.match(recovery, /Never offer or perform an immediate retry/); + }); + test('stops stale planning after execution starts', () => { + assert.match(recovery, /PLAN_PHASE_BASELINE_SUMMARIES/); + assert.match(recovery, /summary_count/); + assert.match(recovery, /find-phase "\$\{phase_number\}"/); + assert.match(recovery, /stop this stale planning run immediately/); + }); + test('times all three agent stages and quota exits', () => { + for (const stage of ['planner', 'checker', 'revision']) assert.match(workflow, new RegExp(`gsd_plan_stage_start ${stage}`)); + assert.match(recovery, /gsd_plan_stage_end quota-exceeded/); + assert.match(recovery, /plan-phase total/); + }); + test('existing plans resume directly at verification', () => { + assert.match(workflow, /Verify existing plans[\s\S]{0,160}proceed to step 10/); + assert.doesNotMatch(workflow, /find-phase "\$\{PHASE_NUMBER\}"/); + }); +});