agent-relay 0.1.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 (143) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +22 -0
  3. package/PROTOCOL.md +319 -0
  4. package/README.md +791 -0
  5. package/dist/cli/index.d.ts +7 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +1591 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/daemon/connection.d.ts +60 -0
  10. package/dist/daemon/connection.d.ts.map +1 -0
  11. package/dist/daemon/connection.js +245 -0
  12. package/dist/daemon/connection.js.map +1 -0
  13. package/dist/daemon/index.d.ts +4 -0
  14. package/dist/daemon/index.d.ts.map +1 -0
  15. package/dist/daemon/index.js +4 -0
  16. package/dist/daemon/index.js.map +1 -0
  17. package/dist/daemon/router.d.ts +72 -0
  18. package/dist/daemon/router.d.ts.map +1 -0
  19. package/dist/daemon/router.js +183 -0
  20. package/dist/daemon/router.js.map +1 -0
  21. package/dist/daemon/server.d.ts +52 -0
  22. package/dist/daemon/server.d.ts.map +1 -0
  23. package/dist/daemon/server.js +186 -0
  24. package/dist/daemon/server.js.map +1 -0
  25. package/dist/dashboard/public/index.html +690 -0
  26. package/dist/dashboard/server.d.ts +2 -0
  27. package/dist/dashboard/server.d.ts.map +1 -0
  28. package/dist/dashboard/server.js +220 -0
  29. package/dist/dashboard/server.js.map +1 -0
  30. package/dist/games/index.d.ts +2 -0
  31. package/dist/games/index.d.ts.map +1 -0
  32. package/dist/games/index.js +2 -0
  33. package/dist/games/index.js.map +1 -0
  34. package/dist/games/tictactoe.d.ts +24 -0
  35. package/dist/games/tictactoe.d.ts.map +1 -0
  36. package/dist/games/tictactoe.js +160 -0
  37. package/dist/games/tictactoe.js.map +1 -0
  38. package/dist/hooks/inbox-check/hook.d.ts +28 -0
  39. package/dist/hooks/inbox-check/hook.d.ts.map +1 -0
  40. package/dist/hooks/inbox-check/hook.js +97 -0
  41. package/dist/hooks/inbox-check/hook.js.map +1 -0
  42. package/dist/hooks/inbox-check/index.d.ts +8 -0
  43. package/dist/hooks/inbox-check/index.d.ts.map +1 -0
  44. package/dist/hooks/inbox-check/index.js +8 -0
  45. package/dist/hooks/inbox-check/index.js.map +1 -0
  46. package/dist/hooks/inbox-check/types.d.ts +31 -0
  47. package/dist/hooks/inbox-check/types.d.ts.map +1 -0
  48. package/dist/hooks/inbox-check/types.js +5 -0
  49. package/dist/hooks/inbox-check/types.js.map +1 -0
  50. package/dist/hooks/inbox-check/utils.d.ts +44 -0
  51. package/dist/hooks/inbox-check/utils.d.ts.map +1 -0
  52. package/dist/hooks/inbox-check/utils.js +107 -0
  53. package/dist/hooks/inbox-check/utils.js.map +1 -0
  54. package/dist/index.d.ts +10 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +10 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/protocol/framing.d.ts +32 -0
  59. package/dist/protocol/framing.d.ts.map +1 -0
  60. package/dist/protocol/framing.js +71 -0
  61. package/dist/protocol/framing.js.map +1 -0
  62. package/dist/protocol/index.d.ts +3 -0
  63. package/dist/protocol/index.d.ts.map +1 -0
  64. package/dist/protocol/index.js +3 -0
  65. package/dist/protocol/index.js.map +1 -0
  66. package/dist/protocol/types.d.ts +104 -0
  67. package/dist/protocol/types.d.ts.map +1 -0
  68. package/dist/protocol/types.js +6 -0
  69. package/dist/protocol/types.js.map +1 -0
  70. package/dist/state/agent-state.d.ts +40 -0
  71. package/dist/state/agent-state.d.ts.map +1 -0
  72. package/dist/state/agent-state.js +120 -0
  73. package/dist/state/agent-state.js.map +1 -0
  74. package/dist/storage/adapter.d.ts +29 -0
  75. package/dist/storage/adapter.d.ts.map +1 -0
  76. package/dist/storage/adapter.js +2 -0
  77. package/dist/storage/adapter.js.map +1 -0
  78. package/dist/storage/sqlite-adapter.d.ts +15 -0
  79. package/dist/storage/sqlite-adapter.d.ts.map +1 -0
  80. package/dist/storage/sqlite-adapter.js +116 -0
  81. package/dist/storage/sqlite-adapter.js.map +1 -0
  82. package/dist/supervisor/inbox.d.ts +38 -0
  83. package/dist/supervisor/inbox.d.ts.map +1 -0
  84. package/dist/supervisor/inbox.js +162 -0
  85. package/dist/supervisor/inbox.js.map +1 -0
  86. package/dist/supervisor/index.d.ts +10 -0
  87. package/dist/supervisor/index.d.ts.map +1 -0
  88. package/dist/supervisor/index.js +10 -0
  89. package/dist/supervisor/index.js.map +1 -0
  90. package/dist/supervisor/spawner.d.ts +54 -0
  91. package/dist/supervisor/spawner.d.ts.map +1 -0
  92. package/dist/supervisor/spawner.js +282 -0
  93. package/dist/supervisor/spawner.js.map +1 -0
  94. package/dist/supervisor/state.d.ts +132 -0
  95. package/dist/supervisor/state.d.ts.map +1 -0
  96. package/dist/supervisor/state.js +465 -0
  97. package/dist/supervisor/state.js.map +1 -0
  98. package/dist/supervisor/supervisor.d.ts +67 -0
  99. package/dist/supervisor/supervisor.d.ts.map +1 -0
  100. package/dist/supervisor/supervisor.js +263 -0
  101. package/dist/supervisor/supervisor.js.map +1 -0
  102. package/dist/supervisor/types.d.ts +139 -0
  103. package/dist/supervisor/types.d.ts.map +1 -0
  104. package/dist/supervisor/types.js +12 -0
  105. package/dist/supervisor/types.js.map +1 -0
  106. package/dist/utils/index.d.ts +2 -0
  107. package/dist/utils/index.d.ts.map +1 -0
  108. package/dist/utils/index.js +2 -0
  109. package/dist/utils/index.js.map +1 -0
  110. package/dist/utils/name-generator.d.ts +17 -0
  111. package/dist/utils/name-generator.d.ts.map +1 -0
  112. package/dist/utils/name-generator.js +52 -0
  113. package/dist/utils/name-generator.js.map +1 -0
  114. package/dist/webhook/spawner.d.ts +79 -0
  115. package/dist/webhook/spawner.d.ts.map +1 -0
  116. package/dist/webhook/spawner.js +288 -0
  117. package/dist/webhook/spawner.js.map +1 -0
  118. package/dist/wrapper/client.d.ts +72 -0
  119. package/dist/wrapper/client.d.ts.map +1 -0
  120. package/dist/wrapper/client.js +306 -0
  121. package/dist/wrapper/client.js.map +1 -0
  122. package/dist/wrapper/inbox.d.ts +37 -0
  123. package/dist/wrapper/inbox.d.ts.map +1 -0
  124. package/dist/wrapper/inbox.js +73 -0
  125. package/dist/wrapper/inbox.js.map +1 -0
  126. package/dist/wrapper/index.d.ts +4 -0
  127. package/dist/wrapper/index.d.ts.map +1 -0
  128. package/dist/wrapper/index.js +7 -0
  129. package/dist/wrapper/index.js.map +1 -0
  130. package/dist/wrapper/parser.d.ts +94 -0
  131. package/dist/wrapper/parser.d.ts.map +1 -0
  132. package/dist/wrapper/parser.js +360 -0
  133. package/dist/wrapper/parser.js.map +1 -0
  134. package/dist/wrapper/pty-wrapper.d.ts +125 -0
  135. package/dist/wrapper/pty-wrapper.d.ts.map +1 -0
  136. package/dist/wrapper/pty-wrapper.js +494 -0
  137. package/dist/wrapper/pty-wrapper.js.map +1 -0
  138. package/dist/wrapper/tmux-wrapper.d.ts +131 -0
  139. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -0
  140. package/dist/wrapper/tmux-wrapper.js +427 -0
  141. package/dist/wrapper/tmux-wrapper.js.map +1 -0
  142. package/install.sh +69 -0
  143. package/package.json +82 -0
