Refactor to toolState

This commit is contained in:
Colin McDonnell
2025-12-16 20:41:10 -08:00
parent 956245962e
commit 4db8e28bf7
7 changed files with 49 additions and 38 deletions
+13 -4
View File
@@ -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<MainResult> {
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}...`);
+3 -1
View File
@@ -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) {
}),
});
}
+3 -1
View File
@@ -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;
+3
View File
@@ -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,
+3
View File
@@ -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,
+3
View File
@@ -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");
+21 -32
View File
@@ -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<AddPullRequestReviewThreadResponse>(
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<SubmitPullRequestReviewResponse>(
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,