Appearance
Build your first PR reviewer
Build a GitHub PR reviewer that classifies changes as trivial, moderate, or large, then approves safe changes or requests human review. Add GitHub event handling so pull requests can trigger reviews.
Getting started
- Get started with an agent in Cursor: follow Scaffold an agent with Cursor and ask Cursor to read
skills/create-agent/SKILL.md. - Get started in the CLI: continue below.
Prerequisites
- Node 22.13 or newer. Bun isn't supported.
- Run commands as
agent-sdk <command>, or usenpx @cursor/july <command>when the CLI isn't onPATH. See Run the CLI for monorepo checkouts and other setups. - A Cursor credential for model turns. Sign in once:
bash
agent-sdk loginYou can also set CURSOR_API_KEY instead of signing in.
- Authenticate with
gh auth loginorGITHUB_TOKEN. You can read public pull requests. Posting reviews requires repository write access.
Create and run the project
Initialize the project and start the development server:
bash
npx @cursor/july init ./sdk-pr-reviewer
cd sdk-pr-reviewer
agent-sdk devKeep agent-sdk dev running. In a second terminal, run:
bash
agent-sdk run --dir . --message "Introduce yourself in one sentence."Confirm the agent replies.
Add review instructions
Replace agent/instructions.md:
md
# PR reviewer
You review GitHub pull requests. Be specific and brief.
For every pull request:
1. Call `inspect_pr` first. Never judge a change you haven't fetched.
2. Match your review to the complexity it reports:
- `trivial`: read the patches. If the diff does what the title says
and nothing looks risky, call `submit_review` with verdict
`approve`.
- `moderate`: read every patch. Approve only when you understand the
whole change and see no risk. Otherwise ask for a human review and
say which files worry you.
- `large`: call `submit_review` with verdict `request_human_review`
right away. Use the stats you already have to point the reviewer at
the biggest files; don't dig further.
3. Never approve a draft. Point out anything surprising, even when you
approve.
End with one sentence: the verdict and why.Remove the demo echo tool:
bash
rm agent/tools/echo.tsAdd a shared helper
Create agent/lib/github.ts:
ts
export interface PullRef {
owner: string;
repo: string;
number: number;
}
/** Split https://github.com/owner/repo/pull/123 into its parts. */
export function parsePullUrl(prUrl: string): PullRef {
const url = new URL(prUrl);
const [owner, repo, pulls, number] = url.pathname.split("/").filter(Boolean);
const parsed = Number.parseInt(number ?? "", 10);
if (pulls !== "pull" || owner === undefined || Number.isNaN(parsed)) {
throw new Error(`Not a pull request URL: ${prUrl}`);
}
return { owner, repo, number: parsed };
}Add the inspect tool
Create agent/tools/inspect_pr.ts:
ts
import { defineTool } from "@cursor/july/tools";
import { z } from "zod";
import { parsePullUrl } from "../lib/github.js";
export type Complexity = "trivial" | "moderate" | "large";
/** Rate complexity from fixed line and file-count thresholds. */
function rateComplexity(linesChanged: number, changedFiles: number): Complexity {
if (linesChanged <= 25 && changedFiles <= 2) {
return "trivial";
}
if (linesChanged <= 400 && changedFiles <= 15) {
return "moderate";
}
return "large";
}
/** Keep one oversized file from flooding the model's context. */
function trimPatch(patch: string | undefined): string | undefined {
if (patch === undefined || patch.length <= 3000) {
return patch;
}
return `${patch.slice(0, 3000)}\n[... patch trimmed ...]`;
}
export default defineTool({
description:
"Fetch a pull request's title, stats, and per-file patches, plus a deterministic complexity rating (trivial, moderate, or large). Call this before any review decision.",
inputSchema: z.object({
prUrl: z
.string()
.describe("Pull request URL: https://github.com/owner/repo/pull/123"),
}),
async execute({ prUrl }, ctx) {
const { owner, repo, number } = parsePullUrl(prUrl);
const octokit = await ctx.host.github.getOctokit();
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: number,
});
const { data: files } = await octokit.rest.pulls.listFiles({
owner,
repo,
pull_number: number,
per_page: 100,
});
const complexity = rateComplexity(
pr.additions + pr.deletions,
pr.changed_files
);
return {
title: pr.title,
author: pr.user?.login,
state: pr.state,
draft: pr.draft ?? false,
additions: pr.additions,
deletions: pr.deletions,
changedFiles: pr.changed_files,
complexity,
files: files.map((file) => ({
path: file.filename,
additions: file.additions,
deletions: file.deletions,
// Large changes get a stats-only skim; a human reads the code.
...(complexity === "large" ? {} : { patch: trimPatch(file.patch) }),
})),
};
},
});Verify complexity classification
Run inspect_pr against a small pull request:
bash
agent-sdk call inspect_pr --dir . \
--input '{"prUrl":"https://github.com/facebook/react/pull/35623"}'Confirm the result contains "complexity": "trivial".
Run it against a large pull request:
bash
agent-sdk call inspect_pr --dir . \
--input '{"prUrl":"https://github.com/facebook/react/pull/25229"}'Confirm the result contains "complexity": "large" and no patch fields.
Add the review tool
Create agent/tools/submit_review.ts to post each verdict:
ts
import { defineTool } from "@cursor/july/tools";
import { z } from "zod";
import { parsePullUrl } from "../lib/github.js";
export default defineTool({
description:
"Post the review decision to GitHub: approve the pull request, or comment asking for a human review.",
inputSchema: z.object({
prUrl: z
.string()
.describe("Pull request URL: https://github.com/owner/repo/pull/123"),
verdict: z.enum(["approve", "request_human_review"]),
summary: z
.string()
.describe("One or two sentences explaining the verdict."),
}),
async execute({ prUrl, verdict, summary }, ctx) {
const { owner, repo, number } = parsePullUrl(prUrl);
const review =
verdict === "approve"
? { event: "APPROVE" as const, body: `PR reviewer: ${summary}` }
: {
event: "COMMENT" as const,
body: `PR reviewer: this change needs a human review. ${summary}`,
};
const octokit = await ctx.host.github.getOctokit();
await octokit.rest.pulls.createReview({
owner,
repo,
pull_number: number,
event: review.event,
body: review.body,
});
return { posted: true, ...review };
},
});Use a GitHub credential with write access and a pull request you didn't author.
Review a pull request
Review an open pull request:
bash
agent-sdk run --dir . \
--message "Review https://github.com/acme/checkout/pull/42"Confirm inspect_pr runs before submit_review, then verify the review on GitHub.
Trigger reviews from GitHub
Create agent/channels/github.ts:
ts
import {
defaultGitHubAuth,
githubChannel,
} from "@cursor/july/channels/github";
const REVIEW_ACTIONS = new Set(["opened", "reopened", "ready_for_review"]);
export default githubChannel({
botName: "sdk-pr-reviewer",
webhookEvents: ["pull_request"],
deliverReplies: false,
progress: { reactions: false },
onPullRequest: (ctx, pr) => {
if (!REVIEW_ACTIONS.has(pr.action) || pr.draft) {
return null;
}
return {
auth: defaultGitHubAuth(ctx),
title: `Review ${ctx.repository.fullName}#${pr.number}`,
context: [
"",
`Review ${pr.url}. Inspect it first, then submit your verdict with submit_review.`,
],
};
},
});Keep agent-sdk dev running. In a second terminal, replay an open pull request. Pull access is enough; repo admin isn't required:
bash
agent-sdk github replay https://github.com/acme/checkout/pull/42 \
--dir . --action openedOpen the playground and confirm Review acme/checkout#42 contains the trigger, both tool calls, and the verdict.
Run options
CLI message
bashagent-sdk run --dir . --message "Review https://github.com/acme/checkout/pull/42"Local replay
bashagent-sdk github replay https://github.com/acme/checkout/pull/42 \ --dir . --action openedForward or
--cursor-eventsPull Cursor events:
bashagent-sdk serve --dir . --cursor-events --repo owner/repoForward GitHub webhooks:
bashagent-sdk github forward --dir .Keep the local
devorserveprocess running.Hosted deployment
Deploy once, set the GitHub App secrets, then redeploy:
bashagent-sdk deploy agent-sdk secrets set sdk-pr-reviewer GITHUB_APP_ID GITHUB_APP_PRIVATE_KEY agent-sdk deploy
See GitHub for local event delivery and Deployment for hosting.
Where to go next
examples/approval-buddy: an example with commit statuses, review subagents, and a deterministic stamp policy- Evals: freeze these two PRs as regression checks so prompt changes can't flip a verdict
- Tools: more on typed tools, approvals, and direct calls
- GitHub: fixtures, forwarding, and pulling events from Cursor
- Building agents with agents: have a coding agent extend the project for you