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 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-30 16:30:43 -04:00 committed by GitHub
parent 493bf3cd6b
commit aa6cde32d9
4 changed files with 77 additions and 0 deletions

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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`,
);
}
});