Skip to content
Closed
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
40 changes: 32 additions & 8 deletions .claude/commands/triage-dedupe.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,23 @@ 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.
run locally), and `AUTOCLOSE` (`active` or `off`; assume `off` locally — in
CI it is not passed to the session at all, see below). If `NUMBER` is
missing, ask — never guess.

## In CI (ai-triage.yml)

The workflow session must NOT post or edit comments — its tool allowlist is
GET-only and every `gh … comment` attempt is denied. Run the pre-checks,
search, and filter pass below unchanged, then, instead of the Post section,
report the result as the `TRIAGE-PAYLOAD:` line the workflow prompt specifies:
`"action":"skip"` for a pre-check exit, `"action":"post"` with an empty
`duplicates` array when nothing credible remains, `"action":"post"` with the
entries otherwise. A deterministic step
(`scripts/render-triage-comments.mjs`) renders the format below from that
payload and upserts the comment by marker as bestaxbot. Everything in this
file about `gh … comment`, `--edit-last`, and the AUTOCLOSE notice therefore
applies to LOCAL runs only.

## Pre-checks (in order; each exit is SILENT — post nothing)

Expand All @@ -23,7 +38,8 @@ run locally), and `AUTOCLOSE` (`active` or `off`; assume `off` locally). If
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
that comment instead of posting a new one. In CI the publish step does
that refresh by marker, so just report fresh findings; locally, 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,
Expand Down Expand Up @@ -78,8 +94,10 @@ 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:
Local runs post this via `gh issue comment NUMBER --repo REPO --body ...` (or
the refresh path from pre-check 2). In CI the renderer produces this exact
format from your payload — `scripts/render-triage-comments.mjs` is the source
of truth for the posted body, and this section documents what it emits:

```markdown
### AI triage
Expand All @@ -100,16 +118,22 @@ Duplicate of #N

- 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.
Include it only when you found at least one credible duplicate. In CI the
renderer emits this line from the payload's `duplicateOf` — report that
field, and never put the line inside a payload string.
- 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.
someone objects (comment or 👎)." If `AUTOCLOSE=off`, omit it. In CI the
renderer decides this from the workflow's own AUTOCLOSE value; the session
is not told 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.
In CI that is an empty `duplicates` array, not a skip.

## 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.
most one comment and nothing else — and in CI, post none: report the
payload.
28 changes: 24 additions & 4 deletions .claude/commands/triage-find-duplicate-prs.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ 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.

## In CI (ai-triage.yml)

The workflow session must NOT post or edit comments — its tool allowlist is
GET-only and every `gh … comment` attempt is denied. Run the pre-checks,
search, and filter pass below unchanged, then, instead of the Post section,
report the result as the `TRIAGE-PAYLOAD:` line the workflow prompt
specifies: `"action":"skip"` for a pre-check exit, `"action":"post"` with an
empty `prs` array when nothing credible remains, `"action":"post"` with the
entries otherwise. A deterministic step
(`scripts/render-triage-comments.mjs`) renders the format below from that
payload and upserts the comment by marker as bestaxbot. Everything in this
file about `gh … comment` and `--edit-last` applies to LOCAL runs only.

## Pre-checks (each exit is SILENT)

1. `gh pr view NUMBER --repo REPO --json state,title,body,files,comments` —
Expand All @@ -21,7 +34,8 @@ assume `labeled` locally). If `NUMBER` is missing, ask.
claude[bot]):
- `TRIGGER=opened` and marker present → stop.
- `TRIGGER=labeled` and marker present → continue; at the end refresh
that comment with
that comment. In CI the publish step does that by marker, so just
report fresh findings; locally, refresh 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
Expand Down Expand Up @@ -64,11 +78,16 @@ 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).
plus the marker (matches the pre-check refresh path). In CI, just report an
empty `prs` array — the renderer applies this opened-silent/labeled-post
rule itself, so the rule cannot be forgotten.

## Post ONE comment (when duplicates exist, or on a labeled rerun)

Via `gh pr comment NUMBER --repo REPO --body ...` (or the refresh path):
Local runs post this via `gh pr comment NUMBER --repo REPO --body ...` (or
the refresh path). In CI the renderer produces this exact format from your
payload — `scripts/render-triage-comments.mjs` is the source of truth for the
posted body, and this section documents what it emits:

```markdown
### AI triage — possible duplicate PRs
Expand All @@ -85,4 +104,5 @@ the end.

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.
most one comment, and none at all when nothing was found — and in CI, post
none: report the payload.
28 changes: 24 additions & 4 deletions .claude/commands/triage-find-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ EXACTLY ONE comment (or nothing). Context from the caller: `REPO`, `NUMBER`
(the target PR), `TRIGGER` (`opened`/`labeled`; assume `labeled` locally),
`AUTOCLOSE` (unused here). If `NUMBER` is missing, ask.

