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 = ``;
+ }
+});
+
// 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}
- \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}
- | 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 = ``;
+
+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();
+}