Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
41d9839
ci: render triage comments from a structured payload, not model prose
allxsmith Aug 28, 2026
333c45c
ci: split ai-triage so the session posts nothing and holds no PAT
allxsmith Aug 28, 2026
17b3606
docs: retire the triage commands' posting instructions and the claims…
allxsmith Aug 28, 2026
93bffda
ci: restart the objection window on a first verdict, not just a chang…
allxsmith Aug 28, 2026
97a125a
Merge branch 'main' into ci/457-structured-triage-payload
allxsmith Aug 28, 2026
6e0ae6a
docs: rewrap the triage paragraph the merge left ragged
allxsmith Aug 28, 2026
09d96de
ci: stop sanitizing credentials, and escape Markdown structure too
allxsmith Aug 28, 2026
ed55c42
ci: close the sanitizer and publish-path gaps the max review found
allxsmith Aug 28, 2026
63764c0
ci: re-check the kill switches in the job that spends model usage
allxsmith Aug 28, 2026
58d2c9e
ci: stop publishing the session transcript, and fix two legacy-commen…
allxsmith Aug 28, 2026
b7a42b3
ci: retry only rate-limited 403s, and stop documenting a payload cap …
allxsmith Aug 28, 2026
b258bc1
ci: record that ai-scan is no longer the show_full_output outlier
allxsmith Aug 28, 2026
103f73a
ci: size the payload cap for escaped JSON, and only close a verdict t…
allxsmith Aug 28, 2026
f17b6e9
ci: require the notice as its own line, and stop cleanup blocking itself
allxsmith Aug 28, 2026
56798aa
ci: restart the objection clock when the notice first appears
allxsmith Aug 28, 2026
f84c80e
ci: stop a Unicode line separator in a title from killing the run
allxsmith Aug 28, 2026
ab8460c
docs: say what the triage path actually does to I2, and what it does not
allxsmith Aug 28, 2026
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
121 changes: 72 additions & 49 deletions .claude/commands/triage-dedupe.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
---
description: Find likely duplicates of an issue with 5 parallel search agents and post one triage comment
allowed-tools: Task, Bash(gh issue view:*), Bash(gh issue list:*), Bash(gh pr list:*), Bash(gh search:*), Bash(gh issue comment:*)
description: Find likely duplicates of an issue with 5 parallel search agents and report them for publication
allowed-tools: Task, Bash(gh issue view:*), Bash(gh issue list:*), Bash(gh pr list:*), Bash(gh search:*)
---

# /triage-dedupe — duplicate search for an issue

Find likely duplicates of the target issue and post EXACTLY ONE comment (or
nothing — see pre-checks). Context comes from the caller (the ai-triage
workflow prompt, or the user when run locally): `REPO` (owner/repo), `NUMBER`
(the target issue), `TRIGGER` (`opened` or `labeled`; assume `labeled` when
run locally), and `AUTOCLOSE` (`active` or `off`; assume `off` locally). If
`NUMBER` is missing, ask — never guess.
Find likely duplicates of the target issue and REPORT them. Context comes from
the caller (the ai-triage workflow prompt, or you when run locally — see "Running
this locally" below):
`REPO` (owner/repo), `NUMBER` (the target issue), and `TRIGGER` (`opened` or `labeled`). If `NUMBER` is missing, ask — never guess.

## Pre-checks (in order; each exit is SILENT — post nothing)
**You do not post anything.** You have no comment tool. Your findings go out as
a structured payload on this command's `TRIAGE-RESULT:` line, and
`scripts/render-triage-comment.mjs` renders and publishes the comment. That
script — not you — owns the marker, the `Duplicate of #N` line, the auto-close
notice, and the decision to comment at all.

## Pre-checks (in order; each exit is a `skip`, never a comment)

1. `gh issue view NUMBER --repo REPO --json state,title,body,comments` — if
the issue is not open, stop.
the issue is not open, stop and report `skip (not open)`.
2. Marker check — does any existing comment authored by bestaxbot or a
bot account contain `<!-- ai-triage:dedupe -->`? (Match the marker +
Comment thread
allxsmith marked this conversation as resolved.
Resolved
that author class, never one specific login — the workflow posts as
bestaxbot today; older comments are from github-actions[bot] or
claude[bot].)
- `TRIGGER=opened` and marker present → stop (already triaged).
- `TRIGGER=labeled` and marker present → continue; at the end REFRESH
that comment instead of posting a new one. Use
`gh issue comment NUMBER --repo REPO --edit-last --body ...` ONLY when
your most recent comment on the issue is itself the
`<!-- ai-triage:dedupe -->` comment: `--edit-last` selects by author,
not by marker, and every automation here posts as bestaxbot now, so a
newer repro draft or other machine comment would be the one it
overwrites. If anything else is newer, post a fresh comment (the old
one stays as history). See `.github/CLAUDE.md` rule 6.
- `TRIGGER=opened` and marker present → stop, report
`skip (already triaged)`. This is a cost gate: it saves the search
fan-out below. The publisher also refuses to overwrite an existing
triage comment on an `opened` run, so the guarantee does not depend on
you getting this right.
- `TRIGGER=labeled` → continue regardless. A maintainer re-ran triage
deliberately; the publisher refreshes its own marker comment in place.
3. If the issue is too vague to search meaningfully (no error text, no
component or file name, no concrete behavior), stop.
component or file name, no concrete behavior), stop and report
`skip (too vague)`.

