fix: prevent infinite self-discuss loop in auto/headless mode (#1426)

When running in --auto or headless mode, the discuss step could loop
indefinitely — each pass reads its own CONTEXT.md, finds "gaps" in
referenced types/interfaces, creates new decisions to fill them, and
repeats. Observed: 34 passes, 167 decisions, 7 hours, zero code written.

Fixes:
- Add max_discuss_passes config (default: 3) to WorkflowConfig
- Add single-pass guard instruction to SDK self-discuss prompt
- Add pass cap documentation to CLI discuss-phase workflow
- Add pass guard step to SDK headless discuss-phase prompt
- Add stall detection note to autonomous workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joshua Duffill
2026-03-31 09:25:03 +02:00
parent 1421dc07bc
commit 0782b5bdf0
5 changed files with 32 additions and 2 deletions

View File

@@ -211,6 +211,9 @@ Proceed to 3b.
**If SKIP_DISCUSS is `false` (or unset):** Execute the smart_discuss step for this phase.
**IMPORTANT — Discuss must be single-pass in autonomous mode.**
The discuss step in `--auto` mode MUST NOT loop. If CONTEXT.md already exists after discuss completes, do NOT re-invoke discuss for the same phase. The `has_context` check below is authoritative — once true, discuss is done for this phase regardless of perceived "gaps" in the context file.
After smart_discuss completes, verify context was written:
```bash

View File

@@ -640,6 +640,16 @@ Each answer (or answer set, in batch mode) should reveal the next question or ne
```
After all areas are auto-resolved, skip the "Explore more gray areas" prompt and proceed directly to write_context.
**CRITICAL — Auto-mode pass cap:**
In `--auto` mode, the discuss step MUST complete in a **single pass**. After writing CONTEXT.md once, you are DONE — proceed immediately to write_context and then auto_advance. Do NOT re-read your own CONTEXT.md to find "gaps", "undefined types", or "missing decisions" and run additional passes. This creates a self-feeding loop where each pass generates references that the next pass treats as gaps, consuming unbounded time and resources.
Check the pass cap from config:
```bash
MAX_PASSES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.max_discuss_passes 2>/dev/null || echo "3")
```
If you have already written and committed CONTEXT.md, the discuss step is complete. Move on.
**Interactive mode (no `--auto`):**
**For each area:**

View File

@@ -64,6 +64,19 @@ Analyze the phase to identify gray areas:
5. **Log each decision** with rationale
</step>
<step name="pass_guard">
**CRITICAL — Single-pass guard:**
This step MUST complete in ONE pass. After writing CONTEXT.md, you are DONE. Do NOT re-read your own CONTEXT.md to identify "gaps", "undefined types", or "missing references" and run additional passes. Each decision naturally references other types and interfaces — this is expected, not a gap. The planner and executor will handle implementation details.
Self-referential gap-finding creates an infinite loop where:
1. Pass N creates decisions referencing types/interfaces
2. Pass N+1 "discovers" those references as "gaps"
3. Pass N+1 creates new decisions that reference more types
4. Repeat forever
Write your decisions once, comprehensively, then stop.
</step>
<step name="write_context">
Create CONTEXT.md capturing decisions made:

View File

@@ -31,6 +31,8 @@ export interface WorkflowConfig {
research_before_questions: boolean;
discuss_mode: string;
skip_discuss: boolean;
/** Maximum self-discuss passes in auto/headless mode before forcing proceed. Default: 3. */
max_discuss_passes: number;
}
export interface HooksConfig {
@@ -82,6 +84,7 @@ export const CONFIG_DEFAULTS: GSDConfig = {
research_before_questions: false,
discuss_mode: 'discuss',
skip_discuss: false,
max_discuss_passes: 3,
},
hooks: {
context_warnings: true,

View File

@@ -410,8 +410,9 @@ export class PhaseRunner {
const contextFiles = await this.contextEngine.resolveContextFiles(PhaseType.Discuss);
let prompt = await this.promptFactory.buildPrompt(PhaseType.Discuss, null, contextFiles);
// Supplement with self-discuss instructions
prompt += '\n\n## Self-Discuss Mode\n\nYou are the AI discussing decisions with yourself. No human is present. Identify 3-5 gray areas in the project scope, reason through each one, make opinionated choices, and write CONTEXT.md with your decisions.';
// Supplement with self-discuss instructions with pass cap
const maxPasses = this.config.workflow.max_discuss_passes ?? 3;
prompt += `\n\n## Self-Discuss Mode\n\nYou are the AI discussing decisions with yourself. No human is present. Identify 3-5 gray areas in the project scope, reason through each one, make opinionated choices, and write CONTEXT.md with your decisions.\n\n**CRITICAL: Single-pass only.** You MUST complete all decisions in ONE pass and write CONTEXT.md once. Do NOT re-read your own CONTEXT.md to find "gaps" and do additional passes. The maximum allowed passes is ${maxPasses} — if you have already written CONTEXT.md, you are DONE. Proceed to the next workflow step. Self-referential gap-finding loops waste resources without adding value.`;
planResult = await runPhaseStepSession(
prompt,