v2.8.0
Added
- GitLab cross-run finding dedup (issue #710): reruns no longer post a brand-new inline discussion for a finding that already has one.
post_findingsfuzzy-matches each finding against this bot’s still-open prior discussions (same file, within 3 lines, compatible category: the same tolerance GitHub’s canonical-review reuse uses) and skips reposting a match, keeping the existing discussion open instead of duplicating it. Every newly-posted discussion carries a per-finding metadata marker (fingerprint/category/severity) so future runs can match against it. Narrower than GitHub’s canonical-review reuse: GitLab has no dismiss/false-positive/wont-fix/fixed verdict system, so there’s no suppression, no escalation reply, and no in-place update of an existing discussion’s body: an unmatched or more-severe finding simply gets its own fresh discussion. SetAI_GITLAB_CROSS_RUN_DEDUP=falseto disable and restore the pre-dedup behavior of always posting a fresh discussion. See Features → Quiet reruns (GitHub).
Fixed
GitLabProvider.resolve_stale()had no concept of “a discussion this same run just created or matched as still active”: it resolved every owned, unresolved, resolvable, marker-bearing discussion on the MR unconditionally, including onepost_findingshad just posted moments earlier in the same run. In effect, every GitLab inline review comment was auto-collapsed as “resolved” before a human ever saw it as open, the same bug class already fixed once on GitHub (issue #718), reintroduced on GitLab from day one. Fixed with the same per-run keep-alive tracking GitHub uses; applies regardless ofAI_GITLAB_CROSS_RUN_DEDUP.GitLabProvider.post_findings’s failure detection comparedinline_posted == 0againstlen(findings) > 0, which would misclassify a run with zero inline candidates (everything routed to the body, or everything matched an existing discussion) as a failure. Now compares againstlen(inline_candidates)(what was actually attempted) and requires a real HTTP error, not just “nothing needed posting.”/ai-pr-review dismiss,false-positive, andwont-fixon a body-level finding could reply with a raw::warning::GitHub Actions annotation glued onto the front of the posted PR comment. The verdict-recording failure path printed it to stdout, the same stream the calling workflow captures as the reply body. It now prints to stderr, matching every other warning in the CLI.- Canonical-review selection (
select_canonical) could pick an empty-body review GitHub auto-creates every time the bot replies to an inline comment: a severity-escalation notice, a recurred-finding reply, or a feedback acknowledgement. Once that empty review became canonical, verdict recording failed with HTTP 422 (“Could not edit a review with a missing body”) on every subsequent dismiss/false-positive/wont-fix/fixed, and canonical-review reuse silently stopped updating in place on every rerun.select_canonicalnow skips reviews with an empty or whitespace-only body. - A finding’s stable
F<n>ID could be misclassified as body-level, never resolving its thread or dismissing the blocking review, if an older review had once rendered it as an out-of-diff body bullet before it moved to an inline thread in a later run. Classification now prefers the newest review that renders the ID at all, so a finding that has moved buckets between cycles is resolved correctly. A truncated review body (over GitHub’s size limit) is also handled: its id-map marker can list an ID whose bullet was cut off by truncation, and that alone is no longer trusted as proof the ID moved inline. - Self-contradictory verdicts marker no longer trusted (issue #755). A GitHub review’s verdicts marker was observed claiming a finding “dismissed” in the very same review body that rendered it as a visible, non-suppressed finding, a state the classification logic proves impossible under any normal code path. Trusting a contradictory entry risks permanently and silently suppressing a finding no human ever acted on.
merge_verdicts()now cross-checks each review’s verdicts marker against that same review’s own id-map and drops anydismissed/fixedentry contradicted by a rendered fingerprint in the identical body.
Changed
- Anthropic premium default:
claude-opus-4-8→claude-opus-5. Verified withtests/canary/live_model_canary.pyagainsttests/canary/stress_diff.txt(thecode-reviewer/silent-failure-hunteragents, the same ones issue #592 crashed): 4/4 calls completed withstop_reason: end_turn. Discovered live in the process: unlike Opus 4.7/4.8, Opus 5 has adaptive thinking on by default, uncapped: a single call against the stress diff took 206s and spent 11,433 thinking tokens (vs. Opus 4.8’s 44s/0 thinking tokens on the identical prompt), exceedingllm/anthropic.py’s 180s client timeout under real load.resolve_effort()now caps Opus 5 ateffort="low"the same way it already does for Sonnet 5 (issue #592), live-verified to bring the same call down to 67s/2,464 thinking tokens.resolve_temperature()also live-verified: Opus 5 rejects an explicittemperatureidentically to Opus 4.8 (HTTP 400`temperature` is deprecated for this model), so the existing strip-to-omit handling now covers it too. Added aconfig/model-pricing.jsonentry forclaude-opus-5(identical rates to Opus 4.8 per Anthropic’s pricing page: $5/$25 per MTok, $6.25/$0.50 cache write/read). SetAI_MODEL_PREMIUM=claude-opus-4-8to restore the previous default.