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 <br/>
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
This commit is contained in:
Colin McDonnell
2026-03-12 05:32:17 +00:00
committed by pullfrog[bot]
parent 6d25adfd1a
commit 4a8c432a48
8 changed files with 309 additions and 28 deletions
+21 -12
View File
@@ -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<void> {
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<void> {
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 {