Files
shockbot/mcp/pr.ts
T
pullfrog[bot] 4c1413d925 fix(action): flip wasUpdated on substantive MCP write tools (#563) (#577)
* fix(action): flip wasUpdated on substantive MCP write tools (#563)

Review/Respond/etc. agents that submit a `create_pull_request_review`,
`create_issue_comment`, or `update_pull_request_body` and exit without
calling `report_progress` were being marked as workflow failures by the
strict completion check in handleAgentResult. Extend the set of tools
that flip toolState.wasUpdated so a substantive user-visible artifact
satisfies the check. The isReviewMode bypass is retained for
IncrementalReview's non-substantive path.

Flag is set BEFORE patchWorkflowRunFields / deleteProgressComment in
each tool so a best-effort cleanup failure does not undo the signal.

* fix(action): use finalSummaryWritten for stranded progress cleanup

The stranded-progress-comment cleanup at the end of main() previously
fired only when toolState.wasUpdated was false (or the tracker was the
last writer). With wasUpdated now set by additional MCP write tools
(create_issue_comment, update_pull_request_body), an agent that produced
a substantive artifact via one of those tools and skipped report_progress
would leave the placeholder "Leaping into action" comment intact — the
post-script then converted it into an error message on a successful run.

Key the cleanup off finalSummaryWritten instead. That flag is only set
when report_progress actually wrote the progress comment, so it cleanly
distinguishes "comment is finalized" from "agent did other work but
never touched the progress comment".

* refactor(mcp): extract markSubstantiveArtifact() helper

replaces 4 inline `ctx.toolState.wasUpdated = true` flips in CreateCommentTool, UpdatePullRequestBodyTool, and CreatePullRequestReviewTool with a single helper in mcp/server.ts. JSDoc on the helper documents the contract (call BEFORE downstream patch/cleanup; gates the strict completion check and stranded-comment cleanup) so future MCP write tool authors only need to grep for one symbol.

no behavioral change.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(mcp): only flip finalSummaryWritten after non-skipped write

Previously the flag was set unconditionally on any non-plan call,
including paths where reportProgress skipped (silent events, deleted
comment, no issue/PR target). The cleanup check in main.ts is
safeguarded by toolState.progressComment so the bug doesn't manifest
today, but aligning the flag with actual writes matches the wasUpdated
pattern and the design intent in the cleanup plan.

* refactor(mcp): inline markSubstantiveArtifact helper

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: David Blass <david@arktype.io>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 21:05:53 +00:00

118 lines
3.9 KiB
TypeScript

import { type } from "arktype";
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 { $ } from "../utils/shell.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const PullRequest = type({
title: type.string.describe("the title of the pull request"),
body: type.string.describe("the body content of the pull request"),
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
"draft?": type.boolean.describe(
"if true, create the pull request as a draft. use when the user explicitly asks for a draft PR."
),
});
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
const footer = buildPullfrogFooter({
triggeredBy: true,
workflowRun: ctx.runId
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
: undefined,
model: ctx.toolState.model,
});
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
return `${bodyWithoutFooter}${footer}`;
}
export const UpdatePullRequestBody = type({
pull_number: type.number.describe("the pull request number to update"),
body: type.string.describe("the new body content for the pull request"),
});
export function UpdatePullRequestBodyTool(ctx: ToolContext) {
return tool({
name: "update_pull_request_body",
description: "Update the body/description of an existing pull request",
parameters: UpdatePullRequestBody,
execute: execute(async (params) => {
const bodyWithFooter = buildPrBodyWithFooter(ctx, params.body);
const result = await ctx.octokit.rest.pulls.update({
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number: params.pull_number,
body: bodyWithFooter,
});
ctx.toolState.wasUpdated = true;
return {
success: true,
number: result.data.number,
url: result.data.html_url,
};
}),
});
}
export function CreatePullRequestTool(ctx: ToolContext) {
return tool({
name: "create_pull_request",
description: "Create a pull request from the current branch",
parameters: PullRequest,
execute: execute(async (params) => {
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
log.debug(`Current branch: ${currentBranch}`);
const bodyWithFooter = buildPrBodyWithFooter(ctx, params.body);
const result = await ctx.octokit.rest.pulls.create({
owner: ctx.repo.owner,
repo: ctx.repo.name,
title: params.title,
body: bodyWithFooter,
head: currentBranch,
base: params.base,
draft: params.draft ?? false,
});
// best-effort: request review from the user who triggered the workflow
const reviewer = ctx.payload.triggerer;
if (reviewer) {
try {
log.debug(`requesting review from ${reviewer} on PR #${result.data.number}`);
await ctx.octokit.rest.pulls.requestReviewers({
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number: result.data.number,
reviewers: [reviewer],
});
} catch {
log.info(`failed to request review from ${reviewer} on PR #${result.data.number}`);
}
}
if (typeof result.data.node_id === "string" && result.data.node_id.length > 0) {
await patchWorkflowRunFields(ctx, {
prNodeId: result.data.node_id,
});
}
return {
success: true,
pullRequestId: result.data.id,
number: result.data.number,
url: result.data.html_url,
title: result.data.title,
head: result.data.head.ref,
base: result.data.base.ref,
};
}),
});
}