Skip to content

Commit e2f3713

Browse files
committed
feat(code): multi-repo support for cloud tasks
1 parent 6cfb8b3 commit e2f3713

17 files changed

Lines changed: 242 additions & 12 deletions

File tree

packages/agent/src/server/agent-server.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { type ServerType, serve } from "@hono/node-server";
1717
import { execGh } from "@posthog/git/gh";
1818
import { getCurrentBranch } from "@posthog/git/queries";
19+
import { buildAdditionalDirectoriesPrompt } from "@posthog/shared";
1920
import { unzipSync } from "fflate";
2021
import { Hono } from "hono";
2122
import { z } from "zod";
@@ -2481,11 +2482,12 @@ export class AgentServer {
24812482
slackThreadUrl?: string | null,
24822483
inboxReportUrl?: string | null,
24832484
): string | { append: string } {
2484-
const cloudAppend = this.buildCloudSystemPrompt(
2485-
prUrl,
2486-
slackThreadUrl,
2487-
inboxReportUrl,
2488-
);
2485+
const cloudAppend = [
2486+
this.buildCloudSystemPrompt(prUrl, slackThreadUrl, inboxReportUrl),
2487+
buildAdditionalDirectoriesPrompt(this.config.additionalDirectories),
2488+
]
2489+
.filter(Boolean)
2490+
.join("\n\n");
24892491
const userPrompt = this.config.claudeCode?.systemPrompt;
24902492

24912493
// String override: combine user prompt with cloud instructions
@@ -2523,8 +2525,12 @@ export class AgentServer {
25232525
const plugins = this.config.claudeCode?.plugins;
25242526
const effort =
25252527
runtimeAdapter === "claude" ? this.config.reasoningEffort : undefined;
2528+
const additionalDirectories =
2529+
runtimeAdapter === "claude"
2530+
? this.config.additionalDirectories
2531+
: undefined;
25262532

2527-
if (!plugins?.length && !effort) {
2533+
if (!plugins?.length && !effort && !additionalDirectories?.length) {
25282534
return undefined;
25292535
}
25302536

@@ -2535,6 +2541,9 @@ export class AgentServer {
25352541
if (effort) {
25362542
options.effort = effort;
25372543
}
2544+
if (additionalDirectories?.length) {
2545+
options.additionalDirectories = additionalDirectories;
2546+
}
25382547
return { claudeCode: { options } };
25392548
}
25402549

packages/agent/src/server/bin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ program
115115
"--allowedDomains <domains>",
116116
"Comma-separated list of domains allowed for web tools (WebFetch, WebSearch)",
117117
)
118+
.option(
119+
"--additionalDirectories <paths>",
120+
"Comma-separated extra repo checkout paths the agent may read/edit beyond --repositoryPath",
121+
)
118122
.action(async (options) => {
119123
const envResult = envSchema.safeParse(process.env);
120124

@@ -149,6 +153,13 @@ program
149153
.filter(Boolean)
150154
: undefined;
151155

156+
const additionalDirectories = options.additionalDirectories
157+
? options.additionalDirectories
158+
.split(",")
159+
.map((d: string) => d.trim())
160+
.filter(Boolean)
161+
: undefined;
162+
152163
if (
153164
env.POSTHOG_CODE_RUNTIME_ADAPTER &&
154165
env.POSTHOG_CODE_MODEL &&
@@ -186,6 +197,7 @@ program
186197
baseBranch: options.baseBranch,
187198
claudeCode,
188199
allowedDomains,
200+
additionalDirectories,
189201
runtimeAdapter: env.POSTHOG_CODE_RUNTIME_ADAPTER,
190202
model: env.POSTHOG_CODE_MODEL,
191203
reasoningEffort: env.POSTHOG_CODE_REASONING_EFFORT,

packages/agent/src/server/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface AgentServerConfig {
2929
mcpServers?: RemoteMcpServer[];
3030
baseBranch?: string;
3131
claudeCode?: ClaudeCodeConfig;
32+
additionalDirectories?: string[];
3233
allowedDomains?: string[];
3334
runtimeAdapter?: "claude" | "codex";
3435
model?: string;

packages/api-client/src/posthog-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,6 +2220,7 @@ export class PostHogAPIClient {
22202220
channel?: string | null;
22212221
pending_user_message?: string;
22222222
pending_user_artifact_ids?: string[];
2223+
additional_repositories?: string[];
22232224
},
22242225
) {
22252226
const teamId = await this.getTeamId();

packages/core/src/task-detail/taskCreationSaga.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,11 @@ export class TaskCreationSaga extends Saga<
762762
input.workspaceMode === "cloud"
763763
? (input.reasoningLevel ?? null)
764764
: undefined,
765+
additional_repositories:
766+
input.workspaceMode === "cloud" &&
767+
input.additionalRepositories?.length
768+
? input.additionalRepositories
769+
: undefined,
765770
signal_report: input.signalReportId ?? undefined,
766771
channel: input.channelId ?? undefined,
767772
pending_user_message: warmPayload?.pendingUserMessage,

packages/core/src/task-detail/taskInput.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface PrepareTaskInputOptions {
1919
sandboxEnvironmentId?: string;
2020
signalReportId?: string;
2121
additionalDirectories?: string[];
22+
additionalRepositories?: string[];
2223
channelContext?: string;
2324
channelName?: string;
2425
channelId?: string;
@@ -58,6 +59,9 @@ export function prepareTaskInput(
5859
options.signalReportId && isCloud ? "signal_report" : undefined,
5960
signalReportId: options.signalReportId,
6061
additionalDirectories: isCloud ? undefined : options.additionalDirectories,
62+
additionalRepositories: isCloud
63+
? options.additionalRepositories
64+
: undefined,
6165
channelContext: options.channelContext,
6266
channelName: options.channelName,
6367
channelId: options.channelId,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildAdditionalDirectoriesPrompt } from "./agent-prompts";
3+
4+
describe("buildAdditionalDirectoriesPrompt", () => {
5+
it.each([
6+
["undefined", undefined],
7+
["empty", []],
8+
])("returns an empty string for %s directories", (_label, directories) => {
9+
expect(buildAdditionalDirectoriesPrompt(directories)).toBe("");
10+
});
11+
12+
it("lists each directory in an <additional_directories> block", () => {
13+
const prompt = buildAdditionalDirectoriesPrompt([
14+
"/tmp/workspace/repos/posthog/posthog-js",
15+
"/tmp/workspace/repos/posthog/posthog.com",
16+
]);
17+
18+
expect(prompt).toContain(
19+
"<additional_directories>\n" +
20+
" <directory>/tmp/workspace/repos/posthog/posthog-js</directory>\n" +
21+
" <directory>/tmp/workspace/repos/posthog/posthog.com</directory>\n" +
22+
"</additional_directories>",
23+
);
24+
});
25+
26+
it("escapes XML-significant characters in paths", () => {
27+
const prompt = buildAdditionalDirectoriesPrompt(["/tmp/a<b>&c"]);
28+
29+
expect(prompt).toContain("<directory>/tmp/a&lt;b&gt;&amp;c</directory>");
30+
expect(prompt).not.toContain("<directory>/tmp/a<b>");
31+
});
32+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function buildAdditionalDirectoriesPrompt(
2+
directories: readonly string[] | undefined,
3+
): string {
4+
if (!directories?.length) return "";
5+
const escapeXml = (value: string) =>
6+
value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7+
const dirs = directories
8+
.map((directory) => ` <directory>${escapeXml(directory)}</directory>`)
9+
.join("\n");
10+
return `The user has granted you access to additional directories outside the working directory. You may read and edit files in these paths just like the working directory:\n<additional_directories>\n${dirs}\n</additional_directories>`;
11+
}

packages/shared/src/domain-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface Task {
4848
created_by?: UserBasic | null;
4949
origin_product: string;
5050
repository?: string | null; // Format: "organization/repository" (e.g., "posthog/posthog-js")
51+
additional_repositories?: string[];
5152
github_integration?: number | null;
5253
github_user_integration?: string | null;
5354
json_schema?: Record<string, unknown> | null;

packages/shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { buildAdditionalDirectoriesPrompt } from "./agent-prompts";
12
export * from "./analytics-events";
23
export { type ArchivedTask, archivedTaskSchema } from "./archive-domain";
34
export { withTimeout } from "./async";

0 commit comments

Comments
 (0)