Add retrying. Improve diff format

This commit is contained in:
Colin McDonnell
2025-12-22 15:53:11 -08:00
parent 615a3bc8e1
commit c518e8b6fd
7 changed files with 213 additions and 39 deletions
+80 -7
View File
@@ -1,12 +1,85 @@
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import type { Octokit } from "@octokit/rest";
import type { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import { log } from "../utils/cli.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
type PullFile = RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"][number];
/**
* formats PR files with explicit line numbers for each code line.
* preserves all original diff info (file headers, hunk headers) and adds:
* OLD | NEW | TYPE | code
*/
export function formatFilesWithLineNumbers(files: PullFile[]): string {
const output: string[] = [];
for (const file of files) {
// file header
output.push(`diff --git a/${file.filename} b/${file.filename}`);
output.push(`--- a/${file.filename}`);
output.push(`+++ b/${file.filename}`);
if (!file.patch) {
output.push("(binary file or no changes)");
output.push("");
continue;
}
// parse and format the patch with line numbers
const lines = file.patch.split("\n");
let oldLine = 0;
let newLine = 0;
for (const line of lines) {
// hunk header: @@ -OLD,COUNT +NEW,COUNT @@ optional context
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (hunkMatch) {
oldLine = parseInt(hunkMatch[1], 10);
newLine = parseInt(hunkMatch[2], 10);
output.push(line); // pass through unchanged
continue;
}
// code lines within hunks
const changeType = line[0] || " ";
const code = line.slice(1);
if (changeType === "-") {
// removed line: show old line number, no new line number
output.push(`${padNum(oldLine)} | | - | ${code}`);
oldLine++;
} else if (changeType === "+") {
// added line: no old line number, show new line number
output.push(` | ${padNum(newLine)} | + | ${code}`);
newLine++;
} else if (changeType === " " || changeType === "\\") {
// context line or "\ No newline at end of file"
if (changeType === "\\") {
output.push(line); // pass through as-is
} else {
output.push(`${padNum(oldLine)} | ${padNum(newLine)} | | ${code}`);
oldLine++;
newLine++;
}
} else {
// unknown line type, pass through
output.push(line);
}
}
output.push(""); // blank line between files
}
return output.join("\n");
}
function padNum(n: number): string {
return n.toString().padStart(4, " ");
}
export const CheckoutPr = type({
pull_number: type.number.describe("the pull request number to checkout"),
});
@@ -170,16 +243,16 @@ export function CheckoutPrTool(ctx: ToolContext) {
throw new Error(`PR #${pull_number} source repository was deleted`);
}
// fetch PR diff via API (authoritative source - not affected by main advancing)
const diffResponse = await ctx.octokit.rest.pulls.get({
// fetch PR files and format with line numbers
const filesResponse = await ctx.octokit.rest.pulls.listFiles({
owner: ctx.owner,
repo: ctx.name,
pull_number,
mediaType: { format: "diff" },
per_page: 100,
});
// write diff to file for grep access
const diffContent = diffResponse.data as unknown as string;
const diffContent = formatFilesWithLineNumbers(filesResponse.data);
const diffPreview = diffContent.split("\n").slice(0, 100).join("\n");
log.debug(`formatted diff preview (first 100 lines):\n${diffPreview}`);
const tempDir = process.env.PULLFROG_TEMP_DIR;
if (!tempDir) {
throw new Error(
+1 -1
View File
@@ -29,7 +29,7 @@ function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
return `${bodyWithoutFooter}${footer}`;
}
export function PullRequestTool(ctx: ToolContext) {
export function CreatePullRequestTool(ctx: ToolContext) {
return tool({
name: "create_pull_request",
description: "Create a pull request from the current branch",
+4 -5
View File
@@ -11,7 +11,7 @@ export const CreatePullRequestReview = type({
pull_number: type.number.describe("The pull request number to review"),
body: type.string
.describe(
"1-2 sentence high-level summary ONLY. Include urgency level and critical callouts (e.g., API key leak). ALL specific feedback MUST go in 'comments' array instead."
"1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
)
.optional(),
commit_id: type.string
@@ -20,12 +20,12 @@ export const CreatePullRequestReview = type({
comments: type({
path: type.string.describe("The file path to comment on (relative to repo root)"),
line: type.number.describe(
"The line number in the file (use line numbers from the diff - usually the RIGHT side/new code)"
"Line number from the diff. Each code line shows 'OLD | NEW | TYPE | code'. Use NEW (second column) for added/context lines, OLD (first column) for removed lines."
),
side: type
.enumerated("LEFT", "RIGHT")
.describe(
"Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided."
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
)
.optional(),
body: type.string.describe(
@@ -37,8 +37,7 @@ export const CreatePullRequestReview = type({
})
.array()
.describe(
// FORK PR NOTE: checkout_pr returns the diff via GitHub API - use that for line numbers
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use the diff returned from checkout_pr to find correct line numbers (RIGHT side for new code, LEFT for old)."
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
)
.optional(),
});
+2 -2
View File
@@ -24,7 +24,7 @@ import { GetIssueCommentsTool } from "./issueComments.ts";
import { GetIssueEventsTool } from "./issueEvents.ts";
import { IssueInfoTool } from "./issueInfo.ts";
import { AddLabelsTool } from "./labels.ts";
import { PullRequestTool } from "./pr.ts";
import { CreatePullRequestTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
import { CreatePullRequestReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
@@ -83,7 +83,7 @@ export async function startMcpHttpServer(
IssueInfoTool(ctx),
GetIssueCommentsTool(ctx),
GetIssueEventsTool(ctx),
PullRequestTool(ctx),
CreatePullRequestTool(ctx),
CreatePullRequestReviewTool(ctx),
PullRequestInfoTool(ctx),
CheckoutPrTool(ctx),