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:
committed by
pullfrog[bot]
parent
6d25adfd1a
commit
4a8c432a48
@@ -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}`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user