## In CI (ai-triage.yml)

The workflow session must NOT post or edit comments — its tool allowlist is
GET-only and every `gh … comment` attempt is denied. Run the pre-checks,
search, and filter pass below unchanged, then, instead of the Post section,
report the result as the `TRIAGE-PAYLOAD:` line the workflow prompt
specifies: `"action":"skip"` for a pre-check exit, `"action":"post"` with an
empty `issues` array when nothing credible remains, `"action":"post"` with the
entries otherwise. A deterministic step
(`scripts/render-triage-comments.mjs`) renders the format below from that
payload and upserts the comment by marker as bestaxbot. Everything in this
file about `gh … comment` and `--edit-last` applies to LOCAL runs only.

## Pre-checks (each exit is SILENT)

1. `gh pr view NUMBER --repo REPO --json state,title,body,comments` — if the
Expand All @@ -21,7 +34,8 @@ EXACTLY ONE comment (or nothing). Context from the caller: `REPO`, `NUMBER`
claude[bot]):
- `TRIGGER=opened` and marker present → stop.
- `TRIGGER=labeled` and marker present → continue; at the end refresh
that comment with
that comment. In CI the publish step does that by marker, so just
report fresh findings; locally, refresh 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-issues -->` comment. `--edit-last` selects by
Expand Down Expand Up @@ -70,11 +84,16 @@ credible remains: on `TRIGGER=opened`, stop SILENTLY — no comment, no
marker; on `TRIGGER=labeled` (an explicit human request — silence would
read as a malfunction), post/refresh the comment with the single line
"No open issues found that this PR resolves." plus the marker (matches
pre-check 2's refresh path).
pre-check 2's refresh path). In CI, just report an empty `issues` array —
the renderer applies this opened-silent/labeled-post rule itself, so the
rule cannot be forgotten.

## Post ONE comment

Via `gh pr comment NUMBER --repo REPO --body ...` (or the refresh path):
Local runs post this via `gh pr comment NUMBER --repo REPO --body ...` (or
the refresh path). In CI the renderer produces this exact format from your
payload — `scripts/render-triage-comments.mjs` is the source of truth for the
posted body, and this section documents what it emits:

````markdown
### AI triage — issues this PR may resolve
Expand All @@ -97,4 +116,5 @@ The marker `<!-- ai-triage:find-issues -->` goes on its own line at the end.

Never apply or remove labels; never close, merge, push, or resolve
anything; never edit the PR body yourself — only suggest; never write
"@claude" or "@coderabbitai" in any text; post at most one comment.
"@claude" or "@coderabbitai" in any text; post at most one comment — and in
CI, post none: report the payload.
24 changes: 19 additions & 5 deletions .github/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ regardless of how convenient it is.
**do**. So a drafted reproduction is sanitized deterministically and posted via
`GITHUB_TOKEN`, and every comment-triggered workflow independently gates out bestaxbot.

Triage holds it by construction since #457: the session emits a validated structured
payload, a deterministic step renders the body from a trusted skeleton, and only that
step holds bestaxbot's PAT — which also means no model session shares an environment
with a durable full-repo-write credential, since reading an env var is not a write and
no allowlist ever guarded it. The sender gating stays as defense in depth, not the sole
control.

## Hard requirements

### 1. Pin every third-party action to a full commit SHA — the same SHA everywhere
Expand Down Expand Up @@ -111,7 +118,7 @@ allowlist stopped mattering":

| Workflow | Credential in the session's job | What the allowlist is holding back |
| --------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ai-triage.yml` | `AI_LOOP_PAT` (bestaxbot) | Full repo write. Confined to GET-only `gh` reads plus the two comment commands. |
| `ai-triage.yml` | `CLAUDE_CODE_OAUTH_TOKEN`; job `GITHUB_TOKEN` is **write**-scoped (`issues`, `pull-requests`) | **Repository write: the allowlist is the only control.** The gate charges the budget marker and the label removal needs write, so the job token is write-scoped; the session's allowlist is GET-only `gh` reads. **The model credential: also the only control** (I1, egress unenforced — rule 10). What is NO LONGER here is bestaxbot's `AI_LOOP_PAT`: since #457 it lives only in the deterministic publish step, so an injected session cannot author as a re-trigger-capable identity even if this allowlist fails. |
| `ai-scan.yml` | `CLAUDE_CODE_OAUTH_TOKEN`; job `GITHUB_TOKEN` is **read**-scoped (`contents`/`issues`/`pull-requests`) since #455 | **Repository write: nothing any more.** The budget marker and the `needs-security-review` label moved to separate `gate` and `label` jobs that run no repository code, and only the coarse verdict enum crosses between them, so widening the allowlist can no longer grant issue/PR write. **The model credential: everything.** This job still runs Bash beside `CLAUDE_CODE_OAUTH_TOKEN`, so by I1 the allowlist is still the only thing standing between an injected session and reading that token out of the environment — and egress is not enforced (rule 10). Narrow, not retired. |

