diff --git a/main.ts b/main.ts index 0d0e856..30e0c1a 100644 --- a/main.ts +++ b/main.ts @@ -11,7 +11,6 @@ import type { AgentName, Payload } from "./external.ts"; import { agentsManifest } from "./external.ts"; import { ensureProgressCommentUpdated, reportProgress } from "./mcp/comment.ts"; import { createMcpConfigs } from "./mcp/config.ts"; -import type { ReviewState } from "./mcp/review.ts"; import { startMcpHttpServer } from "./mcp/server.ts"; import { getModes, type Mode, modes } from "./modes.ts"; import packageJson from "./package.json" with { type: "json" }; @@ -64,6 +63,7 @@ export async function main(inputs: Inputs): Promise { const partialCtx = await initializeContext(inputs, payload); const ctx = partialCtx as Context; + ctx.toolState = {}; timer.checkpoint("initializeContext"); await setupGit(ctx); @@ -244,8 +244,17 @@ export interface Context { runId: string; jobId: string | undefined; - // iterative review state (set by start_review, cleared by submit_review) - reviewState: ReviewState | undefined; + // tool state - mutable scratchpad for tools + toolState: ToolState; +} + +export interface ToolState { + prNumber?: number; + issueNumber?: number; + review?: { + id: number; // REST API database ID (for fix URLs) + nodeId: string; // GraphQL node ID (for mutations) + }; } async function initializeContext( @@ -263,7 +272,7 @@ async function initializeContext( | "prepResults" | "runId" | "jobId" - | "reviewState" + | "toolState" > > { log.info(`🐸 Running pullfrog/action@${packageJson.version}...`); diff --git a/mcp/checkout.ts b/mcp/checkout.ts index e23ec63..5ae0113 100644 --- a/mcp/checkout.ts +++ b/mcp/checkout.ts @@ -88,6 +88,9 @@ export function CheckoutPrTool(ctx: Context) { $("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]); } + // set PR context + ctx.toolState.prNumber = pull_number; + return { success: true, number: pr.data.number, @@ -102,4 +105,3 @@ export function CheckoutPrTool(ctx: Context) { }), }); } - diff --git a/mcp/comment.ts b/mcp/comment.ts index 153db3f..3c6d927 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -204,7 +204,9 @@ export async function reportProgress( } // no existing comment - create one - const issueNumber = ctx.payload.event.issue_number; + // use fallback chain: dynamically set context > event payload + const issueNumber = + ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number; if (issueNumber === undefined) { // cannot create comment without issue_number (e.g., workflow_dispatch events) return undefined; diff --git a/mcp/issueComments.ts b/mcp/issueComments.ts index 9678a4a..fa6c4d7 100644 --- a/mcp/issueComments.ts +++ b/mcp/issueComments.ts @@ -13,6 +13,9 @@ export function GetIssueCommentsTool(ctx: Context) { "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.", parameters: GetIssueComments, execute: execute(ctx, async ({ issue_number }) => { + // set issue context + ctx.toolState.issueNumber = issue_number; + const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, { owner: ctx.owner, repo: ctx.name, diff --git a/mcp/issueEvents.ts b/mcp/issueEvents.ts index dcbcdd1..9afc930 100644 --- a/mcp/issueEvents.ts +++ b/mcp/issueEvents.ts @@ -13,6 +13,9 @@ export function GetIssueEventsTool(ctx: Context) { "Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.", parameters: GetIssueEvents, execute: execute(ctx, async ({ issue_number }) => { + // set issue context + ctx.toolState.issueNumber = issue_number; + const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, { owner: ctx.owner, repo: ctx.name, diff --git a/mcp/issueInfo.ts b/mcp/issueInfo.ts index 7535e57..656f990 100644 --- a/mcp/issueInfo.ts +++ b/mcp/issueInfo.ts @@ -20,6 +20,9 @@ export function IssueInfoTool(ctx: Context) { const data = issue.data; + // set issue context + ctx.toolState.issueNumber = issue_number; + const hints: string[] = []; if (data.comments > 0) { hints.push("use get_issue_comments to retrieve all comments for this issue"); diff --git a/mcp/review.ts b/mcp/review.ts index fa2fb00..e5d213b 100644 --- a/mcp/review.ts +++ b/mcp/review.ts @@ -84,15 +84,6 @@ type SubmitPullRequestReviewResponse = { }; }; -// review state stored in ctx -export interface ReviewState { - reviewId: string; // graphql node ID - reviewDatabaseId: number; // rest API ID - pullNumber: number; - scratchpadPath: string; - commentCount: number; -} - // start_review tool export const StartReview = type({ pull_number: type.number.describe("The pull request number to review"), @@ -106,9 +97,9 @@ export function StartReviewTool(ctx: Context) { parameters: StartReview, execute: execute(ctx, async ({ pull_number }) => { // check if review already started - if (ctx.reviewState) { + if (ctx.toolState.review) { throw new Error( - `Review session already started for PR #${ctx.reviewState.pullNumber}. Call submit_review first to finish it.` + `Review session already in progress. Call submit_review first to finish it.` ); } @@ -136,13 +127,11 @@ export function StartReviewTool(ctx: Context) { const scratchpadContent = `# Review ${scratchpadId}\n\n`; writeFileSync(scratchpadPath, scratchpadContent); - // store review state in ctx - ctx.reviewState = { - reviewId, - reviewDatabaseId, - pullNumber: pull_number, - scratchpadPath, - commentCount: 0, + // set PR context and review state + ctx.toolState.prNumber = pull_number; + ctx.toolState.review = { + nodeId: reviewId, + id: reviewDatabaseId, }; return { @@ -175,7 +164,7 @@ export function AddReviewCommentTool(ctx: Context) { parameters: AddReviewComment, execute: execute(ctx, async ({ path, line, body, side }) => { // check if review started - if (!ctx.reviewState) { + if (!ctx.toolState.review) { throw new Error("No review session started. Call start_review first."); } @@ -183,7 +172,7 @@ export function AddReviewCommentTool(ctx: Context) { await ctx.octokit.graphql( ADD_PULL_REQUEST_REVIEW_THREAD, { - pullRequestReviewId: ctx.reviewState.reviewId, + pullRequestReviewId: ctx.toolState.review.nodeId, path, line, body, @@ -191,11 +180,8 @@ export function AddReviewCommentTool(ctx: Context) { } ); - ctx.reviewState.commentCount++; - return { success: true, - commentCount: ctx.reviewState.commentCount, message: `Comment added to ${path}:${line}`, }; }), @@ -219,17 +205,19 @@ export function SubmitReviewTool(ctx: Context) { parameters: SubmitReview, execute: execute(ctx, async ({ body }) => { // check if review started - if (!ctx.reviewState) { + if (!ctx.toolState.review) { throw new Error("No review session started. Call start_review first."); } + if (ctx.toolState.prNumber === undefined) { + throw new Error("No PR context. Call checkout_pr or start_review first."); + } - const pullNumber = ctx.reviewState.pullNumber; - const reviewDatabaseId = ctx.reviewState.reviewDatabaseId; + const reviewId = ctx.toolState.review.id; // build quick links footer const apiUrl = process.env.API_URL || "https://pullfrog.com"; - const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pullNumber}?action=fix&review_id=${reviewDatabaseId}`; - const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pullNumber}?action=fix-approved&review_id=${reviewDatabaseId}`; + const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${ctx.toolState.prNumber}?action=fix&review_id=${reviewId}`; + const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${ctx.toolState.prNumber}?action=fix-approved&review_id=${reviewId}`; const footer = buildPullfrogFooter({ workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId }, @@ -242,17 +230,16 @@ export function SubmitReviewTool(ctx: Context) { const response = await ctx.octokit.graphql( SUBMIT_PULL_REQUEST_REVIEW, { - pullRequestReviewId: ctx.reviewState.reviewId, + pullRequestReviewId: ctx.toolState.review.nodeId, body: bodyWithFooter, event: "COMMENT", } ); const result = response.submitPullRequestReview.pullRequestReview; - const commentCount = ctx.reviewState.commentCount; // clear review state - ctx.reviewState = undefined; + delete ctx.toolState.review; // delete progress comment await deleteProgressComment(ctx); @@ -262,7 +249,6 @@ export function SubmitReviewTool(ctx: Context) { reviewId: result.databaseId, html_url: result.url, state: result.state, - commentCount, }; }), }); @@ -316,6 +302,9 @@ export function ReviewTool(ctx: Context) { "Only use 'body' for a 1-2 sentence summary with urgency and critical callouts.", parameters: Review, execute: execute(ctx, async ({ pull_number, body, commit_id, comments = [] }) => { + // set PR context + ctx.toolState.prNumber = pull_number; + // get the PR to determine the head commit if commit_id not provided const pr = await ctx.octokit.rest.pulls.get({ owner: ctx.owner,