Skip to content

ci: both AI review lanes reported success having reviewed nothing, and the #3147 evidence guard passes a lane that posts nothing at all #4093

Description

@kyle-sexton

This was generated by AI during work.

What happened

On PR #4059 (head c94be900), both AI review lanes concluded success and produced no review and no finding. The check rows went green, the required ci-status check went green, and the PR was mergeable on that evidence. The only substantive review on that PR came from Codex, which found three real bugs.

The initial hypothesis was quota exhaustion on the account behind the lanes. That is not what happened. Both sessions ran, were billed, and exited cleanly well inside every budget.

Evidence

From the two sessions' own result records in the job logs:

claude-review claude-security-review
Run 34569637210 34569636940
subtype / is_error success / false success / false
Turns used 20 of 75 34 of 75
Wall time 3m17s of a 15m cap 8m03s
permission_denials_count 23 8
total_cost_usd 1.70 2.11
Review posted none none
Findings produced none none

No quota, rate-limit, 429, overload, or credit signal appears anywhere in either log. Neither ran out of turns or time. Roughly $3.81 was spent to produce nothing, and both lanes reported green.

Each session's tracking comment records its own incomplete state. claude-review logged:

Invoke /review:code-review (skill errored in this environment; proceeding with an equivalent manual review: correctness + maintainability, security out of scope)

and then stopped with all seven of its review items unchecked. claude-security-review stopped with all four of its passes unchecked.

Root cause, and what is inferred rather than proven

Proven: the string Skill appears zero times in the claude-review job log, including in the resolved ALLOWED_TOOLS, which is:

Glob, Grep, LS, Read, mcp__github_comment__update_claude_comment,
Bash(git add:*), Bash(git commit:*), Bash(<action>/scripts/git-push.sh:*), Bash(git rm:*)

plus Bash(gh pr diff:*) and mcp__github_inline_comment__create_inline_comment from claude_args. The prompt handed to that session is Invoke /review:code-review now and follow its instructions exactly for this pull request. The tool required to satisfy that instruction is not in the grant.

Inferred: that the missing Skill grant accounts for the 23 denials. The per-turn stream is not in the job log (only the init and result records are), so the denied tool names cannot be read back. The count, the absent grant, and the session's own admission that the skill errored all point the same way, but the attribution is not directly observable from the log.

This is the same failure named in #3147 ("claude-review lane cannot invoke /review:code-review: Skill tool errors in the workflow environment, lane degrades to manual review"), which is closed with all three of its acceptance criteria unchecked. Criteria 2 and 3 are both still failing:

  • "A claude-review run on a PR invokes /review:code-review successfully, or the lane fails loudly when it cannot"
  • "The silent-degrade path (manual review with a green check row) no longer masks a broken skill chain"

The guard built for this does not catch it

review-skill-evidence exists in .github/workflows/claude-review.yml precisely to redden this class, and it passed. Reproduced locally against the real PR at the real head:

$ GITHUB_EVENT_NAME=pull_request GITHUB_ACTOR=kyle-sexton PR_NUMBER=4059 \
  EVENT_HEAD_SHA=c94be900... LANE_RESULT=success REVIEWER_LOGINS='claude[bot]' \
  bash scripts/verify-claude-review-skill.sh
claude-review skill evidence OK (no silent Skill-tool fallback in posted review bodies)
$ echo $?
0

The guard fetches review bodies posted by claude[bot] and classifies each one for a Skill-failure admission. On this PR claude[bot] posted 0 reviews and 3 issue comments. With no review body to classify, the loop body never executes and the script falls through to its success line.

Two gaps compound:

  1. Absence of a bad body is read as absence of a problem. The guard can only find a degrade inside a posted review. A lane that posts nothing has nothing to classify and passes. "No review was posted" and "a clean review was posted" are the same verdict.
  2. The admission was in a surface the guard does not read. The skill errored in this environment sentence was in the tracking issue comment, not in a review body. The guard reads review bodies by design (deliberately not the job log, per security-evidence guard fails every successful in-scope PR by matching the lane's echoed script source #2517), so the one piece of direct evidence sat where it could not be seen.

The sibling verify-security-review-evidence.sh is structurally sounder: it branches on LANE_REVIEW_RAN, a positive declaration that a review ran, and errors when that is false. It passed here because the upstream reusable declared review_ran=true, which evidently means "the session ran", not "a review was produced".

Proposed work

  • Decide whether to reopen ci: claude-review lane cannot invoke /review:code-review — Skill tool errors in the workflow environment, lane degrades to manual review #3147 or track the remainder here. Its criteria 2 and 3 are unmet.
  • Grant the Skill tool to the review lane's session, or confirm with the reusable workflow's owner how a skill is meant to be invoked there, and make the lane fail loudly when the invocation is refused.
  • Give verify-claude-review-skill.sh a zero-review branch: an in-scope lane that concluded success and posted no review body at all is a failure, not a pass. Distinguish "reviewed and found nothing" from "never reviewed".
  • Have the guard read the tracking issue comment as well as review bodies, since that is where a degrading session actually confesses.
  • Reconcile what review_ran=true means upstream. If it reports session completion rather than review production, the security guard's central signal does not mean what its name says.
  • Consider surfacing permission_denials_count from the result record as a lane signal. A double-digit denial count is a strong indicator that a session could not do the job it was given.

Acceptance criteria

  • A claude-review run that cannot invoke /review:code-review fails its check row rather than reporting success.
  • verify-claude-review-skill.sh fails for an in-scope lane that concluded success and posted no review body, with a regression case covering the zero-review input.
  • verify-security-review-evidence.sh's LANE_REVIEW_RAN signal is confirmed to mean a review was produced, or the guard stops relying on it as though it does.
  • A lane that produced no review and no finding is visible as such on the PR, without reading the job log.

Workaround now in place

plugins/source-control 0.55.75 adds step B2 to the PR monitor's per-iteration checklist: a review lane that posted no review body and no finding is classified ABSENT rather than PASS, and a local review (/review:fanout, or /review:code-review for the correctness lane alone) is run over the same diff and named in the report. The readiness gate in step E does not clear until every review lane is productive or substituted. That is an agent-side compensation for a CI-side defect and does not close this issue.

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions