Stop dropping the reviewer's intent verdict when its formatting drifts - #41
Open
AdamT wants to merge 1 commit into
Open
Stop dropping the reviewer's intent verdict when its formatting drifts#41AdamT wants to merge 1 commit into
AdamT wants to merge 1 commit into
Conversation
Kai reviewWarning Couldn't finish reviewing this change. Push a new commit or re-run the review to try again. |
AdamT
force-pushed
the
fix/review-verdict-parse
branch
5 times, most recently
from
August 5, 2026 12:03
bf0b253 to
2d495a3
Compare
AdamT
force-pushed
the
fix/review-verdict-parse
branch
2 times, most recently
from
August 18, 2026 06:53
72d1b0b to
d54d7c7
Compare
review-commit asks the reviewer to finish with a fixed block:
FINDINGS:
- [category] path:line — defect
INTENT_MATCH: verified|partial|diverges
NOTE: <assessment>
The parser accepted that block in one exact shape only: an unadorned key and a
value equal to one of three words. Anything else fell through to MatchUnknown,
which the console renders as "no contract resolved for this change" — a claim
that the reviewer never judged the change, when it had. Observed on
kai-server#70: two runs of the identical commit, one recording intent=unknown
and the next intent=verified with a full note.
Four things now parse that did not:
- Markdown around a key. The model wraps the block it was told to emit verbatim
("**INTENT_MATCH:** verified"), so keys are matched with * and ` removed.
- "matches". The prompt glosses the verdict as "'verified' (matches)", showing
the model two words for one meaning. This is the spelling that was observed
being dropped.
- A justification after the verdict ("verified — no material gaps"): the first
word of the value is the verdict, the rest is prose.
- A finding the model wrapped in bold. Matching the raw line dropped it, which
is worse than the verdict bug: a clean-looking review with a real defect
missing from it and nothing anywhere saying so.
Every relaxation is bounded so it cannot invent a verdict, because a confidently
wrong "verified" is worse than an honest unknown:
- Only the FIRST word of the value is read. Reading further makes "not verified"
resolve to verified, and makes an ordinary justification ("diverges — the code
no longer matches the stated intent") name two verdicts and resolve to neither.
- Only a real INTENT_MATCH line is read — no scanning prose for verdict words.
"matches" is a common English word and "Intent:" is a natural recap heading.
- Two verdict lines that disagree resolve to unknown. Otherwise the result
depends on emission order, and a model quoting the block back as an example is
enough to flip it.
- Decoration is stripped from the verdict, which is matched word by word, but
only the exact wrapper token is removed from the note and the findings, which
are prose. Blanket-stripping turned `a*b` into "ab"; trimming any trailing run
ate the closing backtick of a note ending in `code`.
- A second FINDINGS: header restarts the list, and the list ends at the first
non-bullet line. A real run recorded 3 findings, 2 of them quoted from this
command's own test fixtures.
The vocabulary is closed at four spellings, and each appears as a WORD in the
prompt this command sends or in the finding.Match enum. A test asserts that in
both directions, so the list and the prompt cannot drift apart. Notably absent:
"match" and "diverge" — kai-engine's judgeIntent accepts them, but no prompt here
emits them, and the two parsers never read the same text.
When no verdict can be read, review-commit logs the reviewer's raw closing text.
Previously the job exited 0, the log showed a clean run, and an amber badge was
the only symptom. That log is also how the vocabulary grows: add a spelling when
a real run produces it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013fuAGomxZJea7MSwY19SQg
AdamT
force-pushed
the
fix/review-verdict-parse
branch
from
August 18, 2026 09:18
d54d7c7 to
dd92767
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
rcParseReviewOutputaccepted the reviewer's closing verdict in exactly one shape — an unadorned key, and a value that is literallyverified,partial, ordiverges:Everything else fell through to
MatchUnknown, which the console renders as "no contract resolved for this change" — asserting the reviewer never judged the change, when it had.Evidence
kaicontext/kai-server#70, identical commit content, two runs:intent_matchunknownverifiedThe job log for the failing run shows a complete review: 25 graph-grounded tool calls (
kai_callersonverifyIngestSecret,kai_grepforIngestFinding), exit 0,Step completed: success. The review worked. The parse didn't.It also isn't a size problem — in the same batch, a 951-line PR verified while this 237-line one did not.
Shapes that silently became
unknownINTENT_MATCH: verified**INTENT_MATCH:** verified`INTENT_MATCH:` verifiedINTENT_MATCH: verified.INTENT_MATCH: verified — no material gapsINTENT_MATCH: matchesINTENT MATCH: verified(key drift)unknownThat sixth row matters: the prompt itself glosses verified as "(matches)", so the model echoing that word back is expected behaviour.
kai-engine's intent parser (reviewanalyze.go:334) already acceptsmatches/match. Two parsers with two vocabularies is the root cause.What changed
*and backticks before matching. Deliberately not_: that would break theINTENT_MATCHkey itself and mangle snake_case identifiers in the note (there's a test).INTENT_MATCH:lines that disagree recordunknownrather than last-one-wins, so a model quoting the block back as an example can't decide the verdict by emission order.review-commitprints the reviewer's raw closing text to stderr whenever it recordsunknown, so the failure stops being silent.Also changed: which findings get recorded
Three rules with a wider blast radius than the verdict fix, found while fixing it:
FINDINGS:header restarts the list. A reviewer that quotes an example block while explaining itself would otherwise have those bullets stored as defects it never reported. Observed in a real run: 3 findings recorded, 2 of them quoted from this command's own test fixtures.FINDINGS, storing its trailing prose as a defect.**- [security] …**is a finding. It was dropped silently before.On not inventing verdicts
An earlier draft had a salvage pass that recovered a verdict from a drifted key (
INTENT MATCH:with a space). It carried a real false-positive bug, caught by the tests here: the keyINTENT_MATCHcontains the word "match", so scanning the whole line resolved any unreadableINTENT_MATCH:line toverified.Salvage was dropped rather than narrowed. A drifted key now stays
unknownand gets logged, which is the honest outcome — the project's own rule is that "a confidently wrongverifiedpoisons the whole proposition". Nothing outside a well-formedINTENT_MATCH:line can produce a verdict, andTestRCParseReviewOutput_ProseNeverBecomesAVerdictpins that, drifted key included.Why the logging is the important half
The parser fix reduces how often this happens. The logging tells you when it still does — previously the job exited 0, the log looked clean, and an amber badge was the only symptom. There was no way to measure the failure rate, which means no way to know whether the cheap-model thesis is holding.
Testing
40 cases across 13 tests: every shape above, unreadable/absent/conflicting verdicts staying
unknown, prose never inventing a verdict, the findings-block rules, and inline code and underscores preserved in notes.go build ./...clean;go test ./cmd/kai/passes in full (no regressions).Deploying
The production review pods run this code via the
kai-ciimage (kai-cli/deploy/kai-ci/Dockerfile→go build -o /out/kai ./cmd/kai), which kai-server's review workflow pins askai-ci:latest. This needs akai-ciimage rebuild + push to take effect — it does not ride kai-server's deploy.pullPolicyForalready forcesPullAlwayson:latest, so pods pick it up once pushed.Follow-up worth considering
This whole class of bug exists because the verdict travels as free text. Having the agent emit it as a structured tool call would remove the parser entirely — larger change, filed as a thought rather than done here.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FjRNnVT1rpLU8ewxjTzxXQ