# Triage Community Message

> Scheduled community Slack feed for #community-mentions. Reviews recent messages in every community Slack channel the bot belongs to, filters for unanswered and relevant messages, drafts suggested human replies, links related GitHub issues, and posts one inline card per grouped item to the internal Slack channel.

- Skill: `warpdotdev/triage-community-message` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add warpdotdev/triage-community-message`
- Raw SKILL.md: https://api.skillmd.com/api/skills/warpdotdev/triage-community-message/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: warpdotdev (https://skillmd.com/u/warpdotdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/warpdotdev/triage-community-message

---


# Triage Community Message

You are a scheduled agent that keeps your team's internal `#community-mentions` channel updated with unanswered community Slack messages that likely need a maintainer reply.

## Defaults

- Schedule: every 30 minutes
- Source Slack token: `COMMUNITY_SLACK_TOKEN`
- Destination Slack token: `BUZZ_SLACK_TOKEN`
- Source channels: all community Slack channels the bot belongs to
- Destination channel: `COMMUNITY_MENTIONS_CHANNEL_ID`
- Lookback window: 90 minutes
- Minimum message age: 20 minutes
- Maximum cards per run: 20
- Maintainer detection: Slack users in the community workspace whose profile email ends in your maintainer domain (`$MAINTAINER_DOMAIN`)
- Deduplication: skip any grouped item whose canonical source permalink already appears in recent destination-channel history
- Answered-card marking: add a ✅ reaction to prior destination cards once their source thread has a maintainer reply (24-hour lookback)
- Ownership source: your repository's `CODEOWNERS` file (and any code-owner mapping you maintain)
- Answer preference source: `.agents/skills/triage-community-message/answer-preferences.md`

## Workflow

### 0. Choose an available Python 3

These scripts use only the Python standard library. Do not assume `.venv/bin/python` exists. From the repository root, choose an available Python 3 interpreter and reuse it for every command:

```bash
PYTHON="$(command -v python3 || command -v python)"
$PYTHON --version
```

If neither command exists, stop and report that Python 3 is unavailable.

### 1. Check token setup

Before running Slack API scripts, check whether `COMMUNITY_SLACK_TOKEN` and `BUZZ_SLACK_TOKEN` are present in the environment without printing their values. If either is missing, look for a local env setup file that can be sourced without printing secrets, such as `.env`, `.env.local`, `.envrc`, or a project-specific shell env file. Source it in the same shell invocation as the Slack command so tokens are available only to that process.

Do not echo, print, or write token values.

### 2. Collect recent source messages

Run from the repository root:

```bash
$PYTHON .agents/skills/triage-community-message/scripts/collect_messages.py --hours 1.5 --output /tmp/community_messages.json
```

This fetches top-level user messages from every source channel the community Slack bot belongs to, plus thread replies attached to those messages.

### 3. Filter unanswered messages

```bash
$PYTHON .agents/skills/triage-community-message/scripts/filter_unanswered.py /tmp/community_messages.json --output /tmp/community_unanswered.json
```

This removes messages posted by maintainers and messages whose threads already contain a maintainer reply.

### 4. Group bursts, apply grace period, and cap output

```bash
$PYTHON .agents/skills/triage-community-message/scripts/prepare_feed_items.py /tmp/community_unanswered.json --min-age-minutes 20 --limit 20 --output /tmp/community_feed_items.json
```

The script groups sequential top-level messages from the same author in the same channel when they are close enough to read as one thought. Each group has a canonical permalink: the first message's permalink, unless a thread makes another message the canonical reply location.

### 5. Deduplicate against `#community-mentions`

Use `COMMUNITY_MENTIONS_CHANNEL_ID` from the environment, then run:

```bash
$PYTHON .agents/skills/triage-community-message/scripts/filter_posted.py /tmp/community_feed_items.json --channel "$COMMUNITY_MENTIONS_CHANNEL_ID" --output /tmp/community_new_items.json
```

The script reads recent destination-channel history using `BUZZ_SLACK_TOKEN` and skips any item whose canonical source permalink is already present in a previous card.

### 6. Draft and post cards

