Routing rules

Write your review-routing intent in plain English. PURA's Translator compiles it once into deterministic CEL, and the Engine evaluates it per PR — no LLM in the hot path.

Writing pura.md

.pura/pura.md is the human source of truth — bullet points of intent. The Translator compiles it into .pura/rules.yaml (committed by the PURA app identity). You author intent; PURA produces deterministic rules.

# pura rules

- Dependabot PRs: cheapest available model.
- payments/ or billing/ with > 200 lines: Opus, high thinking.
- @mercato/search code:
  - Until 50% org budget consumed: GPT-5 high.
  - After: GPT-5 low.
- "security" label: Opus.
- Below $100 org budget remaining: GPT-5-mini for non-security PRs.
- Drafts: skip.
- Default: Sonnet, medium.

Two homes for config. pura.md lives in the repo and holds routing rules only. Budgets, caps, allowed-model lists, BYO keys, and ZDR live in the dashboard — and repo rules can never escape org caps. Never hand-edit rules.yaml.

The CEL evaluation context

Compiled rules use when: CEL expressions over a fixed schema. The pr.* fields are deterministic at a given head SHA; the budget.* fields are time-varying.

pr.title                          string
pr.body                           string
pr.labels                         list<string>
pr.author                         string
pr.author_type                    string    # "User" | "Bot"
pr.is_bot_author                  bool
pr.is_draft                       bool
pr.base_ref                       string
pr.head_ref                       string
pr.changed_paths                  list<string>
pr.lines_added / lines_deleted    int
pr.lines_changed / files_changed  int
pr.codeowners                     list<string>
pr.repo                           string
pr.repo_visibility                string    # "public" | "private" | "internal"

budget.org_cap_remaining_usd      double    # null if no org cap
budget.org_cap_remaining_pct      double    # null if no org cap
budget.org_spend_mtd_usd          double
budget.project_cap_remaining_usd  double    # null if no project cap
budget.project_cap_remaining_pct  double    # null if no project cap
budget.project_spend_mtd_usd      double
budget.org_hard_cap_breached      bool
budget.project_hard_cap_breached  bool

Null when unset. The numeric *_usd / *_pct fields are null when no cap is configured — always guard with != null before comparing. The *_hard_cap_breached booleans are always set (false when no cap), and *_remaining_usd is clamped at zero.

The compiled rules.yaml

The pura.md above compiles into deterministic rules. A rule has either action: skip or use, never both. The default block is required and cannot be a skip.

version: 1
default:
  provider: anthropic
  model: claude-sonnet
  thinking: medium

rules:
  - name: skip-drafts
    when: 'pr.is_draft'
    action: skip

  - name: dependabot
    when: 'pr.author == "dependabot[bot]"'
    use: { provider: openai, model: gpt-5-mini, thinking: none }

  - name: payments-critical
    when: 'pr.changed_paths.exists(p, p.startsWith("payments/") || p.startsWith("billing/")) && pr.lines_changed > 200'
    use: { provider: anthropic, model: claude-opus, thinking: high }

  - name: search-team-high-budget
    when: 'pr.codeowners.exists(o, o == "@mercato/search") && budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct > 0.5'
    use: { provider: openai, model: gpt-5, thinking: high }

  - name: search-team-low-budget
    when: 'pr.codeowners.exists(o, o == "@mercato/search") && (budget.org_cap_remaining_pct == null || budget.org_cap_remaining_pct <= 0.5)'
    use: { provider: openai, model: gpt-5, thinking: low }

  - name: security-label
    when: '"security" in pr.labels'
    use: { provider: anthropic, model: claude-opus, thinking: high }

  - name: low-budget-cheap
    when: 'budget.org_cap_remaining_usd != null && budget.org_cap_remaining_usd < 100 && !("security" in pr.labels)'
    use: { provider: openai, model: gpt-5-mini, thinking: none }

anthropic and openaiare the catalog's current providers; the schema accepts any catalog provider. Valid model ids also come from the catalog — they are not hardcoded in the schema.

Conflict resolution — bigger model wins

When multiple rules match the same PR, the most powerful model winsper the catalog's power ordering. Ties break by thinking (high > medium > low > none), then by file order. When a skip rule and a use rule both match, the use rule wins.

Budget rules don't auto-override high-stakes rules. In the example above, low-budget-cheap does not override payments-critical — Opus wins the tie. To make budget pressure win, fold the budget condition into the high-stakes rule's own when:.

To step thinking down by condition on the same model, write mutually-exclusive when: expressions so only one rule matches — as search-team-high-budget and search-team-low-budget do. They split on the budget threshold, so conflict resolution never engages between them.

A budget-tiered model ladder

A common pattern: spend the best model early in the month, then step down as the org budget burns. Spend is 1 − remaining, so "50% spent" is remaining_pct == 0.5. Express the ladder as mutually-exclusive bands on budget.org_cap_remaining_pct that leave no gap:

SpentRemaining %Model
0–50%> 0.5gpt-5 · high
50–75%0.25 – 0.5gpt-5-mini · medium
75–100%≤ 0.25gpt-5-nano · none
rules:
  - name: budget-0-50-spent
    when: 'budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct > 0.5'
    use: { provider: openai, model: gpt-5, thinking: high }

  - name: budget-50-75-spent
    when: 'budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct > 0.25 && budget.org_cap_remaining_pct <= 0.5'
    use: { provider: openai, model: gpt-5-mini, thinking: medium }

  - name: budget-75-100-spent
    when: 'budget.org_cap_remaining_pct != null && budget.org_cap_remaining_pct <= 0.25'
    use: { provider: openai, model: gpt-5-nano, thinking: none }

No cap, no ladder. The three bands are mutually exclusive and cover all of 0–100% remaining. When no org cap is set, org_cap_remaining_pct is null, none match, and routing falls to default. Swap in project_cap_remaining_pct to tier on the per-project cap instead.

CODEOWNERS & path matching

PURA hydrates pr.codeownersfrom the repo's CODEOWNERS file and pr.changed_paths from the diff — ideal for routing per-team in a monorepo without forking:

# Route a team's code to GPT-5 high
when: 'pr.codeowners.exists(o, o == "@mercato/search")'

# Path prefix match (any changed file under a directory)
when: 'pr.changed_paths.exists(p, p.startsWith("payments/"))'

# Combine path + size for high-stakes diffs
when: 'pr.changed_paths.exists(p, p.startsWith("billing/")) && pr.lines_changed > 200'

# Cheap model for bot authors
when: 'pr.is_bot_author'

Because routing is deterministic, the same PR always routes the same way at a given head SHA (budget permitting) — different paths route to different models without any branching in your code.

Next