Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions .github/workflows/ai-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: AI Triage
# AI triage v2 — fan-out duplicate/related search on issues and PRs. An
# adaptation of oven-sh/bun's claude-dedupe-issues.yml +
# claude-find-issues-for-pr.yml, budget-capped for a metered Claude plan.
# One sonnet session per run drives the repo's triage slash commands
# (.claude/commands/triage-*.md): issues get /triage-dedupe; PRs get
# /triage-find-issues + /triage-find-duplicate-prs. Each command fans out
# One sonnet session per run reads and follows the repo's triage command
# files (.claude/commands/triage-*.md): issues get triage-dedupe; PRs get
# triage-find-issues + triage-find-duplicate-prs. Each command fans out
# parallel read-only search sub-agents and posts at most ONE marker-tagged
# comment, so re-runs are idempotent.
#
Expand Down Expand Up @@ -272,6 +272,7 @@ jobs:
persist-credentials: false

- name: Run Claude (triage)
id: claude
if: steps.gate.outputs.run == 'true'
uses: anthropics/claude-code-action@51c47629174b5dbca955cf93690fd39c8a41dadf # v1
with:
Expand All @@ -283,13 +284,25 @@ jobs:
# GITHUB_TOKEN-authored comments can't re-trigger workflows.
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# Full session output in the public job log (#317): the session
# only ever sees public issue/PR text and GET-only gh output, so
# nothing sensitive can leak, and without it a session that ends
# early (e.g. on permission denials) is indistinguishable from a
# healthy one.
show_full_output: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Task is required for the commands' fan-out search sub-agents
# (they inherit this same allowlist). Still no `gh api` and no
# Edit/Write — the session ingests untrusted issue/PR text.
# Read/Glob/Grep are read-only and always available — the prompt
# relies on Read for the command files. --disallowedTools is
# defense-in-depth on top of the allowlist: it hard-blocks the
# mutating tools and SlashCommand (whose availability caused the
# #317 silent no-op) instead of leaving them to per-call denials.
claude_args: |
--max-turns 50
--model claude-sonnet-5
--allowedTools "Task,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh pr list:*),Bash(gh search:*),Bash(gh pr comment:*),Bash(gh issue comment:*)"
--disallowedTools "Edit,Write,MultiEdit,NotebookEdit,SlashCommand,WebFetch,WebSearch"
prompt: |
REPO: ${{ github.repository }}
NUMBER: ${{ steps.gate.outputs.number }}
Expand All @@ -298,17 +311,19 @@ jobs:
AUTOCLOSE: ${{ (vars.AI_TRIAGE_AUTOCLOSE == 'on' || vars.AI_TRIAGE_AUTOCLOSE == 'dry-run') && 'active' || 'off' }}

You are the TRIAGE agent for this repository. Your entire job is
to run this repo's triage slash commands against the item above:
to carry out this repo's triage procedures against the item
above. The procedures are markdown files in .claude/commands/ in
this checkout:

- If IS_PR is false: run the /triage-dedupe command.
- If IS_PR is true: run /triage-find-issues, then
/triage-find-duplicate-prs.
- If IS_PR is false: .claude/commands/triage-dedupe.md
- If IS_PR is true: .claude/commands/triage-find-issues.md,
then .claude/commands/triage-find-duplicate-prs.md

The command definitions live in .claude/commands/ in this
checkout (triage-dedupe.md, triage-find-issues.md,
triage-find-duplicate-prs.md). Read the matching file(s) and
follow them exactly — they consume the REPO / NUMBER / TRIGGER /
AUTOCLOSE context lines above.
Open the matching file(s) with the Read tool and follow their
instructions exactly — they consume the REPO / NUMBER / TRIGGER /
AUTOCLOSE context lines above. Do NOT invoke them as slash
commands and do NOT use the SlashCommand tool — it is not
permitted in this session and every attempt will be denied.

