---
title: "PR Review Agents"
slug: pr-review-agents
section: Governance & Operations
status: stable
description: "Two cooperating services review pull requests automatically: `pr-review-eligibility-agent`, a Cloud Run webhook receiver that decides whether a PR is ready for review, and `pr-review-agent`, a Vertex AI Agent Engine agent that performs t…"
last-updated: 2026-10-07
---

# PR Review Agents

Two cooperating services review pull requests automatically: `pr-review-eligibility-agent`, a Cloud Run webhook receiver that decides whether a PR is ready for review, and `pr-review-agent`, a Vertex AI Agent Engine agent that performs the review and publishes one verdict. Neither can merge, in any mode. [1] [2]

## Overview

The human-facing review process (Gate 0 to Gate 7) is described on [PR and Code Review Process](/docs/onboarding-pr-review). This page describes the two services that run it and how repositories are enrolled. The fleet context is on [Vertex Agents](/docs/vertex-agents), and the follow-on documentation step is on [Docs Update Agent](/docs/docs-update-agent). [3] [4] [5]

The pair replaced an earlier single-file reviewer that was deleted on 2026-08-07 after it proved unreliable; the current `pr-review-agent` is a from-scratch rebuild under the same name, split so that the dispatcher decides readiness and the reviewer only reviews. [1] [2]

> **Code-verified baseline**
>
> Read against `main` of `vertex-agents` at commit `d1c8408` (2026-10-02): `pr-review-eligibility-agent/agent.py`, `pr-review-agent/agent.py`, `shared/asana_god_file_ticket.py` and `registry.toml`. Both are registered for the `development` environment only. [6]

## End-to-End Flow

1. A CI check suite completes on a PR's head commit and GitHub sends a `check_suite` webhook to the eligibility agent. [7]
2. The eligibility agent re-verifies everything live (it never trusts the webhook payload) and, if the PR is eligible, calls `pr-review-agent` synchronously through its Agent Engine `query` method. [7]
3. The review agent runs deterministic prechecks, then one Gemini call for the Gate 0 to Gate 7 judgement using the live `pr-review` gateway skill, and returns a structured verdict. [2]
4. In `review_only` mode the review agent posts exactly one native GitHub review and one Teams message to the PR Review channel. [2]
5. When the PR later merges, the eligibility agent posts a one-time "Merged" Teams message, files an audit record and hands the PR to the Docs Update Agent. [7]

## Eligibility Agent

The service is deployed to Cloud Run in project `skilled-script-469819-f2` with unauthenticated access allowed on purpose: GitHub's webhook infrastructure cannot present a Cloud Run identity token, so `/webhook/github` is protected by an HMAC signature check on the raw body, while `/status` and `/run/recheck` require an API key. [8] [7]

### Eligibility Gates

A PR is dispatched for review only when all of these hold, each re-checked against live GitHub data through the gateway: [7]

1. The PR author is not `dependabot[bot]` (those PRs are triaged and merged by `pr-hygiene-agent`; sending them to an LLM reviewer as well produced duplicate cost and contradictory outcomes).
2. The PR is open and not a draft.
3. The base branch is in the repo's configured `baseBranches`.
4. The PR's current head SHA exactly matches the SHA the event fired for.
5. Every check name in the repo's `requiredChecks.names` is `completed` with conclusion `success` on that SHA. Missing, pending, neutral, skipped, cancelled or failed checks make the PR ineligible; there is no partial credit.

A repo with an empty required-checks list has no CI gate: every completed check suite is trivially "all green". [7]

The only dispatch trigger is `check_suite`. `workflow_run` was removed because it fires once per workflow file and raced with `check_suite`; if the GitHub App is still subscribed to it, those deliveries are ignored as an unsupported event type. A separate `pull_request` event with action `closed` drives the landed-PR path. [7] [9]

### Review Cycles

A review cycle is keyed by `repo:pr_number:head_sha:policy_version`, with policy version `v1`. State is stored one document per key in a dedicated Firestore database named `pr-review-eligibility-agent`, with a native TTL of 30 days. [7]

