@posthog/agent 2.3.647 → 2.3.655
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/permissions/permission-options.js +700 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/tools.js +700 -0
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/adapters/codex/local-tools-mcp-server.d.ts +2 -0
- package/dist/adapters/codex/local-tools-mcp-server.js +1172 -0
- package/dist/adapters/codex/local-tools-mcp-server.js.map +1 -0
- package/dist/agent.js +1488 -219
- package/dist/agent.js.map +1 -1
- package/dist/execution-mode.js +700 -0
- package/dist/execution-mode.js.map +1 -1
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +1604 -339
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1520 -258
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +3 -3
- package/src/adapters/claude/claude-agent.ts +32 -2
- package/src/adapters/claude/hooks.test.ts +54 -0
- package/src/adapters/claude/hooks.ts +86 -0
- package/src/adapters/claude/mcp/local-tools.test.ts +50 -0
- package/src/adapters/claude/mcp/local-tools.ts +40 -0
- package/src/adapters/claude/session/options.ts +14 -9
- package/src/adapters/claude/types.ts +1 -0
- package/src/adapters/codex/codex-agent.ts +117 -22
- package/src/adapters/codex/local-tools-mcp-server.ts +71 -0
- package/src/adapters/local-tools/index.ts +22 -0
- package/src/adapters/local-tools/registry.test.ts +57 -0
- package/src/adapters/local-tools/registry.ts +81 -0
- package/src/adapters/local-tools/tools/signed-commit.ts +26 -0
- package/src/adapters/session-meta.ts +16 -0
- package/src/adapters/signed-commit-shared.ts +82 -0
- package/src/server/agent-server.test.ts +2 -4
- package/src/server/agent-server.ts +27 -30
- package/src/utils/common.ts +14 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone stdio MCP server exposing the general local tools to the Codex
|
|
3
|
+
* adapter. Spawned by codex-acp as an MCP server process. Reads its context
|
|
4
|
+
* (cwd, taskId, token) from POSTHOG_LOCAL_TOOLS_CTX and the set of tools to
|
|
5
|
+
* register from POSTHOG_LOCAL_TOOLS_ENABLED (both set by the parent, which has
|
|
6
|
+
* already evaluated each tool's gate) — then registers those registry tools,
|
|
7
|
+
* the same ones the Claude adapter exposes in-process.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* POSTHOG_LOCAL_TOOLS_CTX=<base64> \
|
|
11
|
+
* POSTHOG_LOCAL_TOOLS_ENABLED=git_signed_commit \
|
|
12
|
+
* node local-tools-mcp-server.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { readGithubTokenFromEnv } from "@posthog/git/signed-commit";
|
|
18
|
+
import {
|
|
19
|
+
LOCAL_TOOLS,
|
|
20
|
+
LOCAL_TOOLS_MCP_NAME,
|
|
21
|
+
type LocalToolCtx,
|
|
22
|
+
} from "../local-tools";
|
|
23
|
+
|
|
24
|
+
function die(message: string): never {
|
|
25
|
+
process.stderr.write(`[local-tools-mcp-server] ${message}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ctxEnv = process.env.POSTHOG_LOCAL_TOOLS_CTX;
|
|
30
|
+
if (!ctxEnv) {
|
|
31
|
+
die("POSTHOG_LOCAL_TOOLS_CTX env var is required");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let parsed: { cwd: string; taskId?: string; token?: string };
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(Buffer.from(ctxEnv, "base64").toString("utf-8"));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
die(`Failed to parse POSTHOG_LOCAL_TOOLS_CTX as base64-encoded JSON: ${err}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!parsed.cwd) {
|
|
42
|
+
die("POSTHOG_LOCAL_TOOLS_CTX must include cwd");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ctx: LocalToolCtx = {
|
|
46
|
+
cwd: parsed.cwd,
|
|
47
|
+
token: parsed.token ?? readGithubTokenFromEnv(),
|
|
48
|
+
taskId: parsed.taskId,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const enabledNames = (process.env.POSTHOG_LOCAL_TOOLS_ENABLED ?? "")
|
|
52
|
+
.split(",")
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
const tools = LOCAL_TOOLS.filter((t) => enabledNames.includes(t.name));
|
|
55
|
+
if (tools.length === 0) {
|
|
56
|
+
die("POSTHOG_LOCAL_TOOLS_ENABLED listed no known tools");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const server = new McpServer({
|
|
60
|
+
name: LOCAL_TOOLS_MCP_NAME,
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
for (const t of tools) {
|
|
65
|
+
server.tool(t.name, t.description, t.schema, async (args) =>
|
|
66
|
+
t.handler(ctx, args),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const transport = new StdioServerTransport();
|
|
71
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LocalTool, LocalToolCtx, LocalToolGateMeta } from "./registry";
|
|
2
|
+
import { signedCommitTool } from "./tools/signed-commit";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
LOCAL_TOOLS_MCP_NAME,
|
|
6
|
+
type LocalTool,
|
|
7
|
+
type LocalToolCtx,
|
|
8
|
+
type LocalToolGateMeta,
|
|
9
|
+
type LocalToolResult,
|
|
10
|
+
qualifiedLocalToolName,
|
|
11
|
+
} from "./registry";
|
|
12
|
+
|
|
13
|
+
/** Every tool the general local MCP server can expose. Add new tools here. */
|
|
14
|
+
export const LOCAL_TOOLS: LocalTool[] = [signedCommitTool];
|
|
15
|
+
|
|
16
|
+
/** Tools whose gate passes for the given context — the set to actually expose. */
|
|
17
|
+
export function enabledLocalTools(
|
|
18
|
+
ctx: LocalToolCtx,
|
|
19
|
+
meta: LocalToolGateMeta | undefined,
|
|
20
|
+
): LocalTool[] {
|
|
21
|
+
return LOCAL_TOOLS.filter((t) => t.isEnabled(ctx, meta));
|
|
22
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
enabledLocalTools,
|
|
4
|
+
LOCAL_TOOLS,
|
|
5
|
+
LOCAL_TOOLS_MCP_NAME,
|
|
6
|
+
qualifiedLocalToolName,
|
|
7
|
+
} from "./index";
|
|
8
|
+
|
|
9
|
+
describe("local-tools registry", () => {
|
|
10
|
+
const savedSandbox = process.env.IS_SANDBOX;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// isCloudRun also keys off IS_SANDBOX; clear it so meta.taskRunId is the
|
|
14
|
+
// only cloud signal under test.
|
|
15
|
+
delete process.env.IS_SANDBOX;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (savedSandbox === undefined) {
|
|
20
|
+
delete process.env.IS_SANDBOX;
|
|
21
|
+
} else {
|
|
22
|
+
process.env.IS_SANDBOX = savedSandbox;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("registers tools with unique names", () => {
|
|
27
|
+
const names = LOCAL_TOOLS.map((t) => t.name);
|
|
28
|
+
expect(new Set(names).size).toBe(names.length);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("qualifies tool names under the general server", () => {
|
|
32
|
+
expect(qualifiedLocalToolName("git_signed_commit")).toBe(
|
|
33
|
+
`mcp__${LOCAL_TOOLS_MCP_NAME}__git_signed_commit`,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
{ name: "cloud run with a token", taskRunId: "run-1", token: "ghs_x" },
|
|
39
|
+
{ name: "cloud run without a token", taskRunId: "run-1", token: undefined },
|
|
40
|
+
{ name: "desktop run with a token", taskRunId: undefined, token: "ghs_x" },
|
|
41
|
+
{
|
|
42
|
+
name: "desktop run without a token",
|
|
43
|
+
taskRunId: undefined,
|
|
44
|
+
token: undefined,
|
|
45
|
+
},
|
|
46
|
+
])(
|
|
47
|
+
"exposes git_signed_commit only in $name when cloud+token",
|
|
48
|
+
({ taskRunId, token }) => {
|
|
49
|
+
const tools = enabledLocalTools(
|
|
50
|
+
{ cwd: "/repo", token },
|
|
51
|
+
taskRunId ? { taskRunId } : undefined,
|
|
52
|
+
);
|
|
53
|
+
const hasSignedCommit = tools.some((t) => t.name === "git_signed_commit");
|
|
54
|
+
expect(hasSignedCommit).toBe(Boolean(taskRunId) && Boolean(token));
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single general-purpose local MCP server hosts every tool registered here,
|
|
5
|
+
* for both adapters: the Claude in-process SDK server and the Codex stdio
|
|
6
|
+
* server. Adding a tool means adding one entry to `LOCAL_TOOLS` (see
|
|
7
|
+
* `./index.ts`) — no per-tool server file or adapter wiring. The name appears
|
|
8
|
+
* in tool ids as `mcp__posthog-local__<tool>`.
|
|
9
|
+
*/
|
|
10
|
+
export const LOCAL_TOOLS_MCP_NAME = "posthog-local";
|
|
11
|
+
|
|
12
|
+
/** Runtime context handed to every local tool's handler and gate. */
|
|
13
|
+
export interface LocalToolCtx {
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** GitHub token available to the sandbox, if any. */
|
|
16
|
+
token?: string;
|
|
17
|
+
taskId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Minimal session-meta shape needed to gate tools (e.g. cloud-only). */
|
|
21
|
+
export interface LocalToolGateMeta {
|
|
22
|
+
taskRunId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* MCP tool result shape. Carries an open index signature so the value is
|
|
27
|
+
* assignable to either SDK's `CallToolResult` (the Claude SDK and the MCP SDK
|
|
28
|
+
* both attach an open `_meta`).
|
|
29
|
+
*/
|
|
30
|
+
export interface LocalToolResult {
|
|
31
|
+
content: { type: "text"; text: string }[];
|
|
32
|
+
isError?: true;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tool definition with its input schema's type preserved for the handler. */
|
|
37
|
+
export interface LocalToolDef<S extends z.ZodRawShape> {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
schema: S;
|
|
41
|
+
/**
|
|
42
|
+
* Keep the tool visible even though MCP tools are offloaded behind ToolSearch
|
|
43
|
+
* by default in the Claude adapter (ENABLE_TOOL_SEARCH). Ignored by Codex.
|
|
44
|
+
*/
|
|
45
|
+
alwaysLoad?: boolean;
|
|
46
|
+
isEnabled(ctx: LocalToolCtx, meta: LocalToolGateMeta | undefined): boolean;
|
|
47
|
+
handler(
|
|
48
|
+
ctx: LocalToolCtx,
|
|
49
|
+
args: z.infer<z.ZodObject<S>>,
|
|
50
|
+
): Promise<LocalToolResult>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Schema-erased tool, the shape stored in the registry array. */
|
|
54
|
+
export interface LocalTool {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
schema: z.ZodRawShape;
|
|
58
|
+
alwaysLoad?: boolean;
|
|
59
|
+
isEnabled(ctx: LocalToolCtx, meta: LocalToolGateMeta | undefined): boolean;
|
|
60
|
+
handler(
|
|
61
|
+
ctx: LocalToolCtx,
|
|
62
|
+
args: Record<string, unknown>,
|
|
63
|
+
): Promise<LocalToolResult>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Registers a tool, preserving its schema's inferred type at the definition
|
|
68
|
+
* site. The returned value erases the schema generic so tools of different
|
|
69
|
+
* shapes can live in one array; the cast is sound because both MCP SDKs
|
|
70
|
+
* validate `args` against `schema` before dispatching to the handler.
|
|
71
|
+
*/
|
|
72
|
+
export function defineLocalTool<S extends z.ZodRawShape>(
|
|
73
|
+
def: LocalToolDef<S>,
|
|
74
|
+
): LocalTool {
|
|
75
|
+
return def as unknown as LocalTool;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The qualified tool id as the model and tool guards see it. */
|
|
79
|
+
export function qualifiedLocalToolName(toolName: string): string {
|
|
80
|
+
return `mcp__${LOCAL_TOOLS_MCP_NAME}__${toolName}`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isCloudRun } from "../../../utils/common";
|
|
2
|
+
import {
|
|
3
|
+
runSignedCommitTool,
|
|
4
|
+
SIGNED_COMMIT_TOOL_DESCRIPTION,
|
|
5
|
+
SIGNED_COMMIT_TOOL_NAME,
|
|
6
|
+
signedCommitToolSchema,
|
|
7
|
+
} from "../../signed-commit-shared";
|
|
8
|
+
import { defineLocalTool } from "../registry";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `git_signed_commit` as a local tool. Cloud runs only, and only when a GitHub
|
|
12
|
+
* token is available (the commit is created via GitHub's API, which also signs
|
|
13
|
+
* it). Committing is core to cloud tasks, so keep it visible past ToolSearch.
|
|
14
|
+
*/
|
|
15
|
+
export const signedCommitTool = defineLocalTool({
|
|
16
|
+
name: SIGNED_COMMIT_TOOL_NAME,
|
|
17
|
+
description: SIGNED_COMMIT_TOOL_DESCRIPTION,
|
|
18
|
+
schema: signedCommitToolSchema,
|
|
19
|
+
alwaysLoad: true,
|
|
20
|
+
isEnabled: (ctx, meta) => isCloudRun(meta) && !!ctx.token,
|
|
21
|
+
handler: (ctx, args) =>
|
|
22
|
+
runSignedCommitTool(
|
|
23
|
+
{ cwd: ctx.cwd, token: ctx.token ?? "", taskId: ctx.taskId },
|
|
24
|
+
args,
|
|
25
|
+
),
|
|
26
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Minimal shape needed to resolve the effective task id from session meta. */
|
|
2
|
+
interface TaskIdSource {
|
|
3
|
+
taskId?: string;
|
|
4
|
+
persistence?: { taskId?: string };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The task id can arrive directly on the session meta or nested under
|
|
9
|
+
* `persistence`; prefer the top-level value. Shared by the Claude and Codex
|
|
10
|
+
* adapters so the fallback chain stays in sync.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveTaskId(
|
|
13
|
+
meta: TaskIdSource | undefined,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
return meta?.taskId ?? meta?.persistence?.taskId;
|
|
16
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignedCommit,
|
|
3
|
+
type SignedCommitCtx,
|
|
4
|
+
type SignedCommitInput,
|
|
5
|
+
type SignedCommitResult,
|
|
6
|
+
} from "@posthog/git/signed-commit";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { qualifiedLocalToolName } from "./local-tools/registry";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared definitions for the `git_signed_commit` tool, used by the local-tools
|
|
12
|
+
* registry entry (which both adapters expose) so the tool name, schema,
|
|
13
|
+
* description, and result formatting can't drift. The qualified name also
|
|
14
|
+
* appears in the cloud system prompt and the PreToolUse guard message.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const SIGNED_COMMIT_TOOL_NAME = "git_signed_commit";
|
|
18
|
+
export const SIGNED_COMMIT_QUALIFIED_TOOL_NAME = qualifiedLocalToolName(
|
|
19
|
+
SIGNED_COMMIT_TOOL_NAME,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const SIGNED_COMMIT_TOOL_DESCRIPTION =
|
|
23
|
+
"Create a GitHub-signed (Verified) commit on the branch. Stage files with `git add` " +
|
|
24
|
+
"first (or pass `paths`), then call this instead of `git commit`/`git push` — those are " +
|
|
25
|
+
"blocked because all commits must be signed. The commit is created via GitHub's API and " +
|
|
26
|
+
"your local checkout is kept in sync. For a new branch, pass `branch` (prefixed with " +
|
|
27
|
+
"`posthog-code/`) and the tool creates it on the remote.";
|
|
28
|
+
|
|
29
|
+
export const signedCommitToolSchema = {
|
|
30
|
+
message: z.string().describe("Commit headline (first line)."),
|
|
31
|
+
body: z.string().optional().describe("Optional extended commit body."),
|
|
32
|
+
branch: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe(
|
|
36
|
+
"Target branch; defaults to the current branch. Use a posthog-code/ prefix for new branches.",
|
|
37
|
+
),
|
|
38
|
+
paths: z
|
|
39
|
+
.array(z.string())
|
|
40
|
+
.optional()
|
|
41
|
+
.describe(
|
|
42
|
+
"Files to stage before committing; defaults to already-staged files.",
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function formatSignedCommitResult(result: SignedCommitResult): string {
|
|
47
|
+
const list = result.commits.map((c) => `- ${c.sha} ${c.url}`).join("\n");
|
|
48
|
+
return `Created ${result.commits.length} signed commit(s) on ${result.branch}:\n${list}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SignedCommitToolResult {
|
|
52
|
+
content: { type: "text"; text: string }[];
|
|
53
|
+
isError?: true;
|
|
54
|
+
// Both SDKs' CallToolResult carries an open `_meta`/index signature; mirror it
|
|
55
|
+
// so this shape is assignable to either adapter's tool-handler return type.
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Runs `git_signed_commit` and formats the MCP result. Shared by the Claude
|
|
61
|
+
* in-process tool and the Codex stdio server so success/error formatting (and
|
|
62
|
+
* the error-message prefix) can't drift between adapters.
|
|
63
|
+
*/
|
|
64
|
+
export async function runSignedCommitTool(
|
|
65
|
+
ctx: SignedCommitCtx,
|
|
66
|
+
args: SignedCommitInput,
|
|
67
|
+
): Promise<SignedCommitToolResult> {
|
|
68
|
+
try {
|
|
69
|
+
const result = await createSignedCommit(ctx, args);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: formatSignedCommitResult(result) }],
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{ type: "text", text: `${SIGNED_COMMIT_TOOL_NAME} failed: ${message}` },
|
|
78
|
+
],
|
|
79
|
+
isError: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -900,10 +900,8 @@ describe("AgentServer HTTP Mode", () => {
|
|
|
900
900
|
expect(prompt).toContain(
|
|
901
901
|
"gh pr checkout https://github.com/org/repo/pull/1",
|
|
902
902
|
);
|
|
903
|
-
expect(prompt).toContain(
|
|
904
|
-
|
|
905
|
-
);
|
|
906
|
-
expect(prompt).toContain("Push to the existing PR branch");
|
|
903
|
+
expect(prompt).toContain("git_signed_commit");
|
|
904
|
+
expect(prompt).toContain("Committing (signed commits required)");
|
|
907
905
|
expect(prompt).not.toContain("Create a draft pull request");
|
|
908
906
|
// Review-comment thread handling: reply + resolve
|
|
909
907
|
expect(prompt).toContain("review thread");
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type AgentErrorClassification,
|
|
26
26
|
classifyAgentError,
|
|
27
27
|
} from "../adapters/error-classification";
|
|
28
|
+
import { SIGNED_COMMIT_QUALIFIED_TOOL_NAME } from "../adapters/signed-commit-shared";
|
|
28
29
|
import type { PermissionMode } from "../execution-mode";
|
|
29
30
|
import { DEFAULT_CODEX_MODEL } from "../gateway-models";
|
|
30
31
|
import { HandoffCheckpointTracker } from "../handoff-checkpoint";
|
|
@@ -949,6 +950,7 @@ export class AgentServer {
|
|
|
949
950
|
_meta: {
|
|
950
951
|
sessionId: payload.run_id,
|
|
951
952
|
taskRunId: payload.run_id,
|
|
953
|
+
taskId: payload.task_id,
|
|
952
954
|
systemPrompt: sessionSystemPrompt,
|
|
953
955
|
...(this.config.model && { model: this.config.model }),
|
|
954
956
|
allowedDomains: this.config.allowedDomains,
|
|
@@ -1599,24 +1601,21 @@ export class AgentServer {
|
|
|
1599
1601
|
private buildCloudSystemPrompt(prUrl?: string | null): string {
|
|
1600
1602
|
const taskId = this.config.taskId;
|
|
1601
1603
|
const shouldAutoCreatePr = this.shouldAutoPublishCloudChanges();
|
|
1602
|
-
const
|
|
1603
|
-
##
|
|
1604
|
-
|
|
1604
|
+
const signedCommitInstructions = `
|
|
1605
|
+
## Committing (signed commits required)
|
|
1606
|
+
Commits MUST be signed. \`git commit\` and \`git push\` are blocked in this environment.
|
|
1607
|
+
To commit: stage your changes with \`git add\`, then call the \`git_signed_commit\` tool (full
|
|
1608
|
+
name \`${SIGNED_COMMIT_QUALIFIED_TOOL_NAME}\`) with a \`message\` (and optional \`body\`/\`paths\`).
|
|
1609
|
+
It creates a GitHub-signed ("Verified") commit on the branch and keeps your local checkout in
|
|
1610
|
+
sync. To start a new branch, pass \`branch\` (prefixed with \`posthog-code/\`) — the tool creates
|
|
1611
|
+
it on the remote for you.
|
|
1605
1612
|
|
|
1606
|
-
|
|
1613
|
+
## Attribution
|
|
1614
|
+
Do NOT add "Co-Authored-By" trailers or "Generated with [Claude Code]" lines to your
|
|
1615
|
+
commit messages. The \`git_signed_commit\` tool automatically appends the only trailers
|
|
1616
|
+
we want:
|
|
1607
1617
|
Generated-By: PostHog Code
|
|
1608
|
-
Task-Id: ${taskId}
|
|
1609
|
-
|
|
1610
|
-
Example:
|
|
1611
|
-
\`\`\`
|
|
1612
|
-
git commit -m "$(cat <<'EOF'
|
|
1613
|
-
fix: resolve login redirect loop
|
|
1614
|
-
|
|
1615
|
-
Generated-By: PostHog Code
|
|
1616
|
-
Task-Id: ${taskId}
|
|
1617
|
-
EOF
|
|
1618
|
-
)"
|
|
1619
|
-
\`\`\``;
|
|
1618
|
+
Task-Id: ${taskId}`;
|
|
1620
1619
|
|
|
1621
1620
|
if (prUrl) {
|
|
1622
1621
|
if (!shouldAutoCreatePr) {
|
|
@@ -1630,7 +1629,7 @@ Do the requested work, but stop with local changes ready for review.
|
|
|
1630
1629
|
Important:
|
|
1631
1630
|
- Do NOT create new commits, push to the branch, or update the pull request unless the user explicitly asks.
|
|
1632
1631
|
- Do NOT create a new branch or a new pull request.
|
|
1633
|
-
${
|
|
1632
|
+
${signedCommitInstructions}
|
|
1634
1633
|
`;
|
|
1635
1634
|
}
|
|
1636
1635
|
|
|
@@ -1641,9 +1640,8 @@ This task already has an open pull request: ${prUrl}
|
|
|
1641
1640
|
|
|
1642
1641
|
After completing the requested changes:
|
|
1643
1642
|
1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\`
|
|
1644
|
-
2. Stage
|
|
1645
|
-
3.
|
|
1646
|
-
4. For every PR review comment or review thread you addressed, treat the thread as done only after BOTH of these:
|
|
1643
|
+
2. Stage your changes with \`git add\`, then call the \`git_signed_commit\` tool with a clear \`message\` (do NOT use \`git commit\`/\`git push\` — they are blocked). This commits to the existing PR branch.
|
|
1644
|
+
3. For every PR review comment or review thread you addressed, treat the thread as done only after BOTH of these:
|
|
1647
1645
|
- Reply on the thread with a short note describing what changed (reference the commit SHA when useful) using \`gh api -X POST /repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies -f body='...'\`.
|
|
1648
1646
|
- Resolve the thread via the \`resolveReviewThread\` GraphQL mutation: \`gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id="<thread-node-id>"\`.
|
|
1649
1647
|
List unresolved threads first with \`gh api graphql -f query='{repository(owner:"<owner>",name:"<repo>"){pullRequest(number:<n>){reviewThreads(first:100){nodes{id isResolved comments(first:1){nodes{body}}}}}}}'\` so you can resolve each one you fixed.
|
|
@@ -1651,7 +1649,7 @@ After completing the requested changes:
|
|
|
1651
1649
|
Important:
|
|
1652
1650
|
- Do NOT create a new branch or a new pull request.
|
|
1653
1651
|
- Do NOT push fixes for review comments without replying to and resolving each related thread.
|
|
1654
|
-
${
|
|
1652
|
+
${signedCommitInstructions}
|
|
1655
1653
|
`;
|
|
1656
1654
|
}
|
|
1657
1655
|
|
|
@@ -1666,7 +1664,7 @@ When the user asks for code changes:
|
|
|
1666
1664
|
When the user explicitly asks to clone or work in a GitHub repository:
|
|
1667
1665
|
- Clone the repository into /tmp/workspace/repos/<owner>/<repo> using \`gh repo clone <owner>/<repo> /tmp/workspace/repos/<owner>/<repo>\`
|
|
1668
1666
|
- Work from inside that cloned repository for follow-up code changes
|
|
1669
|
-
- If the user explicitly asks you to open or update a pull request, create a branch, commit the
|
|
1667
|
+
- If the user explicitly asks you to open or update a pull request, create a branch, stage your changes with \`git add\` and commit them with the \`git_signed_commit\` tool (do NOT use \`git commit\`/\`git push\` — they are blocked), and open a draft pull request from inside the clone. Before opening the PR, check the cloned repo for a PR template at \`.github/pull_request_template.md\` (or variants; fall back to the org's \`.github\` repo via \`gh api\`) and use it as the body structure, and search for matching open issues with \`gh issue list --search\` to include \`Closes #<n>\` / \`Refs #<n>\` links.
|
|
1670
1668
|
- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`;
|
|
1671
1669
|
|
|
1672
1670
|
return `
|
|
@@ -1686,7 +1684,7 @@ ${publishInstructions}
|
|
|
1686
1684
|
|
|
1687
1685
|
Important:
|
|
1688
1686
|
- Prefer using MCP tools to answer questions with real data over giving generic advice.
|
|
1689
|
-
${
|
|
1687
|
+
${signedCommitInstructions}
|
|
1690
1688
|
`;
|
|
1691
1689
|
}
|
|
1692
1690
|
|
|
@@ -1698,7 +1696,7 @@ Do the requested work, but stop with local changes ready for review.
|
|
|
1698
1696
|
|
|
1699
1697
|
Important:
|
|
1700
1698
|
- Do NOT create a branch, commit, push, or open a pull request unless the user explicitly asks.
|
|
1701
|
-
${
|
|
1699
|
+
${signedCommitInstructions}
|
|
1702
1700
|
`;
|
|
1703
1701
|
}
|
|
1704
1702
|
|
|
@@ -1706,14 +1704,13 @@ ${attributionInstructions}
|
|
|
1706
1704
|
# Cloud Task Execution
|
|
1707
1705
|
|
|
1708
1706
|
After completing the requested changes:
|
|
1709
|
-
1.
|
|
1710
|
-
2. Stage
|
|
1711
|
-
3.
|
|
1712
|
-
4. Before opening the PR, prepare the body:
|
|
1707
|
+
1. Pick a new branch name prefixed with \`posthog-code/\` (e.g. \`posthog-code/fix-login-redirect\`)
|
|
1708
|
+
2. Stage your changes with \`git add\`, then call the \`git_signed_commit\` tool with \`branch\` set to that name and a clear \`message\` (do NOT use \`git commit\`/\`git push\` — they are blocked). The tool creates the branch on the remote and a signed commit on it.
|
|
1709
|
+
3. Before opening the PR, prepare the body:
|
|
1713
1710
|
- Check the repo for a PR template at \`.github/pull_request_template.md\` (also try \`.github/PULL_REQUEST_TEMPLATE.md\`, \`docs/pull_request_template.md\`, and root variants). If one exists, use its exact section headings as the PR body — do NOT fall back to a generic Summary/Test plan format.
|
|
1714
1711
|
- If no repo-level template exists, check the org's \`.github\` repo via \`gh api /repos/<owner>/.github/contents/.github/pull_request_template.md\` (and other common paths) and use that as a fallback.
|
|
1715
1712
|
- Search for matching open issues with \`gh issue list --state open --search '<keywords>'\` (derive keywords from the branch name, commits, and changed files; \`gh issue view <n>\` to confirm relevance). For every issue this PR would resolve, include a \`Closes #<n>\` line in the body so GitHub auto-links and auto-closes it on merge. For issues that are related but not fully resolved, use \`Refs #<n>\` instead.
|
|
1716
|
-
|
|
1713
|
+
4. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and the body prepared above. Add the following footer at the end of the PR description:
|
|
1717
1714
|
\`\`\`
|
|
1718
1715
|
---
|
|
1719
1716
|
*Created with [PostHog Code](https://posthog.com/code?ref=pr)*
|
|
@@ -1721,7 +1718,7 @@ After completing the requested changes:
|
|
|
1721
1718
|
|
|
1722
1719
|
Important:
|
|
1723
1720
|
- Always create the PR as a draft. Do not ask for confirmation.
|
|
1724
|
-
${
|
|
1721
|
+
${signedCommitInstructions}
|
|
1725
1722
|
`;
|
|
1726
1723
|
}
|
|
1727
1724
|
|
package/src/utils/common.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readGithubTokenFromEnv } from "@posthog/git/signed-commit";
|
|
1
2
|
import type { Logger } from "./logger";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -25,6 +26,19 @@ export const IS_ROOT =
|
|
|
25
26
|
|
|
26
27
|
export const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* A cloud sandbox run, as opposed to a local desktop session. Cloud sandboxes
|
|
31
|
+
* always set IS_SANDBOX and carry a taskRunId; desktop sessions have neither.
|
|
32
|
+
*/
|
|
33
|
+
export function isCloudRun(meta: { taskRunId?: string } | undefined): boolean {
|
|
34
|
+
return !!process.env.IS_SANDBOX || !!meta?.taskRunId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The GitHub token available to the sandbox, if any. */
|
|
38
|
+
export function resolveGithubToken(): string | undefined {
|
|
39
|
+
return readGithubTokenFromEnv();
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
export function unreachable(value: never, logger: Logger): void {
|
|
29
43
|
let valueAsString: string;
|
|
30
44
|
try {
|