agent-relay 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +23 -9
  2. package/dist/bridge/spawner.js +39 -75
  3. package/dist/cli/index.d.ts +8 -6
  4. package/dist/cli/index.js +251 -30
  5. package/dist/daemon/agent-manager.js +4 -0
  6. package/dist/daemon/connection.js +17 -9
  7. package/dist/daemon/router.js +2 -2
  8. package/dist/dashboard/out/404.html +1 -0
  9. package/dist/dashboard/out/_next/static/R-uQOUcOLINtsp6ACeZa9/_buildManifest.js +1 -0
  10. package/dist/dashboard/out/_next/static/R-uQOUcOLINtsp6ACeZa9/_ssgManifest.js +1 -0
  11. package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
  12. package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +2 -0
  13. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  14. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  15. package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +1 -0
  16. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  17. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  18. package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  19. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  20. package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
  21. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +1 -0
  22. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8553743baca53a00.js +1 -0
  23. package/dist/dashboard/out/_next/static/chunks/app/app/page-7f64824ae7d06707.js +1 -0
  24. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
  25. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +1 -0
  26. package/dist/dashboard/out/_next/static/chunks/app/history/page-abb9ab2d329f56e9.js +1 -0
  27. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  28. package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
  29. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
  30. package/dist/dashboard/out/_next/static/chunks/app/page-814efc4d77b4191d.js +1 -0
  31. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +1 -0
  32. package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
  33. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
  34. package/dist/dashboard/out/_next/static/chunks/app/signup/page-68d34f50baa8ab6b.js +1 -0
  35. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  36. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
  37. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  38. package/dist/dashboard/out/_next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
  39. package/dist/dashboard/out/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +1 -0
  40. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  41. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  42. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  43. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  44. package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
  45. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  46. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  47. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  48. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  49. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  50. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  51. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  52. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  53. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  54. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  55. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  56. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  57. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  58. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  59. package/dist/dashboard/out/app/onboarding.html +1 -0
  60. package/dist/dashboard/out/app/onboarding.txt +7 -0
  61. package/dist/dashboard/out/app.html +1 -0
  62. package/dist/dashboard/out/app.txt +7 -0
  63. package/dist/dashboard/out/apple-icon.png +0 -0
  64. package/dist/dashboard/out/cloud/link.html +1 -0
  65. package/dist/dashboard/out/cloud/link.txt +7 -0
  66. package/dist/dashboard/out/connect-repos.html +1 -0
  67. package/dist/dashboard/out/connect-repos.txt +7 -0
  68. package/dist/dashboard/out/history.html +1 -0
  69. package/dist/dashboard/out/history.txt +7 -0
  70. package/dist/dashboard/out/index.html +1 -0
  71. package/dist/dashboard/out/index.txt +7 -0
  72. package/dist/dashboard/out/login.html +5 -0
  73. package/dist/dashboard/out/login.txt +7 -0
  74. package/dist/dashboard/out/metrics.html +1 -0
  75. package/dist/dashboard/out/metrics.txt +7 -0
  76. package/dist/dashboard/out/pricing.html +13 -0
  77. package/dist/dashboard/out/pricing.txt +7 -0
  78. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  79. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  80. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  81. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  82. package/dist/dashboard/out/providers.html +1 -0
  83. package/dist/dashboard/out/providers.txt +7 -0
  84. package/dist/dashboard/out/signup.html +6 -0
  85. package/dist/dashboard/out/signup.txt +7 -0
  86. package/dist/dashboard-server/metrics.d.ts +105 -0
  87. package/dist/dashboard-server/metrics.js +193 -0
  88. package/dist/dashboard-server/needs-attention.d.ts +24 -0
  89. package/dist/dashboard-server/needs-attention.js +78 -0
  90. package/dist/dashboard-server/server.d.ts +15 -0
  91. package/dist/dashboard-server/server.js +3992 -0
  92. package/dist/dashboard-server/start.d.ts +6 -0
  93. package/dist/dashboard-server/start.js +13 -0
  94. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  95. package/dist/dashboard-server/user-bridge.js +189 -0
  96. package/dist/wrapper/base-wrapper.d.ts +4 -0
  97. package/dist/wrapper/base-wrapper.js +12 -4
  98. package/dist/wrapper/client.js +5 -0
  99. package/dist/wrapper/parser.js +2 -2
  100. package/dist/wrapper/pty-wrapper.d.ts +7 -1
  101. package/dist/wrapper/pty-wrapper.js +81 -5
  102. package/dist/wrapper/shared.d.ts +1 -1
  103. package/dist/wrapper/shared.js +1 -1
  104. package/dist/wrapper/tmux-wrapper.d.ts +8 -1
  105. package/dist/wrapper/tmux-wrapper.js +103 -28
  106. package/package.json +5 -3
