overhaul git setup

This commit is contained in:
Colin McDonnell
2025-12-16 18:00:22 -08:00
parent f6ac916e22
commit 1bff21f7fb
19 changed files with 514 additions and 195 deletions
+2 -2
View File
@@ -14,11 +14,11 @@ export const claude = agent({
executablePath: "cli.js",
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
// Ensure API key is NOT in process.env - only pass via SDK's env option
delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions({ payload, prepResults });
const prompt = addInstructions({ payload, prepResults, repo });
console.log(prompt);
// configure sandbox mode if enabled
+2 -2
View File
@@ -20,7 +20,7 @@ export const codex = agent({
executablePath: "bin/codex.js",
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
// create config directory for codex before setting HOME
const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "codex");
@@ -61,7 +61,7 @@ export const codex = agent({
);
try {
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults }));
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults, repo }));
let finalOutput = "";
for await (const event of streamedTurn.events) {
+2 -2
View File
@@ -91,7 +91,7 @@ export const cursor = agent({
executableName: "cursor-agent",
});
},
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => {
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults, repo }) => {
configureCursorMcpServers({ mcpServers, cliPath });
configureCursorSandbox({ sandbox: payload.sandbox ?? false });
@@ -166,7 +166,7 @@ export const cursor = agent({
};
try {
const fullPrompt = addInstructions({ payload, prepResults });
const fullPrompt = addInstructions({ payload, prepResults, repo });
// configure sandbox mode if enabled
// in sandbox mode: remove --force flag and rely on cli-config.json sandbox settings
+2 -2
View File
@@ -154,14 +154,14 @@ export const gemini = agent({
...(githubInstallationToken && { githubInstallationToken }),
});
},
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => {
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults, repo }) => {
configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
}
const sessionPrompt = addInstructions({ payload, prepResults });
const sessionPrompt = addInstructions({ payload, prepResults, repo });
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
// configure sandbox mode if enabled
+73 -12
View File
@@ -1,3 +1,4 @@
import { execSync } from "node:child_process";
import { encode as toonEncode } from "@toon-format/toon";
import type { Payload } from "../external.ts";
import { ghPullfrogMcpName } from "../external.ts";
@@ -62,19 +63,74 @@ function formatPrepResults(results: PrepResult[]): string {
return "";
}
return `************* ENVIRONMENT SETUP *************
return lines.join("\n");
}
${lines.join("\n")}
interface RepoInfo {
owner: string;
name: string;
defaultBranch: string;
}
`;
interface BuildRuntimeContextParams {
repo: RepoInfo;
prepResults: PrepResult[];
}
/**
* Build runtime context string with git status, repo data, and GitHub Actions variables
*/
function buildRuntimeContext({ repo, prepResults }: BuildRuntimeContextParams): string {
const lines: string[] = [];
// working directory
lines.push(`working_directory: ${process.cwd()}`);
// git status (try to get it, but don't fail if git isn't available)
try {
const gitStatus = execSync("git status --short", { encoding: "utf-8", stdio: "pipe" }).trim();
lines.push(`git_status: ${gitStatus || "(clean)"}`);
} catch {
// git not available or not in a repo
}
// repo data
lines.push(`repo: ${repo.owner}/${repo.name}`);
lines.push(`default_branch: ${repo.defaultBranch}`);
// GitHub Actions variables (when running in CI)
const ghVars: Record<string, string | undefined> = {
github_event_name: process.env.GITHUB_EVENT_NAME,
github_ref: process.env.GITHUB_REF,
github_sha: process.env.GITHUB_SHA?.slice(0, 7),
github_actor: process.env.GITHUB_ACTOR,
github_run_id: process.env.GITHUB_RUN_ID,
github_workflow: process.env.GITHUB_WORKFLOW,
};
for (const [key, value] of Object.entries(ghVars)) {
if (value) {
lines.push(`${key}: ${value}`);
}
}
// environment setup (dependency installation results)
const envSetup = formatPrepResults(prepResults);
if (envSetup) {
lines.push("");
lines.push("environment_setup:");
lines.push(envSetup);
}
return lines.join("\n");
}
interface AddInstructionsParams {
payload: Payload;
prepResults: PrepResult[];
repo: RepoInfo;
}
export const addInstructions = ({ payload, prepResults }: AddInstructionsParams) => {
export const addInstructions = ({ payload, prepResults, repo }: AddInstructionsParams) => {
let encodedEvent = "";
const eventKeys = Object.keys(payload.event);
@@ -86,7 +142,7 @@ export const addInstructions = ({ payload, prepResults }: AddInstructionsParams)
encodedEvent = toonEncode(payload.event);
}
const envSetup = formatPrepResults(prepResults);
const runtimeContext = buildRuntimeContext({ repo, prepResults });
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || undefined;
return `
@@ -163,14 +219,21 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author.
**Available git MCP tools**:
- \`${ghPullfrogMcpName}/checkout_pr\` - Checkout an existing PR branch locally (handles fork PRs automatically)
- \`${ghPullfrogMcpName}/create_branch\` - Create a new branch from a base branch
- \`${ghPullfrogMcpName}/commit_files\` - Stage and commit files with proper authentication
- \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote repository
- \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote (automatically uses correct remote for fork PRs)
- \`${ghPullfrogMcpName}/create_pull_request\` - Create a PR from the current branch
**Workflow for making code changes**:
1. Use file operations to create/modify files
2. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
**Workflow for working on an existing PR**:
1. Use \`${ghPullfrogMcpName}/checkout_pr\` to checkout the PR branch
2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push (automatically pushes to fork for fork PRs)
**Workflow for creating new changes**:
1. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch
5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR
@@ -221,7 +284,5 @@ ${encodedEvent}`
************* RUNTIME CONTEXT *************
working_directory: ${process.cwd()}
${envSetup}`;
${runtimeContext}`;
};
+2 -2
View File
@@ -304,7 +304,7 @@ export const opencode = agent({
installDependencies: true,
});
},
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults }) => {
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults, repo }) => {
// 1. configure home/config directory
const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "opencode");
@@ -313,7 +313,7 @@ export const opencode = agent({
configureOpenCodeMcpServers({ mcpServers });
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
const prompt = addInstructions({ payload, prepResults });
const prompt = addInstructions({ payload, prepResults, repo });
const args = ["run", "--format", "json", prompt];
if (payload.sandbox) {
+10
View File
@@ -21,6 +21,15 @@ export interface AgentResult {
metadata?: Record<string, unknown>;
}
/**
* Repo info for agent context
*/
export interface RepoInfo {
owner: string;
name: string;
defaultBranch: string;
}
/**
* Configuration for agent creation
*/
@@ -31,6 +40,7 @@ export interface AgentConfig {
mcpServers: Record<string, McpHttpServerConfig>;
cliPath: string;
prepResults: PrepResult[];
repo: RepoInfo;
}
/**
+202 -86
View File
@@ -80489,6 +80489,9 @@ function formatIndentedField(label, content) {
return formatted;
}
// agents/instructions.ts
import { execSync } from "node:child_process";
// ../node_modules/.pnpm/@toon-format+toon@1.0.0/node_modules/@toon-format/toon/dist/index.js
var LIST_ITEM_MARKER = "-";
var LIST_ITEM_PREFIX = "- ";
@@ -89416,10 +89419,11 @@ function getModes({
name: "Address Reviews",
description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically fetches and checks out the PR branch)
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and configures push settings (including for fork PRs).
2. Review the feedback provided. Understand each review comment and what changes are being requested.
- **EVENT DATA may contain review comment details**: If available, \`approved_comments\` are comments to address, \`unapproved_comments\` are for context only. The \`triggerer\` field indicates who initiated this action - prioritize their replies when deciding how to implement fixes.
- You can use ${ghPullfrogMcpName}/get_pull_request to get PR metadata if needed.
3. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists.
@@ -89429,7 +89433,7 @@ function getModes({
6. Test your changes to ensure they work correctly.
7. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one.
7. When done, commit your changes with ${ghPullfrogMcpName}/commit_files, then push with ${ghPullfrogMcpName}/push_branch. The push will automatically go to the correct remote (including fork repos). Do not create a new branch or PR - you are updating an existing one.
${disableProgressComment ? "" : `
8. ${reportProgressInstruction}
@@ -89439,9 +89443,9 @@ ${disableProgressComment ? "" : `
name: "Review",
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and base branch, preparing the repo for review.
2. **IMPORTANT**: After calling ${ghPullfrogMcpName}/get_pull_request, the PR branch is already checked out locally. View diff using: \`git diff origin/<base>...HEAD\` (replace <base> with 'base' from PR info). Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch.
2. **IMPORTANT**: After calling checkout_pr, the PR branch is checked out locally. View diff using: \`git diff origin/<base>..HEAD\` (replace <base> with 'base' from checkout_pr result, e.g., \`git diff origin/main..HEAD\`). Use two dots (..) not three dots (...) for reliable diffs. Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch. This works for both same-repo and fork PRs.
3. Read files from the checked-out PR branch to understand the implementation. Always use **relative paths** from repo root (e.g., \`src/index.ts\`), never absolute paths.
@@ -89449,7 +89453,8 @@ ${disableProgressComment ? "" : `
**GENERAL GUIDANCE**
- *CRITICAL* \u2014\xA0Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
- Do not leave any comments that are not potentially actionable. Do not leave complimentary comments just to be nice.
- *CRITICAL* \u2014 Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
- For line numbers, use the NEW file line number from the diff (shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12)
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
- **CRITICAL: Prioritize per-line feedback over summary text.**
@@ -89459,7 +89464,6 @@ ${disableProgressComment ? "" : `
- 95%+ of review content should be in per-line comments; the body should be just a couple sentences
- The review body will include quick action links for addressing feedback, so keep it concise
- Do not nitpick unless instructed explicity to do so by the user's additional instructions. This includes: requesting documentation/docstrings/JSDoc.
- Do not leave any comments that are not potentially actionable.
- The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach:
- 1) conceptualize the changes made. make sure you understand it.
- 2) evaluate conceptual approach. leave feedback as needed.
@@ -89554,20 +89558,47 @@ function formatPrepResults(results) {
if (lines.length === 0) {
return "";
}
return `************* ENVIRONMENT SETUP *************
${lines.join("\n")}
`;
return lines.join("\n");
}
var addInstructions = ({ payload, prepResults }) => {
function buildRuntimeContext({ repo, prepResults }) {
const lines = [];
lines.push(`working_directory: ${process.cwd()}`);
try {
const gitStatus = execSync("git status --short", { encoding: "utf-8", stdio: "pipe" }).trim();
lines.push(`git_status: ${gitStatus || "(clean)"}`);
} catch {
}
lines.push(`repo: ${repo.owner}/${repo.name}`);
lines.push(`default_branch: ${repo.defaultBranch}`);
const ghVars = {
github_event_name: process.env.GITHUB_EVENT_NAME,
github_ref: process.env.GITHUB_REF,
github_sha: process.env.GITHUB_SHA?.slice(0, 7),
github_actor: process.env.GITHUB_ACTOR,
github_run_id: process.env.GITHUB_RUN_ID,
github_workflow: process.env.GITHUB_WORKFLOW
};
for (const [key, value2] of Object.entries(ghVars)) {
if (value2) {
lines.push(`${key}: ${value2}`);
}
}
const envSetup = formatPrepResults(prepResults);
if (envSetup) {
lines.push("");
lines.push("environment_setup:");
lines.push(envSetup);
}
return lines.join("\n");
}
var addInstructions = ({ payload, prepResults, repo }) => {
let encodedEvent = "";
const eventKeys = Object.keys(payload.event);
if (eventKeys.length === 1 && eventKeys[0] === "trigger") {
} else {
encodedEvent = encode(payload.event);
}
const envSetup = formatPrepResults(prepResults);
const runtimeContext = buildRuntimeContext({ repo, prepResults });
const dependenciesPreinstalled = prepResults.every((r) => r.dependenciesInstalled) || void 0;
return `
***********************************************
@@ -89643,14 +89674,21 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author.
**Available git MCP tools**:
- \`${ghPullfrogMcpName}/checkout_pr\` - Checkout an existing PR branch locally (handles fork PRs automatically)
- \`${ghPullfrogMcpName}/create_branch\` - Create a new branch from a base branch
- \`${ghPullfrogMcpName}/commit_files\` - Stage and commit files with proper authentication
- \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote repository
- \`${ghPullfrogMcpName}/push_branch\` - Push a branch to the remote (automatically uses correct remote for fork PRs)
- \`${ghPullfrogMcpName}/create_pull_request\` - Create a PR from the current branch
**Workflow for making code changes**:
1. Use file operations to create/modify files
2. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
**Workflow for working on an existing PR**:
1. Use \`${ghPullfrogMcpName}/checkout_pr\` to checkout the PR branch
2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push (automatically pushes to fork for fork PRs)
**Workflow for creating new changes**:
1. Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch
2. Make your changes using file operations
3. Use \`${ghPullfrogMcpName}/commit_files\` to commit your changes
4. Use \`${ghPullfrogMcpName}/push_branch\` to push the branch
5. Use \`${ghPullfrogMcpName}/create_pull_request\` to create a PR
@@ -89694,9 +89732,7 @@ ${encodedEvent}` : ""}
************* RUNTIME CONTEXT *************
working_directory: ${process.cwd()}
${envSetup}`;
${runtimeContext}`;
};
// agents/shared.ts
@@ -90117,9 +90153,9 @@ var claude = agent({
executablePath: "cli.js"
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
delete process.env.ANTHROPIC_API_KEY;
const prompt = addInstructions({ payload, prepResults });
const prompt = addInstructions({ payload, prepResults, repo });
console.log(prompt);
const sandboxOptions = payload.sandbox ? {
permissionMode: "default",
@@ -90575,7 +90611,7 @@ var codex = agent({
executablePath: "bin/codex.js"
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, prepResults, repo }) => {
const tempHome = process.env.PULLFROG_TEMP_DIR;
const configDir = join5(tempHome, ".config", "codex");
mkdirSync2(configDir, { recursive: true });
@@ -90605,7 +90641,7 @@ var codex = agent({
}
);
try {
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults }));
const streamedTurn = await thread.runStreamed(addInstructions({ payload, prepResults, repo }));
let finalOutput2 = "";
for await (const event of streamedTurn.events) {
const handler2 = messageHandlers2[event.type];
@@ -90747,7 +90783,7 @@ var cursor = agent({
executableName: "cursor-agent"
});
},
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults }) => {
run: async ({ payload, apiKey, cliPath, mcpServers, prepResults, repo }) => {
configureCursorMcpServers({ mcpServers, cliPath });
configureCursorSandbox({ sandbox: payload.sandbox ?? false });
const loggedModelCallIds = /* @__PURE__ */ new Set();
@@ -90800,7 +90836,7 @@ var cursor = agent({
}
};
try {
const fullPrompt = addInstructions({ payload, prepResults });
const fullPrompt = addInstructions({ payload, prepResults, repo });
const cursorArgs = payload.sandbox ? [
"--print",
fullPrompt,
@@ -91104,12 +91140,12 @@ var gemini = agent({
...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 }
});
},
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults }) => {
run: async ({ payload, apiKey, mcpServers, cliPath, prepResults, repo }) => {
configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
}
const sessionPrompt = addInstructions({ payload, prepResults });
const sessionPrompt = addInstructions({ payload, prepResults, repo });
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
const args3 = payload.sandbox ? [
"--allowed-tools",
@@ -91365,13 +91401,13 @@ var opencode = agent({
installDependencies: true
});
},
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults }) => {
run: async ({ payload, apiKey: _apiKey, apiKeys, mcpServers, cliPath, prepResults, repo }) => {
const tempHome = process.env.PULLFROG_TEMP_DIR;
const configDir = join7(tempHome, ".config", "opencode");
mkdirSync4(configDir, { recursive: true });
configureOpenCodeMcpServers({ mcpServers });
configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false });
const prompt = addInstructions({ payload, prepResults });
const prompt = addInstructions({ payload, prepResults, repo });
const args3 = ["run", "--format", "json", prompt];
if (payload.sandbox) {
log.info("\u{1F512} sandbox mode enabled: restricting to read-only operations");
@@ -95064,7 +95100,8 @@ function buildPullfrogFooter(params) {
parts.push(`Using [${params.agent.displayName}](${params.agent.url})`);
}
if (params.workflowRun) {
const url2 = params.workflowRun.htmlUrl ?? `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`;
const baseUrl = `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`;
const url2 = params.workflowRun.jobId ? `${baseUrl}/job/${params.workflowRun.jobId}` : baseUrl;
parts.push(`[View workflow run](${url2})`);
}
if (params.customParts) {
@@ -123934,6 +123971,70 @@ function $(cmd, args3, options) {
return stdout.trim();
}
// mcp/checkout.ts
var CheckoutPr = type({
pull_number: type.number.describe("the pull request number to checkout")
});
function CheckoutPrTool(ctx) {
return tool({
name: "checkout_pr",
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
parameters: CheckoutPr,
execute: execute(ctx, async ({ pull_number }) => {
log.info(`\u{1F500} checking out PR #${pull_number}...`);
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${pull_number} source repository was deleted`);
}
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
const baseBranch = pr.data.base.ref;
const headBranch = pr.data.head.ref;
log.info(`\u{1F4E5} fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
log.info(`\u{1F33F} fetching PR #${pull_number} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
$("git", ["checkout", headBranch]);
log.info(`\u2713 checked out PR #${pull_number}`);
if (isFork) {
const remoteName = `pr-${pull_number}`;
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
try {
$("git", ["remote", "add", remoteName, forkUrl]);
log.info(`\u{1F4CC} added remote '${remoteName}' for fork ${headRepo.full_name}`);
} catch {
$("git", ["remote", "set-url", remoteName, forkUrl]);
log.info(`\u{1F4CC} updated remote '${remoteName}' for fork ${headRepo.full_name}`);
}
$("git", ["config", `branch.${headBranch}.pushRemote`, remoteName]);
log.info(`\u{1F4CC} configured branch '${headBranch}' to push to '${remoteName}'`);
if (!pr.data.maintainer_can_modify) {
log.warning(
`\u26A0\uFE0F 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.`
);
}
} else {
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
}
return {
success: true,
number: pr.data.number,
title: pr.data.title,
base: baseBranch,
head: headBranch,
isFork,
maintainerCanModify: pr.data.maintainer_can_modify,
url: pr.data.html_url,
headRepo: headRepo.full_name
};
})
});
}
// mcp/debug.ts
var DebugShellCommand = type({});
var DebugShellCommandTool = tool({
@@ -124079,27 +124180,30 @@ var PushBranch = type({
branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
force: type.boolean.describe("Force push (use with caution)").default(false)
});
function PushBranchTool(ctx) {
const remote = ctx.pushRemote;
function PushBranchTool(_ctx) {
return tool({
name: "push_branch",
description: "Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.",
description: "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
parameters: PushBranch,
execute: execute(ctx, async ({ branchName, force }) => {
execute: execute(_ctx, async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
const isUrl = remote.startsWith("https://");
const args3 = force ? ["push", "--force", ...isUrl ? [] : ["-u"], remote, branch] : ["push", ...isUrl ? [] : ["-u"], remote, branch];
log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`);
let remote = "origin";
try {
remote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
} catch {
}
const args3 = force ? ["push", "--force", "-u", remote, branch] : ["push", "-u", remote, branch];
log.info(`pushing branch ${branch} to ${remote}`);
if (force) {
log.warning(`Force pushing - this will overwrite remote history`);
log.warning(`force pushing - this will overwrite remote history`);
}
$("git", args3);
return {
success: true,
branch,
remote: isUrl ? "(fork URL)" : remote,
remote,
force,
message: `Successfully pushed branch ${branch}`
message: `successfully pushed branch ${branch}`
};
})
});
@@ -124325,6 +124429,19 @@ var PullRequest = type({
body: type.string.describe("the body content of the pull request"),
base: type.string.describe("the base branch to merge into (e.g., 'main')")
});
function buildPrBodyWithFooter(ctx, body) {
const repoContext = parseRepoContext();
const runId = process.env.GITHUB_RUN_ID;
const agentName = ctx.payload.agent;
const agentInfo = agentName ? agentsManifest[agentName] : null;
const footer = buildPullfrogFooter({
triggeredBy: true,
agent: agentInfo ? { displayName: agentInfo.displayName, url: agentInfo.url } : void 0,
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0
});
const bodyWithoutFooter = stripExistingFooter(body);
return `${bodyWithoutFooter}${footer}`;
}
function PullRequestTool(ctx) {
return tool({
name: "create_pull_request",
@@ -124338,17 +124455,18 @@ function PullRequestTool(ctx) {
"PR creation blocked: secrets detected in PR title or body. Please remove any sensitive information (API keys, tokens, passwords) before creating a PR."
);
}
const diff = $("git", ["diff", `origin/${base}...HEAD`], { log: false });
const diff = $("git", ["diff", `origin/${base}..HEAD`], { log: false });
if (containsSecrets(diff)) {
throw new Error(
"PR creation blocked: secrets detected in changes. Please remove any sensitive information (API keys, tokens, passwords) before creating a PR."
);
}
const bodyWithFooter = buildPrBodyWithFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner,
repo: ctx.name,
title,
body,
body: bodyWithFooter,
head: currentBranch,
base
});
@@ -124372,7 +124490,7 @@ var PullRequestInfo = type({
function PullRequestInfoTool(ctx) {
return tool({
name: "get_pull_request",
description: "Retrieve PR information (metadata only). PR branch is already checked out during setup.",
description: "Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
parameters: PullRequestInfo,
execute: execute(ctx, async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({
@@ -124417,7 +124535,9 @@ var Review = type({
),
start_line: type.number.describe("Start line for multi-line comments (optional, for commenting on ranges)").optional()
}).array().describe(
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>...origin/<head>' to find correct line numbers (RIGHT side for new code, LEFT for old)."
// FORK PR NOTE: use HEAD not origin/<head> - for fork PRs, origin/<head> doesn't exist
// because the head branch is in a different repo (the fork). HEAD is the locally checked out PR branch.
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>..HEAD' to find correct line numbers (RIGHT side for new code, LEFT for old). Works for both fork and same-repo PRs."
).optional()
});
function ReviewTool(ctx) {
@@ -124462,6 +124582,7 @@ function ReviewTool(ctx) {
const fixAllUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix&review_id=${reviewId}`;
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
const footer = buildPullfrogFooter({
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
customParts: [`[Fix all \u2794](${fixAllUrl})`, `[Fix \u{1F44D}s \u2794](${fixApprovedUrl})`]
});
const updatedBody = (body || "") + footer;
@@ -124695,6 +124816,7 @@ async function startMcpHttpServer(ctx) {
PullRequestTool(ctx),
ReviewTool(ctx),
PullRequestInfoTool(ctx),
CheckoutPrTool(ctx),
GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx),
GetCheckSuiteLogsTool(ctx),
@@ -125328,16 +125450,16 @@ ${error42}` : `\u274C ${error42}`;
}
// utils/setup.ts
import { execSync } from "node:child_process";
import { execSync as execSync2 } from "node:child_process";
function setupGitConfig() {
const repoDir = process.cwd();
log.info("\u{1F527} Setting up git configuration...");
try {
execSync('git config --local user.email "team@pullfrog.com"', {
execSync2('git config --local user.email "team@pullfrog.com"', {
cwd: repoDir,
stdio: "pipe"
});
execSync('git config --local user.name "pullfrog"', {
execSync2('git config --local user.name "pullfrog"', {
cwd: repoDir,
stdio: "pipe"
});
@@ -125350,44 +125472,19 @@ function setupGitConfig() {
}
async function setupGit(ctx) {
const repoDir = process.cwd();
log.info("\u{1F527} Setting up git authentication...");
log.info("\u{1F527} setting up git authentication...");
try {
execSync("git config --local --unset-all http.https://github.com/.extraheader", {
execSync2("git config --local --unset-all http.https://github.com/.extraheader", {
cwd: repoDir,
stdio: "pipe"
});
log.info("\u2713 Removed existing authentication headers");
log.info("\u2713 removed existing authentication headers");
} catch {
log.debug("No existing authentication headers to remove");
log.debug("no existing authentication headers to remove");
}
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
log.info("\u2713 Updated origin URL with authentication token");
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
return { pushRemote: "origin" };
}
const prNumber = ctx.payload.event.issue_number;
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number: prNumber
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${prNumber} source repository was deleted`);
}
const branch = pr.data.head.ref;
log.info(`\u{1F33F} Checking out PR #${prNumber} (${branch})...`);
$("git", ["fetch", "origin", `pull/${prNumber}/head:${branch}`], { cwd: repoDir });
$("git", ["checkout", branch], { cwd: repoDir });
log.info(`\u2713 Successfully checked out PR #${prNumber}`);
const token = process.env.GITHUB_TOKEN || ctx.githubInstallationToken;
const pushUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
if (isFork) {
log.info(`\u{1F374} Fork PR detected, will push to: ${headRepo.full_name}`);
}
return { pushRemote: pushUrl };
log.info("\u2713 updated origin URL with authentication token");
}
// utils/timer.ts
@@ -125426,8 +125523,7 @@ async function main(inputs) {
const partialCtx = await initializeContext(inputs, payload);
const ctx = partialCtx;
timer.checkpoint("initializeContext");
const { pushRemote } = await setupGit(ctx);
ctx.pushRemote = pushRemote;
await setupGit(ctx);
timer.checkpoint("setupGit");
await setupTempDirectory(ctx);
timer.checkpoint("setupTempDirectory");
@@ -125636,11 +125732,26 @@ function parsePayload(inputs) {
}
async function startMcpServer(ctx) {
const runId = process.env.GITHUB_RUN_ID;
if (runId) {
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
if (!runId) {
throw new Error("GITHUB_RUN_ID environment variable is required");
}
ctx.runId = runId;
const workflowRunInfo = await fetchWorkflowRunInfo(ctx.runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`\u{1F4DD} Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
}
const jobName = process.env.GITHUB_JOB;
if (jobName) {
const jobs = await ctx.octokit.rest.actions.listJobsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
run_id: parseInt(ctx.runId, 10)
});
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
if (matchingJob) {
ctx.jobId = String(matchingJob.id);
log.info(`\u{1F4CB} Found job ID: ${ctx.jobId}`);
}
}
const { url: url2, close } = await startMcpHttpServer(ctx);
@@ -125695,7 +125806,12 @@ ${encode(eventWithoutContext)}`;
apiKey: ctx.apiKey,
apiKeys: ctx.apiKeys,
cliPath: ctx.cliPath,
prepResults: ctx.prepResults
prepResults: ctx.prepResults,
repo: {
owner: ctx.owner,
name: ctx.name,
defaultBranch: ctx.repo.default_branch
}
});
}
async function handleAgentResult(result) {
+1 -1
View File
@@ -1 +1 @@
Tell me a joke
Implement .omit() in a new PR
+36 -10
View File
@@ -65,8 +65,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const ctx = partialCtx as Context;
timer.checkpoint("initializeContext");
const { pushRemote } = await setupGit(ctx);
ctx.pushRemote = pushRemote;
await setupGit(ctx);
timer.checkpoint("setupGit");
await setupTempDirectory(ctx);
@@ -225,7 +224,6 @@ export interface Context {
modes: Mode[];
// setup fields
pushRemote: string;
sharedTempDir: string;
// mcp fields
@@ -240,6 +238,10 @@ export interface Context {
// prep phase results
prepResults: PrepResult[];
// workflow run info
runId: string;
jobId: string | undefined;
}
async function initializeContext(
@@ -254,8 +256,9 @@ async function initializeContext(
| "cliPath"
| "apiKey"
| "apiKeys"
| "pushRemote"
| "prepResults"
| "runId"
| "jobId"
>
> {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
@@ -407,14 +410,32 @@ function parsePayload(inputs: Inputs): Payload {
}
async function startMcpServer(ctx: Context): Promise<void> {
const runId = process.env.GITHUB_RUN_ID;
if (!runId) {
throw new Error("GITHUB_RUN_ID environment variable is required");
}
ctx.runId = runId;
// fetch the pre-created progress comment ID from the database
// this must be set BEFORE starting the MCP server so comment.ts can read it
const runId = process.env.GITHUB_RUN_ID;
if (runId) {
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`📝 Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
const workflowRunInfo = await fetchWorkflowRunInfo(ctx.runId);
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`📝 Using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
}
// fetch job ID by matching GITHUB_JOB name
const jobName = process.env.GITHUB_JOB;
if (jobName) {
const jobs = await ctx.octokit.rest.actions.listJobsForWorkflowRun({
owner: ctx.owner,
repo: ctx.name,
run_id: parseInt(ctx.runId, 10),
});
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
if (matchingJob) {
ctx.jobId = String(matchingJob.id);
log.info(`📋 Found job ID: ${ctx.jobId}`);
}
}
@@ -485,6 +506,11 @@ async function runAgent(ctx: Context): Promise<AgentResult> {
apiKeys: ctx.apiKeys,
cliPath: ctx.cliPath,
prepResults: ctx.prepResults,
repo: {
owner: ctx.owner,
name: ctx.name,
defaultBranch: ctx.repo.default_branch,
},
});
}
+105
View File
@@ -0,0 +1,105 @@
import { type } from "arktype";
import type { Context } from "../main.ts";
import { log } from "../utils/cli.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
export const CheckoutPr = type({
pull_number: type.number.describe("the pull request number to checkout"),
});
export type CheckoutPrResult = {
success: true;
number: number;
title: string;
base: string;
head: string;
isFork: boolean;
maintainerCanModify: boolean;
url: string;
headRepo: string;
};
export function CheckoutPrTool(ctx: Context) {
return tool({
name: "checkout_pr",
description:
"Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Use this when you need to work on an existing PR.",
parameters: CheckoutPr,
execute: execute(ctx, async ({ pull_number }) => {
log.info(`🔀 checking out PR #${pull_number}...`);
// fetch PR metadata
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number,
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${pull_number} source repository was deleted`);
}
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
const baseBranch = pr.data.base.ref;
const headBranch = pr.data.head.ref;
// fetch base branch so origin/<base> exists for diff operations
log.info(`📥 fetching base branch (${baseBranch})...`);
$("git", ["fetch", "--no-tags", "origin", baseBranch]);
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
log.info(`🌿 fetching PR #${pull_number} (${headBranch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${pull_number}/head:${headBranch}`]);
// checkout the branch
$("git", ["checkout", headBranch]);
log.info(`✓ checked out PR #${pull_number}`);
// configure push remote for this branch
if (isFork) {
const remoteName = `pr-${pull_number}`;
const forkUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${headRepo.full_name}.git`;
// add fork as a named remote (ignore error if already exists)
try {
$("git", ["remote", "add", remoteName, forkUrl]);
log.info(`📌 added remote '${remoteName}' for fork ${headRepo.full_name}`);
} catch {
// remote already exists, update its URL
$("git", ["remote", "set-url", remoteName, forkUrl]);
log.info(`📌 updated remote '${remoteName}' for fork ${headRepo.full_name}`);
}
// set branch push config so `git push` knows where to push
$("git", ["config", `branch.${headBranch}.pushRemote`, remoteName]);
log.info(`📌 configured branch '${headBranch}' to push to '${remoteName}'`);
// 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. ` +
`ask the PR author to enable "Allow edits from maintainers" or the fork may be owned by an organization.`
);
}
} else {
// for same-repo PRs, push to origin
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
}
return {
success: true,
number: pr.data.number,
title: pr.data.title,
base: baseBranch,
head: headBranch,
isFork,
maintainerCanModify: pr.data.maintainer_can_modify,
url: pr.data.html_url,
headRepo: headRepo.full_name,
} satisfies CheckoutPrResult;
}),
});
}
+18 -14
View File
@@ -141,35 +141,39 @@ export const PushBranch = type({
force: type.boolean.describe("Force push (use with caution)").default(false),
});
export function PushBranchTool(ctx: Context) {
const remote = ctx.pushRemote;
export function PushBranchTool(_ctx: Context) {
return tool({
name: "push_branch",
description:
"Push the current branch (or specified branch) to the remote repository. Never force push unless explicitly requested.",
"Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
parameters: PushBranch,
execute: execute(ctx, async ({ branchName, force }) => {
execute: execute(_ctx, async ({ branchName, force }) => {
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
// skip -u flag when pushing to URL (can't set upstream without a remote name)
const isUrl = remote.startsWith("https://");
const args = force
? ["push", "--force", ...(isUrl ? [] : ["-u"]), remote, branch]
: ["push", ...(isUrl ? [] : ["-u"]), remote, branch];
// check if branch has a configured pushRemote
let remote = "origin";
try {
remote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
} catch {
// no configured pushRemote, default to origin
}
log.info(`Pushing branch ${branch} to ${isUrl ? "(fork URL)" : remote}`);
const args = force
? ["push", "--force", "-u", remote, branch]
: ["push", "-u", remote, branch];
log.info(`pushing branch ${branch} to ${remote}`);
if (force) {
log.warning(`Force pushing - this will overwrite remote history`);
log.warning(`force pushing - this will overwrite remote history`);
}
$("git", args);
return {
success: true,
branch,
remote: isUrl ? "(fork URL)" : remote,
remote,
force,
message: `Successfully pushed branch ${branch}`,
message: `successfully pushed branch ${branch}`,
};
}),
});
+26 -2
View File
@@ -1,6 +1,9 @@
import { type } from "arktype";
import { agentsManifest } from "../external.ts";
import type { Context } from "../main.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts";
import { parseRepoContext } from "../utils/github.ts";
import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
@@ -11,6 +14,23 @@ export const PullRequest = type({
base: type.string.describe("the base branch to merge into (e.g., 'main')"),
});
function buildPrBodyWithFooter(ctx: Context, body: string): string {
const repoContext = parseRepoContext();
const runId = process.env.GITHUB_RUN_ID;
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,
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined,
});
const bodyWithoutFooter = stripExistingFooter(body);
return `${bodyWithoutFooter}${footer}`;
}
export function PullRequestTool(ctx: Context) {
return tool({
name: "create_pull_request",
@@ -29,7 +49,9 @@ export function PullRequestTool(ctx: Context) {
}
// validate all changes that would be in the PR (from base to HEAD)
const diff = $("git", ["diff", `origin/${base}...HEAD`], { log: false });
// FORK PR NOTE: origin/<base> is fetched by setupGit, so this works for both fork and same-repo PRs
// use two-dot (..) not three-dot (...) for reliable diffs with shallow clones
const diff = $("git", ["diff", `origin/${base}..HEAD`], { log: false });
if (containsSecrets(diff)) {
throw new Error(
"PR creation blocked: secrets detected in changes. " +
@@ -37,11 +59,13 @@ export function PullRequestTool(ctx: Context) {
);
}
const bodyWithFooter = buildPrBodyWithFooter(ctx, body);
const result = await ctx.octokit.rest.pulls.create({
owner: ctx.owner,
repo: ctx.name,
title: title,
body: body,
body: bodyWithFooter,
head: currentBranch,
base: base,
});
+1 -1
View File
@@ -10,7 +10,7 @@ export function PullRequestInfoTool(ctx: Context) {
return tool({
name: "get_pull_request",
description:
"Retrieve PR information (metadata only). PR branch is already checked out during setup.",
"Retrieve PR metadata (number, title, state, base/head branches, fork status). To checkout a PR branch locally, use checkout_pr instead.",
parameters: PullRequestInfo,
execute: execute(ctx, async ({ pull_number }) => {
const pr = await ctx.octokit.rest.pulls.get({
+4 -1
View File
@@ -35,7 +35,9 @@ export const Review = type({
})
.array()
.describe(
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>...origin/<head>' to find correct line numbers (RIGHT side for new code, LEFT for old)."
// FORK PR NOTE: use HEAD not origin/<head> - for fork PRs, origin/<head> doesn't exist
// because the head branch is in a different repo (the fork). HEAD is the locally checked out PR branch.
"PRIMARY location for ALL feedback. 95%+ of review content should be here. Use 'git diff origin/<base>..HEAD' to find correct line numbers (RIGHT side for new code, LEFT for old). Works for both fork and same-repo PRs."
)
.optional(),
});
@@ -93,6 +95,7 @@ export function ReviewTool(ctx: Context) {
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.owner}/${ctx.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
const footer = buildPullfrogFooter({
workflowRun: { owner: ctx.owner, repo: ctx.name, runId: ctx.runId, jobId: ctx.jobId },
customParts: [`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`],
});
+2
View File
@@ -5,6 +5,7 @@ import { FastMCP, type Tool } from "fastmcp";
import { ghPullfrogMcpName } from "../external.ts";
import type { Context } from "../main.ts";
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
import { CheckoutPrTool } from "./checkout.ts";
import {
CreateCommentTool,
EditCommentTool,
@@ -78,6 +79,7 @@ export async function startMcpHttpServer(
PullRequestTool(ctx),
ReviewTool(ctx),
PullRequestInfoTool(ctx),
CheckoutPrTool(ctx),
GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx),
GetCheckSuiteLogsTool(ctx),
+7 -6
View File
@@ -64,10 +64,11 @@ export function getModes({
description:
"Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically fetches and checks out the PR branch)
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and configures push settings (including for fork PRs).
2. Review the feedback provided. Understand each review comment and what changes are being requested.
- **EVENT DATA may contain review comment details**: If available, \`approved_comments\` are comments to address, \`unapproved_comments\` are for context only. The \`triggerer\` field indicates who initiated this action - prioritize their replies when deciding how to implement fixes.
- You can use ${ghPullfrogMcpName}/get_pull_request to get PR metadata if needed.
3. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context. Read AGENTS.md if it exists.
@@ -77,7 +78,7 @@ export function getModes({
6. Test your changes to ensure they work correctly.
7. When done, commit and push your changes to the existing PR branch. Do not create a new branch or PR - you are updating an existing one.
7. When done, commit your changes with ${ghPullfrogMcpName}/commit_files, then push with ${ghPullfrogMcpName}/push_branch. The push will automatically go to the correct remote (including fork repos). Do not create a new branch or PR - you are updating an existing one.
${
disableProgressComment
? ""
@@ -92,9 +93,9 @@ ${
description:
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
prompt: `Follow these steps:
1. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and base branch, preparing the repo for review.
2. **IMPORTANT**: After calling ${ghPullfrogMcpName}/get_pull_request, the PR branch is already checked out locally. View diff using: \`git diff origin/<base>...HEAD\` (replace <base> with 'base' from PR info). Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch.
2. **IMPORTANT**: After calling checkout_pr, the PR branch is checked out locally. View diff using: \`git diff origin/<base>..HEAD\` (replace <base> with 'base' from checkout_pr result, e.g., \`git diff origin/main..HEAD\`). Use two dots (..) not three dots (...) for reliable diffs. Do NOT use \`origin/<head>\` - the branch is checked out locally, not as a remote tracking branch. This works for both same-repo and fork PRs.
3. Read files from the checked-out PR branch to understand the implementation. Always use **relative paths** from repo root (e.g., \`src/index.ts\`), never absolute paths.
@@ -102,7 +103,8 @@ ${
**GENERAL GUIDANCE**
- *CRITICAL* — Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
- Do not leave any comments that are not potentially actionable. Do not leave complimentary comments just to be nice.
- *CRITICAL* — Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
- For line numbers, use the NEW file line number from the diff (shown after \`+\` in hunk headers like \`@@ -10,5 +12,8 @@\` means new file starts at line 12)
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
- **CRITICAL: Prioritize per-line feedback over summary text.**
@@ -112,7 +114,6 @@ ${
- 95%+ of review content should be in per-line comments; the body should be just a couple sentences
- The review body will include quick action links for addressing feedback, so keep it concise
- Do not nitpick unless instructed explicity to do so by the user's additional instructions. This includes: requesting documentation/docstrings/JSDoc.
- Do not leave any comments that are not potentially actionable.
- The review should be thoughtful. When evaluating complex changes, consider the following conceptual approach:
- 1) conceptualize the changes made. make sure you understand it.
- 2) evaluate conceptual approach. leave feedback as needed.
+6 -5
View File
@@ -7,12 +7,12 @@ export interface AgentInfo {
url: string;
}
export interface WorkflowRunInfo {
export interface WorkflowRunFooterInfo {
owner: string;
repo: string;
runId: string;
/** optional job URL - if provided, will be used instead of building from runId */
htmlUrl?: string;
/** optional job ID - if provided, will append /job/{jobId} to the workflow run URL */
jobId?: string | undefined;
}
export interface BuildPullfrogFooterParams {
@@ -21,7 +21,7 @@ export interface BuildPullfrogFooterParams {
/** add "Using [agent](url)" link */
agent?: AgentInfo | undefined;
/** add "View workflow run" link */
workflowRun?: WorkflowRunInfo | undefined;
workflowRun?: WorkflowRunFooterInfo | undefined;
/** arbitrary custom parts (e.g., action links) */
customParts?: string[];
}
@@ -42,7 +42,8 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
}
if (params.workflowRun) {
const url = params.workflowRun.htmlUrl ?? `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`;
const baseUrl = `https://github.com/${params.workflowRun.owner}/${params.workflowRun.repo}/actions/runs/${params.workflowRun.runId}`;
const url = params.workflowRun.jobId ? `${baseUrl}/job/${params.workflowRun.jobId}` : baseUrl;
parts.push(`[View workflow run](${url})`);
}
+13 -47
View File
@@ -62,19 +62,19 @@ export function setupGitConfig(): void {
}
}
export type SetupGitResult = {
pushRemote: string;
};
/**
* Unified git setup: configures authentication and checks out PR branch if applicable.
* For PR events, always returns a push URL constructed from the PR's head repo.
* This works for both same-repo and fork PRs with a single code path.
* Setup git authentication for the repository.
* PR checkout is handled dynamically by the checkout_pr MCP tool.
*
* FORK PR ARCHITECTURE (handled by checkout_pr tool):
* - origin: always points to BASE REPO (where PR targets)
* - checkout_pr sets per-branch pushRemote config for fork PRs
* - diff operations use: git diff origin/<base>..HEAD
*/
export async function setupGit(ctx: Context): Promise<SetupGitResult> {
export async function setupGit(ctx: Context): Promise<void> {
const repoDir = process.cwd();
log.info("🔧 Setting up git authentication...");
log.info("🔧 setting up git authentication...");
// remove existing git auth headers that actions/checkout might have set
try {
@@ -82,47 +82,13 @@ export async function setupGit(ctx: Context): Promise<SetupGitResult> {
cwd: repoDir,
stdio: "pipe",
});
log.info("✓ Removed existing authentication headers");
log.info("✓ removed existing authentication headers");
} catch {
log.debug("No existing authentication headers to remove");
log.debug("no existing authentication headers to remove");
}
// set up origin with token (needed for fetch operations)
// authenticate origin - needed for all fetch operations including PR fetches
const originUrl = `https://x-access-token:${ctx.githubInstallationToken}@github.com/${ctx.owner}/${ctx.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
log.info("✓ Updated origin URL with authentication token");
// non-PR events: stay on default branch
if (ctx.payload.event.is_pr !== true || !ctx.payload.event.issue_number) {
return { pushRemote: "origin" };
}
// PR event: fetch PR info to get branch name and head repo
const prNumber = ctx.payload.event.issue_number;
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
repo: ctx.name,
pull_number: prNumber,
});
const headRepo = pr.data.head.repo;
if (!headRepo) {
throw new Error(`PR #${prNumber} source repository was deleted`);
}
// checkout PR branch using plain git (no gh cli needed)
const branch = pr.data.head.ref;
log.info(`🌿 Checking out PR #${prNumber} (${branch})...`);
$("git", ["fetch", "--no-tags", "origin", `pull/${prNumber}/head:${branch}`], { cwd: repoDir });
$("git", ["checkout", branch], { cwd: repoDir });
log.info(`✓ Successfully checked out PR #${prNumber}`);
// unified push URL - works for both same-repo and fork PRs
const token = process.env.GITHUB_TOKEN || ctx.githubInstallationToken;
const pushUrl = `https://x-access-token:${token}@github.com/${headRepo.full_name}.git`;
const isFork = headRepo.full_name !== pr.data.base.repo.full_name;
if (isFork) {
log.info(`🍴 Fork PR detected, will push to: ${headRepo.full_name}`);
}
return { pushRemote: pushUrl };
log.info("✓ updated origin URL with authentication token");
}