- A duplicate delivery for a cycle already `completed` or `superseded` is a no-op.
- A new head SHA marks the PR's older unresolved cycle `superseded`, and a superseded cycle can never publish.
- A Firestore transaction claims the cycle before dispatch, so concurrent deliveries cannot double-dispatch.
- A transient Agent Engine error (reauthentication, 503, service unavailable, deadline exceeded) is retried twice, after 2 and 5 seconds. Any other error marks the cycle `error` and sends a Teams alert.
- An `automation_error` verdict returned by the reviewer keeps the cycle retryable instead of caching it as completed, so a transient GitHub failure does not leave a commit permanently unreviewed.

Source: [7]

## Repo Scope and Policies

The eligibility agent holds a hard-coded `REPO_POLICIES` map in `agent.py`. A repo is reviewed only if it is in that map, in the GitHub owner `Alliance-Strategies`, and still confirmed in scope by the Nexus knowledge graph. [7] [10]

| Repo | Base branch | Mode | Required checks |
| --- | --- | --- | --- |
| `BackOfficePrototype` | `development` | `review_only` | 14 named checks (slopgate, lockfile sync, type check, lint, 8 unit-test shards, coverage gate, dependency setup) |
| `HostPortalPrototype` | `development` | `review_only` | none configured |
| `PPPToo` | `development` | `review_only` | 2 (`secret-scan (gitleaks)`, `forbidden-file-categories`) |
| `alliance-gcp-infra` | `main` | `review_only` | none configured |
| `WildPacksApplicationForm` | `development` | `review_only` | none configured (ruleset requires approval only) |
| `branching-sandbox` | `main` | `review_only` | `quality-gate` |
| `form-engine-portable`, `gcp-connector`, `globus-translation-infra`, `SevisSDK` | `main` | `review_only` | none configured |
| `n8n-workflows`, `PartnerPortal` | `development` | `review_only` | none configured |
| `nexus-docs` | `main` | `review_only` | `Typecheck & Build` |
| `alliance-mcp-gateway` | `main` | `review_only` | none (no branch protection) |

Source: [7]

Modes per repo are `disabled` (ignored), `observe` (full eligibility and review, verdict recorded, nothing posted) and `review_only`. There is no merge-capable mode; that is a code invariant, not a flag. [7]

The knowledge-graph scope check compares `REPO_POLICIES` against the repos `registry.toml` lists for this agent, read through the `nexusdocs_graphNeighbors` gateway tool with a 5 minute cache. When the graph affirmatively says a repo is no longer scoped, eligibility for that repo is blocked and a Platform Pulse alert is posted. A graph or gateway outage is treated as no signal and does not block. [7] [11]

The registry `repos` array for the eligibility agent lists the same 14 repos; for `pr-review-agent` it lists `BackOfficePrototype`, `HostPortalPrototype`, `PPPToo` and `branching-sandbox`. The array alone does not enable review: `REPO_POLICIES` is the actual eligibility gate. [6]

## Review Agent

`pr-review-agent` is the class `PrReviewAgent`, deployed as a Vertex AI Agent Engine resource (update-in-place deploys via `deploy.py`). It is stateless: it does not subscribe to webhooks, does not judge CI eligibility and does not dedupe cycles. [2] [12] [16]

### Modes and Verdicts

| Mode | Review runs | GitHub review posted | Teams message posted |
| --- | --- | --- | --- |
| `disabled` | no | no | no |
| `observe` | yes | no | no |
| `review_only` | yes | yes, exactly one | yes, exactly one |

Source: [12]

The verdict is one of `approved`, `changes_requested`, `commented_pending`, `skipped` or `automation_error`. Only the first three come from the model and only they are published, mapped to GitHub review events `APPROVE`, `REQUEST_CHANGES` and `COMMENT`. `skipped` and `automation_error` come from deterministic logic and are never published. [2]

