Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/agent/src/adapters/claude/claude-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1682,7 +1682,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
...initialInProcess,
};

const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
const systemPrompt = buildSystemPrompt(
meta?.systemPrompt,
meta?.branchPrefix,
);

if (meta?.mcpToolApprovals) {
setMcpToolApprovalStates(meta.mcpToolApprovals);
Expand Down
14 changes: 11 additions & 3 deletions packages/agent/src/adapters/claude/session/instructions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const BRANCH_NAMING = `
import { BRANCH_PREFIX, normalizeBranchPrefix } from "@posthog/shared";

const buildBranchNaming = (prefix: string) => `
# Branch Naming

When working in a detached HEAD state, create a descriptive branch name based on the work being done before committing. Do this automatically without asking the user.

When creating a new branch, prefix it with \`posthog-code/\` (e.g. \`posthog-code/fix-login-redirect\`).
When creating a new branch, prefix it with \`${prefix}\` (e.g. \`${prefix}fix-login-redirect\`).
`;

const PLAN_MODE = `
Expand All @@ -26,4 +28,10 @@ If an MCP tool call is explicitly denied with a message, relay that denial messa
If an MCP tool call returns an error, treat it as a normal tool error — troubleshoot, retry, or inform the user about the specific error. Do NOT assume it is a permissions issue and do NOT direct the user to any settings page.
`;

export const APPENDED_INSTRUCTIONS = BRANCH_NAMING + PLAN_MODE + MCP_TOOLS;
export const buildAppendedInstructions = (branchPrefix?: string | null) =>
buildBranchNaming(normalizeBranchPrefix(branchPrefix)) +
PLAN_MODE +
MCP_TOOLS;

/** Default appended instructions using the standard {@link BRANCH_PREFIX}. */
export const APPENDED_INSTRUCTIONS = buildAppendedInstructions(BRANCH_PREFIX);
10 changes: 6 additions & 4 deletions packages/agent/src/adapters/claude/session/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from "../hooks";
import type { CodeExecutionMode } from "../tools";
import type { EffortLevel } from "../types";
import { APPENDED_INSTRUCTIONS } from "./instructions";
import { buildAppendedInstructions } from "./instructions";
import { loadUserClaudeJsonMcpServers } from "./mcp-config";
import { DEFAULT_MODEL } from "./models";
import type { SettingsManager } from "./settings";
Expand Down Expand Up @@ -74,19 +74,21 @@ export interface BuildOptionsParams {

export function buildSystemPrompt(
customPrompt?: unknown,
branchPrefix?: string | null,
): Options["systemPrompt"] {
const appended = buildAppendedInstructions(branchPrefix);
const defaultPrompt: Options["systemPrompt"] = {
type: "preset",
preset: "claude_code",
append: APPENDED_INSTRUCTIONS,
append: appended,
};

if (!customPrompt) {
return defaultPrompt;
}

if (typeof customPrompt === "string") {
return customPrompt + APPENDED_INSTRUCTIONS;
return customPrompt + appended;
}

if (
Expand All @@ -97,7 +99,7 @@ export function buildSystemPrompt(
) {
return {
...defaultPrompt,
append: customPrompt.append + APPENDED_INSTRUCTIONS,
append: customPrompt.append + appended,
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/adapters/claude/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export type NewSessionMeta = {
model?: string;
/** Base branch of the task's repo (e.g. "master"), for the signed-git tools. */
baseBranch?: string;
/** Prefix the agent applies to branches it creates (e.g. "posthog-code/"). */
branchPrefix?: string;
/**
* Repo-less channel "generic chat box" session: enables the lazy-repo tools
* (list_repos / clone_repo) and channel guidance. The agent decides at
Expand Down
9 changes: 9 additions & 0 deletions packages/agent/src/server/agent-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,15 @@ describe("AgentServer HTTP Mode", () => {
delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
});

it("uses a configured branch prefix in the cloud prompt", () => {
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "signal_report";
const s = createServer({ branchPrefix: "team/" });
const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt();
expect(prompt).toContain("team/fix-login-redirect");
expect(prompt).not.toContain("posthog-code/fix-login-redirect");
delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN;
});

it("returns PR-update prompt for existing PRs on Slack-origin runs", () => {
process.env.POSTHOG_CODE_INTERACTION_ORIGIN = "slack";
const s = createServer();
Expand Down
9 changes: 7 additions & 2 deletions packages/agent/src/server/agent-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { type ServerType, serve } from "@hono/node-server";
import { execGh } from "@posthog/git/gh";
import { getCurrentBranch } from "@posthog/git/queries";
import { normalizeBranchPrefix } from "@posthog/shared";
import { Hono } from "hono";
import { z } from "zod";
import packageJson from "../../package.json" with { type: "json" };
Expand Down Expand Up @@ -1071,6 +1072,9 @@ export class AgentServer {
jsonSchema: preTask?.json_schema ?? null,
permissionMode: initialPermissionMode,
...(this.config.baseBranch && { baseBranch: this.config.baseBranch }),
...(this.config.branchPrefix && {
branchPrefix: this.config.branchPrefix,
}),
...this.buildClaudeCodeSessionMeta(runtimeAdapter),
},
});
Expand Down Expand Up @@ -1736,6 +1740,7 @@ export class AgentServer {
slackThreadUrl?: string | null,
): string {
const taskId = this.config.taskId;
const branchPrefix = normalizeBranchPrefix(this.config.branchPrefix);
const shouldAutoCreatePr = this.shouldAutoPublishCloudChanges();
const isSlack = this.getCloudInteractionOrigin() === "slack";
const identityInstructions = isSlack
Expand Down Expand Up @@ -1764,7 +1769,7 @@ Commits MUST be signed. \`git commit\` and \`git push\` are blocked in this envi
To commit: stage your changes with \`git add\`, then call the \`git_signed_commit\` tool (full
name \`${SIGNED_COMMIT_QUALIFIED_TOOL_NAME}\`) with a \`message\` (and optional \`body\`/\`paths\`).
It creates a GitHub-signed ("Verified") commit on the branch and keeps your local checkout in
sync. To start a new branch, pass \`branch\` (prefixed with \`posthog-code/\`) — the tool creates
sync. To start a new branch, pass \`branch\` (prefixed with \`${branchPrefix}\`) — the tool creates
it on the remote for you.

## Updating from the base branch
Expand Down Expand Up @@ -1901,7 +1906,7 @@ ${signedCommitInstructions}
# Cloud Task Execution

After completing the requested changes:
1. Pick a new branch name prefixed with \`posthog-code/\` (e.g. \`posthog-code/fix-login-redirect\`)
1. Pick a new branch name prefixed with \`${branchPrefix}\` (e.g. \`${branchPrefix}fix-login-redirect\`)
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.
3. Before opening the PR, prepare the body:
- Keep the PR description brief overall. Summarize only the most important changes — do NOT enumerate every change you made. A few sentences or bullets is plenty.
Expand Down
5 changes: 5 additions & 0 deletions packages/agent/src/server/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ program
)
.option("--createPr <boolean>", "Whether this run may publish changes")
.option("--baseBranch <branch>", "Base branch for PR creation")
.option(
"--branchPrefix <prefix>",
'Prefix for branches the agent creates (default "posthog-code/")',
)
.option(
"--claudeCodeConfig <json>",
"Claude Code config as JSON (systemPrompt, systemPromptAppend, plugins)",
Expand Down Expand Up @@ -170,6 +174,7 @@ program
createPr,
mcpServers,
baseBranch: options.baseBranch,
branchPrefix: options.branchPrefix,
claudeCode,
allowedDomains,
runtimeAdapter: env.POSTHOG_CODE_RUNTIME_ADAPTER,
Expand Down
2 changes: 2 additions & 0 deletions packages/agent/src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface AgentServerConfig {
version?: string;
mcpServers?: RemoteMcpServer[];
baseBranch?: string;
/** Prefix the agent applies to branches it creates (e.g. "posthog-code/"). */
branchPrefix?: string;
claudeCode?: ClaudeCodeConfig;
allowedDomains?: string[];
runtimeAdapter?: "claude" | "codex";
Expand Down
7 changes: 7 additions & 0 deletions packages/api-client/src/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
StoredLogEntry,
} from "@posthog/shared";
import {
BRANCH_PREFIX,
DISMISSAL_REASON_OPTIONS,
type DismissalReasonOptionValue,
SEAT_PRODUCT_KEY,
Expand Down Expand Up @@ -465,6 +466,8 @@ interface CloudRunOptions {
adapter?: CloudRuntimeAdapter;
model?: string;
reasoningLevel?: string;
/** Prefix the agent applies to branches it creates (e.g. "posthog-code/"). */
branchPrefix?: string;
sandboxEnvironmentId?: string;
prAuthorshipMode?: PrAuthorshipMode;
runSource?: CloudRunSource;
Expand Down Expand Up @@ -552,6 +555,10 @@ function buildCloudRunRequestBody(
if (options?.homeQuickAction) {
body.home_quick_action = options.homeQuickAction;
}
// Only send a customized prefix; the backend defaults to "posthog-code/".
if (options?.branchPrefix && options.branchPrefix !== BRANCH_PREFIX) {
body.branch_prefix = options.branchPrefix;
}

return body;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/git-interaction/branchName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,13 @@ describe("suggestBranchName", () => {
]),
).toBe("posthog-code/fix-bug-4");
});

it("uses a custom prefix and dedupes against it", () => {
expect(suggestBranchName("Fix bug", "abc", [], "team/")).toBe(
"team/fix-bug",
);
expect(suggestBranchName("Fix bug", "abc", ["team/fix-bug"], "team/")).toBe(
"team/fix-bug-2",
);
});
});
13 changes: 9 additions & 4 deletions packages/core/src/git-interaction/branchName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export function validateBranchName(name: string): string | null {
return null;
}

export function deriveBranchName(title: string, fallbackId: string): string {
export function deriveBranchName(
title: string,
fallbackId: string,
prefix: string = BRANCH_PREFIX,
): string {
const slug = title
.toLowerCase()
.trim()
Expand All @@ -64,16 +68,17 @@ export function deriveBranchName(title: string, fallbackId: string): string {
.slice(0, 60)
.replace(/-$/, "");

if (!slug) return `${BRANCH_PREFIX}task-${fallbackId}`;
return `${BRANCH_PREFIX}${slug}`;
if (!slug) return `${prefix}task-${fallbackId}`;
return `${prefix}${slug}`;
}

export function suggestBranchName(
title: string,
fallbackId: string,
existingBranches: string[],
prefix: string = BRANCH_PREFIX,
): string {
const base = deriveBranchName(title, fallbackId);
const base = deriveBranchName(title, fallbackId, prefix);

if (!existingBranches.includes(base)) return base;

Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/git-interaction/deriveBranchName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,14 @@ describe("deriveBranchName", () => {
"posthog-code/task-abc123",
);
});

it("applies a custom prefix when provided", () => {
expect(deriveBranchName("Fix login bug", "abc123", "team/")).toBe(
"team/fix-login-bug",
);
});

it("applies a custom prefix to the task-ID fallback", () => {
expect(deriveBranchName("", "abc123", "team/")).toBe("team/task-abc123");
});
});
14 changes: 11 additions & 3 deletions packages/core/src/sessions/sessionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ export interface SessionServiceDeps {
setAdapter(taskRunId: string, adapter: Adapter): void;
removeAdapter(taskRunId: string): void;
};
readonly settings: { customInstructions?: string | null };
readonly settings: {
customInstructions?: string | null;
branchPrefix?: string | null;
};
usageLimit: { show: (...args: any[]) => any };
readonly addDirectoryDialog: { open: boolean };
taskViewedApi: { markActivity(taskId: string): void };
Expand Down Expand Up @@ -817,7 +820,7 @@ export class SessionService {
this.d.log.warn("Failed to verify workspace", { taskId, err });
});

const { customInstructions } = this.d.settings;
const { customInstructions, branchPrefix } = this.d.settings;
const result = await this.d.trpc.agent.reconnect.mutate({
taskId,
taskRunId,
Expand All @@ -830,6 +833,7 @@ export class SessionService {
permissionMode: persistedMode,
model: persistedModel,
customInstructions: customInstructions || undefined,
branchPrefix: branchPrefix || undefined,
});

if (result) {
Expand Down Expand Up @@ -1129,7 +1133,10 @@ export class SessionService {
throw new Error("Failed to create task run. Please try again.");
}

const { customInstructions: startCustomInstructions } = this.d.settings;
const {
customInstructions: startCustomInstructions,
branchPrefix: startBranchPrefix,
} = this.d.settings;
const preferredModel = model ?? this.d.DEFAULT_GATEWAY_MODEL;
const result = await this.d.trpc.agent.start.mutate({
taskId,
Expand All @@ -1140,6 +1147,7 @@ export class SessionService {
permissionMode: executionMode,
adapter,
customInstructions: startCustomInstructions || undefined,
branchPrefix: startBranchPrefix || undefined,
effort: effortLevelSchema.safeParse(reasoningLevel).success
? (reasoningLevel as EffortLevel)
: undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/task-detail/taskCreationApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface CreateTaskRunClientOptions {
adapter?: "claude" | "codex";
model?: string;
reasoningLevel?: string;
/** Prefix the agent applies to branches it creates (e.g. "posthog-code/"). */
branchPrefix?: string;
sandboxEnvironmentId?: string;
prAuthorshipMode?: PrAuthorshipMode;
runSource?: CloudRunSource;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/task-detail/taskCreationSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export class TaskCreationSaga extends Saga<
adapter: input.adapter,
model: input.model,
reasoningLevel: input.reasoningLevel,
branchPrefix: input.branchPrefix,
sandboxEnvironmentId: input.sandboxEnvironmentId,
prAuthorshipMode,
runSource: input.cloudRunSource ?? "manual",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/task-detail/taskInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface PrepareTaskInputOptions {
adapter?: "claude" | "codex";
model?: string;
reasoningLevel?: string;
branchPrefix?: string;
environmentId?: string | null;
sandboxEnvironmentId?: string;
signalReportId?: string;
Expand Down Expand Up @@ -46,6 +47,7 @@ export function prepareTaskInput(
adapter: options.adapter,
model: options.model,
reasoningLevel: options.reasoningLevel,
branchPrefix: options.branchPrefix,
environmentId: options.environmentId ?? undefined,
sandboxEnvironmentId: options.sandboxEnvironmentId,
cloudPrAuthorshipMode:
Expand Down
31 changes: 31 additions & 0 deletions packages/shared/src/git-naming.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { BRANCH_PREFIX, normalizeBranchPrefix } from "./git-naming";

describe("normalizeBranchPrefix", () => {
it("falls back to the default for empty input", () => {
expect(normalizeBranchPrefix("")).toBe(BRANCH_PREFIX);
expect(normalizeBranchPrefix(" ")).toBe(BRANCH_PREFIX);
expect(normalizeBranchPrefix(undefined)).toBe(BRANCH_PREFIX);
expect(normalizeBranchPrefix(null)).toBe(BRANCH_PREFIX);
});

it("trims surrounding whitespace", () => {
expect(normalizeBranchPrefix(" team/ ")).toBe("team/");
});

it("guarantees exactly one trailing slash", () => {
expect(normalizeBranchPrefix("team")).toBe("team/");
expect(normalizeBranchPrefix("team/")).toBe("team/");
expect(normalizeBranchPrefix("team//")).toBe("team/");
expect(normalizeBranchPrefix("team-")).toBe("team-/");
});

it("strips leading slashes and collapses repeated slashes", () => {
expect(normalizeBranchPrefix("/team/")).toBe("team/");
expect(normalizeBranchPrefix("team//sub/")).toBe("team/sub/");
});

it("falls back to the default when only slashes are provided", () => {
expect(normalizeBranchPrefix("/")).toBe(BRANCH_PREFIX);
});
});
Loading
Loading