From 66362fce3ffe43bf3c05315a19259c4c83983171 Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Tue, 25 Aug 2026 16:50:39 -0700 Subject: [PATCH 1/6] ci(v5.2): sync bot caller workflows with main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Content matches bump-ai-review-prompts-pin (#2328) — identical to main once that merges. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt --- .github/workflows/claude-issue-to-pr.yml | 4 ++-- .github/workflows/claude-mention.yml | 4 ++-- .github/workflows/claude-review.yml | 12 ++---------- .github/workflows/gemini-review.yml | 4 ++-- .github/workflows/validate-caller-workflows.yml | 4 ++-- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/.github/workflows/claude-issue-to-pr.yml b/.github/workflows/claude-issue-to-pr.yml index ba9bc2ef69..1f818433e5 100644 --- a/.github/workflows/claude-issue-to-pr.yml +++ b/.github/workflows/claude-issue-to-pr.yml @@ -22,11 +22,11 @@ concurrency: jobs: work: - uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@54d9e61c9c827d19e34229a5c460773a9fb080a0 # main 2026-07-24 (#79: deep path / :bug,:test escalation -> claude-opus-5) + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) with: # Same SHA as the `uses:` ref above. See the comment in # claude-mention.yml for why the duplication is unavoidable. - ai-review-prompts-ref: 54d9e61c9c827d19e34229a5c460773a9fb080a0 + ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 repo-specific-conventions: | ## Harper core notes diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index 56853acd73..03ac48ff37 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -24,7 +24,7 @@ concurrency: jobs: mention: - uses: HarperFast/ai-review-prompts/.github/workflows/_claude-mention.yml@54d9e61c9c827d19e34229a5c460773a9fb080a0 # main 2026-07-24 (#79: deep path / :bug,:test escalation -> claude-opus-5) + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-mention.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) with: # Same SHA as the `uses:` ref above. The reusable uses this to # check out HarperFast/ai-review-prompts (parse + auth scripts) @@ -34,7 +34,7 @@ jobs: # introspect their own ref (`github.workflow_ref` resolves to # the CALLER's ref in `workflow_call` context), and `uses: …@` # is parsed literally so we can't interpolate a variable. - ai-review-prompts-ref: 54d9e61c9c827d19e34229a5c460773a9fb080a0 + ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 repo-specific-conventions: | ## Harper core notes diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 77d438c5ff..9d6fc3d9aa 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -33,7 +33,7 @@ jobs: # Note: the `claude-review` label name is matched there too — # `_claude-review.yml`'s authorize `if:`, not in this caller. if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' }} - uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@4b59dc0ddb15aff517884127b58204f79193b4f2 # main 2026-08-20 (#86 reliable review evidence, context, and run binding) + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below # what they need and breaks the workflow at startup; see @@ -60,15 +60,7 @@ jobs: # introspect their own ref (`github.workflow_ref` resolves to the # CALLER's ref in `workflow_call` context), and `uses: …@` # is parsed literally so we can't interpolate a variable. - ai-review-prompts-ref: 4b59dc0ddb15aff517884127b58204f79193b4f2 - # CANARY (2026-07-11): run this repo's Claude reviews on Sonnet 5 - # while the fleet default stays claude-sonnet-4-6. Every ai-review-log - # entry records `Model:`, so calibration can compare sonnet-5 vs - # sonnet-4-6 verdict mix directly — watch the severity-deflation rate - # (Sonnet 5 follows blocker-only instructions more literally; known - # code-review-harness effect). Promote to the reusable's default or - # revert based on the next calibration cycle. - model: claude-sonnet-5 + ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 review-layers: | universal harper/common diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index 46462868b6..8404e7e419 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -52,7 +52,7 @@ jobs: # Note: the `gemini-review` label name is matched there too — # `_gemini-review.yml`'s authorize `if:`, not in this caller. if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' }} - uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@4b59dc0ddb15aff517884127b58204f79193b4f2 # main 2026-08-20 (#86 reliable review evidence, context, and run binding) + uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below # what they need and breaks the workflow at startup; see @@ -70,7 +70,7 @@ jobs: # duplication is unavoidable: reusable workflows can't introspect # their own ref (`github.workflow_ref` resolves to the CALLER's # ref in workflow_call context), and `uses: …@` is literal. - ai-review-prompts-ref: 4b59dc0ddb15aff517884127b58204f79193b4f2 + ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 review-layers: | universal harper/common diff --git a/.github/workflows/validate-caller-workflows.yml b/.github/workflows/validate-caller-workflows.yml index bc820f47c9..5f500f364d 100644 --- a/.github/workflows/validate-caller-workflows.yml +++ b/.github/workflows/validate-caller-workflows.yml @@ -27,9 +27,9 @@ on: jobs: validate: - uses: HarperFast/ai-review-prompts/.github/workflows/_validate-caller-workflows.yml@4b59dc0ddb15aff517884127b58204f79193b4f2 # main 2026-08-20 (#86 reliable review evidence, context, and run binding) + uses: HarperFast/ai-review-prompts/.github/workflows/_validate-caller-workflows.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) with: # Same SHA as the `uses:` ref above — the reusable uses this # to check out the validator script at the matching version. # Same SHA-twice pattern as the other caller workflows. - ai-review-prompts-ref: 4b59dc0ddb15aff517884127b58204f79193b4f2 + ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 From b705a473a4c3036e768a76148f27acfe5a6158ba Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Tue, 25 Aug 2026 19:06:05 -0700 Subject: [PATCH 2/6] ci: adopt ai-review-prompts #90 cost gates (pin be549ad) + ready_for_review trigger Draft PRs skip review until flipped ready (label still opts one in), mechanical diffs skip pre-run, reasoning effort scales with diff size (60/high, 1500/xhigh, else max), synchronize runs debounce 120s. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt --- .github/workflows/claude-issue-to-pr.yml | 4 ++-- .github/workflows/claude-mention.yml | 4 ++-- .github/workflows/claude-review.yml | 6 +++--- .github/workflows/gemini-review.yml | 6 +++--- .github/workflows/validate-caller-workflows.yml | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/claude-issue-to-pr.yml b/.github/workflows/claude-issue-to-pr.yml index 1f818433e5..a42bb06bb9 100644 --- a/.github/workflows/claude-issue-to-pr.yml +++ b/.github/workflows/claude-issue-to-pr.yml @@ -22,11 +22,11 @@ concurrency: jobs: work: - uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-issue-to-pr.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) with: # Same SHA as the `uses:` ref above. See the comment in # claude-mention.yml for why the duplication is unavoidable. - ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 + ai-review-prompts-ref: be549ad08aa6d34b909ea8b542a7ffebdaae1e81 repo-specific-conventions: | ## Harper core notes diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index 03ac48ff37..a19072a1bf 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -24,7 +24,7 @@ concurrency: jobs: mention: - uses: HarperFast/ai-review-prompts/.github/workflows/_claude-mention.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-mention.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) with: # Same SHA as the `uses:` ref above. The reusable uses this to # check out HarperFast/ai-review-prompts (parse + auth scripts) @@ -34,7 +34,7 @@ jobs: # introspect their own ref (`github.workflow_ref` resolves to # the CALLER's ref in `workflow_call` context), and `uses: …@` # is parsed literally so we can't interpolate a variable. - ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 + ai-review-prompts-ref: be549ad08aa6d34b909ea8b542a7ffebdaae1e81 repo-specific-conventions: | ## Harper core notes diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 9d6fc3d9aa..1edfcfdb96 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -18,7 +18,7 @@ on: pull_request: # `labeled` admits the `claude-review` label gesture for # bot-authored PRs (renovate, dependabot). See ai-review-prompts#38. - types: [opened, synchronize, reopened, labeled] + types: [opened, synchronize, reopened, ready_for_review, labeled] concurrency: group: claude-review-${{ github.event.pull_request.number }} @@ -33,7 +33,7 @@ jobs: # Note: the `claude-review` label name is matched there too — # `_claude-review.yml`'s authorize `if:`, not in this caller. if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' }} - uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) + uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below # what they need and breaks the workflow at startup; see @@ -60,7 +60,7 @@ jobs: # introspect their own ref (`github.workflow_ref` resolves to the # CALLER's ref in `workflow_call` context), and `uses: …@` # is parsed literally so we can't interpolate a variable. - ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 + ai-review-prompts-ref: be549ad08aa6d34b909ea8b542a7ffebdaae1e81 review-layers: | universal harper/common diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index 8404e7e419..eb4feb5cd5 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -32,7 +32,7 @@ on: # `labeled` admits the `gemini-review` opt-in gesture. `vars.*` # can't be read in `on:` (only in a job `if:`), so the trigger # lists the union and the `review` job gates on GEMINI_ALWAYS_ON. - types: [opened, synchronize, reopened, labeled] + types: [opened, synchronize, reopened, ready_for_review, labeled] concurrency: # Different group key from claude-review so the two providers can run @@ -52,7 +52,7 @@ jobs: # Note: the `gemini-review` label name is matched there too — # `_gemini-review.yml`'s authorize `if:`, not in this caller. if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' }} - uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) + uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below # what they need and breaks the workflow at startup; see @@ -70,7 +70,7 @@ jobs: # duplication is unavoidable: reusable workflows can't introspect # their own ref (`github.workflow_ref` resolves to the CALLER's # ref in workflow_call context), and `uses: …@` is literal. - ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 + ai-review-prompts-ref: be549ad08aa6d34b909ea8b542a7ffebdaae1e81 review-layers: | universal harper/common diff --git a/.github/workflows/validate-caller-workflows.yml b/.github/workflows/validate-caller-workflows.yml index 5f500f364d..3d11870bd6 100644 --- a/.github/workflows/validate-caller-workflows.yml +++ b/.github/workflows/validate-caller-workflows.yml @@ -27,9 +27,9 @@ on: jobs: validate: - uses: HarperFast/ai-review-prompts/.github/workflows/_validate-caller-workflows.yml@4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 # main 2026-08-25 (#89 fleet turn/effort/model defaults; #88 sibling + cannot-fail-test lenses; #85/#87 calibration) + uses: HarperFast/ai-review-prompts/.github/workflows/_validate-caller-workflows.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) with: # Same SHA as the `uses:` ref above — the reusable uses this # to check out the validator script at the matching version. # Same SHA-twice pattern as the other caller workflows. - ai-review-prompts-ref: 4632c5d93dae1e6548e6c30c5ced8fb2df69f4e5 + ai-review-prompts-ref: be549ad08aa6d34b909ea8b542a7ffebdaae1e81 From cedfc1a1ca057a518259cc4285a676840938e45d Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Wed, 26 Aug 2026 08:01:04 -0700 Subject: [PATCH 3/6] ci: admit ready_for_review on label-opted PRs in opt-in mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kriszyp review finding on the pin-bump PR: with *_ALWAYS_ON unset, the caller gate admitted only labeled events, so a PR opted in by label while draft never resumed review when flipped ready — the event died at the caller gate. Admit ready_for_review when the opt-in label is still present. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt --- .github/workflows/claude-review.yml | 5 ++++- .github/workflows/gemini-review.yml | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 1edfcfdb96..ebb021868a 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -32,7 +32,10 @@ jobs: # label. The reusable's authorize job still owns WHO is admitted. # Note: the `claude-review` label name is matched there too — # `_claude-review.yml`'s authorize `if:`, not in this caller. - if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' }} + # `ready_for_review` is admitted when the PR still carries the + # opt-in label, so a draft opted in via `claude-review` resumes + # review when it flips ready even with CLAUDE_ALWAYS_ON unset. + if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review')) }} uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index eb4feb5cd5..272e609d08 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -51,7 +51,9 @@ jobs: # (CODEOWNERS trust set; the labeler, not the author, on `labeled`). # Note: the `gemini-review` label name is matched there too — # `_gemini-review.yml`'s authorize `if:`, not in this caller. - if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' }} + # `ready_for_review` is admitted when the PR still carries the + # opt-in label — mirrors claude-review.yml. + if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review')) }} uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below From b501f2c8248808fcbbba34cf5b86d6f0d0b657c6 Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Wed, 26 Aug 2026 09:07:28 -0700 Subject: [PATCH 4/6] ci: keep ineligible runs out of the review concurrency group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kriszyp review finding: workflow-level cancel-in-progress fires before job if:, so in opt-in mode an ineligible event (a push, or a ready-flip without the opt-in label) cancels an in-flight label-triggered review and then skips — silently losing the requested review. Ineligible runs now take a unique run_id group and can never cancel an eligible one; eligible runs keep cancelling each other (the debounce contract). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt --- .github/workflows/claude-review.yml | 5 ++++- .github/workflows/gemini-review.yml | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index ebb021868a..87ecf38df0 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -21,7 +21,10 @@ on: types: [opened, synchronize, reopened, ready_for_review, labeled] concurrency: - group: claude-review-${{ github.event.pull_request.number }} + # Ineligible events (per the review job's opt-in gate) get a unique + # run_id group so they can never cancel an in-flight eligible review — + # workflow-level cancel-in-progress fires before job `if:` is evaluated. + group: claude-review-${{ github.event.pull_request.number }}-${{ (vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review'))) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index 272e609d08..ceb34bc5d4 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -39,7 +39,8 @@ concurrency: # in parallel on the same PR. cancel-in-progress is per-group, so a # synchronize push cancels the in-flight Gemini run without touching # the Claude run (and vice versa). - group: gemini-review-${{ github.event.pull_request.number }} + # Ineligible events get a unique run_id group — see claude-review.yml. + group: gemini-review-${{ github.event.pull_request.number }}-${{ (vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review'))) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: From 0715e9fa4a46c747c0ff957c6fe6226a443e246b Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Wed, 26 Aug 2026 10:40:59 -0700 Subject: [PATCH 5/6] ci: only the provider's own label makes a labeled run eligible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding (Codex + kriszyp, independently): the eligibility predicate admitted every labeled event, so in opt-in mode an unrelated label applied mid-review joined the eligible concurrency group, cancelled the running review, and the replacement then failed the reusable's exact-label gate — no completed review. The same unrelated-label cancellation existed in always-on mode before this series (any labeled event shared the group and authorize then skipped). The labeled branch now requires the provider's own label, in both the concurrency predicate and the review job gate, and unrelated-label events are ineligible in both modes. Event matrix (opt-in / always-on): - labeled(provider label): eligible / eligible — supersedes in-flight - labeled(other): ineligible / ineligible (was: cancelled + no review) - synchronize: ineligible / eligible - ready_for_review + label: eligible / eligible - ready_for_review, no label: ineligible / eligible - opened, reopened: ineligible / eligible Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt --- .github/workflows/claude-review.yml | 6 ++++-- .github/workflows/gemini-review.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 87ecf38df0..3b91edabab 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -21,10 +21,12 @@ on: types: [opened, synchronize, reopened, ready_for_review, labeled] concurrency: + # NOTE: this predicate and the review job's `if:` must stay textually + # in sync — GitHub Actions YAML has no anchors to share them. # Ineligible events (per the review job's opt-in gate) get a unique # run_id group so they can never cancel an in-flight eligible review — # workflow-level cancel-in-progress fires before job `if:` is evaluated. - group: claude-review-${{ github.event.pull_request.number }}-${{ (vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review'))) && 'eligible' || github.run_id }} + group: claude-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'claude-review') || (github.event.action != 'labeled' && vars.CLAUDE_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review'))) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: @@ -38,7 +40,7 @@ jobs: # `ready_for_review` is admitted when the PR still carries the # opt-in label, so a draft opted in via `claude-review` resumes # review when it flips ready even with CLAUDE_ALWAYS_ON unset. - if: ${{ vars.CLAUDE_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review')) }} + if: ${{ (github.event.action == 'labeled' && github.event.label.name == 'claude-review') || (github.event.action != 'labeled' && vars.CLAUDE_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review')) }} uses: HarperFast/ai-review-prompts/.github/workflows/_claude-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index ceb34bc5d4..c8941064f6 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -39,8 +39,10 @@ concurrency: # in parallel on the same PR. cancel-in-progress is per-group, so a # synchronize push cancels the in-flight Gemini run without touching # the Claude run (and vice versa). + # NOTE: this predicate and the review job's `if:` must stay textually + # in sync — GitHub Actions YAML has no anchors to share them. # Ineligible events get a unique run_id group — see claude-review.yml. - group: gemini-review-${{ github.event.pull_request.number }}-${{ (vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review'))) && 'eligible' || github.run_id }} + group: gemini-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'gemini-review') || (github.event.action != 'labeled' && vars.GEMINI_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review'))) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: @@ -54,7 +56,7 @@ jobs: # `_gemini-review.yml`'s authorize `if:`, not in this caller. # `ready_for_review` is admitted when the PR still carries the # opt-in label — mirrors claude-review.yml. - if: ${{ vars.GEMINI_ALWAYS_ON == 'true' || github.event.action == 'labeled' || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review')) }} + if: ${{ (github.event.action == 'labeled' && github.event.label.name == 'gemini-review') || (github.event.action != 'labeled' && vars.GEMINI_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review')) }} uses: HarperFast/ai-review-prompts/.github/workflows/_gemini-review.yml@be549ad08aa6d34b909ea8b542a7ffebdaae1e81 # main 2026-08-25 (#90 cost gates: draft skip, mechanical-diff skip, effort-by-size, debounce; #89 defaults; #88 lenses) # Caller-side permissions at the calling-job level (NOT workflow- # level — that placement caps the reusable's per-job grants below From 9c0e00d329c2b982de38147ed9fc9eef2805afe7 Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Wed, 26 Aug 2026 14:15:24 -0700 Subject: [PATCH 6/6] ci: ready_for_review runs review but never cancels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kriszyp follow-up finding: the label-opted ready_for_review event sat in the shared eligible concurrency group, so on a bot-authored PR it could cancel the trusted labeler's in-flight review and then be rejected by author-based authorization — no completed review. The cancelling set (concurrency predicate) now excludes ready_for_review; the running set (job gate) keeps it, so trusted-author ready-flips still review, without the power to cancel. Revisit if the reusable gains persisted-label authorization. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt --- .github/workflows/claude-review.yml | 9 ++++++--- .github/workflows/gemini-review.yml | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 3b91edabab..cc90d00c78 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -21,12 +21,15 @@ on: types: [opened, synchronize, reopened, ready_for_review, labeled] concurrency: - # NOTE: this predicate and the review job's `if:` must stay textually - # in sync — GitHub Actions YAML has no anchors to share them. + # NOTE: this predicate is deliberately NARROWER than the review job's + # `if:` — the cancelling set must be a subset of the running set. + # ready_for_review runs review (job gate) but never cancel: their + # authorization is author-based, so on a bot-authored PR they could + # kill a labeler-authorized run and then be rejected themselves. # Ineligible events (per the review job's opt-in gate) get a unique # run_id group so they can never cancel an in-flight eligible review — # workflow-level cancel-in-progress fires before job `if:` is evaluated. - group: claude-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'claude-review') || (github.event.action != 'labeled' && vars.CLAUDE_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'claude-review'))) && 'eligible' || github.run_id }} + group: claude-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'claude-review') || (github.event.action != 'labeled' && vars.CLAUDE_ALWAYS_ON == 'true')) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml index c8941064f6..bcab436224 100644 --- a/.github/workflows/gemini-review.yml +++ b/.github/workflows/gemini-review.yml @@ -39,10 +39,13 @@ concurrency: # in parallel on the same PR. cancel-in-progress is per-group, so a # synchronize push cancels the in-flight Gemini run without touching # the Claude run (and vice versa). - # NOTE: this predicate and the review job's `if:` must stay textually - # in sync — GitHub Actions YAML has no anchors to share them. + # NOTE: this predicate is deliberately NARROWER than the review job's + # `if:` — the cancelling set must be a subset of the running set. + # ready_for_review runs review (job gate) but never cancel: their + # authorization is author-based, so on a bot-authored PR they could + # kill a labeler-authorized run and then be rejected themselves. # Ineligible events get a unique run_id group — see claude-review.yml. - group: gemini-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'gemini-review') || (github.event.action != 'labeled' && vars.GEMINI_ALWAYS_ON == 'true') || (github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'gemini-review'))) && 'eligible' || github.run_id }} + group: gemini-review-${{ github.event.pull_request.number }}-${{ ((github.event.action == 'labeled' && github.event.label.name == 'gemini-review') || (github.event.action != 'labeled' && vars.GEMINI_ALWAYS_ON == 'true')) && 'eligible' || github.run_id }} cancel-in-progress: true jobs: