Surface every place someone has pinged you — Slack, Jira, Confluence, GitHub — and help you respond without losing your mind.
Before doing anything else, verify the following MCP connectors are reachable:
slack_search_public_and_private (or slack_search_public as fallback) AND slack_send_message (required for dispatch — if absent, warn the human upfront that Slack replies won’t be possible)getAccessibleAtlassianResourcesgh CLI authenticated (gh auth status) AND gh api access — required for PR/issue comment scanning. If unavailable, warn and skip the GitHub scan.If all connectors are unavailable, stop and tell the human: “No connectors are configured. Fix at least one and try again.”
If only some connectors are missing, warn the human which sources will be skipped, then proceed with the available ones.
Resolve the human’s identity once and cache for the session:
atlassianUserInfo with the human’s email (kfaham@embarkvet.com). Extract and cache:
accountId — used in JQL and CQL queriesdisplayName — used as the display name in CQL text searchesIf atlassianUserInfo does not return a displayName, fall back to lookupJiraAccountId using the email. If display name is still null after both attempts, stop and ask the human to provide it manually — do not proceed with CQL queries that depend on it.
Slack member ID and handle — use slack_search_users to resolve the Slack member ID for the human’s name or email. Cache both the raw member ID (e.g. U012AB3CD) and the @handle. If resolution fails, warn and skip the Slack scan.
GitHub login — run gh api user --jq .login to get the canonical GitHub username. Cache it for all GitHub queries. If gh is not authenticated, skip the GitHub scan and warn the human.
kfaham@embarkvet.com — use as lookup seed.Run scans across all three sources. Each scan should look back 7 days by default (override if the human specifies a different window via skill args, e.g. /triage-mentions 14d).
Compute the lookback cutoff date as an absolute ISO 8601 timestamp at scan start (e.g. 2024-01-08T14:00:00Z). Use this value in all queries — do not rely on relative date helpers whose syntax varies across API versions.
Rate limiting note: Jira and Confluence share the same Atlassian OAuth token. Run their scans sequentially (Jira first, then Confluence) to avoid concurrent 429s. If a 429 is received, back off 10 seconds and retry once before surfacing the error.
Search for messages where the human is directly mentioned using their cached Slack member ID:
query: "@{slack_member_id}"
Use slack_search_public_and_private for full coverage. Fall back to slack_search_public if private search is unavailable.
For each result:
slack_read_thread to get the full question/comment and all replies.channel_id (not just channel name), thread_ts (the raw float timestamp, e.g. 1714000000.123456), author, message text, date.Limit to 50 results. If the raw search returns more, note the overflow count in the triage list header (e.g. Showing 50 of 83 — re-invoke with a shorter window to see the rest).
Run these two JQL queries sequentially:
Query 1 — direct mentions:
(comment ~ "accountId:{account_id}" OR description ~ "accountId:{account_id}") AND updatedDate >= "{cutoff_iso}" ORDER BY updated DESC
Query 2 — issues owned by the human with new activity:
(assignee = "{account_id}" OR reporter = "{account_id}") AND updatedDate >= "{cutoff_iso}" ORDER BY updated DESC
Note: use the cached {account_id}, not currentUser() — currentUser() resolves to the MCP app’s OAuth identity, not the human.
For each result, fetch the issue via getJiraIssue to get the full comment list. Filter to comments that either:
Skip any issue where the human’s most recent comment is newer than all other comments — they have already responded.
Capture: issue key, summary, comment ID, author, comment text (truncated to 300 chars), date, direct link.
Limit to 30 issues total.
Confluence does not store @mentions as plain text — they are ADF user-mention nodes. Full-text search on display name is lossy (misses most real mentions, may include false positives). Use this two-pronged approach and accept that results may be incomplete:
Query 1 — comments referencing the human’s account ID or display name:
type = "comment" AND (text ~ "{account_id}" OR text ~ "{display_name}") AND lastModified >= "{cutoff_iso}" ORDER BY lastModified DESC
Query 2 — pages the human contributes to with recent comment activity:
type = "page" AND space.type = "global" AND contributor = "{account_id}" AND lastModified >= "{cutoff_iso}" ORDER BY lastModified DESC
For each result, fetch the page comments via getConfluencePageFooterComments and getConfluencePageInlineComments. Filter to comments that:
Limitation note: ADF-encoded @mentions may not appear in full-text search results. When presenting Confluence results in Phase 3, prepend a one-line warning: ⚠ Confluence scan may miss some @mention notifications due to API limitations.
Capture: page title, page URL, comment ID, author, comment text (truncated to 300 chars), date.
Limit to 30 results.
Search for GitHub notifications where the human is mentioned. Run these gh commands sequentially:
Query 1 — PR and issue review requests / mentions:
gh api "notifications?all=false&participating=true&per_page=50" \
--paginate --jq '.[] | select(.updated_at >= "{cutoff_iso}")'
This returns notifications the human is subscribed to and participating in. Filter to those with reason of mention, review_requested, or comment.
Query 2 — PRs where the human is reviewed and new comments have arrived since their last review:
gh search prs \
--involves=@me \
--state=open \
--updated=">={cutoff_date}" \
--json number,title,url,repository,updatedAt \
--limit 30
For each PR returned by Query 2, fetch comments since the human’s last activity:
gh api repos/{owner}/{repo}/pulls/{number}/comments \
--jq '.[] | select(.created_at >= "{cutoff_iso}" and .user.login != "{gh_login}")'
gh api repos/{owner}/{repo}/issues/{number}/comments \
--jq '.[] | select(.created_at >= "{cutoff_iso}" and .user.login != "{gh_login}")'
Query 3 — Issues where the human is mentioned or assigned with new comments:
gh search issues \
--involves=@me \
--state=open \
--updated=">={cutoff_date}" \
--json number,title,url,repository,updatedAt \
--limit 30
For each item, capture: repo (owner/name), PR/issue number, title, comment author, comment text (truncated to 300 chars), URL to the specific comment, date.
Skip rule: Skip any PR/issue where the human’s most recent comment is newer than all other comments since the cutoff — they have already responded.
Limit to 50 total GitHub items. Use gh api for pagination where needed.
Merge all results into a single list. Remove duplicates (same URL/ID). Apply this priority sort:
@mention of the human (highest priority)Present the list to the human grouped by source for readability:
Found {N} items requiring your attention ({date range}):
{If overflow: Showing {50} of {total} — re-invoke with a shorter window to see the rest.}
{If Confluence: ⚠ Confluence scan may miss some @mention notifications due to API limitations.}
── SLACK ──────────────────────────────────────────
[1] #channel-name — @author — "snippet of message..." (2h ago)
https://...
── JIRA ───────────────────────────────────────────
[2] PROJ-123 — "Issue title" — @author commented: "snippet..." (1d ago)
https://...
── CONFLUENCE ─────────────────────────────────────
[3] "Page title" — @author commented: "snippet..." (3d ago)
https://...
── GITHUB ─────────────────────────────────────────
[4] owner/repo #123 — "PR title" — @author commented: "snippet..." (45m ago)
https://...
── ALSO ACTIVE (lower confidence) ─────────────────
[4] JIRA PROJ-456 — new comment, you commented here before (2d ago)
https://...
Enter item numbers to address (e.g. "1 3 5"), "all", or "none":
Wait for the human’s selection before proceeding. If the human says “none”, stop cleanly. If they say “all”, select every item.
For each selected item, draft a response. Use all available context:
Tone by platform:
post-on-slack skill tone guidance.Write all drafts to a temporary file at:
/tmp/triage-mentions-{YYYYMMDD-HHMMSS}.md
(Include seconds to avoid collisions if the skill is re-invoked within the same minute.)
Use this exact structure for each item in the file:
---
## Item {N} — {SOURCE}: {brief title}
**Source:** {SLACK | JIRA | CONFLUENCE}
**From:** @{author}
**Where:** {channel name / issue key / page title}
**Link:** {direct URL}
**Date:** {date}
<!-- Internal: channel_id={channel_id} thread_ts={thread_ts} (Slack only) -->
<!-- Internal: gh_repo={owner/repo} gh_number={number} gh_comment_id={id} (GitHub only) -->
### Context
> {full quoted message or comment, verbatim, max 500 chars — truncate with "…" if longer}
### Draft Response
{Reply content here — first person, direct. Do NOT open with "Posted by Farty Bobo" — dispatch handles attribution.}
### Decision
APPROVE
<!-- Replace APPROVE with one of: APPROVE | DISMISS | RETRY: <your notes> -->
<!-- Parser looks for the first line in this section that matches APPROVE, DISMISS, or RETRY: -->
Note: the Decision section defaults to APPROVE so the human only has to change items they want to dismiss or retry — not approve every single one.
After writing the file, tell the human:
Drafts written to: /tmp/triage-mentions-{timestamp}.md
Open the file, review each draft, and set the Decision for each item:
APPROVE — post as-is (default)
DISMISS — skip this item
RETRY: <your notes> — revise and re-present before posting
Save the file, then tell me "done" to dispatch.
Do NOT proceed until the human explicitly says they’re done annotating.
Re-read the triage file. For each item’s ### Decision section, find the first non-comment line that matches one of:
/^APPROVE$/i → dispatch as-is (Phase 6)/^DISMISS$/i → skip; log “dismissed” in session summary/^RETRY:/i → extract the notes after the colon; revise the draft using those notesIf no match is found (unannotated, blank, or unrecognized text), ask the human to clarify that specific item before proceeding with others.
RETRY handling:
After RETRY items are resolved, update the corresponding block in the triage file: replace the Draft Response with the final accepted version and change the Decision line to APPROVED (revised).
Tally: {A} approved, {D} dismissed, {R} retried
Secret scan first: Before dispatching any item, scan both the ### Context block AND the ### Draft Response block for secrets — API keys, tokens, passwords, internal hostnames, .env values, connection strings. If found in either block, surface the finding to the human and pause that item’s dispatch — do not redact silently.
For each APPROVE item, route to the correct skill:
| Source | Dispatch method |
|---|---|
| SLACK | Use slack_send_message with channel_id and thread_ts from the cached metadata (HTML comment in the file). Open the message with the identity disclosure line: _{your identity} on behalf of @{human_slack_handle}:_ then the draft body. |
| JIRA | Invoke /comment-jira with the ticket ID and draft body. The skill handles ADF formatting and identity footer. |
| CONFLUENCE | Invoke /comment-confluence with the page URL and draft body. The skill handles ADF formatting, identity footer, and space visibility check. |
| GITHUB | Use gh api repos/{owner}/{repo}/issues/{number}/comments -f body="{body}" to post a PR or issue comment. Open the body with the identity disclosure line: _Posted by {your identity} on behalf of @{gh_login}._ then the draft body. For PR review comments on specific lines, use gh api repos/{owner}/{repo}/pulls/{number}/comments with appropriate path, line, and commit_id fields. |
After dispatch, report outcomes:
Dispatch complete:
✓ [1] SLACK #channel — replied in thread
✓ [3] JIRA PROJ-456 — comment posted
✗ [2] CONFLUENCE "Page title" — FAILED: {error message}
— [4] DISMISSED
~ [5] RETRY → APPROVED (revised) — posted
For any failures, surface the full error and ask the human how to proceed.
/tmp file is local. Never upload it to Slack, Jira, Confluence, Pastebin, or anywhere else./comment-jira, /comment-confluence, or /post-on-slack manually.