@desplega.ai/agent-swarm 1.2.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/.env.docker.example +22 -1
  3. package/.env.example +17 -0
  4. package/.github/workflows/docker-publish.yml +92 -0
  5. package/CONTRIBUTING.md +270 -0
  6. package/DEPLOYMENT.md +391 -0
  7. package/Dockerfile.worker +29 -1
  8. package/FAQ.md +19 -0
  9. package/LICENSE +21 -0
  10. package/MCP.md +249 -0
  11. package/README.md +103 -207
  12. package/assets/agent-swarm-logo-orange.png +0 -0
  13. package/assets/agent-swarm-logo.png +0 -0
  14. package/docker-compose.example.yml +137 -0
  15. package/docker-entrypoint.sh +223 -7
  16. package/package.json +8 -3
  17. package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
  18. package/plugin/README.md +1 -0
  19. package/plugin/agents/.gitkeep +0 -0
  20. package/plugin/agents/codebase-analyzer.md +143 -0
  21. package/plugin/agents/codebase-locator.md +122 -0
  22. package/plugin/agents/codebase-pattern-finder.md +227 -0
  23. package/plugin/agents/web-search-researcher.md +109 -0
  24. package/plugin/commands/create-plan.md +415 -0
  25. package/plugin/commands/implement-plan.md +89 -0
  26. package/plugin/commands/research.md +200 -0
  27. package/plugin/commands/start-leader.md +101 -0
  28. package/plugin/commands/start-worker.md +56 -0
  29. package/plugin/commands/swarm-chat.md +78 -0
  30. package/plugin/commands/todos.md +66 -0
  31. package/plugin/commands/work-on-task.md +44 -0
  32. package/plugin/skills/.gitkeep +0 -0
  33. package/scripts/generate-mcp-docs.ts +415 -0
  34. package/slack-manifest.json +69 -0
  35. package/src/be/db.ts +1431 -25
  36. package/src/cli.tsx +135 -11
  37. package/src/commands/lead.ts +13 -0
  38. package/src/commands/runner.ts +255 -0
  39. package/src/commands/worker.ts +8 -220
  40. package/src/hooks/hook.ts +102 -14
  41. package/src/http.ts +361 -5
  42. package/src/prompts/base-prompt.ts +131 -0
  43. package/src/server.ts +56 -0
  44. package/src/slack/app.ts +73 -0
  45. package/src/slack/commands.ts +88 -0
  46. package/src/slack/handlers.ts +281 -0
  47. package/src/slack/index.ts +3 -0
  48. package/src/slack/responses.ts +175 -0
  49. package/src/slack/router.ts +170 -0
  50. package/src/slack/types.ts +20 -0
  51. package/src/slack/watcher.ts +119 -0
  52. package/src/tools/create-channel.ts +80 -0
  53. package/src/tools/get-tasks.ts +54 -21
  54. package/src/tools/join-swarm.ts +28 -4
  55. package/src/tools/list-channels.ts +37 -0
  56. package/src/tools/list-services.ts +110 -0
  57. package/src/tools/poll-task.ts +46 -3
  58. package/src/tools/post-message.ts +87 -0
  59. package/src/tools/read-messages.ts +192 -0
  60. package/src/tools/register-service.ts +118 -0
  61. package/src/tools/send-task.ts +80 -7
  62. package/src/tools/store-progress.ts +9 -3
  63. package/src/tools/task-action.ts +211 -0
  64. package/src/tools/unregister-service.ts +110 -0
  65. package/src/tools/update-profile.ts +105 -0
  66. package/src/tools/update-service-status.ts +118 -0
  67. package/src/types.ts +110 -3
  68. package/src/utils/pretty-print.ts +224 -0
  69. package/thoughts/shared/plans/.gitkeep +0 -0
  70. package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
  71. package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
  72. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
  73. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
  74. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
  75. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
  76. package/thoughts/shared/research/.gitkeep +0 -0
  77. package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
  78. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
  79. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
  80. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
  81. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
  82. package/tsconfig.json +3 -1
  83. package/ui/bun.lock +692 -0
  84. package/ui/index.html +22 -0
  85. package/ui/package.json +32 -0
  86. package/ui/pnpm-lock.yaml +3034 -0
  87. package/ui/postcss.config.js +6 -0
  88. package/ui/public/logo.png +0 -0
  89. package/ui/src/App.tsx +43 -0
  90. package/ui/src/components/ActivityFeed.tsx +415 -0
  91. package/ui/src/components/AgentDetailPanel.tsx +534 -0
  92. package/ui/src/components/AgentsPanel.tsx +549 -0
  93. package/ui/src/components/ChatPanel.tsx +1820 -0
  94. package/ui/src/components/ConfigModal.tsx +232 -0
  95. package/ui/src/components/Dashboard.tsx +534 -0
  96. package/ui/src/components/Header.tsx +168 -0
  97. package/ui/src/components/ServicesPanel.tsx +612 -0
  98. package/ui/src/components/StatsBar.tsx +288 -0
  99. package/ui/src/components/StatusBadge.tsx +124 -0
  100. package/ui/src/components/TaskDetailPanel.tsx +807 -0
  101. package/ui/src/components/TasksPanel.tsx +575 -0
  102. package/ui/src/hooks/queries.ts +170 -0
  103. package/ui/src/index.css +235 -0
  104. package/ui/src/lib/api.ts +161 -0
  105. package/ui/src/lib/config.ts +35 -0
  106. package/ui/src/lib/theme.ts +214 -0
  107. package/ui/src/lib/utils.ts +48 -0
  108. package/ui/src/main.tsx +32 -0
  109. package/ui/src/types/api.ts +164 -0
  110. package/ui/src/vite-env.d.ts +1 -0
  111. package/ui/tailwind.config.js +35 -0
  112. package/ui/tsconfig.json +31 -0
  113. package/ui/vite.config.ts +22 -0
  114. package/cc-plugin/README.md +0 -49
  115. package/cc-plugin/commands/setup-leader.md +0 -73
  116. package/cc-plugin/commands/start-worker.md +0 -64
  117. package/docker-compose.worker.yml +0 -35
  118. package/example-req-meta.json +0 -24
  119. /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
