Skip to content

Keep PR approval policy deterministic with Approval Buddy

Approval Buddy approves eligible pull requests from a fixed roster and declines every other request. GitHub still blocks self-approval when the stamp identity authored the PR. Code decides eligibility. The model prepares evidence, runs two specialist reviews, and passes their findings to the approval tool without changing the policy decision.

Use this example when an agent can make a judgment inside a workflow, but authorization and the final side effect must stay in deterministic code.

Browse the Approval Buddy source.

Keep approval policy in code

Approval Buddy draws three hard boundaries:

  • prepare_review and approve_pr re-read the live PR and apply the same eligibility rules.
  • Two subagents inspect prepared evidence, but their findings never grant or block approval.
  • Only approve_pr posts the GitHub review.

A spoofed webhook, Slack message, or model claim can't add someone to the buddy roster. The mutating tool checks the source of truth immediately before it acts.

Follow the intended stamp flow

The root instructions ask the model to run this sequence for a qualifying PR:

  1. A non-draft pull_request event arrives with action opened, reopened, or ready_for_review.
  2. The GitHub channel checks its repository allowlist and starts a session.
  3. turn.started posts a pending commit status.
  4. The model calls prepare_review.
  5. Host code fetches the live PR. It checks the author, open state, merged state, and draft state.
  6. A qualifying PR gets pr/MANIFEST.md, pr/meta.json, and pr/diff.patch in the session workspace. Diffs above 2,000,000 characters are truncated and marked in metadata.
  7. The model calls both review subagents through the built-in task tool.
  8. It concatenates their contracted replies and calls approve_pr.
  9. approve_pr re-runs eligibility, posts an APPROVE review, and returns the outcome.
  10. The channel posts a final commit status. A self-approval block also gets a short timeline comment because no approval review can appear.

Ineligible PRs skip evidence and subagents. The model still calls approve_pr so the deterministic tool returns the formal decline reason.

Steps 4 through 9 are prompt-driven. The channel doesn't enforce tool order or prove both subagents ran, and approve_pr accepts missing findings. A failed turn clears the pending status with a green non-blocking result without approving the PR.

Map the framework features

CapabilitySourceRole
Root agent and policy promptagent/agent.ts, agent/instructions.mdConfigure the local agent and describe orchestration order.
GitHub channelagent/channels/github.tsFilter wakes, lease GitHub access, and publish status events.
Slack channelagent/channels/slack.tsAccept approval-bot stamp and qualification requests.
Server toolsagent/tools/Prepare evidence, approve, list buddies, and search GIFs.
Deterministic policyagent/lib/approve.ts, agent/lib/buddies.tsOwn the roster and live eligibility checks.
Review subagentsagent/subagents/Run deep audit and code-quality passes over the same evidence.
Storageagent/storage.tsPersist sessions and events with cursorHostedStorage (Bugbot agent_serve_*).
Live A/B experimentagent/ab.tsCompare baseline responses with a concise, presentation-only treatment (concise-results).
Evals and unit testsevals/, agent/lib/Protect routing, output contracts, policy, and GitHub behavior.

There are no authored skills, MCP connections, schedules, reminders, hooks, sandbox seeds, or tool approvals.

Prepare credentials

You need:

  • Node 22.13 or newer.
  • An agent-runtime credential.
  • GitHub access to read PRs, post reviews, create commit statuses, and post the self-approval visibility comment.

Optional GIF selection uses:

  • GIPHY_API_KEY or APPROVAL_BUDDY_GIPHY_API_KEY,
  • APPROVAL_BUDDY_STAMP_GIF, or
  • severity-specific APPROVAL_BUDDY_STAMP_GIF_<LEVEL> variables.

If you enable Giphy in a hosted copy, declare its secret and api.giphy.com egress.

Validate without approving a PR

bash
agent-sdk validate --dir examples/approval-buddy
agent-sdk info --dir examples/approval-buddy --json

List the deterministic roster:

bash
agent-sdk call list_buddies \
  --dir examples/approval-buddy \
  --input '{}'

Set a known merged PR, then run the read-only precheck:

bash
MERGED_PR_URL=https://github.com/your-org/your-repo/pull/123
agent-sdk call prepare_review \
  --dir examples/approval-buddy \
  --input "{\"prUrl\":\"$MERGED_PR_URL\"}"

