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}`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+21
-12
@@ -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 {
|
||||
|
||||
+11
@@ -218,6 +218,8 @@ const NOSHELL_BLOCKED_SUBCOMMANDS: Record<string, string> = {
|
||||
// (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 };
|
||||
}),
|
||||
});
|
||||
|
||||
+73
-1
@@ -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<Extract<SummaryCommentResponsePayload, { commentId: number }> | 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);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user