## Search — 5 parallel agents

Expand All @@ -44,7 +46,7 @@ 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,
agents before anything is reported (#338). Collect every agent's result,
then continue with the filter pass.

The five strategies:
Expand Down Expand Up @@ -76,40 +78,61 @@ same feature request = duplicate; same area or a blocking relationship =
related. DROP weak keyword-only matches — an empty result beats a wrong one.
Read the top candidates with `gh issue view` when unsure.

## Post ONE comment

Via `gh issue comment NUMBER --repo REPO --body ...` (or the refresh path
from pre-check 2), formatted exactly like this:

```markdown
### AI triage

**Likely duplicates**
## Report

- #N — <title>: <one-line reason>
- (at most 3 entries, best match first)
Emit this command's sentinel line, best match first — the first `items` entry
becomes the published `Duplicate of #N`:

Duplicate of #N
```
TRIAGE-RESULT: triage-dedupe publish {"items":[{"number":123,"title":"…","reason":"…"}],"related":[{"number":456,"title":"…","reason":"…"}]}
```

**Related** (optional, at most 3)
- `items` — the duplicates. REQUIRED, and may be `[]`: an empty list is a
real finding ("I searched and found nothing credible"), and the renderer
publishes "No duplicates found." for it. The renderer keeps at most 3.
- `related` — optional, same shape, at most 3 kept.
- `number` must be a JSON integer, never a string. `title` is required;
`reason` is a short one-liner and is optional.
- Compact JSON on ONE line. A pretty-printed payload fails the job.
- If a search tool errors, or you are rate-limited and cannot complete the
fan-out, report `skip (search failed)`. Do NOT report an empty list in that
case — empty means "I searched and found nothing", which is a claim a failed
search has not earned.
- Do NOT write markers, `Duplicate of #N`, the auto-close notice, or any
heading into a field. The renderer emits all of them from its own
constants; structural text inside a field is defanged, not honored.

## Running this locally

The workflow supplies `REPO`, `NUMBER` and `TRIGGER`; run by hand, assume
`TRIGGER=labeled` and ask for `NUMBER` rather than guessing. Locally the
sentinel line is the whole output — nothing publishes it, and that is the
point: you get to read the payload before it ever becomes a comment. To see
the comment it would produce, pipe the line into the renderer's dry run:

```bash
# with the TRIAGE-RESULT line saved to /tmp/result.txt
printf '[{"type":"result","is_error":false,"result":%s}]' \
"$(jq -Rs . </tmp/result.txt)" > /tmp/exec.json
node scripts/render-triage-comment.mjs --mode=render \
--exec-file=/tmp/exec.json --expect=triage-dedupe --number=<n> \
--is-pr=false \
--trigger=labeled --autoclose=off --out-dir=/tmp/out --dry-run
```

- #M — <title>: <one-line reason>
Nothing in that path can post: `--mode=render` never opens a network
connection, and `--mode=publish --dry-run` stops before its first request.

<!-- ai-triage:dedupe -->
```
## Size limits

- The line `Duplicate of #N` — with N the single best duplicate — must use
THIS EXACT FORMAT on its own line: the auto-close cron machine-parses it.
Include it only when you found at least one credible duplicate.
- If `AUTOCLOSE=active`, add this sentence directly after the
`Duplicate of #N` line: "This issue may be auto-closed in 14 days unless
someone objects (comment or 👎)." If `AUTOCLOSE=off`, omit it.
- The marker `<!-- ai-triage:dedupe -->` goes on its own line at the end.
- No credible duplicates → post "No duplicates found." under the heading,
plus the marker, WITHOUT any `Duplicate of #N` line or auto-close notice.
The whole payload must stay under **8000 bytes** or the run fails, so report
your best candidates rather than everything you saw: at most **3** items and **3** related. Keep
`title` under **256** characters and `reason` under **400** — one line each,
no markdown, no HTML. The renderer escapes both, so formatting is wasted
effort.

## Hard rules

Never apply or remove labels; never close, merge, push, or resolve
anything; never write "@claude" or "@coderabbitai" in any text; post at
most one comment and nothing else.
anything; never write "@claude" or "@coderabbitai" in any text. You have no
comment tool — report your findings and stop.
102 changes: 71 additions & 31 deletions .claude/commands/triage-find-duplicate-prs.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
---
description: Find open PRs that duplicate a PR with 3 parallel search agents; silent when none found
allowed-tools: Task, Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh pr list:*), Bash(gh search:*), Bash(gh pr comment:*)
allowed-tools: Task, Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh pr list:*), Bash(gh search:*)
---