GitHub rejects an approval or change request from the PR's own author, so for a PR opened by the agent's own gateway identity the review is deterministically downgraded to `COMMENT` with the real verdict in the body. A guard checks whether the bot already reviewed the commit before posting, and the Teams message is sent only when the GitHub review was actually posted. [2] [6]

Gate 0 to Gate 7 policy is fetched live from the gateway `pr-review` skill on every call, with the Merge Procedure section stripped before it reaches the model, so the agent never drifts from the written policy. If the fetch fails a short built-in fallback summary is used. The model is `gemini-2.5-pro` by default, with one model call per review and a diff cap of 200000 characters. If GitHub refuses to return a diff over its 20,000-line cap, the agent reconstructs one from per-file patches. [2] [12]

### Deterministic Prechecks

Several checks have a ground truth that can be fetched, so they run in code before the model and the model is told the result rather than left to infer it. [2]

| Precheck | Behavior |
| --- | --- |
| Stale head | If the PR head no longer matches the requested SHA, the cycle is `skipped` as superseded |
| Draft | `skipped` |
| File-data completeness | Changed files are fetched with an adaptive page size (100, 25, 5, 1); if the list still cannot be completed the verdict is an automatic `SLOP` `changes_requested` [12] |
| PPP spec-only | `ParticipantPortalPrototype` PRs touching only markdown under `features/` or `openspec/requirements/` are auto-approved [12] |
| Gate 0 Sub-check A | `mergeable_state` of `dirty` gives an automatic `SLOP` `changes_requested` without running specialists; an `unknown` state is re-read after 5 seconds |
| Gate 2 (skill enforcement) | Uses the CI check conclusion first, then a PR comment, committed report or commit-message line; always returns PASS, FAIL or UNCONFIRMED |
| Gate 4 (endpoint snapshot) | If `server/routes/endpointSchemas/` is touched, `generatedAt` must be newer than the PR's creation; otherwise a fixed PASS |
| Gate 5 (environment files) | A `deploy/env/*.yaml` path referenced by a changed workflow is checked for existence at its exact case-sensitive path at the head commit |
| Gate 0 Sub-check F | Deterministic god-file scoring (next section) |

The required-check results the eligibility agent already verified are passed through as ground truth for Gate 1, so the model never infers CI status from `mergeable_state`. [7]

## God-File Handling

Sub-check F scores every touched `.ts`, `.tsx`, `.js` and `.jsx` file by its current total line count at the head commit (not the lines in the diff). The hard threshold defaults to 800 lines and the emerging threshold to 70 percent of that, 560 lines; both are overridable by environment variables. [2]

Since 2026-09-18 god-file findings are advisory only. They are reported under a separate "God-File (advisory)" heading and can never cause `changes_requested` or a `SLOP` result by themselves; a deterministic safety net overrides the model's verdict if every block-severity finding is a god-file finding. [2]

In `review_only` mode only, for each qualifying file the agent:

1. Drops files already reported on this PR at the same or higher tier, so re-reviews stay quiet (a file is reported again only when it rises from emerging to hard).
2. Files one ticket per file in the Platform Pulse intake project, titled `[God File] repo/path`, assigned to the PR author when the author's commit email matches an Asana user, otherwise to a default owner. If an open ticket with that exact title exists anywhere in the project it receives a comment instead.
3. Posts one standalone PR comment listing each file with its ticket or "already tracked" link, stating that it does not block the PR.

Source: [2] [13]

The ticket module is a scoped exception to the Vertex-agent ticket ban and must not be removed in a cleanup pass without confirming with the owner. [13] Observe mode never files tickets or posts comments. [2]

## After a PR Lands

When GitHub reports that a PR in a `review_only` repo merged, the eligibility agent runs once per PR (claimed by a Firestore create): it creates the merge-record task, hands the PR to the Docs Update Agent, then posts a "Merged" Teams message to the PR Review channel. A failure in any step is logged and does not fail the webhook ack. [7]

