diff --git a/.github/workflows/tend-ci-fix.yaml b/.github/workflows/tend-ci-fix.yaml index 1dd5aed94733..da0da7a6a7e1 100644 --- a/.github/workflows/tend-ci-fix.yaml +++ b/.github/workflows/tend-ci-fix.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-mention.yaml b/.github/workflows/tend-mention.yaml index 725a430b1e54..80fd6bda4756 100644 --- a/.github/workflows/tend-mention.yaml +++ b/.github/workflows/tend-mention.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -117,14 +117,25 @@ jobs: - name: Verify bot engagement id: check run: | - # A relayed review event arrives as identifiers only ({kind, pr, - # id}): resolve them against the API before judging anything, so the - # words weighed below are the ones GitHub holds rather than whatever - # the payload carried. Any write-scoped actor can POST a dispatch, - # so a forged payload faces the same checks a real event does — and - # fetching by PR and id binds the two, so a payload pairing a real - # review with some other PR dies here instead of steering the - # handle job. The ids are spliced into API paths here and into the + # shellcheck shell=bash + # Pre-check for tend-mention: decide whether the mention is addressed to the + # bot — by name or by engagement — and so whether the agent boots at all. + # + # Inlined into the generated workflow (adopter repos have no copy of this + # file), so it stays self-contained: env in, GITHUB_OUTPUT out. + # + # env: BOT_NAME, EVENT_NAME, COMMENT_BODY, COMMENT_AUTHOR, COMMENT_AUTHOR_TYPE, + # ISSUE_BODY, ISSUE_OR_PR_NUMBER, ISSUE_AUTHOR, PR_URL, PAYLOAD_KIND, + # PAYLOAD_PR, PAYLOAD_ID, GITHUB_REPOSITORY, GITHUB_OUTPUT, GITHUB_TOKEN + # out: should_run, reason, url, ts + + # A relayed review event arrives as identifiers only ({kind, pr, id}): resolve + # them against the API before judging anything, so the words weighed below are + # the ones GitHub holds rather than whatever the payload carried. Any + # write-scoped actor can POST a dispatch, so a forged payload faces the same + # checks a real event does — and fetching by PR and id binds the two, so a + # payload pairing a real review with some other PR dies here instead of + # steering the handle job. The ids are spliced into API paths here and into the # prompt later, so reject anything but digits at this edge. KIND="$EVENT_NAME" if [ "$EVENT_NAME" = "repository_dispatch" ]; then @@ -140,16 +151,15 @@ jobs: echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0 fi REVIEW_AUTHOR=$(echo "$REVIEW" | jq -r '.user.login') - # REST reports the state uppercase (a webhook payload's is - # lowercase); normalize so the terminal-approval gate below - # reads one shape. + # REST reports the state uppercase (a webhook payload's is lowercase); + # normalize so the terminal-approval gate below reads one shape. REVIEW_STATE=$(echo "$REVIEW" | jq -r '.state | ascii_downcase') COMMENT_BODY=$(echo "$REVIEW" | jq -r '.body // ""') echo "url=$(echo "$REVIEW" | jq -r '.html_url')" >> "$GITHUB_OUTPUT" - # A review without `submitted_at` (a PENDING one, which only a - # forged dispatch can name) would otherwise write the string - # `null`, which handle's `date -d` rejects — failing the job - # red where the empty-value guard would have skipped it. + # A review without `submitted_at` (a PENDING one, which only a forged + # dispatch can name) would otherwise write the string `null`, which + # handle's `date -d` rejects — failing the job red where the empty-value + # guard would have skipped it. echo "ts=$(echo "$REVIEW" | jq -r '.submitted_at // empty')" >> "$GITHUB_OUTPUT" ;; pull_request_review_comment) @@ -180,117 +190,60 @@ jobs: exit 0 fi - # Skip the bot's own comments before anything else. The Bot-type - # check below only catches GitHub App / Bot accounts; our own bot is a - # PAT-based User account, so its comments would otherwise fall through - # to the engagement heuristics (bot-authored issue, "bot has prior - # comments") and spin up a handle job the prompt's self-loop guard - # then exits silently — pure waste. There is no legitimate - # self-summons: a bot comment that quotes a prior @-mention would - # re-match the mention check below and escape an after-placed guard, - # so this runs *before* the mention check. Covers issue_comment and - # pull_request_review_comment (the latter resolved from the dispatch - # above). Fires on, e.g., the monthly tracking-issue rollover where - # the bot posts evidence-gist links on its own issue. Scoped to - # comments on purpose: the pull_request_review *submission* kind is - # deliberately left out, since a review the bot leaves on its own PR - # is its reviewer role (the prompt is told to action it), not a - # self-loop. The three self-review skips below are author-keyed, but - # each is also narrowed to a case that leaves this run nothing to do: - # the terminal empty-body APPROVED gate, the synthetic reply - # container, and a bot review on a PR the bot did not author. None of - # them licenses a blanket self-review skip. + # The bot's own comments never summon it (its PAT-based User account is + # invisible to the Bot-type skip below). Placed *before* the mention check, + # since a bot comment can quote a prior @-mention. Comments only: the bot's + # review *submissions* are judged with the review kind below — a review carries + # reviewer-role signal a comment can't. if { [ "$KIND" = "issue_comment" ] || [ "$KIND" = "pull_request_review_comment" ]; } \ - && [ "$COMMENT_AUTHOR" = "prql-bot" ]; then + && [ "$COMMENT_AUTHOR" = "$BOT_NAME" ]; then echo "should_run=false" >> "$GITHUB_OUTPUT" exit 0 fi - if [ -n "$COMMENT_BODY" ] && printf '%s\n' "$COMMENT_BODY" | grep -qF '@prql-bot'; then + if [ -n "$COMMENT_BODY" ] && printf '%s\n' "$COMMENT_BODY" | grep -qF "@$BOT_NAME"; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "reason=mention" >> "$GITHUB_OUTPUT" exit 0 fi - # Undirected bot comments (deploy notifications, CI status) on a - # bot-authored PR otherwise fall through to the PR-author short-circuit - # below and spin up a no-op session — twice per notification when the - # source bot edits its comment. Skip them here. - # TODO: reassess — a quick pass of a small/fast model could replace - # this blanket skip with content-aware filtering (only drop comments - # that have no actionable signal), so we don't miss bot comments that - # actually warrant a response. + # Other bots' undirected comments (deploy notifications, CI status) summon by + # mention only, never by the engagement heuristics below — which would boot a + # no-op session per notification, twice when the source bot edits its comment. if [ "$KIND" = "issue_comment" ] && [ "$COMMENT_AUTHOR_TYPE" = "Bot" ]; then echo "should_run=false" >> "$GITHUB_OUTPUT" exit 0 fi - # The bot's own empty-body APPROVED review is terminal — nothing to - # act on. The engagement heuristic below counts this very review - # (BOT_REVIEWS > 0) and would spin up a no-op session. Gate on three - # things, not just author: review *state* (an approval is terminal; a - # bot CHANGES_REQUESTED/COMMENTED review keeps actionable signal) and - # an empty *body* (an approval carrying nits in its body may warrant - # follow-up changes, so let it fire). Together these preserve #166's - # actionable bot self-reviews. COMMENT_BODY holds the review body - # for a review kind. - if [ "$KIND" = "pull_request_review" ] \ - && [ "$REVIEW_AUTHOR" = "prql-bot" ] \ - && [ "$REVIEW_STATE" = "approved" ] \ - && [ -z "$COMMENT_BODY" ]; then - echo "should_run=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # A review's record includes review.body (checked above) but NOT the - # bodies of the inline comments attached to the review. Fetch them - # so a first-contact @-mention inside an inline comment is detected - # on PRs where the bot has no prior engagement. One object per line, - # so `--paginate` concatenates pages instead of reducing within one. - # Keep the `{body, in_reply_to_id}` construction: `in_reply_to_id` is - # an *optional* property, absent rather than null on a fresh comment, - # and building the object normalizes absent to null so the `== null` - # select below counts both shapes. A bare `.in_reply_to_id` stream - # would emit nothing for a fresh comment and skip every container. + # A review's record includes review.body (checked above) but NOT the bodies of + # the inline comments attached to the review. Fetch them so a first-contact + # @-mention inside an inline comment is detected on PRs where the bot has no + # prior engagement. One object per line, so `--paginate` concatenates pages + # instead of reducing within one. Keep the `{body, in_reply_to_id}` + # construction: `in_reply_to_id` is an *optional* property, absent rather than + # null on a fresh comment, and building the object normalizes absent to null so + # the `== null` select below counts both shapes. A bare `.in_reply_to_id` + # stream would emit nothing for a fresh comment and count every review as + # reply-only. if [ "$KIND" = "pull_request_review" ]; then INLINE=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID/comments" \ --jq '.[] | {body, in_reply_to_id}') - if printf '%s\n' "$INLINE" | jq -r '.body' | grep -qF '@prql-bot'; then + if printf '%s\n' "$INLINE" | jq -r '.body' | grep -qF "@$BOT_NAME"; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "reason=mention" >> "$GITHUB_OUTPUT" exit 0 fi - # Replying to a review thread wraps the reply in a synthetic - # zero-body COMMENTED review, so the bot's own inline reply arrives - # here as a `pull_request_review` submission as well as the - # `pull_request_review_comment` the skip above already drops. Same - # artifact, second event path: without this the engagement heuristic - # below sees a bot-authored PR (or BOT_REVIEWS > 0) and spins up a - # full handle job that the prompt's self-loop guard then exits. - # Narrower than the comment skip on purpose — a real review the bot - # submits with inline comments and no body carries actionable signal, - # so require that *every* inline comment be a reply - # (`in_reply_to_id` set). A container with any fresh inline comment - # still fires. - if [ "$REVIEW_AUTHOR" = "prql-bot" ] \ - && [ -z "$COMMENT_BODY" ] \ - && [ "$(printf '%s\n' "$INLINE" | jq -s '[.[] | select(.in_reply_to_id == null)] | length')" = "0" ]; then - echo "should_run=false" >> "$GITHUB_OUTPUT" - exit 0 - fi + FRESH_INLINE=$(printf '%s\n' "$INLINE" | jq -s '[.[] | select(.in_reply_to_id == null)] | length') - # Somebody else's content-free approval is terminal too. The gate - # above is author-keyed, so a human's bare APPROVED falls through to - # the PR-author short-circuit (on a bot-authored PR) or to - # BOT_REVIEWS (on one the bot has reviewed) and starts a session - # whose only possible outcome is a silent exit: an approval with no - # body and no inline comments asks for nothing, and the bot cannot - # merge on its own. Unlike the bot-authored gates this one requires - # `$INLINE` to be empty rather than reply-only — an approval whose - # nits live inline is a request to the PR's author, which on a - # bot-authored PR is a role the bot has to act in. + # A contentless approval — no body, no inline comments — asks for nothing, + # whoever submitted it, and the bot cannot merge on its own. Without this it + # reads as engagement below and boots a session whose only outcome is a + # silent exit. `approved` and `$INLINE` empty are both load-bearing: a bare + # COMMENTED review is how GitHub wraps a human's inline reply (not terminal), + # and an approval whose nits live inline is a request to the PR's author — on + # a bot-authored PR, a role the bot has to act in. if [ "$REVIEW_STATE" = "approved" ] \ && [ -z "$COMMENT_BODY" ] \ && [ -z "$INLINE" ]; then @@ -304,22 +257,22 @@ jobs: ISSUE_NUMBER="$ISSUE_OR_PR_NUMBER" if [ -z "$PR_URL" ]; then - if [ "$ISSUE_AUTHOR" = "prql-bot" ]; then + if [ "$ISSUE_AUTHOR" = "$BOT_NAME" ]; then echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 fi - if printf '%s\n' "$ISSUE_BODY" | grep -qF '@prql-bot'; then + if printf '%s\n' "$ISSUE_BODY" | grep -qF "@$BOT_NAME"; then echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 fi - # Don't reduce inside jq: `gh api --paginate` applies `--jq` once - # per page, so `| length` emits one count per page rather than one - # overall. Past 100 comments the variable holds e.g. `100\n7`, a - # numeric test on it errors with `integer expression expected`, - # and the failed test falls through to should_run=false — the bot - # goes quiet on its most-engaged threads. Capture the per-element - # stream and test it for emptiness — the bare substitution also - # keeps a failing `gh api` fatal under GHA's default `bash -e`. + # Don't reduce inside jq: `gh api --paginate` applies `--jq` once per page, + # so `| length` emits one count per page rather than one overall. Past 100 + # comments the variable holds e.g. `100\n7`, a numeric test on it errors + # with `integer expression expected`, and the failed test falls through to + # should_run=false — the bot goes quiet on its most-engaged threads. + # Capture the per-element stream and test it for emptiness — the bare + # substitution also keeps a failing `gh api` fatal under GHA's default + # `bash -e`. BOT_COMMENTS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \ - --jq '.[] | select(.user.login == "prql-bot") | .id') + --jq ".[] | select(.user.login == \"$BOT_NAME\") | .id") if [ -n "$BOT_COMMENTS" ]; then echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 fi @@ -332,41 +285,41 @@ jobs: fi PR_AUTHOR=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json author --jq '.author.login') - if [ "$PR_AUTHOR" = "prql-bot" ]; then - echo "should_run=true" >> "$GITHUB_OUTPUT" - echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0 - fi - # A review the bot leaves on someone else's PR leaves this run - # nothing to do: whatever the review warranted, the tend-review - # session that submitted it has already done — left the findings for - # a human author to act on (pushing to their branch unbidden is - # barred by conduct rules), or, on a dependency-bot PR where no - # author will act, pushed the fix itself. The BOT_REVIEWS heuristic - # below counts this very review, so without this gate the session - # always starts and always exits silently. Keyed on author alone — - # the APPROVED + empty-body gate above is the same shape narrowed to - # its one terminal leg, and reusing those clauses here would let every - # bodied COMMENTED review through. Placement carries the rest of the - # design: *after* the PR_AUTHOR short-circuit, which has already - # exited when the PR is the bot's own, so the reviewer-to-author - # handoff on a bot PR still fires; *after* the body and inline - # @-mention scans, so an explicit summons still wins. - if [ "$KIND" = "pull_request_review" ] \ - && [ "$REVIEW_AUTHOR" = "prql-bot" ]; then + # The bot's own review summons a session in exactly one shape: the reviewer + # role handing work to the author role — fresh content (a body, or an inline + # comment that isn't a reply) on a PR the bot authored. On another author's PR + # the review session already did whatever the review warranted; an empty-body + # reply-only review is the synthetic container GitHub wraps around an inline + # reply — the same comment the self-comment skip above drops on its other event + # path. Either would otherwise read as engagement below (the BOT_REVIEWS + # heuristic counts this very review) and boot a session that exits silently. + # Sits *after* the inline @-mention scan, so an explicit summons the bot quotes + # still wins. + if [ "$KIND" = "pull_request_review" ] && [ "$REVIEW_AUTHOR" = "$BOT_NAME" ]; then + if [ "$PR_AUTHOR" = "$BOT_NAME" ] \ + && { [ -n "$COMMENT_BODY" ] || [ "$FRESH_INLINE" -gt 0 ]; }; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0 + fi echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0 fi + if [ "$PR_AUTHOR" = "$BOT_NAME" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0 + fi + # Captured, not counted — see the note on the issue-comment lookup above. BOT_REVIEWS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \ - --jq '.[] | select(.user.login == "prql-bot") | .id') + --jq ".[] | select(.user.login == \"$BOT_NAME\") | .id") if [ -n "$BOT_REVIEWS" ]; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0 fi BOT_COMMENTS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ - --jq '.[] | select(.user.login == "prql-bot") | .id') + --jq ".[] | select(.user.login == \"$BOT_NAME\") | .id") if [ -n "$BOT_COMMENTS" ]; then echo "should_run=true" >> "$GITHUB_OUTPUT" echo "reason=participation" >> "$GITHUB_OUTPUT"; exit 0 @@ -375,6 +328,7 @@ jobs: echo "should_run=false" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + BOT_NAME: prql-bot EVENT_NAME: ${{ github.event_name }} COMMENT_BODY: ${{ github.event.comment.body }} COMMENT_AUTHOR: ${{ github.event.comment.user.login }} @@ -458,7 +412,7 @@ jobs: # the API record — the dispatch payload never carries one to spoof. EVENT_TS: ${{ github.event.comment.updated_at || needs.verify.outputs.ts || github.event.issue.updated_at }} - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-nightly.yaml b/.github/workflows/tend-nightly.yaml index 26bde9da4151..61225499004e 100644 --- a/.github/workflows/tend-nightly.yaml +++ b/.github/workflows/tend-nightly.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-notifications.yaml b/.github/workflows/tend-notifications.yaml index 3071412d5d69..c6f89addf7b4 100644 --- a/.github/workflows/tend-notifications.yaml +++ b/.github/workflows/tend-notifications.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -148,7 +148,7 @@ jobs: - uses: ./.github/actions/tend-setup if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' with: github_token: ${{ secrets.TEND_BOT_TOKEN }} diff --git a/.github/workflows/tend-review-runs.yaml b/.github/workflows/tend-review-runs.yaml index 6816e91fb044..b857f3a5e80a 100644 --- a/.github/workflows/tend-review-runs.yaml +++ b/.github/workflows/tend-review-runs.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-review.yaml b/.github/workflows/tend-review.yaml index fcbadded54b0..0eb8c028c931 100644 --- a/.github/workflows/tend-review.yaml +++ b/.github/workflows/tend-review.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -137,7 +137,7 @@ jobs: fetch-tags: true token: ${{ secrets.TEND_BOT_TOKEN }} - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 if: steps.gate.outputs.should_run == 'true' with: github_token: ${{ secrets.TEND_BOT_TOKEN }} diff --git a/.github/workflows/tend-triage.yaml b/.github/workflows/tend-triage.yaml index 4c166a936095..619188b47d4d 100644 --- a/.github/workflows/tend-triage.yaml +++ b/.github/workflows/tend-triage.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -37,7 +37,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/tend-weekly.yaml b/.github/workflows/tend-weekly.yaml index a75842b81841..416c773537b4 100644 --- a/.github/workflows/tend-weekly.yaml +++ b/.github/workflows/tend-weekly.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.1.15. Regenerate with: uvx tend@latest init +# Generated by tend 0.1.16. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To customize behavior, edit the relevant skill (for example, @@ -34,7 +34,7 @@ jobs: - uses: ./.github/actions/tend-setup - - uses: max-sixty/tend/claude@0.1.15 + - uses: max-sixty/tend/claude@0.1.16 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}