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 is a list of path globs. A reviewer runs only when at least one changed file matches one of its globs. That is what makes a hundred reviewers cheap: a docs-only change wakes the docs reviewers and nothing else.
trigger: [src/server/**, src/client/**] # runs when server or client code changed
The changeset is everything in your working tree that differs from the base branch, including uncommitted edits and new untracked files, not just committed history. This is deliberate: it lets an author loop against reviewers before committing anything. (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: it must produce a clean pass for the merge to
proceed. If it 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 (its findings still surface). 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 reviewer 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:
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.
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 runs all matched reviewers in parallel (they have wildly different latencies, one might take 90 seconds, another 15 minutes) and aggregates their verdicts into a single decision:
- All gates must pass. The aggregate is
passonly when every gate returned a cleanpass. - 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 or
codex 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.
How it all fits
.bastion.yaml you author this
|
v
bastion review ---> compute changeset (working tree vs base)
|
v
route: select reviewers whose trigger globs match
|
v
run matched reviewers in parallel (each on its backend, each timeout-bounded)
|
v
each returns a verdict (pass/block + summary + findings)
|
v
aggregate: all gates 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.