agent-relay 2.0.29 → 2.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/index.cjs +85691 -0
- package/dist/src/bridge/index.d.ts.map +1 -0
- package/dist/src/bridge/index.js.map +1 -0
- package/dist/src/cli/commands/doctor.d.ts +2 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +451 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +29 -1
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/config/relay-config.d.ts.map +1 -0
- package/dist/src/config/relay-config.js.map +1 -0
- package/dist/src/continuity/index.d.ts.map +1 -0
- package/dist/src/continuity/index.js.map +1 -0
- package/dist/src/daemon/index.d.ts.map +1 -0
- package/dist/src/daemon/index.js.map +1 -0
- package/dist/src/health-worker-manager.d.ts.map +1 -0
- package/dist/src/health-worker-manager.js.map +1 -0
- package/dist/src/health-worker.d.ts.map +1 -0
- package/dist/src/health-worker.js.map +1 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/memory/index.d.ts.map +1 -0
- package/dist/src/memory/index.js.map +1 -0
- package/dist/src/policy/index.d.ts.map +1 -0
- package/dist/src/policy/index.js.map +1 -0
- package/dist/src/protocol/index.d.ts.map +1 -0
- package/dist/src/protocol/index.js.map +1 -0
- package/dist/src/resiliency/index.d.ts.map +1 -0
- package/dist/src/resiliency/index.js.map +1 -0
- package/dist/src/shared/cli-auth-config.d.ts.map +1 -0
- package/dist/src/shared/cli-auth-config.js.map +1 -0
- package/dist/src/state/index.d.ts.map +1 -0
- package/dist/src/state/index.js.map +1 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/trajectory/index.d.ts.map +1 -0
- package/dist/src/trajectory/index.js.map +1 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/wrapper/index.d.ts.map +1 -0
- package/dist/src/wrapper/index.js.map +1 -0
- package/package.json +83 -20
- package/packages/api-types/dist/index.d.ts.map +1 -0
- package/packages/api-types/dist/index.js.map +1 -0
- package/packages/api-types/dist/schemas/agent.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/agent.js.map +1 -0
- package/packages/api-types/dist/schemas/api.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/api.js.map +1 -0
- package/packages/api-types/dist/schemas/decision.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/decision.js.map +1 -0
- package/packages/api-types/dist/schemas/fleet.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/fleet.js.map +1 -0
- package/packages/api-types/dist/schemas/history.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/history.js.map +1 -0
- package/packages/api-types/dist/schemas/index.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/index.js.map +1 -0
- package/packages/api-types/dist/schemas/message.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/message.js.map +1 -0
- package/packages/api-types/dist/schemas/session.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/session.js.map +1 -0
- package/packages/api-types/dist/schemas/task.d.ts.map +1 -0
- package/packages/api-types/dist/schemas/task.js.map +1 -0
- package/packages/api-types/package.json +1 -1
- package/packages/api-types/src/index.ts +22 -0
- package/packages/api-types/src/schemas/agent.test.ts +164 -0
- package/packages/api-types/src/schemas/agent.ts +110 -0
- package/packages/api-types/src/schemas/api.test.ts +372 -0
- package/packages/api-types/src/schemas/api.ts +194 -0
- package/packages/api-types/src/schemas/decision.test.ts +324 -0
- package/packages/api-types/src/schemas/decision.ts +136 -0
- package/packages/api-types/src/schemas/fleet.test.ts +212 -0
- package/packages/api-types/src/schemas/fleet.ts +83 -0
- package/packages/api-types/src/schemas/history.test.ts +242 -0
- package/packages/api-types/src/schemas/history.ts +84 -0
- package/packages/api-types/src/schemas/index.ts +148 -0
- package/packages/api-types/src/schemas/message.test.ts +192 -0
- package/packages/api-types/src/schemas/message.ts +98 -0
- package/packages/api-types/src/schemas/session.test.ts +104 -0
- package/packages/api-types/src/schemas/session.ts +40 -0
- package/packages/api-types/src/schemas/task.test.ts +192 -0
- package/packages/api-types/src/schemas/task.ts +78 -0
- package/packages/api-types/tsconfig.json +19 -0
- package/packages/api-types/vitest.config.ts +9 -0
- package/packages/benchmark/README.md +200 -0
- package/packages/benchmark/datasets/coding-tasks.yaml +127 -0
- package/packages/benchmark/datasets/coordination-tasks.yaml +122 -0
- package/packages/benchmark/dist/benchmark.d.ts +47 -0
- package/packages/benchmark/dist/benchmark.d.ts.map +1 -0
- package/packages/benchmark/dist/benchmark.js +224 -0
- package/packages/benchmark/dist/benchmark.js.map +1 -0
- package/packages/benchmark/dist/cli.d.ts +8 -0
- package/packages/benchmark/dist/cli.d.ts.map +1 -0
- package/packages/benchmark/dist/cli.js +185 -0
- package/packages/benchmark/dist/cli.js.map +1 -0
- package/packages/benchmark/dist/harbor.d.ts +53 -0
- package/packages/benchmark/dist/harbor.d.ts.map +1 -0
- package/packages/benchmark/dist/harbor.js +127 -0
- package/packages/benchmark/dist/harbor.js.map +1 -0
- package/packages/benchmark/dist/index.d.ts +48 -0
- package/packages/benchmark/dist/index.d.ts.map +1 -0
- package/packages/benchmark/dist/index.js +50 -0
- package/packages/benchmark/dist/index.js.map +1 -0
- package/packages/benchmark/dist/runners/base.d.ts +63 -0
- package/packages/benchmark/dist/runners/base.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/base.js +155 -0
- package/packages/benchmark/dist/runners/base.js.map +1 -0
- package/packages/benchmark/dist/runners/index.d.ts +10 -0
- package/packages/benchmark/dist/runners/index.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/index.js +10 -0
- package/packages/benchmark/dist/runners/index.js.map +1 -0
- package/packages/benchmark/dist/runners/single.d.ts +19 -0
- package/packages/benchmark/dist/runners/single.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/single.js +111 -0
- package/packages/benchmark/dist/runners/single.js.map +1 -0
- package/packages/benchmark/dist/runners/subagent.d.ts +32 -0
- package/packages/benchmark/dist/runners/subagent.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/subagent.js +212 -0
- package/packages/benchmark/dist/runners/subagent.js.map +1 -0
- package/packages/benchmark/dist/runners/swarm.d.ts +36 -0
- package/packages/benchmark/dist/runners/swarm.d.ts.map +1 -0
- package/packages/benchmark/dist/runners/swarm.js +273 -0
- package/packages/benchmark/dist/runners/swarm.js.map +1 -0
- package/packages/benchmark/dist/types.d.ts +178 -0
- package/packages/benchmark/dist/types.d.ts.map +1 -0
- package/packages/benchmark/dist/types.js +16 -0
- package/packages/benchmark/dist/types.js.map +1 -0
- package/packages/benchmark/package.json +80 -0
- package/packages/benchmark/src/benchmark.ts +298 -0
- package/packages/benchmark/src/cli.ts +240 -0
- package/packages/benchmark/src/harbor.ts +170 -0
- package/packages/benchmark/src/index.ts +73 -0
- package/packages/benchmark/src/runners/base.ts +204 -0
- package/packages/benchmark/src/runners/index.ts +10 -0
- package/packages/benchmark/src/runners/single.ts +121 -0
- package/packages/benchmark/src/runners/subagent.ts +240 -0
- package/packages/benchmark/src/runners/swarm.ts +326 -0
- package/packages/benchmark/src/types.ts +205 -0
- package/packages/benchmark/tsconfig.json +20 -0
- package/packages/bridge/dist/index.d.ts.map +1 -0
- package/packages/bridge/dist/index.js.map +1 -0
- package/packages/bridge/dist/multi-project-client.d.ts.map +1 -0
- package/packages/bridge/dist/multi-project-client.js.map +1 -0
- package/packages/bridge/dist/shadow-cli.d.ts.map +1 -0
- package/packages/bridge/dist/shadow-cli.js.map +1 -0
- package/packages/bridge/dist/spawner.d.ts.map +1 -0
- package/packages/bridge/dist/spawner.js +10 -2
- package/packages/bridge/dist/spawner.js.map +1 -0
- package/packages/bridge/dist/types.d.ts.map +1 -0
- package/packages/bridge/dist/types.js.map +1 -0
- package/packages/bridge/dist/utils.d.ts.map +1 -0
- package/packages/bridge/dist/utils.js.map +1 -0
- package/packages/bridge/package.json +8 -8
- package/packages/bridge/src/index.ts +25 -0
- package/packages/bridge/src/multi-project-client.test.ts +340 -0
- package/packages/bridge/src/multi-project-client.ts +469 -0
- package/packages/bridge/src/shadow-cli.ts +95 -0
- package/packages/bridge/src/spawner-mcp.test.ts +505 -0
- package/packages/bridge/src/spawner.ts +1724 -0
- package/packages/bridge/src/types.ts +145 -0
- package/packages/bridge/src/utils.test.ts +98 -0
- package/packages/bridge/src/utils.ts +67 -0
- package/packages/bridge/tsconfig.json +29 -0
- package/packages/bridge/vitest.config.ts +9 -0
- package/packages/cli-tester/dist/index.d.ts.map +1 -0
- package/packages/cli-tester/dist/index.js.map +1 -0
- package/packages/cli-tester/dist/utils/credential-check.d.ts.map +1 -0
- package/packages/cli-tester/dist/utils/credential-check.js.map +1 -0
- package/packages/cli-tester/dist/utils/socket-client.d.ts.map +1 -0
- package/packages/cli-tester/dist/utils/socket-client.js.map +1 -0
- package/packages/cli-tester/docker/Dockerfile +61 -0
- package/packages/cli-tester/docker/docker-compose.yml +71 -0
- package/packages/cli-tester/package.json +1 -1
- package/packages/cli-tester/src/index.ts +40 -0
- package/packages/cli-tester/src/utils/credential-check.ts +284 -0
- package/packages/cli-tester/src/utils/socket-client.ts +211 -0
- package/packages/cli-tester/tests/credential-check.test.ts +56 -0
- package/packages/cli-tester/tsconfig.json +11 -0
- package/packages/config/dist/agent-config.d.ts.map +1 -0
- package/packages/config/dist/agent-config.js.map +1 -0
- package/packages/config/dist/bridge-config.d.ts.map +1 -0
- package/packages/config/dist/bridge-config.js.map +1 -0
- package/packages/config/dist/bridge-utils.d.ts.map +1 -0
- package/packages/config/dist/bridge-utils.js.map +1 -0
- package/packages/config/dist/cli-auth-config.d.ts.map +1 -0
- package/packages/config/dist/cli-auth-config.js.map +1 -0
- package/packages/config/dist/cloud-config.d.ts.map +1 -0
- package/packages/config/dist/cloud-config.js.map +1 -0
- package/packages/config/dist/index.d.ts.map +1 -0
- package/packages/config/dist/index.js.map +1 -0
- package/packages/config/dist/project-namespace.d.ts.map +1 -0
- package/packages/config/dist/project-namespace.js.map +1 -0
- package/packages/config/dist/relay-config.d.ts.map +1 -0
- package/packages/config/dist/relay-config.js.map +1 -0
- package/packages/config/dist/relay-file-writer.d.ts.map +1 -0
- package/packages/config/dist/relay-file-writer.js.map +1 -0
- package/packages/config/dist/schemas.d.ts.map +1 -0
- package/packages/config/dist/schemas.js.map +1 -0
- package/packages/config/dist/shadow-config.d.ts.map +1 -0
- package/packages/config/dist/shadow-config.js.map +1 -0
- package/packages/config/dist/teams-config.d.ts.map +1 -0
- package/packages/config/dist/teams-config.js.map +1 -0
- package/packages/config/dist/trajectory-config.d.ts.map +1 -0
- package/packages/config/dist/trajectory-config.js.map +1 -0
- package/packages/config/package.json +2 -2
- package/packages/config/src/agent-config.test.ts +245 -0
- package/packages/config/src/agent-config.ts +160 -0
- package/packages/config/src/bridge-config.test.ts +132 -0
- package/packages/config/src/bridge-config.ts +189 -0
- package/packages/config/src/bridge-utils.ts +59 -0
- package/packages/config/src/cli-auth-config.ts +548 -0
- package/packages/config/src/cloud-config.ts +208 -0
- package/packages/config/src/index.ts +12 -0
- package/packages/config/src/project-namespace.ts +344 -0
- package/packages/config/src/relay-config.test.ts +51 -0
- package/packages/config/src/relay-config.ts +36 -0
- package/packages/config/src/relay-file-writer.test.ts +351 -0
- package/packages/config/src/relay-file-writer.ts +508 -0
- package/packages/config/src/schemas.test.ts +59 -0
- package/packages/config/src/schemas.ts +201 -0
- package/packages/config/src/shadow-config.ts +205 -0
- package/packages/config/src/teams-config.ts +135 -0
- package/packages/config/src/trajectory-config.ts +222 -0
- package/packages/config/tsconfig.json +21 -0
- package/packages/config/vitest.config.ts +9 -0
- package/packages/continuity/dist/formatter.d.ts.map +1 -0
- package/packages/continuity/dist/formatter.js.map +1 -0
- package/packages/continuity/dist/handoff-store.d.ts.map +1 -0
- package/packages/continuity/dist/handoff-store.js.map +1 -0
- package/packages/continuity/dist/index.d.ts.map +1 -0
- package/packages/continuity/dist/index.js.map +1 -0
- package/packages/continuity/dist/ledger-store.d.ts.map +1 -0
- package/packages/continuity/dist/ledger-store.js.map +1 -0
- package/packages/continuity/dist/manager.d.ts.map +1 -0
- package/packages/continuity/dist/manager.js.map +1 -0
- package/packages/continuity/dist/parser.d.ts.map +1 -0
- package/packages/continuity/dist/parser.js.map +1 -0
- package/packages/continuity/dist/types.d.ts.map +1 -0
- package/packages/continuity/dist/types.js.map +1 -0
- package/packages/continuity/package.json +1 -1
- package/packages/continuity/src/formatter.ts +371 -0
- package/packages/continuity/src/handoff-store.ts +523 -0
- package/packages/continuity/src/index.ts +9 -0
- package/packages/continuity/src/ledger-store.ts +594 -0
- package/packages/continuity/src/manager.test.ts +291 -0
- package/packages/continuity/src/manager.ts +774 -0
- package/packages/continuity/src/parser.test.ts +292 -0
- package/packages/continuity/src/parser.ts +680 -0
- package/packages/continuity/src/types.ts +211 -0
- package/packages/continuity/tsconfig.json +21 -0
- package/packages/continuity/vitest.config.ts +9 -0
- package/packages/daemon/dist/agent-manager.d.ts.map +1 -0
- package/packages/daemon/dist/agent-manager.js.map +1 -0
- package/packages/daemon/dist/agent-registry.d.ts.map +1 -0
- package/packages/daemon/dist/agent-registry.js.map +1 -0
- package/packages/daemon/dist/agent-signing.d.ts.map +1 -0
- package/packages/daemon/dist/agent-signing.js.map +1 -0
- package/packages/daemon/dist/api.d.ts.map +1 -0
- package/packages/daemon/dist/api.js.map +1 -0
- package/packages/daemon/dist/auth.d.ts.map +1 -0
- package/packages/daemon/dist/auth.js.map +1 -0
- package/packages/daemon/dist/channel-membership-store.d.ts.map +1 -0
- package/packages/daemon/dist/channel-membership-store.js.map +1 -0
- package/packages/daemon/dist/cli-auth.d.ts.map +1 -0
- package/packages/daemon/dist/cli-auth.js.map +1 -0
- package/packages/daemon/dist/cloud-sync.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-sync.js.map +1 -0
- package/packages/daemon/dist/connection.d.ts.map +1 -0
- package/packages/daemon/dist/connection.js.map +1 -0
- package/packages/daemon/dist/consensus-integration.d.ts.map +1 -0
- package/packages/daemon/dist/consensus-integration.js.map +1 -0
- package/packages/daemon/dist/consensus.d.ts.map +1 -0
- package/packages/daemon/dist/consensus.js.map +1 -0
- package/packages/daemon/dist/delivery-tracker.d.ts.map +1 -0
- package/packages/daemon/dist/delivery-tracker.js.map +1 -0
- package/packages/daemon/dist/enhanced-features.d.ts.map +1 -0
- package/packages/daemon/dist/enhanced-features.js.map +1 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/migrations/index.d.ts.map +1 -0
- package/packages/daemon/dist/migrations/index.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.js.map +1 -0
- package/packages/daemon/dist/rate-limiter.d.ts.map +1 -0
- package/packages/daemon/dist/rate-limiter.js.map +1 -0
- package/packages/daemon/dist/registry.d.ts.map +1 -0
- package/packages/daemon/dist/registry.js.map +1 -0
- package/packages/daemon/dist/relay-ledger.d.ts.map +1 -0
- package/packages/daemon/dist/relay-ledger.js.map +1 -0
- package/packages/daemon/dist/relay-watchdog.d.ts.map +1 -0
- package/packages/daemon/dist/relay-watchdog.js.map +1 -0
- package/packages/daemon/dist/repo-manager.d.ts.map +1 -0
- package/packages/daemon/dist/repo-manager.js.map +1 -0
- package/packages/daemon/dist/router.d.ts.map +1 -0
- package/packages/daemon/dist/router.js.map +1 -0
- package/packages/daemon/dist/server.d.ts +1 -0
- package/packages/daemon/dist/server.d.ts.map +1 -0
- package/packages/daemon/dist/server.js +46 -16
- package/packages/daemon/dist/server.js.map +1 -0
- package/packages/daemon/dist/spawn-manager.d.ts.map +1 -0
- package/packages/daemon/dist/spawn-manager.js.map +1 -0
- package/packages/daemon/dist/sync-queue.d.ts.map +1 -0
- package/packages/daemon/dist/sync-queue.js.map +1 -0
- package/packages/daemon/dist/types.d.ts.map +1 -0
- package/packages/daemon/dist/types.js.map +1 -0
- package/packages/daemon/dist/workspace-manager.d.ts.map +1 -0
- package/packages/daemon/dist/workspace-manager.js.map +1 -0
- package/packages/daemon/package.json +12 -12
- package/packages/daemon/src/agent-manager.ts +679 -0
- package/packages/daemon/src/agent-registry.ts +284 -0
- package/packages/daemon/src/agent-signing.ts +707 -0
- package/packages/daemon/src/api.ts +1012 -0
- package/packages/daemon/src/auth.ts +276 -0
- package/packages/daemon/src/channel-membership-store.ts +217 -0
- package/packages/daemon/src/cli-auth.ts +906 -0
- package/packages/daemon/src/cloud-sync.ts +902 -0
- package/packages/daemon/src/connection.ts +534 -0
- package/packages/daemon/src/consensus-integration.ts +510 -0
- package/packages/daemon/src/consensus.ts +848 -0
- package/packages/daemon/src/delivery-tracker.ts +145 -0
- package/packages/daemon/src/enhanced-features.ts +390 -0
- package/packages/daemon/src/index.ts +52 -0
- package/packages/daemon/src/migrations/0001_initial.sql +72 -0
- package/packages/daemon/src/migrations/index.test.ts +195 -0
- package/packages/daemon/src/migrations/index.ts +286 -0
- package/packages/daemon/src/orchestrator.test.ts +231 -0
- package/packages/daemon/src/orchestrator.ts +1376 -0
- package/packages/daemon/src/rate-limiter.ts +172 -0
- package/packages/daemon/src/registry.ts +8 -0
- package/packages/daemon/src/relay-ledger.test.ts +358 -0
- package/packages/daemon/src/relay-ledger.ts +713 -0
- package/packages/daemon/src/relay-watchdog.test.ts +881 -0
- package/packages/daemon/src/relay-watchdog.ts +785 -0
- package/packages/daemon/src/repo-manager.ts +468 -0
- package/packages/daemon/src/router.test.ts +149 -0
- package/packages/daemon/src/router.ts +1885 -0
- package/packages/daemon/src/server.ts +1871 -0
- package/packages/daemon/src/spawn-manager.ts +275 -0
- package/packages/daemon/src/sync-queue.ts +477 -0
- package/packages/daemon/src/types.ts +158 -0
- package/packages/daemon/src/workspace-manager.ts +371 -0
- package/packages/daemon/tsconfig.json +21 -0
- package/packages/hooks/dist/browser.d.ts.map +1 -0
- package/packages/hooks/dist/browser.js.map +1 -0
- package/packages/hooks/dist/emitter.d.ts.map +1 -0
- package/packages/hooks/dist/emitter.js.map +1 -0
- package/packages/hooks/dist/inbox-check/hook.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/hook.js.map +1 -0
- package/packages/hooks/dist/inbox-check/index.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/index.js.map +1 -0
- package/packages/hooks/dist/inbox-check/types.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/types.js.map +1 -0
- package/packages/hooks/dist/inbox-check/utils.d.ts.map +1 -0
- package/packages/hooks/dist/inbox-check/utils.js.map +1 -0
- package/packages/hooks/dist/index.d.ts.map +1 -0
- package/packages/hooks/dist/index.js.map +1 -0
- package/packages/hooks/dist/registry.d.ts.map +1 -0
- package/packages/hooks/dist/registry.js.map +1 -0
- package/packages/hooks/dist/trajectory-hooks.d.ts.map +1 -0
- package/packages/hooks/dist/trajectory-hooks.js.map +1 -0
- package/packages/hooks/dist/types.d.ts.map +1 -0
- package/packages/hooks/dist/types.js.map +1 -0
- package/packages/hooks/package.json +4 -4
- package/packages/hooks/src/browser.ts +2 -0
- package/packages/hooks/src/emitter.ts +84 -0
- package/packages/hooks/src/inbox-check/hook.ts +114 -0
- package/packages/hooks/src/inbox-check/index.ts +8 -0
- package/packages/hooks/src/inbox-check/types.ts +39 -0
- package/packages/hooks/src/inbox-check/utils.test.ts +287 -0
- package/packages/hooks/src/inbox-check/utils.ts +125 -0
- package/packages/hooks/src/index.ts +11 -0
- package/packages/hooks/src/registry.ts +614 -0
- package/packages/hooks/src/shims.d.ts +3 -0
- package/packages/hooks/src/trajectory-hooks.ts +251 -0
- package/packages/hooks/src/types.ts +342 -0
- package/packages/hooks/tsconfig.json +21 -0
- package/packages/hooks/vitest.config.ts +9 -0
- package/packages/mcp/dist/bin.d.ts.map +1 -0
- package/packages/mcp/dist/bin.js.map +1 -0
- package/packages/mcp/dist/client.d.ts +9 -15
- package/packages/mcp/dist/client.d.ts.map +1 -0
- package/packages/mcp/dist/client.js +42 -74
- package/packages/mcp/dist/client.js.map +1 -0
- package/packages/mcp/dist/cloud.d.ts.map +1 -0
- package/packages/mcp/dist/cloud.js.map +1 -0
- package/packages/mcp/dist/errors.d.ts.map +1 -0
- package/packages/mcp/dist/errors.js.map +1 -0
- package/packages/mcp/dist/file-transport.d.ts.map +1 -0
- package/packages/mcp/dist/file-transport.js.map +1 -0
- package/packages/mcp/dist/hybrid-client.d.ts.map +1 -0
- package/packages/mcp/dist/hybrid-client.js.map +1 -0
- package/packages/mcp/dist/index.d.ts.map +1 -0
- package/packages/mcp/dist/index.js.map +1 -0
- package/packages/mcp/dist/install-cli.d.ts.map +1 -0
- package/packages/mcp/dist/install-cli.js.map +1 -0
- package/packages/mcp/dist/install.d.ts.map +1 -0
- package/packages/mcp/dist/install.js.map +1 -0
- package/packages/mcp/dist/prompts/index.d.ts.map +1 -0
- package/packages/mcp/dist/prompts/index.js.map +1 -0
- package/packages/mcp/dist/prompts/protocol.d.ts.map +1 -0
- package/packages/mcp/dist/prompts/protocol.js.map +1 -0
- package/packages/mcp/dist/resources/agents.d.ts.map +1 -0
- package/packages/mcp/dist/resources/agents.js.map +1 -0
- package/packages/mcp/dist/resources/inbox.d.ts.map +1 -0
- package/packages/mcp/dist/resources/inbox.js.map +1 -0
- package/packages/mcp/dist/resources/index.d.ts.map +1 -0
- package/packages/mcp/dist/resources/index.js.map +1 -0
- package/packages/mcp/dist/resources/project.d.ts.map +1 -0
- package/packages/mcp/dist/resources/project.js.map +1 -0
- package/packages/mcp/dist/server.d.ts.map +1 -0
- package/packages/mcp/dist/server.js.map +1 -0
- package/packages/mcp/dist/simple.d.ts +2 -5
- package/packages/mcp/dist/simple.d.ts.map +1 -0
- package/packages/mcp/dist/simple.js.map +1 -0
- package/packages/mcp/dist/tools/index.d.ts.map +1 -0
- package/packages/mcp/dist/tools/index.js.map +1 -0
- package/packages/mcp/dist/tools/relay-broadcast.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-broadcast.js.map +1 -0
- package/packages/mcp/dist/tools/relay-channel.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-channel.js.map +1 -0
- package/packages/mcp/dist/tools/relay-connected.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-connected.js.map +1 -0
- package/packages/mcp/dist/tools/relay-consensus.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-consensus.js.map +1 -0
- package/packages/mcp/dist/tools/relay-continuity.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-continuity.js.map +1 -0
- package/packages/mcp/dist/tools/relay-health.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-health.js.map +1 -0
- package/packages/mcp/dist/tools/relay-inbox.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-inbox.js.map +1 -0
- package/packages/mcp/dist/tools/relay-logs.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-logs.js.map +1 -0
- package/packages/mcp/dist/tools/relay-metrics.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-metrics.js.map +1 -0
- package/packages/mcp/dist/tools/relay-release.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-release.js.map +1 -0
- package/packages/mcp/dist/tools/relay-remove-agent.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-remove-agent.js.map +1 -0
- package/packages/mcp/dist/tools/relay-send.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-send.js +4 -2
- package/packages/mcp/dist/tools/relay-send.js.map +1 -0
- package/packages/mcp/dist/tools/relay-shadow.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-shadow.js.map +1 -0
- package/packages/mcp/dist/tools/relay-spawn.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-spawn.js.map +1 -0
- package/packages/mcp/dist/tools/relay-status.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-status.js.map +1 -0
- package/packages/mcp/dist/tools/relay-subscribe.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-subscribe.js.map +1 -0
- package/packages/mcp/dist/tools/relay-who.d.ts.map +1 -0
- package/packages/mcp/dist/tools/relay-who.js.map +1 -0
- package/packages/mcp/package.json +3 -3
- package/packages/mcp/src/bin.ts +149 -0
- package/packages/mcp/src/client.ts +400 -0
- package/packages/mcp/src/cloud.ts +523 -0
- package/packages/mcp/src/errors.ts +54 -0
- package/packages/mcp/src/file-transport.ts +268 -0
- package/packages/mcp/src/hybrid-client.ts +209 -0
- package/packages/mcp/src/index.ts +122 -0
- package/packages/mcp/src/install-cli.ts +210 -0
- package/packages/mcp/src/install.ts +745 -0
- package/packages/mcp/src/prompts/index.ts +1 -0
- package/packages/mcp/src/prompts/protocol.ts +164 -0
- package/packages/mcp/src/resources/agents.ts +21 -0
- package/packages/mcp/src/resources/inbox.ts +21 -0
- package/packages/mcp/src/resources/index.ts +3 -0
- package/packages/mcp/src/resources/project.ts +29 -0
- package/packages/mcp/src/server.ts +431 -0
- package/packages/mcp/src/simple.ts +214 -0
- package/packages/mcp/src/tools/index.ts +133 -0
- package/packages/mcp/src/tools/relay-broadcast.ts +32 -0
- package/packages/mcp/src/tools/relay-channel.ts +93 -0
- package/packages/mcp/src/tools/relay-connected.ts +52 -0
- package/packages/mcp/src/tools/relay-consensus.ts +92 -0
- package/packages/mcp/src/tools/relay-continuity.ts +127 -0
- package/packages/mcp/src/tools/relay-health.ts +148 -0
- package/packages/mcp/src/tools/relay-inbox.ts +70 -0
- package/packages/mcp/src/tools/relay-logs.ts +106 -0
- package/packages/mcp/src/tools/relay-metrics.ts +140 -0
- package/packages/mcp/src/tools/relay-release.ts +54 -0
- package/packages/mcp/src/tools/relay-remove-agent.ts +58 -0
- package/packages/mcp/src/tools/relay-send.ts +84 -0
- package/packages/mcp/src/tools/relay-shadow.ts +67 -0
- package/packages/mcp/src/tools/relay-spawn.ts +87 -0
- package/packages/mcp/src/tools/relay-status.ts +57 -0
- package/packages/mcp/src/tools/relay-subscribe.ts +61 -0
- package/packages/mcp/src/tools/relay-who.ts +59 -0
- package/packages/mcp/tests/client.test.ts +476 -0
- package/packages/mcp/tests/discover.test.ts +195 -0
- package/packages/mcp/tests/install.test.ts +123 -0
- package/packages/mcp/tests/prompts.test.ts +12 -0
- package/packages/mcp/tests/resources.test.ts +53 -0
- package/packages/mcp/tests/tools.test.ts +1242 -0
- package/packages/mcp/tsconfig.json +22 -0
- package/packages/mcp/vitest.config.ts +9 -0
- package/packages/memory/dist/adapters/index.d.ts.map +1 -0
- package/packages/memory/dist/adapters/index.js.map +1 -0
- package/packages/memory/dist/adapters/inmemory.d.ts.map +1 -0
- package/packages/memory/dist/adapters/inmemory.js.map +1 -0
- package/packages/memory/dist/adapters/supermemory.d.ts.map +1 -0
- package/packages/memory/dist/adapters/supermemory.js.map +1 -0
- package/packages/memory/dist/context-compaction.d.ts.map +1 -0
- package/packages/memory/dist/context-compaction.js.map +1 -0
- package/packages/memory/dist/factory.d.ts.map +1 -0
- package/packages/memory/dist/factory.js.map +1 -0
- package/packages/memory/dist/index.d.ts.map +1 -0
- package/packages/memory/dist/index.js.map +1 -0
- package/packages/memory/dist/memory-hooks.d.ts.map +1 -0
- package/packages/memory/dist/memory-hooks.js.map +1 -0
- package/packages/memory/dist/service.d.ts.map +1 -0
- package/packages/memory/dist/service.js.map +1 -0
- package/packages/memory/dist/types.d.ts.map +1 -0
- package/packages/memory/dist/types.js.map +1 -0
- package/packages/memory/package.json +2 -2
- package/packages/memory/src/adapters/index.ts +8 -0
- package/packages/memory/src/adapters/inmemory.ts +265 -0
- package/packages/memory/src/adapters/supermemory.ts +449 -0
- package/packages/memory/src/context-compaction.test.ts +660 -0
- package/packages/memory/src/context-compaction.ts +612 -0
- package/packages/memory/src/factory.ts +170 -0
- package/packages/memory/src/index.ts +33 -0
- package/packages/memory/src/memory-hooks.ts +410 -0
- package/packages/memory/src/service.ts +194 -0
- package/packages/memory/src/types.ts +211 -0
- package/packages/memory/tsconfig.json +21 -0
- package/packages/memory/vitest.config.ts +9 -0
- package/packages/policy/dist/agent-policy.d.ts.map +1 -0
- package/packages/policy/dist/agent-policy.js.map +1 -0
- package/packages/policy/dist/cloud-policy-fetcher.d.ts.map +1 -0
- package/packages/policy/dist/cloud-policy-fetcher.js.map +1 -0
- package/packages/policy/dist/index.d.ts.map +1 -0
- package/packages/policy/dist/index.js.map +1 -0
- package/packages/policy/package.json +2 -2
- package/packages/policy/src/agent-policy.ts +866 -0
- package/packages/policy/src/cloud-policy-fetcher.ts +78 -0
- package/packages/policy/src/index.ts +21 -0
- package/packages/policy/tsconfig.json +21 -0
- package/packages/policy/vitest.config.ts +9 -0
- package/packages/protocol/dist/channels.d.ts.map +1 -0
- package/packages/protocol/dist/channels.js.map +1 -0
- package/packages/protocol/dist/framing.d.ts.map +1 -0
- package/packages/protocol/dist/framing.js.map +1 -0
- package/packages/protocol/dist/id-generator.d.ts.map +1 -0
- package/packages/protocol/dist/id-generator.js.map +1 -0
- package/packages/protocol/dist/index.d.ts.map +1 -0
- package/packages/protocol/dist/index.js.map +1 -0
- package/packages/protocol/dist/relay-pty-schemas.d.ts +70 -2
- package/packages/protocol/dist/relay-pty-schemas.d.ts.map +1 -0
- package/packages/protocol/dist/relay-pty-schemas.js.map +1 -0
- package/packages/protocol/dist/types.d.ts +8 -0
- package/packages/protocol/dist/types.d.ts.map +1 -0
- package/packages/protocol/dist/types.js.map +1 -0
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/channels.test.ts +330 -0
- package/packages/protocol/src/channels.ts +270 -0
- package/packages/protocol/src/framing.test.ts +164 -0
- package/packages/protocol/src/framing.ts +242 -0
- package/packages/protocol/src/id-generator.ts +69 -0
- package/packages/protocol/src/index.ts +4 -0
- package/packages/protocol/src/relay-pty-schemas.ts +400 -0
- package/packages/protocol/src/types.test.ts +271 -0
- package/packages/protocol/src/types.ts +846 -0
- package/packages/protocol/tsconfig.json +21 -0
- package/packages/protocol/vitest.config.ts +9 -0
- package/packages/resiliency/dist/cgroup-manager.d.ts.map +1 -0
- package/packages/resiliency/dist/cgroup-manager.js.map +1 -0
- package/packages/resiliency/dist/context-persistence.d.ts.map +1 -0
- package/packages/resiliency/dist/context-persistence.js.map +1 -0
- package/packages/resiliency/dist/crash-insights.d.ts.map +1 -0
- package/packages/resiliency/dist/crash-insights.js.map +1 -0
- package/packages/resiliency/dist/gossip-health.d.ts.map +1 -0
- package/packages/resiliency/dist/gossip-health.js.map +1 -0
- package/packages/resiliency/dist/health-monitor.d.ts.map +1 -0
- package/packages/resiliency/dist/health-monitor.js.map +1 -0
- package/packages/resiliency/dist/index.d.ts.map +1 -0
- package/packages/resiliency/dist/index.js.map +1 -0
- package/packages/resiliency/dist/leader-watchdog.d.ts.map +1 -0
- package/packages/resiliency/dist/leader-watchdog.js.map +1 -0
- package/packages/resiliency/dist/logger.d.ts.map +1 -0
- package/packages/resiliency/dist/logger.js.map +1 -0
- package/packages/resiliency/dist/memory-monitor.d.ts.map +1 -0
- package/packages/resiliency/dist/memory-monitor.js.map +1 -0
- package/packages/resiliency/dist/metrics.d.ts.map +1 -0
- package/packages/resiliency/dist/metrics.js.map +1 -0
- package/packages/resiliency/dist/provider-context.d.ts.map +1 -0
- package/packages/resiliency/dist/provider-context.js.map +1 -0
- package/packages/resiliency/dist/stateless-lead.d.ts.map +1 -0
- package/packages/resiliency/dist/stateless-lead.js.map +1 -0
- package/packages/resiliency/dist/supervisor.d.ts.map +1 -0
- package/packages/resiliency/dist/supervisor.js.map +1 -0
- package/packages/resiliency/package.json +1 -1
- package/packages/resiliency/src/cgroup-manager.ts +468 -0
- package/packages/resiliency/src/context-persistence.ts +538 -0
- package/packages/resiliency/src/crash-insights.test.ts +620 -0
- package/packages/resiliency/src/crash-insights.ts +660 -0
- package/packages/resiliency/src/gossip-health.ts +333 -0
- package/packages/resiliency/src/health-monitor.ts +371 -0
- package/packages/resiliency/src/index.ts +157 -0
- package/packages/resiliency/src/leader-watchdog.ts +260 -0
- package/packages/resiliency/src/logger.ts +320 -0
- package/packages/resiliency/src/memory-monitor.test.ts +637 -0
- package/packages/resiliency/src/memory-monitor.ts +740 -0
- package/packages/resiliency/src/metrics.ts +311 -0
- package/packages/resiliency/src/provider-context.ts +452 -0
- package/packages/resiliency/src/stateless-lead.ts +408 -0
- package/packages/resiliency/src/supervisor.ts +578 -0
- package/packages/resiliency/tsconfig.json +21 -0
- package/packages/resiliency/vitest.config.ts +9 -0
- package/packages/sdk/dist/client.d.ts.map +1 -0
- package/packages/sdk/dist/client.js.map +1 -0
- package/packages/sdk/dist/index.d.ts.map +1 -0
- package/packages/sdk/dist/index.js.map +1 -0
- package/packages/sdk/dist/logs.d.ts.map +1 -0
- package/packages/sdk/dist/logs.js.map +1 -0
- package/packages/sdk/dist/protocol/index.d.ts.map +1 -0
- package/packages/sdk/dist/protocol/index.js.map +1 -0
- package/packages/sdk/dist/standalone.d.ts.map +1 -0
- package/packages/sdk/dist/standalone.js.map +1 -0
- package/packages/sdk/examples/SWARM_CAPABILITIES.md +498 -0
- package/packages/sdk/examples/SWARM_PATTERNS.md +541 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/client.test.ts +568 -0
- package/packages/sdk/src/client.ts +1418 -0
- package/packages/sdk/src/index.ts +103 -0
- package/packages/sdk/src/logs.test.ts +98 -0
- package/packages/sdk/src/logs.ts +126 -0
- package/packages/sdk/src/protocol/framing.test.ts +164 -0
- package/packages/sdk/src/protocol/index.ts +8 -0
- package/packages/sdk/src/standalone.ts +176 -0
- package/packages/sdk/tsconfig.json +22 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/packages/spawner/.trajectories/index.json +5 -0
- package/packages/spawner/dist/index.d.ts.map +1 -0
- package/packages/spawner/dist/index.js.map +1 -0
- package/packages/spawner/dist/types.d.ts.map +1 -0
- package/packages/spawner/dist/types.js.map +1 -0
- package/packages/spawner/package.json +1 -1
- package/packages/spawner/src/index.ts +8 -0
- package/packages/spawner/src/types.test.ts +385 -0
- package/packages/spawner/src/types.ts +228 -0
- package/packages/spawner/tsconfig.json +19 -0
- package/packages/spawner/vitest.config.ts +9 -0
- package/packages/state/dist/agent-state.d.ts.map +1 -0
- package/packages/state/dist/agent-state.js.map +1 -0
- package/packages/state/dist/index.d.ts.map +1 -0
- package/packages/state/dist/index.js.map +1 -0
- package/packages/state/package.json +1 -1
- package/packages/state/src/agent-state.test.ts +335 -0
- package/packages/state/src/agent-state.ts +153 -0
- package/packages/state/src/index.ts +12 -0
- package/packages/state/tsconfig.json +21 -0
- package/packages/state/vitest.config.ts +9 -0
- package/packages/storage/dist/adapter.d.ts +28 -1
- package/packages/storage/dist/adapter.d.ts.map +1 -0
- package/packages/storage/dist/adapter.js +104 -10
- package/packages/storage/dist/adapter.js.map +1 -0
- package/packages/storage/dist/batched-sqlite-adapter.d.ts.map +1 -0
- package/packages/storage/dist/batched-sqlite-adapter.js.map +1 -0
- package/packages/storage/dist/dead-letter-queue.d.ts.map +1 -0
- package/packages/storage/dist/dead-letter-queue.js.map +1 -0
- package/packages/storage/dist/dlq-adapter.d.ts.map +1 -0
- package/packages/storage/dist/dlq-adapter.js.map +1 -0
- package/packages/storage/dist/index.d.ts +1 -0
- package/packages/storage/dist/index.d.ts.map +1 -0
- package/packages/storage/dist/index.js +1 -0
- package/packages/storage/dist/index.js.map +1 -0
- package/packages/storage/dist/jsonl-adapter.d.ts +77 -0
- package/packages/storage/dist/jsonl-adapter.d.ts.map +1 -0
- package/packages/storage/dist/jsonl-adapter.js +505 -0
- package/packages/storage/dist/jsonl-adapter.js.map +1 -0
- package/packages/storage/dist/sqlite-adapter.d.ts +6 -1
- package/packages/storage/dist/sqlite-adapter.d.ts.map +1 -0
- package/packages/storage/dist/sqlite-adapter.js +47 -0
- package/packages/storage/dist/sqlite-adapter.js.map +1 -0
- package/packages/storage/package.json +2 -2
- package/packages/storage/src/adapter.ts +438 -0
- package/packages/storage/src/batched-sqlite-adapter.test.ts +240 -0
- package/packages/storage/src/batched-sqlite-adapter.ts +239 -0
- package/packages/storage/src/dead-letter-queue.ts +643 -0
- package/packages/storage/src/dlq-adapter.test.ts +492 -0
- package/packages/storage/src/dlq-adapter.ts +954 -0
- package/packages/storage/src/index.ts +6 -0
- package/packages/storage/src/jsonl-adapter.test.ts +200 -0
- package/packages/storage/src/jsonl-adapter.ts +618 -0
- package/packages/storage/src/memory-adapter.test.ts +36 -0
- package/packages/storage/src/sqlite-adapter.test.ts +562 -0
- package/packages/storage/src/sqlite-adapter.ts +1058 -0
- package/packages/storage/tsconfig.json +21 -0
- package/packages/storage/vitest.config.ts +9 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -0
- package/packages/telemetry/dist/client.js.map +1 -0
- package/packages/telemetry/dist/config.d.ts.map +1 -0
- package/packages/telemetry/dist/config.js.map +1 -0
- package/packages/telemetry/dist/events.d.ts.map +1 -0
- package/packages/telemetry/dist/events.js.map +1 -0
- package/packages/telemetry/dist/index.d.ts.map +1 -0
- package/packages/telemetry/dist/index.js.map +1 -0
- package/packages/telemetry/dist/machine-id.d.ts.map +1 -0
- package/packages/telemetry/dist/machine-id.js.map +1 -0
- package/packages/telemetry/dist/posthog-config.d.ts.map +1 -0
- package/packages/telemetry/dist/posthog-config.js.map +1 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +158 -0
- package/packages/telemetry/src/config.ts +110 -0
- package/packages/telemetry/src/events.ts +137 -0
- package/packages/telemetry/src/index.ts +46 -0
- package/packages/telemetry/src/machine-id.ts +63 -0
- package/packages/telemetry/src/posthog-config.ts +39 -0
- package/packages/telemetry/tsconfig.json +21 -0
- package/packages/trajectory/dist/index.d.ts.map +1 -0
- package/packages/trajectory/dist/index.js.map +1 -0
- package/packages/trajectory/dist/integration.d.ts.map +1 -0
- package/packages/trajectory/dist/integration.js.map +1 -0
- package/packages/trajectory/package.json +2 -2
- package/packages/trajectory/src/index.ts +1 -0
- package/packages/trajectory/src/integration.ts +1268 -0
- package/packages/trajectory/tsconfig.json +21 -0
- package/packages/trajectory/vitest.config.ts +9 -0
- package/packages/user-directory/dist/index.d.ts.map +1 -0
- package/packages/user-directory/dist/index.js.map +1 -0
- package/packages/user-directory/dist/user-directory.d.ts.map +1 -0
- package/packages/user-directory/dist/user-directory.js.map +1 -0
- package/packages/user-directory/package.json +2 -2
- package/packages/user-directory/src/index.ts +12 -0
- package/packages/user-directory/src/user-directory.ts +393 -0
- package/packages/user-directory/tsconfig.json +21 -0
- package/packages/user-directory/vitest.config.ts +9 -0
- package/packages/utils/dist/cjs/client-helpers.js +127 -0
- package/packages/utils/dist/cjs/command-resolver.js +89 -0
- package/packages/utils/dist/cjs/error-tracking.js +106 -0
- package/packages/utils/dist/cjs/git-remote.js +120 -0
- package/packages/utils/dist/cjs/index.js +40 -0
- package/packages/utils/dist/cjs/logger.js +105 -0
- package/packages/utils/dist/cjs/model-mapping.js +54 -0
- package/packages/utils/dist/cjs/name-generator.js +179 -0
- package/packages/utils/dist/cjs/package.json +3 -0
- package/packages/utils/dist/cjs/precompiled-patterns.js +271 -0
- package/packages/utils/dist/cjs/relay-pty-path.js +143 -0
- package/packages/utils/dist/cjs/update-checker.js +185 -0
- package/packages/utils/dist/client-helpers.d.ts +73 -0
- package/packages/utils/dist/client-helpers.d.ts.map +1 -0
- package/packages/utils/dist/client-helpers.js +130 -0
- package/packages/utils/dist/client-helpers.js.map +1 -0
- package/packages/utils/dist/command-resolver.d.ts.map +1 -0
- package/packages/utils/dist/command-resolver.js.map +1 -0
- package/packages/utils/dist/error-tracking.d.ts.map +1 -0
- package/packages/utils/dist/error-tracking.js.map +1 -0
- package/packages/utils/dist/git-remote.d.ts.map +1 -0
- package/packages/utils/dist/git-remote.js.map +1 -0
- package/packages/utils/dist/index.d.ts +1 -0
- package/packages/utils/dist/index.d.ts.map +1 -0
- package/packages/utils/dist/index.js +1 -0
- package/packages/utils/dist/index.js.map +1 -0
- package/packages/utils/dist/logger.d.ts.map +1 -0
- package/packages/utils/dist/logger.js.map +1 -0
- package/packages/utils/dist/model-mapping.d.ts.map +1 -0
- package/packages/utils/dist/model-mapping.js.map +1 -0
- package/packages/utils/dist/name-generator.d.ts.map +1 -0
- package/packages/utils/dist/name-generator.js.map +1 -0
- package/packages/utils/dist/precompiled-patterns.d.ts.map +1 -0
- package/packages/utils/dist/precompiled-patterns.js.map +1 -0
- package/packages/utils/dist/relay-pty-path.d.ts +11 -5
- package/packages/utils/dist/relay-pty-path.d.ts.map +1 -0
- package/packages/utils/dist/relay-pty-path.js +60 -5
- package/packages/utils/dist/relay-pty-path.js.map +1 -0
- package/packages/utils/dist/update-checker.d.ts.map +1 -0
- package/packages/utils/dist/update-checker.js.map +1 -0
- package/packages/utils/package.json +37 -14
- package/packages/utils/scripts/build-cjs.mjs +24 -0
- package/packages/utils/src/client-helpers.ts +221 -0
- package/packages/utils/src/command-resolver.ts +82 -0
- package/packages/utils/src/error-tracking.ts +189 -0
- package/packages/utils/src/git-remote.ts +143 -0
- package/packages/utils/src/index.ts +10 -0
- package/packages/utils/src/logger.ts +107 -0
- package/packages/utils/src/model-mapping.test.ts +122 -0
- package/packages/utils/src/model-mapping.ts +58 -0
- package/packages/utils/src/name-generator.test.ts +259 -0
- package/packages/utils/src/name-generator.ts +56 -0
- package/packages/utils/src/precompiled-patterns.test.ts +452 -0
- package/packages/utils/src/precompiled-patterns.ts +395 -0
- package/packages/utils/src/relay-pty-path.ts +196 -0
- package/packages/utils/src/update-checker.test.ts +260 -0
- package/packages/utils/src/update-checker.ts +211 -0
- package/packages/utils/tsconfig.json +21 -0
- package/packages/utils/vitest.config.ts +9 -0
- package/packages/wrapper/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/claude-outputs.js.map +1 -0
- package/packages/wrapper/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/codex-outputs.js.map +1 -0
- package/packages/wrapper/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/gemini-outputs.js.map +1 -0
- package/packages/wrapper/dist/__fixtures__/index.d.ts.map +1 -0
- package/packages/wrapper/dist/__fixtures__/index.js.map +1 -0
- package/packages/wrapper/dist/auth-detection.d.ts.map +1 -0
- package/packages/wrapper/dist/auth-detection.js.map +1 -0
- package/packages/wrapper/dist/base-wrapper.d.ts.map +1 -0
- package/packages/wrapper/dist/base-wrapper.js.map +1 -0
- package/packages/wrapper/dist/client.d.ts.map +1 -0
- package/packages/wrapper/dist/client.js.map +1 -0
- package/packages/wrapper/dist/id-generator.d.ts.map +1 -0
- package/packages/wrapper/dist/id-generator.js.map +1 -0
- package/packages/wrapper/dist/idle-detector.d.ts.map +1 -0
- package/packages/wrapper/dist/idle-detector.js.map +1 -0
- package/packages/wrapper/dist/inbox.d.ts.map +1 -0
- package/packages/wrapper/dist/inbox.js.map +1 -0
- package/packages/wrapper/dist/index.d.ts.map +1 -0
- package/packages/wrapper/dist/index.js.map +1 -0
- package/packages/wrapper/dist/parser.d.ts.map +1 -0
- package/packages/wrapper/dist/parser.js.map +1 -0
- package/packages/wrapper/dist/prompt-composer.d.ts.map +1 -0
- package/packages/wrapper/dist/prompt-composer.js.map +1 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +10 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts.map +1 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.js +69 -0
- package/packages/wrapper/dist/relay-pty-orchestrator.js.map +1 -0
- package/packages/wrapper/dist/shared.d.ts.map +1 -0
- package/packages/wrapper/dist/shared.js.map +1 -0
- package/packages/wrapper/dist/stuck-detector.d.ts.map +1 -0
- package/packages/wrapper/dist/stuck-detector.js.map +1 -0
- package/packages/wrapper/dist/tmux-resolver.d.ts.map +1 -0
- package/packages/wrapper/dist/tmux-resolver.js.map +1 -0
- package/packages/wrapper/dist/tmux-wrapper.d.ts.map +1 -0
- package/packages/wrapper/dist/tmux-wrapper.js.map +1 -0
- package/packages/wrapper/dist/trajectory-integration.d.ts.map +1 -0
- package/packages/wrapper/dist/trajectory-integration.js.map +1 -0
- package/packages/wrapper/dist/wrapper-types.d.ts.map +1 -0
- package/packages/wrapper/dist/wrapper-types.js.map +1 -0
- package/packages/wrapper/package.json +6 -9
- package/packages/wrapper/src/__fixtures__/claude-outputs.ts +471 -0
- package/packages/wrapper/src/__fixtures__/codex-outputs.ts +99 -0
- package/packages/wrapper/src/__fixtures__/gemini-outputs.ts +151 -0
- package/packages/wrapper/src/__fixtures__/index.ts +47 -0
- package/packages/wrapper/src/auth-detection.ts +244 -0
- package/packages/wrapper/src/base-wrapper.test.ts +589 -0
- package/packages/wrapper/src/base-wrapper.ts +810 -0
- package/packages/wrapper/src/client.test.ts +262 -0
- package/packages/wrapper/src/client.ts +984 -0
- package/packages/wrapper/src/id-generator.test.ts +71 -0
- package/packages/wrapper/src/id-generator.ts +69 -0
- package/packages/wrapper/src/idle-detector.test.ts +418 -0
- package/packages/wrapper/src/idle-detector.ts +384 -0
- package/packages/wrapper/src/inbox.test.ts +233 -0
- package/packages/wrapper/src/inbox.ts +89 -0
- package/packages/wrapper/src/index.ts +170 -0
- package/packages/wrapper/src/parser.regression.test.ts +251 -0
- package/packages/wrapper/src/parser.test.ts +1359 -0
- package/packages/wrapper/src/parser.ts +1477 -0
- package/packages/wrapper/src/prompt-composer.test.ts +219 -0
- package/packages/wrapper/src/prompt-composer.ts +231 -0
- package/packages/wrapper/src/relay-pty-orchestrator.test.ts +1204 -0
- package/packages/wrapper/src/relay-pty-orchestrator.ts +2626 -0
- package/packages/wrapper/src/shared.test.ts +322 -0
- package/packages/wrapper/src/shared.ts +495 -0
- package/packages/wrapper/src/stuck-detector.test.ts +303 -0
- package/packages/wrapper/src/stuck-detector.ts +511 -0
- package/packages/wrapper/src/tmux-resolver.test.ts +104 -0
- package/packages/wrapper/src/tmux-resolver.ts +207 -0
- package/packages/wrapper/src/tmux-wrapper.test.ts +316 -0
- package/packages/wrapper/src/tmux-wrapper.ts +2095 -0
- package/packages/wrapper/src/trajectory-detection.test.ts +151 -0
- package/packages/wrapper/src/trajectory-integration.ts +1261 -0
- package/packages/wrapper/src/wrapper-types.ts +45 -0
- package/packages/wrapper/tsconfig.json +19 -0
- package/packages/wrapper/vitest.config.ts +9 -0
- package/scripts/build-cjs.mjs +23 -0
- package/scripts/postinstall.js +132 -0
- package/.cursor/mcp.json +0 -11
- package/.gitattributes +0 -3
- package/.gitleaks.toml +0 -26
- package/.mcp.json +0 -11
- package/.nvmrc +0 -1
- package/ARCHITECTURE.md +0 -1245
- package/CHANGELOG.md +0 -231
- package/TESTING.md +0 -278
- package/TRAIL_GIT_AUTH_FIX.md +0 -113
- package/scripts/demos/README.md +0 -79
- package/scripts/demos/server-capacity.sh +0 -69
- package/scripts/demos/sprint-planning.sh +0 -73
- package/scripts/hooks/install.sh +0 -16
- package/scripts/hooks/pre-commit +0 -60
- package/scripts/post-publish-verify/README.md +0 -80
- package/scripts/post-publish-verify/run-verify.sh +0 -127
- package/scripts/post-publish-verify/verify-install.sh +0 -249
- package/scripts/stress-test-orchestrator-integration.mts +0 -1366
- package/scripts/stress-test-orchestrator.mjs +0 -584
- package/scripts/stress-test-relay-pty.sh +0 -452
- package/scripts/test-interactive-terminal.sh +0 -248
- package/specs/PRIMITIVES_ROADMAP.md +0 -2154
- package/tests/benchmarks/protocol.bench.ts +0 -310
- package/turbo.json +0 -37
|
@@ -0,0 +1,1724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Spawner
|
|
3
|
+
* Handles spawning and releasing worker agents via relay-pty.
|
|
4
|
+
* Workers run headlessly with output capture for logs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { sleep } from './utils.js';
|
|
12
|
+
import { getProjectPaths, getAgentOutboxTemplate } from '@agent-relay/config';
|
|
13
|
+
import { resolveCommand } from '@agent-relay/utils/command-resolver';
|
|
14
|
+
import { createTraceableError } from '@agent-relay/utils/error-tracking';
|
|
15
|
+
import { createLogger } from '@agent-relay/utils/logger';
|
|
16
|
+
import { mapModelToCli } from '@agent-relay/utils/model-mapping';
|
|
17
|
+
import { findRelayPtyBinary as findRelayPtyBinaryUtil, getLastSearchPaths } from '@agent-relay/utils/relay-pty-path';
|
|
18
|
+
import { RelayPtyOrchestrator, type RelayPtyOrchestratorConfig } from '@agent-relay/wrapper';
|
|
19
|
+
import type { SummaryEvent, SessionEndEvent } from '@agent-relay/wrapper';
|
|
20
|
+
import { selectShadowCli } from './shadow-cli.js';
|
|
21
|
+
|
|
22
|
+
// Get the directory where this module is located (for binary path resolution)
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
import { AgentPolicyService, type CloudPolicyFetcher } from '@agent-relay/policy';
|
|
26
|
+
import { buildClaudeArgs, findAgentConfig } from '@agent-relay/config/agent-config';
|
|
27
|
+
import { composeForAgent, type AgentRole } from '@agent-relay/wrapper';
|
|
28
|
+
import { getUserDirectoryService } from '@agent-relay/user-directory';
|
|
29
|
+
import { installMcpConfig } from '@agent-relay/mcp';
|
|
30
|
+
import type {
|
|
31
|
+
SpawnRequest,
|
|
32
|
+
SpawnResult,
|
|
33
|
+
WorkerInfo,
|
|
34
|
+
SpawnWithShadowRequest,
|
|
35
|
+
SpawnWithShadowResult,
|
|
36
|
+
SpeakOnTrigger,
|
|
37
|
+
} from './types.js';
|
|
38
|
+
|
|
39
|
+
// Logger instance for spawner (uses daemon log system instead of console)
|
|
40
|
+
const log = createLogger('spawner');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* CLI command mapping for providers
|
|
44
|
+
* Maps provider names to actual CLI command names
|
|
45
|
+
*/
|
|
46
|
+
const CLI_COMMAND_MAP: Record<string, string> = {
|
|
47
|
+
cursor: 'agent', // Cursor CLI installs as 'agent'
|
|
48
|
+
google: 'gemini', // Google provider uses 'gemini' CLI
|
|
49
|
+
// Other providers use their name as the command (claude, codex, etc.)
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function extractGhTokenFromHosts(content: string): string | null {
|
|
53
|
+
const lines = content.split(/\r?\n/);
|
|
54
|
+
let inGithubSection = false;
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!line.startsWith(' ') && !line.startsWith('\t')) {
|
|
61
|
+
const host = trimmed.replace(/:$/, '');
|
|
62
|
+
inGithubSection = host === 'github.com';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!inGithubSection) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const match = line.match(/^\s*(oauth_token|token):\s*(.+)$/);
|
|
69
|
+
if (!match) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let token = match[2].split('#')[0].trim();
|
|
73
|
+
token = token.replace(/^['"]|['"]$/g, '');
|
|
74
|
+
if (token) {
|
|
75
|
+
return token;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Cloud persistence handler interface.
|
|
83
|
+
* Implement this to persist agent session data to cloud storage.
|
|
84
|
+
*/
|
|
85
|
+
export interface CloudPersistenceHandler {
|
|
86
|
+
onSummary: (agentName: string, event: SummaryEvent) => Promise<void>;
|
|
87
|
+
onSessionEnd: (agentName: string, event: SessionEndEvent) => Promise<void>;
|
|
88
|
+
/** Optional cleanup method for tests and graceful shutdown */
|
|
89
|
+
destroy?: () => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Worker metadata stored in workers.json */
|
|
93
|
+
interface WorkerMeta {
|
|
94
|
+
name: string;
|
|
95
|
+
cli: string;
|
|
96
|
+
task: string;
|
|
97
|
+
/** Optional team name this agent belongs to */
|
|
98
|
+
team?: string;
|
|
99
|
+
/** Optional user ID for per-user credential scoping */
|
|
100
|
+
userId?: string;
|
|
101
|
+
spawnedAt: number;
|
|
102
|
+
pid?: number;
|
|
103
|
+
logFile?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Stored listener references for cleanup */
|
|
107
|
+
interface ListenerBindings {
|
|
108
|
+
output?: (data: string) => void;
|
|
109
|
+
summary?: (event: SummaryEvent) => void;
|
|
110
|
+
sessionEnd?: (event: SessionEndEvent) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Type alias for the wrapper - uses RelayPtyOrchestrator (relay-pty Rust binary) */
|
|
114
|
+
type AgentWrapper = RelayPtyOrchestrator;
|
|
115
|
+
|
|
116
|
+
interface ActiveWorker extends WorkerInfo {
|
|
117
|
+
pty: AgentWrapper;
|
|
118
|
+
logFile?: string;
|
|
119
|
+
listeners?: ListenerBindings;
|
|
120
|
+
userId?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Callback for agent death notifications */
|
|
124
|
+
export type OnAgentDeathCallback = (info: {
|
|
125
|
+
name: string;
|
|
126
|
+
exitCode: number | null;
|
|
127
|
+
agentId?: string;
|
|
128
|
+
resumeInstructions?: string;
|
|
129
|
+
/** Traceable error ID for support lookup */
|
|
130
|
+
errorId?: string;
|
|
131
|
+
}) => void;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ensure MCP permissions are pre-configured for the given CLI type.
|
|
135
|
+
* This prevents MCP approval prompts from blocking agent initialization.
|
|
136
|
+
*
|
|
137
|
+
* For Claude Code: Creates/updates .claude/settings.local.json with:
|
|
138
|
+
* - enableAllProjectMcpServers: true (auto-approve project MCP servers)
|
|
139
|
+
* - permissions.allow: ["mcp__agent-relay__*"] (pre-approve all agent-relay MCP tools)
|
|
140
|
+
*
|
|
141
|
+
* For Cursor: Creates/updates .cursor/settings.json with MCP permissions
|
|
142
|
+
* For Gemini: Creates/updates .gemini/settings.json with MCP permissions
|
|
143
|
+
* For Windsurf: Creates/updates .windsurf/settings.json with MCP permissions
|
|
144
|
+
* Other CLIs: May use CLI flags instead of config-based permissions
|
|
145
|
+
*
|
|
146
|
+
* @param projectRoot - The project root directory
|
|
147
|
+
* @param cliType - The CLI type (claude, codex, gemini, cursor, etc.)
|
|
148
|
+
* @param debug - Whether to log debug information
|
|
149
|
+
*/
|
|
150
|
+
export function ensureMcpPermissions(projectRoot: string, cliType: string, debug = false): void {
|
|
151
|
+
// Determine settings path based on CLI type
|
|
152
|
+
interface McpPermissionConfig {
|
|
153
|
+
settingsDir: string;
|
|
154
|
+
settingsFile: string;
|
|
155
|
+
permissionKey?: string; // If different from 'permissions.allow'
|
|
156
|
+
enableAllKey?: string; // If supports enableAllProjectMcpServers
|
|
157
|
+
globalSettingsDir?: string; // For global settings that enable project MCP
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const home = process.env.HOME || '';
|
|
161
|
+
const configMap: Record<string, McpPermissionConfig> = {
|
|
162
|
+
claude: {
|
|
163
|
+
// Use global settings for Claude
|
|
164
|
+
// enableAllProjectMcpServers enables project-local .mcp.json files
|
|
165
|
+
settingsDir: path.join(home, '.claude'),
|
|
166
|
+
settingsFile: 'settings.local.json',
|
|
167
|
+
permissionKey: 'permissions.allow',
|
|
168
|
+
enableAllKey: 'enableAllProjectMcpServers',
|
|
169
|
+
},
|
|
170
|
+
cursor: {
|
|
171
|
+
settingsDir: path.join(projectRoot, '.cursor'),
|
|
172
|
+
settingsFile: 'settings.json',
|
|
173
|
+
permissionKey: 'permissions.allow',
|
|
174
|
+
},
|
|
175
|
+
gemini: {
|
|
176
|
+
settingsDir: path.join(projectRoot, '.gemini'),
|
|
177
|
+
settingsFile: 'settings.json',
|
|
178
|
+
permissionKey: 'permissions.allow',
|
|
179
|
+
},
|
|
180
|
+
windsurf: {
|
|
181
|
+
settingsDir: path.join(projectRoot, '.windsurf'),
|
|
182
|
+
settingsFile: 'settings.json',
|
|
183
|
+
permissionKey: 'permissions.allow',
|
|
184
|
+
},
|
|
185
|
+
// Codex uses TOML config and --dangerously-bypass-approvals-and-sandbox flag
|
|
186
|
+
// OpenCode and Droid may not need config-based permissions
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Normalize CLI type
|
|
190
|
+
const normalizedCli = cliType.toLowerCase().replace(/^(claude|codex|gemini|cursor|agent|windsurf).*/, '$1');
|
|
191
|
+
|
|
192
|
+
// Map 'agent' (Cursor CLI) to 'cursor'
|
|
193
|
+
const effectiveCli = normalizedCli === 'agent' ? 'cursor' : normalizedCli;
|
|
194
|
+
|
|
195
|
+
const config = configMap[effectiveCli];
|
|
196
|
+
if (!config) {
|
|
197
|
+
// CLI doesn't use config-based MCP permissions (uses CLI flags instead)
|
|
198
|
+
if (debug) log.debug(`CLI ${cliType} uses flag-based permissions, skipping config setup`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const settingsPath = path.join(config.settingsDir, config.settingsFile);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// Ensure settings directory exists
|
|
206
|
+
if (!fs.existsSync(config.settingsDir)) {
|
|
207
|
+
fs.mkdirSync(config.settingsDir, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Read existing settings or start fresh
|
|
211
|
+
let settings: Record<string, unknown> = {};
|
|
212
|
+
if (fs.existsSync(settingsPath)) {
|
|
213
|
+
try {
|
|
214
|
+
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
215
|
+
settings = JSON.parse(content);
|
|
216
|
+
} catch {
|
|
217
|
+
// Invalid JSON, start fresh
|
|
218
|
+
settings = {};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Set enableAllProjectMcpServers if supported (Claude-specific)
|
|
223
|
+
if (config.enableAllKey && settings[config.enableAllKey] !== true) {
|
|
224
|
+
settings[config.enableAllKey] = true;
|
|
225
|
+
if (debug) log.debug(`Setting ${config.enableAllKey}: true`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Ensure permissions.allow includes agent-relay MCP
|
|
229
|
+
if (config.permissionKey) {
|
|
230
|
+
// Parse nested key (e.g., 'permissions.allow')
|
|
231
|
+
const keyParts = config.permissionKey.split('.');
|
|
232
|
+
let current: Record<string, unknown> = settings;
|
|
233
|
+
|
|
234
|
+
// Navigate/create nested structure
|
|
235
|
+
for (let i = 0; i < keyParts.length - 1; i++) {
|
|
236
|
+
const key = keyParts[i];
|
|
237
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
238
|
+
current[key] = {};
|
|
239
|
+
}
|
|
240
|
+
current = current[key] as Record<string, unknown>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Ensure allow list exists
|
|
244
|
+
const finalKey = keyParts[keyParts.length - 1];
|
|
245
|
+
if (!Array.isArray(current[finalKey])) {
|
|
246
|
+
current[finalKey] = [];
|
|
247
|
+
}
|
|
248
|
+
const allowList = current[finalKey] as string[];
|
|
249
|
+
|
|
250
|
+
// Add agent-relay MCP permission if not already present
|
|
251
|
+
// Format: mcp__<serverName>__* approves all tools from that server
|
|
252
|
+
const agentRelayPermission = 'mcp__agent-relay__*';
|
|
253
|
+
if (!allowList.includes(agentRelayPermission)) {
|
|
254
|
+
allowList.push(agentRelayPermission);
|
|
255
|
+
if (debug) log.debug(`Added MCP permission: ${agentRelayPermission}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Write updated settings
|
|
260
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
261
|
+
if (debug) log.debug(`MCP permissions configured at ${settingsPath}`);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// Log but don't fail - this is a best-effort optimization
|
|
264
|
+
log.warn('Failed to pre-configure MCP permissions', {
|
|
265
|
+
cli: cliType,
|
|
266
|
+
error: err instanceof Error ? err.message : String(err),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get MCP tools reference for spawned agents.
|
|
273
|
+
* Only included when MCP is configured for the project.
|
|
274
|
+
*/
|
|
275
|
+
function getMcpToolsReference(): string {
|
|
276
|
+
return [
|
|
277
|
+
'## MCP Tools Available',
|
|
278
|
+
'',
|
|
279
|
+
'You have access to MCP tools for agent communication (recommended over file protocol):',
|
|
280
|
+
'- `relay_send(to, message)` - Send message to agent/channel',
|
|
281
|
+
'- `relay_spawn(name, cli, task)` - Create worker agent',
|
|
282
|
+
'- `relay_inbox()` - Check your messages',
|
|
283
|
+
'- `relay_who()` - List online agents',
|
|
284
|
+
'- `relay_release(name)` - Stop a worker agent',
|
|
285
|
+
'- `relay_status()` - Check connection status',
|
|
286
|
+
'',
|
|
287
|
+
].join('\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get relay protocol instructions for a spawned agent.
|
|
292
|
+
* This provides the agent with the communication protocol it needs to work with the relay.
|
|
293
|
+
*
|
|
294
|
+
* Uses the legacy outbox path (/tmp/relay-outbox/) which is symlinked to workspace paths.
|
|
295
|
+
* This keeps agent instructions simple while supporting workspace isolation.
|
|
296
|
+
*
|
|
297
|
+
* @param agentName - Name of the agent
|
|
298
|
+
* @param options - Configuration options
|
|
299
|
+
* @param options.hasMcp - Whether MCP tools are available (based on .mcp.json existence)
|
|
300
|
+
* @param options.includeWorkflowConventions - Include ACK/DONE workflow conventions (default: false)
|
|
301
|
+
*/
|
|
302
|
+
function getRelayInstructions(agentName: string, options: { hasMcp?: boolean; includeWorkflowConventions?: boolean } = {}): string {
|
|
303
|
+
const { hasMcp = false, includeWorkflowConventions = false } = options;
|
|
304
|
+
// Get the outbox path template and replace variable with actual agent name
|
|
305
|
+
const outboxBase = getAgentOutboxTemplate(agentName);
|
|
306
|
+
|
|
307
|
+
const parts: string[] = [
|
|
308
|
+
'# Agent Relay Protocol',
|
|
309
|
+
'',
|
|
310
|
+
`You are agent "${agentName}" connected to Agent Relay for multi-agent coordination.`,
|
|
311
|
+
'',
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// Add MCP tools reference if available
|
|
315
|
+
if (hasMcp) {
|
|
316
|
+
parts.push(getMcpToolsReference());
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
parts.push(
|
|
320
|
+
'## Sending Messages',
|
|
321
|
+
'',
|
|
322
|
+
'Write a file to your outbox, then output the trigger:',
|
|
323
|
+
'',
|
|
324
|
+
'```bash',
|
|
325
|
+
`cat > ${outboxBase}/msg << 'EOF'`,
|
|
326
|
+
'TO: TargetAgent',
|
|
327
|
+
'',
|
|
328
|
+
'Your message here.',
|
|
329
|
+
'EOF',
|
|
330
|
+
'```',
|
|
331
|
+
'',
|
|
332
|
+
'Then output: `->relay-file:msg`',
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Only include ACK/DONE workflow conventions if explicitly requested
|
|
336
|
+
if (includeWorkflowConventions) {
|
|
337
|
+
parts.push(
|
|
338
|
+
'',
|
|
339
|
+
'## Communication Rules',
|
|
340
|
+
'',
|
|
341
|
+
'1. **ACK immediately** - When you receive a task:',
|
|
342
|
+
'```bash',
|
|
343
|
+
`cat > ${outboxBase}/ack << 'EOF'`,
|
|
344
|
+
'TO: Sender',
|
|
345
|
+
'',
|
|
346
|
+
'ACK: Brief description of task received',
|
|
347
|
+
'EOF',
|
|
348
|
+
'```',
|
|
349
|
+
'Then: `->relay-file:ack`',
|
|
350
|
+
'',
|
|
351
|
+
'2. **Report completion** - When done:',
|
|
352
|
+
'```bash',
|
|
353
|
+
`cat > ${outboxBase}/done << 'EOF'`,
|
|
354
|
+
'TO: Sender',
|
|
355
|
+
'',
|
|
356
|
+
'DONE: Brief summary of what was completed',
|
|
357
|
+
'EOF',
|
|
358
|
+
'```',
|
|
359
|
+
'Then: `->relay-file:done`',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
parts.push(
|
|
364
|
+
'',
|
|
365
|
+
'## Message Format',
|
|
366
|
+
'',
|
|
367
|
+
'```',
|
|
368
|
+
'TO: Target',
|
|
369
|
+
'THREAD: optional-thread',
|
|
370
|
+
'',
|
|
371
|
+
'Message body (everything after blank line)',
|
|
372
|
+
'```',
|
|
373
|
+
'',
|
|
374
|
+
'| TO Value | Behavior |',
|
|
375
|
+
'|----------|----------|',
|
|
376
|
+
'| `AgentName` | Direct message |',
|
|
377
|
+
'| `*` | Broadcast to all |',
|
|
378
|
+
'| `#channel` | Channel message |',
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return parts.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check if the relay-pty binary is available.
|
|
386
|
+
* Returns the path to the binary if found, null otherwise.
|
|
387
|
+
* Uses shared utility from @agent-relay/utils.
|
|
388
|
+
*/
|
|
389
|
+
function findRelayPtyBinary(): string | null {
|
|
390
|
+
return findRelayPtyBinaryUtil(__dirname);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Cached result of relay-pty binary check */
|
|
394
|
+
let relayPtyBinaryPath: string | null | undefined;
|
|
395
|
+
let relayPtyBinaryChecked = false;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check if relay-pty binary is available (cached).
|
|
399
|
+
* Returns true if the binary exists, false otherwise.
|
|
400
|
+
*/
|
|
401
|
+
function hasRelayPtyBinary(): boolean {
|
|
402
|
+
if (!relayPtyBinaryChecked) {
|
|
403
|
+
relayPtyBinaryPath = findRelayPtyBinary();
|
|
404
|
+
relayPtyBinaryChecked = true;
|
|
405
|
+
if (process.env.DEBUG_SPAWN === '1') {
|
|
406
|
+
if (relayPtyBinaryPath) {
|
|
407
|
+
log.debug(`relay-pty binary found: ${relayPtyBinaryPath}`);
|
|
408
|
+
} else {
|
|
409
|
+
log.debug('relay-pty binary not found, will use PtyWrapper fallback');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return relayPtyBinaryPath !== null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Options for AgentSpawner constructor */
|
|
417
|
+
export interface AgentSpawnerOptions {
|
|
418
|
+
projectRoot: string;
|
|
419
|
+
/** Explicit socket path for daemon connection (if not provided, derived from projectRoot) */
|
|
420
|
+
socketPath?: string;
|
|
421
|
+
/** Explicit team directory for agent registration files (if not provided, derived from projectRoot) */
|
|
422
|
+
teamDir?: string;
|
|
423
|
+
tmuxSession?: string;
|
|
424
|
+
dashboardPort?: number;
|
|
425
|
+
/**
|
|
426
|
+
* Callback to mark an agent as spawning (before HELLO completes).
|
|
427
|
+
* Messages sent to this agent will be queued for delivery after registration.
|
|
428
|
+
*/
|
|
429
|
+
onMarkSpawning?: (agentName: string) => void;
|
|
430
|
+
/**
|
|
431
|
+
* Callback to clear the spawning flag for an agent.
|
|
432
|
+
* Called when spawn fails or is cancelled.
|
|
433
|
+
*/
|
|
434
|
+
onClearSpawning?: (agentName: string) => void;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export class AgentSpawner {
|
|
438
|
+
private static readonly ONLINE_THRESHOLD_MS = 30_000;
|
|
439
|
+
private activeWorkers: Map<string, ActiveWorker> = new Map();
|
|
440
|
+
private agentsPath: string;
|
|
441
|
+
private registryPath: string;
|
|
442
|
+
private projectRoot: string;
|
|
443
|
+
private socketPath?: string;
|
|
444
|
+
private logsDir: string;
|
|
445
|
+
private workersPath: string;
|
|
446
|
+
private dashboardPort?: number;
|
|
447
|
+
private onAgentDeath?: OnAgentDeathCallback;
|
|
448
|
+
private cloudPersistence?: CloudPersistenceHandler;
|
|
449
|
+
private policyService?: AgentPolicyService;
|
|
450
|
+
private policyEnforcementEnabled = false;
|
|
451
|
+
private onMarkSpawning?: (agentName: string) => void;
|
|
452
|
+
private onClearSpawning?: (agentName: string) => void;
|
|
453
|
+
|
|
454
|
+
constructor(projectRoot: string, _tmuxSession?: string, dashboardPort?: number);
|
|
455
|
+
constructor(options: AgentSpawnerOptions);
|
|
456
|
+
constructor(projectRootOrOptions: string | AgentSpawnerOptions, _tmuxSession?: string, dashboardPort?: number) {
|
|
457
|
+
// Handle both old and new constructor signatures
|
|
458
|
+
const options: AgentSpawnerOptions = typeof projectRootOrOptions === 'string'
|
|
459
|
+
? { projectRoot: projectRootOrOptions, tmuxSession: _tmuxSession, dashboardPort }
|
|
460
|
+
: projectRootOrOptions;
|
|
461
|
+
|
|
462
|
+
const paths = getProjectPaths(options.projectRoot);
|
|
463
|
+
this.projectRoot = paths.projectRoot;
|
|
464
|
+
// Use explicit teamDir if provided (ensures spawner checks same files as daemon)
|
|
465
|
+
// This is critical in cloud workspaces where detectWorkspacePath may return different paths
|
|
466
|
+
const effectiveTeamDir = options.teamDir ?? paths.teamDir;
|
|
467
|
+
// Use connected-agents.json (live socket connections) instead of agents.json (historical registry)
|
|
468
|
+
// This ensures spawned agents have actual daemon connections for channel message delivery
|
|
469
|
+
this.agentsPath = path.join(effectiveTeamDir, 'connected-agents.json');
|
|
470
|
+
this.registryPath = path.join(effectiveTeamDir, 'agents.json');
|
|
471
|
+
|
|
472
|
+
// Debug: log path configuration
|
|
473
|
+
log.info(`AgentSpawner paths: projectRoot=${this.projectRoot} teamDir=${effectiveTeamDir} (explicit=${!!options.teamDir}) agentsPath=${this.agentsPath}`);
|
|
474
|
+
// Use explicit socketPath if provided (ensures spawned agents connect to same daemon)
|
|
475
|
+
// Otherwise derive from project paths
|
|
476
|
+
this.socketPath = options.socketPath ?? paths.socketPath;
|
|
477
|
+
this.logsDir = path.join(effectiveTeamDir, 'worker-logs');
|
|
478
|
+
this.workersPath = path.join(effectiveTeamDir, 'workers.json');
|
|
479
|
+
this.dashboardPort = options.dashboardPort;
|
|
480
|
+
|
|
481
|
+
// Store spawn tracking callbacks
|
|
482
|
+
this.onMarkSpawning = options.onMarkSpawning;
|
|
483
|
+
this.onClearSpawning = options.onClearSpawning;
|
|
484
|
+
|
|
485
|
+
// Ensure logs directory exists
|
|
486
|
+
fs.mkdirSync(this.logsDir, { recursive: true });
|
|
487
|
+
|
|
488
|
+
// Initialize policy service if enforcement is enabled
|
|
489
|
+
if (process.env.AGENT_POLICY_ENFORCEMENT === '1') {
|
|
490
|
+
this.policyEnforcementEnabled = true;
|
|
491
|
+
this.policyService = new AgentPolicyService({
|
|
492
|
+
projectRoot: this.projectRoot,
|
|
493
|
+
workspaceId: process.env.WORKSPACE_ID,
|
|
494
|
+
strictMode: process.env.AGENT_POLICY_STRICT === '1',
|
|
495
|
+
});
|
|
496
|
+
log.info('Policy enforcement enabled');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Set cloud policy fetcher for workspace-level policies
|
|
502
|
+
*/
|
|
503
|
+
setCloudPolicyFetcher(fetcher: CloudPolicyFetcher): void {
|
|
504
|
+
if (this.policyService) {
|
|
505
|
+
// Recreate policy service with cloud fetcher
|
|
506
|
+
this.policyService = new AgentPolicyService({
|
|
507
|
+
projectRoot: this.projectRoot,
|
|
508
|
+
workspaceId: process.env.WORKSPACE_ID,
|
|
509
|
+
cloudFetcher: fetcher,
|
|
510
|
+
strictMode: process.env.AGENT_POLICY_STRICT === '1',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get the policy service (for external access to policy checks)
|
|
517
|
+
*/
|
|
518
|
+
getPolicyService(): AgentPolicyService | undefined {
|
|
519
|
+
return this.policyService;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private async fetchGhTokenFromCloud(): Promise<string | null> {
|
|
523
|
+
const cloudApiUrl = process.env.CLOUD_API_URL || process.env.AGENT_RELAY_CLOUD_URL;
|
|
524
|
+
const workspaceId = process.env.WORKSPACE_ID;
|
|
525
|
+
const workspaceToken = process.env.WORKSPACE_TOKEN;
|
|
526
|
+
|
|
527
|
+
if (!cloudApiUrl || !workspaceId || !workspaceToken) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const normalizedUrl = cloudApiUrl.replace(/\/$/, '');
|
|
532
|
+
const url = `${normalizedUrl}/api/git/token?workspaceId=${encodeURIComponent(workspaceId)}`;
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Use AbortController for timeout (5 seconds - don't block spawning)
|
|
536
|
+
const controller = new AbortController();
|
|
537
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
538
|
+
|
|
539
|
+
const response = await fetch(url, {
|
|
540
|
+
headers: {
|
|
541
|
+
Authorization: `Bearer ${workspaceToken}`,
|
|
542
|
+
},
|
|
543
|
+
signal: controller.signal,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
clearTimeout(timeoutId);
|
|
547
|
+
|
|
548
|
+
if (!response.ok) {
|
|
549
|
+
log.warn(`Failed to fetch GH token from cloud: ${response.status} ${response.statusText}`);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const data = await response.json() as { userToken?: string | null; token?: string | null };
|
|
554
|
+
return data.userToken || data.token || null;
|
|
555
|
+
} catch (err) {
|
|
556
|
+
// Don't log timeout errors loudly - this is expected when cloud is unreachable
|
|
557
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
558
|
+
if (message.includes('abort')) {
|
|
559
|
+
log.info('Cloud API timeout (5s) - using local auth');
|
|
560
|
+
} else {
|
|
561
|
+
log.warn('Failed to fetch GH token from cloud', { error: message });
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private resolveGhTokenFromHostsFile(homeDir?: string): string | null {
|
|
568
|
+
const resolvedHome = homeDir || process.env.HOME;
|
|
569
|
+
const configHome = process.env.XDG_CONFIG_HOME || (resolvedHome ? path.join(resolvedHome, '.config') : undefined);
|
|
570
|
+
const candidates = new Set<string>();
|
|
571
|
+
if (configHome) {
|
|
572
|
+
candidates.add(path.join(configHome, 'gh', 'hosts.yml'));
|
|
573
|
+
}
|
|
574
|
+
if (resolvedHome) {
|
|
575
|
+
candidates.add(path.join(resolvedHome, '.config', 'gh', 'hosts.yml'));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const hostPath of candidates) {
|
|
579
|
+
if (!hostPath || !fs.existsSync(hostPath)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const content = fs.readFileSync(hostPath, 'utf8');
|
|
584
|
+
const token = extractGhTokenFromHosts(content);
|
|
585
|
+
if (token) {
|
|
586
|
+
return token;
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private async resolveGhTokenFromGhCli(): Promise<string | null> {
|
|
597
|
+
// Check common gh CLI installation paths across platforms
|
|
598
|
+
const ghPathCandidates = [
|
|
599
|
+
'/usr/bin/gh', // Linux package managers
|
|
600
|
+
'/usr/local/bin/gh', // Homebrew (Intel Mac), manual install
|
|
601
|
+
'/opt/homebrew/bin/gh', // Homebrew (Apple Silicon Mac)
|
|
602
|
+
'/home/linuxbrew/.linuxbrew/bin/gh', // Linuxbrew
|
|
603
|
+
];
|
|
604
|
+
|
|
605
|
+
const ghPath = ghPathCandidates.find((p) => fs.existsSync(p));
|
|
606
|
+
if (!ghPath) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return await new Promise((resolve) => {
|
|
611
|
+
execFile(ghPath, ['auth', 'token', '--hostname', 'github.com'], { timeout: 5000 }, (err, stdout) => {
|
|
612
|
+
if (err) {
|
|
613
|
+
resolve(null);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const token = stdout.trim();
|
|
617
|
+
resolve(token || null);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Resolve GitHub token using multiple fallback sources.
|
|
624
|
+
*
|
|
625
|
+
* Fallback order (same as git-credential-relay for consistency):
|
|
626
|
+
* 1. Environment - GH_TOKEN or GITHUB_TOKEN (fastest, set by entrypoint)
|
|
627
|
+
* 2. hosts.yml - gh CLI config file (~/.config/gh/hosts.yml)
|
|
628
|
+
* 3. gh CLI - execute `gh auth token` command
|
|
629
|
+
* 4. Cloud API - workspace-scoped token from Nango (requires network)
|
|
630
|
+
*
|
|
631
|
+
* Environment is checked first because:
|
|
632
|
+
* - It's the fastest (no I/O or network)
|
|
633
|
+
* - The entrypoint pre-fetches and caches GH_TOKEN at startup
|
|
634
|
+
* - This avoids delays when cloud API is slow/unreachable
|
|
635
|
+
*/
|
|
636
|
+
private async resolveGhToken(homeDir?: string): Promise<string | null> {
|
|
637
|
+
// 1. Check environment variables first (fastest - set by entrypoint at startup)
|
|
638
|
+
const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
639
|
+
if (envToken) {
|
|
640
|
+
return envToken;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 2. Parse gh CLI hosts.yml config file
|
|
644
|
+
const hostsToken = this.resolveGhTokenFromHostsFile(homeDir);
|
|
645
|
+
if (hostsToken) {
|
|
646
|
+
return hostsToken;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 3. Execute gh CLI if available
|
|
650
|
+
const cliToken = await this.resolveGhTokenFromGhCli();
|
|
651
|
+
if (cliToken) {
|
|
652
|
+
return cliToken;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 4. Try cloud API as last resort (may be slow or unreachable)
|
|
656
|
+
return await this.fetchGhTokenFromCloud();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Set the dashboard port (for nested spawn API calls).
|
|
661
|
+
* Called after the dashboard server starts and we know the actual port.
|
|
662
|
+
*/
|
|
663
|
+
setDashboardPort(port: number): void {
|
|
664
|
+
log.info(`Dashboard port set to ${port} - nested spawns now enabled`);
|
|
665
|
+
this.dashboardPort = port;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Set callback for agent death notifications.
|
|
670
|
+
* Called when an agent exits unexpectedly (non-zero exit code).
|
|
671
|
+
*/
|
|
672
|
+
setOnAgentDeath(callback: OnAgentDeathCallback): void {
|
|
673
|
+
this.onAgentDeath = callback;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Set cloud persistence handler for forwarding RelayPtyOrchestrator events.
|
|
678
|
+
* When set, 'summary' and 'session-end' events from spawned agents
|
|
679
|
+
* are forwarded to the handler for cloud persistence (PostgreSQL/Redis).
|
|
680
|
+
*
|
|
681
|
+
* Note: Enable via RELAY_CLOUD_ENABLED=true environment variable.
|
|
682
|
+
*/
|
|
683
|
+
setCloudPersistence(handler: CloudPersistenceHandler): void {
|
|
684
|
+
this.cloudPersistence = handler;
|
|
685
|
+
log.info('Cloud persistence handler set');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Bind cloud persistence event handlers to a RelayPtyOrchestrator.
|
|
690
|
+
* Returns the listener references for cleanup.
|
|
691
|
+
*/
|
|
692
|
+
private bindCloudPersistenceEvents(name: string, pty: AgentWrapper): Partial<ListenerBindings> {
|
|
693
|
+
if (!this.cloudPersistence) return {};
|
|
694
|
+
|
|
695
|
+
const summaryListener = async (event: SummaryEvent) => {
|
|
696
|
+
try {
|
|
697
|
+
await this.cloudPersistence!.onSummary(name, event);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
log.error(`Cloud persistence summary error for ${name}`, { error: err instanceof Error ? err.message : String(err) });
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const sessionEndListener = async (event: SessionEndEvent) => {
|
|
704
|
+
try {
|
|
705
|
+
await this.cloudPersistence!.onSessionEnd(name, event);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
log.error(`Cloud persistence session-end error for ${name}`, { error: err instanceof Error ? err.message : String(err) });
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
pty.on('summary', summaryListener);
|
|
712
|
+
pty.on('session-end', sessionEndListener);
|
|
713
|
+
|
|
714
|
+
return { summary: summaryListener, sessionEnd: sessionEndListener };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Unbind all tracked listeners from a RelayPtyOrchestrator.
|
|
719
|
+
*/
|
|
720
|
+
private unbindListeners(pty: AgentWrapper, listeners?: ListenerBindings): void {
|
|
721
|
+
if (!listeners) return;
|
|
722
|
+
|
|
723
|
+
if (listeners.output) {
|
|
724
|
+
pty.off('output', listeners.output);
|
|
725
|
+
}
|
|
726
|
+
if (listeners.summary) {
|
|
727
|
+
pty.off('summary', listeners.summary);
|
|
728
|
+
}
|
|
729
|
+
if (listeners.sessionEnd) {
|
|
730
|
+
pty.off('session-end', listeners.sessionEnd);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Spawn a new worker agent using relay-pty
|
|
736
|
+
*/
|
|
737
|
+
async spawn(request: SpawnRequest): Promise<SpawnResult> {
|
|
738
|
+
const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive } = request;
|
|
739
|
+
const debug = process.env.DEBUG_SPAWN === '1';
|
|
740
|
+
|
|
741
|
+
// Validate agent name to prevent path traversal attacks
|
|
742
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
743
|
+
return {
|
|
744
|
+
success: false,
|
|
745
|
+
name,
|
|
746
|
+
error: `Invalid agent name: "${name}" contains path traversal characters`,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Check if worker already exists in this spawner
|
|
751
|
+
if (this.activeWorkers.has(name)) {
|
|
752
|
+
return {
|
|
753
|
+
success: false,
|
|
754
|
+
name,
|
|
755
|
+
error: `Agent "${name}" is already running. Use a different name or release the existing agent first.`,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Check if agent is already connected to daemon (prevents duplicate connection storms)
|
|
760
|
+
if (this.isAgentConnected(name)) {
|
|
761
|
+
return {
|
|
762
|
+
success: false,
|
|
763
|
+
name,
|
|
764
|
+
error: `Agent "${name}" is already connected to the daemon. Use a different name or wait for the existing agent to disconnect.`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Enforce agent limit based on plan (MAX_AGENTS is set by provisioner based on plan)
|
|
769
|
+
const maxAgents = parseInt(process.env.MAX_AGENTS ?? '', 10) || 10_000;
|
|
770
|
+
const currentAgentCount = this.activeWorkers.size;
|
|
771
|
+
if (currentAgentCount >= maxAgents) {
|
|
772
|
+
log.warn(`Agent limit reached: ${currentAgentCount}/${maxAgents}`);
|
|
773
|
+
return {
|
|
774
|
+
success: false,
|
|
775
|
+
name,
|
|
776
|
+
error: `Agent limit reached (${currentAgentCount}/${maxAgents}). Upgrade your plan for more agents.`,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Policy enforcement: check if the spawner is authorized to spawn this agent
|
|
781
|
+
if (this.policyEnforcementEnabled && this.policyService && spawnerName) {
|
|
782
|
+
const decision = await this.policyService.canSpawn(spawnerName, name, cli);
|
|
783
|
+
if (!decision.allowed) {
|
|
784
|
+
log.warn(`Policy blocked spawn: ${spawnerName} -> ${name}: ${decision.reason}`);
|
|
785
|
+
return {
|
|
786
|
+
success: false,
|
|
787
|
+
name,
|
|
788
|
+
error: `Policy denied: ${decision.reason}`,
|
|
789
|
+
policyDecision: decision,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (debug) {
|
|
793
|
+
log.debug(`Policy allowed spawn: ${spawnerName} -> ${name} (source: ${decision.policySource})`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
// Parse CLI command and apply mapping (e.g., cursor -> agent)
|
|
799
|
+
const cliParts = cli.split(' ');
|
|
800
|
+
const rawCommandName = cliParts[0];
|
|
801
|
+
const commandName = CLI_COMMAND_MAP[rawCommandName] || rawCommandName;
|
|
802
|
+
const args = cliParts.slice(1);
|
|
803
|
+
|
|
804
|
+
if (commandName !== rawCommandName && debug) {
|
|
805
|
+
log.debug(`Mapped CLI '${rawCommandName}' -> '${commandName}'`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Resolve full path to avoid posix_spawnp failures
|
|
809
|
+
const command = resolveCommand(commandName);
|
|
810
|
+
if (debug) log.debug(`Resolved '${commandName}' -> '${command}'`);
|
|
811
|
+
if (command === commandName && !commandName.startsWith('/')) {
|
|
812
|
+
// Command wasn't resolved - it might not exist
|
|
813
|
+
log.warn(`Could not resolve path for '${commandName}', spawn may fail`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Pre-configure MCP permissions for all supported CLIs
|
|
817
|
+
// This creates/updates CLI-specific settings files with agent-relay permissions
|
|
818
|
+
ensureMcpPermissions(this.projectRoot, commandName, debug);
|
|
819
|
+
|
|
820
|
+
// Add auto-accept flags for non-interactive agents (normal spawns, not setup terminals)
|
|
821
|
+
// When interactive=true (setup flows), we SKIP these flags so users can respond to prompts
|
|
822
|
+
const isClaudeCli = commandName.startsWith('claude');
|
|
823
|
+
const isCursorCli = commandName === 'agent' || rawCommandName === 'cursor';
|
|
824
|
+
|
|
825
|
+
if (!interactive) {
|
|
826
|
+
// Add --dangerously-skip-permissions for Claude agents
|
|
827
|
+
if (isClaudeCli && !args.includes('--dangerously-skip-permissions')) {
|
|
828
|
+
args.push('--dangerously-skip-permissions');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Add --force for Cursor agents (auto-approve tool usage in non-interactive mode)
|
|
832
|
+
if (isCursorCli && !args.includes('--force')) {
|
|
833
|
+
args.push('--force');
|
|
834
|
+
}
|
|
835
|
+
} else {
|
|
836
|
+
// Interactive mode: log that we're skipping auto-accept flags
|
|
837
|
+
if (debug) log.debug(`Interactive mode: skipping auto-accept flags for ${name}`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Apply agent config (model, --agent flag) from .claude/agents/ if available
|
|
841
|
+
// This ensures spawned agents respect their profile settings
|
|
842
|
+
if (isClaudeCli) {
|
|
843
|
+
// Get agent config for model tracking and CLI variant selection
|
|
844
|
+
const agentConfig = findAgentConfig(name, this.projectRoot);
|
|
845
|
+
const modelFromProfile = agentConfig?.model?.trim();
|
|
846
|
+
|
|
847
|
+
// Map model to CLI variant (e.g., 'opus' -> 'claude:opus')
|
|
848
|
+
// This allows agent profiles to specify model preferences
|
|
849
|
+
const cliVariant = modelFromProfile
|
|
850
|
+
? mapModelToCli(modelFromProfile)
|
|
851
|
+
: mapModelToCli(); // defaults to claude:sonnet
|
|
852
|
+
|
|
853
|
+
// Extract effective model name for logging
|
|
854
|
+
const effectiveModel = modelFromProfile || 'opus';
|
|
855
|
+
|
|
856
|
+
const configuredArgs = buildClaudeArgs(name, args, this.projectRoot);
|
|
857
|
+
// Replace args with configured version (includes --model and --agent if found)
|
|
858
|
+
args.length = 0;
|
|
859
|
+
args.push(...configuredArgs);
|
|
860
|
+
|
|
861
|
+
// Cost tracking: log which model is being used
|
|
862
|
+
log.info(`Agent ${name}: model=${effectiveModel}, cli=${cli}, variant=${cliVariant}`);
|
|
863
|
+
if (debug) log.debug(`Applied agent config for ${name}: ${args.join(' ')}`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Add auto-accept flags for Codex and Gemini (only in non-interactive mode)
|
|
867
|
+
const isCodexCli = commandName.startsWith('codex');
|
|
868
|
+
const isGeminiCli = commandName === 'gemini';
|
|
869
|
+
|
|
870
|
+
if (!interactive) {
|
|
871
|
+
// Add --dangerously-bypass-approvals-and-sandbox for Codex agents
|
|
872
|
+
if (isCodexCli && !args.includes('--dangerously-bypass-approvals-and-sandbox')) {
|
|
873
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Add --yolo for Gemini agents (auto-accept all prompts)
|
|
877
|
+
if (isGeminiCli && !args.includes('--yolo')) {
|
|
878
|
+
args.push('--yolo');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Auto-install MCP config if not present (project-local)
|
|
883
|
+
// Uses .mcp.json in the project root - doesn't modify global settings
|
|
884
|
+
// Feature gated: set RELAY_MCP_AUTO_INSTALL=1 to enable
|
|
885
|
+
const projectMcpConfigPath = path.join(this.projectRoot, '.mcp.json');
|
|
886
|
+
const mcpSocketPath = path.join(this.projectRoot, '.agent-relay', 'relay.sock');
|
|
887
|
+
const hasMcpConfig = fs.existsSync(projectMcpConfigPath);
|
|
888
|
+
const mcpAutoInstallEnabled = process.env.RELAY_MCP_AUTO_INSTALL === '1';
|
|
889
|
+
|
|
890
|
+
if (!hasMcpConfig && mcpAutoInstallEnabled) {
|
|
891
|
+
try {
|
|
892
|
+
const result = installMcpConfig(projectMcpConfigPath, {
|
|
893
|
+
configKey: 'mcpServers',
|
|
894
|
+
// Set RELAY_SOCKET so MCP server finds daemon regardless of CWD
|
|
895
|
+
env: { RELAY_SOCKET: mcpSocketPath },
|
|
896
|
+
});
|
|
897
|
+
if (result.success) {
|
|
898
|
+
if (debug) log.debug(`Auto-installed MCP config at ${projectMcpConfigPath}`);
|
|
899
|
+
} else {
|
|
900
|
+
log.warn(`Failed to auto-install MCP config: ${result.error}`);
|
|
901
|
+
}
|
|
902
|
+
} catch (err) {
|
|
903
|
+
log.warn('Failed to auto-install MCP config', {
|
|
904
|
+
error: err instanceof Error ? err.message : String(err),
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Check if MCP tools are available
|
|
910
|
+
// Must verify BOTH conditions (matching inbox hook behavior from commit 18bab59):
|
|
911
|
+
// 1. MCP config exists (user or project scope)
|
|
912
|
+
// 2. Relay daemon socket is accessible (daemon must be running)
|
|
913
|
+
// Without both, MCP context would be shown but tools wouldn't work
|
|
914
|
+
// Use the actual socket path from config (project-local .agent-relay/relay.sock)
|
|
915
|
+
// or fall back to environment variable
|
|
916
|
+
const relaySocket = this.socketPath || process.env.RELAY_SOCKET || path.join(this.projectRoot, '.agent-relay', 'relay.sock');
|
|
917
|
+
let hasMcp = false;
|
|
918
|
+
// Check either user-scope or project-scope MCP config
|
|
919
|
+
// hasMcpConfig was already computed above
|
|
920
|
+
if (hasMcpConfig) {
|
|
921
|
+
try {
|
|
922
|
+
hasMcp = fs.statSync(relaySocket).isSocket();
|
|
923
|
+
} catch {
|
|
924
|
+
// Socket doesn't exist or isn't accessible - daemon not running
|
|
925
|
+
hasMcp = false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (debug && hasMcp) log.debug(`MCP tools available for ${name} (MCP config found, socket ${relaySocket})`);
|
|
929
|
+
|
|
930
|
+
// Inject relay protocol instructions via CLI-specific system prompt
|
|
931
|
+
let relayInstructions = getRelayInstructions(name, { hasMcp, includeWorkflowConventions });
|
|
932
|
+
|
|
933
|
+
// Compose role-specific prompts if agent has a role defined in .claude/agents/
|
|
934
|
+
const agentConfigForRole = isClaudeCli ? findAgentConfig(name, this.projectRoot) : null;
|
|
935
|
+
if (agentConfigForRole?.role) {
|
|
936
|
+
const validRoles: AgentRole[] = ['planner', 'worker', 'reviewer', 'lead', 'shadow'];
|
|
937
|
+
const role = agentConfigForRole.role.toLowerCase() as AgentRole;
|
|
938
|
+
if (validRoles.includes(role)) {
|
|
939
|
+
try {
|
|
940
|
+
const composed = await composeForAgent(
|
|
941
|
+
{ name, role },
|
|
942
|
+
this.projectRoot,
|
|
943
|
+
{ taskDescription: task }
|
|
944
|
+
);
|
|
945
|
+
if (composed.content) {
|
|
946
|
+
relayInstructions = `${composed.content}\n\n---\n\n${relayInstructions}`;
|
|
947
|
+
if (debug) log.debug(`Composed role prompt for ${name} (role: ${role})`);
|
|
948
|
+
}
|
|
949
|
+
} catch (err: any) {
|
|
950
|
+
log.warn(`Failed to compose role prompt for ${name}: ${err.message}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (isClaudeCli && !args.includes('--append-system-prompt')) {
|
|
956
|
+
args.push('--append-system-prompt', relayInstructions);
|
|
957
|
+
} else if (isCodexCli && !args.some(a => a.includes('developer_instructions'))) {
|
|
958
|
+
args.push('--config', `developer_instructions=${relayInstructions}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Codex requires an initial prompt in TTY mode (unlike Claude which waits for input)
|
|
962
|
+
// Pass the task as the initial prompt, or a generic "ready" message if no task
|
|
963
|
+
if (isCodexCli) {
|
|
964
|
+
const initialPrompt = task || 'You are ready. Wait for messages from the relay system.';
|
|
965
|
+
args.push(initialPrompt);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Cursor requires 'login' subcommand for interactive auth flows (setup terminals)
|
|
969
|
+
// Unlike Claude/Codex which auto-start auth, Cursor needs explicit 'agent login'
|
|
970
|
+
if (isCursorCli && interactive && !task) {
|
|
971
|
+
args.unshift('login');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (debug) log.debug(`Spawning ${name} with: ${command} ${args.join(' ')}`);
|
|
975
|
+
|
|
976
|
+
// Create PtyWrapper config
|
|
977
|
+
// Use dashboardPort for nested spawns (API-based, works in non-TTY contexts)
|
|
978
|
+
// Fall back to callbacks only if no dashboardPort is not set
|
|
979
|
+
// Note: Spawned agents CAN spawn sub-workers intentionally - the parser is strict enough
|
|
980
|
+
// to avoid accidental spawns from documentation text (requires line start, PascalCase, known CLI)
|
|
981
|
+
// Use request.cwd if specified, otherwise use projectRoot
|
|
982
|
+
// Validate cwd to prevent path traversal attacks
|
|
983
|
+
let agentCwd: string;
|
|
984
|
+
if (request.cwd && typeof request.cwd === 'string') {
|
|
985
|
+
// Resolve cwd relative to project root and ensure it stays within that root
|
|
986
|
+
const resolvedCwd = path.resolve(this.projectRoot, request.cwd);
|
|
987
|
+
const normalizedProjectRoot = path.resolve(this.projectRoot);
|
|
988
|
+
const projectRootWithSep = normalizedProjectRoot.endsWith(path.sep)
|
|
989
|
+
? normalizedProjectRoot
|
|
990
|
+
: normalizedProjectRoot + path.sep;
|
|
991
|
+
|
|
992
|
+
// Ensure the resolved cwd is within the project root to prevent traversal
|
|
993
|
+
if (resolvedCwd !== normalizedProjectRoot && !resolvedCwd.startsWith(projectRootWithSep)) {
|
|
994
|
+
return {
|
|
995
|
+
success: false,
|
|
996
|
+
name,
|
|
997
|
+
error: `Invalid cwd: "${request.cwd}" must be within the project root`,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
agentCwd = resolvedCwd;
|
|
1001
|
+
} else {
|
|
1002
|
+
agentCwd = this.projectRoot;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Log whether nested spawning will be enabled for this agent
|
|
1006
|
+
log.info(`Spawning ${name}: dashboardPort=${this.dashboardPort || 'none'} (${this.dashboardPort ? 'nested spawns enabled' : 'nested spawns disabled'})`);
|
|
1007
|
+
|
|
1008
|
+
let userEnv: Record<string, string> | undefined;
|
|
1009
|
+
if (userId) {
|
|
1010
|
+
try {
|
|
1011
|
+
const userDirService = getUserDirectoryService();
|
|
1012
|
+
userEnv = userDirService.getUserEnvironment(userId);
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
log.warn('Failed to resolve user environment, using default', {
|
|
1015
|
+
userId,
|
|
1016
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const mergedUserEnv = { ...(userEnv ?? {}) };
|
|
1022
|
+
if (!mergedUserEnv.GH_TOKEN) {
|
|
1023
|
+
const ghToken = await this.resolveGhToken(userEnv?.HOME);
|
|
1024
|
+
if (ghToken) {
|
|
1025
|
+
mergedUserEnv.GH_TOKEN = ghToken;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (Object.keys(mergedUserEnv).length > 0) {
|
|
1029
|
+
userEnv = mergedUserEnv;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (debug) log.debug(`Socket path for ${name}: ${this.socketPath ?? 'undefined'}`);
|
|
1033
|
+
|
|
1034
|
+
// Require relay-pty binary
|
|
1035
|
+
if (!hasRelayPtyBinary()) {
|
|
1036
|
+
const checkedPaths = getLastSearchPaths();
|
|
1037
|
+
const tracedError = createTraceableError('relay-pty binary not found', {
|
|
1038
|
+
agentName: name,
|
|
1039
|
+
cli,
|
|
1040
|
+
callerDir: __dirname,
|
|
1041
|
+
checkedPaths: checkedPaths.slice(0, 5), // First 5 paths for brevity
|
|
1042
|
+
totalPathsChecked: checkedPaths.length,
|
|
1043
|
+
hint: 'Set RELAY_PTY_BINARY env var to override, or reinstall: npm install agent-relay',
|
|
1044
|
+
});
|
|
1045
|
+
log.error(tracedError.logMessage);
|
|
1046
|
+
if (debug) {
|
|
1047
|
+
log.debug('All paths checked for relay-pty binary:');
|
|
1048
|
+
checkedPaths.forEach((p, i) => log.debug(` ${i + 1}. ${p}`));
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
success: false,
|
|
1052
|
+
name,
|
|
1053
|
+
error: tracedError.userMessage,
|
|
1054
|
+
errorId: tracedError.errorId,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Common exit handler for both wrapper types
|
|
1059
|
+
const onExitHandler = (code: number) => {
|
|
1060
|
+
if (debug) log.debug(`Worker ${name} exited with code ${code}`);
|
|
1061
|
+
|
|
1062
|
+
// Get the agentId and clean up listeners before removing from active workers
|
|
1063
|
+
const worker = this.activeWorkers.get(name);
|
|
1064
|
+
const agentId = worker?.pty?.getAgentId?.();
|
|
1065
|
+
if (worker?.listeners) {
|
|
1066
|
+
this.unbindListeners(worker.pty, worker.listeners);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
this.activeWorkers.delete(name);
|
|
1070
|
+
try {
|
|
1071
|
+
this.saveWorkersMetadata();
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
log.error('Failed to save metadata on exit', { error: err instanceof Error ? err.message : String(err) });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Notify if agent died unexpectedly (non-zero exit)
|
|
1077
|
+
if (code !== 0 && code !== null && this.onAgentDeath) {
|
|
1078
|
+
const crashError = createTraceableError('Agent crashed unexpectedly', {
|
|
1079
|
+
agentName: name,
|
|
1080
|
+
exitCode: code,
|
|
1081
|
+
cli,
|
|
1082
|
+
agentId,
|
|
1083
|
+
});
|
|
1084
|
+
log.error(crashError.logMessage);
|
|
1085
|
+
this.onAgentDeath({
|
|
1086
|
+
name,
|
|
1087
|
+
exitCode: code,
|
|
1088
|
+
agentId,
|
|
1089
|
+
errorId: crashError.errorId,
|
|
1090
|
+
resumeInstructions: agentId
|
|
1091
|
+
? `To resume this agent's work, use: --resume ${agentId}`
|
|
1092
|
+
: undefined,
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// Common spawn/release handlers
|
|
1098
|
+
const onSpawnHandler = this.dashboardPort ? undefined : async (workerName: string, workerCli: string, workerTask: string) => {
|
|
1099
|
+
if (debug) log.debug(`Nested spawn: ${workerName}`);
|
|
1100
|
+
await this.spawn({
|
|
1101
|
+
name: workerName,
|
|
1102
|
+
cli: workerCli,
|
|
1103
|
+
task: workerTask,
|
|
1104
|
+
userId,
|
|
1105
|
+
});
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const onReleaseHandler = this.dashboardPort ? undefined : async (workerName: string) => {
|
|
1109
|
+
if (debug) log.debug(`Release request: ${workerName}`);
|
|
1110
|
+
await this.release(workerName);
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
// Create RelayPtyOrchestrator (relay-pty Rust binary)
|
|
1114
|
+
const ptyConfig: RelayPtyOrchestratorConfig = {
|
|
1115
|
+
name,
|
|
1116
|
+
command,
|
|
1117
|
+
args,
|
|
1118
|
+
socketPath: this.socketPath,
|
|
1119
|
+
cwd: agentCwd,
|
|
1120
|
+
dashboardPort: this.dashboardPort,
|
|
1121
|
+
env: {
|
|
1122
|
+
...userEnv,
|
|
1123
|
+
...(spawnerName ? { AGENT_RELAY_SPAWNER: spawnerName } : {}),
|
|
1124
|
+
// Pass socket path for MCP server discovery
|
|
1125
|
+
// This allows the MCP server (started by Claude Code) to connect to the daemon
|
|
1126
|
+
...(relaySocket ? { RELAY_SOCKET: relaySocket } : {}),
|
|
1127
|
+
// Pass agent name so MCP server knows its identity
|
|
1128
|
+
RELAY_AGENT_NAME: name,
|
|
1129
|
+
},
|
|
1130
|
+
streamLogs: true,
|
|
1131
|
+
shadowOf: request.shadowOf,
|
|
1132
|
+
shadowSpeakOn: request.shadowSpeakOn,
|
|
1133
|
+
skipContinuity: true,
|
|
1134
|
+
onSpawn: onSpawnHandler,
|
|
1135
|
+
onRelease: onReleaseHandler,
|
|
1136
|
+
onExit: onExitHandler,
|
|
1137
|
+
headless: true, // Force headless mode for spawned agents to enable task injection via stdin
|
|
1138
|
+
// In cloud environments (WORKSPACE_ID set), limit CPU per agent to prevent
|
|
1139
|
+
// one agent (e.g., running npm install) from starving others
|
|
1140
|
+
// Default: 100% of one core per agent. Set AGENT_CPU_LIMIT to override.
|
|
1141
|
+
cpuLimitPercent: process.env.WORKSPACE_ID
|
|
1142
|
+
? parseInt(process.env.AGENT_CPU_LIMIT || '100', 10)
|
|
1143
|
+
: undefined,
|
|
1144
|
+
};
|
|
1145
|
+
const pty = new RelayPtyOrchestrator(ptyConfig);
|
|
1146
|
+
if (debug) log.debug(`Using RelayPtyOrchestrator for ${name}`);
|
|
1147
|
+
|
|
1148
|
+
// Track listener references for proper cleanup
|
|
1149
|
+
const listeners: ListenerBindings = {};
|
|
1150
|
+
|
|
1151
|
+
// Hook up output events for live log streaming
|
|
1152
|
+
const outputListener = (data: string) => {
|
|
1153
|
+
// Broadcast to any connected WebSocket clients via global function
|
|
1154
|
+
const broadcast = (global as any).__broadcastLogOutput;
|
|
1155
|
+
if (broadcast) {
|
|
1156
|
+
broadcast(name, data);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
pty.on('output', outputListener);
|
|
1160
|
+
listeners.output = outputListener;
|
|
1161
|
+
|
|
1162
|
+
// Bind cloud persistence events (if enabled) and store references
|
|
1163
|
+
const cloudListeners = this.bindCloudPersistenceEvents(name, pty);
|
|
1164
|
+
if (cloudListeners.summary) listeners.summary = cloudListeners.summary;
|
|
1165
|
+
if (cloudListeners.sessionEnd) listeners.sessionEnd = cloudListeners.sessionEnd;
|
|
1166
|
+
|
|
1167
|
+
// Mark agent as spawning BEFORE starting PTY
|
|
1168
|
+
// This allows messages sent to this agent to be queued until HELLO completes
|
|
1169
|
+
if (this.onMarkSpawning) {
|
|
1170
|
+
this.onMarkSpawning(name);
|
|
1171
|
+
if (debug) log.debug(`Marked ${name} as spawning`);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
await pty.start();
|
|
1175
|
+
|
|
1176
|
+
if (debug) log.debug(`PTY started, pid: ${pty.pid}`);
|
|
1177
|
+
|
|
1178
|
+
// Cursor CLI shows "Press any key to log in..." prompt that blocks all progress
|
|
1179
|
+
// Send a keystroke after startup to bypass this initial prompt
|
|
1180
|
+
// Only do this for interactive/setup flows where auth is needed
|
|
1181
|
+
// For normal spawns (already authenticated), this prompt won't appear
|
|
1182
|
+
if (isCursorCli && interactive) {
|
|
1183
|
+
// Wait a moment for CLI to initialize and show the prompt
|
|
1184
|
+
await sleep(1500);
|
|
1185
|
+
if (debug) log.debug(`Sending initial keystroke for Cursor setup to bypass "Press any key" prompt`);
|
|
1186
|
+
try {
|
|
1187
|
+
// Send Enter key to proceed past the initial prompt
|
|
1188
|
+
await pty.write('\r');
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
log.warn(`Failed to send initial keystroke for Cursor: ${err}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Wait for the agent to register with the daemon
|
|
1195
|
+
const registered = await this.waitForAgentRegistration(name, 30_000, 500);
|
|
1196
|
+
if (!registered) {
|
|
1197
|
+
const tracedError = createTraceableError('Agent registration timeout', {
|
|
1198
|
+
agentName: name,
|
|
1199
|
+
cli,
|
|
1200
|
+
pid: pty.pid,
|
|
1201
|
+
timeoutMs: 30_000,
|
|
1202
|
+
});
|
|
1203
|
+
log.error(tracedError.logMessage);
|
|
1204
|
+
// Clear spawning flag since spawn failed
|
|
1205
|
+
if (this.onClearSpawning) {
|
|
1206
|
+
this.onClearSpawning(name);
|
|
1207
|
+
}
|
|
1208
|
+
await pty.kill();
|
|
1209
|
+
return {
|
|
1210
|
+
success: false,
|
|
1211
|
+
name,
|
|
1212
|
+
error: tracedError.userMessage,
|
|
1213
|
+
errorId: tracedError.errorId,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Send task to the newly spawned agent if provided
|
|
1218
|
+
// We do this AFTER registration AND after the orchestrator is FULLY ready for messages
|
|
1219
|
+
// This includes: CLI started, CLI idle, socket connected, readyForMessages flag set
|
|
1220
|
+
if (task && task.trim()) {
|
|
1221
|
+
const maxRetries = 3;
|
|
1222
|
+
const retryDelayMs = 2000;
|
|
1223
|
+
let taskSent = false;
|
|
1224
|
+
|
|
1225
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1226
|
+
try {
|
|
1227
|
+
// Wait for full orchestrator readiness (CLI + socket + internal flags)
|
|
1228
|
+
if ('waitUntilReadyForMessages' in pty) {
|
|
1229
|
+
const orchestrator = pty as RelayPtyOrchestrator;
|
|
1230
|
+
const ready = await orchestrator.waitUntilReadyForMessages(20000, 100);
|
|
1231
|
+
if (!ready) {
|
|
1232
|
+
// Log retry attempts at DEBUG level to avoid terminal noise
|
|
1233
|
+
log.debug(`Attempt ${attempt}/${maxRetries}: ${name} not ready for messages within timeout`);
|
|
1234
|
+
if (attempt < maxRetries) {
|
|
1235
|
+
await sleep(retryDelayMs);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
log.error(`${name} failed to become ready after ${maxRetries} attempts - task may be lost`);
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
} else if ('waitUntilCliReady' in pty) {
|
|
1242
|
+
// Fallback for older wrapper types
|
|
1243
|
+
await (pty as RelayPtyOrchestrator).waitUntilCliReady(15000, 100);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Inject task via socket (with verification and retries)
|
|
1247
|
+
const success = await pty.injectTask(task, spawnerName || 'spawner');
|
|
1248
|
+
if (success) {
|
|
1249
|
+
taskSent = true;
|
|
1250
|
+
if (debug) log.debug(`Task injected to ${name} (attempt ${attempt})`);
|
|
1251
|
+
break;
|
|
1252
|
+
} else {
|
|
1253
|
+
throw new Error('Task injection returned false');
|
|
1254
|
+
}
|
|
1255
|
+
} catch (err: any) {
|
|
1256
|
+
// Log retry attempts at DEBUG level to avoid terminal noise
|
|
1257
|
+
// Only the final summary (if all attempts fail) is logged at ERROR level
|
|
1258
|
+
log.debug(`Attempt ${attempt}/${maxRetries}: Error injecting task for ${name}: ${err.message}`);
|
|
1259
|
+
if (attempt < maxRetries) {
|
|
1260
|
+
await sleep(retryDelayMs);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (!taskSent) {
|
|
1266
|
+
const tracedError = createTraceableError('Task injection failed', {
|
|
1267
|
+
agentName: name,
|
|
1268
|
+
cli,
|
|
1269
|
+
attempts: maxRetries,
|
|
1270
|
+
taskLength: task.length,
|
|
1271
|
+
});
|
|
1272
|
+
log.error(`CRITICAL: ${tracedError.logMessage}`);
|
|
1273
|
+
// Note: We don't return an error here because the agent is running,
|
|
1274
|
+
// but we track the errorId so support can investigate if user reports it
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Track the worker
|
|
1279
|
+
const workerInfo: ActiveWorker = {
|
|
1280
|
+
name,
|
|
1281
|
+
cli,
|
|
1282
|
+
task,
|
|
1283
|
+
team,
|
|
1284
|
+
userId,
|
|
1285
|
+
spawnedAt: Date.now(),
|
|
1286
|
+
pid: pty.pid,
|
|
1287
|
+
pty,
|
|
1288
|
+
logFile: pty.logPath,
|
|
1289
|
+
listeners, // Store for cleanup
|
|
1290
|
+
};
|
|
1291
|
+
this.activeWorkers.set(name, workerInfo);
|
|
1292
|
+
this.saveWorkersMetadata();
|
|
1293
|
+
|
|
1294
|
+
const teamInfo = team ? ` [team: ${team}]` : '';
|
|
1295
|
+
const shadowInfo = request.shadowOf ? ` [shadow of: ${request.shadowOf}]` : '';
|
|
1296
|
+
log.info(`Spawned ${name} (${cli})${teamInfo}${shadowInfo} [pid: ${pty.pid}]`);
|
|
1297
|
+
|
|
1298
|
+
return {
|
|
1299
|
+
success: true,
|
|
1300
|
+
name,
|
|
1301
|
+
pid: pty.pid,
|
|
1302
|
+
};
|
|
1303
|
+
} catch (err: any) {
|
|
1304
|
+
const tracedError = createTraceableError('Agent spawn failed', {
|
|
1305
|
+
agentName: name,
|
|
1306
|
+
cli,
|
|
1307
|
+
task: task?.substring(0, 100),
|
|
1308
|
+
}, err instanceof Error ? err : undefined);
|
|
1309
|
+
log.error(tracedError.logMessage);
|
|
1310
|
+
if (debug) log.debug('Full error', { error: err?.stack || String(err) });
|
|
1311
|
+
// Clear spawning flag since spawn failed
|
|
1312
|
+
if (this.onClearSpawning) {
|
|
1313
|
+
this.onClearSpawning(name);
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
success: false,
|
|
1317
|
+
name,
|
|
1318
|
+
error: tracedError.userMessage,
|
|
1319
|
+
errorId: tracedError.errorId,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/** Role presets for shadow agents */
|
|
1325
|
+
private static readonly ROLE_PRESETS: Record<string, SpeakOnTrigger[]> = {
|
|
1326
|
+
reviewer: ['CODE_WRITTEN', 'REVIEW_REQUEST', 'EXPLICIT_ASK'],
|
|
1327
|
+
auditor: ['SESSION_END', 'EXPLICIT_ASK'],
|
|
1328
|
+
active: ['ALL_MESSAGES'],
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Spawn a primary agent with its shadow agent
|
|
1333
|
+
*
|
|
1334
|
+
* Example usage:
|
|
1335
|
+
* ```ts
|
|
1336
|
+
* const result = await spawner.spawnWithShadow({
|
|
1337
|
+
* primary: { name: 'Lead', command: 'claude', task: 'Implement feature X' },
|
|
1338
|
+
* shadow: { name: 'Auditor', role: 'reviewer', speakOn: ['CODE_WRITTEN'] }
|
|
1339
|
+
* });
|
|
1340
|
+
* ```
|
|
1341
|
+
*/
|
|
1342
|
+
async spawnWithShadow(request: SpawnWithShadowRequest): Promise<SpawnWithShadowResult> {
|
|
1343
|
+
const { primary, shadow } = request;
|
|
1344
|
+
const debug = process.env.DEBUG_SPAWN === '1';
|
|
1345
|
+
|
|
1346
|
+
// Resolve shadow speakOn triggers
|
|
1347
|
+
let speakOn: SpeakOnTrigger[] = ['EXPLICIT_ASK']; // Default
|
|
1348
|
+
|
|
1349
|
+
// Check for role preset
|
|
1350
|
+
if (shadow.role && AgentSpawner.ROLE_PRESETS[shadow.role.toLowerCase()]) {
|
|
1351
|
+
speakOn = AgentSpawner.ROLE_PRESETS[shadow.role.toLowerCase()];
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Override with explicit speakOn if provided
|
|
1355
|
+
if (shadow.speakOn && shadow.speakOn.length > 0) {
|
|
1356
|
+
speakOn = shadow.speakOn;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Build shadow task prompt
|
|
1360
|
+
const defaultPrompt = `You are a shadow agent monitoring "${primary.name}". You receive copies of their messages. Your role: ${shadow.role || 'observer'}. Stay passive unless your triggers activate: ${speakOn.join(', ')}.`;
|
|
1361
|
+
const shadowTask = shadow.prompt || defaultPrompt;
|
|
1362
|
+
|
|
1363
|
+
// Decide how to run the shadow (subagent for Claude/OpenCode primaries, process fallback otherwise)
|
|
1364
|
+
let shadowSelection: Awaited<ReturnType<typeof selectShadowCli>> | null = null;
|
|
1365
|
+
try {
|
|
1366
|
+
shadowSelection = await selectShadowCli(primary.command || 'claude', {
|
|
1367
|
+
preferredShadowCli: shadow.command,
|
|
1368
|
+
});
|
|
1369
|
+
} catch (err: any) {
|
|
1370
|
+
log.warn(`Shadow CLI selection failed for ${shadow.name}: ${err.message}`);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (debug) {
|
|
1374
|
+
const mode = shadowSelection?.mode ?? 'unknown';
|
|
1375
|
+
const cli = shadowSelection?.command ?? shadow.command ?? primary.command ?? 'claude';
|
|
1376
|
+
log.debug(
|
|
1377
|
+
`spawnWithShadow: primary=${primary.name}, shadow=${shadow.name}, mode=${mode}, cli=${cli}, speakOn=${speakOn.join(',')}`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Step 1: Spawn primary agent
|
|
1382
|
+
const primaryResult = await this.spawn({
|
|
1383
|
+
name: primary.name,
|
|
1384
|
+
cli: primary.command || 'claude',
|
|
1385
|
+
task: primary.task || '',
|
|
1386
|
+
team: primary.team,
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
if (!primaryResult.success) {
|
|
1390
|
+
return {
|
|
1391
|
+
success: false,
|
|
1392
|
+
primary: primaryResult,
|
|
1393
|
+
error: `Failed to spawn primary agent: ${primaryResult.error}`,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Step 2: Wait for primary to register before spawning shadow
|
|
1398
|
+
// The spawn() method already waits, but we add a small delay for stability
|
|
1399
|
+
await sleep(1000);
|
|
1400
|
+
|
|
1401
|
+
// Subagent mode: no separate process needed
|
|
1402
|
+
if (shadowSelection?.mode === 'subagent') {
|
|
1403
|
+
log.info(
|
|
1404
|
+
`Shadow ${shadow.name} will run as ${shadowSelection.cli} subagent inside ${primary.name} (no separate process)`
|
|
1405
|
+
);
|
|
1406
|
+
return {
|
|
1407
|
+
success: true,
|
|
1408
|
+
primary: primaryResult,
|
|
1409
|
+
shadow: {
|
|
1410
|
+
success: true,
|
|
1411
|
+
name: shadow.name,
|
|
1412
|
+
},
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// No available shadow CLI - proceed without spawning a shadow process
|
|
1417
|
+
if (!shadowSelection) {
|
|
1418
|
+
log.warn(`No authenticated shadow CLI available; ${primary.name} will run without a shadow`);
|
|
1419
|
+
return {
|
|
1420
|
+
success: true,
|
|
1421
|
+
primary: primaryResult,
|
|
1422
|
+
error: 'Shadow spawn skipped: no authenticated shadow CLI available',
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Step 3: Spawn shadow agent with shadowOf and shadowSpeakOn
|
|
1427
|
+
const shadowResult = await this.spawn({
|
|
1428
|
+
name: shadow.name,
|
|
1429
|
+
// Use the selected/validated CLI for process-mode shadows
|
|
1430
|
+
cli: shadowSelection.command || shadow.command || primary.command || 'claude',
|
|
1431
|
+
task: shadowTask,
|
|
1432
|
+
shadowOf: primary.name,
|
|
1433
|
+
shadowSpeakOn: speakOn,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
if (!shadowResult.success) {
|
|
1437
|
+
log.warn(`Shadow agent ${shadow.name} failed to spawn, primary ${primary.name} continues without shadow`);
|
|
1438
|
+
return {
|
|
1439
|
+
success: true, // Primary succeeded, overall operation is partial success
|
|
1440
|
+
primary: primaryResult,
|
|
1441
|
+
shadow: shadowResult,
|
|
1442
|
+
error: `Shadow spawn failed: ${shadowResult.error}`,
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
log.info(`Spawned pair: ${primary.name} with shadow ${shadow.name} (speakOn: ${speakOn.join(',')})`);
|
|
1447
|
+
|
|
1448
|
+
return {
|
|
1449
|
+
success: true,
|
|
1450
|
+
primary: primaryResult,
|
|
1451
|
+
shadow: shadowResult,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Release (terminate) a worker
|
|
1457
|
+
*/
|
|
1458
|
+
async release(name: string): Promise<boolean> {
|
|
1459
|
+
const worker = this.activeWorkers.get(name);
|
|
1460
|
+
if (!worker) {
|
|
1461
|
+
log.debug(`Worker ${name} not found`);
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
try {
|
|
1466
|
+
// Unbind all listeners first to prevent memory leaks
|
|
1467
|
+
this.unbindListeners(worker.pty, worker.listeners);
|
|
1468
|
+
|
|
1469
|
+
// Stop the pty process gracefully (handles auto-save internally)
|
|
1470
|
+
await worker.pty.stop();
|
|
1471
|
+
|
|
1472
|
+
// Force kill if still running
|
|
1473
|
+
if (worker.pty.isRunning) {
|
|
1474
|
+
await worker.pty.kill();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
this.activeWorkers.delete(name);
|
|
1478
|
+
this.saveWorkersMetadata();
|
|
1479
|
+
log.info(`Released ${name}`);
|
|
1480
|
+
|
|
1481
|
+
return true;
|
|
1482
|
+
} catch (err: any) {
|
|
1483
|
+
log.error(`Failed to release ${name}: ${err.message}`);
|
|
1484
|
+
// Still unbind and remove from tracking
|
|
1485
|
+
this.unbindListeners(worker.pty, worker.listeners);
|
|
1486
|
+
this.activeWorkers.delete(name);
|
|
1487
|
+
this.saveWorkersMetadata();
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Release all workers
|
|
1494
|
+
*/
|
|
1495
|
+
async releaseAll(): Promise<void> {
|
|
1496
|
+
const workers = Array.from(this.activeWorkers.keys());
|
|
1497
|
+
for (const name of workers) {
|
|
1498
|
+
await this.release(name);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Get all active workers (returns WorkerInfo without pty reference)
|
|
1504
|
+
*/
|
|
1505
|
+
getActiveWorkers(): WorkerInfo[] {
|
|
1506
|
+
return Array.from(this.activeWorkers.values()).map((w) => ({
|
|
1507
|
+
name: w.name,
|
|
1508
|
+
cli: w.cli,
|
|
1509
|
+
task: w.task,
|
|
1510
|
+
team: w.team,
|
|
1511
|
+
spawnedAt: w.spawnedAt,
|
|
1512
|
+
pid: w.pid,
|
|
1513
|
+
}));
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Check if a worker exists
|
|
1518
|
+
*/
|
|
1519
|
+
hasWorker(name: string): boolean {
|
|
1520
|
+
return this.activeWorkers.has(name);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Get worker info
|
|
1525
|
+
*/
|
|
1526
|
+
getWorker(name: string): WorkerInfo | undefined {
|
|
1527
|
+
const worker = this.activeWorkers.get(name);
|
|
1528
|
+
if (!worker) return undefined;
|
|
1529
|
+
return {
|
|
1530
|
+
name: worker.name,
|
|
1531
|
+
cli: worker.cli,
|
|
1532
|
+
task: worker.task,
|
|
1533
|
+
team: worker.team,
|
|
1534
|
+
spawnedAt: worker.spawnedAt,
|
|
1535
|
+
pid: worker.pid,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Get output logs from a worker
|
|
1541
|
+
*/
|
|
1542
|
+
getWorkerOutput(name: string, limit?: number): string[] | null {
|
|
1543
|
+
const worker = this.activeWorkers.get(name);
|
|
1544
|
+
if (!worker) return null;
|
|
1545
|
+
return worker.pty.getOutput(limit);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Get raw output from a worker
|
|
1550
|
+
*/
|
|
1551
|
+
getWorkerRawOutput(name: string): string | null {
|
|
1552
|
+
const worker = this.activeWorkers.get(name);
|
|
1553
|
+
if (!worker) return null;
|
|
1554
|
+
return worker.pty.getRawOutput();
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Send input to a worker's PTY (for interactive terminal support)
|
|
1559
|
+
* @param name - Worker name
|
|
1560
|
+
* @param data - Input data to send (keystrokes, text, etc.)
|
|
1561
|
+
* @returns true if input was sent, false if worker not found
|
|
1562
|
+
*/
|
|
1563
|
+
sendWorkerInput(name: string, data: string): boolean {
|
|
1564
|
+
const worker = this.activeWorkers.get(name);
|
|
1565
|
+
if (!worker) return false;
|
|
1566
|
+
worker.pty.write(data);
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Wait for an agent to appear in the connected list and registry (connected-agents.json + agents.json).
|
|
1572
|
+
*/
|
|
1573
|
+
private async waitForAgentRegistration(
|
|
1574
|
+
name: string,
|
|
1575
|
+
timeoutMs = 30_000,
|
|
1576
|
+
pollIntervalMs = 500
|
|
1577
|
+
): Promise<boolean> {
|
|
1578
|
+
const deadline = Date.now() + timeoutMs;
|
|
1579
|
+
let pollCount = 0;
|
|
1580
|
+
|
|
1581
|
+
while (Date.now() < deadline) {
|
|
1582
|
+
pollCount++;
|
|
1583
|
+
const connected = this.isAgentConnected(name);
|
|
1584
|
+
const recentlySeen = this.isAgentRecentlySeen(name);
|
|
1585
|
+
|
|
1586
|
+
// Log first few polls and every 10th poll after that
|
|
1587
|
+
if (pollCount <= 3 || pollCount % 10 === 0) {
|
|
1588
|
+
log.info(`Registration poll #${pollCount} for ${name}: connected=${connected} recentlySeen=${recentlySeen} agentsPath=${this.agentsPath}`);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (connected && recentlySeen) {
|
|
1592
|
+
log.info(`Agent ${name} registered after ${pollCount} polls`);
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
await sleep(pollIntervalMs);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
log.info(`Registration timeout for ${name} after ${pollCount} polls`);
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
private isAgentRegistered(name: string): boolean {
|
|
1604
|
+
return this.isAgentConnected(name) && this.isAgentRecentlySeen(name);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
private isAgentConnected(name: string): boolean {
|
|
1608
|
+
const debug = process.env.DEBUG_SPAWN === '1';
|
|
1609
|
+
if (!this.agentsPath) {
|
|
1610
|
+
if (debug) log.debug(`isAgentConnected(${name}): no agentsPath`);
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
if (!fs.existsSync(this.agentsPath)) {
|
|
1614
|
+
if (debug) log.debug(`isAgentConnected(${name}): file not found: ${this.agentsPath}`);
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
try {
|
|
1619
|
+
const raw = JSON.parse(fs.readFileSync(this.agentsPath, 'utf-8'));
|
|
1620
|
+
// connected-agents.json format: { agents: string[], users: string[], updatedAt: number }
|
|
1621
|
+
// agents is a string array of connected agent names (not objects)
|
|
1622
|
+
const agents: string[] = Array.isArray(raw?.agents) ? raw.agents : [];
|
|
1623
|
+
const updatedAt = typeof raw?.updatedAt === 'number' ? raw.updatedAt : 0;
|
|
1624
|
+
const isFresh = Date.now() - updatedAt <= AgentSpawner.ONLINE_THRESHOLD_MS;
|
|
1625
|
+
|
|
1626
|
+
if (debug) {
|
|
1627
|
+
log.debug(`isAgentConnected(${name}): path=${this.agentsPath} agents=${agents.join(',')} updatedAt=${updatedAt} isFresh=${isFresh}`);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (!isFresh) return false;
|
|
1631
|
+
|
|
1632
|
+
// Case-insensitive check to match router behavior
|
|
1633
|
+
const lowerName = name.toLowerCase();
|
|
1634
|
+
return agents.some((a) => typeof a === 'string' && a.toLowerCase() === lowerName);
|
|
1635
|
+
} catch (err: any) {
|
|
1636
|
+
log.error('Failed to read connected-agents.json', { error: err.message, path: this.agentsPath });
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
private isAgentRecentlySeen(name: string): boolean {
|
|
1642
|
+
if (!this.registryPath) return false;
|
|
1643
|
+
if (!fs.existsSync(this.registryPath)) return false;
|
|
1644
|
+
|
|
1645
|
+
try {
|
|
1646
|
+
const raw = JSON.parse(fs.readFileSync(this.registryPath, 'utf-8'));
|
|
1647
|
+
const agents = Array.isArray(raw?.agents)
|
|
1648
|
+
? raw.agents
|
|
1649
|
+
: typeof raw?.agents === 'object' && raw?.agents !== null
|
|
1650
|
+
? Object.values(raw.agents)
|
|
1651
|
+
: [];
|
|
1652
|
+
const lowerName = name.toLowerCase();
|
|
1653
|
+
const agent = agents.find((entry: { name?: string; lastSeen?: string }) => typeof entry?.name === 'string' && entry.name.toLowerCase() === lowerName);
|
|
1654
|
+
if (!agent?.lastSeen) return false;
|
|
1655
|
+
return Date.now() - new Date(agent.lastSeen).getTime() <= AgentSpawner.ONLINE_THRESHOLD_MS;
|
|
1656
|
+
} catch (err: any) {
|
|
1657
|
+
log.error('Failed to read agents.json', { error: err.message });
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Save workers metadata to disk for CLI access
|
|
1664
|
+
*/
|
|
1665
|
+
private saveWorkersMetadata(): void {
|
|
1666
|
+
try {
|
|
1667
|
+
const workers: WorkerMeta[] = Array.from(this.activeWorkers.values()).map((w) => ({
|
|
1668
|
+
name: w.name,
|
|
1669
|
+
cli: w.cli,
|
|
1670
|
+
task: w.task,
|
|
1671
|
+
team: w.team,
|
|
1672
|
+
userId: w.userId,
|
|
1673
|
+
spawnedAt: w.spawnedAt,
|
|
1674
|
+
pid: w.pid,
|
|
1675
|
+
logFile: w.logFile,
|
|
1676
|
+
}));
|
|
1677
|
+
|
|
1678
|
+
fs.writeFileSync(this.workersPath, JSON.stringify({ workers }, null, 2));
|
|
1679
|
+
} catch (err: any) {
|
|
1680
|
+
log.error('Failed to save workers metadata', { error: err.message });
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/**
|
|
1685
|
+
* Get path to logs directory
|
|
1686
|
+
*/
|
|
1687
|
+
getLogsDir(): string {
|
|
1688
|
+
return this.logsDir;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Get path to workers metadata file
|
|
1693
|
+
*/
|
|
1694
|
+
getWorkersPath(): string {
|
|
1695
|
+
return this.workersPath;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Read workers metadata from disk (for CLI use)
|
|
1701
|
+
*/
|
|
1702
|
+
export function readWorkersMetadata(projectRoot: string): WorkerMeta[] {
|
|
1703
|
+
const paths = getProjectPaths(projectRoot);
|
|
1704
|
+
const workersPath = path.join(paths.teamDir, 'workers.json');
|
|
1705
|
+
|
|
1706
|
+
if (!fs.existsSync(workersPath)) {
|
|
1707
|
+
return [];
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
try {
|
|
1711
|
+
const raw = JSON.parse(fs.readFileSync(workersPath, 'utf-8'));
|
|
1712
|
+
return Array.isArray(raw?.workers) ? raw.workers : [];
|
|
1713
|
+
} catch {
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Get the worker logs directory path
|
|
1720
|
+
*/
|
|
1721
|
+
export function getWorkerLogsDir(projectRoot: string): string {
|
|
1722
|
+
const paths = getProjectPaths(projectRoot);
|
|
1723
|
+
return path.join(paths.teamDir, 'worker-logs');
|
|
1724
|
+
}
|