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