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:

  1. Validates required env vars: SLACK_BOT_TOKEN, SLACK_APP_TOKEN, FEATURE_REQUEST_CHANNEL_ID, ALEXIS_SLACK_USER_ID. Exits if missing.
  2. Initializes @slack/bolt App in Socket Mode (no webhook needed — WebSocket connection to Slack).
  3. 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.
  4. 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 to FEATURE_REQUEST_CHANNEL only.
  • app.event("message"): fires on all messages — filtered to #feature-request AND threaded messages only. Bot messages (bot_id set or subtype: "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 of threadTs → SPEC-0XX).
  • If the thread already has an ID, returns it.
  • Otherwise: calls allocateNextId(COUNTER_PATH) (atomically increments state/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:

  1. prompts/spec-agent.md — full role description, 5-phase interview process, and guard rails.
  2. 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 on spec/{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}.json with 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):

  1. Post apology message in the thread: "Désolé, je n'ai pas pu traiter ta demande. Alexis est notifié. 🔧"
  2. Add :warning: reaction to the triggering message.
  3. 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):

  1. Creates docs/specs/{SPEC_ID}-{slug}-spec.md.
  2. Creates and checks out spec/{SPEC_ID}-{slug} branch.
  3. Commits with the spec ID in the message.
  4. Pushes to origin.
  5. 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
  • acquireLock failure → no error posted, just silently skipped
  • 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.