From 04b625b3480b8ffc53c5e27d7560415cfe8176d3 Mon Sep 17 00:00:00 2001 From: Vader Yang Date: Fri, 22 May 2026 20:28:38 +0800 Subject: [PATCH 1/2] Add agent-bot: triage + wiwi dev agent + auto-merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to the vivi PR-reviewer. Two new workflows + one wired-in step on pr-review.yml. issue-triage.yml — fires on label `agent:assess`. Strict 5-gate verdict (clear acceptance criteria, <300 LOC, contained scope, no new deps/secrets, deterministic test). verdict=`do` auto-adds `agent:try`; else posts a comment and waits for a human. issue-implement.yml — fires on label `agent:try` (added by triage or manually). "wiwi" branches off main, implements, runs the build, opens a DRAFT PR labelled `auto-agent` with `Closes #N`. Uses AGENT_GH_TOKEN (PAT) so the PR's downstream ci/pr-review workflows actually fire. pr-review.yml — appended an "Auto-merge if eligible" step. After vivi posts her review: if the PR has `auto-agent` AND vivi's latest verdict is APPROVED AND the linked issue's author is on the TEAM list (vaderyang / william / timmy), promote draft → ready and admin-squash-merge. Non-team-authored PRs always wait for human merge. Reuses existing LITELLM_BASE_URL / LITELLM_API_KEY / LITELLM_NO_PROXY secrets. New secret AGENT_GH_TOKEN required (PAT with repo + workflow scopes) so the spawned PR triggers downstream checks and admin-merge has bypass. --- .github/workflows/issue-implement.yml | 62 ++++++++++++++++++ .github/workflows/issue-triage.yml | 55 ++++++++++++++++ .github/workflows/pr-review.yml | 11 ++++ scripts/agent-bot/auto_merge.sh | 37 +++++++++++ scripts/agent-bot/run_triage.sh | 91 +++++++++++++++++++++++++++ scripts/agent-bot/run_wiwi.sh | 83 ++++++++++++++++++++++++ 6 files changed, 339 insertions(+) create mode 100644 .github/workflows/issue-implement.yml create mode 100644 .github/workflows/issue-triage.yml create mode 100755 scripts/agent-bot/auto_merge.sh create mode 100755 scripts/agent-bot/run_triage.sh create mode 100755 scripts/agent-bot/run_wiwi.sh diff --git a/.github/workflows/issue-implement.yml b/.github/workflows/issue-implement.yml new file mode 100644 index 0000000..dbd611b --- /dev/null +++ b/.github/workflows/issue-implement.yml @@ -0,0 +1,62 @@ +name: issue-implement + +# Dev agent **wiwi**. Fires only when label `agent:try` lands on an +# issue — typically added by the triage agent when verdict=do, or by +# a human bypassing triage. wiwi branches off main, implements the +# change, runs the build, and opens a DRAFT PR labelled `auto-agent`. +# Downstream auto-merge gating happens in pr-review.yml. + +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: implement-${{ github.event.issue.number }} + cancel-in-progress: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + +jobs: + implement: + if: ${{ github.event.label.name == 'agent:try' }} + runs-on: [self-hosted, tokenscope] + timeout-minutes: 45 + env: + ANTHROPIC_BASE_URL: ${{ secrets.LITELLM_BASE_URL }} + ANTHROPIC_API_KEY: ${{ secrets.LITELLM_API_KEY }} + ANTHROPIC_MODEL: claude-3-5-sonnet-20241022 + steps: + - name: Configure no_proxy for LiteLLM + run: | + { + echo "no_proxy=${{ secrets.LITELLM_NO_PROXY }}" + echo "NO_PROXY=${{ secrets.LITELLM_NO_PROXY }}" + } >> "$GITHUB_ENV" + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + # AGENT_GH_TOKEN is a PAT (not GITHUB_TOKEN) so wiwi's + # `gh pr create` actually triggers the downstream `ci` and + # `pr-review` workflows. With the default GITHUB_TOKEN the + # spawned PR's check runs would be skipped. + token: ${{ secrets.AGENT_GH_TOKEN }} + + - name: Implement issue + env: + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_AUTHOR: ${{ github.event.issue.user.login }} + GH_TOKEN: ${{ secrets.AGENT_GH_TOKEN }} + run: bash scripts/agent-bot/run_wiwi.sh + + - name: Cleanup transient files + if: always() + run: rm -f /tmp/wiwi-*.md /tmp/wiwi-*.log /tmp/wiwi-*.txt || true diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 0000000..5164ad3 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,55 @@ +name: issue-triage + +# Triage agent. Fires only when a human (or repo automation) adds the +# `agent:assess` label to an issue. The triage agent decides whether +# the issue is small/safe enough for the dev agent **wiwi** to attempt +# unattended. Strict 5-gate verdict — see scripts/agent-bot/run_triage.sh. + +on: + issues: + types: [labeled] + +permissions: + contents: read + issues: write + +concurrency: + group: triage-${{ github.event.issue.number }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + +jobs: + triage: + if: ${{ github.event.label.name == 'agent:assess' }} + runs-on: [self-hosted, tokenscope] + timeout-minutes: 10 + env: + ANTHROPIC_BASE_URL: ${{ secrets.LITELLM_BASE_URL }} + ANTHROPIC_API_KEY: ${{ secrets.LITELLM_API_KEY }} + ANTHROPIC_MODEL: claude-3-5-sonnet-20241022 + steps: + - name: Configure no_proxy for LiteLLM + run: | + { + echo "no_proxy=${{ secrets.LITELLM_NO_PROXY }}" + echo "NO_PROXY=${{ secrets.LITELLM_NO_PROXY }}" + } >> "$GITHUB_ENV" + + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Triage issue + env: + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_AUTHOR: ${{ github.event.issue.user.login }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash scripts/agent-bot/run_triage.sh + + - name: Cleanup transient files + if: always() + run: rm -f /tmp/triage-*.md /tmp/triage-*.log /tmp/triage-*.json || true diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 87a5cc7..7483df2 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -132,6 +132,17 @@ jobs: AGENT_EXIT: ${{ steps.review.outcome }} run: python3 scripts/pr-review/post_review.py "$PR_NUMBER" + - name: Auto-merge if eligible + # Only effective for wiwi-spawned PRs (label `auto-agent`) whose + # linked issue author is on the team. Idempotent + safe to call + # for every PR — bails out for human PRs. Needs AGENT_GH_TOKEN + # so the admin-merge call has admin bypass on branch protection. + if: ${{ steps.review.outcome == 'success' }} + env: + GH_TOKEN: ${{ secrets.AGENT_GH_TOKEN }} + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + run: bash scripts/agent-bot/auto_merge.sh + - name: Cleanup transient files if: always() run: rm -f /tmp/pr-review-*.md /tmp/pr-review-*.log /tmp/pr-review-*.json || true diff --git a/scripts/agent-bot/auto_merge.sh b/scripts/agent-bot/auto_merge.sh new file mode 100755 index 0000000..060a5e7 --- /dev/null +++ b/scripts/agent-bot/auto_merge.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Called from the tail of pr-review.yml AFTER vivi posts her review. +# Auto-merges iff: +# - PR has label `auto-agent` +# - PR is not draft (wiwi may have flipped it; or the linked issue +# author was a team member and we promoted earlier — see below) +# - vivi's latest review state == APPROVED +# - the linked issue's author is in TEAM +set -euo pipefail + +TEAM='vaderyang william timmy' + +PR="${PR_NUMBER:?PR_NUMBER required}" + +meta=$(gh pr view "$PR" --json isDraft,labels,body) +labels=$(echo "$meta" | jq -r '.labels[].name') +echo "$labels" | grep -qx auto-agent || { echo "not auto-agent PR; skip"; exit 0; } + +# Latest review state. +state=$(gh pr view "$PR" --json reviews --jq '[.reviews[] | select(.author.login=="vivi" or (.body | contains("vivi")))] | last | .state // empty') +[ "$state" = "APPROVED" ] || { echo "vivi verdict=$state; skip"; exit 0; } + +# Extract issue number from PR body `Closes #N`. +issue=$(echo "$meta" | jq -r '.body' | grep -oE 'Closes #[0-9]+' | head -1 | tr -dc 0-9) +[ -n "$issue" ] || { echo "no linked issue; skip"; exit 0; } + +author=$(gh issue view "$issue" --json author --jq '.author.login') +for m in $TEAM; do + if [ "$m" = "$author" ]; then + echo "vivi APPROVED + author=$author ∈ TEAM → admin-merge" + # Lift draft (if still draft) and merge. + gh pr ready "$PR" >/dev/null 2>&1 || true + gh pr merge "$PR" --admin --squash --delete-branch + exit 0 + fi +done +echo "author=$author not in TEAM; leaving PR for human review" diff --git a/scripts/agent-bot/run_triage.sh b/scripts/agent-bot/run_triage.sh new file mode 100755 index 0000000..3252c7a --- /dev/null +++ b/scripts/agent-bot/run_triage.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Triage agent: read issue, decide do/skip/needs_info under STRICT gates. +# verdict=do → add `agent:try` label (kicks off wiwi). +# else → post a comment explaining gate failure; human can manually +# add `agent:try` to force, or `agent:skip` to mute. +set -euo pipefail + +PROMPT=$(mktemp) +OUT=$(mktemp) + +cat > "$PROMPT" <","reason":"<≤200 chars>","files":["..."],"gates":{"1":true,"2":true,"3":true,"4":true,"5":true}} + +Issue title: ${ISSUE_TITLE} +Author: ${ISSUE_AUTHOR} +EOF + +# Run claude in print mode against our LiteLLM-style endpoint. +claude --print \ + --allowed-tools Bash Read Grep Glob WebFetch \ + --model "${ANTHROPIC_MODEL:-claude-3-5-sonnet-20241022}" \ + < "$PROMPT" > "$OUT" 2> /tmp/triage-stderr.log || { + echo "triage agent failed (see workflow log)" >&2 + cat /tmp/triage-stderr.log >&2 + exit 1 +} + +# Strict JSON parse: take last non-empty line, must parse, must contain verdict. +LAST=$(grep -E '^\{.*"verdict"' "$OUT" | tail -1 || true) +if [ -z "$LAST" ]; then + echo "triage agent produced no JSON verdict; aborting" >&2 + cat "$OUT" >&2 + exit 1 +fi + +VERDICT=$(echo "$LAST" | jq -r '.verdict') +REASON=$(echo "$LAST" | jq -r '.reason') +SCOPE=$(echo "$LAST" | jq -r '.scope') + +# Defense-in-depth: require all 5 gates true for verdict=do. +if [ "$VERDICT" = "do" ]; then + ALLPASS=$(echo "$LAST" | jq -r '[.gates."1",.gates."2",.gates."3",.gates."4",.gates."5"] | all') + if [ "$ALLPASS" != "true" ]; then + echo "verdict=do but not all gates true; downgrading to needs_info" >&2 + VERDICT=needs_info + REASON="triage gates incomplete: $REASON" + fi +fi + +case "$VERDICT" in + do) + gh issue edit "$ISSUE_NUMBER" --add-label "agent:try" + gh issue comment "$ISSUE_NUMBER" --body "🤖 Triage: **${VERDICT}** — scope: ${SCOPE} + +${REASON} + +Auto-labeled \`agent:try\`. **wiwi** will pick this up shortly." + ;; + needs_info|skip) + gh issue comment "$ISSUE_NUMBER" --body "🤖 Triage: **${VERDICT}** + +${REASON} + +Manually add the \`agent:try\` label to override this verdict and run **wiwi** anyway, or \`agent:skip\` to mute future re-triage." + ;; + *) + echo "unknown verdict: $VERDICT" >&2; exit 1 ;; +esac diff --git a/scripts/agent-bot/run_wiwi.sh b/scripts/agent-bot/run_wiwi.sh new file mode 100755 index 0000000..5b74718 --- /dev/null +++ b/scripts/agent-bot/run_wiwi.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# wiwi: dev agent. Branch off main, implement, ensure cargo build + tests +# green, open a DRAFT PR labelled `auto-agent`. Auto-merge gating happens +# downstream in pr-review.yml. +set -euo pipefail + +TEAM='vaderyang william timmy' +is_team_member() { + local who="$1" + for m in $TEAM; do [ "$m" = "$who" ] && return 0; done + return 1 +} + +BRANCH="agent/wiwi/issue-${ISSUE_NUMBER}" +git config user.email "wiwi-agent@noreply.local" +git config user.name "wiwi" +git fetch origin main +git checkout -B "$BRANCH" origin/main + +PROMPT=$(mktemp) +cat > "$PROMPT" <300 LOC or cross-cutting), STOP, leave + a note in /tmp/wiwi-abort.txt explaining why, and exit non-zero. +- Add a deterministic test for the change (unit / integration / a tiny + fixture). Don't claim done without one. +- After edits, run \`just build\` (or \`cargo check\` + \`bun run build\` + in console/) — must be green before you stop. +- Do NOT add new dependencies, new secrets, new network calls. +- Do NOT modify CI workflows, branch protection, or this script. +- Commit in logical chunks; sign-off line not required. + +When done, write a brief summary to /tmp/wiwi-summary.md (Markdown) for +the PR body. End it with the literal line: + + Closes #${ISSUE_NUMBER} + +Issue title: ${ISSUE_TITLE} +EOF + +claude --print \ + --allowed-tools Bash Read Write Edit Grep Glob \ + --model "${ANTHROPIC_MODEL:-claude-3-5-sonnet-20241022}" \ + < "$PROMPT" > /tmp/wiwi-run.log 2>&1 || { + echo "wiwi run failed (see /tmp/wiwi-run.log)" >&2 + gh issue comment "$ISSUE_NUMBER" --body "🤖 wiwi could not complete this task. See workflow log." + exit 1 +} + +if [ -f /tmp/wiwi-abort.txt ]; then + gh issue comment "$ISSUE_NUMBER" --body "🤖 wiwi aborted: $(cat /tmp/wiwi-abort.txt)" + exit 0 +fi + +# Sanity: must have produced commits. +if [ "$(git rev-list --count origin/main..HEAD)" = "0" ]; then + gh issue comment "$ISSUE_NUMBER" --body "🤖 wiwi finished without any commit; nothing to PR." + exit 0 +fi + +git push -u origin "$BRANCH" + +BODY_FILE=$(mktemp) +{ + cat /tmp/wiwi-summary.md 2>/dev/null || echo "(wiwi did not write a summary)" + echo + echo "---" + echo "🤖 Implemented by **wiwi** • issue author: @${ISSUE_AUTHOR}" + if is_team_member "$ISSUE_AUTHOR"; then + echo "Eligible for auto-merge on vivi APPROVE." + fi +} > "$BODY_FILE" + +gh pr create \ + --draft \ + --base main \ + --head "$BRANCH" \ + --title "${ISSUE_TITLE}" \ + --body-file "$BODY_FILE" \ + --label auto-agent From bee64efb8919e9b0de4c9fa52ca362e537c7f447 Mon Sep 17 00:00:00 2001 From: Vader Yang Date: Mon, 25 May 2026 10:22:32 +0800 Subject: [PATCH 2/2] Address vivi review: jq null-safety + race + dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auto_merge.sh: coerce review .body to "" before contains() so the jq pipeline doesn't abort on bodyless APPROVE reviews. - post_review.py: skip its own auto-merge path when the PR carries the `auto-agent` label — that PR's merge decision is owned by scripts/agent-bot/auto_merge.sh (different gates: linked-issue author, not PR author). Stops the two paths racing on wiwi PRs. - New scripts/agent-bot/TEAM file as the single source of truth for team logins; auto_merge.sh and run_wiwi.sh both source it (was duplicated inline). - run_triage.sh: replace fragile grep-based JSON detection with `jq fromjson? | select(.verdict)` so prose lines that merely mention "verdict" or partial JSON fragments in code fences can't be picked up. - run_wiwi.sh: append a UTC timestamp to the branch name (agent/wiwi/issue-N-YYYYMMDD-HHMMSS) so re-runs against the same issue don't collide with leftover branches from prior attempts. - issue-implement.yml: flip concurrency cancel-in-progress to true so re-labelling `agent:try` preempts a stuck wiwi run instead of queueing behind it. --- .github/workflows/issue-implement.yml | 5 ++++- scripts/agent-bot/TEAM | 5 +++++ scripts/agent-bot/auto_merge.sh | 14 ++++++++---- scripts/agent-bot/run_triage.sh | 9 +++++--- scripts/agent-bot/run_wiwi.sh | 8 +++++-- scripts/pr-review/post_review.py | 31 ++++++++++++++++++++++----- 6 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 scripts/agent-bot/TEAM diff --git a/.github/workflows/issue-implement.yml b/.github/workflows/issue-implement.yml index dbd611b..60f9fad 100644 --- a/.github/workflows/issue-implement.yml +++ b/.github/workflows/issue-implement.yml @@ -16,8 +16,11 @@ permissions: pull-requests: write concurrency: + # Re-labelling `agent:try` on the same issue cancels any in-flight + # wiwi run and starts a fresh attempt. Useful when an operator + # spots a stuck run and wants to retry without manual cleanup. group: implement-${{ github.event.issue.number }} - cancel-in-progress: false + cancel-in-progress: true env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' diff --git a/scripts/agent-bot/TEAM b/scripts/agent-bot/TEAM new file mode 100644 index 0000000..bcec67a --- /dev/null +++ b/scripts/agent-bot/TEAM @@ -0,0 +1,5 @@ +# Team logins eligible for auto-merge on wiwi-spawned PRs. +# One GitHub login per line. Lines starting with `#` are ignored. +vaderyang +william +timmy diff --git a/scripts/agent-bot/auto_merge.sh b/scripts/agent-bot/auto_merge.sh index 060a5e7..34c4ad3 100755 --- a/scripts/agent-bot/auto_merge.sh +++ b/scripts/agent-bot/auto_merge.sh @@ -5,10 +5,12 @@ # - PR is not draft (wiwi may have flipped it; or the linked issue # author was a team member and we promoted earlier — see below) # - vivi's latest review state == APPROVED -# - the linked issue's author is in TEAM +# - the linked issue's author is in the TEAM file set -euo pipefail -TEAM='vaderyang william timmy' +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# TEAM file is the single source of truth, shared with run_wiwi.sh. +TEAM=$(grep -vE '^\s*(#|$)' "$HERE/TEAM" | tr '\n' ' ') PR="${PR_NUMBER:?PR_NUMBER required}" @@ -16,8 +18,12 @@ meta=$(gh pr view "$PR" --json isDraft,labels,body) labels=$(echo "$meta" | jq -r '.labels[].name') echo "$labels" | grep -qx auto-agent || { echo "not auto-agent PR; skip"; exit 0; } -# Latest review state. -state=$(gh pr view "$PR" --json reviews --jq '[.reviews[] | select(.author.login=="vivi" or (.body | contains("vivi")))] | last | .state // empty') +# Latest review state. Some reviews have a null .body (e.g. quick +# APPROVE clicks without a comment); coerce to "" before contains() +# or jq aborts the whole pipeline. +state=$(gh pr view "$PR" --json reviews --jq ' + [.reviews[] | select(.author.login=="vivi" or ((.body // "") | contains("vivi")))] + | last | .state // empty') [ "$state" = "APPROVED" ] || { echo "vivi verdict=$state; skip"; exit 0; } # Extract issue number from PR body `Closes #N`. diff --git a/scripts/agent-bot/run_triage.sh b/scripts/agent-bot/run_triage.sh index 3252c7a..ea1f523 100755 --- a/scripts/agent-bot/run_triage.sh +++ b/scripts/agent-bot/run_triage.sh @@ -48,10 +48,13 @@ claude --print \ exit 1 } -# Strict JSON parse: take last non-empty line, must parse, must contain verdict. -LAST=$(grep -E '^\{.*"verdict"' "$OUT" | tail -1 || true) +# Strict JSON parse: scan every line, keep ones that are valid JSON AND +# carry a `verdict` field, take the last. This rejects lines inside +# code fences, partial fragments, and lines that merely *mention* +# "verdict" in prose — only a parsable JSON object survives. +LAST=$(jq -Rrc 'fromjson? | select(.verdict)' "$OUT" 2>/dev/null | tail -1) if [ -z "$LAST" ]; then - echo "triage agent produced no JSON verdict; aborting" >&2 + echo "triage agent produced no parsable JSON verdict; aborting" >&2 cat "$OUT" >&2 exit 1 fi diff --git a/scripts/agent-bot/run_wiwi.sh b/scripts/agent-bot/run_wiwi.sh index 5b74718..7b0b5a1 100755 --- a/scripts/agent-bot/run_wiwi.sh +++ b/scripts/agent-bot/run_wiwi.sh @@ -4,14 +4,18 @@ # downstream in pr-review.yml. set -euo pipefail -TEAM='vaderyang william timmy' +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TEAM=$(grep -vE '^\s*(#|$)' "$HERE/TEAM" | tr '\n' ' ') is_team_member() { local who="$1" for m in $TEAM; do [ "$m" = "$who" ] && return 0; done return 1 } -BRANCH="agent/wiwi/issue-${ISSUE_NUMBER}" +# Branch name includes a short UTC timestamp so re-runs against the +# same issue don't collide with leftover branches from prior attempts. +STAMP=$(date -u +%Y%m%d-%H%M%S) +BRANCH="agent/wiwi/issue-${ISSUE_NUMBER}-${STAMP}" git config user.email "wiwi-agent@noreply.local" git config user.name "wiwi" git fetch origin main diff --git a/scripts/pr-review/post_review.py b/scripts/pr-review/post_review.py index 359ec45..d4af8bb 100755 --- a/scripts/pr-review/post_review.py +++ b/scripts/pr-review/post_review.py @@ -140,6 +140,19 @@ def pr_author(number: str) -> str | None: return proc.stdout.strip() or None +def pr_has_label(number: str, name: str) -> bool: + proc = subprocess.run( + ["gh", "pr", "view", number, "--json", "labels", "--jq", + f'any(.labels[]; .name == "{name}")'], + capture_output=True, + text=True, + ) + if proc.returncode != 0: + sys.stderr.write(f"gh pr view (labels) failed: {proc.stderr}\n") + return False + return proc.stdout.strip() == "true" + + def auto_merge(number: str) -> None: """Squash-merge with admin bypass. Repo doesn't have native `--auto` enabled, so we squash inline. Branch is deleted on @@ -200,13 +213,21 @@ def main() -> int: # that an APPROVE from the AI is enough signal for low-stakes # changes by the project maintainer, but anyone else's PR # still gets human review. + # + # PRs labelled `auto-agent` are wiwi-spawned; their auto-merge + # decision is owned by scripts/agent-bot/auto_merge.sh (different + # gates: linked-issue author, not PR author). Skip here so the + # two paths never race on the same PR. if event == "APPROVE": - author = pr_author(PR_NUMBER) - if author and author in AUTO_MERGE_AUTHORS: - print(f"author={author} in AUTO_MERGE_AUTHORS — squash-merging") - auto_merge(PR_NUMBER) + if pr_has_label(PR_NUMBER, "auto-agent"): + print("PR has `auto-agent` label — leaving auto-merge to agent-bot/auto_merge.sh") else: - print(f"author={author} not in AUTO_MERGE_AUTHORS={AUTO_MERGE_AUTHORS} — left for human") + author = pr_author(PR_NUMBER) + if author and author in AUTO_MERGE_AUTHORS: + print(f"author={author} in AUTO_MERGE_AUTHORS — squash-merging") + auto_merge(PR_NUMBER) + else: + print(f"author={author} not in AUTO_MERGE_AUTHORS={AUTO_MERGE_AUTHORS} — left for human") return 0