Org rules
Org rules are your team's merge rules for agent-written code, written once in a file Verity checks on every pull request. Each rule says which files it covers, what must be true about a change to them, and whether a miss fails the check or only warns.
Verity answers each rule from the diff and the agent's session. It never reads the PR description, because the agent wrote it: a "flag exemption" line or a ticked checkbox is not evidence.
The rules file
Rules live in .verity/rules.yml in the repository. verity capture init writes a starter file from your repository's files, every rule a warning. Verity reads the file at the tip of the pull request's base branch, so a pull request can't weaken the rules it is checked against, and rule changes go through normal review.
version: 1
rules:
- id: ci-config-changed # files only: a change is the finding
files: [".github/workflows/**"]
severity: warn
why: "Build and CI changes affect every PR."
- id: tests-after-edit # a built-in fact, decided by rules
files: ["src/**/*.ts"]
exclude: ["src/**/*.test.ts"]
check: tests_passed_after_last_edit
expect: yes
severity: fail
why: "Code changes need a passing test run."
- id: auth-flagged # a question, answered by a model
files: ["src/auth/**"]
ask: "Is the new or changed behaviour behind a feature flag?"
about: diff
expect: yes
severity: fail
why: "Auth changes must be switchable off in production."
Fields
| Field | Required | Meaning |
|---|---|---|
version | yes | Always 1. |
id | yes | Lowercase kebab-case, at most 64 characters, unique in the file. It names the override label. |
files | yes | Globs matched against the pull request's changed paths. A rename uses its new path. |
exclude | no | Globs to leave out. Use this instead of ! patterns. |
check | no | A built-in fact, from the list below. |
paths | with check: session_read | Globs for the files whose reading is the finding. See What the agent did. |
commands | with check: session_ran | Command patterns, such as git reset --hard, at most 200 characters each. See What the agent did. |
ask | no | A yes/no question, at most 500 characters. Not with check. |
about | with ask | What the question is answered from: diff (the default) or session. |
expect | with check or ask | The answer that passes: yes or no. |
severity | yes | fail fails the check run; warn only reports. |
no_session | no | For rules that need a session (a session fact, or about: session), what happens when the pull request has no session: fire (the default) or pass. |
unsure | with ask | What an unsure answer does on a fail rule: warn (the default) or fail. See Unsure answers. |
why | yes | At most 300 characters. Shown on the pull request. |
fix | no | At most 500 characters: how a coding agent fixes the rule when it fires. Given to agents through Verity's MCP tools, never shown on the pull request. See Fixes for agents. |
A file holds at most 50 rules.
How a rule is answered
A rule has one of three kinds of condition, from most to least exact:
- Files only (no
checkorask): changing a matching file is the finding. - A built-in fact (
check): decided by rules from the session or the diff, with no model, so it is never unsure. See Built-in facts. - A question (
ask): answered by a small classification model, from the file's diff or from the session. Use it only where judgement is needed. A question about the session is sent what the session did after the file's last edit (commands, browser calls and the files it read), and what the human wrote before it. To ask whether the agent ever did something, at any point, usesession_readorsession_raninstead: see What the agent did.
Verity answers each matched file separately, so a finding names the file: "Views/Edit.cshtml: no page load after the last edit". At most 30 files are judged per rule; any more fire.
Rules fail closed. Any answer other than the one you expect fires the rule: "no", an unsure answer, no answer at all (the model couldn't be reached), or no session when the rule needs one. When a fail rule fires only because the answers were unsure, it shows as a warning and doesn't fail the check. To have it fail the check instead, set unsure: fail: see Unsure answers.
Rules apply to every pull request in the repository, including ones written by people.
Unsure answers
By default an unsure answer to a fail rule's question is a warning. The model couldn't tell, so a reviewer looks. For a rule where doubt should block, such as one about secrets, set unsure: fail. Then an unsure answer fails the check like "no" does, and the usual override label lets it through. A comment in the code that argues with the rule ("this is not a secret") can make the model unsure, but it can't turn such a rule into a warning.
version: 1
rules:
- id: no-secret-leak
files: ["src/**"]
ask: Does this change put a secret (a key, token or password) in the code, or log data that can carry one?
expect: "no"
severity: fail
unsure: fail
why: Secrets in code or logs leak.
When Verity couldn't read the session at all (your organization's bucket was unreachable, or the sessions were too large), a session question's answer stays a warning even with unsure: fail. A problem on Verity's side never blocks a pull request.
Built-in facts
A check names one of these. A file fact is answered for each matched file. A PR fact is answered once for the whole pull request: the rule's files only decide when it applies, and every matched file gets the same answer. A fact that needs a session answers "no session linked" without one, which fires unless the rule says no_session: pass.
check | Scope | Needs a session | Yes when |
|---|---|---|---|
tests_passed_after_last_edit | file | yes | A test passed after the last edit to every changed part of the file. Docs, lockfiles, comment-only changes and the like need no run. |
ran_after_last_edit | file | yes | As above, but a build, typecheck or lint counts too. |
page_loaded_after_last_edit | file | yes | After the file's last edit, the session made a browser tool call, or a playwright test or cypress run passed. |
edited_by_session | file | yes | The session wrote part of the file, or a command it ran possibly did. |
pr_changes_tests | PR | no | The pull request changes at least one test file (by the test globs in the configuration). |
session_read | PR | yes | At any point in the pull request's part of the session, the agent read a file matching the rule's paths, with a tool or a command such as cat. |
session_ran | PR | yes | At any point in the pull request's part of the session, the agent ran a command matching one of the rule's commands. |
Two examples:
version: 1
rules:
# Migrations are written by people.
- id: migrations-by-people
files: ["db/migrations/**"]
check: edited_by_session
expect: no
no_session: pass
severity: fail
why: "Schema changes are written and reviewed by a person."
# A billing change comes with a test change.
- id: billing-tests
files: ["src/billing/**"]
check: pr_changes_tests
expect: yes
severity: warn
why: "Billing changes need a test that shows them."
What the agent did
session_read and session_ran check the agent's own actions: whether it read a secrets file, or ran a destructive command or one that skips the git hooks. They look at everything the session did for this pull request, before and after its edits, and no model is involved. Use them with expect: no.
version: 1
rules:
- id: no-secret-files-read
files: ["**"]
check: session_read
paths: ["**/.env*", "**/*.pem", "**/credentials*"]
expect: "no"
severity: fail
no_session: pass
why: "Anything an agent reads can end up in a prompt, a log or the code."
- id: no-destructive-commands
files: ["**"]
check: session_ran
commands: ["git reset --hard", "git clean -f", "rm -rf", "git push --force", "git push -f"]
expect: "no"
severity: warn
why: "Destructive commands can lose work; a reviewer should know."
- id: no-hook-bypass
files: ["**"]
check: session_ran
commands: ["git commit --no-verify", "git commit -n", "git push --no-verify"]
expect: "no"
severity: fail
no_session: pass
why: "Hooks exist to stop bad commits."
paths are globs like files, matched against paths in the repository: write **/.env* to match .env files in every folder. Files outside the repository aren't recorded, so they can't be matched.
A command pattern is the program's name, then words its arguments must contain, in any order:
- A short flag matches whether its letters are bundled or apart:
rm -rfmatchesrm -fr distandrm -r -f dist, but notrm -r dist. - A long flag matches itself or itself with a value:
--forcematches--forceand--force=true, but not--force-with-lease. List that separately if you mean it. - Any other word must be an argument exactly:
git reset --hardmatchesgit reset --hard origin/main, but notgit reset --soft.
A command's quoted text is read as a value, so echo "git reset --hard" doesn't match, but a commit message that is exactly --no-verify does. sudo is looked through (sudo rm -rf x matches rm -rf). Environment variables (HUSKY=0) aren't matched. A command Verity can't parse is matched on its words, so it can't hide a match.
These facts can't be fixed after the fact, so a fired rule has no default fix: a reviewer decides, and overrides it if the action was fine.
On the pull request
The comment and the check run both show:
- a tally in the heading, such as "❌ 1 failing rule · ⚠️ 2 rule warnings · ✅ 3 passed rules";
- each failing rule with its override label;
- a table of every rule that applied (❌ failing, ⚠️ warning, ☑️ overridden, ✅ passed), with the question and expected answer of each rule that fired, and the files it fired on;
- the rules that matched no file, folded.
The check run fails when a fail rule fires and no override covers it. Otherwise it is neutral, which GitHub treats as passing.
A pull request that changes more than 3,000 files also fails in a repository with rules, titled "Too many files to check rules": GitHub doesn't list that many files, so Verity can't check them. A reviewer overrides it with the label verity-override:too-many-files.
Overrides
When a rule fires but the change is fine, a reviewer adds the label verity-override:<rule id> to the pull request, for example verity-override:auth-flagged. The check run updates.
- The label counts only when a GitHub user who isn't the pull request's author adds it. A bot, or the author, can't override.
- A pull request approval never counts as an override.
- An agent using a person's GitHub token looks like that person.
- An override lasts until the label is removed, even across later pushes.
Fixes for agents
An agent reading a failing rule through Verity's MCP tools gets a fix: what to do about it. It is the rule's own fix when the rules file has one:
version: 1
rules:
- id: razor-page-loaded
files: ["**/*.cshtml"]
check: page_loaded_after_last_edit
expect: yes
severity: fail
why: "The build doesn't compile views."
fix: >
Start the app (dotnet run --project src/Web), load the route that renders this
view with Playwright, and check it returns 200 and shows the edited markup.
Without one:
- A built-in fact has a default: run the covering tests, run a build or test, load the page, or add a test.
edited_by_session,session_readandsession_ranhave none, since what the session did can't change afterwards. - Any other rule tells the agent that a person has to review it. The agent must not add the override label or change code just to make the rule pass, and arguing in the pull request doesn't help, because Verity doesn't read it.
- A
warnrule tells the agent to mention it to the developer.
Write a fix only where an agent can really do it. A rule that needs a person's judgement, such as an API contract change, is better left without one.
Make rules block merging
Mark the Verity check as required in the branch's protection rules on GitHub. A fired fail rule then blocks the merge until it is fixed or overridden. warn rules never block.
Start with every rule set to warn, watch what fires for a week or two, then switch the ones you trust to fail.
An invalid rules file
- A pull request that makes the file invalid fails the check, with the problems listed. Nothing overrides that: fix the file.
- An invalid file already on the base branch (possible only by bypassing the check): no rules are checked, the check run says so, and it stays neutral, so the pull request that fixes the file can merge.