From aa6cde32d9a2fe9c2974e356570686e043a26dac Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 30 Mar 2026 16:30:43 -0400 Subject: [PATCH] fix: prevent ask_user_questions from poisoning auto-mode dispatch (#2936) (#3240) Add autonomous execution guard to plan-slice, execute-task, and complete-slice prompts prohibiting ask_user_questions and secure_env_collect in auto-mode. When the LLM called these interactive tools mid-unit, the queued user response caused the subsequent gsd_plan_slice/gsd_complete_task call to fail with "Skipped due to queued user message", leading to artifact verification failure and stuck-loop re-dispatch 2-4x. Co-authored-by: Claude Opus 4.6 --- .../extensions/gsd/prompts/complete-slice.md | 2 + .../extensions/gsd/prompts/execute-task.md | 2 + .../extensions/gsd/prompts/plan-slice.md | 2 + .../tests/auto-mode-interactive-guard.test.ts | 71 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/auto-mode-interactive-guard.test.ts diff --git a/src/resources/extensions/gsd/prompts/complete-slice.md b/src/resources/extensions/gsd/prompts/complete-slice.md index e062a4aee..1be3fd51d 100644 --- a/src/resources/extensions/gsd/prompts/complete-slice.md +++ b/src/resources/extensions/gsd/prompts/complete-slice.md @@ -33,6 +33,8 @@ Then: 12. Do not run git commands — the system commits your changes and handles any merge after this unit succeeds. 13. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed. +**Autonomous execution:** Do not call `ask_user_questions` or `secure_env_collect`. You are running in auto-mode — there is no human available to answer questions. Make reasonable assumptions and document them in the slice summary. If a decision genuinely requires human input, note it in the summary and proceed with the best available option. + **You MUST call `gsd_complete_slice` with the slice summary and UAT content before finishing. The tool persists to both DB and disk and renders `{{sliceSummaryPath}}` and `{{sliceUatPath}}` automatically.** When done, say: "Slice {{sliceId}} complete." diff --git a/src/resources/extensions/gsd/prompts/execute-task.md b/src/resources/extensions/gsd/prompts/execute-task.md index 9428fa68a..ad14ee34c 100644 --- a/src/resources/extensions/gsd/prompts/execute-task.md +++ b/src/resources/extensions/gsd/prompts/execute-task.md @@ -73,6 +73,8 @@ Then: All work stays in your working directory: `{{workingDirectory}}`. +**Autonomous execution:** Do not call `ask_user_questions` or `secure_env_collect`. You are running in auto-mode — there is no human available to answer questions. Make reasonable assumptions and document them in the task summary. If a decision genuinely requires human input, note it in the summary and proceed with the best available option. + **You MUST call `gsd_complete_task` AND write `{{taskSummaryPath}}` before finishing.** When done, say: "Task {{taskId}} complete." diff --git a/src/resources/extensions/gsd/prompts/plan-slice.md b/src/resources/extensions/gsd/prompts/plan-slice.md index 6b38c4667..69e103f72 100644 --- a/src/resources/extensions/gsd/prompts/plan-slice.md +++ b/src/resources/extensions/gsd/prompts/plan-slice.md @@ -82,6 +82,8 @@ Then: The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. All work stays in your working directory: `{{workingDirectory}}`. +**Autonomous execution:** Do not call `ask_user_questions` or `secure_env_collect`. You are running in auto-mode — there is no human available to answer questions. Make reasonable assumptions and document them in the plan. If a decision genuinely requires human input, write a note in the relevant task's description and call `gsd_plan_slice` with what you have. + **You MUST call `gsd_plan_slice` to persist the planning state before finishing.** When done, say: "Slice {{sliceId}} planned." diff --git a/src/resources/extensions/gsd/tests/auto-mode-interactive-guard.test.ts b/src/resources/extensions/gsd/tests/auto-mode-interactive-guard.test.ts new file mode 100644 index 000000000..ee830e081 --- /dev/null +++ b/src/resources/extensions/gsd/tests/auto-mode-interactive-guard.test.ts @@ -0,0 +1,71 @@ +/** + * Test: auto-mode prompts must prohibit ask_user_questions / secure_env_collect + * + * Bug #2936: When the LLM calls ask_user_questions during auto-mode units + * (plan-slice, execute-task, complete-slice), the interactive tool queues a + * user response which causes the subsequent gsd_plan_slice / gsd_complete_task + * call to fail with "Skipped due to queued user message." The canonical GSD + * tool call is never recorded, verifyExpectedArtifact finds no artifact, and + * the dispatch loop re-dispatches the same unit 2-4x. + * + * Fix: Each auto-mode prompt must contain an "Autonomous execution" guard + * that explicitly prohibits ask_user_questions and secure_env_collect. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const promptsDir = join(__dirname, "..", "prompts"); + +function loadPromptRaw(name: string): string { + return readFileSync(join(promptsDir, `${name}.md`), "utf-8"); +} + +const AUTO_MODE_PROMPTS = ["plan-slice", "execute-task", "complete-slice"]; + +for (const promptName of AUTO_MODE_PROMPTS) { + test(`${promptName} prompt prohibits ask_user_questions in auto-mode`, () => { + const content = loadPromptRaw(promptName); + + assert.ok( + content.includes("ask_user_questions"), + `${promptName}.md must mention ask_user_questions (to prohibit it)`, + ); + + assert.ok( + content.includes("secure_env_collect"), + `${promptName}.md must mention secure_env_collect (to prohibit it)`, + ); + + // The guard must clearly state this is autonomous / auto-mode + assert.ok( + content.toLowerCase().includes("auto-mode") || content.toLowerCase().includes("autonomous"), + `${promptName}.md must reference auto-mode or autonomous execution`, + ); + + // The guard must indicate no human is available + assert.ok( + content.includes("no human") || content.includes("no user"), + `${promptName}.md must state that no human/user is available to answer`, + ); + }); +} + +test("auto-mode prompts contain autonomous guard before final tool call reminder", () => { + for (const promptName of AUTO_MODE_PROMPTS) { + const content = loadPromptRaw(promptName); + + // The guard should appear before the final "MUST call" line + const guardIndex = content.indexOf("ask_user_questions"); + const mustCallIndex = content.lastIndexOf("MUST call"); + + assert.ok( + guardIndex !== -1 && mustCallIndex !== -1 && guardIndex < mustCallIndex, + `${promptName}.md: autonomous guard (ask_user_questions prohibition) must appear before the final MUST call reminder`, + ); + } +});