The intent check
Checking a change against its stated intent
The gap between what a change says it does and what it actually does is the most important signal in a review. Onus checks it deterministically: you (or the agent that wrote the change) state the intent in a small block, and every row outside it is an intent mismatch, ranked at the very top and marked as needing a person.
Stating an intent
In a pull request body, a fenced block:
```onus-intent
summary: Add SMS alerts on OrderShipped
touches: [notifications]
contracts: [UserPreferences]
externals: [Acme SMS]
```Or a YAML file with the same keys. Pass either to --intent:
onus report --base main --head HEAD --intent pr-body.md
onus diff old new --intent intent.yamlAll keys are optional; unknown keys are an error. A file ending in .md, .markdown or .txt is read as Markdown: only its onus-intent block counts, and without one the check is skipped with a warning on stderr. Other files are read as YAML, unless they look like Markdown and have no block.
What each key covers
summary: free text, shown in the report.touches: the components the change may touch, by id (notifications), or path globs (services/notifications/**, any entry with/or*). A row is insidetoucheswhen its component is listed, or when every file it points at matches one of the globs.contracts: the contracts the change may alter, by name (UserPreferences) or map id. Applies to contract rows (subkindscontract-*andexport-*).externals: the external services the change may start calling, by vendor (Acme SMS), id (acme-sms) or SDK package (@acme/sms), case-insensitive. Applies tonew-external-serviceandnew-external-callrows.
touches. Contract rows must also be listed in contracts, and external-service rows in externals. Everything else is a mismatch, including the internal row of a component outside touches.
What the report shows
**Intent check:** **2 changes outside stated intent** · **Boundary rules:** no new violations
Stated intent: "Add structured fields to log lines"
- 10 lines of new code inside `billing`: `billing` is not in `touches` (logger)
- `billing` now writes to the `payment` table: `billing` is not in `touches` (logger)The mismatching rows move to the top of the table with "Outside stated intent" in their Kind column. In JSON, they have hints.intentMismatch: true, and intentCheck.mismatches lists { changeId, reason } for each.
Tips
- Ask coding agents to end every pull request description with an
onus-intentblock. It costs them nothing and makes scope creep visible. - Keep
touchestight. A change that legitimately touches more should say so; that is the point. - An intent never hides a row: rows inside it are still reported and ranked as usual.