From d3b8b28d8dc9cbf1ab2df86b1a0c7f619ad3c332 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 23 Apr 2026 20:49:43 -0400 Subject: [PATCH] feat(agents): /claude-triage comment nudge Mirror of adcontextprotocol/adcp#2970. Repo members can fire the triage routine on demand by commenting /claude-triage on any issue. Supports modifiers: execute / clarify / defer. Co-Authored-By: Claude Opus 4.7 (1M context) --- .agents/routines/triage-prompt.md | 11 ++++ .github/workflows/claude-issue-triage.yml | 65 +++++++++++++++++------ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/.agents/routines/triage-prompt.md b/.agents/routines/triage-prompt.md index 610b49aa4..4ed30a193 100644 --- a/.agents/routines/triage-prompt.md +++ b/.agents/routines/triage-prompt.md @@ -58,8 +58,19 @@ gh api repos/adcontextprotocol/adcp-client-python/issues//comments \ If > 0, skip — another session beat you to it. +## Manual nudge — overrides the already-engaged check + +If the event context contains a `MANUAL NUDGE:` line, a repo member +explicitly requested triage via `/claude-triage`. **Skip the +already-engaged check** and proceed with full triage. + +Modifiers: `/claude-triage execute` / `clarify` / `defer` bias the +outcome. No modifier = standard logic. + ## Already-engaged check — before any expert work +(Skip if the event is a MANUAL NUDGE — see above.) + Silent-defer (apply `claude-triaged`, no comment) if any of these: 1. **Assigned to a repo member** — any assignee is diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml index 573f33179..58c901105 100644 --- a/.github/workflows/claude-issue-triage.yml +++ b/.github/workflows/claude-issue-triage.yml @@ -1,13 +1,16 @@ name: Claude Issue Triage Bridge -# Bridges GitHub `issues` events to a Claude Code routine's /fire endpoint. -# Routines do not natively subscribe to issue events, so this workflow -# POSTs issue context to a per-routine URL the moment an issue is opened. +# Bridges GitHub events to a Claude Code routine's /fire endpoint. +# Two entry points: +# 1. `issues.opened` / `issues.reopened` — automatic triage on new issues. +# 2. `issue_comment.created` with `/claude-triage` in the body — manual +# nudge from a repo member, useful when you want the routine to +# (re-)look at a specific issue on demand. +# # The issue body is passed as *data* (fenced, size-capped) — the routine's -# prompt treats anything inside the fence as untrusted content, never -# instructions. +# prompt treats anything inside the fence as untrusted content. # -# Required repo secrets (set after creating the routine at claude.ai/code/routines): +# Required repo secrets: # CLAUDE_ROUTINE_TRIAGE_URL — full /fire URL including routine ID # CLAUDE_ROUTINE_TRIAGE_TOKEN — bearer token for that routine (shown once in web UI) # @@ -16,6 +19,8 @@ name: Claude Issue Triage Bridge on: issues: types: [opened, reopened] + issue_comment: + types: [created] permissions: contents: read @@ -29,10 +34,24 @@ jobs: name: Fire triage routine runs-on: ubuntu-latest timeout-minutes: 2 + # Skip bots always. For comment events: only repo members can nudge, + # and the comment body must contain `/claude-triage`. if: >- github.event.issue.user.type != 'Bot' && !endsWith(github.event.issue.user.login, '[bot]') && - github.event.sender.type != 'Bot' + github.event.sender.type != 'Bot' && + ( + github.event_name == 'issues' || + ( + github.event_name == 'issue_comment' && + contains(github.event.comment.body, '/claude-triage') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) + ) defaults: run: shell: bash @@ -41,6 +60,8 @@ jobs: env: ROUTINE_URL: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_URL }} ROUTINE_TOKEN: ${{ secrets.CLAUDE_ROUTINE_TRIAGE_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + ACTION: ${{ github.event.action }} ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_URL: ${{ github.event.issue.html_url }} @@ -48,7 +69,8 @@ jobs: ISSUE_AUTHOR_ASSOC: ${{ github.event.issue.author_association }} ISSUE_BODY: ${{ github.event.issue.body || '' }} ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }} - ACTION: ${{ github.event.action }} + COMMENT_AUTHOR: ${{ github.event.comment.user.login }} + COMMENT_BODY: ${{ github.event.comment.body || '' }} REPO: ${{ github.repository }} run: | set -euo pipefail @@ -58,11 +80,20 @@ jobs: exit 0 fi - # Strip NUL bytes and cap body at 8KB to reduce prompt-injection surface and cost. + # Strip NUL bytes and cap body sizes to reduce prompt-injection surface and cost. ISSUE_BODY_SAFE=$(printf '%s' "${ISSUE_BODY}" | tr -d '\000' | head -c 8192) + COMMENT_BODY_SAFE=$(printf '%s' "${COMMENT_BODY}" | tr -d '\000' | head -c 2048) + + # For manual nudges, build a trusted signal line (outside the fence) + # telling the routine a repo member explicitly asked for triage. + # The already-engaged check should pass this through; the nudge IS + # the explicit request. + if [ "$EVENT_NAME" = "issue_comment" ]; then + 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." + else + nudge_note="" + fi - # Build the payload. `text` wraps the untrusted body in a clearly- - # fenced block. Labels are passed as native JSON via --argjson. payload=$(jq -n \ --arg repo "$REPO" \ --arg num "$ISSUE_NUMBER" \ @@ -71,21 +102,24 @@ jobs: --arg author "$ISSUE_AUTHOR" \ --arg assoc "$ISSUE_AUTHOR_ASSOC" \ --arg action "$ACTION" \ + --arg event "$EVENT_NAME" \ --argjson labels "$ISSUE_LABELS" \ --arg body "$ISSUE_BODY_SAFE" \ + --arg nudge "$nudge_note" \ '{text: ( - "Event: issues." + $action + "\n" + + "Event: " + $event + "." + $action + "\n" + "Repo: " + $repo + "\n" + "Issue: #" + $num + " \"" + $title + "\"\n" + "URL: " + $url + "\n" + "Author: @" + $author + " (association: " + $assoc + ")\n" + - "Labels: " + ($labels | join(", ")) + "\n\n" + + "Labels: " + ($labels | join(", ")) + "\n" + + (if $nudge == "" then "" else $nudge + "\n" end) + + "\n" + "<<>>\n" + $body + "\n" + "<<>>" )}') - # Capture status and response separately so curl exit code is checked. set +e http_code=$(curl --fail-with-body -sS -o /tmp/fire-response.json -w "%{http_code}" \ -X POST "$ROUTINE_URL" \ @@ -103,7 +137,6 @@ jobs: fi echo "HTTP $http_code" - # Redact any `Bearer` substrings defensively in case the response echoes them. sed 's/[Bb]earer [A-Za-z0-9._-]*/Bearer [REDACTED]/g' /tmp/fire-response.json echo @@ -112,4 +145,4 @@ jobs: exit 1 fi - echo "::notice::Fired triage routine for #${ISSUE_NUMBER} (@${ISSUE_AUTHOR}, ${ISSUE_AUTHOR_ASSOC})" + echo "::notice::Fired triage routine for #${ISSUE_NUMBER} (event=${EVENT_NAME}, author=@${ISSUE_AUTHOR}/${ISSUE_AUTHOR_ASSOC})"