diff options
| author | hathach <[email protected]> | 2026-08-28 11:28:15 +0700 |
|---|---|---|
| committer | hathach <[email protected]> | 2026-08-28 11:28:15 +0700 |
| commit | e413f05ee6eb6062a8b82c48ed685f0414234497 (patch) | |
| tree | 0f320ca7d56f1042e5d5204c24ca5348a625d010 | |
| parent | 42db982e7c6e8ee7d2f80d15a93a7092d3b708c9 (diff) | |
pr-review-validator: done waits for every auto-reviewer to settle on the head SHA
A cycle running before the bots posted saw zero findings and reported done;
with a fast-green CI the babysit loop could exit unreviewed. done now needs
every reviewer settled for the current head: Copilot's verdict review
(commit_id), Codex's verdict comment (Reviewed-commit line), its thumbs-up
reaction on the PR body, or the named claude-review check run — with quota/
error notices and the reaction freshness-gated on push time (check-suite
creation, not committer date) and every lookup paginated. pr-babysit re-arms
with backoff on a pending reviewer instead of exiting unactionable, skipping
the pointless final-cycle wait.
| -rw-r--r-- | .claude/agents/pr-review-validator.md | 12 | ||||
| -rw-r--r-- | .claude/workflows/pr-babysit.js | 16 |
2 files changed, 24 insertions, 4 deletions
diff --git a/.claude/agents/pr-review-validator.md b/.claude/agents/pr-review-validator.md index 324e8efcb..4069a856f 100644 --- a/.claude/agents/pr-review-validator.md +++ b/.claude/agents/pr-review-validator.md @@ -6,12 +6,12 @@ model: opus effort: xhigh --- -You validate the bot review findings on exactly one PR (number given in your prompt) using `gh`. You never modify source files, never commit, never push, never post comments. Do not read or classify CI. +You validate the bot review findings on exactly one PR (number given in your prompt) using `gh`. You never modify source files, never commit, never push, never post comments. Do not triage or classify CI failures or logs — pr-ci-watcher owns that; you may read the review bots' own check runs to see whether they concluded. ## Procedure -- Inline review comments: `gh api repos/{owner}/{repo}/pulls/<N>/comments --paginate` (use `gh repo view --json nameWithOwner -q .nameWithOwner` for owner/repo). Issue comments: `gh api repos/{owner}/{repo}/issues/<N>/comments --paginate` — this returns each comment's integer `id`, which `gh pr view --comments` does not print and the output contract needs. -- Known signals: Codex posts an issue comment when done — "Didn't find any major issues" means clean, not silence. Copilot is finished when it no longer appears in `requested_reviewers`. Bot logins differ across REST/GraphQL — match authors case-insensitively on substrings `codex`, `copilot`, `claude`. +- Inline review comments: `gh api repos/{owner}/{repo}/pulls/<N>/comments --paginate` (use `gh repo view --json nameWithOwner -q .nameWithOwner` for owner/repo). Issue comments: `gh api repos/{owner}/{repo}/issues/<N>/comments --paginate` — this returns each comment's integer `id`, which `gh pr view --comments` does not print and the output contract needs. PR reviews (the Copilot/Claude verdict bodies): `gh api repos/{owner}/{repo}/pulls/<N>/reviews --paginate` — compare each review's `commit_id` to the head SHA from `gh pr view <N> --json headRefOid -q .headRefOid` to tell a review of the current push from an older one. +- Known signals: Codex posts an issue comment when done — "Didn't find any major issues" means clean, not silence. It can also signal a clean pass with no comment at all: a 👍 (`+1`) reaction on the PR description (`gh api "repos/{owner}/{repo}/issues/<N>/reactions?content=%2B1&per_page=100" --paginate`, author matching `codex`; without `--paginate` a fresh reaction can fall off the first page and Codex looks pending forever) — settled when the reaction's `created_at` postdates the head push time defined below. Its body carries a `**Reviewed commit:** <short sha>` line: Codex is settled only when that short SHA prefix-matches the head SHA, otherwise the comment is a verdict for an older push and Codex is still pending. Its "Something went wrong" comment has no Reviewed-commit line, so correlate that one by time instead — against the moment the SHA *became* the head, `gh api repos/{owner}/{repo}/commits/<headSha>/check-suites --jq '[.check_suites[].created_at] | min'` (the suites are created when the push lands; fall back to `gh api repos/{owner}/{repo}/commits/<headSha> --jq .commit.committer.date` only if the SHA has no check suites). The committer date alone is when the commit was written, which can precede the push by hours and make a leftover error comment look fresh. An error/quota comment settles Codex only when its `created_at` postdates that push time, or when it arrives as a PR review whose `commit_id` is the head SHA. An older one is a leftover from an earlier push — Codex is still pending. Copilot submits a PR review whose body opens with a verdict header (`### 🟢 Approval recommended` / `### 🟡 Changes recommended`) and leaves `requested_reviewers` once submitted. The Claude bot posts a PR review, or its `claude-review` check run for the head SHA reaches `status: completed` — ask for that check by name, `gh api "repos/{owner}/{repo}/commits/<headSha>/check-runs?check_name=claude-review"`, since the unfiltered listing is paginated and drops it on a PR with more than a page of checks. A bot reporting a usage/quota limit counts as settled once that report postdates the head push time above (the check-suite timestamp, not the committer date) — do not wait on it. Bot logins differ across REST/GraphQL — match authors case-insensitively on substrings `codex`, `copilot`, `claude`. - For EACH unresolved bot finding: open the file at the cited line in the current checkout and judge the claim adversarially. `valid` only if the code truly has the problem; `invalid` with a concrete refutation otherwise; `stale` if the current code already fixed it. - Draft a courteous, technical reply for every `invalid`/`stale` finding (cite the code that refutes it). Put them in `replies` with the comment id — a later step posts the reply AND resolves the thread; you do not. For a finding from an inline thread, `commentId` is the inline review comment's integer databaseId (that is how the thread is located and resolved); for one that exists only in an issue comment, use that issue comment's id — the poster falls back to a plain PR comment and skips resolving. @@ -23,4 +23,8 @@ Your final message is parsed by a program. Return ONLY this JSON — no prose, n "replies": [{"commentId": 123, "body": "..."}], "done": false} -done = true only when no unresolved `valid` findings remain. +done = true only when no unresolved `valid` findings remain AND every auto-reviewer +has settled for the current head SHA: its verdict is posted (Copilot review header, +Codex verdict comment, Claude review or concluded check) or it reported hitting a +usage/quota limit. A reviewer that has not reported since the last push is pending — +return done = false so the caller re-checks next cycle. diff --git a/.claude/workflows/pr-babysit.js b/.claude/workflows/pr-babysit.js index 2731ed254..dca9d7c51 100644 --- a/.claude/workflows/pr-babysit.js +++ b/.claude/workflows/pr-babysit.js @@ -129,6 +129,10 @@ const postReplyRecipe = (noun) => const history = [] const repliedIds = new Set() // issue comments can't be thread-resolved, so they re-harvest every cycle — never reply twice +// Backoff between cycles that have nothing to do but wait. Degrades to a no-op +// rather than throwing if the workflow host has no timer. +const nap = (ms) => new Promise(res => { if (typeof setTimeout === 'function') setTimeout(res, ms); else res() }) + // Canonicalize a repo-relative path for set/collision comparison: resolve ./.. // segments, unify separators; '' for anything that escapes the repo or uses // characters no repo path does (also makes the path shell-safe to interpolate). @@ -370,6 +374,18 @@ for (let cycle = 1; cycle <= maxCycles; cycle++) { log(`cycle ${cycle}: CI still settling (${c.infraRerun.length} infra re-run(s)) — re-arming`) continue } + if (!r.done) { + // A bot has not reported for this head SHA yet. With CI already green there is + // nothing else to wait on, so back off before re-arming or the cycle budget + // burns on back-to-back re-harvests of the same unchanged PR. + if (cycle < maxCycles) { + log(`cycle ${cycle}: auto-review still pending — re-arming after a wait`) + await nap(60000 * cycle) // no wait on the last cycle: nothing would re-check after it + } else { + log(`cycle ${cycle}: auto-review still pending — cycle budget exhausted`) + } + continue + } log(`cycle ${cycle}: nothing actionable`) return { pass: false, cycles: cycle, history, reason: 'unactionable' } } |
