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
17 changes: 16 additions & 1 deletion .claude/commands/triage-dedupe.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ run locally), and `AUTOCLOSE` (`active` or `off`; assume `off` locally). If

Summarize the issue first: symptoms, component/file names, API/prop names,
exact error strings, and the general area. Then fan out FIVE Task agents in
a single message, each pursuing ONE distinct strategy:
a single message, each pursuing ONE distinct strategy.

Sub-agents run SYNCHRONOUSLY: every Task call MUST pass
`run_in_background: false`. If the runtime backgrounds them anyway, NEVER
end your turn while any sub-agent is still pending — in the headless CI
session an ended turn terminates the session immediately, orphaning the
agents before any comment is posted (#338). Collect every agent's result,
then continue with the filter pass.

The five strategies:

1. **Error strings** — exact quoted messages, stack-trace lines, warning text.
2. **Component + file names** — `Button`, `Modal`, `useConfig`, source paths.
Expand All @@ -46,6 +55,12 @@ Rules for every agent:
the `--repo REPO` flag). Never search outside it.
- Use `gh search issues`, `gh issue list --search`; include closed issues
(a duplicate of a closed issue is still a duplicate).
- ONE `gh` command per Bash call, starting with the `gh` binary — no shell
loops, `echo` prefixes, `;`/`&&` chains, pipes (`| head`, `| jq`), or
command substitution (`$(...)`) (the permission allowlist matches command
prefixes; anything else is denied and wastes turns).
- At most 6 searches per agent — the GitHub API limit is shared across all
agents, and a burst of searches 403s the whole session.
- Return candidate numbers with title and a one-line justification each.

## Filter pass (you, not the agents)
Expand Down
20 changes: 17 additions & 3 deletions .claude/commands/triage-find-duplicate-prs.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,30 @@ assume `labeled` locally). If `NUMBER` is missing, ask.
## Search — 3 parallel agents

Read the PR title, body, and changed file list, then fan out THREE Task
agents in a single message, one strategy each:
agents in a single message, one strategy each.

Sub-agents run SYNCHRONOUSLY: every Task call MUST pass
`run_in_background: false`. If the runtime backgrounds them anyway, NEVER
end your turn while any sub-agent is still pending — in the headless CI
session an ended turn terminates the session immediately, orphaning the
agents before any comment is posted (#338). Collect every agent's result,
then continue with the filter pass.

The three strategies:

1. **Changed files** — other PRs touching the same components or paths.
2. **Title + body keywords** — the strongest words plus synonyms.
3. **Feature/fix intent** — what the PR accomplishes, phrased differently.

Rules for every agent: EVERY search scoped
`repo:REPO is:pr is:open is:unmerged` (via `gh search prs` or
`gh pr list --search`); exclude the target PR itself and draft PRs; return
numbers + titles + a one-line justification.
`gh pr list --search`); exclude the target PR itself and draft PRs; ONE
`gh` command per Bash call, starting with the `gh` binary — no shell
loops, `echo` prefixes, `;`/`&&` chains, pipes, or command substitution
(the permission allowlist matches command prefixes; anything else is
denied and wastes turns); at most 6 searches per agent (the shared API
limit 403s on bursts); return numbers + titles + a one-line
justification.

## Filter pass

Expand Down
18 changes: 16 additions & 2 deletions .claude/commands/triage-find-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ EXACTLY ONE comment (or nothing). Context from the caller: `REPO`, `NUMBER`

Read the PR title, body, and `gh pr diff NUMBER --repo REPO` (never check
out or execute its code). Extract signals, then fan out FIVE Task agents in
a single message, one strategy each:
a single message, one strategy each.

Sub-agents run SYNCHRONOUSLY: every Task call MUST pass
`run_in_background: false`. If the runtime backgrounds them anyway, NEVER
end your turn while any sub-agent is still pending — in the headless CI
session an ended turn terminates the session immediately, orphaning the
agents before any comment is posted (#338). Collect every agent's result,
then continue with the filter pass.

The five strategies:

1. **Error strings** — messages or test names the diff fixes or touches.
2. **Component + file names** — components and paths the diff changes.
Expand All @@ -38,7 +47,12 @@ a single message, one strategy each:

Rules for every agent: scope EVERY search to this repository
(`repo:REPO` or `--repo REPO`); OPEN issues only (`is:open` /
`--state open`); return numbers + titles + a one-line justification.
`--state open`); ONE `gh` command per Bash call, starting with the `gh`
binary — no shell loops, `echo` prefixes, `;`/`&&` chains, pipes, or
command substitution (the permission allowlist matches command prefixes;
anything else is denied and wastes turns); at most 6 searches per agent
(the shared API limit 403s on bursts); return numbers + titles + a
one-line justification.