The merge record is an already-completed task in the "Completed (DoD)" section of the Platform Pulse Asana project, created through the gateway by an Asana-scoped robot role that can only create and update tasks; its name starts with the agent name and its notes record author, merger and merge commit. [14]

## Onboarding a Repo

To bring a repository under automated review:

1. Read the repo's real branch protection and rulesets and record the required check names exactly. A stale or wrong `names` list is a silent total outage for that repo: if CI renames a required job (for example, changing the shard count), every PR stays ineligible until `REPO_POLICIES` is updated in the same change. [7]
2. Add a `REPO_POLICIES` entry (`baseBranches`, `mode`, `requiredChecks` with a `source` note, `teams`).
3. Add the repo to the `repos` array of the `pr-review-eligibility-agent` entry in `registry.toml` (and to `pr-review-agent` if it should be declared there). Without it the knowledge-graph scope check blocks the repo. [6] [7]
4. Make sure the GitHub App is subscribed to `check_suite` and `pull_request` events for the repo. [7]
5. Merge to `main`. Changes to the agent directory, `shared/**` or `registry.toml` redeploy the Cloud Run service; changes to the reviewer redeploy its engine in place. [15] [12]
6. Typical rollout so far has gone straight to `review_only` on instruction rather than waiting in `observe`. [7]

## Operations

- `GET /status` (API key) lists each repo's mode, base branches, required checks and source. `POST /run/recheck` with `repo` and `number` re-runs eligibility and dispatch against the PR's current head SHA, which is the supported way to retry a stuck PR. [7]
- A repo whose required checks can never be satisfied produces no log output on the ineligible path, which looks identical to the bot not running. [7]
- The dispatcher sends a Platform Pulse alert for dispatch crashes and for verdicts larger than 500000 characters. [7]

## Known Gaps

- The registry trigger text for the eligibility agent still lists `workflow_run`, which is no longer handled. [6] [7]
- The eligibility agent's module docstring still names the earlier "Merged PRs (audit)" Asana project as the merge-record destination; the shared module writes to the Platform Pulse "Completed (DoD)" section. [7] [14]
- The god-file ticket module's header comment still says Foundation Hardening; the code files into the Platform Pulse intake project. [13]
- The [PR and Code Review Process](/docs/onboarding-pr-review) page still describes the dispatcher as listening for `workflow_run` events. [3] [7]

## References

[1]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/registry.toml "registry.toml (pr-review-agent history notes)"
[2]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/pr-review-agent/agent.py "pr-review-agent/agent.py (prechecks, verdicts, god-file handling)"
[3]: /docs/onboarding-pr-review "PR and Code Review Process"
[4]: /docs/vertex-agents "Vertex Agents"
[5]: /docs/docs-update-agent "Docs Update Agent"
[6]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/registry.toml "registry.toml pr-review entries"
[7]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/pr-review-eligibility-agent/agent.py "pr-review-eligibility-agent/agent.py"
[8]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/pr-review-eligibility-agent/cloudbuild.yaml "pr-review-eligibility-agent/cloudbuild.yaml"
[9]: https://github.com/Alliance-Strategies/vertex-agents/pull/155 "PR 155: remove workflow_run as a PR-review dispatch trigger"
[10]: https://github.com/Alliance-Strategies/vertex-agents/pull/173 "PR 173: onboard 8 repos into REPO_POLICIES"
[11]: https://github.com/Alliance-Strategies/vertex-agents/pull/151 "PR 151: block eligibility on knowledge-graph scope drift"
[12]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/pr-review-agent/README.md "pr-review-agent/README.md"
[13]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/shared/asana_god_file_ticket.py "shared/asana_god_file_ticket.py"
[14]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/shared/asana_merge_record.py "shared/asana_merge_record.py"
[15]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/.github/workflows/deploy-pr-review-eligibility-agent.yml "deploy-pr-review-eligibility-agent.yml"
[16]: https://github.com/Alliance-Strategies/vertex-agents/blob/main/.github/workflows/deploy-pr-review-agent.yml "deploy-pr-review-agent.yml"
