diff --git a/packages/core/commands/index.ts b/packages/core/commands/index.ts index 687577e..ed940ce 100644 --- a/packages/core/commands/index.ts +++ b/packages/core/commands/index.ts @@ -56,6 +56,7 @@ export const commandDefinitions: Record = { description: "Fix PR feedback, push updates, and reply", agent: "worker", templatePath: "commands/pr/fix.md", + subtask: false, }, "pr/review": { description: "Review the current PR and publish review feedback", diff --git a/packages/core/commands/pr/create.md b/packages/core/commands/pr/create.md index da644bb..393e44a 100644 --- a/packages/core/commands/pr/create.md +++ b/packages/core/commands/pr/create.md @@ -106,6 +106,8 @@ Run `git push` and use its output as the source of truth. ### Create PR Use `pr_sync` to create the pull request: +- This step is PR creation only +- Omit `review`, `replies`, `commentBody`, and `commitId` entirely unless you are intentionally updating or reviewing an existing PR instead of creating one - Generate a concise title (max 70 chars) summarizing the change and store it as `` - Generate a short description that briefly describes the intent and scope - Pass `` as `head` when it is available so PR creation does not depend on local upstream inference diff --git a/packages/core/test/pr-sync.test.ts b/packages/core/test/pr-sync.test.ts index 7fa505e..a536a65 100644 --- a/packages/core/test/pr-sync.test.ts +++ b/packages/core/test/pr-sync.test.ts @@ -79,6 +79,28 @@ describe("pr_sync", () => { assert.equal(result.action, "approved"); }); + test("ignores an empty review object", async () => { + const executedCommands: string[] = []; + const shell = createMockShell(executedCommands, [ + { + contains: "gh pr create", + stdout: "https://github.com/acme/repo/pull/12\n", + }, + ]); + + const tool = createPrSyncTool(shell); + const output = await tool.execute({ + title: "Create PR without review payload", + body: "Omit review-only fields during PR creation.", + review: {}, + }, createToolContextForDirectory("/tmp/repo")); + + const result = JSON.parse(output); + assert.equal(result.url, "https://github.com/acme/repo/pull/12"); + assert.equal(result.action, "created"); + assert.equal(executedCommands.length, 1); + }); + test("updates and approves an existing PR in one call", async () => { const executedCommands: string[] = []; const shell = createMockShell(executedCommands, [ diff --git a/packages/core/tools/pr-sync.ts b/packages/core/tools/pr-sync.ts index 466b2c2..00cd4c0 100644 --- a/packages/core/tools/pr-sync.ts +++ b/packages/core/tools/pr-sync.ts @@ -51,6 +51,18 @@ type PrSyncArgs = { commentBody?: string; }; +function normalizeReviewInput(review?: ReviewInput): ReviewInput | undefined { + if (!review) { + return undefined; + } + + const hasBody = Boolean(review.body?.trim()); + const hasComments = (review.comments?.length ?? 0) > 0; + const hasApprove = "approve" in review && review.approve === true; + + return hasBody || hasComments || hasApprove ? review : undefined; +} + function renderPrBody(args: PrSyncArgs) { if (args.body?.trim()) { return args.body.trim(); @@ -400,12 +412,12 @@ export function createPrSyncTool($: Shell) { commitId: { type: "string", optional: true, - description: "Commit ID to associate with the approval", + description: "Commit SHA to anchor review comments to; omit unless sending review comments", }, review: { type: "json", optional: true, - description: "Structured PR review submission with event, optional body, commitId, and inline comments", + description: "Optional structured PR review submission; omit the field entirely unless submitting a review body, inline comments, or approval", }, replies: { type: "json", @@ -420,7 +432,7 @@ export function createPrSyncTool($: Shell) { }, async execute(args: PrSyncArgs, ctx: ToolExecutionContext) { const body = renderPrBody(args); - const review = args.review; + const review = normalizeReviewInput(args.review); const metadataUpdate = hasMetadataUpdate(args, body); const existingPrActions = requiresExistingPullRequest(args, review); diff --git a/packages/opencode/.opencode/commands/pr/create.md b/packages/opencode/.opencode/commands/pr/create.md index 931c111..22d5964 100644 --- a/packages/opencode/.opencode/commands/pr/create.md +++ b/packages/opencode/.opencode/commands/pr/create.md @@ -142,6 +142,8 @@ Run `git push` and use its output as the source of truth. ### Create PR Use `kompass_pr_sync` to create the pull request: +- This step is PR creation only +- Omit `review`, `replies`, `commentBody`, and `commitId` entirely unless you are intentionally updating or reviewing an existing PR instead of creating one - Generate a concise title (max 70 chars) summarizing the change and store it as `` - Generate a short description that briefly describes the intent and scope - Pass `` as `head` when it is available so PR creation does not depend on local upstream inference diff --git a/packages/opencode/index.ts b/packages/opencode/index.ts index f32f1a7..f8b2b5f 100644 --- a/packages/opencode/index.ts +++ b/packages/opencode/index.ts @@ -203,7 +203,7 @@ const opencodeToolCreators: Record = { })).describe("Checklist sections rendered as markdown").optional(), draft: tool.schema.boolean().describe("Create as draft PR").optional(), refUrl: tool.schema.string().describe("Optional PR URL to update").optional(), - commitId: tool.schema.string().describe("Commit SHA to anchor review comments to").optional(), + commitId: tool.schema.string().describe("Commit SHA to anchor review comments to; omit unless sending review comments").optional(), review: tool.schema.object({ body: tool.schema.string().describe("Optional review summary body").optional(), comments: tool.schema.array(tool.schema.object({ @@ -218,7 +218,7 @@ const opencodeToolCreators: Record = { ? { approve: tool.schema.boolean().describe("Approve the PR with this review comment").optional() } : {} ), - }).describe("Structured review submission").optional(), + }).describe("Optional structured review submission; omit the field entirely unless submitting a review body, inline comments, or approval").optional(), replies: tool.schema.array(tool.schema.object({ inReplyTo: tool.schema.number().int().describe("Existing review comment ID to reply to"), body: tool.schema.string().describe("Reply body"), diff --git a/packages/opencode/test/commands-config.test.ts b/packages/opencode/test/commands-config.test.ts index 69c1640..6757a7f 100644 --- a/packages/opencode/test/commands-config.test.ts +++ b/packages/opencode/test/commands-config.test.ts @@ -340,6 +340,7 @@ describe("applyCommandsConfig", () => { await applyCommandsConfig(cfg as never, process.cwd()); assert.ok(cfg.command); + assert.equal(cfg.command!["pr/fix"]?.subtask, false); assert.equal(cfg.command!["pr/review"]?.subtask, true); assert.equal(cfg.command!["dev"]?.subtask, true); assert.equal(cfg.command!["ship"]?.subtask, true); @@ -353,6 +354,7 @@ describe("applyCommandsConfig", () => { await applyCommandsConfig(cfg as never, process.cwd()); assert.ok(cfg.command); + assert.equal(cfg.command!["pr/fix"]?.subtask, false); assert.equal(cfg.command!["pr/review"]?.subtask, false); assert.equal(cfg.command!["dev"]?.subtask, false); assert.equal(cfg.command!["ship"]?.subtask, false);