From f0953dec0c8ab43e70ec6337d112aa0c924a6300 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sat, 25 Apr 2026 13:57:43 -0600 Subject: [PATCH] fix(sdk): prevent interactive tool calls in headless self-discuss mode The discuss step loaded the full interactive workflow prompt which instructs the agent to use AskUserQuestion, Skill(), and area selection UIs. In headless auto mode, the agent followed these instructions and tried to interact with a non-existent user. Fix: prepend a mandatory headless override BEFORE the workflow prompt that explicitly forbids interactive tools and instructs the agent to make all decisions autonomously. Prepending (not appending) ensures the override takes priority over conflicting instructions later in the prompt. Co-Authored-By: Claude Opus 4.6 (1M context) --- sdk/src/phase-runner.test.ts | 4 ++-- sdk/src/phase-runner.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/sdk/src/phase-runner.test.ts b/sdk/src/phase-runner.test.ts index dab899055..baf2bc5af 100644 --- a/sdk/src/phase-runner.test.ts +++ b/sdk/src/phase-runner.test.ts @@ -1989,8 +1989,8 @@ Use TypeScript.`, 'utf-8'); ); expect(discussCalls.length).toBeGreaterThanOrEqual(1); const prompt = discussCalls[0][0] as string; - expect(prompt).toContain('Self-Discuss Mode'); - expect(prompt).toContain('No human is present'); + expect(prompt).toContain('HEADLESS MODE'); + expect(prompt).toContain('no human present'); }); it('skips self-discuss when context already exists even in auto-mode', async () => { diff --git a/sdk/src/phase-runner.ts b/sdk/src/phase-runner.ts index 8e3539842..1ce27ee0f 100644 --- a/sdk/src/phase-runner.ts +++ b/sdk/src/phase-runner.ts @@ -438,9 +438,34 @@ 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 with pass cap + // Prepend self-discuss override BEFORE the workflow prompt. + // The workflow prompt contains interactive patterns (user questions, area selection) + // that the agent will follow unless explicitly overridden up front. 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.`; + const selfDiscussOverride = [ + '## HEADLESS MODE — MANDATORY OVERRIDE', + '', + '**This session is running headless with no human present.**', + '', + 'You MUST NOT:', + '- Use AskUserQuestion or any interactive tools', + '- Invoke Skill() or SlashCommand()', + '- Ask the user anything or wait for input', + '- Use multi-select, checkbox, or prompt UIs', + '', + 'You MUST:', + '- Make all decisions autonomously and opinionatedly', + '- Identify 3-5 gray areas in the project scope', + '- Reason through each one yourself and pick the best option', + '- Write CONTEXT.md with your decisions in a single pass', + `- Complete within ${maxPasses} pass(es) maximum — do not re-read your own output to find gaps`, + '', + 'Any instructions below about "asking the user", "discussing with the user", or "interactive" should be read as "decide yourself."', + '', + '---', + '', + ].join('\n'); + prompt = selfDiscussOverride + prompt; planResult = await runPhaseStepSession( prompt,