From 0dd8af14ec72474d2e8c5eb74e709e68b7a80ae4 Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Tue, 2 Jun 2026 00:13:48 -0700 Subject: [PATCH 1/5] Auto-address PR reviews: respond, push fixes, resolve threads Lets Claude automatically handle review feedback without an @claude mention. A submitted PR review now triggers the workflow when the reviewer is the Codex bot or a trusted human (OWNER/MEMBER/COLLABORATOR); approved/dismissed reviews are skipped. The @claude mention path is unchanged. - Broaden the job 'if:' with a second (no-mention) trigger branch, and guard against self-triggering via github.actor != 'claude[bot]'. - Add a per-PR concurrency group (cancel-in-progress) to bound the Claude<->reviewer feedback loop. - Set allowed_bots to the Codex bot so the action does not skip the bot-triggered run (it skips bot actors by default). - Extend the system prompt: enumerate unresolved review threads via GraphQL, fix valid findings, run tests, push, reply to each thread, and resolve ONLY the threads actually fixed (leave disputed ones open). Needs pull-requests: write (already granted) for resolveReviewThread. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/claude.yml | 55 +++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index fbbf2587a..e0a7bf977 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -10,16 +10,44 @@ on: pull_request_review: types: [submitted] +concurrency: + # Serialize Claude runs per PR/issue and cancel an in-progress run when a + # newer trigger arrives. This bounds the Claude<->reviewer feedback loop: + # when Claude pushes a fix and the reviewer (e.g. Codex) re-reviews, the new + # review supersedes the old run instead of stacking concurrent jobs. + group: claude-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: true + jobs: claude: + # Two ways to invoke Claude: + # 1. @claude mention path — any of the four events, but only from a + # trusted author (OWNER / MEMBER / COLLABORATOR). + # 2. Auto-address-review path — a submitted PR review (no @claude needed) + # from either the Codex review bot or a trusted human reviewer. This is + # what lets Claude automatically respond to reviews, push fixes, and + # resolve threads. Approved/dismissed reviews are skipped (nothing to do). + # `github.actor != 'claude[bot]'` guards against Claude triggering itself. if: | - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), - github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association) && - ( - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + github.actor != 'claude[bot]' && ( + ( + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), + github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association) + && ( + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ) + ) || ( + github.event_name == 'pull_request_review' + && github.event.review.state != 'approved' + && github.event.review.state != 'dismissed' + && ( + github.event.review.user.login == 'chatgpt-codex-connector[bot]' + || contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) + ) + ) ) runs-on: ubuntu-latest permissions: @@ -40,6 +68,12 @@ jobs: with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # The action skips bot-triggered runs by default. The auto-address-review + # path is triggered BY the Codex review bot, so allow that bot explicitly + # (scoped, not '*') — otherwise the run is silently skipped. Human @claude + # triggers are unaffected. + allowed_bots: 'chatgpt-codex-connector[bot]' + # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read @@ -75,10 +109,11 @@ jobs: # Allowed tools let Claude edit files, run the build/test suite, commit, # push, and open PRs. Bash is deny-by-default, so only the listed # commands are permitted (this list adds to the built-in Edit/Write). - # The job-level `if:` (OWNER / MEMBER / COLLABORATOR) is what gates who - # can invoke Claude; the system prompt only shapes WHAT it does. + # The job-level `if:` is what gates who can invoke Claude (trusted + # @claude mentions, plus auto-runs on Codex / trusted-human reviews); + # the system prompt only shapes WHAT it does. claude_args: | --model claude-opus-4-8 --allowed-tools "Bash(git:*),Bash(gh:*),Bash(python:*),Bash(pytest:*),Bash(pip:*),Edit,Write,WebSearch,WebFetch(domain:github.com),WebFetch(domain:api.github.com),WebFetch(domain:raw.githubusercontent.com),WebFetch(domain:arxiv.org),WebFetch(domain:docs.python.org),WebFetch(domain:readthedocs.io),WebFetch(domain:pyscf.org),WebFetch(domain:github.io),WebFetch(domain:wikipedia.org),WebFetch(domain:doi.org),WebFetch(domain:stackoverflow.com),WebFetch(domain:stackexchange.com),WebFetch(domain:chemrxiv.org),WebFetch(domain:semanticscholar.org)" - --append-system-prompt "When you are triggered on an existing open pull request (via a PR review, a review comment, or an issue comment on the PR), address the feedback directly: make the code changes, run the relevant tests when code is touched, commit to that PR's branch and push, then post a comment summarizing what you changed and how you verified it. When you are triggered on an issue, only create a branch and open a new pull request if the triggering comment explicitly asks for one (for example 'open a PR' or 'implement and push a branch'); otherwise respond with your findings or a suggested patch as a comment and ask for confirmation. IMPORTANT: GitHub forbids your token from creating or updating files under .github/workflows/ (the 'workflows permission' restriction), so any git push that touches a workflow file will be rejected. If a change requires editing .github/workflows/, do NOT attempt to commit or push it — instead post the full change as a diff/patch in a comment and ask a human to apply it manually." + --append-system-prompt "When you are triggered on an existing open pull request (via a PR review, a review comment, or an issue comment on the PR), address the feedback directly: make the code changes, run the relevant tests when code is touched, commit to that PR's branch and push, then post a comment summarizing what you changed and how you verified it. When you are triggered on an issue, only create a branch and open a new pull request if the triggering comment explicitly asks for one (for example 'open a PR' or 'implement and push a branch'); otherwise respond with your findings or a suggested patch as a comment and ask for confirmation. IMPORTANT: GitHub forbids your token from creating or updating files under .github/workflows/ (the 'workflows permission' restriction), so any git push that touches a workflow file will be rejected. If a change requires editing .github/workflows/, do NOT attempt to commit or push it — instead post the full change as a diff/patch in a comment and ask a human to apply it manually. When you are triggered by a submitted pull request review (including automated reviewers such as Codex), treat every UNRESOLVED review thread as a task. First enumerate the PR's review threads with `gh api graphql`, querying repository.pullRequest.reviewThreads and reading each node's id (the thread node id), isResolved, isOutdated, path, line, and its comments' databaseId, author login and body; skip any thread whose isResolved is already true. For each unresolved thread, judge the finding on its merits: if it is valid, implement the fix in the code. After addressing the findings, run the relevant tests when code is touched, then commit to the PR branch and push. Then, for each thread you handled, reply to it by POSTing to the REST endpoint repos/{owner}/{repo}/pulls/{number}/comments/{comment_databaseId}/replies (using the databaseId of the thread's first comment) explaining what you changed or why you did not change anything. ONLY resolve a thread whose finding you actually fixed, by calling the GraphQL resolveReviewThread mutation with that thread's node id. Do NOT resolve a thread you disagreed with or could not fix — leave it open with a reply explaining your reasoning. Finally, post one summary comment listing which findings you fixed and resolved, and which you left open and why." From b93f021bbd80f1d821af74ff0577450713c79bda Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Tue, 2 Jun 2026 00:20:29 -0700 Subject: [PATCH 2/5] Restrict review auto-run to Claude-authored PRs Only auto-address reviews (the no-@claude path) on PRs opened by claude[bot]. Reviews on human-authored PRs no longer trigger an Opus run automatically; maintainers can still invoke Claude there with an @claude mention. This narrows scope and further bounds the Claude<->reviewer loop. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/claude.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index e0a7bf977..105c23283 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -24,9 +24,11 @@ jobs: # 1. @claude mention path — any of the four events, but only from a # trusted author (OWNER / MEMBER / COLLABORATOR). # 2. Auto-address-review path — a submitted PR review (no @claude needed) - # from either the Codex review bot or a trusted human reviewer. This is - # what lets Claude automatically respond to reviews, push fixes, and - # resolve threads. Approved/dismissed reviews are skipped (nothing to do). + # from either the Codex review bot or a trusted human reviewer, but ONLY + # on PRs that Claude itself opened (pull_request.user is claude[bot]). + # This is what lets Claude automatically respond to reviews, push fixes, + # and resolve threads on its own PRs. Approved/dismissed reviews are + # skipped (nothing to do). # `github.actor != 'claude[bot]'` guards against Claude triggering itself. if: | github.actor != 'claude[bot]' && ( @@ -41,6 +43,7 @@ jobs: ) ) || ( github.event_name == 'pull_request_review' + && github.event.pull_request.user.login == 'claude[bot]' && github.event.review.state != 'approved' && github.event.review.state != 'dismissed' && ( From a5782d1b95edfc6b57de5c9c5e4a07cc65761c63 Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Tue, 2 Jun 2026 00:33:15 -0700 Subject: [PATCH 3/5] Fix review auto-run: set an explicit prompt so the run is not skipped Codex P1 (verified against the claude-code-action@v1 source): a pull_request_review with no @claude mention and no prompt resolves to agent mode (detector.ts default), and the prepare step skips agent-mode runs when the prompt is empty (prepare.ts gates on a non-empty prompt). So the no-mention auto-address path silently no-opped. Set a conditional 'prompt' that is non-empty ONLY for the no-mention pull_request_review case. A non-empty prompt selects agent mode and makes checkContainsTrigger true (trigger.ts: prompt provided -> always trigger), so the run proceeds with the PR number + task in context. Every other trigger evaluates to '' -> tag mode, unchanged. The prompt also tells Claude to 'gh pr checkout' the PR branch first, since the run starts on the base branch in this event. The detailed thread-handling procedure remains in --append-system-prompt and applies in both modes. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/claude.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 105c23283..3bd07ad3d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -102,12 +102,24 @@ jobs: } } - # NOTE: no `prompt:` input. Setting `prompt:` forces the action into - # "agent mode", which runs headlessly and does NOT post a reply back to - # the issue/PR. Omitting it selects "tag mode", which responds to the - # @claude mention and manages a response comment automatically. The - # behavioral guidance that used to live in `prompt:` is moved into - # `--append-system-prompt` below so tag mode keeps it. + # PROMPT / MODE SELECTION. + # claude-code-action auto-detects "tag" vs "agent" mode: + # - tag mode (empty prompt + @claude mention): posts/updates a tracking + # comment and auto-fetches issue/PR context. This is the @claude path. + # - agent mode (non-empty prompt): runs headlessly; the prepare step + # proceeds only when `prompt` is non-empty. + # A pull_request_review submitted WITHOUT an @claude mention matches + # neither the tag trigger (no mention) nor the agent trigger (empty + # prompt): the detector falls through to agent mode and the prepare step + # then SKIPS the run because the prompt is empty (verified in the action + # source: detector.ts returns "agent" by default, prepare.ts gates agent + # mode on a non-empty prompt). So set a prompt ONLY for that no-mention + # auto-review case — it selects agent mode, lets the run proceed, and + # carries the PR number + task. Every other trigger yields '' -> tag mode, + # unchanged. The detailed thread-handling procedure stays in + # `--append-system-prompt` below and applies in both modes. + prompt: "${{ (github.event_name == 'pull_request_review' && !contains(github.event.review.body, '@claude')) && format('A pull request review was submitted on PR #{0} of {1} without an @claude mention. First check out the PR branch with `gh pr checkout {0}` (this run starts on the base branch). Then follow the review-handling procedure in your system prompt: enumerate the PR''s unresolved review threads with `gh api graphql`, fix the valid findings, run the relevant tests when you touch code, commit and push to the PR branch, reply to each thread you handled, resolve ONLY the threads you actually fixed, and post one summary comment of what you fixed and what you left open.', github.event.pull_request.number, github.repository) || '' }}" + # # Allowed tools let Claude edit files, run the build/test suite, commit, # push, and open PRs. Bash is deny-by-default, so only the listed From 99366cf9fa23009ed8be0d8c74e329d28a95fef2 Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Tue, 2 Jun 2026 01:12:38 -0700 Subject: [PATCH 4/5] Address Codex P2s: review-body feedback + scoped concurrency Two follow-up findings on the auto-address-review path: 1. Body-only review feedback (P2): the auto prompt only told Claude to enumerate inline reviewThreads, so a COMMENTED/CHANGES_REQUESTED review whose feedback lives only in the top-level review body would be missed (the body is not a review thread, and agent mode does not auto-fetch the triggering review). Pass github.event.review.body into the prompt and instruct Claude to address it in addition to inline threads. 2. Over-broad concurrency (P2): the per-PR group with cancel-in-progress would cancel an in-flight @claude mention run whenever any later event hit the same PR/issue. Make the group conditional: a shared per-PR group ('claude-autoreview-') only for the auto-review path so a newer review supersedes the in-flight auto-run (the loop bound), and a unique group ('claude-mention-') keyed by the triggering comment/review/issue id for mention runs so they always run to completion. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/claude.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 3bd07ad3d..144ea9888 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -11,11 +11,15 @@ on: types: [submitted] concurrency: - # Serialize Claude runs per PR/issue and cancel an in-progress run when a - # newer trigger arrives. This bounds the Claude<->reviewer feedback loop: - # when Claude pushes a fix and the reviewer (e.g. Codex) re-reviews, the new - # review supersedes the old run instead of stacking concurrent jobs. - group: claude-${{ github.event.issue.number || github.event.pull_request.number }} + # Bound the Claude<->reviewer loop WITHOUT letting unrelated events cancel a + # normal @claude run: + # - Auto-review path: one shared per-PR group, so a newer review supersedes + # an in-flight auto-run (this is the loop bound). + # - @claude mention path: a unique group keyed by the triggering + # comment/review/issue id, so each mention run completes and is never + # cancelled by a later event on the same PR/issue. + # cancel-in-progress only ever has an effect on the shared auto-review group. + group: "${{ (github.event_name == 'pull_request_review' && !contains(github.event.review.body, '@claude')) && format('claude-autoreview-{0}', github.event.pull_request.number) || format('claude-mention-{0}', github.event.comment.id || github.event.review.id || github.event.issue.id || github.run_id) }}" cancel-in-progress: true jobs: @@ -118,7 +122,7 @@ jobs: # carries the PR number + task. Every other trigger yields '' -> tag mode, # unchanged. The detailed thread-handling procedure stays in # `--append-system-prompt` below and applies in both modes. - prompt: "${{ (github.event_name == 'pull_request_review' && !contains(github.event.review.body, '@claude')) && format('A pull request review was submitted on PR #{0} of {1} without an @claude mention. First check out the PR branch with `gh pr checkout {0}` (this run starts on the base branch). Then follow the review-handling procedure in your system prompt: enumerate the PR''s unresolved review threads with `gh api graphql`, fix the valid findings, run the relevant tests when you touch code, commit and push to the PR branch, reply to each thread you handled, resolve ONLY the threads you actually fixed, and post one summary comment of what you fixed and what you left open.', github.event.pull_request.number, github.repository) || '' }}" + prompt: "${{ (github.event_name == 'pull_request_review' && !contains(github.event.review.body, '@claude')) && format('A pull request review was submitted on PR #{0} of {1} without an @claude mention. First check out the PR branch with `gh pr checkout {0}` (this run starts on the base branch). The reviewer''s top-level review body was: <<<{2}>>>. Treat that body as feedback to address as well, in addition to any inline review threads (the body is NOT a review thread and will not appear when you enumerate threads). Then follow the review-handling procedure in your system prompt: enumerate the PR''s unresolved review threads with `gh api graphql`, fix the valid findings (and the points raised in the review body), run the relevant tests when you touch code, commit and push to the PR branch, reply to each inline thread you handled, resolve ONLY the threads you actually fixed, and post one summary comment of what you fixed and what you left open (replying to the review body there if it had no inline threads).', github.event.pull_request.number, github.repository, github.event.review.body) || '' }}" # # Allowed tools let Claude edit files, run the build/test suite, commit, From 1e32503dfd03a0c34ebd9f2a281762dd89d33fa5 Mon Sep 17 00:00:00 2001 From: Xing Zhang Date: Tue, 2 Jun 2026 01:17:30 -0700 Subject: [PATCH 5/5] Scope auto-review concurrency to eligible reviews only Codex P2: the workflow-level concurrency group is evaluated before the job-level if, so an ineligible pull_request_review (approved/dismissed, untrusted reviewer, non-Claude PR, or an @claude mention) still mapped to the shared 'claude-autoreview-' group and, with cancel-in-progress, would cancel a legitimate in-flight auto-fix run before its own job was skipped. Duplicate the auto-path eligibility checks from the job 'if' into the group expression: only reviews the job would actually accept for the auto path share 'claude-autoreview-'; every other event gets a unique 'claude-solo-' group and cancels nothing. Added a comment to keep the two predicates in sync. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/claude.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 144ea9888..1ece2010c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -13,13 +13,19 @@ on: concurrency: # Bound the Claude<->reviewer loop WITHOUT letting unrelated events cancel a # normal @claude run: - # - Auto-review path: one shared per-PR group, so a newer review supersedes - # an in-flight auto-run (this is the loop bound). - # - @claude mention path: a unique group keyed by the triggering - # comment/review/issue id, so each mention run completes and is never - # cancelled by a later event on the same PR/issue. - # cancel-in-progress only ever has an effect on the shared auto-review group. - group: "${{ (github.event_name == 'pull_request_review' && !contains(github.event.review.body, '@claude')) && format('claude-autoreview-{0}', github.event.pull_request.number) || format('claude-mention-{0}', github.event.comment.id || github.event.review.id || github.event.issue.id || github.run_id) }}" + # - Auto-review path: only reviews that the job-level `if` would actually + # ACCEPT for the auto path share one per-PR group ('claude-autoreview-'), + # so a newer eligible review supersedes an in-flight auto-run (the loop + # bound). The eligibility test is duplicated here because concurrency is + # evaluated BEFORE the job `if`: an ineligible review (approved/dismissed, + # untrusted reviewer, non-Claude PR, or @claude-mention) must NOT land in + # the shared group, or it would cancel a legitimate in-flight auto-fix run + # and then skip its own job. Keep these checks in sync with the job `if`. + # - Everything else (mention runs + skipped/ineligible review events): a + # unique group ('claude-solo-') keyed by the triggering event id, so it + # runs to completion and never cancels anything. + # cancel-in-progress therefore only ever affects the shared auto-review group. + group: "${{ (github.event_name == 'pull_request_review' && !contains(github.event.review.body, '@claude') && github.actor != 'claude[bot]' && github.event.pull_request.user.login == 'claude[bot]' && github.event.review.state != 'approved' && github.event.review.state != 'dismissed' && (github.event.review.user.login == 'chatgpt-codex-connector[bot]' || github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'MEMBER' || github.event.review.author_association == 'COLLABORATOR')) && format('claude-autoreview-{0}', github.event.pull_request.number) || format('claude-solo-{0}', github.event.comment.id || github.event.review.id || github.event.issue.id || github.run_id) }}" cancel-in-progress: true jobs: