33# Listens for @<agent-name> please <request> mentions from authorized users
44# and runs Claude Code to respond.
55#
6+ # Authenticates as the `ai4c-agent` GitHub App (installation token minted per
7+ # job), not as a machine account PAT. Same app and secret names already used by
8+ # claude-code-review.yml.
9+ #
610# Required secrets:
7- # - PAT_FOR_PR
11+ # - AI4C_AGENT_APP_ID
12+ # - AI4C_AGENT_PRIVATE_KEY
813# - CLAUDE_CODE_OAUTH_TOKEN
914#
1015# Configuration:
1318name : AI Agent GitHub Mentions
1419
1520env :
16- AGENT_NAME : dragon-ai-agent
21+ # The handle controllers type to invoke the agent. NOT a real GitHub account:
22+ # the app's login is AGENT_LOGIN (`<name>[bot]`), and apps cannot be
23+ # @-mentioned, so "@ai4c-agent" renders as plain text.
24+ AGENT_MENTION : ai4c-agent
25+ # Deprecated handles still honoured as triggers (comma-separated). Also the
26+ # only logins that can work as assignment triggers -- see ASSIGNMENT below.
27+ AGENT_MENTION_LEGACY : dragon-ai-agent
28+ # GitHub App bot identity, for commit attribution. AGENT_USER_ID is the
29+ # numeric id of the ai4c-agent[bot] *account*
30+ # (`gh api /users/ai4c-agent%5Bbot%5D`), NOT the app id in
31+ # AI4C_AGENT_APP_ID -- different numbers. Public, hence plain env.
32+ AGENT_LOGIN : ai4c-agent[bot]
33+ AGENT_USER_ID : 242316268
1734 MODEL : claude-opus-4-7
1835 TOOLS_DIR : ${{ github.workspace }}/tools
1936 TIMEOUT_MINUTES : 30
3451 prompt :
3552 description : " The request/prompt for the agent"
3653 required : true
37- # `assigned` lets authorized controllers dispatch work by assigning
38- # `dragon-ai-agent` directly.
54+ # ASSIGNMENT: `assigned` lets authorized controllers dispatch work by
55+ # assigning the agent directly. NOTE this cannot work with the GitHub App --
56+ # `ai4c-agent[bot]` is not an assignable user, so only the legacy
57+ # `dragon-ai-agent` machine account can still trigger this path, and only for
58+ # as long as that account exists. See `agentAssignees` in the check script.
3959 issues :
4060 types : [opened, edited, assigned]
4161 issue_comment :
@@ -55,17 +75,27 @@ jobs:
5575 outputs :
5676 result : ${{ steps.check.outputs.result }}
5777 steps :
78+ # Minted per-job: installation tokens are short-lived (1h) and must not be
79+ # passed between jobs, since GitHub's log masking does not follow job
80+ # outputs.
81+ - name : Mint app token
82+ id : app-token
83+ uses : actions/create-github-app-token@v3
84+ with :
85+ app-id : ${{ secrets.AI4C_AGENT_APP_ID }}
86+ private-key : ${{ secrets.AI4C_AGENT_PRIVATE_KEY }}
87+
5888 - name : Checkout repository
5989 uses : actions/checkout@v6
6090 with :
6191 fetch-depth : 1
62- token : ${{ secrets.PAT_FOR_PR }}
92+ token : ${{ steps.app-token.outputs.token }}
6393
6494 - name : Check for qualifying mention
6595 id : check
6696 uses : actions/github-script@v8
6797 with :
68- github-token : ${{ secrets.PAT_FOR_PR }}
98+ github-token : ${{ steps.app-token.outputs.token }}
6999 script : |
70100 const fs = require("fs");
71101
@@ -78,7 +108,18 @@ jobs:
78108 allowedUsers = ["cmungall"];
79109 }
80110
81- const agentName = process.env.AGENT_NAME || "dragon-ai-agent";
111+ // Trigger on the canonical handle plus any legacy aliases, with an
112+ // optional [bot] suffix (people paste back what they see the bot
113+ // signing as).
114+ const agentName = process.env.AGENT_MENTION;
115+ const legacyNames = (process.env.AGENT_MENTION_LEGACY || "")
116+ .split(",").map((s) => s.trim()).filter(Boolean);
117+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118+ const mentionAlternation = [agentName, ...legacyNames].map(escapeRe).join("|");
119+ // Assignment dispatch can only ever match a real, assignable user
120+ // account. The app's `ai4c-agent[bot]` login is not assignable, so
121+ // this is the legacy names only.
122+ const agentAssignees = legacyNames;
82123
83124 if (context.eventName === "workflow_dispatch") {
84125 const inputs = context.payload.inputs;
@@ -139,8 +180,16 @@ jobs:
139180 reactionTarget = "pull_request_review_comment";
140181 }
141182
183+ // Never act on our own output. Unlike GITHUB_TOKEN, events authored
184+ // with an app installation token do retrigger workflows.
185+ if (userLogin.endsWith("[bot]")) {
186+ console.log(`Ignoring bot author: ${userLogin}`);
187+ return { qualifiedMention: false };
188+ }
189+
142190 const isAllowed = allowedUsers.includes(userLogin);
143- const mentionRegex = new RegExp(`@${agentName}\\s+please\\s+([\\s\\S]*)`, "i");
191+ const mentionRegex = new RegExp(
192+ `@(${mentionAlternation})(?:\\[bot\\])?\\s+please\\s+([\\s\\S]*)`, "i");
144193 const mentionMatch = content.match(mentionRegex);
145194 // On synchronize we want this workflow to revalidate on push without
146195 // re-dispatching the agent from a stale PR body mention.
@@ -152,19 +201,23 @@ jobs:
152201 const isAgentAssignment =
153202 (context.eventName === "issues" || context.eventName === "pull_request") &&
154203 context.payload.action === "assigned" &&
155- context.payload.assignee?.login === agentName ;
204+ agentAssignees.includes( context.payload.assignee?.login) ;
156205 const hasQualifiedBodyMention =
157206 isAllowed && shouldHonorBodyMention && mentionMatch !== null;
158207 const qualifiedMention =
159208 hasQualifiedBodyMention || (isAllowed && isAgentAssignment);
160209 const prompt = hasQualifiedBodyMention
161- ? mentionMatch[1 ].trim()
210+ ? mentionMatch[2 ].trim()
162211 : (isAgentAssignment
163212 ? "You were assigned to this item. Read the full GitHub context, identify the next concrete action, and carry it through."
164213 : "");
214+ const mentionUsed = mentionMatch ? mentionMatch[1] : "";
215+ const usedLegacyMention =
216+ hasQualifiedBodyMention && mentionUsed.toLowerCase() !== agentName.toLowerCase();
165217
166218 console.log(
167219 `User: ${userLogin}, Allowed: ${isAllowed}, Has mention: ${mentionMatch !== null}, ` +
220+ `Handle: ${mentionUsed}, ` +
168221 `Honor body mention: ${shouldHonorBodyMention}, ` +
169222 `Agent assignment: ${isAgentAssignment}`
170223 );
@@ -202,7 +255,10 @@ jobs:
202255
203256 try {
204257 const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
205- const commentBody = `🤖 Working on it...\n\nFollow along: [View workflow run](${runUrl})\n\n*— @${agentName}*`;
258+ const deprecationNote = usedLegacyMention
259+ ? `\n\n> [!NOTE]\n> \`@${mentionUsed}\` is deprecated and will stop working. Please use \`@${agentName} please ...\` from now on.`
260+ : "";
261+ const commentBody = `🤖 Working on it...\n\nFollow along: [View workflow run](${runUrl})${deprecationNote}\n\n*— @${agentName}*`;
206262
207263 await github.rest.issues.createComment({
208264 owner: context.repo.owner,
@@ -245,16 +301,26 @@ jobs:
245301 runs-on : ubuntu-latest
246302 container : ${{ fromJSON(needs.check-mention.outputs.result).useOdkContainer && 'obolibrary/odkfull:v1.6' || null }}
247303 steps :
304+ # Must precede checkout: the token is persisted as the git credential and
305+ # is what later `git push` calls authenticate with. Valid for 1h, which
306+ # bounds how far timeout-minutes above can safely be raised.
307+ - name : Mint app token
308+ id : app-token
309+ uses : actions/create-github-app-token@v3
310+ with :
311+ app-id : ${{ secrets.AI4C_AGENT_APP_ID }}
312+ private-key : ${{ secrets.AI4C_AGENT_PRIVATE_KEY }}
313+
248314 - name : Checkout repository
249315 uses : actions/checkout@v6
250316 with :
251317 fetch-depth : 1
252- token : ${{ secrets.PAT_FOR_PR }}
318+ token : ${{ steps.app-token.outputs.token }}
253319
254320 - name : Configure Git
255321 run : |
256- git config --global user.name "${{ env.AGENT_NAME }}[bot] "
257- git config --global user.email "${{ env.AGENT_NAME }}[bot] @users.noreply.github.com"
322+ git config --global user.name "${{ env.AGENT_LOGIN }}"
323+ git config --global user.email "${{ env.AGENT_USER_ID }}+${{ env.AGENT_LOGIN }} @users.noreply.github.com"
258324
259325 - name : Create tools directory
260326 run : mkdir -p "${{ env.TOOLS_DIR }}"
@@ -278,20 +344,20 @@ jobs:
278344 echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH"
279345
280346 - name : Export GitHub token for gh CLI
281- run : echo "GH_TOKEN=${{ secrets.PAT_FOR_PR }}" >> "$GITHUB_ENV"
347+ run : echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> "$GITHUB_ENV"
282348
283349 - name : Run Claude Code
284350 uses : anthropics/claude-code-action@v1
285351 with :
286352 claude_code_oauth_token : ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
287- github_token : ${{ secrets.PAT_FOR_PR }}
353+ github_token : ${{ steps.app-token.outputs.token }}
288354 allowed_bots : " claude,github-actions"
289355 show_full_output : true
290356 claude_args : |
291357 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,LS,MultiEdit,NotebookEdit,TodoRead,TodoWrite,WebFetch,WebSearch"
292358 --model "${{ env.MODEL }}"
293359 prompt : |
294- You are @${{ env.AGENT_NAME }}, responding to a request from @${{ fromJSON(needs.check-mention.outputs.result).user }} on GitHub ${{ fromJSON(needs.check-mention.outputs.result).itemType }} #${{ fromJSON(needs.check-mention.outputs.result).itemNumber }}.
360+ You are @${{ env.AGENT_MENTION }}, responding to a request from @${{ fromJSON(needs.check-mention.outputs.result).user }} on GitHub ${{ fromJSON(needs.check-mention.outputs.result).itemType }} #${{ fromJSON(needs.check-mention.outputs.result).itemNumber }}.
295361
296362 Follow the repository instructions in `CLAUDE.md`.
297363
@@ -327,7 +393,7 @@ jobs:
327393 Always include the following signature block in commit messages and GitHub comments:
328394 ```
329395 ---
330- 🤖 **Generated by @${{ env.AGENT_NAME }}**
396+ 🤖 **Generated by @${{ env.AGENT_MENTION }}**
331397 - Model: `${{ env.MODEL }}`
332398 - Agent harness: claude-code
333399 - Triggered by: @${{ fromJSON(needs.check-mention.outputs.result).user }}
0 commit comments