Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d572f3ce8 | |||
| 73139a169c | |||
| d5bec7499b |
+28
-10
@@ -19,6 +19,7 @@ export type CheckoutPrResult = {
|
|||||||
maintainerCanModify: boolean;
|
maintainerCanModify: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
headRepo: string;
|
headRepo: string;
|
||||||
|
diff: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CheckoutPrBranchParams {
|
interface CheckoutPrBranchParams {
|
||||||
@@ -60,12 +61,17 @@ export async function checkoutPrBranch(
|
|||||||
const baseBranch = pr.data.base.ref;
|
const baseBranch = pr.data.base.ref;
|
||||||
const headBranch = pr.data.head.ref;
|
const headBranch = pr.data.head.ref;
|
||||||
|
|
||||||
// check if we're already on the correct branch
|
// always use pr-{number} as local branch name for consistency
|
||||||
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
// this avoids naming conflicts and makes push config simpler
|
||||||
const alreadyOnBranch = currentBranch === headBranch;
|
const localBranch = `pr-${pullNumber}`;
|
||||||
|
|
||||||
|
// check if we're already on the correct commit (not just branch name)
|
||||||
|
// this handles fork PRs where head branch name might match base branch name
|
||||||
|
const currentSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
||||||
|
const alreadyOnBranch = currentSha === pr.data.head.sha;
|
||||||
|
|
||||||
if (alreadyOnBranch) {
|
if (alreadyOnBranch) {
|
||||||
log.debug(`already on PR branch ${headBranch}, skipping checkout`);
|
log.debug(`already on PR branch ${localBranch}, skipping checkout`);
|
||||||
} else {
|
} else {
|
||||||
// fetch base branch so origin/<base> exists for diff operations
|
// fetch base branch so origin/<base> exists for diff operations
|
||||||
log.debug(`📥 fetching base branch (${baseBranch})...`);
|
log.debug(`📥 fetching base branch (${baseBranch})...`);
|
||||||
@@ -76,11 +82,11 @@ export async function checkoutPrBranch(
|
|||||||
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
$("git", ["checkout", "-B", baseBranch, `origin/${baseBranch}`]);
|
||||||
|
|
||||||
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
|
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
|
||||||
log.debug(`🌿 fetching PR #${pullNumber} (${headBranch})...`);
|
log.debug(`🌿 fetching PR #${pullNumber} (${localBranch})...`);
|
||||||
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${headBranch}`]);
|
$("git", ["fetch", "--no-tags", "origin", `pull/${pullNumber}/head:${localBranch}`]);
|
||||||
|
|
||||||
// checkout the branch
|
// checkout the branch
|
||||||
$("git", ["checkout", headBranch]);
|
$("git", ["checkout", localBranch]);
|
||||||
log.debug(`✓ checked out PR #${pullNumber}`);
|
log.debug(`✓ checked out PR #${pullNumber}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +115,10 @@ export async function checkoutPrBranch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set branch push config so `git push` knows where to push
|
// set branch push config so `git push` knows where to push
|
||||||
$("git", ["config", `branch.${headBranch}.pushRemote`, remoteName]);
|
$("git", ["config", `branch.${localBranch}.pushRemote`, remoteName]);
|
||||||
log.debug(`📌 configured branch '${headBranch}' to push to '${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}'`);
|
||||||
|
|
||||||
// warn if maintainer can't modify (push will likely fail)
|
// warn if maintainer can't modify (push will likely fail)
|
||||||
if (!pr.data.maintainer_can_modify) {
|
if (!pr.data.maintainer_can_modify) {
|
||||||
@@ -121,7 +129,8 @@ export async function checkoutPrBranch(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// for same-repo PRs, push to origin
|
// for same-repo PRs, push to origin
|
||||||
$("git", ["config", `branch.${headBranch}.pushRemote`, "origin"]);
|
$("git", ["config", `branch.${localBranch}.pushRemote`, "origin"]);
|
||||||
|
$("git", ["config", `branch.${localBranch}.merge`, `refs/heads/${headBranch}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { prNumber: pullNumber };
|
return { prNumber: pullNumber };
|
||||||
@@ -157,6 +166,14 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
|||||||
throw new Error(`PR #${pull_number} source repository was deleted`);
|
throw new Error(`PR #${pull_number} source repository was deleted`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetch PR diff via API (authoritative source - not affected by main advancing)
|
||||||
|
const diffResponse = await ctx.octokit.rest.pulls.get({
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.name,
|
||||||
|
pull_number,
|
||||||
|
mediaType: { format: "diff" },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
number: pr.data.number,
|
number: pr.data.number,
|
||||||
@@ -167,6 +184,7 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
|||||||
maintainerCanModify: pr.data.maintainer_can_modify,
|
maintainerCanModify: pr.data.maintainer_can_modify,
|
||||||
url: pr.data.html_url,
|
url: pr.data.html_url,
|
||||||
headRepo: headRepo.full_name,
|
headRepo: headRepo.full_name,
|
||||||
|
diff: diffResponse.data as unknown as string,
|
||||||
} satisfies CheckoutPrResult;
|
} satisfies CheckoutPrResult;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+18
-5
@@ -158,11 +158,23 @@ export function PushBranchTool(_ctx: ToolContext) {
|
|||||||
// no configured pushRemote, default to origin
|
// no configured pushRemote, default to origin
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = force
|
// check if branch has a configured merge ref (remote branch name may differ from local)
|
||||||
? ["push", "--force", "-u", remote, branch]
|
let remoteBranch = branch;
|
||||||
: ["push", "-u", remote, branch];
|
try {
|
||||||
|
const mergeRef = $("git", ["config", `branch.${branch}.merge`], { log: false }).trim();
|
||||||
|
// merge ref is like "refs/heads/main", extract the branch name
|
||||||
|
remoteBranch = mergeRef.replace("refs/heads/", "");
|
||||||
|
} catch {
|
||||||
|
// no configured merge ref, use local branch name
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(`pushing branch ${branch} to ${remote}`);
|
// use refspec when local and remote branch names differ
|
||||||
|
const refspec = branch === remoteBranch ? branch : `${branch}:${remoteBranch}`;
|
||||||
|
const args = force
|
||||||
|
? ["push", "--force", "-u", remote, refspec]
|
||||||
|
: ["push", "-u", remote, refspec];
|
||||||
|
|
||||||
|
log.debug(`pushing ${branch} to ${remote}/${remoteBranch}`);
|
||||||
if (force) {
|
if (force) {
|
||||||
log.warning(`force pushing - this will overwrite remote history`);
|
log.warning(`force pushing - this will overwrite remote history`);
|
||||||
}
|
}
|
||||||
@@ -171,9 +183,10 @@ export function PushBranchTool(_ctx: ToolContext) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
branch,
|
branch,
|
||||||
|
remoteBranch,
|
||||||
remote,
|
remote,
|
||||||
force,
|
force,
|
||||||
message: `successfully pushed branch ${branch}`,
|
message: `successfully pushed ${branch} to ${remote}/${remoteBranch}`,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-16
@@ -1,6 +1,3 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
|
||||||
import { writeFileSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { ToolContext } from "../main.ts";
|
import type { ToolContext } from "../main.ts";
|
||||||
@@ -65,7 +62,7 @@ export function StartReviewTool(ctx: ToolContext) {
|
|||||||
return tool({
|
return tool({
|
||||||
name: "start_review",
|
name: "start_review",
|
||||||
description:
|
description:
|
||||||
"Start a new review session for a pull request. Creates a scratchpad file for gathering thoughts and a pending review on GitHub. Must be called before add_review_comment.",
|
"Start a new review session for a pull request. Creates a pending review on GitHub. Must be called before add_review_comment.",
|
||||||
parameters: StartReview,
|
parameters: StartReview,
|
||||||
execute: execute(async ({ pull_number }) => {
|
execute: execute(async ({ pull_number }) => {
|
||||||
// check if review already started in this session
|
// check if review already started in this session
|
||||||
@@ -119,12 +116,6 @@ export function StartReviewTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create scratchpad file
|
|
||||||
const scratchpadId = randomBytes(4).toString("hex");
|
|
||||||
const scratchpadPath = join(ctx.sharedTempDir, `pullfrog-review-${scratchpadId}.md`);
|
|
||||||
const scratchpadContent = `# Review ${scratchpadId}\n\n`;
|
|
||||||
writeFileSync(scratchpadPath, scratchpadContent);
|
|
||||||
|
|
||||||
// set PR context and review state
|
// set PR context and review state
|
||||||
ctx.toolState.prNumber = pull_number;
|
ctx.toolState.prNumber = pull_number;
|
||||||
ctx.toolState.review = {
|
ctx.toolState.review = {
|
||||||
@@ -133,9 +124,18 @@ export function StartReviewTool(ctx: ToolContext) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reviewId: scratchpadId,
|
message: `Review session started for PR #${pull_number}.`,
|
||||||
scratchpadPath,
|
guidance: {
|
||||||
message: `Review session started. Use the scratchpad file to gather your thoughts, then call add_review_comment for each comment.`,
|
analyze: [
|
||||||
|
"What does this PR change? Summarize in 1-2 sentences.",
|
||||||
|
"Is the approach sound? If not, stop and comment on approach first.",
|
||||||
|
"What bugs, edge cases, or security issues exist?",
|
||||||
|
],
|
||||||
|
beforeCommenting: [
|
||||||
|
"Skip nitpicks unless explicitly requested.",
|
||||||
|
"Would the codebase maintainer care about this feedback, based on what you can infer about the code quality standards in this repo?",
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -281,9 +281,8 @@ export const Review = type({
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.describe(
|
.describe(
|
||||||
// FORK PR NOTE: use HEAD not origin/<head> - for fork PRs, origin/<head> doesn't exist
|
// FORK PR NOTE: checkout_pr returns the diff via GitHub API - use that for line numbers
|
||||||
// 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 the diff returned from checkout_pr to find correct line numbers (RIGHT side for new code, LEFT for old)."
|
||||||
"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(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,29 +100,26 @@ ${
|
|||||||
description:
|
description:
|
||||||
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||||
prompt: `Follow these steps:
|
prompt: `Follow these steps:
|
||||||
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.
|
1. Checkout the PR using ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch and returns the PR diff in the \`diff\` field of the response. Use this diff for your review - it shows exactly what's in the PR (fetched via GitHub API, so it's not affected by main advancing after the branch was created).
|
||||||
|
|
||||||
2. **IMPORTANT**: After calling ${ghPullfrogMcpName}/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.
|
2. Start review session using ${ghPullfrogMcpName}/start_review. This creates a pending review on GitHub and returns analysis guidance. Follow the guidance before adding comments.
|
||||||
|
|
||||||
3. Start review session using ${ghPullfrogMcpName}/start_review. This creates a scratchpad file at a temp path (e.g., \`/tmp/pullfrog-review-abc123.md\`) and returns a session ID. The scratchpad file header contains the session ID for reference. Use this file as free-form space to gather your thoughts before adding comments.
|
3. **ANALYZE** - Before adding any comments, think through:
|
||||||
|
- What does this PR change? Summarize in 1-2 sentences.
|
||||||
|
- Is the approach sound? If not, **stop here** and comment on the approach first. Don't waste time on implementation details if the approach is wrong.
|
||||||
|
- What bugs, edge cases, or security issues exist?
|
||||||
|
|
||||||
4. **ANALYZE** - Use the scratchpad to gather your thoughts:
|
4. **BEFORE COMMENTING** - For each potential comment, ask yourself:
|
||||||
- Summarize what changes this PR makes
|
- Is this a nitpick? Skip it unless explicitly requested.
|
||||||
- Evaluate the approach - is it sound? If not, **stop here** and leave feedback on the approach. Don't waste time on implementation details if the approach is wrong.
|
- Would the codebase maintainer care about this feedback, based on what you can infer about the code quality standards in this repo?
|
||||||
- If approach is sound, analyze implementation - consider potential issues per file
|
|
||||||
- Identify bugs, security issues, edge cases
|
|
||||||
|
|
||||||
5. **SELF-CRITIQUE** - Before adding comments, review your scratchpad:
|
5. Add inline review comments one-by-one using ${ghPullfrogMcpName}/add_review_comment
|
||||||
- Remove nitpicks unless explicitly requested. Think documentation, JSDoc/docstrings, useless comments (compliments)
|
|
||||||
- Your level of nitpickiness should be proportional to the current state of the codebase. Try to guess how much the user will care about a specific critique.
|
|
||||||
|
|
||||||
6. Add inline review comments one-by-one using ${ghPullfrogMcpName}/add_review_comment
|
|
||||||
- Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
|
- Use **relative paths** from repo root (e.g., \`packages/core/src/utils.ts\`)
|
||||||
- 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)
|
- 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.
|
- Only comment on lines that appear in the diff. GitHub will reject comments on unchanged lines.
|
||||||
- For issues appearing in multiple places, comment on the FIRST occurrence and reference others (e.g., "also at lines X, Y")
|
- For issues appearing in multiple places, comment on the FIRST occurrence and reference others (e.g., "also at lines X, Y")
|
||||||
|
|
||||||
7. Submit the review using ${ghPullfrogMcpName}/submit_review
|
6. Submit the review using ${ghPullfrogMcpName}/submit_review
|
||||||
- The "body" field is ONLY for: (1) a 1-3 sentence high-level overview, (2) urgency level (e.g., "minor suggestions" vs "blocking issues"), (3) critical security callouts (e.g., API key exposure)
|
- The "body" field is ONLY for: (1) a 1-3 sentence high-level overview, (2) urgency level (e.g., "minor suggestions" vs "blocking issues"), (3) critical security callouts (e.g., API key exposure)
|
||||||
|
|
||||||
**GENERAL GUIDANCE**
|
**GENERAL GUIDANCE**
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.148",
|
"version": "0.0.149",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+6
-5
@@ -341,12 +341,13 @@ export const log = {
|
|||||||
* Log tool call information to console with formatted output
|
* Log tool call information to console with formatted output
|
||||||
*/
|
*/
|
||||||
toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => {
|
toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => {
|
||||||
let output = `→ ${toolName}\n`;
|
|
||||||
|
|
||||||
const inputFormatted = formatJsonValue(input);
|
const inputFormatted = formatJsonValue(input);
|
||||||
if (inputFormatted !== "{}") {
|
// if (inputFormatted !== "{}")
|
||||||
output += formatIndentedField("input", inputFormatted);
|
const output = inputFormatted !== "{}" ? `${toolName}(${inputFormatted})` : `${toolName}()`;
|
||||||
}
|
|
||||||
|
// if (inputFormatted !== "{}") {
|
||||||
|
// output += formatIndentedField("input", inputFormatted);
|
||||||
|
// }
|
||||||
|
|
||||||
log.info(output.trimEnd());
|
log.info(output.trimEnd());
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ interface SetupGitAuthParams {
|
|||||||
* FORK PR ARCHITECTURE:
|
* FORK PR ARCHITECTURE:
|
||||||
* - origin: always points to BASE REPO (where PR targets)
|
* - origin: always points to BASE REPO (where PR targets)
|
||||||
* - checkoutPrBranch sets per-branch pushRemote config for fork PRs
|
* - checkoutPrBranch sets per-branch pushRemote config for fork PRs
|
||||||
* - diff operations use: git diff origin/<base>..HEAD
|
* - checkout_pr returns the PR diff via GitHub API (authoritative source)
|
||||||
*/
|
*/
|
||||||
export async function setupGitAuth(params: SetupGitAuthParams): Promise<void> {
|
export async function setupGitAuth(params: SetupGitAuthParams): Promise<void> {
|
||||||
const repoDir = process.cwd();
|
const repoDir = process.cwd();
|
||||||
|
|||||||
Reference in New Issue
Block a user