@@ -0,0 +1,1591 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Relay CLI
4
+ * Command-line interface for agent-relay.
5
+ */
6
+ import { Command } from 'commander';
7
+ import { config as dotenvConfig } from 'dotenv';
8
+ import { Daemon, DEFAULT_SOCKET_PATH } from '../daemon/server.js';
9
+ import { RelayClient } from '../wrapper/client.js';
10
+ import { generateAgentName } from '../utils/name-generator.js';
11
+ import { Supervisor } from '../supervisor/supervisor.js';
12
+ import { setupTicTacToe } from '../games/tictactoe.js';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { spawn } from 'node:child_process';
16
+ // Load .env file if present
17
+ dotenvConfig();
18
+ // Default dashboard port (can be overridden via .env or CLI)
19
+ const DEFAULT_DASHBOARD_PORT = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
20
+ const program = new Command();
21
+ function pidFilePathForSocket(socketPath) {
22
+ return `${socketPath}.pid`;
23
+ }
24
+ function supervisorPidFilePath(dataDir) {
25
+ return path.join(dataDir, 'supervisor.pid');
26
+ }
27
+ function isProcessAlive(pid) {
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ function readPidFile(pidPath) {
37
+ try {
38
+ if (!fs.existsSync(pidPath))
39
+ return null;
40
+ const raw = fs.readFileSync(pidPath, 'utf-8').trim();
41
+ const pid = Number(raw);
42
+ if (!Number.isFinite(pid) || pid <= 0)
43
+ return null;
44
+ return pid;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function startDetachedSupervisor(options) {
51
+ const pidPath = supervisorPidFilePath(options.dataDir);
52
+ const existingPid = readPidFile(pidPath);
53
+ if (existingPid && isProcessAlive(existingPid)) {
54
+ return existingPid;
55
+ }
56
+ // process.argv[1] points to the current CLI entrypoint (dist/cli/index.js in normal usage).
57
+ const cliEntrypoint = process.argv[1];
58
+ if (!cliEntrypoint) {
59
+ throw new Error('Unable to determine CLI entrypoint path for supervisor spawn');
60
+ }
61
+ fs.mkdirSync(options.dataDir, { recursive: true });
62
+ const child = spawn(process.execPath, [
63
+ cliEntrypoint,
64
+ 'supervisor',
65
+ '-s',
66
+ options.socket,
67
+ '-d',
68
+ options.dataDir,
69
+ '-p',
70
+ String(options.pollInterval),
71
+ ...(options.verbose ? ['-v'] : []),
72
+ ], {
73
+ detached: true,
74
+ stdio: 'ignore',
75
+ env: process.env,
76
+ });
77
+ child.unref();
78
+ fs.writeFileSync(pidPath, `${child.pid ?? ''}\n`, 'utf-8');
79
+ return child.pid ?? -1;
80
+ }
81
+ function stopDetachedSupervisor(dataDir) {
82
+ const pidPath = supervisorPidFilePath(dataDir);
83
+ const pid = readPidFile(pidPath);
84
+ if (!pid)
85
+ return { pid: null, stopped: false };
86
+ try {
87
+ process.kill(pid, 'SIGTERM');
88
+ }
89
+ catch {
90
+ // process may already be dead
91
+ }
92
+ try {
93
+ fs.unlinkSync(pidPath);
94
+ }
95
+ catch {
96
+ // ignore
97
+ }
98
+ return { pid, stopped: true };
99
+ }
100
+ program
101
+ .name('agent-relay')
102
+ .description('Real-time agent-to-agent communication system')
103
+ .version('0.1.0');
104
+ // Start daemon
105
+ program
106
+ .command('start')
107
+ .description('Start the relay daemon')
108
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
109
+ .option('-f, --foreground', 'Run in foreground', false)
110
+ .option('--db-path <path>', 'SQLite DB path for message storage (defaults near socket)')
111
+ .action(async (options) => {
112
+ const socketPath = options.socket;
113
+ const pidFilePath = pidFilePathForSocket(socketPath);
114
+ const daemon = new Daemon({
115
+ socketPath,
116
+ pidFilePath,
117
+ storagePath: options.dbPath ?? undefined,
118
+ });
119
+ // Handle shutdown
120
+ process.on('SIGINT', async () => {
121
+ console.log('\nShutting down...');
122
+ await daemon.stop();
123
+ process.exit(0);
124
+ });
125
+ process.on('SIGTERM', async () => {
126
+ await daemon.stop();
127
+ process.exit(0);
128
+ });
129
+ try {
130
+ await daemon.start();
131
+ console.log('Daemon started. Press Ctrl+C to stop.');
132
+ // Keep process alive
133
+ if (options.foreground) {
134
+ await new Promise(() => { }); // Never resolves
135
+ }
136
+ }
137
+ catch (err) {
138
+ console.error('Failed to start daemon:', err);
139
+ process.exit(1);
140
+ }
141
+ });
142
+ // Stop daemon
143
+ program
144
+ .command('stop')
145
+ .description('Stop the relay daemon (and background supervisor if running)')
146
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
147
+ .option('-d, --data-dir <path>', 'Data directory (for supervisor pidfile)', '/tmp/agent-relay')
148
+ .option('--daemon-only', 'Only stop the daemon (leave supervisor running)', false)
149
+ .action(async (options) => {
150
+ const socketPath = options.socket;
151
+ const pidFilePath = pidFilePathForSocket(socketPath);
152
+ // Stop supervisor first (best-effort) so it doesn't keep spawning while daemon stops.
153
+ if (!options.daemonOnly) {
154
+ const res = stopDetachedSupervisor(options.dataDir);
155
+ if (res.pid) {
156
+ console.log(`Supervisor stop requested (pid ${res.pid})`);
157
+ }
158
+ }
159
+ if (!fs.existsSync(pidFilePath)) {
160
+ console.log('Daemon not running (pid file not found)');
161
+ return;
162
+ }
163
+ const pidRaw = fs.readFileSync(pidFilePath, 'utf-8').trim();
164
+ const pid = Number(pidRaw);
165
+ if (!Number.isFinite(pid) || pid <= 0) {
166
+ console.error(`Invalid pid file: ${pidFilePath}`);
167
+ return;
168
+ }
169
+ try {
170
+ process.kill(pid, 'SIGTERM');
171
+ }
172
+ catch (err) {
173
+ // Stale pid file
174
+ console.warn(`Failed to signal pid ${pid} (${err.message}); cleaning up pid file`);
175
+ fs.unlinkSync(pidFilePath);
176
+ }
177
+ // Wait briefly for socket/pid file cleanup
178
+ const deadline = Date.now() + 2000;
179
+ while (Date.now() < deadline) {
180
+ const socketExists = fs.existsSync(socketPath);
181
+ const pidExists = fs.existsSync(pidFilePath);
182
+ if (!socketExists && !pidExists) {
183
+ console.log('Daemon stopped');
184
+ return;
185
+ }
186
+ await new Promise((r) => setTimeout(r, 50));
187
+ }
188
+ console.warn('Stop requested, but daemon did not exit within 2s');
189
+ console.warn(`Socket: ${socketPath}`);
190
+ console.warn(`PID file: ${pidFilePath}`);
191
+ });
192
+ // Wrap an agent
193
+ program
194
+ .command('wrap')
195
+ .description('Wrap an agent CLI command')
196
+ .option('-n, --name <name>', 'Agent name (auto-generated if not provided)')
197
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
198
+ .option('-r, --raw', 'Raw mode - bypass parsing for terminal-heavy CLIs', false)
199
+ .option('--pty', 'Use direct PTY mode (legacy)', false)
200
+ .option('-t, --tmux', 'Use tmux for message injection (old implementation)', false)
201
+ .option('-q, --quiet', 'Disable debug logging', false)
202
+ .option('--log-interval <ms>', 'Throttle debug logs (ms)', (val) => parseInt(val, 10))
203
+ .option('--inject-idle-ms <ms>', 'Idle time before injecting messages (ms)', (val) => parseInt(val, 10))
204
+ .option('--inject-retry-ms <ms>', 'Retry interval while waiting to inject (ms)', (val) => parseInt(val, 10))
205
+ .option('-o, --osascript', 'Use osascript for OS-level keyboard simulation (macOS)', false)
206
+ .option('-i, --inbox', 'Use file-based inbox (agent reads messages from file)', false)
207
+ .option('--inbox-dir <path>', 'Custom inbox directory', '/tmp/agent-relay')
208
+ .argument('<command...>', 'Command to wrap')
209
+ .action(async (commandParts, options) => {
210
+ // For tmux2, we need to preserve args separately for proper quoting
211
+ const [mainCommand, ...commandArgs] = commandParts;
212
+ const command = commandParts.join(' ');
213
+ // Auto-generate name if not provided
214
+ const agentName = options.name ?? generateAgentName();
215
+ process.stderr.write(`Agent name: ${agentName}\n`);
216
+ // Determine mode - tmux is now the default
217
+ const usePty = options.pty || options.tmux || options.osascript;
218
+ if (options.inbox) {
219
+ process.stderr.write(`Mode: inbox (file-based messaging)\n`);
220
+ }
221
+ else if (options.osascript) {
222
+ process.stderr.write(`Mode: osascript (OS-level keyboard simulation)\n`);
223
+ }
224
+ else if (options.tmux) {
225
+ process.stderr.write(`Mode: tmux (old implementation)\n`);
226
+ }
227
+ else if (options.pty) {
228
+ process.stderr.write(`Mode: direct PTY (legacy)\n`);
229
+ }
230
+ else {
231
+ process.stderr.write(`Mode: tmux (default)\n`);
232
+ }
233
+ // Use the new TmuxWrapper by default (unless --pty, --tmux, or --osascript specified)
234
+ if (!usePty) {
235
+ let TmuxWrapperClass;
236
+ try {
237
+ ({ TmuxWrapper: TmuxWrapperClass } = await import('../wrapper/tmux-wrapper.js'));
238
+ }
239
+ catch (err) {
240
+ console.error('Failed to load TmuxWrapper.');
241
+ console.error('Original error:', err);
242
+ process.exit(1);
243
+ }
244
+ const wrapper = new TmuxWrapperClass({
245
+ name: agentName,
246
+ command: mainCommand,
247
+ args: commandArgs,
248
+ socketPath: options.socket,
249
+ useInbox: options.inbox,
250
+ inboxDir: options.inboxDir,
251
+ debug: !options.quiet,
252
+ debugLogIntervalMs: options.logInterval,
253
+ idleBeforeInjectMs: options.injectIdleMs,
254
+ injectRetryMs: options.injectRetryMs,
255
+ });
256
+ // Handle shutdown
257
+ process.on('SIGINT', () => {
258
+ wrapper.stop();
259
+ process.exit(0);
260
+ });
261
+ try {
262
+ await wrapper.start();
263
+ }
264
+ catch (err) {
265
+ console.error('Failed to start tmux wrapper:', err);
266
+ process.exit(1);
267
+ }
268
+ return;
269
+ }
270
+ // Use the original PtyWrapper
271
+ let PtyWrapperClass;
272
+ try {
273
+ ({ PtyWrapper: PtyWrapperClass } = await import('../wrapper/pty-wrapper.js'));
274
+ }
275
+ catch (err) {
276
+ console.error('Failed to load PTY wrapper dependencies (node-pty).');
277
+ console.error('If you recently changed Node versions, rebuild native deps:');
278
+ console.error(' npm rebuild node-pty');
279
+ console.error('Original error:', err);
280
+ process.exit(1);
281
+ }
282
+ const wrapper = new PtyWrapperClass({
283
+ name: agentName,
284
+ command,
285
+ socketPath: options.socket,
286
+ raw: options.raw,
287
+ useTmux: options.tmux,
288
+ useOsascript: options.osascript,
289
+ useInbox: options.inbox,
290
+ inboxDir: options.inboxDir,
291
+ });
292
+ // Handle shutdown
293
+ process.on('SIGINT', () => {
294
+ wrapper.stop();
295
+ process.exit(0);
296
+ });
297
+ try {
298
+ await wrapper.start();
299
+ }
300
+ catch (err) {
301
+ console.error('Failed to start wrapper:', err);
302
+ process.exit(1);
303
+ }
304
+ });
305
+ // Status
306
+ program
307
+ .command('status')
308
+ .description('Show relay daemon status')
309
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
310
+ .action(async (options) => {
311
+ if (!fs.existsSync(options.socket)) {
312
+ console.log('Status: STOPPED (socket not found)');
313
+ return;
314
+ }
315
+ // Try to connect
316
+ const client = new RelayClient({
317
+ agentName: '__status_check__',
318
+ socketPath: options.socket,
319
+ reconnect: false,
320
+ });
321
+ try {
322
+ await client.connect();
323
+ console.log('Status: RUNNING');
324
+ console.log(`Socket: ${options.socket}`);
325
+ client.disconnect();
326
+ }
327
+ catch {
328
+ console.log('Status: STOPPED (connection failed)');
329
+ }
330
+ });
331
+ // Send a message (for testing)
332
+ program
333
+ .command('send')
334
+ .description('Send a message to an agent')
335
+ .option('-f, --from <name>', 'Sender agent name (auto-generated if not provided)')
336
+ .requiredOption('-t, --to <name>', 'Recipient agent name (or * for broadcast)')
337
+ .requiredOption('-m, --message <text>', 'Message body')
338
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
339
+ .action(async (options) => {
340
+ const senderName = options.from ?? generateAgentName();
341
+ const client = new RelayClient({
342
+ agentName: senderName,
343
+ socketPath: options.socket,
344
+ });
345
+ try {
346
+ await client.connect();
347
+ const success = client.sendMessage(options.to, options.message);
348
+ if (success) {
349
+ console.log(`Sent: ${options.message}`);
350
+ }
351
+ else {
352
+ console.error('Failed to send message');
353
+ }
354
+ // Wait a bit for delivery then exit cleanly
355
+ await new Promise((r) => setTimeout(r, 200));
356
+ client.destroy();
357
+ process.exit(0);
358
+ }
359
+ catch (err) {
360
+ console.error('Error:', err);
361
+ process.exit(1);
362
+ }
363
+ });
364
+ // List connected agents
365
+ program
366
+ .command('agents')
367
+ .description('List connected agents')
368
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
369
+ .action(async (_options) => {
370
+ console.log('Note: Agent listing requires daemon introspection (not yet implemented)');
371
+ console.log('Use the status command to check if daemon is running.');
372
+ });
373
+ // Supervisor command
374
+ program
375
+ .command('supervisor')
376
+ .description('Run the spawn-per-message supervisor (CLI-agnostic agent management)')
377
+ .option('-s, --socket <path>', 'Socket path', DEFAULT_SOCKET_PATH)
378
+ .option('-d, --data-dir <path>', 'Data directory for agent state', '/tmp/agent-relay')
379
+ .option('-p, --poll-interval <ms>', 'Polling interval in milliseconds', '2000')
380
+ .option('-v, --verbose', 'Enable verbose logging', false)
381
+ .option('--detach', 'Run supervisor in background (writes supervisor.pid)', false)
382
+ .action(async (options) => {
383
+ if (options.detach) {
384
+ const pid = startDetachedSupervisor({
385
+ socket: options.socket,
386
+ dataDir: options.dataDir,
387
+ pollInterval: parseInt(options.pollInterval, 10),
388
+ verbose: Boolean(options.verbose),
389
+ });
390
+ console.log(`Supervisor started in background (pid ${pid})`);
391
+ console.log(`PID file: ${supervisorPidFilePath(options.dataDir)}`);
392
+ return;
393
+ }
394
+ const supervisor = new Supervisor({
395
+ socketPath: options.socket,
396
+ dataDir: options.dataDir,
397
+ pollIntervalMs: parseInt(options.pollInterval, 10),
398
+ verbose: options.verbose,
399
+ });
400
+ // Handle shutdown
401
+ process.on('SIGINT', () => {
402
+ console.log('\nShutting down supervisor...');
403
+ supervisor.stop();
404
+ process.exit(0);
405
+ });
406
+ process.on('SIGTERM', () => {
407
+ supervisor.stop();
408
+ process.exit(0);
409
+ });
410
+ try {
411
+ await supervisor.start();
412
+ // Keep process alive
413
+ await new Promise(() => { });
414
+ }
415
+ catch (err) {
416
+ console.error('Failed to start supervisor:', err);
417
+ process.exit(1);
418
+ }
419
+ });
420
+ program
421
+ .command('supervisor-status')
422
+ .description('Show background supervisor status (pidfile-based)')
423
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
424
+ .action((options) => {
425
+ const pidPath = supervisorPidFilePath(options.dataDir);
426
+ const pid = readPidFile(pidPath);
427
+ if (!pid) {
428
+ console.log('Supervisor: STOPPED (pid file not found)');
429
+ console.log(`PID file: ${pidPath}`);
430
+ return;
431
+ }
432
+ console.log(`Supervisor: ${isProcessAlive(pid) ? 'RUNNING' : 'STOPPED'} (pid ${pid})`);
433
+ console.log(`PID file: ${pidPath}`);
434
+ });
435
+ program
436
+ .command('supervisor-stop')
437
+ .description('Stop background supervisor (pidfile-based)')
438
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
439
+ .action((options) => {
440
+ const res = stopDetachedSupervisor(options.dataDir);
441
+ if (!res.pid) {
442
+ console.log('Supervisor not running (pid file not found)');
443
+ return;
444
+ }
445
+ console.log(`Stop requested for supervisor pid ${res.pid}`);
446
+ });
447
+ // Register an agent with the supervisor
448
+ program
449
+ .command('register')
450
+ .description('Register an agent with the supervisor for spawn-per-message handling')
451
+ .requiredOption('-n, --name <name>', 'Agent name')
452
+ .requiredOption('-c, --cli <type>', 'CLI type: claude, codex, cursor, or custom')
453
+ .option('-w, --cwd <path>', 'Working directory', process.cwd())
454
+ .option('--command <cmd>', 'Custom command (required for cli=custom)')
455
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
456
+ .option('-s, --socket <path>', 'Socket path (for supervisor)', DEFAULT_SOCKET_PATH)
457
+ .option('--no-autostart-supervisor', 'Do not auto-start supervisor', false)
458
+ .action(async (options) => {
459
+ const validCLIs = ['claude', 'codex', 'cursor', 'custom'];
460
+ if (!validCLIs.includes(options.cli)) {
461
+ console.error(`Invalid CLI type: ${options.cli}. Must be one of: ${validCLIs.join(', ')}`);
462
+ process.exit(1);
463
+ }
464
+ if (options.cli === 'custom' && !options.command) {
465
+ console.error('--command is required when using cli=custom');
466
+ process.exit(1);
467
+ }
468
+ const supervisor = new Supervisor({ dataDir: options.dataDir });
469
+ const state = supervisor.registerAgent({
470
+ name: options.name,
471
+ cli: options.cli,
472
+ cwd: options.cwd,
473
+ customCommand: options.command,
474
+ });
475
+ console.log(`Registered agent: ${state.name}`);
476
+ console.log(` CLI: ${state.cli}`);
477
+ console.log(` CWD: ${state.cwd}`);
478
+ console.log(` State: ${options.dataDir}/${state.name}/state.json`);
479
+ console.log(` Inbox: ${options.dataDir}/${state.name}/inbox.md`);
480
+ if (options.autostartSupervisor !== false) {
481
+ try {
482
+ const pid = startDetachedSupervisor({
483
+ socket: options.socket,
484
+ dataDir: options.dataDir,
485
+ pollInterval: 2000,
486
+ verbose: false,
487
+ });
488
+ console.log(` Supervisor: running (pid ${pid})`);
489
+ }
490
+ catch (err) {
491
+ console.warn(` Supervisor: failed to autostart (${err.message})`);
492
+ console.warn(` Start manually: agent-relay supervisor -d ${options.dataDir}`);
493
+ }
494
+ }
495
+ else {
496
+ console.log(` Supervisor: not started (run: agent-relay supervisor -d ${options.dataDir})`);
497
+ }
498
+ });
499
+ // Poll inbox (blocking wait for messages)
500
+ program
501
+ .command('inbox-poll')
502
+ .description('Wait for messages in inbox (blocking poll for live agent sessions)')
503
+ .requiredOption('-n, --name <name>', 'Agent name')
504
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
505
+ .option('-i, --interval <ms>', 'Poll interval in milliseconds', '2000')
506
+ .option('-t, --timeout <s>', 'Timeout in seconds (0 = forever)', '0')
507
+ .option('--clear', 'Clear inbox after reading', false)
508
+ .option('--pattern <regex>', 'Only return when inbox matches pattern', '## Message from')
509
+ .action(async (options) => {
510
+ // Validate agent name
511
+ if (!options.name || options.name.includes('/') || options.name.includes('..')) {
512
+ console.error('Error: Invalid agent name. Name cannot be empty or contain path separators.');
513
+ process.exit(1);
514
+ }
515
+ // Validate poll interval
516
+ const pollInterval = parseInt(options.interval, 10);
517
+ if (isNaN(pollInterval) || pollInterval < 100) {
518
+ console.error('Error: Poll interval must be at least 100ms');
519
+ process.exit(1);
520
+ }
521
+ // Validate timeout
522
+ const timeout = parseInt(options.timeout, 10) * 1000;
523
+ if (isNaN(timeout) || timeout < 0) {
524
+ console.error('Error: Timeout must be a non-negative number');
525
+ process.exit(1);
526
+ }
527
+ // Validate regex pattern
528
+ let pattern;
529
+ try {
530
+ pattern = new RegExp(options.pattern);
531
+ }
532
+ catch (err) {
533
+ console.error(`Error: Invalid regex pattern: ${err.message}`);
534
+ process.exit(1);
535
+ }
536
+ const inboxPath = path.join(options.dataDir, options.name, 'inbox.md');
537
+ const startTime = Date.now();
538
+ // Ensure inbox directory exists
539
+ const inboxDir = path.dirname(inboxPath);
540
+ if (!fs.existsSync(inboxDir)) {
541
+ fs.mkdirSync(inboxDir, { recursive: true });
542
+ }
543
+ // Initialize empty inbox if doesn't exist
544
+ if (!fs.existsSync(inboxPath)) {
545
+ fs.writeFileSync(inboxPath, '', 'utf-8');
546
+ }
547
+ process.stderr.write(`Polling inbox: ${inboxPath}\n`);
548
+ process.stderr.write(`Pattern: ${options.pattern}\n`);
549
+ process.stderr.write(`Interval: ${pollInterval}ms\n`);
550
+ if (timeout > 0) {
551
+ process.stderr.write(`Timeout: ${options.timeout}s\n`);
552
+ }
553
+ while (true) {
554
+ // Check timeout
555
+ if (timeout > 0 && Date.now() - startTime > timeout) {
556
+ process.stderr.write('Timeout reached\n');
557
+ process.exit(1);
558
+ }
559
+ // Check inbox
560
+ try {
561
+ const content = fs.readFileSync(inboxPath, 'utf-8');
562
+ if (content.trim() && pattern.test(content)) {
563
+ // Found matching content
564
+ process.stdout.write(content);
565
+ // Clear if requested
566
+ if (options.clear) {
567
+ fs.writeFileSync(inboxPath, '', 'utf-8');
568
+ }
569
+ process.exit(0);
570
+ }
571
+ }
572
+ catch {
573
+ // File might not exist yet, that's ok
574
+ }
575
+ // Wait before next poll
576
+ await new Promise((r) => setTimeout(r, pollInterval));
577
+ }
578
+ });
579
+ // Read inbox without waiting
580
+ program
581
+ .command('inbox-read')
582
+ .description('Read current inbox contents (non-blocking)')
583
+ .requiredOption('-n, --name <name>', 'Agent name')
584
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
585
+ .option('--clear', 'Clear inbox after reading', false)
586
+ .action((options) => {
587
+ // Validate agent name
588
+ if (!options.name || options.name.includes('/') || options.name.includes('..')) {
589
+ console.error('Error: Invalid agent name. Name cannot be empty or contain path separators.');
590
+ process.exit(1);
591
+ }
592
+ const inboxPath = path.join(options.dataDir, options.name, 'inbox.md');
593
+ if (!fs.existsSync(inboxPath)) {
594
+ console.log('(inbox empty)');
595
+ return;
596
+ }
597
+ try {
598
+ const content = fs.readFileSync(inboxPath, 'utf-8');
599
+ if (!content.trim()) {
600
+ console.log('(inbox empty)');
601
+ return;
602
+ }
603
+ process.stdout.write(content);
604
+ if (options.clear) {
605
+ fs.writeFileSync(inboxPath, '', 'utf-8');
606
+ }
607
+ }
608
+ catch (err) {
609
+ console.error(`Error reading inbox: ${err.message}`);
610
+ process.exit(1);
611
+ }
612
+ });
613
+ // Write to another agent's inbox
614
+ program
615
+ .command('inbox-write')
616
+ .description('Write a message to agent inbox(es). Supports multiple recipients or broadcast.')
617
+ .requiredOption('-t, --to <names>', 'Recipient(s): agent name, comma-separated list, or * for broadcast')
618
+ .requiredOption('-f, --from <name>', 'Sender agent name')
619
+ .requiredOption('-m, --message <text>', 'Message body')
620
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
621
+ .action((options) => {
622
+ // Validate sender name
623
+ if (!options.from || options.from.includes('/') || options.from.includes('..')) {
624
+ console.error('Error: Invalid sender name. Name cannot be empty or contain path separators.');
625
+ process.exit(1);
626
+ }
627
+ // Validate message is not empty
628
+ if (!options.message || !options.message.trim()) {
629
+ console.error('Error: Message cannot be empty');
630
+ process.exit(1);
631
+ }
632
+ const timestamp = new Date().toISOString();
633
+ // Match wrapper/supervisor inbox format:
634
+ // ## Message from <sender> | <timestamp>
635
+ // <body>
636
+ const formattedMessage = `\n## Message from ${options.from} | ${timestamp}\n${options.message}\n`;
637
+ // Determine recipients
638
+ let recipients = [];
639
+ if (options.to === '*') {
640
+ // Broadcast to all agents in data-dir except sender
641
+ if (fs.existsSync(options.dataDir)) {
642
+ const entries = fs.readdirSync(options.dataDir, { withFileTypes: true });
643
+ recipients = entries
644
+ .filter(e => e.isDirectory() && e.name !== options.from)
645
+ .map(e => e.name);
646
+ }
647
+ if (recipients.length === 0) {
648
+ console.log('No other agents found for broadcast');
649
+ return;
650
+ }
651
+ }
652
+ else {
653
+ // Parse comma-separated list
654
+ recipients = options.to.split(',').map((r) => r.trim()).filter((r) => r);
655
+ // Validate recipient names
656
+ for (const r of recipients) {
657
+ if (r.includes('/') || r.includes('..')) {
658
+ console.error(`Error: Invalid recipient name "${r}". Name cannot contain path separators.`);
659
+ process.exit(1);
660
+ }
661
+ }
662
+ if (recipients.length === 0) {
663
+ console.error('Error: At least one recipient is required');
664
+ process.exit(1);
665
+ }
666
+ }
667
+ // Write to each recipient
668
+ let successCount = 0;
669
+ for (const recipient of recipients) {
670
+ const inboxPath = path.join(options.dataDir, recipient, 'inbox.md');
671
+ // Ensure directory exists
672
+ const inboxDir = path.dirname(inboxPath);
673
+ try {
674
+ if (!fs.existsSync(inboxDir)) {
675
+ fs.mkdirSync(inboxDir, { recursive: true });
676
+ }
677
+ fs.appendFileSync(inboxPath, formattedMessage, 'utf-8');
678
+ console.log(`Message written to ${recipient}`);
679
+ successCount++;
680
+ }
681
+ catch (err) {
682
+ console.error(`Error writing to ${recipient}: ${err.message}`);
683
+ }
684
+ }
685
+ if (successCount === 0) {
686
+ process.exit(1);
687
+ }
688
+ });
689
+ // Dynamic team management
690
+ program
691
+ .command('team-init')
692
+ .description('Initialize a team workspace for multi-agent collaboration')
693
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
694
+ .option('-p, --project <path>', 'Project directory agents will work on', process.cwd())
695
+ .option('-n, --name <name>', 'Team/project name', 'agent-team')
696
+ .action((options) => {
697
+ const teamDir = options.dataDir;
698
+ const configPath = path.join(teamDir, 'team.json');
699
+ fs.mkdirSync(teamDir, { recursive: true });
700
+ const config = {
701
+ name: options.name,
702
+ projectDir: options.project,
703
+ createdAt: new Date().toISOString(),
704
+ agents: [],
705
+ };
706
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
707
+ console.log(`Team workspace initialized: ${teamDir}`);
708
+ console.log(`Project: ${options.project}`);
709
+ console.log('');
710
+ console.log('Add agents with:');
711
+ console.log(` agent-relay team-add -n AgentName -c claude -r "Role" -t "Task" -d ${teamDir}`);
712
+ });
713
+ program
714
+ .command('team-add')
715
+ .description('Add an agent to the team')
716
+ .requiredOption('-n, --name <name>', 'Agent name')
717
+ .requiredOption('-c, --cli <type>', 'CLI type: claude, codex, gemini, cursor')
718
+ .requiredOption('-r, --role <role>', 'Agent role (e.g., "Documentation Lead")')
719
+ .option('-t, --task <task>', 'Task (repeatable)', (val, arr) => [...arr, val], [])
720
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
721
+ .action((options) => {
722
+ const teamDir = options.dataDir;
723
+ const configPath = path.join(teamDir, 'team.json');
724
+ let config;
725
+ if (fs.existsSync(configPath)) {
726
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
727
+ }
728
+ else {
729
+ fs.mkdirSync(teamDir, { recursive: true });
730
+ config = { name: 'agent-team', projectDir: process.cwd(), createdAt: new Date().toISOString(), agents: [] };
731
+ }
732
+ const existingIdx = config.agents.findIndex(a => a.name === options.name);
733
+ const agentData = { name: options.name, cli: options.cli, role: options.role, tasks: options.task };
734
+ if (existingIdx >= 0) {
735
+ config.agents[existingIdx] = agentData;
736
+ console.log(`Updated agent: ${options.name}`);
737
+ }
738
+ else {
739
+ config.agents.push(agentData);
740
+ console.log(`Added agent: ${options.name}`);
741
+ }
742
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
743
+ const agentDir = path.join(teamDir, options.name);
744
+ fs.mkdirSync(agentDir, { recursive: true });
745
+ const inboxPath = path.join(agentDir, 'inbox.md');
746
+ if (!fs.existsSync(inboxPath))
747
+ fs.writeFileSync(inboxPath, '');
748
+ const teammates = config.agents.filter(a => a.name !== options.name).map(a => a.name);
749
+ const taskList = options.task.length > 0 ? options.task.map((t, i) => `${i + 1}. ${t}`).join('\n') : '(Check with teammates for tasks)';
750
+ const teammateList = teammates.length > 0 ? teammates.join(', ') : '(No other agents yet)';
751
+ const instructions = `# You are ${options.name} - ${options.role}
752
+
753
+ ## Project Location
754
+ \`${config.projectDir}\`
755
+
756
+ ## Your Tasks
757
+ ${taskList}
758
+
759
+ ## Communication
760
+
761
+ **Your inbox:** \`${teamDir}/${options.name}/inbox.md\`
762
+ **Teammates:** ${teammateList}
763
+
764
+ ### Commands
765
+ \`\`\`bash
766
+ # Check inbox (non-blocking peek)
767
+ node ${config.projectDir}/dist/cli/index.js team-check -n ${options.name} -d ${teamDir} --no-wait
768
+
769
+ # Check inbox (BLOCKS until message arrives)
770
+ node ${config.projectDir}/dist/cli/index.js team-check -n ${options.name} -d ${teamDir} --clear
771
+
772
+ # Send to teammate
773
+ node ${config.projectDir}/dist/cli/index.js team-send -n ${options.name} -t TEAMMATE -m "MESSAGE" -d ${teamDir}
774
+
775
+ # Broadcast to all
776
+ node ${config.projectDir}/dist/cli/index.js team-send -n ${options.name} -t "*" -m "MESSAGE" -d ${teamDir}
777
+
778
+ # Team status
779
+ node ${config.projectDir}/dist/cli/index.js team-status -d ${teamDir}
780
+ \`\`\`
781
+
782
+ ### Protocol
783
+ - \`STATUS: <doing what>\` - Progress update
784
+ - \`DONE: <task>\` - Completed
785
+ - \`QUESTION: @Name <q>\` - Ask teammate
786
+ - \`BLOCKER: <issue>\` - Blocked
787
+
788
+ ## CRITICAL: Work Loop (MUST FOLLOW)
789
+ \`\`\`
790
+ REPEAT:
791
+ 1. CHECK inbox (--no-wait)
792
+ 2. RESPOND to any messages
793
+ 3. DO one small task step (max 5 min work)
794
+ 4. BROADCAST status update
795
+ 5. GOTO 1
796
+ \`\`\`
797
+
798
+ **You MUST check inbox and broadcast after EVERY task step. Never go silent!**
799
+
800
+ ## Start Now
801
+ 1. Run team-check --no-wait to see any messages
802
+ 2. Broadcast: STATUS: ${options.name} starting [first task]
803
+ 3. Follow the work loop above
804
+ `;
805
+ const instructionsPath = path.join(agentDir, 'INSTRUCTIONS.md');
806
+ fs.writeFileSync(instructionsPath, instructions);
807
+ console.log(` CLI: ${options.cli}`);
808
+ console.log(` Role: ${options.role}`);
809
+ console.log(` Instructions: ${instructionsPath}`);
810
+ console.log('');
811
+ console.log('To start:');
812
+ console.log(` cd ${config.projectDir} && ${options.cli}`);
813
+ console.log(` Say: Read ${instructionsPath} and start working`);
814
+ });
815
+ program
816
+ .command('team-list')
817
+ .description('List all agents in the team')
818
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
819
+ .option('--instructions', 'Show startup instructions', false)
820
+ .action((options) => {
821
+ const configPath = path.join(options.dataDir, 'team.json');
822
+ if (!fs.existsSync(configPath)) {
823
+ console.log('No team found. Initialize with: agent-relay team-init');
824
+ return;
825
+ }
826
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
827
+ console.log(`Team: ${config.name}`);
828
+ console.log(`Project: ${config.projectDir}`);
829
+ console.log(`Agents: ${config.agents.length}`);
830
+ console.log('');
831
+ for (const agent of config.agents) {
832
+ const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
833
+ const hasMessages = fs.existsSync(inboxPath) && fs.statSync(inboxPath).size > 10;
834
+ const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
835
+ console.log(`━━━ ${agent.name} (${agent.cli}) ${hasMessages ? 'šŸ“¬' : ''}`);
836
+ console.log(` Role: ${agent.role}`);
837
+ if (agent.tasks?.length > 0)
838
+ console.log(` Tasks: ${agent.tasks.join(', ')}`);
839
+ if (options.instructions) {
840
+ console.log(` Start: cd ${config.projectDir} && ${agent.cli}`);
841
+ console.log(` Say: Read ${instructionsPath} and start working`);
842
+ }
843
+ console.log('');
844
+ }
845
+ });
846
+ // One-shot team setup from JSON config
847
+ program
848
+ .command('team-setup')
849
+ .description('Create a complete team from a JSON config file or inline JSON')
850
+ .option('-f, --file <path>', 'Path to JSON config file')
851
+ .option('-c, --config <json>', 'Inline JSON config')
852
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
853
+ .action((options) => {
854
+ let config;
855
+ if (options.file) {
856
+ if (!fs.existsSync(options.file)) {
857
+ console.error(`Config file not found: ${options.file}`);
858
+ process.exit(1);
859
+ }
860
+ config = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
861
+ }
862
+ else if (options.config) {
863
+ config = JSON.parse(options.config);
864
+ }
865
+ else {
866
+ console.error('Provide --file or --config');
867
+ process.exit(1);
868
+ }
869
+ const teamDir = options.dataDir;
870
+ const projectDir = config.project || process.cwd();
871
+ // Create team config
872
+ fs.mkdirSync(teamDir, { recursive: true });
873
+ const teamConfig = {
874
+ name: config.name || 'agent-team',
875
+ projectDir,
876
+ createdAt: new Date().toISOString(),
877
+ agents: config.agents.map(a => ({ ...a, tasks: a.tasks || [] })),
878
+ };
879
+ fs.writeFileSync(path.join(teamDir, 'team.json'), JSON.stringify(teamConfig, null, 2));
880
+ // Create each agent
881
+ for (const agent of config.agents) {
882
+ const agentDir = path.join(teamDir, agent.name);
883
+ fs.mkdirSync(agentDir, { recursive: true });
884
+ fs.writeFileSync(path.join(agentDir, 'inbox.md'), '');
885
+ const teammates = config.agents.filter(a => a.name !== agent.name).map(a => a.name);
886
+ const taskList = (agent.tasks || []).map((t, i) => `${i + 1}. ${t}`).join('\n') || '(Check with teammates)';
887
+ const instructions = `# You are ${agent.name} - ${agent.role}
888
+
889
+ ## Project: \`${projectDir}\`
890
+
891
+ ## Tasks
892
+ ${taskList}
893
+
894
+ ## Teammates: ${teammates.join(', ') || 'none yet'}
895
+
896
+ ## Commands
897
+ \`\`\`bash
898
+ # Check inbox (non-blocking peek)
899
+ node ${projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${teamDir} --no-wait
900
+
901
+ # Check inbox (BLOCKS until message arrives)
902
+ node ${projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${teamDir} --clear
903
+
904
+ # Send message
905
+ node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t RECIPIENT -m "message" -d ${teamDir}
906
+
907
+ # Broadcast to all
908
+ node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t "*" -m "message" -d ${teamDir}
909
+
910
+ # Team status
911
+ node ${projectDir}/dist/cli/index.js team-status -d ${teamDir}
912
+ \`\`\`
913
+
914
+ ## Protocol
915
+ - \`STATUS: <doing>\` - Update
916
+ - \`DONE: <task>\` - Complete
917
+ - \`QUESTION: @Name <q>\` - Ask
918
+ - \`BLOCKER: <issue>\` - Stuck
919
+
920
+ ## CRITICAL: Work Loop (MUST FOLLOW)
921
+ \`\`\`
922
+ REPEAT:
923
+ 1. CHECK inbox (--no-wait)
924
+ 2. RESPOND to any messages
925
+ 3. DO one small task step (max 5 min work)
926
+ 4. BROADCAST status update
927
+ 5. GOTO 1
928
+ \`\`\`
929
+
930
+ **You MUST check inbox and broadcast after EVERY task step. Never go silent!**
931
+
932
+ ## Start Now
933
+ 1. Run team-check --no-wait to see any messages
934
+ 2. Broadcast: STATUS: ${agent.name} starting [first task]
935
+ 3. Follow the work loop above
936
+ `;
937
+ fs.writeFileSync(path.join(agentDir, 'INSTRUCTIONS.md'), instructions);
938
+ }
939
+ console.log(`Team "${teamConfig.name}" created with ${config.agents.length} agents`);
940
+ console.log(`Directory: ${teamDir}`);
941
+ console.log('');
942
+ console.log('Start agents with:');
943
+ for (const agent of config.agents) {
944
+ console.log(` ${agent.cli}: Read ${teamDir}/${agent.name}/INSTRUCTIONS.md and start`);
945
+ }
946
+ });
947
+ // Self-register to a team (for agents to join)
948
+ program
949
+ .command('team-join')
950
+ .description('Join an existing team (for agents to self-register)')
951
+ .requiredOption('-n, --name <name>', 'Your agent name')
952
+ .requiredOption('-c, --cli <type>', 'Your CLI type')
953
+ .requiredOption('-r, --role <role>', 'Your role')
954
+ .option('-t, --task <task>', 'Your task (repeatable)', (v, a) => [...a, v], [])
955
+ .option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
956
+ .action((options) => {
957
+ const configPath = path.join(options.dataDir, 'team.json');
958
+ if (!fs.existsSync(configPath)) {
959
+ console.error('No team found. Create one first with team-init or team-setup');
960
+ process.exit(1);
961
+ }
962
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
963
+ // Add or update agent
964
+ const idx = config.agents.findIndex((a) => a.name === options.name);
965
+ const agentData = { name: options.name, cli: options.cli, role: options.role, tasks: options.task };
966
+ if (idx >= 0) {
967
+ config.agents[idx] = agentData;
968
+ }
969
+ else {
970
+ config.agents.push(agentData);
971
+ }
972
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
973
+ // Create inbox
974
+ const agentDir = path.join(options.dataDir, options.name);
975
+ fs.mkdirSync(agentDir, { recursive: true });
976
+ if (!fs.existsSync(path.join(agentDir, 'inbox.md'))) {
977
+ fs.writeFileSync(path.join(agentDir, 'inbox.md'), '');
978
+ }
979
+ console.log(`Joined team as ${options.name} (${options.role})`);
980
+ console.log(`Teammates: ${config.agents.filter((a) => a.name !== options.name).map((a) => a.name).join(', ')}`);
981
+ });
982
+ // Quick team status
983
+ program
984
+ .command('team-status')
985
+ .description('Show team status with message counts')
986
+ .option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
987
+ .action((options) => {
988
+ const configPath = path.join(options.dataDir, 'team.json');
989
+ if (!fs.existsSync(configPath)) {
990
+ console.log('No team found');
991
+ return;
992
+ }
993
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
994
+ console.log(`\nšŸ“‹ ${config.name} | ${config.agents.length} agents\n`);
995
+ for (const agent of config.agents) {
996
+ const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
997
+ let msgCount = 0;
998
+ if (fs.existsSync(inboxPath)) {
999
+ const content = fs.readFileSync(inboxPath, 'utf-8');
1000
+ msgCount = (content.match(/## Message from/g) || []).length;
1001
+ }
1002
+ const icon = msgCount > 0 ? 'šŸ“¬' : 'šŸ“­';
1003
+ console.log(` ${icon} ${agent.name} (${agent.cli}) - ${agent.role}${msgCount > 0 ? ` [${msgCount} msg]` : ''}`);
1004
+ }
1005
+ console.log('');
1006
+ });
1007
+ // Simplified send (team-aware)
1008
+ program
1009
+ .command('team-send')
1010
+ .description('Send a message to teammate(s)')
1011
+ .requiredOption('-n, --name <name>', 'Your agent name')
1012
+ .requiredOption('-t, --to <recipient>', 'Recipient name or * for broadcast')
1013
+ .requiredOption('-m, --message <text>', 'Message')
1014
+ .option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
1015
+ .action((options) => {
1016
+ const configPath = path.join(options.dataDir, 'team.json');
1017
+ if (!fs.existsSync(configPath)) {
1018
+ console.error('No team found');
1019
+ process.exit(1);
1020
+ }
1021
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1022
+ const timestamp = new Date().toISOString();
1023
+ const msg = `\n## Message from ${options.name} | ${timestamp}\n${options.message}\n`;
1024
+ let recipients = [];
1025
+ if (options.to === '*') {
1026
+ recipients = config.agents
1027
+ .filter((a) => a.name !== options.name)
1028
+ .map((a) => a.name);
1029
+ }
1030
+ else {
1031
+ recipients = options.to.split(',').map((r) => r.trim());
1032
+ }
1033
+ for (const r of recipients) {
1034
+ const inbox = path.join(options.dataDir, r, 'inbox.md');
1035
+ if (fs.existsSync(path.dirname(inbox))) {
1036
+ fs.appendFileSync(inbox, msg);
1037
+ console.log(`→ ${r}`);
1038
+ }
1039
+ else {
1040
+ console.log(`āœ— ${r} (not found)`);
1041
+ }
1042
+ }
1043
+ });
1044
+ // Simplified check inbox (team-aware)
1045
+ program
1046
+ .command('team-check')
1047
+ .description('Check your inbox (blocking wait for messages)')
1048
+ .requiredOption('-n, --name <name>', 'Your agent name')
1049
+ .option('-d, --data-dir <path>', 'Team directory', '/tmp/agent-relay-team')
1050
+ .option('--no-wait', 'Just read, don\'t wait for messages')
1051
+ .option('--clear', 'Clear inbox after reading')
1052
+ .option('-t, --timeout <seconds>', 'Timeout in seconds (0=forever)', '0')
1053
+ .action(async (options) => {
1054
+ const inboxPath = path.join(options.dataDir, options.name, 'inbox.md');
1055
+ if (!fs.existsSync(path.dirname(inboxPath))) {
1056
+ fs.mkdirSync(path.dirname(inboxPath), { recursive: true });
1057
+ }
1058
+ if (!fs.existsSync(inboxPath)) {
1059
+ fs.writeFileSync(inboxPath, '');
1060
+ }
1061
+ const timeout = parseInt(options.timeout, 10) * 1000;
1062
+ const startTime = Date.now();
1063
+ if (options.wait === false) {
1064
+ // Just read
1065
+ const content = fs.readFileSync(inboxPath, 'utf-8');
1066
+ if (content.trim()) {
1067
+ process.stdout.write(content);
1068
+ if (options.clear)
1069
+ fs.writeFileSync(inboxPath, '');
1070
+ }
1071
+ else {
1072
+ console.log('(no messages)');
1073
+ }
1074
+ return;
1075
+ }
1076
+ // Blocking wait
1077
+ process.stderr.write(`Waiting for messages...\n`);
1078
+ while (true) {
1079
+ if (timeout > 0 && Date.now() - startTime > timeout) {
1080
+ console.log('(timeout)');
1081
+ process.exit(1);
1082
+ }
1083
+ const content = fs.readFileSync(inboxPath, 'utf-8');
1084
+ if (content.includes('## Message from')) {
1085
+ process.stdout.write(content);
1086
+ if (options.clear)
1087
+ fs.writeFileSync(inboxPath, '');
1088
+ process.exit(0);
1089
+ }
1090
+ await new Promise(r => setTimeout(r, 2000));
1091
+ }
1092
+ });
1093
+ // List agents in a data directory (for games)
1094
+ program
1095
+ .command('inbox-agents')
1096
+ .description('List all agents with inboxes in a data directory')
1097
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
1098
+ .action((options) => {
1099
+ if (!fs.existsSync(options.dataDir)) {
1100
+ console.log('No agents found');
1101
+ return;
1102
+ }
1103
+ const entries = fs.readdirSync(options.dataDir, { withFileTypes: true });
1104
+ const agents = entries
1105
+ .filter(e => e.isDirectory())
1106
+ .map(e => e.name);
1107
+ if (agents.length === 0) {
1108
+ console.log('No agents found');
1109
+ return;
1110
+ }
1111
+ console.log('Agents:');
1112
+ for (const agent of agents) {
1113
+ const inboxPath = path.join(options.dataDir, agent, 'inbox.md');
1114
+ const hasInbox = fs.existsSync(inboxPath);
1115
+ const inboxSize = hasInbox ? fs.statSync(inboxPath).size : 0;
1116
+ console.log(` ${agent}${inboxSize > 0 ? ' (has messages)' : ''}`);
1117
+ }
1118
+ });
1119
+ // Tic-tac-toe setup helper (writes instruction files + clears inboxes)
1120
+ program
1121
+ .command('tictactoe-setup')
1122
+ .description('Create tic-tac-toe instruction files + empty inboxes for two agents')
1123
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay-ttt')
1124
+ .option('--player-x <name>', 'Player X name', 'PlayerX')
1125
+ .option('--player-o <name>', 'Player O name', 'PlayerO')
1126
+ .action((options) => {
1127
+ const res = setupTicTacToe({
1128
+ dataDir: options.dataDir,
1129
+ playerX: options.playerX,
1130
+ playerO: options.playerO,
1131
+ });
1132
+ console.log('Created tic-tac-toe instructions:');
1133
+ console.log(` X: ${res.instructionsXPath}`);
1134
+ console.log(` O: ${res.instructionsOPath}`);
1135
+ console.log('');
1136
+ console.log('To start (2 terminals):');
1137
+ console.log(` Terminal 1: start your agent, then read ${res.instructionsXPath}`);
1138
+ console.log(` Terminal 2: start your agent, then read ${res.instructionsOPath}`);
1139
+ });
1140
+ // List registered agents
1141
+ program
1142
+ .command('list')
1143
+ .description('List registered agents')
1144
+ .option('-d, --data-dir <path>', 'Data directory', '/tmp/agent-relay')
1145
+ .action(async (options) => {
1146
+ const supervisor = new Supervisor({ dataDir: options.dataDir });
1147
+ const agents = supervisor.getAgents();
1148
+ if (agents.length === 0) {
1149
+ console.log('No registered agents');
1150
+ return;
1151
+ }
1152
+ const supPidPath = supervisorPidFilePath(options.dataDir);
1153
+ const supPid = readPidFile(supPidPath);
1154
+ const supRunning = supPid ? isProcessAlive(supPid) : false;
1155
+ console.log(`Supervisor: ${supRunning ? `RUNNING (pid ${supPid})` : 'STOPPED'}`);
1156
+ console.log('Registered agents:');
1157
+ for (const name of agents) {
1158
+ const diag = supervisor.getAgentDiagnostics(name);
1159
+ const state = diag.state;
1160
+ if (state) {
1161
+ const status = state.status ?? 'idle';
1162
+ const lock = diag.locked ? 'locked' : 'unlocked';
1163
+ const inbox = diag.hasUnreadInbox ? 'unread' : 'clear';
1164
+ console.log(` ${name} (${state.cli}) - ${status}, ${lock}, inbox:${inbox}`);
1165
+ console.log(` cwd: ${state.cwd}`);
1166
+ console.log(` state: ${diag.statePath}`);
1167
+ console.log(` inbox: ${diag.inboxPath}`);
1168
+ }
1169
+ else {
1170
+ console.log(` ${name} (missing/invalid state.json)`);
1171
+ }
1172
+ }
1173
+ });
1174
+ // Dashboard command
1175
+ program
1176
+ .command('dashboard')
1177
+ .description('Start the web dashboard')
1178
+ .option('-p, --port <number>', 'Port to run on', DEFAULT_DASHBOARD_PORT)
1179
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
1180
+ .option('--db-path <path>', 'SQLite DB path for message storage', '/tmp/agent-relay.sqlite')
1181
+ .action(async (options) => {
1182
+ const { startDashboard } = await import('../dashboard/server.js');
1183
+ await startDashboard(parseInt(options.port, 10), options.dataDir, options.dbPath);
1184
+ });
1185
+ // Team listen daemon - watches inboxes and spawns agents when messages arrive
1186
+ program
1187
+ .command('team-listen')
1188
+ .description('Watch inboxes and spawn agents when messages arrive (for Codex, Gemini, etc.)')
1189
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
1190
+ .option('-a, --agents <names>', 'Comma-separated agent names to watch (default: all)')
1191
+ .option('--debounce <ms>', 'Debounce time before spawning (ms)', '3000')
1192
+ .option('--cooldown <s>', 'Minimum seconds between spawns per agent', '60')
1193
+ .option('--dry-run', 'Log what would happen without spawning', false)
1194
+ .action(async (options) => {
1195
+ const chokidar = await import('chokidar');
1196
+ const { spawn } = await import('child_process');
1197
+ const { AgentStateManager } = await import('../state/agent-state.js');
1198
+ const configPath = path.join(options.dataDir, 'team.json');
1199
+ if (!fs.existsSync(configPath)) {
1200
+ console.error('No team found. Initialize with: team-init or team-setup');
1201
+ process.exit(1);
1202
+ }
1203
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1204
+ const stateManager = new AgentStateManager(options.dataDir);
1205
+ const debounceMs = parseInt(options.debounce, 10);
1206
+ const cooldownMs = parseInt(options.cooldown, 10) * 1000;
1207
+ // Filter agents
1208
+ const watchAgents = options.agents
1209
+ ? options.agents.split(',').map((n) => n.trim())
1210
+ : config.agents.map((a) => a.name);
1211
+ const agents = config.agents.filter((a) => watchAgents.includes(a.name));
1212
+ if (agents.length === 0) {
1213
+ console.error('No matching agents found');
1214
+ process.exit(1);
1215
+ }
1216
+ // Track state
1217
+ const lastSpawn = new Map();
1218
+ const debounceTimers = new Map();
1219
+ const lastInboxSize = new Map();
1220
+ console.log('Agent Relay Listener');
1221
+ console.log('====================');
1222
+ console.log(`Directory: ${options.dataDir}`);
1223
+ console.log(`Watching: ${agents.map((a) => a.name).join(', ')}`);
1224
+ console.log(`Debounce: ${debounceMs}ms, Cooldown: ${cooldownMs / 1000}s`);
1225
+ console.log('');
1226
+ // Get spawn command for CLI type
1227
+ const getSpawnCmd = (cli) => {
1228
+ switch (cli.toLowerCase()) {
1229
+ case 'claude':
1230
+ return { cmd: 'claude', args: ['--dangerously-skip-permissions'] };
1231
+ case 'codex':
1232
+ return { cmd: 'codex', args: [] };
1233
+ case 'gemini':
1234
+ return { cmd: 'gemini', args: [] };
1235
+ case 'cursor':
1236
+ return { cmd: 'cursor', args: ['--cli'] };
1237
+ default:
1238
+ return { cmd: cli, args: [] };
1239
+ }
1240
+ };
1241
+ // Spawn agent with context
1242
+ const spawnAgent = (agent) => {
1243
+ const now = Date.now();
1244
+ const last = lastSpawn.get(agent.name) || 0;
1245
+ // Check cooldown
1246
+ if (now - last < cooldownMs) {
1247
+ console.log(`[${new Date().toISOString()}] ${agent.name}: cooling down (${Math.round((cooldownMs - (now - last)) / 1000)}s remaining)`);
1248
+ return;
1249
+ }
1250
+ // Check inbox has content
1251
+ const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
1252
+ if (!fs.existsSync(inboxPath))
1253
+ return;
1254
+ const content = fs.readFileSync(inboxPath, 'utf-8');
1255
+ if (!content.includes('## Message from'))
1256
+ return;
1257
+ const { cmd, args } = getSpawnCmd(agent.cli);
1258
+ const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
1259
+ // Build context-aware prompt
1260
+ const stateContext = stateManager.formatAsContext(agent.name);
1261
+ const prompt = `${stateContext}
1262
+
1263
+ You have NEW MESSAGES! Read ${instructionsPath} for your role.
1264
+
1265
+ IMMEDIATE ACTIONS:
1266
+ 1. Check inbox: node ${config.projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${options.dataDir} --no-wait
1267
+ 2. Respond to messages
1268
+ 3. Do ONE task step
1269
+ 4. Broadcast status update
1270
+ 5. Before exiting, save your state by outputting:
1271
+ [[STATE]]{"currentTask": "what you're working on", "context": "brief summary of progress"}[[/STATE]]
1272
+
1273
+ GO!`;
1274
+ console.log(`[${new Date().toISOString()}] Spawning ${agent.name} (${agent.cli})...`);
1275
+ if (options.dryRun) {
1276
+ console.log(` DRY RUN: ${cmd} -p "${prompt.substring(0, 100)}..."`);
1277
+ return;
1278
+ }
1279
+ lastSpawn.set(agent.name, now);
1280
+ try {
1281
+ const child = spawn(cmd, [...args, '-p', prompt], {
1282
+ cwd: config.projectDir,
1283
+ stdio: 'inherit',
1284
+ });
1285
+ child.on('exit', (code) => {
1286
+ console.log(`[${new Date().toISOString()}] ${agent.name} exited (code ${code})`);
1287
+ });
1288
+ child.on('error', (err) => {
1289
+ console.error(`[${new Date().toISOString()}] ${agent.name} error: ${err.message}`);
1290
+ });
1291
+ }
1292
+ catch (err) {
1293
+ console.error(`Failed to spawn ${agent.name}:`, err);
1294
+ }
1295
+ };
1296
+ // Handle inbox changes
1297
+ const handleInboxChange = (filePath) => {
1298
+ const agentName = path.basename(path.dirname(filePath));
1299
+ const agent = agents.find((a) => a.name === agentName);
1300
+ if (!agent)
1301
+ return;
1302
+ // Check if file grew (new messages)
1303
+ const currentSize = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
1304
+ const previousSize = lastInboxSize.get(agentName) || 0;
1305
+ lastInboxSize.set(agentName, currentSize);
1306
+ if (currentSize <= previousSize)
1307
+ return; // File shrunk or same
1308
+ console.log(`[${new Date().toISOString()}] New message for ${agentName}`);
1309
+ // Debounce
1310
+ const timer = debounceTimers.get(agentName);
1311
+ if (timer)
1312
+ clearTimeout(timer);
1313
+ debounceTimers.set(agentName, setTimeout(() => {
1314
+ debounceTimers.delete(agentName);
1315
+ spawnAgent(agent);
1316
+ }, debounceMs));
1317
+ };
1318
+ // Initialize inbox sizes
1319
+ for (const agent of agents) {
1320
+ const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
1321
+ if (fs.existsSync(inboxPath)) {
1322
+ lastInboxSize.set(agent.name, fs.statSync(inboxPath).size);
1323
+ }
1324
+ }
1325
+ // Start watching
1326
+ const watcher = chokidar.watch(path.join(options.dataDir, '*/inbox.md'), {
1327
+ persistent: true,
1328
+ ignoreInitial: true,
1329
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
1330
+ });
1331
+ watcher.on('change', handleInboxChange);
1332
+ watcher.on('add', handleInboxChange);
1333
+ console.log('Listening... (Ctrl+C to stop)\n');
1334
+ process.on('SIGINT', () => {
1335
+ console.log('\nShutting down...');
1336
+ watcher.close();
1337
+ for (const timer of debounceTimers.values())
1338
+ clearTimeout(timer);
1339
+ process.exit(0);
1340
+ });
1341
+ });
1342
+ // One command to start everything
1343
+ program
1344
+ .command('team-start')
1345
+ .description('Start a team - sets up, listens, and spawns all agents')
1346
+ .option('-f, --file <path>', 'Team config JSON file')
1347
+ .option('-d, --data-dir <path>', 'Team data directory', '/tmp/agent-relay-team')
1348
+ .option('--spawn', 'Immediately spawn all agents', false)
1349
+ .option('--dashboard', 'Start dashboard server', false)
1350
+ .option('--dashboard-port <port>', 'Dashboard port', '3888')
1351
+ .action(async (options) => {
1352
+ const chokidar = await import('chokidar');
1353
+ const { spawn, execSync } = await import('child_process');
1354
+ const { AgentStateManager } = await import('../state/agent-state.js');
1355
+ console.log('');
1356
+ console.log('╔═══════════════════════════════════════╗');
1357
+ console.log('ā•‘ AGENT RELAY - TEAM START ā•‘');
1358
+ console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
1359
+ console.log('');
1360
+ // Step 1: Setup team if config provided
1361
+ const configPath = path.join(options.dataDir, 'team.json');
1362
+ if (options.file && fs.existsSync(options.file)) {
1363
+ console.log('šŸ“‹ Setting up team from config...');
1364
+ const fileConfig = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
1365
+ const projectDir = fileConfig.project || process.cwd();
1366
+ // Create team directory
1367
+ fs.mkdirSync(options.dataDir, { recursive: true });
1368
+ // Save team config
1369
+ const teamConfig = {
1370
+ name: fileConfig.name || 'team',
1371
+ projectDir,
1372
+ createdAt: new Date().toISOString(),
1373
+ agents: fileConfig.agents || [],
1374
+ };
1375
+ fs.writeFileSync(configPath, JSON.stringify(teamConfig, null, 2));
1376
+ // Create agent directories and instructions
1377
+ for (const agent of teamConfig.agents) {
1378
+ const agentDir = path.join(options.dataDir, agent.name);
1379
+ fs.mkdirSync(agentDir, { recursive: true });
1380
+ fs.writeFileSync(path.join(agentDir, 'inbox.md'), '');
1381
+ const teammates = teamConfig.agents.filter((a) => a.name !== agent.name).map((a) => a.name);
1382
+ const taskList = (agent.tasks || []).map((t, i) => `${i + 1}. ${t}`).join('\n') || '(Check with teammates)';
1383
+ const instructions = `# You are ${agent.name} - ${agent.role}
1384
+
1385
+ ## Project: \`${projectDir}\`
1386
+
1387
+ ## Tasks
1388
+ ${taskList}
1389
+
1390
+ ## Teammates: ${teammates.join(', ') || 'none'}
1391
+
1392
+ ## Commands
1393
+ \`\`\`bash
1394
+ # Check inbox
1395
+ node ${projectDir}/dist/cli/index.js team-check -n ${agent.name} -d ${options.dataDir} --no-wait
1396
+
1397
+ # Send message
1398
+ node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t RECIPIENT -m "message" -d ${options.dataDir}
1399
+
1400
+ # Broadcast
1401
+ node ${projectDir}/dist/cli/index.js team-send -n ${agent.name} -t "*" -m "message" -d ${options.dataDir}
1402
+ \`\`\`
1403
+
1404
+ ## Work Loop (MUST FOLLOW)
1405
+ 1. CHECK inbox
1406
+ 2. RESPOND to messages
1407
+ 3. DO one task step
1408
+ 4. BROADCAST status
1409
+ 5. REPEAT
1410
+
1411
+ **Check inbox after every action!**
1412
+ `;
1413
+ fs.writeFileSync(path.join(agentDir, 'INSTRUCTIONS.md'), instructions);
1414
+ console.log(` āœ“ ${agent.name} (${agent.cli})`);
1415
+ }
1416
+ console.log('');
1417
+ }
1418
+ // Load config
1419
+ if (!fs.existsSync(configPath)) {
1420
+ console.error('āŒ No team config found. Provide -f <config.json> or run team-setup first.');
1421
+ process.exit(1);
1422
+ }
1423
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1424
+ const stateManager = new AgentStateManager(options.dataDir);
1425
+ console.log(`šŸ“ Team: ${config.name}`);
1426
+ console.log(`šŸ“‚ Directory: ${options.dataDir}`);
1427
+ console.log(`šŸ‘„ Agents: ${config.agents.map((a) => a.name).join(', ')}`);
1428
+ console.log('');
1429
+ // Step 2: Start dashboard if requested
1430
+ if (options.dashboard) {
1431
+ console.log(`šŸ–„ļø Starting dashboard on port ${options.dashboardPort}...`);
1432
+ const dashboardChild = spawn('node', [
1433
+ path.join(config.projectDir, 'dist/cli/index.js'),
1434
+ 'dashboard',
1435
+ '-p', options.dashboardPort,
1436
+ '-d', options.dataDir,
1437
+ ], {
1438
+ cwd: config.projectDir,
1439
+ stdio: 'ignore',
1440
+ detached: true,
1441
+ });
1442
+ dashboardChild.unref();
1443
+ console.log(` āœ“ Dashboard: http://localhost:${options.dashboardPort}`);
1444
+ console.log('');
1445
+ }
1446
+ // Step 3: Spawn agents if requested
1447
+ if (options.spawn) {
1448
+ console.log('šŸš€ Opening agent terminals...');
1449
+ const getCliCmd = (cli) => {
1450
+ switch (cli.toLowerCase()) {
1451
+ case 'claude': return 'claude --dangerously-skip-permissions';
1452
+ case 'codex': return 'codex';
1453
+ case 'gemini': return 'gemini';
1454
+ default: return cli;
1455
+ }
1456
+ };
1457
+ for (const agent of config.agents) {
1458
+ const cliCmd = getCliCmd(agent.cli);
1459
+ const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
1460
+ const prompt = `Read ${instructionsPath} and start working. Check inbox first, then begin your tasks.`;
1461
+ // Escape for shell
1462
+ const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/'/g, "'\\''");
1463
+ const fullCmd = `cd "${config.projectDir}" && ${cliCmd} -p "${escapedPrompt}"`;
1464
+ try {
1465
+ // Open in new Terminal.app window (macOS)
1466
+ const appleScript = `
1467
+ tell application "Terminal"
1468
+ activate
1469
+ do script "${fullCmd.replace(/"/g, '\\"')}"
1470
+ set custom title of front window to "${agent.name} (${agent.cli})"
1471
+ end tell
1472
+ `;
1473
+ execSync(`osascript -e '${appleScript.replace(/'/g, "'\\''")}'`, { stdio: 'ignore' });
1474
+ console.log(` āœ“ ${agent.name} (${agent.cli}) - new terminal window`);
1475
+ // Small delay between opening windows
1476
+ await new Promise(r => setTimeout(r, 500));
1477
+ }
1478
+ catch (_err) {
1479
+ // Fallback: try iTerm2
1480
+ try {
1481
+ const iTermScript = `
1482
+ tell application "iTerm"
1483
+ activate
1484
+ create window with default profile
1485
+ tell current session of current window
1486
+ write text "${fullCmd.replace(/"/g, '\\"')}"
1487
+ end tell
1488
+ end tell
1489
+ `;
1490
+ execSync(`osascript -e '${iTermScript.replace(/'/g, "'\\''")}'`, { stdio: 'ignore' });
1491
+ console.log(` āœ“ ${agent.name} (${agent.cli}) - new iTerm window`);
1492
+ }
1493
+ catch {
1494
+ console.log(` āœ— ${agent.name} - couldn't open terminal (run manually)`);
1495
+ console.log(` ${fullCmd}`);
1496
+ }
1497
+ }
1498
+ }
1499
+ console.log('');
1500
+ }
1501
+ // Step 4: Start listening for messages
1502
+ console.log('šŸ‘‚ Listening for messages...');
1503
+ console.log(' When agents receive messages, they will be notified.');
1504
+ console.log('');
1505
+ const lastSpawn = new Map();
1506
+ const debounceTimers = new Map();
1507
+ const lastInboxSize = new Map();
1508
+ const cooldownMs = 60000;
1509
+ const debounceMs = 3000;
1510
+ const getSpawnCmd = (cli) => {
1511
+ switch (cli.toLowerCase()) {
1512
+ case 'claude': return { cmd: 'claude', args: ['--dangerously-skip-permissions'] };
1513
+ case 'codex': return { cmd: 'codex', args: [] };
1514
+ case 'gemini': return { cmd: 'gemini', args: [] };
1515
+ default: return { cmd: cli, args: [] };
1516
+ }
1517
+ };
1518
+ const spawnAgent = (agent) => {
1519
+ const now = Date.now();
1520
+ const last = lastSpawn.get(agent.name) || 0;
1521
+ if (now - last < cooldownMs)
1522
+ return;
1523
+ const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
1524
+ if (!fs.existsSync(inboxPath))
1525
+ return;
1526
+ const content = fs.readFileSync(inboxPath, 'utf-8');
1527
+ if (!content.includes('## Message from'))
1528
+ return;
1529
+ const { cmd, args } = getSpawnCmd(agent.cli);
1530
+ const instructionsPath = path.join(options.dataDir, agent.name, 'INSTRUCTIONS.md');
1531
+ const stateContext = stateManager.formatAsContext(agent.name);
1532
+ const prompt = `${stateContext}
1533
+
1534
+ NEW MESSAGES! Read ${instructionsPath}, check inbox, respond, do one task, broadcast status.`;
1535
+ console.log(` → Spawning ${agent.name}...`);
1536
+ lastSpawn.set(agent.name, now);
1537
+ try {
1538
+ const child = spawn(cmd, [...args, '-p', prompt], {
1539
+ cwd: config.projectDir,
1540
+ stdio: 'inherit',
1541
+ });
1542
+ child.on('exit', () => console.log(` ← ${agent.name} exited`));
1543
+ }
1544
+ catch (_err) {
1545
+ console.error(` āœ— Failed to spawn ${agent.name}`);
1546
+ }
1547
+ };
1548
+ // Initialize sizes
1549
+ for (const agent of config.agents) {
1550
+ const inboxPath = path.join(options.dataDir, agent.name, 'inbox.md');
1551
+ if (fs.existsSync(inboxPath)) {
1552
+ lastInboxSize.set(agent.name, fs.statSync(inboxPath).size);
1553
+ }
1554
+ }
1555
+ // Watch inboxes
1556
+ const watcher = chokidar.watch(path.join(options.dataDir, '*/inbox.md'), {
1557
+ persistent: true,
1558
+ ignoreInitial: true,
1559
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
1560
+ });
1561
+ watcher.on('change', (filePath) => {
1562
+ const agentName = path.basename(path.dirname(filePath));
1563
+ const agent = config.agents.find((a) => a.name === agentName);
1564
+ if (!agent)
1565
+ return;
1566
+ const currentSize = fs.statSync(filePath).size;
1567
+ const previousSize = lastInboxSize.get(agentName) || 0;
1568
+ lastInboxSize.set(agentName, currentSize);
1569
+ if (currentSize <= previousSize)
1570
+ return;
1571
+ console.log(` šŸ“Ø New message for ${agentName}`);
1572
+ const timer = debounceTimers.get(agentName);
1573
+ if (timer)
1574
+ clearTimeout(timer);
1575
+ debounceTimers.set(agentName, setTimeout(() => {
1576
+ debounceTimers.delete(agentName);
1577
+ spawnAgent(agent);
1578
+ }, debounceMs));
1579
+ });
1580
+ console.log('Ready! Send messages with:');
1581
+ console.log(` node dist/cli/index.js team-send -n You -t AgentName -m "message" -d ${options.dataDir}`);
1582
+ console.log('');
1583
+ console.log('Press Ctrl+C to stop.');
1584
+ process.on('SIGINT', () => {
1585
+ console.log('\nšŸ‘‹ Shutting down...');
1586
+ watcher.close();
1587
+ process.exit(0);
1588
+ });
1589
+ });
1590
+ program.parse();
1591
+ //# sourceMappingURL=index.js.map