Postil

The gate and branch protection

Postil completes two check-runs on every reviewed PR. They have different jobs and must never be conflated.

Why the gate is a separate check

A single check-run cannot do both jobs well. postil/review always completes (even on an operational error), so findings and inline comments are visible on every PR, including ones nobody has required. postil/gate is the one context you put in branch protection: its name is stable, it exists on every reviewed commit, and it fails closed when the review itself fails. Collapsing them into one check forces a choice between hiding findings on PRs that do not require the check, or giving the required check an unstable name (or unstable pass/fail rules) as its job changes. Two checks with fixed, separate jobs avoids both.

CheckJobFails when
postil/gateThe blocking verdict. Require this one in branch protection.A finding at or above gate.failOn (default error) exists, or the review could not complete (fail closed).
postil/reviewAdvisory findings and inline comments. Never require this one.Never blocks. Completes neutral on operational error, green otherwise.

Requiring the gate

GitHub's own reference for this feature is the protected branches guide and the required status checks API. To require the gate on a specific repository:

  1. Go to github.com/<org>/<repo>/settings/branches (Repository Settings → Branches).
  2. Add or edit a branch protection rule for your default branch (or the equivalent ruleset).
  3. Enable Require status checks to pass before merging.
  4. Search for and add postil/gate to the required checks. Do not add postil/review: advisory findings should inform, not block.
  5. Save the rule.

postil/gate only appears in that search box after it has run at least once on the repository. Open one PR first, then come back and require it.

Bind the required check to the Postil GitHub App, not any source. Rulesets record that source as an integration id, and classic branch protection records it as an App id. Postil verifies either exact binding under Settings → Installation health. Missing or unreadable source identity is shown as unverified.

For merges covered by this rule, a PR with an error-severity finding cannot merge until the finding is fixed (the next push re-reviews incrementally and resolves it) or the threshold is deliberately changed in config, a reviewable, auditable act. Configured bypass actors remain exempt.

Choosing a threshold

# .postil.yaml
gate:
  failOn: error   # default: block only on error-severity findings
  # failOn: warn  # stricter: block on warnings too

The default blocks only on error: findings the model is confident affect correctness, security, or data integrity. Warnings and informational findings stay in the advisory check where they cannot stop a merge. This is the missing primitive the category has: teams that wanted "block on critical, ignore nits" previously had to accept blocking on everything or nothing.

humanEscalation findings are also kind-blocking by default once their calibrated confidence reaches 0.30. Weaker escalation signals remain visible in the review but do not fail the gate. The pull request is the escalation channel: update the change and push again, or have an organization admin approve an eligible judgment call with a rationale in the linked Postil run. A finding that also blocks by severity requires a change and cannot be approved away.

Fail-closed semantics

If the review crashes, times out (10-minute watchdog), or the model returns garbage, postil/gate completes as failure with the operational error in the summary. It is never left in-progress and never marked neutral. The gate never marks an unreviewed head as passing; pushing again or re-requesting the check re-runs the review. This is the default, gate.onError: block.

Repos that prefer fail-open over a blocked merge queue during a model outage can set gate.onError: advisory. This only changes behavior on provider errors (a provider outage, an exhausted key, a connection timeout) and lets the gate pass in those cases instead of failing closed. Model output that fails validation even after a retry is deliberately not covered: a diff can be crafted to induce that class of failure, so it always fails the gate, even under advisory. Nothing else weakens either: findings the model did produce still gate normally, and a review that completes successfully with an error-severity finding still fails the gate regardless of this setting. Choose advisory deliberately; it trades an unreviewed head being treated as passing for never blocking merges on Postil's own availability. See configuration.

Local parity

The same gate runs locally: postil review --staged exits 1 exactly when the hosted gate would fail, so a pre-push hook (postil hook install) gives you the verdict before CI does. Preview threshold changes with postil plan.