Repository navigation
chore: take back stranded review in progress and never wait on CI (#178) - #187
Merged
Merged
Conversation
…178) A review-issue cron run that died before posting a verdict (timeout, crash) left the PR labeled `review in progress`, which find-review-candidate.sh never selected again. - find-review-candidate.sh takes a stale `review in progress` back once its label is older than the new [stale-after-seconds] argument (default 900). It allows at most one take-back per head commit, counted from `labeled` events after the head commit or the last reset to `needs review`. - It defers PRs whose head check runs haven't completed and moves on to the next PR. - Issue-events lookups (including the existing `changes requested` one) now read every page. - New pending-checks.sh prints the number of pending check runs. Both the candidate query and the skill's cron-mode CI check use it, so the two can't disagree. - set-review-label.sh re-applies `review in progress` even when it's already present, so each attempt leaves a `labeled` event. - review-issue SKILL.md and the job's append_system_prompt now say never to wait for CI: if checks are pending, set `needs review` and stop. The job also disallows ScheduleWakeup and Monitor. - Tests use a fake gh driven by a JSON fixture.
…178) All four review-issue runs that timed out today were killed while their second-opinion reviewer subagent was still working; a full review of a medium-sized PR takes well over 10 minutes. Raise the job timeout to 25m and the take-back threshold (find-review-candidate.sh default and both call sites) to 1800s, the timeout plus 5 minutes.
garretpremo
commented
Oct 19, 2026
garretpremo
left a comment
Contributor
Author
There was a problem hiding this comment.
🤖 Automated review by claude — generated by the
review-issueskill. Treat as advisory; a human still owns the merge decision.
Looks good. A, B and C are implemented as the #178 triage decisions describe, every acceptance criterion is met and covered by tests, and CI is green at 4270a84. Nothing blocking.
What I checked beyond the diff:
- The 33 new tests pass locally (
bun teston the three new files). - I ran the PR's
find-review-candidate.sh 1800andpending-checks.shread-only against the real repo. Both PRs open right now are inreview in progress. The script took back #186 (label from 18:55Z, with its head committed after that, so it had 0 attempts), andpending-checks.sh 187printed0. An unknown PR number exits 1 with no count, so it fails closed. review-issue.yamlparses under claude-cron'sJobSchema(timeout: 25m), andbuildClaudeArgvemits--disallowed-tools "ScheduleWakeup Monitor".- claude-cron takes the job lock before preflight (
executor/run.ts), so the 1800s threshold only matters for interactive sessions, as the header says. The abandoned-run sweep scales to2 × timeout(50m), so the longer timeout doesn't trip it.
Non-blocking observations
- A check run that never completes (an orphaned
queuedrun, or a job waiting on a runner or an environment approval) defers the PR indefinitely. The only signal is thedeferrednote in the preflight stderr of claude-cron's collapsedskipped_preflightrow. That's the same invisible-stall shape #178 set out to fix, though much rarer. A later follow-up could stop treating a check as pending after some maximum age. - A new commit re-arms the cap by its committer date, the same as the existing
changes requestedbranch. A commit dated before the last attempt (committed locally earlier, pushed after the cap hit) won't re-arm it. The reset toneeds reviewremains the way out.
Nitpicks
- When the stranded label predates the current head (this PR's own state:
review in progressat 19:10Z, head committed at 19:31Z), the first run on the new head goes through the take-back path, and the cap still allows one more take-back after that. That means two take-backs on one head. It matches the triage rule exactly ("count events newer than the head commit; at 2 or more, skip"), and the invariant really is "at most two attempts per head commit or per reset". The header's "taken back at most once per head commit" is slightly loose for this case. pending-checks.shre-resolves the head withgh pr viewand re-sourcesgh-pin-account.sheven whenfind-review-candidate.shalready has the SHA. That's about two extraghcalls per gated PR inside the 30s preflight budget. Fine at today's PR volume.- Step 1 of
SKILL.mdstill runsgh pr checksup front, and the cron-mode guidance lives in step 4 and in the system prompt. A one-line pointer from step 1 to the step 4 callout would mean an agent that sees pending checks early doesn't have to rely on the system prompt alone.
garretpremo
commented
Oct 19, 2026
garretpremo
left a comment
Contributor
Author
There was a problem hiding this comment.
🤖 Final review by claude — generated by the
final-reviewskill. CI is green, no blockers, soak window elapsed. Marking approved and merging.
Approved.
Outstanding observations from first-pass review
- A check run that never completes (an orphaned
queuedrun, or a job waiting on a runner or an environment approval) defers the PR indefinitely. The only signal is thedeferrednote in the preflight stderr of claude-cron's collapsedskipped_preflightrow. That's the same invisible-stall shape #178 set out to fix, though much rarer. A later follow-up could stop treating a check as pending after some maximum age. - A new commit re-arms the cap by its committer date, the same as the existing
changes requestedbranch. A commit dated before the last attempt (committed locally earlier, pushed after the cap hit) won't re-arm it. The reset toneeds reviewremains the way out.
This was referenced Oct 19, 2026
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.
Closes #178
Summary
If a
review-issuecron run dies before posting a verdict (timeout, crash), the PR stays labeledreview in progress, andfind-review-candidate.shnever selects it again. PR #177 hit this: the run waited on queued CI, was killed at the 10-minute job timeout, and every later tick wasskipped_preflightuntil someone reset the label by hand.This PR implements the approach agreed in the #178 triage decisions:
review in progress, at most once per head commit.It also paginates the script's issue-events lookups, which the maintainer added to the scope from #183's triage. And it raises the job
timeoutfrom 10m to 25m: today's runs on #185, #186 and #187 all timed out because the review's second-opinion subagent runs past 10 minutes, not because of CI.What changes
find-review-candidate.shreview in progressreview in progressis eligible again once its latestlabeledevent is older than the new optional[stale-after-seconds]argument. The default is 1800: the job's 25-minute timeout plus 5 minutes./review-issuesessions.labeled: review in progressevents after the head commit's committer date, or after the lastlabeled: needs reviewif that is later. At 2 or more it skips the PR and prints a note on stderr. So a review that dies every time is retried once, not forever.needs review, re-arms it. That includes a cron run that gives up because CI is pending (C), so giving up doesn't use up the take-back.pending-checks.shbelow) is skipped, and the script moves on to the next PR.changes requestedbranch compared new commits against an old label.labeled_events(gh api --paginate ".../events?per_page=100", one line per event, because--jqruns once per page) andlabel_times(exact label match, then sort) now serve both thechanges requestedbranch and the newreview in progressbranch.changes requestedbranch was re-indented into anif/elifchain so that every path falls through to the shared pending-checks gate. Apart from the paginated lookup, its logic is unchanged.New
pending-checks.sh <pr-number>completed.0means nothing is pending. That covers checks that completed with any conclusion (failed, skipped and cancelled included), and a head with no check runs at all. The second case is real here:ci.ymlande2e.ymlare path-filtered.--paginate(because--jqruns once per page) and counts them in shell.commits/<sha>/check-runs, not the combined/statusendpoint (which reportspendingwhen there are no statuses) and notgh pr checks(which says "no checks reported" and exits 1 when there are none).needs reviewreset loop forever, because that reset re-arms the cap.set-review-label.shreview in progressis re-applied (DELETE, then POST) even when the PR already has it. GitHub records nolabeledevent when you add a label that is already there, so without this a take-back attempt would leave no trace and the cap could never trigger. Other labels keep the old behavior, because re-applyingfirst pass reviewedwould restart final-review's soak window.review-issue/SKILL.mdset-review-label.sheven when the PR already showsreview in progress.pending-checks.sh <pr>once, right before deciding.needs review, and stop.--watch, sleep or poll loops, background watchers,Monitor, orScheduleWakeup..claude-jobs/review-issue.yamltimeout: 10mbecomes25m. A full review of a medium-sized PR, including the reviewer subagent, needs roughly 10 to 15 minutes. Each of today's four timed-out runs was killed while its subagent was still working.1800explicitly at both call sites, with a comment tying it totimeout.append_system_prompt.pending-checks.sh.disallowed_tools: ["ScheduleWakeup", "Monitor"]. This was checked against claude-cron's strictJobSchema, and its argv builder emits--disallowed-tools "ScheduleWakeup Monitor".Tests
tests/fixtures/fake-gh-review.sh: a fakeghdriven by a JSON fixture.--paginateis passed.--jqruns once per page.labeled/unlabeledevents.gh pr viewfails for an unknown PR.failingCheckRunsknob makes the check-runs call fail with HTTP 502.tests/fixtures/fake-gh-review.ts: fixture builders and a runner, in the same pattern astests/notify-shipped-issues.test.ts.tests/find-review-candidate.test.ts: 24 tests.set-review-label.sh, and the next lookup is capped.tests/set-review-label.test.ts: 3 tests.tests/pending-checks.test.ts: 6 tests.0.0.All new tests
describe.skipIf(win32), like the existing script tests.Acceptance criteria
review in progresslabel is older than the threshold is selected. One younger than the threshold is not.labeled: review in progressevent, even though the label is already present.labeled: review in progressevents newer than its head commit is skipped, with a stderr warning. A newer head commit makes it eligible again.needs revieweligiblechanges requestedonly with a newer commitfirst pass reviewedandapprovednever selectedneeds reviewand the run exits without posting.changes requestedandreview in progressbranches. (Added to the scope by the maintainer.)tests/find-review-candidate.test.ts(fake-ghharness) covers all of the above, plusset-review-label.sh's re-apply path.chore:commit prefix.Test plan
bun test: 1124 pass / 0 failbun run lint: 0 errors (the 118 warnings already exist ondev; none come from the new files)shellcheck v0.10.0
--severity=warningover CI's file set (scripts/,.claude/skills/**/scripts/,.claude-jobs/,tests/): cleanMutation check on
find-review-candidate.sh. Each of these makes at least one test fail:--paginateremovedchanges requestedjudged from its earliest labelqueuedtreated as pendingRemoving
pipefailfrompending-checks.sh, or adding|| trueto its pipeline, makes both fail-closed tests fail. Without them, an API error would print0.Removing the
set-review-label.shre-apply fails the re-apply test and the end-to-end cap test.review-issue.yamlparses under claude-cron'sJobSchema(timeout 25m).The threshold tests pin the 1800 default: a 26-minute-old label (just past the timeout) is left alone, so a default at or below the 25-minute timeout fails that test, and so does the old 900.
Self-review: an independent reviewer subagent (superpowers:requesting-code-review) reviewed the diff, and its findings were addressed:
pending-checks.sh.Monitorwas added todisallowed_tools.A second pass on the delta found no Critical or Important issues.
This doesn't reach the live cron until it's on
mainand the shared checkout claude-cron runs from has been pulled. Until then, the manualset-review-label.sh <pr> "needs review"workaround still applies.Notes
set-review-label.sh's re-apply leaves a tiny window, between its DELETE and POST, in which a failed POST would leave the PR with no review-state label. The script exits non-zero in that case, so the agent sees the error.first pass reviewed, or a gate fails #183, which builds onlabeled_eventsandpending-checks.sh.