A review gate is a check on a run's plan or its final change. Each gate ends with a result such as `passed` or `failed`. A run that changed files cannot finish until every required gate is green for its current plan and change.

Gates only read. They never commit, push, merge or open a pull request. Syndai does those steps after the gates are green.

## The gates

| gate | looks at | what it checks |
|---|---|---|
| `planning_review` | the plan the agent writes before it builds | that the agent sent in a plan for this work |
| `security_review` | the final change | secrets in code, routes that skip sign-in checks, injection, open debug or admin routes, and server requests to a URL a user controls |
| `preflight` | the final change, its check results and the security review | that the checks ran and none that block are red, that `security_review` is green for this same change, and a scan of the diff for secrets, unsafe database rules and risky files |
| `adversarial_review` | the final change | a second, tougher review pass; runs only if you ask for it |

`planning_review`, `security_review` and `preflight` are always required. You cannot turn them off.

Syndai also runs its own reviews on the final change, like `code_review` and `simplify`. You may see them in a run's gate list. You cannot add or remove them in policy.

## Add a gate

You can require more gates on top of the three. Today the one you can add is `adversarial_review`.

Set it in `policy_config` on a run:

```json
{
  "policy_config": {
    "reviews": { "required_gates": ["adversarial_review"] }
  }
}
```

You can also set it in your saved policy defaults or in your repo's `syndai.yaml`. See [Policy](/docs/api/policy) and [syndai.yaml](/docs/api/syndai-yaml).

The three required gates are always added back, so you never need to list them. A gate name Syndai does not know is refused with `400`. So is a name of a step that changes the repo, like `merge` or `push`.

## When a gate holds a run

Gates are checked once, when the run is ready to finish. A gate that is not green holds the run there. Syndai tells the agent which gate holds it and why. The agent fixes what the review found, or sends in its plan for `planning_review`. The run goes on once every required gate is green.

A gate holds the run if it is:

- not run yet;
- `failed` or `blocked`;
- still `pending` or `running`;
- green, but for an older plan or change.

### Bound to what it saw

`planning_review` is tied to the exact plan it looked at. If the agent changes the plan, it sends the new plan in again.

Every other gate is tied to the exact change it looked at. It keeps the commit (`reviewed_head_sha`) and a hash of the diff (`reviewed_diff_sha256`). If the change moves, the old result no longer counts. A new commit or a rebase onto a newer base both move it. The gate must run again on the new change.

## Read gate results

`GET /runs/{id}` returns a `review_gates` list. There is one row for each gate that has a result.

| field | what it is |
|---|---|
| `id` | the gate row's ID |
| `gate_name` | the gate, like `security_review` |
| `status` | `pending`, `running`, `passed`, `failed`, `waived`, `blocked` or `cancelled` |
| `iteration` | which pass of the run this result is from |
| `blocking` | whether a failure here holds the run |
| `findings` | what the review found |
| `artifact_refs` | links to files the review made |
| `reviewed_head_sha` | the commit it reviewed |
| `reviewed_diff_sha256` | the hash of the diff it reviewed |
| `created_at`, `updated_at` | when the row was made and last changed |

Secrets in the `message` and `file` of each finding are masked before they are stored.

## Gate webhooks

When a gate records `passed`, `failed` or `waived`, Syndai sends `coding.review_gate.completed`. The event's `payload.payload` looks like this:

```json
{
  "gate_name": "security_review",
  "status": "passed",
  "iteration": 0,
  "head_sha": "4f2bbe04ba1c0d3e5f6a7b8c9d0e1f2a3b4c5d6e",
  "diff_fingerprint": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
  "blocking": true
}
```

`head_sha` and `diff_fingerprint` match `reviewed_head_sha` and `reviewed_diff_sha256` on the gate row. See [Webhooks](/docs/api/webhooks) for the full delivery body and how to check its signature.
