e4e93ea6d3
* PR summary as agent-edited tmpfile snapshot Replaces the comment-based PR summary path (and the in-progress update_pr_summary tool from #534) with a snapshot file the agent edits in place during Review / IncrementalReview / pr-summary Task runs. The server seeds the tmpfile with the previous snapshot (incremental) or a stable scaffold (first run), exposes the path via select_mode, and reads it back at end-of-run to persist to WorkflowRun.summarySnapshot and (when the prSummaryComment toggle is on) splice into the PR description body. Why a tmpfile rather than a tool call: incremental snapshot edits are output-token-cheap when the agent uses native file-editing tools, and range-diff cleanly across runs because section headings are stable. The agent never has to regurgitate the full snapshot to update it. Gating: snapshot generation is opt-in via either prSummaryComment="enabled" (splice into PR body) or prReReview="enabled" (snapshot feeds future incremental review runs as context). Users who disable both pay nothing end-to-end — no seeding, DB write, or body splice. Behavior changes: - Drop the Summarize mode and the Summary comment type entirely; the rolling summary is no longer a separate run shape. - pull_request_synchronize with re-review off and summary on still dispatches a silent pr-summary Task, but it edits the snapshot file instead of posting a fresh comment. - /api/repo/.../pr/.../summary-comment now returns { snapshot: string | null } from the DB instead of fetching a comment via GraphQL. URL kept stable so deployed older actions degrade gracefully. - summaryCommentNodeId is retained on WorkflowRun for legacy data and a future backfill of pre-snapshot comment-based summaries. Supersedes #534. The commit-tool/sub-agent direction in that PR is abandoned in favor of this file-based shape. * address review pass #1: synchronize fallback, splice idempotency, docs * address review pass #2: in-flight skip should not race summary fallback * address review pass #3: signal-handler flush, doc clarifications * address review pass #4: in-flight persist promise + bounded body-splice timeout * address review pass #5: defensive catch on persist worker, doc nit * add summary-stale post-run gate When generateSummary is set, we capture the bytes of the seeded snapshot file and pass them to the agent's post-run loop alongside the file path. After each agent attempt, the loop diffs the current file against the seed; if they're byte-identical the agent never touched it, and we nudge once via a resume turn (similar to the dirty-tree gate, but soft and fire-once so smaller models that legitimately decide no edit is warranted don't burn the retry budget). Mostly defends against forgetful smaller models on the Review path — their mode prompt asks them to edit the snapshot file, but the multi-step instruction can fall through when the diff is large. * trigger: retry vercel preview build * fix(action): drop unused re-export that pulled node:fs/promises into next bundle action/internal/index.ts was re-exporting DEFAULT_PR_SUMMARY_INSTRUCTIONS from action/utils/prSummary.ts, but nothing in the next.js app imports it. prSummary.ts uses node:fs/promises, and pullfrog/internal is aliased into the next bundle by next.config.ts, which made turbopack try to resolve node:fs/promises in client chunks and fail with: the chunking context (unknown) does not support external modules (request: node:fs/promises) drop the re-export — selectMode.ts (the only real consumer) already imports it directly from action/utils/prSummary.ts. * firewall PR summary snapshot from user instructions; resurrect rich format for Review The agent-internal snapshot (the markdown file the agent edits in place across runs) is exclusively durable context for future agent runs — user-supplied summarization instructions warp it and degrade that context. Drop the prSummaryCommentInstructions read path end-to-end: - handleWebhook: stop reading prSummaryCommentInstructions, stop passing prSummaryInstructions through dispatch options - action payload + ToolState + selectMode addendum: drop the instructions appendix; the snapshot prompt is fixed, not user-shaped - TriggersSettings: drop the InstructionsEditor for prSummaryCommentInstructions - prSummary.ts: reframe DEFAULT_PR_SUMMARY_INSTRUCTIONS as agent-targeted (durable context, not human-facing prose) Prisma columns (prSummaryComment, prSummaryCommentInstructions) and the matching zod schema entry stay for graceful retreat. Separately, resurrect PR_SUMMARY_FORMAT (deleted along with the Summarize mode in the original PR) and wire it into Review mode only. Initial PR reviews now include a structured summary section in the review body using the rich format (TL;DR, key changes, ## sections with before/after, file-link trails). IncrementalReview keeps its existing terser bullet-list shape since re-review bodies are deltas, not introductions. The user-facing review summary and the agent-internal snapshot are deliberately separate artifacts with separate prompts and zero shared content. * address review comments: prompt self-consistency + stale-doc cleanup PR 568 self-review (4232488109) flagged a self-contradiction the firewall commit introduced and three stale doc references that survived. - action/modes.ts: Review-mode step 2's trivial-PR shortcut said `submit "Reviewed — no issues found." per step 5`, but step 5's rewrite removed exactly that preamble. Aligned both: trivial PRs and no-actionable-issues PRs now produce a body that opens with "No new issues found." followed by the PR summary, so the user gets the headline up front and still sees what was reviewed. - docs/pr-reviews.mdx: dropped the "customize the summary style with Summary instructions in the console" sentence (the editor was removed in the firewall commit). Replaced with a note that the snapshot uses Pullfrog's built-in format and is not user-customizable. - wiki/prompt.md, wiki/modes.md: rewrote the snapshot-prompt entries to reflect the firewall — DEFAULT_PR_SUMMARY_INSTRUCTIONS is the entire prompt, prSummaryCommentInstructions is no longer wired in. * drop orphaned prSummaryCommentInstructions column Prod audit (455 repos): 5 non-null rows on a single account, all containing the literal placeholder text from the InstructionsEditor we removed in the firewall commit. No account has an intentional preference set, so silent-ignore (the keep-for-retreat option) costs us nothing meaningful while leaving an orphan column in the schema. Drop it. - prisma/schema.prisma: remove the column - prisma/migrations/20260506000000_drop_pr_summary_comment_instructions: ALTER TABLE ... DROP COLUMN - utils/schemas/triggers.ts: drop the matching zod entry * drop body splicing; snapshot is internal-only User-visible PR summarization continues to ship in Review and IncrementalReview review bodies (which already render PR_SUMMARY_FORMAT and "Reviewed changes" respectively). The snapshot tmpfile is now purely durable cross-run agent context — seed, edit-in-place, save to DB, feed the next run. Massive simplification: the body splice mechanics, the two-toggle gating matrix, the summaryHandlingCovered race tracking, and the synchronize summary-only Task fallback all go away. Code: - prSummary.ts: drop splice/strip/marker code (`splicePrSummary`, `stripExistingSummaryBlock`, `buildSummaryBlock`, `extractPrSummary`, PULLFROG_SUMMARY_START/END). keep scaffold, instructions, seed/read. - main.ts: rename persistAndPostSummary -> persistSummary; collapse to a single DB PATCH. drop pulls.get/pulls.update, drop AbortSignal timeout, drop in-flight promise machinery, drop prSummaryToBody plumbing. - ToolState: add summarySeed (replaces local var in main.ts so persist can compare). drop prSummaryToBody and summaryPersistInFlight. - persistSummary now compares against the seed and skips the DB write with a warning when unchanged — saving the seed verbatim is either a no-op or persists the placeholder scaffold, neither useful. - postRun.ts: when summary-stale is the only failing gate and the resume turn itself fails, restore the pre-resume successful result and break. symmetric with the existing reflection-failure preservation. summary-stale can no longer flip a successful run to failed. Webhook: - pull_request_opened: generateSummary follows prReReview only (the snapshot has no consumer when re-review is off). - pull_request_synchronize: collapses to "if prReReview enabled, dispatch IncrementalReview". the summaryHandlingCovered flag, the same-SHA/in-flight coordination it was protecting, and the summary-only Task fallback all delete cleanly. UI / config: - drop SummarizePRsTrigger (the toggle gated body splice; with that gone it has no behavior). drop sidebar entry, console import, Text icon import. - drop prSummaryComment from triggers zod schema, prisma schema, preview settings script. Migration: squash the two existing migrations into one timestamped 20260507000000_pr_summary_snapshot covering all three column changes (add summarySnapshot on workflow_runs, drop prSummaryCommentInstructions and prSummaryComment on repos). repo convention is one migration per PR. Action: bump 0.0.203 -> 0.0.205 (payload contract changed: prSummaryToBody removed; main is at 0.0.204). Out-of-diff cleanup: - review.ts:190 + review.test.ts:651 — "Reviewed — no issues found." -> "No new issues found." to match the canonical body in modes.ts. Verified: pnpm typecheck clean, pnpm lint clean, postRun + review tests pass, dev DB reset against production and the squashed migration applied cleanly (summarySnapshot present, prSummaryComment / prSummaryCommentInstructions both gone). * re-orient snapshot toward functional summary; drop prior-review-feedback section Empirical audit on preview-568 PR #5 showed the snapshot IS load-bearing for the orchestrator: lens-dispatch prompts on incremental runs carried forward context from the snapshot's risk register (e.g. "the JSDoc explicitly scopes to code points — do not flag grapheme-cluster issues" on the surrogate-pair fix run, "consistency with native padStart" on the padStart-added run). The orchestrator was reading the snapshot, reasoning about it, and using it to anti-prime / focus subagents — exactly the high-leverage path. My earlier "snapshot is write-only" claim was wrong. The shape, however, was steering it toward review-history-log instead of functional summary. This commit re-orients: - prSummary.ts: replace the four-section scaffold (~580 chars of placeholder italics under "What this PR does / Key changes / Risk / Reviewed in prior runs") with a minimal seed (~150 chars: just a header + a one-line comment about what the file is for). different PRs warrant different organization; forcing a refactor and a feature into the same template is procrustean. minimal seed also makes the unchanged-from-seed gate in persistSummary more sensitive. - selectMode.ts addendum: rewrite around three principles. (1) the snapshot is a FUNCTIONAL summary of what the PR does and the risks it carries, not a chronological review log — commit history can already be reconstructed from list_pull_request_reviews. (2) the orchestrator should USE the snapshot during triage and dispatch — concrete example given of carrying snapshot context into subagent lens prompts. (3) structure is the agent's call; stable headings make snapshots range-diff cleanly when they fit, but riff when they don't. - modes.ts IncrementalReview: drop the "Prior review feedback" checklist from the user-facing review body (step 6b gone, step 7 ELSE IFs cleaned up). It duplicated content that's already covered by the Reviewed-changes bullets and tracked durably in the snapshot for the next agent run; in the user-facing body it was noise. step 3 still fetches prior reviews but its role is now just filtering aggregation in step 5, not rendering. - AGENTS.md: codify "no follow-ups" rule. when an issue is identified during code review, fix it in this PR — PR scope does not constrain quality. follow-up TODOs are forbidden as a substitute for doing the work now. Empirical evidence supporting the re-orientation: - Run 25568912293 (PR#5 incr1, surrogate-pair fix): orchestrator's correctness lens dispatch said "Do NOT flag grapheme-cluster issues — the JSDoc scopes to code points." The grapheme-cluster framing was not in the diff; it was downstream of the snapshot's prior risk-section framing of truncate's contract. Snapshot influencing dispatch. - Run 25569054779 (PR#5 incr2, padStart added): orchestrator's correctness lens dispatch enumerated edge cases including "consistency with native String.prototype.padStart contract" and "fill = multi-code-point string (e.g. emoji)". Both threads carried over from the snapshot's prior truncate code-point-vs-code-unit discussion. Snapshot informing the shape of what was looked for. The cost of maintaining the snapshot (~800 tokens, ~$0.005/run) is trivially affordable when it materially improves orchestrator triage on the 1-5 lenses dispatched per review.
425 lines
15 KiB
TypeScript
425 lines
15 KiB
TypeScript
import { type } from "arktype";
|
|
import { getApiUrl } from "../utils/apiUrl.ts";
|
|
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
|
import { log } from "../utils/cli.ts";
|
|
import { fixDoubleEscapedString } from "../utils/fixDoubleEscapedString.ts";
|
|
import { patchWorkflowRunFields } from "../utils/patchWorkflowRunFields.ts";
|
|
import {
|
|
createLeapingProgressComment,
|
|
deleteProgressCommentApi,
|
|
updateProgressComment,
|
|
} from "../utils/progressComment.ts";
|
|
import type { ToolContext } from "./server.ts";
|
|
import { execute, tool } from "./shared.ts";
|
|
|
|
// re-export for backward compat with anything importing the leaping helpers from mcp/comment
|
|
export {
|
|
isLeapingIntoActionCommentBody,
|
|
LEAPING_INTO_ACTION_PREFIX,
|
|
} from "../utils/leapingComment.ts";
|
|
|
|
function buildCommentFooter(ctx: ToolContext, customParts?: string[]): string {
|
|
const runId = ctx.runId;
|
|
return buildPullfrogFooter({
|
|
triggeredBy: true,
|
|
workflowRun:
|
|
runId !== undefined
|
|
? {
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
runId,
|
|
jobId: ctx.jobId,
|
|
}
|
|
: undefined,
|
|
customParts,
|
|
model: ctx.toolState.model,
|
|
});
|
|
}
|
|
|
|
function buildImplementPlanLink(ctx: ToolContext, issueNumber: number, commentId: number): string {
|
|
const apiUrl = getApiUrl();
|
|
return `[Implement plan ➔](${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=implement&comment_id=${commentId})`;
|
|
}
|
|
|
|
export function addFooter(ctx: ToolContext, body: string): string {
|
|
if (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
|
throw new Error(
|
|
"body contains <br/> followed by a non-blank line, which breaks GitHub markdown rendering. always add a blank line after <br/> tags."
|
|
);
|
|
}
|
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
const footer = buildCommentFooter(ctx);
|
|
return `${bodyWithoutFooter}${footer}`;
|
|
}
|
|
|
|
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")
|
|
.describe("Plan: record as the plan for this run. Comment: regular comment (default).")
|
|
.optional(),
|
|
});
|
|
|
|
export function CreateCommentTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "create_issue_comment",
|
|
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.",
|
|
parameters: Comment,
|
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
|
const bodyWithFooter = addFooter(ctx, body);
|
|
|
|
const result = await ctx.octokit.rest.issues.createComment({
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
issue_number: issueNumber,
|
|
body: bodyWithFooter,
|
|
});
|
|
|
|
ctx.toolState.wasUpdated = true;
|
|
|
|
if (commentType === "Plan") {
|
|
if (result.data.node_id) {
|
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
|
}
|
|
// add "Implement plan" link (needs comment ID, so create-then-update)
|
|
const customParts = [buildImplementPlanLink(ctx, issueNumber, result.data.id)];
|
|
const footer = buildCommentFooter(ctx, customParts);
|
|
const bodyWithPlanLink = `${stripExistingFooter(body)}${footer}`;
|
|
|
|
const updateResult = await ctx.octokit.rest.issues.updateComment({
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
comment_id: result.data.id,
|
|
body: bodyWithPlanLink,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
commentId: updateResult.data.id,
|
|
url: updateResult.data.html_url,
|
|
body: updateResult.data.body,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
commentId: result.data.id,
|
|
url: result.data.html_url,
|
|
body: result.data.body,
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
export const EditComment = type({
|
|
commentId: type.number.describe("the ID of the comment to edit"),
|
|
body: type.string.describe("the new comment body content"),
|
|
});
|
|
|
|
export function EditCommentTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "edit_issue_comment",
|
|
description: "Edit a GitHub issue comment by its ID",
|
|
parameters: EditComment,
|
|
execute: execute(async ({ commentId, body }) => {
|
|
const bodyWithFooter = addFooter(ctx, body);
|
|
|
|
const result = await ctx.octokit.rest.issues.updateComment({
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
comment_id: commentId,
|
|
body: bodyWithFooter,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
commentId: result.data.id,
|
|
url: result.data.html_url,
|
|
body: result.data.body,
|
|
updatedAt: result.data.updated_at,
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
export const ReportProgress = type({
|
|
body: type.string.describe("the progress update content to share"),
|
|
"target_plan_comment?": type("boolean").describe(
|
|
"when true, update the existing plan comment (from select_mode lookup) instead of the progress comment; use when editing an existing plan"
|
|
),
|
|
});
|
|
|
|
/**
|
|
* Report progress to a GitHub comment.
|
|
*
|
|
* progressComment has three states:
|
|
* - undefined: no comment yet — will create one if an issue/PR target exists
|
|
* - object: active comment — will update it in place via the right REST endpoint for its type
|
|
* - null: deliberately deleted (e.g. after submitting a PR review) — skips silently
|
|
*
|
|
* The body is always tracked in lastProgressBody for the job summary regardless of comment state.
|
|
*
|
|
* The "existing plan comment" path always targets a top-level issue comment (plan comments are
|
|
* created by create_issue_comment with type:"Plan", never as review-thread replies).
|
|
*/
|
|
export async function reportProgress(
|
|
ctx: ToolContext,
|
|
params: { body: string; target_plan_comment?: boolean }
|
|
): Promise<{
|
|
commentId?: number;
|
|
url?: string;
|
|
body: string;
|
|
action: "created" | "updated" | "skipped";
|
|
}> {
|
|
const { body, target_plan_comment } = params;
|
|
// always track the body for job summary
|
|
ctx.toolState.lastProgressBody = body;
|
|
|
|
// silent events (e.g., auto-label, pr-summary Task) should never create or update progress comments.
|
|
// the body is still tracked above for the GitHub Actions job summary.
|
|
if (ctx.payload.event.silent) {
|
|
return { body, action: "skipped" };
|
|
}
|
|
|
|
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
|
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
|
const apiCtx = { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name };
|
|
|
|
// when editing existing plan: update the plan comment from tool state (set by select_mode)
|
|
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === undefined) {
|
|
log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
|
|
}
|
|
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId !== undefined) {
|
|
const commentId = ctx.toolState.existingPlanCommentId;
|
|
const customParts =
|
|
issueNumber !== undefined ? [buildImplementPlanLink(ctx, issueNumber, commentId)] : undefined;
|
|
const bodyWithoutFooter = stripExistingFooter(body);
|
|
const footer = buildCommentFooter(ctx, customParts);
|
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
|
|
|
const result = await updateProgressComment(
|
|
apiCtx,
|
|
{ id: commentId, type: "issue" },
|
|
bodyWithFooter
|
|
);
|
|
|
|
ctx.toolState.wasUpdated = true;
|
|
|
|
if (isPlanMode && result.node_id) {
|
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.node_id });
|
|
}
|
|
|
|
return {
|
|
commentId: result.id,
|
|
url: result.html_url,
|
|
body: result.body || "",
|
|
action: "updated",
|
|
};
|
|
}
|
|
|
|
const existingComment = ctx.toolState.progressComment;
|
|
|
|
// if we already have a progress comment, update it
|
|
if (existingComment) {
|
|
const customParts =
|
|
isPlanMode && issueNumber !== undefined
|
|
? [buildImplementPlanLink(ctx, issueNumber, existingComment.id)]
|
|
: undefined;
|
|
|
|
const bodyWithoutFooter = stripExistingFooter(body);
|
|
const footer = buildCommentFooter(ctx, customParts);
|
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
|
|
|
const result = await updateProgressComment(apiCtx, existingComment, bodyWithFooter);
|
|
|
|
ctx.toolState.wasUpdated = true;
|
|
|
|
if (isPlanMode && result.node_id) {
|
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.node_id });
|
|
}
|
|
|
|
return {
|
|
commentId: result.id,
|
|
url: result.html_url,
|
|
body: result.body || "",
|
|
action: "updated",
|
|
};
|
|
}
|
|
|
|
// null = progress comment was deleted by stranded-comment cleanup in main.ts
|
|
if (existingComment === null) {
|
|
return { body, action: "skipped" };
|
|
}
|
|
|
|
// no existing comment - need an issue/PR to create one on
|
|
// use fallback chain: dynamically set context > event payload
|
|
if (issueNumber === undefined) {
|
|
// no-op: no comment target (e.g., workflow_dispatch events)
|
|
// body is already tracked for job summary
|
|
return { body, action: "skipped" };
|
|
}
|
|
|
|
// for new comments, we need to create first, then update with Plan link if in Plan mode
|
|
// self-created progress comments are always top-level issue comments — review-reply
|
|
// progress comments only originate from the dispatch path and arrive pre-created.
|
|
const initialBody = addFooter(ctx, body);
|
|
const created = await createLeapingProgressComment(
|
|
apiCtx,
|
|
{ kind: "issue", issueNumber },
|
|
initialBody
|
|
);
|
|
|
|
ctx.toolState.progressComment = created.comment;
|
|
ctx.toolState.wasUpdated = true;
|
|
|
|
// if Plan mode, update the comment to add the "Implement plan" link
|
|
if (isPlanMode) {
|
|
const customParts = [buildImplementPlanLink(ctx, issueNumber, created.comment.id)];
|
|
const bodyWithoutFooter = stripExistingFooter(body);
|
|
const footer = buildCommentFooter(ctx, customParts);
|
|
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
|
|
|
|
const updateResult = await updateProgressComment(apiCtx, created.comment, bodyWithPlanLink);
|
|
|
|
if (updateResult.node_id) {
|
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: updateResult.node_id });
|
|
}
|
|
|
|
return {
|
|
commentId: updateResult.id,
|
|
url: updateResult.html_url,
|
|
body: updateResult.body || "",
|
|
action: "created",
|
|
};
|
|
}
|
|
|
|
return {
|
|
commentId: created.comment.id,
|
|
url: created.html_url,
|
|
body: created.body || "",
|
|
action: "created",
|
|
};
|
|
}
|
|
|
|
export function ReportProgressTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "report_progress",
|
|
description:
|
|
"Share progress on the associated GitHub issue/PR. The first call creates a comment; subsequent calls update it in place. Call this at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. The current task list is automatically appended in a collapsible section — do not restate individual steps.",
|
|
parameters: ReportProgress,
|
|
execute: execute(async (params) => {
|
|
let body = params.body;
|
|
|
|
// for non-plan calls: stop auto-updates, wait for in-flight writes to settle,
|
|
// then append completed task list collapsible
|
|
if (!params.target_plan_comment && ctx.toolState.todoTracker) {
|
|
ctx.toolState.todoTracker.cancel();
|
|
await ctx.toolState.todoTracker.settled();
|
|
const collapsible = ctx.toolState.todoTracker.renderCollapsible({
|
|
completeInProgress: true,
|
|
});
|
|
if (collapsible) {
|
|
body = `${body}\n\n${collapsible}`;
|
|
}
|
|
}
|
|
|
|
const reportParams: { body: string; target_plan_comment?: boolean } = { body };
|
|
if (params.target_plan_comment !== undefined) {
|
|
reportParams.target_plan_comment = params.target_plan_comment;
|
|
}
|
|
const result = await reportProgress(ctx, reportParams);
|
|
|
|
if (result.action === "skipped") {
|
|
return {
|
|
success: true,
|
|
message:
|
|
"progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)",
|
|
};
|
|
}
|
|
|
|
if (!params.target_plan_comment) {
|
|
ctx.toolState.finalSummaryWritten = true;
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
...result,
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete the progress comment if it exists.
|
|
* Used by main.ts for stranded-comment cleanup (orphaned "Leaping into action" or
|
|
* checklist left by the todo tracker when the agent didn't call report_progress).
|
|
* Sets progressComment to null so subsequent report_progress calls are no-ops.
|
|
*/
|
|
export async function deleteProgressComment(ctx: ToolContext): Promise<boolean> {
|
|
const existing = ctx.toolState.progressComment;
|
|
if (!existing) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await deleteProgressCommentApi(
|
|
{ octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name },
|
|
existing
|
|
);
|
|
} catch (error) {
|
|
// ignore 404 - comment already deleted
|
|
if (error instanceof Error && error.message.includes("Not Found")) {
|
|
// comment already deleted, continue
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// set to null (not undefined) so report_progress skips instead of creating a new comment
|
|
ctx.toolState.progressComment = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
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(
|
|
"extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'"
|
|
),
|
|
});
|
|
|
|
export function ReplyToReviewCommentTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "reply_to_review_comment",
|
|
description:
|
|
"Reply to a PR review comment thread (NOT issue comments — this only works for inline review comments on PR diffs). Call this for EACH comment you address in AddressReviews mode. Keep replies extremely brief (1 sentence max).",
|
|
parameters: ReplyToReviewComment,
|
|
execute: execute(async ({ pull_number, comment_id, body }) => {
|
|
const bodyWithFooter = addFooter(ctx, body);
|
|
|
|
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
|
owner: ctx.repo.owner,
|
|
repo: ctx.repo.name,
|
|
pull_number,
|
|
comment_id,
|
|
body: bodyWithFooter,
|
|
});
|
|
|
|
// mark progress as updated so error reporting + run-result handling know
|
|
// a substantive write happened (used by reportErrorToComment / handleAgentResult)
|
|
ctx.toolState.wasUpdated = true;
|
|
|
|
return {
|
|
success: true,
|
|
commentId: result.data.id,
|
|
url: result.data.html_url,
|
|
body: result.data.body,
|
|
in_reply_to_id: result.data.in_reply_to_id,
|
|
};
|
|
}, "reply_to_review_comment"),
|
|
});
|
|
}
|