From bb55216a6b6ca91e57d9fa3be2470a21dda1b859 Mon Sep 17 00:00:00 2001 From: David Blass Date: Thu, 11 Dec 2025 18:02:44 -0500 Subject: [PATCH] iterate on pr fix --- entry | 157 +++++++++++++++++++++++++++-------- external.ts | 9 ++ main.ts | 14 +++- mcp/comment.ts | 64 +++++++++----- mcp/review.ts | 48 ++++++++--- modes.ts | 26 ++++-- package.json | 2 +- utils/buildPullfrogFooter.ts | 71 ++++++++++++++++ 8 files changed, 312 insertions(+), 79 deletions(-) create mode 100644 utils/buildPullfrogFooter.ts diff --git a/entry b/entry index aa0e516..f08c0b8 100755 --- a/entry +++ b/entry @@ -39243,6 +39243,47 @@ var init_api = __esm({ } }); +// utils/buildPullfrogFooter.ts +function buildPullfrogFooter(params) { + const parts = []; + if (params.triggeredBy) { + parts.push("Triggered by [Pullfrog](https://pullfrog.com)"); + } + if (params.agent) { + parts.push(`Using [${params.agent.displayName}](${params.agent.url})`); + } + if (params.workflowRun) { + const { owner, repo, runId } = params.workflowRun; + parts.push(`[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})`); + } + if (params.customParts) { + parts.push(...params.customParts); + } + const allParts = [ + ...parts, + "[pullfrog.com](https://pullfrog.com)", + "[\u{1D54F}](https://x.com/pullfrogai)" + ]; + return ` +${PULLFROG_DIVIDER} +${FROG_LOGO}  \uFF5C ${allParts.join(" \uFF5C ")}`; +} +function stripExistingFooter(body) { + const dividerIndex = body.indexOf(PULLFROG_DIVIDER); + if (dividerIndex === -1) { + return body; + } + return body.substring(0, dividerIndex).trimEnd(); +} +var PULLFROG_DIVIDER, FROG_LOGO; +var init_buildPullfrogFooter = __esm({ + "utils/buildPullfrogFooter.ts"() { + "use strict"; + PULLFROG_DIVIDER = ""; + FROG_LOGO = `Pullfrog`; + } +}); + // mcp/shared.ts function initMcpContext(state) { mcpInitContext = state; @@ -39404,6 +39445,7 @@ __export(comment_exports, { ReplyToReviewCommentTool: () => ReplyToReviewCommentTool, ReportProgress: () => ReportProgress, ReportProgressTool: () => ReportProgressTool, + deleteProgressComment: () => deleteProgressComment, ensureProgressCommentUpdated: () => ensureProgressCommentUpdated, reportProgress: () => reportProgress, wasProgressCommentUpdated: () => wasProgressCommentUpdated @@ -39413,19 +39455,14 @@ function buildCommentFooter(payload) { const runId = process.env.GITHUB_RUN_ID; const agentName = payload.agent; const agentInfo = agentName ? agentsManifest[agentName] : null; - const agentDisplayName = agentInfo?.displayName || "Unknown agent"; - const agentUrl = agentInfo?.url || "https://pullfrog.com"; - const workflowRunPart = runId ? `[View workflow run](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})` : "View workflow run"; - return ` -${PULLFROG_DIVIDER} -Pullfrog  \uFF5C Triggered by [Pullfrog](https://pullfrog.com) \uFF5C Using [${agentDisplayName}](${agentUrl}) \uFF5C ${workflowRunPart} \uFF5C [\u{1D54F}](https://x.com/pullfrogai)`; -} -function stripExistingFooter(body) { - const dividerIndex = body.indexOf(PULLFROG_DIVIDER); - if (dividerIndex === -1) { - return body; - } - return body.substring(0, dividerIndex).trimEnd(); + return buildPullfrogFooter({ + triggeredBy: true, + agent: { + displayName: agentInfo?.displayName || "Unknown agent", + url: agentInfo?.url || "https://pullfrog.com" + }, + workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0 + }); } function addFooter(body, payload) { const bodyWithoutFooter = stripExistingFooter(body); @@ -39494,6 +39531,22 @@ async function reportProgress({ body }) { function wasProgressCommentUpdated() { return progressCommentWasUpdated; } +async function deleteProgressComment() { + const existingCommentId = getProgressCommentId(); + if (!existingCommentId) { + return false; + } + const ctx = getMcpContext(); + await ctx.octokit.rest.issues.deleteComment({ + owner: ctx.owner, + repo: ctx.name, + comment_id: existingCommentId + }); + progressCommentId = null; + progressCommentIdInitialized = true; + progressCommentWasUpdated = true; + return true; +} async function ensureProgressCommentUpdated(payload) { if (progressCommentWasUpdated) { return; @@ -39553,7 +39606,7 @@ The workflow encountered an error before any progress could be reported. Please body }); } -var PULLFROG_DIVIDER, LEAPING_INTO_ACTION_PREFIX, Comment, CreateCommentTool, EditComment, EditCommentTool, progressCommentId, progressCommentIdInitialized, progressCommentWasUpdated, ReportProgress, ReportProgressTool, ReplyToReviewComment, ReplyToReviewCommentTool; +var LEAPING_INTO_ACTION_PREFIX, Comment, CreateCommentTool, EditComment, EditCommentTool, progressCommentId, progressCommentIdInitialized, progressCommentWasUpdated, ReportProgress, ReportProgressTool, ReplyToReviewComment, ReplyToReviewCommentTool; var init_comment = __esm({ "mcp/comment.ts"() { "use strict"; @@ -39561,9 +39614,9 @@ var init_comment = __esm({ init_out4(); init_external(); init_api(); + init_buildPullfrogFooter(); init_github(); init_shared3(); - PULLFROG_DIVIDER = ""; LEAPING_INTO_ACTION_PREFIX = "Leaping into action"; Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), @@ -39571,7 +39624,7 @@ var init_comment = __esm({ }); CreateCommentTool = tool({ name: "create_issue_comment", - description: "Create a comment on a GitHub issue", + description: "Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.", parameters: Comment, execute: contextualize(async ({ issueNumber, body }, ctx) => { const bodyWithFooter = addFooter(body, ctx.payload); @@ -39635,11 +39688,13 @@ var init_comment = __esm({ ReplyToReviewComment = type({ pull_number: type.number.describe("the pull request number"), comment_id: type.number.describe("the ID of the review comment to reply to"), - body: type.string.describe("the reply text explaining how the feedback was addressed") + body: type.string.describe( + "extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'" + ) }); ReplyToReviewCommentTool = tool({ name: "reply_to_review_comment", - description: "Reply to a PR review comment thread explaining how the feedback was addressed. Use this after addressing each review comment to provide specific context about the changes made.", + description: "Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).", parameters: ReplyToReviewComment, execute: contextualize(async ({ pull_number, comment_id, body }, ctx) => { const bodyWithFooter = addFooter(body, ctx.payload); @@ -97388,7 +97443,7 @@ function query({ // package.json var package_default = { name: "@pullfrog/action", - version: "0.0.131", + version: "0.0.132", type: "module", files: [ "index.js", @@ -97901,16 +97956,15 @@ function getModes({ disableProgressComment }) { 4. Make the necessary code changes to address the feedback. Work through each review comment systematically. -5. After addressing each review comment, use ${ghPullfrogMcpName}/reply_to_review_comment to reply directly to that comment thread explaining what change was made (keep replies concise, 1-2 sentences). +5. **CRITICAL: Reply to EACH review comment individually.** After fixing each comment, use ${ghPullfrogMcpName}/reply_to_review_comment to reply directly to that comment thread. Keep replies extremely brief (1 sentence max, e.g., "Fixed by renaming to X" or "Added null check"). -6. Test your changes to ensure they work correctly.${disableProgressComment ? "" : ` +6. Test your changes to ensure they work correctly. -7. ${reportProgressInstruction}`} +7. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one. +${disableProgressComment ? "" : ` +8. ${reportProgressInstruction} -8. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one. - -9. Call report_progress one final time ONLY if you haven't already included a complete summary in a previous report_progress call. If you already called report_progress with complete information, you do NOT need to call it again. Only make a final call if you need to add missing information. **IMPORTANT**: Do NOT overwrite a good comment with details with a generic message. -` +**CRITICAL: Keep the progress comment extremely brief.** The summary should be 1-2 sentences max (e.g., "Fixed 3 review comments and pushed changes."). Almost all detail belongs in the individual reply_to_review_comment calls, NOT in the progress comment.`}` }, { name: "Review", @@ -97920,13 +97974,16 @@ function getModes({ disableProgressComment }) { 2. View diff: git diff origin/...origin/ (use line numbers from this for inline comments, replace and with 'base' and 'head' from PR info) -3. Read files from the checked-out PR branch to understand the implementation${disableProgressComment ? "" : ` +3. Read files from the checked-out PR branch to understand the implementation -4. ${reportProgressInstruction}`} +4. Submit review using ${ghPullfrogMcpName}/submit_pull_request_review -5. When submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff - -6. Only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location` +**CRITICAL: Prioritize per-line feedback over summary text.** +- ALL specific feedback MUST go in the 'comments' array with file paths and line numbers from the diff +- for issues appearing in multiple places, comment on the FIRST occurrence and reference others (e.g., "also at lines X, Y" or "similar issue in otherFile.ts:42") +- the 'body' field is ONLY for: (1) a 1-2 sentence high-level overview, (2) urgency level (e.g., "minor suggestions" vs "blocking issues"), (3) critical security callouts (e.g., API key exposure) +- 95%+ of review content should be in per-line comments; the body should be just a couple sentences +- the review body will include quick action links for addressing feedback, so keep it concise` }, { name: "Plan", @@ -124751,11 +124808,13 @@ var PullRequestInfoTool = tool({ // mcp/review.ts init_out4(); +init_buildPullfrogFooter(); +init_comment(); init_shared3(); var Review = type({ pull_number: type.number.describe("The pull request number to review"), body: type.string.describe( - "Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array." + "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." ).optional(), commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(), comments: type({ @@ -124766,15 +124825,17 @@ var Review = type({ side: type.enumerated("LEFT", "RIGHT").describe( "Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided." ).optional(), - body: type.string.describe("The comment text for this specific line"), + body: type.string.describe( + "The comment text for this specific line. For issues appearing multiple times, comment on the first occurrence and reference others." + ), start_line: type.number.describe("Start line for multi-line comments (optional, for commenting on ranges)").optional() }).array().describe( - "REQUIRED: Array of inline comments for specific code issues. Use this for all location-specific feedback. Use 'git diff origin/...origin/' to find the correct line numbers (typically use the line numbers shown on the RIGHT side for new code, LEFT side for old code)." + "PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/...origin/' to find correct line numbers (RIGHT side for new code, LEFT for old)." ).optional() }); var ReviewTool = tool({ name: "submit_pull_request_review", - description: "Submit a review (approve, request changes, or comment) for an existing pull request. IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.", + description: "Submit a review for an existing pull request. IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. Only use 'body' for a 1-2 sentence summary with urgency and critical callouts.", parameters: Review, execute: contextualize(async ({ pull_number, body, commit_id, comments = [] }, ctx) => { const pr = await ctx.octokit.rest.pulls.get({ @@ -124808,9 +124869,25 @@ var ReviewTool = tool({ }); } const result = await ctx.octokit.rest.pulls.createReview(params); + const reviewId = result.data.id; + const apiUrl = process.env.API_URL || "https://pullfrog.com"; + const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix&review_id=${reviewId}`; + const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`; + const footer = buildPullfrogFooter({ + customParts: [`[Fix all \u2794](${fixAllUrl})`, `[Fix \u{1F44D}s \u2794](${fixApprovedUrl})`] + }); + const updatedBody = (body || "") + footer; + await ctx.octokit.rest.pulls.updateReview({ + owner: ctx.owner, + repo: ctx.name, + pull_number, + review_id: reviewId, + body: updatedBody + }); + await deleteProgressComment(); return { success: true, - reviewId: result.data.id, + reviewId, html_url: result.data.html_url, state: result.data.state, user: result.data.user?.login, @@ -125172,6 +125249,14 @@ async function main(inputs) { await startMcpServer(ctx); mcpServerClose = ctx.mcpServerClose; timer.checkpoint("startMcpServer"); + if (ctx.payload.event.trigger === "fix_review" && Array.isArray(ctx.payload.event.comment_ids) && ctx.payload.event.comment_ids.length === 0) { + await reportProgress({ + body: `\u{1F44D} **No approved comments found** + +To use "Fix \u{1F44D}s", add a \u{1F44D} reaction to one or more inline review comments you want fixed.` + }); + return { success: true }; + } setupMcpServers(ctx); await installAgentCli(ctx); timer.checkpoint("installAgentCli"); diff --git a/external.ts b/external.ts index 62d0146..ee34a66 100644 --- a/external.ts +++ b/external.ts @@ -149,6 +149,15 @@ export type PayloadEvent = trigger: "workflow_dispatch"; [key: string]: any; } + | { + trigger: "fix_review"; + issue_number: number; + review_id: number; + /** "all" to fix all comments, or specific comment IDs to fix */ + comment_ids: number[] | "all"; + branch: string; + [key: string]: any; + } | { trigger: "unknown"; [key: string]: any; diff --git a/main.ts b/main.ts index 40eef6d..60e64e5 100644 --- a/main.ts +++ b/main.ts @@ -8,7 +8,7 @@ import { agents } from "./agents/index.ts"; import type { AgentResult } from "./agents/shared.ts"; import type { AgentName, Payload } from "./external.ts"; import { agentsManifest } from "./external.ts"; -import { ensureProgressCommentUpdated } from "./mcp/comment.ts"; +import { ensureProgressCommentUpdated, reportProgress } from "./mcp/comment.ts"; import { createMcpConfigs } from "./mcp/config.ts"; import { startMcpHttpServer } from "./mcp/server.ts"; import { getModes, modes } from "./modes.ts"; @@ -78,6 +78,18 @@ export async function main(inputs: Inputs): Promise { mcpServerClose = ctx.mcpServerClose; timer.checkpoint("startMcpServer"); + // check for empty comment_ids in fix_review trigger - report and exit early + if ( + ctx.payload.event.trigger === "fix_review" && + Array.isArray(ctx.payload.event.comment_ids) && + ctx.payload.event.comment_ids.length === 0 + ) { + await reportProgress({ + body: `👍 **No approved comments found**\n\nTo use "Fix 👍s", add a 👍 reaction to one or more inline review comments you want fixed.`, + }); + return { success: true }; + } + setupMcpServers(ctx); await installAgentCli(ctx); diff --git a/mcp/comment.ts b/mcp/comment.ts index 2713988..cec52ad 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -3,11 +3,10 @@ import { type } from "arktype"; import type { Payload } from "../external.ts"; import { agentsManifest } from "../external.ts"; import { fetchWorkflowRunInfo } from "../utils/api.ts"; +import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts"; import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts"; import { contextualize, getMcpContext, tool } from "./shared.ts"; -const PULLFROG_DIVIDER = ""; - /** * The prefix text for the initial "leaping into action" comment. * This is used to identify if a comment is still in its initial state @@ -21,25 +20,15 @@ function buildCommentFooter(payload: Payload): string { const agentName = payload.agent; const agentInfo = agentName ? agentsManifest[agentName] : null; - const agentDisplayName = agentInfo?.displayName || "Unknown agent"; - const agentUrl = agentInfo?.url || "https://pullfrog.com"; - // build workflow run link or show unavailable message - const workflowRunPart = runId - ? `[View workflow run](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})` - : "View workflow run"; - - return ` -${PULLFROG_DIVIDER} -Pullfrog  | Triggered by [Pullfrog](https://pullfrog.com) | Using [${agentDisplayName}](${agentUrl}) | ${workflowRunPart} | [𝕏](https://x.com/pullfrogai)`; -} - -function stripExistingFooter(body: string): string { - const dividerIndex = body.indexOf(PULLFROG_DIVIDER); - if (dividerIndex === -1) { - return body; - } - return body.substring(0, dividerIndex).trimEnd(); + return buildPullfrogFooter({ + triggeredBy: true, + agent: { + displayName: agentInfo?.displayName || "Unknown agent", + url: agentInfo?.url || "https://pullfrog.com", + }, + workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined, + }); } function addFooter(body: string, payload: Payload): string { @@ -55,7 +44,8 @@ export const Comment = type({ export const CreateCommentTool = tool({ name: "create_issue_comment", - description: "Create a comment on a GitHub issue", + description: + "Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.", parameters: Comment, execute: contextualize(async ({ issueNumber, body }, ctx) => { const bodyWithFooter = addFooter(body, ctx.payload); @@ -231,6 +221,32 @@ export function wasProgressCommentUpdated(): boolean { return progressCommentWasUpdated; } +/** + * Delete the progress comment if it exists. + * Used after submitting a PR review since the review body contains all necessary info. + */ +export async function deleteProgressComment(): Promise { + const existingCommentId = getProgressCommentId(); + if (!existingCommentId) { + return false; + } + + const ctx = getMcpContext(); + + await ctx.octokit.rest.issues.deleteComment({ + owner: ctx.owner, + repo: ctx.name, + comment_id: existingCommentId, + }); + + // reset state but mark as "updated" so ensureProgressCommentUpdated doesn't try to handle it + progressCommentId = null; + progressCommentIdInitialized = true; // keep initialized so we don't re-fetch from env + progressCommentWasUpdated = true; // mark as handled so ensureProgressCommentUpdated skips + + return true; +} + /** * Ensure the progress comment is updated with a generic error message if it was never updated. * This should be called after agent execution completes to handle cases where the agent @@ -327,13 +343,15 @@ The workflow encountered an error before any progress could be reported. Please export const ReplyToReviewComment = type({ pull_number: type.number.describe("the pull request number"), comment_id: type.number.describe("the ID of the review comment to reply to"), - body: type.string.describe("the reply text explaining how the feedback was addressed"), + body: type.string.describe( + "extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'" + ), }); export const ReplyToReviewCommentTool = tool({ name: "reply_to_review_comment", description: - "Reply to a PR review comment thread explaining how the feedback was addressed. Use this after addressing each review comment to provide specific context about the changes made.", + "Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).", parameters: ReplyToReviewComment, execute: contextualize(async ({ pull_number, comment_id, body }, ctx) => { const bodyWithFooter = addFooter(body, ctx.payload); diff --git a/mcp/review.ts b/mcp/review.ts index 4a4436a..e4ee0e2 100644 --- a/mcp/review.ts +++ b/mcp/review.ts @@ -1,12 +1,14 @@ import type { RestEndpointMethodTypes } from "@octokit/rest"; import { type } from "arktype"; +import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts"; +import { deleteProgressComment } from "./comment.ts"; import { contextualize, tool } from "./shared.ts"; export const Review = type({ pull_number: type.number.describe("The pull request number to review"), body: type.string .describe( - "Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array." + "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." ) .optional(), commit_id: type.string @@ -23,14 +25,16 @@ export const Review = type({ "Side of the diff: LEFT (old code) or RIGHT (new code). Defaults to RIGHT if not provided." ) .optional(), - body: type.string.describe("The comment text for this specific line"), + body: type.string.describe( + "The comment text for this specific line. For issues appearing multiple times, comment on the first occurrence and reference others." + ), start_line: type.number .describe("Start line for multi-line comments (optional, for commenting on ranges)") .optional(), }) .array() .describe( - "REQUIRED: Array of inline comments for specific code issues. Use this for all location-specific feedback. Use 'git diff origin/...origin/' to find the correct line numbers (typically use the line numbers shown on the RIGHT side for new code, LEFT side for old code)." + "PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/...origin/' to find correct line numbers (RIGHT side for new code, LEFT for old)." ) .optional(), }); @@ -38,19 +42,19 @@ export const Review = type({ export const ReviewTool = tool({ name: "submit_pull_request_review", description: - "Submit a review (approve, request changes, or comment) for an existing pull request. " + - "IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. " + - "Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.", + "Submit a review for an existing pull request. " + + "IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. " + + "Only use 'body' for a 1-2 sentence summary with urgency and critical callouts.", parameters: Review, execute: contextualize(async ({ pull_number, body, commit_id, comments = [] }, ctx) => { - // Get the PR to determine the head commit if commit_id not provided + // get the PR to determine the head commit if commit_id not provided const pr = await ctx.octokit.rest.pulls.get({ owner: ctx.owner, repo: ctx.name, pull_number, }); - // Compose the request + // compose the request const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] = { owner: ctx.owner, repo: ctx.name, @@ -65,7 +69,7 @@ export const ReviewTool = tool({ } if (comments.length > 0) { type ReviewComment = (typeof params.comments & {})[number]; - // Convert comments to the format expected by GitHub API + // convert comments to the format expected by GitHub API params.comments = comments.map((comment) => { const reviewComment: ReviewComment = { ...comment, @@ -79,9 +83,33 @@ export const ReviewTool = tool({ }); } const result = await ctx.octokit.rest.pulls.createReview(params); + const reviewId = result.data.id; + + // build quick links footer and update the review body + const apiUrl = process.env.API_URL || "https://pullfrog.com"; + const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix&review_id=${reviewId}`; + const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`; + + const footer = buildPullfrogFooter({ + customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`], + }); + + const updatedBody = (body || "") + footer; + + // update the review with the footer + await ctx.octokit.rest.pulls.updateReview({ + owner: ctx.owner, + repo: ctx.name, + pull_number, + review_id: reviewId, + body: updatedBody, + }); + + await deleteProgressComment(); + return { success: true, - reviewId: result.data.id, + reviewId, html_url: result.data.html_url, state: result.data.state, user: result.data.user?.login, diff --git a/modes.ts b/modes.ts index 856deae..bd9203d 100644 --- a/modes.ts +++ b/modes.ts @@ -64,14 +64,19 @@ export function getModes({ disableProgressComment }: GetModesParams): Mode[] { 4. Make the necessary code changes to address the feedback. Work through each review comment systematically. -5. After addressing each review comment, use ${ghPullfrogMcpName}/reply_to_review_comment to reply directly to that comment thread explaining what change was made (keep replies concise, 1-2 sentences). +5. **CRITICAL: Reply to EACH review comment individually.** After fixing each comment, use ${ghPullfrogMcpName}/reply_to_review_comment to reply directly to that comment thread. Keep replies extremely brief (1 sentence max, e.g., "Fixed by renaming to X" or "Added null check"). -6. Test your changes to ensure they work correctly.${disableProgressComment ? "" : `\n\n7. ${reportProgressInstruction}`} +6. Test your changes to ensure they work correctly. -8. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one. +7. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one. +${ + disableProgressComment + ? "" + : ` +8. ${reportProgressInstruction} -9. Call report_progress one final time ONLY if you haven't already included a complete summary in a previous report_progress call. If you already called report_progress with complete information, you do NOT need to call it again. Only make a final call if you need to add missing information. **IMPORTANT**: Do NOT overwrite a good comment with details with a generic message. -`, +**CRITICAL: Keep the progress comment extremely brief.** The summary should be 1-2 sentences max (e.g., "Fixed 3 review comments and pushed changes."). Almost all detail belongs in the individual reply_to_review_comment calls, NOT in the progress comment.` +}`, }, { name: "Review", @@ -82,11 +87,16 @@ export function getModes({ disableProgressComment }: GetModesParams): Mode[] { 2. View diff: git diff origin/...origin/ (use line numbers from this for inline comments, replace and with 'base' and 'head' from PR info) -3. Read files from the checked-out PR branch to understand the implementation${disableProgressComment ? "" : `\n\n4. ${reportProgressInstruction}`} +3. Read files from the checked-out PR branch to understand the implementation -5. When submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff +4. Submit review using ${ghPullfrogMcpName}/submit_pull_request_review -6. Only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location`, +**CRITICAL: Prioritize per-line feedback over summary text.** +- ALL specific feedback MUST go in the 'comments' array with file paths and line numbers from the diff +- for issues appearing in multiple places, comment on the FIRST occurrence and reference others (e.g., "also at lines X, Y" or "similar issue in otherFile.ts:42") +- the 'body' field is ONLY for: (1) a 1-2 sentence high-level overview, (2) urgency level (e.g., "minor suggestions" vs "blocking issues"), (3) critical security callouts (e.g., API key exposure) +- 95%+ of review content should be in per-line comments; the body should be just a couple sentences +- the review body will include quick action links for addressing feedback, so keep it concise`, }, { name: "Plan", diff --git a/package.json b/package.json index 1e8b379..afd8b5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/action", - "version": "0.0.131", + "version": "0.0.132", "type": "module", "files": [ "index.js", diff --git a/utils/buildPullfrogFooter.ts b/utils/buildPullfrogFooter.ts new file mode 100644 index 0000000..d8cef95 --- /dev/null +++ b/utils/buildPullfrogFooter.ts @@ -0,0 +1,71 @@ +export const PULLFROG_DIVIDER = ""; + +const FROG_LOGO = `Pullfrog`; + +export interface AgentInfo { + displayName: string; + url: string; +} + +export interface WorkflowRunInfo { + owner: string; + repo: string; + runId: string; +} + +export interface BuildPullfrogFooterParams { + /** add "Triggered by Pullfrog" link */ + triggeredBy?: boolean; + /** add "Using [agent](url)" link */ + agent?: AgentInfo | undefined; + /** add "View workflow run" link */ + workflowRun?: WorkflowRunInfo | undefined; + /** arbitrary custom parts (e.g., action links) */ + customParts?: string[]; +} + +/** + * build a pullfrog footer with configurable parts + * always includes: frog logo at start, pullfrog.com link and X link at end + */ +export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string { + const parts: string[] = []; + + if (params.triggeredBy) { + parts.push("Triggered by [Pullfrog](https://pullfrog.com)"); + } + + if (params.agent) { + parts.push(`Using [${params.agent.displayName}](${params.agent.url})`); + } + + if (params.workflowRun) { + const { owner, repo, runId } = params.workflowRun; + parts.push(`[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})`); + } + + if (params.customParts) { + parts.push(...params.customParts); + } + + const allParts = [ + ...parts, + "[pullfrog.com](https://pullfrog.com)", + "[𝕏](https://x.com/pullfrogai)", + ]; + + return ` +${PULLFROG_DIVIDER} +${FROG_LOGO}  | ${allParts.join(" | ")}`; +} + +/** + * strip any existing pullfrog footer from a comment body + */ +export function stripExistingFooter(body: string): string { + const dividerIndex = body.indexOf(PULLFROG_DIVIDER); + if (dividerIndex === -1) { + return body; + } + return body.substring(0, dividerIndex).trimEnd(); +}