AI-generated patches often fail in a boring way. The code compiles, CI passes, and the wrong reviewer clicks approve because the diff landed in a folder they nominally own but do not actually understand.
That gets worse when agents move fast. A bot can touch auth middleware, billing jobs, and internal SDK glue in one pull request, then route review using a flat CODEOWNERS match as if all files carry the same risk.
The fix is not more reviewers everywhere. It is better escalation. In this post I’ll show how to combine CODEOWNERS, diff classification, and a small risk score so agent-generated patches reach the right humans before ownership drift turns into production cleanup.
Why this matters
CODEOWNERS is useful, but it is a routing primitive, not a safety system. It tells you who should be asked, not whether the change is scary, cross-cutting, or sitting in a folder with stale ownership.
For AI coding workflows, that distinction matters a lot:
- agents tend to edit multiple layers in one run
- reviewers can over-trust tidy diffs with good commit messages
- ownership files drift more slowly than team reality
- risky changes often look mechanically simple
A good review router answers three questions before it requests approval:
- Which owners match the files?
- How risky is the patch?
- Should approval stay with path owners, or escalate to a broader lane?
Architecture or workflow overview
The core idea is simple. Keep CODEOWNERS for the first pass, but add a second layer that understands file type, blast radius, and ownership confidence.
Minimal routing model
| Signal | Example | Why it matters |
|---|---|---|
| Path ownership | services/billing/** @payments-team |
Baseline reviewer routing |
| Risk markers | auth, infra, secrets, migrations | Some files deserve stronger gates |
| Cross-service spread | 1 file vs 8 directories | Wide diffs deserve more skepticism |
| Ownership confidence | active owner vs stale team alias | Prevent silent owner drift |
| Agent confidence | low-verification patch vs fully tested patch | Good evidence can lower noise, not eliminate review |
Implementation details
A practical version fits in one small policy service or CI step.
1. Parse CODEOWNERS and map files to candidate reviewers
from pathlib import Path
from codeowners import CodeOwners
owners = CodeOwners(Path(".github/CODEOWNERS").read_text())
def match_owners(changed_files: list[str]) -> dict[str, list[str]]:
matches: dict[str, list[str]] = {}
for file_path in changed_files:
owners_for_path = owners.of(file_path)
handles = [entry[1] for entry in owners_for_path]
matches[file_path] = handles
return matchesThis is the easy part. Most teams already stop here, which is why review routing looks correct while still missing the real decision.
2. Add a risk score that looks beyond path matches
risk_rules:
critical_paths:
- "infra/**"
- "terraform/**"
- "services/auth/**"
- "db/migrations/**"
sensitive_terms:
- "token"
- "secret"
- "iam"
- "encryption"
escalation_thresholds:
medium: 4
high: 7
spread_penalty:
per_top_level_dir: 1
unowned_file_penalty: 3
stale_owner_penalty: 2def score_patch(changed_files: list[str], file_summaries: dict[str, str], owner_matches: dict[str, list[str]], stale_owners: set[str]) -> int:
score = 0
top_level_dirs = {path.split("/")[0] for path in changed_files if "/" in path}
score += len(top_level_dirs)
for path in changed_files:
summary = file_summaries.get(path, "").lower()
if path.startswith(("infra/", "terraform/", "services/auth/", "db/migrations/")):
score += 3
if not owner_matches.get(path):
score += 3
if any(term in summary for term in ["token", "secret", "iam", "encryption"]):
score += 2
if any(owner in stale_owners for owner in owner_matches.get(path, [])):
score += 2
return scoreThe point is not perfect math. The point is to surface obvious reasons a seemingly normal patch should get a stronger review lane.
3. Turn the score into an escalation decision
{
"patch_id": "pr-1842",
"owners": ["@payments-team", "@platform-core"],
"risk_score": 8,
"coverage": {
"matched_files": 11,
"unowned_files": 1,
"stale_owners": ["@infra-legacy"]
},
"lane": "high",
"required_reviewers": ["@payments-team", "@platform-oncall", "@security-reviewers"],
"why": [
"touches auth and migration paths",
"contains one unowned file",
"crosses four top-level directories"
]
}That JSON payload is what I want attached to the PR, not just a raw reviewer list. Humans should see why the escalation happened.
$ review-router classify --pr 1842 Patch risk: HIGH (8) Matched owners: @payments-team, @platform-core Unowned files: scripts/backfill_customer_flags.py Escalation: add @platform-oncall and @security-reviewers Reason: db/migrations touched, auth code changed, ownership gap detected
What went wrong and the tradeoffs
A cheap fix is to maintain an owner freshness job that flags teams or handles that have not reviewed matching paths in the last 60 to 90 days.
If you escalate too aggressively, people learn to ignore the router. Security, platform, and staff engineers become default reviewers for routine changes. That kills velocity fast.
| Lane | Typical patch | Reviewer pattern | Cost |
|---|---|---|---|
| Low | docs, UI copy, isolated tests | path owners only | fast |
| Medium | service logic, SDK, background jobs | path owners plus service maintainer | moderate |
| High | auth, secrets, infra, migrations, wide diffs | path owners plus central gate | slow but safer |
Some teams route every agent PR as if it were a blind code dump. That is too blunt. A patch with focused tests, a migration plan, and before/after traces should still get review, but it should not be treated the same as an unverifiable sweep across ten directories.
Evidence should reduce uncertainty, not erase risk.
Practical checklist
- [ ] Parse CODEOWNERS and show matched owners per file
- [ ] Detect unowned files and fail closed on critical paths
- [ ] Score cross-directory spread, not just raw file count
- [ ] Maintain stale-owner detection from actual review activity
- [ ] Attach the escalation reasons directly to the PR
- [ ] Distinguish low, medium, and high review lanes
- [ ] Require stronger review for infra, auth, secrets, and migrations
- [ ] Let evidence reduce noise, but never skip human review for risky lanes
What I would keep
- Keep the scoring model small and inspectable.
- Explain every escalation in plain language.
- Review the router itself every few weeks, because ownership drift is continuous.
- Measure false positives, or the system will become background wallpaper.
Conclusion
CODEOWNERS is still the right starting point, but it is not enough for fast AI coding workflows. Once agents can generate plausible multi-file patches on demand, review routing has to become risk-aware, not just path-aware.
If I were building this today, I would keep CODEOWNERS, add a tiny risk scorer, and make escalation reasons visible in every PR. That gets you a review system that is still lightweight, but much harder to fool.