# /triage-find-duplicate-prs — overlapping-work check for a PR

Find OPEN, unmerged PRs that duplicate or overlap the target PR. SILENT by
default: when nothing credible turns up, post NOTHING. Context from the
caller: `REPO`, `NUMBER` (the target PR), `TRIGGER` (`opened`/`labeled`;
assume `labeled` locally). If `NUMBER` is missing, ask.
Find OPEN, unmerged PRs that duplicate or overlap the target PR, and REPORT
them. Context from the caller: `REPO`, `NUMBER` (the target PR), and `TRIGGER`
(`opened`/`labeled`). If `NUMBER` is missing, ask.

## Pre-checks (each exit is SILENT)
**You do not post anything.** You have no comment tool. Your findings go out as
a structured payload on this command's `TRIAGE-RESULT:` line, and
`scripts/render-triage-comment.mjs` renders and publishes the comment — it owns
the marker and the decision to comment at all, including this command's
silent-by-default behavior.

## Pre-checks (each exit is a `skip`, never a comment)

1. `gh pr view NUMBER --repo REPO --json state,title,body,files,comments` —
if the PR is not open, stop.
if the PR is not open, stop and report `skip (not open)`.
2. Marker check — a comment authored by bestaxbot or a bot account
containing `<!-- ai-triage:find-duplicate-prs -->` (match marker + that
Comment thread
allxsmith marked this conversation as resolved.
Resolved
author class, never one specific login — the workflow posts as
bestaxbot today; older comments are from github-actions[bot] or
claude[bot]):
- `TRIGGER=opened` and marker present → stop.
- `TRIGGER=labeled` and marker present → continue; at the end refresh
that comment with
`gh pr comment NUMBER --repo REPO --edit-last --body ...` ONLY when
your most recent comment on the PR is itself the
`<!-- ai-triage:find-duplicate-prs -->` comment. `--edit-last` selects
by author, not by marker, and the other triage commands post as the
same account — so a newer `find-issues` marker is what it would
overwrite. Otherwise post fresh. See `.github/CLAUDE.md` rule 6.
- `TRIGGER=opened` and marker present → stop, report
`skip (already triaged)`. This is a cost gate that saves the fan-out
below; the publisher independently refuses to overwrite an existing
triage comment on an `opened` run.
- `TRIGGER=labeled` → continue regardless; the publisher refreshes its
own marker comment in place.