@@ -1,225 +1,13 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { type RunnerConfig, type RunnerOptions, runAgent } from "./runner.ts";
2
2
 
3
- export interface WorkerOptions {
4
- prompt?: string;
5
- yolo?: boolean;
6
- additionalArgs?: string[];
7
- }
8
-
9
- interface RunClaudeIterationOptions {
10
- prompt: string;
11
- logFile: string;
12
- additionalArgs?: string[];
13
- }
14
-
15
- async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<number> {
16
- const CMD = [
17
- "claude",
18
- "--verbose",
19
- "--output-format",
20
- "stream-json",
21
- "--dangerously-skip-permissions",
22
- "--allow-dangerously-skip-permissions",
23
- "--permission-mode",
24
- "bypassPermissions",
25
- "-p",
26
- opts.prompt,
27
- ];
28
-
29
- if (opts.additionalArgs && opts.additionalArgs.length > 0) {
30
- CMD.push(...opts.additionalArgs);
31
- }
32
-
33
- console.log(`[worker] Running: claude ... -p "${opts.prompt}"`);
34
-
35
- const logFileHandle = Bun.file(opts.logFile).writer();
36
-
37
- // Collect stderr for better error reporting
38
- let stderrOutput = "";
39
-
40
- const proc = Bun.spawn(CMD, {
41
- env: process.env,
42
- stdout: "pipe",
43
- stderr: "pipe",
44
- });
45
-
46
- console.log(`[worker] Process spawned, PID: ${proc.pid}`);
47
- console.log(`[worker] Waiting for output streams...`);
48
-
49
- let stdoutChunks = 0;
50
- let stderrChunks = 0;
51
-
52
- // Read stdout and stderr concurrently
53
- const stdoutPromise = (async () => {
54
- console.log(`[worker] stdout stream: ${proc.stdout ? "available" : "not available"}`);
55
- if (proc.stdout) {
56
- for await (const chunk of proc.stdout) {
57
- stdoutChunks++;
58
- const text = new TextDecoder().decode(chunk);
59
- logFileHandle.write(text);
60
- console.log(`[worker] stdout chunk #${stdoutChunks} (${chunk.length} bytes)`);
61
-
62
- // Also parse and log to console for visibility
63
- const lines = text.split("\n");
64
- for (const line of lines) {
65
- if (line.trim() === "") continue;
66
- try {
67
- const json = JSON.parse(line.trim());
68
- // Log a summary of what's happening
69
- if (json.type === "assistant" && json.message) {
70
- const preview = json.message.slice(0, 100);
71
- console.log(
72
- `[worker] Assistant: ${preview}${json.message.length > 100 ? "..." : ""}`,
73
- );
74
- } else if (json.type === "tool_use") {
75
- console.log(`[worker] Tool: ${json.tool || json.name || "unknown"}`);
76
- } else if (json.type === "result") {
77
- // Log result details
78
- const resultPreview = JSON.stringify(json).slice(0, 200);
79
- console.log(
80
- `[worker] Result: ${resultPreview}${JSON.stringify(json).length > 200 ? "..." : ""}`,
81
- );
82
- } else if (json.type === "error") {
83
- console.error(
84
- `[worker] Error from Claude: ${json.error || json.message || JSON.stringify(json)}`,
85
- );
86
- } else if (json.type === "system") {
87
- // Log system message details
88
- const msg = json.message || json.content || "";
89
- const preview =
90
- typeof msg === "string" ? msg.slice(0, 150) : JSON.stringify(msg).slice(0, 150);
91
- console.log(`[worker] System: ${preview}${preview.length >= 150 ? "..." : ""}`);
92
- } else {
93
- // Log unknown event types with content
94
- console.log(
95
- `[worker] Event type: ${json.type} - ${JSON.stringify(json).slice(0, 100)}`,
96
- );
97
- }
98
- } catch {
99
- // Non-JSON line, just log it
100
- if (line.trim()) {
101
- console.log(`[worker] Raw output: ${line.trim()}`);
102
- }
103
- }
104
- }
105
- }
106
- console.log(`[worker] stdout stream ended (total ${stdoutChunks} chunks)`);
107
- }
108
- })();
109
-
110
- const stderrPromise = (async () => {
111
- console.log(`[worker] stderr stream: ${proc.stderr ? "available" : "not available"}`);
112
- if (proc.stderr) {
113
- for await (const chunk of proc.stderr) {
114
- stderrChunks++;
115
- const text = new TextDecoder().decode(chunk);
116
- stderrOutput += text;
117
- // Log stderr to console immediately
118
- console.error(`[worker] stderr chunk #${stderrChunks}: ${text.trim()}`);
119
- logFileHandle.write(
120
- JSON.stringify({ type: "stderr", content: text, timestamp: new Date().toISOString() }) +
121
- "\n",
122
- );
123
- }
124
- console.log(`[worker] stderr stream ended (total ${stderrChunks} chunks)`);
125
- }
126
- })();
127
-
128
- // Wait for both streams to finish
129
- console.log(`[worker] Waiting for streams to complete...`);
130
- await Promise.all([stdoutPromise, stderrPromise]);
131
-
132
- await logFileHandle.end();
133
- console.log(`[worker] Waiting for process to exit...`);
134
- const exitCode = await proc.exited;
135
-
136
- // Log final status
137
- console.log(`[worker] Claude exited with code ${exitCode}`);
138
- console.log(`[worker] Total stdout chunks: ${stdoutChunks}, stderr chunks: ${stderrChunks}`);
3
+ export type WorkerOptions = RunnerOptions;
139
4
 
140
- if (exitCode !== 0 && stderrOutput) {
141
- console.error(`[worker] Full stderr output:\n${stderrOutput}`);
142
- }
143
-
144
- if (stdoutChunks === 0 && stderrChunks === 0) {
145
- console.warn(`[worker] WARNING: No output received from Claude at all!`);
146
- console.warn(`[worker] This might indicate Claude failed to start or auth issues.`);
147
- }
148
-
149
- return exitCode ?? 1;
150
- }
5
+ const workerConfig: RunnerConfig = {
6
+ role: "worker",
7
+ defaultPrompt: "/start-worker",
8
+ metadataType: "worker_metadata",
9
+ };
151
10
 
