Repository navigation
fix(ci): json envelope and a 120-turn cap for the reviewer (#41 items 1-2) - #45
Conversation
…items 1-2) Ported from the Cortexa-LLC/mcp implementation, which has been running these across 8+ live reviews on 5 PRs. Item 1 — `--output-format text` made a max-turns exhaustion undiagnosable. `text` emits the final assistant message and nothing else, so a run that ends WITHOUT a final message exits non-zero with BOTH streams empty. PR #35's stderr capture cannot help there: there is no stderr to capture. The observed shape was ~3 minutes of work, exit 1, and nothing to explain it. `--output-format json` always carries subtype / is_error / num_turns. The review text now comes from `.result`, a non-zero exit and an `is_error` envelope are treated as INDEPENDENT failure signals, and the envelope is logged on every run — healthy or not — so the turn budget is visible before it becomes a problem rather than after. Item 2 — `--max-turns 15` was far too low and failed silently. Four real runs show turn count tracks how far the reviewer wanders, not diff size: a 2,704-line PR used 35 turns while an 848-line PR hit the cap at 41. Two of the four stopped exactly AT the cap, so the true ceiling is unknown and a snug cap is the wrong shape. Raised to 120 and treated as a runaway guard rather than a budget; timeout-minutes goes 20 -> 30 since the two bound the same run (~6s/turn puts 120 turns near 12 minutes). This mattered because exceeding the cap yields NO review rather than a short one, and the job is continue-on-error — the reviewer could have stopped posting on every PR with nothing going red. Verified by extracting the invocation and failure-handling block from the workflow and driving it with a stubbed CLI across six envelope shapes: healthy review exit 0, body extracted from .result max-turns exhaustion exit 1, "Reviewer hit the turn cap", turns named error_during_execution exit 1, same treatment exit 0 with is_error=true exit 1 (independent signals both honored) unparseable json exit 1, fails closed healthy envelope, .result null exit 1, caught before posting Also: YAML parses, `bash -n` on the extracted run block, actionlint clean, and the backslash-continuation guard from #40 passes. No version bump: .github/ only, nothing under plugin/. Refs #41 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc
There was a problem hiding this comment.
Review Summary
- Files reviewed:
.github/workflows/claude-pr-review.yml - Overall verdict: APPROVE
Critical Issues
None.
Major Issues
None.
Minor Issues
-
.github/workflows/claude-pr-review.yml:442—RAW_JSON="$(mktemp)"is not added to thetrap 'rm -f ...' EXITthat coversSYSTEM_FILE(line 248). BothREVIEW_RAWandCLAUDE_ERR(line ~423) have the same omission. In the containerized runner this is harmless since the process environment is ephemeral, but it's inconsistent with the existingSYSTEM_FILEpattern. Fix: extend the trap totrap 'rm -f "$SYSTEM_FILE" "$RAW_JSON" "$CLAUDE_ERR" "$REVIEW_RAW" "$REVIEW_BODY"' EXIT(or set it after all mktemps are created). -
.github/workflows/claude-pr-review.yml:499— the error path doescat "$RAW_JSON"which dumps the entire JSON envelope, including.result, to the Actions log. The comment at line 484 says "Never log the review text — that gets posted." On a failure with partial output,.resultcan be non-empty. In a failure case no review is posted so there's no real confusion, but the comment and the behaviour are in tension. Suppress.resultin the error dump, or note the exception in the comment. -
.github/workflows/claude-pr-review.yml:506— the subtype stringserror_max_turnsanderror_during_executionare undocumented CLI internals (per the PR's own prose on--max-turns). If the CLI emits a different string (e.g.max_turns_reached), the specific annotation fires on the*)catch-all instead, which is still a loud failure — just with a less actionable message. Not a silent failure. Worth noting in the comment alongside the--max-turnsundocumented-flag warning so future maintainers know both names may drift. -
.github/workflows/claude-pr-review.yml:104— the comment "At the observed ~6s/turn, 120 turns is ~12 minutes, well inside this" understates tail latency. Tool-call-heavy turns (filesystem reads,ghcalls) can take 20–30 s; 120 of them at 25 s/turn is 50 minutes, well outside 30. Thetimeout-minuteswall clock is the real bound and does the right thing regardless, but the comment's arithmetic should acknowledge P99, not just the median, to avoid giving future maintainers false confidence thattimeout-minuteshas headroom to spare.
Security Findings
None.
Positive Observations
- The core fix is correct and well-motivated: switching to
--output-format jsonand capturingCLAUDE_RCindependently ofis_errorcloses a real silent-failure mode that had already been observed in production. - The fail-closed default for unparseable JSON (
|| echo true) is the right choice. - Structured
::group::/::endgroup::blocks are a clean upgrade over barecatdumps. - The
null-result guard ([ "$(cat "$REVIEW_RAW")" = "null" ]) correctly handles thejq -rbehaviour when.resultis absent or JSON null. - Turn-cap reasoning (treat as runaway guard, not budget; size from observed P99 at the cap, not mean) is sound, and the inline data table makes the rationale auditable.
- The
set +e/CLAUDE_RC=$?/set -ebracket is the right pattern for capturing a non-zero exit without triggeringset -eprematurely.
Addresses items 1 and 2 of #41. Ported from the
Cortexa-LLC/mcpimplementation, which has been running these across 8+ live reviews on 5 PRs — the reporter offered the port back and it is better evidence than anything I'd write fresh.Items 3–5 (the shepherd's model of this reviewer) are a separate theme and follow in their own PR.
Item 1 —
--output-format textmade a max-turns exhaustion undiagnosabletextemits the final assistant message and nothing else. A run that ends without a final message exits non-zero with both streams empty. PR #35's stderr capture cannot reach this case — there is no stderr to capture. The observed shape was ~3 minutes of work, exit 1, and nothing to explain it.--output-format jsonalways carriessubtype/is_error/num_turns. Now:.resultis_errorenvelope are independent failure signals — either is fatalis_errorto true, so it fails closedItem 2 —
--max-turns 15was too low and failed silentlyFour real runs, same reviewer persona:
Turn count tracks how far the reviewer wanders, not diff size — the largest diff used the fewest turns. And two of four stopped exactly at the cap, so the true ceiling is unknown and a snug cap is the wrong shape.
Raised to 120, treated as a runaway guard rather than a budget;
timeout-minutes20 → 30 since the two bound the same run (~6s/turn puts 120 turns near 12 minutes, inside a 30-minute wall clock).This is the item with teeth: exceeding the cap yields no review at all, not a short one, and the review job is
continue-on-error— so the reviewer could have stopped posting on every PR while everything stayed green.Verification
The invocation and failure-handling block was extracted from the workflow and driven with a stubbed CLI across six envelope shapes:
.resulterror_during_executionis_error=true.resultnullPlus: YAML parses,
bash -non the extracted run block,actionlintclean, and the backslash-continuation guard from #40 passes.Note
No version bump —
.github/only, nothing underplugin/, which the new gate from #44 confirms is the right call.🤖 Generated with Claude Code
https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc