The model's APPROVE is not the merge
Why an AI reviewer's verdict is an input signal, and how to turn it into deterministic policy.
Ler em português Markdown version

The problem in one sentence
A model can write "verdict": "APPROVE" and, three lines down, list a high-severity implementation bug inside the task's scope. Both fit in the same JSON. If the pipeline only reads the first one, the bug ships to the pull request with an approval stamp on it.
That isn't a flaw of one particular model. It's what happens when the same text carries both the evidence and the conclusion, and the system trusts the conclusion.
The rule: evidence in, verdict out
T25's reviewer returns structured output with two fields: a verdict and a list of findings. Each finding has a severity, a category, whether it's inside the plan's scope, and what's wrong.
{
"verdict": "APPROVE",
"findings": [
{
"severity": "high",
"category": "implementation",
"inScope": true,
"description": "retry reuses the expired token"
}
]
}
The pipeline doesn't use the verdict the model sent. It runs the report through evaluateReview(), which does the math again:
// src/pipeline/evaluate.ts (simplified)
export function evaluateReview(report: ReviewReport): ReviewReport {
const blocking = blockingFindings(report.findings);
return {
verdict: blocking.length === 0 ? 'APPROVE' : 'REQUEST_CHANGES',
findings: report.findings,
};
}
blockingFindings() drops anything out of scope and keeps the rest. In the example above the result is REQUEST_CHANGES, and the task goes back to implementation with the finding attached. The model is still useful: it found the bug. It just doesn't get to decide what the bug means.
That's exactly what the interactive card on our home page reproduces (in Portuguese): you press APPROVE on a review that already has an in-scope finding, and the policy refuses.
The bug that taught us this
During a dogfooding round in August 2026, a QA agent ended its report with the word "REPROVADO" (Portuguese for "failed") and listed two blocking problems. The task moved on to REVIEW as if it had passed.
The cause was simple and uncomfortable. Reviewer and security had verdict parsers. QA didn't. The QA loop only checked whether the agent's process exited successfully, and a process that successfully writes "failed" exits successfully. Every time a task had moved from QA to review until then, it proved the CLI didn't crash, not that QA approved.
The fix was to give QA the same treatment: a mandatory PASS/FAIL verdict in structured output, validated by the pipeline, with a failure sending the task back to IMPLEMENTING. The lesson is broader than the bug:
If a stage has no verdict that code reads, that stage isn't a gate. It's a log.
What counts as blocking
Recomputing the verdict only works if the rule is explicit. T25's fits in three rows:
| Finding | Blocks? | Why |
|---|---|---|
In scope, known cause (implementation, spec, evidence, policy...) |
Yes | It's work the task promised to deliver |
Out of the plan's scope (inScope: false or out-of-scope) |
No | It gets recorded, not reworked; otherwise the fix loop turns into a rewrite |
In scope, legacy category with no cause (bug, style, other) |
Yes, fail closed | Becomes unknown: it blocks, but policy won't authorize blind rework |
The second row matters as much as the first. A reviewer who spots a real problem in another module shouldn't spend the current task's budget fixing something nobody asked for. The finding is logged; the task moves on.
Where the human comes in
Taking the decision away from the model doesn't mean handing all of it to code. T25 has three human stops:
- Spec approval. Always. A spec only becomes a plan once it has parseable acceptance criteria, the required checklist is complete, and no comment is left open.
- Plan approval. Driven by the task's risk. With the defaults, low risk needs no approval, medium needs plan approval, high needs plan and pull request approval, and critical always requires a human. Each project can turn the plan gate on or off.
- The merge. Always human. The factory opens the pull request, checks the required status checks, and stops. Whoever answers for main presses the button.
Policy decides what reaches you. You decide what goes in.
How to apply this in your own pipeline, with or without T25
If you already run AI review agents, you can adopt the idea today:
- Ask for structured output. Findings with severity, category and scope, in JSON, not a paragraph.
- Ignore the model's verdict when deciding. Keep it for audit, but decide with your own function over the findings.
- Fail closed. Output that doesn't parse isn't approval. It's an error, with the original text attached.
- Give every stage you call a gate a verdict. QA included. If code doesn't read it, it isn't a gate.
- Separate scope. An out-of-scope finding becomes an issue, not rework.
FAQ
Why not trust APPROVE if the model is good?
Because the problem isn't model quality, it's who signs. Even an excellent reviewer can list a serious problem and still conclude everything is fine. Recomputing from findings costs one function and removes that contradiction.
Does T25 auto-merge when review approves?
No. An approved review takes the task as far as the pull request. The merge is always a human action, after the repository's required checks.
Doesn't this slow the pipeline down?
It sends tasks back to implementation more often when there's an in-scope finding, which is the point. What gets faster is your review, because whatever reaches the pull request has already passed a rule that doesn't depend on the model's mood.
Does it work with any agent CLI?
Yes. The verdict is recomputed from structured output, so it doesn't matter whether the review came from Claude Code, Codex or another CLI in the fallback list.