152
11
  export async function runWorker(opts: WorkerOptions) {
153
- const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
154
- // WORKER_LOG_DIR env var for Docker, otherwise ./logs
155
- const baseLogDir = process.env.WORKER_LOG_DIR || "./logs";
156
- const logDir = `${baseLogDir}/${sessionId}`;
157
-
158
- // Create log directory
159
- await mkdir(logDir, { recursive: true });
160
-
161
- const defaultPrompt = "/start-worker Start or continue the tasks your leader assigned you!";
162
- const prompt = opts.prompt || defaultPrompt;
163
-
164
- const isYolo = opts.yolo || process.env.WORKER_YOLO === "true";
165
-
166
- console.log(`[worker] Starting worker`);
167
- console.log(`[worker] Session ID: ${sessionId}`);
168
- console.log(`[worker] Log directory: ${logDir}`);
169
- console.log(`[worker] YOLO mode: ${isYolo ? "enabled" : "disabled"}`);
170
- console.log(`[worker] Prompt: ${prompt}`);
171
-
172
- let iteration = 0;
173
-
174
- while (true) {
175
- iteration++;
176
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
177
- const logFile = `${logDir}/${timestamp}.jsonl`;
178
-
179
- console.log(`\n[worker] === Iteration ${iteration} ===`);
180
- console.log(`[worker] Logging to: ${logFile}`);
181
-
182
- // Write iteration metadata at the start of each log file
183
- const metadata = {
184
- type: "worker_metadata",
185
- sessionId,
186
- iteration,
187
- timestamp: new Date().toISOString(),
188
- prompt,
189
- yolo: isYolo,
190
- };
191
- await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
192
-
193
- const exitCode = await runClaudeIteration({
194
- prompt,
195
- logFile,
196
- additionalArgs: opts.additionalArgs,
197
- });
198
-
199
- if (exitCode !== 0) {
200
- const errorLog = {
201
- timestamp: new Date().toISOString(),
202
- iteration,
203
- exitCode,
204
- error: true,
205
- };
206
-
207
- // Append to errors.jsonl
208
- const errorsFile = `${logDir}/errors.jsonl`;
209
- const existingErrors = (await Bun.file(errorsFile).exists())
210
- ? await Bun.file(errorsFile).text()
211
- : "";
212
- await Bun.write(errorsFile, `${existingErrors + JSON.stringify(errorLog)}\n`);
213
-
214
- if (!isYolo) {
215
- console.error(`[worker] Claude exited with code ${exitCode}. Stopping.`);
216
- console.error(`[worker] Error logged to: ${errorsFile}`);
217
- process.exit(exitCode);
218
- }
219
-
220
- console.warn(`[worker] Claude exited with code ${exitCode}. YOLO mode - continuing...`);
221
- }
222
-
223
- console.log(`[worker] Iteration ${iteration} complete. Starting next iteration...`);
224
- }
12
+ return runAgent(workerConfig, opts);
225
13
  }