Idempotency (the commands restate this; it is binding):
- TRIGGER is opened: if a command's marker comment already
Expand All @@ -328,6 +343,36 @@ jobs:
"@coderabbitai" in any text; keep every search scoped to REPO;
post nothing beyond what the commands specify.

# The action reports step success even when the Claude session itself
# errors (result message has is_error: true — e.g. an API auth or
# usage-limit failure on the very first call). The item then silently
# gets no triage comment while the run shows green — the exact #317
# failure shape. Fail loudly instead. An empty/missing execution file
# (e.g. the action's workflow-validation skip) is tolerated: there was
# no session to judge.
- name: Fail on silent session error
if: steps.claude.outcome == 'success'
env:
EXEC_FILE: ${{ steps.claude.outputs.execution_file }}
run: |
set -euo pipefail
if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then
echo "no execution file — the action ran no session; skipping check"
exit 0
fi
# Fail closed: accept a JSON array or NDJSON, and require a final
# result record whose is_error is exactly false. Malformed JSON,
# a missing result record, or any other shape fails the job.
if jq -es '[.[] | if type == "array" then .[] else . end]
| map(select(type == "object" and .type == "result"))
| last | .is_error == false' \
"$EXEC_FILE" >/dev/null 2>&1; then
echo "session result is_error: false — triage session completed"
else
echo "::error::Claude triage session did not complete cleanly (is_error true, missing result record, or unparsable execution file) — no triage comment was posted. See the session output above."
exit 1
fi

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Always remove the trigger label on labeled runs — even when the
# Claude step failed or the gate said no — so the label stays a
# re-pressable button and never wedges "on". Removal uses only
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,23 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Run Claude (deep review)
id: claude
if: steps.perm.outputs.allowed == 'true' && steps.dedupe.outputs.skip == 'false'
uses: anthropics/claude-code-action@51c47629174b5dbca955cf93690fd39c8a41dadf # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# On loop PRs the opener/labeler is claude[bot] (the implementer)
# by design.
allowed_bots: 'claude'
# Full session output ONLY on debug re-runs (the "Enable debug
# logging" checkbox). Unlike ai-triage (GET-only public surface),
# this session runs pnpm against untrusted PR-head code, whose
# output enters the transcript — a malicious test printing
# transformed env values would evade GitHub's secret masking, so
# the transcript stays out of normal public logs. The is_error
# gate below still turns silent session failures red; a debug
# re-run then reveals the actual error.
show_full_output: ${{ runner.debug == '1' }}
claude_args: |
--max-turns 120
--model claude-opus-4-8
Expand Down Expand Up @@ -245,3 +255,34 @@ jobs:
still include the Overall line and the surfer blurb.
6. Never push, merge, close, or label anything, and never write
"@claude" or "@coderabbitai" in any text.

# The action reports step success even when the Claude session itself
# errors (result message has is_error: true — e.g. an API auth or
# usage-limit failure on the very first call). The PR then silently
# gets no review while the run shows green, and the AI loop's
# deep-review gate waits on a review that never comes. Fail loudly.
# An empty/missing execution file is tolerated: the action skips
# without a session when the PR modifies this workflow file (its
# OIDC exchange requires the file to match the default branch).
- name: Fail on silent session error
if: steps.claude.outcome == 'success'
env:
EXEC_FILE: ${{ steps.claude.outputs.execution_file }}
run: |
set -euo pipefail
if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then
echo "no execution file — the action ran no session; skipping check"
exit 0
fi
# Fail closed: accept a JSON array or NDJSON, and require a final
# result record whose is_error is exactly false. Malformed JSON,
# a missing result record, or any other shape fails the job.
if jq -es '[.[] | if type == "array" then .[] else . end]
| map(select(type == "object" and .type == "result"))
| last | .is_error == false' \
"$EXEC_FILE" >/dev/null 2>&1; then
echo "session result is_error: false — review session completed"
else
echo "::error::Claude deep-review session did not complete cleanly (is_error true, missing result record, or unparsable execution file) — no review was posted. Re-run with debug logging for the full session output."
exit 1
fi
Loading