Skip to content

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

Prerequisites

  • Node 22.13 or newer. Bun isn't supported.
  • Run commands as agent-sdk <command>, or use npx @cursor/july <command> when the CLI isn't on PATH. See Run the CLI for monorepo checkouts and other setups.
  • A Cursor credential for model turns. Sign in once:
bash
agent-sdk login

You can also set CURSOR_API_KEY instead of signing in.

  • Authenticate with gh auth login or GITHUB_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 dev

Keep 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.ts

Add 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 opened

Open the playground and confirm Review acme/checkout#42 contains the trigger, both tool calls, and the verdict.

Run options

  1. CLI message

    bash
    agent-sdk run --dir . --message "Review https://github.com/acme/checkout/pull/42"
  2. Local replay

    bash
    agent-sdk github replay https://github.com/acme/checkout/pull/42 \
      --dir . --action opened
  3. Forward or --cursor-events

    Pull Cursor events:

    bash
    agent-sdk serve --dir . --cursor-events --repo owner/repo

    Forward GitHub webhooks:

    bash
    agent-sdk github forward --dir .

    Keep the local dev or serve process running.

  4. Hosted deployment

    Deploy once, set the GitHub App secrets, then redeploy:

    bash
    agent-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