@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
package/src/cli.tsx CHANGED
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
5
5
  import pkg from "../package.json";
6
6
  import { runClaude } from "./claude.ts";
7
7
  import { runHook } from "./commands/hook.ts";
8
+ import { runLead } from "./commands/lead.ts";
8
9
  import { Setup } from "./commands/setup.tsx";
9
10
  import { runWorker } from "./commands/worker.ts";
10
11
 
@@ -29,6 +30,8 @@ interface ParsedArgs {
29
30
  restore: boolean;
30
31
  yes: boolean;
31
32
  yolo: boolean;
33
+ systemPrompt: string;
34
+ systemPromptFile: string;
32
35
  additionalArgs: string[];
33
36
  }
34
37
 
@@ -42,6 +45,8 @@ function parseArgs(args: string[]): ParsedArgs {
42
45
  let restore = false;
43
46
  let yes = false;
44
47
  let yolo = false;
48
+ let systemPrompt = "";
49
+ let systemPromptFile = "";
45
50
  let additionalArgs: string[] = [];
46
51
 
47
52
  // Find if there's a "--" separator for additional args
@@ -70,10 +75,29 @@ function parseArgs(args: string[]): ParsedArgs {
70
75
  yes = true;
71
76
  } else if (arg === "--yolo") {
72
77
  yolo = true;
78
+ } else if (arg === "--system-prompt") {
79
+ systemPrompt = mainArgs[i + 1] || systemPrompt;
80
+ i++;
81
+ } else if (arg === "--system-prompt-file") {
82
+ systemPromptFile = mainArgs[i + 1] || systemPromptFile;
83
+ i++;
73
84
  }
74
85
  }
75
86
 
76
- return { command, port, key, msg, headless, dryRun, restore, yes, yolo, additionalArgs };
87
+ return {
88
+ command,
89
+ port,
90
+ key,
91
+ msg,
92
+ headless,
93
+ dryRun,
94
+ restore,
95
+ yes,
96
+ yolo,
97
+ systemPrompt,
98
+ systemPromptFile,
99
+ additionalArgs,
100
+ };
77
101
  }
78
102
 