## Filter pass

Expand Down
78 changes: 66 additions & 12 deletions .github/workflows/ai-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ name: AI Triage
# file-editing tools, because the session ingests untrusted issue/PR
# text.
#
# SILENT NO-OPS (#317, #338): the action reports step success even when the
# session did no useful work, so the "Fail on silent session error" step
# checks the execution file. Two known shapes: an is_error result record
# (#317 — API/auth failure on the first call), and a CLEAN result record
# from a session that ended its turn while background Task sub-agents were
# still running (#338 — a CLI update made Task background-by-default; an
# ended turn kills the headless session and orphans the agents). The prompt
# therefore forbids backgrounded sub-agents AND requires a final
# `TRIAGE-RESULT:` sentinel line per command; the watchdog fails the job
# unless the result text is nothing but such lines, exactly one per
# expected command (one for issues, two for PRs — so finishing the first
# PR command and bailing during the second still fails).
#
# KILL SWITCHES: AI_TRIAGE_MODE=off (this workflow only) or
# AI_LOOP_ENABLED=false (all Claude spend, repo-wide).
#
Expand Down Expand Up @@ -325,6 +338,27 @@ jobs:
commands and do NOT use the SlashCommand tool — it is not
permitted in this session and every attempt will be denied.

Sub-agents run SYNCHRONOUSLY (#338): every Task call MUST pass
run_in_background: false. If the runtime backgrounds them
anyway, you MUST NOT end your turn while any sub-agent is still
pending — this is a headless one-shot session, and an ended
turn terminates it immediately, orphaning the agents with no
comment posted. Keep collecting results until every sub-agent
has returned, then finish the command's filter/post steps.

FINAL MESSAGE (machine-checked; the job FAILS without it): your
last output message must consist of exactly one line per
command listed above — ONE line when IS_PR is false, TWO lines
when IS_PR is true (the job verifies the count, so a run that
finishes the first command but bails during the second cannot
pass) — each of the form
TRIAGE-RESULT: <command> commented | refreshed | skipped (<reason>)
e.g. `TRIAGE-RESULT: triage-dedupe commented` or
`TRIAGE-RESULT: triage-find-duplicate-prs skipped (no credible
duplicates)` — and nothing else. A command's silent exit is
still reported here as `skipped (<reason>)`; "silent" means no
comment on the item, not no sentinel.

Idempotency (the commands restate this; it is binding):
- TRIGGER is opened: if a command's marker comment already
exists on the item, that command exits silently — post
Expand All @@ -344,32 +378,52 @@ jobs:
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.
# did no useful work. Two known silent-no-op shapes (see header):
# #317 — the result record has is_error: true (e.g. an API auth or
# usage-limit failure on the very first call).
# #338 — the result record is a CLEAN success, but the session
# ended its turn while background Task sub-agents were still
# running ("I'll wait for them to finish…"), so nothing was
# ever posted. Session status alone cannot catch this, which
# is why the prompt demands the TRIAGE-RESULT sentinel: a
# session that actually finished its commands ends with only
# sentinel lines; any other final text fails here.
# Fail loudly on both. 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 }}
IS_PR: ${{ steps.gate.outputs.is_pr }}
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]
# result record whose is_error is exactly false AND whose result
# text is nothing but TRIAGE-RESULT sentinel lines — exactly one
# per expected command (issues run one command, PRs run two), so
# a session that finishes the first PR command but bails during
# the second cannot pass on the strength of one sentinel.
# Malformed JSON, a missing result record, a mid-task bailout
# message, or any other shape fails the job.
if [ "$IS_PR" = "true" ]; then EXPECTED=2; else EXPECTED=1; fi
if jq -es --argjson n "$EXPECTED" \
'[.[] | if type == "array" then .[] else . end]
| map(select(type == "object" and .type == "result"))
| last | .is_error == false' \
| last
| (.is_error == false)
and ((.result // "") | split("\n")
| map(select(length > 0))
| (length == $n)
and all(startswith("TRIAGE-RESULT:")))' \
"$EXEC_FILE" >/dev/null 2>&1; then
echo "session result is_error: false — triage session completed"
echo "session ended cleanly with $EXPECTED TRIAGE-RESULT sentinel(s) — triage 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."
echo "::error::Claude triage session did not complete its commands (is_error true, wrong sentinel count for the item type, non-sentinel final message, or unparsable execution file — see #317/#338). The item likely got no (or incomplete) triage. See the session output above."
exit 1
fi

Expand Down
Loading