@desplega.ai/agent-swarm 1.2.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/.env.docker.example +22 -1
  3. package/.env.example +17 -0
  4. package/.github/workflows/docker-publish.yml +92 -0
  5. package/CONTRIBUTING.md +270 -0
  6. package/DEPLOYMENT.md +391 -0
  7. package/Dockerfile.worker +29 -1
  8. package/FAQ.md +19 -0
  9. package/LICENSE +21 -0
  10. package/MCP.md +249 -0
  11. package/README.md +103 -207
  12. package/assets/agent-swarm-logo-orange.png +0 -0
  13. package/assets/agent-swarm-logo.png +0 -0
  14. package/docker-compose.example.yml +137 -0
  15. package/docker-entrypoint.sh +223 -7
  16. package/package.json +8 -3
  17. package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
  18. package/plugin/README.md +1 -0
  19. package/plugin/agents/.gitkeep +0 -0
  20. package/plugin/agents/codebase-analyzer.md +143 -0
  21. package/plugin/agents/codebase-locator.md +122 -0
  22. package/plugin/agents/codebase-pattern-finder.md +227 -0
  23. package/plugin/agents/web-search-researcher.md +109 -0
  24. package/plugin/commands/create-plan.md +415 -0
  25. package/plugin/commands/implement-plan.md +89 -0
  26. package/plugin/commands/research.md +200 -0
  27. package/plugin/commands/start-leader.md +101 -0
  28. package/plugin/commands/start-worker.md +56 -0
  29. package/plugin/commands/swarm-chat.md +78 -0
  30. package/plugin/commands/todos.md +66 -0
  31. package/plugin/commands/work-on-task.md +44 -0
  32. package/plugin/skills/.gitkeep +0 -0
  33. package/scripts/generate-mcp-docs.ts +415 -0
  34. package/slack-manifest.json +69 -0
  35. package/src/be/db.ts +1431 -25
  36. package/src/cli.tsx +135 -11
  37. package/src/commands/lead.ts +13 -0
  38. package/src/commands/runner.ts +255 -0
  39. package/src/commands/worker.ts +8 -220
  40. package/src/hooks/hook.ts +102 -14
  41. package/src/http.ts +361 -5
  42. package/src/prompts/base-prompt.ts +131 -0
  43. package/src/server.ts +56 -0
  44. package/src/slack/app.ts +73 -0
  45. package/src/slack/commands.ts +88 -0
  46. package/src/slack/handlers.ts +281 -0
  47. package/src/slack/index.ts +3 -0
  48. package/src/slack/responses.ts +175 -0
  49. package/src/slack/router.ts +170 -0
  50. package/src/slack/types.ts +20 -0
  51. package/src/slack/watcher.ts +119 -0
  52. package/src/tools/create-channel.ts +80 -0
  53. package/src/tools/get-tasks.ts +54 -21
  54. package/src/tools/join-swarm.ts +28 -4
  55. package/src/tools/list-channels.ts +37 -0
  56. package/src/tools/list-services.ts +110 -0
  57. package/src/tools/poll-task.ts +46 -3
  58. package/src/tools/post-message.ts +87 -0
  59. package/src/tools/read-messages.ts +192 -0
  60. package/src/tools/register-service.ts +118 -0
  61. package/src/tools/send-task.ts +80 -7
  62. package/src/tools/store-progress.ts +9 -3
  63. package/src/tools/task-action.ts +211 -0
  64. package/src/tools/unregister-service.ts +110 -0
  65. package/src/tools/update-profile.ts +105 -0
  66. package/src/tools/update-service-status.ts +118 -0
  67. package/src/types.ts +110 -3
  68. package/src/utils/pretty-print.ts +224 -0
  69. package/thoughts/shared/plans/.gitkeep +0 -0
  70. package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
  71. package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
  72. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
  73. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
  74. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
  75. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
  76. package/thoughts/shared/research/.gitkeep +0 -0
  77. package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
  78. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
  79. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
  80. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
  81. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
  82. package/tsconfig.json +3 -1
  83. package/ui/bun.lock +692 -0
  84. package/ui/index.html +22 -0
  85. package/ui/package.json +32 -0
  86. package/ui/pnpm-lock.yaml +3034 -0
  87. package/ui/postcss.config.js +6 -0
  88. package/ui/public/logo.png +0 -0
  89. package/ui/src/App.tsx +43 -0
  90. package/ui/src/components/ActivityFeed.tsx +415 -0
  91. package/ui/src/components/AgentDetailPanel.tsx +534 -0
  92. package/ui/src/components/AgentsPanel.tsx +549 -0
  93. package/ui/src/components/ChatPanel.tsx +1820 -0
  94. package/ui/src/components/ConfigModal.tsx +232 -0
  95. package/ui/src/components/Dashboard.tsx +534 -0
  96. package/ui/src/components/Header.tsx +168 -0
  97. package/ui/src/components/ServicesPanel.tsx +612 -0
  98. package/ui/src/components/StatsBar.tsx +288 -0
  99. package/ui/src/components/StatusBadge.tsx +124 -0
  100. package/ui/src/components/TaskDetailPanel.tsx +807 -0
  101. package/ui/src/components/TasksPanel.tsx +575 -0
  102. package/ui/src/hooks/queries.ts +170 -0
  103. package/ui/src/index.css +235 -0
  104. package/ui/src/lib/api.ts +161 -0
  105. package/ui/src/lib/config.ts +35 -0
  106. package/ui/src/lib/theme.ts +214 -0
  107. package/ui/src/lib/utils.ts +48 -0
  108. package/ui/src/main.tsx +32 -0
  109. package/ui/src/types/api.ts +164 -0
  110. package/ui/src/vite-env.d.ts +1 -0
  111. package/ui/tailwind.config.js +35 -0
  112. package/ui/tsconfig.json +31 -0
  113. package/ui/vite.config.ts +22 -0
  114. package/cc-plugin/README.md +0 -49
  115. package/cc-plugin/commands/setup-leader.md +0 -73
  116. package/cc-plugin/commands/start-worker.md +0 -64
  117. package/docker-compose.worker.yml +0 -35
  118. package/example-req-meta.json +0 -24
  119. /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