79
103
  function Help() {
@@ -136,6 +160,12 @@ function Help() {
136
160
  </Box>
137
161
  <Text>Run Claude in headless loop mode</Text>
138
162
  </Box>
163
+ <Box>
164
+ <Box width={12}>
165
+ <Text color="green">lead</Text>
166
+ </Box>
167
+ <Text>Run Claude as lead agent in headless loop</Text>
168
+ </Box>
139
169
  <Box>
140
170
  <Box width={12}>
141
171
  <Text color="green">version</Text>
@@ -211,21 +241,33 @@ function Help() {
211
241
  </Box>
212
242
 
213
243
  <Box marginTop={1} flexDirection="column">
214
- <Text bold>Options for 'worker':</Text>
244
+ <Text bold>Options for 'worker' and 'lead':</Text>
215
245
  <Box>
216
- <Box width={24}>
246
+ <Box width={30}>
217
247
  <Text color="yellow">-m, --msg {"<prompt>"}</Text>
218
248
  </Box>
219
249
  <Text>Custom prompt (default: /agent-swarm:start-worker)</Text>
220
250
  </Box>
221
251
  <Box>
222
- <Box width={24}>
252
+ <Box width={30}>
223
253
  <Text color="yellow">--yolo</Text>
224
254
  </Box>
225
255
  <Text>Continue on errors instead of stopping</Text>
226
256
  </Box>
227
257
  <Box>
228
- <Box width={24}>
258
+ <Box width={30}>
259
+ <Text color="yellow">--system-prompt {"<text>"}</Text>
260
+ </Box>
261
+ <Text>Custom system prompt (appended to Claude)</Text>
262
+ </Box>
263
+ <Box>
264
+ <Box width={30}>
265
+ <Text color="yellow">--system-prompt-file {"<path>"}</Text>
266
+ </Box>
267
+ <Text>Read system prompt from file</Text>
268
+ </Box>
269
+ <Box>
270
+ <Box width={30}>
229
271
  <Text color="yellow">-- {"<args...>"}</Text>
230
272
  </Box>
231
273
  <Text>Additional arguments to pass to Claude CLI</Text>
@@ -246,6 +288,12 @@ function Help() {
246
288
  <Text dimColor> {binName} worker</Text>
247
289
  <Text dimColor> {binName} worker --yolo</Text>
248
290
  <Text dimColor> {binName} worker -m "Custom prompt"</Text>
291
+ <Text dimColor> {binName} worker --system-prompt "You are a Python specialist"</Text>
292
+ <Text dimColor> {binName} worker --system-prompt-file ./prompts/specialist.txt</Text>
293
+ <Text dimColor> {binName} lead</Text>
294
+ <Text dimColor> {binName} lead --yolo</Text>
295
+ <Text dimColor> {binName} lead -m "Custom prompt"</Text>
296
+ <Text dimColor> {binName} lead --system-prompt "You are a project coordinator"</Text>
249
297
  </Box>
250
298
 
251
299
  <Box marginTop={1} flexDirection="column">
@@ -282,10 +330,28 @@ function Help() {
282
330
  </Box>
283
331
  <Box>
284
332
  <Box width={24}>
285
- <Text color="magenta">WORKER_YOLO</Text>
333
+ <Text color="magenta">YOLO</Text>
286
334
  </Box>
287
335
  <Text>If "true", worker continues on errors</Text>
288
336
  </Box>
337
+ <Box>
338
+ <Box width={32}>
339
+ <Text color="magenta">LOG_DIR</Text>
340
+ </Box>
341
+ <Text>Directory for agent logs, defaults to ./logs</Text>
342
+ </Box>
343
+ <Box>
344
+ <Box width={32}>
345
+ <Text color="magenta">SYSTEM_PROMPT</Text>
346
+ </Box>
347
+ <Text>Custom system prompt for worker</Text>
348
+ </Box>
349
+ <Box>
350
+ <Box width={32}>
351
+ <Text color="magenta">SYSTEM_PROMPT_FILE</Text>
352
+ </Box>
353
+ <Text>Path to system prompt file</Text>
354
+ </Box>
289
355
  </Box>
290
356
  </Box>
291
357
  );
@@ -363,23 +429,50 @@ function ClaudeRunner({ msg, headless, additionalArgs }: ClaudeRunnerProps) {
363
429
  return null;
364
430
  }
365
431
 
366
- interface WorkerRunnerProps {
432
+ interface RunnerProps {
367
433
  prompt: string;
368
434
  yolo: boolean;
435
+ systemPrompt: string;
436
+ systemPromptFile: string;
369
437
  additionalArgs: string[];
370
438
  }
371
439
 
372
- function WorkerRunner({ prompt, yolo, additionalArgs }: WorkerRunnerProps) {
440
+ function WorkerRunner({
441
+ prompt,
442
+ yolo,
443
+ systemPrompt,
444
+ systemPromptFile,
445
+ additionalArgs,
446
+ }: RunnerProps) {
373
447
  const { exit } = useApp();
374
448
 
375
449
  useEffect(() => {
376
450
  runWorker({
377
451
  prompt: prompt || undefined,
378
452
  yolo,
453
+ systemPrompt: systemPrompt || undefined,
454
+ systemPromptFile: systemPromptFile || undefined,
379
455
  additionalArgs,
380
456
  }).catch((err) => exit(err));
381
457
  // Note: runWorker runs indefinitely, so we don't call exit() on success
382
- }, [prompt, yolo, additionalArgs, exit]);
458
+ }, [prompt, yolo, systemPrompt, systemPromptFile, additionalArgs, exit]);
459
+
460
+ return null;
461
+ }
462
+
463
+ function LeadRunner({ prompt, yolo, systemPrompt, systemPromptFile, additionalArgs }: RunnerProps) {
464
+ const { exit } = useApp();
465
+
466
+ useEffect(() => {
467
+ runLead({
468
+ prompt: prompt || undefined,
469
+ yolo,
470
+ systemPrompt: systemPrompt || undefined,
471
+ systemPromptFile: systemPromptFile || undefined,
472
+ additionalArgs,
473
+ }).catch((err) => exit(err));
474
+ // Note: runLead runs indefinitely, so we don't call exit() on success
475
+ }, [prompt, yolo, systemPrompt, systemPromptFile, additionalArgs, exit]);
383
476
 
384
477
  return null;
385
478
  }
@@ -414,7 +507,20 @@ function Version() {
414
507
  }
415
508
 
416
509
  function App({ args }: { args: ParsedArgs }) {
417
- const { command, port, key, msg, headless, dryRun, restore, yes, yolo, additionalArgs } = args;
510
+ const {
511
+ command,
512
+ port,
513
+ key,
514
+ msg,
515
+ headless,
516
+ dryRun,
517
+ restore,
518
+ yes,
519
+ yolo,
520
+ systemPrompt,
521
+ systemPromptFile,
522
+ additionalArgs,
523
+ } = args;
418
524
 
419
525
  switch (command) {
420
526
  case "setup":
@@ -424,7 +530,25 @@ function App({ args }: { args: ParsedArgs }) {
424
530
  case "claude":
425
531
  return <ClaudeRunner msg={msg} headless={headless} additionalArgs={additionalArgs} />;
426
532
  case "worker":
427
- return <WorkerRunner prompt={msg} yolo={yolo} additionalArgs={additionalArgs} />;
533
+ return (
534
+ <WorkerRunner
535
+ prompt={msg}
536
+ yolo={yolo}
537
+ systemPrompt={systemPrompt}
538
+ systemPromptFile={systemPromptFile}
539
+ additionalArgs={additionalArgs}
540
+ />
541
+ );
542
+ case "lead":
543
+ return (
544
+ <LeadRunner
545
+ prompt={msg}
546
+ yolo={yolo}
547
+ systemPrompt={systemPrompt}
548
+ systemPromptFile={systemPromptFile}
549
+ additionalArgs={additionalArgs}
550
+ />
551
+ );
428
552
  case "version":
429
553
  return <Version />;
430
554
  case "help":
@@ -0,0 +1,13 @@
1
+ import { type RunnerConfig, type RunnerOptions, runAgent } from "./runner.ts";
2
+
3
+ export type LeadOptions = RunnerOptions;
4
+
5
+ const leadConfig: RunnerConfig = {
6
+ role: "lead",
7
+ defaultPrompt: "/start-leader",
8
+ metadataType: "lead_metadata",
9
+ };
10
+
11
+ export async function runLead(opts: LeadOptions) {
12
+ return runAgent(leadConfig, opts);
13
+ }
@@ -0,0 +1,255 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { getBasePrompt } from "../prompts/base-prompt.ts";
3
+ import { prettyPrintLine, prettyPrintStderr } from "../utils/pretty-print.ts";
4
+
5
+ /** Save PM2 process list for persistence across container restarts */
6
+ async function savePm2State(role: string): Promise<void> {
7
+ try {
8
+ console.log(`[${role}] Saving PM2 process list...`);
9
+ await Bun.$`pm2 save`.quiet();
10
+ console.log(`[${role}] PM2 state saved`);
11
+ } catch {
12
+ // PM2 not available or no processes - silently ignore
13
+ }
14
+ }
15
+
16
+ /** Setup signal handlers for graceful shutdown */
17
+ function setupShutdownHandlers(role: string): void {
18
+ const shutdown = async (signal: string) => {
19
+ console.log(`\n[${role}] Received ${signal}, shutting down...`);
20
+ await savePm2State(role);
21
+ process.exit(0);
22
+ };
23
+
24
+ process.on("SIGINT", () => shutdown("SIGINT"));
25
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
26
+ }
27
+
28
+ /** Configuration for a runner role (worker or lead) */
29
+ export interface RunnerConfig {
30
+ /** Role name for logging, e.g., "worker" or "lead" */
31
+ role: string;
32
+ /** Default prompt if none provided */
33
+ defaultPrompt: string;
34
+ /** Metadata type for log files, e.g., "worker_metadata" */
35
+ metadataType: string;
36
+ }
37
+
38
+ export interface RunnerOptions {
39
+ prompt?: string;
40
+ yolo?: boolean;
41
+ systemPrompt?: string;
42
+ systemPromptFile?: string;
43
+ additionalArgs?: string[];
44
+ }
45
+
46
+ interface RunClaudeIterationOptions {
47
+ prompt: string;
48
+ logFile: string;
49
+ systemPrompt?: string;
50
+ additionalArgs?: string[];
51
+ role: string;
52
+ }
53
+
54
+ async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<number> {
55
+ const { role } = opts;
56
+ const CMD = [
57
+ "claude",
58
+ "--verbose",
59
+ "--output-format",
60
+ "stream-json",
61
+ "--dangerously-skip-permissions",
62
+ "--allow-dangerously-skip-permissions",
63
+ "--permission-mode",
64
+ "bypassPermissions",
65
+ "-p",
66
+ opts.prompt,
67
+ ];
68
+
69
+ if (opts.additionalArgs && opts.additionalArgs.length > 0) {
70
+ CMD.push(...opts.additionalArgs);
71
+ }
72
+
73
+ if (opts.systemPrompt) {
74
+ CMD.push("--append-system-prompt", opts.systemPrompt);
75
+ }
76
+
77
+ console.log(`\x1b[2m[${role}]\x1b[0m \x1b[36m▸\x1b[0m Starting Claude (PID will follow)`);
78
+
79
+ const logFileHandle = Bun.file(opts.logFile).writer();
80
+ let stderrOutput = "";
81
+
82
+ const proc = Bun.spawn(CMD, {
83
+ env: process.env,
84
+ stdout: "pipe",
85
+ stderr: "pipe",
86
+ });
87
+
88
+ let stdoutChunks = 0;
89
+ let stderrChunks = 0;
90
+
91
+ const stdoutPromise = (async () => {
92
+ if (proc.stdout) {
93
+ for await (const chunk of proc.stdout) {
94
+ stdoutChunks++;
95
+ const text = new TextDecoder().decode(chunk);
96
+ logFileHandle.write(text);
97
+
98
+ const lines = text.split("\n");
99
+ for (const line of lines) {
100
+ prettyPrintLine(line, role);
101
+ }
102
+ }
103
+ }
104
+ })();
105
+
106
+ const stderrPromise = (async () => {
107
+ if (proc.stderr) {
108
+ for await (const chunk of proc.stderr) {
109
+ stderrChunks++;
110
+ const text = new TextDecoder().decode(chunk);
111
+ stderrOutput += text;
112
+ prettyPrintStderr(text, role);
113
+ logFileHandle.write(
114
+ `${JSON.stringify({ type: "stderr", content: text, timestamp: new Date().toISOString() })}\n`,
115
+ );
116
+ }
117
+ }
118
+ })();
119
+
120
+ await Promise.all([stdoutPromise, stderrPromise]);
121
+ await logFileHandle.end();
122
+ const exitCode = await proc.exited;
123
+
124
+ if (exitCode !== 0 && stderrOutput) {
125
+ console.error(`\x1b[31m[${role}] Full stderr:\x1b[0m\n${stderrOutput}`);
126
+ }
127
+
128
+ if (stdoutChunks === 0 && stderrChunks === 0) {
129
+ console.warn(`\x1b[33m[${role}] WARNING: No output from Claude - check auth/startup\x1b[0m`);
130
+ }
131
+
132
+ return exitCode ?? 1;
133
+ }
134
+
135
+ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
136
+ const { role, defaultPrompt, metadataType } = config;
137
+
138
+ // Setup graceful shutdown handlers (saves PM2 state on Ctrl+C)
139
+ setupShutdownHandlers(role);
140
+
141
+ const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
142
+ const baseLogDir = process.env.LOG_DIR || "/logs";
143
+ const logDir = `${baseLogDir}/${sessionId}`;
144
+
145
+ await mkdir(logDir, { recursive: true });
146
+
147
+ const prompt = opts.prompt || defaultPrompt;
148
+ const isYolo = opts.yolo || process.env.YOLO === "true";
149
+
150
+ // Get agent identity and swarm URL for base prompt
151
+ const agentId = process.env.AGENT_ID || "unknown";
152
+ const swarmUrl = process.env.SWARM_URL || "localhost";
153
+
154
+ // Generate base prompt that's always included
155
+ const basePrompt = getBasePrompt({ role, agentId, swarmUrl });
156
+
157
+ // Resolve additional system prompt: CLI flag > env var
158
+ let additionalSystemPrompt: string | undefined;
159
+ const systemPromptText = opts.systemPrompt || process.env.SYSTEM_PROMPT;
160
+ const systemPromptFilePath = opts.systemPromptFile || process.env.SYSTEM_PROMPT_FILE;
161
+
162
+ if (systemPromptText) {
163
+ additionalSystemPrompt = systemPromptText;
164
+ console.log(
165
+ `[${role}] Using additional system prompt from ${opts.systemPrompt ? "CLI flag" : "env var"}`,
166
+ );
167
+ } else if (systemPromptFilePath) {
168
+ try {
169
+ const file = Bun.file(systemPromptFilePath);
170
+ if (!(await file.exists())) {
171
+ console.error(`[${role}] ERROR: System prompt file not found: ${systemPromptFilePath}`);
172
+ process.exit(1);
173
+ }
174
+ additionalSystemPrompt = await file.text();
175
+ console.log(`[${role}] Loaded additional system prompt from file: ${systemPromptFilePath}`);
176
+ console.log(
177
+ `[${role}] Additional system prompt length: ${additionalSystemPrompt.length} characters`,
178
+ );
179
+ } catch (error) {
180
+ console.error(`[${role}] ERROR: Failed to read system prompt file: ${systemPromptFilePath}`);
181
+ console.error(error);
182
+ process.exit(1);
183
+ }
184
+ }
185
+
186
+ // Combine base prompt with any additional system prompt
187
+ const resolvedSystemPrompt = additionalSystemPrompt
188
+ ? `${basePrompt}\n\n${additionalSystemPrompt}`
189
+ : basePrompt;
190
+
191
+ console.log(`[${role}] Starting ${role}`);
192
+ console.log(`[${role}] Session ID: ${sessionId}`);
193
+ console.log(`[${role}] Log directory: ${logDir}`);
194
+ console.log(`[${role}] YOLO mode: ${isYolo ? "enabled" : "disabled"}`);
195
+ console.log(`[${role}] Prompt: ${prompt}`);
196
+ console.log(`[${role}] Swarm URL: ${swarmUrl}`);
197
+ console.log(`[${role}] Base prompt: included (${basePrompt.length} chars)`);
198
+ console.log(
199
+ `[${role}] Additional system prompt: ${additionalSystemPrompt ? "provided" : "none"}`,
200
+ );
201
+ console.log(`[${role}] Total system prompt length: ${resolvedSystemPrompt.length} chars`);
202
+
203
+ let iteration = 0;
204
+
205
+ while (true) {
206
+ iteration++;
207
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
208
+ const logFile = `${logDir}/${timestamp}.jsonl`;
209
+
210
+ console.log(`\n[${role}] === Iteration ${iteration} ===`);
211
+ console.log(`[${role}] Logging to: ${logFile}`);
212
+
213
+ const metadata = {
214
+ type: metadataType,
215
+ sessionId,
216
+ iteration,
217
+ timestamp: new Date().toISOString(),
218
+ prompt,
219
+ yolo: isYolo,
220
+ };
221
+ await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
222
+
223
+ const exitCode = await runClaudeIteration({
224
+ prompt,
225
+ logFile,
226
+ systemPrompt: resolvedSystemPrompt,
227
+ additionalArgs: opts.additionalArgs,
228
+ role,
229
+ });
230
+
231
+ if (exitCode !== 0) {
232
+ const errorLog = {
233
+ timestamp: new Date().toISOString(),
234
+ iteration,
235
+ exitCode,
236
+ error: true,
237
+ };
238
+
239
+ const errorsFile = `${logDir}/errors.jsonl`;
240
+ const errorsFileRef = Bun.file(errorsFile);
241
+ const existingErrors = (await errorsFileRef.exists()) ? await errorsFileRef.text() : "";
242
+ await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
243
+
244
+ if (!isYolo) {
245
+ console.error(`[${role}] Claude exited with code ${exitCode}. Stopping.`);
246
+ console.error(`[${role}] Error logged to: ${errorsFile}`);
247
+ process.exit(exitCode);
248
+ }
249
+
250
+ console.warn(`[${role}] Claude exited with code ${exitCode}. YOLO mode - continuing...`);
251
+ }
252
+
253
+ console.log(`[${role}] Iteration ${iteration} complete. Starting next iteration...`);
254
+ }
255
+ }