chore(ci): fail the on-demand review when it posts nothing - #185
Merged
Conversation
thecodedrift
force-pushed
the
fix/review-workflow-silent-success
branch
from
August 26, 2026 06:02
20dfed5 to
3363036
Compare
MEASURED on PR #182 (run 32933651692): the review action exited `success` with `is_error: false` having done no work at all — `num_turns: 0`, `permission_denials_count: 4`, `total_cost_usd: 1.076`. The tracking comment kept its placeholder ("I'll analyze this and get back to you"), the run logged `No buffered inline comments`, and the PR received zero inline comments. The job went green. That is the worst failure this workflow can have. A silent no-op is indistinguishable from a clean review, so a pull request reads as reviewed when nothing read it, and the cost is paid either way. `is_error` cannot gate this; it was false in that very run. The load-bearing signal is in the action's execution file: `num_turns` counts the model's completed turns, and a review that never took a turn cannot have posted anything. `permission_denials_count` is failed on separately, because a denial is how a review dies quietly — the tool it needs is not on the allowlist, it has nowhere to say so, and it stops. A denial with turns is also failed: a review that lost a tool is a review that could not see everything. The step names the denied tools when the record carries them. Tool names are structured data the runner produced, so printing them is safe here; the tool INPUT is never printed, because it can quote the untrusted diff. That is what makes the next occurrence self-diagnosing rather than a log excavation. `show_full_output: true` would answer the same question and is deliberately not used: this is a public repo and it dumps text the model produced while ingesting an untrusted diff into a world-readable log. Counters are not attacker controlled; model prose is. Both execution-file shapes the action has written (a JSON array of messages, and JSONL) are accepted, rather than pinning a shape it may change. Verified against six fixtures including the real record from run 32933651692: the failing shape, a healthy run, denials-with-turns, a file with no result record, a missing file, and an unset variable.
thecodedrift
force-pushed
the
fix/review-workflow-silent-success
branch
from
August 26, 2026 06:04
3363036 to
1782fb2
Compare
thecodedrift
added a commit
that referenced
this pull request
Aug 27, 2026
The guard added in #185 caught the zero-turn run on #196, which is what it was for. It then reported `permission_denials=0` while the streamed job log carried `permission_denials_count: 6`, so it skipped naming the denied tools, and that zero was read as evidence that permissions were not the problem. Reporting the wrong number was worse than reporting nothing. MEASURED on run 33039882331: the saved execution file's result record omits `permission_denials_count`, while the streamed stdout carries it. Every other field matched exactly, including `total_cost_usd` to sixteen digits, so the guard found the right record and the file simply lacks that one field. Denials are now the maximum of the count field and the number of denial records found in the file, and the naming loop reads those records rather than re-deriving them behind a gate that could never open. The old code already had the scan; it sat inside `if denials:`, so a missing count made it unreachable. Also added: a structural census of the execution file, message types and counts only. Two zero-turn runs have now billed real inference, #182 at $1.08 and #196 at $2.20, and neither left anything to diagnose from afterwards. No message content, no tool inputs, nothing the model produced while reading an untrusted diff; those are the reason `show_full_output` stays off on a public repo, and none of it is needed to say what shape a run had. Verified against three fixtures: the #196 shape with the count omitted and denial records present now reports 2 denials and names them, where the old guard reported 0 and named nothing; the #182 shape with the count present is unchanged; a healthy run still exits 0.
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 on-demand review workflow can report
successwhile posting nothing. This makes that impossible.What happened
On #182 (run 32933651692) the review job went green having done no work:
{ "type": "result", "subtype": "success", "is_error": false, "duration_ms": 147421, "num_turns": 0, "total_cost_usd": 1.076006, "permission_denials_count": 4 }The tracking comment kept its placeholder — "I'll analyze this and get back to you" — the run logged
No buffered inline comments, and the PR received zero inline comments. The final✅ Updated issue comment … with job linkstep stamped the job URL onto that stub, which is easy to mistake for a review being posted.A silent no-op is indistinguishable from a clean review. The PR reads as reviewed when nothing read it, and the run is billed either way. That is worse than the job failing outright.
Why
is_errorcannot gate itis_errorwasfalsein that run. The load-bearing signals are in the action's execution file:num_turns == 0permission_denials_count > 0is_errorThe step also writes the counters to the job summary on every run, pass or fail.
Naming the denied tools
When the record carries them, the denied tool names are printed. Tool names are structured data the runner produced, so they are safe here; the tool input is never printed, because it can quote the untrusted diff. This is what makes the next occurrence self-diagnosing instead of a log excavation — the root cause of the #182 run is still unknown precisely because nothing surfaced it.
show_full_output: truewould answer the same question and is deliberately not used: this is a public repo, and it dumps text the model produced while ingesting an untrusted diff into a world-readable log. Counters are not attacker-controlled; model prose is.Verification
Both execution-file shapes the action has written are accepted (a JSON array of messages, and JSONL) rather than pinning a shape it may change. Tested against six fixtures, including the real record from run 32933651692:
num_turns: 0, 4 denials)Denied tool(s): TodoWrite, Bashif: always()so the step still runs when the action itself fails.Not addressed here
The root cause of the four denials on that run. It is not visible in the log (
full output hidden for security), and shipping a speculative permissions change to a security-sensitive workflow is worse than shipping a guard that turns the next occurrence into a loud, self-diagnosing failure. One plausible mechanism is the action restoring.claudefromorigin/mainand running withsettingSources: ["user","project","local"], so this repo's own rules reach the review agent on top of the action'sallowedToolsallowlist — but that is a hypothesis, not a measurement, and the guard is what will produce the measurement.CI-only, so no changeset.