The result should decline because the PR is no longer open. prepare_review never posts an approval.

CAUTION

Don't use agent-sdk call approve_pr as a smoke test. The tool has no needsApproval gate and posts a real GitHub review when the PR qualifies.

See why preparation is separate

prepare_review is read-only. It checks policy before fetching a large diff, so declined requests don't spend review-agent work.

Direct calls return the evidence file map because their scratch workspace is deleted after the call. In-session calls write the tree to ctx.workspaceDir, where both subagents can read it.

approve_pr repeats the live check instead of trusting preparation. A PR can close, merge, become a draft, or change author-related context between the two steps. Revalidation keeps the final write bound to current state.

This is a reusable two-tool pattern:

  • a read-only tool prepares and explains the decision,
  • a mutating tool repeats policy at the side-effect boundary.

Fan out two review contracts

The two discovered subagents have different contracts:

  • The security reviewer reports bugs, breaking changes, and security findings with High, Medium, or Low tags.
  • The code-quality reviewer reports maintainability and structure concerns with Blocker, Major, or Minor tags.

The parent calls both through the harness task tool. They inherit the root agent's execution surface and read the same pr/ workspace. The prompt asks the parent not to rewrite either reply. The review body trims the combined text and caps it at 16,000 characters.

Findings are informational. A high-severity finding doesn't veto the stamp. That policy is explicit in the root instructions and approval code.

Trace GitHub channel behavior

The channel uses githubChannel with:

  • a configured repository allowlist on the account-linked GitHub transport,
  • a second optional APPROVAL_BUDDY_REPOS wake filter,
  • deliverReplies: false,
  • progress reactions disabled, and
  • event handlers for turn start, approve_pr results, and failed turns.

The source requests contents-write, even though the documented workflow posts reviews, statuses, and comments. When adapting the example, start with pr-write and opt up only if a tool must push code.

Every terminal status is green by design. Declines and crashed turns are informational, not merge-blocking. This is a product decision in the example, not an Agent SDK default.

A successful turn that never calls approve_pr leaves the pending status in place. The channel clears it on approve_pr results and turn.failed, but has no turn.completed fallback.

github replay reaches the same channel and can post a real approval, status, or comment. Use replay only against a repository and PR created for this test.

Use Slack for explicit requests

Start the dev server:

bash
agent-sdk dev examples/approval-buddy

Then ask through the signed-in account-linked Slack connection:

Would this PR qualify for a stamp?

The instructions route qualification questions to prepare_review only. A stamp request runs the complete flow and may approve the PR.

This channel uses the account-linked transport instead of a dedicated Socket Mode app.

See how durable storage fits

defineStorage replaces the default local session store with a shared, durable key-value adapter. Approval Buddy chooses:

  • a 15-second write debounce,
  • startup restoration for up to 200 sessions, and
  • a 14-day restore window.

That policy fits long-lived Slack threads and a small webhook fleet. The security reviewer uses the same adapter with lazy restore, which fits its shorter sessions.

Run the regression suite

List the four eval cases:

bash
agent-sdk eval --dir examples/approval-buddy --list

The suite covers:

  • buddy-list routing,
  • declining a merged PR,
  • using only prepare_review for a qualification question, and
  • the combined findings headings and severity format over seeded evidence.

Run the safe qualification case:

bash
agent-sdk eval \
  --dir examples/approval-buddy \
  qualify/merged-pr-question \
  --json

The qualification case reads a live merged PR. The seeded format case shown by --list uses a planted auth-bypass diff and checks for a task call, both headings, and severity tags. It doesn't prove both named subagents ran or whether their output reached approve_pr. Unit tests under agent/lib/ cover policy, self-approval handling, evidence limits, status mapping, GIF selection, and severity parsing.

Reuse the policy boundary

Keep these properties when you replace the buddy policy:

  1. Put authorization in typed code.
  2. Fetch the source of truth inside both prepare and mutate steps.
  3. Give the model evidence only after the request qualifies.
  4. Treat specialist findings as data, not authority.
  5. Keep the core domain mutation in one named tool. Treat channel status and visibility writes as separate, audited effects.
  6. Add a human approval gate if your policy still needs operator consent.
  7. Test read-only routing separately from mutation.

Where to go next