@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.
- package/.claude/settings.local.json +20 -1
- package/.env.docker.example +22 -1
- package/.env.example +17 -0
- package/.github/workflows/docker-publish.yml +92 -0
- package/CONTRIBUTING.md +270 -0
- package/DEPLOYMENT.md +391 -0
- package/Dockerfile.worker +29 -1
- package/FAQ.md +19 -0
- package/LICENSE +21 -0
- package/MCP.md +249 -0
- package/README.md +103 -207
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/docker-compose.example.yml +137 -0
- package/docker-entrypoint.sh +223 -7
- package/package.json +8 -3
- package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/codebase-analyzer.md +143 -0
- package/plugin/agents/codebase-locator.md +122 -0
- package/plugin/agents/codebase-pattern-finder.md +227 -0
- package/plugin/agents/web-search-researcher.md +109 -0
- package/plugin/commands/create-plan.md +415 -0
- package/plugin/commands/implement-plan.md +89 -0
- package/plugin/commands/research.md +200 -0
- package/plugin/commands/start-leader.md +101 -0
- package/plugin/commands/start-worker.md +56 -0
- package/plugin/commands/swarm-chat.md +78 -0
- package/plugin/commands/todos.md +66 -0
- package/plugin/commands/work-on-task.md +44 -0
- package/plugin/skills/.gitkeep +0 -0
- package/scripts/generate-mcp-docs.ts +415 -0
- package/slack-manifest.json +69 -0
- package/src/be/db.ts +1431 -25
- package/src/cli.tsx +135 -11
- package/src/commands/lead.ts +13 -0
- package/src/commands/runner.ts +255 -0
- package/src/commands/worker.ts +8 -220
- package/src/hooks/hook.ts +102 -14
- package/src/http.ts +361 -5
- package/src/prompts/base-prompt.ts +131 -0
- package/src/server.ts +56 -0
- package/src/slack/app.ts +73 -0
- package/src/slack/commands.ts +88 -0
- package/src/slack/handlers.ts +281 -0
- package/src/slack/index.ts +3 -0
- package/src/slack/responses.ts +175 -0
- package/src/slack/router.ts +170 -0
- package/src/slack/types.ts +20 -0
- package/src/slack/watcher.ts +119 -0
- package/src/tools/create-channel.ts +80 -0
- package/src/tools/get-tasks.ts +54 -21
- package/src/tools/join-swarm.ts +28 -4
- package/src/tools/list-channels.ts +37 -0
- package/src/tools/list-services.ts +110 -0
- package/src/tools/poll-task.ts +46 -3
- package/src/tools/post-message.ts +87 -0
- package/src/tools/read-messages.ts +192 -0
- package/src/tools/register-service.ts +118 -0
- package/src/tools/send-task.ts +80 -7
- package/src/tools/store-progress.ts +9 -3
- package/src/tools/task-action.ts +211 -0
- package/src/tools/unregister-service.ts +110 -0
- package/src/tools/update-profile.ts +105 -0
- package/src/tools/update-service-status.ts +118 -0
- package/src/types.ts +110 -3
- package/src/utils/pretty-print.ts +224 -0
- package/thoughts/shared/plans/.gitkeep +0 -0
- package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
- package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
- package/tsconfig.json +3 -1
- package/ui/bun.lock +692 -0
- package/ui/index.html +22 -0
- package/ui/package.json +32 -0
- package/ui/pnpm-lock.yaml +3034 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/logo.png +0 -0
- package/ui/src/App.tsx +43 -0
- package/ui/src/components/ActivityFeed.tsx +415 -0
- package/ui/src/components/AgentDetailPanel.tsx +534 -0
- package/ui/src/components/AgentsPanel.tsx +549 -0
- package/ui/src/components/ChatPanel.tsx +1820 -0
- package/ui/src/components/ConfigModal.tsx +232 -0
- package/ui/src/components/Dashboard.tsx +534 -0
- package/ui/src/components/Header.tsx +168 -0
- package/ui/src/components/ServicesPanel.tsx +612 -0
- package/ui/src/components/StatsBar.tsx +288 -0
- package/ui/src/components/StatusBadge.tsx +124 -0
- package/ui/src/components/TaskDetailPanel.tsx +807 -0
- package/ui/src/components/TasksPanel.tsx +575 -0
- package/ui/src/hooks/queries.ts +170 -0
- package/ui/src/index.css +235 -0
- package/ui/src/lib/api.ts +161 -0
- package/ui/src/lib/config.ts +35 -0
- package/ui/src/lib/theme.ts +214 -0
- package/ui/src/lib/utils.ts +48 -0
- package/ui/src/main.tsx +32 -0
- package/ui/src/types/api.ts +164 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tailwind.config.js +35 -0
- package/ui/tsconfig.json +31 -0
- package/ui/vite.config.ts +22 -0
- package/cc-plugin/README.md +0 -49
- package/cc-plugin/commands/setup-leader.md +0 -73
- package/cc-plugin/commands/start-worker.md +0 -64
- package/docker-compose.worker.yml +0 -35
- package/example-req-meta.json +0 -24
- /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/
|