Skip to content

feat(review): a verdict follows from the blockers, not from a confidence percentage - #28

Merged
jtomaszewski merged 5 commits into
mainfrom
jtomaszewski/verdict-from-blockers
Aug 21, 2026
Merged

feat(review): a verdict follows from the blockers, not from a confidence percentage#28
jtomaszewski merged 5 commits into
mainfrom
jtomaszewski/verdict-from-blockers

Conversation

@jtomaszewski

Copy link
Copy Markdown
Contributor

The problem

A review of a PR with three minors and one nit — no blockers at all — came back "comment · 72%", and the cockpit gave no approval. Its own verdict said why: "…is the team's call, which is why I take no position on merging."

Two separate things were wrong.

The verdict had an escape hatch. The prompt's rule read "any blocker → request_changes; none → approve (or "comment" when you genuinely take no position)", and the run took the parenthesis — over a regression it had itself graded minor. That is exactly the hedge the prompt forbids one line earlier for grades ("Never lower a grade to hedge doubt"); it just moved the hedge from severity to recommendation. The result is a review that found nothing blocking and still withholds approval, which is the one thing a reviewer's verdict is for.

The percentage was answering a question nobody asked. 72% was the loudest thing on the chip, and a reader can't check it against anything. Meanwhile the fact the verdict actually rests on — how many blockers are still standing — wasn't shown at all.

The fix

Close the hatch. Blockers → request changes, none → approve. No third option. If something would stop you approving, that is a blocker: grade it. comment is left for a review that graded nothing and only asks questions.

Show the blocker count wherever there's room, because that is the fact a reader can check the verdict against:

surface before after
review page chip comment · 72% request changes · 1 blocker · 92%
queue verdict cell approve 91 approve 78%
queue strip 4 comments drafted 4 comments drafted, no blockers
"why" card ⚠️ 3 minors · 🎨 1 nit ⚠️ 3 minors · 🎨 1 nit · 78% sure of the findings
.md export **Verdict: comment** (confidence 72%) **Verdict: approve** (no blockers · 78% sure of the findings)

Narrow what the percentage means. With the verdict following from the blockers, "should this merge" is already answered — so confidence is left saying one thing, and the prompt now says it in those words:

"confidence" is how sure you are that THIS REVIEW is right — that each finding is real, each grade is the right one, and nothing worth blocking got past you. It is not how good an idea merging is: the verdict says that, and it follows from the blockers. A thorough read that found nothing is high confidence with an "approve"; a skim of an unfamiliar codebase is low confidence whatever the verdict.

It now moves on what the run could not check rather than on how the PR looks — which also makes the auto-send gate more coherent than it was: ≥90% now means the review vouches for itself, not the review feels good about merging.

Technical details

  • src/runner/prompt.ts — the verdict rule loses its third option and gains an explicit "taking no position is not a verdict" clause; both confidence rules (source and diff-only) are rewritten around the review rather than the merge.
  • web/src/review.ts — new verdictBasis(): "no blockers" / "2 blockers", or null for a review drafted before severities existed, which makes no claim.
  • src/server/index.tsweb/src/types.ts — the queue payload gains blockerCount / gradedCount.
  • web/src/inbox.ts — the narrow verdict cell keeps verdict + percentage; the count goes to the strip, which has room to spell it out.
  • web/src/styles.css.col-verdict 136px → 152px. At 136 it clipped its own longest label to request changes 9…; the widest it can ever hold is request changes 100%, measured at 144.5px in the row's monospace face.
  • No schema change: confidence stays on the artifact as the auto-send threshold's dial, with its jsdoc rewritten to say what it does and does not claim.

What is deliberately not here

confidence is still load-bearing in one place — the auto-send gate (--auto-send-threshold, default 90), the only path that writes to GitHub unattended. Auto-send already has a blocker backstop (autosend.ts refuses an approve standing next to a live blocker), so the gate could become approve + no blockers and the field could go entirely. That rewrites the safety contract stated at the top of CLAUDE.md, so it's a separate, deliberate decision rather than a side effect of this one.

Also unchanged: no code derives the verdict from severity. The rule lives in the prompt; verdictMismatch() still only points out a disagreement. That principle was reaffirmed, not dropped.

Verification

  • pnpm typecheck && pnpm test — 369 passing (10 new: verdictBasis, the reworked verdictCell, and the strip's blocker line).
  • pnpm build clean.
  • Column width verified in a real browser, not from character arithmetic — which is what got it wrong the first time. Against a live queue: every .col-verdict cell reports scrollWidth === clientWidth (no clipping), and a hidden probe of request changes 100% in the cell's own computed font measures 144.5px against the 152px column.
  • Review page checked the same way: chip renders request changes · 1 blocker · 92% unclipped; the "why" card renders 🚨 1 blocker · ⚠️ 2 minors · 🎨 1 nit · 92% sure of the findings.
  • CLAUDE.md's severity/verdict principle updated to match, since the prompt rules are meant to be changed from evidence and this PR is the evidence.

jtomaszewski and others added 4 commits August 21, 2026 09:18
The cockpit's verdict chip read "comment · 72%" on a review whose own
findings were 3 minors and a nit — no blockers, no approval. The
percentage was never what withheld it: nothing derives the
recommendation from confidence. The prompt's verdict rule carried an
escape hatch — "approve (or comment when you genuinely take no
position)" — and the run took it, over a regression it had graded
minor. That is the hedge the prompt forbids one line earlier for
grades, moved one field over.

