Skip to content

Commit 6bd434d

Browse files
bokelleyclaude
andauthored
feat(agents): migrate manual triage nudge to /triage slash-command-dispatch (#267)
Mirror of adcontextprotocol/adcp. Resolves the @claude autocomplete collision by moving to peter-evans/slash-command-dispatch. Adds reactions (eyes/+1/-1) on the triggering comment, separates the dispatcher from the triage handler, extensible to future commands. Requires new repo secret: TRIAGE_DISPATCH_PAT. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2227a6b commit 6bd434d

3 files changed

Lines changed: 135 additions & 64 deletions

File tree

.agents/routines/triage-prompt.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ If > 0, skip — another session beat you to it.
6161
## Manual nudge — overrides the already-engaged check
6262

6363
If the event context contains a `MANUAL NUDGE:` line, a repo member
64-
explicitly requested triage via `@claude-triage`. **Skip the
64+
explicitly requested triage via `/triage`. **Skip the
6565
already-engaged check** and proceed with full triage.
6666

67-
Modifiers: `@claude-triage execute` / `clarify` / `defer` bias the
67+
Modifiers: `/triage execute` / `clarify` / `defer` bias the
6868
outcome. No modifier = standard logic.
6969

7070
## Already-engaged check — before any expert work
Lines changed: 88 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,121 @@
1-
name: Claude Issue Triage Bridge
2-
3-
# Bridges GitHub events to a Claude Code routine's /fire endpoint.
4-
# Two entry points:
5-
# 1. `issues.opened` / `issues.reopened` — automatic triage on new issues.
6-
# 2. `issue_comment.created` with `@claude-triage` in the body — manual
7-
# nudge from a repo member, useful when you want the routine to
8-
# (re-)look at a specific issue on demand.
1+
name: Claude Issue Triage
2+
3+
# Fires the Claude Code triage routine's /fire endpoint when a new issue
4+
# opens OR when a repo member invokes `/triage` via comment (dispatched by
5+
# `.github/workflows/slash-command-dispatch.yml`).
96
#
10-
# The issue body is passed as *data* (fenced, size-capped) — the routine's
11-
# prompt treats anything inside the fence as untrusted content.
7+
# The issue body is fetched fresh and passed as *data* (fenced, size-capped) —
8+
# the routine's prompt treats anything inside the fence as untrusted content.
129
#
1310
# Required repo secrets:
1411
# CLAUDE_ROUTINE_TRIAGE_URL — full /fire URL including routine ID
15-
# CLAUDE_ROUTINE_TRIAGE_TOKEN — bearer token for that routine (shown once in web UI)
12+
# CLAUDE_ROUTINE_TRIAGE_TOKEN — bearer token for that routine
13+
# TRIAGE_DISPATCH_PAT — PAT for reaction on the /triage-triggering
14+
# comment (same secret as slash-command-dispatch.yml)
1615
#
1716
# Token last rotated: 2026-04-23 — rotate every 90 days.
1817

1918
on:
2019
issues:
2120
types: [opened, reopened]
22-
issue_comment:
23-
types: [created]
21+
repository_dispatch:
22+
types: [triage-command]
2423

2524
permissions:
2625
contents: read
2726

2827
concurrency:
29-
group: claude-triage-${{ github.event.issue.number }}
28+
group: claude-triage-${{ github.event.issue.number || github.event.client_payload.github.payload.issue.number }}
3029
cancel-in-progress: false
3130

3231
jobs:
3332
fire-routine:
3433
name: Fire triage routine
3534
runs-on: ubuntu-latest
36-
timeout-minutes: 2
37-
# Skip bots always. For comment events: only repo members can nudge,
38-
# and the comment body must contain `@claude-triage`.
35+
timeout-minutes: 3
36+
# Skip bot-opened issues. Manual path already gated by slash-dispatch
37+
# permission check.
3938
if: >-
40-
github.event.issue.user.type != 'Bot' &&
41-
!endsWith(github.event.issue.user.login, '[bot]') &&
42-
github.event.sender.type != 'Bot' &&
39+
github.event_name != 'issues' ||
4340
(
44-
github.event_name == 'issues' ||
45-
(
46-
github.event_name == 'issue_comment' &&
47-
contains(github.event.comment.body, '@claude-triage') &&
48-
(
49-
github.event.comment.author_association == 'OWNER' ||
50-
github.event.comment.author_association == 'MEMBER' ||
51-
github.event.comment.author_association == 'COLLABORATOR'
52-
)
53-
)
41+
github.event.issue.user.type != 'Bot' &&
42+
!endsWith(github.event.issue.user.login, '[bot]') &&
43+
github.event.sender.type != 'Bot'
5444
)
5545
defaults:
5646
run:
5747
shell: bash
5848
steps:
49+
- name: Resolve issue number + event kind
50+
id: ctx
51+
run: |
52+
set -euo pipefail
53+
if [ "${{ github.event_name }}" = "issues" ]; then
54+
echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
55+
echo "kind=auto" >> "$GITHUB_OUTPUT"
56+
echo "action=${{ github.event.action }}" >> "$GITHUB_OUTPUT"
57+
echo "commenter=" >> "$GITHUB_OUTPUT"
58+
echo "args=" >> "$GITHUB_OUTPUT"
59+
echo "comment_id=" >> "$GITHUB_OUTPUT"
60+
else
61+
echo "number=${{ github.event.client_payload.github.payload.issue.number }}" >> "$GITHUB_OUTPUT"
62+
echo "kind=manual" >> "$GITHUB_OUTPUT"
63+
echo "action=triage" >> "$GITHUB_OUTPUT"
64+
echo "commenter=${{ github.event.client_payload.github.payload.comment.user.login }}" >> "$GITHUB_OUTPUT"
65+
echo "args=${{ github.event.client_payload.slash_command.args.all }}" >> "$GITHUB_OUTPUT"
66+
echo "comment_id=${{ github.event.client_payload.github.payload.comment.id }}" >> "$GITHUB_OUTPUT"
67+
fi
68+
5969
- name: POST to routine /fire
70+
id: fire
6071
env:
72+
GH_TOKEN: ${{ github.token }}
6173
ROUTINE_URL: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_URL }}
6274
ROUTINE_TOKEN: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_TOKEN }}
63-
EVENT_NAME: ${{ github.event_name }}
64-
ACTION: ${{ github.event.action }}
65-
ISSUE_NUMBER: ${{ github.event.issue.number }}
66-
ISSUE_TITLE: ${{ github.event.issue.title }}
67-
ISSUE_URL: ${{ github.event.issue.html_url }}
68-
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
69-
ISSUE_AUTHOR_ASSOC: ${{ github.event.issue.author_association }}
70-
ISSUE_BODY: ${{ github.event.issue.body || '' }}
71-
ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
72-
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
73-
COMMENT_BODY: ${{ github.event.comment.body || '' }}
7475
REPO: ${{ github.repository }}
76+
ISSUE_NUMBER: ${{ steps.ctx.outputs.number }}
77+
EVENT_KIND: ${{ steps.ctx.outputs.kind }}
78+
ACTION: ${{ steps.ctx.outputs.action }}
79+
COMMENTER: ${{ steps.ctx.outputs.commenter }}
80+
ARGS: ${{ steps.ctx.outputs.args }}
7581
run: |
7682
set -euo pipefail
7783
7884
if [ -z "${ROUTINE_URL:-}" ] || [ -z "${ROUTINE_TOKEN:-}" ]; then
79-
echo "::warning::CLAUDE_ROUTINE_TRIAGE_URL or CLAUDE_ROUTINE_TRIAGE_TOKEN not set — skipping."
85+
echo "::warning::CLAUDE_ROUTINE_TRIAGE_URL or _TOKEN not set — skipping."
8086
exit 0
8187
fi
8288
83-
# Strip NUL bytes and cap body sizes to reduce prompt-injection surface and cost.
84-
ISSUE_BODY_SAFE=$(printf '%s' "${ISSUE_BODY}" | tr -d '\000' | head -c 8192)
85-
COMMENT_BODY_SAFE=$(printf '%s' "${COMMENT_BODY}" | tr -d '\000' | head -c 2048)
89+
# Fetch the issue fresh so both event paths use the same source of truth.
90+
issue=$(gh api "repos/$REPO/issues/$ISSUE_NUMBER")
91+
title=$(echo "$issue" | jq -r '.title')
92+
body=$(echo "$issue" | jq -r '.body // ""')
93+
author=$(echo "$issue" | jq -r '.user.login')
94+
assoc=$(echo "$issue" | jq -r '.author_association // "NONE"')
95+
labels=$(echo "$issue" | jq -c '[.labels[].name]')
96+
html_url=$(echo "$issue" | jq -r '.html_url')
8697
87-
# For manual nudges, build a trusted signal line (outside the fence)
88-
# telling the routine a repo member explicitly asked for triage.
89-
# The already-engaged check should pass this through; the nudge IS
90-
# the explicit request.
91-
if [ "$EVENT_NAME" = "issue_comment" ]; then
92-
nudge_note="MANUAL NUDGE: @${COMMENT_AUTHOR} requested triage via @claude-triage. Comment body (trimmed): \"${COMMENT_BODY_SAFE}\". Treat this as an explicit request; do NOT silent-defer on already-engaged signals — the nudge overrides."
93-
else
94-
nudge_note=""
98+
body_safe=$(printf '%s' "$body" | tr -d '\000' | head -c 8192)
99+
100+
nudge_note=""
101+
if [ "$EVENT_KIND" = "manual" ]; then
102+
nudge_note="MANUAL NUDGE: @${COMMENTER} requested triage via /triage ${ARGS}. Treat as an explicit request; skip already-engaged check. Honor any modifier (execute / clarify / defer) in the args."
95103
fi
96104
97105
payload=$(jq -n \
98106
--arg repo "$REPO" \
99107
--arg num "$ISSUE_NUMBER" \
100-
--arg title "$ISSUE_TITLE" \
101-
--arg url "$ISSUE_URL" \
102-
--arg author "$ISSUE_AUTHOR" \
103-
--arg assoc "$ISSUE_AUTHOR_ASSOC" \
108+
--arg title "$title" \
109+
--arg url "$html_url" \
110+
--arg author "$author" \
111+
--arg assoc "$assoc" \
112+
--arg kind "$EVENT_KIND" \
104113
--arg action "$ACTION" \
105-
--arg event "$EVENT_NAME" \
106-
--argjson labels "$ISSUE_LABELS" \
107-
--arg body "$ISSUE_BODY_SAFE" \
114+
--argjson labels "$labels" \
115+
--arg body "$body_safe" \
108116
--arg nudge "$nudge_note" \
109117
'{text: (
110-
"Event: " + $event + "." + $action + "\n" +
118+
"Event: " + $kind + "." + $action + "\n" +
111119
"Repo: " + $repo + "\n" +
112120
"Issue: #" + $num + " \"" + $title + "\"\n" +
113121
"URL: " + $url + "\n" +
@@ -145,4 +153,22 @@ jobs:
145153
exit 1
146154
fi
147155
148-
echo "::notice::Fired triage routine for #${ISSUE_NUMBER} (event=${EVENT_NAME}, author=@${ISSUE_AUTHOR}/${ISSUE_AUTHOR_ASSOC})"
156+
echo "::notice::Fired triage routine for #${ISSUE_NUMBER} (kind=${EVENT_KIND}, author=@${author}/${assoc})"
157+
158+
- name: React +1 on manual-nudge comment (success)
159+
if: steps.ctx.outputs.kind == 'manual' && success() && steps.ctx.outputs.comment_id != ''
160+
uses: peter-evans/create-or-update-comment@v5
161+
with:
162+
token: ${{ secrets.TRIAGE_DISPATCH_PAT }}
163+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
164+
comment-id: ${{ steps.ctx.outputs.comment_id }}
165+
reactions: "+1"
166+
167+
- name: React -1 on manual-nudge comment (failure)
168+
if: steps.ctx.outputs.kind == 'manual' && failure() && steps.ctx.outputs.comment_id != ''
169+
uses: peter-evans/create-or-update-comment@v5
170+
with:
171+
token: ${{ secrets.TRIAGE_DISPATCH_PAT }}
172+
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
173+
comment-id: ${{ steps.ctx.outputs.comment_id }}
174+
reactions: "-1"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Slash Command Dispatch
2+
3+
# Routes `/<command>` comments on issues and PRs to dedicated handler
4+
# workflows via repository_dispatch events. Uses peter-evans/slash-command-
5+
# dispatch so we get:
6+
# - Built-in permission checks (only OWNER / MEMBER / COLLABORATOR trigger)
7+
# - Reactions on the triggering comment (eyes on ack, +1 on success, -1 on fail)
8+
# - Clean separation: this workflow routes, downstream handlers do the work
9+
#
10+
# Required repo secret:
11+
# TRIAGE_DISPATCH_PAT — Personal Access Token with `repo` scope (or fine-
12+
# grained: contents:write, issues:write, pull-requests:write
13+
# on this repo). GITHUB_TOKEN cannot fire
14+
# repository_dispatch events.
15+
#
16+
# Current commands:
17+
# /triage [modifier] — fire the triage routine on this issue. Modifiers
18+
# bias the outcome (execute / clarify / defer).
19+
20+
on:
21+
issue_comment:
22+
types: [created]
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
dispatch:
29+
name: Dispatch slash command
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 2
32+
# Skip bot-authored comments always.
33+
if: >-
34+
github.event.comment.user.type != 'Bot' &&
35+
!endsWith(github.event.comment.user.login, '[bot]')
36+
steps:
37+
- name: Slash command dispatch
38+
uses: peter-evans/slash-command-dispatch@v5
39+
with:
40+
token: ${{ secrets.TRIAGE_DISPATCH_PAT }}
41+
commands: |
42+
triage
43+
permission: write
44+
reactions: true
45+
issue-type: both

0 commit comments

Comments
 (0)