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.`,