package/README.md CHANGED
@@ -17,15 +17,27 @@ sudo apt-get update && sudo apt-get install -y build-essential
17
17
 
18
18
  ## Quick Start
19
19
 
20
+ ```bash
21
+ # Start Mega coordinator with Claude (starts daemon automatically)
22
+ agent-relay claude
23
+
24
+ # Or with Codex
25
+ agent-relay codex
26
+ ```
27
+
28
+ The `claude` and `codex` commands start a Mega coordinator agent that can spawn and manage worker agents.
29
+
30
+ ### Manual Setup
31
+
20
32
  ```bash
21
33
  # Terminal 1: Start daemon
22
34
  agent-relay up
23
35
 
24
36
  # Terminal 2: Start an agent
25
- agent-relay -n Alice claude
37
+ agent-relay create-agent -n Alice claude
26
38
 
27
39
  # Terminal 3: Start another agent
28
- agent-relay -n Bob codex
40
+ agent-relay create-agent -n Bob codex
29
41
  ```
30
42
 
31
43
  Agents communicate by outputting `->relay:` patterns. Always use the fenced format:
@@ -42,8 +54,10 @@ Broadcasting to everyone>>>
42
54
 
43
55
  | Command | Description |
44
56
  |---------|-------------|
45
- | `agent-relay <cmd>` | Wrap agent with messaging |
46
- | `agent-relay -n Name <cmd>` | Wrap with specific name |
57
+ | `agent-relay claude` | Start daemon + Mega coordinator with Claude |
58
+ | `agent-relay codex` | Start daemon + Mega coordinator with Codex |
59
+ | `agent-relay create-agent <cmd>` | Wrap agent with messaging |
60
+ | `agent-relay create-agent -n Name <cmd>` | Wrap with specific name |
47
61
  | `agent-relay up` | Start daemon + dashboard |
48
62
  | `agent-relay down` | Stop daemon |
49
63
  | `agent-relay status` | Check if running |
@@ -53,7 +67,7 @@ Broadcasting to everyone>>>
53
67
  ## How It Works
54
68
 
55
69
  1. `agent-relay up` starts a daemon that routes messages via Unix socket
56
- 2. `agent-relay <cmd>` wraps your agent in tmux, parsing output for `->relay:` patterns
70
+ 2. `agent-relay create-agent <cmd>` wraps your agent in tmux, parsing output for `->relay:` patterns
57
71
  3. Messages are injected into recipient terminals in real-time
58
72
 
59
73
  ```
@@ -109,9 +123,9 @@ Agent names automatically match role definitions (case-insensitive):
109
123
 
110
124
  ```bash
111
125
  # If .claude/agents/lead.md exists:
112
- agent-relay -n Lead claude # matches lead.md
113
- agent-relay -n LEAD claude # matches lead.md
114
- agent-relay -n lead claude # matches lead.md
126
+ agent-relay create-agent -n Lead claude # matches lead.md
127
+ agent-relay create-agent -n LEAD claude # matches lead.md
128
+ agent-relay create-agent -n lead claude # matches lead.md
115
129
 
116
130
  # Supported locations:
117
131
  # - .claude/agents/<name>.md
@@ -140,7 +154,7 @@ agent-relay bridge ~/auth ~/frontend ~/api
140
154
  ### Workflow
141
155
 
142
156
  1. **Start daemons** in each project: `agent-relay up`
143
- 2. **Start agents** in each project: `agent-relay -n Alice claude`
157
+ 2. **Start agents** in each project: `agent-relay create-agent -n Alice claude`
144
158
  3. **Bridge** from anywhere: `agent-relay bridge ~/project1 ~/project2`
145
159
 
146
160
  ### Cross-Project Messaging
@@ -14,16 +14,33 @@ import { AgentPolicyService } from '../policy/agent-policy.js';
14
14
  import { buildClaudeArgs } from '../utils/agent-config.js';
15
15
  import { getUserDirectoryService } from '../daemon/user-directory.js';
16
16
  /**
17
- * Get a minimal relay reminder.
18
- * Agents already have full relay docs via CLAUDE.md - this is just a brief reminder.
19
- * Loading full docs (400+ lines) overwhelms agents and causes "meandering".
17
+ * Get relay protocol instructions for a spawned agent.
18
+ * This provides the agent with the communication protocol it needs to work with the relay.
20
19
  */