@@ -0,0 +1,1195 @@
1
+ # Slack Multi-Agent Bot Integration Implementation Plan
2
+
3
+ ## Overview
4
+
5
+ Integrate Slack as a communication interface for the Agent Swarm MCP, allowing users to interact with agents via Slack messages. Agents will respond with custom personas, and tasks can be created directly from Slack conversations.
6
+
7
+ ## Current State Analysis
8
+
9
+ The Agent Swarm MCP currently provides:
10
+ - HTTP server with REST API and MCP transport (`src/http.ts`)
11
+ - SQLite database with `agents`, `agent_tasks`, and `agent_log` tables (`src/be/db.ts`)
12
+ - 8 MCP tools for agent coordination (`src/tools/*.ts`)
13
+ - CLI runners for lead/worker agents (`src/commands/*.ts`)
14
+ - React dashboard UI (`ui/src/`)
15
+
16
+ **What's Missing:**
17
+ - No Slack integration code exists
18
+ - No task source tracking (can't distinguish MCP vs Slack-created tasks)
19
+ - No mechanism for external systems to create tasks directly
20
+
21
+ ### Key Discoveries:
22
+ - Socket Mode enabled in `slack-manifest.json:61` - no webhook endpoints needed
23
+ - Task creation flows through `send-task` tool only (`src/tools/send-task.ts`)
24
+ - Database uses `CREATE TABLE IF NOT EXISTS` for idempotent schema updates (`src/be/db.ts:23-65`)
25
+ - Zod v4 used for schema validation (`src/types.ts`)
26
+
27
+ ## Desired End State
28
+
29
+ After implementation:
30
+ 1. Slack bot connects via Socket Mode on server startup
31
+ 2. Users can mention agents by name in Slack messages to create tasks
32
+ 3. Agents respond in Slack with custom personas matching their swarm identity
33
+ 4. Task progress/completion updates appear in the original Slack thread
34
+ 5. `/agent-swarm-status` slash command shows current swarm state
35
+
36
+ ### Verification:
37
+ - Bot appears online in Slack workspace
38
+ - Mentioning an agent name creates a task visible in the dashboard
39
+ - Agent completion messages appear in Slack with correct persona
40
+ - Slash command returns agent list with statuses
41
+
42
+ ## What We're NOT Doing
43
+
44
+ - **Multi-workspace OAuth flow** - Single workspace only (env vars for tokens)
45
+ - **Interactive components** - No buttons, modals, or block kit interactions
46
+ - **App Home tab** - Disabled in manifest, not implementing
47
+ - **Message threading for sub-tasks** - Each mention = one task, no hierarchy
48
+ - **Slack-side task management** - No editing/canceling tasks from Slack
49
+ - **Rate limiting** - Rely on Slack's built-in rate limits
50
+ - **Message history sync** - Only process new messages, not historical
51
+
52
+ ## Implementation Approach
53
+
54
+ Use Slack's Bolt SDK with Socket Mode for real-time event delivery. This avoids exposing public endpoints and simplifies deployment. The integration will:
55
+
56
+ 1. Run alongside the existing HTTP server (not replace it)
57
+ 2. Create tasks directly in the database (bypassing MCP tools)
58
+ 3. Poll for task completion to send Slack responses
59
+ 4. Use the agent's name as the Slack display name via `chat:write.customize`
60
+
61
+ ## Phase 1: Database Schema Updates
62
+
63
+ ### Overview
64
+ Add task source tracking and prepare schema for Slack token storage.
65
+
66
+ ### Changes Required:
67
+
68
+ #### 1. Types (`src/types.ts`)
69
+
70
+ Add task source enum and extend task schema:
71
+
72
+ ```typescript
73
+ // After line 3 (AgentTaskStatusSchema)
74
+ export const AgentTaskSourceSchema = z.enum(["mcp", "slack", "api"]);
75
+ export type AgentTaskSource = z.infer<typeof AgentTaskSourceSchema>;
76
+ ```
77
+
78
+ Update `AgentTaskSchema` to include source:
79
+
80
+ ```typescript
81
+ // Modify lines 5-19
82
+ export const AgentTaskSchema = z.object({
83
+ id: z.uuid(),
84
+ agentId: z.uuid(),
85
+ task: z.string().min(1),
86
+ status: AgentTaskStatusSchema,
87
+ source: AgentTaskSourceSchema.default("mcp"), // NEW
88
+
89
+ createdAt: z.iso.datetime().default(() => new Date().toISOString()),
90
+ lastUpdatedAt: z.iso.datetime().default(() => new Date().toISOString()),
91
+
92
+ finishedAt: z.iso.datetime().optional(),
93
+
94
+ failureReason: z.string().optional(),
95
+ output: z.string().optional(),
96
+ progress: z.string().optional(),
97
+
98
+ // Slack-specific metadata (optional)
99
+ slackChannelId: z.string().optional(),
100
+ slackThreadTs: z.string().optional(),
101
+ slackUserId: z.string().optional(),
102
+ });
103
+ ```
104
+
105
+ #### 2. Database Schema (`src/be/db.ts`)
106
+
107
+ Add `source` column and Slack metadata to `agent_tasks` table. Update the CREATE TABLE statement:
108
+
109
+ ```sql
110
+ -- Modify lines 33-45
111
+ CREATE TABLE IF NOT EXISTS agent_tasks (
112
+ id TEXT PRIMARY KEY,
113
+ agentId TEXT NOT NULL,
114
+ task TEXT NOT NULL,
115
+ status TEXT NOT NULL CHECK(status IN ('pending', 'in_progress', 'completed', 'failed')),
116
+ source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api')),
117
+ slackChannelId TEXT,
118
+ slackThreadTs TEXT,
119
+ slackUserId TEXT,
120
+ createdAt TEXT NOT NULL,
121
+ lastUpdatedAt TEXT NOT NULL,
122
+ finishedAt TEXT,
123
+ failureReason TEXT,
124
+ output TEXT,
125
+ progress TEXT,
126
+ FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE
127
+ );
128
+ ```
129
+
130
+ Add migration for existing databases (add after table creation, before indexes):
131
+
132
+ ```sql
133
+ -- Add column if it doesn't exist (SQLite doesn't support IF NOT EXISTS for columns)
134
+ -- We'll handle this in code with a try/catch
135
+ ```
136
+
137
+ #### 3. Database Functions (`src/be/db.ts`)
138
+
139
+ Update `AgentTaskRow` type:
140
+
141
+ ```typescript
142
+ // Modify lines 178-189
143
+ type AgentTaskRow = {
144
+ id: string;
145
+ agentId: string;
146
+ task: string;
147
+ status: AgentTaskStatus;
148
+ source: AgentTaskSource;
149
+ slackChannelId: string | null;
150
+ slackThreadTs: string | null;
151
+ slackUserId: string | null;
152
+ createdAt: string;
153
+ lastUpdatedAt: string;
154
+ finishedAt: string | null;
155
+ failureReason: string | null;
156
+ output: string | null;
157
+ progress: string | null;
158
+ };
159
+ ```
160
+
161
+ Update `rowToAgentTask` converter:
162
+
163
+ ```typescript
164
+ // Modify lines 191-204
165
+ function rowToAgentTask(row: AgentTaskRow): AgentTask {
166
+ return {
167
+ id: row.id,
168
+ agentId: row.agentId,
169
+ task: row.task,
170
+ status: row.status,
171
+ source: row.source,
172
+ slackChannelId: row.slackChannelId ?? undefined,
173
+ slackThreadTs: row.slackThreadTs ?? undefined,
174
+ slackUserId: row.slackUserId ?? undefined,
175
+ createdAt: row.createdAt,
176
+ lastUpdatedAt: row.lastUpdatedAt,
177
+ finishedAt: row.finishedAt ?? undefined,
178
+ failureReason: row.failureReason ?? undefined,
179
+ output: row.output ?? undefined,
180
+ progress: row.progress ?? undefined,
181
+ };
182
+ }
183
+ ```
184
+
185
+ Update `createTask` function signature and query:
186
+
187
+ ```typescript
188
+ // Modify lines 249-257
189
+ export function createTask(
190
+ agentId: string,
191
+ task: string,
192
+ options?: {
193
+ source?: AgentTaskSource;
194
+ slackChannelId?: string;
195
+ slackThreadTs?: string;
196
+ slackUserId?: string;
197
+ }
198
+ ): AgentTask {
199
+ const id = crypto.randomUUID();
200
+ const source = options?.source ?? "mcp";
201
+ const row = getDb()
202
+ .prepare<AgentTaskRow, [string, string, string, AgentTaskStatus, AgentTaskSource, string | null, string | null, string | null]>(
203
+ `INSERT INTO agent_tasks (id, agentId, task, status, source, slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt)
204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`
205
+ )
206
+ .get(id, agentId, task, "pending", source, options?.slackChannelId ?? null, options?.slackThreadTs ?? null, options?.slackUserId ?? null);
207
+ if (!row) throw new Error("Failed to create task");
208
+ try {
209
+ createLogEntry({ eventType: "task_created", agentId, taskId: id, newValue: "pending", metadata: { source } });
210
+ } catch { }
211
+ return rowToAgentTask(row);
212
+ }
213
+ ```
214
+
215
+ Add function to get Slack tasks awaiting response:
216
+
217
+ ```typescript
218
+ // Add after getAllTasks function (around line 335)
219
+ export function getCompletedSlackTasks(): AgentTask[] {
220
+ return getDb()
221
+ .prepare<AgentTaskRow, []>(
222
+ `SELECT * FROM agent_tasks
223
+ WHERE source = 'slack'
224
+ AND slackChannelId IS NOT NULL
225
+ AND status IN ('completed', 'failed')
226
+ ORDER BY lastUpdatedAt DESC`
227
+ )
228
+ .all()
229
+ .map(rowToAgentTask);
230
+ }
231
+ ```
232
+
233
+ ### Success Criteria:
234
+
235
+ #### Automated Verification:
236
+ - [x] TypeScript compiles: `bun run tsc:check`
237
+ - [x] Server starts without errors: `bun run dev:http`
238
+ - [x] Existing tasks still work (backward compatible)
239
+
240
+ #### Manual Verification:
241
+ - [x] New task created via MCP has `source: "mcp"`
242
+ - [x] Database schema updated correctly (check with sqlite3 CLI)
243
+
244
+ **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human that the database changes work correctly before proceeding to the next phase.
245
+
246
+ ---
247
+
248
+ ## Phase 2: Slack Dependencies and Configuration
249
+
250
+ ### Overview
251
+ Add Slack Bolt SDK and configure environment variables.
252
+
253
+ ### Changes Required:
254
+
255
+ #### 1. Install Dependencies
256
+
257
+ ```bash
258
+ bun add @slack/bolt
259
+ ```
260
+
261
+ #### 2. Environment Variables
262
+
263
+ Create `.env.example` update (document required vars):
264
+
265
+ ```bash
266
+ # Slack Bot Configuration (Socket Mode)
267
+ SLACK_BOT_TOKEN=xoxb-... # Bot User OAuth Token
268
+ SLACK_APP_TOKEN=xapp-... # App-Level Token (for Socket Mode)
269
+ SLACK_SIGNING_SECRET=... # Signing Secret (optional for Socket Mode)
270
+ ```
271
+
272
+ #### 3. Slack Types (`src/slack/types.ts`)
273
+
274
+ **File**: `src/slack/types.ts` (NEW)
275
+
276
+ ```typescript
277
+ import type { Agent } from "../types";
278
+
279
+ export interface SlackMessageContext {
280
+ channelId: string;
281
+ threadTs?: string;
282
+ userId: string;
283
+ text: string;
284
+ botUserId: string;
285
+ }
286
+
287
+ export interface AgentMatch {
288
+ agent: Agent;
289
+ matchedText: string;
290
+ }
291
+
292
+ export interface SlackConfig {
293
+ botToken: string;
294
+ appToken: string;
295
+ signingSecret?: string;
296
+ }
297
+ ```
298
+
299
+ ### Success Criteria:
300
+
301
+ #### Automated Verification:
302
+ - [x] Dependencies install: `bun install`
303
+ - [x] TypeScript compiles: `bun run tsc:check`
304
+ - [x] No runtime errors on import
305
+
306
+ #### Manual Verification:
307
+ - [x] `.env` file has required Slack tokens configured
308
+ - [ ] Tokens are valid (can be verified in Phase 3)
309
+
310
+ **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation before proceeding to the next phase.
311
+
312
+ ---
313
+
314
+ ## Phase 3: Slack Bot Core Implementation
315
+
316
+ ### Overview
317
+ Initialize Bolt app with Socket Mode and implement basic message handling.
318
+
319
+ ### Changes Required:
320
+
321
+ #### 1. Slack App Initialization (`src/slack/app.ts`)
322
+
323
+ **File**: `src/slack/app.ts` (NEW)
324
+
325
+ ```typescript
326
+ import { App, LogLevel } from "@slack/bolt";
327
+
328
+ let app: App | null = null;
329
+
330
+ export function getSlackApp(): App | null {
331
+ return app;
332
+ }
333
+
334
+ export async function initSlackApp(): Promise<App | null> {
335
+ const botToken = process.env.SLACK_BOT_TOKEN;
336
+ const appToken = process.env.SLACK_APP_TOKEN;
337
+
338
+ if (!botToken || !appToken) {
339
+ console.log("[Slack] Missing SLACK_BOT_TOKEN or SLACK_APP_TOKEN, Slack integration disabled");
340
+ return null;
341
+ }
342
+
343
+ app = new App({
344
+ token: botToken,
345
+ appToken: appToken,
346
+ socketMode: true,
347
+ logLevel: process.env.NODE_ENV === "development" ? LogLevel.DEBUG : LogLevel.INFO,
348
+ });
349
+
350
+ // Register handlers
351
+ const { registerMessageHandler } = await import("./handlers");
352
+ const { registerCommandHandler } = await import("./commands");
353
+
354
+ registerMessageHandler(app);
355
+ registerCommandHandler(app);
356
+
357
+ return app;
358
+ }
359
+
360
+ export async function startSlackApp(): Promise<void> {
361
+ if (!app) {
362
+ await initSlackApp();
363
+ }
364
+
365
+ if (app) {
366
+ await app.start();
367
+ console.log("[Slack] Bot connected via Socket Mode");
368
+ }
369
+ }
370
+
371
+ export async function stopSlackApp(): Promise<void> {
372
+ if (app) {
373
+ await app.stop();
374
+ app = null;
375
+ console.log("[Slack] Bot disconnected");
376
+ }
377
+ }
378
+ ```
379
+
380
+ #### 2. Agent Router (`src/slack/router.ts`)
381
+
382
+ **File**: `src/slack/router.ts` (NEW)
383
+
384
+ ```typescript
385
+ import type { Agent } from "../types";
386
+ import { getAllAgents, getAgentById } from "../be/db";
387
+ import type { AgentMatch } from "./types";
388
+
389
+ /**
390
+ * Routes a Slack message to the appropriate agent(s) based on mentions.
391
+ *
392
+ * Routing rules:
393
+ * - `swarm#<uuid>` → exact agent by ID
394
+ * - `swarm#all` → all non-lead agents
395
+ * - Partial name match (words >3 chars) → agent by name
396
+ * - Bot @mention only → lead agent
397
+ */
398
+ export function routeMessage(
399
+ text: string,
400
+ botUserId: string,
401
+ botMentioned: boolean
402
+ ): AgentMatch[] {
403
+ const matches: AgentMatch[] = [];
404
+ const agents = getAllAgents().filter(a => a.status !== "offline");
405
+
406
+ // Check for explicit swarm#<id> syntax
407
+ const idMatches = text.matchAll(/swarm#([a-f0-9-]{36})/gi);
408
+ for (const match of idMatches) {
409
+ const agent = getAgentById(match[1]);
410
+ if (agent && agent.status !== "offline") {
411
+ matches.push({ agent, matchedText: match[0] });
412
+ }
413
+ }
414
+
415
+ // Check for swarm#all broadcast
416
+ if (/swarm#all/i.test(text)) {
417
+ const nonLeadAgents = agents.filter(a => !a.isLead);
418
+ for (const agent of nonLeadAgents) {
419
+ if (!matches.some(m => m.agent.id === agent.id)) {
420
+ matches.push({ agent, matchedText: "swarm#all" });
421
+ }
422
+ }
423
+ }
424
+
425
+ // Check for partial name matches (words > 3 chars)
426
+ if (matches.length === 0) {
427
+ for (const agent of agents) {
428
+ const nameWords = agent.name.split(/\s+/).filter(w => w.length > 3);
429
+ for (const word of nameWords) {
430
+ const regex = new RegExp(`\\b${escapeRegex(word)}\\b`, "i");
431
+ if (regex.test(text)) {
432
+ if (!matches.some(m => m.agent.id === agent.id)) {
433
+ matches.push({ agent, matchedText: word });
434
+ }
435
+ break;
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ // If only bot was mentioned and no agents matched, route to lead
442
+ if (matches.length === 0 && botMentioned) {
443
+ const lead = agents.find(a => a.isLead);
444
+ if (lead) {
445
+ matches.push({ agent: lead, matchedText: "@bot" });
446
+ }
447
+ }
448
+
449
+ return matches;
450
+ }
451
+
452
+ function escapeRegex(str: string): string {
453
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
454
+ }
455
+
456
+ /**
457
+ * Extracts the task description from a message, removing bot mentions and agent references.
458
+ */
459
+ export function extractTaskFromMessage(text: string, botUserId: string): string {
460
+ return text
461
+ .replace(new RegExp(`<@${botUserId}>`, "g"), "") // Remove bot mentions
462
+ .replace(/swarm#[a-f0-9-]{36}/gi, "") // Remove swarm#<id>
463
+ .replace(/swarm#all/gi, "") // Remove swarm#all
464
+ .trim();
465
+ }
466
+ ```
467
+
468
+ #### 3. Message Handlers (`src/slack/handlers.ts`)
469
+
470
+ **File**: `src/slack/handlers.ts` (NEW)
471
+
472
+ ```typescript
473
+ import type { App, GenericMessageEvent } from "@slack/bolt";
474
+ import { createTask, getAgentById } from "../be/db";
475
+ import { routeMessage, extractTaskFromMessage } from "./router";
476
+
477
+ export function registerMessageHandler(app: App): void {
478
+ // Handle all message events
479
+ app.event("message", async ({ event, client, say }) => {
480
+ // Ignore bot messages and message_changed events
481
+ if (event.subtype === "bot_message" || event.subtype === "message_changed") {
482
+ return;
483
+ }
484
+
485
+ const msg = event as GenericMessageEvent;
486
+ if (!msg.text || !msg.user) return;
487
+
488
+ // Get bot's user ID
489
+ const authResult = await client.auth.test();
490
+ const botUserId = authResult.user_id as string;
491
+
492
+ // Check if bot was mentioned
493
+ const botMentioned = msg.text.includes(`<@${botUserId}>`);
494
+
495
+ // Route message to agents
496
+ const matches = routeMessage(msg.text, botUserId, botMentioned);
497
+
498
+ if (matches.length === 0) {
499
+ // No agents matched - ignore message unless bot was directly mentioned
500
+ if (botMentioned) {
501
+ await say({
502
+ text: "No agents are currently available. Use `/agent-swarm-status` to check the swarm.",
503
+ thread_ts: msg.thread_ts || msg.ts,
504
+ });
505
+ }
506
+ return;
507
+ }
508
+
509
+ // Extract task description
510
+ const taskDescription = extractTaskFromMessage(msg.text, botUserId);
511
+ if (!taskDescription) {
512
+ await say({
513
+ text: "Please provide a task description after mentioning an agent.",
514
+ thread_ts: msg.thread_ts || msg.ts,
515
+ });
516
+ return;
517
+ }
518
+
519
+ // Create tasks for each matched agent
520
+ const createdTasks: string[] = [];
521
+ for (const match of matches) {
522
+ // Check agent is still idle
523
+ const agent = getAgentById(match.agent.id);
524
+ if (!agent || agent.status !== "idle") {
525
+ await say({
526
+ text: `Agent "${match.agent.name}" is currently ${agent?.status || "unavailable"} and cannot accept tasks.`,
527
+ thread_ts: msg.thread_ts || msg.ts,
528
+ });
529
+ continue;
530
+ }
531
+
532
+ const task = createTask(match.agent.id, taskDescription, {
533
+ source: "slack",
534
+ slackChannelId: msg.channel,
535
+ slackThreadTs: msg.thread_ts || msg.ts,
536
+ slackUserId: msg.user,
537
+ });
538
+
539
+ createdTasks.push(`${match.agent.name} (${task.id.slice(0, 8)})`);
540
+ }
541
+
542
+ if (createdTasks.length > 0) {
543
+ await say({
544
+ text: `Task created for: ${createdTasks.join(", ")}`,
545
+ thread_ts: msg.thread_ts || msg.ts,
546
+ });
547
+ }
548
+ });
549
+
550
+ // Handle app_mention events specifically
551
+ app.event("app_mention", async ({ event, client, say }) => {
552
+ // app_mention is already handled by the message event above
553
+ // but we can add specific behavior here if needed
554
+ console.log(`[Slack] App mentioned in channel ${event.channel}`);
555
+ });
556
+ }
557
+ ```
558
+
559
+ #### 4. Slash Commands (`src/slack/commands.ts`)
560
+
561
+ **File**: `src/slack/commands.ts` (NEW)
562
+
563
+ ```typescript
564
+ import type { App } from "@slack/bolt";
565
+ import { getAllAgents, getAllTasks } from "../be/db";
566
+
567
+ export function registerCommandHandler(app: App): void {
568
+ app.command("/agent-swarm-status", async ({ command, ack, respond }) => {
569
+ await ack();
570
+
571
+ const agents = getAllAgents();
572
+ const tasks = getAllTasks({ status: "in_progress" });
573
+
574
+ const statusEmoji: Record<string, string> = {
575
+ idle: ":white_circle:",
576
+ busy: ":large_blue_circle:",
577
+ offline: ":black_circle:",
578
+ };
579
+
580
+ const agentLines = agents.map(agent => {
581
+ const emoji = statusEmoji[agent.status] || ":question:";
582
+ const role = agent.isLead ? " (Lead)" : "";
583
+ const activeTask = tasks.find(t => t.agentId === agent.id);
584
+ const taskInfo = activeTask ? ` - Working on: ${activeTask.task.slice(0, 50)}...` : "";
585
+ return `${emoji} *${agent.name}*${role}: ${agent.status}${taskInfo}`;
586
+ });
587
+
588
+ const summary = {
589
+ total: agents.length,
590
+ idle: agents.filter(a => a.status === "idle").length,
591
+ busy: agents.filter(a => a.status === "busy").length,
592
+ offline: agents.filter(a => a.status === "offline").length,
593
+ };
594
+
595
+ await respond({
596
+ response_type: "ephemeral",
597
+ blocks: [
598
+ {
599
+ type: "header",
600
+ text: { type: "plain_text", text: "Agent Swarm Status" },
601
+ },
602
+ {
603
+ type: "section",
604
+ text: {
605
+ type: "mrkdwn",
606
+ text: `*Summary:* ${summary.total} agents (${summary.idle} idle, ${summary.busy} busy, ${summary.offline} offline)`,
607
+ },
608
+ },
609
+ {
610
+ type: "divider",
611
+ },
612
+ {
613
+ type: "section",
614
+ text: {
615
+ type: "mrkdwn",
616
+ text: agentLines.join("\n") || "_No agents registered_",
617
+ },
618
+ },
619
+ ],
620
+ });
621
+ });
622
+ }
623
+ ```
624
+
625
+ #### 5. Export Module (`src/slack/index.ts`)
626
+
627
+ **File**: `src/slack/index.ts` (NEW)
628
+
629
+ ```typescript
630
+ export { initSlackApp, startSlackApp, stopSlackApp, getSlackApp } from "./app";
631
+ export { routeMessage, extractTaskFromMessage } from "./router";
632
+ export type { SlackMessageContext, AgentMatch, SlackConfig } from "./types";
633
+ ```
634
+
635
+ #### 6. Integrate with HTTP Server (`src/http.ts`)
636
+
637
+ Add Slack startup/shutdown to the HTTP server lifecycle:
638
+
639
+ ```typescript
640
+ // Add import at top (after line 24)
641
+ import { startSlackApp, stopSlackApp } from "./slack";
642
+
643
+ // Modify shutdown function (around line 391)
644
+ async function shutdown() {
645
+ console.log("Shutting down...");
646
+
647
+ // Stop Slack bot
648
+ await stopSlackApp();
649
+
650
+ // Close all active transports (SSE connections, etc.)
651
+ for (const [id, transport] of Object.entries(transports)) {
652
+ console.log(`[HTTP] Closing transport ${id}`);
653
+ transport.close();
654
+ delete transports[id];
655
+ }
656
+
657
+ // Close all active connections forcefully
658
+ httpServer.closeAllConnections();
659
+ httpServer.close(() => {
660
+ closeDb();
661
+ console.log("MCP HTTP server closed, and database connection closed");
662
+ process.exit(0);
663
+ });
664
+ }
665
+
666
+ // Add Slack startup after HTTP server starts (after line 418)
667
+ httpServer
668
+ .listen(port, async () => {
669
+ console.log(`MCP HTTP server running on http://localhost:${port}/mcp`);
670
+
671
+ // Start Slack bot (if configured)
672
+ await startSlackApp();
673
+ })
674
+ ```
675
+
676
+ ### Success Criteria:
677
+
678
+ #### Automated Verification:
679
+ - [x] TypeScript compiles: `bun run tsc:check`
680
+ - [ ] Linting passes: `bun run lint`
681
+ - [ ] Server starts: `bun run dev:http`
682
+ - [ ] Console shows "[Slack] Bot connected via Socket Mode" (with valid tokens)
683
+
684
+ #### Manual Verification:
685
+ - [ ] Bot appears online in Slack workspace
686
+ - [ ] `/agent-swarm-status` command works
687
+ - [ ] Mentioning an agent name creates a task in the database
688
+ - [ ] Task shows `source: "slack"` in database/dashboard
689
+
690
+ **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation that the Slack bot connects and basic commands work before proceeding to the next phase.
691
+
692
+ ---
693
+
694
+ ## Phase 4: Task Completion Responses
695
+
696
+ ### Overview
697
+ Send task completion/failure messages back to Slack with custom agent personas.
698
+
699
+ ### Changes Required:
700
+
701
+ #### 1. Response Sender (`src/slack/responses.ts`)
702
+
703
+ **File**: `src/slack/responses.ts` (NEW)
704
+
705
+ ```typescript
706
+ import type { WebClient } from "@slack/web-api";
707
+ import type { AgentTask, Agent } from "../types";
708
+ import { getAgentById } from "../be/db";
709
+ import { getSlackApp } from "./app";
710
+
711
+ /**
712
+ * Send a task completion message to Slack with the agent's persona.
713
+ */
714
+ export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
715
+ const app = getSlackApp();
716
+ if (!app || !task.slackChannelId || !task.slackThreadTs) {
717
+ return false;
718
+ }
719
+
720
+ const agent = getAgentById(task.agentId);
721
+ if (!agent) {
722
+ console.error(`[Slack] Agent not found for task ${task.id}`);
723
+ return false;
724
+ }
725
+
726
+ const client = app.client;
727
+
728
+ try {
729
+ if (task.status === "completed") {
730
+ await sendWithPersona(client, {
731
+ channel: task.slackChannelId,
732
+ thread_ts: task.slackThreadTs,
733
+ text: task.output || "Task completed.",
734
+ username: agent.name,
735
+ icon_emoji: getAgentEmoji(agent),
736
+ });
737
+ } else if (task.status === "failed") {
738
+ await sendWithPersona(client, {
739
+ channel: task.slackChannelId,
740
+ thread_ts: task.slackThreadTs,
741
+ text: `:x: Task failed: ${task.failureReason || "Unknown error"}`,
742
+ username: agent.name,
743
+ icon_emoji: getAgentEmoji(agent),
744
+ });
745
+ }
746
+
747
+ return true;
748
+ } catch (error) {
749
+ console.error(`[Slack] Failed to send response for task ${task.id}:`, error);
750
+ return false;
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Send a progress update to Slack.
756
+ */
757
+ export async function sendProgressUpdate(task: AgentTask, progress: string): Promise<boolean> {
758
+ const app = getSlackApp();
759
+ if (!app || !task.slackChannelId || !task.slackThreadTs) {
760
+ return false;
761
+ }
762
+
763
+ const agent = getAgentById(task.agentId);
764
+ if (!agent) return false;
765
+
766
+ try {
767
+ await sendWithPersona(app.client, {
768
+ channel: task.slackChannelId,
769
+ thread_ts: task.slackThreadTs,
770
+ text: `:hourglass_flowing_sand: ${progress}`,
771
+ username: agent.name,
772
+ icon_emoji: getAgentEmoji(agent),
773
+ });
774
+ return true;
775
+ } catch (error) {
776
+ console.error(`[Slack] Failed to send progress update:`, error);
777
+ return false;
778
+ }
779
+ }
780
+
781
+ async function sendWithPersona(
782
+ client: WebClient,
783
+ options: {
784
+ channel: string;
785
+ thread_ts: string;
786
+ text: string;
787
+ username: string;
788
+ icon_emoji: string;
789
+ }
790
+ ): Promise<void> {
791
+ await client.chat.postMessage({
792
+ channel: options.channel,
793
+ thread_ts: options.thread_ts,
794
+ text: options.text,
795
+ username: options.username,
796
+ icon_emoji: options.icon_emoji,
797
+ });
798
+ }
799
+
800
+ function getAgentEmoji(agent: Agent): string {
801
+ if (agent.isLead) return ":crown:";
802
+
803
+ // Generate consistent emoji based on agent name hash
804
+ const emojis = [":robot_face:", ":gear:", ":zap:", ":rocket:", ":star:", ":crystal_ball:", ":bulb:", ":wrench:"];
805
+ const hash = agent.name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
806
+ return emojis[hash % emojis.length];
807
+ }
808
+ ```
809
+
810
+ #### 2. Completion Watcher (`src/slack/watcher.ts`)
811
+
812
+ **File**: `src/slack/watcher.ts` (NEW)
813
+
814
+ ```typescript
815
+ import { getCompletedSlackTasks, updateTaskSlackNotified } from "../be/db";
816
+ import { sendTaskResponse } from "./responses";
817
+ import { getSlackApp } from "./app";
818
+
819
+ let watcherInterval: ReturnType<typeof setInterval> | null = null;
820
+
821
+ /**
822
+ * Start watching for completed Slack tasks and sending responses.
823
+ */
824
+ export function startTaskWatcher(intervalMs = 5000): void {
825
+ if (watcherInterval) {
826
+ console.log("[Slack] Task watcher already running");
827
+ return;
828
+ }
829
+
830
+ watcherInterval = setInterval(async () => {
831
+ if (!getSlackApp()) return;
832
+
833
+ const tasks = getCompletedSlackTasks();
834
+
835
+ for (const task of tasks) {
836
+ const sent = await sendTaskResponse(task);
837
+ if (sent) {
838
+ // Mark task as Slack-notified to prevent re-sending
839
+ markTaskNotified(task.id);
840
+ }
841
+ }
842
+ }, intervalMs);
843
+
844
+ console.log(`[Slack] Task watcher started (interval: ${intervalMs}ms)`);
845
+ }
846
+
847
+ export function stopTaskWatcher(): void {
848
+ if (watcherInterval) {
849
+ clearInterval(watcherInterval);
850
+ watcherInterval = null;
851
+ console.log("[Slack] Task watcher stopped");
852
+ }
853
+ }
854
+
855
+ // Track notified tasks in memory (persists across watcher cycles)
856
+ const notifiedTasks = new Set<string>();
857
+
858
+ function markTaskNotified(taskId: string): void {
859
+ notifiedTasks.add(taskId);
860
+ }
861
+
862
+ // Override getCompletedSlackTasks to filter already-notified
863
+ export function getUnnotifiedCompletedSlackTasks() {
864
+ const { getCompletedSlackTasks } = require("../be/db");
865
+ return getCompletedSlackTasks().filter((t: { id: string }) => !notifiedTasks.has(t.id));
866
+ }
867
+ ```
868
+
869
+ **Note:** We use an in-memory set for tracking notified tasks. For production, consider adding a `slackNotifiedAt` column to the database.
870
+
871
+ #### 3. Update Slack App to Start Watcher (`src/slack/app.ts`)
872
+
873
+ Add watcher startup:
874
+
875
+ ```typescript
876
+ // Add import at top
877
+ import { startTaskWatcher, stopTaskWatcher } from "./watcher";
878
+
879
+ // Update startSlackApp function
880
+ export async function startSlackApp(): Promise<void> {
881
+ if (!app) {
882
+ await initSlackApp();
883
+ }
884
+
885
+ if (app) {
886
+ await app.start();
887
+ console.log("[Slack] Bot connected via Socket Mode");
888
+
889
+ // Start watching for task completions
890
+ startTaskWatcher();
891
+ }
892
+ }
893
+
894
+ // Update stopSlackApp function
895
+ export async function stopSlackApp(): Promise<void> {
896
+ stopTaskWatcher();
897
+
898
+ if (app) {
899
+ await app.stop();
900
+ app = null;
901
+ console.log("[Slack] Bot disconnected");
902
+ }
903
+ }
904
+ ```
905
+
906
+ ### Success Criteria:
907
+
908
+ #### Automated Verification:
909
+ - [x] TypeScript compiles: `bun run tsc:check`
910
+ - [ ] Linting passes: `bun run lint`
911
+ - [ ] Server starts without errors: `bun run dev:http`
912
+
913
+ #### Manual Verification:
914
+ - [ ] Create a task from Slack message
915
+ - [ ] Complete the task via MCP (or manually update DB)
916
+ - [ ] Verify completion message appears in Slack thread
917
+ - [ ] Message shows agent's name as the sender
918
+ - [ ] Failed tasks show error message in Slack
919
+
920
+ **Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation that task responses appear correctly in Slack before proceeding to the next phase.
921
+
922
+ ---
923
+
924
+ ## Phase 5: Polish and Edge Cases
925
+
926
+ ### Overview
927
+ Handle multi-agent mentions, broadcasts, and improve error handling.
928
+
929
+ ### Changes Required:
930
+
931
+ #### 1. Update Handlers for Better UX (`src/slack/handlers.ts`)
932
+
933
+ Update message handler with better feedback:
934
+
935
+ ```typescript
936
+ // Replace the task creation loop in registerMessageHandler
937
+
938
+ // Create tasks for each matched agent
939
+ const results: { success: string[]; failed: string[] } = { success: [], failed: [] };
940
+
941
+ for (const match of matches) {
942
+ const agent = getAgentById(match.agent.id);
943
+
944
+ if (!agent) {
945
+ results.failed.push(`${match.agent.name} (not found)`);
946
+ continue;
947
+ }
948
+
949
+ if (agent.status !== "idle") {
950
+ results.failed.push(`${agent.name} (${agent.status})`);
951
+ continue;
952
+ }
953
+
954
+ try {
955
+ const task = createTask(agent.id, taskDescription, {
956
+ source: "slack",
957
+ slackChannelId: msg.channel,
958
+ slackThreadTs: msg.thread_ts || msg.ts,
959
+ slackUserId: msg.user,
960
+ });
961
+ results.success.push(`${agent.name}`);
962
+ } catch (error) {
963
+ results.failed.push(`${agent.name} (error)`);
964
+ }
965
+ }
966
+
967
+ // Send summary
968
+ const parts: string[] = [];
969
+ if (results.success.length > 0) {
970
+ parts.push(`:white_check_mark: Task assigned to: ${results.success.join(", ")}`);
971
+ }
972
+ if (results.failed.length > 0) {
973
+ parts.push(`:warning: Could not assign to: ${results.failed.join(", ")}`);
974
+ }
975
+
976
+ if (parts.length > 0) {
977
+ await say({
978
+ text: parts.join("\n"),
979
+ thread_ts: msg.thread_ts || msg.ts,
980
+ });
981
+ }
982
+ ```
983
+
984
+ #### 2. Add Help Command
985
+
986
+ Add to `src/slack/commands.ts`:
987
+
988
+ ```typescript
989
+ app.command("/agent-swarm-help", async ({ command, ack, respond }) => {
990
+ await ack();
991
+
992
+ await respond({
993
+ response_type: "ephemeral",
994
+ blocks: [
995
+ {
996
+ type: "header",
997
+ text: { type: "plain_text", text: "Agent Swarm Help" },
998
+ },
999
+ {
1000
+ type: "section",
1001
+ text: {
1002
+ type: "mrkdwn",
1003
+ text: `*How to assign tasks:*
1004
+ • Mention an agent by name: \`Hey Alpha, can you review this code?\`
1005
+ • Use explicit ID: \`swarm#<uuid> please analyze the logs\`
1006
+ • Broadcast to all: \`swarm#all status report please\`
1007
+ • Mention the bot: \`@agent-swarm help me\` (routes to lead agent)
1008
+
1009
+ *Commands:*
1010
+ • \`/agent-swarm-status\` - Show all agents and their current status
1011
+ • \`/agent-swarm-help\` - Show this help message`,
1012
+ },
1013
+ },
1014
+ ],
1015
+ });
1016
+ });
1017
+ ```
1018
+
1019
+ Update manifest if needed for the new command.
1020
+
1021
+ #### 3. Rate Limiting Protection
1022
+
1023
+ Add simple rate limiting to prevent spam:
1024
+
1025
+ ```typescript
1026
+ // Add to src/slack/handlers.ts
1027
+
1028
+ const rateLimitMap = new Map<string, number>();
1029
+ const RATE_LIMIT_WINDOW = 60_000; // 1 minute
1030
+ const MAX_REQUESTS_PER_WINDOW = 10;
1031
+
1032
+ function checkRateLimit(userId: string): boolean {
1033
+ const now = Date.now();
1034
+ const userRequests = rateLimitMap.get(userId) || 0;
1035
+
1036
+ // Simple sliding window (resets after window)
1037
+ if (userRequests >= MAX_REQUESTS_PER_WINDOW) {
1038
+ return false;
1039
+ }
1040
+
1041
+ rateLimitMap.set(userId, userRequests + 1);
1042
+
1043
+ // Clean up after window
1044
+ setTimeout(() => {
1045
+ const current = rateLimitMap.get(userId) || 0;
1046
+ if (current > 0) {
1047
+ rateLimitMap.set(userId, current - 1);
1048
+ }
1049
+ }, RATE_LIMIT_WINDOW);
1050
+
1051
+ return true;
1052
+ }
1053
+
1054
+ // Use in message handler:
1055
+ if (!checkRateLimit(msg.user)) {
1056
+ await say({
1057
+ text: "You're sending too many requests. Please slow down.",
1058
+ thread_ts: msg.thread_ts || msg.ts,
1059
+ });
1060
+ return;
1061
+ }
1062
+ ```
1063
+
1064
+ ### Success Criteria:
1065
+
1066
+ #### Automated Verification:
1067
+ - [x] TypeScript compiles: `bun run tsc:check`
1068
+ - [x] Linting passes: `bun run lint`
1069
+
1070
+ #### Manual Verification:
1071
+ - [ ] Multi-agent mention creates separate tasks
1072
+ - [ ] `swarm#all` creates tasks for all non-lead agents
1073
+ - [ ] Rate limiting prevents spam
1074
+ - [ ] Help command shows usage instructions
1075
+
1076
+ **Implementation Note**: After completing this phase and all automated verification passes, the Slack integration should be fully functional for manual testing.
1077
+
1078
+ ---
1079
+
1080
+ ## Testing Strategy
1081
+
1082
+ ### Unit Tests
1083
+
1084
+ Create `src/slack/router.test.ts`:
1085
+
1086
+ ```typescript
1087
+ import { test, expect, describe, beforeEach, mock } from "bun:test";
1088
+ import { routeMessage, extractTaskFromMessage } from "./router";
1089
+
1090
+ // Mock the database
1091
+ mock.module("../be/db", () => ({
1092
+ getAllAgents: () => [
1093
+ { id: "1", name: "Alpha Worker", isLead: false, status: "idle" },
1094
+ { id: "2", name: "Beta Tester", isLead: false, status: "idle" },
1095
+ { id: "3", name: "Lead Agent", isLead: true, status: "idle" },
1096
+ ],
1097
+ getAgentById: (id: string) => {
1098
+ const agents: Record<string, any> = {
1099
+ "1": { id: "1", name: "Alpha Worker", isLead: false, status: "idle" },
1100
+ "2": { id: "2", name: "Beta Tester", isLead: false, status: "idle" },
1101
+ "3": { id: "3", name: "Lead Agent", isLead: true, status: "idle" },
1102
+ };
1103
+ return agents[id] || null;
1104
+ },
1105
+ }));
1106
+
1107
+ describe("routeMessage", () => {
1108
+ test("matches agent by partial name", () => {
1109
+ const matches = routeMessage("Hey Alpha, can you help?", "BOT123", false);
1110
+ expect(matches).toHaveLength(1);
1111
+ expect(matches[0].agent.name).toBe("Alpha Worker");
1112
+ });
1113
+
1114
+ test("routes to lead when only bot mentioned", () => {
1115
+ const matches = routeMessage("<@BOT123> help", "BOT123", true);
1116
+ expect(matches).toHaveLength(1);
1117
+ expect(matches[0].agent.isLead).toBe(true);
1118
+ });
1119
+
1120
+ test("handles swarm#all broadcast", () => {
1121
+ const matches = routeMessage("swarm#all status check", "BOT123", false);
1122
+ expect(matches).toHaveLength(2); // All non-lead agents
1123
+ });
1124
+ });
1125
+
1126
+ describe("extractTaskFromMessage", () => {
1127
+ test("removes bot mention", () => {
1128
+ const task = extractTaskFromMessage("<@BOT123> please review this", "BOT123");
1129
+ expect(task).toBe("please review this");
1130
+ });
1131
+ });
1132
+ ```
1133
+
1134
+ ### Integration Tests
1135
+
1136
+ Manual testing checklist:
1137
+
1138
+ 1. **Bot Connection**
1139
+ - Start server with valid Slack tokens
1140
+ - Verify bot shows as online in Slack
1141
+
1142
+ 2. **Task Creation**
1143
+ - Send message: "Alpha, please check the logs"
1144
+ - Verify task appears in dashboard with source: "slack"
1145
+ - Verify confirmation message in Slack
1146
+
1147
+ 3. **Task Completion**
1148
+ - Complete task via MCP/dashboard
1149
+ - Verify completion message appears in Slack thread
1150
+ - Verify message shows agent's name
1151
+
1152
+ 4. **Error Handling**
1153
+ - Message busy agent - verify error response
1154
+ - Invalid agent name - verify no task created
1155
+ - Missing task description - verify error response
1156
+
1157
+ 5. **Commands**
1158
+ - `/agent-swarm-status` - verify agent list
1159
+ - `/agent-swarm-help` - verify help text
1160
+
1161
+ ## Performance Considerations
1162
+
1163
+ 1. **Task Watcher Interval**: Default 5 seconds. Increase for high-volume deployments.
1164
+ 2. **Rate Limiting**: 10 requests/minute per user. Adjust based on team size.
1165
+ 3. **Memory**: Notified task tracking uses in-memory Set. For long-running instances, consider periodic cleanup or DB column.
1166
+
1167
+ ## Migration Notes
1168
+
1169
+ ### Database Migration
1170
+
1171
+ The schema changes add new columns with defaults, so existing data is preserved:
1172
+ - `source` defaults to `"mcp"` for existing tasks
1173
+ - `slackChannelId`, `slackThreadTs`, `slackUserId` default to NULL
1174
+
1175
+ No manual migration needed - SQLite `ALTER TABLE` is handled by the schema update.
1176
+
1177
+ ### Environment Variables
1178
+
1179
+ Add to deployment configuration:
1180
+ ```
1181
+ SLACK_BOT_TOKEN=xoxb-...
1182
+ SLACK_APP_TOKEN=xapp-...
1183
+ ```
1184
+
1185
+ Bot will gracefully disable if tokens are missing.
1186
+
1187
+ ## References
1188
+
1189
+ - Research document: `thoughts/shared/research/2025-12-18-slack-integration.md`
1190
+ - Slack manifest: `slack-manifest.json`
1191
+ - HTTP server: `src/http.ts`
1192
+ - Database layer: `src/be/db.ts`
1193
+ - Types: `src/types.ts`
1194
+ - Task creation: `src/tools/send-task.ts`
1195
+ - Slack Bolt docs: https://slack.dev/bolt-js/