Skip to content

Policy

The policy file .wayglass/policy.yaml maps tags to one action per change. The full syntax is in the policy.yaml reference; this page covers how a decision is made.

From least to most restrictive:

Action Decision status What happens propose exits
auto_merge approved The author can land it 0
agent_review pending_agent A reviewer agent, not the author, approves or rejects it 0
human_approve pending_human One person approves or rejects it 4
multi_approve pending_human N distinct people approve it (approvers, default 2) 4
quarantine quarantined Held off the trunk until someone other than the author releases it with approve or rejects it 4
block blocked Stopped; it can never be approved or landed 3

A rejected decision has status rejected. To try again, change the code, push and propose again; each proposal gets a new decision.

Rules are checked top to bottom. The first enforcing rule whose when matches decides, and later rules are not consulted. If none matches, default decides; without a default in the file, the default is human_approve.

Within a when, every field that is set must match (AND). Within one field, a list matches if any value matches (OR):

rules:
- { when: { data_class: secrets }, action: block, reason: "Secrets never land." }
- { when: { area: [auth, infra], risk: [high, critical] }, action: multi_approve, approvers: 2, reason: "Big change to a security boundary." }
- { when: { only_area: docs, risk: low }, action: auto_merge, reason: "Routine docs change; every file is docs." }
default: { action: human_approve, reason: "No rule matched; a person decides." }

So put the strictest rules first. In the example above, a docs change that also adds an API key is blocked by the first rule before the docs rule is reached.

area matches when any changed file is in a listed area; only_area matches only when every changed file is. Use only_area for permissive rules: area: docs would also match a README edit bundled with arbitrary code.

The policy file is strict: unknown keys, an empty when, a missing reason, an area or data class used in a rule but not defined under classify, or approvers on anything but multi_approve are all errors.

The Worker reads .wayglass/policy.yaml from the trunk, at the commit the intent forked from. It never reads the policy from the fork. An agent cannot relax the rules its own change is judged by: editing the policy file in a fork has no effect on that fork’s decision, and the edit itself goes to a person (see the next section).

The CLI classifies the change locally first and prints the result as a preview. The Worker’s decision is the one that counts. If the two differ, propose warns and sets agrees: false in its JSON output.

Some rules sit outside the policy file because they protect the policy file itself, or cover cases where the file could not be applied. A guard only raises a decision to a floor; it never makes one less strict. When a guard raises a decision, the rule it overrode is kept in the record (overrode).

Guard When Floor
builtin:protect-policy The change touches anything under .wayglass/ human_approve
builtin:no-policy The trunk has no .wayglass/policy.yaml at the base commit human_approve
builtin:invalid-policy The trunk’s policy file does not parse human_approve
builtin:diff-truncated The change was too large for the Worker to read in full, so its tags may be incomplete human_approve

Two more rules hold whatever the policy says. The agent that wrote a change cannot approve or reject it. And an approval or rejection needs a reason that is not a placeholder (TODO, n/a, <what you checked> and similar are refused).

A rule with mode: shadow is evaluated on every change but never decides. Use it to watch a new rule on real proposals before enforcing it.

rules:
- name: trial-pii-review
when: { data_class: pii }
action: quarantine
reason: "Trial: all personal data gets a DPO look."
mode: shadow

Each decision records every shadow rule that matched, in file order, under shadow_matches. A shadow match has preempts: true when it comes before the rule that decided, meaning it would have decided had it been enforced. would_differ is true when enforcing the first preempting shadow rule would have changed the action (or, for multi_approve, the number of approvers). Built-in guards apply to the hypothetical too: a lax shadow rule on a .wayglass/ change does not “differ”, because builtin:protect-policy would still send it to a person.

propose prints one line when a shadow rule matched:

shadow: trial-pii-review would have quarantined this

Removing every shadow rule from a file leaves every decision unchanged. shadow: true is the older spelling and still works.

Give shadow rules a name. Without one, a rule is labelled by its position (rules[3]), and inserting a rule above it renumbers it.

wayglass policy test replays recent commits on the current branch (first parent only) through the local policy file, or the one given with --file, with the same classifier and evaluator as propose. It prints each commit’s decision and shadow line, then a table of every rule: how often it matched, how often it decided, and for shadow rules how often enforcing it would have changed the action. Commits that carry a why note also show the decision recorded when they landed, if today’s file would decide differently. It runs offline and records nothing.

Terminal window
wayglass policy test # newest 20 commits on HEAD
wayglass policy test --since v0.1.0 # every commit after a revision
wayglass policy test --file ../drafts/policy.yaml -n 50 --json

A workflow for a new rule: add it with mode: shadow, run wayglass policy test, land the policy change (a person approves it, because of builtin:protect-policy), watch the shadow lines on real proposals, then remove mode: shadow.