| name | slack-messaging |
| description | Slack messaging patterns and principles for gh-pr-linear-issue-linker. Use when writing Slack notifications, formatting messages, or working with Block Kit layouts. |
Slack Messaging Principles
Core Principle: Thread Replies Should Not Repeat Context
CRITICAL RULE: Thread replies must only add new information. Never repeat data that's already in the parent message.
When posting to an existing thread, the parent message contains:
- PR title
- PR number
- PR author
- Repository name
- PR URL
- Original event context
Thread replies should be concise and focused on what changed.
Message Type Patterns
1. Parent Messages (New Threads)
Parent messages establish context and should include full details:
SlackNotification(
event_type=SlackNotificationEvent.FIND_MATCHES,
repo="owner/repo",
pr_number=123,
pr_title="Fix authentication bug",
pr_author="john-doe",
pr_url="https://github.com/owner/repo/pull/123",
result_summary="Matched 1 ticket: ENG-456",
is_thread_reply=False,
thread_ts=None,
)
Block Kit structure:
┌──────────────────────────────────────┐
│ 🔍 Gupri matched a ticket │ (header)
├──────────────────────────────────────┤
│ owner/repo | PR #123 | by @john-doe │ (context)
│ Fix authentication bug │
├──────────────────────────────────────┤
│ Matched 1 ticket: ENG-456 │ (result)
└──────────────────────────────────────┘
2. PR Lifecycle Thread Replies (Merged/Closed)
For PR state changes, use minimal format - just the action and actor:
SlackNotification(
event_type=SlackNotificationEvent.PR_MERGED,
actor="merger-username",
is_thread_reply=True,
thread_ts="1234567890.123456",
)
Block Kit structure:
┌──────────────────────────────────────┐
│ ✅ Merged by @sarah-smith │
└──────────────────────────────────────┘
Why this format?
- Parent already shows PR details
- Only new info: what happened and who did it
- Keeps thread clean and scannable
- Respects Slack threading UX
3. Action Thread Replies (Link/Unknown Intent)
For actions with context, include relevant details but omit redundant PR info:
SlackNotification(
event_type=SlackNotificationEvent.LINK,
result_summary="Linked ticket ENG-789",
ticket_identifier="ENG-789",
is_thread_reply=True,
thread_ts="1234567890.123456",
)
Block Kit structure:
┌──────────────────────────────────────┐
│ 🔗 Ticket linked to PR │
├──────────────────────────────────────┤
│ Linked ticket ENG-789 │
│ View ticket: [link] │
└──────────────────────────────────────┘
Event Type Configuration
All event types must be configured in three places:
1. Event Enum (src/slack/models.py)
class SlackNotificationEvent(str, Enum):
"""Slack notification event types."""
FIND_MATCHES = "find_matches"
LINK = "link"
UNKNOWN_INTENT = "unknown_intent"
PR_MERGED = "pr_merged"
PR_CLOSED = "pr_closed"
2. Emoji Mapping (src/services/slack_notifier.py)
_EMOJI_MAP: Final[dict[SlackNotificationEvent, str]] = {
SlackNotificationEvent.FIND_MATCHES: ":mag:",
SlackNotificationEvent.LINK: ":link:",
SlackNotificationEvent.UNKNOWN_INTENT: ":question:",
SlackNotificationEvent.PR_MERGED: ":white_check_mark:",
SlackNotificationEvent.PR_CLOSED: ":no_entry_sign:",
}
Emoji selection guidelines:
- Use emojis that clearly indicate the event type
- Prefer standard Slack emojis (work everywhere)
- Green (✅) for success, red (❌) for closure/failure
- Neutral colors for informational events
3. Header Mapping (src/services/slack_notifier.py)
_HEADER_MAP: Final[dict[SlackNotificationEvent, str]] = {
SlackNotificationEvent.FIND_MATCHES: "Gupri matched a ticket",
SlackNotificationEvent.LINK: "Ticket linked to PR",
SlackNotificationEvent.UNKNOWN_INTENT: "Clarification requested",
SlackNotificationEvent.PR_MERGED: "PR merged",
SlackNotificationEvent.PR_CLOSED: "PR closed",
}
User Mention Resolution
Always resolve GitHub usernames to Slack mentions when possible:
async def _resolve_author_mention(self, github_username: str) -> str:
"""Resolve GitHub username to Slack mention.
Returns:
"<@U123ABC>" if mapped, else "@github-username"
"""
try:
slack_user_id = await self._slack_user_mapper.get_slack_user_id(github_username)
if slack_user_id:
return f"<@{slack_user_id}>"
except Exception:
logger.exception("Failed to resolve Slack user")
return f"@{github_username}"
Benefits:
- Slack users get notified when mentioned
- Clickable user profiles in Slack
- Graceful fallback to GitHub username if no mapping exists
Actor vs Author
For PR lifecycle events, distinguish between:
- Author (
pr_author): Person who created the PR
- Actor (
actor): Person who performed the current action (merged/closed)
SlackNotification(
event_type=SlackNotificationEvent.PR_MERGED,
pr_author="john-doe",
actor="sarah-smith",
)
Threading Behavior
Thread replies use thread_ts to nest under parent messages:
notification = SlackNotification(
event_type=SlackNotificationEvent.FIND_MATCHES,
is_thread_reply=False,
thread_ts=None,
)
response = await slack_client.post_message(...)
thread_ts = response["ts"]
notification = SlackNotification(
event_type=SlackNotificationEvent.PR_MERGED,
is_thread_reply=True,
thread_ts=thread_ts,
)
Threading rules:
- Store
slack_thread_ts in Firestore PR state when PR opens
- All subsequent events reply to that thread
- Missing
slack_thread_ts for lifecycle events = log warning, skip notification
Error Handling
Critical Path (Blocking)
For initial PR events, missing data should raise errors:
if not pr_state or not pr_state.slack_thread_ts:
msg = f"slack_thread_ts missing for {repo}#{pr_number} on link command"
raise RuntimeError(msg)
Best Effort (Non-Blocking)
For PR lifecycle events, degrade gracefully:
pr_state = await firestore_client.get_pr_state(repo, pr_number)
if not pr_state or not pr_state.slack_thread_ts:
logger.warning(
"No slack_thread_ts for closed PR %s#%d - skipping notification",
repo,
pr_number,
)
return
Rationale: A closed PR with no thread is not actionable. Better to skip than crash.
Block Kit Formatting
Slack mrkdwn Syntax (CRITICAL)
Slack uses mrkdwn, NOT standard Markdown. The syntax is different:
| Format | Slack mrkdwn | Standard Markdown | ❌ Wrong |
|---|
| Bold | *bold* | **bold** | **bold** in Slack shows as-is |
| Italic | _italic_ | *italic* | *italic* becomes bold |
| Code | `code` | `code` | ✅ Same |
| Code block | code | code | ✅ Same |
| Link | <url|text> | [text](url) | [text](url) shows as-is |
Common mistake: AI agents often generate **bold** because they default to Markdown. Always use *bold* for Slack.
Example of correct mrkdwn:
I found *1 finding* with high severity.
View the <https://github.com/org/repo/pull/123|pull request> for details.
What the user sees if you use wrong syntax:
I found **1 finding** with high severity. ← Shows literal asterisks
Section Block with Markdown
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"✅ Merged by {actor_mention}",
},
}
Header Block
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"🔍 {header}",
"emoji": True,
},
}
Link Formatting
pr_link = f"<{notification.pr_url}|PR #{notification.pr_number}>"
ticket_link = f"<{ticket_url}|{ticket_identifier}>"
Testing Patterns
Always test:
- Emoji mapping exists for event type
- Header mapping exists for event type
- Block structure matches expected format
- User mention resolution (Slack user ID vs GitHub username fallback)
- Thread reply vs parent message logic
@pytest.mark.asyncio
async def test_pr_merged_thread_reply():
"""Verify merged PR sends short thread reply."""
notification = SlackNotification(
event_type=SlackNotificationEvent.PR_MERGED,
actor="merger-user",
is_thread_reply=True,
thread_ts="1234.567",
)
await notifier.notify(notification)
blocks = mock_slack_client.post_message.call_args.kwargs["blocks"]
assert len(blocks) == 1
assert "Merged by @merger-user" in blocks[0]["text"]["text"]
assert "PR #" not in blocks[0]["text"]["text"]
Quality Checklist
Before adding new Slack notifications:
Anti-Patterns to Avoid
❌ Repeating PR details in thread replies:
"✅ PR merged\nPR #123: Fix auth bug\nby @author\nView PR: [link]"
✅ Concise thread reply with only new info:
"✅ Merged by @merger"
❌ Inconsistent event type naming:
SlackNotificationEvent.MERGED = "pr_merged"
SlackNotificationEvent.TICKET_LINK = "link"
✅ Consistent enum naming:
SlackNotificationEvent.PR_MERGED = "pr_merged"
SlackNotificationEvent.PR_CLOSED = "pr_closed"
SlackNotificationEvent.LINK = "link"
❌ Hard-coded emoji strings in message builders:
return f"✅ Merged by {user}"
✅ Centralized emoji configuration:
emoji = _EMOJI_MAP.get(event_type)
return f"{emoji} Merged by {user}"
❌ Using standard Markdown syntax in Slack messages:
text = "I found **1 finding** with high severity."
✅ Using Slack mrkdwn syntax:
text = "I found *1 finding* with high severity."
❌ Using Markdown link syntax in Slack:
text = "[View PR](https://github.com/org/repo/pull/123)"
✅ Using Slack link syntax:
text = "<https://github.com/org/repo/pull/123|View PR>"