Skip to content

Commit cc0b4ab

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

17 files changed

Lines changed: 302 additions & 12 deletions

File tree

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

Lines changed: 19 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,14 @@ 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+
// Extra cloned repos are surfaced to the model exactly like the local
2486+
// agent's additional directories, so it knows it may work across them.
2487+
const cloudAppend = [
2488+
this.buildCloudSystemPrompt(prUrl, slackThreadUrl, inboxReportUrl),
2489+
buildAdditionalDirectoriesPrompt(this.config.additionalDirectories),
2490+
]
2491+
.filter(Boolean)
2492+
.join("\n\n");
24892493
const userPrompt = this.config.claudeCode?.systemPrompt;
24902494

24912495
// String override: combine user prompt with cloud instructions
@@ -2523,8 +2527,14 @@ export class AgentServer {
25232527
const plugins = this.config.claudeCode?.plugins;
25242528
const effort =
25252529
runtimeAdapter === "claude" ? this.config.reasoningEffort : undefined;
2530+
// Claude-only: the SDK enforces directory access. Codex relies on the
2531+
// <additional_directories> prompt block plus the sandbox's open filesystem.
2532+
const additionalDirectories =
2533+
runtimeAdapter === "claude"
2534+
? this.config.additionalDirectories
2535+
: undefined;
25262536

2527-
if (!plugins?.length && !effort) {
2537+
if (!plugins?.length && !effort && !additionalDirectories?.length) {
25282538
return undefined;
25292539
}
25302540

@@ -2535,6 +2545,9 @@ export class AgentServer {
25352545
if (effort) {
25362546
options.effort = effort;
25372547
}
2548+
if (additionalDirectories?.length) {
2549+
options.additionalDirectories = additionalDirectories;
2550+
}
25382551
return { claudeCode: { options } };
25392552
}
25402553

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export interface AgentServerConfig {
2929
mcpServers?: RemoteMcpServer[];
3030
baseBranch?: string;
3131
claudeCode?: ClaudeCodeConfig;
32+
/**
33+
* Extra repo checkout paths (e.g. /tmp/workspace/repos/org/repo) the agent may
34+
* read and edit beyond the primary `repositoryPath`. Surfaced to the model as
35+
* an `<additional_directories>` prompt block and passed to the Claude SDK's
36+
* `additionalDirectories`.
37+
*/
38+
additionalDirectories?: string[];
3239
allowedDomains?: string[];
3340
runtimeAdapter?: "claude" | "codex";
3441
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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Builds the `<additional_directories>` system-prompt block shared by the local
3+
* agent (workspace-server) and the cloud agent-server, so both describe extra
4+
* working directories to the model identically. The block itself is returned
5+
* without surrounding whitespace; callers join it into their prompt. Returns an
6+
* empty string when there are no additional directories.
7+
*/
8+
export function buildAdditionalDirectoriesPrompt(
9+
directories: readonly string[] | undefined,
10+
): string {
11+
if (!directories?.length) return "";
12+
const escapeXml = (value: string) =>
13+
value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
14+
const dirs = directories
15+
.map((directory) => ` <directory>${escapeXml(directory)}</directory>`)
16+
.join("\n");
17+
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>`;
18+
}

packages/shared/src/domain-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ 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+
/** Extra "org/repo" repos cloned into the cloud sandbox alongside `repository`. */
52+
additional_repositories?: string[];
5153
github_integration?: number | null;
5254
github_user_integration?: string | null;
5355
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";

packages/shared/src/task-creation-domain.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export interface TaskCreationInput {
1818
filePaths?: string[];
1919
repoPath?: string;
2020
repository?: string | null;
21+
/**
22+
* Cloud-only. Extra "org/repo" repositories cloned into the sandbox alongside
23+
* the primary `repository`, so the agent can read/work across them. The PR
24+
* still targets the primary repo. Local tasks use `additionalDirectories`
25+
* (absolute paths) instead — the two are mutually exclusive by workspaceMode.
26+
*/
27+
additionalRepositories?: string[];
2128
workspaceMode?: WorkspaceMode;
2229
branch?: string | null;
2330
// When the branch exists only on the remote, opt in to fetching and checking

0 commit comments

Comments
 (0)