Skip to main content

Command Palette

Search for a command to run...

BDK

Build a PR reviewer with BDK

Build a PR reviewer with the Bot Development Kit (BDK). It classifies changes as trivial, moderate, or large, then approves safe changes or requests human review.

Prerequisites

  • Node 22.13 or newer.

  • The @cursor/bdk package:

npm install -g @cursor/bdk
  • A Cursor credential for model turns. Sign in once:
bdk login

Create and run the project

The preferred BDK workflow uses bdk init and bdk dev.

bdk init ./pr-reviewercd pr-reviewerbdk dev

Keep bdk dev running. In a second terminal, run:

bdk run --dir . --message "Introduce yourself in one sentence."

Confirm the agent replies.

Add review instructions

Replace bot/instructions.md:

# PR reviewerYou 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.

Add the inspect tool

Create bot/tools/inspect_pr.ts:

import { defineTool } from "@cursor/bdk/tools";import { z } from "zod";export type Complexity = "trivial" | "moderate" | "large";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 url = new URL(prUrl);    const [owner, repo, pulls, number] = url.pathname.split("/").filter(Boolean);    const pull_number = Number.parseInt(number ?? "", 10);    if (pulls !== "pull" || owner === undefined || Number.isNaN(pull_number)) {      throw new Error(`Not a pull request URL: ${prUrl}`);    }    const octokit = await ctx.host.github.getOctokit();    const { data: pr } = await octokit.rest.pulls.get({      owner,      repo,      pull_number,    });    const { data: files } = await octokit.rest.pulls.listFiles({      owner,      repo,      pull_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,        ...(complexity === "large" ? {} : { patch: trimPatch(file.patch) }),      })),    };  },});

Verify complexity classification

Run inspect_pr against a small pull request:

bdk 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:

bdk 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 bot/tools/submit_review.ts to post each verdict:

import { defineTool } from "@cursor/bdk/tools";import { z } from "zod";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 url = new URL(prUrl);    const [owner, repo, pulls, number] = url.pathname.split("/").filter(Boolean);    const pull_number = Number.parseInt(number ?? "", 10);    if (pulls !== "pull" || owner === undefined || Number.isNaN(pull_number)) {      throw new Error(`Not a pull request URL: ${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,      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:

bdk 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 bot/channels/github.ts:

import {  defaultGitHubAuth,  githubChannel,} from "@cursor/bdk/channels/github";const REVIEW_ACTIONS = new Set(["opened", "reopened", "ready_for_review"]);export default githubChannel({  botName: "bdk-pr-reviewer",  cursorAccount: { repos: ["acme/checkout"] },  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.`,      ],    };  },});

Channel-turn context is a readonly string[], so wrap each context block in an array. The context label under progress.commitStatus is a separate string option.

Keep bdk dev running. In a second terminal, replay an open pull request. Pull access is enough; repo admin isn't required:

bdk 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
 bdk run --dir . --message "Review https://github.com/acme/checkout/pull/42"
  1. Local replay
 bdk github replay https://github.com/acme/checkout/pull/42 \   --dir . --action opened

Where to go next

  • Evals: freeze these two PRs as regression checks so prompt changes can't flip a verdict
  • Tools: more on typed tools, dry-run effects, and direct calls
  • GitHub: fixtures, forwarding, and pulling events from Cursor
  • Building agents with agents: have a coding agent extend the project for you