Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions apps/claude-code-swe-agent/src/claude.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
}
});
});
10 changes: 8 additions & 2 deletions apps/claude-code-swe-agent/src/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.`,
Expand Down