fix(plan-phase): recover safely from quota exhaustion
This commit is contained in:
@@ -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
|
||||
<revision_context>
|
||||
**Phase:** {phase_number}
|
||||
|
||||
@@ -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 <planner|checker|revision>` immediately before each corresponding
|
||||
Agent dispatch. Call `gsd_plan_stage_end <marker|stalled|quota-exceeded|unrecognized>` 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.
|
||||
32
tests/plan-phase-agent-recovery.test.cjs
Normal file
32
tests/plan-phase-agent-recovery.test.cjs
Normal file
@@ -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\}"/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user