An AI review agent in CI is only worth the trouble if it fails loudly, flakes rarely, and never blocks a merge on vibes. This article walks through a pipeline design that does all three: a hook that fires on every pull request, a fixture suite that tests the reviewer before it reviews, and a severity-based gate with a visible escape hatch. The cost model matters too. With free model access and a free server option, the per-PR price of this gate is zero — and that changes when you can afford to run it.
Why most AI review gates annoy everyone
Three failure modes kill AI review gates in practice.
First, cost. A paid model call on every PR adds up fast, so teams run the reviewer only on big PRs — exactly when it's least useful. Second, infrastructure. A dedicated runner that sits idle between PRs is a bill you can see. Third, flakiness. The same diff gets a pass on Monday and a block on Tuesday, and nobody trusts the gate anymore.
This design addresses all three. The agent runs in a short-lived CI job on the free server option. The model calls use free model access. And a fixture suite keeps the output honest.
The pieces
MonkeyCode is an open-source AI coding agent you can run from the command line. In this pipeline it plays one role: turn a diff into a structured review report. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The pipeline has four parts: a hook, a wrapper, a gate, and a fixture suite. You can steal all four and swap the agent for any CLI tool that emits JSON.
Step 1: The hook
The hook is a GitHub Actions workflow. It fires on opened and synchronize so the gate re-runs when you push. It also fires on labeled and unlabeled — that's the override mechanism.
name: ai-review-gate
on:
pull_request:
types: [opened, synchronize, labeled, unlabeled]
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run the reviewer
id: review
run: |
./scripts/review_agent.sh \
--base "${{ github.event.pull_request.base.sha }}" \
--head "${{ github.event.pull_request.head.sha }}" \
--out /tmp/review.json
- name: Upload the report
uses: actions/upload-artifact@v4
with:
name: review-report
path: /tmp/review.json
- name: Gate on severity
env:
HAS_OVERRIDE: ${{ contains(github.event.pull_request.labels.*.name, 'override:ai-review') }}
run: |
if [ "$HAS_OVERRIDE" = "true" ]; then
echo "override:ai-review present — passing with override logged"
exit 0
fi
python scripts/gate.py /tmp/review.json
Two details matter. The fetch-depth: 0 checkout gives the wrapper the full history it needs to diff two SHAs. And the gate step reads the override label from the event payload, not from the repo, so a maintainer can unblock a PR without touching the branch.
Step 2: The wrapper
The wrapper keeps the agent call in one place. It takes two SHAs, produces a diff, and hands it to the agent. The CLI shape below is a placeholder; check your installed agent version for the exact flags.
#!/usr/bin/env bash
set -euo pipefail
BASE=""; HEAD=""; OUT=""; DIFF=""
while [[ $# -gt 0 ]]; do
case "$1" in
--base) BASE="$2"; shift 2 ;;
--head) HEAD="$2"; shift 2 ;;
--diff) DIFF="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
if [[ -n "$DIFF" ]]; then
DIFF_FILE="$DIFF"
else
DIFF_FILE=$(mktemp)
git diff --unified=3 "$BASE" "$HEAD" > "$DIFF_FILE"
fi
monkeycode review \
--diff "$DIFF_FILE" \
--repo "$PWD" \
--format json \
--out "$OUT"
Keep the wrapper thin. The fixture suite will call it hundreds of times.
Step 3: The fixtures
Here's the part most people skip. Before you let an AI gate block real PRs, you need to know what it does on diffs you already understand. Create a fixtures/ directory with four kinds of cases:
-
bug.diff— a real, injected bug (off-by-one, missing null check) -
security.diff— an injection or a leaked secret -
clean.diff— a clean refactor with no findings -
noise.diff— style nits only
A runner executes the reviewer against each fixture and compares the result with an expected JSON file.
#!/usr/bin/env bash
set -euo pipefail
PASS=0; FAIL=0
for fixture in fixtures/*.diff; do
name=$(basename "$fixture" .diff)
expected="fixtures/${name}.expected.json"
./scripts/review_agent.sh --diff "$fixture" --out "/tmp/${name}.json"
if python scripts/compare_findings.py \
--actual "/tmp/${name}.json" \
--expected "$expected"; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
echo "FAIL: $name"
fi
done
echo "fixtures passed: $PASS, failed: $FAIL"
[ "$FAIL" -eq 0 ]
compare_findings.py compares the severity and location of each finding against the expected JSON — strict on critical, lenient on nits. Three numbers come out of the suite: recall (does it find the injected bug?), false positives (does it stay quiet on clean?), and stability (does it give the same answer three times in a row?). If any of the three is bad, the gate is not ready for real PRs.
Step 4: Flake control
Flakes are what kill trust in AI gates. Five rules keep them down:
- Pin the model version in the agent config. Unpinned models drift, and the gate changes behavior without a code change.
- Set temperature to 0 if the model supports it. Review output should be reproducible.
- Retry only on transport or parse errors, never on content disagreement. A model that says "clean" twice and "bug" once is a flake you want to see, not retry away.
- Give the agent an explicit abstain path. A low-confidence "maybe" should never block a merge.
- Run every prompt or config change through the fixture suite before it reaches the gate.
Step 5: The gate
The gate is a small script that reads the report and exits 0 or 1. GitHub turns that into a required status check.
#!/usr/bin/env python3
import json
import sys
report = json.load(open(sys.argv[1]))
blocking = [f for f in report.get("findings", [])
if f.get("severity") == "critical"]
for finding in blocking:
print(f"BLOCK: {finding.get('file')} — {finding.get('message')}")
sys.exit(1 if blocking else 0)
The decision table is the contract between the agent and the team:
| Severity | Example | Gate behavior |
|---|---|---|
| critical | SQL injection, auth bypass, secret in diff | Block merge; human review required |
| warning | null deref, resource leak, race | Comment on the PR; no block |
| nit | naming, formatting | Ignored |
| abstain | confidence below threshold | Pass; logged for audit |
Step 6: The green-to-merge path
Here's the full loop:
- You push a PR. The hook fires.
- The reviewer runs in a short-lived job on the free server option. A few minutes per PR is the right shape for this; it's not a production runner.
- The report is uploaded as an artifact. Every decision is auditable later.
- The gate exits 0 or 1. No critical findings means the check is green.
- A critical finding turns the check red. A maintainer can add the
override:ai-reviewlabel, which re-triggers the workflow and lets the gate pass — but the override is visible in the log and the label history, not silent.
The override is the part that keeps the gate honest. An AI reviewer that cannot be overridden becomes a process problem. One that logs its overrides stays a tool.
What this costs
The reason this pattern is worth building now is the cost curve. The job runs on the free server option, and the model calls use free model access. As of this writing, the free tier includes 10 million tokens, which covers a lot of small diffs; check the project repo for current terms, because free tiers change.
Compare that with the alternative: a paid model on a dedicated runner, used only on PRs that feel risky. The free tier flips the default. You can afford to review every PR, and the fixture suite keeps the reviewer cheap to trust.
Limitations and who should skip this
The reviewer sees a diff, not the whole codebase. It will miss cross-file issues that a human with context would catch. Free model access usually means shared or rate-limited resources, so don't build a gate that needs a 99.9% SLA. And the fixture suite is a maintenance cost: if the fixtures rot, the gate lies.
Skip this pattern if you're under a compliance regime that requires human-only review, or if your PRs are so large that the diff doesn't fit the model's context. If your team won't maintain the fixtures, the gate will quietly become noise — which is worse than no gate at all.
Start with four diffs
The fixture directory is the real deliverable here. Start with four diffs, wire the hook, and let the gate earn trust before you make it required. If you want to try the pattern, the MonkeyCode repo has the agent and the free tier — the scripts above are a good starting point.
Top comments (0)