21
- function getMinimalRelayReminder() {
22
- return `# Quick Relay Reference
23
- - Send: \`->relay:Name <<<message>>>\`
24
- - ACK tasks, send DONE when complete
25
- - Use \`trail start/decision/complete\` for trajectories
26
- - Output \`[[SESSION_END]]..[[/SESSION_END]]\` when done`;
20
+ function getRelayInstructions(agentName) {
21
+ return [
22
+ '# Agent Relay Protocol',
23
+ '',
24
+ `You are agent "${agentName}" connected to Agent Relay for multi-agent coordination.`,
25
+ '',
26
+ '## Sending Messages',
27
+ '',
28
+ 'Use fenced format for all messages:',
29
+ '->relay:TargetAgent <<<',
30
+ 'Your message here.>>>',
31
+ '',
32
+ '## Communication Rules',
33
+ '',
34
+ '1. **ACK immediately** - When you receive a task:',
35
+ ' ->relay:Sender <<<',
36
+ ' ACK: Brief description of task received>>>',
37
+ '',
38
+ '2. **Report completion** - When done:',
39
+ ' ->relay:Sender <<<',
40
+ ' DONE: Brief summary of what was completed>>>',
41
+ '',
42
+ '3. Close >>> must immediately follow content (no blank lines before it)',
43
+ ].join('\n');
27
44
  }
28
45
  export class AgentSpawner {
29
46
  activeWorkers = new Map();
@@ -209,6 +226,14 @@ export class AgentSpawner {
209
226
  if (isCodexCli && !args.includes('--dangerously-bypass-approvals-and-sandbox')) {
210
227
  args.push('--dangerously-bypass-approvals-and-sandbox');
211
228
  }
229
+ // Inject relay protocol instructions via CLI-specific system prompt
230
+ const relayInstructions = getRelayInstructions(name);
231
+ if (isClaudeCli && !args.includes('--append-system-prompt')) {
232
+ args.push('--append-system-prompt', relayInstructions);
233
+ }
234
+ else if (isCodexCli && !args.some(a => a.includes('developer_instructions'))) {
235
+ args.push('--config', `developer_instructions=${relayInstructions}`);
236
+ }
212
237
  if (debug)
213
238
  console.log(`[spawner:debug] Spawning ${name} with: ${command} ${args.join(' ')}`);
214
239
  // Create PtyWrapper config
@@ -247,6 +272,8 @@ export class AgentSpawner {
247
272
  // Shadow agent configuration
248
273
  shadowOf: request.shadowOf,
249
274
  shadowSpeakOn: request.shadowSpeakOn,
275
+ // Skip continuity for spawned agents - they're short-lived workers
276
+ skipContinuity: true,
250
277
  // Only use callbacks if dashboardPort is not set (for backwards compatibility)
251
278
  onSpawn: this.dashboardPort ? undefined : async (workerName, workerCli, workerTask) => {
252
279
  // Handle nested spawn requests (legacy path, may fail in non-TTY)
@@ -330,72 +357,9 @@ export class AgentSpawner {
330
357
  error,
331
358
  };
332
359
  }
333
- // Build the full message: minimal relay reminder + policy instructions (if any) + task
334
- // Only build message if there's an actual task - empty task means interactive mode
335
- let fullMessage = task || '';
336
- // Only prepend relay reminder if we have an actual task
337
- // Empty task = interactive mode, user will respond to prompts directly
338
- if (fullMessage.trim()) {
339
- // Prepend a brief relay reminder (agents have full docs via CLAUDE.md)
340
- // Note: Previously loaded full 400+ line docs which overwhelmed agents
341
- const relayReminder = getMinimalRelayReminder();
342
- if (relayReminder) {
343
- fullMessage = `${relayReminder}\n\n---\n\n${fullMessage}`;
344
- if (debug)
345
- console.log(`[spawner:debug] Prepended relay reminder for ${name}`);
346
- }
347
- }
348
- // Prepend policy instructions if enforcement is enabled (only if we have a task)
349
- if (fullMessage.trim() && this.policyEnforcementEnabled && this.policyService) {
350
- const policyInstruction = await this.policyService.getPolicyInstruction(name);
351
- if (policyInstruction) {
352
- fullMessage = `${policyInstruction}\n\n${fullMessage}`;
353
- if (debug)
354
- console.log(`[spawner:debug] Prepended policy instructions to task for ${name}`);
355
- }
356
- }
357
- // Send task via relay message if provided (not via direct PTY injection)
358
- // This ensures the agent is ready to receive before processing the task
359
- if (fullMessage && fullMessage.trim()) {
360
- if (debug)
361
- console.log(`[spawner:debug] Will send task via relay: ${fullMessage.substring(0, 50)}...`);
362
- // If we have dashboard API, send task as relay message
363
- if (this.dashboardPort) {
364
- // Wait a moment for the agent's relay client to be ready
365
- await sleep(1000);
366
- try {
367
- const response = await fetch(`http://localhost:${this.dashboardPort}/api/send`, {
368
- method: 'POST',
369
- headers: { 'Content-Type': 'application/json' },
370
- body: JSON.stringify({
371
- to: name,
372
- message: fullMessage,
373
- from: '__spawner__',
374
- }),
375
- });
376
- const result = await response.json();
377
- if (result.success) {
378
- if (debug)
379
- console.log(`[spawner:debug] Task sent via relay to ${name}`);
380
- }
381
- else {
382
- console.warn(`[spawner] Failed to send task via relay: ${result.error}`);
383
- // Fall back to direct injection
384
- pty.write(fullMessage + '\r');
385
- }
386
- }
387
- catch (err) {
388
- console.warn(`[spawner] Relay send failed, falling back to direct injection: ${err.message}`);
389
- pty.write(fullMessage + '\r');
390
- }
391
- }
392
- else {
393
- // No dashboard API available - use direct injection as fallback
394
- if (debug)
395
- console.log(`[spawner:debug] No dashboard API, using direct injection`);
396
- pty.write(fullMessage + '\r');
397
- }
398
- }
360
+ // Note: Task is NOT sent here. The spawning agent (wrapper) waits for the worker
361
+ // to come online and then sends the task via normal relay message.
362
+ // This avoids race conditions with the agent's readyForMessages state.
399
363
  // Track the worker
400
364
  const workerInfo = {
401
365
  name,
@@ -3,12 +3,14 @@
3
3
  * Agent Relay CLI
4
4
  *
5
5
  * Commands:
6
- * relay <cmd> - Wrap agent with real-time messaging (default)
7
- * relay -n Name cmd - Wrap with specific agent name
8
- * relay up - Start daemon
9
- * relay read <id> - Read full message by ID
10
- * relay agents - List connected agents
11
- * relay who - Show currently active agents
6
+ * relay claude - Start daemon + Dashboard coordinator with Claude
7
+ * relay codex - Start daemon + Dasbboard coordinator with Codex
8
+ * relay create-agent <cmd> - Wrap agent with real-time messaging
9
+ * relay create-agent -n Name cmd - Wrap with specific agent name
10
+ * relay up - Start daemon + dashboard
11
+ * relay read <id> - Read full message by ID
12
+ * relay agents - List connected agents
13
+ * relay who - Show currently active agents
12
14
  */
13
15
  export {};
14
16
  //# sourceMappingURL=index.d.ts.map
package/dist/cli/index.js CHANGED
@@ -3,12 +3,14 @@
3
3
  * Agent Relay CLI
4
4
  *
5
5
  * Commands:
6
- * relay <cmd> - Wrap agent with real-time messaging (default)
7
- * relay -n Name cmd - Wrap with specific agent name
8
- * relay up - Start daemon
9
- * relay read <id> - Read full message by ID
10
- * relay agents - List connected agents
11
- * relay who - Show currently active agents
6
+ * relay claude - Start daemon + Dashboard coordinator with Claude
7
+ * relay codex - Start daemon + Dasbboard coordinator with Codex
8
+ * relay create-agent <cmd> - Wrap agent with real-time messaging
9
+ * relay create-agent -n Name cmd - Wrap with specific agent name
10
+ * relay up - Start daemon + dashboard
11
+ * relay read <id> - Read full message by ID
12
+ * relay agents - List connected agents
13
+ * relay who - Show currently active agents
12
14
  */
13
15
  import { Command } from 'commander';
14
16
  import { config as dotenvConfig } from 'dotenv';
@@ -26,6 +28,7 @@ import { promisify } from 'node:util';
26
28
  import { exec } from 'node:child_process';
27
29
  import { fileURLToPath } from 'node:url';
28
30
  dotenvConfig();
31
+ const DEFAULT_DASHBOARD_PORT = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
29
32
  // Read version from package.json
30
33
  const __filename = fileURLToPath(import.meta.url);
31
34
  const __dirname = path.dirname(__filename);
@@ -35,7 +38,7 @@ const VERSION = packageJson.version;
35
38
  const execAsync = promisify(exec);
36
39
  // Check for updates in background (non-blocking)
37
40
  // Only show notification for interactive commands, not when wrapping agents or running update
38
- const interactiveCommands = ['up', 'down', 'status', 'agents', 'who', 'version', '--version', '-V', '--help', '-h'];
41
+ const interactiveCommands = ['up', 'down', 'status', 'agents', 'who', 'version', '--version', '-V', '--help', '-h', 'create-agent', 'claude', 'codex'];
39
42
  const shouldCheckUpdates = process.argv.length > 2 &&
40
43
  interactiveCommands.includes(process.argv[2]);
41
44
  if (shouldCheckUpdates) {
@@ -49,20 +52,19 @@ program
49
52
  .name('agent-relay')
50
53
  .description('Agent-to-agent messaging')
51
54
  .version(VERSION, '-V, --version', 'Output the version number');
52
- // Default action = wrap agent
55
+ // create-agent - Wrap agent with real-time messaging
53
56
  program
57
+ .command('create-agent')
58
+ .description('Wrap an agent with real-time messaging')
54
59
  .option('-n, --name <name>', 'Agent name (auto-generated if not set)')
55
- .option('-q, --quiet', 'Disable debug output', false)
60
+ .option('-d, --debug', 'Enable debug output')
56
61
  .option('--prefix <pattern>', 'Relay prefix pattern (default: ->relay:)')
62
+ .option('--dashboard-port <port>', 'Dashboard port for spawn/release API (auto-detected if not set)')
57
63
  .option('--shadow <name>', 'Spawn a shadow agent with this name that monitors the primary')
58
64
  .option('--shadow-role <role>', 'Shadow role: reviewer, auditor, or triggers (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
59
- .argument('[command...]', 'Command to wrap (e.g., claude)')
65
+ .option('--skip-instructions', 'Skip initial instruction injection (use with --append-system-prompt)')
66
+ .argument('<command...>', 'Command to wrap (e.g., claude)')
60
67
  .action(async (commandParts, options) => {
61
- // If no command provided, show help
62
- if (!commandParts || commandParts.length === 0) {
63
- program.help();
64
- return;
65
- }
66
68
  const { getProjectPaths } = await import('../utils/project-namespace.js');
67
69
  const { findAgentConfig, isClaudeCli, buildClaudeArgs } = await import('../utils/agent-config.js');
68
70
  const paths = getProjectPaths();
@@ -84,18 +86,53 @@ program
84
86
  }
85
87
  const { TmuxWrapper } = await import('../wrapper/tmux-wrapper.js');
86
88
  const { AgentSpawner } = await import('../bridge/spawner.js');
87
- // Create spawner for spawn/release callbacks
88
- const spawner = new AgentSpawner(paths.projectRoot);
89
+ // Determine dashboard port for spawn/release API
90
+ // Priority: CLI flag > env var > auto-detect default port
91
+ let dashboardPort;
92
+ if (options.dashboardPort) {
93
+ dashboardPort = parseInt(options.dashboardPort, 10);
94
+ }
95
+ else {
96
+ // Try to detect if dashboard is running at common ports
97
+ const portsToTry = [
98
+ parseInt(DEFAULT_DASHBOARD_PORT, 10),
99
+ 3889, 3890, 3891, // Common fallback ports when default is in use
100
+ ];
101
+ for (const port of portsToTry) {
102
+ try {
103
+ const response = await fetch(`http://localhost:${port}/api/health`, {
104
+ method: 'GET',
105
+ signal: AbortSignal.timeout(300), // Quick timeout for detection
106
+ });
107
+ if (response.ok) {
108
+ const health = await response.json();
109
+ if (health.status === 'healthy') {
110
+ dashboardPort = port;
111
+ console.error(`Dashboard detected: http://localhost:${dashboardPort}`);
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ // Try next port
118
+ }
119
+ }
120
+ }
121
+ // Create spawner as fallback for direct spawn (if dashboard API not available)
122
+ const spawner = new AgentSpawner(paths.projectRoot, undefined, dashboardPort);
89
123
  const wrapper = new TmuxWrapper({
90
124
  name: agentName,
91
125
  command: mainCommand,
92
126
  args: finalArgs,
93
127
  socketPath: paths.socketPath,
94
- debug: false, // Use -q to keep quiet (debug off by default)
128
+ debug: options.debug ?? false,
95
129
  relayPrefix: options.prefix,
96
130
  useInbox: true,
97
131
  inboxDir: paths.dataDir, // Use the project-specific data directory for the inbox
98
- // Wire up spawn/release callbacks
132
+ skipInstructions: options.skipInstructions,
133
+ // Use dashboard API for spawn/release when available (preferred - works from any context)
134
+ dashboardPort,
135
+ // Wire up spawn/release callbacks as fallback (if no dashboardPort)
99
136
  onSpawn: async (workerName, workerCli, task) => {
100
137
  console.error(`[${agentName}] Spawning ${workerName} (${workerCli})...`);
101
138
  const result = await spawner.spawn({
@@ -199,10 +236,12 @@ program
199
236
  }
200
237
  }
201
238
  });
202
- // up - Start daemon
239
+ // up - Start daemon + dashboard
203
240
  program
204
241
  .command('up')
205
- .description('Start daemon')
242
+ .description('Start daemon + dashboard')
243
+ .option('--no-dashboard', 'Disable web dashboard')
244
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
206
245
  .option('--spawn', 'Force spawn all agents from teams.json')
207
246
  .option('--no-spawn', 'Do not auto-spawn agents (just start daemon)')
208
247
  .option('--watch', 'Auto-restart daemon on crash (supervisor mode)')
@@ -219,6 +258,10 @@ program
219
258
  const startDaemon = () => {
220
259
  // Build args without --watch to prevent infinite recursion
221
260
  const args = ['up'];
261
+ if (options.dashboard === false)
262
+ args.push('--no-dashboard');
263
+ if (options.port)
264
+ args.push('--port', options.port);
222
265
  if (options.spawn === true)
223
266
  args.push('--spawn');
224
267
  if (options.spawn === false)
@@ -330,6 +373,28 @@ program
330
373
  try {
331
374
  await daemon.start();
332
375
  console.log('Daemon started.');
376
+ let dashboardPort;
377
+ // Dashboard starts by default (use --no-dashboard to disable)
378
+ if (options.dashboard !== false) {
379
+ const port = parseInt(options.port, 10);
380
+ const { startDashboard } = await import('../dashboard-server/server.js');
381
+ dashboardPort = await startDashboard({
382
+ port,
383
+ dataDir: paths.dataDir,
384
+ teamDir: paths.teamDir,
385
+ dbPath,
386
+ enableSpawner: true,
387
+ projectRoot: paths.projectRoot,
388
+ });
389
+ console.log(`Dashboard: http://localhost:${dashboardPort}`);
390
+ // Hook daemon log output to dashboard WebSocket
391
+ daemon.onLogOutput = (agentName, data, _timestamp) => {
392
+ const broadcast = global.__broadcastLogOutput;
393
+ if (broadcast) {
394
+ broadcast(agentName, data);
395
+ }
396
+ };
397
+ }
333
398
  // Determine if we should auto-spawn agents
334
399
  // --spawn: force spawn
335
400
  // --no-spawn: never spawn
@@ -342,7 +407,7 @@ program
342
407
  if (shouldSpawn && teamsConfig && teamsConfig.agents.length > 0) {
343
408
  console.log('');
344
409
  console.log('Auto-spawning agents from teams.json...');
345
- spawner = new AgentSpawner(paths.projectRoot);
410
+ spawner = new AgentSpawner(paths.projectRoot, undefined, dashboardPort);
346
411
  for (const agent of teamsConfig.agents) {
347
412
  console.log(` Spawning ${agent.name} (${agent.cli})...`);
348
413
  const result = await spawner.spawn({
@@ -393,6 +458,162 @@ program
393
458
  console.log('Cleaned up stale pid');
394
459
  }
395
460
  });
461
+ // System prompt for Dashboard agent - plain text to avoid shell escaping issues
462
+ const MEGA_SYSTEM_PROMPT = [
463
+ 'You are Dashboard, a lead coordinator in agent-relay.',
464
+ 'Your PRIMARY job is to delegate - you should almost NEVER do implementation work yourself.',
465
+ 'ALWAYS SPAWN AGENTS: For any non-trivial task, spawn specialized workers.',
466
+ ].join(' ');
467
+ // Helper function for starting Dashboard coordinator with a specific provider
468
+ async function startDashboardCoordinator(operator) {
469
+ const { spawn } = await import('node:child_process');
470
+ const { getProjectPaths } = await import('../utils/project-namespace.js');
471
+ const { resolveTmux, TmuxNotFoundError } = await import('../utils/tmux-resolver.js');
472
+ // Check for tmux first
473
+ const tmuxInfo = resolveTmux();
474
+ if (!tmuxInfo) {
475
+ const error = new TmuxNotFoundError();
476
+ console.error(`Error: ${error.message}`);
477
+ process.exit(1);
478
+ }
479
+ const paths = getProjectPaths();
480
+ console.log(`Starting Dashboard with ${operator}...`);
481
+ console.log(`Project: ${paths.projectRoot}`);
482
+ console.log(`Tmux: ${tmuxInfo.path} (v${tmuxInfo.version})`);
483
+ // Step 1: Check if daemon is already running, start if needed
484
+ console.log('\n[1/3] Checking daemon...');
485
+ // Check if socket exists (daemon running)
486
+ const socketExists = fs.existsSync(paths.socketPath);
487
+ // Ports to try for dashboard detection
488
+ const portsToTry = [
489
+ parseInt(DEFAULT_DASHBOARD_PORT, 10),
490
+ 3889, 3890, 3891,
491
+ ];
492
+ // Check if dashboard is responding for THIS project
493
+ let dashboardReady = false;
494
+ let detectedPort;
495
+ // Helper to check health at a port
496
+ const checkPort = async (port) => {
497
+ try {
498
+ const response = await fetch(`http://localhost:${port}/api/health`, {
499
+ signal: AbortSignal.timeout(500),
500
+ });
501
+ if (response.ok) {
502
+ const health = await response.json();
503
+ return health.status === 'healthy';
504
+ }
505
+ }
506
+ catch {
507
+ // Port not responding
508
+ }
509
+ return false;
510
+ };
511
+ if (socketExists) {
512
+ for (const port of portsToTry) {
513
+ if (await checkPort(port)) {
514
+ dashboardReady = true;
515
+ detectedPort = port;
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ if (dashboardReady && detectedPort) {
521
+ console.log(`Daemon already running at port ${detectedPort}, reusing...`);
522
+ }
523
+ else {
524
+ console.log('Starting daemon...');
525
+ const daemonProc = spawn(process.execPath, [process.argv[1], 'up'], {
526
+ stdio: 'ignore',
527
+ detached: true,
528
+ });
529
+ daemonProc.unref();
530
+ // Wait for dashboard to be ready (up to 10 seconds)
531
+ const maxWait = 10000;
532
+ const startTime = Date.now();
533
+ while (Date.now() - startTime < maxWait) {
534
+ await new Promise((resolve) => setTimeout(resolve, 500));
535
+ for (const port of portsToTry) {
536
+ if (await checkPort(port)) {
537
+ dashboardReady = true;
538
+ detectedPort = port;
539
+ break;
540
+ }
541
+ }
542
+ if (dashboardReady)
543
+ break;
544
+ }
545
+ if (!dashboardReady) {
546
+ console.error('Warning: Dashboard may not be fully ready. Spawn might not work.');
547
+ detectedPort = parseInt(DEFAULT_DASHBOARD_PORT, 10); // Fallback
548
+ }
549
+ }
550
+ const dashboardPort = detectedPort || parseInt(DEFAULT_DASHBOARD_PORT, 10);
551
+ // Step 2: Install prpm snippet via npx
552
+ console.log('[2/3] Installing agent-relay snippet...');
553
+ const prpmArgs = operator.toLowerCase() === 'claude'
554
+ ? ['prpm', 'install', '@agent-relay/agent-relay-snippet', '--location', 'CLAUDE.md']
555
+ : ['prpm', 'install', '@agent-relay/agent-relay-snippet'];
556
+ try {
557
+ await new Promise((resolve, reject) => {
558
+ const prpmProc = spawn('npx', prpmArgs, {
559
+ stdio: 'inherit',
560
+ });
561
+ prpmProc.on('close', (code) => {
562
+ if (code === 0)
563
+ resolve();
564
+ else
565
+ reject(new Error(`npx prpm exited with code ${code}`));
566
+ });
567
+ prpmProc.on('error', reject);
568
+ });
569
+ }
570
+ catch (err) {
571
+ console.warn(`Warning: prpm install failed: ${err.message}`);
572
+ console.warn('Continuing without snippet installation...');
573
+ }
574
+ // Step 3: Start Dashboard agent with system prompt
575
+ console.log(`[3/3] Starting Dashboard agent with ${operator}...`);
576
+ console.log('');
577
+ const op = operator.toLowerCase();
578
+ // Build CLI-specific arguments for system prompt
579
+ // These args go AFTER the operator command, passed through to the CLI
580
+ let cliArgs = [];
581
+ if (op === 'claude') {
582
+ // Claude: --append-system-prompt <content> (takes content directly, not file)
583
+ cliArgs = ['--append-system-prompt', MEGA_SYSTEM_PROMPT];
584
+ }
585
+ else if (op === 'codex') {
586
+ // Codex: --config developer_instructions="<content>"
587
+ cliArgs = ['--config', `developer_instructions=${MEGA_SYSTEM_PROMPT}`];
588
+ }
589
+ // Use '--' to separate agent-relay options from the command + its args
590
+ // Format: agent-relay create-agent -n Dashboard --skip-instructions --dashboard-port <port> -- claude --append-system-prompt "..."
591
+ const agentProc = spawn(process.execPath, [process.argv[1], 'create-agent', '-n', 'Dashboard', '--skip-instructions', '--dashboard-port', String(dashboardPort), '--', operator, ...cliArgs], { stdio: 'inherit' });
592
+ // Forward signals to agent process
593
+ process.on('SIGINT', () => {
594
+ agentProc.kill('SIGINT');
595
+ });
596
+ process.on('SIGTERM', () => {
597
+ agentProc.kill('SIGTERM');
598
+ });
599
+ agentProc.on('close', (code) => {
600
+ process.exit(code ?? 0);
601
+ });
602
+ }
603
+ // claude - Start daemon and spawn Dashboard coordinator with Claude
604
+ program
605
+ .command('claude')
606
+ .description('Start daemon and Dashboard coordinator with Claude')
607
+ .action(async () => {
608
+ await startDashboardCoordinator('claude');
609
+ });
610
+ // codex - Start daemon and spawn Dashboard coordinator with Codex
611
+ program
612
+ .command('codex')
613
+ .description('Start daemon and Dashboard coordinator with Codex')
614
+ .action(async () => {
615
+ await startDashboardCoordinator('codex');
616
+ });
396
617
  // status - Check daemon status
397
618
  program
398
619
  .command('status')
@@ -1293,7 +1514,7 @@ program
1293
1514
  .argument('<name>', 'Agent name')
1294
1515
  .argument('<cli>', 'CLI to use (claude, codex, gemini, etc.)')
1295
1516
  .argument('[task]', 'Task description (can also be piped via stdin)')
1296
- .option('--port <port>', 'Dashboard port', '3888')
1517
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1297
1518
  .option('--team <team>', 'Team name for the agent')
1298
1519
  .option('--spawner <name>', 'Name of the agent requesting the spawn (for policy enforcement)')
1299
1520
  .option('--interactive', 'Disable auto-accept of permission prompts (for auth setup flows)')
@@ -1304,7 +1525,7 @@ program
1304
1525
  .option('--shadow-triggers <triggers>', 'When to trigger shadow (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
1305
1526
  .option('--shadow-speak-on <triggers>', 'When shadow should speak (comma-separated, same values as --shadow-triggers)')
1306
1527
  .action(async (name, cli, task, options) => {
1307
- const port = options.port || '3888';
1528
+ const port = options.port || DEFAULT_DASHBOARD_PORT;
1308
1529
  // Read task from stdin if not provided as argument
1309
1530
  let finalTask = task;
1310
1531
  if (!finalTask && !process.stdin.isTTY) {
@@ -1390,9 +1611,9 @@ program
1390
1611
  .command('release')
1391
1612
  .description('Release a spawned agent via API (no terminal required)')
1392
1613
  .argument('<name>', 'Agent name to release')
1393
- .option('--port <port>', 'Dashboard port', '3888')
1614
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1394
1615
  .action(async (name, options) => {
1395
- const port = options.port || '3888';
1616
+ const port = options.port || DEFAULT_DASHBOARD_PORT;
1396
1617
  try {
1397
1618
  const response = await fetch(`http://localhost:${port}/api/spawned/${encodeURIComponent(name)}`, {
1398
1619
  method: 'DELETE',
@@ -1960,12 +2181,12 @@ program
1960
2181
  .command('metrics')
1961
2182
  .description('Show agent memory metrics and resource usage')
1962
2183
  .option('--agent <name>', 'Show metrics for specific agent')
1963
- .option('--port <port>', 'Dashboard port', '3888')
2184
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
1964
2185
  .option('--json', 'Output as JSON')
1965
2186
  .option('--watch', 'Continuously update metrics')
1966
2187
  .option('--interval <ms>', 'Update interval for watch mode', '5000')
1967
2188
  .action(async (options) => {
1968
- const port = options.port || '3888';
2189
+ const port = options.port || DEFAULT_DASHBOARD_PORT;
1969
2190
  const fetchMetrics = async () => {
1970
2191
  try {
1971
2192
  const response = await fetch(`http://localhost:${port}/api/metrics/agents`);
@@ -2079,12 +2300,12 @@ program
2079
2300
  program
2080
2301
  .command('health')
2081
2302
  .description('Show system health, crash insights, and recommendations')
2082
- .option('--port <port>', 'Dashboard port', '3888')
2303
+ .option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
2083
2304
  .option('--json', 'Output as JSON')
2084
2305
  .option('--crashes', 'Show recent crash history')
2085
2306
  .option('--alerts', 'Show unacknowledged alerts')
2086
2307
  .action(async (options) => {
2087
- const port = options.port || '3888';
2308
+ const port = options.port || DEFAULT_DASHBOARD_PORT;
2088
2309
  try {
2089
2310
  const response = await fetch(`http://localhost:${port}/api/metrics/health`);
2090
2311
  if (!response.ok) {
@@ -97,6 +97,8 @@ export class AgentManager extends EventEmitter {
97
97
  args,
98
98
  cwd: workspacePath,
99
99
  logsDir: this.logsDir,
100
+ // Skip continuity for cloud-managed agents
101
+ skipContinuity: true,
100
102
  env: {
101
103
  CLOUD_API_URL: process.env.CLOUD_API_URL || '',
102
104
  WORKSPACE_TOKEN: process.env.WORKSPACE_TOKEN || '',
@@ -341,6 +343,8 @@ export class AgentManager extends EventEmitter {
341
343
  args,
342
344
  cwd: workspacePath,
343
345
  logsDir: this.logsDir,
346
+ // Skip continuity for cloud-managed agents
347
+ skipContinuity: true,
344
348
  env: {
345
349
  CLOUD_API_URL: process.env.CLOUD_API_URL || '',
346
350
  WORKSPACE_TOKEN: process.env.WORKSPACE_TOKEN || '',