Refactor to toolState
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user