Clean up actions and payloads (#98)

* Clean up actions and payloads

* Clean up action

* Cleanup
This commit is contained in:
Colin McDonnell
2026-01-16 07:16:25 +00:00
committed by pullfrog[bot]
parent 5c60791b34
commit 9e019d89d2
68 changed files with 28182 additions and 306308 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import { type ChildProcess, spawn } from "node:child_process";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const BashParams = type({
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const GetCheckSuiteLogs = type({
+10 -10
View File
@@ -2,9 +2,9 @@ import { writeFileSync } from "node:fs";
import { join } from "node:path";
import type { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import { log } from "../utils/cli.ts";
import { $ } from "../utils/shell.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
type PullFile = RestEndpointMethodTypes["pulls"]["listFiles"]["response"]["data"][number];
@@ -118,7 +118,7 @@ export async function checkoutPrBranch(
params: CheckoutPrBranchParams
): Promise<CheckoutPrBranchResult> {
const { octokit, owner, name, token, pullNumber } = params;
log.info(`🔀 checking out PR #${pullNumber}...`);
log.info(`» checking out PR #${pullNumber}...`);
// fetch PR metadata
const pr = await octokit.rest.pulls.get({
@@ -149,7 +149,7 @@ export async function checkoutPrBranch(
log.debug(`already on PR branch ${localBranch}, skipping checkout`);
} else {
// fetch base branch so origin/<base> exists for diff operations
log.debug(`📥 fetching base branch (${baseBranch})...`);
log.debug(`» fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
// checkout base branch first to avoid "refusing to fetch into current branch" error
@@ -157,18 +157,18 @@ export async function checkoutPrBranch(
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
log.debug(`🌿 fetching PR #${pullNumber} (${localBranch})...`);
log.debug(`» fetching PR #${pullNumber} (${localBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${localBranch}`]);
// checkout the branch
$("git", ["checkout", localBranch]);
log.debug(` checked out PR #${pullNumber}`);
log.debug(`» checked out PR #${pullNumber}`);
}
// ensure base branch is fetched (needed for diff operations)
// fetch if we skipped checkout (already on branch) - otherwise already fetched above
if (alreadyOnBranch) {
log.debug(`📥 fetching base branch (${baseBranch})...`);
log.debug(`» fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
}
@@ -182,23 +182,23 @@ export async function checkoutPrBranch(
// add fork as a named remote (suppress logging to avoid "error: remote already exists" spam)
try {
$("git", ["remote", "add", remoteName, forkUrl], { log: false });
log.debug(`📌 added remote '${remoteName}' for fork ${headRepo.full_name}`);
log.debug(`» added remote '${remoteName}' for fork ${headRepo.full_name}`);
} catch {
// remote already exists, update its URL
$("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
log.debug(`📌 updated remote '${remoteName}' for fork ${headRepo.full_name}`);
log.debug(`» updated remote '${remoteName}' for fork ${headRepo.full_name}`);
}
// set branch push config so `git push` knows where to push
$("git", ["config", `branch.${localBranch}.pushRemote`, remoteName]);
// set merge ref so git knows the remote branch name (may differ from local)
$("git", ["config", `branch.${localBranch}.merge`, `refs/heads/${headBranch}`]);
log.debug(`📌 configured branch '${localBranch}' to push to '${remoteName}/${headBranch}'`);
log.debug(`» configured branch '${localBranch}' to push to '${remoteName}/${headBranch}'`);
// warn if maintainer can't modify (push will likely fail)
if (!pr.data.maintainer_can_modify) {
log.warning(
`⚠️ fork PR has maintainer_can_modify=false - push operations will fail. ` +
`» fork PR has maintainer_can_modify=false - push operations will fail. ` +
`ask the PR author to enable "Allow edits from maintainers" or the fork may be owned by an organization.`
);
}
+37 -34
View File
@@ -1,16 +1,11 @@
import { type } from "arktype";
import type { Payload } from "../external.ts";
import { agentsManifest } from "../external.ts";
import type { ToolContext } from "../main.ts";
import { fetchWorkflowRunInfo } from "../utils/api.ts";
import type { Agent } from "../agents/index.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { writeSummary } from "../utils/cli.ts";
import {
createOctokit,
getGitHubInstallationToken,
type OctokitWithPlugins,
parseRepoContext,
} from "../utils/github.ts";
import { createOctokit, type OctokitWithPlugins, parseRepoContext } from "../utils/github.ts";
import { getGitHubInstallationToken } from "../utils/token.ts";
import { fetchWorkflowRunInfo } from "../utils/workflowRun.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
/**
@@ -21,22 +16,19 @@ import { execute, tool } from "./shared.ts";
export const LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
interface BuildCommentFooterParams {
payload: Payload;
agent: Agent | undefined;
octokit?: OctokitWithPlugins | undefined;
customParts?: string[] | undefined;
}
async function buildCommentFooter({
payload,
agent,
octokit,
customParts,
}: BuildCommentFooterParams): Promise<string> {
const repoContext = parseRepoContext();
const runId = process.env.GITHUB_RUN_ID;
const agentName = payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
let workflowRunHtmlUrl: string | undefined;
if (runId && octokit) {
try {
@@ -56,8 +48,8 @@ async function buildCommentFooter({
const footerParams = {
triggeredBy: true,
agent: {
displayName: agentInfo?.displayName || "Unknown agent",
url: agentInfo?.url || "https://pullfrog.com",
displayName: agent?.displayName || "Unknown agent",
url: agent?.url || "https://pullfrog.com",
},
workflowRun: runId
? {
@@ -88,13 +80,14 @@ function buildImplementPlanLink(
return `[Implement plan ➔](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`;
}
async function addFooter(
body: string,
payload: Payload,
octokit?: OctokitWithPlugins
): Promise<string> {
interface AddFooterCtx {
agent?: Agent | undefined;
octokit?: OctokitWithPlugins | undefined;
}
async function addFooter(ctx: AddFooterCtx, body: string): Promise<string> {
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({ payload, octokit });
const footer = await buildCommentFooter({ agent: ctx.agent, octokit: ctx.octokit });
return `${bodyWithoutFooter}${footer}`;
}
@@ -110,7 +103,7 @@ export function CreateCommentTool(ctx: ToolContext) {
"Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
parameters: Comment,
execute: execute(async ({ issueNumber, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
@@ -140,7 +133,7 @@ export function EditCommentTool(ctx: ToolContext) {
description: "Edit a GitHub issue comment by its ID",
parameters: EditComment,
execute: execute(async ({ commentId, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
@@ -218,8 +211,7 @@ export async function reportProgress(
| undefined
> {
const existingCommentId = getProgressCommentId();
const issueNumber =
ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number;
const issueNumber = ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.event.issue_number;
const isPlanMode = ctx.toolState.selectedMode === "Plan";
// if we already have a progress comment, update it
@@ -231,7 +223,7 @@ export async function reportProgress(
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
payload: ctx.payload,
agent: ctx.agent,
octokit: ctx.octokit,
customParts,
});
@@ -264,7 +256,7 @@ export async function reportProgress(
}
// for new comments, we need to create first, then update with Plan link if in Plan mode
const initialBody = await addFooter(body, ctx.payload, ctx.octokit);
const initialBody = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
@@ -282,7 +274,7 @@ export async function reportProgress(
const customParts = [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, result.data.id)];
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
payload: ctx.payload,
agent: ctx.agent,
octokit: ctx.octokit,
customParts,
});
@@ -382,6 +374,10 @@ export async function deleteProgressComment(ctx: ToolContext): Promise<boolean>
return true;
}
interface EnsureProgressCommentUpdatedParams {
disableProgressComment: boolean;
}
/**
* Ensure the progress comment is updated with a generic error message if it was never updated.
* This should be called after agent execution completes to handle cases where the agent
@@ -390,12 +386,19 @@ export async function deleteProgressComment(ctx: ToolContext): Promise<boolean>
* Works even if MCP context is not initialized (e.g., if error occurs before MCP server starts).
* Will fetch comment ID from database if not available in environment variable.
*/
export async function ensureProgressCommentUpdated(payload?: Payload): Promise<void> {
export async function ensureProgressCommentUpdated(
params: EnsureProgressCommentUpdatedParams
): Promise<void> {
// skip if comment was already updated during execution
if (progressComment.wasUpdated) {
return;
}
// skip if progress comments are disabled
if (params.disableProgressComment) {
return;
}
// try to get comment ID from env var first, then from database if needed
let existingCommentId = getProgressCommentId();
@@ -453,8 +456,8 @@ export async function ensureProgressCommentUpdated(payload?: Payload): Promise<v
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
// add footer if we have payload, otherwise use plain message
const body = payload ? await addFooter(errorMessage, payload, octokit) : errorMessage;
// add footer without agent info (we don't have context here)
const body = await addFooter({ octokit }, errorMessage);
await octokit.rest.issues.updateComment({
owner: repoContext.owner,
@@ -479,7 +482,7 @@ export function ReplyToReviewCommentTool(ctx: ToolContext) {
"Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
parameters: ReplyToReviewComment,
execute: execute(async ({ pull_number, comment_id, body }) => {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
owner: ctx.owner,
-19
View File
@@ -1,19 +0,0 @@
/**
* Simple MCP configuration helper for adding our minimal GitHub comment server
*/
import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { ghPullfrogMcpName } from "../external.ts";
export type McpName = typeof ghPullfrogMcpName;
export type McpConfigs = Record<McpName, McpHttpServerConfig>;
export function createMcpConfigs(mcpServerUrl: string): McpConfigs {
return {
[ghPullfrogMcpName]: {
type: "http",
url: mcpServerUrl,
},
};
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import type { PrepResult } from "../prep/index.ts";
import { runPrepPhase } from "../prep/index.ts";
import { execute, tool } from "./shared.ts";
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.ts";
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const Issue = type({
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const GetIssueComments = type({
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const GetIssueEvents = type({
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const IssueInfo = type({
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const AddLabelsParams = type({
+2 -6
View File
@@ -1,6 +1,5 @@
import { type } from "arktype";
import { agentsManifest } from "../external.ts";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts";
@@ -14,12 +13,9 @@ export const PullRequest = type({
});
function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
const agentName = ctx.payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
const footer = buildPullfrogFooter({
triggeredBy: true,
agent: agentInfo ? { displayName: agentInfo.displayName, url: agentInfo.url } : undefined,
agent: { displayName: ctx.agent.displayName, url: ctx.agent.url },
workflowRun: ctx.runId
? { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId }
: undefined,
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const PullRequestInfo = type({
+1 -1
View File
@@ -1,6 +1,6 @@
import type { RestEndpointMethodTypes } from "@octokit/rest";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts";
import { deleteProgressComment } from "./comment.ts";
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
// graphql query to fetch all review threads with comments and replies
+1 -1
View File
@@ -1,5 +1,5 @@
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
export const SelectMode = type({
+38 -4
View File
@@ -1,9 +1,44 @@
import "./arkConfig.ts";
import { createServer } from "node:net";
// this must be imported first
import type { Octokit } from "@octokit/rest";
import { FastMCP, type Tool } from "fastmcp";
import { ghPullfrogMcpName } from "../external.ts";
import type { ToolContext } from "../main.ts";
import type { Agent } from "../agents/index.ts";
import { ghPullfrogMcpName, type PayloadEvent } from "../external.ts";
import type { Mode } from "../modes.ts";
import type { PrepResult } from "../prep/index.ts";
export interface ToolState {
prNumber?: number;
issueNumber?: number;
selectedMode?: string;
review?: {
id: number;
nodeId: string;
};
dependencyInstallation?: {
status: "not_started" | "in_progress" | "completed" | "failed";
promise: Promise<PrepResult[]> | undefined;
results: PrepResult[] | undefined;
};
}
export interface ToolContext {
owner: string;
name: string;
repo: { default_branch: string; private: boolean };
githubInstallationToken: string;
octokit: Octokit;
agent: Agent;
event: PayloadEvent;
disableProgressComment: boolean;
modes: Mode[];
toolState: ToolState;
runId: string;
jobId: string | undefined;
}
import { BashTool } from "./bash.ts";
import { CheckoutPrTool } from "./checkout.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
import {
@@ -29,7 +64,6 @@ import { CreatePullRequestReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts";
import { BashTool } from "./bash.ts";
/**
* Find an available port starting from the given port
@@ -98,7 +132,7 @@ export async function startMcpHttpServer(
BashTool(ctx),
];
if (!ctx.payload.disableProgressComment) {
if (!ctx.disableProgressComment) {
tools.push(ReportProgressTool(ctx));
}
+1 -1
View File
@@ -1,8 +1,8 @@
import type { StandardSchemaV1 } from "@standard-schema/spec";
import { encode as toonEncode } from "@toon-format/toon";
import type { FastMCP, Tool } from "fastmcp";
import type { ToolContext } from "../main.ts";
import { formatJsonValue, log } from "../utils/cli.ts";
import type { ToolContext } from "./server.ts";
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => toolDef;