Verification pass: a second reader for the brief
The review brief is written by deterministic detectors and rankers, which is what makes it
reproducible and injection-proof. It is also why some of its items are noise: a co-change partner
that never contained the changed identifier, a “no test changed” question on a manifest bump. The
verification pass (llm.verify, off by default) sends every item of the brief to a model with
fresh context, in one batched call, and asks it to keep, drop or downgrade each one. It changes
what the brief shows and how. It never changes the score, the tier, the policy decision or the
captain priority.
This is Phase 2 of the verification-pass spec; Phase 1 is the author’s self-check.
What it verifies
Three pools, all in one call: the captain facts (## For the captain), the review-first hunks
(## Review first) and the checks (## Check specifically). Each candidate goes to the model with
its own small excerpt, the unified text of the hunk it rests on (at most 2,000 characters), never
the whole diff. A coupling.missing fact also carries the first 2,000 characters of its partner
file at base, since that is the only way to tell whether the partner has anything to update. The
judge’s risky_snippets are not verified.
Candidates reuse the self-check ids (contract.api:src/api.ts:42, hunk:src/session.ts:3,
check:src/session.ts:42:0), so a self-check entry, a verifier verdict and a feedback --fact
dismissal all name the same thing. The model never sees the ids, only numbered blocks; a verdict
for a number that was not shown is ignored.
The verdicts and their suffixes
Every surviving item gets exactly one suffix, appended after a space:
| Suffix | Meaning |
|---|---|
[verified · new] |
The model kept it; the change introduces what the item describes. |
[verified · pre-existing] |
Kept; the thing described was already in the file at base, this PR only touches it. |
[verified] |
Kept; new or pre-existing could not be decided. |
[downgraded by verifier: <reason>] |
The model would dismiss it; it stays visible, at lower priority. |
[downgraded: dismissed earlier as <verdict>] |
The dismissal memory (below) downgraded it; no model call was needed. |
[unverified] |
Nobody decided: the call failed, was skipped, or the model did not answer for it. |
A downgraded fact drops from strong to info and re-sorts. A downgraded hunk moves to the end
of its file’s list and stops counting toward the file, so the file re-sorts too. A downgraded
check moves to the end of the checks. All three keep their text.
A dropped item never renders in its section, in the PR comment’s five-fact cap, or in the short
“why”. Instead the brief carries one note per drop, dropped by verifier: <class> — <reason>
(<id>), and brief.verification.verdicts lists every candidate id with its final verdict,
dropped ones included. An audit trail, not a disappearance.
With llm.verify.enabled: false nothing changes: no suffixes, no notes, no verification field.
Dropped classes
A drop is allowed only when the item’s only subject is one of five named classes, docs,
tests, fixtures, generated or formatting, and the verdict cites a file:line from the
excerpt that shows it. opendrs re-labels any verdict that does not meet that bar before it is
applied: a drop without a named class or without a valid cited line becomes downgrade, and a
downgrade without a valid cited line becomes keep. So a model that returns drop for
everything with no evidence changes nothing. Anything else the model wants to dismiss is a
downgrade, still visible.
NEW and PRE-EXISTING
status is decided deterministically first. Captain facts are new by construction; a
coupling.missing fact has no new/pre-existing axis and is unknown. A review-first hunk or a
check is pre_existing when every non-blank added line of its hunk already occurs verbatim in the
file at base (a moved or re-indented block), else new; a pure deletion or a new file is new.
The model fills in status only where that leaves unknown. Status and action are independent:
a pre-existing item can still be kept, it only changes who is blamed for it.
What it never does
- The gate.
score,tierandpolicyare computed before the brief exists and are byte-identical with the pass off, on, failing, or returningdropfor everything. - The captain priority.
captain.priorityis computed from the unverified facts, exactly as without the pass. The verifier changes what is shown and how, neverstrong/infoas the priority sees them, so it can never turn careful into standard. (A detector downgrade, like thecoupling.missingnoise classes, can lower the priority; that is a detector fix and is meant to.) The captain “why” line and the PR comment’s five facts come from the post-verification list; the priority does not.
Fail open
Any error, in the provider, the reply’s parse, the memory file or the token cap, leaves every
candidate [unverified] and adds one note: verifier failed: <reason>; everything below is
unverified. Nothing is dropped or downgraded on an error. Two cases skip the call on purpose,
with a note:
verifier skipped: quick-confirm PR with only info facts, when the unverified priority is quick confirm, no fact is strong, and the review-first and checks pools are empty (depth follows risk);verifier skipped: prompt of N tokens exceeds llm.verify.maxTokens (M), the per-PR cap below.
Dismissal memory and feedback --fact
A wrong fact should not come back on the next push. The verifier’s verdicts are not cached by
commit, but .opendrs/facts.jsonl remembers verdicts by content:
opendrs feedback --fact coupling.missing:src/session.ts --verdict wrong --note "config-only pairing"
appends one line, {"hash":"…","id":"coupling.missing","file":"src/session.ts","verdict":"wrong",
"note":"…","at":"2026-10-22T10:00:00Z"}, where hash is the first 16 hex of sha256 over the
detector id, the file and the fact text with every digit run replaced by # and whitespace
collapsed, so “14 of 17 commits” and “15 of 18 commits” are the same fact. --fact takes the
id shown by opendrs brief --self-check-template, scores the diff (--base, --head as for
score) to find it, and lists the available ids when it is not there. Verdicts:
wrong,intentional,ignored: the fact is downgraded on every later run without a model call,[downgraded: dismissed earlier as <verdict>].useful: the fact is pinned; a verifierdropordowngradeon it is turned intokeep.
Like .opendrs/incidents.jsonl, the file is read from the base commit, so a PR cannot dismiss
its own facts; commit it on the base branch. It is a blocked path, so the PR that adds a line goes
to a human reviewer.
Configuration
llm:
enabled: true # the verifier uses the judge's provider, model and key
verify:
enabled: true # default false
maxTokens: 40000 # estimated prompt tokens (chars / 3.5) above which the call is skipped
The pass needs llm.enabled: true; it runs on the same provider as the judge,
and --no-llm disables both. In the Action, the key comes from the llm-api-key input as for the
judge.
Cost and the prompt hash
One call per PR, each candidate with a short excerpt: about $0.005–$0.01 per PR on Sonnet at low
effort, and never more than maxTokens of prompt. The brief’s verification field records the
model, the token usage, and promptHash, the first 12 hex of sha256 over the system prompt
and the output schema, so a precision measurement can be tied to the exact prompt it was made with,
the way bench/apachejit/results-llm.md
does for the judge. The brief’s notes carry the same
line: verifier: <model> · prompt <hash> · <input>+<output> tokens.
Updated 24 Oct 2026 · Edit this page on GitHub