Concrete rules:
Expand Down Expand Up @@ -201,10 +208,17 @@ The two #454 parsers are extracted: the scan-verdict parser
(`scripts/parse-scan-verdict.mjs`, called by `ai-scan.yml`) and the publish sanitizer
(`scripts/sanitize-repro-draft.mjs`, called by `claude-repro.yml`) — their test siblings pin
the fail-closed matrix and the byte behavior of the shell they replaced, so edit script and
tests together. Smaller instances of the same shape remain inline (the exec-file sentinel
checks in `ai-triage.yml`, `claude-review.yml`, and `claude-repro.yml`'s author job); when
one of those next needs an edit, extract it and reuse `parse-scan-verdict.mjs`'s exported
helpers rather than growing the YAML.
tests together. #457 added two more, both called by `ai-triage.yml` and both taking the
route this rule prescribes rather than growing the YAML:
`scripts/render-triage-comments.mjs` (which replaced that workflow's inline sentinel jq and
reuses `parse-scan-verdict.mjs`'s helpers) and `scripts/pick-triage-upsert.mjs` (which
replaced the publish step's comment-selection jq and reuses
`auto-close-duplicates.mjs`'s `isAutomationAuthor`, so the two consumers of the triage
markers cannot drift on the author-class test rule 6 mandates — the jq it replaced had
already drifted, testing only `login == "bestaxbot" or type == "Bot"`). Reusing an existing
tested helper beats re-deriving one in jq every time. Smaller instances of the same shape remain
inline (the exec-file sentinel checks in `claude-review.yml` and `claude-repro.yml`'s author
job); when one of those next needs an edit, extract it the same way.

**Extraction has a bootstrap gap: a new script flags its own introducing PR.** A workflow runs
the YAML from the PR merge ref, but the jobs here deliberately check out the default branch
Expand Down
Loading
Loading