Concepts
The vocabulary Bastion runs on: reviewers, triggers, modes, verdicts, and the merge gate.
This chapter defines the terms the rest of the guide uses. It is short on purpose; each idea has a deeper home later, linked as it comes up.
The reviewer
A reviewer is the unit of the system: a focused agent prompt responsible for
exactly one property of a changeset. It is a bundle of prompt + trigger + mode,
plus an optional execution profile (backend, timeout, environment, inputs, a
container runner, and capabilities, among others). All of it is declared
statically in .bastion.yaml; Authoring reviewers
is the full field reference. The repository’s .bastion.yaml is the shared, governed
set; locally you can also keep personal reviewers in a user-level .bastion.yaml,
and bastion review runs the merged set (see
Authoring reviewers).
Two properties matter most:
- Single concern. A reviewer checks one thing and checks it well. You scale coverage by adding reviewers, never by widening one. This is what keeps recall high (see Introduction).
- Declarative and static. Reviewers are data, not code. Bastion never generates them on the fly. That keeps the trigger set stable and makes every reviewer reviewable, which is the foundation of governance.
The trigger and the changeset
A reviewer’s trigger decides whether the reviewer applies to a changeset. The usual form is a list of path globs; the reviewer runs when at least one changed file matches. A docs-only change then wakes the docs reviewers and nothing else.
trigger: [src/server/**, src/client/**] # runs when server or client code changed
For a concern that paths cannot identify narrowly, kind: agent asks a cheaper
model whether the full reviewer applies. Optional paths run first as a cheap
prefilter. The agent sees the actual changeset, and any failure or uncertainty
runs the full reviewer. A confident skip is recorded separately from a pass
verdict. See Agent triggers for the schema and
cost model.
The changeset is everything in your working tree that differs from the point where your branch forked from the base branch (the merge base), including uncommitted edits and new untracked files, not just committed history. This is deliberate on both ends. Including uncommitted work lets an author loop against reviewers before committing anything. Diffing at the merge base rather than the base branch’s tip means the changeset is only ever your work: changes that landed on the base after you forked are never routed on, never shown to a reviewer, and never flagged as yours. (Locally, this means a reviewer sees your work in progress; in CI the head is already committed, so the same definition gives the same result.)
The mode: gate vs. advisor
Every reviewer has a mode that decides whether it can block a merge:
| Mode | Blocks the merge? | On crash/timeout/bad output |
|---|---|---|
gate | Yes, when it returns block | Fails closed: resolves to block |
advisor | No, ever | Fails open: ignored in the aggregate |
A gate is a hard requirement when its trigger says the reviewer applies: the
full reviewer must produce a clean pass for the merge to proceed. An agent trigger
may instead record a semantic skip, which is counted separately from a pass. If an
applicable gate crashes, times out, or cannot produce a valid verdict, it resolves
to a block, never a silent pass. An advisor comments but never holds up the
merge; even a clean block verdict from an advisor is treated as a pass for
aggregation, and its findings are recorded as optional (an advisor’s findings
are advice, never a merge blocker) so they still surface as suggestions. A failed
advisor is dropped.
Use a gate for properties that must hold (tenant isolation, fail-closed error handling). Use an advisor for guidance you want surfaced but not enforced (test coverage, doc gaps, style preferences).
The verdict
Every full reviewer execution returns a structured verdict, captured through the
backend’s structured-output mechanism (a JSON schema for Claude Code, a requested
verdict block for Codex) so Bastion can parse and aggregate it. An agent trigger
that skips the full reviewer records reviewer.skipped instead, with no verdict or
findings. A full reviewer’s verdict has this shape:
verdict: pass | block # the authoritative gate decision (ignored for advisors)
summary: "..." # a human-friendly one-paragraph explanation
findings: # specific, located comments
- kind: blocking # blocking | optional
path: src/server/db.rs
line_start: 88
line_end: 91
detail: "scope this query by tenant_id"
The top-level verdict is the decision; findings explain it. A block should
carry at least one blocking finding (the reason), and a pass may still carry
optional findings as non-blocking suggestions. A finding’s kind changes how it
is surfaced, not whether the merge proceeds; only verdict decides that. A
pass never carries a blocking finding: the two would contradict each other.
Because an advisor is always resolved to a pass, its findings are recorded as
optional regardless of what the reviewer emitted.
Findings are the actionable surface. An agent fixing a PR gets everything it needs from the findings: a file, a line range, and what to change. It should never have to open a transcript to learn what to do.
A reviewer reports the complete actionable set in one pass, one finding per distinct instance, not just one representative reason. The author can then fix everything from a single run instead of meeting the next issue on the following review cycle. Bastion requests this from every reviewer automatically, so a prompt does not need to ask for it.
The merge gate
Bastion resolves the reviewer candidates in parallel (they have wildly different latencies, one might take 90 seconds, another 15 minutes) and aggregates their terminal outcomes into a single decision. A candidate may execute, record an agent-trigger skip, replay from a verified attestation in CI, or carry an unchanged prior pass on a re-run:
- Every applicable gate must pass. An agent trigger may decide its reviewer
does not apply; that gate increments
gates.skippedwithout producing a pass verdict. The aggregate ispasswhen every gate that did apply passed. - Any blocked, errored, or timed-out gate blocks the aggregate. “All gates pass” never includes a gate that failed to produce a verdict.
- Advisors never affect the aggregate. They contribute findings, not gate decisions.
Locally, that aggregate is the exit code of bastion review. In CI it is the result
of the Bastion review job, and bastion github report also posts it as a single
always-present check named bastion. Either way the aggregation rule is the same, and
CI runs the repository’s reviewers. The decision matches when both runs see the same
reviewers and context; two things can make a local run differ: CI can add the PR’s
description and discussion that a default local run does not, and a purely local run
can include your personal user-level reviewers, which CI never runs (see
Authoring reviewers).
The backend
A backend is the agent harness a reviewer runs on. Bastion does not implement its own agent loop; it translates the reviewer into the backend’s native config and shells out to its CLI, reusing your local auth and billing.
any(the default): Bastion chooses; that resolves to Claude Code.claude-code: Anthropic’s Claude Code CLI.codex: OpenAI’s Codex CLI.pi: the Pi CLI; uses whatever provider you have configured it with locally, unless a reviewer pins amodel(Pi’sprovider/idform selects the provider too).
You pin a backend when a subscription’s terms require a specific harness, or when one model is better at a given concern. See Authoring reviewers and, for CI billing, Continuous integration.
By default the backend CLI runs natively on the host, using the claude,
codex, or pi already on your PATH and the auth and billing that CLI is
configured with.
A reviewer that declares a runner
instead runs that same backend inside a container (which requires
capabilities.network: true; without it the reviewer is rejected before it runs, so a
gate blocks and an advisor is skipped): Bastion invokes the container engine on the
host, and the backend CLI resolves inside the image. A fixed set of
model-provider credential variables (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN,
ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, CLAUDE_CODE_OAUTH_TOKEN, OPENAI_API_KEY,
OPENAI_BASE_URL, CODEX_API_KEY) is forwarded from Bastion’s environment into the
container by name, so the in-container agent can still reach its provider; an image
can also bake in its own auth. If the reviewer’s own env sets one of those names,
that value wins and the host’s is not also forwarded, so the reviewer can pin a
specific credential. Nothing else from your host environment crosses that boundary. To
give the in-container agent another value, set it as a literal in the reviewer’s env,
which is forwarded in alongside the credentials. The fixed set covers the Anthropic
and OpenAI variables only, so a containerized Pi reviewer on another provider
authenticates from auth baked into its image or from a credential written into its
env.
How it all fits
.bastion.yaml you author this
|
v
bastion review ---> compute changeset (working tree vs merge base)
|
v
route: apply path prefilters, then resolve agent triggers
|
v
run matched reviewers in parallel; a reviewer may instead replay from a verified
attestation (CI), or carry an unchanged prior pass forward from the branch's
previous run (local or CI), both with no backend dispatch (each executed reviewer
is timeout-bounded)
|
v
each returns a verdict, or records an agent-trigger skip
|
v
aggregate: every applicable gate must pass ---> one decision (exit code locally;
the review gate in CI)
Next: Authoring reviewers. The full registry schema, from the four required fields out to timeouts, environment, and prompt inputs.