fix(#724): block convergence on actionable review findings (#728)

* fix(#724): block convergence on actionable review findings

* merge: integrate clean next (#936 inline) onto author tip + re-apply cursor fixes (Mode field, REVIEWS.md extraction) and review hardening (#724)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeremy McSpadden
2026-06-10 13:54:15 -05:00
committed by GitHub
parent fb37fa7dd5
commit f61b97276e
10 changed files with 250 additions and 79 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 728
---
**`/gsd-plan-review-convergence` now blocks on actionable review findings outside PLAN.md (#724).** The convergence summary contract includes current_actionable alongside current_high, and reviews-mode planning/checking requires actionable MEDIUM/LOW feedback to be incorporated or explicitly deferred in executable PLAN.md content.

View File

@@ -76,6 +76,15 @@ If CONTEXT.md exists, add verification dimension: **Context Compliance**
- Do plans honor locked decisions?
- Are deferred ideas excluded?
- Are discretion areas handled appropriately?
**REVIEWS.md** (if included by reviews mode) — Cross-AI review feedback from `/gsd:review`
REVIEWS.md is audit trail and feedback input, not a hidden execution contract. /gsd:execute-phase primarily consumes PLAN.md plus normal phase context. Add verification dimension: **Review Incorporation**.
- Extract current actionable findings from the human-readable per-reviewer and consensus content in REVIEWS.md. Do NOT look for a `CYCLE_SUMMARY: current_high=<N> current_actionable=<M>` line or `## Current HIGH Concerns` / `## Current Actionable Non-HIGH Concerns` section headers — those machine-readable fields exist only in the convergence orchestrator's return message, never in REVIEWS.md (which contains only human-readable review content).
- Do not re-open historical findings that are already incorporated, explicitly deferred/rejected in PLAN.md, or marked fully resolved.
- Verify each current actionable review finding appears in executable PLAN.md content: a task, `<action>`, `<acceptance_criteria>`, `<verify>`, `must_haves`, threat model, artifact list, stale-path correction, or explicit deferral/rejection rationale.
- If a current actionable finding remains only in REVIEWS.md and would be invisible to /gsd:execute-phase, return `## ISSUES FOUND`. Use WARNING by default; use BLOCKER when the missing incorporation can prevent the phase goal, create unsafe execution, or invalidate verification.
</upstream_input>
<core_principle>

View File

@@ -1,6 +1,6 @@
---
name: gsd:plan-review-convergence
description: "Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain."
description: "Cross-AI plan convergence - replan until review concerns are resolved."
argument-hint: "<phase> [--codex] [--gemini] [--claude] [--opencode] [--ollama] [--lm-studio] [--llama-cpp] [--text] [--ws <name>] [--all] [--max-cycles N]"
allowed-tools:
- Read
@@ -9,19 +9,20 @@ allowed-tools:
- Glob
- Grep
- Agent
- Skill
- AskUserQuestion
requires: [phase, review]
---
<objective>
Cross-AI plan convergence loop — an outer revision gate around gsd-review and gsd-planner.
Repeatedly: review plans with external AI CLIs → if HIGH concerns found → replan with --reviews feedback → re-review. Stops when no HIGH concerns remain or max cycles reached.
Repeatedly: review plans with external AI CLIs → if HIGH or actionable non-HIGH concerns remain → replan with --reviews feedback → re-review. Stops when no unresolved HIGH concerns or actionable MEDIUM/LOW findings remain outside PLAN.md, or when max cycles is reached.
**Flow:** Skill("gsd-plan-phase") → Agent→Skill("gsd-review") → check HIGHs → Skill("gsd-plan-phase --reviews") → Agent→Skill("gsd-review") → ... → Converge or escalate
**Flow:** Skill("gsd-plan-phase") → Agent→Skill("gsd-review") → check unresolved HIGH + actionable non-HIGH → Skill("gsd-plan-phase --reviews") → Agent→Skill("gsd-review") → ... → Converge or escalate
Replaces gsd-plan-phase's internal gsd-plan-checker with external AI reviewers (codex, gemini, etc.). Plan-phase runs **inline** (bare Skill at depth 0) so it can spawn gsd-planner/gsd-plan-checker at depth 1. Review runs inside an isolated Agent (gsd-review is a Bash leaf — no sub-agents needed). Orchestrator only does loop control.
**Orchestrator role:** Parse arguments, validate phase, run plan-phase inline (Skill at depth 0), spawn an Agent for gsd-review, check HIGHs, stall detection, escalation gate.
**Orchestrator role:** Parse arguments, validate phase, run plan-phase inline (Skill at depth 0), spawn an Agent for gsd-review, check unresolved HIGH and actionable non-HIGH counts, stall detection, escalation gate.
</objective>
<execution_context>

View File

@@ -205,7 +205,7 @@ See [Package Legitimacy Gate in the User Guide](USER-GUIDE.md#package-legitimacy
### `/gsd-plan-review-convergence`
Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain. Runs `plan-phase → review → replan → re-review` cycles (max 3 cycles by default). Spawns isolated agents for planning and review; orchestrator handles loop control, HIGH-concern counting, stall detection, and escalation.
Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain and no actionable MEDIUM/LOW findings remain outside `PLAN.md`. Runs `plan-phase → review → replan → re-review` cycles (max 3 cycles by default). Plan-phase runs inline (bare Skill at depth 0 so it can spawn gsd-planner/gsd-plan-checker at depth 1); only gsd-review runs in an isolated Agent. Orchestrator handles loop control, unresolved review counting (HIGH + actionable non-HIGH), stall detection, and escalation.
| Argument / Flag | Required | Description |
|-----------------|----------|-------------|
@@ -214,7 +214,7 @@ Cross-AI plan convergence loop — replan with review feedback until no HIGH con
| `--all` | No | Run every configured reviewer in parallel |
| `--max-cycles N` | No | Override cycle cap (default 3) |
**Exit behavior:** Loop exits when HIGH count hits zero. Stall detection warns when HIGH count is not decreasing across cycles. Escalation gate asks the user to proceed or review manually when `--max-cycles` is hit with HIGH concerns still open.
**Exit behavior:** Loop exits when both `current_high` and `current_actionable` hit zero. Stall detection warns when the total unresolved review count is not decreasing across cycles. Escalation gate asks the user to proceed or review manually when `--max-cycles` is hit with HIGH or actionable non-HIGH concerns still open.
```bash
/gsd-plan-review-convergence 3 # Default reviewers, 3 cycles

View File

@@ -256,7 +256,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| `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.post_planning_gaps` | boolean | `true` | Unified post-planning gap report (#2493). After all plans are generated and committed, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against every PLAN.md in the phase directory, then prints one `Source \| Item \| Status` table. Word-boundary matching (REQ-1 vs REQ-10) and natural sort (REQ-02 before REQ-10). Non-blocking — informational report only. Set to `false` to skip Step 13e of plan-phase. |
| `workflow.plan_review_convergence` | boolean | `false` | Enable the `/gsd-plan-review-convergence` command. Disabled by default — the command exits with an enable instruction when this key is `false`. The command automates the manual plan→review→replan loop: it spawns configured reviewers (Codex, Gemini, Claude, OpenCode, Ollama, LM Studio, llama.cpp), counts unresolved HIGH concerns via the CYCLE_SUMMARY contract, replans with `--reviews` feedback, and repeats until converged or max cycles reached. Enable with `gsd config-set workflow.plan_review_convergence true`. Added in v1.39 |
| `workflow.plan_review_convergence` | boolean | `false` | Enable the `/gsd-plan-review-convergence` command. Disabled by default — the command exits with an enable instruction when this key is `false`. The command automates the manual plan→review→replan loop: it spawns configured reviewers (Codex, Gemini, Claude, OpenCode, Ollama, LM Studio, llama.cpp), counts unresolved HIGH concerns and actionable MEDIUM/LOW findings via the CYCLE_SUMMARY contract, replans with `--reviews` feedback, and repeats until converged or max cycles reached. Enable with `gsd config-set workflow.plan_review_convergence true`. Added in v1.39 |
| `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 |

View File

@@ -85,7 +85,7 @@ These six routers are descriptor-only entries that the model picks first; the bo
| `/gsd-ui-phase` | Generate UI design contract (UI-SPEC.md) for frontend phases. | [commands/gsd/ui-phase.md](../commands/gsd/ui-phase.md) |
| `/gsd-ai-integration-phase` | Generate AI design contract (AI-SPEC.md) via framework selection, research, and eval planning. | [commands/gsd/ai-integration-phase.md](../commands/gsd/ai-integration-phase.md) |
| `/gsd-plan-phase` | Create detailed phase plan (PLAN.md) with verification loop. | [commands/gsd/plan-phase.md](../commands/gsd/plan-phase.md) |
| `/gsd-plan-review-convergence` | Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain (max 3 cycles). | [commands/gsd/plan-review-convergence.md](../commands/gsd/plan-review-convergence.md) |
| `/gsd-plan-review-convergence` | Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns or actionable non-HIGH findings remain (max 3 cycles). | [commands/gsd/plan-review-convergence.md](../commands/gsd/plan-review-convergence.md) |
| `/gsd-ultraplan-phase` | [BETA] Offload plan phase to Claude Code's ultraplan cloud — drafts remotely, review in browser, import back via `/gsd-import`. Claude Code only. | [commands/gsd/ultraplan-phase.md](../commands/gsd/ultraplan-phase.md) |
| `/gsd-spike` | Rapidly spike an idea with throwaway experiments; use `--wrap-up` to package findings as a persistent skill. | [commands/gsd/spike.md](../commands/gsd/spike.md) |
| `/gsd-sketch` | Rapidly sketch UI/design ideas using throwaway HTML mockups; use `--wrap-up` to package findings. | [commands/gsd/sketch.md](../commands/gsd/sketch.md) |
@@ -224,7 +224,7 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that
| `note.md` | Zero-friction idea capture — one Write call, one confirmation line. | `/gsd-capture --note` |
| `pause-work.md` | Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files. | `/gsd-pause-work` |
| `plan-phase.md` | Create executable PLAN.md files with integrated research and verification loop. | `/gsd-plan-phase`, `/gsd-quick` |
| `plan-review-convergence.md` | Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain. | `/gsd-plan-review-convergence` |
| `plan-review-convergence.md` | Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns or actionable non-HIGH findings remain. | `/gsd-plan-review-convergence` |
| `plant-seed.md` | Capture a forward-looking idea as a structured seed file with trigger conditions. | `/gsd-capture --seed` |
| `pr-branch.md` | Create a clean branch for pull requests by filtering `.planning/` commits. | `/gsd-pr-branch` |
| `profile-user.md` | Orchestrate the full developer profiling flow — consent, session scan, profile generation. | `/gsd-profile-user` |

View File

@@ -4,6 +4,8 @@ Triggered when orchestrator sets Mode to `reviews`. Replanning from scratch with
**Mindset:** Fresh planner with review insights — not a surgeon making patches, but an architect who has read peer critiques.
**Execution contract:** REVIEWS.md is audit trail and feedback input, not a second execution contract. /gsd:execute-phase primarily consumes PLAN.md plus the normal phase context. Every current actionable review finding must therefore be incorporated into the relevant PLAN.md or explicitly deferred/rejected in that PLAN.md.
### Step 1: Load REVIEWS.md
Read the reviews file from `<files_to_read>`. Parse:
- Per-reviewer feedback (strengths, concerns, suggestions)
@@ -13,13 +15,14 @@ Read the reviews file from `<files_to_read>`. Parse:
### Step 2: Categorize Feedback
Group review feedback into:
- **Must address**: HIGH severity consensus concerns
- **Should address**: MEDIUM severity concerns from 2+ reviewers
- **Must represent in PLAN.md**: actionable MEDIUM/LOW findings that require task, action, acceptance criteria, verify command, must_haves, threat-model, artifact, stale-path, or execution-contract changes
- **Should address**: MEDIUM severity concerns from 2+ reviewers that improve quality but do not change the executable contract
- **Consider**: Individual reviewer suggestions, LOW severity items
### Step 3: Plan Fresh with Review Context
Create new plans following the standard planning process, but with review feedback as additional constraints:
- Each HIGH severity consensus concern MUST have a task that addresses it
- MEDIUM concerns should be addressed where feasible without over-engineering
- Each current actionable MEDIUM/LOW finding MUST either appear in the relevant PLAN.md executable content or have a deferral/rejection rationale in that PLAN.md
- Note in task actions: "Addresses review concern: {concern}" for traceability
### Step 4: Return

View File

@@ -934,7 +934,7 @@ Planner prompt:
- {PATTERNS_PATH} (Pattern Map — analog files and code excerpts, if exists)
- {verification_path} (Verification Gaps - if --gaps)
- {uat_path} (UAT Gaps - if --gaps)
- {reviews_path} (Cross-AI Review Feedback - if --reviews)
- {reviews_path} (Cross-AI Review Feedback - if --reviews; actionable findings must be incorporated or explicitly deferred/rejected in PLAN.md)
- {UI_SPEC_PATH} (UI Design Contract — visual/interaction specs, if exists)
- {SPIKE_FINDINGS_PATH} (Spike Findings — validated patterns, constraints, landmines from experiments, if exists)
- {SKETCH_FINDINGS_PATH} (Sketch Findings — validated design decisions, CSS patterns, visual direction, if exists)
@@ -955,6 +955,16 @@ ${API_SURFACE_PATH ? `
` : ''}
${AGENT_SKILLS_PLANNER}
<review_incorporation_contract>
**If Mode is reviews:** REVIEWS.md is feedback input, not a hidden execution contract. /gsd:execute-phase primarily consumes PLAN.md plus the normal phase context, so every current actionable review finding must become visible in the relevant PLAN.md before planning can pass.
For each current actionable finding in REVIEWS.md, the planner MUST either:
- incorporate it into a PLAN.md task, `<action>`, `<acceptance_criteria>`, `<verify>`, `must_haves`, threat model, or artifact list; or
- explicitly document a deferral/rejection rationale in the relevant PLAN.md so the executor and reviewer can see the decision.
Historical findings already incorporated, explicitly deferred/rejected in PLAN.md, or marked fully resolved do not require new plan changes.
</review_incorporation_contract>
**Phase requirement IDs (every ID MUST appear in a plan's `requirements` field):** {phase_req_ids}
**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines
@@ -1273,6 +1283,7 @@ Checker prompt:
<verification_context>
**Phase:** {phase_number}
**Phase Goal:** {goal from ROADMAP}
**Mode:** {standard | gap_closure | reviews}
<files_to_read>
- {PHASE_DIR}/*-PLAN.md (Plans to verify)
@@ -1280,10 +1291,17 @@ Checker prompt:
- {requirements_path} (Requirements)
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
- {research_path} (Technical Research — includes Validation Architecture)
- {reviews_path} (Cross-AI Review Feedback - if --reviews; verify actionable findings are represented in PLAN.md)
</files_to_read>
${AGENT_SKILLS_CHECKER}
<review_incorporation_verification>
**If Mode is reviews:** Read REVIEWS.md and verify each current actionable review finding is visible in executable PLAN.md content or explicitly deferred/rejected in the relevant PLAN.md. A finding remains actionable if it requires a concrete plan task, `<action>`, `<acceptance_criteria>`, `<verify>`, `must_haves`, threat-model item, stale-path correction, or execution contract change before /gsd:execute-phase runs.
If an actionable finding remains only in REVIEWS.md and would be invisible to /gsd:execute-phase, return `## ISSUES FOUND`. Use WARNING by default; use BLOCKER when the missing incorporation can prevent the phase goal, create unsafe execution, or invalidate verification.
</review_incorporation_verification>
**Phase requirement IDs (MUST ALL be covered):** {phase_req_ids}
**Project instructions:** Read ./CLAUDE.md if exists — verify plans honor project guidelines

View File

@@ -3,7 +3,7 @@ Cross-AI plan convergence loop — automates the manual chain:
gsd-plan-phase N → gsd-review N --codex → gsd-plan-phase N --reviews → gsd-review N --codex → ...
Plan-phase runs inline (bare Skill at depth 0) so it can spawn gsd-planner/gsd-plan-checker at depth 1.
Review runs inside an isolated Agent (leaf skill — Bash only, no sub-agents needed).
Orchestrator only does: init, loop control, parse CYCLE_SUMMARY for HIGH count, stall detection, escalation.
Orchestrator only does: init, loop control, parse CYCLE_SUMMARY for HIGH and actionable non-HIGH counts, stall detection, escalation.
</purpose>
<required_reading>
@@ -122,7 +122,7 @@ Initialize loop variables:
```text
cycle = 0
prev_high_count = Infinity
prev_unresolved_count = Infinity
```
### 5a. Review (Spawn Agent)
@@ -143,9 +143,10 @@ Complete the full review workflow. Do NOT return until REVIEWS.md is committed.
IMPORTANT — CYCLE_SUMMARY contract (required):
Your final response MUST include a machine-readable line of exactly this form:
CYCLE_SUMMARY: current_high=<N>
CYCLE_SUMMARY: current_high=<N> current_actionable=<M>
Where <N> is the integer count of HIGH-severity concerns that REMAIN UNRESOLVED in this cycle's findings.
Where <M> is the integer count of actionable MEDIUM/LOW concerns that REMAIN UNRESOLVED because the latest PLAN.md files do not yet incorporate them or explicitly defer/reject them.
Counting rules:
INCLUDE in the count:
@@ -157,16 +158,23 @@ Counting rules:
- FULLY RESOLVED HIGHs: concern addressed with verification complete (closed ticket, verification log, or reviewer sign-off)
- HIGH mentions in retrospective/summary tables comparing cycles
- Quoted excerpts from prior reviews referencing past HIGH items
- MEDIUM/LOW concerns that are already incorporated into a PLAN.md task, action, acceptance_criteria, verify command, must_haves item, threat model, artifact list, or explicit deferral/rejection rationale
Definitions:
PARTIALLY RESOLVED — concern acknowledged and mitigation is in progress but not yet verified/completed (e.g., open ticket exists but fix not landed).
FULLY RESOLVED — concern addressed with verification complete (closed ticket, verification log, or explicit reviewer sign-off confirming closure).
ACTIONABLE — a non-HIGH review finding that would be invisible to /gsd:execute-phase unless it is incorporated into PLAN.md or explicitly deferred/rejected in PLAN.md.
Your final response MUST also include this section immediately after the CYCLE_SUMMARY line:
## Current HIGH Concerns
[List each unresolved HIGH with a brief description, one per bullet]
[If none: write exactly 'None.']",
[If none: write exactly 'None.']
## Current Actionable Non-HIGH Concerns
[List each unresolved actionable MEDIUM/LOW with a brief description and the PLAN.md change still needed, one per bullet]
[If none: write exactly 'None.']
These two sections MUST be the final content of your response, in this exact order, with no additional "## " headings after them (the source-grounding "Verification coverage" block is appended to REVIEWS.md, not to this return message).",
mode="auto"
)
```
@@ -194,35 +202,49 @@ REVIEWS_FILE=$(ls ${phase_dir}/${padded_phase}-REVIEWS.md 2>/dev/null)
If REVIEWS_FILE is empty: Error — review agent did not produce REVIEWS.md. Exit.
### 5b. Extract HIGH Count from CYCLE_SUMMARY Contract
### 5b. Extract unresolved counts from CYCLE_SUMMARY Contract
**Do NOT grep REVIEWS.md for HIGH count.** REVIEWS.md accumulates history across cycles — resolved HIGHs from prior cycles remain in the file as audit trail, inflating a raw grep count and causing false stall detection.
**Do NOT grep REVIEWS.md for HIGH or actionable counts.** REVIEWS.md accumulates history across cycles — resolved findings from prior cycles remain in the file as audit trail, inflating a raw grep count and causing false stall detection.
Parse HIGH_COUNT from the review agent's return message via the CYCLE_SUMMARY contract:
Parse HIGH_COUNT and ACTIONABLE_COUNT from the review agent's return message via the CYCLE_SUMMARY contract:
```bash
# Extract the integer from "CYCLE_SUMMARY: current_high=N" in the agent's return message
HIGH_COUNT=$(echo "$REVIEW_AGENT_RETURN" | grep -oE 'CYCLE_SUMMARY:\s*current_high=[0-9]+' | head -1 | grep -oE '[0-9]+$')
# Extract integers from "CYCLE_SUMMARY: current_high=N current_actionable=M" in the agent's return message
SUMMARY_LINE=$(echo "$REVIEW_AGENT_RETURN" | grep -oE 'CYCLE_SUMMARY:.*' | head -1)
HIGH_COUNT=$(echo "$SUMMARY_LINE" | grep -oE 'current_high=[0-9]+' | head -1 | grep -oE '[0-9]+$')
ACTIONABLE_COUNT=$(echo "$SUMMARY_LINE" | grep -oE 'current_actionable=[0-9]+' | head -1 | grep -oE '[0-9]+$')
if [ -z "$HIGH_COUNT" ]; then
# Distinguish malformed contract from completely absent contract
if echo "$REVIEW_AGENT_RETURN" | grep -q 'CYCLE_SUMMARY:'; then
echo "CYCLE_SUMMARY present but current_high is malformed — expected integer, got non-numeric value. Retry or switch reviewer."
else
echo "Review agent did not honor the CYCLE_SUMMARY contract — cannot determine HIGH count. Retry or switch reviewer."
fi
if [ -z "$SUMMARY_LINE" ]; then
echo "Review agent did not honor the CYCLE_SUMMARY contract — cannot determine unresolved review counts. Retry or switch reviewer."
exit 1
fi
if [ -z "$HIGH_COUNT" ]; then
echo "CYCLE_SUMMARY present but current_high is missing or malformed — expected integer, got non-numeric or absent value. Retry or switch reviewer."
exit 1
fi
if [ -z "$ACTIONABLE_COUNT" ]; then
echo "CYCLE_SUMMARY present but current_actionable is missing or malformed — expected integer, got non-numeric or absent value. Retry or switch reviewer."
exit 1
fi
UNRESOLVED_COUNT=$((HIGH_COUNT + ACTIONABLE_COUNT))
# Extract the ## Current HIGH Concerns section from the agent's return message
HIGH_LINES=$(echo "$REVIEW_AGENT_RETURN" | awk '/^## Current HIGH Concerns/{found=1; next} found && /^##/{exit} found{print}')
ACTIONABLE_LINES=$(echo "$REVIEW_AGENT_RETURN" | awk '/^## Current Actionable Non-HIGH Concerns/{found=1; next} found && /^##/{exit} found{print}')
if [ "${HIGH_COUNT}" -gt 0 ] && [ -z "${HIGH_LINES}" ]; then
echo "⚠ Review agent's CYCLE_SUMMARY reports ${HIGH_COUNT} HIGHs but did not provide ## Current HIGH Concerns section — continuing with incomplete escalation details."
fi
if [ "${ACTIONABLE_COUNT}" -gt 0 ] && [ -z "${ACTIONABLE_LINES}" ]; then
echo "⚠ Review agent's CYCLE_SUMMARY reports ${ACTIONABLE_COUNT} actionable non-HIGH concerns but did not provide ## Current Actionable Non-HIGH Concerns section — continuing with incomplete escalation details."
fi
```
**If HIGH_COUNT == 0 (converged):**
**If HIGH_COUNT == 0 and ACTIONABLE_COUNT == 0 (converged):**
```bash
gsd_run state planned-phase --phase "${PHASE}" --name "${phase_name}" --plans "${PLAN_COUNT}"
@@ -236,6 +258,7 @@ Display:
Phase {phase_number} converged in {cycle} cycle(s).
No HIGH concerns remaining.
No actionable MEDIUM/LOW review findings remain outside PLAN.md.
REVIEWS.md: {REVIEWS_FILE}
Next: /gsd:execute-phase {PHASE}
@@ -243,16 +266,16 @@ Display:
Exit — convergence achieved.
**If HIGH_COUNT > 0:** Continue to 5c.
**If HIGH_COUNT > 0 or ACTIONABLE_COUNT > 0:** Continue to 5c.
### 5c. Stall Detection + Escalation Check
Display: `◆ Cycle {cycle}/{MAX_CYCLES} — {HIGH_COUNT} HIGH concerns found`
Display: `◆ Cycle {cycle}/{MAX_CYCLES} — {HIGH_COUNT} HIGH, {ACTIONABLE_COUNT} actionable non-HIGH review concerns found`
**Stall detection:** If `HIGH_COUNT >= prev_high_count`:
**Stall detection:** If `UNRESOLVED_COUNT >= prev_unresolved_count`:
```text
⚠ Convergence stalled — HIGH concern count not decreasing
({HIGH_COUNT} HIGH concerns, previous cycle had {prev_high_count})
⚠ Convergence stalled — unresolved review concern count not decreasing
({UNRESOLVED_COUNT} unresolved concerns, previous cycle had {prev_unresolved_count})
```
**Max cycles check:** If `cycle >= MAX_CYCLES`:
@@ -260,13 +283,15 @@ Display: `◆ Cycle {cycle}/{MAX_CYCLES} — {HIGH_COUNT} HIGH concerns found`
If `TEXT_MODE` is true, present as plain-text numbered list:
```text
Plan convergence did not complete after {MAX_CYCLES} cycles.
{HIGH_COUNT} HIGH concerns remain:
{HIGH_COUNT} HIGH concerns and {ACTIONABLE_COUNT} actionable non-HIGH concerns remain:
{HIGH_LINES}
{ACTIONABLE_LINES}
How would you like to proceed?
1. Proceed anyway — Accept plans with remaining HIGH concerns and move to execution
1. Proceed anyway — Accept plans with remaining review concerns and move to execution
2. Manual review — Stop here, review REVIEWS.md and address concerns manually
Enter number:
@@ -276,11 +301,11 @@ Otherwise use AskUserQuestion:
```js
AskUserQuestion([
{
question: "Plan convergence did not complete after {MAX_CYCLES} cycles. {HIGH_COUNT} HIGH concerns remain:\n\n{HIGH_LINES}\n\nHow would you like to proceed?",
question: "Plan convergence did not complete after {MAX_CYCLES} cycles. {HIGH_COUNT} HIGH concerns and {ACTIONABLE_COUNT} actionable non-HIGH concerns remain:\n\n{HIGH_LINES}\n\n{ACTIONABLE_LINES}\n\nHow would you like to proceed?",
header: "Convergence",
multiSelect: false,
options: [
{ label: "Proceed anyway", description: "Accept plans with remaining HIGH concerns and move to execution" },
{ label: "Proceed anyway", description: "Accept plans with remaining review concerns and move to execution" },
{ label: "Manual review", description: "Stop here — review REVIEWS.md and address concerns manually" }
]
}
@@ -301,7 +326,7 @@ Exit workflow.
**If under max cycles:**
Update `prev_high_count = HIGH_COUNT`.
Update `prev_unresolved_count = UNRESOLVED_COUNT`.
Display: `◆ Replanning inline with review feedback... (plan-phase runs here in the orchestrator — no output until replanning is complete, ~1–5 min; expected, not a freeze)`
@@ -309,7 +334,7 @@ Display: `◆ Replanning inline with review feedback... (plan-phase runs here in
Skill(skill="gsd-plan-phase", args="{PHASE} --reviews --skip-research {GSD_WS}")
```
Run plan-phase **inline** (do NOT wrap it in Agent()). Same rationale as step 4: the convergence orchestrator runs at depth 0 with Agent available, so inline plan-phase can spawn gsd-planner and gsd-plan-checker at depth 1. Wrapping in Agent() pushes plan-phase to depth 1 where the Agent tool is absent — the replan loop can never produce a revised plan when HIGHs are found. This is the root cause of bug #936. Wait until plan-phase completes (outputs '## PLANNING COMPLETE') and updated PLAN.md files are committed before continuing.
Run plan-phase **inline** (do NOT wrap it in Agent()). Same rationale as step 4: the convergence orchestrator runs at depth 0 with Agent available, so inline plan-phase can spawn gsd-planner and gsd-plan-checker at depth 1. Wrapping in Agent() pushes plan-phase to depth 1 where the Agent tool is absent — the replan loop can never produce a revised plan when HIGHs are found. This is the root cause of bug #936. Actionable MEDIUM/LOW findings must be incorporated into executable PLAN.md content or explicitly deferred/rejected in the relevant PLAN.md before convergence can complete. Wait until plan-phase completes (outputs '## PLANNING COMPLETE') and updated PLAN.md files are committed before continuing.
After plan-phase completes → go back to **step 5a** (review again).
@@ -320,13 +345,15 @@ After plan-phase completes → go back to **step 5a** (review again).
- [ ] Initial planning via inline Skill("gsd-plan-phase") if no plans exist — NOT wrapped in Agent() (bug #936: depth-1 Agent has no Agent tool)
- [ ] Review via Agent → Skill("gsd-review") — isolated Agent is correct; gsd-review is a Bash leaf with no sub-agent spawns; {GSD_WS} forwarded
- [ ] Replan via inline Skill("gsd-plan-phase --reviews") — NOT wrapped in Agent(); inline lets plan-phase spawn gsd-planner/gsd-plan-checker at depth 1
- [ ] Orchestrator only does: init, config gate, loop control, parse CYCLE_SUMMARY for HIGH count, stall detection, escalation
- [ ] HIGH count extracted from review agent's CYCLE_SUMMARY return message (not by grepping REVIEWS.md)
- [ ] Review agent prompt defines CYCLE_SUMMARY: current_high=<N> contract with PARTIALLY/FULLY RESOLVED definitions
- [ ] Orchestrator only does: init, config gate, loop control, parse CYCLE_SUMMARY for HIGH and actionable non-HIGH counts, stall detection, escalation
- [ ] HIGH and actionable non-HIGH counts extracted from review agent's CYCLE_SUMMARY return message (not by grepping REVIEWS.md)
- [ ] Review agent prompt defines CYCLE_SUMMARY: current_high=<N> current_actionable=<M> contract with PARTIALLY/FULLY RESOLVED/ACTIONABLE definitions
- [ ] Abort with clear error if CYCLE_SUMMARY is absent; distinguish malformed from absent
- [ ] Warn if HIGH_COUNT > 0 but ## Current HIGH Concerns section is absent from return message
- [ ] Abort with clear error if current_actionable is absent or malformed
- [ ] Warn if ACTIONABLE_COUNT > 0 but ## Current Actionable Non-HIGH Concerns section is absent from return message
- [ ] The review Agent fully completes gsd-review before returning (plan-phase runs inline — no Agent wrap)
- [ ] Loop exits on: no HIGH concerns (converged) OR max cycles (escalation)
- [ ] Stall detection reported when HIGH count not decreasing
- [ ] Loop exits on: no HIGH concerns and no actionable non-HIGH concerns (converged) OR max cycles (escalation)
- [ ] Stall detection reported when total unresolved review concern count is not decreasing
- [ ] STATE.md updated on convergence completion
</success_criteria>

View File

@@ -4,7 +4,7 @@
* Validates that the command source and workflow contain the key structural
* elements required for correct cross-AI plan convergence loop behavior:
* initial planning gate, review agent spawning, CYCLE_SUMMARY contract for
* HIGH count extraction, stall detection, escalation gate, and STATE.md update
* unresolved review count extraction, stall detection, escalation gate, and STATE.md update
* on convergence.
*
* v2 additions (#2306-v2):
@@ -15,6 +15,11 @@
* - PARTIALLY RESOLVED / FULLY RESOLVED definitions in contract
* - HIGH_LINES validation warning when HIGH_COUNT > 0 but section absent
* - Success criteria updated to reflect CYCLE_SUMMARY parsing
*
* v3 additions (#724):
* - CYCLE_SUMMARY includes current_actionable for unresolved actionable MEDIUM/LOW findings
* - convergence requires HIGH_COUNT == 0 and ACTIONABLE_COUNT == 0
* - reviews-mode planner/checker prompts require REVIEWS.md feedback to land in PLAN.md
*/
// allow-test-rule: source-text-is-the-product
@@ -30,6 +35,9 @@ const path = require('path');
const COMMAND_PATH = path.join(__dirname, '..', 'commands', 'gsd', 'plan-review-convergence.md');
const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-review-convergence.md');
const CONFIG_DOC_PATH = path.join(__dirname, '..', 'docs', 'CONFIGURATION.md');
const PLAN_PHASE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-phase.md');
const PLANNER_REVIEWS_PATH = path.join(__dirname, '..', 'gsd-core', 'references', 'planner-reviews.md');
const PLAN_CHECKER_PATH = path.join(__dirname, '..', 'agents', 'gsd-plan-checker.md');
// ─── Command source ────────────────────────────────────────────────────────
@@ -80,10 +88,17 @@ describe('plan-review-convergence command source (#2306)', () => {
);
});
test('command declares Agent in allowed-tools (required for spawning sub-agents)', () => {
test('command declares Agent in allowed-tools (required for spawning review sub-agents)', () => {
assert.ok(
command.includes('- Agent'),
'Agent must be in allowed-tools — command spawns isolated agents for planning and reviewing'
'Agent must be in allowed-tools — command spawns isolated agents for reviewing'
);
});
test('command declares Skill in allowed-tools (required for inline plan-phase invocations)', () => {
assert.ok(
command.includes('- Skill'),
'Skill must be in allowed-tools — command invokes gsd-plan-phase inline via Skill() at depth 0 (#936 fix)'
);
});
@@ -182,7 +197,7 @@ describe('plan-review-convergence workflow: initial planning gate (#2306)', () =
test('workflow skips initial planning when plans already exist', () => {
assert.ok(
workflow.includes('has_plans') || workflow.includes('plan_count'),
'workflow must check whether plans already exist before spawning planning agent'
'workflow must check whether plans already exist before running inline planning'
);
});
@@ -213,10 +228,10 @@ describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
);
});
test('workflow extracts HIGH count from CYCLE_SUMMARY contract, NOT from grepping REVIEWS.md', () => {
test('workflow extracts HIGH and actionable counts from CYCLE_SUMMARY contract, NOT from grepping REVIEWS.md', () => {
// Critical regression guard: REVIEWS.md accumulates history across cycles;
// resolved HIGHs from cycle N remain in the file during cycle N+1 as audit trail,
// inflating raw grep counts and causing false stalls. HIGH count must come from
// inflating raw grep counts and causing false stalls. Counts must come from
// the review agent's CYCLE_SUMMARY return message, not from the file.
assert.ok(
workflow.includes('CYCLE_SUMMARY'),
@@ -226,6 +241,10 @@ describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
workflow.includes('current_high'),
'workflow must parse current_high from CYCLE_SUMMARY line'
);
assert.ok(
workflow.includes('ACTIONABLE_COUNT') && workflow.includes('current_actionable'),
'workflow must parse current_actionable from CYCLE_SUMMARY line (#724)'
);
});
test('workflow aborts if review agent omits CYCLE_SUMMARY contract', () => {
@@ -245,6 +264,13 @@ describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
);
});
test('workflow fails closed when current_actionable is missing or malformed', () => {
assert.ok(
workflow.includes('current_actionable is missing or malformed'),
'missing or malformed current_actionable must abort instead of silently treating actionable findings as zero (#724)'
);
});
test('review agent spawn forwards --ws via GSD_WS (symmetric with replan agent)', () => {
// Critical correctness bug: if GSD_WS is not forwarded to the review agent,
// the review reads from the wrong workspace while replanning reads from the correct one.
@@ -257,12 +283,14 @@ describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
);
});
test('workflow exits loop when HIGH_COUNT == 0 (converged)', () => {
test('workflow exits loop only when HIGH_COUNT and ACTIONABLE_COUNT are zero', () => {
assert.ok(
workflow.includes('HIGH_COUNT == 0') ||
workflow.includes('HIGH_COUNT === 0') ||
workflow.includes('converged'),
'workflow must exit the loop when no HIGH concerns remain'
workflow.includes('HIGH_COUNT == 0 and ACTIONABLE_COUNT == 0'),
'workflow must require both HIGH_COUNT and ACTIONABLE_COUNT to be zero before convergence (#724)'
);
assert.ok(
workflow.includes('If HIGH_COUNT > 0 or ACTIONABLE_COUNT > 0'),
'current_high=0 with current_actionable>0 must continue to replan/escalation instead of converging (#724)'
);
});
@@ -273,17 +301,17 @@ describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
);
});
test('workflow spawns replan agent with --reviews flag', () => {
test('workflow invokes inline replan with --reviews flag', () => {
assert.ok(
workflow.includes('--reviews'),
'replan agent must pass --reviews so gsd-plan-phase incorporates review feedback'
'inline replan must pass --reviews so gsd-plan-phase incorporates review feedback'
);
});
test('workflow passes --skip-research to replan agent (research already done)', () => {
test('workflow passes --skip-research to inline replan (research already done)', () => {
assert.ok(
workflow.includes('--skip-research'),
'replan agent must skip research — only initial planning needs research'
'inline replan must skip research — only initial planning needs research'
);
});
});
@@ -293,10 +321,10 @@ describe('plan-review-convergence workflow: convergence loop (#2306)', () => {
describe('plan-review-convergence workflow: CYCLE_SUMMARY contract definition (#2306-v2)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('review agent prompt defines CYCLE_SUMMARY: current_high=<N> format', () => {
test('review agent prompt defines CYCLE_SUMMARY current_high/current_actionable format', () => {
assert.ok(
workflow.includes('CYCLE_SUMMARY: current_high='),
'review agent spawn prompt must define the CYCLE_SUMMARY: current_high=<N> output format (#2306-v2)'
workflow.includes('CYCLE_SUMMARY: current_high=<N> current_actionable=<M>'),
'review agent spawn prompt must define the CYCLE_SUMMARY current_high/current_actionable output format (#724)'
);
});
@@ -320,6 +348,13 @@ describe('plan-review-convergence workflow: CYCLE_SUMMARY contract definition (#
'review agent must provide ## Current HIGH Concerns section so escalation gate can show specific issues (#2306-v2)'
);
});
test('CYCLE_SUMMARY contract defines ACTIONABLE non-HIGH findings and requires their section', () => {
assert.ok(
workflow.includes('ACTIONABLE') && workflow.includes('Current Actionable Non-HIGH Concerns'),
'review agent must define actionable non-HIGH findings and list current unresolved actionable items (#724)'
);
});
});
// ─── Workflow: HIGH_LINES validation ──────────────────────────────────────
@@ -335,6 +370,14 @@ describe('plan-review-convergence workflow: HIGH_LINES validation (#2306-v2)', (
'workflow must warn when HIGH_COUNT > 0 but HIGH_LINES is empty (contract partially violated) (#2306-v2)'
);
});
test('workflow warns when ACTIONABLE_COUNT > 0 but actionable section is absent', () => {
assert.ok(
workflow.includes('ACTIONABLE_LINES') &&
workflow.includes('Current Actionable Non-HIGH Concerns'),
'workflow must warn when ACTIONABLE_COUNT > 0 but actionable details are empty (#724)'
);
});
});
// ─── Workflow: stall detection ─────────────────────────────────────────────
@@ -342,17 +385,17 @@ describe('plan-review-convergence workflow: HIGH_LINES validation (#2306-v2)', (
describe('plan-review-convergence workflow: stall detection (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow tracks previous HIGH count to detect stalls', () => {
test('workflow tracks previous unresolved count to detect stalls', () => {
assert.ok(
workflow.includes('prev_high_count') || workflow.includes('prev_HIGH'),
'workflow must track the previous cycle HIGH count for stall detection'
workflow.includes('prev_unresolved_count'),
'workflow must track the previous total unresolved review count for stall detection (#724)'
);
});
test('workflow warns when HIGH count is not decreasing', () => {
test('workflow warns when unresolved count is not decreasing', () => {
assert.ok(
workflow.includes('stall') || workflow.includes('Stall') || workflow.includes('not decreasing'),
'workflow must warn user when HIGH count is not decreasing between cycles'
'workflow must warn user when unresolved review count is not decreasing between cycles'
);
});
});
@@ -397,20 +440,19 @@ describe('plan-review-convergence workflow: escalation gate (#2306)', () => {
describe('plan-review-convergence workflow: stall detection behavioral (#2306)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('workflow surfaces stall warning when prev_high_count equals current HIGH_COUNT', () => {
test('workflow surfaces stall warning when unresolved count stops decreasing', () => {
assert.ok(
workflow.includes('prev_high_count') || workflow.includes('prev_HIGH'),
'workflow must track prev_high_count across cycles'
workflow.includes('prev_unresolved_count'),
'workflow must track prev_unresolved_count across cycles (#724)'
);
assert.ok(
workflow.includes('HIGH_COUNT >= prev_high_count') ||
workflow.includes('HIGH_COUNT >= prev_HIGH') ||
workflow.includes('UNRESOLVED_COUNT >= prev_unresolved_count') ||
workflow.includes('not decreasing'),
'workflow must compare current HIGH count against previous to detect stall'
'workflow must compare current unresolved count against previous to detect stall (#724)'
);
assert.ok(
workflow.includes('stall') || workflow.includes('Stall') || workflow.includes('not decreasing'),
'workflow must emit a stall warning when HIGH count is not decreasing'
'workflow must emit a stall warning when unresolved review count is not decreasing'
);
});
});
@@ -429,9 +471,10 @@ describe('plan-review-convergence workflow: --max-cycles 1 immediate escalation
);
assert.ok(
workflow.includes('HIGH_COUNT > 0') ||
workflow.includes('ACTIONABLE_COUNT > 0') ||
workflow.includes('HIGH concerns remain') ||
workflow.includes('Proceed anyway'),
'escalation gate must be reachable when HIGH_COUNT > 0 after a single cycle'
'escalation gate must be reachable when unresolved findings remain after a single cycle'
);
});
});
@@ -461,11 +504,12 @@ describe('plan-review-convergence workflow: artifact verification (#2306)', () =
describe('plan-review-convergence workflow: success criteria (#2306-v2)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
test('success criteria references CYCLE_SUMMARY parsing, not grep HIGHs', () => {
test('success criteria references CYCLE_SUMMARY parsing, not grep findings', () => {
const successBlock = workflow.slice(workflow.lastIndexOf('<success_criteria>'));
assert.ok(
successBlock.includes('CYCLE_SUMMARY') || successBlock.includes('parse'),
'success_criteria must reflect that orchestrator parses CYCLE_SUMMARY, not greps REVIEWS.md (#2306-v2)'
(successBlock.includes('CYCLE_SUMMARY') || successBlock.includes('parse')) &&
successBlock.includes('actionable non-HIGH'),
'success_criteria must reflect that orchestrator parses HIGH and actionable CYCLE_SUMMARY counts, not greps REVIEWS.md (#724)'
);
assert.ok(
!successBlock.includes('grep HIGHs'),
@@ -511,6 +555,70 @@ describe('plan-review-convergence CONFIGURATION.md documentation (#2306-v2)', ()
});
});
// ─── Reviews-mode incorporation contract (#724) ────────────────────────────
describe('plan-review-convergence reviews-mode incorporation contract (#724)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const planPhase = fs.readFileSync(PLAN_PHASE_PATH, 'utf8');
const plannerReviews = fs.readFileSync(PLANNER_REVIEWS_PATH, 'utf8');
const planChecker = fs.readFileSync(PLAN_CHECKER_PATH, 'utf8');
test('workflow replans while actionable non-HIGH findings remain', () => {
assert.ok(
workflow.includes('Actionable MEDIUM/LOW findings must be incorporated into executable PLAN.md content'),
'inline replan must route actionable non-HIGH findings back through plan-phase --reviews (#724)'
);
});
test('plan-phase planner prompt says REVIEWS.md is feedback input, not the execution contract', () => {
assert.ok(
planPhase.includes('<review_incorporation_contract>') &&
planPhase.includes('REVIEWS.md is feedback input') &&
planPhase.includes('/gsd:execute-phase primarily consumes PLAN.md'),
'planner prompt must explain that actionable review feedback must land in PLAN.md for execute-phase (#724)'
);
});
test('plan-phase checker prompt reads REVIEWS.md in reviews mode and fails hidden actionable findings', () => {
assert.ok(
planPhase.includes('{reviews_path}') &&
planPhase.includes('<review_incorporation_verification>') &&
planPhase.includes('return `## ISSUES FOUND`'),
'checker prompt must read REVIEWS.md and fail if actionable findings remain only there (#724)'
);
});
test('planner reviews reference requires actionable findings to appear in PLAN.md or be deferred there', () => {
assert.ok(
plannerReviews.includes('/gsd:execute-phase primarily consumes PLAN.md') &&
plannerReviews.includes('Every current actionable review finding') &&
plannerReviews.includes('deferral/rejection rationale in that PLAN.md'),
'planner reviews reference must keep REVIEWS.md from becoming a hidden execution contract (#724)'
);
});
test('gsd-plan-checker has a Review Incorporation dimension for reviews mode', () => {
assert.ok(
planChecker.includes('Review Incorporation') &&
planChecker.includes('current_actionable=<M>') &&
planChecker.includes('remains only in REVIEWS.md'),
'plan checker must validate review incorporation when REVIEWS.md is present (#724)'
);
// The current_actionable=<M> reference must appear in a prohibition context,
// not as an instruction to parse machine-readable fields from REVIEWS.md.
// The CYCLE_SUMMARY line exists only in the convergence orchestrator's return message.
assert.ok(
planChecker.includes('Do NOT look for') || planChecker.includes('do NOT look for'),
'plan checker must explicitly prohibit looking for CYCLE_SUMMARY/current_actionable=<M> in REVIEWS.md — those machine-readable fields are only on the orchestrator return message, never in the file'
);
assert.ok(
planChecker.includes('CYCLE_SUMMARY') &&
(planChecker.includes('Do NOT look for') || planChecker.includes('do NOT look for')),
'CYCLE_SUMMARY must appear in plan-checker only as a prohibited pattern, not as a parsing instruction'
);
});
});
// ─── Local model reviewer support ────────────────────────────────────────
describe('plan-review-convergence local model reviewer flags (#2306-local)', () => {