From 2a93346ba3463f6f4541420b936c19a275805f5b Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jul 2026 10:51:35 +0100 Subject: [PATCH 1/4] feat(settings): auto-publish cloud runs toggle in advanced settings Adds a default-on "Always create pull requests for cloud runs" setting. When enabled, cloud task runs are created with auto_publish, and the sandbox agent-server (via the new --autoPublish flag) prompts the agent to push its work and open a draft PR on completion instead of stopping with local changes. Generated-By: PostHog Code Task-Id: 49d73463-ec66-491c-b645-f6116972bd02 --- .../agent/src/server/agent-server.test.ts | 26 +++++++++++++++++++ packages/agent/src/server/agent-server.ts | 24 ++++++++++++----- packages/agent/src/server/bin.ts | 9 +++++++ packages/agent/src/server/types.ts | 3 +++ packages/api-client/src/posthog-client.ts | 4 +++ packages/core/src/sessions/cloudRunOptions.ts | 6 +++++ packages/core/src/sessions/sessionService.ts | 2 ++ .../src/task-detail/taskCreationApiClient.ts | 1 + .../src/task-detail/taskCreationSaga.test.ts | 2 ++ .../core/src/task-detail/taskCreationSaga.ts | 1 + packages/core/src/task-detail/taskInput.ts | 2 ++ packages/shared/src/task-creation-domain.ts | 5 ++++ .../settings/sections/AdvancedSettings.tsx | 14 ++++++++++ .../ui/src/features/settings/settingsStore.ts | 8 ++++++ .../task-detail/hooks/useTaskCreation.ts | 2 ++ 15 files changed, 102 insertions(+), 7 deletions(-) diff --git a/packages/agent/src/server/agent-server.test.ts b/packages/agent/src/server/agent-server.test.ts index 615bd854c1..e3245de60a 100644 --- a/packages/agent/src/server/agent-server.test.ts +++ b/packages/agent/src/server/agent-server.test.ts @@ -1990,6 +1990,32 @@ describe("AgentServer HTTP Mode", () => { delete process.env.POSTHOG_CODE_INTERACTION_ORIGIN; }); + it("returns auto-PR prompt for manual runs when the user opted into auto-publish", () => { + const s = createServer({ autoPublish: true }); + const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt(); + expect(prompt).toContain("gh pr create --draft"); + expect(prompt).not.toContain("stop with local changes ready for review"); + // Manual runs keep the PostHog Code attribution. + expect(prompt).toContain( + "Created with [PostHog Code](https://posthog.com/code?ref=pr)", + ); + }); + + it("keeps review-first prompt when auto-publish is on but createPr is false", () => { + const s = createServer({ autoPublish: true, createPr: false }); + const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt(); + expect(prompt).toContain("stop with local changes ready for review"); + expect(prompt).not.toContain("gh pr create --draft"); + }); + + it("auto-publishes in no-repository mode when the user opted in", () => { + const s = createServer({ repositoryPath: undefined, autoPublish: true }); + const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt(); + expect(prompt).toContain("Cloud Task Execution — No Repository Mode"); + expect(prompt).toContain("without waiting to be asked"); + expect(prompt).not.toContain("unless the user explicitly asks for that"); + }); + it.each([ { label: "Slack", origin: "slack" }, { label: "signal_report", origin: "signal_report" }, diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 172e5e666f..ef6291f954 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -2557,12 +2557,16 @@ export class AgentServer { } /** - * Automated-origin cloud runs auto-publish by default. Every other origin is - * review-first unless the user explicitly asks, and createPr=false always - * disables publishing. + * Automated-origin cloud runs auto-publish by default, and manual runs + * auto-publish when the user opted in (Settings → Advanced, sent as + * autoPublish). Every other run is review-first unless the user explicitly + * asks, and createPr=false always disables publishing. */ private shouldAutoPublishCloudChanges(): boolean { - return this.isAutomatedOrigin() && this.config.createPr !== false; + return ( + (this.isAutomatedOrigin() || this.config.autoPublish === true) && + this.config.createPr !== false + ); } private buildDetectedPrContext(prUrl: string): string { @@ -2725,6 +2729,12 @@ ${signedCommitInstructions}${prLinkInstructions}${shellEfficiencyInstructions} } if (!this.config.repositoryPath) { + const publishTiming = shouldAutoCreatePr + ? `- After completing code changes in a cloned repository, 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 without waiting to be asked.` + : `- 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.`; + const publishClosing = shouldAutoCreatePr + ? `- Always create the PR as a draft. Do not ask for confirmation before publishing completed code changes` + : `- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`; const publishInstructions = this.config.createPr === false ? ` @@ -2732,15 +2742,15 @@ When the user asks for code changes: - You may clone a repository and make local edits in that clone - Do NOT create branches, commits, push changes, or open pull requests in this run` : ` -When the user explicitly asks to clone or work in a GitHub repository: +When the user ${shouldAutoCreatePr ? "asks" : "explicitly asks"} to clone or work in a GitHub repository: - Clone the repository into /tmp/workspace/repos// using \`gh repo clone / /tmp/workspace/repos//\` - Work from inside that cloned repository for follow-up code changes -- 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 #\` / \`Refs #\` links. +${publishTiming} 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 #\` / \`Refs #\` links. - 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. ${whyContextInstruction.trimStart()} ${publicRepoSafetyInstruction.trimStart()} - End the PR description with a horizontal rule followed by this footer line: ${prFooter} -- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`; +${publishClosing}`; return `${identityInstructions} # Cloud Task Execution — No Repository Mode diff --git a/packages/agent/src/server/bin.ts b/packages/agent/src/server/bin.ts index 4f515b15ec..3904517b71 100644 --- a/packages/agent/src/server/bin.ts +++ b/packages/agent/src/server/bin.ts @@ -106,6 +106,10 @@ program "MCP servers config as JSON array (ACP McpServer[] format)", ) .option("--createPr ", "Whether this run may publish changes") + .option( + "--autoPublish ", + "Whether this run should push and open a draft PR on completion without an explicit ask", + ) .option("--baseBranch ", "Base branch for PR creation") .option( "--claudeCodeConfig ", @@ -130,6 +134,10 @@ program const mode = options.mode === "background" ? "background" : "interactive"; const createPr = parseBooleanOption(options.createPr, "--createPr"); + const autoPublish = parseBooleanOption( + options.autoPublish, + "--autoPublish", + ); const mcpServers = parseJsonOption( options.mcpServers, @@ -182,6 +190,7 @@ program taskId: options.taskId, runId: options.runId, createPr, + autoPublish, mcpServers, baseBranch: options.baseBranch, claudeCode, diff --git a/packages/agent/src/server/types.ts b/packages/agent/src/server/types.ts index 50a3fa1395..264aecc0ac 100644 --- a/packages/agent/src/server/types.ts +++ b/packages/agent/src/server/types.ts @@ -25,6 +25,9 @@ export interface AgentServerConfig { taskId: string; runId: string; createPr?: boolean; + // User-opted auto-publish: push and open a draft PR on completion even for + // manual (non-automated-origin) cloud runs. createPr=false still wins. + autoPublish?: boolean; version?: string; mcpServers?: RemoteMcpServer[]; baseBranch?: string; diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index e359023d56..06542c55d3 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -478,6 +478,7 @@ interface CloudRunOptions { reasoningLevel?: string; sandboxEnvironmentId?: string; prAuthorshipMode?: PrAuthorshipMode; + autoPublish?: boolean; runSource?: CloudRunSource; signalReportId?: string; initialPermissionMode?: PermissionMode; @@ -551,6 +552,9 @@ function buildCloudRunRequestBody( if (options?.prAuthorshipMode) { body.pr_authorship_mode = options.prAuthorshipMode; } + if (options?.autoPublish) { + body.auto_publish = true; + } if (options?.runSource) { body.run_source = options.runSource; } diff --git a/packages/core/src/sessions/cloudRunOptions.ts b/packages/core/src/sessions/cloudRunOptions.ts index 402d936f00..cf8861c323 100644 --- a/packages/core/src/sessions/cloudRunOptions.ts +++ b/packages/core/src/sessions/cloudRunOptions.ts @@ -31,6 +31,12 @@ export function getCloudRunSource( return state.run_source === "signal_report" ? "signal_report" : "manual"; } +export function getCloudAutoPublish( + state: Record, +): boolean | undefined { + return state.auto_publish === true ? true : undefined; +} + export interface CloudRuntimeOptions { adapter?: Adapter; model?: string; diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index 4c960bdca2..a519333175 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -51,6 +51,7 @@ import { CloudLogGapReconciler } from "./cloudLogGapReconciler"; import { CloudRunIdleTracker } from "./cloudRunIdleTracker"; import { type CloudRuntimeOptions, + getCloudAutoPublish, getCloudPrAuthorshipMode, getCloudRunSource, getCloudRuntimeOptions, @@ -2927,6 +2928,7 @@ export class SessionService { pendingUserArtifactIds: artifactIds.length > 0 ? artifactIds : undefined, prAuthorshipMode, + autoPublish: getCloudAutoPublish(previousState), runSource: getCloudRunSource(previousState), signalReportId: typeof previousState.signal_report_id === "string" diff --git a/packages/core/src/task-detail/taskCreationApiClient.ts b/packages/core/src/task-detail/taskCreationApiClient.ts index 3ef2f5ed56..1150c4790b 100644 --- a/packages/core/src/task-detail/taskCreationApiClient.ts +++ b/packages/core/src/task-detail/taskCreationApiClient.ts @@ -10,6 +10,7 @@ export interface CreateTaskRunClientOptions { reasoningLevel?: string; sandboxEnvironmentId?: string; prAuthorshipMode?: PrAuthorshipMode; + autoPublish?: boolean; runSource?: CloudRunSource; signalReportId?: string; initialPermissionMode?: string; diff --git a/packages/core/src/task-detail/taskCreationSaga.test.ts b/packages/core/src/task-detail/taskCreationSaga.test.ts index 7c295c0abe..c728b79ae2 100644 --- a/packages/core/src/task-detail/taskCreationSaga.test.ts +++ b/packages/core/src/task-detail/taskCreationSaga.test.ts @@ -152,6 +152,7 @@ describe("TaskCreationSaga", () => { adapter: "codex", model: "gpt-5.4", reasoningLevel: "high", + cloudAutoPublish: true, }); expect(result.success).toBe(true); @@ -168,6 +169,7 @@ describe("TaskCreationSaga", () => { reasoningLevel: "high", sandboxEnvironmentId: undefined, prAuthorshipMode: "user", + autoPublish: true, runSource: "manual", signalReportId: undefined, initialPermissionMode: "auto", diff --git a/packages/core/src/task-detail/taskCreationSaga.ts b/packages/core/src/task-detail/taskCreationSaga.ts index b021119f1d..8c5d60aa5d 100644 --- a/packages/core/src/task-detail/taskCreationSaga.ts +++ b/packages/core/src/task-detail/taskCreationSaga.ts @@ -401,6 +401,7 @@ export class TaskCreationSaga extends Saga< reasoningLevel: input.reasoningLevel, sandboxEnvironmentId: input.sandboxEnvironmentId, prAuthorshipMode, + autoPublish: input.cloudAutoPublish, runSource: input.cloudRunSource ?? "manual", signalReportId: input.signalReportId, homeQuickAction: input.homeQuickActionLabel, diff --git a/packages/core/src/task-detail/taskInput.ts b/packages/core/src/task-detail/taskInput.ts index dba4749919..a0d49eae24 100644 --- a/packages/core/src/task-detail/taskInput.ts +++ b/packages/core/src/task-detail/taskInput.ts @@ -23,6 +23,7 @@ export interface PrepareTaskInputOptions { channelName?: string; channelId?: string; customInstructions?: string; + autoPublishCloudRuns?: boolean; allowNoRepo?: boolean; } @@ -56,6 +57,7 @@ export function prepareTaskInput( options.signalReportId && isCloud ? "user" : undefined, cloudRunSource: options.signalReportId && isCloud ? "signal_report" : undefined, + cloudAutoPublish: isCloud ? options.autoPublishCloudRuns : undefined, signalReportId: options.signalReportId, additionalDirectories: isCloud ? undefined : options.additionalDirectories, channelContext: options.channelContext, diff --git a/packages/shared/src/task-creation-domain.ts b/packages/shared/src/task-creation-domain.ts index 3208609fc3..f000b51f02 100644 --- a/packages/shared/src/task-creation-domain.ts +++ b/packages/shared/src/task-creation-domain.ts @@ -36,6 +36,11 @@ export interface TaskCreationInput { sandboxEnvironmentId?: string; cloudPrAuthorshipMode?: PrAuthorshipMode; cloudRunSource?: CloudRunSource; + /** + * When true, the cloud run agent pushes its work and opens a draft PR on + * completion without waiting for an explicit ask (Settings → Advanced). + */ + cloudAutoPublish?: boolean; signalReportId?: string; additionalDirectories?: string[]; /** diff --git a/packages/ui/src/features/settings/sections/AdvancedSettings.tsx b/packages/ui/src/features/settings/sections/AdvancedSettings.tsx index bb1a125567..2763015ea8 100644 --- a/packages/ui/src/features/settings/sections/AdvancedSettings.tsx +++ b/packages/ui/src/features/settings/sections/AdvancedSettings.tsx @@ -23,10 +23,24 @@ export function AdvancedSettings() { ); const useNewChatThread = useSettingsStore((s) => s.useNewChatThread); const setUseNewChatThread = useSettingsStore((s) => s.setUseNewChatThread); + const autoPublishCloudRuns = useSettingsStore((s) => s.autoPublishCloudRuns); + const setAutoPublishCloudRuns = useSettingsStore( + (s) => s.setAutoPublishCloudRuns, + ); const devModeClient = useServiceOptional(DEV_MODE_CLIENT); return ( + + + void; setPreventSleepWhileRunning: (enabled: boolean) => void; setDebugLogsCloudRuns: (enabled: boolean) => void; + setAutoPublishCloudRuns: (enabled: boolean) => void; // Terminal terminalFont: TerminalFont; @@ -319,11 +323,14 @@ export const useSettingsStore = create()( allowBypassPermissions: false, preventSleepWhileRunning: false, debugLogsCloudRuns: false, + autoPublishCloudRuns: true, setAllowBypassPermissions: (enabled) => set({ allowBypassPermissions: enabled }), setPreventSleepWhileRunning: (enabled) => set({ preventSleepWhileRunning: enabled }), setDebugLogsCloudRuns: (enabled) => set({ debugLogsCloudRuns: enabled }), + setAutoPublishCloudRuns: (enabled) => + set({ autoPublishCloudRuns: enabled }), // Terminal terminalFont: "berkeley-mono", @@ -436,6 +443,7 @@ export const useSettingsStore = create()( allowBypassPermissions: state.allowBypassPermissions, preventSleepWhileRunning: state.preventSleepWhileRunning, debugLogsCloudRuns: state.debugLogsCloudRuns, + autoPublishCloudRuns: state.autoPublishCloudRuns, // Terminal terminalFont: state.terminalFont, diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index d1e1251a56..0ac192d20e 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -322,6 +322,8 @@ export function useTaskCreation({ channelName, channelId, customInstructions: useSettingsStore.getState().customInstructions, + autoPublishCloudRuns: + useSettingsStore.getState().autoPublishCloudRuns, allowNoRepo, }); From 27dbf15111e3a283aa9cb759e230116a0fe73a4b Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jul 2026 11:18:40 +0100 Subject: [PATCH 2/4] chore(settings): simplify auto-publish threading after review Inline the one-ternary getCloudAutoPublish helper, pass the option value through in the request body builder, take a single settings snapshot in useTaskCreation, and split the no-repo cloud prompt into per-mode blocks mirroring the repository branch. Generated-By: PostHog Code Task-Id: 49d73463-ec66-491c-b645-f6116972bd02 --- packages/agent/src/server/agent-server.ts | 25 +++++++++++-------- packages/api-client/src/posthog-client.ts | 2 +- packages/core/src/sessions/cloudRunOptions.ts | 6 ----- packages/core/src/sessions/sessionService.ts | 3 +-- .../task-detail/hooks/useTaskCreation.ts | 6 ++--- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index ef6291f954..3b0406955f 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -2729,28 +2729,33 @@ ${signedCommitInstructions}${prLinkInstructions}${shellEfficiencyInstructions} } if (!this.config.repositoryPath) { - const publishTiming = shouldAutoCreatePr - ? `- After completing code changes in a cloned repository, 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 without waiting to be asked.` - : `- 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.`; - const publishClosing = shouldAutoCreatePr - ? `- Always create the PR as a draft. Do not ask for confirmation before publishing completed code changes` - : `- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`; const publishInstructions = this.config.createPr === false ? ` When the user asks for code changes: - You may clone a repository and make local edits in that clone - Do NOT create branches, commits, push changes, or open pull requests in this run` - : ` -When the user ${shouldAutoCreatePr ? "asks" : "explicitly asks"} to clone or work in a GitHub repository: + : shouldAutoCreatePr + ? ` +When the user asks to clone or work in a GitHub repository: - Clone the repository into /tmp/workspace/repos// using \`gh repo clone / /tmp/workspace/repos//\` - Work from inside that cloned repository for follow-up code changes -${publishTiming} 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 #\` / \`Refs #\` links. +- After completing code changes in a cloned repository, 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 without waiting to be asked. 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 #\` / \`Refs #\` links. - 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. ${whyContextInstruction.trimStart()} ${publicRepoSafetyInstruction.trimStart()} - End the PR description with a horizontal rule followed by this footer line: ${prFooter} -${publishClosing}`; +- Always create the PR as a draft. Do not ask for confirmation before publishing completed code changes` + : ` +When the user explicitly asks to clone or work in a GitHub repository: +- Clone the repository into /tmp/workspace/repos// using \`gh repo clone / /tmp/workspace/repos//\` +- Work from inside that cloned repository for follow-up code changes +- 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 #\` / \`Refs #\` links. +- 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. +${whyContextInstruction.trimStart()} +${publicRepoSafetyInstruction.trimStart()} +- End the PR description with a horizontal rule followed by this footer line: ${prFooter} +- Do NOT create branches, commits, push changes, or open pull requests unless the user explicitly asks for that`; return `${identityInstructions} # Cloud Task Execution — No Repository Mode diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index 06542c55d3..ae9412f578 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -553,7 +553,7 @@ function buildCloudRunRequestBody( body.pr_authorship_mode = options.prAuthorshipMode; } if (options?.autoPublish) { - body.auto_publish = true; + body.auto_publish = options.autoPublish; } if (options?.runSource) { body.run_source = options.runSource; diff --git a/packages/core/src/sessions/cloudRunOptions.ts b/packages/core/src/sessions/cloudRunOptions.ts index cf8861c323..402d936f00 100644 --- a/packages/core/src/sessions/cloudRunOptions.ts +++ b/packages/core/src/sessions/cloudRunOptions.ts @@ -31,12 +31,6 @@ export function getCloudRunSource( return state.run_source === "signal_report" ? "signal_report" : "manual"; } -export function getCloudAutoPublish( - state: Record, -): boolean | undefined { - return state.auto_publish === true ? true : undefined; -} - export interface CloudRuntimeOptions { adapter?: Adapter; model?: string; diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index a519333175..b51904fc69 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -51,7 +51,6 @@ import { CloudLogGapReconciler } from "./cloudLogGapReconciler"; import { CloudRunIdleTracker } from "./cloudRunIdleTracker"; import { type CloudRuntimeOptions, - getCloudAutoPublish, getCloudPrAuthorshipMode, getCloudRunSource, getCloudRuntimeOptions, @@ -2928,7 +2927,7 @@ export class SessionService { pendingUserArtifactIds: artifactIds.length > 0 ? artifactIds : undefined, prAuthorshipMode, - autoPublish: getCloudAutoPublish(previousState), + autoPublish: previousState.auto_publish === true || undefined, runSource: getCloudRunSource(previousState), signalReportId: typeof previousState.signal_report_id === "string" diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 0ac192d20e..6f2e4992ab 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -299,6 +299,7 @@ export function useTaskCreation({ } } + const settings = useSettingsStore.getState(); const input = prepareTaskInput(serializedContent, filePaths, { // In channels chat-box mode no repo is attached up front, even if a // directory/repo is lingering in the persisted picker state. @@ -321,9 +322,8 @@ export function useTaskCreation({ channelContext, channelName, channelId, - customInstructions: useSettingsStore.getState().customInstructions, - autoPublishCloudRuns: - useSettingsStore.getState().autoPublishCloudRuns, + customInstructions: settings.customInstructions, + autoPublishCloudRuns: settings.autoPublishCloudRuns, allowNoRepo, }); From 5faf2554e597ef04121d96293c7a25ee0b7d8219 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jul 2026 14:40:41 +0100 Subject: [PATCH 3/4] fix(settings): send auto_publish on task creation so warm activations carry it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When task creation reuses a pre-warmed run, the saga skips run creation entirely — so the auto-publish choice, previously only sent on the run-creation request, never reached the backend for warm runs. Send auto_publish in the createTask payload for cloud creations; the backend persists it into the activated warm run's state so resumes honor it (PostHog/posthog#68864). Generated-By: PostHog Code Task-Id: 79bdd820-1678-48d9-a12b-922127eab1c1 --- packages/api-client/src/posthog-client.ts | 1 + packages/core/src/task-detail/taskCreationSaga.test.ts | 3 +++ packages/core/src/task-detail/taskCreationSaga.ts | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index ae9412f578..5f171b21f4 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -2224,6 +2224,7 @@ export class PostHogAPIClient { channel?: string | null; pending_user_message?: string; pending_user_artifact_ids?: string[]; + auto_publish?: boolean; }, ) { const teamId = await this.getTeamId(); diff --git a/packages/core/src/task-detail/taskCreationSaga.test.ts b/packages/core/src/task-detail/taskCreationSaga.test.ts index c728b79ae2..8a6cb369e2 100644 --- a/packages/core/src/task-detail/taskCreationSaga.test.ts +++ b/packages/core/src/task-detail/taskCreationSaga.test.ts @@ -557,6 +557,7 @@ describe("TaskCreationSaga", () => { repository: "posthog/posthog", workspaceMode: "cloud", branch: "main", + cloudAutoPublish: true, }); expect(result.success).toBe(true); @@ -580,6 +581,8 @@ describe("TaskCreationSaga", () => { branch: "main", pending_user_message: "/my-skill do it", pending_user_artifact_ids: ["skill-artifact-1"], + // Warm activation skips run creation, so the choice must ride along here. + auto_publish: true, }), ); // Warm-activated at create time: no fresh run is created or started. diff --git a/packages/core/src/task-detail/taskCreationSaga.ts b/packages/core/src/task-detail/taskCreationSaga.ts index 8c5d60aa5d..f0b8d1528e 100644 --- a/packages/core/src/task-detail/taskCreationSaga.ts +++ b/packages/core/src/task-detail/taskCreationSaga.ts @@ -767,6 +767,12 @@ export class TaskCreationSaga extends Saga< channel: input.channelId ?? undefined, pending_user_message: warmPayload?.pendingUserMessage, pending_user_artifact_ids: warmPayload?.pendingUserArtifactIds, + // If creation activates a pre-warmed run, this is the only request + // that can carry the choice — the saga skips run creation entirely. + auto_publish: + input.workspaceMode === "cloud" && input.cloudAutoPublish + ? true + : undefined, }); return result as unknown as Task; }, From cecf237aebf1de814f7c32a8cfd243ca6d006bac Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Tue, 7 Jul 2026 14:56:53 +0100 Subject: [PATCH 4/4] feat(agent): honor auto-publish on prewarmed runs via first-message upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prewarmed run's agent-server boots before the user's first message exists, so the --autoPublish launch flag can't carry the user's choice and the whole run stayed review-first. Nothing is sent to the agent until the forwarded first message arrives, so the choice can still govern the entire conversation: on that first message, prewarmed runs re-read the run's state (where the backend persists auto_publish at warm activation), flip the config, and inject the auto-publish cloud instructions into the prompt as an override — reusing buildCloudSystemPrompt and the existing prContext injection channel, so both runtime adapters work unchanged. createPr=false still wins (PostHog AI warm runs), automated origins already auto-publish, and a failed state fetch retries on the next message. Generated-By: PostHog Code Task-Id: 79bdd820-1678-48d9-a12b-922127eab1c1 --- .../agent/src/server/agent-server.test.ts | 80 +++++++++++++++++++ packages/agent/src/server/agent-server.ts | 76 +++++++++++++++++- 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/packages/agent/src/server/agent-server.test.ts b/packages/agent/src/server/agent-server.test.ts index e3245de60a..8ad788196e 100644 --- a/packages/agent/src/server/agent-server.test.ts +++ b/packages/agent/src/server/agent-server.test.ts @@ -2016,6 +2016,86 @@ describe("AgentServer HTTP Mode", () => { expect(prompt).not.toContain("unless the user explicitly asks for that"); }); + // Prewarmed runs boot before the user's choice exists; the upgrade is + // resolved from run state when the first message arrives. + type WarmTestable = { + prewarmedRun: boolean; + session: { payload: { task_id: string; run_id: string } } | null; + posthogAPI: { getTaskRun: ReturnType }; + resolveWarmAutoPublishUpgrade(): Promise; + buildCloudSystemPrompt(): string; + }; + const makeWarmServer = ( + state: Record | Error, + overrides: Partial[0]> = {}, + ): WarmTestable => { + const t = createServer(overrides) as unknown as WarmTestable; + t.prewarmedRun = true; + t.session = { + payload: { task_id: "test-task-id", run_id: "test-run-id" }, + }; + t.posthogAPI = { + getTaskRun: + state instanceof Error + ? vi.fn(async () => { + throw state; + }) + : vi.fn(async () => ({ state })), + }; + return t; + }; + + it("upgrades a prewarmed run to auto-publish from run state on the first message", async () => { + const t = makeWarmServer({ prewarmed: true, auto_publish: true }); + + const override = await t.resolveWarmAutoPublishUpgrade(); + expect(override).toContain("OVERRIDE PREVIOUS INSTRUCTIONS"); + expect(override).toContain("gh pr create --draft"); + // The flip persists for the rest of the session... + expect(t.buildCloudSystemPrompt()).toContain("gh pr create --draft"); + // ...and the override is injected only once. + expect(await t.resolveWarmAutoPublishUpgrade()).toBeNull(); + expect(t.posthogAPI.getTaskRun).toHaveBeenCalledTimes(1); + }); + + it("keeps a prewarmed run review-first when run state has no auto_publish", async () => { + const t = makeWarmServer({ prewarmed: true }); + + expect(await t.resolveWarmAutoPublishUpgrade()).toBeNull(); + expect(t.buildCloudSystemPrompt()).toContain( + "stop with local changes ready for review", + ); + expect(await t.resolveWarmAutoPublishUpgrade()).toBeNull(); + expect(t.posthogAPI.getTaskRun).toHaveBeenCalledTimes(1); + }); + + it("never upgrades a prewarmed run when createPr is false", async () => { + // PostHog AI warm runs launch with createPr=false; auto-publish must not + // override that even if auto_publish somehow lands in state. + const t = makeWarmServer( + { prewarmed: true, auto_publish: true }, + { createPr: false }, + ); + + expect(await t.resolveWarmAutoPublishUpgrade()).toBeNull(); + expect(t.posthogAPI.getTaskRun).not.toHaveBeenCalled(); + expect(t.buildCloudSystemPrompt()).toContain( + "stop with local changes ready for review", + ); + }); + + it("retries the state fetch on a later message when it fails", async () => { + const t = makeWarmServer(new Error("fetch failed")); + expect(await t.resolveWarmAutoPublishUpgrade()).toBeNull(); + + t.posthogAPI.getTaskRun = vi.fn(async () => ({ + state: { prewarmed: true, auto_publish: true }, + })); + expect(await t.resolveWarmAutoPublishUpgrade()).toContain( + "gh pr create --draft", + ); + }); + it.each([ { label: "Slack", origin: "slack" }, { label: "signal_report", origin: "signal_report" }, diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 3b0406955f..ea94745721 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -314,6 +314,11 @@ export class AgentServer { private lastReportedBranch: string | null = null; private resumeState: ResumeState | null = null; private nativeResume: { sessionId: string; warm: boolean } | null = null; + // Prewarmed runs boot before the user's first message exists, so the boot-time + // --autoPublish flag can't carry the user's choice; it is resolved from run + // state when the first message arrives (see resolveWarmAutoPublishUpgrade). + private prewarmedRun = false; + private warmAutoPublishResolved = false; private installedSkillBundles = new Set(); private installedSkillBundleInfo = new Map(); private installingSkillBundles = new Map>(); @@ -877,12 +882,19 @@ export class AgentServer { this.session.logWriter.resetTurnMessages(this.session.payload.run_id); + // Resolve before buildDetectedPrContext so a warm auto-publish upgrade + // also flips the detected-PR context to its push variant. + const autoPublishUpgrade = await this.resolveWarmAutoPublishUpgrade(); + const hostContext = [ + ...(autoPublishUpgrade ? [autoPublishUpgrade] : []), + ...(this.detectedPrUrl + ? [this.buildDetectedPrContext(this.detectedPrUrl)] + : []), + ]; const promptMeta: Record = { ...(builtPrompt.meta ?? {}), - ...(this.detectedPrUrl - ? { - prContext: this.buildDetectedPrContext(this.detectedPrUrl), - } + ...(hostContext.length > 0 + ? { prContext: hostContext.join("\n\n") } : {}), }; @@ -1100,6 +1112,8 @@ export class AgentServer { this.resumeState = null; this.nativeResume = null; + this.prewarmedRun = false; + this.warmAutoPublishResolved = false; this.logger.debug("Initializing session", { runId: payload.run_id, @@ -1131,6 +1145,10 @@ export class AgentServer { }), ]); + this.prewarmedRun = + (preTaskRun?.state as Record | undefined)?.prewarmed === + true; + const gatewayEnv = this.configureEnvironment({ isInternal: preTask?.internal === true, originProduct: preTask?.origin_product, @@ -2569,6 +2587,56 @@ export class AgentServer { ); } + /** + * A prewarmed run boots before the user's first message exists, so the + * --autoPublish flag can't carry the user's choice; the backend persists it + * into the run's state at warm activation instead. Nothing has been sent to + * the agent until that first message arrives, so resolving it here still + * governs the whole conversation: flip the config (so later consumers like + * buildDetectedPrContext see it) and return the auto-publish cloud + * instructions to inject into the first prompt as an override. + */ + private async resolveWarmAutoPublishUpgrade(): Promise { + if (!this.prewarmedRun || this.warmAutoPublishResolved || !this.session) { + return null; + } + if ( + this.config.autoPublish === true || + this.config.createPr === false || + this.isAutomatedOrigin() + ) { + // The boot decision already publishes (or never may) — nothing to upgrade. + this.warmAutoPublishResolved = true; + return null; + } + let state: Record | undefined; + try { + const run = await this.posthogAPI.getTaskRun( + this.session.payload.task_id, + this.session.payload.run_id, + ); + state = run?.state as Record | undefined; + } catch (error) { + // Leave unresolved so the next message retries; stay review-first for now. + this.logger.debug("Failed to fetch run state for auto-publish upgrade", { + error, + }); + return null; + } + this.warmAutoPublishResolved = true; + if (state?.auto_publish !== true) { + return null; + } + this.config.autoPublish = true; + this.logger.debug("Warm run upgraded to auto-publish from run state"); + return [ + "IMPORTANT — OVERRIDE PREVIOUS INSTRUCTIONS ABOUT CREATING BRANCHES/PRs.", + "The user has auto-publish enabled for this run. The review-first cloud task instructions in your system prompt are replaced by the following:", + "", + this.buildCloudSystemPrompt(this.detectedPrUrl), + ].join("\n"); + } + private buildDetectedPrContext(prUrl: string): string { if (!this.shouldAutoPublishCloudChanges()) { return (