@desplega.ai/agent-swarm 1.67.1 → 1.67.3
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/LICENSE +1 -1
- package/README.md +96 -387
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/crypto/key-bootstrap.ts +30 -13
- package/src/be/db.ts +35 -4
- package/src/be/migrations/040_slack_thread_composite_index.sql +3 -0
- package/src/http/index.ts +9 -0
- package/src/prompts/session-templates.ts +1 -1
- package/src/tests/follow-up-redelivery-guard.test.ts +267 -0
- package/src/tests/key-bootstrap.test.ts +51 -0
- package/src/tests/prompt-template-remaining.test.ts +5 -0
- package/src/tools/send-task.ts +31 -0
- package/src/tools/templates.ts +5 -0
- package/tsconfig.json +2 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
<sub>Built by <a href="https://desplega.sh">desplega.sh</a> — by builders, for builders.</sub>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<p align="center">
|
|
15
|
+
<video src="https://github.com/user-attachments/assets/e220712e-c54d-4f46-b059-bac04639d229" controls muted playsinline width="720"></video>
|
|
16
|
+
</p>
|
|
17
|
+
<p align="center">
|
|
18
|
+
<sub>▸ <a href="./assets/agent-swarm.mp4">daily evolution</a> · <a href="./assets/agent-swarm-slack-to-pr.mp4">slack → pr</a> · <a href="./assets/video-source">Making of</a></sub>
|
|
19
|
+
</p>
|
|
15
20
|
|
|
16
21
|
<p align="center">
|
|
17
22
|
<a href="https://agent-swarm.dev">
|
|
@@ -33,101 +38,90 @@ https://github.com/user-attachments/assets/bd308567-d21e-44a5-87ec-d25aeb1de3d3
|
|
|
33
38
|
|
|
34
39
|
> **What if your AI agents remembered everything, learned from every mistake, and got better with every task?**
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## Key Features
|
|
39
|
-
|
|
40
|
-
- **Lead/Worker coordination** — A lead agent delegates and tracks work across multiple workers
|
|
41
|
-
- **Docker isolation** — Each worker runs in its own container with a full dev environment
|
|
42
|
-
- **Slack, GitHub, GitLab & Email integration** — Create tasks by messaging the bot, @mentioning it in issues/PRs/MRs, or sending an email
|
|
43
|
-
- **Task lifecycle** — Priority queues, dependencies, pause/resume across deployments
|
|
44
|
-
- **Compounding memory** — Agents learn from every session and get smarter over time
|
|
45
|
-
- **Persistent identity** — Each agent has its own personality, expertise, and working style that evolves
|
|
46
|
-
- **Dashboard UI** — Real-time monitoring of agents, tasks, and inter-agent chat
|
|
47
|
-
- **Service discovery** — Workers can expose HTTP services and discover each other
|
|
48
|
-
- **Scheduled tasks** — Cron-based recurring task automation
|
|
49
|
-
- **Templates registry** — Pre-built agent templates (9 official: lead, coder, researcher, reviewer, tester, FDE, content-writer, content-reviewer, content-strategist) with a gallery UI and docker-compose builder
|
|
50
|
-
- **GitLab integration** — Full GitLab webhook support alongside GitHub via provider adapter pattern
|
|
51
|
-
- **Working directory support** — Tasks can specify a custom starting directory for agents via the `dir` parameter
|
|
52
|
-
- **Multi-provider** — Run agents with Claude Code, pi-mono, or OpenAI Codex (`HARNESS_PROVIDER=claude|pi|codex`)
|
|
53
|
-
- **Agent-fs integration** — Persistent, searchable filesystem shared across the swarm with auto-registration on first boot
|
|
54
|
-
- **Debug dashboard** — SQL query interface with Monaco editor and AG Grid results for database inspection
|
|
55
|
-
- **Workflow engine** — DAG-based workflow automation with executor registry, checkpoint durability, webhook/schedule/manual triggers, per-step retry, structured I/O schemas, fan-out/convergence, configurable failure handling, and version history
|
|
56
|
-
- **Linear integration** — Bidirectional ticket tracker sync via OAuth + webhooks with AgentSession lifecycle and generic tracker abstraction
|
|
57
|
-
- **Portless local dev** — Friendly URLs for local development (`api.swarm.localhost:1355`) via portless proxy
|
|
58
|
-
- **Onboarding wizard** — Interactive CLI wizard (`agent-swarm onboard`) to set up a new swarm from scratch with presets, credential collection, and docker-compose generation
|
|
59
|
-
- **Skill system** — Reusable procedural knowledge: create, install, publish, and sync skills from GitHub with scope resolution (agent → swarm → global)
|
|
60
|
-
- **Human-in-the-Loop** — Workflow nodes that pause for human approval or input, with a dashboard UI for reviewing and responding to requests
|
|
61
|
-
- **MCP server management** — Register, install, and manage MCP servers for agents with scope cascade (agent → swarm → global) and auto-injection into worker containers
|
|
62
|
-
- **Context usage tracking** — Monitor context window utilization and compaction events per task with visual indicators in the dashboard
|
|
41
|
+
## What it does
|
|
63
42
|
|
|
64
|
-
|
|
43
|
+
Agent Swarm runs a team of AI coding agents that coordinate autonomously. A **lead agent** receives tasks — from Slack, GitHub, GitLab, email, or the API — breaks them down, and delegates to **worker agents** running in Docker containers. Workers execute tasks, ship code, and write their learnings back to a shared memory so the whole swarm gets smarter every session.
|
|
65
44
|
|
|
66
|
-
|
|
45
|
+
Learn more in the [architecture overview](https://docs.agent-swarm.dev/docs/architecture/overview).
|
|
67
46
|
|
|
68
|
-
|
|
69
|
-
|
|
47
|
+
```mermaid
|
|
48
|
+
flowchart LR
|
|
49
|
+
subgraph IN["Tasks come in"]
|
|
50
|
+
direction TB
|
|
51
|
+
S["Slack"]
|
|
52
|
+
G["GitHub / GitLab"]
|
|
53
|
+
E["Email"]
|
|
54
|
+
A["API / CLI"]
|
|
55
|
+
end
|
|
70
56
|
|
|
71
|
-
|
|
57
|
+
LEAD(["Lead Agent<br/>plans & delegates"])
|
|
72
58
|
|
|
73
|
-
|
|
59
|
+
subgraph WORKERS["Workers in Docker"]
|
|
60
|
+
direction TB
|
|
61
|
+
W1["Worker"]
|
|
62
|
+
W2["Worker"]
|
|
63
|
+
W3["Worker"]
|
|
64
|
+
end
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
subgraph BRAIN["Persistent brain"]
|
|
67
|
+
direction TB
|
|
68
|
+
MEM["Memory<br/>(vector search)"]
|
|
69
|
+
ID["Identity<br/>(SOUL, CLAUDE.md)"]
|
|
70
|
+
end
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
subgraph OUT["Work ships"]
|
|
73
|
+
direction TB
|
|
74
|
+
PR["Pull Requests"]
|
|
75
|
+
REPLY["Slack replies"]
|
|
76
|
+
EMAIL["Email replies"]
|
|
77
|
+
end
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
IN --> LEAD --> WORKERS
|
|
80
|
+
WORKERS -->|reads context| BRAIN
|
|
81
|
+
WORKERS -->|writes learnings| BRAIN
|
|
82
|
+
WORKERS --> OUT
|
|
85
83
|
```
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
## Highlights
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
- **Lead/worker orchestration in Docker** — isolated dev environments, priority queues, pause/resume across deploys. [Architecture →](https://docs.agent-swarm.dev/docs/architecture/overview)
|
|
88
|
+
- **Compounding memory & persistent identity** — agents remember past sessions and evolve their own persona, expertise, and notes. [Memory →](https://docs.agent-swarm.dev/docs/architecture/memory) · [Agents →](https://docs.agent-swarm.dev/docs/architecture/agents)
|
|
89
|
+
- **Multi-channel inputs** — Slack, GitHub, GitLab, email, Linear, and the HTTP API all create tasks. [Integrations](#integrations)
|
|
90
|
+
- **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
|
|
91
|
+
- **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
|
|
92
|
+
- **Multi-provider** — run with Claude Code, OpenAI Codex, or pi-mono. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration)
|
|
93
|
+
- **Skills & MCP servers** — reusable procedural knowledge and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
|
|
94
|
+
- **Real-time dashboard** — monitor agents, tasks, and inter-agent chat. [app.agent-swarm.dev →](https://app.agent-swarm.dev)
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
## Quick Start
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
**Prerequisites:** [Docker](https://docker.com) and a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) OAuth token (`claude setup-token`).
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
git clone https://github.com/desplega-ai/agent-swarm.git
|
|
97
|
-
cd agent-swarm
|
|
98
|
-
bun install
|
|
100
|
+
The fastest way is the onboarding wizard — it collects credentials, picks presets, and generates a working `docker-compose.yml`:
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Edit .env — set API_KEY
|
|
103
|
-
bun run start:http
|
|
102
|
+
```bash
|
|
103
|
+
bunx @desplega.ai/agent-swarm onboard
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Prefer manual setup? Clone and run with Docker Compose:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
bun run docker:run:worker
|
|
109
|
+
git clone https://github.com/desplega-ai/agent-swarm.git
|
|
110
|
+
cd agent-swarm
|
|
111
|
+
cp .env.docker.example .env
|
|
112
|
+
# edit .env — set API_KEY and CLAUDE_CODE_OAUTH_TOKEN
|
|
113
|
+
docker compose -f docker-compose.example.yml --env-file .env up -d
|
|
115
114
|
```
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
The API runs on port `3013`, with interactive docs at `http://localhost:3013/docs` and an OpenAPI 3.1 spec at `http://localhost:3013/openapi.json`.
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
# After starting the API server (Option B, step 1):
|
|
123
|
-
bunx @desplega.ai/agent-swarm connect
|
|
124
|
-
```
|
|
118
|
+
<details>
|
|
119
|
+
<summary><strong>Other setups</strong></summary>
|
|
125
120
|
|
|
126
|
-
|
|
121
|
+
- **Local API + Docker workers** — run the API on your host, workers in Docker. See [Getting Started](https://docs.agent-swarm.dev/docs/getting-started).
|
|
122
|
+
- **Claude Code as the lead agent** — `bunx @desplega.ai/agent-swarm connect`, then tell Claude Code to register as the lead.
|
|
127
123
|
|
|
128
|
-
|
|
129
|
-
Register yourself as the lead agent in the agent-swarm.
|
|
130
|
-
```
|
|
124
|
+
</details>
|
|
131
125
|
|
|
132
126
|
## How It Works
|
|
133
127
|
|
|
@@ -141,312 +135,37 @@ Worker Worker Worker
|
|
|
141
135
|
(Docker containers with full dev environments)
|
|
142
136
|
```
|
|
143
137
|
|
|
144
|
-
1.
|
|
145
|
-
2.
|
|
146
|
-
3.
|
|
147
|
-
4.
|
|
148
|
-
5.
|
|
149
|
-
6.
|
|
150
|
-
|
|
151
|
-
## Agents Get Smarter Over Time
|
|
152
|
-
|
|
153
|
-
Agent Swarm agents aren't stateless. They build compounding knowledge through multiple automatic mechanisms:
|
|
154
|
-
|
|
155
|
-
### Memory System
|
|
138
|
+
1. A task arrives via Slack DM, GitHub @mention, email, or the API.
|
|
139
|
+
2. The lead plans and delegates subtasks to workers.
|
|
140
|
+
3. Workers execute in isolated Docker containers (git, Node.js, Python, etc.).
|
|
141
|
+
4. Progress streams to the dashboard, Slack threads, or the API.
|
|
142
|
+
5. Results ship back out as PRs, issue replies, or Slack messages.
|
|
143
|
+
6. Session learnings are extracted and become memory for future tasks.
|
|
156
144
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- **Session summaries** — At the end of each session, a lightweight model extracts key learnings: mistakes made, patterns discovered, failed approaches, and codebase knowledge. These summaries become searchable memories.
|
|
160
|
-
- **Task completions** — Every completed (or failed) task's output is indexed. Failed tasks include notes about what went wrong, so the agent avoids repeating the same mistake.
|
|
161
|
-
- **File-based notes** — Agents write to `/workspace/personal/memory/` in their per-agent directory. Files are automatically indexed and can be promoted to swarm scope.
|
|
162
|
-
- **Lead-to-worker injection** — The lead agent can push specific learnings into any worker's memory using the `inject-learning` tool, closing the feedback loop.
|
|
163
|
-
|
|
164
|
-
Before starting each task, the runner automatically searches for relevant memories and includes them in the agent's context. Past experience directly informs future work.
|
|
165
|
-
|
|
166
|
-
### Persistent Identity
|
|
167
|
-
|
|
168
|
-
Each agent has four identity files that persist across sessions and evolve over time:
|
|
169
|
-
|
|
170
|
-
| File | Purpose | Example |
|
|
171
|
-
|------|---------|---------|
|
|
172
|
-
| **SOUL.md** | Core persona, values, behavioral directives | "You're not a chatbot. Be thorough. Own your mistakes." |
|
|
173
|
-
| **IDENTITY.md** | Expertise, working style, track record | "I'm the coding arm of the swarm. I ship fast and clean." |
|
|
174
|
-
| **TOOLS.md** | Environment knowledge — repos, services, APIs | "The API runs on port 3013. Use `wts` for worktree management." |
|
|
175
|
-
| **CLAUDE.md** | Persistent notes and instructions | Learnings, preferences, important context |
|
|
176
|
-
|
|
177
|
-
Agents can edit these files directly during a session. Changes are synced to the database in real-time (on every file edit) and at session end. When the agent restarts, its identity is restored from the database. Version history is tracked for all changes.
|
|
178
|
-
|
|
179
|
-
The default templates encourage self-improvement:
|
|
180
|
-
- Tools you wished you had? Update your startup script.
|
|
181
|
-
- Environment knowledge gained? Record it in TOOLS.md.
|
|
182
|
-
- Patterns discovered? Add them to your notes.
|
|
183
|
-
- Mistakes to avoid? Add guardrails.
|
|
184
|
-
|
|
185
|
-
### Startup Scripts
|
|
186
|
-
|
|
187
|
-
Each agent has a startup script (`/workspace/start-up.sh`) that runs at every container start. Agents can modify this script to install tools, configure their environment, or set up workflows — and the changes persist across restarts. An agent that discovers it needs `ripgrep` will install it once, and it'll be there for every future session.
|
|
188
|
-
|
|
189
|
-
## Agent Configuration
|
|
190
|
-
|
|
191
|
-
### Identity Management
|
|
192
|
-
|
|
193
|
-
Agent identity is stored in the database and synced to the filesystem at session start. There are three ways to configure it:
|
|
194
|
-
|
|
195
|
-
1. **Default generation** — On first registration, the system generates templates based on the agent's name, role, and description.
|
|
196
|
-
2. **Self-editing** — Agents modify their own identity files during sessions. A PostToolUse hook syncs changes to the database in real-time.
|
|
197
|
-
3. **API / MCP tool** — Use the `update-profile` tool to programmatically set any identity field (soulMd, identityMd, toolsMd, claudeMd, setupScript).
|
|
198
|
-
|
|
199
|
-
### System Prompt Assembly
|
|
200
|
-
|
|
201
|
-
The system prompt is built from multiple layers, assembled at task start:
|
|
202
|
-
|
|
203
|
-
1. **Base role instructions** — Lead or worker-specific behavior rules
|
|
204
|
-
2. **Agent identity** — SOUL.md + IDENTITY.md content
|
|
205
|
-
3. **Repository context** — If the task targets a specific GitHub repo, that repo's CLAUDE.md is included
|
|
206
|
-
4. **Filesystem guide** — Memory directories, personal/shared workspace, setup script instructions
|
|
207
|
-
5. **Self-awareness** — How the agent is built (runtime, hooks, memory system, task lifecycle)
|
|
208
|
-
6. **Additional prompt** — Custom text from `SYSTEM_PROMPT` env var or `--system-prompt` CLI flag
|
|
209
|
-
|
|
210
|
-
### Hook System
|
|
211
|
-
|
|
212
|
-
Six hooks fire during each Claude Code session, providing safety, context management, and persistence:
|
|
213
|
-
|
|
214
|
-
| Hook | When | What it does |
|
|
215
|
-
|------|------|-------------|
|
|
216
|
-
| **SessionStart** | Session begins | Writes CLAUDE.md from DB, loads concurrent session context for leads |
|
|
217
|
-
| **PreCompact** | Before context compaction | Injects a "goal reminder" with current task details so the agent doesn't lose track |
|
|
218
|
-
| **PreToolUse** | Before each tool call | Checks for task cancellation, detects tool loops (same tool/args repeated), blocks excessive polling |
|
|
219
|
-
| **PostToolUse** | After each tool call | Sends heartbeat, syncs identity file edits to DB, auto-indexes memory files |
|
|
220
|
-
| **UserPromptSubmit** | New iteration starts | Checks for task cancellation |
|
|
221
|
-
| **Stop** | Session ends | Saves PM2 state, syncs all identity files, runs session summarization via Haiku, marks agent offline |
|
|
145
|
+
More detail in the [task lifecycle docs](https://docs.agent-swarm.dev/docs/concepts/task-lifecycle).
|
|
222
146
|
|
|
223
147
|
## Integrations
|
|
224
148
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Message the bot directly to create tasks. Workers reply in threads with progress updates. Optionally restrict access with `SLACK_ALLOWED_EMAIL_DOMAINS` or `SLACK_ALLOWED_USER_IDS`.
|
|
236
|
-
|
|
237
|
-
### GitHub App
|
|
238
|
-
|
|
239
|
-
Set up a [GitHub App](https://github.com/settings/apps/new) to receive webhooks when the bot is @mentioned or assigned to issues/PRs.
|
|
240
|
-
|
|
241
|
-
**Webhook URL:** `https://<your-domain>/api/github/webhook`
|
|
242
|
-
|
|
243
|
-
**Required permissions:**
|
|
244
|
-
- Issues: Read & Write
|
|
245
|
-
- Pull requests: Read & Write
|
|
246
|
-
|
|
247
|
-
**Subscribe to events:** Issues, Issue comments, Pull requests, Pull request reviews, Pull request review comments, Check runs, Check suites, Workflow runs
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
# Add to your .env
|
|
251
|
-
GITHUB_WEBHOOK_SECRET=your-webhook-secret
|
|
252
|
-
GITHUB_BOT_NAME=your-bot-name # Default: agent-swarm-bot
|
|
253
|
-
|
|
254
|
-
# Optional: Enable bot reactions (emoji acknowledgments on GitHub)
|
|
255
|
-
GITHUB_APP_ID=123456
|
|
256
|
-
GITHUB_APP_PRIVATE_KEY=base64-encoded-key
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
**Supported events:**
|
|
260
|
-
|
|
261
|
-
| Event | What happens |
|
|
262
|
-
|-------|-------------|
|
|
263
|
-
| Bot assigned to PR/issue | Creates a task for the lead agent |
|
|
264
|
-
| Review requested from bot | Creates a review task |
|
|
265
|
-
| `@bot-name` in comment/issue/PR | Creates a task with the mention context |
|
|
266
|
-
| PR review submitted (on bot's PR) | Creates a notification task with review feedback |
|
|
267
|
-
| CI failure (on PRs with existing tasks) | Creates a CI notification task |
|
|
268
|
-
|
|
269
|
-
<details>
|
|
270
|
-
<summary><strong>Flow Diagrams</strong> (click to expand)</summary>
|
|
271
|
-
|
|
272
|
-
#### Task Creation Flow
|
|
273
|
-
|
|
274
|
-
How GitHub events become tasks in the swarm:
|
|
275
|
-
|
|
276
|
-
```mermaid
|
|
277
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px', 'nodeSpacing': 30, 'rankSpacing': 40}}}%%
|
|
278
|
-
flowchart TB
|
|
279
|
-
subgraph ENTRY["1. GitHub Webhook Entry Points"]
|
|
280
|
-
direction LR
|
|
281
|
-
E1["Issue<br/>opened/edited"]
|
|
282
|
-
E2["PR<br/>opened/edited"]
|
|
283
|
-
E3["Comment<br/>created"]
|
|
284
|
-
E4["Bot Assigned<br/>to Issue/PR"]
|
|
285
|
-
E5["Review Requested<br/>from Bot"]
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
subgraph GATE["2. Trigger Gate"]
|
|
289
|
-
M{"@agent-swarm<br/>mention?"}
|
|
290
|
-
A{"Bot is<br/>assignee?"}
|
|
291
|
-
D{"Duplicate?<br/>60s TTL"}
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
subgraph CREATE["3. Task Creation"]
|
|
295
|
-
LEAD["Find Lead Agent<br/>(online > offline > none)"]
|
|
296
|
-
TPL["resolveTemplate()"]
|
|
297
|
-
TASK["createTaskExtended()"]
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
subgraph OUT["4. Output"]
|
|
301
|
-
ASSIGN["Task assigned<br/>to Lead"]
|
|
302
|
-
POOL["Task in pool<br/>(no lead)"]
|
|
303
|
-
REACT["eyes reaction<br/>on GitHub"]
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
E1 & E2 & E3 --> M
|
|
307
|
-
E4 & E5 --> A
|
|
308
|
-
|
|
309
|
-
M -->|Yes| D
|
|
310
|
-
A -->|Yes| D
|
|
311
|
-
M & A -->|No| DROP1(("skip"))
|
|
312
|
-
|
|
313
|
-
D -->|New| LEAD
|
|
314
|
-
D -->|Dup| DROP2(("skip"))
|
|
315
|
-
|
|
316
|
-
LEAD --> TPL --> TASK
|
|
317
|
-
|
|
318
|
-
TASK -->|lead found| ASSIGN
|
|
319
|
-
TASK -.->|no lead| POOL
|
|
320
|
-
TASK --> REACT
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
[PNG fallback](assets/github-task-creation-flow.png)
|
|
324
|
-
|
|
325
|
-
#### Follow-up Flows
|
|
326
|
-
|
|
327
|
-
Events that create secondary tasks when an active task already exists for a PR:
|
|
328
|
-
|
|
329
|
-
```mermaid
|
|
330
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px'}}}%%
|
|
331
|
-
flowchart TB
|
|
332
|
-
subgraph EVENTS["GitHub Follow-up Events (require existing active task)"]
|
|
333
|
-
direction LR
|
|
334
|
-
F1["PR Closed<br/>(merged/closed)"]
|
|
335
|
-
F2["PR Synchronize<br/>(new commits)"]
|
|
336
|
-
F3["Review Submitted<br/>(approved/changes_requested)"]
|
|
337
|
-
F4["Check Run Failed"]
|
|
338
|
-
F5["Check Suite Failed"]
|
|
339
|
-
F6["Workflow Run Failed"]
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
FIND{"findTaskByVcs()<br/>Active task for<br/>repo + PR number?"}
|
|
343
|
-
|
|
344
|
-
EVENTS --> FIND
|
|
345
|
-
|
|
346
|
-
FIND -->|No task| SKIP(("skip"))
|
|
347
|
-
|
|
348
|
-
subgraph FOLLOWUP["Follow-up Task Created (assigned to Lead)"]
|
|
349
|
-
direction LR
|
|
350
|
-
T1["github-pr-status<br/>PR merged/closed"]
|
|
351
|
-
T2["github-pr-update<br/>New commits pushed"]
|
|
352
|
-
T3["github-review<br/>Review feedback"]
|
|
353
|
-
T4["github-ci<br/>CI failure alert"]
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
F1 --> FIND -->|task found| T1
|
|
357
|
-
F2 --> FIND -->|task found| T2
|
|
358
|
-
F3 --> FIND -->|task found| T3
|
|
359
|
-
F4 & F5 & F6 --> FIND -->|task found| T4
|
|
360
|
-
|
|
361
|
-
NOTE["All follow-up tasks reference<br/>the original task ID for routing"]
|
|
362
|
-
|
|
363
|
-
FOLLOWUP --> NOTE
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
[PNG fallback](assets/github-followup-flows.png)
|
|
367
|
-
|
|
368
|
-
#### Cancellation Flows
|
|
369
|
-
|
|
370
|
-
How unassigning the bot cancels active tasks:
|
|
371
|
-
|
|
372
|
-
```mermaid
|
|
373
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'fontSize': '13px'}}}%%
|
|
374
|
-
flowchart TB
|
|
375
|
-
subgraph EVENTS["Cancellation Events"]
|
|
376
|
-
direction LR
|
|
377
|
-
C1["Bot Unassigned<br/>from Issue"]
|
|
378
|
-
C2["Bot Unassigned<br/>from PR"]
|
|
379
|
-
C3["Review Request<br/>Removed from Bot"]
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
BOT{"isBotAssignee()"}
|
|
383
|
-
FIND{"findTaskByVcs()<br/>Active task?"}
|
|
384
|
-
CANCEL["failTask()<br/>Cancel with reason"]
|
|
385
|
-
NOOP(("no-op"))
|
|
386
|
-
|
|
387
|
-
EVENTS --> BOT
|
|
388
|
-
BOT -->|Not bot| NOOP
|
|
389
|
-
BOT -->|Is bot| FIND
|
|
390
|
-
FIND -->|No task| NOOP
|
|
391
|
-
FIND -->|Task found| CANCEL
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
[PNG fallback](assets/github-cancellation-flows.png)
|
|
395
|
-
|
|
396
|
-
</details>
|
|
397
|
-
|
|
398
|
-
### GitLab
|
|
399
|
-
|
|
400
|
-
Set up a GitLab webhook to receive events when the bot is @mentioned or assigned to issues/MRs.
|
|
401
|
-
|
|
402
|
-
**Webhook URL:** `https://<your-domain>/api/gitlab/webhook`
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
# Add to your .env
|
|
406
|
-
GITLAB_WEBHOOK_SECRET=your-webhook-secret
|
|
407
|
-
GITLAB_TOKEN=your-gitlab-token # PAT or Group Access Token
|
|
408
|
-
GITLAB_BOT_NAME=agent-swarm-bot # Bot name for @mentions
|
|
409
|
-
GITLAB_URL=https://gitlab.com # GitLab instance URL
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
**Supported events:**
|
|
413
|
-
|
|
414
|
-
| Event | What happens |
|
|
415
|
-
|-------|-------------|
|
|
416
|
-
| Bot assigned to MR/issue | Creates a task for the lead agent |
|
|
417
|
-
| `@bot-name` in comment/issue/MR | Creates a task with the mention context |
|
|
418
|
-
| Pipeline failure (on MRs with existing tasks) | Creates a CI notification task |
|
|
419
|
-
|
|
420
|
-
Workers have `glab` CLI pre-installed for GitLab operations (creating MRs, commenting on issues, etc.).
|
|
421
|
-
|
|
422
|
-
### AgentMail
|
|
423
|
-
|
|
424
|
-
Give your agents email addresses via [AgentMail](https://agentmail.to). Emails are routed to agents as tasks or inbox messages.
|
|
425
|
-
|
|
426
|
-
**Webhook URL:** `https://<your-domain>/api/agentmail/webhook`
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
# Add to your .env
|
|
430
|
-
AGENTMAIL_WEBHOOK_SECRET=your-svix-secret
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
Agents self-register which inboxes they receive mail from using the `register-agentmail-inbox` MCP tool. Emails to a worker's inbox become tasks; emails to a lead's inbox become inbox messages for triage. Follow-up emails in the same thread are automatically routed to the same agent.
|
|
434
|
-
|
|
435
|
-
### Sentry
|
|
436
|
-
|
|
437
|
-
Workers can investigate Sentry issues directly with the `/investigate-sentry-issue` command. Add `SENTRY_AUTH_TOKEN` and `SENTRY_ORG` to your worker's environment.
|
|
149
|
+
| Integration | What it does | Setup |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| **Slack** | DM or @mention the bot to create tasks; workers reply in threads | [Guide](https://docs.agent-swarm.dev/docs/guides/slack-integration) |
|
|
152
|
+
| **GitHub App** | @mention or assign the bot on issues/PRs; CI failures create follow-up tasks | [Guide](https://docs.agent-swarm.dev/docs/guides/github-integration) |
|
|
153
|
+
| **GitLab** | Same model as GitHub — webhooks on issues/MRs, `glab` preinstalled in workers | [Guide](https://docs.agent-swarm.dev/docs/guides/gitlab-integration) |
|
|
154
|
+
| **AgentMail** | Give each agent an inbox; emails become tasks or lead messages | [Guide](https://docs.agent-swarm.dev/docs/guides/agentmail-integration) |
|
|
155
|
+
| **Linear** | Bidirectional ticket sync via OAuth + webhooks | [Guide](https://docs.agent-swarm.dev/docs/guides/linear-integration) |
|
|
156
|
+
| **Sentry** | Workers can triage Sentry issues with the `/investigate-sentry-issue` command | [Guide](https://docs.agent-swarm.dev/docs/guides/sentry-integration) |
|
|
438
157
|
|
|
439
158
|
## Dashboard
|
|
440
159
|
|
|
441
|
-
|
|
160
|
+
Real-time monitoring of agents, tasks, and inter-agent chat. Use the hosted version at [app.agent-swarm.dev](https://app.agent-swarm.dev), or run locally:
|
|
442
161
|
|
|
443
162
|
```bash
|
|
444
163
|
cd new-ui && pnpm install && pnpm run dev
|
|
445
164
|
```
|
|
446
165
|
|
|
447
|
-
Opens at `http://localhost:
|
|
166
|
+
Opens at `http://localhost:5274`.
|
|
448
167
|
|
|
449
|
-
## CLI
|
|
168
|
+
## [CLI](https://docs.agent-swarm.dev/docs/reference/cli)
|
|
450
169
|
|
|
451
170
|
```bash
|
|
452
171
|
bunx @desplega.ai/agent-swarm <command>
|
|
@@ -456,33 +175,23 @@ bunx @desplega.ai/agent-swarm <command>
|
|
|
456
175
|
|---------|-------------|
|
|
457
176
|
| `onboard` | Set up a new swarm from scratch (Docker Compose wizard) |
|
|
458
177
|
| `connect` | Connect this project to an existing swarm |
|
|
459
|
-
| `api`
|
|
460
|
-
| `
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `docs` | Open documentation (`--open` to launch in browser) |
|
|
464
|
-
| `artifact` | Manage agent artifacts |
|
|
178
|
+
| `api` | Start the API + MCP HTTP server |
|
|
179
|
+
| `worker` | Run a worker agent |
|
|
180
|
+
| `lead` | Run a lead agent |
|
|
181
|
+
| `docs` | Open documentation (`--open` to launch in browser) |
|
|
465
182
|
|
|
466
183
|
## Deployment
|
|
467
184
|
|
|
468
|
-
For production deployments, see [DEPLOYMENT.md](./DEPLOYMENT.md)
|
|
469
|
-
|
|
470
|
-
- Docker Compose setup with multiple workers
|
|
471
|
-
- systemd deployment for the API server
|
|
472
|
-
- Graceful shutdown and task resume
|
|
473
|
-
- Integration configuration (Slack, GitHub, AgentMail, Sentry)
|
|
185
|
+
For production deployments (Docker Compose with multiple workers, systemd for the API, graceful shutdown, integration config), see [DEPLOYMENT.md](./DEPLOYMENT.md) and the [deployment guide](https://docs.agent-swarm.dev/docs/guides/deployment).
|
|
474
186
|
|
|
475
187
|
## Documentation
|
|
476
188
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
| [CONTRIBUTING.md](./CONTRIBUTING.md) | Development setup and project structure |
|
|
484
|
-
| [UI.md](./UI.md) | Dashboard UI overview |
|
|
485
|
-
| [MCP.md](./MCP.md) | MCP tools reference (auto-generated) |
|
|
189
|
+
Everything lives at **[docs.agent-swarm.dev](https://docs.agent-swarm.dev)**. Good starting points:
|
|
190
|
+
|
|
191
|
+
- [Getting Started](https://docs.agent-swarm.dev/docs/getting-started) — install, configure, and run your first task
|
|
192
|
+
- [Architecture overview](https://docs.agent-swarm.dev/docs/architecture/overview) — how the swarm is wired together
|
|
193
|
+
- [CLI reference](https://docs.agent-swarm.dev/docs/reference/cli) and [Environment variables](https://docs.agent-swarm.dev/docs/reference/environment-variables)
|
|
194
|
+
- [API reference](https://docs.agent-swarm.dev/docs/api-reference) — every HTTP endpoint
|
|
486
195
|
|
|
487
196
|
## Contributing
|
|
488
197
|
|
|
@@ -509,4 +218,4 @@ Join our [Discord](https://discord.gg/KZgfyyDVZa) if you have questions or want
|
|
|
509
218
|
|
|
510
219
|
## License
|
|
511
220
|
|
|
512
|
-
[MIT](./LICENSE) — 2025-2026 [desplega.
|
|
221
|
+
[MIT](./LICENSE) — 2025-2026 [desplega.sh](https://desplega.sh)
|
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.67.
|
|
5
|
+
"version": "1.67.3",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
|
@@ -26,22 +26,39 @@ const ENV_KEY = "SECRETS_ENCRYPTION_KEY";
|
|
|
26
26
|
const ENV_KEY_FILE = "SECRETS_ENCRYPTION_KEY_FILE";
|
|
27
27
|
const KEY_FILENAME = ".encryption-key";
|
|
28
28
|
|
|
29
|
+
const HEX_32_BYTE_RE = /^[0-9a-fA-F]{64}$/;
|
|
30
|
+
|
|
31
|
+
function keyGenerationHelp(): string {
|
|
32
|
+
return [
|
|
33
|
+
"",
|
|
34
|
+
"How to generate a valid key:",
|
|
35
|
+
" openssl rand -base64 32 # 43-char base64 (recommended)",
|
|
36
|
+
" openssl rand -hex 32 # 64-char hex (also accepted)",
|
|
37
|
+
"",
|
|
38
|
+
"Common mistake: `openssl rand -base64 39` produces a 52-char string that",
|
|
39
|
+
"decodes to 39 bytes — the number passed to `openssl rand` is the decoded",
|
|
40
|
+
"byte count, and AES-256 requires exactly 32.",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
function decodeAndValidate(source: string, content: string): Buffer {
|
|
30
45
|
const trimmed = content.trim();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
if (decoded.length !== AES_KEY_BYTES) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Invalid encryption key at ${source}: expected ${AES_KEY_BYTES} bytes after base64 decode, got ${decoded.length} bytes`,
|
|
42
|
-
);
|
|
46
|
+
|
|
47
|
+
// Hex path: a 64-char string of [0-9a-fA-F] is unambiguously hex — it would
|
|
48
|
+
// decode to 48 bytes as base64, never 32, so there is no overlap with the
|
|
49
|
+
// base64 happy path.
|
|
50
|
+
if (HEX_32_BYTE_RE.test(trimmed)) {
|
|
51
|
+
const decoded = Buffer.from(trimmed, "hex");
|
|
52
|
+
if (decoded.length === AES_KEY_BYTES) return decoded;
|
|
43
53
|
}
|
|
44
|
-
|
|
54
|
+
|
|
55
|
+
const decoded = Buffer.from(trimmed, "base64");
|
|
56
|
+
if (decoded.length === AES_KEY_BYTES) return decoded;
|
|
57
|
+
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Invalid encryption key at ${source}: expected ${AES_KEY_BYTES} bytes, ` +
|
|
60
|
+
`got ${decoded.length} bytes after base64 decode.\n${keyGenerationHelp()}`,
|
|
61
|
+
);
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
type ResolveEncryptionKeyOptions = {
|
package/src/be/db.ts
CHANGED
|
@@ -84,6 +84,7 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
84
84
|
const templateBytes = templateGlobals.__testMigrationTemplate;
|
|
85
85
|
if (templateBytes) {
|
|
86
86
|
db = Database.deserialize(templateBytes);
|
|
87
|
+
db.run("PRAGMA busy_timeout = 5000;");
|
|
87
88
|
db.run("PRAGMA foreign_keys = ON;");
|
|
88
89
|
configureDbResolver(resolvePromptTemplate);
|
|
89
90
|
// Ensure the encryption key is resolved even when restoring from the test
|
|
@@ -98,14 +99,24 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
98
99
|
|
|
99
100
|
const database = db;
|
|
100
101
|
database.run("PRAGMA journal_mode = WAL;");
|
|
102
|
+
database.run("PRAGMA busy_timeout = 5000;");
|
|
101
103
|
database.run("PRAGMA foreign_keys = ON;");
|
|
102
104
|
|
|
103
|
-
// Load sqlite-vec extension for vector search
|
|
105
|
+
// Load sqlite-vec extension for vector search.
|
|
106
|
+
// In compiled binaries (`bun build --compile`) the JS lives in /$bunfs/ and
|
|
107
|
+
// `require.resolve("sqlite-vec-<platform>/vec0.so")` can't find the native
|
|
108
|
+
// asset — so we prefer an explicit filesystem path when set, and only fall
|
|
109
|
+
// back to the npm resolver for normal dev runs.
|
|
104
110
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
111
|
+
const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
|
|
112
|
+
if (extensionPath) {
|
|
113
|
+
database.loadExtension(extensionPath);
|
|
114
|
+
} else {
|
|
115
|
+
const sqliteVec = require("sqlite-vec");
|
|
116
|
+
sqliteVec.load(database);
|
|
117
|
+
}
|
|
107
118
|
sqliteVecAvailable = true;
|
|
108
|
-
console.log(
|
|
119
|
+
console.log(`[db] sqlite-vec loaded${extensionPath ? ` from ${extensionPath}` : ""}`);
|
|
109
120
|
} catch (err) {
|
|
110
121
|
console.warn(
|
|
111
122
|
"[db] sqlite-vec not available, falling back to in-memory cosine:",
|
|
@@ -1479,6 +1490,26 @@ export function getMostRecentTaskInThread(channelId: string, threadTs: string):
|
|
|
1479
1490
|
return row ? rowToAgentTask(row) : null;
|
|
1480
1491
|
}
|
|
1481
1492
|
|
|
1493
|
+
export function findCompletedTaskInThread(
|
|
1494
|
+
channelId: string,
|
|
1495
|
+
threadTs: string,
|
|
1496
|
+
windowMinutes: number,
|
|
1497
|
+
): AgentTask | null {
|
|
1498
|
+
const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
|
|
1499
|
+
const row = getDb()
|
|
1500
|
+
.prepare<AgentTaskRow, [string, string, string]>(
|
|
1501
|
+
`SELECT * FROM agent_tasks
|
|
1502
|
+
WHERE slackChannelId = ?
|
|
1503
|
+
AND slackThreadTs = ?
|
|
1504
|
+
AND status = 'completed'
|
|
1505
|
+
AND lastUpdatedAt > ?
|
|
1506
|
+
ORDER BY lastUpdatedAt DESC
|
|
1507
|
+
LIMIT 1`,
|
|
1508
|
+
)
|
|
1509
|
+
.get(channelId, threadTs, since);
|
|
1510
|
+
return row ? rowToAgentTask(row) : null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1482
1513
|
export function completeTask(id: string, output?: string): AgentTask | null {
|
|
1483
1514
|
const oldTask = getTaskById(id);
|
|
1484
1515
|
const finishedAt = new Date().toISOString();
|
package/src/http/index.ts
CHANGED
|
@@ -43,6 +43,15 @@ import { getPathSegments, parseQueryParams, setCorsHeaders } from "./utils";
|
|
|
43
43
|
import { handleWebhooks } from "./webhooks";
|
|
44
44
|
import { handleWorkflows } from "./workflows";
|
|
45
45
|
|
|
46
|
+
// Last-line-of-defense: never let a single bad request (e.g. a SQLITE_BUSY
|
|
47
|
+
// thrown out of a transaction callback) kill the API process. Log and keep going.
|
|
48
|
+
process.on("uncaughtException", (err) => {
|
|
49
|
+
console.error("[fatal] uncaughtException:", err);
|
|
50
|
+
});
|
|
51
|
+
process.on("unhandledRejection", (reason) => {
|
|
52
|
+
console.error("[fatal] unhandledRejection:", reason);
|
|
53
|
+
});
|
|
54
|
+
|
|
46
55
|
const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
|
|
47
56
|
const apiKey = process.env.API_KEY || "";
|
|
48
57
|
|
|
@@ -94,7 +94,7 @@ If you explicitly assign to a different worker, session resume gracefully falls
|
|
|
94
94
|
When a worker completes or fails a task, you receive an automatic follow-up task. Handle it by:
|
|
95
95
|
1. Review the output/failure reason
|
|
96
96
|
2. If the task has Slack metadata, use \`slack-reply\` with the task's ID to post the result back to the originating thread
|
|
97
|
-
3.
|
|
97
|
+
3. Complete this task. Do NOT re-delegate or create new worker tasks from a follow-up \u2014 the worker's result IS the answer. Only escalate to the stakeholder if the worker explicitly failed and the failure needs human attention.
|
|
98
98
|
|
|
99
99
|
#### Heartbeat Checklist
|
|
100
100
|
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
completeTask,
|
|
6
|
+
createAgent,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
findCompletedTaskInThread,
|
|
9
|
+
getDb,
|
|
10
|
+
getTaskById,
|
|
11
|
+
initDb,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-follow-up-redelivery-guard.sqlite";
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
initDb(TEST_DB_PATH);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
closeDb();
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(TEST_DB_PATH);
|
|
24
|
+
unlinkSync(`${TEST_DB_PATH}-wal`);
|
|
25
|
+
unlinkSync(`${TEST_DB_PATH}-shm`);
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore if files don't exist
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("findCompletedTaskInThread", () => {
|
|
32
|
+
test("finds completed tasks in a thread within the time window", () => {
|
|
33
|
+
const agent = createAgent({
|
|
34
|
+
name: "dedup-worker-1",
|
|
35
|
+
isLead: false,
|
|
36
|
+
status: "idle",
|
|
37
|
+
capabilities: [],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const task = createTaskExtended("test task for thread", {
|
|
41
|
+
agentId: agent.id,
|
|
42
|
+
slackChannelId: "C_DEDUP_1",
|
|
43
|
+
slackThreadTs: "1000.0001",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Mark as completed
|
|
47
|
+
completeTask(task.id, "done");
|
|
48
|
+
|
|
49
|
+
// Should find the completed task within a 2880-minute (48h) window
|
|
50
|
+
const result = findCompletedTaskInThread("C_DEDUP_1", "1000.0001", 2880);
|
|
51
|
+
expect(result).not.toBeNull();
|
|
52
|
+
expect(result!.id).toBe(task.id);
|
|
53
|
+
expect(result!.status).toBe("completed");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns null when no completed tasks exist in the thread", () => {
|
|
57
|
+
const agent = createAgent({
|
|
58
|
+
name: "dedup-worker-2",
|
|
59
|
+
isLead: false,
|
|
60
|
+
status: "idle",
|
|
61
|
+
capabilities: [],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create a task but don't complete it
|
|
65
|
+
createTaskExtended("pending task in thread", {
|
|
66
|
+
agentId: agent.id,
|
|
67
|
+
slackChannelId: "C_DEDUP_2",
|
|
68
|
+
slackThreadTs: "2000.0001",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = findCompletedTaskInThread("C_DEDUP_2", "2000.0001", 2880);
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("returns null outside the time window", () => {
|
|
76
|
+
const agent = createAgent({
|
|
77
|
+
name: "dedup-worker-3",
|
|
78
|
+
isLead: false,
|
|
79
|
+
status: "idle",
|
|
80
|
+
capabilities: [],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const task = createTaskExtended("old completed task", {
|
|
84
|
+
agentId: agent.id,
|
|
85
|
+
slackChannelId: "C_DEDUP_3",
|
|
86
|
+
slackThreadTs: "3000.0001",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
completeTask(task.id, "done long ago");
|
|
90
|
+
|
|
91
|
+
// Backdate the lastUpdatedAt to 49 hours ago (beyond the 48h window)
|
|
92
|
+
const fortyNineHoursAgo = new Date(Date.now() - 49 * 60 * 60 * 1000).toISOString();
|
|
93
|
+
getDb().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
94
|
+
fortyNineHoursAgo,
|
|
95
|
+
task.id,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// Should not find with a 48 hour window
|
|
99
|
+
const result = findCompletedTaskInThread("C_DEDUP_3", "3000.0001", 2880);
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns null for a different thread", () => {
|
|
104
|
+
const agent = createAgent({
|
|
105
|
+
name: "dedup-worker-4",
|
|
106
|
+
isLead: false,
|
|
107
|
+
status: "idle",
|
|
108
|
+
capabilities: [],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const task = createTaskExtended("task in different thread", {
|
|
112
|
+
agentId: agent.id,
|
|
113
|
+
slackChannelId: "C_DEDUP_4",
|
|
114
|
+
slackThreadTs: "4000.0001",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
completeTask(task.id, "done");
|
|
118
|
+
|
|
119
|
+
// Search in a different thread — should not find
|
|
120
|
+
const result = findCompletedTaskInThread("C_DEDUP_4", "4000.9999", 2880);
|
|
121
|
+
expect(result).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("follow-up re-delegation guard logic", () => {
|
|
126
|
+
let leadAgent: ReturnType<typeof createAgent>;
|
|
127
|
+
let workerAgent: ReturnType<typeof createAgent>;
|
|
128
|
+
|
|
129
|
+
beforeAll(() => {
|
|
130
|
+
leadAgent = createAgent({
|
|
131
|
+
name: "guard-lead",
|
|
132
|
+
isLead: true,
|
|
133
|
+
status: "idle",
|
|
134
|
+
capabilities: [],
|
|
135
|
+
});
|
|
136
|
+
workerAgent = createAgent({
|
|
137
|
+
name: "guard-worker",
|
|
138
|
+
isLead: false,
|
|
139
|
+
status: "idle",
|
|
140
|
+
capabilities: [],
|
|
141
|
+
maxTasks: 5,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("blocks re-delegation when source task is a follow-up and thread has completed work", () => {
|
|
146
|
+
// Step 1: Create and complete a worker task in a Slack thread
|
|
147
|
+
const workerTask = createTaskExtended("implement feature X", {
|
|
148
|
+
agentId: workerAgent.id,
|
|
149
|
+
slackChannelId: "C_GUARD_1",
|
|
150
|
+
slackThreadTs: "5000.0001",
|
|
151
|
+
});
|
|
152
|
+
completeTask(workerTask.id, "Feature X implemented");
|
|
153
|
+
|
|
154
|
+
// Step 2: Create a follow-up task (as store-progress would)
|
|
155
|
+
const followUpTask = createTaskExtended("Worker task completed — review needed.", {
|
|
156
|
+
agentId: leadAgent.id,
|
|
157
|
+
source: "system",
|
|
158
|
+
taskType: "follow-up",
|
|
159
|
+
parentTaskId: workerTask.id,
|
|
160
|
+
slackChannelId: "C_GUARD_1",
|
|
161
|
+
slackThreadTs: "5000.0001",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Step 3: Simulate the guard logic from send-task.ts
|
|
165
|
+
// The lead's sourceTaskId would be the follow-up task
|
|
166
|
+
const sourceTask = getTaskById(followUpTask.id);
|
|
167
|
+
expect(sourceTask).not.toBeNull();
|
|
168
|
+
expect(sourceTask!.taskType).toBe("follow-up");
|
|
169
|
+
expect(sourceTask!.slackChannelId).toBe("C_GUARD_1");
|
|
170
|
+
expect(sourceTask!.slackThreadTs).toBe("5000.0001");
|
|
171
|
+
|
|
172
|
+
// The guard should find the completed worker task
|
|
173
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
174
|
+
sourceTask!.slackChannelId!,
|
|
175
|
+
sourceTask!.slackThreadTs!,
|
|
176
|
+
2880,
|
|
177
|
+
);
|
|
178
|
+
expect(recentCompleted).not.toBeNull();
|
|
179
|
+
expect(recentCompleted!.id).toBe(workerTask.id);
|
|
180
|
+
|
|
181
|
+
// → Guard would block: re-delegation should be prevented
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("allows delegation when source task is NOT a follow-up (normal behavior)", () => {
|
|
185
|
+
// Create a normal Slack task (not a follow-up)
|
|
186
|
+
const slackTask = createTaskExtended("user asked a question", {
|
|
187
|
+
agentId: leadAgent.id,
|
|
188
|
+
source: "slack",
|
|
189
|
+
taskType: "inbox",
|
|
190
|
+
slackChannelId: "C_GUARD_2",
|
|
191
|
+
slackThreadTs: "6000.0001",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Even if there are completed tasks in the thread, guard shouldn't trigger
|
|
195
|
+
// because the source task is not a "follow-up"
|
|
196
|
+
const sourceTask = getTaskById(slackTask.id);
|
|
197
|
+
expect(sourceTask).not.toBeNull();
|
|
198
|
+
expect(sourceTask!.taskType).not.toBe("follow-up");
|
|
199
|
+
|
|
200
|
+
// Guard condition: sourceTask?.taskType === "follow-up" → false
|
|
201
|
+
// → Guard does NOT block: delegation proceeds normally
|
|
202
|
+
const shouldBlock =
|
|
203
|
+
sourceTask?.taskType === "follow-up" && sourceTask.slackThreadTs && sourceTask.slackChannelId;
|
|
204
|
+
expect(shouldBlock).toBeFalsy();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("allows delegation when source task is a follow-up but thread has NO completed work", () => {
|
|
208
|
+
// Create a follow-up task in a thread with no completed work
|
|
209
|
+
const followUpTask = createTaskExtended("Worker task failed — action needed.", {
|
|
210
|
+
agentId: leadAgent.id,
|
|
211
|
+
source: "system",
|
|
212
|
+
taskType: "follow-up",
|
|
213
|
+
slackChannelId: "C_GUARD_3",
|
|
214
|
+
slackThreadTs: "7000.0001",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const sourceTask = getTaskById(followUpTask.id);
|
|
218
|
+
expect(sourceTask).not.toBeNull();
|
|
219
|
+
expect(sourceTask!.taskType).toBe("follow-up");
|
|
220
|
+
|
|
221
|
+
// No completed tasks in this thread
|
|
222
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
223
|
+
sourceTask!.slackChannelId!,
|
|
224
|
+
sourceTask!.slackThreadTs!,
|
|
225
|
+
2880,
|
|
226
|
+
);
|
|
227
|
+
expect(recentCompleted).toBeNull();
|
|
228
|
+
|
|
229
|
+
// → Guard does NOT block: first-time delegation is fine
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("allows delegation when source task is a follow-up but completed work is outside time window", () => {
|
|
233
|
+
// Create and complete a worker task, then backdate it
|
|
234
|
+
const oldWorkerTask = createTaskExtended("old task", {
|
|
235
|
+
agentId: workerAgent.id,
|
|
236
|
+
slackChannelId: "C_GUARD_4",
|
|
237
|
+
slackThreadTs: "8000.0001",
|
|
238
|
+
});
|
|
239
|
+
completeTask(oldWorkerTask.id, "done long ago");
|
|
240
|
+
|
|
241
|
+
// Backdate to 49 hours ago (beyond the 48h window)
|
|
242
|
+
const fortyNineHoursAgo = new Date(Date.now() - 49 * 60 * 60 * 1000).toISOString();
|
|
243
|
+
getDb().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
244
|
+
fortyNineHoursAgo,
|
|
245
|
+
oldWorkerTask.id,
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
// Create a follow-up in the same thread
|
|
249
|
+
const followUpTask = createTaskExtended("Worker task completed — review needed.", {
|
|
250
|
+
agentId: leadAgent.id,
|
|
251
|
+
source: "system",
|
|
252
|
+
taskType: "follow-up",
|
|
253
|
+
slackChannelId: "C_GUARD_4",
|
|
254
|
+
slackThreadTs: "8000.0001",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const sourceTask = getTaskById(followUpTask.id);
|
|
258
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
259
|
+
sourceTask!.slackChannelId!,
|
|
260
|
+
sourceTask!.slackThreadTs!,
|
|
261
|
+
2880,
|
|
262
|
+
);
|
|
263
|
+
expect(recentCompleted).toBeNull();
|
|
264
|
+
|
|
265
|
+
// → Guard does NOT block: completed work is too old
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -157,6 +157,57 @@ describe("key-bootstrap: validation", () => {
|
|
|
157
157
|
expect(() => resolveEncryptionKey(dbPath)).toThrow(/got 16 bytes/);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
it("error message includes openssl generation commands and the -base64 39 hint", () => {
|
|
161
|
+
process.env[ENV_KEY] = Buffer.alloc(16).toString("base64");
|
|
162
|
+
const dbPath = path.join(tmpDir, "test.sqlite");
|
|
163
|
+
let caught: Error | null = null;
|
|
164
|
+
try {
|
|
165
|
+
resolveEncryptionKey(dbPath);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
caught = err as Error;
|
|
168
|
+
}
|
|
169
|
+
expect(caught).toBeTruthy();
|
|
170
|
+
const msg = caught?.message ?? "";
|
|
171
|
+
expect(msg).toContain("openssl rand -base64 32");
|
|
172
|
+
expect(msg).toContain("openssl rand -hex 32");
|
|
173
|
+
expect(msg).toContain("openssl rand -base64 39");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("accepts a hex-encoded 32-byte key via SECRETS_ENCRYPTION_KEY env var", () => {
|
|
177
|
+
const keyBytes = randomBytes(AES_KEY_BYTES);
|
|
178
|
+
process.env[ENV_KEY] = keyBytes.toString("hex");
|
|
179
|
+
const dbPath = path.join(tmpDir, "test.sqlite");
|
|
180
|
+
const resolved = resolveEncryptionKey(dbPath);
|
|
181
|
+
expect(resolved.equals(keyBytes)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("accepts a hex-encoded 32-byte key via SECRETS_ENCRYPTION_KEY_FILE", () => {
|
|
185
|
+
const keyBytes = randomBytes(AES_KEY_BYTES);
|
|
186
|
+
const keyPath = path.join(tmpDir, "hex.key");
|
|
187
|
+
writeFileSync(keyPath, keyBytes.toString("hex"));
|
|
188
|
+
process.env[ENV_KEY_FILE] = keyPath;
|
|
189
|
+
const dbPath = path.join(tmpDir, "test.sqlite");
|
|
190
|
+
const resolved = resolveEncryptionKey(dbPath);
|
|
191
|
+
expect(resolved.equals(keyBytes)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("accepts a hex-encoded 32-byte key via on-disk .encryption-key", () => {
|
|
195
|
+
const keyBytes = randomBytes(AES_KEY_BYTES);
|
|
196
|
+
const keyFilePath = path.join(tmpDir, ".encryption-key");
|
|
197
|
+
writeFileSync(keyFilePath, keyBytes.toString("hex"));
|
|
198
|
+
const dbPath = path.join(tmpDir, "test.sqlite");
|
|
199
|
+
const resolved = resolveEncryptionKey(dbPath);
|
|
200
|
+
expect(resolved.equals(keyBytes)).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("accepts uppercase hex-encoded keys", () => {
|
|
204
|
+
const keyBytes = randomBytes(AES_KEY_BYTES);
|
|
205
|
+
process.env[ENV_KEY] = keyBytes.toString("hex").toUpperCase();
|
|
206
|
+
const dbPath = path.join(tmpDir, "test.sqlite");
|
|
207
|
+
const resolved = resolveEncryptionKey(dbPath);
|
|
208
|
+
expect(resolved.equals(keyBytes)).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
160
211
|
it("throws when SECRETS_ENCRYPTION_KEY_FILE points to a malformed file", () => {
|
|
161
212
|
const keyPath = path.join(tmpDir, "bad.key");
|
|
162
213
|
writeFileSync(keyPath, Buffer.alloc(10).toString("base64"));
|
|
@@ -339,6 +339,11 @@ Task: "Fix the login page CSS"
|
|
|
339
339
|
Output:
|
|
340
340
|
Fixed the CSS alignment issue on the login form
|
|
341
341
|
|
|
342
|
+
IMPORTANT: Do NOT re-delegate or re-answer the original request. The worker has already handled it. Your job is ONLY to:
|
|
343
|
+
1. Review the output above
|
|
344
|
+
2. If the task has Slack metadata, use \`slack-reply\` to post the result to the thread (if the worker hasn't already)
|
|
345
|
+
3. Complete this follow-up task
|
|
346
|
+
|
|
342
347
|
Use \`get-task-details\` with taskId "task-abc-123" for full details.`;
|
|
343
348
|
|
|
344
349
|
expect(result.text).toBe(expected);
|
package/src/tools/send-task.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import {
|
|
4
4
|
createTaskExtended,
|
|
5
|
+
findCompletedTaskInThread,
|
|
5
6
|
getActiveTaskCount,
|
|
6
7
|
getAgentById,
|
|
7
8
|
getDb,
|
|
@@ -187,6 +188,36 @@ export const registerSendTaskTool = (server: McpServer) => {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// Guard: prevent re-delegation from follow-up tasks
|
|
192
|
+
// When the source task is a "follow-up" (worker completed/failed notification),
|
|
193
|
+
// check if there are completed tasks in the same Slack thread recently.
|
|
194
|
+
// This prevents the cycle: worker completes → follow-up → Lead re-delegates → repeat.
|
|
195
|
+
if (requestInfo.sourceTaskId) {
|
|
196
|
+
const sourceTask = getTaskById(requestInfo.sourceTaskId);
|
|
197
|
+
if (
|
|
198
|
+
sourceTask?.taskType === "follow-up" &&
|
|
199
|
+
sourceTask.slackThreadTs &&
|
|
200
|
+
sourceTask.slackChannelId
|
|
201
|
+
) {
|
|
202
|
+
const recentCompleted = findCompletedTaskInThread(
|
|
203
|
+
sourceTask.slackChannelId,
|
|
204
|
+
sourceTask.slackThreadTs,
|
|
205
|
+
2880, // 48 hours in minutes
|
|
206
|
+
);
|
|
207
|
+
if (recentCompleted) {
|
|
208
|
+
const msg = `Blocked: re-delegation from follow-up task in a thread that already has completed work (task ${recentCompleted.id.slice(0, 8)}). The original request was already handled.`;
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: msg }],
|
|
211
|
+
structuredContent: {
|
|
212
|
+
yourAgentId: requestInfo.agentId,
|
|
213
|
+
success: false,
|
|
214
|
+
message: msg,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
190
221
|
const txn = getDb().transaction(() => {
|
|
191
222
|
const finalTags = tags;
|
|
192
223
|
|
package/src/tools/templates.ts
CHANGED
|
@@ -22,6 +22,11 @@ Task: "{{task_desc}}"
|
|
|
22
22
|
Output:
|
|
23
23
|
{{output_summary}}
|
|
24
24
|
|
|
25
|
+
IMPORTANT: Do NOT re-delegate or re-answer the original request. The worker has already handled it. Your job is ONLY to:
|
|
26
|
+
1. Review the output above
|
|
27
|
+
2. If the task has Slack metadata, use \`slack-reply\` to post the result to the thread (if the worker hasn't already)
|
|
28
|
+
3. Complete this follow-up task
|
|
29
|
+
|
|
25
30
|
Use \`get-task-details\` with taskId "{{task_id}}" for full details.`,
|
|
26
31
|
variables: [
|
|
27
32
|
{ name: "agent_name", description: "Worker agent name or ID prefix" },
|