Close the hatch: blockers → request changes, none → approve, no third
option. If something would stop you approving it is a blocker, so grade
it. "comment" is left for a review that graded nothing and only asks
questions.

Then show the fact behind the verdict instead of the feeling. The chip,
the queue cell and the markdown export all name the live blocker count
("approve · no blockers") — something a reader can check against the
findings list, unlike how sure the AI reported being. Confidence stays
in the artifact as the auto-send threshold's dial and is shown nowhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e means

The blocker count in the queue's verdict column didn't fit — 136px of
column turned "approve · no blockers" into "approve · no bloc…", which
tells a reader less than either half alone would. The cell goes back to
the verdict and the percentage; the count moves down to the strip, which
has room to spell it out ("4 comments drafted, no blockers").

The percentage now means something narrower than it used to. With the
verdict following mechanically from the blockers, "should this merge" is
already answered, so confidence is left saying one thing: how sure the
review is of itself — findings real, grades right, nothing worth
blocking missed. The prompt says that in those words, and it now moves
on what the run could not check rather than on how the PR looks. On the
review page it sits beside the findings it is about, labelled "72% sure
of the findings" rather than left as a bare number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two places lost it in the last change. The queue cell printed a bare
"approve 78" — a number with no unit next to a word, which reads as a
count of something. And the review page's chip dropped the number
entirely when it took on the blocker count, so the one screen with room
for both showed one.

Both now carry the verdict, what it rests on, and how sure the review is
of itself: "approve 78%" in the queue, "approve · no blockers · 78%" on
the review. The "why" card keeps the spelled-out version that says which
of the two the number is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
At 136px the column clipped its own longest label to "request changes
9…", eating the number the last change had just put there. The widest
the cell can ever be is "request changes 100%", which measures 144.5px
in the row's monospace face, so the column goes to 152px — verified in
the browser rather than from character arithmetic, which is what got it
wrong the first time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens cerber’s review contract so the verdict is determined mechanically by live blockers (blockers → request changes; none → approve), while “confidence” is re-scoped to mean confidence in the review’s findings/grades (not confidence that merging is a good idea). It also updates the cockpit UI/export surfaces to show the blocker count where it helps readers validate the verdict, and relocates confidence to sit beside the findings it describes.

Changes:

  • Adds blockerCount / gradedCount to the queue payload and uses them to surface “no blockers / N blockers” in the queue strip.
  • Introduces verdictBasis() and updates the review page chip + “why” card to show blocker basis and “% sure of the findings”.
  • Rewrites prompt/docs/help text so “confidence” is explicitly about the review’s correctness rather than the merge decision.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/src/types.ts Extends queue item types with blockerCount / gradedCount and clarifies verdict confidence semantics.
web/src/styles.css Widens the verdict column and adjusts “why” card layout for new basis/confidence display.
web/src/review.ts Adds verdictBasis() to compute the blocker-based rationale string for verdict displays.
web/src/review.test.ts Adds unit tests covering verdictBasis() behavior (including dropped blockers).
web/src/inbox.ts Updates queue verdict cell to show “verdict + %” and moves blocker basis text into the strip meta.
web/src/inbox.test.ts Updates/extends tests for the new verdict cell formatting and strip blocker-basis text.
web/src/Detail.tsx Shows verdictBasis() in the review chip and adds explicit confidence wording in the “why” card.
src/server/index.ts Adds blockerCount / gradedCount to the API payload for queue rows.
src/runner/prompt.ts Reframes confidence as “confidence in the review” and tightens verdict rules in the reviewer prompt.
src/core/export.ts Updates Markdown export to include blocker basis (when graded) and “% sure of the findings”.
src/core/artifact.ts Updates verdict schema JSDoc to match the new confidence meaning.
src/cli/index.ts Updates --auto-send-threshold help text to match the new confidence definition.
CLAUDE.md Updates project principles/docs to reflect the stricter verdict/blocker rule and confidence semantics.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/server/index.ts Outdated
Comment thread src/runner/prompt.ts Outdated
… "comment"

Review feedback on #28, both findings correct.

The verdict rule claimed there was no third option and then, one line
later, reserved "comment" for a review that graded nothing — a
contradiction to anyone reading the prompt top to bottom, and the kind
that reintroduces the inconsistent recommendations this change exists to
remove. Scope the rule to what it actually governs: once anything is
graded there is nothing between approve and request changes, and
"comment" is named separately as the one case that rule cannot reach.

The queue payload's comment also went stale in the same series — it
still said the verdict cell names the blocker count, which stopped being
true when the count moved to the strip to stop the cell truncating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@jtomaszewski
jtomaszewski merged commit f0efccb into main Aug 21, 2026
3 checks passed
@jtomaszewski
jtomaszewski deleted the jtomaszewski/verdict-from-blockers branch August 21, 2026 07:47
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.20.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants