diff --git a/apps/web/src/lib/bot/agent-runner.ts b/apps/web/src/lib/bot/agent-runner.ts index 1562b2e51b..b4727ce42d 100644 --- a/apps/web/src/lib/bot/agent-runner.ts +++ b/apps/web/src/lib/bot/agent-runner.ts @@ -268,6 +268,8 @@ export async function runBotAgent(params: RunBotAgentParams): Promise { diff --git a/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.test.ts b/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.test.ts index 8d20abb374..5fa20fa55d 100644 --- a/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.test.ts +++ b/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.test.ts @@ -68,7 +68,7 @@ let mockGetGitLabInstanceUrlForUser: jest.MockedFunction; let mockResolveBotSessionProfile: jest.MockedFunction; -describe('spawnCloudAgentSession attachment forwarding', () => { +describe('spawnCloudAgentSession', () => { beforeAll(async () => { const client = await import('@/lib/cloud-agent-next/cloud-agent-client'); const github = await import('@/lib/cloud-agent/github-integration-helpers'); @@ -136,4 +136,74 @@ describe('spawnCloudAgentSession attachment forwarding', () => { expect(prepareInput).toEqual(expect.objectContaining({ attachments })); expect(prepareInput).not.toHaveProperty('images'); }); + + it('adds separate PR strategy guidance when preparing a requested GitHub session', async () => { + await spawnCloudAgentSession( + { + githubRepo: 'owner/repo', + githubPullRequestStrategy: 'separate_pull_request', + prompt: 'Update REVIEW.md', + mode: 'code', + }, + 'model', + platformIntegration, + 'auth-token', + 'ticket-user', + 'request-3' + ); + + const prepareInput = mockPrepareSession.mock.calls[0]?.[0]; + expect(prepareInput).toEqual( + expect.objectContaining({ + prompt: expect.stringContaining('create a fresh branch'), + }) + ); + expect(prepareInput).toEqual( + expect.objectContaining({ + prompt: expect.stringContaining("must not push to or modify the current PR's head branch"), + }) + ); + }); + + it('adds current PR branch guidance when preparing a current-branch GitHub session', async () => { + await spawnCloudAgentSession( + { + githubRepo: 'owner/repo', + githubPullRequestStrategy: 'current_pr_branch', + prompt: 'Update REVIEW.md', + mode: 'code', + }, + 'model', + platformIntegration, + 'auth-token', + 'ticket-user', + 'request-4' + ); + + const prepareInput = mockPrepareSession.mock.calls[0]?.[0]; + expect(prepareInput).toEqual( + expect.objectContaining({ + prompt: expect.stringContaining('Work on the current PR head branch'), + }) + ); + expect(prepareInput).toEqual( + expect.objectContaining({ + prompt: expect.not.stringContaining('create a fresh branch'), + }) + ); + }); + + it('leaves the prompt unchanged when separate PR strategy is not requested', async () => { + await spawnCloudAgentSession( + { githubRepo: 'owner/repo', prompt: 'Update REVIEW.md', mode: 'code' }, + 'model', + platformIntegration, + 'auth-token', + 'ticket-user', + 'request-5' + ); + + const prepareInput = mockPrepareSession.mock.calls[0]?.[0]; + expect(prepareInput).toEqual(expect.objectContaining({ prompt: 'Update REVIEW.md' })); + }); }); diff --git a/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts b/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts index d178c92011..092ed26883 100644 --- a/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts +++ b/apps/web/src/lib/bot/tools/spawn-cloud-agent-session.ts @@ -31,6 +31,18 @@ import { captureException } from '@sentry/nextjs'; import type { PlatformIntegration } from '@kilocode/db'; import z from 'zod'; +const SEPARATE_PULL_REQUEST_STRATEGY_INSTRUCTION = ` + +--- +**Required GitHub PR strategy:** +The user explicitly requested a separate new PR. You must create a fresh branch from the original PR's base branch, open a separate new GitHub PR targeting that same base branch, and must not push to or modify the current PR's head branch.`; + +const CURRENT_PULL_REQUEST_BRANCH_STRATEGY_INSTRUCTION = ` + +--- +**Required GitHub PR strategy:** +Work on the current PR head branch for this task and do not open a separate GitHub PR.`; + /** * Derive a per-request callback token so the dedicated callback HMAC secret * is never stored in session metadata (which is visible via getSession). @@ -64,6 +76,12 @@ export const spawnCloudAgentInputSchema = z.object({ .regex(/^[-a-zA-Z0-9_.]+\/[-a-zA-Z0-9_.]+$/) .describe('The GitHub repository in owner/repo format (e.g., "facebook/react")') .optional(), + githubPullRequestStrategy: z + .enum(['current_pr_branch', 'separate_pull_request']) + .describe( + 'For GitHub PR or review-thread work only. Select "separate_pull_request" when the user asks for a new, fresh, separate, or different PR. Select "current_pr_branch" when the user wants the current PR updated or does not ask for a separate PR.' + ) + .optional(), gitlabProject: z .string() .regex(/^[-a-zA-Z0-9_.]+(?:\/[-a-zA-Z0-9_.]+)+$/) @@ -74,7 +92,7 @@ export const spawnCloudAgentInputSchema = z.object({ prompt: z .string() .describe( - 'The task description for the Cloud Agent. Be specific about what changes or analysis you want.' + 'The task description for the Cloud Agent. Be specific about what changes or analysis you want. Cloud Agent does not see bot-only GitHub PR/review-thread context unless you include it here. For GitHub PR or review-thread work, include the PR URL or number, relevant review-thread/comment details, and the chosen current-PR or separate-PR strategy.' ), mode: z .enum(['code', 'ask']) @@ -133,6 +151,12 @@ export default async function spawnCloudAgentSession( let prompt = args.prompt; + if (args.githubPullRequestStrategy === 'separate_pull_request' && args.githubRepo) { + prompt += SEPARATE_PULL_REQUEST_STRATEGY_INSTRUCTION; + } else if (args.githubPullRequestStrategy === 'current_pr_branch' && args.githubRepo) { + prompt += CURRENT_PULL_REQUEST_BRANCH_STRATEGY_INSTRUCTION; + } + // Append PR/MR signature to the prompt if available if (options?.prSignature) { prompt += options.prSignature;