From 105543690e5688fba9028c12128e88774194a067 Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Thu, 23 Jul 2026 15:31:20 +0200 Subject: [PATCH 1/5] fix: go live on tier 1, self-provision ai:blocked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - on_issues_ai_triage.yaml: remove DRY_RUN entirely. AI_PR_TOKEN is now configured, tier 1 has been watched in dry-run, and the toggle was meant to be temporary scaffolding, not a permanent code path — verdicts now apply labels/comments unconditionally. - daily_ai_fix.yaml: fold ai:blocked into the existing "ensure labels exist up front" step (renamed to reflect that). It was the one control label neither workflow ever created: the forbidden-paths guard applies it directly, and under set -euo pipefail a missing label there aborts that step's loop entirely, silently skipping every remaining PR behind the one that failed. No repo had hit this yet only because no label in the ai:*/ai-* namespace existed at all before now. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/daily_ai_fix.yaml | 10 +++++++--- .github/workflows/on_issues_ai_triage.yaml | 9 --------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.github/workflows/daily_ai_fix.yaml b/.github/workflows/daily_ai_fix.yaml index 8c807a4834..d16497c642 100644 --- a/.github/workflows/daily_ai_fix.yaml +++ b/.github/workflows/daily_ai_fix.yaml @@ -87,15 +87,19 @@ jobs: # Created up front so issue-fix.md's per-issue relabel never has to # improvise a color or retry a "label does not exist" error — that's a - # wasted turn multiplied by every issue in the batch. - - name: Ensure relabel targets exist + # wasted turn multiplied by every issue in the batch. Also covers + # ai:blocked, which the forbidden-paths guard applies later in this + # same job: with set -euo pipefail, `gh pr edit --add-label` on a + # label that doesn't exist yet fails and aborts that step's loop + # entirely, silently skipping every remaining PR behind it. + - name: Ensure control labels exist if: steps.batch.outputs.count != '0' env: GH_TOKEN: ${{ secrets.AI_PR_TOKEN }} REPO: ${{ github.repository }} run: | set -euo pipefail - for l in ai:fixed ai:upstream ai:needs-human; do + for l in ai:fixed ai:upstream ai:needs-human ai:blocked; do gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true done diff --git a/.github/workflows/on_issues_ai_triage.yaml b/.github/workflows/on_issues_ai_triage.yaml index e1875f0804..74bb18a5e3 100644 --- a/.github/workflows/on_issues_ai_triage.yaml +++ b/.github/workflows/on_issues_ai_triage.yaml @@ -24,9 +24,6 @@ concurrency: env: MAINTAINER: alexbelgium - # Leave "true" for the first couple of weeks. The verdict is printed in the - # job log and nothing is written to the issue. Flip when it looks right. - DRY_RUN: "true" jobs: classify: @@ -120,12 +117,6 @@ jobs: [ "$VERDICT" = "addon-bug" ] && LABELS+=("ai-triage") LABELS+=("ai:classified") - if [ "${DRY_RUN:-true}" = "true" ]; then - echo "DRY_RUN: would apply labels: ${LABELS[*]}" - echo "DRY_RUN: would post comment:"; printf '%s\n' "$COMMENT" - exit 0 - fi - for l in "${LABELS[@]}"; do gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true done From 3755fd97bccfb16c9fcec2cc3e780fe13e738c70 Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Thu, 23 Jul 2026 15:35:31 +0200 Subject: [PATCH 2/5] fix: grant id-token: write for claude-code-action OAuth flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A manual AI fix sweep failed with "Could not fetch an OIDC token. Did you remember to add id-token: write to your workflow permissions?". The action mints a GitHub OIDC token to authenticate the claude_code_oauth_token flow, which needs id-token: write — absent from both jobs' permissions. Tier 2 failed on it now; tier 1 would have failed identically the first time it ran live. Added to both. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/daily_ai_fix.yaml | 1 + .github/workflows/on_issues_ai_triage.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/daily_ai_fix.yaml b/.github/workflows/daily_ai_fix.yaml index d16497c642..057cfa06d3 100644 --- a/.github/workflows/daily_ai_fix.yaml +++ b/.github/workflows/daily_ai_fix.yaml @@ -33,6 +33,7 @@ permissions: contents: write issues: write pull-requests: write + id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow concurrency: group: ai-fix-sweep diff --git a/.github/workflows/on_issues_ai_triage.yaml b/.github/workflows/on_issues_ai_triage.yaml index 74bb18a5e3..0fe03bdf15 100644 --- a/.github/workflows/on_issues_ai_triage.yaml +++ b/.github/workflows/on_issues_ai_triage.yaml @@ -17,6 +17,7 @@ on: permissions: contents: read issues: write + id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow concurrency: group: ai-triage-${{ github.event.issue.number }} From 0fae882a687862d6bc1c717a17366eb5fbd46029 Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Thu, 23 Jul 2026 15:37:45 +0200 Subject: [PATCH 3/5] feat: allow manual tier-1 triage of a single issue number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a workflow_dispatch trigger with a required `issue` input to on_issues_ai_triage.yaml, so a specific (existing) issue can be triaged on demand instead of only on issues.opened. - Every issue-number reference now reads `github.event.issue.number || inputs.issue`, so it resolves from the event on the auto path and from the input on manual dispatch. - The job's auto-trigger guards (skip the maintainer's own issues, honour no-ai) are bypassed on workflow_dispatch — a manual run is a deliberate override. - The 60s ping_submitter wait is skipped on manual dispatch; there's no race to lose against an issue whose ping already landed. The input flows only through env vars and expression contexts, never inline into a run: block, so there's no shell-injection surface; a bad number just fails `gh issue view` cleanly. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/on_issues_ai_triage.yaml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/on_issues_ai_triage.yaml b/.github/workflows/on_issues_ai_triage.yaml index 0fe03bdf15..8977955a47 100644 --- a/.github/workflows/on_issues_ai_triage.yaml +++ b/.github/workflows/on_issues_ai_triage.yaml @@ -13,6 +13,11 @@ name: AI issue triage on: issues: types: [opened] + workflow_dispatch: + inputs: + issue: + description: "Issue number to (re-)triage manually" + required: true permissions: contents: read @@ -20,7 +25,7 @@ permissions: id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow concurrency: - group: ai-triage-${{ github.event.issue.number }} + group: ai-triage-${{ github.event.issue.number || inputs.issue }} cancel-in-progress: false env: @@ -28,9 +33,13 @@ env: jobs: classify: + # Manual dispatch is a deliberate override: skip the auto-trigger guards + # (don't self-triage the maintainer's own issues; honour the no-ai + # opt-out) that only make sense for the fire-on-every-open path. if: >- - github.event.issue.user.login != 'alexbelgium' && - !contains(github.event.issue.labels.*.name, 'no-ai') + github.event_name == 'workflow_dispatch' || + (github.event.issue.user.login != 'alexbelgium' && + !contains(github.event.issue.labels.*.name, 'no-ai')) runs-on: ubuntu-latest timeout-minutes: 15 environment: CR_PAT @@ -41,7 +50,10 @@ jobs: # Both workflows fire on the same issues.opened event and race. The # submitter ping completes in 6-11s of job time across recent runs; 60s # leaves a generous margin for runner-queue skew between the two jobs. + # A manual dispatch runs against an existing issue whose ping (if any) + # landed long ago, so there is nothing to wait for. - name: Wait for ping_submitter + if: github.event_name == 'issues' run: sleep 60 - name: Checkout tooling @@ -57,7 +69,7 @@ jobs: - name: Build context bundle env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue }} REPO: ${{ github.repository }} run: bash .github/scripts/ai_triage_context.sh @@ -83,7 +95,7 @@ jobs: - name: Apply verdict env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE: ${{ github.event.issue.number }} + ISSUE: ${{ github.event.issue.number || inputs.issue }} REPO: ${{ github.repository }} run: | set -euo pipefail From 82085bca71db6076dc62c0af444670cdcde77f8b Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Thu, 23 Jul 2026 15:41:16 +0200 Subject: [PATCH 4/5] fix: don't tier-2 low-confidence verdicts; stop recoloring labels Two findings from Codex review of #2899, both live now that DRY_RUN is gone: - A low-confidence `addon-bug` had ai:needs-human set by the low branch and then ai-triage appended right back unconditionally, so it would enter the unattended tier-2 fix pass despite Rule 2 saying an uncertain call should only flag a human. Guard the ai-triage add on CONF != low. - The label-create loop used `--force`, which updates existing labels; with a model-supplied cosmetic label like `bug` that already exists, triage recolored it to ededed as a side effect. Drop `--force` so existing labels are left untouched (create fails harmlessly via || true) while missing ones are still created. tier 2's own label step keeps --force intentionally: its list is a fixed set of workflow-owned ai:* labels meant to be gray, not model input. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/on_issues_ai_triage.yaml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/on_issues_ai_triage.yaml b/.github/workflows/on_issues_ai_triage.yaml index 8977955a47..f307161a5f 100644 --- a/.github/workflows/on_issues_ai_triage.yaml +++ b/.github/workflows/on_issues_ai_triage.yaml @@ -127,11 +127,24 @@ jobs: LABELS=("ai:needs-human"); COMMENT="" fi - [ "$VERDICT" = "addon-bug" ] && LABELS+=("ai-triage") + # ai-triage is the tier-2 trigger, so it must never be added to a + # low-confidence verdict — Rule 2 of issue-classify.md says an + # uncertain addon/upstream call should only flag a human, not enter + # the unattended fix pass. (Above, low confidence already reset + # LABELS to ai:needs-human; this guard keeps ai-triage from being + # appended right back.) + if [ "$VERDICT" = "addon-bug" ] && [ "$CONF" != "low" ]; then + LABELS+=("ai-triage") + fi LABELS+=("ai:classified") + # No --force: an existing label (e.g. a model-supplied cosmetic + # "bug") must be left as-is. --force would update it, recoloring + # every such label to ededed as a side effect of triage. Without it, + # create fails harmlessly on labels that already exist (|| true), + # and still creates the workflow-owned ones the first time. for l in "${LABELS[@]}"; do - gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true + gh label create "$l" --repo "$REPO" --color ededed >/dev/null 2>&1 || true done gh issue edit "$ISSUE" --repo "$REPO" \ "${LABELS[@]/#/--add-label=}" From c44206decf8ce91cc3b2aea46477559a3e3361d6 Mon Sep 17 00:00:00 2001 From: alexbelgium Date: Thu, 23 Jul 2026 15:44:33 +0200 Subject: [PATCH 5/5] fix: clear stale tier-1 control labels on manual re-triage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified: gh issue edit --add-label=... is purely additive, and the "owned" branch exited without touching labels at all — so a manual workflow_dispatch re-triage that changes the verdict (e.g. a prior addon-bug run now comes back needs-info, upstream-bug, or owned) left the old ai-triage label in place, and daily_ai_fix.yaml would still pick the issue up for the unattended fix pass despite the fresh verdict. Both label-applying paths now also remove whichever of ai-triage/ai:classified/ai:needs-human this run did NOT re-apply, as a separate best-effort call that can't block the add. Simulated every verdict/confidence transition, including the reported case (addon-bug -> needs-info): ai-triage is now correctly removed instead of left stale. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/on_issues_ai_triage.yaml | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/on_issues_ai_triage.yaml b/.github/workflows/on_issues_ai_triage.yaml index f307161a5f..597a8aaf40 100644 --- a/.github/workflows/on_issues_ai_triage.yaml +++ b/.github/workflows/on_issues_ai_triage.yaml @@ -117,9 +117,15 @@ jobs: # of ai-triage). mapfile -t LABELS < <(jq -r '.labels[]? // empty' "$F" | grep -vE '^ai[:-]' || true) - # Someone already owns this one: ping_submitter did its job. + # Someone already owns this one: ping_submitter did its job. Best- + # effort clear of a manual re-triage's stale control labels (e.g. a + # prior addon-bug run) — nothing to do if they were never set. if [ "$VERDICT" = "owned" ]; then - echo "issue already has an owner, nothing to do"; exit 0 + echo "issue already has an owner, nothing to do" + gh issue edit "$ISSUE" --repo "$REPO" \ + --remove-label=ai-triage --remove-label=ai:classified --remove-label=ai:needs-human \ + >/dev/null 2>&1 || true + exit 0 fi # Low confidence never speaks. It just flags for a human. @@ -149,6 +155,21 @@ jobs: gh issue edit "$ISSUE" --repo "$REPO" \ "${LABELS[@]/#/--add-label=}" + # Manual re-triage can flip the verdict (e.g. a prior addon-bug + # re-run now comes back needs-info/upstream-bug): clear whichever + # of tier 1's own control labels this run did NOT re-apply, so a + # stale ai-triage doesn't keep the issue in tomorrow's fix sweep. + # Separate, best-effort call — must not block the add above. + declare -A FRESH=() + for l in "${LABELS[@]}"; do FRESH["$l"]=1; done + STALE=() + for l in ai-triage ai:classified ai:needs-human; do + [ -z "${FRESH[$l]:-}" ] && STALE+=("$l") + done + if [ "${#STALE[@]}" -gt 0 ]; then + gh issue edit "$ISSUE" --repo "$REPO" "${STALE[@]/#/--remove-label=}" >/dev/null 2>&1 || true + fi + if [ -n "$COMMENT" ]; then { printf '%s\n\n' "$COMMENT"