From f8d8cb66885e823d5cd6d6c742d4cb2667f95b05 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 17 Jun 2026 14:52:06 -0400 Subject: [PATCH 1/6] refactor: remove the local relay dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local web dashboard (relay-dashboard-server / @agent-relay/dashboard-server) is gone. `agent-relay up` is now a plain broker launcher — attached by default, `--background` to detach — and the dashboard-only flags (`--no-dashboard`, `--port`, `--foreground`) are removed. - broker-lifecycle: drop dashboard spawning, static-asset discovery/refresh, the GitHub dashboard-ui download, the dead reuse-broker branch, and dashboard-port logic. Add `AGENT_RELAY_BROKER_PORT` (resolveBrokerBasePort) for the broker base port; orphan detection now keys on `up` minus `--background` instead of the removed `--foreground`. - core / core-maintenance: remove the dashboard flags, binary resolver, and uninstall handling for dashboard assets/binary/npm package. - config: drop the unused localDashboardUrl / LOCAL_DASHBOARD_URL. - telemetry: drop the `dashboard` / `human_dashboard` enum members. - fleet + e2e harness: AGENT_RELAY_DASHBOARD_PORT -> AGENT_RELAY_BROKER_PORT. - scripts: delete the dashboard launcher and the stale sibling-repo test script; fix removed-flag usages in CI/e2e/demo scripts. - docs: drop dashboard wording and removed flags from the current CLI docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 4 +- packages/cli/src/cli/bootstrap.test.ts | 2 +- packages/cli/src/cli/bootstrap.ts | 2 +- packages/cli/src/cli/commands/core.test.ts | 359 +------- packages/cli/src/cli/commands/core.ts | 90 +- packages/cli/src/cli/commands/fleet.ts | 10 +- packages/cli/src/cli/commands/local-agent.ts | 3 +- .../cli/src/cli/lib/broker-lifecycle.test.ts | 13 +- packages/cli/src/cli/lib/broker-lifecycle.ts | 869 +----------------- packages/cli/src/cli/lib/core-maintenance.ts | 53 +- packages/cli/src/cli/telemetry/events.ts | 6 +- packages/config/src/cloud-config.ts | 9 +- packages/config/src/schemas.ts | 1 - packages/harness-driver/src/spawn-config.ts | 2 +- packages/policy/src/agent-policy.ts | 2 +- scripts/ci-standalone-smoke.sh | 4 +- scripts/demos/server-capacity.sh | 2 +- scripts/demos/sprint-planning.sh | 2 +- scripts/e2e-test.sh | 19 +- scripts/run-dashboard.js | 3 - scripts/test-interactive-terminal.sh | 2 +- scripts/test-spawn-refactor.sh | 510 ---------- scripts/watch-cli-tools.sh | 57 +- tests/e2e/fleet/fleet-e2e.test.ts | 6 +- tests/e2e/fleet/harness.ts | 4 +- web/content/docs/cli-broker-lifecycle.mdx | 9 +- web/content/docs/cli-overview.mdx | 2 +- web/content/docs/reference-cli.mdx | 2 +- 28 files changed, 140 insertions(+), 1907 deletions(-) delete mode 100644 scripts/run-dashboard.js delete mode 100755 scripts/test-spawn-refactor.sh diff --git a/package.json b/package.json index 1f9ed0a05..4dcf23951 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,11 @@ "build:sdk": "npm --prefix packages/sdk run build", "build:telemetry": "npm --prefix packages/telemetry run build", "dev:watch": "cd packages/cli && npx tsc -w", - "watch:start": "npm run build && concurrently -k \"npm run dev:watch\" \"node --watch packages/cli/dist/cli/index.js start dashboard.js claude\"", + "watch:start": "npm run build && concurrently -k \"npm run dev:watch\" \"node --watch packages/cli/dist/cli/index.js up\"", "watch:start:cli-tools": "npm run build && bash ./scripts/watch-cli-tools.sh", "watch:start:claude": "npm run watch:start:cli-tools -- --tool=claude", "predev": "npm run clean && npm run build:packages && cd packages/cli && npx tsc && cd ../.. && chmod +x packages/cli/dist/cli/index.js", - "dev": "node packages/cli/dist/cli/index.js up --port 3888", + "dev": "node packages/cli/dist/cli/index.js up", "dev:local": "npm run build && (cd packages/cli && npm link) && echo '✓ agent-relay linked globally'", "dev:unlink": "(cd packages/cli && npm unlink -g agent-relay) && echo '✓ agent-relay unlinked'", "dev:rebuild": "npm run build && echo '✓ Rebuilt (linked version updated)'", diff --git a/packages/cli/src/cli/bootstrap.test.ts b/packages/cli/src/cli/bootstrap.test.ts index 50712c212..dcf379373 100644 --- a/packages/cli/src/cli/bootstrap.test.ts +++ b/packages/cli/src/cli/bootstrap.test.ts @@ -147,7 +147,7 @@ describe('bootstrap CLI', () => { 'mcp', ]) ); - // The dashboard-era surface is gone. + // The legacy command surface is gone. expect(topLevelCommands).not.toEqual( expect.arrayContaining([ 'driver', diff --git a/packages/cli/src/cli/bootstrap.ts b/packages/cli/src/cli/bootstrap.ts index 24fbeb163..892bb3c4a 100644 --- a/packages/cli/src/cli/bootstrap.ts +++ b/packages/cli/src/cli/bootstrap.ts @@ -109,7 +109,7 @@ function resolveProgramName(argv: string[] = process.argv): string { /** * Export the resolved CLI + SDK versions on the current process env so that - * any child process we spawn (the Rust broker, the dashboard server, etc.) + * any child process we spawn (the Rust broker, etc.) * inherits them and can attach them as common telemetry properties without * having to re-resolve `package.json`s on its own. * diff --git a/packages/cli/src/cli/commands/core.test.ts b/packages/cli/src/cli/commands/core.test.ts index 098e0749a..58a0c6005 100644 --- a/packages/cli/src/cli/commands/core.test.ts +++ b/packages/cli/src/cli/commands/core.test.ts @@ -103,7 +103,6 @@ function createHarness(options?: { relay?: CoreRelay; createRelay?: CoreDependencies['createRelay']; teamsConfig?: CoreTeamsConfig | null; - dashboardBinary?: string | null; env?: NodeJS.ProcessEnv; spawnedProcess?: SpawnedProcess; spawnImpl?: CoreDependencies['spawnProcess']; @@ -140,7 +139,6 @@ function createHarness(options?: { })), loadTeamsConfig: vi.fn(() => options?.teamsConfig ?? null), createRelay: options?.createRelay ?? vi.fn(() => relay), - findDashboardBinary: vi.fn(() => options?.dashboardBinary ?? '/usr/local/bin/relay-dashboard-server'), spawnProcess: options?.spawnImpl ?? (vi.fn(() => spawnedProcess) as unknown as CoreDependencies['spawnProcess']), execCommand: options?.execCommand ?? vi.fn(async () => ({ stdout: '', stderr: '' })), @@ -158,7 +156,6 @@ function createHarness(options?: { pid: 4242, now: options?.nowImpl ?? vi.fn(() => Date.now()), isPortInUse: vi.fn(async () => false), - findBrokerApiPort: vi.fn(async () => 3889), sleep: options?.sleepImpl ?? vi.fn(async () => undefined), onSignal: vi.fn(() => undefined), holdOpen: vi.fn(async () => undefined), @@ -213,32 +210,10 @@ describe('registerCoreCommands', () => { }); const { program, deps } = createHarness({ relay }); - const exitCode = await runCommand(program, ['up', '--port', '4999', '--broker-name', 'relayfile-dev']); + const exitCode = await runCommand(program, ['up', '--broker-name', 'relayfile-dev']); expect(exitCode).toBeUndefined(); - expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000, 'relayfile-dev'); - }); - - it('up starts broker and dashboard process', async () => { - const relay = createRelayMock({ - getStatus: vi.fn(async () => ({ agent_count: 1, pending_delivery_count: 0 })), - }); - const { program, deps, fs } = createHarness({ relay }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 5000, undefined); - expect(deps.spawnProcess).toHaveBeenCalledWith( - '/usr/local/bin/relay-dashboard-server', - expect.arrayContaining(['--port', '4999', '--relay-url', 'http://127.0.0.1:5000']), - expect.any(Object) - ); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).not.toContain('--no-spawn'); - expect(relay.getStatus).toHaveBeenCalledTimes(1); - expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 3889, 'relayfile-dev'); }); it('up exits early when connection metadata points to a running process', async () => { @@ -287,165 +262,6 @@ describe('registerCoreCommands', () => { ); }); - it('up infers static-dir for local dashboard JS entrypoint', async () => { - const staticDir = '/tmp/relay-dashboard/packages/dashboard-server/out'; - const fs = createFsMock({ [staticDir]: '' }); - const { program, deps } = createHarness({ - fs, - dashboardBinary: '/tmp/relay-dashboard/packages/dashboard-server/dist/start.js', - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - const dashboardOptions = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][2] as { env?: NodeJS.ProcessEnv }; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--relay-url', 'http://127.0.0.1:5000'])); - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', staticDir])); - expect(dashboardOptions.env?.RELAY_URL).toBe('http://127.0.0.1:5000'); - }); - - it('up infers static-dir for install-dir dashboard layout', async () => { - const home = '/Users/tester'; - const staticDir = `${home}/.agentworkforce/relay/dashboard/out`; - const fs = createFsMock({ - [staticDir]: '', - [`${staticDir}/index.html`]: '', - }); - const { program, deps } = createHarness({ - fs, - env: { HOME: home }, - dashboardBinary: `${home}/.local/bin/relay-dashboard-server`, - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', staticDir])); - }); - - it('up infers static-dir from a custom install-dir dashboard binary', async () => { - const installDir = '/opt/agent-relay'; - const staticDir = `${installDir}/dashboard/out`; - const fs = createFsMock({ - [staticDir]: '', - [`${staticDir}/index.html`]: '', - }); - const { program, deps } = createHarness({ - fs, - env: { HOME: '/Users/tester' }, - dashboardBinary: `${installDir}/bin/relay-dashboard-server`, - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', staticDir])); - }); - - it('up infers static-dir for prior ~/.relay dashboard layout (fallback)', async () => { - const home = '/Users/tester'; - const staticDir = `${home}/.relay/dashboard/out`; - const fs = createFsMock({ - [staticDir]: '', - [`${staticDir}/index.html`]: '', - }); - const { program, deps } = createHarness({ - fs, - env: { HOME: home }, - dashboardBinary: `${home}/.local/bin/relay-dashboard-server`, - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', staticDir])); - }); - - it('up skips dashboard asset refresh when custom install-dir assets match binary version', async () => { - const installDir = '/opt/agent-relay'; - const staticDir = `${installDir}/dashboard/out`; - const execCommand = vi.fn(async (command: string) => { - if (command === `${JSON.stringify(`${installDir}/bin/relay-dashboard-server`)} --version`) { - return { stdout: '1.2.3\n', stderr: '' }; - } - throw new Error(`unexpected command: ${command}`); - }); - const fs = createFsMock({ - [staticDir]: '', - [`${staticDir}/index.html`]: '', - [`${installDir}/dashboard/.version`]: '1.2.3\n', - }); - const { program, deps } = createHarness({ - fs, - execCommand, - env: { HOME: '/Users/tester' }, - dashboardBinary: `${installDir}/bin/relay-dashboard-server`, - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - expect(execCommand).toHaveBeenCalledWith( - `${JSON.stringify(`${installDir}/bin/relay-dashboard-server`)} --version` - ); - expect(execCommand).not.toHaveBeenCalledWith(expect.stringContaining('curl -fsSL')); - expect(fs.rmSync).not.toHaveBeenCalledWith(staticDir, { recursive: true, force: true }); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', staticDir])); - }); - - it('up prefers static-dir candidate that includes metrics page', async () => { - const dashboardServerOut = '/tmp/relay-dashboard/packages/dashboard-server/out'; - const dashboardOut = '/tmp/relay-dashboard/packages/dashboard/out'; - const fs = createFsMock({ - [dashboardServerOut]: '', - [dashboardOut]: '', - [`${dashboardOut}/metrics.html`]: '', - }); - const { program, deps } = createHarness({ - fs, - dashboardBinary: '/tmp/relay-dashboard/packages/dashboard-server/dist/start.js', - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', dashboardOut])); - }); - - it('up prefers static-dir candidate that includes nested metrics page', async () => { - const dashboardServerOut = '/tmp/relay-dashboard/packages/dashboard-server/out'; - const dashboardOut = '/tmp/relay-dashboard/packages/dashboard/out'; - const fs = createFsMock({ - [dashboardServerOut]: '', - [dashboardOut]: '', - [`${dashboardOut}/metrics/index.html`]: '', - }); - const { program, deps } = createHarness({ - fs, - dashboardBinary: '/tmp/relay-dashboard/packages/dashboard-server/dist/start.js', - }); - - const exitCode = await runCommand(program, ['up', '--port', '4999']); - - expect(exitCode).toBeUndefined(); - const dashboardArgs = (deps.spawnProcess as unknown as { mock: { calls: unknown[][] } }).mock - .calls[0][1] as string[]; - expect(dashboardArgs).toEqual(expect.arrayContaining(['--static-dir', dashboardOut])); - }); - it('up auto-spawns agents from teams config', async () => { const relay = createRelayMock(); const { program } = createHarness({ @@ -457,7 +273,7 @@ describe('registerCoreCommands', () => { }, }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(relay.spawn).toHaveBeenCalledWith({ @@ -469,60 +285,25 @@ describe('registerCoreCommands', () => { }); }); - it('up skips teams auto-spawn when dashboard mode manages broker', async () => { - const relay = createRelayMock(); - const { program, deps } = createHarness({ - relay, - teamsConfig: { - team: 'platform', - autoSpawn: true, - agents: [{ name: 'WorkerA', cli: 'codex', task: 'Ship tests' }], - }, - }); - - const exitCode = await runCommand(program, ['up']); - - expect(exitCode).toBeUndefined(); - expect(relay.spawn).toHaveBeenCalledTimes(0); - expect(deps.warn).toHaveBeenCalledWith( - 'Warning: auto-spawn from teams.json is skipped when dashboard mode manages the broker' - ); - }); - - it('up exits when dashboard port is already in use', async () => { - const spawnImpl = vi.fn(() => { - const error = new Error('listen EADDRINUSE') as Error & { code?: string }; - error.code = 'EADDRINUSE'; - throw error; - }) as unknown as CoreDependencies['spawnProcess']; - - const { program, deps } = createHarness({ spawnImpl }); - - const exitCode = await runCommand(program, ['up', '--port', '3888']); - - expect(exitCode).toBe(1); - expect(deps.error).toHaveBeenCalledWith('Dashboard port 3888 is already in use.'); - }); - it('up probes for a free API port before spawning the broker', async () => { const relay = createRelayMock(); const { program, deps } = createHarness({ relay }); - const exitCode = await runCommand(program, ['up', '--port', '3888']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); // Port probing happens before createRelay — only one broker is spawned expect(deps.createRelay).toHaveBeenCalledTimes(1); - // API port = dashboard port (3888) + 1 = 3889 + // API port = base port (3888) + 1 = 3889 expect(deps.createRelay).toHaveBeenCalledWith('/tmp/project', 3889, undefined); expect(relay.getStatus).toHaveBeenCalledTimes(1); }); - it('up without dashboard still enables the local broker API', async () => { + it('up enables the local broker API', async () => { const relay = createRelayMock(); const { program, deps } = createHarness({ relay }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground', '--port', '3888']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(deps.createRelay).toHaveBeenCalledTimes(1); @@ -530,7 +311,7 @@ describe('registerCoreCommands', () => { expect(relay.getStatus).toHaveBeenCalledTimes(1); }); - it('up --no-dashboard detaches by default for headless sessions', async () => { + it('up --background detaches for headless sessions', async () => { const spawnedProcess = createSpawnedProcessMock(); let now = 0; const fs = createFsMock(); @@ -550,12 +331,12 @@ describe('registerCoreCommands', () => { sleepImpl, }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(0); expect(deps.spawnProcess).toHaveBeenCalledWith( '/usr/bin/node', - ['/tmp/agent-relay.js', 'up', '--no-dashboard', '--foreground'], + ['/tmp/agent-relay.js', 'up'], { detached: true, stdio: 'ignore', @@ -571,7 +352,7 @@ describe('registerCoreCommands', () => { expect(relay.getStatus).not.toHaveBeenCalled(); }); - it('up --background --no-dashboard preserves state and workspace args in the foreground child', async () => { + it('up --background preserves state and workspace args in the detached child', async () => { const spawnedProcess = createSpawnedProcessMock(); let now = 0; const fs = createFsMock(); @@ -596,7 +377,6 @@ describe('registerCoreCommands', () => { '/tmp/agent-relay.js', 'up', '--background', - '--no-dashboard', '--state-dir', stateDir, '--workspace-key', @@ -608,7 +388,6 @@ describe('registerCoreCommands', () => { const exitCode = await runCommand(program, [ 'up', '--background', - '--no-dashboard', '--state-dir', stateDir, '--workspace-key', @@ -623,14 +402,12 @@ describe('registerCoreCommands', () => { [ '/tmp/agent-relay.js', 'up', - '--no-dashboard', '--state-dir', stateDir, '--workspace-key', 'rk_live_custom', '--broker-name', 'relayfile-dev', - '--foreground', ], { detached: true, @@ -643,17 +420,7 @@ describe('registerCoreCommands', () => { expect(deps.log).toHaveBeenCalledWith('Broker PID: 5151'); }); - it('up rejects mutually exclusive background and foreground flags', async () => { - const { program, deps } = createHarness(); - - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--background', '--foreground']); - - expect(exitCode).toBe(1); - expect(deps.error).toHaveBeenCalledWith('Cannot use --background and --foreground together.'); - expect(deps.spawnProcess).not.toHaveBeenCalled(); - }); - - it('up --no-dashboard re-execs a Bun standalone binary without adding its virtual entrypoint', async () => { + it('up --background re-execs a Bun standalone binary without adding its virtual entrypoint', async () => { const spawnedProcess = createSpawnedProcessMock(); let now = 0; const fs = createFsMock(); @@ -673,15 +440,15 @@ describe('registerCoreCommands', () => { sleepImpl, execPath: '/tmp/agent-relay-darwin-arm64', cliScript: '/$bunfs/root/agent-relay-darwin-arm64', - argv: ['bun', '/$bunfs/root/agent-relay-darwin-arm64', 'up', '--no-dashboard'], + argv: ['bun', '/$bunfs/root/agent-relay-darwin-arm64', 'up', '--background'], }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(0); expect(deps.spawnProcess).toHaveBeenCalledWith( '/tmp/agent-relay-darwin-arm64', - ['up', '--no-dashboard', '--foreground'], + ['up'], { detached: true, stdio: 'ignore', @@ -690,7 +457,7 @@ describe('registerCoreCommands', () => { ); }); - it('up --no-dashboard exits non-zero when the detached broker never becomes ready', async () => { + it('up --background exits non-zero when the detached broker never becomes ready', async () => { const spawnedProcess = createSpawnedProcessMock(); let now = 0; let childRunning = true; @@ -712,7 +479,7 @@ describe('registerCoreCommands', () => { sleepImpl, }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(1); expect(deps.error).toHaveBeenCalledWith( @@ -738,7 +505,7 @@ describe('registerCoreCommands', () => { 'khaliqgant 333 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /opt/bin/agent-relay-broker init --name project --channels general --persist', 'khaliqgant 444 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /opt/bin/agent-relay-broker init --state-dir /tmp/project/.agentworkforce/relay --persist', 'khaliqgant 555 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /opt/bin/agent-relay-broker init --state-dir /tmp/project-other/.agentworkforce/relay --persist', - 'khaliqgant 666 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /Users/test/.agentworkforce/relay/bin/agent-relay up --no-dashboard --foreground', + 'khaliqgant 666 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /Users/test/.agentworkforce/relay/bin/agent-relay up', 'khaliqgant 777 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /Users/test/.agentworkforce/relay/bin/agent-relay status --wait-for=30', ].join('\n'), stderr: '', @@ -788,7 +555,7 @@ describe('registerCoreCommands', () => { expect(deps.log).toHaveBeenCalledWith('Cleaned up (was not running)'); }); - it('up --no-dashboard reaps a foreground child orphan before starting cleanly', async () => { + it('up --background reaps a broker orphan before starting cleanly', async () => { const spawnedProcess = createSpawnedProcessMock({ pid: 9001 }); const runningPids = new Set([777, 9001, 4242]); const fs = createFsMock(); @@ -798,7 +565,7 @@ describe('registerCoreCommands', () => { return { stdout: [ 'USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND', - 'khaliqgant 777 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /Users/test/.agentworkforce/relay/bin/agent-relay up --no-dashboard --foreground', + 'khaliqgant 777 0.0 0.0 1 1 ?? S 1:00PM 0:00.01 /Users/test/.agentworkforce/relay/bin/agent-relay up', ].join('\n'), stderr: '', }; @@ -828,7 +595,7 @@ describe('registerCoreCommands', () => { sleepImpl, }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(0); expect(killImpl).toHaveBeenCalledWith(777, 'SIGTERM'); @@ -838,7 +605,7 @@ describe('registerCoreCommands', () => { expect(deps.log).toHaveBeenCalledWith('Broker PID: 4242'); }); - it('up --no-dashboard replaces a live broker PID whose API never becomes ready', async () => { + it('up --background replaces a live broker PID whose API never becomes ready', async () => { const spawnedProcess = createSpawnedProcessMock({ pid: 9001 }); const runningPids = new Set([3030, 9001, 4242]); const fs = createFsMock({ ['/tmp/project/.agentworkforce/relay/connection.json']: connectionFile(3030) }); @@ -865,7 +632,7 @@ describe('registerCoreCommands', () => { sleepImpl, }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(0); expect(killImpl).toHaveBeenCalledWith(3030, 'SIGTERM'); @@ -877,7 +644,7 @@ describe('registerCoreCommands', () => { expect(deps.log).toHaveBeenCalledWith('Broker PID: 4242'); }); - it('up --no-dashboard reports the broker PID when the detached broker is live but API-unready', async () => { + it('up --background reports the broker PID when the detached broker is live but API-unready', async () => { const spawnedProcess = createSpawnedProcessMock({ pid: 9001 }); let now = 0; const runningPids = new Set([9001, 4242]); @@ -906,7 +673,7 @@ describe('registerCoreCommands', () => { sleepImpl, }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(1); expect(deps.error).toHaveBeenCalledWith( @@ -917,43 +684,26 @@ describe('registerCoreCommands', () => { expect(killImpl).toHaveBeenCalledWith(4242, 'SIGTERM'); }); - it('up --no-dashboard reports spawn failures without claiming background success', async () => { + it('up --background reports spawn failures without claiming background success', async () => { const { program, deps } = createHarness({ spawnImpl: vi.fn(() => { throw new Error('spawn EACCES'); }) as unknown as CoreDependencies['spawnProcess'], }); - const exitCode = await runCommand(program, ['up', '--no-dashboard']); + const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(1); expect(deps.error).toHaveBeenCalledWith('Failed to start broker in background: spawn EACCES'); expect(deps.log).not.toHaveBeenCalledWith('Broker started.'); }); - it('up force exits on repeated SIGINT during hung shutdown and suppresses expected dashboard signal noise', async () => { + it('up force exits on repeated SIGINT during a hung shutdown', async () => { const relay = createRelayMock({ shutdown: vi.fn(() => new Promise(() => undefined)), }); - let dashboardExitHandler: ((...args: unknown[]) => void) | undefined; - - const spawnedProcess = { - pid: 9001, - killed: false, - kill: vi.fn((signal?: NodeJS.Signals | number) => { - spawnedProcess.killed = true; - dashboardExitHandler?.(null, typeof signal === 'string' ? signal : null); - }), - unref: vi.fn(() => undefined), - on: vi.fn((event: string, cb: (...args: unknown[]) => void) => { - if (event === 'exit') { - dashboardExitHandler = cb; - } - }), - stderr: { on: vi.fn(() => undefined) }, - } as unknown as SpawnedProcess; - const { program, deps } = createHarness({ relay, spawnedProcess }); + const { program, deps } = createHarness({ relay }); const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); @@ -974,11 +724,6 @@ describe('registerCoreCommands', () => { const logCalls = (deps.log as unknown as { mock: { calls: unknown[][] } }).mock.calls; expect(logCalls.filter((call) => call[0] === '\nStopping...')).toHaveLength(1); - - const errorCalls = (deps.error as unknown as { mock: { calls: unknown[][] } }).mock.calls; - expect( - errorCalls.filter((call) => String(call[0]).includes('Dashboard process killed by signal')) - ).toHaveLength(0); }); it('down stops broker and cleans stale files', async () => { @@ -1291,19 +1036,12 @@ describe('registerCoreCommands', () => { }); }); - it('uninstall dry-run covers renamed and legacy installer asset directories', async () => { + it('uninstall dry-run covers renamed and legacy installer bin directories', async () => { const { deps } = createHarness(); const program = new Command(); registerCoreMaintenance(program, deps); const home = os.homedir(); - const paths = [ - `${home}/.agentworkforce/relay/dashboard/out`, - `${home}/.agentworkforce/relay/dashboard/.version`, - `${home}/.agent-relay/dashboard/out`, - `${home}/.agent-relay/dashboard/.version`, - `${home}/.agentworkforce/relay/bin`, - `${home}/.agent-relay/bin`, - ]; + const paths = [`${home}/.agentworkforce/relay/bin`, `${home}/.agent-relay/bin`]; for (const filePath of paths) { deps.fs.writeFileSync(filePath, ''); } @@ -1311,10 +1049,7 @@ describe('registerCoreCommands', () => { const exitCode = await runCommand(program, ['uninstall', '--dry-run']); expect(exitCode).toBeUndefined(); - for (const filePath of paths.slice(0, 4)) { - expect(deps.log).toHaveBeenCalledWith(`[dry-run] Would remove dashboard asset path: ${filePath}`); - } - for (const filePath of paths.slice(4)) { + for (const filePath of paths) { expect(deps.log).toHaveBeenCalledWith(`[dry-run] Would remove directory: ${filePath}`); } expect(deps.execCommand).not.toHaveBeenCalled(); @@ -1324,17 +1059,17 @@ describe('registerCoreCommands', () => { const relay = createRelayMock(); const { program, deps } = createHarness({ relay }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(deps.log).toHaveBeenCalledWith('Workspace Key: rk_live_default'); }); - it('up logs the auto-created workspace key with dashboard enabled', async () => { + it('up logs the auto-created workspace key', async () => { const relay = createRelayMock({ workspaceKey: 'rk_live_auto456' }); const { program, deps } = createHarness({ relay }); - const exitCode = await runCommand(program, ['up', '--port', '4999']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(deps.log).toHaveBeenCalledWith('Workspace Key: rk_live_auto456'); @@ -1345,13 +1080,7 @@ describe('registerCoreCommands', () => { const relay = createRelayMock({ workspaceKey: 'rk_live_custom' }); const { program, deps } = createHarness({ relay, env }); - const exitCode = await runCommand(program, [ - 'up', - '--no-dashboard', - '--foreground', - '--workspace-key', - 'rk_live_custom', - ]); + const exitCode = await runCommand(program, ['up', '--workspace-key', 'rk_live_custom']); expect(exitCode).toBeUndefined(); expect(env.RELAY_WORKSPACE_KEY).toBe('rk_live_custom'); @@ -1365,7 +1094,7 @@ describe('registerCoreCommands', () => { const relay = createRelayMock(); const { program } = createHarness({ relay, env }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(env.RELAY_WORKSPACE_KEY).toBeUndefined(); @@ -1378,7 +1107,7 @@ describe('registerCoreCommands', () => { const relay = createRelayMock(); const { program } = createHarness({ relay, env, fs }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(env.AGENT_RELAY_MCP_COMMAND).toBe('/usr/bin/node /tmp/agent-relay-mcp.js'); @@ -1390,7 +1119,7 @@ describe('registerCoreCommands', () => { const relay = createRelayMock(); const { program } = createHarness({ relay, env, fs }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(env.AGENT_RELAY_MCP_COMMAND).toBe('node /custom/agent-relay-mcp.js'); @@ -1400,7 +1129,7 @@ describe('registerCoreCommands', () => { const relay = createRelayMock({ workspaceKey: undefined }); const { program, deps } = createHarness({ relay }); - const exitCode = await runCommand(program, ['up', '--no-dashboard', '--foreground']); + const exitCode = await runCommand(program, ['up']); expect(exitCode).toBeUndefined(); expect(deps.log).toHaveBeenCalledWith('Workspace Key: unknown'); @@ -1411,13 +1140,7 @@ describe('registerCoreCommands', () => { const relay = createRelayMock({ workspaceKey: 'rk_live_new' }); const { program, deps } = createHarness({ relay, env }); - const exitCode = await runCommand(program, [ - 'up', - '--no-dashboard', - '--foreground', - '--workspace-key', - 'rk_live_new', - ]); + const exitCode = await runCommand(program, ['up', '--workspace-key', 'rk_live_new']); expect(exitCode).toBeUndefined(); expect(env.RELAY_WORKSPACE_KEY).toBe('rk_live_new'); diff --git a/packages/cli/src/cli/commands/core.ts b/packages/cli/src/cli/commands/core.ts index bf083ebde..d1e00572a 100644 --- a/packages/cli/src/cli/commands/core.ts +++ b/packages/cli/src/cli/commands/core.ts @@ -16,7 +16,6 @@ import { createRuntimeClient, spawnAgentWithClient } from '../lib/client-factory import { defaultExit, runSignalHandler } from '../lib/exit.js'; const execAsync = promisify(exec); -const DEFAULT_DASHBOARD_PORT = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888'; type ExitFn = (code: number) => never; @@ -85,7 +84,6 @@ export interface CoreDependencies { getProjectPaths: () => CoreProjectPaths; loadTeamsConfig: (projectRoot: string) => CoreTeamsConfig | null; createRelay: (cwd: string, apiPort?: number, brokerName?: string) => CoreRelay | Promise; - findDashboardBinary: () => string | null; spawnProcess: (command: string, args: string[], options?: Record) => SpawnedProcess; execCommand: (command: string) => Promise<{ stdout: string; stderr: string }>; killProcess: (pid: number, signal?: NodeJS.Signals | number) => void; @@ -103,7 +101,6 @@ export interface CoreDependencies { onSignal: (signal: NodeJS.Signals, handler: () => void | Promise) => void; holdOpen: () => Promise; isPortInUse: (port: number) => Promise; - findBrokerApiPort: () => Promise; log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; @@ -140,69 +137,6 @@ function resolveCliVersion(fileSystem: CoreFileSystem): string { } } -function findDashboardBinaryDefault(fileSystem: CoreFileSystem): string | null { - // Allow explicit override via env var (for local development) - const envOverride = process.env.RELAY_DASHBOARD_BINARY; - if (envOverride && fileSystem.existsSync(envOverride)) { - return envOverride; - } - - // In local multi-repo workspaces, prefer a sibling relay-dashboard build when available. - // Only when RELAY_LOCAL_DEV is set — otherwise the installed binary should win so - // users don't accidentally run a stale dev build. - if (process.env.RELAY_LOCAL_DEV === '1') { - const siblingWorkspaceBuild = path.resolve( - process.cwd(), - '..', - 'relay-dashboard', - 'packages', - 'dashboard-server', - 'dist', - 'start.js' - ); - if (fileSystem.existsSync(siblingWorkspaceBuild)) { - return siblingWorkspaceBuild; - } - } - - const binaryName = 'relay-dashboard-server'; - const homeDir = process.env.HOME || process.env.USERPROFILE || ''; - - const searchPaths = [ - path.join(homeDir, '.local', 'bin', binaryName), - path.join(homeDir, '.agentworkforce/relay', 'bin', binaryName), - path.join('/usr/local/bin', binaryName), - ]; - - for (const candidate of searchPaths) { - try { - if (!fileSystem.existsSync(candidate)) { - continue; - } - fileSystem.accessSync(candidate, fs.constants.X_OK); - return candidate; - } catch { - // Continue searching. - } - } - - const envPath = process.env.PATH || ''; - for (const dir of envPath.split(path.delimiter)) { - const candidate = path.join(dir, binaryName); - try { - if (!fileSystem.existsSync(candidate)) { - continue; - } - fileSystem.accessSync(candidate, fs.constants.X_OK); - return candidate; - } catch { - // Continue searching. - } - } - - return null; -} - async function createDefaultRelay(cwd: string, apiPort = 0, brokerName?: string): Promise { const binaryArgs: BrokerInitArgs = {}; if (apiPort > 0) { @@ -259,7 +193,6 @@ export function withDefaults(overrides: Partial = {}): CoreDep loadTeamsConfig: (projectRoot: string) => (loadTeamsConfig(projectRoot) as unknown as CoreTeamsConfig | null) ?? null, createRelay: createDefaultRelay, - findDashboardBinary: () => findDashboardBinaryDefault(fileSystem), spawnProcess: (command, args, options) => spawnProcess(command, args, options as Parameters[2]) as unknown as SpawnedProcess, execCommand: async (command: string) => { @@ -307,21 +240,6 @@ export function withDefaults(overrides: Partial = {}): CoreDep log: (...args: unknown[]) => console.log(...args), error: (...args: unknown[]) => console.error(...args), warn: (...args: unknown[]) => console.warn(...args), - findBrokerApiPort: async () => { - const dp = Number.parseInt(process.env.AGENT_RELAY_DASHBOARD_PORT ?? '3888', 10); - const startPort = (Number.isFinite(dp) ? dp : 3888) + 1; - for (let i = 0; i < 25; i++) { - const port = startPort + i; - if (port > 65535) break; - try { - const res = await fetch(`http://localhost:${port}/health`); - if (res.ok) return port; - } catch { - // Not responding, keep scanning. - } - } - return 0; - }, exit: defaultExit, ...overrides, }; @@ -332,13 +250,10 @@ export function registerCoreCommands(program: Command, overrides: Partial', 'Dashboard port', DEFAULT_DASHBOARD_PORT) + .description('Start the local broker') .option('--spawn', 'Force spawn all agents from teams.json') .option('--no-spawn', 'Do not auto-spawn agents (just start broker)') .option('--background', 'Run broker in the background (detached)') - .option('--foreground', 'Run --no-dashboard attached to this terminal') .option('--verbose', 'Enable verbose logging') .option('--workspace-key ', 'Use a pre-established Relaycast workspace key') .option( @@ -348,11 +263,8 @@ export function registerCoreCommands(program: Command, overrides: Partial', 'Override the broker name (defaults to project directory basename)') .action( async (options: { - dashboard?: boolean; - port?: string; spawn?: boolean; background?: boolean; - foreground?: boolean; verbose?: boolean; workspaceKey?: string; stateDir?: string; diff --git a/packages/cli/src/cli/commands/fleet.ts b/packages/cli/src/cli/commands/fleet.ts index a0d77a9fb..bc4873465 100644 --- a/packages/cli/src/cli/commands/fleet.ts +++ b/packages/cli/src/cli/commands/fleet.ts @@ -12,7 +12,11 @@ import * as fleetSdk from '@agent-relay/fleet'; const { isFleetNodeDefinition } = fleetSdk; import { withDefaults, type CoreDependencies, type CoreProjectPaths } from './core.js'; -import { readBrokerConnection, startBrokerWithPortFallback } from '../lib/broker-lifecycle.js'; +import { + readBrokerConnection, + resolveBrokerBasePort, + startBrokerWithPortFallback, +} from '../lib/broker-lifecycle.js'; import { buildNodeSupervision, createImplicitLocalFleetNode, @@ -264,8 +268,8 @@ async function runFleetServe( const nameOverride = nameOption ?? enrollment?.nodeName ?? undefined; const baseUrl = baseUrlOverride || enrollment?.relaycastUrl || undefined; - const dashboardPort = Number.parseInt(deps.core.env.AGENT_RELAY_DASHBOARD_PORT ?? '3888', 10) || 3888; - const started = await startBrokerWithPortFallback(paths, dashboardPort, deps.core); + const basePort = resolveBrokerBasePort(deps.core); + const started = await startBrokerWithPortFallback(paths, basePort, deps.core); const connection = connectionFromFile(paths.dataDir); const controller = new AbortController(); const stop = () => controller.abort(); diff --git a/packages/cli/src/cli/commands/local-agent.ts b/packages/cli/src/cli/commands/local-agent.ts index 6f93d825e..5c2bce589 100644 --- a/packages/cli/src/cli/commands/local-agent.ts +++ b/packages/cli/src/cli/commands/local-agent.ts @@ -168,8 +168,7 @@ function brokerOptionsFromOpts(opts: Record): LocalAgentMessage /** * Register the `local agent …` subtree (and `runtime tail`) onto the driver - * group. List/spawn/release/kill talk to a running local broker; attach/new/tail - * are interactive PTY operations and point the user at the dashboard. + * group. List/spawn/release/kill talk to a running local broker. */ export function registerLocalAgentCommands( group: Command, diff --git a/packages/cli/src/cli/lib/broker-lifecycle.test.ts b/packages/cli/src/cli/lib/broker-lifecycle.test.ts index 41482e073..c07f026d7 100644 --- a/packages/cli/src/cli/lib/broker-lifecycle.test.ts +++ b/packages/cli/src/cli/lib/broker-lifecycle.test.ts @@ -67,28 +67,23 @@ describe('classifyBrokerStartError', () => { }); describe('classifyBrokerStartStage', () => { - it('marks dashboard port conflicts when the dashboard was requested', () => { - const err = Object.assign(new Error('listen EADDRINUSE'), { code: 'EADDRINUSE' }); - expect(classifyBrokerStartStage(err, 'listen EADDRINUSE', true)).toBe('dashboard_port'); - }); - it('marks already-running brokers from the message text', () => { const message = 'another broker instance is already running in this directory (/tmp/x)'; - expect(classifyBrokerStartStage(new Error(message), message, false)).toBe('already_running'); + expect(classifyBrokerStartStage(new Error(message), message)).toBe('already_running'); }); it('classifies fetch failures as connect-stage errors', () => { const cause = Object.assign(new Error('ECONNREFUSED'), { code: 'ECONNREFUSED' }); const err = new TypeError('fetch failed', { cause }); - expect(classifyBrokerStartStage(err, 'fetch failed', false)).toBe('connect'); + expect(classifyBrokerStartStage(err, 'fetch failed')).toBe('connect'); }); it('classifies broker-exited-before-ready as a spawn failure', () => { const message = 'Broker process exited with code 1 before becoming ready (pid=123; …)'; - expect(classifyBrokerStartStage(new Error(message), message, false)).toBe('spawn'); + expect(classifyBrokerStartStage(new Error(message), message)).toBe('spawn'); }); it('falls back to startup for everything else', () => { - expect(classifyBrokerStartStage(new Error('???'), '???', false)).toBe('startup'); + expect(classifyBrokerStartStage(new Error('???'), '???')).toBe('startup'); }); }); diff --git a/packages/cli/src/cli/lib/broker-lifecycle.ts b/packages/cli/src/cli/lib/broker-lifecycle.ts index 622c671a7..a3b012820 100644 --- a/packages/cli/src/cli/lib/broker-lifecycle.ts +++ b/packages/cli/src/cli/lib/broker-lifecycle.ts @@ -1,5 +1,4 @@ import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; import { HarnessDriverClient } from '@agent-relay/harness-driver'; @@ -21,14 +20,9 @@ import { } from './fleet-sidecar.js'; type UpOptions = { - dashboard?: boolean; - port?: string; spawn?: boolean; background?: boolean; - foreground?: boolean; verbose?: boolean; - dashboardPath?: string; - reuseExistingBroker?: boolean; workspaceKey?: string; stateDir?: string; brokerName?: string; @@ -42,8 +36,8 @@ type DownOptions = { }; const MAX_API_PORT_ATTEMPTS = 25; -const MAX_DASHBOARD_PORT_ATTEMPTS = 25; const MAX_PORT = 65535; +const DEFAULT_BROKER_BASE_PORT = 3888; /** The broker writes this file with URL, port, API key, and PID. */ const CONNECTION_FILENAME = 'connection.json'; @@ -187,8 +181,7 @@ export function classifyBrokerStartError(err: unknown): string { } /** Exported for testing. */ -export function classifyBrokerStartStage(err: unknown, message: string, wantsDashboard: boolean): string { - if (errorCode(err) === 'EADDRINUSE' && wantsDashboard) return 'dashboard_port'; +export function classifyBrokerStartStage(_err: unknown, message: string): string { if (isBrokerAlreadyRunningError(message)) return 'already_running'; if (/fetch failed/i.test(message)) return 'connect'; if (/Broker did not report API port/i.test(message)) return 'spawn'; @@ -219,16 +212,26 @@ async function resolveApiPortWithFallback( throw new Error(`Failed to find an available API port near ${startApiPort}.`); } +/** + * The broker base port. `AGENT_RELAY_BROKER_PORT` overrides the default so + * multiple brokers can run side by side (e.g. in tests); the broker HTTP API + * binds near `basePort + 1` with fallback scanning. + */ +export function resolveBrokerBasePort(deps: Pick): number { + const raw = Number.parseInt(deps.env.AGENT_RELAY_BROKER_PORT ?? '', 10); + return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BROKER_BASE_PORT; +} + export async function startBrokerWithPortFallback( paths: CoreProjectPaths, - dashboardPort: number, + basePort: number, deps: CoreDependencies, brokerName?: string ): Promise<{ relay: CoreRelay; apiPort: number }> { // Resolve a free API port BEFORE spawning the broker. This avoids // spawning (and flocking) multiple --persist brokers during retry, // which caused stale-flock "already running" errors. - const startApiPort = dashboardPort + 1; + const startApiPort = basePort + 1; const apiPort = await resolveApiPortWithFallback(startApiPort, MAX_API_PORT_ATTEMPTS, deps); const candidate = await deps.createRelay(paths.projectRoot, apiPort, brokerName); @@ -267,25 +270,6 @@ function startImplicitLocalFleetSidecar( }); } -async function resolveDashboardPortWithFallback( - dashboardPort: number, - dashboardPortCandidates: number, - deps: CoreDependencies -): Promise { - for (let attempt = 0; attempt < dashboardPortCandidates; attempt += 1) { - const candidatePort = dashboardPort + attempt; - const inUse = await deps.isPortInUse(candidatePort); - if (!inUse) { - if (attempt > 0) { - deps.warn(`Dashboard port ${dashboardPort} is already in use; trying ${candidatePort}`); - } - return candidatePort; - } - } - - throw new Error(`Failed to find an available dashboard port near ${dashboardPort}.`); -} - function isBrokerAlreadyRunningError(message: string): boolean { return /another broker instance is already running in this directory/i.test(message); } @@ -365,18 +349,20 @@ function isBrokerExecutableCommand(command: string): boolean { return basename === 'agent-relay-broker' || basename.startsWith('agent-relay-broker-'); } -function isForegroundBrokerCliCommand(command: string): boolean { +function isAttachedBrokerCliCommand(command: string): boolean { if (command.includes('agent-relay-mcp')) { return false; } - if (!/(?:^|\s)up(?:\s|$)/.test(command) || !/(?:^|\s)--foreground(?:\s|=|$)/.test(command)) { + // The attached `up` process holds the broker. Skip the transient + // `up --background` launcher, which exits as soon as the child is ready. + if (!/(?:^|\s)up(?:\s|$)/.test(command) || /(?:^|\s)--background(?:\s|=|$)/.test(command)) { return false; } return /(?:^|\s)(?:\S*agent-relay(?:\.js)?|\S*agent-relay-[^\s]+)(?:\s|$)/.test(command); } function isBrokerProcessCommand(command: string): boolean { - return isBrokerExecutableCommand(command) || isForegroundBrokerCliCommand(command); + return isBrokerExecutableCommand(command) || isAttachedBrokerCliCommand(command); } function escapeRegExp(value: string): string { @@ -591,12 +577,7 @@ function cleanupBrokerFiles(paths: CoreProjectPaths, deps: CoreDependencies): vo } function childUpArgsForDetachedStart(options: UpOptions, deps: CoreDependencies): string[] { - const args = cliUserArgs(deps).filter( - (arg) => !['--background', '--foreground'].some((name) => matchesCliOption(arg, name)) - ); - if (options.dashboard === false && !args.includes('--no-dashboard')) { - args.push('--no-dashboard'); - } + const args = cliUserArgs(deps).filter((arg) => !matchesCliOption(arg, '--background')); if (options.stateDir && !hasCliOption(args, '--state-dir')) { args.push('--state-dir', path.resolve(options.stateDir)); } @@ -609,9 +590,6 @@ function childUpArgsForDetachedStart(options: UpOptions, deps: CoreDependencies) if (options.verbose === true && !args.includes('--verbose')) { args.push('--verbose'); } - if (options.dashboard === false && !args.includes('--foreground')) { - args.push('--foreground'); - } return args; } @@ -711,609 +689,19 @@ async function waitForBrokerReadiness( return latest; } -function pickDashboardStaticDir(candidates: string[], deps: CoreDependencies): string | null { - const existingCandidates = Array.from(new Set(candidates)).filter((candidate) => - deps.fs.existsSync(candidate) - ); - if (existingCandidates.length === 0) { - return null; - } - - const pageMarkerPriority = [ - ['metrics.html', path.join('metrics', 'index.html')], - ['app.html'], - ['index.html'], - ]; - - for (const markerGroup of pageMarkerPriority) { - const withMarker = existingCandidates.find((candidate) => - markerGroup.some((marker) => deps.fs.existsSync(path.join(candidate, marker))) - ); - if (withMarker) { - return withMarker; - } - } - - return existingCandidates[0]; -} - -function getHomeDashboardRoot(deps: CoreDependencies): string { - const homeDir = deps.env.HOME || deps.env.USERPROFILE || os.homedir(); - return path.join(homeDir, '.agentworkforce/relay', 'dashboard'); -} - -function getPriorDashboardRoot(deps: CoreDependencies): string | null { - const homeDir = deps.env.HOME || deps.env.USERPROFILE || ''; - if (!homeDir) { - return null; - } - return path.join(homeDir, '.relay', 'dashboard'); -} - -function getDashboardRootFromBinary(dashboardBinary: string | null, deps: CoreDependencies): string | null { - if (!dashboardBinary || dashboardBinary.endsWith('.js') || dashboardBinary.endsWith('.ts')) { - return null; - } - - const binaryDir = path.dirname(dashboardBinary); - if (path.basename(binaryDir) !== 'bin') { - return null; - } - - const homeDir = deps.env.HOME || deps.env.USERPROFILE || ''; - const resolvedBinaryDir = path.resolve(binaryDir); - const ignoredBinDirs = [ - homeDir ? path.join(homeDir, '.local', 'bin') : null, - path.join('/usr/local', 'bin'), - ] - .filter((candidate): candidate is string => Boolean(candidate)) - .map((candidate) => path.resolve(candidate)); - if (ignoredBinDirs.includes(resolvedBinaryDir)) { - return null; - } - - return path.join(path.dirname(binaryDir), 'dashboard'); -} - -function resolveDashboardStaticDir(dashboardBinary: string | null, deps: CoreDependencies): string | null { - const explicitStaticDir = deps.env.RELAY_DASHBOARD_STATIC_DIR ?? deps.env.STATIC_DIR; - if (explicitStaticDir && explicitStaticDir.trim()) { - return explicitStaticDir; - } - - if (!dashboardBinary) { - return null; - } - - if (dashboardBinary.endsWith('.js') || dashboardBinary.endsWith('.ts')) { - const dashboardServerOutDir = path.resolve(path.dirname(dashboardBinary), '..', 'out'); - const siblingDashboardOutDir = path.resolve( - path.dirname(dashboardBinary), - '..', - '..', - 'dashboard', - 'out' - ); - return pickDashboardStaticDir([dashboardServerOutDir, siblingDashboardOutDir], deps); - } - - // Installs place UI assets under the install dir (~/.agentworkforce/relay/dashboard/out - // by default, or next to a custom install's bin/ directory). ~/.relay/dashboard/out is - // read as a fallback for installs predating that move. - const installDashboardRoot = getDashboardRootFromBinary(dashboardBinary, deps); - const priorDashboardRoot = getPriorDashboardRoot(deps); - const candidates = [ - installDashboardRoot ? path.join(installDashboardRoot, 'out') : null, - path.join(getHomeDashboardRoot(deps), 'out'), - priorDashboardRoot ? path.join(priorDashboardRoot, 'out') : null, - ].filter((candidate): candidate is string => Boolean(candidate)); - return pickDashboardStaticDir(candidates, deps); -} - -function normalizeLocalhostRelayUrl(relayUrl: string): string { - try { - const parsed = new URL(relayUrl); - if (parsed.hostname === 'localhost') { - parsed.hostname = '127.0.0.1'; - } - return parsed.toString().replace(/\/+$/, ''); - } catch { - return relayUrl; - } -} - -function getDefaultDashboardRelayUrl(apiPort: number): string { - return normalizeLocalhostRelayUrl(`http://localhost:${apiPort}`); -} - -function resolveDashboardRelayUrl(apiPort: number, deps: CoreDependencies): string { - const explicitRelayUrl = deps.env.RELAY_DASHBOARD_RELAY_URL; - if (explicitRelayUrl && explicitRelayUrl.trim()) { - return normalizeLocalhostRelayUrl(explicitRelayUrl.trim()); - } - - return getDefaultDashboardRelayUrl(apiPort); -} - -function isDebugLikeLoggingEnabled(deps: CoreDependencies): boolean { - const rawLevel = String(deps.env.RUST_LOG ?? '').toLowerCase(); - return rawLevel.includes('debug') || rawLevel.includes('trace'); -} - -function getDashboardSpawnEnv( - deps: CoreDependencies, - relayUrl: string, - enableVerboseLogging: boolean, - relayApiKey?: string, - brokerApiKey?: string -): NodeJS.ProcessEnv { - const env: NodeJS.ProcessEnv = { - ...deps.env, - RELAY_URL: relayUrl, - VERBOSE: enableVerboseLogging || deps.env.VERBOSE === 'true' ? 'true' : deps.env.VERBOSE, - }; - // Pass the workspace key so the dashboard can make Agent Relay calls - // (e.g. posting thread replies) without requiring a relaycast.json file. - if (relayApiKey) { - if (!env.RELAY_WORKSPACE_KEY) { - env.RELAY_WORKSPACE_KEY = relayApiKey; - } - if (!env.RELAY_API_KEY) { - env.RELAY_API_KEY = relayApiKey; - } - } - // Pass the broker API key so the dashboard can authenticate with the - // broker's HTTP API (e.g. /api/spawn, /api/spawned). - if (brokerApiKey) { - env.RELAY_BROKER_API_KEY = brokerApiKey; - } - return env; -} - -function getDashboardSpawnArgs( - paths: CoreProjectPaths, - port: number, - apiPort: number, - dashboardBinary: string | null, - relayUrl: string, - enableVerboseLogging: boolean, - deps: CoreDependencies -): string[] { - const args = ['--port', String(port), '--data-dir', paths.dataDir]; - args.push('--relay-url', relayUrl); - const staticDir = resolveDashboardStaticDir(dashboardBinary, deps); - if (staticDir) { - args.push('--static-dir', staticDir); - } - if (enableVerboseLogging) { - args.push('--verbose'); - } - return args; -} - -function normalizeDashboardPath(rawDashboardPath: string | undefined): string | undefined { - const trimmed = rawDashboardPath?.trim(); - if (!trimmed) return undefined; - if (trimmed.startsWith('/')) { - return trimmed; - } - return `/${trimmed}`; -} - -interface DashboardStartupProcess extends SpawnedProcess { - stdout?: { - on?: (event: string, cb: (chunk: Buffer) => void) => void; - removeListener?: (event: string, cb: (...args: unknown[]) => void) => void; - off?: (event: string, cb: (...args: unknown[]) => void) => void; - }; - stderr?: { - on?: (event: string, cb: (chunk: Buffer) => void) => void; - removeListener?: (event: string, cb: (...args: unknown[]) => void) => void; - off?: (event: string, cb: (...args: unknown[]) => void) => void; - }; -} - -function startDashboard( - paths: CoreProjectPaths, - port: number, - apiPort: number, - deps: CoreDependencies, - enableVerboseLogging: boolean, - dashboardBinaryOverride?: string | null, - relayApiKey?: string, - brokerApiKey?: string -): DashboardStartupProcess { - const dashboardBinary = - dashboardBinaryOverride === undefined ? deps.findDashboardBinary() : dashboardBinaryOverride; - const relayUrl = resolveDashboardRelayUrl(apiPort, deps); - const shouldEnableVerbose = enableVerboseLogging || isDebugLikeLoggingEnabled(deps); - const args = getDashboardSpawnArgs( - paths, - port, - apiPort, - dashboardBinary, - relayUrl, - shouldEnableVerbose, - deps - ); - const launchTarget = dashboardBinary - ? dashboardBinary.endsWith('.js') - ? `node ${dashboardBinary}` - : dashboardBinary - : 'npx --yes @agent-relay/dashboard-server@latest'; - - const spawnOpts = { - stdio: ['ignore', 'pipe', 'pipe'] as unknown, - env: getDashboardSpawnEnv(deps, relayUrl, shouldEnableVerbose, relayApiKey, brokerApiKey), - }; - if (shouldEnableVerbose) { - deps.log(`[dashboard] Starting: ${launchTarget} ${args.join(' ')}`); - } - - let child: SpawnedProcess; - if (dashboardBinary) { - // If the binary is a .js file (local dev), run it with node - if (dashboardBinary.endsWith('.js')) { - child = deps.spawnProcess('node', [dashboardBinary, ...args], spawnOpts); - } else { - child = deps.spawnProcess(dashboardBinary, args, spawnOpts); - } - } else { - child = deps.spawnProcess('npx', ['--yes', '@agent-relay/dashboard-server@latest', ...args], spawnOpts); - } - - // Capture stderr for error reporting - const childAny = child as unknown as { - stdout?: { on?: (event: string, cb: (chunk: Buffer) => void) => void }; - stderr?: { on?: (event: string, cb: (chunk: Buffer) => void) => void }; - on?: (event: string, cb: (...args: unknown[]) => void) => void; - }; - let stderrBuf = ''; - - const logChunk = (chunk: Buffer, logger: (line: string) => void, prefix: string) => { - if (!shouldEnableVerbose) { - return; - } - const text = chunk.toString(); - for (const line of text.split(/\r?\n/)) { - const trimmed = line.trim(); - if (trimmed) { - logger(`[dashboard] ${prefix}: ${trimmed}`); - } - } - }; - - childAny.stdout?.on?.('data', (chunk: Buffer) => { - logChunk(chunk, deps.log, 'stdout'); - }); - childAny.stderr?.on?.('data', (chunk: Buffer) => { - stderrBuf += chunk.toString(); - logChunk(chunk, deps.warn, 'stderr'); - }); - - // Report early crashes - childAny.on?.('exit', (...exitArgs: unknown[]) => { - const code = exitArgs[0] as number | null; - const signal = exitArgs[1] as string | null; - if (code !== null && code !== 0) { - deps.error(`Dashboard process exited with code ${code}`); - if (stderrBuf.trim()) { - deps.error(stderrBuf.trim().split('\n').slice(-5).join('\n')); - } - } else if (signal && signal !== 'SIGINT' && signal !== 'SIGTERM') { - deps.error(`Dashboard process killed by signal ${signal}`); - } - }); - - return child; -} - -async function resolveStartedDashboardPort( - process: DashboardStartupProcess, - preferredPort: number, - deps: CoreDependencies -): Promise { - return new Promise((resolve) => { - let resolved = false; - const processAny = process as DashboardStartupProcess & { - on?: (event: string, cb: (...args: unknown[]) => void) => void; - off?: (event: string, cb: (...args: unknown[]) => void) => void; - removeListener?: (event: string, cb: (...args: unknown[]) => void) => void; - }; - const detach = () => { - process.stdout?.off?.('data', extractPort); - process.stdout?.removeListener?.('data', extractPort); - process.stderr?.off?.('data', extractPort); - process.stderr?.removeListener?.('data', extractPort); - processAny.off?.('exit', handleExit); - processAny.removeListener?.('exit', handleExit); - clearTimeout(timer); - }; - const timer = setTimeout(() => { - if (resolved) return; - resolved = true; - detach(); - deps.warn(`Dashboard did not report its bound port quickly; assuming requested port ${preferredPort}`); - resolve(preferredPort); - }, 3000); - - const finalize = (port: number) => { - if (resolved) return; - resolved = true; - detach(); - resolve(port); - }; - const handleExit = (...exitArgs: unknown[]) => { - const code = exitArgs[0] as number | null; - const signal = exitArgs[1] as string | null; - if (resolved) { - return; - } - resolved = true; - detach(); - if (code !== null && code !== 0) { - deps.warn(`Dashboard exited before reporting its port (code: ${code}).`); - } else if (signal && signal !== 'SIGINT' && signal !== 'SIGTERM') { - deps.warn(`Dashboard exited before reporting its port (signal: ${signal}).`); - } else { - deps.warn('Dashboard exited before reporting its bound port.'); - } - resolve(null); - }; - - const extractPort = (...chunkArgs: unknown[]) => { - const firstChunk = chunkArgs[0]; - if (!firstChunk) { - return; - } - - const chunk = Buffer.isBuffer(firstChunk) - ? firstChunk - : typeof firstChunk === 'string' - ? Buffer.from(firstChunk) - : Buffer.from(JSON.stringify(firstChunk)); - - const match = chunk.toString().match(/Server running at http:\/\/localhost:(\d+)/i); - if (!match?.[1]) { - return; - } - const parsed = Number.parseInt(match[1], 10); - if (!Number.isNaN(parsed)) { - finalize(parsed); - } - }; - - process.stdout?.on?.('data', extractPort); - process.stderr?.on?.('data', extractPort); - processAny.on?.('exit', handleExit); - }); -} - -/** - * Check if the cached dashboard UI assets match the installed dashboard-server - * binary version. If they are stale (or missing a version marker), re-download - * the latest assets from the relay-dashboard GitHub release. - */ -async function refreshDashboardAssetsIfStale( - dashboardBinary: string | null, - deps: CoreDependencies -): Promise { - if (!dashboardBinary || dashboardBinary.endsWith('.js') || dashboardBinary.endsWith('.ts')) { - // Dev mode or npx — skip - return; - } - - // Get installed binary version (async to avoid blocking event loop) - let binaryVersion: string; - try { - const versionResult = await deps.execCommand(`${JSON.stringify(dashboardBinary)} --version`); - binaryVersion = versionResult.stdout.trim(); - } catch { - return; // Can't determine version — skip - } - - if (!binaryVersion) { - return; - } - - const targetDir = getDashboardRootFromBinary(dashboardBinary, deps) ?? getHomeDashboardRoot(deps); - const assetsDir = path.join(targetDir, 'out'); - const versionFile = path.join(targetDir, '.version'); - - // Check if assets match the binary version - try { - const cachedVersion = deps.fs.readFileSync(versionFile, 'utf-8').trim(); - if (cachedVersion === binaryVersion) { - return; // Up to date - } - } catch { - // No version file — need to download if assets exist but are unversioned, - // or if assets don't exist at all - if (deps.fs.existsSync(assetsDir)) { - // Assets exist but no version marker — they're from an old install - } else { - // No assets at all — need to download - } - } - - deps.log(`Updating dashboard UI assets (${binaryVersion})...`); - - const uiUrl = - 'https://github.com/AgentWorkforce/relay-dashboard/releases/latest/download/dashboard-ui.tar.gz'; - let tempDir: string | undefined; - let tempFile: string | undefined; - - try { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `dashboard-ui-${deps.pid}-`)); - tempFile = path.join(tempDir, 'dashboard-ui.tar.gz'); - // Download (async to avoid blocking event loop during network I/O) - await deps.execCommand( - `curl -fsSL --max-time 30 ${JSON.stringify(uiUrl)} -o ${JSON.stringify(tempFile)}` - ); - - // Verify it's a valid gzip - const header = Buffer.alloc(2); - const fd = fs.openSync(tempFile, 'r'); - fs.readSync(fd, header, 0, 2, 0); - fs.closeSync(fd); - if (header[0] !== 0x1f || header[1] !== 0x8b) { - if (tempFile) deps.fs.unlinkSync(tempFile); - return; // Not a valid gzip file - } - - // Remove old assets and extract (async to avoid blocking event loop) - deps.fs.rmSync(assetsDir, { recursive: true, force: true }); - deps.fs.mkdirSync(targetDir, { recursive: true }); - await deps.execCommand(`tar -xzf ${JSON.stringify(tempFile)} -C ${JSON.stringify(targetDir)}`); - if (tempFile) deps.fs.unlinkSync(tempFile); - - // Write version marker only after confirming extraction succeeded - if (deps.fs.existsSync(path.join(assetsDir, 'index.html'))) { - deps.fs.writeFileSync(versionFile, binaryVersion); - deps.log(`Dashboard UI assets updated to ${binaryVersion}`); - } else { - deps.warn('Dashboard UI extraction may be incomplete — skipping version marker'); - } - } catch { - // Best-effort — don't block startup - try { - if (tempFile) deps.fs.unlinkSync(tempFile); - } catch { - /* ignore */ - } - } finally { - try { - if (tempDir) deps.fs.rmSync(tempDir, { recursive: true, force: true }); - } catch { - /* ignore */ - } - } -} - -async function startDashboardWithFallback( - paths: CoreProjectPaths, - dashboardPort: number, - apiPort: number, - deps: CoreDependencies, - enableVerboseLogging: boolean, - relayApiKey?: string, - brokerApiKey?: string -): Promise<{ process: SpawnedProcess; port: number | null }> { - const preferredBinary = deps.findDashboardBinary(); - await refreshDashboardAssetsIfStale(preferredBinary, deps); - let process = startDashboard( - paths, - dashboardPort, - apiPort, - deps, - enableVerboseLogging, - preferredBinary, - relayApiKey, - brokerApiKey - ); - let port = await resolveStartedDashboardPort(process as DashboardStartupProcess, dashboardPort, deps); - - if (port === null && preferredBinary) { - deps.warn('Retrying dashboard startup using npx @agent-relay/dashboard-server@latest'); - process = startDashboard( - paths, - dashboardPort, - apiPort, - deps, - enableVerboseLogging, - null, - relayApiKey, - brokerApiKey - ); - port = await resolveStartedDashboardPort(process as DashboardStartupProcess, dashboardPort, deps); - } - - return { process, port }; -} - -async function waitForDashboard( - port: number, - process: SpawnedProcess, - deps: Pick, - isShuttingDown: () => boolean -): Promise { - for (let i = 0; i < 20; i++) { - await new Promise((r) => setTimeout(r, 500)); - if (process.killed) { - if (!isShuttingDown()) { - deps.warn(`Warning: Dashboard process exited before becoming ready on port ${port}`); - } - return; - } - try { - const resp = await fetch(`http://localhost:${port}/health`); - if (resp.ok) return; // Dashboard is up - } catch { - // Not ready yet - } - } - if (!isShuttingDown()) { - deps.warn(`Warning: Dashboard not responding on port ${port} after 10s`); - } -} - -async function discoverExistingBrokerApiPort( - preferredApiPort: number, - maxAttempts: number, - deps: Pick -): Promise { - const attempts = Math.max(1, maxAttempts); - for (let attempt = 0; attempt < attempts; attempt += 1) { - const candidatePort = preferredApiPort + attempt; - if (candidatePort > MAX_PORT) { - return preferredApiPort; - } - try { - const response = await fetch(`http://localhost:${candidatePort}/health`); - if (response.ok) { - if (attempt > 0) { - deps.warn(`Detected existing broker API on port ${candidatePort}.`); - } - return candidatePort; - } - } catch { - // Keep scanning. - } - } - return preferredApiPort; -} - async function shutdownUpResources( relay: CoreRelay, - dashboardProcess: SpawnedProcess | undefined, dataDir: string, - deps: CoreDependencies, - ownsBroker: boolean + deps: CoreDependencies ): Promise { - if (dashboardProcess && !dashboardProcess.killed) { - try { - dashboardProcess.kill('SIGTERM'); - } catch { - // Best-effort cleanup. - } - } - await relay.shutdown().catch(() => undefined); - if (ownsBroker) { - safeUnlink(path.join(dataDir, CONNECTION_FILENAME), deps); - } + safeUnlink(path.join(dataDir, CONNECTION_FILENAME), deps); } // eslint-disable-next-line complexity export async function runUpCommand(options: UpOptions, deps: CoreDependencies): Promise { ensureBundledAgentRelayMcpCommand(deps); - if (options.background && options.foreground) { - deps.error('Cannot use --background and --foreground together.'); - deps.exit(1); - return; - } - const paths = deps.getProjectPaths(); // --state-dir overrides where the broker writes state / connection files if (options.stateDir) { @@ -1322,7 +710,7 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies): deps.env.AGENT_RELAY_STATE_DIR = resolved; } - if (options.background || (options.dashboard === false && !options.foreground)) { + if (options.background) { const preflight = await recoverHalfStartedBroker(paths, deps); if (preflight === 'running') { const pid = readBrokerPid(paths.dataDir, deps); @@ -1397,27 +785,12 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies): return; } - const wantsDashboard = options.dashboard !== false; - const requestedDashboardPort = Number.parseInt(options.port ?? '3888', 10) || 3888; - const shouldReuseExistingBroker = options.reuseExistingBroker === true; - const dashboardPort = wantsDashboard - ? await resolveDashboardPortWithFallback(requestedDashboardPort, MAX_DASHBOARD_PORT_ATTEMPTS, deps) - : requestedDashboardPort; - if (wantsDashboard && dashboardPort !== requestedDashboardPort) { - deps.warn( - `Requested dashboard port ${requestedDashboardPort} is already in use; active dashboard will run on ${dashboardPort}.` - ); - } - + const basePort = resolveBrokerBasePort(deps); deps.fs.mkdirSync(paths.dataDir, { recursive: true }); - let existingPid = readBrokerPid(paths.dataDir, deps); - let ownsBroker = true; + const existingPid = readBrokerPid(paths.dataDir, deps); let relay: CoreRelay | null = null; - let apiPort = dashboardPort + 1; - let dashboardProcess: SpawnedProcess | undefined; let fleetSidecar: RunningFleetSidecar | undefined; - const dashboardVerbose = Boolean(options.verbose) || isDebugLikeLoggingEnabled(deps); let shuttingDown = false; let sigintCount = 0; let shutdownPromise: Promise | undefined; @@ -1429,7 +802,7 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies): } else { shutdownPromise = (async () => { await fleetSidecar?.stop(); - await shutdownUpResources(relay, dashboardProcess, paths.dataDir, deps, ownsBroker); + await shutdownUpResources(relay, paths.dataDir, deps); })(); } } @@ -1438,116 +811,12 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies): try { if (existingPid !== null) { if (isProcessRunning(existingPid, deps)) { - if (!shouldReuseExistingBroker || !wantsDashboard) { - deps.error(`Broker already running for this project (pid: ${existingPid}).`); - deps.error('Run `agent-relay status` to inspect it, then `agent-relay down` to stop it.'); - deps.exit(1); - return; - } - - apiPort = await discoverExistingBrokerApiPort(Math.max(1, apiPort), MAX_API_PORT_ATTEMPTS, deps); - const reusableRelay = await deps.createRelay(paths.projectRoot, apiPort); - try { - await reusableRelay.getStatus(); - } catch { - await reusableRelay.shutdown().catch(() => undefined); - deps.warn( - `Broker already running for this project (pid: ${existingPid}), but API port ${apiPort} is not responding.` - ); - deps.warn('Treating this as stale broker state and starting a fresh broker.'); - safeUnlink(path.join(paths.dataDir, CONNECTION_FILENAME), deps); - existingPid = null; - } - - if (existingPid === null) { - // fallthrough and start a fresh broker - } else { - relay = reusableRelay; - ownsBroker = false; - const dashboardRelayUrl = resolveDashboardRelayUrl(apiPort, deps); - const expectedRelayUrl = getDefaultDashboardRelayUrl(apiPort); - if ( - deps.env.RELAY_DASHBOARD_RELAY_URL && - deps.env.RELAY_DASHBOARD_RELAY_URL.trim() !== '' && - deps.env.RELAY_DASHBOARD_RELAY_URL.trim() !== expectedRelayUrl - ) { - deps.warn( - `RELAY_DASHBOARD_RELAY_URL is set to ${deps.env.RELAY_DASHBOARD_RELAY_URL.trim()}, ` + - `but this session computed ${expectedRelayUrl}.` - ); - } - deps.log(`Relay API: ${dashboardRelayUrl}`); - if (dashboardVerbose) { - deps.log(`[dashboard] relay target resolved from config: ${dashboardRelayUrl}`); - } - deps.log(`Project: ${paths.projectRoot}`); - deps.log('Mode: broker (stdio)'); - deps.log(`Workspace Key: ${relay.workspaceKey ?? 'unknown'}`); - deps.log('Broker already running for this project; reusing existing broker.'); - - if (wantsDashboard) { - const brokerConn = readBrokerConnectionFromFs(deps.fs, paths.dataDir); - const dashboardStart = await startDashboardWithFallback( - paths, - dashboardPort, - apiPort, - deps, - dashboardVerbose, - relay?.workspaceKey, - brokerConn?.api_key - ); - dashboardProcess = dashboardStart.process; - const startedDashboardPort = dashboardStart.port; - if (startedDashboardPort === null) { - deps.warn('Dashboard failed to start. Check dashboard error logs above.'); - } else { - if (startedDashboardPort !== dashboardPort) { - deps.warn( - `Dashboard port ${dashboardPort} was already in use, so dashboard started on ${startedDashboardPort}` - ); - } - const dashboardPath = normalizeDashboardPath(options.dashboardPath); - const dashboardUrl = dashboardPath - ? `http://localhost:${startedDashboardPort}${dashboardPath}` - : `http://localhost:${startedDashboardPort}`; - deps.log(`Dashboard: ${dashboardUrl}`); - - waitForDashboard(startedDashboardPort, dashboardProcess, deps, () => shuttingDown).catch( - () => {} - ); - } - } - - fleetSidecar = startImplicitLocalFleetSidecar(paths, relay, options, deps); - - deps.onSignal('SIGINT', async () => { - sigintCount += 1; - if (shuttingDown) { - if (sigintCount >= 2) { - deps.warn('Force exiting...'); - deps.exit(130); - } - return; - } - deps.log('\nStopping...'); - await shutdownOnce(); - deps.exit(0); - }); - deps.onSignal('SIGTERM', async () => { - if (shuttingDown) { - return; - } - await shutdownOnce(); - deps.exit(0); - }); - - await deps.holdOpen(); - return; - } + deps.error(`Broker already running for this project (pid: ${existingPid}).`); + deps.error('Run `agent-relay status` to inspect it, then `agent-relay down` to stop it.'); + deps.exit(1); + return; } - safeUnlink(path.join(paths.dataDir, CONNECTION_FILENAME), deps); - existingPid = null; } // If a workspace key was explicitly provided, inject it into the environment @@ -1561,81 +830,29 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies): // files (e.g. user deleted .agentworkforce/relay/ while broker was running). await killOrphanedBrokerProcesses(paths.projectRoot, deps); - const started = await startBrokerWithPortFallback(paths, dashboardPort, deps, options.brokerName); + const started = await startBrokerWithPortFallback(paths, basePort, deps, options.brokerName); relay = started.relay; - apiPort = started.apiPort; - const dashboardRelayUrl = resolveDashboardRelayUrl(apiPort, deps); - const expectedRelayUrl = getDefaultDashboardRelayUrl(apiPort); - if ( - deps.env.RELAY_DASHBOARD_RELAY_URL && - deps.env.RELAY_DASHBOARD_RELAY_URL.trim() !== '' && - deps.env.RELAY_DASHBOARD_RELAY_URL.trim() !== expectedRelayUrl - ) { - deps.warn( - `RELAY_DASHBOARD_RELAY_URL is set to ${deps.env.RELAY_DASHBOARD_RELAY_URL.trim()}, ` + - `but this session computed ${expectedRelayUrl}.` - ); - } - deps.log(`Relay API: ${dashboardRelayUrl}`); - if (dashboardVerbose) { - deps.log(`[dashboard] relay target resolved from config: ${dashboardRelayUrl}`); - } + deps.log(`Relay API: http://localhost:${started.apiPort}`); deps.log(`Project: ${paths.projectRoot}`); deps.log('Mode: broker (stdio)'); deps.log(`Workspace Key: ${relay.workspaceKey ?? 'unknown'}`); deps.log('Broker started.'); - if (wantsDashboard) { - const brokerConn = readBrokerConnectionFromFs(deps.fs, paths.dataDir); - const dashboardStart = await startDashboardWithFallback( - paths, - dashboardPort, - apiPort, - deps, - dashboardVerbose, - relay?.workspaceKey, - brokerConn?.api_key - ); - dashboardProcess = dashboardStart.process; - const startedDashboardPort = dashboardStart.port; - if (startedDashboardPort === null) { - deps.warn('Dashboard failed to start. Check dashboard error logs above.'); - } else { - if (startedDashboardPort !== dashboardPort) { - deps.warn( - `Dashboard port ${dashboardPort} was already in use, so dashboard started on ${startedDashboardPort}` - ); - } - const dashboardPath = normalizeDashboardPath(options.dashboardPath); - const dashboardUrl = dashboardPath - ? `http://localhost:${startedDashboardPort}${dashboardPath}` - : `http://localhost:${startedDashboardPort}`; - deps.log(`Dashboard: ${dashboardUrl}`); - - // Verify the dashboard is actually reachable (non-blocking) - waitForDashboard(startedDashboardPort, dashboardProcess, deps, () => shuttingDown).catch(() => {}); - } - } - const teamsConfig = deps.loadTeamsConfig(paths.projectRoot); fleetSidecar = startImplicitLocalFleetSidecar(paths, relay, options, deps, teamsConfig); const shouldSpawn = options.spawn === true ? true : options.spawn === false ? false : Boolean(teamsConfig?.autoSpawn); if (shouldSpawn && teamsConfig && teamsConfig.agents.length > 0) { - if (wantsDashboard) { - deps.warn('Warning: auto-spawn from teams.json is skipped when dashboard mode manages the broker'); - } else { - for (const agent of teamsConfig.agents) { - await relay.spawn({ - name: agent.name, - cli: agent.cli, - channels: ['general'], - task: agent.task ?? '', - team: teamsConfig.team, - }); - } + for (const agent of teamsConfig.agents) { + await relay.spawn({ + name: agent.name, + cli: agent.cli, + channels: ['general'], + task: agent.task ?? '', + team: teamsConfig.team, + }); } } else if (options.spawn === true && !teamsConfig) { deps.warn('Warning: --spawn specified but no teams.json found'); @@ -1666,14 +883,12 @@ export async function runUpCommand(options: UpOptions, deps: CoreDependencies): } catch (err: unknown) { await shutdownOnce(); const message = toErrorMessage(err); - const stage = classifyBrokerStartStage(err, message, wantsDashboard); + const stage = classifyBrokerStartStage(err, message); track('broker_start_failed', { stage, error_class: classifyBrokerStartError(err), }); - if (errorCode(err) === 'EADDRINUSE' && wantsDashboard) { - deps.error(`Dashboard port ${dashboardPort} is already in use.`); - } else if (isBrokerAlreadyRunningError(message)) { + if (isBrokerAlreadyRunningError(message)) { reportAlreadyRunningError(message, paths.dataDir, deps); } else { deps.error(`Failed to start broker: ${describeError(err)}`); diff --git a/packages/cli/src/cli/lib/core-maintenance.ts b/packages/cli/src/cli/lib/core-maintenance.ts index 046e6bde5..b38d1ece3 100644 --- a/packages/cli/src/cli/lib/core-maintenance.ts +++ b/packages/cli/src/cli/lib/core-maintenance.ts @@ -278,43 +278,13 @@ export async function runUninstallCommand( removeZedConfig(serverName, deps.fs, isDryRun, deps.log); } - // --- Dashboard static assets removal --- - const homeDir = os.homedir(); - const dashboardAssetPaths = [ - path.join(homeDir, '.relay', 'dashboard', 'out'), - path.join(homeDir, '.relay', 'dashboard', '.version'), - ...INSTALL_DIR_NAMES.flatMap((installDir) => [ - path.join(homeDir, installDir, 'dashboard', 'out'), - path.join(homeDir, installDir, 'dashboard', '.version'), - ]), - ]; - for (const assetPath of dashboardAssetPaths) { - if (deps.fs.existsSync(assetPath)) { - if (isDryRun) { - deps.log(`[dry-run] Would remove dashboard asset path: ${assetPath}`); - } else { - try { - deps.fs.rmSync(assetPath, { recursive: true, force: true }); - deps.log(`Removed dashboard asset path: ${assetPath}`); - } catch { - // Best-effort fallback for file-only implementations. - try { - deps.fs.unlinkSync(assetPath); - deps.log(`Removed dashboard asset path: ${assetPath}`); - } catch { - // Best-effort. - } - } - } - } - } - // --- Binary removal (standalone binaries + npm packages) --- + const homeDir = os.homedir(); const standaloneBinDir = path.join(homeDir, '.local', 'bin'); const installBinDirs = INSTALL_DIR_NAMES.map((installDir) => path.join(homeDir, installDir, 'bin')); // Remove standalone binaries from ~/.local/bin - for (const binaryName of ['agent-relay', 'relay-dashboard-server', 'relay-acp']) { + for (const binaryName of ['agent-relay', 'relay-acp']) { const binPath = path.join(standaloneBinDir, binaryName); if (deps.fs.existsSync(binPath)) { if (isDryRun) { @@ -330,21 +300,6 @@ export async function runUninstallCommand( } } - // Remove relay-dashboard-server from /usr/local/bin (another search path used by findDashboardBinary) - const usrLocalBinDashboard = path.join('/usr/local/bin', 'relay-dashboard-server'); - if (deps.fs.existsSync(usrLocalBinDashboard)) { - if (isDryRun) { - deps.log(`[dry-run] Would remove binary: ${usrLocalBinDashboard}`); - } else { - try { - deps.fs.unlinkSync(usrLocalBinDashboard); - deps.log(`Removed ${usrLocalBinDashboard}`); - } catch { - // Best-effort. - } - } - } - // Remove installer bin dirs without deleting the parent data directories. for (const installBinDir of installBinDirs) { if (!deps.fs.existsSync(installBinDir)) { @@ -364,7 +319,7 @@ export async function runUninstallCommand( // Remove npm-installed packages if (!isDryRun) { - for (const pkg of ['agent-relay', '@agent-relay/dashboard-server']) { + for (const pkg of ['agent-relay']) { try { await deps.execCommand(`npm uninstall -g ${pkg}`); deps.log(`Uninstalled npm package: ${pkg}`); @@ -373,7 +328,7 @@ export async function runUninstallCommand( } } } else { - deps.log('[dry-run] Would run: npm uninstall -g agent-relay @agent-relay/dashboard-server'); + deps.log('[dry-run] Would run: npm uninstall -g agent-relay'); } // --- Snippet cleanup (CLAUDE.md, GEMINI.md, AGENTS.md) --- diff --git a/packages/cli/src/cli/telemetry/events.ts b/packages/cli/src/cli/telemetry/events.ts index 3344f2806..2fdb1bf79 100644 --- a/packages/cli/src/cli/telemetry/events.ts +++ b/packages/cli/src/cli/telemetry/events.ts @@ -19,13 +19,13 @@ */ /** Source of spawn/release action */ -export type ActionSource = 'human_cli' | 'human_dashboard' | 'agent' | 'protocol'; +export type ActionSource = 'human_cli' | 'agent' | 'protocol'; /** Component that emitted the telemetry event. */ -export type TelemetryApp = 'cli' | 'broker' | 'sdk' | 'relaycast-server' | 'dashboard' | 'unknown'; +export type TelemetryApp = 'cli' | 'broker' | 'sdk' | 'relaycast-server' | 'unknown'; /** User-facing product surface responsible for the event. */ -export type TelemetrySurface = 'cli' | 'broker' | 'sdk' | 'cloud' | 'mcp' | 'dashboard' | 'unknown'; +export type TelemetrySurface = 'cli' | 'broker' | 'sdk' | 'cloud' | 'mcp' | 'unknown'; /** * Reason for agent release. diff --git a/packages/config/src/cloud-config.ts b/packages/config/src/cloud-config.ts index b94bd44ca..b1391a73c 100644 --- a/packages/config/src/cloud-config.ts +++ b/packages/config/src/cloud-config.ts @@ -6,13 +6,9 @@ export interface CloudConfig { // Server port: number; publicUrl: string; - appUrl: string; // Dashboard app URL (e.g., app.agentrelay.com) + appUrl: string; // Hosted web app URL (e.g., app.agentrelay.com) sessionSecret: string; - // Local dashboard URL for channel API proxying (optional) - // When set, channel requests are proxied to this URL instead of the workspace - localDashboardUrl?: string; - // Database databaseUrl: string; redisUrl: string; @@ -105,9 +101,6 @@ export function loadConfig(): CloudConfig { appUrl: process.env.APP_URL || process.env.PUBLIC_URL || 'http://localhost:4567', sessionSecret: requireEnv('SESSION_SECRET'), - // Local dashboard for channel API (auto-detected if not set) - localDashboardUrl: optionalEnv('LOCAL_DASHBOARD_URL'), - databaseUrl: requireEnv('DATABASE_URL'), redisUrl: process.env.REDIS_URL || 'redis://localhost:6379', diff --git a/packages/config/src/schemas.ts b/packages/config/src/schemas.ts index 532f0ab89..622695092 100644 --- a/packages/config/src/schemas.ts +++ b/packages/config/src/schemas.ts @@ -127,7 +127,6 @@ export const CloudConfigSchema = z.object({ publicUrl: z.string(), appUrl: z.string(), sessionSecret: z.string(), - localDashboardUrl: z.string().optional(), databaseUrl: z.string(), redisUrl: z.string(), github: z.object({ diff --git a/packages/harness-driver/src/spawn-config.ts b/packages/harness-driver/src/spawn-config.ts index 831c0fe0c..b67a3cd11 100644 --- a/packages/harness-driver/src/spawn-config.ts +++ b/packages/harness-driver/src/spawn-config.ts @@ -4,7 +4,7 @@ import type { EventBus } from './event-bus.js'; import type { HarnessDriverEvents } from './lifecycle-hooks.js'; export interface BrokerInitArgs { - /** Optional HTTP API port for dashboard proxy (0 = disabled). */ + /** Optional HTTP API port for the broker (0 = disabled). */ apiPort?: number; /** Bind address for the HTTP API. Defaults to 127.0.0.1 in the broker. */ apiBind?: string; diff --git a/packages/policy/src/agent-policy.ts b/packages/policy/src/agent-policy.ts index d4a37e694..bb5984d12 100644 --- a/packages/policy/src/agent-policy.ts +++ b/packages/policy/src/agent-policy.ts @@ -20,7 +20,7 @@ import os from 'node:os'; * * Policy files are loaded from (in order of precedence): * 1. User-level: ~/.config/agent-relay/policies/*.yaml (NOT in source control) - * 2. Cloud: Workspace config from dashboard (stored in database) + * 2. Cloud: Workspace config from the cloud web app (stored in database) * * PRPM packages install to the user-level location to avoid polluting repos. * Install via: prpm install @org/strict-agent-rules --global diff --git a/scripts/ci-standalone-smoke.sh b/scripts/ci-standalone-smoke.sh index c5e9e8d51..7e8da2b3d 100755 --- a/scripts/ci-standalone-smoke.sh +++ b/scripts/ci-standalone-smoke.sh @@ -118,9 +118,9 @@ echo "=== Smoke: standalone down --force ===" DOWN_OUTPUT="$(run_cli local down --force 2>&1 || true)" assert_exact_count "$DOWN_OUTPUT" '^Cleaned up \(was not running\)$' 1 'down cleanup line' -echo "=== Smoke: standalone up --no-dashboard ===" +echo "=== Smoke: standalone up ===" UP_LOG="$TMP_ROOT/up.log" -run_cli local up --no-dashboard >"$UP_LOG" 2>&1 & +run_cli local up >"$UP_LOG" 2>&1 & UP_PID=$! sleep 8 diff --git a/scripts/demos/server-capacity.sh b/scripts/demos/server-capacity.sh index 59b00d6fc..53eb76edf 100755 --- a/scripts/demos/server-capacity.sh +++ b/scripts/demos/server-capacity.sh @@ -57,7 +57,7 @@ echo "Created: $DEMO_DIR/server-capacity.md" echo "" echo "=== HOW TO RUN ===" echo "" -echo "1. Start daemon: agent-relay up --dashboard" +echo "1. Start daemon: agent-relay up" echo "" echo "2. Start 3 agents in separate terminals:" echo " agent-relay -n WebAPI claude" diff --git a/scripts/demos/sprint-planning.sh b/scripts/demos/sprint-planning.sh index a249a8e55..360e3587b 100755 --- a/scripts/demos/sprint-planning.sh +++ b/scripts/demos/sprint-planning.sh @@ -61,7 +61,7 @@ echo "Created: $DEMO_DIR/sprint-planning.md" echo "" echo "=== HOW TO RUN ===" echo "" -echo "1. Start daemon: agent-relay up --dashboard" +echo "1. Start daemon: agent-relay up" echo "" echo "2. Start 3 agents in separate terminals:" echo " agent-relay -n ProductLead claude" diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh index b755ef710..f8ecc52bb 100755 --- a/scripts/e2e-test.sh +++ b/scripts/e2e-test.sh @@ -18,7 +18,7 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # Configuration AGENT_NAME="e2e-test-agent" -DASHBOARD_PORT=3889 # Use different port to avoid conflicts with running instances +BROKER_PORT=3889 # Broker API binds BROKER_PORT+1; kept distinct to avoid conflicts SPAWN_TIMEOUT=120 DAEMON_ONLY=false @@ -30,11 +30,11 @@ while [[ $# -gt 0 ]]; do shift ;; --port) - DASHBOARD_PORT="$2" + BROKER_PORT="$2" shift 2 ;; --port=*) - DASHBOARD_PORT="${1#*=}" + BROKER_PORT="${1#*=}" shift ;; *) @@ -118,7 +118,7 @@ fi log_info "Configuration:" log_info " Agent name: $AGENT_NAME" -log_info " Dashboard port: $DASHBOARD_PORT" +log_info " Broker port: $BROKER_PORT" log_info " Daemon only: $DAEMON_ONLY" log_info " CLI command: $CLI_CMD" @@ -131,9 +131,6 @@ cleanup() { log_info "Ensuring daemon is stopped..." run_with_timeout 10 "$CLI_CMD" local down --force --timeout 5000 2>/dev/null || true - # Force kill any remaining processes if timeout occurred - pkill -9 -f "relay-dashboard-server.*--port.*$DASHBOARD_PORT" 2>/dev/null || true - log_info "Cleanup complete." } trap cleanup EXIT @@ -154,16 +151,16 @@ log_phase "Phase 1: Broker Startup" # Kill any existing daemon (with timeout to prevent hanging) run_with_timeout 10 "$CLI_CMD" local down --force --timeout 5000 2>/dev/null || true -# Kill any process using our target port (ensures dashboard can bind) +# Kill any process using our target port (ensures the broker can bind) if command -v lsof &> /dev/null; then - lsof -ti:$DASHBOARD_PORT | xargs kill -9 2>/dev/null || true + lsof -ti:$BROKER_PORT | xargs kill -9 2>/dev/null || true fi sleep 1 -# Start broker+dashboard in background, redirect output to log file +# Start broker in background, redirect output to log file DAEMON_LOG="$PROJECT_DIR/.agentworkforce/relay/e2e-daemon.log" mkdir -p "$(dirname "$DAEMON_LOG")" -"$CLI_CMD" local up --port "$DASHBOARD_PORT" > "$DAEMON_LOG" 2>&1 & +AGENT_RELAY_BROKER_PORT="$BROKER_PORT" "$CLI_CMD" local up > "$DAEMON_LOG" 2>&1 & DAEMON_PID=$! log_info "Daemon started (PID: $DAEMON_PID)" log_info "Daemon log: $DAEMON_LOG" diff --git a/scripts/run-dashboard.js b/scripts/run-dashboard.js deleted file mode 100644 index 065ce4510..000000000 --- a/scripts/run-dashboard.js +++ /dev/null @@ -1,3 +0,0 @@ -import { startDashboard } from '../dist/dashboard/server.js'; - -startDashboard(3888, '/tmp/agent-relay-team', '/tmp/agent-relay.sqlite'); diff --git a/scripts/test-interactive-terminal.sh b/scripts/test-interactive-terminal.sh index 68f792dd0..24bd2551d 100755 --- a/scripts/test-interactive-terminal.sh +++ b/scripts/test-interactive-terminal.sh @@ -123,7 +123,7 @@ setup_test_environment() { echo "" echo " Codex: $CLOUD_URL/api/test/auto-login?redirect=/providers/setup/codex?workspace=$WORKSPACE_ID" echo "" - log "Or access the dashboard:" + log "Or access the cloud web app:" echo "" echo " $CLOUD_URL/api/test/auto-login?redirect=/app" echo "" diff --git a/scripts/test-spawn-refactor.sh b/scripts/test-spawn-refactor.sh deleted file mode 100755 index 24d3e3666..000000000 --- a/scripts/test-spawn-refactor.sh +++ /dev/null @@ -1,510 +0,0 @@ -#!/bin/bash -# -# Test Script: SDK Spawn Refactor (GitHub #374) -# -# Tests all changes from the sdk-spawn-refactor branch: -# 1. Unit tests (relay + dashboard) -# 2. SDK integration tests (spawn, release, messaging) -# 3. E2E daemon lifecycle (daemon-only mode, no API key needed) -# 4. Dashboard fleet endpoint verification -# 5. Manual test checklist for live agent spawning -# -# Usage: -# ./scripts/test-spawn-refactor.sh # Run automated tests -# ./scripts/test-spawn-refactor.sh --full # Run all tests incl. live agent spawn -# ./scripts/test-spawn-refactor.sh --checklist # Print manual test checklist only -# - -set -uo pipefail -# Note: NOT using set -e because we handle failures via the FAILURES counter - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -DASHBOARD_DIR="$PROJECT_DIR/../relay-dashboard" -CLI_CMD="$PROJECT_DIR/packages/cli/dist/cli/index.js" -DASHBOARD_PORT=3891 # Use unique port to avoid conflicts -FULL_TEST=false -CHECKLIST_ONLY=false - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - --full) FULL_TEST=true; shift ;; - --checklist) CHECKLIST_ONLY=true; shift ;; - *) shift ;; - esac -done - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -pass() { echo -e " ${GREEN}PASS${NC} $1"; } -fail() { echo -e " ${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); } -skip() { echo -e " ${YELLOW}SKIP${NC} $1"; } -phase() { echo -e "\n${CYAN}${BOLD}=== $1 ===${NC}\n"; } -info() { echo -e " ${BOLD}INFO${NC} $1"; } - -FAILURES=0 -TOTAL=0 - -check() { - TOTAL=$((TOTAL + 1)) - if eval "$1" > /dev/null 2>&1; then - pass "$2" - else - fail "$2" - fi - return 0 # Never fail the script itself -} - -# ------------------------------------------------------- -# Manual test checklist -# ------------------------------------------------------- -print_checklist() { - echo "" - echo -e "${CYAN}${BOLD}=================================================${NC}" - echo -e "${CYAN}${BOLD} SDK Spawn Refactor - Manual Test Checklist${NC}" - echo -e "${CYAN}${BOLD}=================================================${NC}" - echo "" - echo -e "${BOLD}Prerequisites:${NC}" - echo " 1. Stop existing daemon: agent-relay down --force" - echo " 2. Start LOCAL daemon: node packages/cli/dist/cli/index.js up --dashboard --port $DASHBOARD_PORT" - echo " 3. Ensure ANTHROPIC_API_KEY is set" - echo "" - echo -e "${BOLD}Test 1: Spawn via Local CLI (daemon socket path)${NC}" - echo " node packages/cli/dist/cli/index.js spawn TestWorker claude 'Say hello then wait' --port $DASHBOARD_PORT" - echo " Expected: Agent spawns via daemon socket (check daemon log for 'SPAWN' envelope)" - echo " Verify: node packages/cli/dist/cli/index.js agents --port $DASHBOARD_PORT" - echo " Cleanup: node packages/cli/dist/cli/index.js release TestWorker --port $DASHBOARD_PORT" - echo "" - echo -e "${BOLD}Test 2: Spawn via Dashboard UI${NC}" - echo " Open http://localhost:$DASHBOARD_PORT in browser" - echo " Click 'Spawn Agent' button" - echo " Fill in: name=UIWorker, cli=claude, task='Hello from dashboard'" - echo " Expected: Dashboard routes spawn through SDK -> daemon" - echo " Verify: Agent appears in fleet view" - echo " Cleanup: Release from UI" - echo "" - echo -e "${BOLD}Test 3: Fleet Endpoints (spawnReader fix)${NC}" - echo " curl http://localhost:$DASHBOARD_PORT/api/fleet/servers | jq ." - echo " curl http://localhost:$DASHBOARD_PORT/api/fleet/stats | jq ." - echo " Expected: localAgents should reflect actual spawned agents (not empty)" - echo "" - echo -e "${BOLD}Test 4: Fallback Chain (daemon-first, no policy bypass)${NC}" - echo " 1. Spawn an agent: node packages/cli/dist/cli/index.js spawn FallbackTest claude 'wait'" - echo " 2. Try spawning same name again: node packages/cli/dist/cli/index.js spawn FallbackTest claude 'wait'" - echo " Expected: Second spawn gets daemon rejection, does NOT fall through to HTTP" - echo " Cleanup: node packages/cli/dist/cli/index.js release FallbackTest" - echo "" - echo -e "${BOLD}Test 5: spawnerName Passthrough${NC}" - echo " Spawn agent with custom spawnerName from dashboard route:" - echo " curl -X POST http://localhost:$DASHBOARD_PORT/api/spawn \\" - echo " -H 'Content-Type: application/json' \\" - echo " -d '{\"name\":\"SpawnerTest\",\"cli\":\"claude\",\"task\":\"wait\",\"spawnerName\":\"MyOrchestrator\"}'" - echo " Expected: SpawnPayload contains spawnerName='MyOrchestrator'" - echo " Verify: Agent shows MyOrchestrator as spawner in agent list" - echo " Cleanup: curl -X POST http://localhost:$DASHBOARD_PORT/api/release -H 'Content-Type: application/json' -d '{\"name\":\"SpawnerTest\"}'" - echo "" - echo -e "${BOLD}Test 6: SDK Integration Tests (requires daemon running)${NC}" - echo " cd $PROJECT_DIR" - echo " node tests/integration/run-all-tests.js --type=sdk" - echo " Expected: All SDK tests pass (spawn, release, messaging)" - echo "" - echo -e "${BOLD}Test 7: E2E Full Lifecycle${NC}" - echo " # Stop the test daemon first, then:" - echo " ./scripts/e2e-test.sh --port $DASHBOARD_PORT" - echo " Expected: Full lifecycle pass (up -> spawn -> release -> down)" - echo "" - echo -e "${BOLD}Key Changes to Verify:${NC}" - echo " [ ] Daemon handles SEND_INPUT, LIST_WORKERS messages" - echo " [ ] SDK client.spawn() accepts spawnerName option" - echo " [ ] Dashboard fleet endpoints use spawnReader (not spawner)" - echo " [ ] Fallback chain only falls through on transport errors" - echo " [ ] LIST_WORKERS_RESULT includes error field on failure" - echo " [ ] Documentation updated (protocol.md, daemon.md, SDK README)" - echo "" -} - -if [ "$CHECKLIST_ONLY" = true ]; then - print_checklist - exit 0 -fi - -# ------------------------------------------------------- -# Phase 0: Verify builds -# ------------------------------------------------------- -phase "Phase 0: Build Verification" - -cd "$PROJECT_DIR" - -check "test -f packages/cli/dist/cli/index.js" "Relay CLI built" -check "test -f packages/sdk/dist/client.js" "SDK package built" -check "test -f packages/daemon/dist/server.js" "Daemon package built" -check "test -f packages/protocol/dist/types.js" "Protocol package built" -check "test -f packages/wrapper/dist/relay-pty-orchestrator.js" "Wrapper package built" - -if [ -d "$DASHBOARD_DIR" ]; then - check "test -f $DASHBOARD_DIR/packages/dashboard-server/dist/server.js" "Dashboard server built" -else - skip "Dashboard repo not found at $DASHBOARD_DIR" -fi - -# ------------------------------------------------------- -# Phase 1: Unit Tests -# ------------------------------------------------------- -phase "Phase 1: Relay Unit Tests" - -info "Running vitest (this takes ~30s)..." -RELAY_TEST_OUTPUT=$(npm test 2>&1) -RELAY_TEST_RESULT=$(echo "$RELAY_TEST_OUTPUT" | grep "Tests" | tail -1) -if echo "$RELAY_TEST_OUTPUT" | grep -q "passed"; then - pass "Relay unit tests: $RELAY_TEST_RESULT" - TOTAL=$((TOTAL + 1)) -else - fail "Relay unit tests failed" - echo "$RELAY_TEST_OUTPUT" | tail -20 -fi - -phase "Phase 1b: Dashboard Unit Tests" - -if [ -d "$DASHBOARD_DIR" ]; then - cd "$DASHBOARD_DIR" - info "Running dashboard vitest..." - DASH_TEST_OUTPUT=$(npm test 2>&1) - DASH_TEST_RESULT=$(echo "$DASH_TEST_OUTPUT" | grep "Tests" | tail -1) - if echo "$DASH_TEST_OUTPUT" | grep -q "passed"; then - pass "Dashboard unit tests: $DASH_TEST_RESULT" - TOTAL=$((TOTAL + 1)) - else - fail "Dashboard unit tests failed" - echo "$DASH_TEST_OUTPUT" | tail -20 - fi - cd "$PROJECT_DIR" -else - skip "Dashboard repo not found" -fi - -# ------------------------------------------------------- -# Phase 2: Protocol Type Verification -# ------------------------------------------------------- -phase "Phase 2: Protocol Type Verification" - -info "Checking new protocol types exist in built output..." - -# Check SEND_INPUT type exists -check "grep -q 'SEND_INPUT' packages/protocol/dist/types.js 2>/dev/null || grep -q 'SEND_INPUT' packages/protocol/dist/types.d.ts 2>/dev/null" \ - "SEND_INPUT message type in protocol" - -# Check LIST_WORKERS type exists -check "grep -q 'LIST_WORKERS' packages/protocol/dist/types.js 2>/dev/null || grep -q 'LIST_WORKERS' packages/protocol/dist/types.d.ts 2>/dev/null" \ - "LIST_WORKERS message type in protocol" - -# Check ListWorkersResultPayload has error field -check "grep -q 'error' packages/protocol/dist/types.d.ts 2>/dev/null && grep -q 'ListWorkersResultPayload' packages/protocol/dist/types.d.ts 2>/dev/null" \ - "ListWorkersResultPayload includes error field" - -# Check SDK has spawnerName in spawn options -check "grep -q 'spawnerName' packages/sdk/dist/client.d.ts 2>/dev/null" \ - "SDK spawn() accepts spawnerName option" - -# Check SDK has sendWorkerInput method -check "grep -q 'sendWorkerInput' packages/sdk/dist/client.d.ts 2>/dev/null" \ - "SDK has sendWorkerInput() method" - -# Check SDK has listWorkers method -check "grep -q 'listWorkers' packages/sdk/dist/client.d.ts 2>/dev/null" \ - "SDK has listWorkers() method" - -# ------------------------------------------------------- -# Phase 3: Spawn Manager Verification -# ------------------------------------------------------- -phase "Phase 3: Daemon Spawn Manager Verification" - -info "Checking spawn-manager handles new message types..." - -check "grep -q 'SEND_INPUT' packages/daemon/dist/spawn-manager.js" \ - "SpawnManager handles SEND_INPUT" - -check "grep -q 'LIST_WORKERS' packages/daemon/dist/spawn-manager.js" \ - "SpawnManager handles LIST_WORKERS" - -check "grep -q 'SEND_INPUT_RESULT' packages/daemon/dist/spawn-manager.js" \ - "SpawnManager sends SEND_INPUT_RESULT" - -check "grep -q 'LIST_WORKERS_RESULT' packages/daemon/dist/spawn-manager.js" \ - "SpawnManager sends LIST_WORKERS_RESULT" - -# ------------------------------------------------------- -# Phase 4: Wrapper Orchestrator Fallback Verification -# ------------------------------------------------------- -phase "Phase 4: Wrapper Fallback Chain Verification" - -info "Checking orchestrator fallback logic..." - -# Verify the fix: daemon responses always return (no fall-through to HTTP) -check "grep -q 'Always return if daemon responded' packages/wrapper/dist/relay-pty-orchestrator.js 2>/dev/null || \ - grep -q 'transport error' packages/wrapper/dist/relay-pty-orchestrator.js 2>/dev/null" \ - "Fallback chain: daemon rejection stops cascade" - -# ------------------------------------------------------- -# Phase 5: Dashboard Integration Checks (static analysis) -# ------------------------------------------------------- -phase "Phase 5: Dashboard Integration Checks" - -if [ -d "$DASHBOARD_DIR" ]; then - DASH_SERVER="$DASHBOARD_DIR/packages/dashboard-server" - - # Check fleet endpoints use spawnReader - check "grep -q 'spawnReader' $DASH_SERVER/dist/server.js 2>/dev/null || \ - grep -q 'spawnReader' $DASH_SERVER/src/server.ts 2>/dev/null" \ - "Fleet endpoints use spawnReader (not spawner)" - - # Check spawn route passes spawnerName - check "grep -q 'spawnerName' $DASH_SERVER/src/server.ts" \ - "Spawn route passes spawnerName to SDK" - - # Make sure fleet doesn't use spawner directly for getActiveWorkers - if grep -n 'spawner?.getActiveWorkers\|spawner\.getActiveWorkers' "$DASH_SERVER/src/server.ts" 2>/dev/null | grep -v 'spawnReader' | grep -v '//' | head -1 | grep -q '.'; then - fail "Fleet endpoint still uses spawner?.getActiveWorkers() directly" - else - pass "Fleet endpoints don't bypass spawnReader" - TOTAL=$((TOTAL + 1)) - fi -else - skip "Dashboard repo not found" -fi - -# ------------------------------------------------------- -# Phase 6: E2E Daemon Lifecycle (daemon-only, no API key needed) -# ------------------------------------------------------- -phase "Phase 6: E2E Daemon Lifecycle (daemon-only mode)" - -# Check if existing daemon is running on the same socket -EXISTING_DAEMON=$(pgrep -f "agent-relay up" 2>/dev/null || true) -if [ -n "$EXISTING_DAEMON" ]; then - info "Existing daemon found (PID: $EXISTING_DAEMON). Stopping it first..." - "$CLI_CMD" down --force --timeout 5000 2>/dev/null || true - # Also try the global CLI - agent-relay down --force --timeout 5000 2>/dev/null || true - sleep 2 - # Force kill if still running - pgrep -f "agent-relay up" | xargs kill -9 2>/dev/null || true - pgrep -f "relay-dashboard-server" | xargs kill -9 2>/dev/null || true - sleep 1 -fi - -info "Starting local daemon on port $DASHBOARD_PORT..." - -# Kill any existing process on our test port -lsof -ti:$DASHBOARD_PORT | xargs kill -9 2>/dev/null || true -sleep 1 - -# Clean stale socket -rm -f "$PROJECT_DIR/.agentworkforce/relay/relay.sock" 2>/dev/null || true - -# Start daemon with local build -DAEMON_LOG="$PROJECT_DIR/.agentworkforce/relay/test-spawn-refactor.log" -mkdir -p "$(dirname "$DAEMON_LOG")" -"$CLI_CMD" up --dashboard --port "$DASHBOARD_PORT" > "$DAEMON_LOG" 2>&1 & -DAEMON_PID=$! - -cleanup_daemon() { - kill $DAEMON_PID 2>/dev/null || true - lsof -ti:$DASHBOARD_PORT | xargs kill -9 2>/dev/null || true -} -trap cleanup_daemon EXIT - -# Wait for daemon -for i in $(seq 1 20); do - if curl -s "http://127.0.0.1:${DASHBOARD_PORT}/health" > /dev/null 2>&1; then - break - fi - if [ $i -eq 20 ]; then - fail "Local daemon failed to start within 20s" - echo " Daemon log:" - tail -20 "$DAEMON_LOG" 2>/dev/null || true - echo "" - echo -e "${RED}${BOLD}Cannot continue without daemon. Aborting.${NC}" - exit 1 - fi - sleep 1 -done -pass "Local daemon started (PID: $DAEMON_PID, port: $DASHBOARD_PORT)" -TOTAL=$((TOTAL + 1)) - -# Wait a bit more for dashboard to fully initialize -sleep 3 - -# Test health endpoint -check "curl -sf http://127.0.0.1:${DASHBOARD_PORT}/health" \ - "Health endpoint responds" - -# Test agents endpoint (may be at /api/agents or via CLI) -AGENTS_RESP=$(curl -s "http://127.0.0.1:${DASHBOARD_PORT}/api/agents" 2>/dev/null || echo "") -if [ -n "$AGENTS_RESP" ]; then - pass "Agents API responds" - TOTAL=$((TOTAL + 1)) -else - # Some dashboard versions don't have /api/agents, test via CLI instead - if "$CLI_CMD" agents --port "$DASHBOARD_PORT" > /dev/null 2>&1; then - pass "Agents available via CLI (API may differ by dashboard version)" - TOTAL=$((TOTAL + 1)) - else - fail "Agents endpoint not available" - fi -fi - -# Test fleet/servers endpoint -FLEET=$(curl -s "http://127.0.0.1:${DASHBOARD_PORT}/api/fleet/servers" 2>/dev/null || echo "") -if [ -n "$FLEET" ]; then - pass "Fleet servers endpoint responds" - TOTAL=$((TOTAL + 1)) -else - skip "Fleet servers endpoint not available (dashboard may be published version)" -fi - -# Test fleet/stats endpoint -STATS=$(curl -s "http://127.0.0.1:${DASHBOARD_PORT}/api/fleet/stats" 2>/dev/null || echo "") -if [ -n "$STATS" ]; then - pass "Fleet stats endpoint responds" - TOTAL=$((TOTAL + 1)) -else - skip "Fleet stats endpoint not available (dashboard may be published version)" -fi - -# Test daemon socket is functional (CLI uses socket, which requires the daemon's PID file) -if [ -S "$PROJECT_DIR/.agentworkforce/relay/relay.sock" ]; then - pass "Daemon socket exists" - TOTAL=$((TOTAL + 1)) -else - fail "Daemon socket not found" -fi - -# Verify daemon PID file -if [ -f "$PROJECT_DIR/.agentworkforce/relay/relay.sock.pid" ]; then - pass "Daemon PID file exists" - TOTAL=$((TOTAL + 1)) -else - # CLI commands may not work without PID file but daemon itself is functional - skip "Daemon PID file not found (CLI status/agents may not work)" -fi - -# ------------------------------------------------------- -# Phase 7: Live Spawn Test (only with --full) -# ------------------------------------------------------- -if [ "$FULL_TEST" = true ]; then - phase "Phase 7: Live Agent Spawn/Release Test" - - if [ -z "${ANTHROPIC_API_KEY:-}" ]; then - skip "ANTHROPIC_API_KEY not set - skipping live spawn test" - else - AGENT_NAME="spawn-refactor-test-$$" - info "Spawning test agent '$AGENT_NAME'..." - - SPAWN_OUTPUT=$("$CLI_CMD" spawn "$AGENT_NAME" claude \ - "You are a test agent. Send a message to yourself saying 'SPAWN_TEST_OK' then wait." \ - --port "$DASHBOARD_PORT" 2>&1) || true - - if echo "$SPAWN_OUTPUT" | grep -qi "success\|spawned\|started"; then - pass "Spawn command succeeded for '$AGENT_NAME'" - TOTAL=$((TOTAL + 1)) - - # Wait for agent to register - info "Waiting for agent registration (max 60s)..." - REGISTERED=false - for i in $(seq 1 60); do - AGENTS=$("$CLI_CMD" agents --json --port "$DASHBOARD_PORT" 2>/dev/null | grep '^\[' || echo "[]") - if echo "$AGENTS" | jq -e --arg name "$AGENT_NAME" '.[] | select(.name == $name)' > /dev/null 2>&1; then - REGISTERED=true - pass "Agent '$AGENT_NAME' registered after ${i}s" - TOTAL=$((TOTAL + 1)) - break - fi - sleep 1 - done - - if [ "$REGISTERED" = false ]; then - fail "Agent '$AGENT_NAME' did not register within 60s" - fi - - # Verify fleet shows the agent - FLEET_AGENTS=$(curl -sf "http://127.0.0.1:${DASHBOARD_PORT}/api/fleet/servers" 2>/dev/null || echo "") - if echo "$FLEET_AGENTS" | grep -q "$AGENT_NAME"; then - pass "Fleet endpoint shows spawned agent" - TOTAL=$((TOTAL + 1)) - else - skip "Fleet endpoint does not show agent (may need time)" - fi - - # Release - info "Releasing agent '$AGENT_NAME'..." - "$CLI_CMD" release "$AGENT_NAME" --port "$DASHBOARD_PORT" 2>/dev/null || true - sleep 3 - - AGENTS_AFTER=$("$CLI_CMD" agents --json --port "$DASHBOARD_PORT" 2>/dev/null | grep '^\[' || echo "[]") - if ! echo "$AGENTS_AFTER" | jq -e --arg name "$AGENT_NAME" '.[] | select(.name == $name)' > /dev/null 2>&1; then - pass "Agent '$AGENT_NAME' released successfully" - TOTAL=$((TOTAL + 1)) - else - fail "Agent '$AGENT_NAME' still present after release" - fi - else - fail "Spawn command failed: $SPAWN_OUTPUT" - fi - fi -else - phase "Phase 7: Live Agent Spawn (skipped, use --full)" - skip "Use --full flag to test live agent spawn/release" -fi - -# ------------------------------------------------------- -# Cleanup -# ------------------------------------------------------- -phase "Cleanup" - -info "Stopping test daemon..." -kill $DAEMON_PID 2>/dev/null || true -sleep 1 -lsof -ti:$DASHBOARD_PORT | xargs kill -9 2>/dev/null || true -pass "Test daemon stopped" - -info "Restarting your daemon on default port (3888)..." -agent-relay up --dashboard > /dev/null 2>&1 & -sleep 3 -if curl -s "http://127.0.0.1:3888/health" > /dev/null 2>&1; then - info "Your daemon is back up on port 3888" -else - info "Daemon restart may still be in progress. Run: agent-relay up --dashboard" -fi - -# ------------------------------------------------------- -# Summary -# ------------------------------------------------------- -echo "" -echo -e "${CYAN}${BOLD}=================================================${NC}" -if [ $FAILURES -eq 0 ]; then - echo -e "${GREEN}${BOLD} ALL $TOTAL CHECKS PASSED${NC}" -else - echo -e "${RED}${BOLD} $FAILURES of $TOTAL CHECKS FAILED${NC}" -fi -echo -e "${CYAN}${BOLD}=================================================${NC}" -echo "" - -if [ $FAILURES -eq 0 ] && [ "$FULL_TEST" = false ]; then - echo -e "${YELLOW}Tip:${NC} Run with ${BOLD}--full${NC} to include live agent spawn/release test" - echo -e "${YELLOW}Tip:${NC} Run with ${BOLD}--checklist${NC} to see manual verification steps" -fi - -# Print checklist reminder -if [ "$FULL_TEST" = false ]; then - echo "" - echo -e "${BOLD}For manual testing, run:${NC}" - echo " ./scripts/test-spawn-refactor.sh --checklist" -fi - -exit $FAILURES diff --git a/scripts/watch-cli-tools.sh b/scripts/watch-cli-tools.sh index 149f16a97..a4d17ea9b 100755 --- a/scripts/watch-cli-tools.sh +++ b/scripts/watch-cli-tools.sh @@ -5,46 +5,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CLI_REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -CLI_TOOL="claude" -if [ -n "${npm_config_tool:-}" ]; then - CLI_TOOL="$npm_config_tool" -fi - -DASHBOARD_PORT=3888 -if [ -n "${npm_config_port:-}" ]; then - DASHBOARD_PORT="$npm_config_port" -fi - -PROJECT_DIR="/Users/khaliqgant/Projects/agent-workforce/test-broker-new" -if [ -n "${npm_config_project:-}" ]; then - PROJECT_DIR="$npm_config_project" -fi - -if [ ! -d "$PROJECT_DIR" ]; then - echo "Project directory not found: $PROJECT_DIR" - exit 1 -fi +PROJECT_DIR="${npm_config_project:-$PWD}" while [ "$#" -gt 0 ]; do case "$1" in - --tool=*) - CLI_TOOL="${1#*=}" - ;; - --tool) - shift - if [ "$#" -gt 0 ]; then - CLI_TOOL="$1" - fi - ;; - --port=*) - DASHBOARD_PORT="${1#*=}" - ;; - --port) - shift - if [ "$#" -gt 0 ]; then - DASHBOARD_PORT="$1" - fi - ;; --project=*) PROJECT_DIR="${1#*=}" ;; @@ -55,28 +19,21 @@ while [ "$#" -gt 0 ]; do fi ;; *) - if [ -z "$1" ]; then - : - elif [ "$1" = "--" ]; then - : - else - CLI_TOOL="$1" - fi + # Other flags (e.g. --tool) are accepted for compatibility but ignored. + : ;; esac shift done -if [ -z "${CLI_TOOL}" ]; then - CLI_TOOL="claude" +if [ ! -d "$PROJECT_DIR" ]; then + echo "Project directory not found: $PROJECT_DIR" + exit 1 fi export RUST_LOG=debug -export RELAY_DASHBOARD_STATIC_DIR=/Users/khaliqgant/Projects/agent-workforce/relay-dashboard/packages/dashboard/out export AGENT_RELAY_MCP_COMMAND="node dist/cli/agent-relay-mcp.js" -export AGENT_RELAY_BIN=/Users/khaliqgant/Projects/agent-workforce/relay-cli-uses-broker/target/debug/agent-relay-broker -export RELAY_DASHBOARD_BINARY=/Users/khaliqgant/Projects/agent-workforce/relay-dashboard/packages/dashboard-server/dist/start.js concurrently -k \ "(cd \"$CLI_REPO_DIR\" && npm run dev:watch)" \ - "cd \"$PROJECT_DIR\" && node --watch /Users/khaliqgant/Projects/agent-workforce/relay-cli-uses-broker/packages/cli/dist/cli/index.js start dashboard.js ${CLI_TOOL} --port ${DASHBOARD_PORT}" + "cd \"$PROJECT_DIR\" && node --watch \"$CLI_REPO_DIR/packages/cli/dist/cli/index.js\" up" diff --git a/tests/e2e/fleet/fleet-e2e.test.ts b/tests/e2e/fleet/fleet-e2e.test.ts index afcabc2a7..4e7721cca 100644 --- a/tests/e2e/fleet/fleet-e2e.test.ts +++ b/tests/e2e/fleet/fleet-e2e.test.ts @@ -106,7 +106,7 @@ describe.skipIf(!pre.ok)('two-node fleet scenario matrix', () => { engineBaseUrl: engine.baseUrl, brokerBinary: pre.brokerBinary!, tmpRoot, - dashboardPort: await getFreePort(), + brokerPort: await getFreePort(), }); nodeB = new FleetNode({ name: 'node-b', @@ -117,7 +117,7 @@ describe.skipIf(!pre.ok)('two-node fleet scenario matrix', () => { engineBaseUrl: engine.baseUrl, brokerBinary: pre.brokerBinary!, tmpRoot, - dashboardPort: await getFreePort(), + brokerPort: await getFreePort(), }); nodeA.start(); nodeB.start(); @@ -170,7 +170,7 @@ describe.skipIf(!pre.ok)('two-node fleet scenario matrix', () => { engineBaseUrl: engine.baseUrl, brokerBinary: pre.brokerBinary!, tmpRoot, - dashboardPort: await getFreePort(), + brokerPort: await getFreePort(), }); badNode.start(); try { diff --git a/tests/e2e/fleet/harness.ts b/tests/e2e/fleet/harness.ts index 5b783cbcf..fde208263 100644 --- a/tests/e2e/fleet/harness.ts +++ b/tests/e2e/fleet/harness.ts @@ -277,7 +277,7 @@ export class FleetNode { engineBaseUrl: string; brokerBinary: string; tmpRoot: string; - dashboardPort: number; + brokerPort: number; } ) { this.projectDir = path.join(opts.tmpRoot, `node-${opts.name}`); @@ -331,7 +331,7 @@ export class FleetNode { RELAY_API_KEY: o.workspaceKey, AGENT_RELAY_PROJECT: this.projectDir, AGENT_RELAY_STATE_DIR: stateDir, - AGENT_RELAY_DASHBOARD_PORT: String(o.dashboardPort), + AGENT_RELAY_BROKER_PORT: String(o.brokerPort), }), stdio: ['ignore', 'pipe', 'pipe'], } diff --git a/web/content/docs/cli-broker-lifecycle.mdx b/web/content/docs/cli-broker-lifecycle.mdx index 190ceda9d..8ee0ffb51 100644 --- a/web/content/docs/cli-broker-lifecycle.mdx +++ b/web/content/docs/cli-broker-lifecycle.mdx @@ -1,9 +1,9 @@ --- title: 'Broker lifecycle' -description: 'Start, inspect, and stop the optional local broker runtime used for managed CLI agents and the local dashboard.' +description: 'Start, inspect, and stop the optional local broker runtime used for managed CLI agents.' --- -The version 8 core product is workspace messaging. The local broker is optional: use it when this machine should run managed CLI agents, expose a local dashboard, or attach to PTY/headless sessions. +The version 8 core product is workspace messaging. The local broker is optional: use it when this machine should run managed CLI agents or attach to PTY/headless sessions. Broker commands live under `agent-relay local`. @@ -17,12 +17,9 @@ Useful flags: | Flag | Description | | --- | --- | -| `--no-dashboard` | Start the broker without the web dashboard. | -| `--port ` | Dashboard port. The broker API uses the next available port. | | `--spawn` | Force auto-spawn from local team config. | | `--no-spawn` | Start only the broker. | -| `--background` | Detach and leave the broker running. | -| `--foreground` | Keep a no-dashboard broker attached to this terminal. | +| `--background` | Detach and leave the broker running. The default is to stay attached to this terminal. | | `--workspace-key ` | Join a pre-existing Relay workspace. | | `--state-dir ` | Write runtime state outside `.agentworkforce/relay/`. | | `--broker-name ` | Override the broker identity. | diff --git a/web/content/docs/cli-overview.mdx b/web/content/docs/cli-overview.mdx index 087b01316..f68719006 100644 --- a/web/content/docs/cli-overview.mdx +++ b/web/content/docs/cli-overview.mdx @@ -114,7 +114,7 @@ agent-relay local agent release reviewer agent-relay local down ``` -The local runtime is optional. It manages a broker process, dashboard, PTY/headless agents, attach modes, workflow logs, and release for CLI agents running on this machine. Local workflow runs execute Relayflows YAML, TypeScript, and Python workflows in the current checkout and keep metadata under `.agentworkforce/relay/local-runs`. +The local runtime is optional. It manages a broker process, PTY/headless agents, attach modes, workflow logs, and release for CLI agents running on this machine. Local workflow runs execute Relayflows YAML, TypeScript, and Python workflows in the current checkout and keep metadata under `.agentworkforce/relay/local-runs`. ## Composite Status And Maintenance diff --git a/web/content/docs/reference-cli.mdx b/web/content/docs/reference-cli.mdx index 119c54f60..1e1009a2c 100644 --- a/web/content/docs/reference-cli.mdx +++ b/web/content/docs/reference-cli.mdx @@ -127,7 +127,7 @@ The SDK-backed groups are `agent`, `channel`, `message`, `integration`, and `cap | Command | Description | | --- | --- | -| `agent-relay local up [flags]` | Start the local broker and optional dashboard. | +| `agent-relay local up [flags]` | Start the local broker. | | `agent-relay local status [--state-dir ] [--wait-for ]` | Check local broker daemon state. | | `agent-relay local metrics [--agent ]` | Show local broker and agent resource usage. | | `agent-relay local run [--file-type ]` | Start a local Relayflows workflow file in the background. | From 9015c50dc3f6e78faa9cd7a2c399f8bf72efd6f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jun 2026 18:54:00 +0000 Subject: [PATCH 2/6] style: auto-format with Prettier --- packages/cli/src/cli/commands/core.test.ts | 28 +++++++------------- packages/cli/src/cli/lib/broker-lifecycle.ts | 6 +---- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/cli/commands/core.test.ts b/packages/cli/src/cli/commands/core.test.ts index 58a0c6005..dd59a0634 100644 --- a/packages/cli/src/cli/commands/core.test.ts +++ b/packages/cli/src/cli/commands/core.test.ts @@ -334,15 +334,11 @@ describe('registerCoreCommands', () => { const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(0); - expect(deps.spawnProcess).toHaveBeenCalledWith( - '/usr/bin/node', - ['/tmp/agent-relay.js', 'up'], - { - detached: true, - stdio: 'ignore', - env: deps.env, - } - ); + expect(deps.spawnProcess).toHaveBeenCalledWith('/usr/bin/node', ['/tmp/agent-relay.js', 'up'], { + detached: true, + stdio: 'ignore', + env: deps.env, + }); expect(spawnedProcess.unref).toHaveBeenCalled(); expect(sleepImpl).toHaveBeenCalledWith(500); expect(sdkStatusClient.getStatus).toHaveBeenCalledTimes(1); @@ -446,15 +442,11 @@ describe('registerCoreCommands', () => { const exitCode = await runCommand(program, ['up', '--background']); expect(exitCode).toBe(0); - expect(deps.spawnProcess).toHaveBeenCalledWith( - '/tmp/agent-relay-darwin-arm64', - ['up'], - { - detached: true, - stdio: 'ignore', - env: deps.env, - } - ); + expect(deps.spawnProcess).toHaveBeenCalledWith('/tmp/agent-relay-darwin-arm64', ['up'], { + detached: true, + stdio: 'ignore', + env: deps.env, + }); }); it('up --background exits non-zero when the detached broker never becomes ready', async () => { diff --git a/packages/cli/src/cli/lib/broker-lifecycle.ts b/packages/cli/src/cli/lib/broker-lifecycle.ts index a3b012820..52982a8b6 100644 --- a/packages/cli/src/cli/lib/broker-lifecycle.ts +++ b/packages/cli/src/cli/lib/broker-lifecycle.ts @@ -689,11 +689,7 @@ async function waitForBrokerReadiness( return latest; } -async function shutdownUpResources( - relay: CoreRelay, - dataDir: string, - deps: CoreDependencies -): Promise { +async function shutdownUpResources(relay: CoreRelay, dataDir: string, deps: CoreDependencies): Promise { await relay.shutdown().catch(() => undefined); safeUnlink(path.join(dataDir, CONNECTION_FILENAME), deps); } From 7251862b9f7758b873353125ac21cddf5f4835fe Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sat, 20 Jun 2026 13:55:35 -0400 Subject: [PATCH 3/6] fix(ci): keep `up` resilient when the implicit fleet node can't start Two CI failures on the dashboard-removal branch: - package-validation "Build & Validate" called `local up --port`, a flag removed with the dashboard. Route the test's port through AGENT_RELAY_BROKER_PORT and start a plain `local up` instead. - Standalone macOS Smoke crashed: `defineDefaultLocalNode is not a function`. bun's `--compile` resolves @agent-relay/fleet's value exports to its type-only .d.ts, so the implicit fleet node helpers are undefined in the standalone binary. This is pre-existing; it was hidden because `up --no-dashboard` previously ran the fleet sidecar in a detached child. Attached `up` surfaced it. The implicit local fleet node is best-effort, so wrap its startup and warn-and-continue instead of aborting the broker. Also drop the now-dead "Download dashboard binary" step from e2e-tests.yml. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e-tests.yml | 60 -------------------- .github/workflows/package-validation.yml | 10 ++-- packages/cli/src/cli/lib/broker-lifecycle.ts | 34 ++++++----- 3 files changed, 26 insertions(+), 78 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 936e7b64e..270ae933e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -85,66 +85,6 @@ jobs: export PATH="$HOME/.npm-global/bin:$PATH" claude --version || echo "Claude CLI ready" - - name: Download dashboard binary - run: | - # Determine platform and architecture - if [ "${{ runner.os }}" = "Linux" ]; then - BINARY_NAME="relay-dashboard-server-linux-x64" - elif [ "${{ runner.os }}" = "macOS" ]; then - # Check architecture - if [ "$(uname -m)" = "arm64" ]; then - BINARY_NAME="relay-dashboard-server-darwin-arm64" - else - BINARY_NAME="relay-dashboard-server-darwin-x64" - fi - else - echo "Unsupported OS: ${{ runner.os }}" - exit 1 - fi - - echo "Downloading dashboard binary: $BINARY_NAME" - - # Get latest release from relay-dashboard repo - RELEASE_URL="https://github.com/AgentWorkforce/relay-dashboard/releases/latest/download/${BINARY_NAME}.gz" - echo "URL: $RELEASE_URL" - - # Install to user local bin (no sudo required) - mkdir -p ~/.local/bin - - # Download with retry logic (transient 504s from GitHub releases are common) - MAX_RETRIES=3 - RETRY_DELAY=5 - for attempt in $(seq 1 $MAX_RETRIES); do - echo "Download attempt $attempt of $MAX_RETRIES..." - if curl -fsSL --retry 3 --retry-delay 2 "$RELEASE_URL" | gunzip > ~/.local/bin/relay-dashboard-server 2>/dev/null; then - echo "Download succeeded on attempt $attempt" - break - fi - if [ "$attempt" -eq "$MAX_RETRIES" ]; then - echo "::warning::Failed to download dashboard binary after $MAX_RETRIES attempts — continuing without it" - rm -f ~/.local/bin/relay-dashboard-server - exit 0 - fi - echo "Attempt $attempt failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - RETRY_DELAY=$((RETRY_DELAY * 2)) - done - - if [ -f ~/.local/bin/relay-dashboard-server ]; then - chmod +x ~/.local/bin/relay-dashboard-server - fi - - # Add to PATH for this workflow - echo "$HOME/.local/bin" >> $GITHUB_PATH - - # Verify - if [ -f ~/.local/bin/relay-dashboard-server ]; then - echo "Installed dashboard binary:" - ~/.local/bin/relay-dashboard-server --version || echo "Binary installed (version check may not be supported)" - else - echo "Dashboard binary not available — tests will run without it" - fi - - name: Check for API key id: check-key run: | diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index 8a0ec604e..22a18242b 100644 --- a/.github/workflows/package-validation.yml +++ b/.github/workflows/package-validation.yml @@ -192,13 +192,13 @@ jobs: - name: Test CLI startup run: | echo "=== Testing CLI broker lifecycle ===" - TEST_PORT=3899 - API_PORT=$((TEST_PORT + 1)) - # Start broker+dashboard in background - node packages/cli/dist/cli/index.js local up --port "$TEST_PORT" & + BROKER_PORT=3899 + API_PORT=$((BROKER_PORT + 1)) + # Start the broker in background + AGENT_RELAY_BROKER_PORT="$BROKER_PORT" node packages/cli/dist/cli/index.js local up & DAEMON_PID=$! - # Wait for health endpoint (broker API is dashboard port + 1) + # Wait for health endpoint (broker API is the base port + 1) for i in {1..20}; do if curl -sf "http://127.0.0.1:${API_PORT}/health" > /dev/null; then break diff --git a/packages/cli/src/cli/lib/broker-lifecycle.ts b/packages/cli/src/cli/lib/broker-lifecycle.ts index 52982a8b6..7697a9079 100644 --- a/packages/cli/src/cli/lib/broker-lifecycle.ts +++ b/packages/cli/src/cli/lib/broker-lifecycle.ts @@ -255,19 +255,27 @@ function startImplicitLocalFleetSidecar( deps.warn('Fleet local node skipped: broker connection file was not available.'); return undefined; } - const node = createImplicitLocalFleetNode({ - paths, - teamsConfig, - name: options.brokerName ?? (path.basename(paths.projectRoot) || 'local-node'), - }); - return startFleetSidecar({ - definition: node, - connection: { url: conn.url, apiKey: conn.api_key }, - workspaceKey: relay.workspaceKey, - statusPath: fleetStatusPath(paths), - reconnect: true, - warn: (message) => deps.warn(message), - }); + // The implicit local fleet node is best-effort: it lets this broker advertise + // itself as a fleet node, but the broker is already up and usable without it. + // Never let a sidecar setup failure abort `up`. + try { + const node = createImplicitLocalFleetNode({ + paths, + teamsConfig, + name: options.brokerName ?? (path.basename(paths.projectRoot) || 'local-node'), + }); + return startFleetSidecar({ + definition: node, + connection: { url: conn.url, apiKey: conn.api_key }, + workspaceKey: relay.workspaceKey, + statusPath: fleetStatusPath(paths), + reconnect: true, + warn: (message) => deps.warn(message), + }); + } catch (err) { + deps.warn(`Fleet local node skipped: ${toErrorMessage(err)}`); + return undefined; + } } function isBrokerAlreadyRunningError(message: string): boolean { From ca9db68d3779afc3973a97f0cf1dd1b950d102f2 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 21 Jun 2026 00:18:31 -0400 Subject: [PATCH 4/6] chore: remove relay dashboard from installer and stale refs - install.sh: drop the dashboard-server binary download, dashboard UI download, `npm install -g @agent-relay/dashboard-server`, the AGENT_RELAY_NO_DASHBOARD env, the REPO_DASHBOARD repo, and the `up --dashboard` post-install help. The installer no longer installs the removed local dashboard. - Fix broken `--no-dashboard` / `up --dashboard` examples in using-agent-relay skill docs and the demos README; rename "dashboard port" -> "broker port" in the fleet e2e README; drop the stale run-dashboard.js line from .npmignore. Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/skills/using-agent-relay/SKILL.md | 2 +- .claude/skills/using-agent-relay/SKILL.md | 2 +- .npmignore | 1 - install.sh | 170 +--------------------- scripts/demos/README.md | 3 +- tests/e2e/fleet/README.md | 2 +- web/content/agent-relay/SKILL.md | 2 +- 7 files changed, 8 insertions(+), 174 deletions(-) diff --git a/.agents/skills/using-agent-relay/SKILL.md b/.agents/skills/using-agent-relay/SKILL.md index 6f66141e6..ef663ecca 100644 --- a/.agents/skills/using-agent-relay/SKILL.md +++ b/.agents/skills/using-agent-relay/SKILL.md @@ -194,7 +194,7 @@ operations: ```bash agent-relay status -agent-relay local up --no-dashboard --verbose +agent-relay local up --verbose agent-relay local status --wait-for 10 agent-relay local agent list agent-relay local agent spawn claude --name Worker --task "Use https://agentrelay.com/skill and ACK over Relay." diff --git a/.claude/skills/using-agent-relay/SKILL.md b/.claude/skills/using-agent-relay/SKILL.md index 6f66141e6..ef663ecca 100644 --- a/.claude/skills/using-agent-relay/SKILL.md +++ b/.claude/skills/using-agent-relay/SKILL.md @@ -194,7 +194,7 @@ operations: ```bash agent-relay status -agent-relay local up --no-dashboard --verbose +agent-relay local up --verbose agent-relay local status --wait-for 10 agent-relay local agent list agent-relay local agent spawn claude --name Worker --task "Use https://agentrelay.com/skill and ACK over Relay." diff --git a/.npmignore b/.npmignore index 69244ceb3..bad11dd4b 100644 --- a/.npmignore +++ b/.npmignore @@ -103,7 +103,6 @@ tsconfig.tsbuildinfo packages/*/*.tsbuildinfo # Other dev files -run-dashboard.js CLAUDE.md AGENTS.md *.tasks.md diff --git a/install.sh b/install.sh index 6748d379c..f43a082c4 100755 --- a/install.sh +++ b/install.sh @@ -8,11 +8,9 @@ set -e # AGENT_RELAY_VERSION - Specific version to install (default: latest) # AGENT_RELAY_INSTALL_DIR - Installation directory (default: ~/.agentworkforce/relay) # AGENT_RELAY_BIN_DIR - Binary directory (default: ~/.local/bin) -# AGENT_RELAY_NO_DASHBOARD - Skip dashboard installation (default: false) # AGENT_RELAY_TELEMETRY_DISABLED - Disable anonymous install telemetry (default: false) REPO_RELAY="AgentWorkforce/relay" -REPO_DASHBOARD="AgentWorkforce/relay-dashboard" VERSION="${AGENT_RELAY_VERSION:-latest}" INSTALL_DIR="${AGENT_RELAY_INSTALL_DIR:-$HOME/.agentworkforce/relay}" BIN_DIR="${AGENT_RELAY_BIN_DIR:-$HOME/.local/bin}" @@ -245,148 +243,6 @@ download_broker_binary() { fi } -# Download standalone dashboard-server binary -download_dashboard_binary() { - if [ "${AGENT_RELAY_NO_DASHBOARD}" = "true" ]; then - info "Skipping dashboard installation (AGENT_RELAY_NO_DASHBOARD=true)" - return 0 - fi - - step "Downloading dashboard-server binary..." - - local binary_name="relay-dashboard-server-${PLATFORM}" - local compressed_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/${binary_name}.gz" - local uncompressed_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/${binary_name}" - local target_path="$BIN_DIR/relay-dashboard-server" - local temp_file="/tmp/dashboard-download-$$" - - mkdir -p "$BIN_DIR" - - # Setup cleanup trap for temp files - trap 'rm -f "${temp_file}.gz" "${temp_file}"' EXIT - - # Try compressed binary first (faster download) - if has_command gunzip; then - info "Trying compressed dashboard binary..." - - if curl -fsSL "$compressed_url" -o "${temp_file}.gz" 2>/dev/null; then - # Check if we got a valid gzip file - local is_gzip=false - if has_command file; then - file "${temp_file}.gz" 2>/dev/null | grep -q "gzip" && is_gzip=true - else - head -c 2 "${temp_file}.gz" 2>/dev/null | od -An -tx1 | grep -q "1f 8b" && is_gzip=true - fi - - if [ "$is_gzip" = true ]; then - if gunzip -c "${temp_file}.gz" > "$target_path" 2>/dev/null; then - rm -f "${temp_file}.gz" - chmod +x "$target_path" - strip_quarantine "$target_path" - - if "$target_path" --version &>/dev/null; then - success "Downloaded standalone dashboard-server binary" - trap - EXIT - return 0 - else - warn "Dashboard binary failed verification, trying uncompressed..." - rm -f "$target_path" - fi - else - rm -f "${temp_file}.gz" "$target_path" - fi - else - rm -f "${temp_file}.gz" - fi - fi - fi - - # Fall back to uncompressed binary - info "Trying uncompressed dashboard binary..." - - if curl -fsSL "$uncompressed_url" -o "$target_path" 2>/dev/null; then - local file_size - file_size=$(stat -f%z "$target_path" 2>/dev/null || stat -c%s "$target_path" 2>/dev/null || echo "0") - - if [ "$file_size" -gt 1000000 ]; then - chmod +x "$target_path" - strip_quarantine "$target_path" - - if "$target_path" --version &>/dev/null; then - success "Downloaded standalone dashboard-server binary" - trap - EXIT - return 0 - else - warn "Dashboard binary failed verification" - rm -f "$target_path" - fi - else - rm -f "$target_path" - fi - fi - - trap - EXIT - info "No standalone dashboard binary available for $PLATFORM" - return 1 -} - -# Download dashboard UI files (required for standalone binary) -download_dashboard_ui() { - if [ "${AGENT_RELAY_NO_DASHBOARD}" = "true" ]; then - return 0 - fi - - step "Downloading dashboard UI files..." - - local ui_url="https://github.com/$REPO_DASHBOARD/releases/latest/download/dashboard-ui.tar.gz" - local target_dir="$INSTALL_DIR/dashboard" - local temp_file="/tmp/dashboard-ui-$$" - - mkdir -p "$target_dir" - - # Setup cleanup trap for temp files - trap 'rm -f "${temp_file}.tar.gz"' EXIT - - if curl -fsSL "$ui_url" -o "${temp_file}.tar.gz" 2>/dev/null; then - # Check if we got a valid gzip file - local is_gzip=false - if has_command file; then - file "${temp_file}.tar.gz" 2>/dev/null | grep -q "gzip" && is_gzip=true - else - head -c 2 "${temp_file}.tar.gz" 2>/dev/null | od -An -tx1 | grep -q "1f 8b" && is_gzip=true - fi - - if [ "$is_gzip" = true ]; then - # Remove old UI files if they exist - rm -rf "$target_dir/out" - - # Extract to target directory - if tar -xzf "${temp_file}.tar.gz" -C "$target_dir" 2>/dev/null; then - rm -f "${temp_file}.tar.gz" - trap - EXIT - - # Verify extraction - if [ -f "$target_dir/out/index.html" ]; then - success "Downloaded dashboard UI files" - return 0 - else - warn "Dashboard UI extraction incomplete" - return 1 - fi - else - warn "Failed to extract dashboard UI" - rm -f "${temp_file}.tar.gz" - fi - else - rm -f "${temp_file}.tar.gz" - fi - fi - - trap - EXIT - info "Dashboard UI files not available (dashboard API will still work)" - return 1 -} - # Check if a command exists has_command() { command -v "$1" &> /dev/null @@ -712,18 +568,6 @@ Or use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/instal prepend_bin_dir_to_path fi - # Install dashboard if not skipped - if [ "${AGENT_RELAY_NO_DASHBOARD}" != "true" ]; then - # Try binary first, fall back to npm - if download_dashboard_binary; then - # Binary downloaded - also need UI files since they're not embedded - download_dashboard_ui || true - else - info "Installing dashboard via npm..." - npm install -g @agent-relay/dashboard-server 2>/dev/null || true - fi - fi - # Install ACP bridge for Zed editor integration install_acp_bridge || true @@ -840,16 +684,13 @@ print_usage() { echo "" echo -e "${BOLD}Quick Start:${NC}" echo "" - echo " # Start the daemon with dashboard" - echo " agent-relay up --dashboard" + echo " # Start the local broker" + echo " agent-relay up" echo "" echo " # Check status" echo " agent-relay status" echo "" - echo " # Open dashboard" - echo " open http://localhost:3888" - echo "" - echo " # Stop daemon" + echo " # Stop the broker" echo " agent-relay down" echo "" echo -e "${BOLD}Documentation:${NC} https://github.com/AgentWorkforce/relay" @@ -881,10 +722,6 @@ main() { INSTALL_METHOD="binary" # Download broker binary for workflow/SDK agent spawning download_broker_binary || true - # Download dashboard-server binary if available - download_dashboard_binary || true - # Download dashboard UI files (required for standalone binary to serve the UI) - download_dashboard_ui || true # Install ACP bridge for Zed editor (requires Node.js) install_acp_bridge || true verify_installation && print_usage && track_event "install_completed" && exit 0 @@ -968,7 +805,6 @@ case "${1:-}" in echo " AGENT_RELAY_VERSION Specific version to install (default: latest)" echo " AGENT_RELAY_INSTALL_DIR Installation directory (default: ~/.agentworkforce/relay)" echo " AGENT_RELAY_BIN_DIR Binary directory (default: ~/.local/bin)" - echo " AGENT_RELAY_NO_DASHBOARD Skip dashboard installation (default: false)" echo " AGENT_RELAY_TELEMETRY_DISABLED Disable anonymous install telemetry (default: false)" echo "" echo "Telemetry: This installer collects anonymous usage data to improve the product." diff --git a/scripts/demos/README.md b/scripts/demos/README.md index a6394f4b0..bc418c171 100644 --- a/scripts/demos/README.md +++ b/scripts/demos/README.md @@ -44,7 +44,7 @@ claude --version ### Step 1: Start Relay Daemon ```bash -agent-relay up --dashboard +agent-relay up ``` Open http://localhost:3888 to watch the conversation. @@ -75,5 +75,4 @@ agent-relay -n Analytics claude ## What You'll See -- **Dashboard**: Agents connect, messages flow in real-time - **Terminals**: Agents negotiate, propose allocations, vote on outcomes diff --git a/tests/e2e/fleet/README.md b/tests/e2e/fleet/README.md index c199ef821..7e022d31f 100644 --- a/tests/e2e/fleet/README.md +++ b/tests/e2e/fleet/README.md @@ -69,7 +69,7 @@ full matrix; the matrix itself is ~30s, the wall-clock is build-dominated. Each `fleet serve` runs in a hermetic env: all ambient `RELAY_*` / `AGENT_RELAY_*` vars are stripped (so the broker never rejoins the operator's real workspace), with -its own `HOME`, project dir, state dir, and dashboard port. The broker reads its +its own `HOME`, project dir, state dir, and broker port. The broker reads its node id from `/agent-relay/machine-id`, which the harness pre-seeds to match the enrolled node id (otherwise `node.register` is rejected `node_id_mismatch`). `FleetNode.stop()` kills the broker child too (by its diff --git a/web/content/agent-relay/SKILL.md b/web/content/agent-relay/SKILL.md index 275bbd829..bdd5e9685 100644 --- a/web/content/agent-relay/SKILL.md +++ b/web/content/agent-relay/SKILL.md @@ -69,7 +69,7 @@ and `join_channel`. 1. Start Relay from the project root: ```bash - agent-relay local up --no-dashboard --verbose + agent-relay local up --verbose agent-relay local status --wait-for 10 ``` From bbb86881e3e80164231186911f7d8ce786e55a52 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 21 Jun 2026 00:24:40 -0400 Subject: [PATCH 5/6] refactor(broker): retire ActionSource::HumanDashboard telemetry variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local dashboard is gone, and the TS `ActionSource` union already dropped `human_dashboard`. Re-sync the Rust mirror: - Remove the `HumanDashboard` enum variant + its `as_str` arm so the broker's taxonomy matches TS (`human_cli | agent | protocol`). - Re-point the only emitter — the `/api/spawn` HTTP handler — to `ActionSource::HumanCli`. That endpoint's human caller is now the CLI (the dashboard GUI that previously posted to it is gone). - Update the telemetry unit tests. Shared broker infrastructure that is merely *named* "dashboard" (the `/ws` SDK event transport, `display_target_for_dashboard`, `sender_is_dashboard_label`, the operator "Dashboard" label) is left intact — it powers the SDK/CLI event stream and the relaycast/Observer message path, not the removed local UI. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/broker/src/runtime/api.rs | 4 +++- crates/broker/src/telemetry.rs | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/broker/src/runtime/api.rs b/crates/broker/src/runtime/api.rs index b23edb9ee..70b1675e6 100644 --- a/crates/broker/src/runtime/api.rs +++ b/crates/broker/src/runtime/api.rs @@ -278,7 +278,9 @@ impl BrokerRuntime { telemetry.track(TelemetryEvent::AgentSpawn { cli: cli.clone(), runtime: runtime_label(&effective_spec.runtime).to_string(), - spawn_source: ActionSource::HumanDashboard, + // `/api/spawn` is the HTTP entry point a human drives + // through the CLI (the broker's only human caller). + spawn_source: ActionSource::HumanCli, has_task: effective_task.is_some(), is_shadow: effective_spec.shadow_of.is_some() || effective_spec.shadow_mode.is_some(), diff --git a/crates/broker/src/telemetry.rs b/crates/broker/src/telemetry.rs index ba6882364..3c08ef06e 100644 --- a/crates/broker/src/telemetry.rs +++ b/crates/broker/src/telemetry.rs @@ -98,7 +98,6 @@ pub enum TelemetryEvent { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ActionSource { HumanCli, - HumanDashboard, Agent, Protocol, } @@ -107,7 +106,6 @@ impl ActionSource { fn as_str(&self) -> &'static str { match self { Self::HumanCli => "human_cli", - Self::HumanDashboard => "human_dashboard", Self::Agent => "agent", Self::Protocol => "protocol", } @@ -821,7 +819,6 @@ mod tests { #[test] fn action_source_serializes_to_snake_case_strings() { assert_eq!(ActionSource::HumanCli.as_str(), "human_cli"); - assert_eq!(ActionSource::HumanDashboard.as_str(), "human_dashboard"); assert_eq!(ActionSource::Agent.as_str(), "agent"); assert_eq!(ActionSource::Protocol.as_str(), "protocol"); } @@ -831,14 +828,14 @@ mod tests { let event = TelemetryEvent::AgentSpawn { cli: "claude".into(), runtime: "pty".into(), - spawn_source: ActionSource::HumanDashboard, + spawn_source: ActionSource::HumanCli, has_task: true, is_shadow: false, }; let props = event.properties(); assert_eq!(props["cli"], "claude"); assert_eq!(props["runtime"], "pty"); - assert_eq!(props["spawn_source"], "human_dashboard"); + assert_eq!(props["spawn_source"], "human_cli"); assert_eq!(props["has_task"], true); assert_eq!(props["is_shadow"], false); } From b5605f225d3d81d9d455990014148214ff72fc54 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 21 Jun 2026 00:59:10 -0400 Subject: [PATCH 6/6] fix(install): use `up --background` in quick-start so the flow is runnable `agent-relay up` runs attached by default, which blocks the terminal and stops the documented `status`/`down` steps from running. Use `--background` so the quick-start sequence is executable as printed. Co-Authored-By: Claude Opus 4.8 (1M context) --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index f43a082c4..18e236a43 100755 --- a/install.sh +++ b/install.sh @@ -684,8 +684,8 @@ print_usage() { echo "" echo -e "${BOLD}Quick Start:${NC}" echo "" - echo " # Start the local broker" - echo " agent-relay up" + echo " # Start the local broker (detached so this terminal stays free)" + echo " agent-relay up --background" echo "" echo " # Check status" echo " agent-relay status"