Slack Spec Agent Bot
Summary
The Slack spec agent (workers/slack-bot/) is a Socket Mode Bolt app that guides the co-founder through functional specification sessions in the #feature-request channel. Every @mention or thread reply triggers a Claude Code subprocess (via spawn("claude", [...])) that reads the full thread history, continues the guided spec interview, and posts the response back to Slack. Specs get a persistent SPEC-0XX ID per thread. When a spec is validated by the user, the bot writes the spec file to docs/specs/, commits it on a dedicated branch, pushes, and mentions the CTO on Slack with a GitHub link.
State Diagram
stateDiagram-v2 state "En écoute" as Listening state "Fil verrouillé" as ThreadLocked state "Message bot ignoré" as SkippedBotLoop state "Mention autre bot ignorée" as SkippedOtherMention state "Claude lancé" as ClaudeSpawned state "Délai dépassé" as Timeout state "Réponse générée" as Success state "Erreur Claude" as Failure state "Verrou libéré" as Released state "Erreur publiée" as ErrorPosted [*] --> Listening: Socket Mode connected, bot user ID resolved Listening --> ThreadLocked: app_mention or message event in #feature-request ThreadLocked --> SkippedBotLoop: event is from a bot (bot_id present) ThreadLocked --> SkippedOtherMention: text starts with @someone-else mention ThreadLocked --> ClaudeSpawned: acquireLock(threadTs), spawnClaude(prompt, mcpConfig) ClaudeSpawned --> Timeout: 5 min elapsed → kill process ClaudeSpawned --> Success: exit code 0 + stdout not empty ClaudeSpawned --> Failure: non-zero exit or empty stdout Success --> Released: releaseLock(threadTs) Failure --> ErrorPosted: post apology in thread + DM Alexis Timeout --> ErrorPosted Released --> [*] ErrorPosted --> [*]
Steps
1. Bot Startup (Actor: system)
workers/slack-bot/src/index.ts:
- Validates required env vars:
SLACK_BOT_TOKEN,SLACK_APP_TOKEN,FEATURE_REQUEST_CHANNEL_ID,ALEXIS_SLACK_USER_ID. Exits if missing. - Initializes
@slack/boltApp in Socket Mode (no webhook needed — WebSocket connection to Slack). - Resolves bot’s own user ID via
app.client.auth.test()before accepting events — prevents race window where bot might respond to its own messages. - Starts listening.
Environment: SLACK_BOT_TOKEN, SLACK_APP_TOKEN, FEATURE_REQUEST_CHANNEL_ID, ALEXIS_SLACK_USER_ID
2. Event Filtering (Actor: system)
Two event listeners are registered:
app.event("app_mention"): fires when bot is @mentioned anywhere — filtered toFEATURE_REQUEST_CHANNELonly.app.event("message"): fires on all messages — filtered to#feature-requestAND threaded messages only. Bot messages (bot_idset orsubtype: "bot_message") are ignored to prevent loops.
Both handlers skip messages that start with a <@U...> mention to a different user than the bot.
threadTs is event.thread_ts || event.ts — for a root message, the thread ID is the message’s own timestamp.
An acknowledgement (:brain: emoji reaction) is added immediately before dispatching to Claude.
3. Spec ID Allocation (Actor: system)
getOrAllocateSpecId(threadTs):
- Reads
state/thread-spec-ids.json(a map ofthreadTs → SPEC-0XX). - If the thread already has an ID, returns it.
- Otherwise: calls
allocateNextId(COUNTER_PATH)(atomically incrementsstate/spec-counter.json), assigns the new ID to the thread, writes the map atomically.
Each thread gets exactly one spec ID for its entire lifetime.
4. Per-Thread Mutex (Actor: system)
acquireLock(threadTs) from lib/lock.ts — file-based or in-memory lock. Returns false if the thread is already being processed. Guards against concurrent Claude invocations on the same thread (e.g. rapid successive messages).
5. Prompt Construction (Actor: system)
The system prompt is built from:
prompts/spec-agent.md— full role description, 5-phase interview process, and guard rails.- Injected session variables: spec ID, Alexis’s Slack user ID, channel ID, thread timestamp.
The prompt instructs Claude to:
- Read the thread history via MCP
slack_get_thread_replies. - Continue the spec interview based on where the conversation left off.
- Post the next response via
slack_reply_to_thread. - On spec validation: write to
docs/specs/{SPEC_ID}-{slug}-spec.md, commit onspec/{SPEC_ID}-{slug}branch, push, and mention Alexis.
6. Claude Subprocess (Actor: system, tools: claude-api)
spawnClaude(prompt, mcpConfigPath):
- Writes a temporary
mcp-config-{timestamp}.jsonwith the Slack MCP server configuration. - Spawns
claude --print --system-prompt {prompt} --mcp-config {mcpConfig} --allowedTools [...] --permission-mode bypassPermissions. - Allowed tools:
mcp__slack-sealion__slack_get_thread_replies,slack_reply_to_thread,slack_get_channel_history,slack_post_message,Bash(git:*),Write,Read,Glob. - Timeout: 5 minutes. Process is killed and failure path is taken.
- Success: exit code 0 AND stdout non-empty.
The temporary MCP config file is deleted in finally regardless of outcome.
7. Error Handling (Actor: system)
On failure (timeout or non-zero exit):
- Post apology message in the thread:
"Désolé, je n'ai pas pu traiter ta demande. Alexis est notifié. 🔧" - Add
:warning:reaction to the triggering message. - DM Alexis with thread link and error details.
8. Spec Delivery (Phase 5, Actor: system)
When the co-founder validates the spec, Claude (inside the subprocess):
- Creates
docs/specs/{SPEC_ID}-{slug}-spec.md. - Creates and checks out
spec/{SPEC_ID}-{slug}branch. - Commits with the spec ID in the message.
- Pushes to origin.
- Posts in the thread: spec summary, GitHub link, and
@{alexisUserId}mention.
Error States
- Missing env vars → process exits on startup
- Thread already locked → silently skipped (returns without processing)
- Bot message detected → silently ignored (loop prevention)
- Claude subprocess timeout (5 min) → error posted to thread, Alexis DMed
- Claude subprocess non-zero exit → error posted to thread, Alexis DMed
acquireLockfailure → no error posted, just silently skipped
Related Processes
- No upstream API processes required. Runs as an independent worker.
- Spec files committed by this bot appear as untracked files in
docs/specs/until reviewed.