## Search — 3 parallel agents

Expand All @@ -38,7 +40,7 @@ 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,
agents before anything is reported (#338). Collect every agent's result,
then continue with the filter pass.

The three strategies:
Expand All @@ -61,28 +63,66 @@ justification.

A duplicate solves the same problem or implements the same feature — same
files alone is NOT enough. Compare diffs (`gh pr diff`) when unsure. Drop
weak matches. Zero credible duplicates: on `TRIGGER=opened`, stop SILENTLY
— no comment, no marker; on `TRIGGER=labeled` (an explicit human request),
post/refresh the comment with the single line "No duplicate PRs found."
plus the marker (matches the pre-check refresh path).
weak matches.

## Post ONE comment (when duplicates exist, or on a labeled rerun)
If nothing credible remains, report an EMPTY `items` list — do not report
`skip`. The two mean different things, and the renderer needs the difference:
an empty result stays silent on an `opened` run and posts "No duplicate PRs
found." on a `labeled` one (an explicit human request). `skip` means a
pre-check stopped you before you searched at all.

Via `gh pr comment NUMBER --repo REPO --body ...` (or the refresh path):
## Report

```markdown
### AI triage — possible duplicate PRs
Emit this command's sentinel line, best match first:

- #N — <title>: <one-line reason> (at most 3, best match first)
```
TRIAGE-RESULT: triage-find-duplicate-prs publish {"items":[{"number":123,"title":"…","reason":"…"}]}
```

<!-- ai-triage:find-duplicate-prs -->
- `items` — REQUIRED, may be `[]` (see the filter pass). The renderer keeps
at most 3.
- `number` must be a JSON integer, never a string. `title` is required;
`reason` is a short one-liner and is optional.
- Compact JSON on ONE line. A pretty-printed payload fails the job.
- If a search tool errors, or you are rate-limited and cannot complete the
fan-out, report `skip (search failed)`. Do NOT report an empty list in that
case — empty means "I searched and found nothing", which is a claim a failed
search has not earned.
- Do NOT write markers or headings into a field — the renderer emits them
from its own constants, and structural text inside a field is defanged,
not honored.

## Running this locally

The workflow supplies `REPO`, `NUMBER` and `TRIGGER`; run by hand, assume
`TRIGGER=labeled` and ask for `NUMBER` rather than guessing. Locally the
sentinel line is the whole output — nothing publishes it, and that is the
point: you get to read the payload before it ever becomes a comment. To see
the comment it would produce, pipe the line into the renderer's dry run:

```bash
# with the TRIAGE-RESULT line saved to /tmp/result.txt
printf '[{"type":"result","is_error":false,"result":%s}]' \
"$(jq -Rs . </tmp/result.txt)" > /tmp/exec.json
node scripts/render-triage-comment.mjs --mode=render \
--exec-file=/tmp/exec.json --expect=triage-find-duplicate-prs --number=<n> \
--is-pr=true \
--trigger=labeled --autoclose=off --out-dir=/tmp/out --dry-run
```

The marker `<!-- ai-triage:find-duplicate-prs -->` goes on its own line at
the end.
Nothing in that path can post: `--mode=render` never opens a network
connection, and `--mode=publish --dry-run` stops before its first request.

## Size limits

The whole payload must stay under **8000 bytes** or the run fails, so report
your best candidates rather than everything you saw: at most **3** items. Keep
`title` under **256** characters and `reason` under **400** — one line each,
no markdown, no HTML. The renderer escapes both, so formatting is wasted
effort.

## Hard rules

Never apply or remove labels; never close, merge, push, or resolve
anything; never write "@claude" or "@coderabbitai" in any text; post at
most one comment, and none at all when nothing was found.
anything; never write "@claude" or "@coderabbitai" in any text. You have no
comment tool — report your findings and stop.
Loading
Loading