For each item in `/tmp/community_new_items.json`:

1. Use a simple relevance gate. Post only messages that likely need a maintainer reply, such as product questions, bug reports, feature requests, confusion, complaints, or substantive discussion. Skip pure acknowledgments, social chatter, and items already resolved by context.
2. Identify likely topic.
3. Identify the best person to answer for internal routing. Use the local `your-org/your-repo-ownership` checkout as the primary source of truth for code ownership, using the STAKEHOLDERS files only as fallbacks when the local ownership checkout has no useful match. When a matched ownership area lists multiple owners, choose exactly one owner instead of listing every owner. Prefer a stable-random choice keyed by the item's `canonical_permalink` plus the matched ownership area name so repeated runs route the same community message to the same owner while distributing different messages across the owner set. Then check `.agents/skills/triage-community-message/answer-preferences.md` for additional preferences on interested parties; only include preference contacts when the preference explicitly applies to the item, and do not let preferences turn every multi-owner match into a broad owner list.
4. Search GitHub for relevant open issues and collect links to likely matches. Always store `github_issues` as a JSON array of link strings; use an empty array `[]` when there are no matches. Do not put the literal string `none found` in this field — the card script renders `none found` for an empty array.
5. Use `draft-brand-reply` for all voice, judgment, and topic-specific guidance when drafting the suggested reply. Do not add separate voice or tone rules here.
6. Write the enriched cards to `/tmp/community_cards.json` with a top-level `cards` array. Each card should include the original item fields plus `author_display_name`, `suggested_reply`, `github_issues` (a JSON array, empty when none), and `display_name` for the selected best person to answer. Preserve `canonical_permalink` exactly as provided so deduplication works on later runs.
7. Post one top-level Slack card to `#community-mentions` using `post_cards.py`: `$PYTHON .agents/skills/triage-community-message/scripts/post_cards.py /tmp/community_cards.json --channel "$COMMUNITY_MENTIONS_CHANNEL_ID"`.

Card structure:

1. Header: `*#<source-channel>* · *<author display name>* · <source permalink|time →>`
2. Body: blockquoted excerpt of the grouped message content
3. Suggested reply: `*Suggested reply* ✅` plus the draft in a copyable preformatted block
4. GitHub issues: relevant open issue links, or `none found`
5. Best person to answer: `*Best people to answer*` plus the selected owner's best available @-mention on Slack
   - To explicitly @-mention a person, call Slack `users.lookupByEmail` with `BUZZ_SLACK_TOKEN` and the person's work email (in your maintainer domain), then render the mention as `<@SLACK_USER_ID>`.
   - If lookup succeeds, set `display_name` to `<@SLACK_USER_ID>`.
   - If lookup fails or the email is missing, do not fabricate a mention. Fall back to the owner's display name, GitHub handle as inline code, or `unknown`.
6. Context: `Source: <source permalink>`

Post with username `Community mention • Buzz`, `unfurl_links: false`, and `unfurl_media: false`.

### 7. Overflow note

If `/tmp/community_new_items.json` contains an `overflow_count` greater than zero after deduplication, post one compact note to `#community-mentions` after the cards:

```text
+N more unanswered community messages were eligible this run but were capped. They may appear in a later run if still unanswered and inside the lookback window.
```

### 8. Mark answered cards

Maintainers often reply to the source message but forget to add a ✅ to the forwarded card. Every run, after posting new cards:

```bash
$PYTHON .agents/skills/triage-community-message/scripts/mark_answered.py --channel "$COMMUNITY_MENTIONS_CHANNEL_ID"
```

The script scans recent destination-channel cards, checks each card's source thread for a maintainer reply, and adds a `white_check_mark` reaction to answered cards. Cards that already have a ✅ are skipped. Run this step even when there are no new cards to post.

## Constraints

- Do not post replies into community channels.
- Do not post skip/no-op items to `#community-mentions`.
- Do not fabricate product behavior, policies, timelines, fixes, account decisions, issue links, codeowners, or best people to answer.
- Do not include a "why reply" field.
- Do not print, log, or save Slack token values.
- Keep each card suitable for a human maintainer to copy, edit, and send.

