From 56ffdc72ef52171323efc98de13cda9cdaf7f953 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Wed, 11 Mar 2026 22:44:55 +0000 Subject: [PATCH 01/10] agent: @U0AJM7X8FBR Tasks - we want to switch the coding agent being used in t --- .../__tests__/runClaudeCodeAgent.test.ts | 150 ++++++++++++++++++ src/sandboxes/runClaudeCodeAgent.ts | 65 ++++++++ src/tasks/__tests__/codingAgentTask.test.ts | 14 +- src/tasks/__tests__/updatePRTask.test.ts | 18 +-- src/tasks/codingAgentTask.ts | 4 +- src/tasks/updatePRTask.ts | 6 +- 6 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 src/sandboxes/__tests__/runClaudeCodeAgent.test.ts create mode 100644 src/sandboxes/runClaudeCodeAgent.ts diff --git a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts new file mode 100644 index 0000000..751294a --- /dev/null +++ b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@trigger.dev/sdk/v3", () => ({ + logger: { log: vi.fn(), error: vi.fn() }, + metadata: { set: vi.fn(), append: vi.fn() }, +})); + +vi.mock("../logStep", () => ({ + logStep: vi.fn(), +})); + +const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); +const { logStep } = await import("../logStep"); + +function mockDetachedCommand(finished: { + exitCode: number; + stdout: () => Promise; + stderr: () => Promise; +}) { + return { wait: vi.fn().mockResolvedValue(finished) }; +} + +function createMockSandbox() { + return { runCommand: vi.fn() } as any; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("runClaudeCodeAgent", () => { + it("calls claude --print with the message", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "done\n", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { + label: "Coding agent", + message: "Fix the bug", + }); + + expect(sandbox.runCommand).toHaveBeenCalledWith({ + cmd: "claude", + args: ["--print", "Fix the bug"], + detached: true, + }); + }); + + it("passes env vars when provided", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { + label: "Apply feedback", + message: "Update the README", + env: { GITHUB_TOKEN: "ghp_test" }, + }); + + expect(sandbox.runCommand).toHaveBeenCalledWith({ + cmd: "claude", + args: ["--print", "Update the README"], + detached: true, + env: { GITHUB_TOKEN: "ghp_test" }, + }); + }); + + it("uses detached mode and waits for completion", async () => { + const sandbox = createMockSandbox(); + const waitMock = vi.fn().mockResolvedValueOnce({ + exitCode: 0, + stdout: async () => "done\n", + stderr: async () => "", + }); + sandbox.runCommand.mockResolvedValueOnce({ wait: waitMock }); + + await runClaudeCodeAgent(sandbox, { label: "Test", message: "Do something" }); + + expect(sandbox.runCommand).toHaveBeenCalledWith( + expect.objectContaining({ detached: true }), + ); + expect(waitMock).toHaveBeenCalled(); + }); + + it("returns stdout, stderr, and exitCode", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "PR_CREATED: https://github.com/org/repo/pull/1\n", + stderr: async () => "warning\n", + }), + ); + + const result = await runClaudeCodeAgent(sandbox, { + label: "Create PRs", + message: "Push changes", + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe("PR_CREATED: https://github.com/org/repo/pull/1\n"); + expect(result.stderr).toBe("warning\n"); + }); + + it("logs completion via logStep", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "output\n", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { label: "Clone repos", message: "Clone" }); + + expect(logStep).toHaveBeenCalledWith("Clone repos completed", false, { + exitCode: 0, + stdout: "output\n", + stderr: "", + }); + }); + + it("logs failure on non-zero exit code", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 1, + stdout: async () => "", + stderr: async () => "fatal error\n", + }), + ); + + await runClaudeCodeAgent(sandbox, { label: "Clone repos", message: "Clone" }); + + expect(logStep).toHaveBeenCalledWith("Clone repos failed", false, { + stderr: "fatal error\n", + }); + }); +}); diff --git a/src/sandboxes/runClaudeCodeAgent.ts b/src/sandboxes/runClaudeCodeAgent.ts new file mode 100644 index 0000000..d2a326e --- /dev/null +++ b/src/sandboxes/runClaudeCodeAgent.ts @@ -0,0 +1,65 @@ +import type { Sandbox } from "@vercel/sandbox"; +import { logStep } from "./logStep"; + +interface RunClaudeCodeAgentOptions { + label: string; + message: string; + env?: Record; +} + +interface RunClaudeCodeAgentResult { + exitCode: number; + stdout: string; + stderr: string; +} + +/** + * Runs a Claude Code agent command with standardized logging and metadata. + * Uses the `claude` CLI with --print flag for non-interactive execution. + * + * @param sandbox - The Vercel Sandbox instance + * @param options - Label for logging/metadata, message prompt, optional env vars + * @returns exitCode, stdout, and stderr from the command + */ +export async function runClaudeCodeAgent( + sandbox: Sandbox, + options: RunClaudeCodeAgentOptions, +): Promise { + const { label, message, env } = options; + + const args = ["--print", message]; + + logStep(label, true, { cmd: "claude", args }); + + const commandOpts: Record = { + cmd: "claude", + args, + detached: true, + }; + + if (env) { + commandOpts.env = env; + } + + const command = await sandbox.runCommand(commandOpts as any); + const result = await command.wait(); + + const stdout = (await result.stdout()) || ""; + const stderr = (await result.stderr()) || ""; + + logStep(`${label} completed`, false, { + exitCode: result.exitCode, + stdout, + stderr, + }); + + if (result.exitCode !== 0) { + logStep(`${label} failed`, false, { stderr }); + } + + return { + exitCode: result.exitCode, + stdout, + stderr, + }; +} diff --git a/src/tasks/__tests__/codingAgentTask.test.ts b/src/tasks/__tests__/codingAgentTask.test.ts index f5b1faa..1e9ae2c 100644 --- a/src/tasks/__tests__/codingAgentTask.test.ts +++ b/src/tasks/__tests__/codingAgentTask.test.ts @@ -39,8 +39,8 @@ vi.mock("../../sandboxes/git/syncMonorepoSubmodules", () => ({ syncMonorepoSubmodules: vi.fn(), })); -vi.mock("../../sandboxes/runOpenClawAgent", () => ({ - runOpenClawAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "done", stderr: "" }), +vi.mock("../../sandboxes/runClaudeCodeAgent", () => ({ + runClaudeCodeAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "done", stderr: "" }), })); vi.mock("../../sandboxes/pushAndCreatePRsViaAgent", () => ({ @@ -84,14 +84,14 @@ describe("codingAgentTask", () => { it("creates a sandbox, clones via agent, runs agent, creates PRs via agent, and notifies", async () => { const { notifyCodingAgentCallback } = await import("../../sandboxes/notifyCodingAgentCallback"); const { cloneMonorepoViaAgent } = await import("../../sandboxes/cloneMonorepoViaAgent"); - const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); const { pushAndCreatePRsViaAgent } = await import("../../sandboxes/pushAndCreatePRsViaAgent"); await mockRun(basePayload); expect(mockGetOrCreateSandbox).toHaveBeenCalledOnce(); expect(cloneMonorepoViaAgent).toHaveBeenCalledOnce(); - expect(runOpenClawAgent).toHaveBeenCalledOnce(); + expect(runClaudeCodeAgent).toHaveBeenCalledOnce(); expect(pushAndCreatePRsViaAgent).toHaveBeenCalledOnce(); expect(notifyCodingAgentCallback).toHaveBeenCalledWith( expect.objectContaining({ @@ -143,18 +143,18 @@ describe("codingAgentTask", () => { it("syncs monorepo submodules after cloning and before running agent", async () => { const { cloneMonorepoViaAgent } = await import("../../sandboxes/cloneMonorepoViaAgent"); const { syncMonorepoSubmodules } = await import("../../sandboxes/git/syncMonorepoSubmodules"); - const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); await mockRun(basePayload); expect(cloneMonorepoViaAgent).toHaveBeenCalledOnce(); expect(syncMonorepoSubmodules).toHaveBeenCalledOnce(); - expect(runOpenClawAgent).toHaveBeenCalledOnce(); + expect(runClaudeCodeAgent).toHaveBeenCalledOnce(); // Verify ordering: clone → sync → agent const cloneOrder = vi.mocked(cloneMonorepoViaAgent).mock.invocationCallOrder[0]; const syncOrder = vi.mocked(syncMonorepoSubmodules).mock.invocationCallOrder[0]; - const agentOrder = vi.mocked(runOpenClawAgent).mock.invocationCallOrder[0]; + const agentOrder = vi.mocked(runClaudeCodeAgent).mock.invocationCallOrder[0]; expect(cloneOrder).toBeLessThan(syncOrder); expect(syncOrder).toBeLessThan(agentOrder); }); diff --git a/src/tasks/__tests__/updatePRTask.test.ts b/src/tasks/__tests__/updatePRTask.test.ts index 817aa65..b67e879 100644 --- a/src/tasks/__tests__/updatePRTask.test.ts +++ b/src/tasks/__tests__/updatePRTask.test.ts @@ -35,8 +35,8 @@ vi.mock("../../sandboxes/setupOpenClaw", () => ({ setupOpenClaw: vi.fn(), })); -vi.mock("../../sandboxes/runOpenClawAgent", () => ({ - runOpenClawAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "done", stderr: "" }), +vi.mock("../../sandboxes/runClaudeCodeAgent", () => ({ + runClaudeCodeAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "done", stderr: "" }), })); vi.mock("../../sandboxes/notifyCodingAgentCallback", () => ({ @@ -91,11 +91,11 @@ describe("updatePRTask", () => { }); it("runs OpenClaw agent with feedback prompt", async () => { - const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); await mockRun(basePayload); - expect(runOpenClawAgent).toHaveBeenCalledWith( + expect(runClaudeCodeAgent).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ message: expect.stringContaining("Make the button blue instead"), @@ -104,13 +104,13 @@ describe("updatePRTask", () => { }); it("delegates push to agent instead of using runGitCommand", async () => { - const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); await mockRun(basePayload); // Should be called twice: once for feedback, once for push - expect(runOpenClawAgent).toHaveBeenCalledTimes(2); - expect(runOpenClawAgent).toHaveBeenCalledWith( + expect(runClaudeCodeAgent).toHaveBeenCalledTimes(2); + expect(runClaudeCodeAgent).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ label: "Push feedback changes", @@ -162,7 +162,7 @@ describe("updatePRTask", () => { }); it("passes sandbox env to both agent calls", async () => { - const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); const { getSandboxEnv } = await import("../../sandboxes/getSandboxEnv"); await mockRun(basePayload); @@ -176,7 +176,7 @@ describe("updatePRTask", () => { }; // Both agent calls should receive env - const calls = vi.mocked(runOpenClawAgent).mock.calls; + const calls = vi.mocked(runClaudeCodeAgent).mock.calls; expect(calls[0][1]).toEqual(expect.objectContaining({ env: expectedEnv })); expect(calls[1][1]).toEqual(expect.objectContaining({ env: expectedEnv })); }); diff --git a/src/tasks/codingAgentTask.ts b/src/tasks/codingAgentTask.ts index 605b72f..7931c52 100644 --- a/src/tasks/codingAgentTask.ts +++ b/src/tasks/codingAgentTask.ts @@ -2,7 +2,7 @@ import { metadata, schemaTask } from "@trigger.dev/sdk/v3"; import { installOpenClaw } from "../sandboxes/installOpenClaw"; import { setupOpenClaw } from "../sandboxes/setupOpenClaw"; import { cloneMonorepoViaAgent } from "../sandboxes/cloneMonorepoViaAgent"; -import { runOpenClawAgent } from "../sandboxes/runOpenClawAgent"; +import { runClaudeCodeAgent } from "../sandboxes/runClaudeCodeAgent"; import { pushAndCreatePRsViaAgent } from "../sandboxes/pushAndCreatePRsViaAgent"; import { notifyCodingAgentCallback } from "../sandboxes/notifyCodingAgentCallback"; import { logStep } from "../sandboxes/logStep"; @@ -44,7 +44,7 @@ export const codingAgentTask = schemaTask({ await syncMonorepoSubmodules(sandbox); logStep("Running AI agent"); - const agentResult = await runOpenClawAgent(sandbox, { + const agentResult = await runClaudeCodeAgent(sandbox, { label: "Coding agent", message: prompt, }); diff --git a/src/tasks/updatePRTask.ts b/src/tasks/updatePRTask.ts index 5921459..05c8f02 100644 --- a/src/tasks/updatePRTask.ts +++ b/src/tasks/updatePRTask.ts @@ -3,7 +3,7 @@ import { Sandbox } from "@vercel/sandbox"; import { getVercelSandboxCredentials } from "../sandboxes/getVercelSandboxCredentials"; import { installOpenClaw } from "../sandboxes/installOpenClaw"; import { setupOpenClaw } from "../sandboxes/setupOpenClaw"; -import { runOpenClawAgent } from "../sandboxes/runOpenClawAgent"; +import { runClaudeCodeAgent } from "../sandboxes/runClaudeCodeAgent"; import { notifyCodingAgentCallback } from "../sandboxes/notifyCodingAgentCallback"; import { logStep } from "../sandboxes/logStep"; import { configureGitAuth } from "../sandboxes/configureGitAuth"; @@ -47,14 +47,14 @@ export const updatePRTask = schemaTask({ const env = getSandboxEnv(CODING_AGENT_ACCOUNT_ID); logStep("Running AI agent with feedback"); - const agentResult = await runOpenClawAgent(sandbox, { + const agentResult = await runClaudeCodeAgent(sandbox, { label: "Apply feedback", message: `The following feedback was given on the existing changes on branch "${branch}":\n\n${feedback}\n\nPlease make the requested changes.`, env, }); logStep("Pushing updates via agent"); - await runOpenClawAgent(sandbox, { + await runClaudeCodeAgent(sandbox, { label: "Push feedback changes", env, message: [ From 1d4706c9da81994dacaabc858ecab7e5be5d5f89 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 15:25:45 +0000 Subject: [PATCH 02/10] agent: address feedback --- src/sandboxes/__tests__/runClaudeCodeAgent.test.ts | 4 ++-- src/sandboxes/runClaudeCodeAgent.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts index 751294a..08bc0a8 100644 --- a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts +++ b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts @@ -46,7 +46,7 @@ describe("runClaudeCodeAgent", () => { expect(sandbox.runCommand).toHaveBeenCalledWith({ cmd: "claude", - args: ["--print", "Fix the bug"], + args: ["-p", "--dangerously-skip-permissions", "Fix the bug"], detached: true, }); }); @@ -69,7 +69,7 @@ describe("runClaudeCodeAgent", () => { expect(sandbox.runCommand).toHaveBeenCalledWith({ cmd: "claude", - args: ["--print", "Update the README"], + args: ["-p", "--dangerously-skip-permissions", "Update the README"], detached: true, env: { GITHUB_TOKEN: "ghp_test" }, }); diff --git a/src/sandboxes/runClaudeCodeAgent.ts b/src/sandboxes/runClaudeCodeAgent.ts index d2a326e..2026e5a 100644 --- a/src/sandboxes/runClaudeCodeAgent.ts +++ b/src/sandboxes/runClaudeCodeAgent.ts @@ -27,7 +27,7 @@ export async function runClaudeCodeAgent( ): Promise { const { label, message, env } = options; - const args = ["--print", message]; + const args = ["-p", "--dangerously-skip-permissions", message]; logStep(label, true, { cmd: "claude", args }); From af70a82185e3e5589790f8a29586ca6f831c1ff3 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 15:29:53 +0000 Subject: [PATCH 03/10] agent: address feedback --- .../pushAndCreatePRsViaAgent.test.ts | 24 +++++++++---------- src/sandboxes/pushAndCreatePRsViaAgent.ts | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/sandboxes/__tests__/pushAndCreatePRsViaAgent.test.ts b/src/sandboxes/__tests__/pushAndCreatePRsViaAgent.test.ts index 1a004dc..c94c41a 100644 --- a/src/sandboxes/__tests__/pushAndCreatePRsViaAgent.test.ts +++ b/src/sandboxes/__tests__/pushAndCreatePRsViaAgent.test.ts @@ -5,8 +5,8 @@ vi.mock("@trigger.dev/sdk/v3", () => ({ metadata: { set: vi.fn(), append: vi.fn() }, })); -vi.mock("../runOpenClawAgent", () => ({ - runOpenClawAgent: vi.fn(), +vi.mock("../runClaudeCodeAgent", () => ({ + runClaudeCodeAgent: vi.fn(), })); const { pushAndCreatePRsViaAgent } = await import("../pushAndCreatePRsViaAgent"); @@ -17,8 +17,8 @@ beforeEach(() => { describe("pushAndCreatePRsViaAgent", () => { it("parses PR_CREATED lines from agent stdout", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); - vi.mocked(runOpenClawAgent).mockResolvedValueOnce({ + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + vi.mocked(runClaudeCodeAgent).mockResolvedValueOnce({ exitCode: 0, stdout: [ "Creating branch...", @@ -53,8 +53,8 @@ describe("pushAndCreatePRsViaAgent", () => { }); it("returns empty array when no PRs are created", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); - vi.mocked(runOpenClawAgent).mockResolvedValueOnce({ + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + vi.mocked(runClaudeCodeAgent).mockResolvedValueOnce({ exitCode: 0, stdout: "No changes detected in any submodule.\n", stderr: "", @@ -70,8 +70,8 @@ describe("pushAndCreatePRsViaAgent", () => { }); it("includes branch and prompt in the agent message", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); - vi.mocked(runOpenClawAgent).mockResolvedValueOnce({ + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + vi.mocked(runClaudeCodeAgent).mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "", @@ -83,14 +83,14 @@ describe("pushAndCreatePRsViaAgent", () => { branch: "agent/add-dark-mode-123", }); - const message = vi.mocked(runOpenClawAgent).mock.calls[0][1].message; + const message = vi.mocked(runClaudeCodeAgent).mock.calls[0][1].message; expect(message).toContain("agent/add-dark-mode-123"); expect(message).toContain("Add dark mode support"); }); it("includes submodule config in the agent message", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); - vi.mocked(runOpenClawAgent).mockResolvedValueOnce({ + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + vi.mocked(runClaudeCodeAgent).mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "", @@ -102,7 +102,7 @@ describe("pushAndCreatePRsViaAgent", () => { branch: "agent/fix-123", }); - const message = vi.mocked(runOpenClawAgent).mock.calls[0][1].message; + const message = vi.mocked(runClaudeCodeAgent).mock.calls[0][1].message; expect(message).toContain("recoupable/api"); expect(message).toContain("base branch=test"); expect(message).toContain("recoupable/tasks"); diff --git a/src/sandboxes/pushAndCreatePRsViaAgent.ts b/src/sandboxes/pushAndCreatePRsViaAgent.ts index 873edce..b9eb717 100644 --- a/src/sandboxes/pushAndCreatePRsViaAgent.ts +++ b/src/sandboxes/pushAndCreatePRsViaAgent.ts @@ -1,5 +1,5 @@ import type { Sandbox } from "@vercel/sandbox"; -import { runOpenClawAgent } from "./runOpenClawAgent"; +import { runClaudeCodeAgent } from "./runClaudeCodeAgent"; import { parsePRUrls, type ParsedPR } from "./parsePRUrls"; import { SUBMODULE_CONFIG } from "./submoduleConfig"; @@ -9,7 +9,7 @@ interface PushAndCreatePRsOptions { } /** - * Delegates push + PR creation to the OpenClaw agent. + * Delegates push + PR creation to the Claude Code agent. * Instructs the agent to create branches, commit, push, and open PRs * for each changed submodule. Parses PR_CREATED sentinel lines from stdout. * @@ -27,7 +27,7 @@ export async function pushAndCreatePRsViaAgent( .map(([name, { repo, baseBranch }]) => ` - ${name}: repo=${repo}, base branch=${baseBranch}`) .join("\n"); - const result = await runOpenClawAgent(sandbox, { + const result = await runClaudeCodeAgent(sandbox, { label: "Push and create PRs via agent", message: [ `For each submodule that has uncommitted changes, create a branch, commit, push, and open a PR.`, From 1b77cdfbf664b91f71bdf42e7a63c9d98625956c Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 15:46:56 +0000 Subject: [PATCH 04/10] agent: address feedback --- .../__tests__/cloneMonorepoViaAgent.test.ts | 20 +++++++++---------- src/sandboxes/cloneMonorepoViaAgent.ts | 4 ++-- src/sandboxes/git/syncMonorepoSubmodules.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts b/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts index d02d8c0..5ada408 100644 --- a/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts +++ b/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts @@ -5,8 +5,8 @@ vi.mock("@trigger.dev/sdk/v3", () => ({ metadata: { set: vi.fn(), append: vi.fn() }, })); -vi.mock("../runOpenClawAgent", () => ({ - runOpenClawAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }), +vi.mock("../runClaudeCodeAgent", () => ({ + runClaudeCodeAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }), })); const { cloneMonorepoViaAgent } = await import("../cloneMonorepoViaAgent"); @@ -16,14 +16,14 @@ beforeEach(() => { }); describe("cloneMonorepoViaAgent", () => { - it("calls runOpenClawAgent with clone instructions", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); + it("calls runClaudeCodeAgent with clone instructions", async () => { + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); const sandbox = {} as any; await cloneMonorepoViaAgent(sandbox); - expect(runOpenClawAgent).toHaveBeenCalledOnce(); - expect(runOpenClawAgent).toHaveBeenCalledWith( + expect(runClaudeCodeAgent).toHaveBeenCalledOnce(); + expect(runClaudeCodeAgent).toHaveBeenCalledWith( sandbox, expect.objectContaining({ label: "Clone monorepo via agent", @@ -33,22 +33,22 @@ describe("cloneMonorepoViaAgent", () => { }); it("instructs agent not to use --recursive", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); const sandbox = {} as any; await cloneMonorepoViaAgent(sandbox); - const message = vi.mocked(runOpenClawAgent).mock.calls[0][1].message; + const message = vi.mocked(runClaudeCodeAgent).mock.calls[0][1].message; expect(message).toContain("do NOT use --recursive"); }); it("does not include git user config (handled by configureGitAuth)", async () => { - const { runOpenClawAgent } = await import("../runOpenClawAgent"); + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); const sandbox = {} as any; await cloneMonorepoViaAgent(sandbox); - const message = vi.mocked(runOpenClawAgent).mock.calls[0][1].message; + const message = vi.mocked(runClaudeCodeAgent).mock.calls[0][1].message; expect(message).not.toContain("git config"); }); diff --git a/src/sandboxes/cloneMonorepoViaAgent.ts b/src/sandboxes/cloneMonorepoViaAgent.ts index 1dd257d..f9eca5f 100644 --- a/src/sandboxes/cloneMonorepoViaAgent.ts +++ b/src/sandboxes/cloneMonorepoViaAgent.ts @@ -1,5 +1,5 @@ import type { Sandbox } from "@vercel/sandbox"; -import { runOpenClawAgent } from "./runOpenClawAgent"; +import { runClaudeCodeAgent } from "./runClaudeCodeAgent"; /** * Delegates monorepo clone to the OpenClaw agent. @@ -11,7 +11,7 @@ import { runOpenClawAgent } from "./runOpenClawAgent"; export async function cloneMonorepoViaAgent( sandbox: Sandbox, ): Promise { - await runOpenClawAgent(sandbox, { + await runClaudeCodeAgent(sandbox, { label: "Clone monorepo via agent", message: [ "Clone the Recoup monorepo and init its submodules:", diff --git a/src/sandboxes/git/syncMonorepoSubmodules.ts b/src/sandboxes/git/syncMonorepoSubmodules.ts index d174ffa..0236532 100644 --- a/src/sandboxes/git/syncMonorepoSubmodules.ts +++ b/src/sandboxes/git/syncMonorepoSubmodules.ts @@ -1,5 +1,5 @@ import type { Sandbox } from "@vercel/sandbox"; -import { runOpenClawAgent } from "../runOpenClawAgent"; +import { runClaudeCodeAgent } from "../runClaudeCodeAgent"; import { logStep } from "../logStep"; /** @@ -32,7 +32,7 @@ export async function syncMonorepoSubmodules(sandbox: Sandbox): Promise { "Continue to the next submodule if one fails.", ].join("\n"); - await runOpenClawAgent(sandbox, { + await runClaudeCodeAgent(sandbox, { label: "Syncing monorepo submodules to latest remote", message, }); From 5170342ed87cc319b73af276449c23794d74b0fd Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 15:53:44 +0000 Subject: [PATCH 05/10] agent: address feedback --- src/sandboxes/cloneMonorepoViaAgent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sandboxes/cloneMonorepoViaAgent.ts b/src/sandboxes/cloneMonorepoViaAgent.ts index f9eca5f..f24b31a 100644 --- a/src/sandboxes/cloneMonorepoViaAgent.ts +++ b/src/sandboxes/cloneMonorepoViaAgent.ts @@ -15,7 +15,7 @@ export async function cloneMonorepoViaAgent( label: "Clone monorepo via agent", message: [ "Clone the Recoup monorepo and init its submodules:", - "1. Run: git clone https://github.com/recoupable/Recoup-Monorepo.git", + "1. Run: git clone https://github.com/recoupable/mono.git", "2. cd into the cloned directory", "3. Run: git submodule update --init (do NOT use --recursive)", ].join("\n"), From 8253d24b74171dec590fd67a2468a1907acbb1d3 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 15:57:28 +0000 Subject: [PATCH 06/10] agent: address feedback --- src/tasks/__tests__/codingAgentTask.test.ts | 8 -------- src/tasks/codingAgentTask.ts | 5 ----- 2 files changed, 13 deletions(-) diff --git a/src/tasks/__tests__/codingAgentTask.test.ts b/src/tasks/__tests__/codingAgentTask.test.ts index 1e9ae2c..e98f1c4 100644 --- a/src/tasks/__tests__/codingAgentTask.test.ts +++ b/src/tasks/__tests__/codingAgentTask.test.ts @@ -19,14 +19,6 @@ vi.mock("../../sandboxes/getOrCreateSandbox", () => ({ getOrCreateSandbox: (...args: unknown[]) => mockGetOrCreateSandbox(...args), })); -vi.mock("../../sandboxes/installOpenClaw", () => ({ - installOpenClaw: vi.fn(), -})); - -vi.mock("../../sandboxes/setupOpenClaw", () => ({ - setupOpenClaw: vi.fn(), -})); - vi.mock("../../sandboxes/configureGitAuth", () => ({ configureGitAuth: vi.fn(), })); diff --git a/src/tasks/codingAgentTask.ts b/src/tasks/codingAgentTask.ts index 7931c52..710bd31 100644 --- a/src/tasks/codingAgentTask.ts +++ b/src/tasks/codingAgentTask.ts @@ -1,6 +1,4 @@ import { metadata, schemaTask } from "@trigger.dev/sdk/v3"; -import { installOpenClaw } from "../sandboxes/installOpenClaw"; -import { setupOpenClaw } from "../sandboxes/setupOpenClaw"; import { cloneMonorepoViaAgent } from "../sandboxes/cloneMonorepoViaAgent"; import { runClaudeCodeAgent } from "../sandboxes/runClaudeCodeAgent"; import { pushAndCreatePRsViaAgent } from "../sandboxes/pushAndCreatePRsViaAgent"; @@ -32,9 +30,6 @@ export const codingAgentTask = schemaTask({ logStep("Sandbox created", false, { sandboxId }); try { - logStep("Installing OpenClaw"); - await installOpenClaw(sandbox); - await setupOpenClaw(sandbox, CODING_AGENT_ACCOUNT_ID); await configureGitAuth(sandbox); logStep("Cloning monorepo via agent"); From 6e26abda12a1b235b1c72b8eb8f10a005942a6e2 Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 16:28:04 +0000 Subject: [PATCH 07/10] agent: address feedback --- .../__tests__/runClaudeCodeAgent.test.ts | 24 +++++++++++ src/sandboxes/runClaudeCodeAgent.ts | 11 +++-- src/tasks/__tests__/updatePRTask.test.ts | 42 ++++++++----------- src/tasks/updatePRTask.ts | 25 ++++++++--- 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts index 08bc0a8..6236fee 100644 --- a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts +++ b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts @@ -51,6 +51,30 @@ describe("runClaudeCodeAgent", () => { }); }); + it("passes cwd when provided", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { + label: "Apply feedback", + message: "Fix it", + cwd: "/root/.openclaw/workspace/mono/tasks", + }); + + expect(sandbox.runCommand).toHaveBeenCalledWith({ + cmd: "claude", + args: ["-p", "--dangerously-skip-permissions", "Fix it"], + detached: true, + cwd: "/root/.openclaw/workspace/mono/tasks", + }); + }); + it("passes env vars when provided", async () => { const sandbox = createMockSandbox(); sandbox.runCommand.mockResolvedValueOnce( diff --git a/src/sandboxes/runClaudeCodeAgent.ts b/src/sandboxes/runClaudeCodeAgent.ts index 2026e5a..ac68e78 100644 --- a/src/sandboxes/runClaudeCodeAgent.ts +++ b/src/sandboxes/runClaudeCodeAgent.ts @@ -5,6 +5,7 @@ interface RunClaudeCodeAgentOptions { label: string; message: string; env?: Record; + cwd?: string; } interface RunClaudeCodeAgentResult { @@ -18,18 +19,18 @@ interface RunClaudeCodeAgentResult { * Uses the `claude` CLI with --print flag for non-interactive execution. * * @param sandbox - The Vercel Sandbox instance - * @param options - Label for logging/metadata, message prompt, optional env vars + * @param options - Label for logging/metadata, message prompt, optional env vars, optional cwd * @returns exitCode, stdout, and stderr from the command */ export async function runClaudeCodeAgent( sandbox: Sandbox, options: RunClaudeCodeAgentOptions, ): Promise { - const { label, message, env } = options; + const { label, message, env, cwd } = options; const args = ["-p", "--dangerously-skip-permissions", message]; - logStep(label, true, { cmd: "claude", args }); + logStep(label, true, { cmd: "claude", args, cwd }); const commandOpts: Record = { cmd: "claude", @@ -41,6 +42,10 @@ export async function runClaudeCodeAgent( commandOpts.env = env; } + if (cwd) { + commandOpts.cwd = cwd; + } + const command = await sandbox.runCommand(commandOpts as any); const result = await command.wait(); diff --git a/src/tasks/__tests__/updatePRTask.test.ts b/src/tasks/__tests__/updatePRTask.test.ts index b67e879..73ab154 100644 --- a/src/tasks/__tests__/updatePRTask.test.ts +++ b/src/tasks/__tests__/updatePRTask.test.ts @@ -27,14 +27,6 @@ vi.mock("../../sandboxes/getVercelSandboxCredentials", () => ({ }), })); -vi.mock("../../sandboxes/installOpenClaw", () => ({ - installOpenClaw: vi.fn(), -})); - -vi.mock("../../sandboxes/setupOpenClaw", () => ({ - setupOpenClaw: vi.fn(), -})); - vi.mock("../../sandboxes/runClaudeCodeAgent", () => ({ runClaudeCodeAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "done", stderr: "" }), })); @@ -90,7 +82,7 @@ describe("updatePRTask", () => { ); }); - it("runs OpenClaw agent with feedback prompt", async () => { + it("runs agent with feedback prompt", async () => { const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); await mockRun(basePayload); @@ -103,7 +95,23 @@ describe("updatePRTask", () => { ); }); - it("delegates push to agent instead of using runGitCommand", async () => { + it("runs agent in the correct submodule cwd derived from repo", async () => { + const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); + + await mockRun(basePayload); + + const calls = vi.mocked(runClaudeCodeAgent).mock.calls; + // Both apply-feedback and push calls should target the submodule directory + calls.forEach(call => { + expect(call[1]).toEqual( + expect.objectContaining({ + cwd: expect.stringContaining("/api"), + }), + ); + }); + }); + + it("delegates push to agent", async () => { const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); await mockRun(basePayload); @@ -148,19 +156,6 @@ describe("updatePRTask", () => { expect(configureGitAuth).toHaveBeenCalledOnce(); }); - it("creates sandbox with correct timeout param (not timeoutMs)", async () => { - await mockRun(basePayload); - - expect(mockSandboxCreate).toHaveBeenCalledWith( - expect.objectContaining({ - timeout: 30 * 60 * 1000, - }), - ); - // Ensure the wrong param name is NOT used - const callArgs = mockSandboxCreate.mock.calls[0][0]; - expect(callArgs).not.toHaveProperty("timeoutMs"); - }); - it("passes sandbox env to both agent calls", async () => { const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); const { getSandboxEnv } = await import("../../sandboxes/getSandboxEnv"); @@ -175,7 +170,6 @@ describe("updatePRTask", () => { GITHUB_TOKEN: "test-gh-token", }; - // Both agent calls should receive env const calls = vi.mocked(runClaudeCodeAgent).mock.calls; expect(calls[0][1]).toEqual(expect.objectContaining({ env: expectedEnv })); expect(calls[1][1]).toEqual(expect.objectContaining({ env: expectedEnv })); diff --git a/src/tasks/updatePRTask.ts b/src/tasks/updatePRTask.ts index 05c8f02..be83ad1 100644 --- a/src/tasks/updatePRTask.ts +++ b/src/tasks/updatePRTask.ts @@ -1,8 +1,6 @@ import { metadata, schemaTask } from "@trigger.dev/sdk/v3"; import { Sandbox } from "@vercel/sandbox"; import { getVercelSandboxCredentials } from "../sandboxes/getVercelSandboxCredentials"; -import { installOpenClaw } from "../sandboxes/installOpenClaw"; -import { setupOpenClaw } from "../sandboxes/setupOpenClaw"; import { runClaudeCodeAgent } from "../sandboxes/runClaudeCodeAgent"; import { notifyCodingAgentCallback } from "../sandboxes/notifyCodingAgentCallback"; import { logStep } from "../sandboxes/logStep"; @@ -11,9 +9,24 @@ import { getSandboxEnv } from "../sandboxes/getSandboxEnv"; import { updatePRPayloadSchema } from "../schemas/updatePRSchema"; import { CODING_AGENT_ACCOUNT_ID } from "../consts"; +const MONOREPO_DIR = `${process.env.HOME ?? "/root"}/.openclaw/workspace/mono`; + +/** + * Derives the absolute path to a submodule inside the monorepo from a + * repo identifier like "recoupable/tasks". + * + * @param repo - Full repo identifier, e.g. "recoupable/tasks" + * @returns Absolute path to the submodule directory in the sandbox + */ +function getSubmoduleCwd(repo: string): string { + const repoName = repo.split("/")[1]; + return `${MONOREPO_DIR}/${repoName}`; +} + /** * Background task that resumes a sandbox from a snapshot, applies feedback - * via the AI agent, and delegates push of updates to the agent. + * via the Claude Code agent in the correct submodule directory, and pushes + * the updates to the existing PR branch. */ export const updatePRTask = schemaTask({ id: "update-pr", @@ -39,24 +52,24 @@ export const updatePRTask = schemaTask({ logStep("Sandbox resumed", false, { sandboxId: sandbox.sandboxId, snapshotId }); try { - logStep("Ensuring OpenClaw is running"); - await installOpenClaw(sandbox); - await setupOpenClaw(sandbox, CODING_AGENT_ACCOUNT_ID); await configureGitAuth(sandbox); const env = getSandboxEnv(CODING_AGENT_ACCOUNT_ID); + const cwd = getSubmoduleCwd(repo); logStep("Running AI agent with feedback"); const agentResult = await runClaudeCodeAgent(sandbox, { label: "Apply feedback", message: `The following feedback was given on the existing changes on branch "${branch}":\n\n${feedback}\n\nPlease make the requested changes.`, env, + cwd, }); logStep("Pushing updates via agent"); await runClaudeCodeAgent(sandbox, { label: "Push feedback changes", env, + cwd, message: [ `Stage, commit, and push the feedback changes to the existing PR branch.`, ``, From 617f3f0695efd052f2bffb76d7a253a0b4c067dc Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 16:34:11 +0000 Subject: [PATCH 08/10] agent: address feedback --- .../__tests__/runClaudeCodeAgent.test.ts | 24 ---------------- src/sandboxes/runClaudeCodeAgent.ts | 11 ++------ src/tasks/__tests__/updatePRTask.test.ts | 28 ++++++++----------- src/tasks/updatePRTask.ts | 20 +------------ 4 files changed, 16 insertions(+), 67 deletions(-) diff --git a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts index 6236fee..08bc0a8 100644 --- a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts +++ b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts @@ -51,30 +51,6 @@ describe("runClaudeCodeAgent", () => { }); }); - it("passes cwd when provided", async () => { - const sandbox = createMockSandbox(); - sandbox.runCommand.mockResolvedValueOnce( - mockDetachedCommand({ - exitCode: 0, - stdout: async () => "", - stderr: async () => "", - }), - ); - - await runClaudeCodeAgent(sandbox, { - label: "Apply feedback", - message: "Fix it", - cwd: "/root/.openclaw/workspace/mono/tasks", - }); - - expect(sandbox.runCommand).toHaveBeenCalledWith({ - cmd: "claude", - args: ["-p", "--dangerously-skip-permissions", "Fix it"], - detached: true, - cwd: "/root/.openclaw/workspace/mono/tasks", - }); - }); - it("passes env vars when provided", async () => { const sandbox = createMockSandbox(); sandbox.runCommand.mockResolvedValueOnce( diff --git a/src/sandboxes/runClaudeCodeAgent.ts b/src/sandboxes/runClaudeCodeAgent.ts index ac68e78..2026e5a 100644 --- a/src/sandboxes/runClaudeCodeAgent.ts +++ b/src/sandboxes/runClaudeCodeAgent.ts @@ -5,7 +5,6 @@ interface RunClaudeCodeAgentOptions { label: string; message: string; env?: Record; - cwd?: string; } interface RunClaudeCodeAgentResult { @@ -19,18 +18,18 @@ interface RunClaudeCodeAgentResult { * Uses the `claude` CLI with --print flag for non-interactive execution. * * @param sandbox - The Vercel Sandbox instance - * @param options - Label for logging/metadata, message prompt, optional env vars, optional cwd + * @param options - Label for logging/metadata, message prompt, optional env vars * @returns exitCode, stdout, and stderr from the command */ export async function runClaudeCodeAgent( sandbox: Sandbox, options: RunClaudeCodeAgentOptions, ): Promise { - const { label, message, env, cwd } = options; + const { label, message, env } = options; const args = ["-p", "--dangerously-skip-permissions", message]; - logStep(label, true, { cmd: "claude", args, cwd }); + logStep(label, true, { cmd: "claude", args }); const commandOpts: Record = { cmd: "claude", @@ -42,10 +41,6 @@ export async function runClaudeCodeAgent( commandOpts.env = env; } - if (cwd) { - commandOpts.cwd = cwd; - } - const command = await sandbox.runCommand(commandOpts as any); const result = await command.wait(); diff --git a/src/tasks/__tests__/updatePRTask.test.ts b/src/tasks/__tests__/updatePRTask.test.ts index 73ab154..0b1c9e7 100644 --- a/src/tasks/__tests__/updatePRTask.test.ts +++ b/src/tasks/__tests__/updatePRTask.test.ts @@ -95,22 +95,6 @@ describe("updatePRTask", () => { ); }); - it("runs agent in the correct submodule cwd derived from repo", async () => { - const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); - - await mockRun(basePayload); - - const calls = vi.mocked(runClaudeCodeAgent).mock.calls; - // Both apply-feedback and push calls should target the submodule directory - calls.forEach(call => { - expect(call[1]).toEqual( - expect.objectContaining({ - cwd: expect.stringContaining("/api"), - }), - ); - }); - }); - it("delegates push to agent", async () => { const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); @@ -156,6 +140,18 @@ describe("updatePRTask", () => { expect(configureGitAuth).toHaveBeenCalledOnce(); }); + it("creates sandbox with correct timeout param", async () => { + await mockRun(basePayload); + + expect(mockSandboxCreate).toHaveBeenCalledWith( + expect.objectContaining({ + timeout: 30 * 60 * 1000, + }), + ); + const callArgs = mockSandboxCreate.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty("timeoutMs"); + }); + it("passes sandbox env to both agent calls", async () => { const { runClaudeCodeAgent } = await import("../../sandboxes/runClaudeCodeAgent"); const { getSandboxEnv } = await import("../../sandboxes/getSandboxEnv"); diff --git a/src/tasks/updatePRTask.ts b/src/tasks/updatePRTask.ts index be83ad1..d5ac75e 100644 --- a/src/tasks/updatePRTask.ts +++ b/src/tasks/updatePRTask.ts @@ -9,24 +9,9 @@ import { getSandboxEnv } from "../sandboxes/getSandboxEnv"; import { updatePRPayloadSchema } from "../schemas/updatePRSchema"; import { CODING_AGENT_ACCOUNT_ID } from "../consts"; -const MONOREPO_DIR = `${process.env.HOME ?? "/root"}/.openclaw/workspace/mono`; - -/** - * Derives the absolute path to a submodule inside the monorepo from a - * repo identifier like "recoupable/tasks". - * - * @param repo - Full repo identifier, e.g. "recoupable/tasks" - * @returns Absolute path to the submodule directory in the sandbox - */ -function getSubmoduleCwd(repo: string): string { - const repoName = repo.split("/")[1]; - return `${MONOREPO_DIR}/${repoName}`; -} - /** * Background task that resumes a sandbox from a snapshot, applies feedback - * via the Claude Code agent in the correct submodule directory, and pushes - * the updates to the existing PR branch. + * via the Claude Code agent, and pushes the updates to the existing PR branch. */ export const updatePRTask = schemaTask({ id: "update-pr", @@ -55,21 +40,18 @@ export const updatePRTask = schemaTask({ await configureGitAuth(sandbox); const env = getSandboxEnv(CODING_AGENT_ACCOUNT_ID); - const cwd = getSubmoduleCwd(repo); logStep("Running AI agent with feedback"); const agentResult = await runClaudeCodeAgent(sandbox, { label: "Apply feedback", message: `The following feedback was given on the existing changes on branch "${branch}":\n\n${feedback}\n\nPlease make the requested changes.`, env, - cwd, }); logStep("Pushing updates via agent"); await runClaudeCodeAgent(sandbox, { label: "Push feedback changes", env, - cwd, message: [ `Stage, commit, and push the feedback changes to the existing PR branch.`, ``, From da8f42d5dac7c6e12509dc98c2fa8a52d51329aa Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Thu, 12 Mar 2026 16:42:07 +0000 Subject: [PATCH 09/10] agent: address feedback --- .../__tests__/cloneMonorepoViaAgent.test.ts | 2 +- .../__tests__/syncMonorepoSubmodules.test.ts | 55 ++++++------------- src/sandboxes/git/syncMonorepoSubmodules.ts | 4 +- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts b/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts index 5ada408..8f338cf 100644 --- a/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts +++ b/src/sandboxes/__tests__/cloneMonorepoViaAgent.test.ts @@ -27,7 +27,7 @@ describe("cloneMonorepoViaAgent", () => { sandbox, expect.objectContaining({ label: "Clone monorepo via agent", - message: expect.stringContaining("Recoup-Monorepo"), + message: expect.stringContaining("recoupable/mono"), }), ); }); diff --git a/src/sandboxes/__tests__/syncMonorepoSubmodules.test.ts b/src/sandboxes/__tests__/syncMonorepoSubmodules.test.ts index 77ea4a1..e76d35c 100644 --- a/src/sandboxes/__tests__/syncMonorepoSubmodules.test.ts +++ b/src/sandboxes/__tests__/syncMonorepoSubmodules.test.ts @@ -5,70 +5,47 @@ vi.mock("@trigger.dev/sdk/v3", () => ({ metadata: { set: vi.fn(), append: vi.fn() }, })); -const { syncMonorepoSubmodules } = await import("../git/syncMonorepoSubmodules"); - -function createMockSandbox() { - const runCommand = vi.fn().mockResolvedValue({ - wait: vi.fn().mockResolvedValue({ - exitCode: 0, - stdout: async () => "", - stderr: async () => "", - }), - exitCode: 0, - stdout: async () => "", - stderr: async () => "", - }); +vi.mock("../runClaudeCodeAgent", () => ({ + runClaudeCodeAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }), +})); - return { runCommand } as any; -} +const { syncMonorepoSubmodules } = await import("../git/syncMonorepoSubmodules"); beforeEach(() => { vi.clearAllMocks(); }); describe("syncMonorepoSubmodules", () => { - it("runs an openclaw agent prompt to sync submodules", async () => { - const sandbox = createMockSandbox(); + it("runs a claude code agent prompt to sync submodules", async () => { + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + const sandbox = {} as any; await syncMonorepoSubmodules(sandbox); - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" - ); - expect(openclawCall).toBeDefined(); + expect(runClaudeCodeAgent).toHaveBeenCalledOnce(); }); it("instructs git fetch and checkout for each submodule", async () => { - const sandbox = createMockSandbox(); + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + const sandbox = {} as any; await syncMonorepoSubmodules(sandbox); - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" - ); - const args = openclawCall![0].args; - const message = args.find( - (a: string, i: number) => args[i - 1] === "--message" - ); + const message = vi.mocked(runClaudeCodeAgent).mock.calls[0][1].message; expect(message).toContain("git fetch"); expect(message).toContain("git checkout"); expect(message).toContain("git reset --hard"); }); - it("targets the Recoup-Monorepo directory", async () => { - const sandbox = createMockSandbox(); + it("targets the mono directory", async () => { + const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); + const sandbox = {} as any; await syncMonorepoSubmodules(sandbox); - const openclawCall = sandbox.runCommand.mock.calls.find( - (call: any[]) => call[0]?.cmd === "openclaw" - ); - const args = openclawCall![0].args; - const message = args.find( - (a: string, i: number) => args[i - 1] === "--message" - ); + const message = vi.mocked(runClaudeCodeAgent).mock.calls[0][1].message; - expect(message).toContain("Recoup-Monorepo"); + expect(message).toContain("mono"); }); }); diff --git a/src/sandboxes/git/syncMonorepoSubmodules.ts b/src/sandboxes/git/syncMonorepoSubmodules.ts index 0236532..d90e0e9 100644 --- a/src/sandboxes/git/syncMonorepoSubmodules.ts +++ b/src/sandboxes/git/syncMonorepoSubmodules.ts @@ -4,7 +4,7 @@ import { logStep } from "../logStep"; /** * Syncs all monorepo submodules to their latest remote base branch - * via an OpenClaw agent prompt. + * via a Claude Code agent prompt. * * This ensures PRs are created against the most up-to-date reference, * preventing stale base branches from causing merge conflicts. @@ -19,7 +19,7 @@ export async function syncMonorepoSubmodules(sandbox: Sandbox): Promise { const message = [ "Sync all monorepo submodules to their latest remote base branch.", - "The monorepo is at ~/.openclaw/workspace/Recoup-Monorepo/", + "The monorepo is at ~/.openclaw/workspace/mono/", "", "For each submodule directory:", "1. cd into the submodule", From ecc1af35c5420687bc33fd40b9389579a7467bd6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 12 Mar 2026 15:31:30 -0500 Subject: [PATCH 10/10] fix: verify git remote matches expected repo when restoring from snapshot Snapshots can carry a stale git remote from a different account's sandbox. When .git already exists, check that origin matches the expected githubRepo and update it if not. Also adds remoteUrl to push logs for easier debugging. Co-Authored-By: Claude Opus 4.6 --- src/sandboxes/ensureGithubRepo.ts | 26 ++++++++++++++++++++++++++ src/sandboxes/pushSandboxToGithub.ts | 10 ++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/sandboxes/ensureGithubRepo.ts b/src/sandboxes/ensureGithubRepo.ts index 73b9f27..1b3c51e 100644 --- a/src/sandboxes/ensureGithubRepo.ts +++ b/src/sandboxes/ensureGithubRepo.ts @@ -67,6 +67,32 @@ export async function ensureGithubRepo( }); if (gitCheck.exitCode === 0) { + // Verify the remote matches the expected repo — snapshots may carry + // a stale remote from a different account's sandbox. + const remoteResult = await sandbox.runCommand({ + cmd: "git", + args: ["remote", "get-url", "origin"], + }); + const currentRemote = ((await remoteResult.stdout()) || "").trim(); + // Strip auth prefix for comparison (remote may include x-access-token) + const normalizedRemote = currentRemote.replace( + /https:\/\/x-access-token:[^@]+@github\.com\//, + "https://github.com/" + ); + + if (normalizedRemote !== githubRepo) { + logger.log("Sandbox remote mismatch, updating origin", { + expected: githubRepo, + actual: normalizedRemote, + }); + const repoUrl = githubRepo.replace("https://github.com/", authPrefix); + await runGitCommand( + sandbox, + ["remote", "set-url", "origin", repoUrl], + "update remote to correct repo" + ); + } + logger.log("GitHub repo already cloned in sandbox", { githubRepo }); return githubRepo; } diff --git a/src/sandboxes/pushSandboxToGithub.ts b/src/sandboxes/pushSandboxToGithub.ts index 4f711b6..c5b691a 100644 --- a/src/sandboxes/pushSandboxToGithub.ts +++ b/src/sandboxes/pushSandboxToGithub.ts @@ -18,7 +18,13 @@ import { pushOrgRepos } from "./git/pushOrgRepos"; export async function pushSandboxToGithub( sandbox: Sandbox ): Promise { - logger.log("Pushing sandbox files to GitHub"); + // Log which repo we're pushing to by reading the current remote + const remoteCheck = await sandbox.runCommand({ + cmd: "git", + args: ["remote", "get-url", "origin"], + }); + const remoteUrl = ((await remoteCheck.stdout()) || "").trim(); + logger.log("Pushing sandbox files to GitHub", { remoteUrl }); // Configure git user for commits if ( @@ -91,6 +97,6 @@ export async function pushSandboxToGithub( return false; } - logger.log("Sandbox files pushed to GitHub successfully"); + logger.log("Sandbox files pushed to GitHub successfully", { remoteUrl }); return true; }