From 05744dc6584b0cdcb4e106dab3dd3d68ae11912a Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Wed, 27 May 2026 14:16:20 +0100 Subject: [PATCH 1/2] feat(signed-commit): add optional cwd arg so the tool works on any clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The signed-commit tool's `ctx.cwd` is bound once at session creation (`agent-server.ts` passes `this.config.repositoryPath ?? "/tmp/workspace"` into the new session, and the local-tools MCP server captures that as its `LocalToolCtx.cwd`). All git invocations inside `createSignedCommit` run against that bound cwd, so a task that clones a second repo (or works in any checkout outside the original session cwd) can't commit from it — `git remote get-url origin` runs from the wrong directory and fails. Add an optional `cwd` to the tool schema. The handler resolves it against the session cwd via `path.resolve` (absolute paths pass through, relative paths join the session cwd) and uses it as the ctx for `createSignedCommit`. Default behavior is unchanged: omit `cwd` and the tool still runs against the session cwd. Test covers the four cases: default-to-session-cwd, absolute override, relative resolution, and that `cwd` isn't forwarded as a `SignedCommitInput` field; plus the existing no-token error path. Generated-By: PostHog Code Task-Id: d4c15be5-816f-4f11-9c71-3e354a272681 --- .../local-tools/tools/signed-commit.test.ts | 94 +++++++++++++++++++ .../local-tools/tools/signed-commit.ts | 12 ++- .../src/adapters/signed-commit-shared.ts | 8 ++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts diff --git a/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts b/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts new file mode 100644 index 000000000..6b96d9f76 --- /dev/null +++ b/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts @@ -0,0 +1,94 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const createSignedCommit = vi.fn(); + +vi.mock("@posthog/git/signed-commit", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + createSignedCommit: (...args: unknown[]) => createSignedCommit(...args), + }; +}); + +// Importing the tool after the mock so its transitive `createSignedCommit` +// reference resolves to the mock above. +const { signedCommitTool } = await import("./signed-commit"); + +describe("signed-commit tool handler", () => { + const savedSandbox = process.env.IS_SANDBOX; + + beforeEach(() => { + createSignedCommit.mockReset(); + createSignedCommit.mockResolvedValue({ + branch: "posthog-code/feature", + commits: [{ sha: "deadbeef", url: "https://github.com/x/y/commit/deadbeef" }], + }); + }); + + afterEach(() => { + if (savedSandbox === undefined) { + delete process.env.IS_SANDBOX; + } else { + process.env.IS_SANDBOX = savedSandbox; + } + }); + + it("defaults to the session cwd when args.cwd is absent", async () => { + await signedCommitTool.handler( + { cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" }, + { message: "chore: bump" }, + ); + const [ctx] = createSignedCommit.mock.calls[0]; + expect(ctx.cwd).toBe("/tmp/workspace/repos/posthog/code"); + }); + + it("uses an absolute args.cwd verbatim so a sibling clone is reachable", async () => { + await signedCommitTool.handler( + { cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" }, + { + message: "chore: bump", + cwd: "/tmp/workspace/repos/posthog/posthog", + }, + ); + const [ctx] = createSignedCommit.mock.calls[0]; + expect(ctx.cwd).toBe("/tmp/workspace/repos/posthog/posthog"); + }); + + it("resolves a relative args.cwd against the session cwd", async () => { + await signedCommitTool.handler( + { cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" }, + { message: "chore: bump", cwd: "../posthog" }, + ); + const [ctx] = createSignedCommit.mock.calls[0]; + expect(ctx.cwd).toBe("/tmp/workspace/repos/posthog/posthog"); + }); + + it("does not forward cwd to createSignedCommit input", async () => { + await signedCommitTool.handler( + { cwd: "/tmp/workspace/repos/posthog/code", token: "ghs_x" }, + { message: "chore: bump", cwd: "/elsewhere" }, + ); + const [, input] = createSignedCommit.mock.calls[0]; + expect(input).not.toHaveProperty("cwd"); + expect(input).toEqual({ message: "chore: bump" }); + }); + + it("returns the no-token error without invoking createSignedCommit", async () => { + const savedGh = process.env.GH_TOKEN; + const savedGithub = process.env.GITHUB_TOKEN; + delete process.env.GH_TOKEN; + delete process.env.GITHUB_TOKEN; + try { + const result = await signedCommitTool.handler( + { cwd: "/tmp/workspace/repos/posthog/code" }, + { message: "chore: bump" }, + ); + expect(result.isError).toBe(true); + expect(createSignedCommit).not.toHaveBeenCalled(); + } finally { + if (savedGh !== undefined) process.env.GH_TOKEN = savedGh; + if (savedGithub !== undefined) process.env.GITHUB_TOKEN = savedGithub; + } + }); +}); diff --git a/packages/agent/src/adapters/local-tools/tools/signed-commit.ts b/packages/agent/src/adapters/local-tools/tools/signed-commit.ts index 24b78e5f0..0b82a8f5f 100644 --- a/packages/agent/src/adapters/local-tools/tools/signed-commit.ts +++ b/packages/agent/src/adapters/local-tools/tools/signed-commit.ts @@ -1,3 +1,4 @@ +import * as path from "node:path"; import { isCloudRun, resolveGithubToken } from "../../../utils/common"; import { runSignedCommitTool, @@ -33,9 +34,12 @@ export const signedCommitTool = defineLocalTool({ isError: true, }); } - return runSignedCommitTool( - { cwd: ctx.cwd, token, taskId: ctx.taskId }, - args, - ); + // Resolve an explicit `cwd` arg against the session cwd so the agent can + // commit from any clone reachable in the sandbox, not just the one the + // session was rooted at. Absolute paths fall through `path.resolve` + // unchanged; relative paths join the session cwd. + const { cwd: argCwd, ...input } = args; + const cwd = argCwd ? path.resolve(ctx.cwd, argCwd) : ctx.cwd; + return runSignedCommitTool({ cwd, token, taskId: ctx.taskId }, input); }, }); diff --git a/packages/agent/src/adapters/signed-commit-shared.ts b/packages/agent/src/adapters/signed-commit-shared.ts index d1014031a..4f7c24fe4 100644 --- a/packages/agent/src/adapters/signed-commit-shared.ts +++ b/packages/agent/src/adapters/signed-commit-shared.ts @@ -41,6 +41,14 @@ export const signedCommitToolSchema = { .describe( "Files to stage before committing; defaults to already-staged files.", ), + cwd: z + .string() + .optional() + .describe( + "Path to the git checkout to commit from; defaults to the session's working directory. " + + "Pass this when committing to a clone outside the session cwd (e.g. a sibling repo cloned during the run). " + + "Relative paths resolve against the session cwd.", + ), }; export function formatSignedCommitResult(result: SignedCommitResult): string { From 792201029ecf8dc7b6954e20dfcf2839afdea4cc Mon Sep 17 00:00:00 2001 From: Joshua Snyder Date: Wed, 27 May 2026 14:32:30 +0100 Subject: [PATCH 2/2] style: apply biome formatter to signed-commit test Generated-By: PostHog Code Task-Id: d4c15be5-816f-4f11-9c71-3e354a272681 --- .../src/adapters/local-tools/tools/signed-commit.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts b/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts index 6b96d9f76..b749929f9 100644 --- a/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts +++ b/packages/agent/src/adapters/local-tools/tools/signed-commit.test.ts @@ -22,7 +22,9 @@ describe("signed-commit tool handler", () => { createSignedCommit.mockReset(); createSignedCommit.mockResolvedValue({ branch: "posthog-code/feature", - commits: [{ sha: "deadbeef", url: "https://github.com/x/y/commit/deadbeef" }], + commits: [ + { sha: "deadbeef", url: "https://github.com/x/y/commit/deadbeef" }, + ], }); });