diff --git a/.changeset/nimble-jays-fly.md b/.changeset/nimble-jays-fly.md new file mode 100644 index 000000000..ba5ea417c --- /dev/null +++ b/.changeset/nimble-jays-fly.md @@ -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. diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index 4f10662cb..35aaf5cf2 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -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= current_actionable=` 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, ``, ``, ``, `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. diff --git a/commands/gsd/plan-review-convergence.md b/commands/gsd/plan-review-convergence.md index c13defcf9..b262304a3 100644 --- a/commands/gsd/plan-review-convergence.md +++ b/commands/gsd/plan-review-convergence.md @@ -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: " [--codex] [--gemini] [--claude] [--opencode] [--ollama] [--lm-studio] [--llama-cpp] [--text] [--ws ] [--all] [--max-cycles N]" allowed-tools: - Read @@ -9,19 +9,20 @@ allowed-tools: - Glob - Grep - Agent + - Skill - AskUserQuestion requires: [phase, review] --- 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. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 4b3b67ba0..41ebf9660 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -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 diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 38495f8f8..68ecdd682 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 `` 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 | diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index e0ffeee1f..76c226cf3 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -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` | diff --git a/gsd-core/references/planner-reviews.md b/gsd-core/references/planner-reviews.md index da9f9ce0f..47f6557f6 100644 --- a/gsd-core/references/planner-reviews.md +++ b/gsd-core/references/planner-reviews.md @@ -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 ``. Parse: - Per-reviewer feedback (strengths, concerns, suggestions) @@ -13,13 +15,14 @@ Read the reviews file from ``. 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 diff --git a/gsd-core/workflows/plan-phase.md b/gsd-core/workflows/plan-phase.md index dc795404b..6b0c5aa7a 100644 --- a/gsd-core/workflows/plan-phase.md +++ b/gsd-core/workflows/plan-phase.md @@ -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} + +**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, ``, ``, ``, `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. + + **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: **Phase:** {phase_number} **Phase Goal:** {goal from ROADMAP} +**Mode:** {standard | gap_closure | reviews} - {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) ${AGENT_SKILLS_CHECKER} + +**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, ``, ``, ``, `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. + + **Phase requirement IDs (MUST ALL be covered):** {phase_req_ids} **Project instructions:** Read ./CLAUDE.md if exists — verify plans honor project guidelines diff --git a/gsd-core/workflows/plan-review-convergence.md b/gsd-core/workflows/plan-review-convergence.md index dba86555e..c6574fe81 100644 --- a/gsd-core/workflows/plan-review-convergence.md +++ b/gsd-core/workflows/plan-review-convergence.md @@ -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. @@ -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= + CYCLE_SUMMARY: current_high= current_actionable= Where is the integer count of HIGH-severity concerns that REMAIN UNRESOLVED in this cycle's findings. +Where 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= 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= current_actionable= 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 diff --git a/tests/plan-review-convergence.test.cjs b/tests/plan-review-convergence.test.cjs index 2f3c08b1a..12c4c57da 100644 --- a/tests/plan-review-convergence.test.cjs +++ b/tests/plan-review-convergence.test.cjs @@ -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= 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= output format (#2306-v2)' + workflow.includes('CYCLE_SUMMARY: current_high= current_actionable='), + '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('')); 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('') && + 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('') && + 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=') && + planChecker.includes('remains only in REVIEWS.md'), + 'plan checker must validate review incorporation when REVIEWS.md is present (#724)' + ); + // The current_actionable= 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= 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)', () => {