package/src/hooks/hook.ts CHANGED
@@ -30,6 +30,26 @@ interface HookMessage {
30
30
  stop_hook_active?: boolean;
31
31
  }
32
32
 
33
+ interface MentionPreview {
34
+ channelName: string;
35
+ agentName: string;
36
+ content: string;
37
+ createdAt: string;
38
+ }
39
+
40
+ interface InboxSummary {
41
+ unreadCount: number;
42
+ mentionsCount: number;
43
+ offeredTasksCount: number;
44
+ poolTasksCount: number;
45
+ inProgressCount: number;
46
+ recentMentions: MentionPreview[];
47
+ }
48
+
49
+ interface AgentWithInbox extends Agent {
50
+ inbox?: InboxSummary;
51
+ }
52
+
33
53
  /**
34
54
  * Main hook handler - processes Claude Code hook events
35
55
  */
@@ -97,11 +117,11 @@ export async function handleHook(): Promise<void> {
97
117
  }
98
118
  };
99
119
 
100
- const getAgentInfo = async (): Promise<Agent | undefined> => {
120
+ const getAgentInfo = async (): Promise<AgentWithInbox | undefined> => {
101
121
  if (!mcpConfig) return;
102
122
 
103
123
  try {
104
- const resp = await fetch(`${getBaseUrl()}/me`, {
124
+ const resp = await fetch(`${getBaseUrl()}/me?include=inbox`, {
105
125
  method: "GET",
106
126
  headers: mcpConfig.headers,
107
127
  });
@@ -110,7 +130,7 @@ export async function handleHook(): Promise<void> {
110
130
  return;
111
131
  }
112
132
 
113
- return (await resp.json()) as Agent;
133
+ return (await resp.json()) as AgentWithInbox;
114
134
  } catch {
115
135
  // Silently fail
116
136
  }
@@ -118,18 +138,88 @@ export async function handleHook(): Promise<void> {
118
138
  return;
119
139
  };
120
140
 
141
+ const formatSystemTray = (inbox: InboxSummary): string | null => {
142
+ const {
143
+ unreadCount,
144
+ mentionsCount,
145
+ offeredTasksCount,
146
+ poolTasksCount,
147
+ inProgressCount,
148
+ recentMentions,
149
+ } = inbox;
150
+
151
+ // If all counts are zero, return null (no tray)
152
+ if (
153
+ unreadCount === 0 &&
154
+ offeredTasksCount === 0 &&
155
+ poolTasksCount === 0 &&
156
+ inProgressCount === 0
157
+ ) {
158
+ return null;
159
+ }
160
+
161
+ const lines: string[] = [];
162
+
163
+ // Main tray line
164
+ const parts: string[] = [];
165
+
166
+ // Messages section
167
+ if (unreadCount > 0) {
168
+ const mentionsSuffix = mentionsCount > 0 ? ` (${mentionsCount} @mention)` : "";
169
+ parts.push(`📬 ${unreadCount} unread${mentionsSuffix}`);
170
+ }
171
+
172
+ // Tasks section
173
+ const taskParts = [
174
+ `${offeredTasksCount} offered`,
175
+ `${poolTasksCount} pool`,
176
+ `${inProgressCount} active`,
177
+ ];
178
+ parts.push(`📋 ${taskParts.join(", ")}`);
179
+
180
+ lines.push(parts.join(" | "));
181
+
182
+ // Inline @mentions (up to 3)
183
+ if (recentMentions && recentMentions.length > 0) {
184
+ for (const mention of recentMentions) {
185
+ lines.push(
186
+ ` └─ @mention from ${mention.agentName} in #${mention.channelName}: "${mention.content}"`,
187
+ );
188
+ }
189
+ }
190
+
191
+ // Nudge - remind to check inbox
192
+ if (unreadCount > 0 || offeredTasksCount > 0) {
193
+ const actions: string[] = [];
194
+ if (unreadCount > 0) actions.push("read-messages");
195
+ if (offeredTasksCount > 0) actions.push("poll-task");
196
+ lines.push(`→ Use ${actions.join(" or ")} to check`);
197
+ }
198
+
199
+ return lines.join("\n");
200
+ };
201
+
121
202
  // Ping the server to indicate activity
122
203
  await ping();
123
204
 
124
205
  // Get current agent info
125
206
  const agentInfo = await getAgentInfo();
126
207
 
127
- // Always output agent status
208
+ // Always output agent status with system tray
128
209
  if (agentInfo) {
210
+ // Base status line
129
211
  console.log(
130
- `You are registered as ${agentInfo.isLead ? "lead" : "worker"} agent "${agentInfo.name}" with ID: ${agentInfo.id} (status: ${agentInfo.status}) as of ${new Date().toISOString()}.`,
212
+ `You are registered as ${agentInfo.isLead ? "lead" : "worker"} agent "${agentInfo.name}" (ID: ${agentInfo.id}, status: ${agentInfo.status}).`,
131
213
  );
132
214
 
215
+ // System tray (if there's activity)
216
+ if (agentInfo.inbox) {
217
+ const tray = formatSystemTray(agentInfo.inbox);
218
+ if (tray) {
219
+ console.log(tray);
220
+ }
221
+ }
222
+
133
223
  if (!agentInfo.isLead && agentInfo.status === "busy") {
134
224
  console.log(
135
225
  `Remember to call store-progress periodically to update the lead agent on your progress as you are currently marked as busy. The comments you leave will be helpful for the lead agent to monitor your work.`,
@@ -150,15 +240,7 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
150
240
  case "SessionStart":
151
241
  if (!agentInfo) break;
152
242
 
153
- if (agentInfo.isLead) {
154
- console.log(
155
- `As the lead agent, you are responsible for coordinating the swarm to fulfill the user's request efficiently. Use the ${SERVER_NAME} tools to assign tasks to worker agents and monitor their progress.`,
156
- );
157
- } else {
158
- console.log(
159
- `As a worker agent, you should call the poll-task tool to wait for tasks assigned by the lead agent, unless specified otherwise.`,
160
- );
161
- }
243
+ // Covered by base system prompt
162
244
  break;
163
245
 
164
246
  case "PreCompact":
@@ -192,6 +274,12 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
192
274
  break;
193
275
 
194
276
  case "Stop":
277
+ // Save PM2 processes before shutdown (for container restart persistence)
278
+ try {
279
+ await Bun.$`pm2 save`.quiet();
280
+ } catch {
281
+ // PM2 not available or no processes - silently ignore
282
+ }
195
283
  // Mark the agent as offline
196
284
  await close();
197
285
  break;