Docs

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:

ModeBlocks the merge?On crash/timeout/bad output
gateYes, when it returns blockFails closed: resolves to block
advisorNo, everFails 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.skipped without producing a pass verdict. The aggregate is pass when 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 a model (Pi’s provider/id form 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.