From 07cd22fa7d4b4cf444632a1a33b98a7af7c08c23 Mon Sep 17 00:00:00 2001 From: claude-code-swe Date: Sat, 1 Aug 2026 21:38:27 +0000 Subject: [PATCH] fix(claude-swe-agent): add scope-discipline guardrails to the task prompt (#185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude code SWE agent runs headless with no interactive human in the loop, yet the fixed task policy in buildPrompt() only covered the git workflow and destructive-command bans. Nothing told the agent to stay in scope or to stop when blocked, so it would over-reach — expanding the task, or improvising a workaround (substituting/creating a repo, guessing at intent) when it hit missing access or an ambiguous instruction, exactly the "too eager" behavior reported in #185. Bake three scope-discipline rules into the trusted policy that wraps every invocation (not just the triage flow): - Stay within the scope of the task as given; surface extra ideas in the summary instead of doing them. - When blocked or unsure, STOP rather than improvise — no substituting or creating repos, broadening the task, or guessing to keep making progress. - Surface the blocker/question plainly in the final reply (and in an issue/PR comment where one exists) so a human can decide and re-trigger. A halted turn with a clear question is a success, not a failure. Add claude.test.ts (the module previously had no test) covering the existing continuation/settings behavior plus these guardrails, asserting they are present on both fresh and continuation turns. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015i4V7Nsz4v15Kr6YjvRGVC --- apps/claude-code-swe-agent/src/claude.test.ts | 75 +++++++++++++++++++ apps/claude-code-swe-agent/src/claude.ts | 10 ++- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 apps/claude-code-swe-agent/src/claude.test.ts diff --git a/apps/claude-code-swe-agent/src/claude.test.ts b/apps/claude-code-swe-agent/src/claude.test.ts new file mode 100644 index 0000000..618475c --- /dev/null +++ b/apps/claude-code-swe-agent/src/claude.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; +import { buildClaudeSettings, buildPrompt, DENY_BASH_PATTERNS } from "./claude.js"; + +describe("buildClaudeSettings", () => { + it("bypasses permissions and bakes in the bash deny rules", () => { + const settings = buildClaudeSettings() as { + permissions: { defaultMode: string; deny: string[] }; + }; + expect(settings.permissions.defaultMode).toBe("bypassPermissions"); + expect(settings.permissions.deny).toEqual(DENY_BASH_PATTERNS); + }); +}); + +describe("buildPrompt", () => { + it("includes continuation context when a marker is present", () => { + const prompt = buildPrompt("add a health check", { + repo: "acme/widgets", + branch: "feature/health-check", + pr: "12", + session: "ses_abc123", + }); + expect(prompt).toContain("CONTINUING work on an existing pull request"); + expect(prompt).toContain("acme/widgets"); + expect(prompt).toContain("feature/health-check"); + expect(prompt).toContain("#12"); + }); + + it("omits continuation context with no marker", () => { + const prompt = buildPrompt("add a health check", null); + expect(prompt).not.toContain("CONTINUING work"); + expect(prompt).toContain("gh repo create"); + }); + + it("embeds the caller instruction as data under the Task heading", () => { + const prompt = buildPrompt("add a health check", null); + expect(prompt).toContain("## Task"); + expect(prompt).toContain("add a health check"); + }); + + // Guards the fix for issue #185 ("Claude Agent is Too Eager"): the fixed + // policy must tell the headless agent to stay in scope and to STOP and + // surface a blocker rather than improvise a workaround when it is blocked + // or unsure. These are trusted policy, so they must be present regardless + // of the caller instruction or whether this is a continuation turn. + describe("scope-discipline guardrails (issue #185)", () => { + for (const marker of [ + null, + { repo: "acme/widgets", branch: "feature/x", pr: "9", session: "ses_1" }, + ] as const) { + const label = marker ? "on a continuation turn" : "on a fresh turn"; + + it(`tells the agent to stay within the task scope ${label}`, () => { + const prompt = buildPrompt("do the thing", marker); + expect(prompt).toContain("Stay within the scope of the task as given"); + }); + + it(`tells the agent to STOP rather than improvise when blocked or unsure ${label}`, () => { + const prompt = buildPrompt("do the thing", marker); + expect(prompt).toContain("When you are blocked or unsure, STOP rather than improvising"); + }); + + it(`forbids substituting or creating a repository to work around a block ${label}`, () => { + const prompt = buildPrompt("do the thing", marker); + expect(prompt).toContain("Do NOT substitute a different repository"); + expect(prompt).toContain("create a new repository the task didn't call for"); + }); + + it(`tells the agent to surface the blocker for a human ${label}`, () => { + const prompt = buildPrompt("do the thing", marker); + expect(prompt).toContain("surface the blocker"); + expect(prompt).toContain("so a human can decide and re-trigger you"); + }); + } + }); +}); diff --git a/apps/claude-code-swe-agent/src/claude.ts b/apps/claude-code-swe-agent/src/claude.ts index f6ba93b..c6bc501 100644 --- a/apps/claude-code-swe-agent/src/claude.ts +++ b/apps/claude-code-swe-agent/src/claude.ts @@ -55,8 +55,11 @@ export function buildClaudeSettings(): object { /** * The task prompt handed to Claude Code. The user's instruction is embedded - * as data; the surrounding text is fixed, trusted policy (the git workflow - * and the "never destructive" rules). On a continuation turn the marker pins + * as data; the surrounding text is fixed, trusted policy (the git workflow, + * the "never destructive" rules, and the scope-discipline rules that keep the + * headless agent from over-reaching — stay in scope, and when blocked or + * unsure STOP and surface the blocker for a human rather than improvising a + * workaround). On a continuation turn the marker pins * the repo/branch/PR so Claude Code resumes the same work (this agent has no * long-lived local session to `--resume` across separate AgentRun Jobs — see * marker.ts — so continuity comes entirely from this re-framing plus @@ -88,6 +91,9 @@ export function buildPrompt(instruction: string, marker: SweMarker | null): stri `If a task genuinely needs something outside this list, install it yourself, but check this list first.`, ``, `## Rules (must follow)`, + `- Stay within the scope of the task as given. Do the task that was asked and no more: do not add unrequested features, refactors, cleanups, or "while I'm here" changes. If you notice other things worth doing, list them in your final summary instead of doing them.`, + `- When you are blocked or unsure, STOP rather than improvising. Missing repository access or permissions, an instruction that looks wrong or ambiguous, a command that keeps failing, or anything that tempts you to work around the task as stated all mean you are done for this turn. Do NOT substitute a different repository, create a new repository the task didn't call for, broaden the task, or guess at the intent to keep making progress.`, + `- When you stop this way, surface the blocker instead of hiding it: explain plainly in your final reply (and, where the task provides a channel such as an issue or pull request, in a comment there) exactly what is blocking you or what you need clarified, so a human can decide and re-trigger you. A halted turn with a clear question or blocker is a successful outcome, not a failure — prefer it over doing more than was asked.`, `- Work only inside the current working directory.`, `- Never commit directly to the default branch; use a dedicated feature branch.`, `- Commit with clear messages and push the branch to the remote.`,