From 4a8c432a48b9cd669f6b1a89f369c2014e332ce0 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 12 Mar 2026 05:32:17 +0000 Subject: [PATCH] add Summarize mode for updatable PR summary comments (#470) * add Summarize mode for updatable PR summary comments Introduces a Summarize mode that manages a single summary comment per PR, updated in place on subsequent pushes. Mirrors the Plan/PlanEdit pattern: API endpoint for existing-comment lookup at select_mode time, node ID tracking on WorkflowRun, and SummaryUpdate guidance for edits. Also fixes summary format instructions: Before/After uses inline
to avoid double line breaks, metadata line placed after key changes, SHA-256 anchor instructions strengthened against fabrication. Made-with: Cursor * fix pre-existing lint error in checkout.ts Made-with: Cursor * fix dead restricted param in deepenForBeforeSha GitAuthOptions dropped the restricted field in the ASKPASS refactor (#478) but deepenForBeforeSha (#471) still passed it. Remove the field and the now-unused shell param from DeepenForBeforeShaParams. Made-with: Cursor --- entry | 149 ++++++++++++++++++++++++++++++++++++++++++---- mcp/checkout.ts | 48 +++++++++++++++ mcp/comment.ts | 33 ++++++---- mcp/git.ts | 11 ++++ mcp/selectMode.ts | 74 ++++++++++++++++++++++- mcp/server.ts | 2 + modes.ts | 16 +++++ post | 4 +- 8 files changed, 309 insertions(+), 28 deletions(-) diff --git a/entry b/entry index 8cd21dd..607c243 100755 --- a/entry +++ b/entry @@ -144705,6 +144705,33 @@ async function checkoutPrBranch(pullNumber, params) { forkUrl: isFork ? `https://github.com/${headRepo.full_name}.git` : void 0 }; } +function deepenForBeforeSha(params) { + const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true"; + if (!isShallow) return; + const maxIterations = 10; + for (let i = 0; i < maxIterations; i++) { + try { + $("git", ["cat-file", "-t", params.beforeSha], { log: false }); + log.debug(`\xBB before_sha ${params.beforeSha.slice(0, 7)} is now reachable`); + return; + } catch { + } + log.debug( + `\xBB deepening by 50 to reach before_sha ${params.beforeSha.slice(0, 7)} (attempt ${i + 1}/${maxIterations})` + ); + try { + $git("fetch", ["--deepen=50", "--no-tags", "origin"], { + token: params.gitToken + }); + } catch { + log.debug(`\xBB deepen for before_sha failed (force-push may have rewritten history)`); + return; + } + } + log.debug( + `\xBB before_sha ${params.beforeSha.slice(0, 7)} not reachable after ${maxIterations * 50} commits` + ); +} function CheckoutPrTool(ctx) { return tool({ name: "checkout_pr", @@ -144720,6 +144747,13 @@ function CheckoutPrTool(ctx) { shell: ctx.payload.shell, postCheckoutScript: ctx.postCheckoutScript }); + const event = ctx.payload.event; + if ("before_sha" in event && event.before_sha) { + deepenForBeforeSha({ + gitToken: ctx.gitToken, + beforeSha: event.before_sha + }); + } const pr = await ctx.octokit.rest.pulls.get({ owner: ctx.repo.owner, repo: ctx.repo.name, @@ -144975,7 +145009,7 @@ function fixDoubleEscapedString(str) { } // mcp/comment.ts -async function updatePlanCommentId(ctx, planCommentNodeId) { +async function updateCommentNodeId(ctx, field, nodeId) { if (ctx.runId === void 0 || !ctx.apiToken) return; try { await retry( @@ -144987,7 +145021,7 @@ async function updatePlanCommentId(ctx, planCommentNodeId) { authorization: `Bearer ${ctx.apiToken}`, "content-type": "application/json" }, - body: JSON.stringify({ planCommentNodeId }), + body: JSON.stringify({ [field]: nodeId }), signal: AbortSignal.timeout(1e4) }); if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`); @@ -144995,11 +145029,11 @@ async function updatePlanCommentId(ctx, planCommentNodeId) { { maxAttempts: 3, delayMs: 2e3, - label: "updatePlanCommentId" + label: `updateCommentNodeId(${field})` } ); } catch (error49) { - log.warning(`updatePlanCommentId exhausted retries: ${error49}`); + log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error49}`); } } async function buildCommentFooter(params) { @@ -145038,14 +145072,14 @@ async function addFooter(ctx, body) { var Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), body: type.string.describe("the comment body content"), - type: type.enumerated("Plan", "Comment").describe( - "Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)." + type: type.enumerated("Plan", "Summary", "Comment").describe( + "Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)." ).optional() }); function CreateCommentTool(ctx) { return tool({ name: "create_issue_comment", - description: "Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.", + description: "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.", parameters: Comment, execute: execute(async ({ issueNumber, body, type: commentType }) => { const bodyWithFooter = await addFooter(ctx, body); @@ -145056,7 +145090,10 @@ function CreateCommentTool(ctx) { body: bodyWithFooter }); if (commentType === "Plan" && result.data.node_id) { - await updatePlanCommentId(ctx, result.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id); + } + if (commentType === "Summary" && result.data.node_id) { + await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id); } return { success: true, @@ -145128,7 +145165,7 @@ async function reportProgress(ctx, params) { }); ctx.toolState.wasUpdated = true; if (isPlanMode && result2.data.node_id) { - await updatePlanCommentId(ctx, result2.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", result2.data.node_id); } return { commentId: result2.data.id, @@ -145154,7 +145191,7 @@ async function reportProgress(ctx, params) { }); ctx.toolState.wasUpdated = true; if (isPlanMode && result2.data.node_id) { - await updatePlanCommentId(ctx, result2.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", result2.data.node_id); } return { commentId: result2.data.id, @@ -145195,7 +145232,7 @@ async function reportProgress(ctx, params) { body: bodyWithPlanLink }); if (updateResult.data.node_id) { - await updatePlanCommentId(ctx, updateResult.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id); } return { commentId: updateResult.data.id, @@ -146219,6 +146256,7 @@ var NOSHELL_BLOCKED_SUBCOMMANDS = { bisect: "Blocked: git bisect run can execute arbitrary shell commands." }; var NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"]; +var COLLAPSE_THRESHOLD = 200; var subcommandPattern = regex("^[a-z][a-z0-9-]*$"); var Git = type({ subcommand: type(subcommandPattern).describe("Git subcommand (e.g., 'status', 'log', 'diff')"), @@ -146253,6 +146291,14 @@ function GitTool(ctx) { } } const output = $("git", [subcommand, ...args2], { log: false }); + const lineCount = output.split("\n").length; + if (lineCount > COLLAPSE_THRESHOLD) { + log.group(`git ${subcommand} output (${lineCount} lines)`, () => { + log.info(output); + }); + } else if (output) { + log.info(output); + } return { success: true, output }; }) }); @@ -147410,7 +147456,7 @@ function ResolveReviewThreadTool(ctx) { // mcp/selectMode.ts var SelectModeParams = type({ mode: type.string.describe( - "the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')" + "the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')" ), "issue_number?": type("number").describe( "optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)" @@ -147577,7 +147623,37 @@ An existing plan comment was found for this issue. Update that comment with the 3. Finalize: - call \`${ghPullfrogMcpName}/report_progress\` with results - if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\` - - if the task involved labeling, commenting, or other GitHub operations, perform those directly` + - if the task involved labeling, commenting, or other GitHub operations, perform those directly`, + Summarize: `### Checklist + +1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`. +2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt: + - the diff file path + - PR metadata (title, file count, commit count, base/head branches) + - format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing + - instruct it to use the TOC to selectively read relevant diff sections, not the entire file + - instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\` +3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body. + +### Effort + +Use mini or auto effort.`, + SummaryUpdate: `### Checklist (updating existing summary) + +An existing summary comment was found for this PR. Update it rather than creating a new one. + +1. Use \`previousSummaryBody\` from this response as the current summary to revise. +2. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`. +3. Delegate a subagent with: + - the diff file path and PR metadata + - the existing summary body (\`previousSummaryBody\`) so it can update rather than rewrite from scratch + - format instructions from EVENT INSTRUCTIONS (if any) + - instruct it to produce an updated summary reflecting the current state of the PR and return via \`${ghPullfrogMcpName}/set_output\` +4. After the subagent completes, call \`${ghPullfrogMcpName}/edit_issue_comment\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body. + +### Effort + +Use mini or auto effort.` }; var modeInstructionParent = { IncrementalReview: "Review", @@ -147609,6 +147685,21 @@ async function fetchExistingPlanComment(ctx, issueNumber) { return null; } } +async function fetchExistingSummaryComment(ctx, prNumber) { + if (!ctx.apiToken) return null; + try { + const response = await apiFetch({ + path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`, + method: "GET", + headers: { authorization: `Bearer ${ctx.apiToken}` }, + signal: AbortSignal.timeout(1e4) + }); + const data = await response.json(); + return response.ok && "commentId" in data ? data : null; + } catch { + return null; + } +} function SelectModeTool(ctx) { return tool({ name: "select_mode", @@ -147651,6 +147742,23 @@ function SelectModeTool(ctx) { } } } + if (selectedMode.name === "Summarize") { + const prNumber = ctx.payload.event.issue_number; + if (prNumber !== void 0) { + const existing = await fetchExistingSummaryComment(ctx, prNumber); + if (existing !== null) { + ctx.toolState.existingSummaryCommentId = existing.commentId; + return { + ...buildOrchestratorGuidance(selectedMode, { + ...guidanceOpts, + overrideGuidance: modeGuidance.SummaryUpdate + }), + existingSummaryCommentId: existing.commentId, + previousSummaryBody: existing.body + }; + } + } + } return buildOrchestratorGuidance(selectedMode, guidanceOpts); }) }); @@ -148426,6 +148534,21 @@ Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in 5. **PROGRESS** - ${reportProgressInstruction} Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.` + }, + { + name: "Summarize", + description: "Summarize a PR with a structured comment that is updated in place on subsequent pushes", + prompt: `Follow these steps. + +1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number to get PR metadata and diffPath. + +2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections \u2014 do not read the entire file unless the PR is small. + +3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing. + +4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body. + +${permalinkTip}` } ]; } diff --git a/mcp/checkout.ts b/mcp/checkout.ts index d8a61e3..3fc37ba 100644 --- a/mcp/checkout.ts +++ b/mcp/checkout.ts @@ -334,6 +334,44 @@ export async function checkoutPrBranch( }; } +type DeepenForBeforeShaParams = { + gitToken: string; + beforeSha: string; +}; + +function deepenForBeforeSha(params: DeepenForBeforeShaParams): void { + const isShallow = + $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true"; + if (!isShallow) return; + + const maxIterations = 10; + for (let i = 0; i < maxIterations; i++) { + try { + $("git", ["cat-file", "-t", params.beforeSha], { log: false }); + log.debug(`» before_sha ${params.beforeSha.slice(0, 7)} is now reachable`); + return; + } catch { + // not reachable yet, deepen + } + + log.debug( + `» deepening by 50 to reach before_sha ${params.beforeSha.slice(0, 7)} (attempt ${i + 1}/${maxIterations})` + ); + try { + $git("fetch", ["--deepen=50", "--no-tags", "origin"], { + token: params.gitToken, + }); + } catch { + log.debug(`» deepen for before_sha failed (force-push may have rewritten history)`); + return; + } + } + + log.debug( + `» before_sha ${params.beforeSha.slice(0, 7)} not reachable after ${maxIterations * 50} commits` + ); +} + export function CheckoutPrTool(ctx: ToolContext) { return tool({ name: "checkout_pr", @@ -352,6 +390,16 @@ export function CheckoutPrTool(ctx: ToolContext) { postCheckoutScript: ctx.postCheckoutScript, }); + // for incremental review/rereview: deepen the clone to include before_sha + // so `git diff before_sha...HEAD` works without the agent needing to fetch manually + const event = ctx.payload.event; + if ("before_sha" in event && event.before_sha) { + deepenForBeforeSha({ + gitToken: ctx.gitToken, + beforeSha: event.before_sha, + }); + } + const pr = await ctx.octokit.rest.pulls.get({ owner: ctx.repo.owner, repo: ctx.repo.name, diff --git a/mcp/comment.ts b/mcp/comment.ts index f39231b..808ca31 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -9,8 +9,14 @@ import { retry } from "../utils/retry.ts"; import type { ToolContext } from "./server.ts"; import { execute, tool } from "./shared.ts"; -/** PATCH workflow-run with plan comment node_id so plan revisions can update that comment in place. */ -async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string): Promise { +type CommentNodeIdField = "planCommentNodeId" | "summaryCommentNodeId"; + +/** PATCH workflow-run with a comment node_id so future runs can update that comment in place. */ +export async function updateCommentNodeId( + ctx: ToolContext, + field: CommentNodeIdField, + nodeId: string +): Promise { if (ctx.runId === undefined || !ctx.apiToken) return; try { await retry( @@ -22,7 +28,7 @@ async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string): authorization: `Bearer ${ctx.apiToken}`, "content-type": "application/json", }, - body: JSON.stringify({ planCommentNodeId }), + body: JSON.stringify({ [field]: nodeId }), signal: AbortSignal.timeout(10_000), }); if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`); @@ -30,11 +36,11 @@ async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string): { maxAttempts: 3, delayMs: 2000, - label: "updatePlanCommentId", + label: `updateCommentNodeId(${field})`, } ); } catch (error) { - log.warning(`updatePlanCommentId exhausted retries: ${error}`); + log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error}`); } } @@ -107,9 +113,9 @@ export const Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), body: type.string.describe("the comment body content"), type: type - .enumerated("Plan", "Comment") + .enumerated("Plan", "Summary", "Comment") .describe( - "Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)." + "Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)." ) .optional(), }); @@ -118,7 +124,7 @@ export function CreateCommentTool(ctx: ToolContext) { return tool({ name: "create_issue_comment", description: - "Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.", + "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.", parameters: Comment, execute: execute(async ({ issueNumber, body, type: commentType }) => { const bodyWithFooter = await addFooter(ctx, body); @@ -131,7 +137,10 @@ export function CreateCommentTool(ctx: ToolContext) { }); if (commentType === "Plan" && result.data.node_id) { - await updatePlanCommentId(ctx, result.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id); + } + if (commentType === "Summary" && result.data.node_id) { + await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id); } return { @@ -241,7 +250,7 @@ export async function reportProgress( ctx.toolState.wasUpdated = true; if (isPlanMode && result.data.node_id) { - await updatePlanCommentId(ctx, result.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id); } return { @@ -278,7 +287,7 @@ export async function reportProgress( ctx.toolState.wasUpdated = true; if (isPlanMode && result.data.node_id) { - await updatePlanCommentId(ctx, result.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id); } return { @@ -336,7 +345,7 @@ export async function reportProgress( }); if (updateResult.data.node_id) { - await updatePlanCommentId(ctx, updateResult.data.node_id); + await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id); } return { diff --git a/mcp/git.ts b/mcp/git.ts index bf4e62f..5f69eb4 100644 --- a/mcp/git.ts +++ b/mcp/git.ts @@ -218,6 +218,8 @@ const NOSHELL_BLOCKED_SUBCOMMANDS: Record = { // (avoids false positives like --exclude matching --exec) const NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"]; +const COLLAPSE_THRESHOLD = 200; + // SECURITY: subcommand must match [a-z][a-z0-9-]* to reject flags passed as the subcommand. // this blocks injection of global git options like -c, -C, --exec-path, --config-env, etc. // @@ -269,6 +271,15 @@ export function GitTool(ctx: ToolContext) { } const output = $("git", [subcommand, ...args], { log: false }); + const lineCount = output.split("\n").length; + if (lineCount > COLLAPSE_THRESHOLD) { + log.group(`git ${subcommand} output (${lineCount} lines)`, () => { + log.info(output); + }); + } else if (output) { + log.info(output); + } + return { success: true, output }; }), }); diff --git a/mcp/selectMode.ts b/mcp/selectMode.ts index 33fceef..87cee1d 100644 --- a/mcp/selectMode.ts +++ b/mcp/selectMode.ts @@ -7,7 +7,7 @@ import { execute, tool } from "./shared.ts"; export const SelectModeParams = type({ mode: type.string.describe( - "the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')" + "the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')" ), "issue_number?": type("number").describe( "optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)" @@ -185,6 +185,38 @@ An existing plan comment was found for this issue. Update that comment with the - call \`${ghPullfrogMcpName}/report_progress\` with results - if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\` - if the task involved labeling, commenting, or other GitHub operations, perform those directly`, + + Summarize: `### Checklist + +1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`. +2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt: + - the diff file path + - PR metadata (title, file count, commit count, base/head branches) + - format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing + - instruct it to use the TOC to selectively read relevant diff sections, not the entire file + - instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\` +3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body. + +### Effort + +Use mini or auto effort.`, + + SummaryUpdate: `### Checklist (updating existing summary) + +An existing summary comment was found for this PR. Update it rather than creating a new one. + +1. Use \`previousSummaryBody\` from this response as the current summary to revise. +2. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`. +3. Delegate a subagent with: + - the diff file path and PR metadata + - the existing summary body (\`previousSummaryBody\`) so it can update rather than rewrite from scratch + - format instructions from EVENT INSTRUCTIONS (if any) + - instruct it to produce an updated summary reflecting the current state of the PR and return via \`${ghPullfrogMcpName}/set_output\` +4. After the subagent completes, call \`${ghPullfrogMcpName}/edit_issue_comment\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body. + +### Effort + +Use mini or auto effort.`, }; type OrchestratorGuidance = { @@ -218,6 +250,9 @@ function buildOrchestratorGuidance(mode: Mode, opts: BuildGuidanceOpts = {}): Or // matches the API response for /repo/[owner]/[repo]/issue/[issueNumber]/plan-comment export type PlanCommentResponsePayload = { error: string } | { commentId: number; body: string }; +// matches the API response for /repo/[owner]/[repo]/pr/[prNumber]/summary-comment +export type SummaryCommentResponsePayload = { error: string } | { commentId: number; body: string }; + async function fetchExistingPlanComment( ctx: ToolContext, issueNumber: number @@ -237,6 +272,25 @@ async function fetchExistingPlanComment( } } +async function fetchExistingSummaryComment( + ctx: ToolContext, + prNumber: number +): Promise | null> { + if (!ctx.apiToken) return null; + try { + const response = await apiFetch({ + path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`, + method: "GET", + headers: { authorization: `Bearer ${ctx.apiToken}` }, + signal: AbortSignal.timeout(10_000), + }); + const data = (await response.json()) as SummaryCommentResponsePayload; + return response.ok && "commentId" in data ? data : null; + } catch { + return null; + } +} + export function SelectModeTool(ctx: ToolContext) { return tool({ name: "select_mode", @@ -287,6 +341,24 @@ export function SelectModeTool(ctx: ToolContext) { } } + if (selectedMode.name === "Summarize") { + const prNumber = ctx.payload.event.issue_number; + if (prNumber !== undefined) { + const existing = await fetchExistingSummaryComment(ctx, prNumber); + if (existing !== null) { + ctx.toolState.existingSummaryCommentId = existing.commentId; + return { + ...buildOrchestratorGuidance(selectedMode, { + ...guidanceOpts, + overrideGuidance: modeGuidance.SummaryUpdate, + }), + existingSummaryCommentId: existing.commentId, + previousSummaryBody: existing.body, + }; + } + } + } + return buildOrchestratorGuidance(selectedMode, guidanceOpts); }), }); diff --git a/mcp/server.ts b/mcp/server.ts index 97c7568..37d5c4a 100644 --- a/mcp/server.ts +++ b/mcp/server.ts @@ -85,6 +85,8 @@ export interface ToolState { // set by select_mode when Plan + issue_number and plan-comment API returns existing plan (for report_progress target_plan_comment) existingPlanCommentId?: number; previousPlanBody?: string; + // set by select_mode when Summarize mode and summary-comment API returns existing summary + existingSummaryCommentId?: number; output?: string; usageEntries: AgentUsage[]; } diff --git a/modes.ts b/modes.ts index 8e6ec59..fcc2c34 100644 --- a/modes.ts +++ b/modes.ts @@ -312,6 +312,22 @@ Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`, }, + { + name: "Summarize", + description: + "Summarize a PR with a structured comment that is updated in place on subsequent pushes", + prompt: `Follow these steps. + +1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number to get PR metadata and diffPath. + +2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections — do not read the entire file unless the PR is small. + +3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing. + +4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body. + +${permalinkTip}`, + }, ]; } diff --git a/post b/post index 103f210..fd3e5c4 100755 --- a/post +++ b/post @@ -41256,8 +41256,8 @@ var LEAPING_INTO_ACTION_PREFIX = "Leaping into action"; var Comment = type({ issueNumber: type.number.describe("the issue number to comment on"), body: type.string.describe("the comment body content"), - type: type.enumerated("Plan", "Comment").describe( - "Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)." + type: type.enumerated("Plan", "Summary", "Comment").describe( + "Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)." ).optional() }); var EditComment = type({