diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 000000000..9c97ee2d0 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"8b0848d6-9418-4a97-9b05-49254dbd4a28","pid":54127,"procStart":"Sun May 24 19:48:26 2026","acquiredAt":1779652664087} \ No newline at end of file diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index b720b51df..7737446e3 100644 --- a/.github/workflows/package-validation.yml +++ b/.github/workflows/package-validation.yml @@ -65,24 +65,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: '22' - - # Cache node_modules - skip npm ci entirely if unchanged - - name: Cache node_modules - id: cache-modules - uses: actions/cache@v4 - with: - # Include nested workspace node_modules so unhoisted deps - # (e.g. @slack/web-api under packages/slack-primitive) survive - # a cache hit — otherwise the cache restores only the root tree - # and skipping `npm ci` leaves the bundler unable to resolve - # them. - path: | - node_modules - packages/*/node_modules - key: modules-v2-${{ hashFiles('package-lock.json') }} + cache: 'npm' - name: Install dependencies - if: steps.cache-modules.outputs.cache-hit != 'true' run: npm ci # Cache turbo build outputs for incremental builds diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96cac9a17..3fccc835f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2033,7 +2033,6 @@ jobs: if (text.startsWith('revert version bump')) return true; return title.length === 0; } - function sectionFor(commit) { if (commit.breaking) return 'Breaking Changes'; if (commit.type === 'feat') return 'Added'; diff --git a/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json new file mode 100644 index 000000000..8739a0d82 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json @@ -0,0 +1,381 @@ +{ + "id": "traj_4b2d63f6ljvh", + "version": 1, + "task": { + "title": "Fix CI run 26263517444" + }, + "status": "completed", + "startedAt": "2026-05-22T01:47:08.312Z", + "completedAt": "2026-05-25T01:45:41.710Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-22T16:13:33.081Z" + } + ], + "chapters": [ + { + "id": "chap_s3v7se9eoey9", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-22T16:13:33.081Z", + "endedAt": "2026-05-25T01:45:41.710Z", + "events": [ + { + "ts": 1779466413082, + "type": "decision", + "content": "Normalized Relay changelog to Burn-style release notes: Normalized Relay changelog to Burn-style release notes", + "raw": { + "question": "Normalized Relay changelog to Burn-style release notes", + "chosen": "Normalized Relay changelog to Burn-style release notes", + "alternatives": [], + "reasoning": "The top-level changelog should track actual released versions with concise impact-oriented Added/Changed/Fixed sections; generated Product/Technical/Release scaffolding made released content look unreleased and duplicated release-only entries." + }, + "significance": "high" + }, + { + "ts": 1779466600291, + "type": "decision", + "content": "Updated release workflow changelog generator: Updated release workflow changelog generator", + "raw": { + "question": "Updated release workflow changelog generator", + "chosen": "Updated release workflow changelog generator", + "alternatives": [], + "reasoning": "Future stable releases should write the same Burn-style cross-package notes now used in CHANGELOG.md; the generator now emits standard sections and filters release-only/trajectory/review placeholders instead of Product/Technical scaffolding." + }, + "significance": "high" + }, + { + "ts": 1779467018012, + "type": "decision", + "content": "Matched Relay AGENTS changelog guidance to Burn: Matched Relay AGENTS changelog guidance to Burn", + "raw": { + "question": "Matched Relay AGENTS changelog guidance to Burn", + "chosen": "Matched Relay AGENTS changelog guidance to Burn", + "alternatives": [], + "reasoning": "Relay should tell agents to curate CHANGELOG.md [Unreleased] as work lands and to avoid generated Product/Technical/Releases sections, matching the Burn repo guidance adapted for Relay's single root changelog." + }, + "significance": "high" + }, + { + "ts": 1779467245541, + "type": "decision", + "content": "Restored Keep a Changelog and SemVer contract: Restored Keep a Changelog and SemVer contract", + "raw": { + "question": "Restored Keep a Changelog and SemVer contract", + "chosen": "Restored Keep a Changelog and SemVer contract", + "alternatives": [], + "reasoning": "Relay should keep the standard changelog header, agent guidance, and generated release sections aligned with Keep a Changelog while explicitly documenting Semantic Versioning." + }, + "significance": "high" + }, + { + "ts": 1779469179928, + "type": "decision", + "content": "Use AWS managed CachingDisabled cache policy for PR previews: Use AWS managed CachingDisabled cache policy for PR previews", + "raw": { + "question": "Use AWS managed CachingDisabled cache policy for PR previews", + "chosen": "Use AWS managed CachingDisabled cache policy for PR previews", + "alternatives": [], + "reasoning": "The failing job hit CloudFront's custom cache policy quota while SST created WebServerCachePolicy per preview stage; previews can safely trade edge response caching for quota stability while production keeps the custom OpenNext cache policy." + }, + "significance": "high" + }, + { + "ts": 1779469371491, + "type": "reflection", + "content": "Preview failures are caused by active PR volume exceeding CloudFront custom cache policy quota; stale cleanup ran successfully but cannot help while open PR previews outnumber the quota. The durable fix is reusing a managed cache policy for PR stages.", + "raw": { + "focalPoints": ["preview-deploys", "cloudfront-quota", "sst"], + "confidence": 0.86 + }, + "significance": "high", + "tags": ["focal:preview-deploys", "focal:cloudfront-quota", "focal:sst", "confidence:0.86"] + }, + { + "ts": 1779640092930, + "type": "decision", + "content": "Added SDK-level harness adapter registry: Added SDK-level harness adapter registry", + "raw": { + "question": "Added SDK-level harness adapter registry", + "chosen": "Added SDK-level harness adapter registry", + "alternatives": [], + "reasoning": "Interactive spawns already accept arbitrary CLI strings, but workflow non-interactive execution was coupled to the built-in CLI registry and hardcoded model flags. A runtime registry plus serializable workflow harness config lets SDK/YAML callers add harnesses without Relay code changes." + }, + "significance": "high" + }, + { + "ts": 1779641004932, + "type": "decision", + "content": "Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server: Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server", + "raw": { + "question": "Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server", + "chosen": "Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server", + "alternatives": [], + "reasoning": "The MCP surface needed by spawned agents is small enough to own locally; using @relaycast/sdk directly avoids importing @relaycast/mcp internals while preserving callback resource updates and existing relaycast tool naming." + }, + "significance": "high" + }, + { + "ts": 1779642175025, + "type": "decision", + "content": "Moved harness adapters into broker spawn path: Moved harness adapters into broker spawn path", + "raw": { + "question": "Moved harness adapters into broker spawn path", + "chosen": "Moved harness adapters into broker spawn path", + "alternatives": [], + "reasoning": "The first pass only affected workflow subprocess execution. AgentRelay spawnPty ultimately posts to the Rust broker, so SDK-provided harness definitions now serialize on spawn requests and the broker applies binary resolution, interactive argv templates, model args, and bypass flags when launching PTY agents." + }, + "significance": "high" + }, + { + "ts": 1779673086822, + "type": "decision", + "content": "Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity: Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity", + "raw": { + "question": "Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity", + "chosen": "Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity", + "alternatives": [], + "reasoning": "This keeps codex/claude/opencode available by default while letting SDK and broker-spawned custom harnesses use the same serializable HarnessDefinition shape; adapter selects built-in lifecycle behavior when config alone is not enough." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Updated harness adapters so built-in coding harnesses are serializable configs, added adapter lifecycle identity/merge behavior, and refreshed docs for defining custom harnesses.", + "approach": "Standard approach", + "confidence": 0.86 + }, + "commits": [ + "c41b133d", + "28ec8236", + "bcc07d62", + "2078bc8f", + "cfa9b0b9", + "ae28c3c3", + "f3f3744d", + "83ab2bba", + "11281768", + "fd09d477", + "423184bb", + "0d49dd41", + "baaef913", + "c148f419", + "f82bd2cc", + "117e76d6", + "2b363811", + "68fddec4", + "bf9427dc", + "eefe64ba", + "d8973ce7", + "f57c3e2f", + "d76b0f40", + "19b22846", + "c1cf84d3", + "6c83d4bd", + "fa3f757f", + "c992acd3", + "9a97be3a", + "489d32cd", + "9d800626", + "de2169fc", + "dc6cad51", + "75771972", + "4be45b44", + "f953d0f9", + "bbe4f00e", + "90dc1faa", + "e5db219c", + "66879e0c", + "79311e9f", + "8bcc3cfe", + "3332e837", + "c13ee318", + "5b4005ef", + "6dea769a", + "c8299b7a", + "839d0cda", + "e1022e4d" + ], + "filesChanged": [ + ".claude/scheduled_tasks.lock", + ".github/workflows/package-validation.yml", + ".github/workflows/preview-web.yml", + ".github/workflows/publish.yml", + ".trajectories/active/traj_4b2d63f6ljvh.json", + ".trajectories/completed/2026-05/traj_5k0jtc1g5l33.json", + ".trajectories/completed/2026-05/traj_5k0jtc1g5l33.md", + ".trajectories/completed/2026-05/traj_78ytpicts778.json", + ".trajectories/completed/2026-05/traj_78ytpicts778.md", + ".trajectories/completed/2026-05/traj_90jmd9z27oap.json", + ".trajectories/completed/2026-05/traj_90jmd9z27oap.md", + ".trajectories/completed/2026-05/traj_bz1a1o15p7px.json", + ".trajectories/completed/2026-05/traj_bz1a1o15p7px.md", + ".trajectories/completed/2026-05/traj_ceo5q9bh2od3.json", + ".trajectories/completed/2026-05/traj_ceo5q9bh2od3.md", + ".trajectories/completed/2026-05/traj_dbsnr453nxjw.json", + ".trajectories/completed/2026-05/traj_dbsnr453nxjw.md", + ".trajectories/completed/2026-05/traj_dcl9hgoiuac5.json", + ".trajectories/completed/2026-05/traj_dcl9hgoiuac5.md", + ".trajectories/completed/2026-05/traj_dqgg2q4scsvt.json", + ".trajectories/completed/2026-05/traj_dqgg2q4scsvt.md", + ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json", + ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.md", + ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.trace.json", + ".trajectories/completed/2026-05/traj_mytnzgfayj3d.json", + ".trajectories/completed/2026-05/traj_mytnzgfayj3d.md", + ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.json", + ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.md", + ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.trace.json", + ".trajectories/completed/2026-05/traj_r3eic6rt84pq.json", + ".trajectories/completed/2026-05/traj_r3eic6rt84pq.md", + ".trajectories/completed/2026-05/traj_s5ojo1f4srz4.json", + ".trajectories/completed/2026-05/traj_s5ojo1f4srz4.md", + ".trajectories/completed/2026-05/traj_vfa1jr6otnjn.json", + ".trajectories/completed/2026-05/traj_vfa1jr6otnjn.md", + ".trajectories/index.json", + "AGENTS.md", + "CHANGELOG.md", + "README.md", + "crates/broker/src/broker/injection_format.rs", + "crates/broker/src/cli/mod.rs", + "crates/broker/src/cli_mcp_args.rs", + "crates/broker/src/codex_session.rs", + "crates/broker/src/lib.rs", + "crates/broker/src/listen_api.rs", + "crates/broker/src/protocol.rs", + "crates/broker/src/relaycast/mod.rs", + "crates/broker/src/runtime/api.rs", + "crates/broker/src/runtime/event_loop.rs", + "crates/broker/src/runtime/init.rs", + "crates/broker/src/runtime/maintenance.rs", + "crates/broker/src/runtime/mod.rs", + "crates/broker/src/runtime/relaycast_events.rs", + "crates/broker/src/runtime/session.rs", + "crates/broker/src/runtime/spawn_spec.rs", + "crates/broker/src/runtime/tests.rs", + "crates/broker/src/runtime/worker_events.rs", + "crates/broker/src/snippets.rs", + "crates/broker/src/supervisor.rs", + "crates/broker/src/types.rs", + "crates/broker/src/worker.rs", + "crates/broker/src/wrap.rs", + "docs/cli-command-tree.md", + "package-lock.json", + "package.json", + "packages/acp-bridge/package.json", + "packages/agent/package.json", + "packages/brand/package.json", + "packages/broker-darwin-arm64/package.json", + "packages/broker-darwin-x64/package.json", + "packages/broker-linux-arm64/package.json", + "packages/broker-linux-x64/package.json", + "packages/broker-win32-x64/package.json", + "packages/browser-primitive/package.json", + "packages/cloud/package.json", + "packages/config/package.json", + "packages/credential-proxy/package.json", + "packages/events/package.json", + "packages/gateway/package.json", + "packages/github-primitive/package.json", + "packages/hooks/package.json", + "packages/memory/package.json", + "packages/openclaw/package.json", + "packages/openclaw/skill/SKILL.md", + "packages/openclaw/src/identity/files.ts", + "packages/openclaw/src/setup.ts", + "packages/openclaw/templates/SOUL.md.template", + "packages/personas/package.json", + "packages/policy/package.json", + "packages/sdk-py/pyproject.toml", + "packages/sdk-py/src/agent_relay/__init__.py", + "packages/sdk-py/src/agent_relay/builder.py", + "packages/sdk-py/src/agent_relay/client.py", + "packages/sdk-py/src/agent_relay/relay.py", + "packages/sdk-py/src/agent_relay/types.py", + "packages/sdk-py/tests/test_builder.py", + "packages/sdk-py/tests/test_relay_harness.py", + "packages/sdk-py/uv.lock", + "packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift", + "packages/sdk/README.md", + "packages/sdk/package.json", + "packages/sdk/src/__tests__/client-broker-exit.test.ts", + "packages/sdk/src/__tests__/lifecycle-hooks.test.ts", + "packages/sdk/src/__tests__/orchestration-upgrades.test.ts", + "packages/sdk/src/__tests__/spawn-harness.test.ts", + "packages/sdk/src/__tests__/unit.test.ts", + "packages/sdk/src/cli-registry.ts", + "packages/sdk/src/cli-resolver.ts", + "packages/sdk/src/client.ts", + "packages/sdk/src/event-bus.ts", + "packages/sdk/src/index.ts", + "packages/sdk/src/lifecycle-hooks.ts", + "packages/sdk/src/protocol.ts", + "packages/sdk/src/relay-adapter.ts", + "packages/sdk/src/relay.ts", + "packages/sdk/src/types.ts", + "packages/sdk/src/workers.ts", + "packages/sdk/src/workflows/README.md", + "packages/sdk/src/workflows/__tests__/harness-adapters.test.ts", + "packages/sdk/src/workflows/builder.ts", + "packages/sdk/src/workflows/index.ts", + "packages/sdk/src/workflows/process-backend-executor.ts", + "packages/sdk/src/workflows/process-spawner.ts", + "packages/sdk/src/workflows/run.ts", + "packages/sdk/src/workflows/runner.ts", + "packages/sdk/src/workflows/schema.json", + "packages/sdk/src/workflows/types.ts", + "packages/slack-primitive/package.json", + "packages/telemetry/package.json", + "packages/trajectory/package.json", + "packages/user-directory/package.json", + "packages/user-directory/src/index.ts", + "packages/user-directory/src/user-directory.ts", + "packages/user-directory/tsconfig.json", + "packages/user-directory/vitest.config.ts", + "packages/utils/package.json", + "packages/workflow-types/package.json", + "packages/workflow-types/src/index.ts", + "plugins/codex-relay-skill/README.md", + "plugins/codex-relay-skill/codex-config/config.toml", + "plugins/codex-relay-skill/scripts/setup.sh", + "plugins/gemini-relay-extension/package-lock.json", + "plugins/gemini-relay-extension/package.json", + "plugins/gemini-relay-extension/relay-server.js", + "readme-banner.png", + "scripts/demos/three-way-debate.gif", + "scripts/demos/three-way-debate.mp4", + "scripts/demos/three-way-debate.tape", + "scripts/watch-cli-tools.sh", + "src/cli/bootstrap.ts", + "src/cli/commands/agent-management.ts", + "src/cli/commands/core.test.ts", + "src/cli/relaycast-mcp.startup.test.ts", + "src/cli/relaycast-mcp.ts", + "tests/mcp_merge_e2e.rs", + "tsconfig.json", + "vitest.config.ts", + "web/components/docs/DocsNav.tsx", + "web/content/docs/cli-broker-lifecycle.mdx", + "web/content/docs/event-handlers.mdx", + "web/content/docs/harnesses.mdx", + "web/content/docs/local-mode.mdx", + "web/content/docs/proactive-agents.mdx", + "web/content/docs/reference-cli.mdx", + "web/content/docs/reference-workflows.mdx", + "web/content/docs/spawning-an-agent.mdx", + "web/content/docs/typescript-sdk.mdx", + "web/lib/docs-nav.ts", + "web/sst.config.ts" + ], + "projectId": "relay", + "tags": [], + "_trace": { + "startRef": "898b8ee37197075913578fdb1c2fe4139b2ac562", + "endRef": "c41b133d8fbe3649ae312b6b670d81861184f178", + "traceId": "56ccacd7-ecd7-476a-b20b-e1d9960e3f70" + } +} diff --git a/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.md b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.md new file mode 100644 index 000000000..744b56435 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.md @@ -0,0 +1,89 @@ +# Trajectory: Fix CI run 26263517444 + +> **Status:** ✅ Completed +> **Confidence:** 86% +> **Started:** May 21, 2026 at 09:47 PM +> **Completed:** May 24, 2026 at 09:45 PM + +--- + +## Summary + +Updated harness adapters so built-in coding harnesses are serializable configs, added adapter lifecycle identity/merge behavior, and refreshed docs for defining custom harnesses. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Normalized Relay changelog to Burn-style release notes + +- **Chose:** Normalized Relay changelog to Burn-style release notes +- **Reasoning:** The top-level changelog should track actual released versions with concise impact-oriented Added/Changed/Fixed sections; generated Product/Technical/Release scaffolding made released content look unreleased and duplicated release-only entries. + +### Updated release workflow changelog generator + +- **Chose:** Updated release workflow changelog generator +- **Reasoning:** Future stable releases should write the same Burn-style cross-package notes now used in CHANGELOG.md; the generator now emits standard sections and filters release-only/trajectory/review placeholders instead of Product/Technical scaffolding. + +### Matched Relay AGENTS changelog guidance to Burn + +- **Chose:** Matched Relay AGENTS changelog guidance to Burn +- **Reasoning:** Relay should tell agents to curate CHANGELOG.md [Unreleased] as work lands and to avoid generated Product/Technical/Releases sections, matching the Burn repo guidance adapted for Relay's single root changelog. + +### Restored Keep a Changelog and SemVer contract + +- **Chose:** Restored Keep a Changelog and SemVer contract +- **Reasoning:** Relay should keep the standard changelog header, agent guidance, and generated release sections aligned with Keep a Changelog while explicitly documenting Semantic Versioning. + +### Use AWS managed CachingDisabled cache policy for PR previews + +- **Chose:** Use AWS managed CachingDisabled cache policy for PR previews +- **Reasoning:** The failing job hit CloudFront's custom cache policy quota while SST created WebServerCachePolicy per preview stage; previews can safely trade edge response caching for quota stability while production keeps the custom OpenNext cache policy. + +### Added SDK-level harness adapter registry + +- **Chose:** Added SDK-level harness adapter registry +- **Reasoning:** Interactive spawns already accept arbitrary CLI strings, but workflow non-interactive execution was coupled to the built-in CLI registry and hardcoded model flags. A runtime registry plus serializable workflow harness config lets SDK/YAML callers add harnesses without Relay code changes. + +### Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server + +- **Chose:** Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server +- **Reasoning:** The MCP surface needed by spawned agents is small enough to own locally; using @relaycast/sdk directly avoids importing @relaycast/mcp internals while preserving callback resource updates and existing relaycast tool naming. + +### Moved harness adapters into broker spawn path + +- **Chose:** Moved harness adapters into broker spawn path +- **Reasoning:** The first pass only affected workflow subprocess execution. AgentRelay spawnPty ultimately posts to the Rust broker, so SDK-provided harness definitions now serialize on spawn requests and the broker applies binary resolution, interactive argv templates, model args, and bypass flags when launching PTY agents. + +### Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity + +- **Chose:** Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity +- **Reasoning:** This keeps codex/claude/opencode available by default while letting SDK and broker-spawned custom harnesses use the same serializable HarnessDefinition shape; adapter selects built-in lifecycle behavior when config alone is not enough. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Normalized Relay changelog to Burn-style release notes: Normalized Relay changelog to Burn-style release notes +- Updated release workflow changelog generator: Updated release workflow changelog generator +- Matched Relay AGENTS changelog guidance to Burn: Matched Relay AGENTS changelog guidance to Burn +- Restored Keep a Changelog and SemVer contract: Restored Keep a Changelog and SemVer contract +- Use AWS managed CachingDisabled cache policy for PR previews: Use AWS managed CachingDisabled cache policy for PR previews +- Preview failures are caused by active PR volume exceeding CloudFront custom cache policy quota; stale cleanup ran successfully but cannot help while open PR previews outnumber the quota. The durable fix is reusing a managed cache policy for PR stages. +- Added SDK-level harness adapter registry: Added SDK-level harness adapter registry +- Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server: Replaced external Relaycast MCP dependency with owned Agent Relay MCP stdio server +- Moved harness adapters into broker spawn path: Moved harness adapters into broker spawn path +- Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity: Built-in harnesses are now data-backed adapter configs with optional lifecycle adapter identity + +--- + +## Artifacts + +**Commits:** c41b133d, 28ec8236, bcc07d62, 2078bc8f, cfa9b0b9, ae28c3c3, f3f3744d, 83ab2bba, 11281768, fd09d477, 423184bb, 0d49dd41, baaef913, c148f419, f82bd2cc, 117e76d6, 2b363811, 68fddec4, bf9427dc, eefe64ba, d8973ce7, f57c3e2f, d76b0f40, 19b22846, c1cf84d3, 6c83d4bd, fa3f757f, c992acd3, 9a97be3a, 489d32cd, 9d800626, de2169fc, dc6cad51, 75771972, 4be45b44, f953d0f9, bbe4f00e, 90dc1faa, e5db219c, 66879e0c, 79311e9f, 8bcc3cfe, 3332e837, c13ee318, 5b4005ef, 6dea769a, c8299b7a, 839d0cda, e1022e4d +**Files changed:** 169 diff --git a/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.trace.json b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.trace.json new file mode 100644 index 000000000..96db5d1de --- /dev/null +++ b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.trace.json @@ -0,0 +1,5268 @@ +{ + "version": "1.0.0", + "id": "56ccacd7-ecd7-476a-b20b-e1d9960e3f70", + "timestamp": "2026-05-25T01:45:41.950Z", + "trajectory": "traj_4b2d63f6ljvh", + "files": [ + { + "path": ".claude/scheduled_tasks.lock", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 1, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".github/workflows/package-validation.yml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 71, + "end_line": 85, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 101, + "end_line": 106, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 138, + "end_line": 143, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 202, + "end_line": 213, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".github/workflows/preview-web.yml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 54, + "end_line": 95, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".github/workflows/publish.yml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 70, + "end_line": 76, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1072, + "end_line": 1077, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1166, + "end_line": 1171, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1914, + "end_line": 1927, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1946, + "end_line": 2084, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2099, + "end_line": 2110, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/active/traj_4b2d63f6ljvh.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 150, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_5k0jtc1g5l33.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_5k0jtc1g5l33.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_78ytpicts778.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_78ytpicts778.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_90jmd9z27oap.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_90jmd9z27oap.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 24, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_bz1a1o15p7px.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_bz1a1o15p7px.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_ceo5q9bh2od3.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_ceo5q9bh2od3.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_dbsnr453nxjw.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_dbsnr453nxjw.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_dcl9hgoiuac5.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_dcl9hgoiuac5.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_dqgg2q4scsvt.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_dqgg2q4scsvt.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 31, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 60, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 38, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.trace.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 30, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_mytnzgfayj3d.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_mytnzgfayj3d.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 24, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 61, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 38, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.trace.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 101, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_r3eic6rt84pq.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 25, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_r3eic6rt84pq.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 14, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_s5ojo1f4srz4.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_s5ojo1f4srz4.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_vfa1jr6otnjn.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_vfa1jr6otnjn.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 24, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": ".trajectories/index.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 321, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 324, + "end_line": 510, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 513, + "end_line": 552, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 560, + "end_line": 594, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 597, + "end_line": 650, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 658, + "end_line": 685, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 693, + "end_line": 832, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 835, + "end_line": 1126, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1134, + "end_line": 1237, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "AGENTS.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 26, + "end_line": 51, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "CHANGELOG.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 7, + "end_line": 1256, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "README.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 49, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 96, + "end_line": 110, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 113, + "end_line": 119, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 121, + "end_line": 127, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/broker/injection_format.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 127, + "end_line": 133, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 267, + "end_line": 273, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/cli/mod.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 224, + "end_line": 236, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/cli_mcp_args.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 369, + "end_line": 377, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/codex_session.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 241, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/lib.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 4, + "end_line": 10, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/listen_api.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 11, + "end_line": 17, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 32, + "end_line": 45, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 52, + "end_line": 58, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 178, + "end_line": 193, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 213, + "end_line": 233, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 421, + "end_line": 427, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 532, + "end_line": 548, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 564, + "end_line": 570, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 649, + "end_line": 681, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 692, + "end_line": 698, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 705, + "end_line": 711, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 817, + "end_line": 898, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2393, + "end_line": 2424, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2429, + "end_line": 2435, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2442, + "end_line": 2460, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2467, + "end_line": 2476, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2492, + "end_line": 2502, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2506, + "end_line": 2512, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2522, + "end_line": 2598, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/protocol.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 19, + "end_line": 71, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 78, + "end_line": 87, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 208, + "end_line": 215, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 321, + "end_line": 335, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 507, + "end_line": 513, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 551, + "end_line": 586, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/relaycast/mod.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 4, + "end_line": 12, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/api.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 18, + "end_line": 24, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 34, + "end_line": 40, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 47, + "end_line": 53, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 60, + "end_line": 66, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 194, + "end_line": 210, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 213, + "end_line": 219, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 269, + "end_line": 275, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 289, + "end_line": 344, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 424, + "end_line": 430, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/event_loop.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 32, + "end_line": 38, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/init.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 5, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 135, + "end_line": 142, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 187, + "end_line": 192, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 336, + "end_line": 349, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 422, + "end_line": 428, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 487, + "end_line": 493, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 500, + "end_line": 569, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/maintenance.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 13, + "end_line": 19, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 170, + "end_line": 176, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 217, + "end_line": 223, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 305, + "end_line": 311, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/mod.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 31, + "end_line": 46, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/relaycast_events.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 14, + "end_line": 20, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 108, + "end_line": 114, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 223, + "end_line": 230, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 235, + "end_line": 241, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 289, + "end_line": 295, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 337, + "end_line": 343, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 436, + "end_line": 443, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 486, + "end_line": 492, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 534, + "end_line": 540, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/session.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 110, + "end_line": 115, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 154, + "end_line": 159, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 196, + "end_line": 201, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/spawn_spec.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 13, + "end_line": 19, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 59, + "end_line": 66, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/tests.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 70, + "end_line": 77, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 305, + "end_line": 311, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 323, + "end_line": 338, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 348, + "end_line": 353, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 378, + "end_line": 388, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1928, + "end_line": 1934, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1945, + "end_line": 1979, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1981, + "end_line": 1987, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2042, + "end_line": 2048, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/runtime/worker_events.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 316, + "end_line": 322, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 324, + "end_line": 333, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 337, + "end_line": 343, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/snippets.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 9, + "end_line": 18, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 39, + "end_line": 45, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 124, + "end_line": 149, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 152, + "end_line": 158, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 170, + "end_line": 176, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 179, + "end_line": 185, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 190, + "end_line": 196, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 206, + "end_line": 212, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 215, + "end_line": 221, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 266, + "end_line": 276, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 291, + "end_line": 298, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 340, + "end_line": 346, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 348, + "end_line": 368, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 407, + "end_line": 435, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 441, + "end_line": 448, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 564, + "end_line": 570, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 573, + "end_line": 592, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 689, + "end_line": 722, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 741, + "end_line": 754, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 784, + "end_line": 815, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 822, + "end_line": 831, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 836, + "end_line": 873, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 880, + "end_line": 890, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 892, + "end_line": 898, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 909, + "end_line": 915, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 985, + "end_line": 995, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 998, + "end_line": 1026, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1064, + "end_line": 1071, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1079, + "end_line": 1085, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1098, + "end_line": 1104, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1106, + "end_line": 1112, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1160, + "end_line": 1189, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1203, + "end_line": 1209, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1339, + "end_line": 1346, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1519, + "end_line": 1526, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1557, + "end_line": 1567, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1585, + "end_line": 1610, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1696, + "end_line": 1727, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1803, + "end_line": 1810, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1830, + "end_line": 1861, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1971, + "end_line": 2005, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2321, + "end_line": 2327, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2337, + "end_line": 2343, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2375, + "end_line": 2381, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2398, + "end_line": 2404, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2448, + "end_line": 2454, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2625, + "end_line": 2631, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2673, + "end_line": 2679, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2718, + "end_line": 2724, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2766, + "end_line": 2772, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2826, + "end_line": 2832, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2869, + "end_line": 2883, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2891, + "end_line": 2938, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2957, + "end_line": 2963, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/supervisor.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 10, + "end_line": 16, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 59, + "end_line": 65, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 76, + "end_line": 82, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 98, + "end_line": 104, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 107, + "end_line": 113, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 120, + "end_line": 126, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 198, + "end_line": 204, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 232, + "end_line": 239, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 278, + "end_line": 284, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 302, + "end_line": 308, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 322, + "end_line": 328, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 351, + "end_line": 357, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 379, + "end_line": 385, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 402, + "end_line": 408, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 418, + "end_line": 424, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 440, + "end_line": 446, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 462, + "end_line": 468, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 481, + "end_line": 487, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 502, + "end_line": 508, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/types.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 5, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 215, + "end_line": 244, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/worker.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 7, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9, + "end_line": 21, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 138, + "end_line": 144, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 160, + "end_line": 166, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 169, + "end_line": 185, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 189, + "end_line": 195, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 202, + "end_line": 208, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 211, + "end_line": 217, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 234, + "end_line": 243, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 263, + "end_line": 339, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 349, + "end_line": 418, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 445, + "end_line": 451, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 484, + "end_line": 494, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 782, + "end_line": 1231, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1535, + "end_line": 1618, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1661, + "end_line": 1741, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "crates/broker/src/wrap.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 599, + "end_line": 604, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "docs/cli-command-tree.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 236, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "package-lock.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 12, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 18, + "end_line": 55, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 175, + "end_line": 180, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2902, + "end_line": 2907, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2918, + "end_line": 2923, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2934, + "end_line": 2939, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2950, + "end_line": 2955, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3819, + "end_line": 3842, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3846, + "end_line": 3854, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3864, + "end_line": 3872, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3928, + "end_line": 3933, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 4654, + "end_line": 4659, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5638, + "end_line": 5643, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5825, + "end_line": 5830, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5836, + "end_line": 5841, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5867, + "end_line": 5872, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5876, + "end_line": 5881, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5913, + "end_line": 5918, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 5940, + "end_line": 5945, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 6643, + "end_line": 6684, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 6795, + "end_line": 6801, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 7072, + "end_line": 7077, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 8593, + "end_line": 8601, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 8609, + "end_line": 8617, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 8620, + "end_line": 8627, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 8705, + "end_line": 8711, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9406, + "end_line": 9411, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9420, + "end_line": 9425, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9742, + "end_line": 9748, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9767, + "end_line": 9773, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9803, + "end_line": 9809, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 9848, + "end_line": 9853, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 11867, + "end_line": 11873, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 11881, + "end_line": 11887, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 12634, + "end_line": 12639, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 12744, + "end_line": 12749, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 12765, + "end_line": 12770, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 12840, + "end_line": 12863, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 13028, + "end_line": 13033, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 13268, + "end_line": 13273, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 13850, + "end_line": 13856, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 13894, + "end_line": 13899, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 14745, + "end_line": 14751, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15580, + "end_line": 15588, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15610, + "end_line": 15630, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15632, + "end_line": 15637, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15763, + "end_line": 15772, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15782, + "end_line": 15790, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15921, + "end_line": 15958, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15966, + "end_line": 15974, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15984, + "end_line": 15990, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 15996, + "end_line": 16002, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16007, + "end_line": 16013, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16071, + "end_line": 16079, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16082, + "end_line": 16090, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16094, + "end_line": 16104, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16107, + "end_line": 16115, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16118, + "end_line": 16128, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16887, + "end_line": 16900, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16903, + "end_line": 16915, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16921, + "end_line": 16929, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16931, + "end_line": 16944, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 16976, + "end_line": 17026, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 17055, + "end_line": 17063, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 17066, + "end_line": 17074, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 17078, + "end_line": 17084, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 20, + "end_line": 29, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 55, + "end_line": 60, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 85, + "end_line": 91, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 135, + "end_line": 168, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/acp-bridge/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 46, + "end_line": 52, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/agent/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 19, + "end_line": 25, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/brand/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/broker-darwin-arm64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/broker-darwin-x64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/broker-linux-arm64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/broker-linux-x64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/broker-win32-x64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/browser-primitive/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 38, + "end_line": 44, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/cloud/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 23, + "end_line": 29, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/config/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/credential-proxy/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/events/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/gateway/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 23, + "end_line": 29, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/github-primitive/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 32, + "end_line": 38, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/hooks/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 37, + "end_line": 45, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/memory/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/openclaw/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 29, + "end_line": 35, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/openclaw/skill/SKILL.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 361, + "end_line": 367, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 512, + "end_line": 518, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/openclaw/src/identity/files.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 23, + "end_line": 29, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 41, + "end_line": 47, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 51, + "end_line": 59, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 71, + "end_line": 77, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 88, + "end_line": 94, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 99, + "end_line": 105, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 112, + "end_line": 118, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 168, + "end_line": 190, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 192, + "end_line": 196, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/openclaw/src/setup.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 397, + "end_line": 404, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 445, + "end_line": 452, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/openclaw/templates/SOUL.md.template", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 25, + "end_line": 31, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/personas/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/policy/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/pyproject.toml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 4, + "end_line": 10, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 21, + "end_line": 26, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/src/agent_relay/__init__.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 63, + "end_line": 73, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 133, + "end_line": 143, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/src/agent_relay/builder.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 36, + "end_line": 42, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 83, + "end_line": 89, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 131, + "end_line": 152, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 391, + "end_line": 398, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/src/agent_relay/client.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 331, + "end_line": 337, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 349, + "end_line": 355, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 370, + "end_line": 376, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 395, + "end_line": 401, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/src/agent_relay/relay.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 17, + "end_line": 23, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 27, + "end_line": 42, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 57, + "end_line": 63, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 82, + "end_line": 94, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 101, + "end_line": 110, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 330, + "end_line": 336, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 361, + "end_line": 367, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 381, + "end_line": 387, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 391, + "end_line": 397, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 423, + "end_line": 429, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 440, + "end_line": 450, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 474, + "end_line": 505, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 570, + "end_line": 576, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 595, + "end_line": 601, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 605, + "end_line": 611, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 653, + "end_line": 659, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 680, + "end_line": 690, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 813, + "end_line": 830, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 850, + "end_line": 860, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 905, + "end_line": 915, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/src/agent_relay/types.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 32, + "end_line": 51, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 194, + "end_line": 242, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 373, + "end_line": 379, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 391, + "end_line": 401, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/tests/test_builder.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 3, + "end_line": 9, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 108, + "end_line": 137, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/tests/test_relay_harness.py", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 28, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/uv.lock", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 10, + "end_line": 16, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 41, + "end_line": 46, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 55, + "end_line": 62, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 82, + "end_line": 87, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 286, + "end_line": 291, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 993, + "end_line": 998, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2151, + "end_line": 2156, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2560, + "end_line": 2565, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3391, + "end_line": 3396, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3802, + "end_line": 3807, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 4312, + "end_line": 4317, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 4434, + "end_line": 4439, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 4472, + "end_line": 4477, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 4502, + "end_line": 4507, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 4894, + "end_line": 4899, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 40, + "end_line": 46, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 48, + "end_line": 54, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 63, + "end_line": 69, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 77, + "end_line": 83, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 386, + "end_line": 392, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 396, + "end_line": 402, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/README.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 58, + "end_line": 80, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 106, + "end_line": 116, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 124, + "end_line": 141, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 165, + "end_line": 175, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 181, + "end_line": 199, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/__tests__/client-broker-exit.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 141, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/__tests__/lifecycle-hooks.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 6, + "end_line": 12, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 231, + "end_line": 264, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/__tests__/orchestration-upgrades.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 130, + "end_line": 154, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 566, + "end_line": 673, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/__tests__/spawn-harness.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 106, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/__tests__/unit.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 82, + "end_line": 90, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/cli-registry.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 11, + "end_line": 17, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 20, + "end_line": 27, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 30, + "end_line": 37, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 54, + "end_line": 63, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 118, + "end_line": 315, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/cli-resolver.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 11, + "end_line": 16, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 53, + "end_line": 59, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 105, + "end_line": 111, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/client.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 71, + "end_line": 87, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 116, + "end_line": 127, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 151, + "end_line": 158, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 171, + "end_line": 177, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 184, + "end_line": 190, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 196, + "end_line": 202, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 209, + "end_line": 215, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 259, + "end_line": 266, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 284, + "end_line": 321, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 462, + "end_line": 468, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 508, + "end_line": 521, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 551, + "end_line": 587, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 592, + "end_line": 598, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 603, + "end_line": 609, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 615, + "end_line": 621, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 633, + "end_line": 639, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 645, + "end_line": 659, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 690, + "end_line": 696, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 991, + "end_line": 1035, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1134, + "end_line": 1146, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/event-bus.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 18, + "end_line": 54, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 57, + "end_line": 63, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 65, + "end_line": 74, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 80, + "end_line": 88, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/index.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 11, + "end_line": 22, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/lifecycle-hooks.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 5, + "end_line": 12, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 27, + "end_line": 33, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 52, + "end_line": 61, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 81, + "end_line": 87, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 154, + "end_line": 160, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/protocol.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 24, + "end_line": 31, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 262, + "end_line": 268, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 403, + "end_line": 409, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 435, + "end_line": 448, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/relay-adapter.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 21, + "end_line": 27, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 87, + "end_line": 93, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 200, + "end_line": 206, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/relay.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 28, + "end_line": 44, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 50, + "end_line": 57, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 128, + "end_line": 155, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 193, + "end_line": 229, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 269, + "end_line": 275, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 301, + "end_line": 311, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 323, + "end_line": 341, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 366, + "end_line": 375, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 389, + "end_line": 396, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 424, + "end_line": 439, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 447, + "end_line": 455, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 458, + "end_line": 465, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 499, + "end_line": 519, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 552, + "end_line": 586, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 608, + "end_line": 614, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 623, + "end_line": 632, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 643, + "end_line": 650, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 669, + "end_line": 692, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 818, + "end_line": 826, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 836, + "end_line": 846, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 849, + "end_line": 855, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 858, + "end_line": 869, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 875, + "end_line": 890, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 892, + "end_line": 910, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 912, + "end_line": 918, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 921, + "end_line": 945, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 960, + "end_line": 969, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 988, + "end_line": 994, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 997, + "end_line": 1003, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1006, + "end_line": 1012, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1155, + "end_line": 1161, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1428, + "end_line": 1460, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1737, + "end_line": 1743, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1755, + "end_line": 1768, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1781, + "end_line": 1794, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1798, + "end_line": 1804, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1898, + "end_line": 1913, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1915, + "end_line": 2060, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2106, + "end_line": 2116, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2195, + "end_line": 2203, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2264, + "end_line": 2279, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2287, + "end_line": 2298, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2307, + "end_line": 2317, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2321, + "end_line": 2337, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2344, + "end_line": 2359, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2361, + "end_line": 2367, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2391, + "end_line": 2400, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/types.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 9, + "end_line": 19, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 31, + "end_line": 37, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 49, + "end_line": 55, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 70, + "end_line": 76, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 79, + "end_line": 85, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 108, + "end_line": 114, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workers.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 38, + "end_line": 44, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/README.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 234, + "end_line": 266, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/__tests__/harness-adapters.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 78, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/builder.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 10, + "end_line": 16, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 150, + "end_line": 176, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 197, + "end_line": 203, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 316, + "end_line": 343, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 500, + "end_line": 510, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 541, + "end_line": 547, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/index.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 47, + "end_line": 60, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/process-backend-executor.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 11, + "end_line": 17, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 49, + "end_line": 55, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/process-spawner.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 10, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 40, + "end_line": 46, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 49, + "end_line": 55, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 179, + "end_line": 185, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/run.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 5, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 29, + "end_line": 36, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 56, + "end_line": 62, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/runner.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 28, + "end_line": 34, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 75, + "end_line": 82, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 310, + "end_line": 317, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 442, + "end_line": 448, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 557, + "end_line": 563, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 568, + "end_line": 575, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1751, + "end_line": 1765, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2086, + "end_line": 2100, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2202, + "end_line": 2254, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2287, + "end_line": 2293, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 2907, + "end_line": 2918, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3082, + "end_line": 3088, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 3217, + "end_line": 3227, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 6396, + "end_line": 6402, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 6413, + "end_line": 6419, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 6467, + "end_line": 6473, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/schema.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 33, + "end_line": 45, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 252, + "end_line": 275, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/types.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 10, + "end_line": 16, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 25, + "end_line": 31, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 50, + "end_line": 57, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 30, + "end_line": 40, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/telemetry/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/trajectory/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/user-directory/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "packages/user-directory/src/index.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "packages/user-directory/src/user-directory.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "packages/user-directory/tsconfig.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "packages/user-directory/vitest.config.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "packages/utils/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 111, + "end_line": 117, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/workflow-types/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "packages/workflow-types/src/index.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 85, + "end_line": 94, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 101, + "end_line": 147, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "plugins/codex-relay-skill/README.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 61, + "end_line": 67, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "plugins/codex-relay-skill/codex-config/config.toml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 5, + "end_line": 9, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "plugins/codex-relay-skill/scripts/setup.sh", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 154, + "end_line": 160, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "plugins/gemini-relay-extension/package-lock.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 8, + "end_line": 16, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "plugins/gemini-relay-extension/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 30, + "end_line": 35, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "plugins/gemini-relay-extension/relay-server.js", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 10, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 72, + "end_line": 101, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "readme-banner.png", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "scripts/demos/three-way-debate.gif", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "scripts/demos/three-way-debate.mp4", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [] + } + ] + }, + { + "path": "scripts/demos/three-way-debate.tape", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 82, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "scripts/watch-cli-tools.sh", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 73, + "end_line": 79, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "src/cli/bootstrap.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 122, + "end_line": 128, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 300, + "end_line": 312, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 319, + "end_line": 327, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "src/cli/commands/agent-management.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 19, + "end_line": 32, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 418, + "end_line": 424, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 430, + "end_line": 436, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 439, + "end_line": 451, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "src/cli/commands/core.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 544, + "end_line": 551, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 556, + "end_line": 563, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 571, + "end_line": 578, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "src/cli/relaycast-mcp.startup.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 7, + "end_line": 13, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 26, + "end_line": 84, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 87, + "end_line": 103, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 111, + "end_line": 117, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 123, + "end_line": 137, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 146, + "end_line": 233, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 240, + "end_line": 247, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 263, + "end_line": 269, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 277, + "end_line": 324, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 338, + "end_line": 414, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 419, + "end_line": 436, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 444, + "end_line": 462, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 467, + "end_line": 473, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 479, + "end_line": 493, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 512, + "end_line": 534, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 539, + "end_line": 548, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 568, + "end_line": 573, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 589, + "end_line": 594, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "src/cli/relaycast-mcp.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 4, + "end_line": 58, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 62, + "end_line": 93, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 102, + "end_line": 108, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 140, + "end_line": 274, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 276, + "end_line": 309, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 362, + "end_line": 686, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 688, + "end_line": 701, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 705, + "end_line": 737, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 739, + "end_line": 745, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 747, + "end_line": 788, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 796, + "end_line": 1289, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1295, + "end_line": 1302, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1305, + "end_line": 1409, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1411, + "end_line": 1423, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1464, + "end_line": 1474, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1476, + "end_line": 1482, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1484, + "end_line": 1493, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1510, + "end_line": 1517, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 1523, + "end_line": 1529, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "tests/mcp_merge_e2e.rs", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 225, + "end_line": 231, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "tsconfig.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 32, + "end_line": 38, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "vitest.config.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 26, + "end_line": 31, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/components/docs/DocsNav.tsx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 25, + "end_line": 31, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 56, + "end_line": 62, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/cli-broker-lifecycle.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 19, + "end_line": 25, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/event-handlers.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 149, + "end_line": 164, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/harnesses.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 201, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/local-mode.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 41, + "end_line": 47, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/proactive-agents.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 261, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/reference-cli.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 24, + "end_line": 30, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/reference-workflows.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 177, + "end_line": 181, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/spawning-an-agent.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 43, + "end_line": 50, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 109, + "end_line": 134, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/content/docs/typescript-sdk.mdx", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 110, + "end_line": 152, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 294, + "end_line": 314, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 401, + "end_line": 413, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/lib/docs-nav.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 20, + "end_line": 26, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 39, + "end_line": 45, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + }, + { + "path": "web/sst.config.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 5, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 8, + "end_line": 21, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + }, + { + "start_line": 23, + "end_line": 35, + "revision": "c41b133d8fbe3649ae312b6b670d81861184f178" + } + ] + } + ] + } + ] +} diff --git a/.trajectories/completed/2026-05/traj_5kytmhye9atg.json b/.trajectories/completed/2026-05/traj_5kytmhye9atg.json new file mode 100644 index 000000000..67e6cc7ad --- /dev/null +++ b/.trajectories/completed/2026-05/traj_5kytmhye9atg.json @@ -0,0 +1,25 @@ +{ + "id": "traj_5kytmhye9atg", + "version": 1, + "task": { + "title": "Fix harness docs language examples" + }, + "status": "completed", + "startedAt": "2026-05-25T13:19:34.556Z", + "completedAt": "2026-05-25T13:19:41.038Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Fixed the harness docs page so TypeScript/Python examples switch with the global docs language selector and added a Harnesses sidebar icon.", + "approach": "Standard approach", + "confidence": 0.92 + }, + "commits": [], + "filesChanged": [], + "projectId": "", + "tags": [], + "_trace": { + "startRef": "bffae82614f439546dab4d4be4e195b59e2575c6", + "endRef": "bffae82614f439546dab4d4be4e195b59e2575c6" + } +} diff --git a/.trajectories/completed/2026-05/traj_5kytmhye9atg.md b/.trajectories/completed/2026-05/traj_5kytmhye9atg.md new file mode 100644 index 000000000..06ea2f639 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_5kytmhye9atg.md @@ -0,0 +1,14 @@ +# Trajectory: Fix harness docs language examples + +> **Status:** ✅ Completed +> **Confidence:** 92% +> **Started:** May 25, 2026 at 09:19 AM +> **Completed:** May 25, 2026 at 09:19 AM + +--- + +## Summary + +Fixed the harness docs page so TypeScript/Python examples switch with the global docs language selector and added a Harnesses sidebar icon. + +**Approach:** Standard approach diff --git a/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json new file mode 100644 index 000000000..c08f4a946 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json @@ -0,0 +1,25 @@ +{ + "id": "traj_60dr7ojudhfu", + "version": 1, + "task": { + "title": "Address final harness adapter PR comments" + }, + "status": "completed", + "startedAt": "2026-05-25T12:12:15.123Z", + "completedAt": "2026-05-25T12:18:37.956Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Addressed final harness adapter review comments: npx -y OpenClaw registration, blank binary handling, inherited binaries override tests, and instance-scoped workflow harness command resolution.", + "approach": "Standard approach", + "confidence": 0.88 + }, + "commits": [], + "filesChanged": [], + "projectId": "", + "tags": [], + "_trace": { + "startRef": "dbbd13fdd5c37535f1782e2234d7e8ee514ed32a", + "endRef": "dbbd13fdd5c37535f1782e2234d7e8ee514ed32a" + } +} diff --git a/.trajectories/completed/2026-05/traj_60dr7ojudhfu.md b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.md new file mode 100644 index 000000000..6efb5dcc6 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.md @@ -0,0 +1,14 @@ +# Trajectory: Address final harness adapter PR comments + +> **Status:** ✅ Completed +> **Confidence:** 88% +> **Started:** May 25, 2026 at 08:12 AM +> **Completed:** May 25, 2026 at 08:18 AM + +--- + +## Summary + +Addressed final harness adapter review comments: npx -y OpenClaw registration, blank binary handling, inherited binaries override tests, and instance-scoped workflow harness command resolution. + +**Approach:** Standard approach diff --git a/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json new file mode 100644 index 000000000..440ecb93b --- /dev/null +++ b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json @@ -0,0 +1,53 @@ +{ + "id": "traj_dqgg2q4scsvt", + "version": 1, + "task": { + "title": "Assess PR 932 event listener need" + }, + "status": "completed", + "startedAt": "2026-05-22T01:05:44.531Z", + "completedAt": "2026-05-22T01:07:31.036Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-22T01:07:27.231Z" + } + ], + "chapters": [ + { + "id": "chap_lq1hef4cotfp", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-22T01:07:27.231Z", + "endedAt": "2026-05-22T01:07:31.036Z", + "events": [ + { + "ts": 1779412047232, + "type": "decision", + "content": "Expose structured agent results through AgentRelay addListener registry: Expose structured agent results through AgentRelay addListener registry", + "raw": { + "question": "Expose structured agent results through AgentRelay addListener registry", + "chosen": "Expose structured agent results through AgentRelay addListener registry", + "alternatives": [], + "reasoning": "PR 936 removed single on* callback fields and made AgentRelayEvents the typed multi-listener surface; PR 932 introduces a new broker event and global observer, so it should be agentResult in addListener rather than relay.onAgentResult." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Assessed PR 932 against recent listener changes. Recommendation: keep a global structured-result listener, but expose it as relay.addListener('agentResult', handler) in AgentRelayEvents, not relay.onAgentResult; PR needs rebase over PR 936 listener registry.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "relay", + "tags": [], + "_trace": { + "startRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b", + "endRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b" + } +} diff --git a/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.md b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.md new file mode 100644 index 000000000..5babb2c18 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.md @@ -0,0 +1,31 @@ +# Trajectory: Assess PR 932 event listener need + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 21, 2026 at 09:05 PM +> **Completed:** May 21, 2026 at 09:07 PM + +--- + +## Summary + +Assessed PR 932 against recent listener changes. Recommendation: keep a global structured-result listener, but expose it as relay.addListener('agentResult', handler) in AgentRelayEvents, not relay.onAgentResult; PR needs rebase over PR 936 listener registry. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Expose structured agent results through AgentRelay addListener registry +- **Chose:** Expose structured agent results through AgentRelay addListener registry +- **Reasoning:** PR 936 removed single on* callback fields and made AgentRelayEvents the typed multi-listener surface; PR 932 introduces a new broker event and global observer, so it should be agentResult in addListener rather than relay.onAgentResult. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Expose structured agent results through AgentRelay addListener registry: Expose structured agent results through AgentRelay addListener registry diff --git a/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json new file mode 100644 index 000000000..2ca107d52 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json @@ -0,0 +1,54 @@ +{ + "id": "traj_ft1pwdlcrmcn", + "version": 1, + "task": { + "title": "Fix CI run 26262957675" + }, + "status": "completed", + "startedAt": "2026-05-22T01:31:45.965Z", + "completedAt": "2026-05-22T01:34:57.413Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-22T01:34:08.396Z" + } + ], + "chapters": [ + { + "id": "chap_tb6hi35n4sv3", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-22T01:34:08.396Z", + "endedAt": "2026-05-22T01:34:57.413Z", + "events": [ + { + "ts": 1779413648397, + "type": "decision", + "content": "Patch SST ACM repair guard to scan state recursively: Patch SST ACM repair guard to scan state recursively", + "raw": { + "question": "Patch SST ACM repair guard to scan state recursively", + "chosen": "Patch SST ACM repair guard to scan state recursively", + "alternatives": [], + "reasoning": "The merged guard reported no WebCdnSslCertificate in state, but deploy still failed on WebCdnSslValidation referencing the timed-out ACM cert. Recursive state scanning and validation-resource ARN extraction matches the observed SST export shape." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Diagnosed follow-up Deploy Web failure after PR 942. The guard ran but missed SST's exported validation resource shape, so I updated it to recursively scan state and extract the ACM ARN from WebCdnSslValidation before deciding to clear stale state. Opened PR 943.", + "approach": "Standard approach", + "confidence": 0.82 + }, + "commits": ["898b8ee3", "adb6d6b9", "e5554b50"], + "filesChanged": [".github/scripts/repair-failed-sst-acm-cert.sh"], + "projectId": "relay", + "tags": [], + "_trace": { + "startRef": "0d716e468654c8d98b867df09298aafce0b23604", + "endRef": "898b8ee37197075913578fdb1c2fe4139b2ac562", + "traceId": "672b9fc7-be82-421b-8103-a18cdd0f914f" + } +} diff --git a/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.md b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.md new file mode 100644 index 000000000..fe97c84cb --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.md @@ -0,0 +1,38 @@ +# Trajectory: Fix CI run 26262957675 + +> **Status:** ✅ Completed +> **Confidence:** 82% +> **Started:** May 21, 2026 at 09:31 PM +> **Completed:** May 21, 2026 at 09:34 PM + +--- + +## Summary + +Diagnosed follow-up Deploy Web failure after PR 942. The guard ran but missed SST's exported validation resource shape, so I updated it to recursively scan state and extract the ACM ARN from WebCdnSslValidation before deciding to clear stale state. Opened PR 943. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Patch SST ACM repair guard to scan state recursively +- **Chose:** Patch SST ACM repair guard to scan state recursively +- **Reasoning:** The merged guard reported no WebCdnSslCertificate in state, but deploy still failed on WebCdnSslValidation referencing the timed-out ACM cert. Recursive state scanning and validation-resource ARN extraction matches the observed SST export shape. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Patch SST ACM repair guard to scan state recursively: Patch SST ACM repair guard to scan state recursively + +--- + +## Artifacts + +**Commits:** 898b8ee3, adb6d6b9, e5554b50 +**Files changed:** 1 diff --git a/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.trace.json b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.trace.json new file mode 100644 index 000000000..e92c6fd0f --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.trace.json @@ -0,0 +1,30 @@ +{ + "version": "1.0.0", + "id": "672b9fc7-be82-421b-8103-a18cdd0f914f", + "timestamp": "2026-05-22T01:34:57.507Z", + "trajectory": "traj_ft1pwdlcrmcn", + "files": [ + { + "path": ".github/scripts/repair-failed-sst-acm-cert.sh", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 17, + "end_line": 36, + "revision": "898b8ee37197075913578fdb1c2fe4139b2ac562" + }, + { + "start_line": 40, + "end_line": 89, + "revision": "898b8ee37197075913578fdb1c2fe4139b2ac562" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json new file mode 100644 index 000000000..f78c7e8e1 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json @@ -0,0 +1,25 @@ +{ + "id": "traj_g35xvgo8fbqq", + "version": 1, + "task": { + "title": "Fix package validation cache key" + }, + "status": "completed", + "startedAt": "2026-05-25T12:24:38.508Z", + "completedAt": "2026-05-25T12:24:47.132Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Bumped the Package Validation node_modules cache key to force npm ci and refresh nested workspace dependencies after the stale cache missed telemetry's posthog-node dependency.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "", + "tags": [], + "_trace": { + "startRef": "b5387872b677403c2d4d0d0346ea574e27b71900", + "endRef": "b5387872b677403c2d4d0d0346ea574e27b71900" + } +} diff --git a/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.md b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.md new file mode 100644 index 000000000..90ae939c3 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.md @@ -0,0 +1,14 @@ +# Trajectory: Fix package validation cache key + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 25, 2026 at 08:24 AM +> **Completed:** May 25, 2026 at 08:24 AM + +--- + +## Summary + +Bumped the Package Validation node_modules cache key to force npm ci and refresh nested workspace dependencies after the stale cache missed telemetry's posthog-node dependency. + +**Approach:** Standard approach diff --git a/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json new file mode 100644 index 000000000..c2b790aef --- /dev/null +++ b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json @@ -0,0 +1,59 @@ +{ + "id": "traj_pjadgfw0mtw4", + "version": 1, + "task": { + "title": "Review package dependencies" + }, + "status": "completed", + "startedAt": "2026-05-21T20:27:44.911Z", + "completedAt": "2026-05-21T20:31:49.784Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-21T20:31:46.039Z" + } + ], + "chapters": [ + { + "id": "chap_vxje78z2u126", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-21T20:31:46.039Z", + "endedAt": "2026-05-21T20:31:49.784Z", + "events": [ + { + "ts": 1779395506040, + "type": "decision", + "content": "Dependency review found duplicated root workspace deps and stale syncpack script: Dependency review found duplicated root workspace deps and stale syncpack script", + "raw": { + "question": "Dependency review found duplicated root workspace deps and stale syncpack script", + "chosen": "Dependency review found duplicated root workspace deps and stale syncpack script", + "alternatives": [], + "reasoning": "knip flags many root dependencies as unused because they are only used inside workspace packages that already declare them; syncpack v14 rejects the configured list-mismatches command and lint shows version drift across manifests." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Reviewed package dependency hygiene. Found root dependencies duplicated from workspace packages, stale syncpack script, missing root @agent-relay/memory dependency for src/index export, and local manifest/version drift for @posthog/next plus several workspace versions.", + "approach": "Standard approach", + "confidence": 0.78 + }, + "commits": ["c13ee318"], + "filesChanged": [ + "packages/sdk/src/__tests__/lifecycle-hooks.test.ts", + "packages/sdk/src/client.ts", + "packages/sdk/src/event-bus.ts", + "packages/sdk/src/relay.ts" + ], + "projectId": "relay", + "tags": [], + "_trace": { + "startRef": "23e07f08a24c2913a918aee2a0c9af9c54e4d40d", + "endRef": "c13ee3189b10c83fc52c4d017e268eedb5119f48", + "traceId": "7daa40cf-b98c-4790-b1d4-06c1fc4607ff" + } +} diff --git a/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.md b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.md new file mode 100644 index 000000000..f5a71f622 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.md @@ -0,0 +1,38 @@ +# Trajectory: Review package dependencies + +> **Status:** ✅ Completed +> **Confidence:** 78% +> **Started:** May 21, 2026 at 04:27 PM +> **Completed:** May 21, 2026 at 04:31 PM + +--- + +## Summary + +Reviewed package dependency hygiene. Found root dependencies duplicated from workspace packages, stale syncpack script, missing root @agent-relay/memory dependency for src/index export, and local manifest/version drift for @posthog/next plus several workspace versions. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Dependency review found duplicated root workspace deps and stale syncpack script +- **Chose:** Dependency review found duplicated root workspace deps and stale syncpack script +- **Reasoning:** knip flags many root dependencies as unused because they are only used inside workspace packages that already declare them; syncpack v14 rejects the configured list-mismatches command and lint shows version drift across manifests. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Dependency review found duplicated root workspace deps and stale syncpack script: Dependency review found duplicated root workspace deps and stale syncpack script + +--- + +## Artifacts + +**Commits:** c13ee318 +**Files changed:** 4 diff --git a/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.trace.json b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.trace.json new file mode 100644 index 000000000..403d231e3 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.trace.json @@ -0,0 +1,101 @@ +{ + "version": "1.0.0", + "id": "7daa40cf-b98c-4790-b1d4-06c1fc4607ff", + "timestamp": "2026-05-21T20:31:49.878Z", + "trajectory": "traj_pjadgfw0mtw4", + "files": [ + { + "path": "packages/sdk/src/__tests__/lifecycle-hooks.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 6, + "end_line": 12, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + }, + { + "start_line": 231, + "end_line": 264, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/client.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 259, + "end_line": 296, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/event-bus.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 18, + "end_line": 54, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + }, + { + "start_line": 57, + "end_line": 63, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + }, + { + "start_line": 65, + "end_line": 74, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + }, + { + "start_line": 80, + "end_line": 88, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/relay.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 31, + "end_line": 37, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + }, + { + "start_line": 466, + "end_line": 500, + "revision": "c13ee3189b10c83fc52c4d017e268eedb5119f48" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_ps68dydvgfuz.json b/.trajectories/completed/2026-05/traj_ps68dydvgfuz.json new file mode 100644 index 000000000..985d1070b --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ps68dydvgfuz.json @@ -0,0 +1,53 @@ +{ + "id": "traj_ps68dydvgfuz", + "version": 1, + "task": { + "title": "Add SDK harness adapter abstraction" + }, + "status": "completed", + "startedAt": "2026-05-25T14:04:38.337Z", + "completedAt": "2026-05-25T14:19:16.978Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-25T14:10:38.799Z" + } + ], + "chapters": [ + { + "id": "chap_ey367c3i3qdl", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-25T14:10:38.799Z", + "endedAt": "2026-05-25T14:19:16.978Z", + "events": [ + { + "ts": 1779718238800, + "type": "decision", + "content": "Split CLI harness config from runtime harness control types: Split CLI harness config from runtime harness control types", + "raw": { + "question": "Split CLI harness config from runtime harness control types", + "chosen": "Split CLI harness config from runtime harness control types", + "alternatives": [], + "reasoning": "The SDK already uses HarnessAdapter for serializable CLI command templates consumed by the Rust broker. A non-CLI harness needs a runtime contract over a serializable boundary, so this pass adds explicit runtime-facing types while preserving existing CLI registration behavior." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Added SDK harness runtime adapter types, clarified CLI harness adapter naming, and propagated broker spawn pid through SDK spawn results, agent handles, lifecycle hooks, and worker_ready events.", + "approach": "Standard approach", + "confidence": 0.86 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "77dc58139ff2674c08aee13bdb2d3ae0db2201a8", + "endRef": "77dc58139ff2674c08aee13bdb2d3ae0db2201a8" + } +} diff --git a/.trajectories/completed/2026-05/traj_ps68dydvgfuz.md b/.trajectories/completed/2026-05/traj_ps68dydvgfuz.md new file mode 100644 index 000000000..c5597fdc1 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ps68dydvgfuz.md @@ -0,0 +1,33 @@ +# Trajectory: Add SDK harness adapter abstraction + +> **Status:** ✅ Completed +> **Confidence:** 86% +> **Started:** May 25, 2026 at 10:04 AM +> **Completed:** May 25, 2026 at 10:19 AM + +--- + +## Summary + +Added SDK harness runtime adapter types, clarified CLI harness adapter naming, and propagated broker spawn pid through SDK spawn results, agent handles, lifecycle hooks, and worker_ready events. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Split CLI harness config from runtime harness control types + +- **Chose:** Split CLI harness config from runtime harness control types +- **Reasoning:** The SDK already uses HarnessAdapter for serializable CLI command templates consumed by the Rust broker. A non-CLI harness needs a runtime contract over a serializable boundary, so this pass adds explicit runtime-facing types while preserving existing CLI registration behavior. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Split CLI harness config from runtime harness control types: Split CLI harness config from runtime harness control types diff --git a/.trajectories/completed/2026-05/traj_q97ei72svf2f.json b/.trajectories/completed/2026-05/traj_q97ei72svf2f.json new file mode 100644 index 000000000..fda007d5c --- /dev/null +++ b/.trajectories/completed/2026-05/traj_q97ei72svf2f.json @@ -0,0 +1,53 @@ +{ + "id": "traj_q97ei72svf2f", + "version": 1, + "task": { + "title": "Address PR 978 review comments" + }, + "status": "completed", + "startedAt": "2026-05-25T02:33:51.921Z", + "completedAt": "2026-05-25T11:42:29.529Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-25T11:40:55.530Z" + } + ], + "chapters": [ + { + "id": "chap_v8c0suy2xbs4", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-25T11:40:55.530Z", + "endedAt": "2026-05-25T11:42:29.529Z", + "events": [ + { + "ts": 1779709255531, + "type": "decision", + "content": "Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution.: Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution.", + "raw": { + "question": "Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution.", + "chosen": "Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution.", + "alternatives": [], + "reasoning": "Review comments called out cross-instance and YAML parse leaks; explicit registerHarnessAdapter remains the opt-in global path, while instance/YAML harnesses are serialized to the broker or installed only during execution." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Addressed PR 978 review comments for harness adapter scoping, executable validation, SDK event/session fixes, MCP command setup, and CI failures.", + "approach": "Standard approach", + "confidence": 0.86 + }, + "commits": [], + "filesChanged": [], + "projectId": "relay", + "tags": [], + "_trace": { + "startRef": "7a265d5a9880642fd887796627ce75a24fa978ee", + "endRef": "7a265d5a9880642fd887796627ce75a24fa978ee" + } +} diff --git a/.trajectories/completed/2026-05/traj_q97ei72svf2f.md b/.trajectories/completed/2026-05/traj_q97ei72svf2f.md new file mode 100644 index 000000000..d5962edb4 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_q97ei72svf2f.md @@ -0,0 +1,33 @@ +# Trajectory: Address PR 978 review comments + +> **Status:** ✅ Completed +> **Confidence:** 86% +> **Started:** May 24, 2026 at 10:33 PM +> **Completed:** May 25, 2026 at 07:42 AM + +--- + +## Summary + +Addressed PR 978 review comments for harness adapter scoping, executable validation, SDK event/session fixes, MCP command setup, and CI failures. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution. + +- **Chose:** Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution. +- **Reasoning:** Review comments called out cross-instance and YAML parse leaks; explicit registerHarnessAdapter remains the opt-in global path, while instance/YAML harnesses are serialized to the broker or installed only during execution. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution.: Scoped SDK-provided harnesses to relay/workflow instances and used temporary registry snapshots for workflow execution. diff --git a/.trajectories/completed/2026-05/traj_qpefcnn38jnx.json b/.trajectories/completed/2026-05/traj_qpefcnn38jnx.json new file mode 100644 index 000000000..fe7a068d4 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_qpefcnn38jnx.json @@ -0,0 +1,53 @@ +{ + "id": "traj_qpefcnn38jnx", + "version": 1, + "task": { + "title": "Document harness lifecycle" + }, + "status": "completed", + "startedAt": "2026-05-25T13:32:44.211Z", + "completedAt": "2026-05-25T13:36:05.155Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-25T13:36:00.434Z" + } + ], + "chapters": [ + { + "id": "chap_f6hx4z58mixx", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-25T13:36:00.434Z", + "endedAt": "2026-05-25T13:36:05.155Z", + "events": [ + { + "ts": 1779716160435, + "type": "decision", + "content": "Documented harness lifecycle as broker-owned adapter preparation: Documented harness lifecycle as broker-owned adapter preparation", + "raw": { + "question": "Documented harness lifecycle as broker-owned adapter preparation", + "chosen": "Documented harness lifecycle as broker-owned adapter preparation", + "alternatives": [], + "reasoning": "The shipped behavior keeps executable config serializable in SDKs while CLI-specific MCP/session setup runs in Rust broker adapters; docs need to clarify where hooks and custom harness definitions fit." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Added harness lifecycle documentation to the Harnesses docs page, covering SDK selection, broker adapter resolution, executable lookup, built-in adapter preparation, argv rendering, runtime events, and how SDK hooks differ from broker lifecycle adapters.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "", + "tags": [], + "_trace": { + "startRef": "2b405d09bd018f5d395a5c07acfc71c1bd3b1488", + "endRef": "2b405d09bd018f5d395a5c07acfc71c1bd3b1488" + } +} diff --git a/.trajectories/completed/2026-05/traj_qpefcnn38jnx.md b/.trajectories/completed/2026-05/traj_qpefcnn38jnx.md new file mode 100644 index 000000000..5b8f33fe5 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_qpefcnn38jnx.md @@ -0,0 +1,33 @@ +# Trajectory: Document harness lifecycle + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 25, 2026 at 09:32 AM +> **Completed:** May 25, 2026 at 09:36 AM + +--- + +## Summary + +Added harness lifecycle documentation to the Harnesses docs page, covering SDK selection, broker adapter resolution, executable lookup, built-in adapter preparation, argv rendering, runtime events, and how SDK hooks differ from broker lifecycle adapters. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Documented harness lifecycle as broker-owned adapter preparation + +- **Chose:** Documented harness lifecycle as broker-owned adapter preparation +- **Reasoning:** The shipped behavior keeps executable config serializable in SDKs while CLI-specific MCP/session setup runs in Rust broker adapters; docs need to clarify where hooks and custom harness definitions fit. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Documented harness lifecycle as broker-owned adapter preparation: Documented harness lifecycle as broker-owned adapter preparation diff --git a/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json new file mode 100644 index 000000000..5387d1831 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json @@ -0,0 +1,25 @@ +{ + "id": "traj_r3eic6rt84pq", + "version": 1, + "task": { + "title": "Fix PR 932 structured result listener surface" + }, + "status": "completed", + "startedAt": "2026-05-22T01:09:21.137Z", + "completedAt": "2026-05-22T01:21:02.199Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Updated PR 932 after PR 936 by merging current main, replacing relay.onAgentResult with relay.addListener('agentResult', ...), documenting the listener, and validating SDK-focused tests plus repo typecheck before pushing codex/structured-agent-results.", + "approach": "Standard approach", + "confidence": 0.92 + }, + "commits": [], + "filesChanged": [], + "projectId": "relay", + "tags": [], + "_trace": { + "startRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b", + "endRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b" + } +} diff --git a/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md new file mode 100644 index 000000000..017d74d03 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md @@ -0,0 +1,14 @@ +# Trajectory: Fix PR 932 structured result listener surface + +> **Status:** ✅ Completed +> **Confidence:** 92% +> **Started:** May 21, 2026 at 09:09 PM +> **Completed:** May 21, 2026 at 09:21 PM + +--- + +## Summary + +Updated PR 932 after PR 936 by merging current main, replacing relay.onAgentResult with relay.addListener('agentResult', ...), documenting the listener, and validating SDK-focused tests plus repo typecheck before pushing codex/structured-agent-results. + +**Approach:** Standard approach diff --git a/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.json b/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.json new file mode 100644 index 000000000..fae4dd8d2 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.json @@ -0,0 +1,25 @@ +{ + "id": "traj_yh2ml7b0fze1", + "version": 1, + "task": { + "title": "Update harnesses docs page for harness adapters" + }, + "status": "completed", + "startedAt": "2026-05-25T14:35:45.053Z", + "completedAt": "2026-05-25T14:35:51.965Z", + "agents": [], + "chapters": [], + "retrospective": { + "summary": "Updated the website harnesses page to reflect CLIHarnessAdapter, HarnessRuntimeAdapter, and spawned session/process metadata.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "8d30ceceac8e9464c2cfb8ba54fb47620bf278ab", + "endRef": "8d30ceceac8e9464c2cfb8ba54fb47620bf278ab" + } +} diff --git a/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.md b/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.md new file mode 100644 index 000000000..42b4c8d8c --- /dev/null +++ b/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.md @@ -0,0 +1,14 @@ +# Trajectory: Update harnesses docs page for harness adapters + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 25, 2026 at 10:35 AM +> **Completed:** May 25, 2026 at 10:35 AM + +--- + +## Summary + +Updated the website harnesses page to reflect CLIHarnessAdapter, HarnessRuntimeAdapter, and spawned session/process metadata. + +**Approach:** Standard approach diff --git a/.trajectories/index.json b/.trajectories/index.json new file mode 100644 index 000000000..a3674eccc --- /dev/null +++ b/.trajectories/index.json @@ -0,0 +1,1203 @@ +{ + "version": 1, + "lastUpdated": "2026-05-25T14:35:52.143Z", + "trajectories": { + "traj_05xg7j388bc4": { + "title": "Add browser workflow step integration", + "status": "completed", + "startedAt": "2026-04-10T14:56:33.229Z", + "completedAt": "2026-04-10T15:05:14.660Z", + "path": "/.trajectories/completed/2026-04/traj_05xg7j388bc4.json" + }, + "traj_0t92gxaz6igh": { + "title": "Move docs sidebar into the mobile hamburger menu", + "status": "completed", + "startedAt": "2026-04-10T16:29:40.674Z", + "completedAt": "2026-04-10T16:32:14.544Z", + "path": "/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json" + }, + "traj_1776105620545_9dcebb3d": { + "title": "fix-inbox-agent-flag-workflow", + "status": "completed", + "startedAt": "2026-04-13T18:40:20.545Z", + "completedAt": "2026-04-13T18:41:52.831Z", + "path": "/.trajectories/completed/2026-04/traj_1776105620545_9dcebb3d.json" + }, + "traj_1776105988184_29f1270c": { + "title": "fix-inbox-agent-flag-workflow", + "status": "completed", + "startedAt": "2026-04-13T18:46:28.184Z", + "completedAt": "2026-04-13T20:23:54.308Z", + "path": "/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json" + }, + "traj_222ha5671idc": { + "title": "validate-cloud-connect-e2e-workflow", + "status": "completed", + "startedAt": "2026-04-15T21:32:51.980Z", + "completedAt": "2026-04-15T21:45:41.024Z", + "path": "/.trajectories/completed/2026-04/traj_222ha5671idc.json" + }, + "traj_3b3p1z4y7qlo": { + "title": "add-mcp-args-subcommand-workflow", + "status": "completed", + "startedAt": "2026-04-20T13:16:22.009Z", + "completedAt": "2026-04-20T13:26:35.142Z", + "path": "/.trajectories/completed/2026-04/traj_3b3p1z4y7qlo.json" + }, + "traj_4zqhfqw7g28l": { + "title": "Investigate GitHub Actions failure for run 24255758219 job 70826792063", + "status": "completed", + "startedAt": "2026-04-10T17:48:33.502Z", + "completedAt": "2026-04-10T17:49:14.485Z", + "path": "/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json" + }, + "traj_530xmbfeljyb": { + "title": "Implement GitHub primitive adapter base layer", + "status": "completed", + "startedAt": "2026-04-10T15:16:25.682Z", + "completedAt": "2026-04-10T15:25:16.937Z", + "path": "/.trajectories/completed/2026-04/traj_530xmbfeljyb.json" + }, + "traj_703m7sqyq89t": { + "title": "Fix production docs loader using build-machine absolute MDX paths", + "status": "completed", + "startedAt": "2026-04-10T16:33:10.601Z", + "completedAt": "2026-04-10T16:35:33.660Z", + "path": "/.trajectories/completed/2026-04/traj_703m7sqyq89t.json" + }, + "traj_8oh4r5km5eic": { + "title": "Implement GitHub primitive actions", + "status": "completed", + "startedAt": "2026-04-10T15:26:11.355Z", + "completedAt": "2026-04-10T15:33:35.150Z", + "path": "/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json" + }, + "traj_9tt55is74dq5": { + "title": "Pin TypeScript build resolution for acp-bridge, memory, trajectory, and cloud", + "status": "completed", + "startedAt": "2026-04-11T13:34:46.304Z", + "completedAt": "2026-04-11T13:35:22.677Z", + "path": "/.trajectories/completed/2026-04/traj_9tt55is74dq5.json" + }, + "traj_abjovknvcijv": { + "title": "Scope CI so web-only changes do not run unrelated relay and SDK tests", + "status": "completed", + "startedAt": "2026-04-10T16:08:30.070Z", + "completedAt": "2026-04-10T16:11:08.673Z", + "path": "/.trajectories/completed/2026-04/traj_abjovknvcijv.json" + }, + "traj_avmkyoo2s3rt": { + "title": "Implement Browser primitive client", + "status": "completed", + "startedAt": "2026-04-10T14:42:17.242Z", + "completedAt": "2026-04-10T14:55:45.196Z", + "path": "/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json" + }, + "traj_d48czxmgx4ac": { + "title": "Shift dark-mode footer from black to Relay blue", + "status": "completed", + "startedAt": "2026-04-10T16:12:27.477Z", + "completedAt": "2026-04-10T16:13:14.348Z", + "path": "/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json" + }, + "traj_dw8ihhdb8ip7": { + "title": "fix-dm-history-workflow", + "status": "abandoned", + "startedAt": "2026-04-13T19:51:57.984Z", + "completedAt": "2026-04-13T19:57:27.195Z", + "path": "/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json" + }, + "traj_e5i62wdjx0jd": { + "title": "Plan autofix finding groups", + "status": "completed", + "startedAt": "2026-04-13T09:40:42.044Z", + "completedAt": "2026-04-13T09:41:07.789Z", + "path": "/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json" + }, + "traj_g3muawdq6bsb": { + "title": "Invert dark footer gradient so the lighter blue is at the top", + "status": "completed", + "startedAt": "2026-04-10T16:13:19.744Z", + "completedAt": "2026-04-10T16:13:43.289Z", + "path": "/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json" + }, + "traj_mk0t0cgn4ytq": { + "title": "Merge origin/main into better-nav and resolve trajectory conflicts", + "status": "completed", + "startedAt": "2026-04-10T15:10:03.877Z", + "completedAt": "2026-04-10T15:10:29.410Z", + "path": "/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json" + }, + "traj_o8kgzhfu6jth": { + "title": "Add /cloud link to footer", + "status": "completed", + "startedAt": "2026-04-10T16:07:15.131Z", + "completedAt": "2026-04-10T16:07:42.930Z", + "path": "/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json" + }, + "traj_qb54w47qwod6": { + "title": "fix-history-from-workflow", + "status": "completed", + "startedAt": "2026-04-13T20:16:10.459Z", + "completedAt": "2026-04-13T20:25:09.219Z", + "path": "/.trajectories/completed/2026-04/traj_qb54w47qwod6.json" + }, + "traj_rs2bt3x0fqba": { + "title": "Compare failed web deploy to prior deploys and identify no-downtime fix", + "status": "completed", + "startedAt": "2026-04-10T17:50:43.088Z", + "completedAt": "2026-04-10T18:00:44.095Z", + "path": "/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json" + }, + "traj_tjadoebpscps": { + "title": "fix-dm-history-workflow", + "status": "completed", + "startedAt": "2026-04-13T20:02:27.719Z", + "completedAt": "2026-04-13T20:02:35.662Z", + "path": "/.trajectories/completed/2026-04/traj_tjadoebpscps.json" + }, + "traj_tv1x9pamkqad": { + "title": "Add GitHub primitive workflow step integration", + "status": "completed", + "startedAt": "2026-04-10T15:34:36.611Z", + "completedAt": "2026-04-10T15:42:17.590Z", + "path": "/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json" + }, + "traj_ui5omrgz819d": { + "title": "cloud-run-start-from-workflow", + "status": "completed", + "startedAt": "2026-04-27T20:00:33.269Z", + "completedAt": "2026-04-27T20:08:46.379Z", + "path": "/.trajectories/completed/2026-04/traj_ui5omrgz819d.json" + }, + "traj_w0xpsaoxuiyw": { + "title": "Pin TypeScript compiler version and build invocation in selected packages", + "status": "completed", + "startedAt": "2026-04-11T13:35:52.600Z", + "completedAt": "2026-04-11T13:36:48.341Z", + "path": "/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json" + }, + "traj_0e8i20oitwvz": { + "title": "Final fresh-eyes review Codex GPT-5.5 fix", + "status": "completed", + "startedAt": "2026-05-15T12:46:11.342Z", + "completedAt": "2026-05-15T12:46:11.500Z", + "path": "/.trajectories/completed/2026-05/traj_0e8i20oitwvz.json" + }, + "traj_0o6gb2wvk59t": { + "title": "Fresh end-to-end validation for headless readiness", + "status": "completed", + "startedAt": "2026-05-15T10:55:49.188Z", + "completedAt": "2026-05-15T11:11:52.324Z", + "path": "/.trajectories/completed/2026-05/traj_0o6gb2wvk59t.json" + }, + "traj_0z98tkaigaxg": { + "title": "ricky-child-update-issue-860-transcript-test-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:06:58.558Z", + "completedAt": "2026-05-15T21:35:56.724Z", + "path": "/.trajectories/completed/2026-05/traj_0z98tkaigaxg.json" + }, + "traj_1775914133873_35667beb": { + "title": "fix-sdk-build-resolution-workflow", + "status": "completed", + "startedAt": "2026-04-11T13:28:53.873Z", + "completedAt": "2026-05-08T13:33:48.161Z", + "path": "/.trajectories/completed/2026-05/traj_1775914133873_35667beb.json" + }, + "traj_1776073106646_1839be2d": { + "title": "autofix-swarm-Agentworkforce-relay-workflow", + "status": "completed", + "startedAt": "2026-04-13T09:38:26.646Z", + "completedAt": "2026-05-08T13:33:45.944Z", + "path": "/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json" + }, + "traj_1776113772922_bc92f121": { + "title": "autofix-swarm-Agentworkforce-relay-workflow", + "status": "completed", + "startedAt": "2026-04-13T20:56:12.922Z", + "completedAt": "2026-05-08T13:33:43.489Z", + "path": "/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json" + }, + "traj_1778873209642_c70e32ab": { + "title": "ricky-child-update-2-workflow", + "status": "completed", + "startedAt": "2026-05-15T19:26:49.642Z", + "completedAt": "2026-05-15T19:36:18.257Z", + "path": "/.trajectories/completed/2026-05/traj_1778873209642_c70e32ab.json" + }, + "traj_1778873211616_6db3b2cd": { + "title": "ricky-child-update-docs-sync-workflow", + "status": "completed", + "startedAt": "2026-05-15T19:26:51.616Z", + "completedAt": "2026-05-15T19:35:07.059Z", + "path": "/.trajectories/completed/2026-05/traj_1778873211616_6db3b2cd.json" + }, + "traj_1rrpe2r7fyem": { + "title": "Use streaming PTY input in CLI drive", + "status": "completed", + "startedAt": "2026-05-20T07:20:33.009Z", + "completedAt": "2026-05-20T07:26:17.567Z", + "path": "/.trajectories/completed/2026-05/traj_1rrpe2r7fyem.json" + }, + "traj_2gpglosdsq7s": { + "title": "Fix broker session read paths and agent listing errors", + "status": "completed", + "startedAt": "2026-05-19T12:37:18.367Z", + "completedAt": "2026-05-19T12:48:50.116Z", + "path": "/.trajectories/completed/2026-05/traj_2gpglosdsq7s.json" + }, + "traj_2tqxnib25omk": { + "title": "Add workflow reliability contract coverage", + "status": "completed", + "startedAt": "2026-05-08T15:27:50.875Z", + "completedAt": "2026-05-08T15:28:02.639Z", + "path": "/.trajectories/completed/2026-05/traj_2tqxnib25omk.json" + }, + "traj_2yicjxgajt0a": { + "title": "Review GPT-5.5 hardening", + "status": "completed", + "startedAt": "2026-05-15T10:54:19.300Z", + "completedAt": "2026-05-15T10:54:19.476Z", + "path": "/.trajectories/completed/2026-05/traj_2yicjxgajt0a.json" + }, + "traj_34b1u84b19gz": { + "title": "Address PR 827 review feedback", + "status": "completed", + "startedAt": "2026-05-08T18:29:34.717Z", + "completedAt": "2026-05-08T18:33:55.607Z", + "path": "/.trajectories/completed/2026-05/traj_34b1u84b19gz.json" + }, + "traj_3gjtcykvybt5": { + "title": "Fix PR CI failures", + "status": "completed", + "startedAt": "2026-05-15T11:24:06.054Z", + "completedAt": "2026-05-15T11:25:49.087Z", + "path": "/.trajectories/completed/2026-05/traj_3gjtcykvybt5.json" + }, + "traj_47akjihewlow": { + "title": "Further split broker runtime module for issue 875", + "status": "completed", + "startedAt": "2026-05-19T01:28:35.746Z", + "completedAt": "2026-05-19T01:38:29.105Z", + "path": "/.trajectories/completed/2026-05/traj_47akjihewlow.json" + }, + "traj_4b2d63f6ljvh": { + "title": "Fix CI run 26263517444", + "status": "completed", + "startedAt": "2026-05-22T01:47:08.312Z", + "completedAt": "2026-05-25T01:45:41.710Z", + "path": "/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json" + }, + "traj_4chzkm724ufo": { + "title": "Fix headless orchestrator worktree CLI E2E issues", + "status": "completed", + "startedAt": "2026-05-15T11:44:28.338Z", + "completedAt": "2026-05-15T11:51:05.319Z", + "path": "/.trajectories/completed/2026-05/traj_4chzkm724ufo.json" + }, + "traj_4t07itef99ug": { + "title": "Implement relay CLI bootstrap commands for proactive runtime M1", + "status": "completed", + "startedAt": "2026-05-11T21:47:37.805Z", + "completedAt": "2026-05-11T21:49:49.859Z", + "path": "/.trajectories/completed/2026-05/traj_4t07itef99ug.json" + }, + "traj_4vucir4qvqa2": { + "title": "Harden headless broker readiness semantics", + "status": "completed", + "startedAt": "2026-05-15T09:46:07.617Z", + "completedAt": "2026-05-15T09:59:00.460Z", + "path": "/.trajectories/completed/2026-05/traj_4vucir4qvqa2.json" + }, + "traj_5k0jtc1g5l33": { + "title": "Resolve PR 932 conflicts and review comments", + "status": "completed", + "startedAt": "2026-05-22T19:13:09.359Z", + "completedAt": "2026-05-22T19:13:16.971Z", + "path": "/.trajectories/completed/2026-05/traj_5k0jtc1g5l33.json" + }, + "traj_5nzj6v56id4z": { + "title": "Fix PR914 review comments", + "status": "completed", + "startedAt": "2026-05-19T13:20:42.407Z", + "completedAt": "2026-05-19T13:26:28.697Z", + "path": "/.trajectories/completed/2026-05/traj_5nzj6v56id4z.json" + }, + "traj_5q8i0iz4klpo": { + "title": "ricky-child-update-2-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:07:02.452Z", + "completedAt": "2026-05-15T21:36:03.688Z", + "path": "/.trajectories/completed/2026-05/traj_5q8i0iz4klpo.json" + }, + "traj_5qbla7w4kzoi": { + "title": "Fix issue 877", + "status": "completed", + "startedAt": "2026-05-19T03:40:40.798Z", + "completedAt": "2026-05-19T03:54:06.889Z", + "path": "/.trajectories/completed/2026-05/traj_5qbla7w4kzoi.json" + }, + "traj_60qc24ufr96g": { + "title": "Expand workflow reliability contract matrix", + "status": "completed", + "startedAt": "2026-05-08T15:40:11.699Z", + "completedAt": "2026-05-08T15:40:22.521Z", + "path": "/.trajectories/completed/2026-05/traj_60qc24ufr96g.json" + }, + "traj_6sjeohtm3php": { + "title": "Address broker headless reliability review findings", + "status": "completed", + "startedAt": "2026-05-15T09:30:56.316Z", + "completedAt": "2026-05-15T09:32:47.870Z", + "path": "/.trajectories/completed/2026-05/traj_6sjeohtm3php.json" + }, + "traj_6ujzpx82gqs9": { + "title": "ricky-slack-primitive-implementation-workflow-status-r-workflow", + "status": "completed", + "startedAt": "2026-05-08T16:06:54.844Z", + "completedAt": "2026-05-08T16:18:16.119Z", + "path": "/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.json" + }, + "traj_78ytpicts778": { + "title": "Address PR 932 result callback review findings", + "status": "completed", + "startedAt": "2026-05-22T15:59:25.187Z", + "completedAt": "2026-05-22T16:05:12.320Z", + "path": "/.trajectories/completed/2026-05/traj_78ytpicts778.json" + }, + "traj_7uznwzoxbao6": { + "title": "Fix standalone detached headless startup", + "status": "completed", + "startedAt": "2026-05-15T10:18:46.273Z", + "completedAt": "2026-05-15T10:25:00.598Z", + "path": "/.trajectories/completed/2026-05/traj_7uznwzoxbao6.json" + }, + "traj_7zu7et53ph3l": { + "title": "Harden Codex GPT-5.5 local CLI compatibility", + "status": "completed", + "startedAt": "2026-05-15T10:35:25.212Z", + "completedAt": "2026-05-15T10:40:53.355Z", + "path": "/.trajectories/completed/2026-05/traj_7zu7et53ph3l.json" + }, + "traj_81kobstnzzwk": { + "title": "Orchestrate team review cycle for #892 #893 #894 #895", + "status": "completed", + "startedAt": "2026-05-19T08:16:32.762Z", + "completedAt": "2026-05-19T08:37:32.966Z", + "path": "/.trajectories/completed/2026-05/traj_81kobstnzzwk.json" + }, + "traj_8ljgydz61do5": { + "title": "ricky-child-update-messaging-2-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:06:54.217Z", + "completedAt": "2026-05-15T21:41:56.478Z", + "path": "/.trajectories/completed/2026-05/traj_8ljgydz61do5.json" + }, + "traj_90jmd9z27oap": { + "title": "Resolve PR 948 merge conflicts", + "status": "completed", + "startedAt": "2026-05-22T19:49:08.797Z", + "completedAt": "2026-05-22T20:45:01.262Z", + "path": "/.trajectories/completed/2026-05/traj_90jmd9z27oap.json" + }, + "traj_947wzpddsg9j": { + "title": "Implement relay CLI bootstrap commands for proactive runtime M1", + "status": "completed", + "startedAt": "2026-05-11T23:11:57.326Z", + "completedAt": "2026-05-11T23:14:23.136Z", + "path": "/.trajectories/completed/2026-05/traj_947wzpddsg9j.json" + }, + "traj_9fdv7hxm0b60": { + "title": "Strict standalone smoke follow-up", + "status": "completed", + "startedAt": "2026-05-15T10:37:17.693Z", + "completedAt": "2026-05-15T10:43:11.587Z", + "path": "/.trajectories/completed/2026-05/traj_9fdv7hxm0b60.json" + }, + "traj_9gq96irkj00s": { + "title": "Update relay to use published relaycast Rust reclaim fix", + "status": "completed", + "startedAt": "2026-05-10T18:45:02.118Z", + "completedAt": "2026-05-10T18:48:11.532Z", + "path": "/.trajectories/completed/2026-05/traj_9gq96irkj00s.json" + }, + "traj_aw7stgf4qau0": { + "title": "Fix publish smoke cloud tarball dependency", + "status": "completed", + "startedAt": "2026-05-11T07:49:42.778Z", + "completedAt": "2026-05-11T07:50:37.848Z", + "path": "/.trajectories/completed/2026-05/traj_aw7stgf4qau0.json" + }, + "traj_bdrlknyl8twj": { + "title": "Add workflow reliability defaults and E2E matrix", + "status": "completed", + "startedAt": "2026-05-08T17:54:45.069Z", + "completedAt": "2026-05-08T18:05:37.305Z", + "path": "/.trajectories/completed/2026-05/traj_bdrlknyl8twj.json" + }, + "traj_bz1a1o15p7px": { + "title": "Fix PR 949 CI failure", + "status": "completed", + "startedAt": "2026-05-22T16:56:55.021Z", + "completedAt": "2026-05-22T16:57:55.372Z", + "path": "/.trajectories/completed/2026-05/traj_bz1a1o15p7px.json" + }, + "traj_cbmwd07phhm2": { + "title": "Implement #869 snapshot module + dump-pty command", + "status": "completed", + "startedAt": "2026-05-17T14:19:10.603Z", + "completedAt": "2026-05-17T14:33:32.293Z", + "path": "/.trajectories/completed/2026-05/traj_cbmwd07phhm2.json" + }, + "traj_ceo5q9bh2od3": { + "title": "Add structured spawned-agent results", + "status": "completed", + "startedAt": "2026-05-20T21:24:17.929Z", + "completedAt": "2026-05-20T21:43:51.936Z", + "path": "/.trajectories/completed/2026-05/traj_ceo5q9bh2od3.json" + }, + "traj_d89s38ddu7cj": { + "title": "Review Codex GPT-5.5 spawn fix", + "status": "completed", + "startedAt": "2026-05-15T12:25:39.946Z", + "completedAt": "2026-05-15T12:25:40.708Z", + "path": "/.trajectories/completed/2026-05/traj_d89s38ddu7cj.json" + }, + "traj_dbsnr453nxjw": { + "title": "Confirm and remove unused package dependencies", + "status": "completed", + "startedAt": "2026-05-22T16:01:11.967Z", + "completedAt": "2026-05-22T16:12:01.466Z", + "path": "/.trajectories/completed/2026-05/traj_dbsnr453nxjw.json" + }, + "traj_dcl9hgoiuac5": { + "title": "Verify --broker-name override for agent-relay up", + "status": "completed", + "startedAt": "2026-05-21T18:56:55.532Z", + "completedAt": "2026-05-21T19:00:06.831Z", + "path": "/.trajectories/completed/2026-05/traj_dcl9hgoiuac5.json" + }, + "traj_dpgn0am1jq1c": { + "title": "Implement M1 relay CLI bootstrap commands", + "status": "completed", + "startedAt": "2026-05-11T18:43:20.429Z", + "completedAt": "2026-05-11T18:43:20.733Z", + "path": "/.trajectories/completed/2026-05/traj_dpgn0am1jq1c.json" + }, + "traj_dqgg2q4scsvt": { + "title": "Assess PR 932 event listener need", + "status": "completed", + "startedAt": "2026-05-22T01:05:44.531Z", + "completedAt": "2026-05-22T01:07:31.036Z", + "path": "/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json" + }, + "traj_e1b7ww3un1u3": { + "title": "Harden agents logs raw and follow output", + "status": "completed", + "startedAt": "2026-05-19T10:59:00.118Z", + "completedAt": "2026-05-19T11:04:44.466Z", + "path": "/.trajectories/completed/2026-05/traj_e1b7ww3un1u3.json" + }, + "traj_elx0fcwgs37x": { + "title": "ricky-child-update-messaging-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:06:50.250Z", + "completedAt": "2026-05-15T21:46:46.167Z", + "path": "/.trajectories/completed/2026-05/traj_elx0fcwgs37x.json" + }, + "traj_erzd7j9nto9r": { + "title": "Strict review and PR prep for headless broker readiness", + "status": "completed", + "startedAt": "2026-05-15T10:02:10.164Z", + "completedAt": "2026-05-15T10:06:38.127Z", + "path": "/.trajectories/completed/2026-05/traj_erzd7j9nto9r.json" + }, + "traj_f1iac9ngymlj": { + "title": "Fix reliability review findings 892-895", + "status": "completed", + "startedAt": "2026-05-19T09:52:54.932Z", + "completedAt": "2026-05-19T10:01:19.068Z", + "path": "/.trajectories/completed/2026-05/traj_f1iac9ngymlj.json" + }, + "traj_f3arvbmmlomn": { + "title": "Address PR feedback for headless broker reliability", + "status": "completed", + "startedAt": "2026-05-15T12:09:02.122Z", + "completedAt": "2026-05-15T12:15:11.435Z", + "path": "/.trajectories/completed/2026-05/traj_f3arvbmmlomn.json" + }, + "traj_f9wxa8ujeg78": { + "title": "Refactor broker main for issue 875", + "status": "completed", + "startedAt": "2026-05-19T00:54:40.328Z", + "completedAt": "2026-05-19T00:55:57.506Z", + "path": "/.trajectories/completed/2026-05/traj_f9wxa8ujeg78.json" + }, + "traj_fh8oosbijpwc": { + "title": "Track A: relaycast subscribe + @self DM routing", + "status": "completed", + "startedAt": "2026-05-12T06:28:56.427Z", + "completedAt": "2026-05-12T11:21:33.352Z", + "path": "/.trajectories/completed/2026-05/traj_fh8oosbijpwc.json" + }, + "traj_ft1pwdlcrmcn": { + "title": "Fix CI run 26262957675", + "status": "completed", + "startedAt": "2026-05-22T01:31:45.965Z", + "completedAt": "2026-05-22T01:34:57.413Z", + "path": "/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json" + }, + "traj_gh05rj5gwsap": { + "title": "Bump relaycast Rust SDK in relay", + "status": "completed", + "startedAt": "2026-05-14T16:41:17.430Z", + "completedAt": "2026-05-14T16:42:32.485Z", + "path": "/.trajectories/completed/2026-05/traj_gh05rj5gwsap.json" + }, + "traj_gnqvtoxtc8dy": { + "title": "Fix broker half-start recovery", + "status": "completed", + "startedAt": "2026-05-19T12:34:36.057Z", + "completedAt": "2026-05-19T12:47:18.115Z", + "path": "/.trajectories/completed/2026-05/traj_gnqvtoxtc8dy.json" + }, + "traj_hfkww5z7trxn": { + "title": "Fresh comprehensive review of PR 856", + "status": "completed", + "startedAt": "2026-05-15T12:56:14.439Z", + "completedAt": "2026-05-15T13:00:15.366Z", + "path": "/.trajectories/completed/2026-05/traj_hfkww5z7trxn.json" + }, + "traj_hrsndfzk0qay": { + "title": "Tighten Codex 5.5 fallback coverage", + "status": "completed", + "startedAt": "2026-05-15T10:57:41.681Z", + "completedAt": "2026-05-15T10:57:41.827Z", + "path": "/.trajectories/completed/2026-05/traj_hrsndfzk0qay.json" + }, + "traj_hysw5o7idqas": { + "title": "Fix issue 924", + "status": "completed", + "startedAt": "2026-05-20T05:35:07.262Z", + "completedAt": "2026-05-20T05:47:54.189Z", + "path": "/.trajectories/completed/2026-05/traj_hysw5o7idqas.json" + }, + "traj_ij5b3kcatvwn": { + "title": "ricky-reading-worker-dm-replies-design-spec-status-draft-date-2026-05-15-issue-860-hea-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:06:45.545Z", + "completedAt": "2026-05-15T21:50:07.842Z", + "path": "/.trajectories/completed/2026-05/traj_ij5b3kcatvwn.json" + }, + "traj_iole5zdt9orr": { + "title": "Fix PR 831 CI conflicts", + "status": "completed", + "startedAt": "2026-05-10T15:18:12.326Z", + "completedAt": "2026-05-10T15:29:41.840Z", + "path": "/.trajectories/completed/2026-05/traj_iole5zdt9orr.json" + }, + "traj_irafiyk6wpw0": { + "title": "Fix agents:logs near-unparseable TTY redraw garbage (codex implement, claude review)", + "status": "completed", + "startedAt": "2026-05-19T10:30:38.222Z", + "completedAt": "2026-05-19T10:44:24.757Z", + "path": "/.trajectories/completed/2026-05/traj_irafiyk6wpw0.json" + }, + "traj_itgr2w8qs3xn": { + "title": "Make workflow deterministic failures repairable by agents", + "status": "completed", + "startedAt": "2026-05-08T14:44:45.732Z", + "completedAt": "2026-05-08T14:44:45.984Z", + "path": "/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json" + }, + "traj_j9k10fez3e81": { + "title": "review-loop-mpb2bvnf-1-workflow", + "status": "completed", + "startedAt": "2026-05-18T10:30:09.927Z", + "completedAt": "2026-05-18T14:53:04.092Z", + "path": "/.trajectories/completed/2026-05/traj_j9k10fez3e81.json" + }, + "traj_jbo2x14y7ovt": { + "title": "Fix issue 876", + "status": "completed", + "startedAt": "2026-05-19T02:48:10.768Z", + "completedAt": "2026-05-19T02:56:24.584Z", + "path": "/.trajectories/completed/2026-05/traj_jbo2x14y7ovt.json" + }, + "traj_jmf9pyt3zikn": { + "title": "Fix issue 874", + "status": "completed", + "startedAt": "2026-05-19T00:07:13.993Z", + "completedAt": "2026-05-19T00:17:27.680Z", + "path": "/.trajectories/completed/2026-05/traj_jmf9pyt3zikn.json" + }, + "traj_k7njijv51iq4": { + "title": "ricky-slack-primitive-implementation-workflow-status-r-workflow", + "status": "completed", + "startedAt": "2026-05-08T16:18:55.300Z", + "completedAt": "2026-05-08T16:26:03.266Z", + "path": "/.trajectories/completed/2026-05/traj_k7njijv51iq4.json" + }, + "traj_lhyrcib40kao": { + "title": "Address PR #914 CodeRabbit reliability review findings", + "status": "completed", + "startedAt": "2026-05-19T11:52:46.110Z", + "completedAt": "2026-05-19T12:07:22.401Z", + "path": "/.trajectories/completed/2026-05/traj_lhyrcib40kao.json" + }, + "traj_lieyyspidhfj": { + "title": "Fix PR 823 conflicts checks and comments", + "status": "completed", + "startedAt": "2026-05-09T08:37:17.563Z", + "completedAt": "2026-05-09T08:47:54.686Z", + "path": "/.trajectories/completed/2026-05/traj_lieyyspidhfj.json" + }, + "traj_m7mpv7j8n78h": { + "title": "Address Relay PR 826 review feedback", + "status": "completed", + "startedAt": "2026-05-08T15:17:53.113Z", + "completedAt": "2026-05-08T15:24:32.409Z", + "path": "/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json" + }, + "traj_mi9eqd4rjfea": { + "title": "Address stdio fresh review findings", + "status": "abandoned", + "startedAt": "2026-05-11T18:25:24.626Z", + "completedAt": "2026-05-11T18:37:05.318Z", + "path": "/.trajectories/completed/2026-05/traj_mi9eqd4rjfea.json" + }, + "traj_mytnzgfayj3d": { + "title": "Fix PR 948 telemetry package validation failure", + "status": "completed", + "startedAt": "2026-05-22T23:34:27.422Z", + "completedAt": "2026-05-22T23:36:15.152Z", + "path": "/.trajectories/completed/2026-05/traj_mytnzgfayj3d.json" + }, + "traj_mz5m5ysjj31e": { + "title": "Fix Relay SDK broker stdout drain", + "status": "completed", + "startedAt": "2026-05-10T20:24:43.831Z", + "completedAt": "2026-05-10T20:35:47.359Z", + "path": "/.trajectories/completed/2026-05/traj_mz5m5ysjj31e.json" + }, + "traj_n8duofq5vq1a": { + "title": "Update Codex registry for GPT-5.5", + "status": "completed", + "startedAt": "2026-05-15T10:27:57.532Z", + "completedAt": "2026-05-15T10:33:19.705Z", + "path": "/.trajectories/completed/2026-05/traj_n8duofq5vq1a.json" + }, + "traj_o251whkvy9rl": { + "title": "Fix Codex GPT-5.5 E2E rough edges", + "status": "completed", + "startedAt": "2026-05-15T11:59:26.764Z", + "completedAt": "2026-05-15T12:12:25.515Z", + "path": "/.trajectories/completed/2026-05/traj_o251whkvy9rl.json" + }, + "traj_o9cx33xn5u39": { + "title": "add-mcp-args-register-flag-workflow", + "status": "completed", + "startedAt": "2026-04-20T15:06:23.387Z", + "completedAt": "2026-05-08T13:33:35.341Z", + "path": "/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json" + }, + "traj_ootb5rt3tozd": { + "title": "ricky-child-update-docs-sync-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:07:04.494Z", + "completedAt": "2026-05-15T21:45:20.368Z", + "path": "/.trajectories/completed/2026-05/traj_ootb5rt3tozd.json" + }, + "traj_oyc528j7suvo": { + "title": "Expose cloud workflow scheduling through Relay SDK", + "status": "completed", + "startedAt": "2026-05-09T19:43:34.805Z", + "completedAt": "2026-05-09T19:44:00.107Z", + "path": "/.trajectories/completed/2026-05/traj_oyc528j7suvo.json" + }, + "traj_piik8r6zu3i7": { + "title": "Issue 867: RelayEventListener", + "status": "completed", + "startedAt": "2026-05-18T01:56:18.236Z", + "completedAt": "2026-05-18T02:01:49.991Z", + "path": "/.trajectories/completed/2026-05/traj_piik8r6zu3i7.json" + }, + "traj_pjadgfw0mtw4": { + "title": "Review package dependencies", + "status": "completed", + "startedAt": "2026-05-21T20:27:44.911Z", + "completedAt": "2026-05-21T20:31:49.784Z", + "path": "/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json" + }, + "traj_pmrcfj6or3pz": { + "title": "Address runtime split review comments", + "status": "completed", + "startedAt": "2026-05-19T02:03:43.962Z", + "completedAt": "2026-05-19T02:09:31.002Z", + "path": "/.trajectories/completed/2026-05/traj_pmrcfj6or3pz.json" + }, + "traj_q97ei72svf2f": { + "title": "Address PR 978 review comments", + "status": "completed", + "startedAt": "2026-05-25T02:33:51.921Z", + "completedAt": "2026-05-25T11:42:29.529Z", + "path": "/.trajectories/completed/2026-05/traj_q97ei72svf2f.json" + }, + "traj_qtmid2nzz0kz": { + "title": "Address PR 929 review comments", + "status": "completed", + "startedAt": "2026-05-20T06:21:54.721Z", + "completedAt": "2026-05-20T06:26:56.700Z", + "path": "/.trajectories/completed/2026-05/traj_qtmid2nzz0kz.json" + }, + "traj_r3eic6rt84pq": { + "title": "Fix PR 932 structured result listener surface", + "status": "completed", + "startedAt": "2026-05-22T01:09:21.137Z", + "completedAt": "2026-05-22T01:21:02.199Z", + "path": "/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json" + }, + "traj_ryf5sstno6p3": { + "title": "Telemetry key from env (P0.5 of #881)", + "status": "completed", + "startedAt": "2026-05-18T02:56:55.314Z", + "completedAt": "2026-05-18T03:02:35.202Z", + "path": "/.trajectories/completed/2026-05/traj_ryf5sstno6p3.json" + }, + "traj_s5ojo1f4srz4": { + "title": "Remove unused user-directory package", + "status": "completed", + "startedAt": "2026-05-22T16:27:44.927Z", + "completedAt": "2026-05-22T16:36:20.545Z", + "path": "/.trajectories/completed/2026-05/traj_s5ojo1f4srz4.json" + }, + "traj_sh2ahp9z2xg6": { + "title": "Fix uuid install deprecation warning", + "status": "completed", + "startedAt": "2026-05-19T17:21:40.756Z", + "completedAt": "2026-05-19T17:21:49.702Z", + "path": "/.trajectories/completed/2026-05/traj_sh2ahp9z2xg6.json" + }, + "traj_sqerp89tc436": { + "title": "Remove legacy root bin fallback", + "status": "completed", + "startedAt": "2026-05-19T00:45:33.159Z", + "completedAt": "2026-05-19T00:50:03.857Z", + "path": "/.trajectories/completed/2026-05/traj_sqerp89tc436.json" + }, + "traj_t5uknesn2fcw": { + "title": "ricky-child-update-skill-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:06:52.453Z", + "completedAt": "2026-05-15T21:40:04.209Z", + "path": "/.trajectories/completed/2026-05/traj_t5uknesn2fcw.json" + }, + "traj_tavtex0db4b0": { + "title": "Make workflow failures repairable by agents", + "status": "completed", + "startedAt": "2026-05-08T14:34:19.969Z", + "completedAt": "2026-05-08T14:44:45.719Z", + "path": "/.trajectories/completed/2026-05/traj_tavtex0db4b0.json" + }, + "traj_tgism98me5na": { + "title": "Implement relay CLI proactive runtime bootstrap commands", + "status": "completed", + "startedAt": "2026-05-11T20:04:48.053Z", + "completedAt": "2026-05-11T20:05:54.956Z", + "path": "/.trajectories/completed/2026-05/traj_tgism98me5na.json" + }, + "traj_u33qn99ijbh4": { + "title": "ricky-child-update-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:07:00.444Z", + "completedAt": "2026-05-15T21:30:50.445Z", + "path": "/.trajectories/completed/2026-05/traj_u33qn99ijbh4.json" + }, + "traj_u3loicehnwb4": { + "title": "Gate broker diagnostic logs behind env flag", + "status": "completed", + "startedAt": "2026-05-21T04:14:44.815Z", + "completedAt": "2026-05-21T04:14:45.063Z", + "path": "/.trajectories/completed/2026-05/traj_u3loicehnwb4.json" + }, + "traj_u4ixmbqqm2y1": { + "title": "Add cloud workflow schedule CLI", + "status": "completed", + "startedAt": "2026-05-09T19:26:42.106Z", + "completedAt": "2026-05-09T19:29:18.024Z", + "path": "/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.json" + }, + "traj_uf8y40ewrfh0": { + "title": "Address PR 831 review feedback and conflicts", + "status": "completed", + "startedAt": "2026-05-09T19:59:24.197Z", + "completedAt": "2026-05-09T19:59:24.403Z", + "path": "/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.json" + }, + "traj_v1wexlfur5zr": { + "title": "Fix broker headless reliability doc", + "status": "completed", + "startedAt": "2026-05-15T09:04:51.316Z", + "completedAt": "2026-05-15T09:13:50.970Z", + "path": "/.trajectories/completed/2026-05/traj_v1wexlfur5zr.json" + }, + "traj_v87cyrs8dke9": { + "title": "Refactor runDriveSession below complexity 15 (#897)", + "status": "completed", + "startedAt": "2026-05-14T14:28:34.155Z", + "completedAt": "2026-05-18T18:06:04.950Z", + "path": "/.trajectories/completed/2026-05/traj_v87cyrs8dke9.json" + }, + "traj_v9x3o92ag682": { + "title": "ricky-child-update-messaging-test-workflow", + "status": "completed", + "startedAt": "2026-05-15T21:06:56.418Z", + "completedAt": "2026-05-15T21:34:35.623Z", + "path": "/.trajectories/completed/2026-05/traj_v9x3o92ag682.json" + }, + "traj_vfa1jr6otnjn": { + "title": "Refresh PR 948 after main advanced", + "status": "completed", + "startedAt": "2026-05-22T23:23:16.807Z", + "completedAt": "2026-05-22T23:23:26.380Z", + "path": "/.trajectories/completed/2026-05/traj_vfa1jr6otnjn.json" + }, + "traj_vkozdglobkyg": { + "title": "Address Relay PR 826 review comments", + "status": "completed", + "startedAt": "2026-05-08T15:50:35.978Z", + "completedAt": "2026-05-08T15:51:38.854Z", + "path": "/.trajectories/completed/2026-05/traj_vkozdglobkyg.json" + }, + "traj_wbn62q4cq16h": { + "title": "Update generated workflow to Codex agents only", + "status": "completed", + "startedAt": "2026-05-15T21:03:02.671Z", + "completedAt": "2026-05-15T21:06:00.384Z", + "path": "/.trajectories/completed/2026-05/traj_wbn62q4cq16h.json" + }, + "traj_whd40oxptlhn": { + "title": "Review spawn persistence fix and open PR", + "status": "completed", + "startedAt": "2026-05-13T10:57:02.796Z", + "completedAt": "2026-05-13T11:00:43.100Z", + "path": "/.trajectories/completed/2026-05/traj_whd40oxptlhn.json" + }, + "traj_wx00tjvpptvg": { + "title": "Investigate agent-relay spawn persistence", + "status": "completed", + "startedAt": "2026-05-13T10:49:12.464Z", + "completedAt": "2026-05-13T10:53:03.748Z", + "path": "/.trajectories/completed/2026-05/traj_wx00tjvpptvg.json" + }, + "traj_wzzboitm85ee": { + "title": "Resolve PR conflicts around platform tradeoff copy", + "status": "completed", + "startedAt": "2026-05-15T12:47:36.508Z", + "completedAt": "2026-05-15T12:50:14.358Z", + "path": "/.trajectories/completed/2026-05/traj_wzzboitm85ee.json" + }, + "traj_x37bhga2j5ph": { + "title": "Deepen broker runtime refactor for PR 906", + "status": "completed", + "startedAt": "2026-05-19T01:42:10.602Z", + "completedAt": "2026-05-19T01:50:40.359Z", + "path": "/.trajectories/completed/2026-05/traj_x37bhga2j5ph.json" + }, + "traj_ybcrij9wg8m1": { + "title": "Implement agent-relay view read-only stream client (#864 sub-1)", + "status": "completed", + "startedAt": "2026-05-18T02:02:07.524Z", + "completedAt": "2026-05-18T02:05:41.120Z", + "path": "/.trajectories/completed/2026-05/traj_ybcrij9wg8m1.json" + }, + "traj_z171lng2fbbi": { + "title": "Address PR 840 review feedback", + "status": "completed", + "startedAt": "2026-05-11T08:06:37.977Z", + "completedAt": "2026-05-11T08:07:48.097Z", + "path": "/.trajectories/completed/2026-05/traj_z171lng2fbbi.json" + }, + "traj_zfa6skfr32vy": { + "title": "Implement relay CLI M1 bootstrap commands", + "status": "completed", + "startedAt": "2026-05-11T19:31:44.734Z", + "completedAt": "2026-05-11T19:34:54.971Z", + "path": "/.trajectories/completed/2026-05/traj_zfa6skfr32vy.json" + }, + "traj_zqwco4gl76g3": { + "title": "Fix issue 878", + "status": "completed", + "startedAt": "2026-05-19T04:18:25.024Z", + "completedAt": "2026-05-19T04:27:18.903Z", + "path": "/.trajectories/completed/2026-05/traj_zqwco4gl76g3.json" + }, + "traj_zu3252hxzoqh": { + "title": "Open PR for reading worker DM replies", + "status": "completed", + "startedAt": "2026-05-16T06:08:22.396Z", + "completedAt": "2026-05-16T06:09:24.599Z", + "path": "/.trajectories/completed/2026-05/traj_zu3252hxzoqh.json" + }, + "traj_1775914296101_a4397efe": { + "title": "fix-sdk-build-resolution-workflow", + "status": "completed", + "startedAt": "2026-04-11T13:31:36.101Z", + "completedAt": "2026-04-11T13:39:53.105Z", + "path": "/.trajectories/completed/traj_1775914296101_a4397efe.json" + }, + "traj_1776024661304_cfc829b9": { + "title": "fix-workflow-resume-elegant-workflow", + "status": "abandoned", + "startedAt": "2026-04-12T20:11:01.304Z", + "completedAt": "2026-04-12T20:11:16.381Z", + "path": "/.trajectories/completed/traj_1776024661304_cfc829b9.json" + }, + "traj_1778873052429_03a4dacb": { + "title": "ricky-reading-worker-dm-replies-design-spec-status-draft-date-2026-05-15-issue-860-hea-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:24:12.429Z", + "completedAt": "2026-05-15T20:01:57.036Z", + "path": "/.trajectories/completed/traj_1778873052429_03a4dacb.json" + }, + "traj_1778873197540_01102ade": { + "title": "ricky-child-update-messaging-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:26:37.540Z", + "completedAt": "2026-05-15T19:43:47.629Z", + "path": "/.trajectories/completed/traj_1778873197540_01102ade.json" + }, + "traj_1778873199489_f2ce4060": { + "title": "ricky-child-update-skill-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:26:39.489Z", + "completedAt": "2026-05-15T19:43:43.149Z", + "path": "/.trajectories/completed/traj_1778873199489_f2ce4060.json" + }, + "traj_1778873201502_0dacf7c5": { + "title": "ricky-child-update-messaging-2-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:26:41.502Z", + "completedAt": "2026-05-15T19:43:35.011Z", + "path": "/.trajectories/completed/traj_1778873201502_0dacf7c5.json" + }, + "traj_1778873203502_4c225b7e": { + "title": "ricky-child-update-messaging-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:26:43.502Z", + "completedAt": "2026-05-15T19:44:33.074Z", + "path": "/.trajectories/completed/traj_1778873203502_4c225b7e.json" + }, + "traj_1778873205470_a4e5f0cb": { + "title": "ricky-child-update-issue-860-transcript-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:26:45.470Z", + "completedAt": "2026-05-15T19:43:37.134Z", + "path": "/.trajectories/completed/traj_1778873205470_a4e5f0cb.json" + }, + "traj_1778873207471_b7def991": { + "title": "ricky-child-update-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:26:47.471Z", + "completedAt": "2026-05-15T19:43:24.329Z", + "path": "/.trajectories/completed/traj_1778873207471_b7def991.json" + }, + "traj_1778874205797_81e92307": { + "title": "ricky-child-update-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:43:25.797Z", + "completedAt": "2026-05-15T19:43:43.995Z", + "path": "/.trajectories/completed/traj_1778874205797_81e92307.json" + }, + "traj_1778874216773_c6b12ab2": { + "title": "ricky-child-update-messaging-2-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:43:36.773Z", + "completedAt": "2026-05-15T19:44:08.270Z", + "path": "/.trajectories/completed/traj_1778874216773_c6b12ab2.json" + }, + "traj_1778874218579_a0225559": { + "title": "ricky-child-update-issue-860-transcript-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:43:38.579Z", + "completedAt": "2026-05-15T19:43:58.721Z", + "path": "/.trajectories/completed/traj_1778874218579_a0225559.json" + }, + "traj_1778874224855_9c722c4b": { + "title": "ricky-child-update-skill-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:43:44.855Z", + "completedAt": "2026-05-15T19:44:16.528Z", + "path": "/.trajectories/completed/traj_1778874224855_9c722c4b.json" + }, + "traj_1778874226983_3367d527": { + "title": "ricky-child-update-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:43:46.983Z", + "completedAt": "2026-05-15T19:44:05.612Z", + "path": "/.trajectories/completed/traj_1778874226983_3367d527.json" + }, + "traj_1778874229373_9cce9465": { + "title": "ricky-child-update-messaging-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:43:49.374Z", + "completedAt": "2026-05-15T19:44:20.096Z", + "path": "/.trajectories/completed/traj_1778874229373_9cce9465.json" + }, + "traj_1778874240339_51b823cd": { + "title": "ricky-child-update-issue-860-transcript-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:00.339Z", + "completedAt": "2026-05-15T19:44:18.916Z", + "path": "/.trajectories/completed/traj_1778874240339_51b823cd.json" + }, + "traj_1778874241076_caa675a9": { + "title": "ricky-child-update-2-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:01.076Z", + "completedAt": "2026-05-15T19:44:32.521Z", + "path": "/.trajectories/completed/traj_1778874241076_caa675a9.json" + }, + "traj_1778874248966_e29c4c54": { + "title": "ricky-child-update-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:08.966Z", + "completedAt": "2026-05-15T19:44:27.330Z", + "path": "/.trajectories/completed/traj_1778874248966_e29c4c54.json" + }, + "traj_1778874249983_12a98df3": { + "title": "ricky-child-update-messaging-2-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:09.983Z", + "completedAt": "2026-05-15T19:44:41.100Z", + "path": "/.trajectories/completed/traj_1778874249983_12a98df3.json" + }, + "traj_1778874258229_0bdc53d8": { + "title": "ricky-child-update-skill-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:18.229Z", + "completedAt": "2026-05-15T19:44:49.274Z", + "path": "/.trajectories/completed/traj_1778874258229_0bdc53d8.json" + }, + "traj_1778874261453_55f49624": { + "title": "ricky-child-update-messaging-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:21.453Z", + "completedAt": "2026-05-15T19:44:53.643Z", + "path": "/.trajectories/completed/traj_1778874261453_55f49624.json" + }, + "traj_1778874261608_48fb9bf5": { + "title": "ricky-child-update-issue-860-transcript-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:21.608Z", + "completedAt": "2026-05-15T19:44:40.761Z", + "path": "/.trajectories/completed/traj_1778874261608_48fb9bf5.json" + }, + "traj_1778874269139_d7d7485a": { + "title": "ricky-child-update-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:29.139Z", + "completedAt": "2026-05-15T19:44:48.083Z", + "path": "/.trajectories/completed/traj_1778874269139_d7d7485a.json" + }, + "traj_1778874274412_70843e0e": { + "title": "ricky-child-update-2-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:34.412Z", + "completedAt": "2026-05-15T19:45:06.798Z", + "path": "/.trajectories/completed/traj_1778874274412_70843e0e.json" + }, + "traj_1778874274581_71efa470": { + "title": "ricky-child-update-messaging-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:34.581Z", + "completedAt": "2026-05-15T19:44:54.182Z", + "path": "/.trajectories/completed/traj_1778874274581_71efa470.json" + }, + "traj_1778874282200_39ad11db": { + "title": "ricky-child-update-issue-860-transcript-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:42.200Z", + "completedAt": "2026-05-15T19:45:02.477Z", + "path": "/.trajectories/completed/traj_1778874282200_39ad11db.json" + }, + "traj_1778874283570_ce3585b8": { + "title": "ricky-child-update-messaging-2-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:43.570Z", + "completedAt": "2026-05-15T19:45:14.888Z", + "path": "/.trajectories/completed/traj_1778874283570_ce3585b8.json" + }, + "traj_1778874289674_e3f868c8": { + "title": "ricky-child-update-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:49.674Z", + "completedAt": "2026-05-15T19:45:08.337Z", + "path": "/.trajectories/completed/traj_1778874289674_e3f868c8.json" + }, + "traj_1778874291950_0b1b5c1f": { + "title": "ricky-child-update-skill-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:51.950Z", + "completedAt": "2026-05-15T19:45:23.421Z", + "path": "/.trajectories/completed/traj_1778874291950_0b1b5c1f.json" + }, + "traj_1778874295927_4083d181": { + "title": "ricky-child-update-messaging-test-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:55.927Z", + "completedAt": "2026-05-15T19:45:15.333Z", + "path": "/.trajectories/completed/traj_1778874295927_4083d181.json" + }, + "traj_1778874296362_bdf727ff": { + "title": "ricky-child-update-messaging-workflow", + "status": "abandoned", + "startedAt": "2026-05-15T19:44:56.362Z", + "completedAt": "2026-05-15T19:45:27.624Z", + "path": "/.trajectories/completed/traj_1778874296362_bdf727ff.json" + }, + "traj_60dr7ojudhfu": { + "title": "Address final harness adapter PR comments", + "status": "completed", + "startedAt": "2026-05-25T12:12:15.123Z", + "completedAt": "2026-05-25T12:18:37.956Z", + "path": "/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json" + }, + "traj_g35xvgo8fbqq": { + "title": "Fix package validation cache key", + "status": "completed", + "startedAt": "2026-05-25T12:24:38.508Z", + "completedAt": "2026-05-25T12:24:47.132Z", + "path": "/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json" + }, + "traj_5kytmhye9atg": { + "title": "Fix harness docs language examples", + "status": "completed", + "startedAt": "2026-05-25T13:19:34.556Z", + "completedAt": "2026-05-25T13:19:41.038Z", + "path": "/.trajectories/completed/2026-05/traj_5kytmhye9atg.json" + }, + "traj_qpefcnn38jnx": { + "title": "Document harness lifecycle", + "status": "completed", + "startedAt": "2026-05-25T13:32:44.211Z", + "completedAt": "2026-05-25T13:36:05.155Z", + "path": "/.trajectories/completed/2026-05/traj_qpefcnn38jnx.json" + }, + "traj_ps68dydvgfuz": { + "title": "Add SDK harness adapter abstraction", + "status": "completed", + "startedAt": "2026-05-25T14:04:38.337Z", + "completedAt": "2026-05-25T14:19:16.978Z", + "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ps68dydvgfuz.json" + }, + "traj_yh2ml7b0fze1": { + "title": "Update harnesses docs page for harness adapters", + "status": "completed", + "startedAt": "2026-05-25T14:35:45.053Z", + "completedAt": "2026-05-25T14:35:51.965Z", + "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_yh2ml7b0fze1.json" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d93eda36b..5ddc2bb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Broker and TypeScript SDK structured result contracts add the `submit_result` MCP tool, `agent.waitForResult()`, per-spawn `result.onResult`, and `relay.addListener('agentResult', ...)` for typed JSON worker outcomes. +- `@agent-relay/sdk`: spawn calls and workflow configs can declare `CLIHarnessAdapter` configs so custom agent CLIs define their own binaries, interactive/non-interactive argument templates, model flags, and process behavior without Relay changes; built-in coding harnesses now use the same serializable config shape by default. +- `@agent-relay/sdk`: `HarnessRuntimeAdapter` types define the lifecycle surface for non-CLI harness control boundaries such as stdio or HTTP adapters. ### Changed +- `agent-relay mcp`: Agent Relay now ships its own Relaycast-backed MCP stdio server and generated MCP configs use `npx -y agent-relay mcp` instead of `@relaycast/mcp`. +- `agent-relay up`: broker startup no longer writes Relaycast MCP entries to project `.mcp.json`; spawned agents receive the MCP server through launch-time configuration. +- `agent-relay spawn` and SDK spawn calls now return harness `sessionId` metadata for resumable Claude and Codex PTY sessions. +- `@agent-relay/sdk`: facade agent handles and spawn lifecycle hooks now expose the broker-returned spawn `pid`. - Release workflow changelog generation now writes concise Keep a Changelog sections and skips web-only, release-only, trajectory, PR-review, placeholder, and withdrawn-tag entries. ### Fixed diff --git a/crates/broker/src/cli/mod.rs b/crates/broker/src/cli/mod.rs index 5f90bf40a..0f18c9f98 100644 --- a/crates/broker/src/cli/mod.rs +++ b/crates/broker/src/cli/mod.rs @@ -224,11 +224,13 @@ pub(crate) struct InitCommand { #[arg(long, default_value = "127.0.0.1")] pub(crate) api_bind: String, - /// Enable persistence: write state, pending-deliveries, lock, PID, and MCP - /// config to `.agent-relay/` in the working directory. When omitted (the - /// default), runtime files are written to a deterministic temp directory and - /// cleaned up opportunistically; identity registration is non-strict to avoid - /// stale-name collisions across short-lived sessions. + /// Enable persistence: write state, pending-deliveries, lock, and PID files + /// to `.agent-relay/` in the working directory. MCP configuration is injected + /// into spawned agents at launch time instead of being written to project + /// config files. When omitted (the default), runtime files are written to a + /// deterministic temp directory and cleaned up opportunistically; identity + /// registration is non-strict to avoid stale-name collisions across + /// short-lived sessions. #[arg(long, default_value_t = false)] pub(crate) persist: bool, diff --git a/crates/broker/src/cli_mcp_args.rs b/crates/broker/src/cli_mcp_args.rs index a58a66004..52fdcb595 100644 --- a/crates/broker/src/cli_mcp_args.rs +++ b/crates/broker/src/cli_mcp_args.rs @@ -369,9 +369,9 @@ mod tests { assert!(output .args .contains(&"mcp_servers.relaycast.command=\"npx\"".to_string())); - assert!(output - .args - .contains(&"mcp_servers.relaycast.args=[\"-y\", \"@relaycast/mcp\"]".to_string())); + assert!(output.args.contains( + &"mcp_servers.relaycast.args=[\"-y\", \"agent-relay\", \"mcp\"]".to_string() + )); assert!(output.side_effect_files.is_empty()); } diff --git a/crates/broker/src/codex_session.rs b/crates/broker/src/codex_session.rs new file mode 100644 index 000000000..3dda6b039 --- /dev/null +++ b/crates/broker/src/codex_session.rs @@ -0,0 +1,242 @@ +use std::{path::Path, process::Stdio, time::Duration}; + +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines}, + process::{ChildStdin, ChildStdout, Command}, + time::timeout, +}; + +const CODEX_BOOTSTRAP_TIMEOUT: Duration = Duration::from_secs(15); + +pub(crate) async fn create_resumable_codex_thread( + codex_bin: &str, + cwd: &Path, + env: &[(String, String)], +) -> Result { + timeout( + CODEX_BOOTSTRAP_TIMEOUT, + create_resumable_codex_thread_inner(codex_bin, cwd, env), + ) + .await + .with_context(|| { + format!("timed out creating Codex session via `{codex_bin} app-server --listen stdio://`") + })? +} + +async fn create_resumable_codex_thread_inner( + codex_bin: &str, + cwd: &Path, + env: &[(String, String)], +) -> Result { + let thread_cwd = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf()); + let mut command = Command::new(codex_bin); + command + .arg("app-server") + .arg("--listen") + .arg("stdio://") + .current_dir(&thread_cwd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + for (key, value) in env { + command.env(key, value); + } + let mut child = command + .spawn() + .with_context(|| format!("failed to start `{codex_bin} app-server --listen stdio://`"))?; + + let mut stdin = child + .stdin + .take() + .context("Codex app-server missing stdin pipe")?; + let stdout = child + .stdout + .take() + .context("Codex app-server missing stdout pipe")?; + if let Some(stderr) = child.stderr.take() { + tokio::spawn(async move { + let mut lines = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = lines.next_line().await { + tracing::debug!(target: "broker::codex_session", stderr = %line, "codex app-server stderr"); + } + }); + } + + let mut lines = BufReader::new(stdout).lines(); + let result = async { + json_rpc_request( + &mut stdin, + &mut lines, + 1, + "initialize", + json!({ + "clientInfo": { + "name": "agent-relay", + "version": crate::util::version::broker_version(), + }, + "capabilities": { + "experimentalApi": true, + "suppressNotifications": [], + }, + }), + ) + .await?; + + let start = json_rpc_request( + &mut stdin, + &mut lines, + 2, + "thread/start", + json!({ + "cwd": thread_cwd.to_string_lossy(), + "ephemeral": false, + }), + ) + .await?; + let thread_id = start + .pointer("/thread/id") + .and_then(Value::as_str) + .context("Codex app-server thread/start response missing thread.id")? + .to_string(); + + json_rpc_request( + &mut stdin, + &mut lines, + 3, + "thread/inject_items", + json!({ + "threadId": thread_id, + "items": [ + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "", + }, + ], + }, + ], + }), + ) + .await?; + + Ok(thread_id) + } + .await; + + let _ = child.kill().await; + let _ = child.wait().await; + + result +} + +async fn json_rpc_request( + stdin: &mut ChildStdin, + lines: &mut Lines>, + id: u64, + method: &str, + params: Value, +) -> Result { + let request = json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + }); + let encoded = serde_json::to_vec(&request)?; + stdin + .write_all(&encoded) + .await + .with_context(|| format!("failed writing Codex app-server request `{method}`"))?; + stdin + .write_all(b"\n") + .await + .with_context(|| format!("failed writing Codex app-server request newline `{method}`"))?; + stdin + .flush() + .await + .with_context(|| format!("failed flushing Codex app-server request `{method}`"))?; + + loop { + let Some(line) = lines + .next_line() + .await + .with_context(|| format!("failed reading Codex app-server response `{method}`"))? + else { + bail!("Codex app-server exited before responding to `{method}`"); + }; + let value = match serde_json::from_str::(&line) { + Ok(value) => value, + Err(error) => { + tracing::debug!( + target: "broker::codex_session", + method = %method, + error = %error, + line = %line, + "skipping non-JSON Codex app-server stdout line" + ); + continue; + } + }; + if value.get("id").and_then(Value::as_u64) != Some(id) { + continue; + } + if let Some(error) = value.get("error") { + bail!("Codex app-server `{method}` failed: {error}"); + } + return value + .get("result") + .cloned() + .with_context(|| format!("Codex app-server `{method}` response missing result")); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(unix)] + #[tokio::test] + async fn create_resumable_codex_thread_uses_app_server_rpc() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().expect("temp dir"); + let fake_codex = dir.path().join("codex"); + std::fs::write( + &fake_codex, + r#"#!/bin/sh +if [ "$1" != "app-server" ]; then + exit 2 +fi +read line +printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{}}' +read line +printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"thread":{"id":"thread-test"}}}' +read line +printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{}}' +while read line; do :; done +"#, + ) + .expect("write fake codex"); + let mut permissions = std::fs::metadata(&fake_codex) + .expect("fake codex metadata") + .permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(&fake_codex, permissions).expect("chmod fake codex"); + + let thread_id = create_resumable_codex_thread( + fake_codex.to_str().expect("utf-8 fake codex path"), + dir.path(), + &[], + ) + .await + .expect("thread id"); + + assert_eq!(thread_id, "thread-test"); + } +} diff --git a/crates/broker/src/lib.rs b/crates/broker/src/lib.rs index d28733d15..3ec31cf8f 100644 --- a/crates/broker/src/lib.rs +++ b/crates/broker/src/lib.rs @@ -4,6 +4,7 @@ pub mod snippets; pub(crate) mod broker; pub(crate) mod cli; pub(crate) mod cli_mcp_args; +pub(crate) mod codex_session; #[allow(dead_code)] pub(crate) mod config; pub(crate) mod control; diff --git a/crates/broker/src/listen_api.rs b/crates/broker/src/listen_api.rs index 69a65963b..fcdd3cb48 100644 --- a/crates/broker/src/listen_api.rs +++ b/crates/broker/src/listen_api.rs @@ -11,7 +11,7 @@ use std::{ }; use crate::{ - protocol::MessageInjectionMode, + protocol::{HarnessDefinition, MessageInjectionMode}, relaycast::WorkspaceMembershipSummary, replay_buffer::ReplayBuffer, types::{InboundDeliveryMode, PendingRelayMessage}, @@ -39,6 +39,7 @@ pub enum ListenApiRequest { cli: String, transport: Option, model: Option, + harness: Option, args: Vec, task: Option, channels: Vec, @@ -648,6 +649,21 @@ async fn listen_api_spawn( .or_else(|| body.get("restartPolicy")) .cloned(), ); + let harness = match body.get("harness").cloned() { + None | Some(Value::Null) => None, + Some(value) => match serde_json::from_value::(value) { + Ok(value) => Some(value), + Err(err) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + axum::Json(json!({ + "success": false, + "error": format!("Invalid field: harness ({err})") + })), + ); + } + }, + }; let agent_token = body .get("agent_token") .or_else(|| body.get("agentToken")) @@ -674,6 +690,7 @@ async fn listen_api_spawn( cli, transport, model, + harness, args, task, channels, @@ -2410,6 +2427,7 @@ mod auth_tests { cli, transport, model, + harness, args, task, channels, @@ -2429,6 +2447,12 @@ mod auth_tests { assert_eq!(cli, "codex"); assert_eq!(transport.as_deref(), Some("headless")); assert_eq!(model.as_deref(), Some("o3")); + assert_eq!( + harness + .as_ref() + .and_then(|definition| definition.binary.as_deref()), + Some("qwen") + ); assert_eq!(args, vec!["--fast".to_string()]); assert_eq!(task.as_deref(), Some("Ship it")); assert_eq!( @@ -2466,6 +2490,11 @@ mod auth_tests { "cli": "codex", "transport": "headless", "model": "o3", + "harness": { + "binary": "qwen", + "interactiveArgs": ["run", "{modelArgs}", "{args}"], + "modelArgs": ["-m", "{model}"], + }, "args": ["--fast"], "task": "Ship it", "channels": ["general", "engineering"], diff --git a/crates/broker/src/protocol.rs b/crates/broker/src/protocol.rs index 5e18f649d..1e8294dc0 100644 --- a/crates/broker/src/protocol.rs +++ b/crates/broker/src/protocol.rs @@ -19,6 +19,55 @@ pub enum HeadlessProvider { Opencode, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct HarnessDefinition { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub adapter: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub binary: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub binaries: Vec, + #[serde( + default, + alias = "interactive_args", + skip_serializing_if = "Vec::is_empty" + )] + pub interactive_args: Vec, + #[serde( + default, + alias = "non_interactive_args", + skip_serializing_if = "Vec::is_empty" + )] + pub non_interactive_args: Vec, + #[serde(default, alias = "model_args", skip_serializing_if = "Vec::is_empty")] + pub model_args: Vec, + #[serde( + default, + alias = "bypass_flag", + skip_serializing_if = "Option::is_none" + )] + pub bypass_flag: Option, + #[serde( + default, + alias = "bypass_aliases", + skip_serializing_if = "Vec::is_empty" + )] + pub bypass_aliases: Vec, + #[serde(default, alias = "search_paths", skip_serializing_if = "Vec::is_empty")] + pub search_paths: Vec, + #[serde(default, alias = "ignore_exit_code")] + pub ignore_exit_code: bool, + #[serde( + default, + alias = "proxy_provider", + skip_serializing_if = "Option::is_none" + )] + pub proxy_provider: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub aliases: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AgentSpec { pub name: String, @@ -31,6 +80,10 @@ pub struct AgentSpec { pub model: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub session_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub harness: Option, #[serde(skip_serializing_if = "Option::is_none")] pub team: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -84,6 +137,7 @@ pub struct ProtocolEnvelope { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "payload", rename_all = "snake_case")] +#[allow(clippy::large_enum_variant)] pub enum SdkToBroker { Hello { client_name: String, @@ -157,6 +211,8 @@ pub enum BrokerEvent { parent: Option, cli: Option, model: Option, + #[serde(default, rename = "sessionId")] + session_id: Option, pid: Option, source: Option, }, @@ -310,6 +366,7 @@ pub enum BrokerEvent { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", content = "payload", rename_all = "snake_case")] +#[allow(clippy::large_enum_variant)] pub enum BrokerToWorker { InitWorker { agent: AgentSpec, @@ -454,6 +511,7 @@ mod tests { parent: Some("Lead".into()), cli: None, model: None, + session_id: None, pid: None, source: None, }); diff --git a/crates/broker/src/relaycast/mod.rs b/crates/broker/src/relaycast/mod.rs index b198f0ae3..790ed4258 100644 --- a/crates/broker/src/relaycast/mod.rs +++ b/crates/broker/src/relaycast/mod.rs @@ -6,7 +6,6 @@ pub(crate) mod ws; pub(crate) use crate::snippets::{ configure_relaycast_mcp_with_result, configure_relaycast_mcp_with_token, - ensure_relaycast_mcp_config, }; pub(crate) use auth::AuthClient; pub(crate) use bridge::{map_ws_broker_command, map_ws_event}; diff --git a/crates/broker/src/runtime/api.rs b/crates/broker/src/runtime/api.rs index 1f812d341..652085a54 100644 --- a/crates/broker/src/runtime/api.rs +++ b/crates/broker/src/runtime/api.rs @@ -34,6 +34,7 @@ impl BrokerRuntime { cli, transport, model, + harness, args, task, channels, @@ -59,6 +60,7 @@ impl BrokerRuntime { cli.clone(), transport, model.clone(), + harness, args, effective_channels.clone(), cwd, @@ -267,6 +269,7 @@ impl BrokerRuntime { "provider": effective_spec.provider.clone(), "cli": effective_spec.cli.clone(), "model": effective_spec.model.clone(), + "sessionId": effective_spec.session_id.clone(), "pid":pid, "source":"http_api", "pre_registered": worker_relay_key.is_some(), @@ -286,6 +289,7 @@ impl BrokerRuntime { "name": name, "runtime": runtime_label(&effective_spec.runtime), "model": effective_spec.model.clone(), + "sessionId": effective_spec.session_id.clone(), "pid": pid, "pre_registered": worker_relay_key.is_some(), "warning": preregistration_warning, diff --git a/crates/broker/src/runtime/init.rs b/crates/broker/src/runtime/init.rs index d96363e24..240925994 100644 --- a/crates/broker/src/runtime/init.rs +++ b/crates/broker/src/runtime/init.rs @@ -187,7 +187,6 @@ pub(crate) async fn run_init(cmd: InitCommand, telemetry: TelemetryClient) -> Re strict_name: cmd.persist, agent_type: Some(agent_type_ref), read_mcp_identity: true, - ensure_mcp_config: cmd.persist, runtime_cwd: &runtime_cwd, }) .await?; diff --git a/crates/broker/src/runtime/mod.rs b/crates/broker/src/runtime/mod.rs index 343e40c3c..b43fd86c5 100644 --- a/crates/broker/src/runtime/mod.rs +++ b/crates/broker/src/runtime/mod.rs @@ -31,11 +31,10 @@ use crate::{ MessageInjectionMode, ProtocolEnvelope, RelayDelivery, PROTOCOL_VERSION, }, relaycast::{ - agent_name_eq, ensure_relaycast_mcp_config, format_worker_preregistration_error, - is_self_name, map_ws_event, registration_retry_after_secs, resolve_dm_participants_cached, - retry_agent_registration, AuthClient, DmParticipantsCache, MultiWorkspaceSession, - RegRetryOutcome, RelaycastHttpClient, WorkspaceInboundMessage, WorkspaceMembershipSummary, - WsControl, + agent_name_eq, format_worker_preregistration_error, is_self_name, map_ws_event, + registration_retry_after_secs, resolve_dm_participants_cached, retry_agent_registration, + AuthClient, DmParticipantsCache, MultiWorkspaceSession, RegRetryOutcome, + RelaycastHttpClient, WorkspaceInboundMessage, WorkspaceMembershipSummary, WsControl, }, replay_buffer::{ReplayBuffer, DEFAULT_REPLAY_CAPACITY}, telemetry::{ActionSource, TelemetryClient, TelemetryEvent}, diff --git a/crates/broker/src/runtime/relaycast_events.rs b/crates/broker/src/runtime/relaycast_events.rs index 9775fe0f1..04358bdf4 100644 --- a/crates/broker/src/runtime/relaycast_events.rs +++ b/crates/broker/src/runtime/relaycast_events.rs @@ -223,6 +223,8 @@ impl BrokerRuntime { cli: Some(cli.clone()), model: None, cwd: None, + session_id: None, + harness: None, team: None, shadow_of: None, shadow_mode: None, @@ -233,7 +235,7 @@ impl BrokerRuntime { let effective_task = normalize_initial_task(task.clone()); // Pre-register an agent token for every spawned worker. - // `@relaycast/mcp` needs RELAY_AGENT_TOKEN + + // The Agent Relay MCP server needs RELAY_AGENT_TOKEN + // RELAY_SKIP_BOOTSTRAP=1 in its environment to expose // tools immediately; otherwise it runs network // registration before responding to the MCP initialize @@ -335,6 +337,7 @@ impl BrokerRuntime { "runtime": "pty", "cli": cli, "model": effective_spec.model.clone(), + "sessionId": effective_spec.session_id.clone(), "pid": pid, "source": "relaycast_ws", "pre_registered": worker_relay_key.is_some(), @@ -433,6 +436,8 @@ impl BrokerRuntime { cli: Some(cli.clone()), model: None, cwd: None, + session_id: None, + harness: None, team: None, shadow_of: None, shadow_mode: None, @@ -529,6 +534,7 @@ impl BrokerRuntime { "runtime": "pty", "cli": cli, "model": effective_spec.model.clone(), + "sessionId": effective_spec.session_id.clone(), "pid": pid, "source": "relaycast_ws_fallback", "pre_registered": worker_relay_key.is_some(), diff --git a/crates/broker/src/runtime/session.rs b/crates/broker/src/runtime/session.rs index 9ae5d82e9..d16d273a9 100644 --- a/crates/broker/src/runtime/session.rs +++ b/crates/broker/src/runtime/session.rs @@ -110,8 +110,6 @@ pub(crate) struct RelaySessionOptions<'a> { pub(crate) agent_type: Option<&'a str>, /// Read .mcp.json for additional self-name identities pub(crate) read_mcp_identity: bool, - /// Write relaycast server entry to .mcp.json - pub(crate) ensure_mcp_config: bool, pub(crate) runtime_cwd: &'a Path, } @@ -156,7 +154,6 @@ pub(crate) async fn connect_relay(opts: RelaySessionOptions<'_>) -> Result, model: Option, + harness: Option, args: Vec, channels: Vec, cwd: Option, @@ -58,6 +59,8 @@ pub(crate) fn build_http_api_spawn_spec( cli: cli_command, model, cwd, + session_id: None, + harness, team, shadow_of, shadow_mode, diff --git a/crates/broker/src/runtime/tests.rs b/crates/broker/src/runtime/tests.rs index 001771807..1c59435f7 100644 --- a/crates/broker/src/runtime/tests.rs +++ b/crates/broker/src/runtime/tests.rs @@ -70,6 +70,8 @@ async fn make_worker_registry_with_worker(name: &str) -> WorkerRegistry { cli: Some("cat".to_string()), model: None, cwd: None, + session_id: None, + harness: None, team: None, shadow_of: None, shadow_mode: None, @@ -1926,6 +1928,7 @@ fn http_api_spawn_spec_defaults_to_pty_runtime() { "codex".to_string(), None, Some("o3".to_string()), + None, vec!["--fast".to_string()], vec!["general".to_string()], Some("/tmp/project".to_string()), @@ -1942,6 +1945,35 @@ fn http_api_spawn_spec_defaults_to_pty_runtime() { assert_eq!(spec.model.as_deref(), Some("o3")); } +#[test] +fn http_api_spawn_spec_carries_harness_definition() { + let spec = build_http_api_spawn_spec( + "worker-a".to_string(), + "qwen".to_string(), + None, + Some("qwen3-coder".to_string()), + Some(crate::protocol::HarnessDefinition { + binary: Some("qwen".to_string()), + interactive_args: vec!["run".to_string(), "{modelArgs}".to_string()], + model_args: vec!["-m".to_string(), "{model}".to_string()], + ..Default::default() + }), + vec![], + vec!["general".to_string()], + None, + None, + None, + None, + None, + ) + .expect("spec should build"); + + let harness = spec.harness.expect("harness should be retained"); + assert_eq!(harness.binary.as_deref(), Some("qwen")); + assert_eq!(harness.interactive_args, vec!["run", "{modelArgs}"]); + assert_eq!(harness.model_args, vec!["-m", "{model}"]); +} + #[test] fn http_api_spawn_spec_uses_headless_runtime_for_supported_providers() { let spec = build_http_api_spawn_spec( @@ -1949,6 +1981,7 @@ fn http_api_spawn_spec_uses_headless_runtime_for_supported_providers() { "opencode".to_string(), Some("headless".to_string()), Some("ignored".to_string()), + None, vec![], vec!["general".to_string()], None, @@ -2009,6 +2042,7 @@ fn http_api_spawn_spec_rejects_unknown_headless_providers() { "codex".to_string(), Some("headless".to_string()), None, + None, vec![], vec!["general".to_string()], None, diff --git a/crates/broker/src/runtime/worker_events.rs b/crates/broker/src/runtime/worker_events.rs index abb5a16a4..c45ae580e 100644 --- a/crates/broker/src/runtime/worker_events.rs +++ b/crates/broker/src/runtime/worker_events.rs @@ -316,7 +316,7 @@ impl BrokerRuntime { .and_then(|p| p.get("runtime")) .and_then(Value::as_str) .unwrap_or("pty"); - let (provider_val, cli_val, model_val) = workers + let (provider_val, cli_val, model_val, session_id_val, pid_val) = workers .workers .get(&name) .map(|h| { @@ -324,9 +324,11 @@ impl BrokerRuntime { h.spec.provider.clone(), h.spec.cli.clone(), h.spec.model.clone(), + h.spec.session_id.clone(), + h.child.id(), ) }) - .unwrap_or((None, None, None)); + .unwrap_or((None, None, None, None, None)); let _ = send_event( sdk_out_tx, json!({ @@ -336,6 +338,8 @@ impl BrokerRuntime { "provider": provider_val, "cli": cli_val, "model": model_val, + "sessionId": session_id_val, + "pid": pid_val, }), ) .await; diff --git a/crates/broker/src/snippets.rs b/crates/broker/src/snippets.rs index b73da9528..8c821ea2d 100644 --- a/crates/broker/src/snippets.rs +++ b/crates/broker/src/snippets.rs @@ -11,7 +11,8 @@ use tokio::process::Command; use crate::types::AgentResultMcpConfig; -const RELAYCAST_MCP_PACKAGE: &str = "@relaycast/mcp"; +const AGENT_RELAY_MCP_PACKAGE: &str = "agent-relay"; +const AGENT_RELAY_MCP_SUBCOMMAND: &str = "mcp"; const MCP_FILE: &str = ".mcp.json"; const RELAYCAST_SERVER: &str = "relaycast"; @@ -269,7 +270,7 @@ fn relaycast_server_config( ) -> Value { let mut server = Map::new(); // Allow overriding the MCP command for local development/testing. - // e.g. RELAYCAST_MCP_COMMAND="node /path/to/relaycast/packages/mcp/dist/stdio.js" + // e.g. RELAYCAST_MCP_COMMAND="node /path/to/agent-relay/dist/src/cli/relaycast-mcp.js" if let Ok(custom_cmd) = std::env::var("RELAYCAST_MCP_COMMAND") { let parts: Vec<&str> = custom_cmd.split_whitespace().collect(); if let Some((cmd, args_slice)) = parts.split_first() { @@ -290,7 +291,8 @@ fn relaycast_server_config( "args".into(), Value::Array(vec![ Value::String("-y".into()), - Value::String(RELAYCAST_MCP_PACKAGE.into()), + Value::String(AGENT_RELAY_MCP_PACKAGE.into()), + Value::String(AGENT_RELAY_MCP_SUBCOMMAND.into()), ]), ); } @@ -439,7 +441,8 @@ pub fn ensure_opencode_config_with_result( Value::Array(vec![ Value::String("npx".into()), Value::String("-y".into()), - Value::String(RELAYCAST_MCP_PACKAGE.into()), + Value::String(AGENT_RELAY_MCP_PACKAGE.into()), + Value::String(AGENT_RELAY_MCP_SUBCOMMAND.into()), ]), ); let mut env = Map::new(); @@ -781,7 +784,7 @@ pub async fn configure_relaycast_mcp_with_result( "--config".to_string(), "mcp_servers.relaycast.command=\"npx\"".to_string(), "--config".to_string(), - "mcp_servers.relaycast.args=[\"-y\", \"@relaycast/mcp\"]".to_string(), + "mcp_servers.relaycast.args=[\"-y\", \"agent-relay\", \"mcp\"]".to_string(), ]); if let Some(key) = api_key { args.extend([ @@ -982,7 +985,7 @@ fn gemini_droid_manual_mcp_add_cmd(cli: &str, is_gemini: bool) -> String { let env_flag = gemini_droid_mcp_env_flag(is_gemini); let cmd_separator = if is_gemini { "" } else { " --" }; format!( - "{cli} mcp add {env_flag} RELAY_API_KEY= {env_flag} RELAY_BASE_URL= relaycast{cmd_separator} npx -y @relaycast/mcp" + "{cli} mcp add {env_flag} RELAY_API_KEY= {env_flag} RELAY_BASE_URL= relaycast{cmd_separator} npx -y agent-relay mcp" ) } @@ -1061,7 +1064,8 @@ fn gemini_droid_mcp_add_args_with_result( } args.push("npx".to_string()); args.push("-y".to_string()); - args.push(RELAYCAST_MCP_PACKAGE.to_string()); + args.push(AGENT_RELAY_MCP_PACKAGE.to_string()); + args.push(AGENT_RELAY_MCP_SUBCOMMAND.to_string()); args } @@ -1161,12 +1165,11 @@ mod tests { use super::ensure_relaycast_mcp_config; - fn assert_is_reaycast_mcp_package(value: Option<&str>) { - let package = value.expect("expected relaycast mcp package string"); - assert!( - package.starts_with("@relaycast/mcp"), - "expected package to start with @relaycast/mcp, got: {package}" - ); + fn assert_is_agent_relay_mcp_args(args: Option<&Vec>) { + let args = args.expect("expected relaycast mcp args"); + assert_eq!(args.get(0).and_then(Value::as_str), Some("-y")); + assert_eq!(args.get(1).and_then(Value::as_str), Some("agent-relay")); + assert_eq!(args.get(2).and_then(Value::as_str), Some("mcp")); } fn test_agent_result_config() -> crate::types::AgentResultMcpConfig { @@ -1200,19 +1203,7 @@ mod tests { json["mcpServers"]["relaycast"]["command"].as_str(), Some("npx") ); - assert_eq!( - json["mcpServers"]["relaycast"]["args"] - .as_array() - .and_then(|a| a.first()) - .and_then(Value::as_str), - Some("-y") - ); - assert_is_reaycast_mcp_package( - json["mcpServers"]["relaycast"]["args"] - .as_array() - .and_then(|a| a.get(1)) - .and_then(Value::as_str), - ); + assert_is_agent_relay_mcp_args(json["mcpServers"]["relaycast"]["args"].as_array()); } #[test] @@ -1348,7 +1339,8 @@ mod tests { .as_array() .expect("args array"); assert_eq!(mcp_args[0].as_str(), Some("-y")); - assert_is_reaycast_mcp_package(mcp_args[1].as_str()); + assert_eq!(mcp_args[1].as_str(), Some("agent-relay")); + assert_eq!(mcp_args[2].as_str(), Some("mcp")); } #[tokio::test] @@ -1527,11 +1519,8 @@ mod tests { assert_eq!(args[relaycast_idx + 1], "--"); assert_eq!(args[relaycast_idx + 2], "npx"); assert_eq!(args[relaycast_idx + 3], "-y"); - assert!( - args[relaycast_idx + 4].starts_with("@relaycast/mcp"), - "expected relaycast package name, got: {}", - args[relaycast_idx + 4] - ); + assert_eq!(args[relaycast_idx + 4], "agent-relay"); + assert_eq!(args[relaycast_idx + 5], "mcp"); } #[test] @@ -1568,11 +1557,11 @@ mod tests { #[test] fn droid_manual_mcp_add_command_uses_option_separator() { let droid_cmd = super::gemini_droid_manual_mcp_add_cmd("droid", false); - assert!(droid_cmd.contains("relaycast -- npx -y @relaycast/mcp")); + assert!(droid_cmd.contains("relaycast -- npx -y agent-relay mcp")); let gemini_cmd = super::gemini_droid_manual_mcp_add_cmd("gemini", true); - assert!(!gemini_cmd.contains("relaycast -- npx -y @relaycast/mcp")); - assert!(gemini_cmd.contains("relaycast npx -y @relaycast/mcp")); + assert!(!gemini_cmd.contains("relaycast -- npx -y agent-relay mcp")); + assert!(gemini_cmd.contains("relaycast npx -y agent-relay mcp")); } #[test] @@ -1814,7 +1803,8 @@ mod tests { let cmd = mcp["command"].as_array().expect("command array"); assert_eq!(cmd[0].as_str(), Some("npx")); assert_eq!(cmd[1].as_str(), Some("-y")); - assert_is_reaycast_mcp_package(cmd[2].as_str()); + assert_eq!(cmd[2].as_str(), Some("agent-relay")); + assert_eq!(cmd[3].as_str(), Some("mcp")); // Environment (note: opencode uses "environment" not "env") let oc_env = &mcp["environment"]; @@ -2331,7 +2321,7 @@ mod tests { }, "relaycast": { "command": "npx", - "args": ["-y", "@relaycast/mcp"], + "args": ["-y", "agent-relay", "mcp"], "env": { "RELAY_API_KEY": "old_stale_key" } } } diff --git a/crates/broker/src/supervisor.rs b/crates/broker/src/supervisor.rs index d1c00f28e..194f2b65a 100644 --- a/crates/broker/src/supervisor.rs +++ b/crates/broker/src/supervisor.rs @@ -232,6 +232,8 @@ mod tests { cli: Some("claude".to_string()), model: None, cwd: None, + session_id: None, + harness: None, team: None, shadow_of: None, shadow_mode: None, diff --git a/crates/broker/src/worker.rs b/crates/broker/src/worker.rs index 2b952c17e..4df6c0dda 100644 --- a/crates/broker/src/worker.rs +++ b/crates/broker/src/worker.rs @@ -1,5 +1,10 @@ +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::{ collections::HashMap, + env, + ffi::OsString, + fs, path::{Path, PathBuf}, process::Stdio, time::{Duration, Instant}, @@ -7,7 +12,10 @@ use std::{ use crate::{ metrics::MetricsCollector, - protocol::{AgentRuntime, AgentSpec, ProtocolEnvelope, RelayDelivery, PROTOCOL_VERSION}, + protocol::{ + AgentRuntime, AgentSpec, HarnessDefinition, ProtocolEnvelope, RelayDelivery, + PROTOCOL_VERSION, + }, relaycast::configure_relaycast_mcp_with_result, supervisor::Supervisor, types::AgentResultMcpConfig, @@ -133,6 +141,7 @@ impl WorkerRegistry { "provider": handle.spec.provider.clone(), "cli": handle.spec.cli, "model": handle.spec.model, + "sessionId": handle.spec.session_id, "team": handle.spec.team, "channels": handle.spec.channels, "parent": handle.parent, @@ -228,9 +237,12 @@ impl WorkerRegistry { match spec.runtime { AgentRuntime::Pty => { let cli = spec.cli.as_deref().context("pty runtime requires `cli`")?; - let (resolved_cli, inline_cli_args) = parse_cli_command(cli) + let (parsed_cli, inline_cli_args) = parse_cli_command(cli) .with_context(|| format!("invalid CLI command '{cli}'"))?; - let normalized_cli = normalize_cli_name(&resolved_cli); + let harness = resolve_harness_definition(&parsed_cli, spec.harness.clone()); + spec.harness = harness.clone(); + let resolved_cli = resolve_harness_command(&parsed_cli, harness.as_ref()); + let adapter_cli = harness_adapter_key(&resolved_cli, harness.as_ref()); let mut effective_args = inline_cli_args; effective_args.extend(spec.args.clone()); @@ -241,7 +253,7 @@ impl WorkerRegistry { } command.arg(&resolved_cli); - let cli_lower = normalized_cli.to_lowercase(); + let cli_lower = adapter_cli.to_lowercase(); let is_claude = cli_lower == "claude" || cli_lower.starts_with("claude:"); let is_codex = cli_lower == "codex"; let is_gemini = cli_lower == "gemini"; @@ -255,28 +267,77 @@ impl WorkerRegistry { { spec.model = Some(model); } + let mut harness_session_args = Vec::new(); + if is_claude { + spec.session_id = prepare_claude_session_args(&mut effective_args); + } else if is_codex { + match codex_session_reference(&effective_args) { + CodexSessionReference::Known(thread_id) => { + spec.session_id = Some(thread_id); + } + CodexSessionReference::Unknown => {} + CodexSessionReference::None => { + if codex_has_positional_arg(&effective_args) { + tracing::debug!( + worker = %spec.name, + "not pre-creating Codex session because args contain a positional prompt or subcommand" + ); + } else { + let cwd = Path::new(spec.cwd.as_deref().unwrap_or(".")); + let thread_id = + crate::codex_session::create_resumable_codex_thread( + &resolved_cli, + cwd, + &self.worker_env, + ) + .await + .with_context(|| { + format!( + "failed to create resumable Codex session for '{}'", + spec.name + ) + })?; + tracing::info!( + worker = %spec.name, + session_id = %thread_id, + "created resumable Codex session for spawned PTY" + ); + spec.session_id = Some(thread_id.clone()); + harness_session_args.push("resume".to_string()); + harness_session_args.push(thread_id); + } + } + } + } // NOTE: Permission-bypass flags are auto-injected for all spawned agents. // This means any actor who can trigger agent.add gets agents with no permission // guardrails. Future work should make this an explicit opt-in per step/agent. - let bypass_flag: Option<&str> = if is_claude - && !effective_args - .iter() - .any(|a| a.contains("dangerously-skip-permissions")) - { - Some("--dangerously-skip-permissions") - } else if is_codex - && !effective_args - .iter() - .any(|a| a.contains("dangerously-bypass") || a.contains("full-auto")) - { - Some("--dangerously-bypass-approvals-and-sandbox") - } else if is_gemini && !effective_args.iter().any(|a| a == "--yolo" || a == "-y") { - Some("--yolo") - } else { - None - }; + let bypass_flag: Option = harness + .as_ref() + .and_then(|definition| harness_bypass_flag(definition, &effective_args)) + .or_else(|| { + if is_claude + && !effective_args + .iter() + .any(|a| a.contains("dangerously-skip-permissions")) + { + Some("--dangerously-skip-permissions".to_string()) + } else if is_codex + && !effective_args.iter().any(|a| { + a.contains("dangerously-bypass") || a.contains("full-auto") + }) + { + Some("--dangerously-bypass-approvals-and-sandbox".to_string()) + } else if is_gemini + && !effective_args.iter().any(|a| a == "--yolo" || a == "-y") + { + Some("--yolo".to_string()) + } else { + None + } + }); - if let Some(flag) = bypass_flag { + if let Some(flag) = bypass_flag.as_deref() { tracing::warn!( worker = %spec.name, flag = %flag, @@ -286,7 +347,7 @@ impl WorkerRegistry { let mcp_args = self .build_mcp_args( - cli, + &adapter_cli, &spec.name, &effective_args, Path::new(spec.cwd.as_deref().unwrap_or(".")), @@ -296,35 +357,66 @@ impl WorkerRegistry { ) .await?; - let model_flag = resolve_model_flag_for_cli( - &resolved_cli, - &cli_lower, - &spec.name, - spec.model.as_deref(), - &effective_args, - ) - .await; - if let Some(ref model) = model_flag { - spec.model = Some(model.clone()); - } - - let has_extra = bypass_flag.is_some() - || model_flag.is_some() - || !effective_args.is_empty() - || !mcp_args.is_empty(); - if has_extra { - command.arg("--"); - if let Some(flag) = bypass_flag { - command.arg(flag); + let model_args = if let Some(definition) = harness.as_ref() { + match resolve_harness_model_args( + definition, + &resolved_cli, + &cli_lower, + &spec.name, + spec.model.as_deref(), + &effective_args, + ) + .await + { + Some((model, args)) => { + spec.model = Some(model); + args + } + None => Vec::new(), } + } else { + let model_flag = resolve_model_flag_for_cli( + &resolved_cli, + &cli_lower, + &spec.name, + spec.model.as_deref(), + &effective_args, + ) + .await; if let Some(ref model) = model_flag { - command.arg("--model"); - command.arg(model); + spec.model = Some(model.clone()); } - for arg in &mcp_args { - command.arg(arg); + model_flag + .map(|model| vec!["--model".to_string(), model]) + .unwrap_or_default() + }; + + let extra_args = if let Some(definition) = harness.as_ref() { + render_harness_interactive_args( + definition, + bypass_flag.as_deref(), + spec.model.as_deref(), + &model_args, + &mcp_args, + &effective_args, + &harness_session_args, + ) + } else { + let mut args = Vec::new(); + if let Some(flag) = bypass_flag { + args.push(flag); } - for arg in &effective_args { + args.extend(model_args); + args.extend(mcp_args); + args.extend(effective_args); + args.extend(harness_session_args); + args + }; + + let has_extra = !extra_args.is_empty(); + if has_extra { + command.arg("--"); + for arg in &extra_args { command.arg(arg); } } @@ -694,12 +786,689 @@ impl WorkerRegistry { } } +#[derive(Debug, Clone, PartialEq, Eq)] +enum CodexSessionReference { + Known(String), + Unknown, + None, +} + +fn prepare_claude_session_args(args: &mut Vec) -> Option { + if let Some(session_id) = cli_flag_value(args, "--session-id") { + return Some(session_id); + } + if cli_flag_present(args, &["--session-id"]) { + return None; + } + if let Some(session_id) = + cli_flag_value(args, "--resume").or_else(|| cli_flag_value(args, "-r")) + { + return Some(session_id); + } + if cli_flag_present(args, &["--resume", "-r", "--continue", "-c"]) { + return None; + } + + let session_id = uuid::Uuid::new_v4().to_string(); + args.push("--session-id".to_string()); + args.push(session_id.clone()); + Some(session_id) +} + +fn codex_session_reference(args: &[String]) -> CodexSessionReference { + let mut index = 0; + let mut skip_next = false; + while index < args.len() { + let arg = args[index].as_str(); + if skip_next { + skip_next = false; + index += 1; + continue; + } + if arg == "--" { + return CodexSessionReference::None; + } + if codex_flag_consumes_next_arg(arg) { + skip_next = true; + index += 1; + continue; + } + if arg == "resume" || arg == "fork" { + let Some(next) = args.get(index + 1).map(String::as_str) else { + return CodexSessionReference::Unknown; + }; + if next == "--last" || next.starts_with('-') { + return CodexSessionReference::Unknown; + } + return CodexSessionReference::Known(next.to_string()); + } + index += 1; + } + CodexSessionReference::None +} + +fn codex_has_positional_arg(args: &[String]) -> bool { + let mut skip_next = false; + for arg in args { + if skip_next { + skip_next = false; + continue; + } + if arg == "--" { + return true; + } + if codex_flag_consumes_next_arg(arg) { + skip_next = true; + continue; + } + if arg.starts_with('-') { + continue; + } + return true; + } + false +} + +fn codex_flag_consumes_next_arg(arg: &str) -> bool { + if arg.contains('=') { + return false; + } + matches!( + arg, + "--model" + | "-m" + | "--profile" + | "--config" + | "-c" + | "--sandbox" + | "-s" + | "--ask-for-approval" + | "--approval-policy" + | "--cd" + | "--cwd" + ) +} + +fn cli_flag_value(args: &[String], flag: &str) -> Option { + let equals_prefix = format!("{flag}="); + let mut index = 0; + while index < args.len() { + let arg = args[index].as_str(); + if arg == flag { + return args + .get(index + 1) + .filter(|value| !value.starts_with('-')) + .cloned(); + } + if let Some(value) = arg.strip_prefix(&equals_prefix) { + if !value.is_empty() { + return Some(value.to_string()); + } + } + index += 1; + } + None +} + +fn cli_flag_present(args: &[String], flags: &[&str]) -> bool { + args.iter().any(|arg| { + let arg = arg.as_str(); + flags.iter().any(|flag| { + arg == *flag + || arg + .strip_prefix(*flag) + .is_some_and(|rest| rest.starts_with('=')) + }) + }) +} + fn args_include_model_override(args: &[String]) -> bool { args.iter().any(|arg| { arg == "--model" || arg.starts_with("--model=") || arg == "-m" || arg.starts_with("-m=") }) } +fn canonicalize_display(path: &Path) -> String { + std::fs::canonicalize(path) + .ok() + .and_then(|resolved| resolved.to_str().map(|s| s.to_string())) + .unwrap_or_else(|| path.to_string_lossy().to_string()) +} + +fn expand_home_path(path: &str) -> PathBuf { + if let Some(rest) = path.strip_prefix("~/") { + if let Ok(home) = env::var("HOME") { + return PathBuf::from(home).join(rest); + } + } + PathBuf::from(path) +} + +fn fallback_path_env() -> OsString { + #[cfg(unix)] + { + let home = env::var("HOME").unwrap_or_else(|_| String::from("/root")); + OsString::from(format!( + "{home}/.local/bin:{home}/.opencode/bin:{home}/.claude/local:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin" + )) + } + #[cfg(windows)] + { + OsString::from(r"C:\Windows\System32;C:\Windows") + } +} + +fn harness_name_key(cli: &str) -> String { + normalize_cli_name(cli) + .split(':') + .next() + .unwrap_or(cli) + .to_lowercase() +} + +fn builtin_harness_definition(adapter: &str) -> Option { + let key = harness_name_key(adapter); + let mut definition = HarnessDefinition { + adapter: Some(key.clone()), + ..Default::default() + }; + + match key.as_str() { + "claude" => { + definition.binary = Some("claude".to_string()); + definition.non_interactive_args = vec![ + "-p".to_string(), + "{bypass}".to_string(), + "{task}".to_string(), + "{args}".to_string(), + ]; + definition.bypass_flag = Some("--dangerously-skip-permissions".to_string()); + definition.search_paths = vec!["~/.claude/local".to_string()]; + Some(definition) + } + "codex" => { + definition.binary = Some("codex".to_string()); + definition.non_interactive_args = vec![ + "exec".to_string(), + "{bypass}".to_string(), + "{task}".to_string(), + "{args}".to_string(), + ]; + definition.bypass_flag = Some("--dangerously-bypass-approvals-and-sandbox".to_string()); + definition.bypass_aliases = vec!["--full-auto".to_string()]; + definition.search_paths = vec!["~/.local/bin".to_string()]; + Some(definition) + } + "gemini" => { + definition.binary = Some("gemini".to_string()); + definition.non_interactive_args = + vec!["-p".to_string(), "{task}".to_string(), "{args}".to_string()]; + definition.bypass_flag = Some("--yolo".to_string()); + definition.bypass_aliases = vec!["-y".to_string()]; + Some(definition) + } + "opencode" => { + definition.binary = Some("opencode".to_string()); + definition.non_interactive_args = vec![ + "run".to_string(), + "{task}".to_string(), + "{args}".to_string(), + ]; + definition.search_paths = vec!["~/.opencode/bin".to_string()]; + definition.ignore_exit_code = true; + Some(definition) + } + "droid" => { + definition.binary = Some("droid".to_string()); + definition.non_interactive_args = vec![ + "exec".to_string(), + "{task}".to_string(), + "{args}".to_string(), + ]; + Some(definition) + } + "aider" => { + definition.binary = Some("aider".to_string()); + definition.non_interactive_args = vec![ + "--message".to_string(), + "{task}".to_string(), + "--yes-always".to_string(), + "--no-git".to_string(), + "{args}".to_string(), + ]; + Some(definition) + } + "goose" => { + definition.binary = Some("goose".to_string()); + definition.non_interactive_args = vec![ + "run".to_string(), + "--text".to_string(), + "{task}".to_string(), + "--no-session".to_string(), + "{args}".to_string(), + ]; + Some(definition) + } + "cursor" => { + definition.adapter = Some("cursor".to_string()); + definition.binaries = vec!["cursor-agent".to_string(), "agent".to_string()]; + definition.non_interactive_args = vec![ + "--force".to_string(), + "-p".to_string(), + "{task}".to_string(), + "{args}".to_string(), + ]; + Some(definition) + } + "cursor-agent" | "agent" => { + definition.adapter = Some("cursor".to_string()); + definition.binary = Some(key.clone()); + definition.non_interactive_args = vec![ + "--force".to_string(), + "-p".to_string(), + "{task}".to_string(), + "{args}".to_string(), + ]; + Some(definition) + } + _ => None, + } +} + +fn merge_harness_definitions( + base: HarnessDefinition, + override_definition: HarnessDefinition, +) -> HarnessDefinition { + let override_binary = override_definition.binary.and_then(|binary| { + if binary.trim().is_empty() { + None + } else { + Some(binary) + } + }); + let overrides_binary = override_binary.is_some(); + HarnessDefinition { + adapter: override_definition.adapter.or(base.adapter), + binary: override_binary.or(base.binary), + binaries: if override_definition.binaries.is_empty() { + if overrides_binary { + Vec::new() + } else { + base.binaries + } + } else { + override_definition.binaries + }, + interactive_args: if override_definition.interactive_args.is_empty() { + base.interactive_args + } else { + override_definition.interactive_args + }, + non_interactive_args: if override_definition.non_interactive_args.is_empty() { + base.non_interactive_args + } else { + override_definition.non_interactive_args + }, + model_args: if override_definition.model_args.is_empty() { + base.model_args + } else { + override_definition.model_args + }, + bypass_flag: override_definition.bypass_flag.or(base.bypass_flag), + bypass_aliases: if override_definition.bypass_aliases.is_empty() { + base.bypass_aliases + } else { + override_definition.bypass_aliases + }, + search_paths: if override_definition.search_paths.is_empty() { + base.search_paths + } else { + override_definition.search_paths + }, + ignore_exit_code: override_definition.ignore_exit_code || base.ignore_exit_code, + proxy_provider: override_definition.proxy_provider.or(base.proxy_provider), + aliases: if override_definition.aliases.is_empty() { + base.aliases + } else { + override_definition.aliases + }, + } +} + +fn resolve_harness_definition( + cli_name: &str, + provided: Option, +) -> Option { + let default_adapter = harness_name_key(cli_name); + let mut definition = if let Some(provided) = provided { + let adapter_key = provided + .adapter + .as_deref() + .map(str::trim) + .filter(|adapter| !adapter.is_empty()) + .map(harness_name_key) + .unwrap_or_else(|| default_adapter.clone()); + if let Some(base) = builtin_harness_definition(&adapter_key) { + merge_harness_definitions(base, provided) + } else { + provided + } + } else { + builtin_harness_definition(&default_adapter)? + }; + if definition + .adapter + .as_deref() + .map(str::trim) + .filter(|adapter| !adapter.is_empty()) + .is_none() + { + definition.adapter = Some(default_adapter); + } + Some(definition) +} + +fn harness_adapter_key(resolved_cli: &str, harness: Option<&HarnessDefinition>) -> String { + harness + .and_then(|definition| definition.adapter.as_deref()) + .map(str::trim) + .filter(|adapter| !adapter.is_empty()) + .map(harness_name_key) + .unwrap_or_else(|| harness_name_key(resolved_cli)) +} + +fn resolve_command_with_paths(command: &str, search_paths: &[String]) -> String { + if command.contains('/') || command.contains('\\') || command.starts_with('.') { + let candidate = expand_home_path(command); + if is_executable_file(&candidate) { + return canonicalize_display(&candidate); + } + return command.to_string(); + } + + for dir in search_paths { + let candidate = expand_home_path(dir).join(command); + if is_executable_file(&candidate) { + return canonicalize_display(&candidate); + } + } + + let path_env = env::var_os("PATH") + .filter(|value| !value.is_empty()) + .unwrap_or_else(fallback_path_env); + for dir in env::split_paths(&path_env) { + let candidate = dir.join(command); + if is_executable_file(&candidate) { + return canonicalize_display(&candidate); + } + } + + command.to_string() +} + +fn resolve_harness_command(default_command: &str, harness: Option<&HarnessDefinition>) -> String { + let Some(harness) = harness else { + return default_command.to_string(); + }; + + let candidates: Vec<&str> = if harness.binaries.is_empty() { + harness + .binary + .as_deref() + .map(|binary| vec![binary]) + .unwrap_or_else(|| vec![default_command]) + } else { + harness.binaries.iter().map(String::as_str).collect() + }; + + for candidate in candidates.iter().copied() { + let resolved = resolve_command_with_paths(candidate, &harness.search_paths); + if resolved != candidate || is_executable_file(Path::new(&resolved)) { + return resolved; + } + } + + candidates + .first() + .copied() + .unwrap_or(default_command) + .to_string() +} + +fn is_executable_file(path: &Path) -> bool { + let Ok(metadata) = fs::metadata(path) else { + return false; + }; + if !metadata.is_file() { + return false; + } + #[cfg(unix)] + { + metadata.permissions().mode() & 0o111 != 0 + } + #[cfg(not(unix))] + { + true + } +} + +fn arg_matches_flag(arg: &str, flag: &str) -> bool { + arg == flag + || arg + .strip_prefix(flag) + .is_some_and(|rest| rest.starts_with('=')) +} + +fn harness_bypass_flag(harness: &HarnessDefinition, existing_args: &[String]) -> Option { + let flag = harness.bypass_flag.as_deref()?.trim(); + if flag.is_empty() { + return None; + } + + let mut flags = vec![flag]; + flags.extend( + harness + .bypass_aliases + .iter() + .map(String::as_str) + .map(str::trim) + .filter(|alias| !alias.is_empty()), + ); + if existing_args.iter().any(|arg| { + flags + .iter() + .any(|candidate| arg_matches_flag(arg, candidate)) + }) { + return None; + } + + Some(flag.to_string()) +} + +fn is_exact_placeholder(value: &str, name: &str) -> bool { + value == format!("{{{name}}}") || value == format!("{{{{{name}}}}}") +} + +fn replace_scalar_placeholders( + template: &str, + bypass: Option<&str>, + model: Option<&str>, +) -> String { + template + .replace("{{bypass}}", bypass.unwrap_or("")) + .replace("{bypass}", bypass.unwrap_or("")) + .replace("{{model}}", model.unwrap_or("")) + .replace("{model}", model.unwrap_or("")) +} + +fn render_harness_arg_template( + template: &[String], + bypass: Option<&str>, + model: Option<&str>, + model_args: &[String], + mcp_args: &[String], + args: &[String], + session_args: &[String], +) -> Vec { + let mut rendered = Vec::new(); + for entry in template { + if is_exact_placeholder(entry, "args") { + rendered.extend(args.iter().cloned()); + continue; + } + if is_exact_placeholder(entry, "modelArgs") { + rendered.extend(model_args.iter().cloned()); + continue; + } + if is_exact_placeholder(entry, "mcpArgs") { + rendered.extend(mcp_args.iter().cloned()); + continue; + } + if is_exact_placeholder(entry, "sessionArgs") { + rendered.extend(session_args.iter().cloned()); + continue; + } + if is_exact_placeholder(entry, "bypass") { + if let Some(flag) = bypass { + rendered.push(flag.to_string()); + } + continue; + } + if is_exact_placeholder(entry, "model") { + if let Some(model) = model { + rendered.push(model.to_string()); + } + continue; + } + + rendered.push(replace_scalar_placeholders(entry, bypass, model)); + } + rendered +} + +fn render_harness_interactive_args( + harness: &HarnessDefinition, + bypass: Option<&str>, + model: Option<&str>, + model_args: &[String], + mcp_args: &[String], + args: &[String], + session_args: &[String], +) -> Vec { + let default_template; + let template = if harness.interactive_args.is_empty() { + default_template = vec![ + "{bypass}".to_string(), + "{modelArgs}".to_string(), + "{mcpArgs}".to_string(), + "{args}".to_string(), + "{sessionArgs}".to_string(), + ]; + &default_template + } else { + &harness.interactive_args + }; + + render_harness_arg_template( + template, + bypass, + model, + model_args, + mcp_args, + args, + session_args, + ) +} + +fn render_harness_model_args(harness: &HarnessDefinition, model: &str) -> Vec { + let default_template; + let template = if harness.model_args.is_empty() { + default_template = vec!["--model".to_string(), "{model}".to_string()]; + &default_template + } else { + &harness.model_args + }; + + render_harness_arg_template(template, None, Some(model), &[], &[], &[], &[]) +} + +fn model_arg_markers(harness: &HarnessDefinition) -> Vec { + let default_template; + let template = if harness.model_args.is_empty() { + default_template = vec!["--model".to_string(), "{model}".to_string()]; + &default_template + } else { + &harness.model_args + }; + + let mut markers = Vec::new(); + for (index, entry) in template.iter().enumerate() { + if is_exact_placeholder(entry, "model") { + if let Some(previous) = index.checked_sub(1).and_then(|i| template.get(i)) { + if previous.starts_with('-') { + markers.push(previous.clone()); + } + } + continue; + } + + let model_token = entry.find("{model}").or_else(|| entry.find("{{model}}")); + if let Some(pos) = model_token { + let marker = entry[..pos].trim_end_matches('='); + if marker.starts_with('-') && !marker.is_empty() { + markers.push(marker.to_string()); + } + } + } + markers.sort(); + markers.dedup(); + markers +} + +fn harness_model_override_present(harness: &HarnessDefinition, existing_args: &[String]) -> bool { + let markers = model_arg_markers(harness); + if markers.is_empty() { + return args_include_model_override(existing_args); + } + existing_args.iter().any(|arg| { + markers + .iter() + .any(|marker| arg_matches_flag(arg.as_str(), marker.as_str())) + }) +} + +async fn resolve_harness_model_args( + harness: &HarnessDefinition, + resolved_cli: &str, + normalized_cli: &str, + worker_name: &str, + requested_model: Option<&str>, + existing_args: &[String], +) -> Option<(String, Vec)> { + let requested = requested_model?.trim(); + if requested.is_empty() || harness_model_override_present(harness, existing_args) { + return None; + } + + let model = if normalized_cli.eq_ignore_ascii_case("codex") { + codex_local_fallback_model(resolved_cli, requested) + .await + .inspect(|&fallback| { + tracing::warn!( + worker = %worker_name, + requested_model = %requested, + fallback_model = %fallback, + "local Codex CLI model catalog does not confirm requested model; using fallback" + ); + }) + .unwrap_or(requested) + } else { + requested + }; + + Some((model.to_string(), render_harness_model_args(harness, model))) +} + async fn apply_codex_model_arg_fallback( resolved_cli: &str, normalized_cli: &str, @@ -1009,6 +1778,184 @@ fn spawn_worker_reader( }); } +#[cfg(test)] +mod harness_adapter_tests { + use super::*; + + #[test] + fn built_in_harnesses_resolve_as_adapter_config() { + let harness = resolve_harness_definition("codex", None).expect("codex harness"); + + assert_eq!(harness.adapter.as_deref(), Some("codex")); + assert_eq!(harness.binary.as_deref(), Some("codex")); + assert_eq!( + harness.bypass_flag.as_deref(), + Some("--dangerously-bypass-approvals-and-sandbox") + ); + assert_eq!(harness.bypass_aliases, vec!["--full-auto".to_string()]); + } + + #[test] + fn custom_harness_can_select_builtin_lifecycle_adapter() { + let harness = resolve_harness_definition( + "company-codex", + Some(HarnessDefinition { + adapter: Some("codex".to_string()), + binary: Some("company-codex".to_string()), + ..Default::default() + }), + ) + .expect("custom harness"); + + assert_eq!( + harness_adapter_key("company-codex", Some(&harness)), + "codex" + ); + assert_eq!( + harness.bypass_flag.as_deref(), + Some("--dangerously-bypass-approvals-and-sandbox") + ); + } + + #[test] + fn binary_override_replaces_inherited_adapter_binaries() { + let harness = resolve_harness_definition( + "company-cursor", + Some(HarnessDefinition { + adapter: Some("cursor".to_string()), + binary: Some("company-cursor".to_string()), + ..Default::default() + }), + ) + .expect("custom cursor harness"); + + assert_eq!(harness.binary.as_deref(), Some("company-cursor")); + assert!(harness.binaries.is_empty()); + } + + #[test] + fn blank_binary_override_does_not_clear_inherited_adapter_binaries() { + let harness = resolve_harness_definition( + "company-cursor", + Some(HarnessDefinition { + adapter: Some("cursor".to_string()), + binary: Some(" ".to_string()), + ..Default::default() + }), + ) + .expect("custom cursor harness"); + + assert_eq!(harness.binary, None); + assert_eq!( + harness.binaries, + vec!["cursor-agent".to_string(), "agent".to_string()] + ); + } + + #[cfg(unix)] + #[test] + fn resolve_harness_command_skips_non_executable_search_path_candidates() { + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().expect("temp dir"); + let non_executable = temp.path().join("relay-non-executable"); + let executable = temp.path().join("relay-executable"); + fs::write(&non_executable, "#!/bin/sh\n").expect("write non-executable"); + fs::write(&executable, "#!/bin/sh\n").expect("write executable"); + fs::set_permissions(&non_executable, fs::Permissions::from_mode(0o644)) + .expect("chmod non-executable"); + fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)) + .expect("chmod executable"); + + let harness = HarnessDefinition { + binaries: vec![ + "relay-non-executable".to_string(), + "relay-executable".to_string(), + ], + search_paths: vec![temp.path().to_string_lossy().to_string()], + ..Default::default() + }; + + assert_eq!( + resolve_harness_command("fallback", Some(&harness)), + canonicalize_display(&executable) + ); + } + + #[test] + fn harness_interactive_args_expand_vector_placeholders() { + let harness = HarnessDefinition { + interactive_args: vec![ + "run".to_string(), + "{bypass}".to_string(), + "{modelArgs}".to_string(), + "{args}".to_string(), + "{sessionArgs}".to_string(), + ], + ..Default::default() + }; + + let args = render_harness_interactive_args( + &harness, + Some("--yes"), + Some("qwen3-coder"), + &["-m".to_string(), "qwen3-coder".to_string()], + &[], + &["--verbose".to_string()], + &["resume".to_string(), "session-1".to_string()], + ); + + assert_eq!( + args, + vec![ + "run", + "--yes", + "-m", + "qwen3-coder", + "--verbose", + "resume", + "session-1" + ] + ); + } + + #[test] + fn harness_model_args_dedup_custom_model_flag() { + let harness = HarnessDefinition { + model_args: vec!["--model-id".to_string(), "{model}".to_string()], + ..Default::default() + }; + + assert!(harness_model_override_present( + &harness, + &["--model-id".to_string(), "existing".to_string()] + )); + assert!(harness_model_override_present( + &harness, + &["--model-id=existing".to_string()] + )); + assert!(!harness_model_override_present( + &harness, + &["--other".to_string()] + )); + } + + #[test] + fn harness_bypass_uses_aliases_for_dedup() { + let harness = HarnessDefinition { + bypass_flag: Some("--yes".to_string()), + bypass_aliases: vec!["-y".to_string()], + ..Default::default() + }; + + assert_eq!( + harness_bypass_flag(&harness, &["--verbose".to_string()]), + Some("--yes".to_string()) + ); + assert_eq!(harness_bypass_flag(&harness, &["-y".to_string()]), None); + } +} + #[cfg(test)] mod tests { use super::*; @@ -1057,6 +2004,81 @@ mod tests { assert!(reg.routing_workers().is_empty()); } + #[test] + fn prepare_claude_session_args_generates_uuid_session_id() { + let mut args = Vec::new(); + let session_id = prepare_claude_session_args(&mut args).expect("session id"); + + assert!(uuid::Uuid::parse_str(&session_id).is_ok()); + assert_eq!(args, vec!["--session-id".to_string(), session_id]); + } + + #[test] + fn prepare_claude_session_args_preserves_explicit_session_id() { + let mut args = vec![ + "--session-id".to_string(), + "session-1".to_string(), + "--print".to_string(), + ]; + let session_id = prepare_claude_session_args(&mut args); + + assert_eq!(session_id.as_deref(), Some("session-1")); + assert_eq!( + args, + vec![ + "--session-id".to_string(), + "session-1".to_string(), + "--print".to_string(), + ] + ); + } + + #[test] + fn prepare_claude_session_args_uses_resume_id_without_injecting() { + let mut args = vec!["--resume=session-2".to_string()]; + let session_id = prepare_claude_session_args(&mut args); + + assert_eq!(session_id.as_deref(), Some("session-2")); + assert_eq!(args, vec!["--resume=session-2".to_string()]); + } + + #[test] + fn codex_session_reference_detects_resume_and_fork_ids() { + assert_eq!( + codex_session_reference(&[ + "--model".into(), + "gpt-5.4".into(), + "resume".into(), + "thread-1".into() + ]), + CodexSessionReference::Known("thread-1".to_string()) + ); + assert_eq!( + codex_session_reference(&["fork".into(), "thread-2".into()]), + CodexSessionReference::Known("thread-2".to_string()) + ); + assert_eq!( + codex_session_reference(&["resume".into(), "--last".into()]), + CodexSessionReference::Unknown + ); + } + + #[test] + fn codex_has_positional_arg_ignores_known_global_flag_values() { + assert!(!codex_has_positional_arg(&[ + "--model".into(), + "gpt-5.4".into(), + "--config".into(), + "model_provider=default".into(), + ])); + assert!(codex_has_positional_arg(&[ + "--model".into(), + "gpt-5.4".into(), + "Fix the bug".into(), + ])); + assert!(codex_has_positional_arg(&["exec".into()])); + } + #[test] fn args_include_model_override_detects_supported_forms() { assert!(args_include_model_override(&[ diff --git a/crates/broker/src/wrap.rs b/crates/broker/src/wrap.rs index 0aeef9163..3006e2e6c 100644 --- a/crates/broker/src/wrap.rs +++ b/crates/broker/src/wrap.rs @@ -599,7 +599,6 @@ pub(crate) async fn run_wrap( strict_name, agent_type: None, read_mcp_identity: true, - ensure_mcp_config: true, runtime_cwd: &runtime_cwd, }) .await?; diff --git a/docs/cli-command-tree.md b/docs/cli-command-tree.md new file mode 100644 index 000000000..0e0d50ae2 --- /dev/null +++ b/docs/cli-command-tree.md @@ -0,0 +1,236 @@ +# CLI Command Tree + +Generated from the source command registrations on 2026-05-24. Treat source as +canonical in this working tree: local `dist/` is stale and does not include the +newer `registerLogCommands()` registration. + +## Published Entrypoints + +| Command | Package | Package path | Source owner | +| --- | --- | --- | --- | +| `agent-relay` | `agent-relay` | `package.json` -> `dist/src/cli/index.js` | `src/cli/index.ts`, `src/cli/bootstrap.ts` | +| `relay` | `agent-relay` | `package.json` -> `dist/src/cli/index.js` | `src/cli/index.ts`, `src/cli/bootstrap.ts` | +| `relay-openclaw` | `@agent-relay/openclaw` | `packages/openclaw/package.json` -> `packages/openclaw/bin/relay-openclaw.mjs` | `packages/openclaw/src/cli.ts` | +| `relay-acp` | `@agent-relay/acp-bridge` | `packages/acp-bridge/package.json` -> `dist/cli.js` | `packages/acp-bridge/src/cli.ts` | +| `agent-relay-browser-mcp` | `@agent-relay/browser-primitive` | `packages/browser-primitive/package.json` -> `dist/mcp-server.js` | `packages/browser-primitive/src/mcp-server.ts` | +| `agent-relay-broker` | Rust broker binary, resolved by SDK/platform packages | `packages/sdk/bin/agent-relay-broker` in dev; optional `@agent-relay/broker-*` packages in installs | `crates/broker/src/main.rs`, `crates/broker/src/cli/mod.rs` | + +## `agent-relay` + +Root package bin: `package.json` -> `dist/src/cli/index.js`. + +Source registration root: `src/cli/bootstrap.ts`. + +```text +agent-relay +|-- up src/cli/commands/core.ts +|-- start [cli] src/cli/commands/core.ts +|-- down src/cli/commands/core.ts +|-- status src/cli/commands/core.ts +|-- uninstall src/cli/commands/core.ts +|-- version src/cli/commands/core.ts (hidden) +|-- update src/cli/commands/core.ts +|-- bridge [projects...] src/cli/commands/core.ts +|-- workflows src/cli/commands/core.ts +| `-- list src/cli/commands/core.ts +| +|-- spawn [task] src/cli/commands/agent-management.ts +|-- broker-spawn src/cli/commands/agent-management.ts (hidden) +|-- agents src/cli/commands/agent-management.ts (hidden) +|-- who src/cli/commands/agent-management.ts +|-- agents:logs src/cli/commands/agent-management.ts +|-- release src/cli/commands/agent-management.ts +|-- set-model src/cli/commands/agent-management.ts +|-- agents:kill src/cli/commands/agent-management.ts (hidden) +| +|-- send src/cli/commands/messaging.ts +|-- read src/cli/commands/messaging.ts (hidden) +|-- history src/cli/commands/messaging.ts +|-- inbox src/cli/commands/messaging.ts +|-- replies src/cli/commands/messaging.ts +| +|-- cloud src/cli/commands/cloud.ts +| |-- login src/cli/commands/cloud.ts +| |-- logout src/cli/commands/cloud.ts +| |-- whoami src/cli/commands/cloud.ts +| |-- connect src/cli/commands/cloud.ts +| |-- run src/cli/commands/cloud.ts +| |-- schedule src/cli/commands/cloud.ts +| |-- schedules src/cli/commands/cloud.ts +| |-- status src/cli/commands/cloud.ts +| |-- logs src/cli/commands/cloud.ts +| |-- sync src/cli/commands/cloud.ts +| `-- cancel src/cli/commands/cloud.ts +| +|-- login src/cli/commands/proactive-bootstrap.ts +|-- init src/cli/commands/proactive-bootstrap.ts +|-- workspaces src/cli/commands/proactive-bootstrap.ts +| `-- create src/cli/commands/proactive-bootstrap.ts +|-- tokens src/cli/commands/proactive-bootstrap.ts +| `-- issue src/cli/commands/proactive-bootstrap.ts +| +|-- metrics src/cli/commands/monitoring.ts (hidden) +|-- health src/cli/commands/monitoring.ts +|-- profile src/cli/commands/monitoring.ts (hidden) +| +|-- auth src/cli/commands/auth.ts +|-- setup src/cli/commands/setup.ts (hidden) +|-- telemetry [action] src/cli/commands/setup.ts +|-- run src/cli/commands/setup.ts +|-- swarm src/cli/commands/swarm.ts +|-- on [cli] src/cli/commands/on.ts +|-- off src/cli/commands/on.ts +|-- connect src/cli/commands/connect.ts (deprecated) +| +|-- dlq src/cli/commands/dlq.ts +| |-- list src/cli/commands/dlq.ts +| |-- inspect src/cli/commands/dlq.ts +| |-- replay [event-id] src/cli/commands/dlq.ts +| `-- purge src/cli/commands/dlq.ts +| +|-- view src/cli/commands/view.ts +|-- activity src/cli/commands/activity.ts +|-- drive src/cli/commands/drive.ts +|-- passthrough src/cli/commands/passthrough.ts +|-- new [args...] src/cli/commands/new.ts +|-- rm src/cli/commands/rm.ts +| +`-- log src/cli/commands/log.ts + |-- path src/cli/commands/log.ts + |-- list [brokerId] src/cli/commands/log.ts + |-- view src/cli/commands/log.ts + |-- rotate src/cli/commands/log.ts + `-- clear src/cli/commands/log.ts +``` + +There is also a pre-Commander shorthand in `src/cli/bootstrap.ts`: + +```text +agent-relay -n NAME CLI [args...] +`-- dispatches like: agent-relay new NAME CLI --attach --mode passthrough --ephemeral +``` + +## `relay` + +`relay` is published as a second bin for the same CLI entrypoint, but it is not +currently just an alias. `src/cli/bootstrap.ts` passes the resolved program name +into `createProgram()`, and `src/cli/commands/relay-runtime.ts` only registers +its proactive-runtime commands when `program.name() === "relay"`. + +Current blocker: the source registration order registers +`src/cli/commands/proactive-bootstrap.ts` first, which creates `init `. +Then `src/cli/commands/relay-runtime.ts` tries to create `init [name]`. Commander +rejects the duplicate command, so constructing `createProgram({ name: "relay" })` +throws before the `relay` command tree can be used. + +Intended `relay`-only additions: + +```text +relay +|-- init [name] src/cli/commands/relay-runtime.ts +|-- deploy src/cli/commands/relay-runtime.ts +|-- logs src/cli/commands/relay-runtime.ts +| +|-- agents src/cli/commands/relay-runtime.ts +| |-- list src/cli/commands/relay-runtime.ts +| |-- inspect src/cli/commands/relay-runtime.ts +| `-- undeploy src/cli/commands/relay-runtime.ts +| +`-- secrets src/cli/commands/relay-runtime.ts + |-- create src/cli/commands/relay-runtime.ts + |-- get src/cli/commands/relay-runtime.ts + `-- delete src/cli/commands/relay-runtime.ts +``` + +## `agent-relay-broker` + +Rust entrypoint: `crates/broker/src/main.rs`. + +Clap command tree: `crates/broker/src/cli/mod.rs`. + +```text +agent-relay-broker +|-- init crates/broker/src/cli/mod.rs +|-- pty -- [args...] crates/broker/src/cli/mod.rs +|-- headless -- [args...] crates/broker/src/cli/mod.rs +|-- mcp-args crates/broker/src/cli/mod.rs +|-- swarm crates/broker/src/swarm.rs +|-- dump-pty crates/broker/src/cli/mod.rs +`-- wrap [args...] crates/broker/src/cli/mod.rs (hidden/internal) +``` + +Note: `crates/broker/src/config.rs` also defines a Clap parser for a legacy +flat `agent-relay-broker [args...]` shape, but `crates/broker/src/lib.rs` +routes the binary through `cli::run()`, so `crates/broker/src/cli/mod.rs` is the +active command tree. + +## `relay-openclaw` + +Package bin: `packages/openclaw/package.json`. + +Runtime shim: `packages/openclaw/bin/relay-openclaw.mjs`. + +Manual parser: `packages/openclaw/src/cli.ts`. + +```text +relay-openclaw +|-- setup [key] packages/openclaw/src/cli.ts +|-- gateway packages/openclaw/src/cli.ts +|-- status packages/openclaw/src/cli.ts +|-- spawn packages/openclaw/src/cli.ts +|-- list packages/openclaw/src/cli.ts +|-- release packages/openclaw/src/cli.ts +|-- mcp-server packages/openclaw/src/cli.ts +|-- add-workspace [key] packages/openclaw/src/cli.ts +|-- list-workspaces packages/openclaw/src/cli.ts +|-- switch-workspace packages/openclaw/src/cli.ts +|-- runtime-setup packages/openclaw/src/cli.ts +|-- help packages/openclaw/src/cli.ts +`-- --version | -v | version packages/openclaw/src/cli.ts +``` + +## `relay-acp` + +Package bin: `packages/acp-bridge/package.json`. + +Manual parser: `packages/acp-bridge/src/cli.ts`. + +```text +relay-acp [options] +|-- --name packages/acp-bridge/src/cli.ts +|-- --socket packages/acp-bridge/src/cli.ts +|-- --debug packages/acp-bridge/src/cli.ts +|-- --help | -h packages/acp-bridge/src/cli.ts +`-- --version | -v packages/acp-bridge/src/cli.ts +``` + +## `agent-relay-browser-mcp` + +Package bin: `packages/browser-primitive/package.json`. + +MCP JSON-RPC stdio server: `packages/browser-primitive/src/mcp-server.ts`. + +This is a machine-facing MCP server rather than a human command tree. Its +runtime surface is JSON-RPC methods and MCP tools inside +`BrowserMcpServer.dispatch()`. + +## Cleanup Targets + +1. `relay` currently collides on `init`: `proactive-bootstrap.ts` registers + `init `, then `relay-runtime.ts` registers `init [name]`. +2. `relay` is semantically different from `agent-relay` even though both bins + point to the same entrypoint. Decide whether it should stay a distinct + product surface or become a true alias. +3. `agents` is overloaded: `agent-management.ts` owns a hidden root `agents` + command for local spawned workers, while `relay-runtime.ts` unhides/reuses it + for deployed proactive agents. +4. `agents:logs` and `agents:kill` coexist with an `agents` group. If the CLI is + cleaned up, `agents logs` and `agents kill` would be a more consistent tree. +5. Cloud/proactive concepts are spread across root (`login`, `init`, + `workspaces`, `tokens`), `cloud *`, and intended `relay *` commands. +6. `connect ` is deprecated but still visible at the root. +7. Hidden/internal commands should be reviewed as an explicit policy set: + `version`, `broker-spawn`, `agents`, `agents:kill`, `read`, `metrics`, + `profile`, and `setup`. +8. `src/cli/commands/doctor.ts` exists but is not registered by + `src/cli/bootstrap.ts`; `on --doctor` uses `src/cli/commands/on/prereqs.ts`. diff --git a/package-lock.json b/package-lock.json index 6caed516a..463c0c05b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "@agent-relay/utils": "7.1.0", "@modelcontextprotocol/sdk": "^1.0.0", "@relayauth/core": "^0.1.2", - "@relaycast/mcp": "1.0.0", "@relaycast/sdk": "^1.1.0", "@relayfile/local-mount": "^0.2.2", "@relayfile/sdk": "^0.6.0", @@ -3931,39 +3930,6 @@ "resolved": "https://registry.npmjs.org/@relayauth/types/-/types-0.1.9.tgz", "integrity": "sha512-bGLjsnUeA+6PbjmWv5U8oDOpgDzF6FDMh6omVtZ5iDU1/iO935PzuQKq9DDUpleB+lC7XbYceb6f18LGXcFTQg==" }, - "node_modules/@relaycast/mcp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@relaycast/mcp/-/mcp-1.0.0.tgz", - "integrity": "sha512-TN5U5ufxxVIFMP7IpoGxVcdlBDQ/59rJUS8dYOiASIeCs7VdQoXHDni/8iZZ52yhsbMFeD/Pk97AXJilFzaBhQ==", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0", - "@relaycast/sdk": "1.0.0", - "@relaycast/types": "1.0.0", - "express": "^5.2.1", - "zod": "^4.3.6" - }, - "bin": { - "relaycast-mcp": "dist/stdio.js" - } - }, - "node_modules/@relaycast/mcp/node_modules/@relaycast/sdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-1.0.0.tgz", - "integrity": "sha512-s01xslec5xyDXxxkVDTJyHpRhzqlXC2gVoglvhu+HK1h5JeOKq13AFlhe2MszkxjJAQ0HJ36MItWXuGogbRdOg==", - "dependencies": { - "@relaycast/types": "1.0.0", - "zod": "^4.3.6" - } - }, - "node_modules/@relaycast/mcp/node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@relaycast/sdk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-1.1.3.tgz", diff --git a/package.json b/package.json index 7a4f0387e..1b8b95877 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,10 @@ "import": "./dist/src/index.js", "default": "./dist/index.cjs" }, + "./mcp": { + "types": "./dist/src/cli/relaycast-mcp.d.ts", + "import": "./dist/src/cli/relaycast-mcp.js" + }, "./package.json": "./package.json" }, "bin": { @@ -141,7 +145,6 @@ "@agent-relay/utils": "7.1.1", "@modelcontextprotocol/sdk": "^1.0.0", "@relayauth/core": "^0.1.2", - "@relaycast/mcp": "1.0.0", "@relaycast/sdk": "^1.1.0", "@relayfile/local-mount": "^0.2.2", "@relayfile/sdk": "^0.6.0", diff --git a/packages/openclaw/skill/SKILL.md b/packages/openclaw/skill/SKILL.md index fe0b97367..ff4546fbe 100644 --- a/packages/openclaw/skill/SKILL.md +++ b/packages/openclaw/skill/SKILL.md @@ -361,7 +361,7 @@ This usually means missing/cleared `RELAY_AGENT_TOKEN` in mcporter config. - **Important:** Re-running `setup` or `register` with an existing agent name does **not** return a new token — it only says "already exists." The token from the original registration is the only valid one. - To get a fresh token, you must register with a **new agent name** (e.g. `my-claw-v2`) via `mcporter call relaycast.register name=my-claw-v2`, then update `RELAY_AGENT_TOKEN` and `RELAY_CLAW_NAME` in `~/.mcporter/mcporter.json` -- After updating the token, kill any stale MCP server processes (`pkill -f "@relaycast/mcp"`) so mcporter starts a fresh one with the new token +- After updating the token, kill any stale MCP server processes (`pkill -f "agent-relay.*mcp"`) so mcporter starts a fresh one with the new token - retry `post_message` / `check_inbox` --- @@ -512,7 +512,7 @@ Confirm what appears auto-injected in your UI stream: | Polling works, injection fails | local WS auth/topology issue | run full recovery runbook above | | Setup succeeds but no MCP tools | `mcporter` missing from PATH | install/verify `mcporter`, re-run setup | | `Not registered` in mcporter calls | missing/cleared `RELAY_AGENT_TOKEN` | restore token in `~/.mcporter/mcporter.json` and retry | -| `Invalid agent token` in mcporter calls while `list_agents` still works | MCP has a stale/invalid per-agent token; workspace auth is still OK | Re-run setup with the **same** claw name first. If it still fails, inspect `~/.mcporter/mcporter.json`, kill stale MCP processes (`pkill -f "@relaycast/mcp"`), and only then consider registering a new claw name. | +| `Invalid agent token` in mcporter calls while `list_agents` still works | MCP has a stale/invalid per-agent token; workspace auth is still OK | Re-run setup with the **same** claw name first. If it still fails, inspect `~/.mcporter/mcporter.json`, kill stale MCP processes (`pkill -f "agent-relay.*mcp"`), and only then consider registering a new claw name. | | Gateway doesn't auto-recover after approval | older version or retry not triggered | upgrade to `@agent-relay/openclaw@latest` (3.1.6+); if still stuck, restart gateway manually (see Step 2) | ### Hardening recommendations diff --git a/packages/openclaw/src/setup.ts b/packages/openclaw/src/setup.ts index 5a1277fe8..7db39c00f 100644 --- a/packages/openclaw/src/setup.ts +++ b/packages/openclaw/src/setup.ts @@ -9,7 +9,13 @@ import { randomBytes } from 'node:crypto'; import { RelayCast } from '@relaycast/sdk'; -import { detectOpenClaw, saveGatewayConfig, addWorkspace, loadWorkspacesConfig, buildWorkspacesJson } from './config.js'; +import { + detectOpenClaw, + saveGatewayConfig, + addWorkspace, + loadWorkspacesConfig, + buildWorkspacesJson, +} from './config.js'; import { InboundGateway } from './gateway.js'; import { DEFAULT_OPENCLAW_GATEWAY_PORT, type GatewayConfig } from './types.js'; @@ -147,8 +153,7 @@ export async function setup(options: SetupOptions): Promise { apiKey: '', clawName, skillDir: '', - message: - 'OpenClaw not found. Please install OpenClaw first (expected ~/.openclaw/ directory).', + message: 'OpenClaw not found. Please install OpenClaw first (expected ~/.openclaw/ directory).', }; } } @@ -159,10 +164,7 @@ export async function setup(options: SetupOptions): Promise { // If all CLI calls fail, mutate the config JSON directly. let configMutated = false; { - const httpEndpointArgs = [ - 'config', 'set', - 'gateway.http.endpoints.responses.enabled', 'true', - ]; + const httpEndpointArgs = ['config', 'set', 'gateway.http.endpoints.responses.enabled', 'true']; const cliCandidates = ['openclaw', 'clawdbot', 'clawdbot-cli.sh']; let cliSuccess = false; @@ -212,13 +214,17 @@ export async function setup(options: SetupOptions): Promise { if (res.status === 409) { // Workspace already exists — look up its API key - const lookupRes = await fetch(`${baseUrl}/v1/workspaces/by-name/${encodeURIComponent(`${clawName}-workspace`)}`, { - headers: { 'Content-Type': 'application/json' }, - }); + const lookupRes = await fetch( + `${baseUrl}/v1/workspaces/by-name/${encodeURIComponent(`${clawName}-workspace`)}`, + { + headers: { 'Content-Type': 'application/json' }, + } + ); if (lookupRes.ok) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const lookupBody = (await lookupRes.json()) as any; - apiKey = lookupBody.apiKey ?? lookupBody.api_key ?? lookupBody.data?.apiKey ?? lookupBody.data?.api_key; + apiKey = + lookupBody.apiKey ?? lookupBody.api_key ?? lookupBody.data?.apiKey ?? lookupBody.data?.api_key; } if (!apiKey) { return { @@ -241,7 +247,8 @@ export async function setup(options: SetupOptions): Promise { } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any const successBody = (await res.json()) as any; - apiKey = successBody.apiKey ?? successBody.api_key ?? successBody.data?.apiKey ?? successBody.data?.api_key; + apiKey = + successBody.apiKey ?? successBody.api_key ?? successBody.data?.apiKey ?? successBody.data?.api_key; } if (!apiKey) { @@ -276,11 +283,7 @@ export async function setup(options: SetupOptions): Promise { await copyFile(skillSrc, join(skillDir, 'SKILL.md')); } else { // Write a minimal SKILL.md inline if the bundled one isn't found - await writeFile( - join(skillDir, 'SKILL.md'), - FALLBACK_SKILL_MD, - 'utf-8', - ); + await writeFile(join(skillDir, 'SKILL.md'), FALLBACK_SKILL_MD, 'utf-8'); } // Extract gateway auth from config (if available). Auto-generate if missing. @@ -308,7 +311,9 @@ export async function setup(options: SetupOptions): Promise { detection.config = cfg; configMutated = true; } catch (writeErr) { - console.warn(`[setup] Could not write generated token to config file: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}`); + console.warn( + `[setup] Could not write generated token to config file: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}` + ); } } else { console.warn('[setup] No config file available to persist generated token. Set manually:'); @@ -368,13 +373,10 @@ export async function setup(options: SetupOptions): Promise { const workspacesJson = wsConfig ? buildWorkspacesJson(wsConfig) : null; const envArgs = [ - '--env', `RELAY_API_KEY=${apiKey}`, - ...(baseUrl !== 'https://api.relaycast.dev' - ? ['--env', `RELAY_BASE_URL=${baseUrl}`] - : []), - ...(workspacesJson - ? ['--env', `RELAY_WORKSPACES_JSON=${workspacesJson}`] - : []), + '--env', + `RELAY_API_KEY=${apiKey}`, + ...(baseUrl !== 'https://api.relaycast.dev' ? ['--env', `RELAY_BASE_URL=${baseUrl}`] : []), + ...(workspacesJson ? ['--env', `RELAY_WORKSPACES_JSON=${workspacesJson}`] : []), ...(wsConfig?.default_workspace ? ['--env', `RELAY_DEFAULT_WORKSPACE=${wsConfig.default_workspace}`] : []), @@ -393,27 +395,54 @@ export async function setup(options: SetupOptions): Promise { if (mcp) { try { // Register relaycast messaging MCP server - execFileSync(mcp.cmd, [ - ...mcp.prefix, - 'config', 'add', 'relaycast', - '--command', 'npx', - '--arg', '@relaycast/mcp', - ...envArgs, - '--scope', 'home', - '--description', 'Relaycast messaging MCP server', - ], { stdio: 'pipe' }); + execFileSync( + mcp.cmd, + [ + ...mcp.prefix, + 'config', + 'add', + 'relaycast', + '--command', + 'npx', + '--arg', + '-y', + '--arg', + 'agent-relay', + '--arg', + 'mcp', + ...envArgs, + '--scope', + 'home', + '--description', + 'Relaycast messaging MCP server', + ], + { stdio: 'pipe' } + ); // Register openclaw-spawner MCP server - execFileSync(mcp.cmd, [ - ...mcp.prefix, - 'config', 'add', 'openclaw-spawner', - '--command', 'npx', - '--arg', '@agent-relay/openclaw', - '--arg', 'mcp-server', - ...envArgs, - '--scope', 'home', - '--description', 'OpenClaw spawner MCP server', - ], { stdio: 'pipe' }); + execFileSync( + mcp.cmd, + [ + ...mcp.prefix, + 'config', + 'add', + 'openclaw-spawner', + '--command', + 'npx', + '--arg', + '-y', + '--arg', + '@agent-relay/openclaw', + '--arg', + 'mcp-server', + ...envArgs, + '--scope', + 'home', + '--description', + 'OpenClaw spawner MCP server', + ], + { stdio: 'pipe' } + ); mcpConfigured = true; @@ -438,25 +467,44 @@ export async function setup(options: SetupOptions): Promise { // Reconfigure mcporter with the agent token so subsequent calls are authenticated try { execFileSync(mcp.cmd, [...mcp.prefix, 'config', 'remove', 'relaycast'], { stdio: 'pipe' }); - } catch { /* may not exist */ } - - execFileSync(mcp.cmd, [ - ...mcp.prefix, - 'config', 'add', 'relaycast', - '--command', 'npx', - '--arg', '@relaycast/mcp', - ...envArgs, - '--env', `RELAY_AGENT_TOKEN=${agentToken}`, - '--scope', 'home', - '--description', 'Relaycast messaging MCP server', - ], { stdio: 'pipe' }); + } catch { + /* may not exist */ + } + + execFileSync( + mcp.cmd, + [ + ...mcp.prefix, + 'config', + 'add', + 'relaycast', + '--command', + 'npx', + '--arg', + '-y', + '--arg', + 'agent-relay', + '--arg', + 'mcp', + ...envArgs, + '--env', + `RELAY_AGENT_TOKEN=${agentToken}`, + '--scope', + 'home', + '--description', + 'Relaycast messaging MCP server', + ], + { stdio: 'pipe' } + ); console.log(`Agent "${clawName}" registered with token.`); } else { console.warn('Agent registered but no token found in response.'); } } catch (regErr) { - console.warn(`Agent registration failed (non-fatal): ${regErr instanceof Error ? regErr.message : String(regErr)}`); + console.warn( + `Agent registration failed (non-fatal): ${regErr instanceof Error ? regErr.message : String(regErr)}` + ); } } catch (err) { console.warn(`mcporter configuration failed: ${err instanceof Error ? err.message : String(err)}`); @@ -478,7 +526,7 @@ export async function setup(options: SetupOptions): Promise { } else { try { const gatewayEnv: Record = { - ...process.env as Record, + ...(process.env as Record), RELAY_API_KEY: apiKey, RELAY_CLAW_NAME: clawName, RELAY_BASE_URL: baseUrl, diff --git a/packages/sdk-py/src/agent_relay/__init__.py b/packages/sdk-py/src/agent_relay/__init__.py index 068291f24..8df671be8 100644 --- a/packages/sdk-py/src/agent_relay/__init__.py +++ b/packages/sdk-py/src/agent_relay/__init__.py @@ -63,9 +63,11 @@ SwarmPattern, AgentDefinition, AgentConstraints, + HarnessDefinition, PathDefinition, RelayYamlConfig, AgentCli, + KnownAgentCli, IdleNudgeConfig, StateConfig, ErrorHandlingConfig, @@ -131,9 +133,11 @@ "SwarmPattern", "AgentDefinition", "AgentConstraints", + "HarnessDefinition", "PathDefinition", "RelayYamlConfig", "AgentCli", + "KnownAgentCli", "IdleNudgeConfig", "StateConfig", "ErrorHandlingConfig", diff --git a/packages/sdk-py/src/agent_relay/builder.py b/packages/sdk-py/src/agent_relay/builder.py index 8ae41bdd2..35f097153 100644 --- a/packages/sdk-py/src/agent_relay/builder.py +++ b/packages/sdk-py/src/agent_relay/builder.py @@ -36,6 +36,7 @@ ConsensusStrategy, ErrorOptions, ErrorStrategy, + HarnessDefinition, IdleNudgeConfig, RunCancelledEvent, RunCompletedEvent, @@ -82,6 +83,7 @@ def __init__(self, name: str) -> None: self._timeout_ms: int | None = None self._channel: str | None = None self._idle_nudge: dict[str, Any] | None = None + self._harnesses: dict[str, dict[str, Any]] | None = None self._agents: list[dict[str, Any]] = [] self._steps: list[dict[str, Any]] = [] self._error_handling: dict[str, Any] | None = None @@ -129,6 +131,22 @@ def idle_nudge( ).to_dict() return self + def harness( + self, + name: str, + definition: HarnessDefinition | dict[str, Any], + ) -> WorkflowBuilder: + """Register a workflow-local harness adapter.""" + key = name.strip() + if not key: + raise ValueError("harness name must be non-empty") + if isinstance(definition, HarnessDefinition): + serialized = definition.to_dict() + else: + serialized = copy.deepcopy(dict(definition)) + self._harnesses = {**(self._harnesses or {}), key: serialized} + return self + def coordination( self, *, @@ -373,6 +391,8 @@ def to_config(self) -> dict[str, Any]: if self._description is not None: config["description"] = self._description + if self._harnesses: + config["harnesses"] = copy.deepcopy(self._harnesses) if self._error_handling is not None: config["errorHandling"] = dict(self._error_handling) if self._coordination is not None: diff --git a/packages/sdk-py/src/agent_relay/client.py b/packages/sdk-py/src/agent_relay/client.py index c3ef41eb4..a79721966 100644 --- a/packages/sdk-py/src/agent_relay/client.py +++ b/packages/sdk-py/src/agent_relay/client.py @@ -331,6 +331,7 @@ async def spawn_pty( channels: Optional[list[str]] = None, task: Optional[str] = None, model: Optional[str] = None, + harness: Optional[dict[str, Any]] = None, cwd: Optional[str] = None, team: Optional[str] = None, shadow_of: Optional[str] = None, @@ -346,16 +347,28 @@ async def spawn_pty( "args": args or [], "channels": channels or [], } - if task is not None: payload["task"] = task - if model is not None: payload["model"] = model - if cwd is not None: payload["cwd"] = cwd - if team is not None: payload["team"] = team - if shadow_of is not None: payload["shadowOf"] = shadow_of - if shadow_mode is not None: payload["shadowMode"] = shadow_mode - if idle_threshold_secs is not None: payload["idleThresholdSecs"] = idle_threshold_secs - if restart_policy is not None: payload["restartPolicy"] = restart_policy - if continue_from is not None: payload["continueFrom"] = continue_from - if skip_relay_prompt is not None: payload["skipRelayPrompt"] = skip_relay_prompt + if task is not None: + payload["task"] = task + if model is not None: + payload["model"] = model + if harness is not None: + payload["harness"] = harness + if cwd is not None: + payload["cwd"] = cwd + if team is not None: + payload["team"] = team + if shadow_of is not None: + payload["shadowOf"] = shadow_of + if shadow_mode is not None: + payload["shadowMode"] = shadow_mode + if idle_threshold_secs is not None: + payload["idleThresholdSecs"] = idle_threshold_secs + if restart_policy is not None: + payload["restartPolicy"] = restart_policy + if continue_from is not None: + payload["continueFrom"] = continue_from + if skip_relay_prompt is not None: + payload["skipRelayPrompt"] = skip_relay_prompt return await self._request("POST", "/api/spawn", json=payload) async def spawn_provider( @@ -368,6 +381,7 @@ async def spawn_provider( channels: Optional[list[str]] = None, task: Optional[str] = None, model: Optional[str] = None, + harness: Optional[dict[str, Any]] = None, cwd: Optional[str] = None, team: Optional[str] = None, shadow_of: Optional[str] = None, @@ -392,6 +406,7 @@ async def spawn_provider( } if task is not None: payload["task"] = task if model is not None: payload["model"] = model + if harness is not None: payload["harness"] = harness if cwd is not None: payload["cwd"] = cwd if team is not None: payload["team"] = team if shadow_of is not None: payload["shadowOf"] = shadow_of diff --git a/packages/sdk-py/src/agent_relay/relay.py b/packages/sdk-py/src/agent_relay/relay.py index 84dd477da..c9ed9a9b1 100644 --- a/packages/sdk-py/src/agent_relay/relay.py +++ b/packages/sdk-py/src/agent_relay/relay.py @@ -17,6 +17,7 @@ from .client import AgentRelayClient from .protocol import AgentRuntime, BrokerEvent, MessageInjectionMode +from .types import HarnessDefinition # ── Public types ────────────────────────────────────────────────────────────── @@ -26,6 +27,16 @@ LifecycleHook = Optional[Callable[[dict[str, Any]], None | Awaitable[None]]] +def _serialize_harness( + definition: Optional[HarnessDefinition | dict[str, Any]], +) -> Optional[dict[str, Any]]: + if definition is None: + return None + if isinstance(definition, HarnessDefinition): + return definition.to_dict() + return dict(definition) + + @dataclass class Message: """A relay message between agents.""" @@ -46,6 +57,7 @@ class SpawnOptions: args: list[str] = field(default_factory=list) channels: list[str] = field(default_factory=list) model: Optional[str] = None + harness: Optional[HarnessDefinition | dict[str, Any]] = None cwd: Optional[str] = None team: Optional[str] = None shadow_of: Optional[str] = None @@ -70,11 +82,13 @@ def __init__( runtime: AgentRuntime, channels: list[str], relay: AgentRelay, + session_id: Optional[str] = None, ): self._name = name self._runtime = runtime self._channels = channels self._relay = relay + self._session_id = session_id self.exit_code: Optional[int] = None self.exit_signal: Optional[str] = None self.exit_reason: Optional[str] = None @@ -87,6 +101,10 @@ def name(self) -> str: def runtime(self) -> AgentRuntime: return self._runtime + @property + def session_id(self) -> Optional[str]: + return self._session_id + @property def channels(self) -> list[str]: return self._channels @@ -312,6 +330,7 @@ async def spawn( channels: Optional[list[str]] = None, task: Optional[str] = None, model: Optional[str] = None, + harness: Optional[HarnessDefinition | dict[str, Any]] = None, cwd: Optional[str] = None, skip_relay_prompt: Optional[bool] = None, on_start: LifecycleHook = None, @@ -342,6 +361,7 @@ async def spawn( channels=agent_channels, task=task, model=model, + harness=self._relay._resolve_harness(self._cli, harness), cwd=cwd, skip_relay_prompt=skip_relay_prompt, ) @@ -361,6 +381,7 @@ async def spawn( runtime=result.get("runtime", "pty"), channels=agent_channels, relay=self._relay, + session_id=result.get("sessionId"), ) self._relay._known_agents[agent.name] = agent self._relay._reset_agent_lifecycle_state(agent.name) @@ -370,6 +391,7 @@ async def spawn( **context, "name": agent.name, "runtime": agent.runtime, + "sessionId": agent.session_id, }, f'spawn("{agent_name}") on_success', ) @@ -401,6 +423,7 @@ def __init__( channels: Optional[list[str]] = None, cwd: Optional[str] = None, env: Optional[dict[str, str]] = None, + harnesses: Optional[dict[str, HarnessDefinition | dict[str, Any]]] = None, request_timeout_ms: int = 10_000, shutdown_timeout_ms: int = 3_000, ): @@ -417,6 +440,11 @@ def __init__( self.on_agent_idle: EventHook = None self._default_channels = channels or ["general"] + self._harnesses: dict[str, dict[str, Any]] = { + name: serialized + for name, definition in (harnesses or {}).items() + if (serialized := _serialize_harness(definition)) is not None + } self._client_kwargs: dict[str, Any] = { "binary_path": binary_path, "binary_args": binary_args, @@ -446,6 +474,32 @@ def __init__( self.gemini = AgentSpawner("gemini", "Gemini", self, transport="pty") self.opencode = AgentSpawner("opencode", "OpenCode", self, transport="headless") + def register_harness( + self, + name: str, + definition: HarnessDefinition | dict[str, Any], + ) -> AgentRelay: + key = name.strip() + if not key: + raise ValueError("register_harness() expects a non-empty harness name") + serialized = _serialize_harness(definition) + if serialized is None: + raise ValueError("register_harness() expects a harness definition") + self._harnesses[key] = serialized + return self + + def _resolve_harness( + self, + cli: str, + explicit: Optional[HarnessDefinition | dict[str, Any]] = None, + ) -> Optional[dict[str, Any]]: + explicit_serialized = _serialize_harness(explicit) + if explicit_serialized is not None: + return explicit_serialized + base_cli = cli.strip().split(":", 1)[0] + harness = self._harnesses.get(base_cli) + return dict(harness) if harness is not None else None + @property def workspace_key(self) -> Optional[str]: return self._client.workspace_key if self._client else None @@ -516,6 +570,7 @@ async def spawn( args=opts.args, channels=channels, model=opts.model, + harness=self._resolve_harness(cli, opts.harness), cwd=opts.cwd, team=opts.team, shadow_of=opts.shadow_of, @@ -540,6 +595,7 @@ async def spawn( runtime=result.get("runtime", "pty"), channels=channels, relay=self, + session_id=result.get("sessionId"), ) self._known_agents[agent.name] = agent self._reset_agent_lifecycle_state(agent.name) @@ -549,6 +605,7 @@ async def spawn( **context, "name": agent.name, "runtime": agent.runtime, + "sessionId": agent.session_id, }, f'spawn("{name}") on_success', ) @@ -589,6 +646,9 @@ async def list_agents(self) -> list[Agent]: name = entry.get("name", "") existing = self._known_agents.get(name) if existing: + session_id = entry.get("sessionId") + if session_id: + existing._session_id = session_id agents.append(existing) else: agent = Agent( @@ -596,6 +656,7 @@ async def list_agents(self) -> list[Agent]: runtime=entry.get("runtime", "pty"), channels=entry.get("channels", []), relay=self, + session_id=entry.get("sessionId"), ) self._known_agents[name] = agent agents.append(agent) @@ -622,7 +683,11 @@ async def wait_for_agent_ready(self, name: str, timeout_ms: int = 60_000) -> Age def on_event(event: BrokerEvent) -> None: if event.get("kind") != "worker_ready" or event.get("name") != name: return - agent = self._ensure_agent_handle(name, event.get("runtime", "pty")) + agent = self._ensure_agent_handle( + name, + event.get("runtime", "pty"), + session_id=event.get("sessionId"), + ) self._ready_agents.add(name) self._exited_agents.discard(name) if not future.done(): @@ -751,12 +816,18 @@ def _reset_agent_lifecycle_state(self, name: str) -> None: self._idle_agents.discard(name) def _ensure_agent_handle( - self, name: str, runtime: AgentRuntime = "pty", channels: Optional[list[str]] = None, + self, + name: str, + runtime: AgentRuntime = "pty", + channels: Optional[list[str]] = None, + session_id: Optional[str] = None, ) -> Agent: existing = self._known_agents.get(name) if existing: + if session_id: + existing._session_id = session_id return existing - agent = Agent(name, runtime, channels or [], self) + agent = Agent(name, runtime, channels or [], self, session_id=session_id) self._known_agents[name] = agent return agent @@ -782,7 +853,11 @@ def on_event(event: BrokerEvent) -> None: self.on_message_received(msg) elif kind == "agent_spawned": - agent = self._ensure_agent_handle(name, event.get("runtime", "pty")) + agent = self._ensure_agent_handle( + name, + event.get("runtime", "pty"), + session_id=event.get("sessionId"), + ) self._ready_agents.discard(name) self._message_ready_agents.discard(name) self._exited_agents.discard(name) @@ -833,7 +908,11 @@ def on_event(event: BrokerEvent) -> None: self.on_agent_exit_requested({"name": name, "reason": event.get("reason", "")}) elif kind == "worker_ready": - agent = self._ensure_agent_handle(name, event.get("runtime", "pty")) + agent = self._ensure_agent_handle( + name, + event.get("runtime", "pty"), + session_id=event.get("sessionId"), + ) self._ready_agents.add(name) self._exited_agents.discard(name) self._idle_agents.discard(name) diff --git a/packages/sdk-py/src/agent_relay/types.py b/packages/sdk-py/src/agent_relay/types.py index fd47ce684..a1f63d708 100644 --- a/packages/sdk-py/src/agent_relay/types.py +++ b/packages/sdk-py/src/agent_relay/types.py @@ -32,7 +32,20 @@ "review-loop", ] -AgentCli = Literal["claude", "codex", "gemini", "aider", "goose", "opencode", "droid", "cursor", "cursor-agent", "agent"] +KnownAgentCli = Literal[ + "claude", + "codex", + "gemini", + "aider", + "goose", + "opencode", + "droid", + "cursor", + "cursor-agent", + "agent", + "api", +] +AgentCli: TypeAlias = str AgentStatus = Literal["healthy", "restarting", "dead", "released"] CrashCategory = Literal["oom", "segfault", "error", "signal", "unknown"] WorkflowOnError = Literal["fail", "skip", "retry"] @@ -181,6 +194,52 @@ def to_dict(self) -> dict[str, Any]: return result +@dataclass +class HarnessDefinition: + """Serializable harness adapter config for spawning and workflows.""" + + adapter: str | None = None + binary: str | None = None + binaries: list[str] | None = None + interactive_args: list[str] | None = None + non_interactive_args: list[str] | None = None + model_args: list[str] | None = None + bypass_flag: str | None = None + bypass_aliases: list[str] | None = None + search_paths: list[str] | None = None + ignore_exit_code: bool | None = None + proxy_provider: Literal["openai", "anthropic", "openrouter"] | None = None + aliases: list[str] | None = None + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {} + if self.adapter is not None: + result["adapter"] = self.adapter + if self.binary is not None: + result["binary"] = self.binary + if self.binaries is not None: + result["binaries"] = self.binaries + if self.interactive_args is not None: + result["interactiveArgs"] = self.interactive_args + if self.non_interactive_args is not None: + result["nonInteractiveArgs"] = self.non_interactive_args + if self.model_args is not None: + result["modelArgs"] = self.model_args + if self.bypass_flag is not None: + result["bypassFlag"] = self.bypass_flag + if self.bypass_aliases is not None: + result["bypassAliases"] = self.bypass_aliases + if self.search_paths is not None: + result["searchPaths"] = self.search_paths + if self.ignore_exit_code is not None: + result["ignoreExitCode"] = self.ignore_exit_code + if self.proxy_provider is not None: + result["proxyProvider"] = self.proxy_provider + if self.aliases is not None: + result["aliases"] = self.aliases + return result + + @dataclass class VerificationCheck: type: Literal["output_contains", "exit_code", "file_exists", "custom"] @@ -317,6 +376,7 @@ class RelayYamlConfig: version: str = "1.0" description: str | None = None paths: list[PathDefinition] | None = None + harnesses: dict[str, HarnessDefinition | dict[str, Any]] | None = None workflows: list[WorkflowDefinition] | None = None coordination: CoordinationConfig | None = None state: StateConfig | None = None @@ -334,6 +394,11 @@ def to_dict(self) -> dict[str, Any]: result["description"] = self.description if self.paths is not None: result["paths"] = [p.to_dict() for p in self.paths] + if self.harnesses is not None: + result["harnesses"] = { + name: harness.to_dict() if isinstance(harness, HarnessDefinition) else dict(harness) + for name, harness in self.harnesses.items() + } if self.workflows is not None: result["workflows"] = [workflow.to_dict() for workflow in self.workflows] if self.coordination is not None: diff --git a/packages/sdk-py/tests/test_builder.py b/packages/sdk-py/tests/test_builder.py index a9e3c45f0..4f3cc8fed 100644 --- a/packages/sdk-py/tests/test_builder.py +++ b/packages/sdk-py/tests/test_builder.py @@ -3,6 +3,7 @@ import yaml import pytest from agent_relay import ( + HarnessDefinition, PipelineStage, TemplateAgent, TemplateStep, @@ -107,6 +108,30 @@ def test_to_yaml_roundtrip(): assert parsed["workflows"][0]["steps"][0]["task"] == "Do something" +def test_builder_serializes_custom_harness(): + config = ( + workflow("custom-harness") + .harness( + "unit-harness", + HarnessDefinition( + binary="unit-agent", + non_interactive_args=["run", "{task}", "{args}"], + model_args=["-m", "{model}"], + ), + ) + .agent("worker", cli="unit-harness", interactive=False, model="model-1") + .step("work", agent="worker", task="Do work") + .to_config() + ) + + assert config["harnesses"]["unit-harness"] == { + "binary": "unit-agent", + "nonInteractiveArgs": ["run", "{task}", "{args}"], + "modelArgs": ["-m", "{model}"], + } + assert config["agents"][0]["cli"] == "unit-harness" + + def test_empty_agents_raises(): with pytest.raises(ValueError, match="at least one agent"): workflow("empty").pattern("dag").step("s", agent="a", task="x").to_config() diff --git a/packages/sdk-py/tests/test_relay_harness.py b/packages/sdk-py/tests/test_relay_harness.py new file mode 100644 index 000000000..249889352 --- /dev/null +++ b/packages/sdk-py/tests/test_relay_harness.py @@ -0,0 +1,28 @@ +"""Tests for AgentRelay spawn harness configuration.""" + +from agent_relay import AgentRelay, HarnessDefinition + + +def test_relay_resolves_constructor_and_registered_harnesses(): + relay = AgentRelay( + harnesses={ + "qwen": HarnessDefinition( + binary="qwen", + interactive_args=["run", "{modelArgs}", "{args}"], + model_args=["-m", "{model}"], + ) + } + ) + + assert relay._resolve_harness("qwen:coder") == { + "binary": "qwen", + "interactiveArgs": ["run", "{modelArgs}", "{args}"], + "modelArgs": ["-m", "{model}"], + } + + relay.register_harness("local", {"binary": "local-agent", "bypassFlag": "--yes"}) + + assert relay._resolve_harness("local") == { + "binary": "local-agent", + "bypassFlag": "--yes", + } diff --git a/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift b/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift index a947e04f8..a788bdef2 100644 --- a/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift +++ b/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift @@ -40,6 +40,7 @@ public struct AgentSpec: Codable, Sendable { public var channels: [String]? public var model: String? public var cwd: String? + public var sessionId: String? public var team: String? public var shadowOf: String? public var shadowMode: String? @@ -47,6 +48,7 @@ public struct AgentSpec: Codable, Sendable { enum CodingKeys: String, CodingKey { case name, runtime, provider, cli, args, channels, model, cwd, team + case sessionId = "session_id" case shadowOf = "shadow_of" case shadowMode = "shadow_mode" case restartPolicy = "restart_policy" @@ -61,6 +63,7 @@ public struct AgentSpec: Codable, Sendable { channels: [String]? = nil, model: String? = nil, cwd: String? = nil, + sessionId: String? = nil, team: String? = nil, shadowOf: String? = nil, shadowMode: String? = nil, @@ -74,6 +77,7 @@ public struct AgentSpec: Codable, Sendable { self.channels = channels self.model = model self.cwd = cwd + self.sessionId = sessionId self.team = team self.shadowOf = shadowOf self.shadowMode = shadowMode @@ -382,7 +386,7 @@ extension OutboundMessage: Encodable { } } -public struct AgentSpawnedEvent: Codable, Sendable { public var kind: String = "agent_spawned"; public var name: String; public var runtime: AgentRuntime; public var provider: HeadlessProvider?; public var cli: String?; public var model: String?; public var parent: String?; public var pid: Int?; public var source: String? } +public struct AgentSpawnedEvent: Codable, Sendable { public var kind: String = "agent_spawned"; public var name: String; public var runtime: AgentRuntime; public var provider: HeadlessProvider?; public var cli: String?; public var model: String?; public var sessionId: String?; public var parent: String?; public var pid: Int?; public var source: String?; enum CodingKeys: String, CodingKey { case kind, name, runtime, provider, cli, model, parent, pid, source; case sessionId = "session_id" } } public struct AgentReleasedEvent: Codable, Sendable { public var kind: String = "agent_released"; public var name: String } public struct AgentExitRequestedEvent: Codable, Sendable { public var kind: String = "agent_exit"; public var name: String; public var reason: String } public struct AgentExitedEvent: Codable, Sendable { public var kind: String = "agent_exited"; public var name: String; public var code: Int?; public var signal: String? } @@ -392,7 +396,7 @@ public struct DeliveryRetryEvent: Codable, Sendable { public var kind: String = public struct DeliveryDroppedEvent: Codable, Sendable { public var kind: String = "delivery_dropped"; public var name: String; public var count: Int; public var reason: String } public struct DeliveryStateEvent: Codable, Sendable { public var kind: String; public var name: String; public var deliveryId: String; public var eventId: String; enum CodingKeys: String, CodingKey { case kind, name; case deliveryId = "delivery_id"; case eventId = "event_id" } } public struct DeliveryFailedEvent: Codable, Sendable { public var kind: String = "delivery_failed"; public var name: String; public var deliveryId: String; public var eventId: String; public var reason: String; enum CodingKeys: String, CodingKey { case kind, name, reason; case deliveryId = "delivery_id"; case eventId = "event_id" } } -public struct WorkerReadyEvent: Codable, Sendable { public var kind: String = "worker_ready"; public var name: String; public var runtime: AgentRuntime; public var provider: HeadlessProvider?; public var cli: String?; public var model: String? } +public struct WorkerReadyEvent: Codable, Sendable { public var kind: String = "worker_ready"; public var name: String; public var runtime: AgentRuntime; public var provider: HeadlessProvider?; public var cli: String?; public var model: String?; public var sessionId: String?; enum CodingKeys: String, CodingKey { case kind, name, runtime, provider, cli, model; case sessionId = "session_id" } } public struct WorkerErrorEvent: Codable, Sendable { public var kind: String = "worker_error"; public var name: String; public var code: String; public var message: String } public struct RelaycastPublishedEvent: Codable, Sendable { public var kind: String = "relaycast_published"; public var eventId: String; public var to: String; public var targetType: String; enum CodingKeys: String, CodingKey { case kind, to; case eventId = "event_id"; case targetType = "target_type" } } public struct RelaycastPublishFailedEvent: Codable, Sendable { public var kind: String = "relaycast_publish_failed"; public var eventId: String; public var to: String; public var reason: String; enum CodingKeys: String, CodingKey { case kind, to, reason; case eventId = "event_id" } } diff --git a/packages/sdk/README.md b/packages/sdk/README.md index bbe70fcc7..2df87e2b0 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -58,6 +58,27 @@ const agent = await relay.spawn('Worker2', 'codex', 'Build the API', { model: 'gpt-4o', }); +// Custom harnesses can be supplied without changing Relay itself +const customRelay = new AgentRelay({ + harnesses: { + qwen: { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{args}'], + modelArgs: ['-m', '{model}'], + bypassFlag: '--yes', + searchPaths: ['~/.local/bin'], + }, + }, +}); + +await customRelay.spawn('QwenWorker', 'qwen', 'Review the diff', { + model: 'qwen3-coder', +}); + +// Spawn results expose broker-controlled process metadata +const localAgent = await customRelay.spawn('LocalWorker', 'codex', 'Plan the change'); +console.log(localAgent.sessionId, localAgent.pid); + // Wait for agent to finish (go idle or exit) const result = await agent.waitForIdle(120_000); @@ -107,6 +128,18 @@ await client.spawnPty({ task: 'Implement user authentication', }); +await client.spawnPty({ + name: 'QwenWorker', + cli: 'qwen', + task: 'Review the diff', + model: 'qwen3-coder', + harness: { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{args}'], + modelArgs: ['-m', '{model}'], + }, +}); + const agents = await client.listAgents(); await client.release('Worker1'); await client.shutdown(); diff --git a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts index 73de966e0..cb9ff38f2 100644 --- a/packages/sdk/src/__tests__/lifecycle-hooks.test.ts +++ b/packages/sdk/src/__tests__/lifecycle-hooks.test.ts @@ -71,6 +71,24 @@ describe('AgentRelayClient lifecycle hooks', () => { expect(captures[0].path).toBe('/api/spawn'); }); + it('preserves broker spawn pid in the result and afterAgentSpawn context', async () => { + const { fetchFn } = makeMockFetch([ + () => ({ name: 'agent-pid', runtime: 'pty', sessionId: 'session-1', pid: 4242 }), + ]); + const client = makeClient(fetchFn); + const after = vi.fn(); + client.addListener('afterAgentSpawn', after); + + const result = await client.spawnPty({ name: 'agent-pid', cli: 'codex' }); + + expect(result).toEqual({ name: 'agent-pid', runtime: 'pty', sessionId: 'session-1', pid: 4242 }); + expect(after).toHaveBeenCalledWith( + expect.objectContaining({ + result: { name: 'agent-pid', runtime: 'pty', sessionId: 'session-1', pid: 4242 }, + }) + ); + }); + it('folds beforeAgentSpawn patches into resolvedInput before POST', async () => { const { fetchFn, captures } = makeMockFetch(); const client = makeClient(fetchFn); diff --git a/packages/sdk/src/__tests__/spawn-harness.test.ts b/packages/sdk/src/__tests__/spawn-harness.test.ts new file mode 100644 index 000000000..77ede0c82 --- /dev/null +++ b/packages/sdk/src/__tests__/spawn-harness.test.ts @@ -0,0 +1,216 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { AgentRelayClient } from '../client.js'; +import { getHarnessDefinition, registerHarnessAdapter } from '../cli-registry.js'; +import { AgentRelay } from '../relay.js'; + +describe('spawn harness adapters', () => { + it('exposes built-in harnesses as serializable adapter config', () => { + expect(getHarnessDefinition('codex')).toMatchObject({ + adapter: 'codex', + binary: 'codex', + nonInteractiveArgs: ['exec', '{bypass}', '{task}', '{args}'], + bypassFlag: '--dangerously-bypass-approvals-and-sandbox', + bypassAliases: ['--full-auto'], + }); + }); + + it('serializes per-spawn harness config to the broker', async () => { + const client = new AgentRelayClient({ baseUrl: 'http://127.0.0.1:3888' }); + const request = vi + .spyOn((client as any).transport, 'request') + .mockResolvedValue({ name: 'worker', runtime: 'pty' }); + + await client.spawnPty({ + name: 'worker', + cli: 'qwen', + model: 'qwen3-coder', + harness: { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{args}'], + modelArgs: ['-m', '{model}'], + searchPaths: ['~/.local/bin'], + }, + }); + + expect(request).toHaveBeenCalledWith('/api/spawn', expect.objectContaining({ method: 'POST' })); + expect(JSON.parse(request.mock.calls[0]?.[1]?.body ?? '{}')).toMatchObject({ + name: 'worker', + cli: 'qwen', + model: 'qwen3-coder', + harness: { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{args}'], + modelArgs: ['-m', '{model}'], + searchPaths: ['~/.local/bin'], + }, + }); + }); + + it('attaches constructor harnesses from the facade spawn API', async () => { + const spawnPty = vi.fn(async (input: { name: string }) => ({ + name: input.name, + runtime: 'pty' as const, + })); + const relay = new AgentRelay({ + harnesses: { + qwen: { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{args}'], + modelArgs: ['-m', '{model}'], + bypassFlag: '--yes', + }, + }, + }); + (relay as any).client = { spawnPty }; + + await relay.spawn('worker', 'qwen', 'ship it', { + channels: ['general'], + model: 'qwen3-coder', + args: ['--verbose'], + }); + + expect(spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'worker', + cli: 'qwen', + model: 'qwen3-coder', + args: ['--verbose'], + harness: { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{args}'], + modelArgs: ['-m', '{model}'], + bypassFlag: '--yes', + }, + }) + ); + }); + + it('exposes broker spawn pid on facade agent handles and success hooks', async () => { + const spawnPty = vi.fn(async (input: { name: string }) => ({ + name: input.name, + runtime: 'pty' as const, + sessionId: 'session-pid', + pid: 4321, + })); + const relay = new AgentRelay(); + (relay as any).client = { spawnPty }; + const onSuccess = vi.fn(); + + const agent = await relay.spawn('worker-pid', 'codex', 'ship it', { + channels: ['general'], + onSuccess, + }); + + expect(agent.sessionId).toBe('session-pid'); + expect(agent.pid).toBe(4321); + expect(onSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'worker-pid', + runtime: 'pty', + sessionId: 'session-pid', + pid: 4321, + }) + ); + }); + + it('keeps constructor harnesses scoped to that relay instance', () => { + new AgentRelay({ + harnesses: { + 'instance-only-harness': { + binary: 'instance-agent', + }, + }, + }); + + expect(getHarnessDefinition('instance-only-harness')).toBeUndefined(); + }); + + it('attaches built-in harness definitions from the facade spawn API', async () => { + const spawnPty = vi.fn(async (input: { name: string }) => ({ + name: input.name, + runtime: 'pty' as const, + })); + const relay = new AgentRelay(); + (relay as any).client = { spawnPty }; + + await relay.spawn('worker', 'codex', 'ship it', { + channels: ['general'], + model: 'gpt-5-codex', + }); + + expect(spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'worker', + cli: 'codex', + model: 'gpt-5-codex', + harness: expect.objectContaining({ + adapter: 'codex', + binary: 'codex', + bypassFlag: '--dangerously-bypass-approvals-and-sandbox', + }), + }) + ); + }); + + it('merges custom wrappers with built-in adapter defaults', async () => { + const spawnPty = vi.fn(async (input: { name: string }) => ({ + name: input.name, + runtime: 'pty' as const, + })); + const relay = new AgentRelay({ + harnesses: { + 'company-codex': { + adapter: 'codex', + binary: 'company-codex', + searchPaths: ['~/company/bin'], + }, + }, + }); + (relay as any).client = { spawnPty }; + + await relay.spawn('worker', 'company-codex', 'ship it', { channels: ['general'] }); + + expect(spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + cli: 'company-codex', + harness: { + adapter: 'codex', + binary: 'company-codex', + nonInteractiveArgs: ['exec', '{bypass}', '{task}', '{args}'], + bypassFlag: '--dangerously-bypass-approvals-and-sandbox', + bypassAliases: ['--full-auto'], + searchPaths: ['~/company/bin'], + }, + }) + ); + }); + + it('attaches serializable details from registered programmatic adapters', async () => { + registerHarnessAdapter('registered-spawn-harness', { + binaries: ['registered-agent'], + nonInteractiveArgs: (task, extra = []) => ['run', task, ...extra], + bypassFlag: '--yes', + searchPaths: ['~/.local/bin'], + }); + const spawnPty = vi.fn(async (input: { name: string }) => ({ + name: input.name, + runtime: 'pty' as const, + })); + const relay = new AgentRelay(); + (relay as any).client = { spawnPty }; + + await relay.spawn('worker', 'registered-spawn-harness', 'ship it', { channels: ['general'] }); + + expect(spawnPty).toHaveBeenCalledWith( + expect.objectContaining({ + cli: 'registered-spawn-harness', + harness: { + binaries: ['registered-agent'], + bypassFlag: '--yes', + searchPaths: ['~/.local/bin'], + }, + }) + ); + }); +}); diff --git a/packages/sdk/src/cli-registry.ts b/packages/sdk/src/cli-registry.ts index ec039e26e..b1ea06aee 100644 --- a/packages/sdk/src/cli-registry.ts +++ b/packages/sdk/src/cli-registry.ts @@ -11,7 +11,7 @@ * in `resolve_command_path()` at crates/broker/src/pty.rs. */ -import type { AgentCli } from './workflows/types.js'; +import type { HarnessDefinition, KnownAgentCli } from '@agent-relay/workflow-types'; // ── Types ────────────────────────────────────────────────────────────────── @@ -20,6 +20,8 @@ export interface CliDefinition { binaries: string[]; /** Build non-interactive mode args for a one-shot task */ nonInteractiveArgs: (task: string, extraArgs?: string[]) => string[]; + /** Build model-selection args for a model id */ + modelArgs?: (model: string) => string[]; /** Bypass flag for auto-approve / unattended mode */ bypassFlag?: string; /** Bypass flag aliases (alternative forms accepted by the CLI) */ @@ -28,6 +30,8 @@ export interface CliDefinition { searchPaths?: string[]; /** When true, non-zero exit codes are not treated as failures (some CLIs exit non-zero on success) */ ignoreExitCode?: boolean; + /** Credential proxy provider used when credentials.proxy is enabled. */ + proxyProvider?: 'openai' | 'anthropic' | 'openrouter'; } // ── Well-known install paths ─────────────────────────────────────────────── @@ -50,79 +54,419 @@ export const COMMON_SEARCH_PATHS = [ // ── Registry ─────────────────────────────────────────────────────────────── -const CLI_REGISTRY: Record = { +const DEFAULT_NON_INTERACTIVE_TEMPLATE = ['{task}', '{args}'] as const; +const DEFAULT_MODEL_ARGS_TEMPLATE = ['--model', '{model}'] as const; + +export const BUILTIN_HARNESS_DEFINITIONS: Readonly>> = { claude: { - binaries: ['claude'], - nonInteractiveArgs: (task, extra = []) => ['-p', '--dangerously-skip-permissions', task, ...extra], + adapter: 'claude', + binary: 'claude', + nonInteractiveArgs: ['-p', '{bypass}', '{task}', '{args}'], bypassFlag: '--dangerously-skip-permissions', searchPaths: ['~/.claude/local'], }, codex: { - binaries: ['codex'], - nonInteractiveArgs: (task, extra = []) => [ - 'exec', - '--dangerously-bypass-approvals-and-sandbox', - task, - ...extra, - ], + adapter: 'codex', + binary: 'codex', + nonInteractiveArgs: ['exec', '{bypass}', '{task}', '{args}'], bypassFlag: '--dangerously-bypass-approvals-and-sandbox', bypassAliases: ['--full-auto'], searchPaths: ['~/.local/bin'], }, gemini: { - binaries: ['gemini'], - nonInteractiveArgs: (task, extra = []) => ['-p', task, ...extra], + adapter: 'gemini', + binary: 'gemini', + nonInteractiveArgs: ['-p', '{task}', '{args}'], bypassFlag: '--yolo', bypassAliases: ['-y'], }, opencode: { - binaries: ['opencode'], - nonInteractiveArgs: (task, extra = []) => ['run', task, ...extra], + adapter: 'opencode', + binary: 'opencode', + nonInteractiveArgs: ['run', '{task}', '{args}'], searchPaths: ['~/.opencode/bin'], ignoreExitCode: true, }, droid: { - binaries: ['droid'], - nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra], + adapter: 'droid', + binary: 'droid', + nonInteractiveArgs: ['exec', '{task}', '{args}'], }, aider: { - binaries: ['aider'], - nonInteractiveArgs: (task, extra = []) => ['--message', task, '--yes-always', '--no-git', ...extra], + adapter: 'aider', + binary: 'aider', + nonInteractiveArgs: ['--message', '{task}', '--yes-always', '--no-git', '{args}'], }, goose: { - binaries: ['goose'], - nonInteractiveArgs: (task, extra = []) => ['run', '--text', task, '--no-session', ...extra], + adapter: 'goose', + binary: 'goose', + nonInteractiveArgs: ['run', '--text', '{task}', '--no-session', '{args}'], }, 'cursor-agent': { - binaries: ['cursor-agent'], - nonInteractiveArgs: (task, extra = []) => ['--force', '-p', task, ...extra], + adapter: 'cursor', + binary: 'cursor-agent', + nonInteractiveArgs: ['--force', '-p', '{task}', '{args}'], }, agent: { - binaries: ['agent'], - nonInteractiveArgs: (task, extra = []) => ['--force', '-p', task, ...extra], + adapter: 'cursor', + binary: 'agent', + nonInteractiveArgs: ['--force', '-p', '{task}', '{args}'], }, cursor: { + adapter: 'cursor', binaries: ['cursor-agent', 'agent'], - nonInteractiveArgs: (task, extra = []) => ['--force', '-p', task, ...extra], + nonInteractiveArgs: ['--force', '-p', '{task}', '{args}'], }, +}; + +const CLI_REGISTRY: Record = { + claude: adapterFromConfig('claude', BUILTIN_HARNESS_DEFINITIONS.claude!), + codex: adapterFromConfig('codex', BUILTIN_HARNESS_DEFINITIONS.codex!), + gemini: adapterFromConfig('gemini', BUILTIN_HARNESS_DEFINITIONS.gemini!), + opencode: adapterFromConfig('opencode', BUILTIN_HARNESS_DEFINITIONS.opencode!), + droid: adapterFromConfig('droid', BUILTIN_HARNESS_DEFINITIONS.droid!), + aider: adapterFromConfig('aider', BUILTIN_HARNESS_DEFINITIONS.aider!), + goose: adapterFromConfig('goose', BUILTIN_HARNESS_DEFINITIONS.goose!), + 'cursor-agent': adapterFromConfig('cursor-agent', BUILTIN_HARNESS_DEFINITIONS['cursor-agent']!), + agent: adapterFromConfig('agent', BUILTIN_HARNESS_DEFINITIONS.agent!), + cursor: adapterFromConfig('cursor', BUILTIN_HARNESS_DEFINITIONS.cursor!), api: { binaries: [], nonInteractiveArgs: (task) => [task], }, }; +const USER_CLI_REGISTRY = new Map(); +const USER_HARNESS_CONFIGS = new Map(); + +function normalizedBaseCliKey(cli: string): string | undefined { + const trimmed = cli.trim(); + if (!trimmed) return undefined; + const base = (trimmed.includes(':') ? trimmed.split(':')[0] : trimmed).trim(); + return base || undefined; +} + +function normalizeCliKey(cli: string): string { + const base = normalizedBaseCliKey(cli); + if (!base) { + throw new Error('Harness name must be a non-empty string'); + } + return base; +} + +function lookupCliKey(cli: string): string | undefined { + return normalizedBaseCliKey(cli); +} + +function validateStringArray(value: readonly string[] | undefined, label: string): string[] | undefined { + if (value === undefined) return undefined; + if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) { + throw new Error(`${label} must be an array of strings`); + } + return [...value]; +} + +function cloneHarnessDefinition(config: HarnessDefinition): HarnessDefinition { + return { + ...config, + ...(config.adapter ? { adapter: config.adapter } : {}), + ...(config.binaries ? { binaries: [...config.binaries] } : {}), + ...(config.interactiveArgs ? { interactiveArgs: [...config.interactiveArgs] } : {}), + ...(config.nonInteractiveArgs ? { nonInteractiveArgs: [...config.nonInteractiveArgs] } : {}), + ...(config.modelArgs ? { modelArgs: [...config.modelArgs] } : {}), + ...(config.bypassAliases ? { bypassAliases: [...config.bypassAliases] } : {}), + ...(config.searchPaths ? { searchPaths: [...config.searchPaths] } : {}), + ...(config.aliases ? { aliases: [...config.aliases] } : {}), + }; +} + +function mergeHarnessDefinitions(base: HarnessDefinition, override: HarnessDefinition): HarnessDefinition { + const overrideBinary = override.binary?.trim() ? override.binary : undefined; + return { + ...cloneHarnessDefinition(base), + ...cloneHarnessDefinition(override), + adapter: override.adapter ?? base.adapter, + binary: overrideBinary ?? base.binary, + ...(override.binaries + ? { binaries: [...override.binaries] } + : overrideBinary !== undefined + ? { binaries: undefined } + : base.binaries + ? { binaries: [...base.binaries] } + : {}), + ...(override.interactiveArgs + ? { interactiveArgs: [...override.interactiveArgs] } + : base.interactiveArgs + ? { interactiveArgs: [...base.interactiveArgs] } + : {}), + ...(override.nonInteractiveArgs + ? { nonInteractiveArgs: [...override.nonInteractiveArgs] } + : base.nonInteractiveArgs + ? { nonInteractiveArgs: [...base.nonInteractiveArgs] } + : {}), + ...(override.modelArgs + ? { modelArgs: [...override.modelArgs] } + : base.modelArgs + ? { modelArgs: [...base.modelArgs] } + : {}), + bypassFlag: override.bypassFlag ?? base.bypassFlag, + ...(override.bypassAliases + ? { bypassAliases: [...override.bypassAliases] } + : base.bypassAliases + ? { bypassAliases: [...base.bypassAliases] } + : {}), + ...(override.searchPaths + ? { searchPaths: [...override.searchPaths] } + : base.searchPaths + ? { searchPaths: [...base.searchPaths] } + : {}), + ignoreExitCode: override.ignoreExitCode ?? base.ignoreExitCode, + proxyProvider: override.proxyProvider ?? base.proxyProvider, + ...(override.aliases + ? { aliases: [...override.aliases] } + : base.aliases + ? { aliases: [...base.aliases] } + : {}), + }; +} + +function resolveHarnessConfig(name: string, config: HarnessDefinition): HarnessDefinition { + const adapterKey = lookupCliKey(config.adapter ?? name); + const base = adapterKey ? BUILTIN_HARNESS_DEFINITIONS[adapterKey as KnownAgentCli] : undefined; + return base ? mergeHarnessDefinitions(base, config) : cloneHarnessDefinition(config); +} + +export function defineHarnessDefinition(name: string, definition: HarnessDefinition): HarnessDefinition { + return resolveHarnessConfig(normalizeCliKey(name), definition); +} + +function harnessConfigFromCliDefinition(definition: CliDefinition): HarnessDefinition { + return { + binaries: [...definition.binaries], + ...(definition.bypassFlag ? { bypassFlag: definition.bypassFlag } : {}), + ...(definition.bypassAliases ? { bypassAliases: [...definition.bypassAliases] } : {}), + ...(definition.searchPaths ? { searchPaths: [...definition.searchPaths] } : {}), + ...(definition.ignoreExitCode !== undefined ? { ignoreExitCode: definition.ignoreExitCode } : {}), + ...(definition.proxyProvider ? { proxyProvider: definition.proxyProvider } : {}), + }; +} + +function argMatchesFlag(arg: string, flag: string): boolean { + return arg === flag || arg.startsWith(`${flag}=`); +} + +function resolveTemplateBypass(config: HarnessDefinition, extraArgs: string[]): string | undefined { + const flag = config.bypassFlag?.trim(); + if (!flag) return undefined; + + const candidates = [flag, ...(config.bypassAliases ?? []).map((alias) => alias.trim()).filter(Boolean)]; + return extraArgs.some((arg) => candidates.some((candidate) => argMatchesFlag(arg, candidate))) + ? undefined + : flag; +} + +function expandTemplateArg( + template: string, + context: { task: string; extraArgs: string[]; bypass?: string; model?: string } +): string[] { + if (template === '{args}' || template === '{{args}}') { + return [...context.extraArgs]; + } + if (template === '{bypass}' || template === '{{bypass}}') { + return context.bypass ? [context.bypass] : []; + } + if ((template === '{model}' || template === '{{model}}') && context.model === undefined) { + return []; + } + return [ + template + .replace(/\{\{\s*task\s*\}\}|\{task\}/g, context.task) + .replace(/\{\{\s*bypass\s*\}\}|\{bypass\}/g, context.bypass ?? '') + .replace(/\{\{\s*model\s*\}\}|\{model\}/g, context.model ?? ''), + ]; +} + +function renderArgTemplate( + template: readonly string[], + context: { task: string; extraArgs?: string[]; bypass?: string; model?: string } +): string[] { + const args: string[] = []; + for (const entry of template) { + args.push( + ...expandTemplateArg(entry, { + task: context.task, + extraArgs: context.extraArgs ?? [], + bypass: context.bypass, + model: context.model, + }) + ); + } + return args; +} + +function adapterFromConfig(name: string, config: HarnessDefinition): CliDefinition { + const binaries = + validateStringArray(config.binaries, `harness "${name}".binaries`) ?? + (config.binary ? [config.binary] : [name]); + if (binaries.length === 0 || binaries.some((binary) => !binary.trim())) { + throw new Error(`harness "${name}".binaries must contain at least one non-empty binary`); + } + + const nonInteractiveTemplate = validateStringArray( + config.nonInteractiveArgs, + `harness "${name}".nonInteractiveArgs` + ) ?? [...DEFAULT_NON_INTERACTIVE_TEMPLATE]; + const modelTemplate = validateStringArray(config.modelArgs, `harness "${name}".modelArgs`) ?? [ + ...DEFAULT_MODEL_ARGS_TEMPLATE, + ]; + + return { + binaries, + nonInteractiveArgs: (task, extraArgs = []) => + renderArgTemplate(nonInteractiveTemplate, { + task, + extraArgs, + bypass: resolveTemplateBypass(config, extraArgs), + }), + modelArgs: (model) => renderArgTemplate(modelTemplate, { task: '', model }), + bypassFlag: config.bypassFlag, + bypassAliases: validateStringArray(config.bypassAliases, `harness "${name}".bypassAliases`), + searchPaths: validateStringArray(config.searchPaths, `harness "${name}".searchPaths`), + ignoreExitCode: config.ignoreExitCode, + proxyProvider: config.proxyProvider, + }; +} + +export type CLIHarnessAdapter = CliDefinition | HarnessDefinition; + +/** + * @deprecated Use {@link CLIHarnessAdapter}. This type describes the + * serializable/function-backed CLI command adapter used to build harness + * argv, not the runtime harness lifecycle contract. + */ +export type HarnessAdapter = CLIHarnessAdapter; + +function isCliDefinition(adapter: CLIHarnessAdapter): adapter is CliDefinition { + return typeof (adapter as { nonInteractiveArgs?: unknown }).nonInteractiveArgs === 'function'; +} + +export function defineHarnessAdapter(name: string, adapter: CLIHarnessAdapter): CliDefinition { + if (isCliDefinition(adapter)) { + return { + ...adapter, + binaries: [...adapter.binaries], + bypassAliases: adapter.bypassAliases ? [...adapter.bypassAliases] : undefined, + searchPaths: adapter.searchPaths ? [...adapter.searchPaths] : undefined, + }; + } + return adapterFromConfig(name, resolveHarnessConfig(name, adapter as HarnessDefinition)); +} + +/** + * Register or override a harness adapter at runtime. + * + * This is the SDK escape hatch for harnesses that are not built into Relay. + * Programmatic adapters can provide functions; YAML/workflow configs should + * use the serializable {@link HarnessDefinition} shape. + */ +export function registerHarnessAdapter(name: string, adapter: CLIHarnessAdapter): void { + const key = normalizeCliKey(name); + const definition = defineHarnessAdapter(key, adapter); + USER_CLI_REGISTRY.set(key, definition); + const serializableConfig = isCliDefinition(adapter) + ? harnessConfigFromCliDefinition(definition) + : resolveHarnessConfig(key, adapter); + USER_HARNESS_CONFIGS.set(key, serializableConfig); + + const aliases = 'aliases' in adapter ? adapter.aliases : undefined; + if (aliases) { + for (const alias of aliases) { + const aliasKey = normalizeCliKey(alias); + USER_CLI_REGISTRY.set(aliasKey, definition); + USER_HARNESS_CONFIGS.set(aliasKey, cloneHarnessDefinition(serializableConfig)); + } + } +} + +/** Backward-compatible name for callers that think in CLI definitions. */ +export const registerCliDefinition = registerHarnessAdapter; + +export function registerHarnessAdapters(adapters: Record | undefined): void { + if (!adapters) return; + for (const [name, adapter] of Object.entries(adapters)) { + registerHarnessAdapter(name, adapter); + } +} + +export interface HarnessRegistrySnapshot { + cliRegistry: Map; + harnessConfigs: Map; +} + +function cloneCliDefinition(definition: CliDefinition): CliDefinition { + return { + ...definition, + binaries: [...definition.binaries], + bypassAliases: definition.bypassAliases ? [...definition.bypassAliases] : undefined, + searchPaths: definition.searchPaths ? [...definition.searchPaths] : undefined, + }; +} + +export function snapshotHarnessAdapters(): HarnessRegistrySnapshot { + return { + cliRegistry: new Map( + [...USER_CLI_REGISTRY].map(([key, definition]) => [key, cloneCliDefinition(definition)]) + ), + harnessConfigs: new Map( + [...USER_HARNESS_CONFIGS].map(([key, config]) => [key, cloneHarnessDefinition(config)]) + ), + }; +} + +export function restoreHarnessAdapters(snapshot: HarnessRegistrySnapshot): void { + USER_CLI_REGISTRY.clear(); + USER_HARNESS_CONFIGS.clear(); + for (const [key, definition] of snapshot.cliRegistry) { + USER_CLI_REGISTRY.set(key, cloneCliDefinition(definition)); + } + for (const [key, config] of snapshot.harnessConfigs) { + USER_HARNESS_CONFIGS.set(key, cloneHarnessDefinition(config)); + } +} + /** * Get the CLI definition for a given CLI identifier. * Handles `cli:model` variants (e.g. `claude:opus`) by extracting the base CLI. */ export function getCliDefinition(cli: string): CliDefinition | undefined { - const baseCli = cli.includes(':') ? cli.split(':')[0] : cli; - return CLI_REGISTRY[baseCli as AgentCli]; + const baseCli = lookupCliKey(cli); + if (!baseCli) return undefined; + return USER_CLI_REGISTRY.get(baseCli) ?? CLI_REGISTRY[baseCli as KnownAgentCli]; +} + +export const getHarnessAdapter = getCliDefinition; + +export function getBuiltInHarnessDefinitions(): Readonly>> { + return BUILTIN_HARNESS_DEFINITIONS; +} + +export function getHarnessDefinition(cli: string): HarnessDefinition | undefined { + const baseCli = lookupCliKey(cli); + if (!baseCli) return undefined; + const config = USER_HARNESS_CONFIGS.get(baseCli); + if (config) return cloneHarnessDefinition(config); + const builtIn = BUILTIN_HARNESS_DEFINITIONS[baseCli as KnownAgentCli]; + return builtIn ? cloneHarnessDefinition(builtIn) : undefined; +} + +export function buildModelArgs(cli: string, model: string | undefined): string[] { + if (!model) return []; + return getCliDefinition(cli)?.modelArgs?.(model) ?? ['--model', model]; } /** * Get the full registry (read-only). */ -export function getCliRegistry(): Readonly> { +export function getCliRegistry(): Readonly> { return CLI_REGISTRY; } diff --git a/packages/sdk/src/cli-resolver.ts b/packages/sdk/src/cli-resolver.ts index 7e5d5dcb2..d28a73ebf 100644 --- a/packages/sdk/src/cli-resolver.ts +++ b/packages/sdk/src/cli-resolver.ts @@ -11,7 +11,6 @@ import { accessSync, constants as constantsSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; import { promisify } from 'node:util'; -import type { AgentCli } from './workflows/types.js'; import { getCliDefinition, COMMON_SEARCH_PATHS } from './cli-registry.js'; const execFileAsync = promisify(execFile); @@ -54,7 +53,7 @@ function expandHome(p: string): string { * * Results are memoized. Returns `undefined` if the binary cannot be found. */ -export async function resolveCli(cli: AgentCli): Promise { +export async function resolveCli(cli: string): Promise { if (resolveCache.has(cli)) { return resolveCache.get(cli) ?? undefined; } @@ -106,7 +105,7 @@ export async function resolveCli(cli: AgentCli): Promise; } +export interface SpawnAgentResult { + name: string; + runtime: AgentRuntime; + sessionId?: string; + pid?: number; +} + export interface SessionInfo { broker_version: string; protocol_version: number; @@ -165,6 +172,7 @@ function buildSpawnPtyBody(input: SpawnPtyInput): Record { name: input.name, cli: input.cli, ...(input.model !== undefined ? { model: input.model } : {}), + ...(input.harness !== undefined ? { harness: input.harness } : {}), args: input.args ?? [], ...(input.task !== undefined ? { task: input.task } : {}), channels: input.channels ?? [], @@ -189,6 +197,7 @@ function buildSpawnProviderBody( name: input.name, cli: input.provider, ...(input.model !== undefined ? { model: input.model } : {}), + ...(input.harness !== undefined ? { harness: input.harness } : {}), args: input.args ?? [], ...(input.task !== undefined ? { task: input.task } : {}), channels: input.channels ?? [], @@ -584,7 +593,7 @@ export class AgentRelayClient { // ── Agent lifecycle ──────────────────────────────────────────────── - async spawnPty(input: SpawnPtyInput): Promise<{ name: string; runtime: AgentRuntime }> { + async spawnPty(input: SpawnPtyInput): Promise { const beforeCtx: BeforeAgentSpawnContext = { kind: 'pty', input, @@ -595,7 +604,7 @@ export class AgentRelayClient { const t0 = Date.now(); const resolvedInput = (await this.runBeforeSpawn(beforeCtx)) as SpawnPtyInput; try { - const result = await this.transport.request<{ name: string; runtime: AgentRuntime }>('/api/spawn', { + const result = await this.transport.request('/api/spawn', { method: 'POST', body: JSON.stringify(buildSpawnPtyBody(resolvedInput)), }); @@ -607,7 +616,7 @@ export class AgentRelayClient { } } - async spawnProvider(input: SpawnProviderInput): Promise<{ name: string; runtime: AgentRuntime }> { + async spawnProvider(input: SpawnProviderInput): Promise { const transport = resolveSpawnTransport(input); if (transport === 'headless' && !isHeadlessProvider(input.provider)) { throw new Error( @@ -625,7 +634,7 @@ export class AgentRelayClient { const t0 = Date.now(); const resolvedInput = (await this.runBeforeSpawn(beforeCtx)) as SpawnProviderInput; try { - const result = await this.transport.request<{ name: string; runtime: AgentRuntime }>('/api/spawn', { + const result = await this.transport.request('/api/spawn', { method: 'POST', body: JSON.stringify(buildSpawnProviderBody(resolvedInput, transport)), }); @@ -637,19 +646,15 @@ export class AgentRelayClient { } } - async spawnHeadless(input: SpawnHeadlessInput): Promise<{ name: string; runtime: AgentRuntime }> { + async spawnHeadless(input: SpawnHeadlessInput): Promise { return this.spawnProvider({ ...input, transport: 'headless' }); } - async spawnClaude( - input: Omit - ): Promise<{ name: string; runtime: AgentRuntime }> { + async spawnClaude(input: Omit): Promise { return this.spawnProvider({ ...input, provider: 'claude' }); } - async spawnOpencode( - input: Omit - ): Promise<{ name: string; runtime: AgentRuntime }> { + async spawnOpencode(input: Omit): Promise { return this.spawnProvider({ ...input, provider: 'opencode' }); } @@ -686,7 +691,7 @@ export class AgentRelayClient { beforeCtx: BeforeAgentSpawnContext, resolvedInput: SpawnPtyInput | SpawnProviderInput, startMs: number, - result: { name: string; runtime: AgentRuntime } | undefined, + result: SpawnAgentResult | undefined, error: unknown ): Promise { const afterCtx: AfterAgentSpawnContext = { diff --git a/packages/sdk/src/harness-runtime.ts b/packages/sdk/src/harness-runtime.ts new file mode 100644 index 000000000..3c2c9d42d --- /dev/null +++ b/packages/sdk/src/harness-runtime.ts @@ -0,0 +1,82 @@ +import type { MessageInjectionMode } from './protocol.js'; + +export type MaybePromise = T | Promise; + +export interface HarnessInitContext { + name: string; + cli: string; + task?: string; + args: string[]; + channels: string[]; + model?: string; + cwd?: string; + env?: Record; +} + +export interface HarnessInitResult { + /** Provider/native session id, when the harness supports resume. */ + sessionId?: string; + /** OS process id for the broker-controlled harness process, when applicable. */ + pid?: number; + /** Alias for consumers that prefer the expanded spelling. */ + processId?: number; + metadata?: Record; +} + +export interface HarnessRegistrationContext { + name: string; + cli: string; + channels: string[]; + relayAgentToken?: string; + relayApiKey?: string; + relayBaseUrl?: string; +} + +export interface HarnessRegistrationResult { + name?: string; + sessionId?: string; + token?: string; + metadata?: Record; +} + +export interface HarnessRelayMessage { + from: string; + to: string; + text: string; + threadId?: string; + workspaceId?: string; + workspaceAlias?: string; + priority?: number; + mode?: MessageInjectionMode; + data?: Record; +} + +export interface HarnessMessageContext { + name: string; + sessionId?: string; + pid?: number; + processId?: number; +} + +export interface HarnessReleaseContext extends HarnessMessageContext { + reason?: string; +} + +/** + * Runtime-facing harness lifecycle contract. + * + * The Rust broker cannot call in-memory TypeScript functions directly; a + * concrete implementation still needs to be exposed through a serializable + * boundary such as a CLI/stdio worker or HTTP service. These method names are + * the control surface that such adapters should implement. + */ +export interface HarnessRuntimeAdapter { + readonly kind: string; + initHarness(context: HarnessInitContext): MaybePromise; + register?(context: HarnessRegistrationContext): MaybePromise; + /** Deliver a Relay message from the broker to the harness. */ + receiveMessage?(message: HarnessRelayMessage, context: HarnessMessageContext): MaybePromise; + /** Emit a harness-originated message back through Relay. */ + sendMessage?(message: HarnessRelayMessage, context: HarnessMessageContext): MaybePromise; + releaseHarness?(context: HarnessReleaseContext): MaybePromise; +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 93c779d0e..761326798 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -16,6 +16,7 @@ export { type AgentRelaySpawnOptions, type SetInboundDeliveryModeResult, type SessionInfo, + type SpawnAgentResult, type WorkerStreamSubscriptionOptions, } from './client.js'; export { EventBus, type EventHandler, type EventMap } from './event-bus.js'; @@ -42,6 +43,7 @@ export * from './broker-logs.js'; export * from './consensus.js'; export * from './shadow.js'; export * from './relay-adapter.js'; +export * from './harness-runtime.js'; export * from './workflows/index.js'; export * from './spawn-from-env.js'; export * from './cli-registry.js'; diff --git a/packages/sdk/src/lifecycle-hooks.ts b/packages/sdk/src/lifecycle-hooks.ts index 6cb72df2a..bba84f173 100644 --- a/packages/sdk/src/lifecycle-hooks.ts +++ b/packages/sdk/src/lifecycle-hooks.ts @@ -52,7 +52,10 @@ import type { SpawnPtyInput, SpawnProviderInput } from './types.js'; * the same key. */ export type SpawnPatch = Partial< - Pick + Pick< + SpawnPtyInput & SpawnProviderInput, + 'args' | 'channels' | 'task' | 'model' | 'harness' | 'team' | 'agentToken' + > >; // ── Call-site contexts ───────────────────────────────────────────────────── @@ -78,7 +81,7 @@ export interface AfterAgentSpawnContext extends BeforeAgentSpawnContext { /** Final input that was sent to the broker — original input merged with every handler's patch. */ resolvedInput: SpawnPtyInput | SpawnProviderInput; /** Broker reply on success. */ - result?: { name: string; runtime: AgentRuntime }; + result?: { name: string; runtime: AgentRuntime; sessionId?: string; pid?: number }; /** Set when the broker call rejected. Mutually exclusive with `result`. */ error?: Error; /** Wall-clock duration from `beforeAgentSpawn` start to here. */ diff --git a/packages/sdk/src/protocol.ts b/packages/sdk/src/protocol.ts index 62cdc1e8a..22ad4dca0 100644 --- a/packages/sdk/src/protocol.ts +++ b/packages/sdk/src/protocol.ts @@ -1,3 +1,6 @@ +import type { HarnessDefinition } from '@agent-relay/workflow-types'; +export type { HarnessDefinition } from '@agent-relay/workflow-types'; + export const PROTOCOL_VERSION = 2 as const; export type AgentRuntime = 'pty' | 'headless'; @@ -21,6 +24,8 @@ export interface AgentSpec { channels?: string[]; model?: string; cwd?: string; + session_id?: string; + harness?: HarnessDefinition; team?: string; shadow_of?: string; shadow_mode?: string; @@ -257,6 +262,7 @@ export type BrokerEvent = provider?: HeadlessProvider; cli?: string; model?: string; + sessionId?: string; parent?: string; pid?: number; source?: string; @@ -397,6 +403,8 @@ export type BrokerEvent = provider?: HeadlessProvider; cli?: string; model?: string; + sessionId?: string; + pid?: number; } | { kind: 'worker_error'; @@ -504,7 +512,7 @@ export type BrokerToWorker = export type WorkerToBroker = | { type: 'worker_ready'; - payload: { name: string; runtime: AgentRuntime; provider?: HeadlessProvider }; + payload: { name: string; runtime: AgentRuntime; provider?: HeadlessProvider; pid?: number }; } | { type: 'delivery_ack'; diff --git a/packages/sdk/src/relay-adapter.ts b/packages/sdk/src/relay-adapter.ts index 189fc7c70..577ecdf30 100644 --- a/packages/sdk/src/relay-adapter.ts +++ b/packages/sdk/src/relay-adapter.ts @@ -21,6 +21,7 @@ import { AgentRelayClient, type AgentRelaySpawnOptions } from './client.js'; import type { SpawnPtyInput, SendMessageInput } from './types.js'; +import type { HarnessDefinition } from '@agent-relay/workflow-types'; import type { BrokerEvent, BrokerStats, BrokerStatus, CrashInsightsResponse } from './protocol.js'; import type { PtyInputStream, PtyInputStreamOptions } from './transport.js'; @@ -86,6 +87,7 @@ export interface RelaySpawnRequest { team?: string; cwd?: string; model?: string; + harness?: HarnessDefinition; interactive?: boolean; shadowMode?: string; shadowOf?: string; @@ -198,6 +200,7 @@ export class RelayAdapter { task: buildSpawnTask(req.task, req.includeWorkflowConventions), channels: ['general'], model: req.model, + harness: req.harness, cwd: req.cwd, team: req.team, shadowOf: req.shadowOf, @@ -205,10 +208,12 @@ export class RelayAdapter { }; const result = await client.spawnPty(input); - let pid: number | undefined; + let pid = result.pid; try { - const agents = await client.listAgents(); - pid = agents.find((a) => a.name === req.name)?.pid; + if (pid === undefined) { + const agents = await client.listAgents(); + pid = agents.find((a) => a.name === req.name)?.pid; + } } catch { // Non-fatal } diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index f9fbf30b1..864c10cf9 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -31,7 +31,12 @@ import { RelayCast } from '@relaycast/sdk'; import { zodToJsonSchema } from 'zod-to-json-schema'; import type { ZodTypeAny } from 'zod'; -import { AgentRelayClient, type AgentRelayBrokerInitArgs, type AgentRelaySpawnOptions } from './client.js'; +import { + AgentRelayClient, + type AgentRelayBrokerInitArgs, + type AgentRelaySpawnOptions, + type SpawnAgentResult, +} from './client.js'; import { EventBus } from './event-bus.js'; import type { AgentRelayEvents, BeforeAgentSpawnHandler } from './lifecycle-hooks.js'; import { @@ -45,7 +50,8 @@ import { type ResolvedPersona, } from './personas.js'; import { AgentRelayProtocolError } from './transport.js'; -import type { JsonSchema, SendMessageInput, SpawnPtyInput } from './types.js'; +import type { HarnessDefinition, JsonSchema, SendMessageInput, SpawnPtyInput } from './types.js'; +import { defineHarnessDefinition, getHarnessDefinition } from './cli-registry.js'; import type { AgentRuntime, BrokerEvent, @@ -122,6 +128,28 @@ function generateWorkspaceId(): string { return `${WORKSPACE_ID_PREFIX}${suffix}`; } +function cloneHarnessDefinition(definition: HarnessDefinition): HarnessDefinition { + return { + ...definition, + ...(definition.binaries ? { binaries: [...definition.binaries] } : {}), + ...(definition.interactiveArgs ? { interactiveArgs: [...definition.interactiveArgs] } : {}), + ...(definition.nonInteractiveArgs ? { nonInteractiveArgs: [...definition.nonInteractiveArgs] } : {}), + ...(definition.modelArgs ? { modelArgs: [...definition.modelArgs] } : {}), + ...(definition.bypassAliases ? { bypassAliases: [...definition.bypassAliases] } : {}), + ...(definition.searchPaths ? { searchPaths: [...definition.searchPaths] } : {}), + ...(definition.aliases ? { aliases: [...definition.aliases] } : {}), + }; +} + +function cloneHarnessMap( + harnesses: Record | undefined +): Record { + if (!harnesses) return {}; + return Object.fromEntries( + Object.entries(harnesses).map(([name, definition]) => [name, defineHarnessDefinition(name, definition)]) + ); +} + function toWorkspaceRegistryEntry(value: unknown): WorkspaceRegistryEntry { if (!value || typeof value !== 'object') { return {}; @@ -241,6 +269,8 @@ export interface SpawnLifecycleContext { export interface SpawnLifecycleSuccessContext extends SpawnLifecycleContext { runtime: AgentRuntime; + sessionId?: string; + pid?: number; } export interface SpawnLifecycleErrorContext extends SpawnLifecycleContext { @@ -276,6 +306,7 @@ export interface SpawnOptions extends SpawnLifecycleHook args?: string[]; channels?: string[]; model?: string; + harness?: HarnessDefinition; cwd?: string; team?: string; shadowOf?: string; @@ -339,6 +370,8 @@ type AgentOutputCallback = ((chunk: string) => void) | ((data: AgentOutputPayloa export interface Agent { readonly name: string; readonly runtime: AgentRuntime; + readonly sessionId?: string; + readonly pid?: number; readonly channels: string[]; /** Current lifecycle status of the agent. */ readonly status: AgentStatus; @@ -402,6 +435,7 @@ export interface SpawnerSpawnOptions extends SpawnLifecy channels?: string[]; task?: string; model?: string; + harness?: HarnessDefinition; cwd?: string; idleThresholdSecs?: number; /** Optional pre-minted relaycast agent token (`at_live_`, from @@ -426,6 +460,8 @@ export interface AgentRelayOptions { cwd?: string; env?: NodeJS.ProcessEnv; requestTimeoutMs?: number; + /** User-defined harness adapters available to spawnPty/spawn calls. */ + harnesses?: Record; /** * Relaycast workspace ID. Auto-generated when omitted. This is the id used * for relaycast key lookup and surfaced via `RELAY_WORKSPACE_ID` / @@ -467,6 +503,8 @@ type OutputListener = { type InternalAgent = Agent & { _setChannels: (channels: string[]) => void; + _setSessionId: (sessionId: string) => void; + _setPid: (pid: number) => void; }; type InternalAgentResultContract = { @@ -573,6 +611,7 @@ export class AgentRelay { private readonly workspaceName?: string; private readonly relaycastBaseUrl?: string; private readonly defaultPersonaDirs?: string[]; + private readonly harnesses: Record; private relayApiKey?: string; private resolvedWorkspaceId?: string; private client?: AgentRelayClient; @@ -607,6 +646,7 @@ export class AgentRelay { this.defaultChannels = options.channels ?? ['general']; this.requestedWorkspaceId = requestedWorkspaceId; this.workspaceName = options.workspaceName; + this.harnesses = cloneHarnessMap(options.harnesses); if (options.workspaceName && !options.workspaceId) { console.warn( '[AgentRelay] workspaceName without workspaceId is deprecated and will be removed in a future major version. ' + @@ -631,6 +671,24 @@ export class AgentRelay { this.opencode = this.createSpawner('opencode', 'OpenCode', 'headless'); } + registerHarness(name: string, definition: HarnessDefinition): this { + const key = name.trim(); + if (!key) { + throw new Error('registerHarness() expects a non-empty harness name'); + } + const resolved = defineHarnessDefinition(key, definition); + this.harnesses[key] = cloneHarnessDefinition(resolved); + return this; + } + + private resolveHarnessForSpawn(cli: string, explicit?: HarnessDefinition): HarnessDefinition | undefined { + if (explicit) return cloneHarnessDefinition(explicit); + const baseCli = cli.trim().split(':')[0]; + const local = this.harnesses[baseCli]; + if (local) return cloneHarnessDefinition(local); + return getHarnessDefinition(cli); + } + private getWorkspaceRegistryPath(): string { return path.join(this.clientOptions.cwd ?? process.cwd(), '.relay', 'workspaces.json'); } @@ -780,7 +838,7 @@ export class AgentRelay { task: input.task, }; await this.invokeLifecycleHook(input.onStart, lifecycleContext, `spawnPty("${input.name}") onStart`); - let result: { name: string; runtime: AgentRuntime }; + let result: SpawnAgentResult; const resultContract = this.prepareAgentResultContract(input.result); if (resultContract) { this.resultContracts.set(input.name, resultContract as InternalAgentResultContract); @@ -793,6 +851,7 @@ export class AgentRelay { channels, task: input.task, model: input.model, + harness: this.resolveHarnessForSpawn(input.cli, input.harness), cwd: input.cwd, team: input.team, agentToken: input.agentToken, @@ -822,7 +881,13 @@ export class AgentRelay { this.resultContracts.delete(input.name); this.resultContracts.set(result.name, resultContract as InternalAgentResultContract); } - const agent = this.makeAgent(result.name, result.runtime, channels) as Agent; + const agent = this.makeAgent( + result.name, + result.runtime, + channels, + result.sessionId, + result.pid + ) as Agent; this.knownAgents.set(agent.name, agent); await this.invokeLifecycleHook( input.onSuccess, @@ -830,6 +895,8 @@ export class AgentRelay { ...lifecycleContext, name: result.name, runtime: result.runtime, + sessionId: result.sessionId, + pid: result.pid, }, `spawnPty("${input.name}") onSuccess` ); @@ -849,6 +916,7 @@ export class AgentRelay { args: options?.args, channels: options?.channels, model: options?.model, + harness: options?.harness, cwd: options?.cwd, team: options?.team, agentToken: options?.agentToken, @@ -933,6 +1001,7 @@ export class AgentRelay { ...(task !== undefined ? { task } : {}), channels: options.channels, model: spec.model, + harness: options.harness, cwd: spawnCwd, team: options.team, agentToken: options.agentToken, @@ -1089,8 +1158,16 @@ export class AgentRelay { const list = await client.listAgents(); return list.map((entry) => { const existing = this.knownAgents.get(entry.name); - if (existing) return existing; - const agent = this.makeAgent(entry.name, entry.runtime, entry.channels); + if (existing) { + if (entry.sessionId) { + (existing as InternalAgent)._setSessionId(entry.sessionId); + } + if (entry.pid !== undefined) { + (existing as InternalAgent)._setPid(entry.pid); + } + return existing; + } + const agent = this.makeAgent(entry.name, entry.runtime, entry.channels, entry.sessionId, entry.pid); this.knownAgents.set(agent.name, agent); return agent; }); @@ -1376,12 +1453,24 @@ export class AgentRelay { // ── Private helpers ───────────────────────────────────────────────────── - private ensureAgentHandle(name: string, runtime: AgentRuntime = 'pty', channels: string[] = []): Agent { + private ensureAgentHandle( + name: string, + runtime: AgentRuntime = 'pty', + channels: string[] = [], + sessionId?: string, + pid?: number + ): Agent { const existing = this.knownAgents.get(name); if (existing) { + if (sessionId) { + (existing as InternalAgent)._setSessionId(sessionId); + } + if (pid !== undefined) { + (existing as InternalAgent)._setPid(pid); + } return existing; } - const agent = this.makeAgent(name, runtime, channels); + const agent = this.makeAgent(name, runtime, channels, sessionId, pid); this.knownAgents.set(name, agent); return agent; } @@ -1664,7 +1753,7 @@ export class AgentRelay { break; } case 'agent_spawned': { - const agent = this.ensureAgentHandle(event.name, event.runtime); + const agent = this.ensureAgentHandle(event.name, event.runtime, [], event.sessionId, event.pid); this.readyAgents.delete(event.name); this.messageReadyAgents.delete(event.name); this.exitedAgents.delete(event.name); @@ -1725,7 +1814,7 @@ export class AgentRelay { break; } case 'worker_ready': { - const agent = this.ensureAgentHandle(event.name, event.runtime); + const agent = this.ensureAgentHandle(event.name, event.runtime, [], event.sessionId, event.pid); this.readyAgents.add(event.name); this.exitedAgents.delete(event.name); this.idleAgents.delete(event.name); @@ -1966,13 +2055,27 @@ export class AgentRelay { }); } - private makeAgent(name: string, runtime: AgentRuntime, channels: string[]): Agent { + private makeAgent( + name: string, + runtime: AgentRuntime, + channels: string[], + sessionId?: string, + pid?: number + ): Agent { // eslint-disable-next-line @typescript-eslint/no-this-alias const relay = this; let agentChannels = [...channels]; + let agentSessionId = sessionId; + let agentPid = pid; const agent: InternalAgent = { name, runtime, + get sessionId() { + return agentSessionId; + }, + get pid() { + return agentPid; + }, get channels() { return [...agentChannels]; }, @@ -2182,6 +2285,12 @@ export class AgentRelay { _setChannels(nextChannels: string[]) { agentChannels = [...nextChannels]; }, + _setSessionId(nextSessionId: string) { + agentSessionId = nextSessionId; + }, + _setPid(nextPid: number) { + agentPid = nextPid; + }, }; return agent; } @@ -2202,6 +2311,7 @@ export class AgentRelay { channels, task, model: options?.model, + harness: this.resolveHarnessForSpawn(cli, options?.harness), cwd: options?.cwd, idleThresholdSecs: options?.idleThresholdSecs, agentToken: options?.agentToken, @@ -2221,7 +2331,7 @@ export class AgentRelay { task, }; await this.invokeLifecycleHook(options?.onStart, lifecycleContext, `spawn("${name}") onStart`); - let result: { name: string; runtime: AgentRuntime }; + let result: SpawnAgentResult; const resultContract = this.prepareAgentResultContract(options?.result); if (resultContract) { this.resultContracts.set(name, resultContract as InternalAgentResultContract); @@ -2235,6 +2345,7 @@ export class AgentRelay { channels, task, model: options?.model, + harness: this.resolveHarnessForSpawn(cli, options?.harness), cwd: options?.cwd, idleThresholdSecs: options?.idleThresholdSecs, agentToken: options?.agentToken, @@ -2261,7 +2372,13 @@ export class AgentRelay { this.resultContracts.delete(name); this.resultContracts.set(result.name, resultContract as InternalAgentResultContract); } - const agent = this.makeAgent(result.name, result.runtime, channels) as Agent; + const agent = this.makeAgent( + result.name, + result.runtime, + channels, + result.sessionId, + result.pid + ) as Agent; this.knownAgents.set(agent.name, agent); await this.invokeLifecycleHook( options?.onSuccess, @@ -2269,6 +2386,8 @@ export class AgentRelay { ...lifecycleContext, name: result.name, runtime: result.runtime, + sessionId: result.sessionId, + pid: result.pid, }, `spawn("${name}") onSuccess` ); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 232103bfe..0f87e60e2 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -9,6 +9,9 @@ import type { MessageInjectionMode, RestartPolicy, } from './protocol.js'; +import type { HarnessDefinition } from '@agent-relay/workflow-types'; + +export type { HarnessDefinition } from '@agent-relay/workflow-types'; export type JsonSchema = Record | boolean; @@ -19,6 +22,7 @@ export interface SpawnPtyInput { channels?: string[]; task?: string; model?: string; + harness?: HarnessDefinition; cwd?: string; team?: string; shadowOf?: string; @@ -66,6 +70,7 @@ export interface SpawnProviderInput { channels?: string[]; task?: string; model?: string; + harness?: HarnessDefinition; cwd?: string; team?: string; shadowOf?: string; @@ -103,6 +108,7 @@ export interface ListAgent { provider?: HeadlessProvider; cli?: string; model?: string; + sessionId?: string; team?: string; channels: string[]; parent?: string; diff --git a/packages/sdk/src/workflows/README.md b/packages/sdk/src/workflows/README.md index ec15bd395..1bd9c090a 100644 --- a/packages/sdk/src/workflows/README.md +++ b/packages/sdk/src/workflows/README.md @@ -23,26 +23,26 @@ agent-relay run workflow.yaml --workflow deploy ### TypeScript ```typescript -import { workflow } from "@agent-relay/sdk/workflows"; - -const result = await workflow("ship-feature") - .pattern("dag") - .agent("planner", { cli: "claude", role: "Plans implementation" }) - .agent("developer", { cli: "codex", role: "Writes code" }) - .agent("reviewer", { cli: "claude", role: "Reviews code" }) - .step("plan", { - agent: "planner", - task: "Create implementation plan for user authentication", +import { workflow } from '@agent-relay/sdk/workflows'; + +const result = await workflow('ship-feature') + .pattern('dag') + .agent('planner', { cli: 'claude', role: 'Plans implementation' }) + .agent('developer', { cli: 'codex', role: 'Writes code' }) + .agent('reviewer', { cli: 'claude', role: 'Reviews code' }) + .step('plan', { + agent: 'planner', + task: 'Create implementation plan for user authentication', }) - .step("implement", { - agent: "developer", - task: "Implement the plan", - dependsOn: ["plan"], + .step('implement', { + agent: 'developer', + task: 'Implement the plan', + dependsOn: ['plan'], }) - .step("review", { - agent: "reviewer", - task: "Review the implementation", - dependsOn: ["implement"], + .step('review', { + agent: 'reviewer', + task: 'Review the implementation', + dependsOn: ['implement'], }) .run(); @@ -86,10 +86,14 @@ export async function POST(req: Request) { const { prompt, escalate, repo } = await req.json(); const relay = new Relay('AppLead'); - const relaySession = onRelay({ - name: 'AppLead', - instructions: 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.', - }, relay); + const relaySession = onRelay( + { + name: 'AppLead', + instructions: + 'You are the customer-facing lead. Keep the user updated and delegate implementation via Relay when needed.', + }, + relay + ); const model = wrapLanguageModel({ model: openai('gpt-4o-mini'), @@ -127,20 +131,20 @@ A compact end-to-end example app for this pattern lives in `examples/ai-sdk-rela Workflows are defined as `relay.yaml` files: ```yaml -version: "1.0" +version: '1.0' name: my-workflow -description: "Optional description" +description: 'Optional description' swarm: - pattern: dag # Execution pattern (see Patterns below) - maxConcurrency: 3 # Max agents running in parallel - timeoutMs: 3600000 # Global timeout (1 hour) - channel: my-channel # Relay channel for agent communication + pattern: dag # Execution pattern (see Patterns below) + maxConcurrency: 3 # Max agents running in parallel + timeoutMs: 3600000 # Global timeout (1 hour) + channel: my-channel # Relay channel for agent communication agents: - name: backend - cli: claude # claude | codex | gemini | aider | goose | opencode | droid - role: "Backend engineer" + cli: claude # claude | codex | gemini | aider | goose | opencode | droid + role: 'Backend engineer' constraints: model: opus timeoutMs: 600000 @@ -148,33 +152,33 @@ agents: - name: tester cli: codex - role: "Test engineer" - interactive: false # Non-interactive: runs as subprocess, no PTY/messaging + role: 'Test engineer' + interactive: false # Non-interactive: runs as subprocess, no PTY/messaging workflows: - name: build-and-test - onError: retry # fail | skip | retry + onError: retry # fail | skip | retry steps: - name: build-api agent: backend - task: "Build the REST API endpoints for user management" + task: 'Build the REST API endpoints for user management' verification: type: file_exists - value: "src/api/users.ts" + value: 'src/api/users.ts' retries: 1 - name: write-tests agent: tester - task: "Write integration tests for: {{steps.build-api.output}}" + task: 'Write integration tests for: {{steps.build-api.output}}' dependsOn: [build-api] - name: run-tests agent: tester - task: "Run the test suite and report results" + task: 'Run the test suite and report results' dependsOn: [write-tests] verification: type: exit_code - value: "0" + value: '0' errorHandling: strategy: retry @@ -193,19 +197,19 @@ Use `{{variable}}` for user-provided values and `{{steps.STEP_NAME.output}}` for steps: - name: plan agent: planner - task: "Plan implementation for: {{task}}" # User variable + task: 'Plan implementation for: {{task}}' # User variable - name: implement agent: developer dependsOn: [plan] - task: "Implement: {{steps.plan.output}}" # Previous step output + task: 'Implement: {{steps.plan.output}}' # Previous step output ``` User variables are passed via the CLI or programmatically: ```typescript -await runWorkflow("workflow.yaml", { - vars: { task: "Add OAuth2 support" }, +await runWorkflow('workflow.yaml', { + vars: { task: 'Add OAuth2 support' }, }); ``` @@ -213,12 +217,12 @@ await runWorkflow("workflow.yaml", { Each step can include a verification check. Verification is one input to the runner's **completion decision pipeline** — when verification passes, the step completes even without a sentinel marker. -| Type | Description | -|------|-------------| -| `exit_code` | Agent must exit with the specified code (preferred for code-editing steps) | -| `file_exists` | A file must exist at the specified path after the step | -| `output_contains` | Step output must contain the specified string (optional accelerator) | -| `custom` | No-op in the runner; handled by external callers | +| Type | Description | +| ----------------- | -------------------------------------------------------------------------- | +| `exit_code` | Agent must exit with the specified code (preferred for code-editing steps) | +| `file_exists` | A file must exist at the specified path after the step | +| `output_contains` | Step output must contain the specified string (optional accelerator) | +| `custom` | No-op in the runner; handled by external callers | ```yaml # Preferred — deterministic verification @@ -234,6 +238,36 @@ verification: description: "Agent confirms completion (optional fast-path)" ``` +### Custom Harnesses + +Workflows can define CLI harness adapters locally instead of waiting for Relay +to add the harness to its built-in registry. The same serializable adapter is +passed through to interactive `agent-relay` spawning. In TypeScript, this +serializable command-template shape is exported as `CLIHarnessAdapter`; the +runtime-facing lifecycle contract for future stdio/HTTP harnesses is exported +as `HarnessRuntimeAdapter`. + +```yaml +harnesses: + qwen: + binary: qwen + interactiveArgs: ['run', '{modelArgs}', '{args}'] + nonInteractiveArgs: ['run', '--prompt', '{task}', '{args}'] + modelArgs: ['-m', '{model}'] + searchPaths: ['~/.local/bin'] + +agents: + - name: reviewer + cli: qwen + interactive: false + constraints: + model: qwen3-coder +``` + +`{task}` expands to the step prompt, `{model}` expands to the configured model, +`{modelArgs}` expands to the rendered model arguments, and `{args}` expands to +Relay's extra argument list. + ### Completion Decision Pipeline The runner uses a multi-signal pipeline to decide step completion: @@ -243,15 +277,15 @@ The runner uses a multi-signal pipeline to decide step completion: 3. **Evidence-based completion** — channel messages, file artifacts, and exit codes are collected as evidence (`completed_by_evidence`) 4. **Marker fast-path** — `STEP_COMPLETE:` still works as an accelerator but is never required -| Completion State | Meaning | -|---|---| -| `completed_verified` | Deterministic verification passed | -| `completed_by_owner_decision` | Owner approved the step | -| `completed_by_evidence` | Evidence-based completion | -| `retry_requested_by_owner` | Owner requested retry | -| `failed_verification` | Verification explicitly failed | -| `failed_owner_decision` | Owner rejected the step | -| `failed_no_evidence` | No verification, no owner decision, no evidence | +| Completion State | Meaning | +| ----------------------------- | ----------------------------------------------- | +| `completed_verified` | Deterministic verification passed | +| `completed_by_owner_decision` | Owner approved the step | +| `completed_by_evidence` | Evidence-based completion | +| `retry_requested_by_owner` | Owner requested retry | +| `failed_verification` | Verification explicitly failed | +| `failed_owner_decision` | Owner rejected the step | +| `failed_no_evidence` | No verification, no owner decision, no evidence | **Review parsing is tolerant:** The runner accepts semantically equivalent outputs like "Approved", "Complete", "LGTM" — not just exact `REVIEW_DECISION: APPROVE` strings. @@ -261,80 +295,80 @@ The `swarm.pattern` field controls how agents are coordinated: ### Core Patterns -| Pattern | Description | -|---------|-------------| -| `dag` | Directed acyclic graph — steps run based on dependency edges (default) | -| `fan-out` | All agents run in parallel | -| `pipeline` | Sequential chaining of steps | -| `hub-spoke` | Central hub coordinates spoke agents | -| `consensus` | Agents vote on decisions | -| `mesh` | Full communication graph between agents | -| `handoff` | Sequential handoff between agents | -| `cascade` | Waterfall with phase gates | -| `debate` | Agents propose and counter-argue | -| `hierarchical` | Multi-level reporting structure | +| Pattern | Description | +| -------------- | ---------------------------------------------------------------------- | +| `dag` | Directed acyclic graph — steps run based on dependency edges (default) | +| `fan-out` | All agents run in parallel | +| `pipeline` | Sequential chaining of steps | +| `hub-spoke` | Central hub coordinates spoke agents | +| `consensus` | Agents vote on decisions | +| `mesh` | Full communication graph between agents | +| `handoff` | Sequential handoff between agents | +| `cascade` | Waterfall with phase gates | +| `debate` | Agents propose and counter-argue | +| `hierarchical` | Multi-level reporting structure | ### Data Processing Patterns -| Pattern | Description | -|---------|-------------| -| `map-reduce` | Split work into chunks (mappers), process in parallel, aggregate results (reducers) | -| `scatter-gather` | Fan out requests to workers, collect and synthesize responses | +| Pattern | Description | +| ---------------- | ----------------------------------------------------------------------------------- | +| `map-reduce` | Split work into chunks (mappers), process in parallel, aggregate results (reducers) | +| `scatter-gather` | Fan out requests to workers, collect and synthesize responses | ### Supervision & Quality Patterns -| Pattern | Description | -|---------|-------------| -| `supervisor` | Monitor agent monitors workers, restarts on failure, manages health | +| Pattern | Description | +| ------------ | ------------------------------------------------------------------------- | +| `supervisor` | Monitor agent monitors workers, restarts on failure, manages health | | `reflection` | Agent produces output, critic reviews and provides feedback for iteration | -| `verifier` | Producer agents submit work to verifier agents for validation | +| `verifier` | Producer agents submit work to verifier agents for validation | ### Adversarial & Validation Patterns -| Pattern | Description | -|---------|-------------| -| `red-team` | Attacker agents probe for weaknesses, defender agents respond | -| `auction` | Auctioneer broadcasts tasks, agents bid based on capability/cost | +| Pattern | Description | +| ---------- | ---------------------------------------------------------------- | +| `red-team` | Attacker agents probe for weaknesses, defender agents respond | +| `auction` | Auctioneer broadcasts tasks, agents bid based on capability/cost | ### Resilience Patterns -| Pattern | Description | -|---------|-------------| -| `escalation` | Start with fast/cheap agents, escalate to more capable on failure | -| `saga` | Distributed transactions with compensating actions on failure | -| `circuit-breaker` | Primary agent with fallback chain, fail fast and recover | +| Pattern | Description | +| ----------------- | ----------------------------------------------------------------- | +| `escalation` | Start with fast/cheap agents, escalate to more capable on failure | +| `saga` | Distributed transactions with compensating actions on failure | +| `circuit-breaker` | Primary agent with fallback chain, fail fast and recover | ### Collaborative Patterns -| Pattern | Description | -|---------|-------------| +| Pattern | Description | +| ------------ | -------------------------------------------------------------------- | | `blackboard` | Shared workspace where agents contribute incrementally to a solution | -| `swarm` | Emergent behavior from simple agent rules (neighbor communication) | +| `swarm` | Emergent behavior from simple agent rules (neighbor communication) | ### Auto-Selection by Role When `swarm.pattern` is omitted, the coordinator auto-selects based on agent roles. Patterns are checked in priority order below (first match wins): -| Priority | Pattern | Required Roles/Config | -|----------|---------|----------------------| -| 1 | `dag` | Steps with `dependsOn` | -| 2 | `consensus` | Uses `coordination.consensusStrategy` config | -| 3 | `map-reduce` | `mapper` + `reducer` | -| 4 | `red-team` | (`attacker` OR `red-team`) + (`defender` OR `blue-team`) | -| 5 | `reflection` | `critic` | -| 6 | `escalation` | `tier-1`, `tier-2`, etc. | -| 7 | `auction` | `auctioneer` | -| 8 | `saga` | `saga-orchestrator` OR `compensate-handler` | -| 9 | `circuit-breaker` | `fallback`, `backup`, OR `primary` | -| 10 | `blackboard` | `blackboard` OR `shared-workspace` | -| 11 | `swarm` | `hive-mind` OR `swarm-agent` | -| 12 | `verifier` | `verifier` | -| 13 | `supervisor` | `supervisor` | -| 14 | `hierarchical` | `lead` (with 4+ agents) | -| 15 | `hub-spoke` | `hub` OR `coordinator` | -| 16 | `pipeline` | Unique agents per step, 3+ steps | -| 17 | `fan-out` | Default fallback | +| Priority | Pattern | Required Roles/Config | +| -------- | ----------------- | -------------------------------------------------------- | +| 1 | `dag` | Steps with `dependsOn` | +| 2 | `consensus` | Uses `coordination.consensusStrategy` config | +| 3 | `map-reduce` | `mapper` + `reducer` | +| 4 | `red-team` | (`attacker` OR `red-team`) + (`defender` OR `blue-team`) | +| 5 | `reflection` | `critic` | +| 6 | `escalation` | `tier-1`, `tier-2`, etc. | +| 7 | `auction` | `auctioneer` | +| 8 | `saga` | `saga-orchestrator` OR `compensate-handler` | +| 9 | `circuit-breaker` | `fallback`, `backup`, OR `primary` | +| 10 | `blackboard` | `blackboard` OR `shared-workspace` | +| 11 | `swarm` | `hive-mind` OR `swarm-agent` | +| 12 | `verifier` | `verifier` | +| 13 | `supervisor` | `supervisor` | +| 14 | `hierarchical` | `lead` (with 4+ agents) | +| 15 | `hub-spoke` | `hub` OR `coordinator` | +| 16 | `pipeline` | Unique agents per step, 3+ steps | +| 17 | `fan-out` | Default fallback | ## Error Handling @@ -344,20 +378,20 @@ Patterns are checked in priority order below (first match wins): steps: - name: risky-step agent: worker - task: "Do something that might fail" - retries: 3 # Retry up to 3 times on failure - timeoutMs: 300000 # 5 minute timeout + task: 'Do something that might fail' + retries: 3 # Retry up to 3 times on failure + timeoutMs: 300000 # 5 minute timeout ``` ### Workflow-Level The `onError` field on a workflow controls what happens when a step fails: -| Value | Behavior | -|-------|----------| -| `fail` / `fail-fast` | Stop immediately, skip downstream steps | -| `skip` / `continue` | Skip downstream dependents, continue independent steps | -| `retry` | Retry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available | +| Value | Behavior | +| -------------------- | --------------------------------------------------------------------------------------------------------------- | +| `fail` / `fail-fast` | Stop immediately, skip downstream steps | +| `skip` / `continue` | Skip downstream dependents, continue independent steps | +| `retry` | Retry the step; deterministic gates ask a workflow agent to repair before each retry when an agent is available | ### Global @@ -377,19 +411,19 @@ Retry-mode workflows are repair-aware by default. Deterministic step failures, v Six pre-built workflow templates are included: -| Template | Pattern | Description | -|----------|---------|-------------| -| `feature-dev` | hub-spoke | Plan, implement, review, and finalize a feature | -| `bug-fix` | hub-spoke | Investigate, patch, validate, and document a bug fix | -| `code-review` | fan-out | Parallel multi-reviewer assessment with consolidated findings | -| `security-audit` | pipeline | Scan, triage, remediate, and verify security issues | -| `refactor` | hierarchical | Analyze, plan, execute, and validate a refactor | -| `documentation` | handoff | Research, draft, review, and publish documentation | +| Template | Pattern | Description | +| ---------------- | ------------ | ------------------------------------------------------------- | +| `feature-dev` | hub-spoke | Plan, implement, review, and finalize a feature | +| `bug-fix` | hub-spoke | Investigate, patch, validate, and document a bug fix | +| `code-review` | fan-out | Parallel multi-reviewer assessment with consolidated findings | +| `security-audit` | pipeline | Scan, triage, remediate, and verify security issues | +| `refactor` | hierarchical | Analyze, plan, execute, and validate a refactor | +| `documentation` | handoff | Research, draft, review, and publish documentation | ### Using Templates ```typescript -import { TemplateRegistry } from "@agent-relay/sdk/workflows"; +import { TemplateRegistry } from '@agent-relay/sdk/workflows'; const registry = new TemplateRegistry(); @@ -397,17 +431,14 @@ const registry = new TemplateRegistry(); const templates = await registry.listTemplates(); // Load and run a template -const config = await registry.loadTemplate("feature-dev"); +const config = await registry.loadTemplate('feature-dev'); const runner = new WorkflowRunner(); const result = await runner.execute(config, undefined, { - task: "Add WebSocket support to the API", + task: 'Add WebSocket support to the API', }); // Install a custom template from a URL -await registry.installExternalTemplate( - "https://example.com/my-template.yaml", - "my-template" -); +await registry.installExternalTemplate('https://example.com/my-template.yaml', 'my-template'); ``` ## TypeScript Builder API @@ -415,50 +446,50 @@ await registry.installExternalTemplate( The builder constructs a `RelayYamlConfig` object and can run it, export it as YAML, or return the raw config. ```typescript -import { workflow } from "@agent-relay/sdk/workflows"; +import { workflow } from '@agent-relay/sdk/workflows'; // Build and run -const result = await workflow("my-workflow") - .pattern("dag") +const result = await workflow('my-workflow') + .pattern('dag') .maxConcurrency(3) .timeout(60 * 60 * 1000) - .channel("my-channel") - .agent("backend", { - cli: "claude", - role: "Backend engineer", - model: "opus", + .channel('my-channel') + .agent('backend', { + cli: 'claude', + role: 'Backend engineer', + model: 'opus', retries: 2, }) - .agent("frontend", { - cli: "codex", - role: "Frontend engineer", - interactive: false, // Non-interactive subprocess mode + .agent('frontend', { + cli: 'codex', + role: 'Frontend engineer', + interactive: false, // Non-interactive subprocess mode }) - .step("api", { - agent: "backend", - task: "Build REST API", - verification: { type: "output_contains", value: "API_READY" }, + .step('api', { + agent: 'backend', + task: 'Build REST API', + verification: { type: 'output_contains', value: 'API_READY' }, }) - .step("ui", { - agent: "frontend", - task: "Build the UI", - dependsOn: ["api"], + .step('ui', { + agent: 'frontend', + task: 'Build the UI', + dependsOn: ['api'], }) - .onError("retry", { maxRetries: 2, retryDelayMs: 5000 }) + .onError('retry', { maxRetries: 2, retryDelayMs: 5000 }) .run(); // Or export to YAML -const yaml = workflow("my-workflow") - .pattern("dag") - .agent("worker", { cli: "claude" }) - .step("task1", { agent: "worker", task: "Do something" }) +const yaml = workflow('my-workflow') + .pattern('dag') + .agent('worker', { cli: 'claude' }) + .step('task1', { agent: 'worker', task: 'Do something' }) .toYaml(); // Or get the raw config object -const config = workflow("my-workflow") - .pattern("dag") - .agent("worker", { cli: "claude" }) - .step("task1", { agent: "worker", task: "Do something" }) +const config = workflow('my-workflow') + .pattern('dag') + .agent('worker', { cli: 'claude' }) + .step('task1', { agent: 'worker', task: 'Do something' }) .toConfig(); ``` @@ -514,11 +545,11 @@ config = ( For full control, use the `WorkflowRunner` directly: ```typescript -import { WorkflowRunner } from "@agent-relay/sdk/workflows"; +import { WorkflowRunner } from '@agent-relay/sdk/workflows'; const runner = new WorkflowRunner({ - cwd: "/path/to/project", // Working directory (default: process.cwd()) - relay: { port: 3000 }, // AgentRelay options (optional) + cwd: '/path/to/project', // Working directory (default: process.cwd()) + relay: { port: 3000 }, // AgentRelay options (optional) }); // Listen to events (broker:event fires frequently — filter it out for cleaner output) @@ -528,9 +559,9 @@ runner.on((event) => { }); // Parse and execute -const config = await runner.parseYamlFile("workflow.yaml"); -const run = await runner.execute(config, "workflow-name", { - task: "Build the feature", +const config = await runner.parseYamlFile('workflow.yaml'); +const run = await runner.execute(config, 'workflow-name', { + task: 'Build the feature', }); // Pause / resume / abort @@ -545,11 +576,11 @@ const resumed = await runner.resume(run.id); ### Zero-Config Convenience Function ```typescript -import { runWorkflow } from "@agent-relay/sdk/workflows"; +import { runWorkflow } from '@agent-relay/sdk/workflows'; -const result = await runWorkflow("workflow.yaml", { - workflow: "deploy", - vars: { environment: "staging" }, +const result = await runWorkflow('workflow.yaml', { + workflow: 'deploy', + vars: { environment: 'staging' }, onEvent: (event) => { if (event.type !== 'broker:event') console.log(event.type); }, @@ -568,7 +599,7 @@ coordination: - name: all-reviews-done waitFor: [review-arch, review-security, review-correctness] timeoutMs: 900000 - consensusStrategy: majority # majority | unanimous | quorum + consensusStrategy: majority # majority | unanimous | quorum ``` ### Shared State @@ -577,22 +608,22 @@ Agents can share state during execution: ```yaml state: - backend: memory # memory | redis | database + backend: memory # memory | redis | database ttlMs: 86400000 namespace: my-workflow ``` ## Supported Agent CLIs -| CLI | Description | -|-----|-------------| -| `claude` | Claude Code (Anthropic) | -| `codex` | Codex CLI (OpenAI) | -| `gemini` | Gemini CLI (Google) | -| `aider` | Aider coding assistant | -| `goose` | Goose AI assistant | -| `opencode` | OpenCode CLI | -| `droid` | Droid CLI | +| CLI | Description | +| ---------- | ----------------------- | +| `claude` | Claude Code (Anthropic) | +| `codex` | Codex CLI (OpenAI) | +| `gemini` | Gemini CLI (Google) | +| `aider` | Aider coding assistant | +| `goose` | Goose AI assistant | +| `opencode` | OpenCode CLI | +| `droid` | Droid CLI | ## Non-Interactive Agents @@ -604,55 +635,55 @@ By default, agents run in interactive PTY mode with full relay messaging. For wo agents: - name: lead cli: claude - role: "Coordinates work" + role: 'Coordinates work' # interactive: true (default) — full PTY, relay messaging, /exit detection - name: worker cli: codex - role: "Executes tasks" - interactive: false # Runs "codex exec ", captures stdout + role: 'Executes tasks' + interactive: false # Runs "codex exec ", captures stdout ``` ### TypeScript ```typescript -workflow("fan-out-analysis") - .pattern("fan-out") - .agent("lead", { cli: "claude", role: "Coordinator" }) - .agent("worker-1", { cli: "codex", interactive: false, role: "Analyst" }) - .agent("worker-2", { cli: "codex", interactive: false, role: "Analyst" }) - .step("analyze-1", { agent: "worker-1", task: "Analyze module A" }) - .step("analyze-2", { agent: "worker-2", task: "Analyze module B" }) - .step("synthesize", { - agent: "lead", - task: "Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}", - dependsOn: ["analyze-1", "analyze-2"], +workflow('fan-out-analysis') + .pattern('fan-out') + .agent('lead', { cli: 'claude', role: 'Coordinator' }) + .agent('worker-1', { cli: 'codex', interactive: false, role: 'Analyst' }) + .agent('worker-2', { cli: 'codex', interactive: false, role: 'Analyst' }) + .step('analyze-1', { agent: 'worker-1', task: 'Analyze module A' }) + .step('analyze-2', { agent: 'worker-2', task: 'Analyze module B' }) + .step('synthesize', { + agent: 'lead', + task: 'Combine: {{steps.analyze-1.output}} + {{steps.analyze-2.output}}', + dependsOn: ['analyze-1', 'analyze-2'], }) .run(); ``` ### How It Works -| Aspect | Interactive (default) | Non-Interactive | -|--------|----------------------|-----------------| -| Execution | Full PTY with stdin/stdout | `child_process.spawn()` with piped stdio | -| CLI invocation | Standard interactive session | One-shot mode (`claude -p`, `codex exec`, etc.) | -| Relay messaging | Can send/receive messages | No messaging — excluded from topology edges | -| Self-termination | Must output `/exit` | Process exits naturally when done | -| Output capture | PTY output buffer | stdout capture | -| Overhead | Higher (PTY, echo verification, SIGWINCH) | Lower (simple subprocess) | +| Aspect | Interactive (default) | Non-Interactive | +| ---------------- | ----------------------------------------- | ----------------------------------------------- | +| Execution | Full PTY with stdin/stdout | `child_process.spawn()` with piped stdio | +| CLI invocation | Standard interactive session | One-shot mode (`claude -p`, `codex exec`, etc.) | +| Relay messaging | Can send/receive messages | No messaging — excluded from topology edges | +| Self-termination | Must output `/exit` | Process exits naturally when done | +| Output capture | PTY output buffer | stdout capture | +| Overhead | Higher (PTY, echo verification, SIGWINCH) | Lower (simple subprocess) | ### Non-Interactive CLI Commands -| CLI | Command | Notes | -|-----|---------|-------| -| `claude` | `claude -p ""` | Print mode, exits after response | -| `codex` | `codex exec ""` | One-shot execution | -| `gemini` | `gemini -p ""` | Prompt mode | -| `opencode` | `opencode --prompt ""` | One-shot prompt | -| `droid` | `droid exec ""` | One-shot execution | -| `aider` | `aider --message "" --yes-always --no-git` | Auto-approve, skip git | -| `goose` | `goose run --text "" --no-session` | Text mode, no session file | +| CLI | Command | Notes | +| ---------- | ------------------------------------------------ | -------------------------------- | +| `claude` | `claude -p ""` | Print mode, exits after response | +| `codex` | `codex exec ""` | One-shot execution | +| `gemini` | `gemini -p ""` | Prompt mode | +| `opencode` | `opencode --prompt ""` | One-shot prompt | +| `droid` | `droid exec ""` | One-shot execution | +| `aider` | `aider --message "" --yes-always --no-git` | Auto-approve, skip git | +| `goose` | `goose run --text "" --no-session` | Text mode, no session file | ### When to Use @@ -709,9 +740,9 @@ Add `idleNudge` to your swarm config: swarm: pattern: hub-spoke idleNudge: - nudgeAfterMs: 120000 # 2 min before first nudge (default) - escalateAfterMs: 120000 # 2 min after nudge before force-release (default) - maxNudges: 1 # Nudges before escalation (default) + nudgeAfterMs: 120000 # 2 min before first nudge (default) + escalateAfterMs: 120000 # 2 min after nudge before force-release (default) + maxNudges: 1 # Nudges before escalation (default) ``` All built-in templates include idle nudging with these defaults. @@ -727,9 +758,9 @@ All built-in templates include idle nudging with these defaults. The runner emits two new events for idle nudging: -| Event | Description | -|-------|-------------| -| `step:nudged` | Fired when a nudge message is sent to an idle agent | +| Event | Description | +| --------------------- | ------------------------------------------------------------- | +| `step:nudged` | Fired when a nudge message is sent to an idle agent | | `step:force-released` | Fired when an agent is force-released after exhausting nudges | ## Automatic Step Owner and Review diff --git a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts new file mode 100644 index 000000000..6f346cb4b --- /dev/null +++ b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts @@ -0,0 +1,201 @@ +import { afterEach, describe, expect, expectTypeOf, it } from 'vitest'; + +import { + buildModelArgs, + registerHarnessAdapter, + restoreHarnessAdapters, + snapshotHarnessAdapters, +} from '../../cli-registry.js'; +import { WorkflowBuilder } from '../builder.js'; +import { buildCommand } from '../process-spawner.js'; +import { WorkflowRunner } from '../runner.js'; +import type { HarnessRuntimeAdapter } from '../../harness-runtime.js'; +import type { ProcessBackend } from '../types.js'; +import type { CLIHarnessAdapter } from '../../cli-registry.js'; + +const registrySnapshot = snapshotHarnessAdapters(); + +describe('workflow harness adapters', () => { + afterEach(() => { + restoreHarnessAdapters(registrySnapshot); + }); + + it('builds built-in commands from declarative harness config', () => { + expect(buildCommand('codex', [], 'do the work')).toEqual([ + 'codex', + 'exec', + '--dangerously-bypass-approvals-and-sandbox', + 'do the work', + ]); + }); + + it('types CLI adapters separately from runtime harness adapters', () => { + const cliAdapter: CLIHarnessAdapter = { + binary: 'unit-agent', + nonInteractiveArgs: ['run', '{task}'], + }; + const runtimeAdapter: HarnessRuntimeAdapter = { + kind: 'http', + initHarness: async () => ({ sessionId: 's1', pid: 123 }), + receiveMessage: async () => undefined, + sendMessage: async () => undefined, + }; + + expectTypeOf(cliAdapter).toMatchTypeOf(); + expectTypeOf(runtimeAdapter).toMatchTypeOf(); + }); + + it('lets SDK callers register a harness command adapter', () => { + registerHarnessAdapter('unit-harness-a', { + binaries: ['unit-agent'], + nonInteractiveArgs: ['run', '--prompt', '{task}', '{args}'], + modelArgs: ['-m', '{model}'], + }); + + const modelArgs = buildModelArgs('unit-harness-a', 'model-1'); + expect(modelArgs).toEqual(['-m', 'model-1']); + expect(buildCommand('unit-harness-a', modelArgs, 'do the work')).toEqual([ + 'unit-agent', + 'run', + '--prompt', + 'do the work', + '-m', + 'model-1', + ]); + }); + + it('serializes workflow-local harnesses from the TypeScript builder', () => { + const config = new WorkflowBuilder('custom-harness') + .harness('unit-harness-b', { + binary: 'unit-b', + nonInteractiveArgs: ['--task', '{{task}}', '{{args}}'], + modelArgs: ['--model-id', '{{model}}'], + }) + .agent('worker', { cli: 'unit-harness-b', interactive: false, model: 'model-b' }) + .step('work', { agent: 'worker', task: 'ship it' }) + .toConfig(); + + expect(config.harnesses?.['unit-harness-b']).toEqual({ + binary: 'unit-b', + nonInteractiveArgs: ['--task', '{{task}}', '{{args}}'], + modelArgs: ['--model-id', '{{model}}'], + }); + }); + + it('keeps harnesses declared in parsed YAML scoped to workflow execution', () => { + const runner = new WorkflowRunner({ cwd: process.cwd() }); + runner.parseYamlString(` +version: "1.0" +name: yaml-harness +swarm: + pattern: dag +harnesses: + unit-harness-c: + binary: unit-c + nonInteractiveArgs: ["exec", "{task}", "{args}"] + modelArgs: ["--m", "{model}"] +agents: + - name: worker + cli: unit-harness-c + interactive: false +workflows: + - name: main + steps: + - name: work + agent: worker + task: do it +`); + + expect(buildModelArgs('unit-harness-c', 'model-c')).toEqual(['--model', 'model-c']); + expect(() => buildCommand('unit-harness-c', [], 'do it')).toThrow( + 'Unknown or non-executable CLI: unit-harness-c' + ); + }); + + it('uses workflow-scoped harnesses for process backend command resolution', async () => { + const commands: string[] = []; + const backend: ProcessBackend = { + async createEnvironment(label) { + return { + id: label, + homeDir: '/tmp', + async exec(command) { + commands.push(command); + return { output: 'done', exitCode: 0 }; + }, + async uploadFile() {}, + async destroy() {}, + }; + }, + }; + const runner = new WorkflowRunner({ cwd: process.cwd(), processBackend: backend }); + const config = runner.parseYamlString(` +version: "1.0" +name: yaml-harness-run +trajectories: false +swarm: + pattern: dag +harnesses: + unit-harness-c: + binary: unit-c + nonInteractiveArgs: ["exec", "{task}", "{args}"] + modelArgs: ["--m", "{model}"] +agents: + - name: worker + cli: unit-harness-c + interactive: false + constraints: + model: model-c +workflows: + - name: main + steps: + - name: work + agent: worker + task: do it +`); + + await runner.execute(config); + + expect(commands).toHaveLength(1); + expect(commands[0]).toContain('unit-c exec'); + expect(commands[0]).toContain('--m model-c'); + expect(buildModelArgs('unit-harness-c', 'model-c')).toEqual(['--model', 'model-c']); + }); + + it('lets binary override inherited adapter binaries', () => { + registerHarnessAdapter('company-cursor-wrapper', { + adapter: 'cursor', + binary: 'company-cursor', + searchPaths: ['~/company/bin'], + }); + + expect(buildCommand('company-cursor-wrapper', [], 'do the work')).toEqual([ + 'company-cursor', + '--force', + '-p', + 'do the work', + ]); + }); + + it('ignores blank binary overrides when inheriting adapter binaries', () => { + registerHarnessAdapter('company-cursor-wrapper', { + adapter: 'cursor', + binary: ' ', + }); + + expect(buildCommand('company-cursor-wrapper', [], 'do the work')).toEqual([ + 'cursor-agent', + '--force', + '-p', + 'do the work', + ]); + }); + + it('rejects empty base harness keys after model suffix normalization', () => { + expect(() => + registerHarnessAdapter(':bad', { + binary: 'bad', + }) + ).toThrow('Harness name must be a non-empty string'); + }); +}); diff --git a/packages/sdk/src/workflows/builder.ts b/packages/sdk/src/workflows/builder.ts index f2eb1f8f7..45f12d46f 100644 --- a/packages/sdk/src/workflows/builder.ts +++ b/packages/sdk/src/workflows/builder.ts @@ -10,6 +10,7 @@ import type { CoordinationConfig, DryRunReport, ErrorHandlingConfig, + HarnessDefinition, IdleNudgeConfig, PathDefinition, RelayYamlConfig, @@ -149,12 +150,27 @@ export interface WorkflowRunOptions { cloudApiToken?: string; /** Environment secrets to forward to cloud agents. */ envSecrets?: Record; + /** User-defined harness adapters available to this run. */ + harnesses?: Record; /** Polling interval in ms for cloud run status checks. */ cloudPollIntervalMs?: number; /** Callback invoked when the cloud run status changes. */ onCloudStatusChange?: (status: string, runId: string) => void; } +function cloneHarnessDefinition(definition: HarnessDefinition): HarnessDefinition { + return { + ...definition, + ...(definition.binaries ? { binaries: [...definition.binaries] } : {}), + ...(definition.interactiveArgs ? { interactiveArgs: [...definition.interactiveArgs] } : {}), + ...(definition.nonInteractiveArgs ? { nonInteractiveArgs: [...definition.nonInteractiveArgs] } : {}), + ...(definition.modelArgs ? { modelArgs: [...definition.modelArgs] } : {}), + ...(definition.bypassAliases ? { bypassAliases: [...definition.bypassAliases] } : {}), + ...(definition.searchPaths ? { searchPaths: [...definition.searchPaths] } : {}), + ...(definition.aliases ? { aliases: [...definition.aliases] } : {}), + }; +} + // ── WorkflowBuilder ───────────────────────────────────────────────────────── /** @@ -181,6 +197,7 @@ export class WorkflowBuilder { private _channel?: string; private _idleNudge?: IdleNudgeConfig; private _paths?: PathDefinition[]; + private _harnesses?: Record; private _agents: AgentDefinition[] = []; private _steps: WorkflowStep[] = []; private _errorHandling?: ErrorHandlingConfig; @@ -299,6 +316,28 @@ export class WorkflowBuilder { return this; } + /** + * Register a workflow-local harness adapter. + * + * The adapter is serialized into `harnesses` so YAML and SDK-created + * workflows can use `agent(..., { cli: name })` without Relay adding that + * harness to its built-in registry. + */ + harness(name: string, definition: HarnessDefinition): this { + const key = name.trim(); + if (!key) { + throw new Error('.harness() expects a non-empty harness name'); + } + if (!definition || typeof definition !== 'object' || Array.isArray(definition)) { + throw new Error('.harness() expects a HarnessDefinition object'); + } + this._harnesses = { + ...(this._harnesses ?? {}), + [key]: cloneHarnessDefinition(definition), + }; + return this; + } + /** Add an agent definition. */ agent(name: string, options: AgentOptions): this { const def: AgentDefinition = { @@ -461,6 +500,11 @@ export class WorkflowBuilder { if (this._paths !== undefined && this._paths.length > 0) { config.paths = this._paths.map((p) => ({ ...p })); } + if (this._harnesses !== undefined && Object.keys(this._harnesses).length > 0) { + config.harnesses = Object.fromEntries( + Object.entries(this._harnesses).map(([name, harness]) => [name, cloneHarnessDefinition(harness)]) + ); + } if (this._maxConcurrency !== undefined) config.swarm.maxConcurrency = this._maxConcurrency; if (this._timeoutMs !== undefined) config.swarm.timeoutMs = this._timeoutMs; if (this._channel !== undefined) config.swarm.channel = this._channel; @@ -497,6 +541,7 @@ export class WorkflowBuilder { relay: options.relay, executor: options.executor, envSecrets: options.envSecrets, + harnesses: options.harnesses, db, }); diff --git a/packages/sdk/src/workflows/index.ts b/packages/sdk/src/workflows/index.ts index 95a8ed6dd..d78780ff8 100644 --- a/packages/sdk/src/workflows/index.ts +++ b/packages/sdk/src/workflows/index.ts @@ -47,6 +47,25 @@ export { executeApiStep, type ApiExecutorOptions } from './api-executor.js'; export type { CloudRunOptions } from './cloud-runner.js'; export * from './proxy-env.js'; export * from './budget-tracker.js'; +export { + defineHarnessAdapter, + getHarnessDefinition, + getHarnessAdapter, + registerHarnessAdapter, + registerHarnessAdapters, + type CLIHarnessAdapter, + type HarnessAdapter, +} from '../cli-registry.js'; +export type { + HarnessInitContext, + HarnessInitResult, + HarnessMessageContext, + HarnessRegistrationContext, + HarnessRegistrationResult, + HarnessReleaseContext, + HarnessRelayMessage, + HarnessRuntimeAdapter, +} from '../harness-runtime.js'; export { applySiblingLinks, buildSiblingLinkScript } from './sibling-links.js'; export type { SiblingLink, SiblingLinkOptions } from './sibling-links.js'; export { diff --git a/packages/sdk/src/workflows/process-backend-executor.ts b/packages/sdk/src/workflows/process-backend-executor.ts index d578fcea7..8ca26a005 100644 --- a/packages/sdk/src/workflows/process-backend-executor.ts +++ b/packages/sdk/src/workflows/process-backend-executor.ts @@ -11,6 +11,7 @@ */ import { buildCommand } from './process-spawner.js'; +import { buildModelArgs } from '../cli-registry.js'; import type { ProcessBackend, AgentDefinition, WorkflowStep, RunnerStepExecutor } from './types.js'; function shellEscape(value: string): string { @@ -26,6 +27,8 @@ function commandToShell(argv: string[]): string { export interface ProcessBackendExecutorOptions { /** Env vars injected into every step (e.g. auth tokens, relayfile config). */ env?: Record; + /** Optional command builder for runner-scoped harness definitions. */ + buildCommand?: (agentDef: AgentDefinition, task: string) => string[]; } export function createProcessBackendExecutor( @@ -48,8 +51,9 @@ export function createProcessBackendExecutor( ); } - const extraArgs = agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : []; - const argv = buildCommand(agentDef.cli, extraArgs, resolvedTask); + const argv = + options.buildCommand?.(agentDef, resolvedTask) ?? + buildCommand(agentDef.cli, buildModelArgs(agentDef.cli, agentDef.constraints?.model), resolvedTask); const commandString = commandToShell(argv); const env = await backend.createEnvironment(step.name); diff --git a/packages/sdk/src/workflows/process-spawner.ts b/packages/sdk/src/workflows/process-spawner.ts index 2c3c74fd7..98b5238dd 100644 --- a/packages/sdk/src/workflows/process-spawner.ts +++ b/packages/sdk/src/workflows/process-spawner.ts @@ -1,10 +1,10 @@ import { spawn as cpSpawn } from 'node:child_process'; import type { ChildProcess, SpawnOptions } from 'node:child_process'; -import { getCliDefinition } from '../cli-registry.js'; +import { buildModelArgs, getCliDefinition } from '../cli-registry.js'; import { resolveCliSync } from '../cli-resolver.js'; import { runVerification } from './verification.js'; -import type { AgentCli, AgentDefinition, VerificationCheck } from './types.js'; +import type { AgentDefinition, VerificationCheck } from './types.js'; export interface SpawnOutcome { output: string; @@ -40,7 +40,7 @@ export interface ProcessSpawner { buildCommand(agent: AgentDefinition, task: string): SpawnCommand; } -function resolveNonInteractiveCli(cli: AgentCli): AgentCli { +function resolveNonInteractiveCli(cli: string): string { if (cli !== 'cursor') { return cli; } @@ -49,7 +49,7 @@ function resolveNonInteractiveCli(cli: AgentCli): AgentCli { return (resolved?.binary as 'cursor-agent' | 'agent' | undefined) ?? 'agent'; } -export function buildCommand(cli: AgentCli, extraArgs: string[] = [], task: string): string[] { +export function buildCommand(cli: string, extraArgs: string[] = [], task: string): string[] { if (cli === 'api') { throw new Error('cli "api" uses direct API calls, not a subprocess command'); } @@ -179,7 +179,7 @@ async function runCommand(command: SpawnCommand, opts: ShellOpts): Promise { - const extraArgs = agent.constraints?.model ? ['--model', agent.constraints.model] : []; + const extraArgs = buildModelArgs(agent.cli, agent.constraints?.model); const [bin, ...args] = buildCommand(agent.cli, extraArgs, task); return { bin, args }; }; diff --git a/packages/sdk/src/workflows/run.ts b/packages/sdk/src/workflows/run.ts index 9e3614b24..d4e382b1c 100644 --- a/packages/sdk/src/workflows/run.ts +++ b/packages/sdk/src/workflows/run.ts @@ -1,5 +1,5 @@ import type { AgentRelayOptions } from '../relay.js'; -import type { DryRunReport, TrajectoryConfig, WorkflowRunRow } from './types.js'; +import type { DryRunReport, HarnessDefinition, TrajectoryConfig, WorkflowRunRow } from './types.js'; import { WorkflowRunner, type WorkflowEventListener } from './runner.js'; import { createDefaultEventLogger } from './default-logger.js'; import { formatDryRunReport } from './dry-run-format.js'; @@ -29,6 +29,8 @@ export interface RunWorkflowOptions { startFrom?: string; /** Previous run ID whose cached step outputs are used with startFrom. */ previousRunId?: string; + /** User-defined harness adapters available to this run. */ + harnesses?: Record; } /** @@ -54,6 +56,7 @@ export async function runWorkflow( const runner = new WorkflowRunner({ cwd: options.cwd, relay: options.relay, + harnesses: options.harnesses, }); const config = await runner.parseYamlFile(yamlPath); diff --git a/packages/sdk/src/workflows/runner.ts b/packages/sdk/src/workflows/runner.ts index 1acdeb77e..3882965f7 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -28,7 +28,7 @@ import { parse as parseYaml } from 'yaml'; import { stripAnsi as stripAnsiFn } from '../pty.js'; import type { BrokerEvent } from '../protocol.js'; import { resolveSpawnPolicy } from '../spawn-from-env.js'; -import { getCliDefinition } from '../cli-registry.js'; +import { defineHarnessAdapter, getCliDefinition, type CliDefinition } from '../cli-registry.js'; import { resolveCliSync } from '../cli-resolver.js'; import { buildNormalizedProxyEnv, @@ -75,8 +75,8 @@ import { } from './template-resolver.js'; import type { AccessPreset, - AgentCli, AgentDefinition, + HarnessDefinition, AgentPermissions, AgentPreset, CompletionEvidenceChannelOrigin, @@ -310,6 +310,8 @@ export interface WorkflowRunnerOptions { * When neither is set, the broker spawns local child processes (default). */ processBackend?: ProcessBackend; + /** User-defined harness adapters available to this runner. */ + harnesses?: Record; } // ── Internal step state ───────────────────────────────────────────────────── @@ -440,7 +442,14 @@ function resolveCursorCli(): 'cursor-agent' | 'agent' { return (resolved?.binary as 'cursor-agent' | 'agent') ?? 'agent'; } -function getWorkflowSdkSpawner(relay: AgentRelay, cli: AgentCli): AgentSpawner | null { +function normalizeWorkflowHarnessKey(cli: string): string | undefined { + const trimmed = cli.trim(); + if (!trimmed) return undefined; + const base = (trimmed.includes(':') ? trimmed.split(':')[0] : trimmed).trim(); + return base || undefined; +} + +function getWorkflowSdkSpawner(relay: AgentRelay, cli: string): AgentSpawner | null { switch (cli) { case 'claude': return relay.claude; @@ -555,6 +564,7 @@ export class WorkflowRunner { private budgetTracker?: BudgetTracker; private static readonly PTY_TASK_ARG_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB private readonly processBackend?: ProcessBackend; + private readonly harnesses?: Record; constructor(options: WorkflowRunnerOptions = {}) { this.db = options.db ?? new InMemoryWorkflowDb(); @@ -565,10 +575,17 @@ export class WorkflowRunner { this.workersPath = path.join(this.cwd, '.agent-relay', 'team', 'workers.json'); this.executor = options.executor; this.processBackend = options.processBackend; + this.harnesses = options.harnesses; this.envSecrets = options.envSecrets; if (!this.executor && this.processBackend) { this.executor = createProcessBackendExecutor(this.processBackend, { env: this.envSecrets, + buildCommand: (agentDef, resolvedTask) => + this.buildWorkflowProcessCommand( + agentDef.cli, + this.buildWorkflowModelArgs(agentDef.cli, agentDef.constraints?.model), + resolvedTask + ), }); } this.templateResolver = new TemplateResolver(); @@ -1739,6 +1756,15 @@ export class WorkflowRunner { return 'openai'; } + const harnessProxyProvider = this.getWorkflowCliDefinition(agentDef.cli)?.proxyProvider; + if ( + harnessProxyProvider === 'openai' || + harnessProxyProvider === 'anthropic' || + harnessProxyProvider === 'openrouter' + ) { + return harnessProxyProvider; + } + if (configuredProviders.length === 1) { const [onlyProvider] = configuredProviders; if (onlyProvider === 'openai' || onlyProvider === 'anthropic' || onlyProvider === 'openrouter') { @@ -2075,6 +2101,74 @@ export class WorkflowRunner { return config; } + private getScopedHarnessDefinition( + cli: string + ): { name: string; definition: HarnessDefinition } | undefined { + const base = normalizeWorkflowHarnessKey(cli); + if (!base) return undefined; + + const harnessMaps = [this.currentConfig?.harnesses, this.harnesses, this.relayOptions.harnesses]; + for (const harnesses of harnessMaps) { + if (!harnesses) continue; + + const direct = harnesses[base]; + if (direct) { + return { name: base, definition: direct }; + } + + for (const [name, definition] of Object.entries(harnesses)) { + for (const alias of definition.aliases ?? []) { + if (normalizeWorkflowHarnessKey(alias) === base) { + return { name, definition }; + } + } + } + } + + return undefined; + } + + private getScopedCliDefinition(cli: string): CliDefinition | undefined { + const match = this.getScopedHarnessDefinition(cli); + return match ? defineHarnessAdapter(match.name, match.definition) : undefined; + } + + private getWorkflowCliDefinition(cli: string): CliDefinition | undefined { + return this.getScopedCliDefinition(cli) ?? getCliDefinition(cli); + } + + private buildWorkflowModelArgs(cli: string, model: string | undefined): string[] { + if (!model) return []; + return this.getWorkflowCliDefinition(cli)?.modelArgs?.(model) ?? ['--model', model]; + } + + private buildWorkflowProcessCommand(cli: string, extraArgs: string[] = [], task: string): string[] { + if (cli === 'api') { + throw new Error('cli "api" uses direct API calls, not a subprocess command'); + } + + const scopedDefinition = this.getScopedCliDefinition(cli); + const resolvedCli = !scopedDefinition && cli === 'cursor' ? resolveCursorCli() : cli; + const definition = scopedDefinition ?? this.getWorkflowCliDefinition(resolvedCli); + if (!definition || definition.binaries.length === 0) { + throw new Error(`Unknown or non-executable CLI: ${resolvedCli}`); + } + + return [definition.binaries[0], ...definition.nonInteractiveArgs(task, extraArgs)]; + } + + private buildWorkflowNonInteractiveCommand( + cli: string, + task: string, + extraArgs: string[] = [] + ): { cmd: string; args: string[] } { + const [cmd, ...args] = this.buildWorkflowProcessCommand(cli, extraArgs, task); + return { + cmd, + args, + }; + } + private normalizeLegacyPermissionConfig(config: RelayYamlConfig): RelayYamlConfig { const legacyPermissions = ( config as RelayYamlConfig & { @@ -2182,6 +2276,53 @@ export class WorkflowRunner { throw new Error(`${source}: "permissions.profiles" must be an object when provided`); } } + if ( + c.harnesses !== undefined && + (typeof c.harnesses !== 'object' || c.harnesses === null || Array.isArray(c.harnesses)) + ) { + throw new Error(`${source}: "harnesses" must be an object when provided`); + } + for (const [name, harness] of Object.entries((c.harnesses ?? {}) as Record)) { + if (!name.trim()) { + throw new Error(`${source}: harness names must not be empty`); + } + if (typeof harness !== 'object' || harness === null || Array.isArray(harness)) { + throw new Error(`${source}: harness "${name}" must be an object`); + } + const h = harness as Record; + const validateHarnessStringArray = (value: unknown, field: string): void => { + if (value === undefined) return; + if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) { + throw new Error(`${source}: harness "${name}".${field} must be an array of strings`); + } + }; + if (h.binary !== undefined && typeof h.binary !== 'string') { + throw new Error(`${source}: harness "${name}".binary must be a string when provided`); + } + validateHarnessStringArray(h.binaries, 'binaries'); + validateHarnessStringArray(h.interactiveArgs, 'interactiveArgs'); + validateHarnessStringArray(h.nonInteractiveArgs, 'nonInteractiveArgs'); + validateHarnessStringArray(h.modelArgs, 'modelArgs'); + validateHarnessStringArray(h.bypassAliases, 'bypassAliases'); + validateHarnessStringArray(h.searchPaths, 'searchPaths'); + validateHarnessStringArray(h.aliases, 'aliases'); + if (h.bypassFlag !== undefined && typeof h.bypassFlag !== 'string') { + throw new Error(`${source}: harness "${name}".bypassFlag must be a string when provided`); + } + if (h.ignoreExitCode !== undefined && typeof h.ignoreExitCode !== 'boolean') { + throw new Error(`${source}: harness "${name}".ignoreExitCode must be a boolean when provided`); + } + if ( + h.proxyProvider !== undefined && + h.proxyProvider !== 'openai' && + h.proxyProvider !== 'anthropic' && + h.proxyProvider !== 'openrouter' + ) { + throw new Error( + `${source}: harness "${name}".proxyProvider must be one of openai, anthropic, openrouter` + ); + } + } for (const agent of c.agents ?? []) { if (typeof agent !== 'object' || agent === null) { @@ -2843,6 +2984,7 @@ export class WorkflowRunner { // Validate config (catches cycles, missing deps, invalid steps, etc.) this.validateConfig(resolved); const runtimeConfig = this.applyReliabilityDefaults(resolved); + this.validateConfig(runtimeConfig); const permissionResult = this.validatePermissions( runtimeConfig.agents, @@ -3012,6 +3154,7 @@ export class WorkflowRunner { const resolvedConfig = this.applyReliabilityDefaults( vars ? this.resolveVariables(run.config, vars) : run.config ); + this.validateConfig(resolvedConfig); // Resolve path definitions (same as execute()) so workdir lookups work on resume const pathResult = this.resolvePathDefinitions(resolvedConfig.paths, this.cwd); @@ -3080,7 +3223,6 @@ export class WorkflowRunner { // Initialize trajectory recording this.trajectory = new WorkflowTrajectory(config.trajectories, runId, this.cwd); - try { await this.updateRunStatus(runId, 'running'); if (!isResume) { @@ -3146,6 +3288,11 @@ export class WorkflowRunner { brokerName, channels: relaycastDisabled ? [] : [channel], env: this.getRelayEnv(), + harnesses: { + ...(this.relayOptions.harnesses ?? {}), + ...(this.harnesses ?? {}), + ...(config.harnesses ?? {}), + }, // Workflows spawn agents across multiple waves; each spawn requires a PTY + // Relaycast registration. 60s is too tight when the broker is saturated with // long-running PTY processes from earlier steps. 120s gives room to breathe. @@ -6320,7 +6467,7 @@ export class WorkflowRunner { * Delegates to the consolidated CLI registry for per-CLI arg formats. */ static buildNonInteractiveCommand( - cli: AgentCli, + cli: string, task: string, extraArgs: string[] = [] ): { cmd: string; args: string[] } { @@ -6337,7 +6484,7 @@ export class WorkflowRunner { */ private static resolveAgentDef(def: AgentDefinition): AgentDefinition { // Resolve "cursor" alias to whichever cursor agent binary is in PATH - const resolvedCli: AgentCli = def.cli === 'cursor' ? resolveCursorCli() : def.cli; + const resolvedCli: string = def.cli === 'cursor' ? resolveCursorCli() : def.cli; if (!def.preset) return resolvedCli !== def.cli ? { ...def, cli: resolvedCli } : def; const nonInteractivePresets: AgentPreset[] = ['worker', 'reviewer', 'analyst']; @@ -6391,7 +6538,7 @@ export class WorkflowRunner { timeoutMs?: number ): Promise { const agentName = `${step.name}-${this.generateShortId()}`; - const modelArgs = agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : []; + const modelArgs = this.buildWorkflowModelArgs(agentDef.cli, agentDef.constraints?.model); // Append strict deliverable enforcement — non-interactive agents MUST produce // clear, structured output since there's no opportunity for follow-up or clarification. @@ -6416,7 +6563,7 @@ export class WorkflowRunner { '- Skip steps or leave work incomplete\n' + '- Output only status messages without the actual deliverable content'; - const { cmd, args } = WorkflowRunner.buildNonInteractiveCommand( + const { cmd, args } = this.buildWorkflowNonInteractiveCommand( agentDef.cli, taskWithDeliverable, modelArgs @@ -6569,7 +6716,7 @@ export class WorkflowRunner { return; } - const cliDef = getCliDefinition(agentDef.cli); + const cliDef = this.getWorkflowCliDefinition(agentDef.cli); if (code !== 0 && code !== null && !cliDef?.ignoreExitCode) { const stderr = stderrChunks.join(''); reject( diff --git a/packages/sdk/src/workflows/schema.json b/packages/sdk/src/workflows/schema.json index c04325384..e56f7988e 100644 --- a/packages/sdk/src/workflows/schema.json +++ b/packages/sdk/src/workflows/schema.json @@ -33,6 +33,13 @@ "$ref": "#/definitions/PathDefinition" } }, + "harnesses": { + "type": "object", + "description": "Workflow-local harness adapters keyed by agent cli name.", + "additionalProperties": { + "$ref": "#/definitions/HarnessDefinition" + } + }, "swarm": { "$ref": "#/definitions/SwarmConfig" }, @@ -245,18 +252,25 @@ }, "AgentCli": { "type": "string", - "enum": [ - "claude", - "codex", - "gemini", - "aider", - "goose", - "opencode", - "droid", - "cursor", - "cursor-agent", - "agent" - ] + "description": "Built-in CLI or a key from top-level harnesses." + }, + "HarnessDefinition": { + "type": "object", + "additionalProperties": false, + "properties": { + "adapter": { "type": "string" }, + "binary": { "type": "string" }, + "binaries": { "type": "array", "items": { "type": "string" } }, + "interactiveArgs": { "type": "array", "items": { "type": "string" } }, + "nonInteractiveArgs": { "type": "array", "items": { "type": "string" } }, + "modelArgs": { "type": "array", "items": { "type": "string" } }, + "bypassFlag": { "type": "string" }, + "bypassAliases": { "type": "array", "items": { "type": "string" } }, + "searchPaths": { "type": "array", "items": { "type": "string" } }, + "ignoreExitCode": { "type": "boolean" }, + "proxyProvider": { "type": "string", "enum": ["openai", "anthropic", "openrouter"] }, + "aliases": { "type": "array", "items": { "type": "string" } } + } }, "PermissionProfile": { "type": "object", diff --git a/packages/sdk/src/workflows/types.ts b/packages/sdk/src/workflows/types.ts index 099e1fd27..a5286f789 100644 --- a/packages/sdk/src/workflows/types.ts +++ b/packages/sdk/src/workflows/types.ts @@ -10,6 +10,7 @@ export type { AccessPreset, AgentCli, AgentDefinition, + HarnessDefinition, AgentPermissions, AgentPreset, NetworkPermission, @@ -24,6 +25,7 @@ export type { import type { AccessPreset, AgentDefinition, + HarnessDefinition, AgentPermissions, NetworkPermission, PermissionProfileDefinition, @@ -48,6 +50,8 @@ export interface RelayYamlConfig { paths?: PathDefinition[]; swarm: SwarmConfig; agents: AgentDefinition[]; + /** User-defined harness adapters keyed by `agents[].cli`. */ + harnesses?: Record; workflows?: WorkflowDefinition[]; coordination?: CoordinationConfig; state?: StateConfig; diff --git a/packages/workflow-types/src/index.ts b/packages/workflow-types/src/index.ts index 31d8ec7c5..77b11ae68 100644 --- a/packages/workflow-types/src/index.ts +++ b/packages/workflow-types/src/index.ts @@ -84,7 +84,9 @@ export interface AgentDefinition { skills?: string; } -export type AgentCli = +export type AgentCli = KnownAgentCli | (string & {}); + +export type KnownAgentCli = | 'claude' | 'codex' | 'gemini' @@ -97,6 +99,53 @@ export type AgentCli = | 'agent' | 'api'; +/** + * Serializable harness adapter config. + * + * SDK and workflow authors can define harnesses without changing Relay's + * built-in CLI registry. `interactiveArgs`, `nonInteractiveArgs`, and + * `modelArgs` are argv templates. Supported placeholders include `{task}`, + * `{{task}}`, `{model}`, `{{model}}`, `{args}`, `{{args}}`, `{modelArgs}`, + * `{mcpArgs}`, `{sessionArgs}`, and `{bypass}` where applicable; vector + * placeholders expand to argument arrays instead of a single string. + */ +export interface HarnessDefinition { + /** + * Lifecycle adapter id for broker-owned behavior. Defaults to the harness + * name. Use a built-in id such as `codex`, `claude`, or `opencode` when a + * custom binary should keep that harness's session/MCP behavior. + */ + adapter?: string; + /** Primary binary to execute. Shorthand for `binaries: [binary]`. */ + binary?: string; + /** Binary names to try, in preference order. Defaults to the harness name. */ + binaries?: string[]; + /** + * Interactive PTY argv template used by `agent-relay` spawning. Defaults to + * `['{bypass}', '{modelArgs}', '{mcpArgs}', '{args}', '{sessionArgs}']`. + */ + interactiveArgs?: string[]; + /** + * Non-interactive argv template. Defaults to `['{task}', '{args}']`, which + * runs the binary with the task followed by any model/extra args. + */ + nonInteractiveArgs?: string[]; + /** Model argv template. Defaults to `['--model', '{model}']`. */ + modelArgs?: string[]; + /** Bypass flag used by spawn-from-env and unattended helpers. */ + bypassFlag?: string; + /** Alternative bypass flags accepted by the harness. */ + bypassAliases?: string[]; + /** Extra install locations to check before common fallback paths. */ + searchPaths?: string[]; + /** Treat non-zero process exits as successful output capture. */ + ignoreExitCode?: boolean; + /** Credential proxy provider used when credentials.proxy is enabled. */ + proxyProvider?: 'openai' | 'anthropic' | 'openrouter'; + /** Additional harness names that should resolve to this adapter. */ + aliases?: string[]; +} + /** Resource and behavioral constraints for an agent. */ export interface AgentConstraints { maxTokens?: number; diff --git a/plugins/codex-relay-skill/README.md b/plugins/codex-relay-skill/README.md index d21f187cf..f1815ed09 100644 --- a/plugins/codex-relay-skill/README.md +++ b/plugins/codex-relay-skill/README.md @@ -61,7 +61,7 @@ features.codex_hooks = true [mcp_servers.relaycast] command = "npx" -args = ["-y", "@relaycast/mcp"] +args = ["-y", "agent-relay", "mcp"] env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://api.relaycast.dev", RELAY_AGENT_TYPE = "agent" } ``` diff --git a/plugins/codex-relay-skill/codex-config/config.toml b/plugins/codex-relay-skill/codex-config/config.toml index 2a50ffa29..860c3ec64 100644 --- a/plugins/codex-relay-skill/codex-config/config.toml +++ b/plugins/codex-relay-skill/codex-config/config.toml @@ -5,5 +5,5 @@ features.codex_hooks = true [mcp_servers.relaycast] command = "npx" -args = ["-y", "@relaycast/mcp"] +args = ["-y", "agent-relay", "mcp"] env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://api.relaycast.dev", RELAY_AGENT_TYPE = "agent" } diff --git a/plugins/codex-relay-skill/scripts/setup.sh b/plugins/codex-relay-skill/scripts/setup.sh index b4e5248f9..56c7f844a 100755 --- a/plugins/codex-relay-skill/scripts/setup.sh +++ b/plugins/codex-relay-skill/scripts/setup.sh @@ -154,7 +154,7 @@ ensure_relaycast_mcp_block() { args_seen = 0 env_seen = 0 command_line = "command = \"npx\"" - args_line = "args = [\"-y\", \"@relaycast/mcp\"]" + args_line = "args = [\"-y\", \"agent-relay\", \"mcp\"]" env_line = "env = { RELAY_API_KEY = \"\", RELAY_BASE_URL = \"https://api.relaycast.dev\", RELAY_AGENT_TYPE = \"agent\" }" } function write_missing_keys() { diff --git a/plugins/gemini-relay-extension/package-lock.json b/plugins/gemini-relay-extension/package-lock.json index 5c5ed9698..ba4b2fa2a 100644 --- a/plugins/gemini-relay-extension/package-lock.json +++ b/plugins/gemini-relay-extension/package-lock.json @@ -8,1167 +8,9 @@ "name": "gemini-relay-extension", "version": "0.1.0", "license": "MIT", - "dependencies": { - "@relaycast/mcp": "^0.5.1" - }, "engines": { "node": ">=18.0.0" } - }, - "node_modules/@hono/node-server": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", - "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@relaycast/mcp": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@relaycast/mcp/-/mcp-0.5.2.tgz", - "integrity": "sha512-WqjdvFHpX/g+8N0JfQFSpQ4j4FLIrLKcY478SPNlLvEduJf2+J3V9/6uRHZOMl0wbQZMmSqzKYVZGdG7ndznvg==", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0", - "@relaycast/sdk": "0.5.2", - "@relaycast/types": "0.5.2", - "express": "^5.2.1", - "zod": "^4.3.6" - }, - "bin": { - "relaycast-mcp": "dist/stdio.js" - } - }, - "node_modules/@relaycast/sdk": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-0.5.2.tgz", - "integrity": "sha512-gD6QXdlvZGzS5VJEAdDB4ZxIg5KiIgQJq5AtgrztEiydhpHbzSnNGP4PXizqQjKMOV+JO47PWHvfX9E19LvffA==", - "dependencies": { - "@relaycast/types": "0.5.2", - "zod": "^4.3.6" - } - }, - "node_modules/@relaycast/types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@relaycast/types/-/types-0.5.2.tgz", - "integrity": "sha512-KBi/GzLYSSmEJCzXkTrUqhzGjKPjGHgTVOoVBdkAXvOF6/KuSZegYRfuAqMamB/DRXwb34fpUvdeopiZg0UE5g==", - "dependencies": { - "zod": "^4.3.6" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", - "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", - "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", - "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } } } } diff --git a/plugins/gemini-relay-extension/package.json b/plugins/gemini-relay-extension/package.json index f3eb2807b..857749d0f 100644 --- a/plugins/gemini-relay-extension/package.json +++ b/plugins/gemini-relay-extension/package.json @@ -30,9 +30,6 @@ "scripts": { "check": "node --check relay-server.js && sh -n hooks/after-tool-inbox.sh && sh -n hooks/after-agent-inbox.sh && sh -n hooks/before-model-inject.sh && sh -n hooks/session-start.sh && sh -n hooks/session-end.sh" }, - "dependencies": { - "@relaycast/mcp": "^1.0.0" - }, "engines": { "node": ">=18.0.0" } diff --git a/plugins/gemini-relay-extension/relay-server.js b/plugins/gemini-relay-extension/relay-server.js index 489121e81..a41d93c40 100755 --- a/plugins/gemini-relay-extension/relay-server.js +++ b/plugins/gemini-relay-extension/relay-server.js @@ -1,10 +1,10 @@ #!/usr/bin/env node import fs from "node:fs"; +import { spawn } from "node:child_process"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { startStdio } from "@relaycast/mcp/dist/transports.js"; const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url)); const ENV_FILE = path.join(EXTENSION_DIR, ".env"); @@ -72,20 +72,30 @@ writeStateFile({ updatedAt: new Date().toISOString(), }); -await startStdio({ - apiKey: workspaceKey, - baseUrl, - agentName, - agentToken, - agentType: "agent", - strictAgentName: true, -}); +await runAgentRelayMcp(); function readEnv(name) { const value = process.env[name]; return typeof value === "string" && value.trim() ? value.trim() : ""; } +function runAgentRelayMcp() { + return new Promise((resolve, reject) => { + const child = spawn("npx", ["-y", "agent-relay", "mcp"], { + stdio: "inherit", + env: process.env, + }); + child.on("error", reject); + child.on("exit", (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`agent-relay mcp exited with ${signal ?? code}`)); + }); + }); +} + function loadDotEnv(filePath) { if (!fs.existsSync(filePath)) { return; diff --git a/scripts/watch-cli-tools.sh b/scripts/watch-cli-tools.sh index 66b1170b3..98ab936a2 100644 --- a/scripts/watch-cli-tools.sh +++ b/scripts/watch-cli-tools.sh @@ -73,7 +73,7 @@ fi export RUST_LOG=debug export RELAY_DASHBOARD_STATIC_DIR=/Users/khaliqgant/Projects/agent-workforce/relay-dashboard/packages/dashboard/out -export RELAYCAST_MCP_COMMAND="node /Users/khaliqgant/Projects/agent-workforce/relaycast/packages/mcp/dist/stdio.js" +export RELAYCAST_MCP_COMMAND="node $CLI_REPO_DIR/dist/src/cli/relaycast-mcp.js" export AGENT_RELAY_BIN=/Users/khaliqgant/Projects/agent-workforce/relay-cli-uses-broker/target/debug/agent-relay-broker export RELAY_DASHBOARD_BINARY=/Users/khaliqgant/Projects/agent-workforce/relay-dashboard/packages/dashboard-server/dist/start.js diff --git a/src/cli/bootstrap.test.ts b/src/cli/bootstrap.test.ts index ae17ac4e5..68dca1434 100644 --- a/src/cli/bootstrap.test.ts +++ b/src/cli/bootstrap.test.ts @@ -13,6 +13,7 @@ const expectedLeafCommands = [ 'version', 'update', 'bridge', + 'mcp', 'spawn', 'agents', 'who', @@ -108,6 +109,7 @@ describe('bootstrap CLI', () => { 'version', 'update', 'bridge', + 'mcp', 'spawn', 'agents', 'who', diff --git a/src/cli/bootstrap.ts b/src/cli/bootstrap.ts index 1e2a97ad9..9060725ed 100644 --- a/src/cli/bootstrap.ts +++ b/src/cli/bootstrap.ts @@ -122,6 +122,7 @@ function propagateVersionsToChildren(): void { // `telemetry` is here so enable/disable/status never triggers PostHog init on // the very run that's toggling the preference. const TELEMETRY_MANAGEMENT_COMMANDS = new Set(['telemetry']); +const STDIO_SERVER_COMMANDS = new Set(['mcp']); // Commands for which we run the background update-check. Keep this narrow to // the interactive / long-lived commands — we don't want short-lived programmatic @@ -299,6 +300,13 @@ export function createProgram(options: { name?: string } = {}): Command { registerNewCommands(program); registerRmCommands(program); registerLogCommands(program); + program + .command('mcp') + .description('Start the Agent Relay MCP server over stdio') + .action(async () => { + const { optionsFromEnv, startAgentRelayMcpStdio } = await import('./relaycast-mcp.js'); + await startAgentRelayMcpStdio(optionsFromEnv()); + }); return program; } @@ -311,7 +319,9 @@ function maybeRunUpdateCheck(version: string, argv: string[]): void { function shouldSkipTelemetryInit(argv: string[]): boolean { const commandName = argv[2]; - return Boolean(commandName && TELEMETRY_MANAGEMENT_COMMANDS.has(commandName)); + return Boolean( + commandName && (TELEMETRY_MANAGEMENT_COMMANDS.has(commandName) || STDIO_SERVER_COMMANDS.has(commandName)) + ); } /** diff --git a/src/cli/commands/agent-management.ts b/src/cli/commands/agent-management.ts index 9f9b6f65c..a5a441649 100644 --- a/src/cli/commands/agent-management.ts +++ b/src/cli/commands/agent-management.ts @@ -19,6 +19,14 @@ interface WorkerInfo { name: string; runtime?: string; pid?: number; + sessionId?: string; +} + +interface SpawnAgentResult { + name?: string; + runtime?: string; + pid?: number; + sessionId?: string; } interface SetModelResult { @@ -410,7 +418,7 @@ export function registerAgentManagementCommands( const continueFrom = options.continueFrom ?? (options.continue ? name : undefined); try { - await client.spawnPty({ + const spawnResult = (await client.spawnPty({ name, cli, channels: ['general'], @@ -422,7 +430,7 @@ export function registerAgentManagementCommands( shadowMode: options.shadowMode as ShadowMode | undefined, continueFrom, skipRelayPrompt: options.skipRelayPrompt, - }); + })) as SpawnAgentResult | undefined; let agents: WorkerInfo[] = []; try { agents = await client.listAgents(); @@ -431,8 +439,13 @@ export function registerAgentManagementCommands( deps.error(`Warning: spawned ${name}, but failed to refresh agent list: ${detail}`); } const spawned = agents.find((agent) => agent.name === name); - if (spawned?.pid) { + const sessionId = spawnResult?.sessionId ?? spawned?.sessionId; + if (spawned?.pid && sessionId) { + deps.log(`Spawned agent: ${name} (pid: ${spawned.pid}, session: ${sessionId})`); + } else if (spawned?.pid) { deps.log(`Spawned agent: ${name} (pid: ${spawned.pid})`); + } else if (sessionId) { + deps.log(`Spawned agent: ${name} (session: ${sessionId})`); } else { deps.log(`Spawned agent: ${name}`); } diff --git a/src/cli/relaycast-mcp.startup.test.ts b/src/cli/relaycast-mcp.startup.test.ts index 84efaab6b..d95f80572 100644 --- a/src/cli/relaycast-mcp.startup.test.ts +++ b/src/cli/relaycast-mcp.startup.test.ts @@ -7,6 +7,7 @@ type LoadOptions = { }; type RelayBehavior = { + createWorkspaceImpl: (name: string) => Promise>; inboxImpl: (token: string) => Promise; registerImpl: (input: { name: string; type?: string }) => Promise<{ name?: string; token: string }>; }; @@ -25,41 +26,59 @@ async function loadRelaycastMcpModule(options: LoadOptions = {}) { } const serverInstances: FakeMcpServer[] = []; - const wsBridgeInstances: FakeWsBridge[] = []; - const subscriptionInstances: FakeSubscriptionManager[] = []; - const channelToolGetters: Array<() => unknown> = []; - const resourceGetters: Array<{ getAgentClient: () => unknown; getRelay: () => unknown }> = []; - const telemetry = { capture: vi.fn() }; - const behavior: RelayBehavior = { - inboxImpl: vi.fn(async () => ({ items: [] })), - registerImpl: vi.fn(async ({ name }) => ({ name, token: `at_live_${name}` })), - }; + const wsClientInstances: FakeWsClient[] = []; const relayInstances: Array<{ config: Record; - origin: Record; - inbox: ReturnType; registerOrRotate: ReturnType; + agentsList: ReturnType; + spawn: ReturnType; + release: ReturnType; as: ReturnType; }> = []; + const behavior: RelayBehavior = { + createWorkspaceImpl: vi.fn(async () => ({ + apiKey: 'rk_live_created', + workspaceName: 'Test Workspace', + })), + inboxImpl: vi.fn(async () => ({ + unreadChannels: [], + mentions: [], + unreadDms: [], + recentReactions: [], + })), + registerImpl: vi.fn(async ({ name }) => ({ name, token: `at_live_${name}` })), + }; + + class FakeResourceTemplate { + constructor( + public readonly template: string, + public readonly options: unknown + ) {} + } - class FakeSubscriptionManager { - clear = vi.fn(); + class FakeWsClient { + readonly handlers = new Map void>>(); + readonly connect = vi.fn(); + readonly disconnect = vi.fn(); - constructor() { - subscriptionInstances.push(this); + constructor(public readonly config: Record) { + if (options.wsClientThrows) { + throw new Error('ws init failed'); + } + wsClientInstances.push(this); } - } - class FakeWsBridge { - start = vi.fn(); - stop = vi.fn(); + on(event: string, handler: (event: unknown) => void): () => void { + const handlers = this.handlers.get(event) ?? new Set(); + handlers.add(handler); + this.handlers.set(event, handlers); + return () => handlers.delete(handler); + } - constructor( - public readonly client: unknown, - public readonly subscriptions: FakeSubscriptionManager, - public readonly onResourceUpdated: (uri: string) => void - ) { - wsBridgeInstances.push(this); + emit(event: unknown): void { + for (const handler of this.handlers.get('*') ?? []) { + handler(event); + } } } @@ -68,16 +87,17 @@ async function loadRelaycastMcpModule(options: LoadOptions = {}) { class FakeMcpServer { readonly tools = new Map Promise }>(); readonly prompts = new Map Promise }>(); + readonly resources = new Map< + string, + { uriOrTemplate: unknown; config: unknown; handler: (...args: any[]) => Promise } + >(); readonly connect = vi.fn(async (_transport: unknown) => { if (options.connectThrows) { throw new Error('stdio connect failed'); } }); readonly server: { - _requestHandlers: Map< - string, - (req: unknown, extra: unknown) => Promise<{ tools?: Array> }> - >; + _requestHandlers: Map Promise>; setRequestHandler: ReturnType; sendResourceUpdated: ReturnType; }; @@ -91,7 +111,7 @@ async function loadRelaycastMcpModule(options: LoadOptions = {}) { vi.fn(async () => ({ tools: [ { - name: 'post_message', + name: 'message.post', title: 'Post Message', execution: { hidden: true }, outputSchema: { type: 'object' }, @@ -103,8 +123,15 @@ async function loadRelaycastMcpModule(options: LoadOptions = {}) { ], ]), setRequestHandler: vi.fn( - (_schema: unknown, handler: (req: unknown, extra: unknown) => Promise) => { - this.listToolsHandler = handler; + (schema: unknown, handler: (req: unknown, extra: unknown) => Promise) => { + const method = + (schema as { method?: string; type?: string }).method ?? (schema as { type?: string }).type; + if (method) { + this.server._requestHandlers.set(method, handler); + } + if (method === 'tools/list') { + this.listToolsHandler = handler; + } } ), sendResourceUpdated: vi.fn(async (_payload: unknown) => undefined), @@ -119,70 +146,88 @@ async function loadRelaycastMcpModule(options: LoadOptions = {}) { registerPrompt(name: string, config: unknown, handler: () => Promise): void { this.prompts.set(name, { config, handler }); } - } - const createInternalRelayCast = vi.fn( - (config: Record, origin: Record) => { - const inbox = vi.fn(async (token?: string) => behavior.inboxImpl(String(token ?? ''))); - const registerOrRotate = vi.fn(async (input: { name: string; type?: string }) => - behavior.registerImpl(input) - ); - const as = vi.fn((token: string) => ({ - inbox: vi.fn(async () => behavior.inboxImpl(token)), - token, - })); - relayInstances.push({ config, origin, inbox, registerOrRotate, as }); - return { - agents: { registerOrRotate }, - as, - }; + registerResource( + name: string, + uriOrTemplate: unknown, + config: unknown, + handler: (...args: any[]) => Promise + ): void { + this.resources.set(name, { uriOrTemplate, config, handler }); } - ); + } - const createInternalWsClient = vi.fn((config: Record, origin: Record) => { - if (options.wsClientThrows) { - throw new Error('ws init failed'); - } - return { config, origin }; + const createAgentClient = (token: string) => ({ + token, + send: vi.fn(async (channel: string, text: string) => ({ id: 'msg_1', channel, text })), + messages: vi.fn(async () => []), + reply: vi.fn(async (messageId: string, text: string) => ({ id: 'reply_1', messageId, text })), + thread: vi.fn(async () => ({ parent: {}, replies: [] })), + dm: vi.fn(async (to: string, text: string) => ({ id: 'dm_1', to, text })), + dms: { + conversations: vi.fn(async () => []), + messages: vi.fn(async () => []), + createGroup: vi.fn(async () => ({ id: 'group_1' })), + sendMessage: vi.fn(async () => ({ id: 'group_msg_1' })), + }, + channels: { + create: vi.fn(async (data: unknown) => data), + list: vi.fn(async () => []), + join: vi.fn(async () => ({})), + leave: vi.fn(async () => ({})), + invite: vi.fn(async () => ({})), + setTopic: vi.fn(async (channel: string, topic: string) => ({ channel, topic })), + archive: vi.fn(async () => ({})), + }, + react: vi.fn(async () => ({})), + unreact: vi.fn(async () => ({})), + search: vi.fn(async () => []), + inbox: vi.fn(async () => behavior.inboxImpl(token)), + markRead: vi.fn(async () => ({})), + readers: vi.fn(async () => []), }); - const enablePiggyback = vi.fn(); - const registerResourceDefinitions = vi.fn( - (_server: unknown, getAgentClient: () => unknown, getRelay: () => unknown) => { - resourceGetters.push({ getAgentClient, getRelay }); - } - ); - const registerChannelTools = vi.fn((_server: unknown, getAgentClient: () => unknown) => { - channelToolGetters.push(getAgentClient); - }); - const registerMessagingTools = vi.fn(); - const registerFeatureTools = vi.fn(); - const registerProgrammabilityTools = vi.fn(); - const createMcpTelemetry = vi.fn(() => telemetry); - const createInitialSession = vi.fn((initial: Record) => ({ - ...initial, - wsBridge: null, - subscriptions: null, - wsInitAttempted: false, - })); + const RelayCast = vi.fn(function (this: unknown, config: Record) { + const registerOrRotate = vi.fn(async (input: { name: string; type?: string }) => + behavior.registerImpl(input) + ); + const agentsList = vi.fn(async () => []); + const spawn = vi.fn(async (input: unknown) => ({ spawned: true, input })); + const release = vi.fn(async (input: { name: string; reason?: string; deleteAgent?: boolean }) => ({ + name: input.name, + released: true, + deleted: Boolean(input.deleteAgent), + reason: input.reason ?? null, + })); + const as = vi.fn((token: string) => createAgentClient(token)); + relayInstances.push({ config, registerOrRotate, agentsList, spawn, release, as }); + return { + agents: { + registerOrRotate, + list: agentsList, + spawn, + release, + }, + as, + }; + }) as any; + RelayCast.createWorkspace = vi.fn((name: string) => behavior.createWorkspaceImpl(name)); - vi.doMock('@modelcontextprotocol/sdk/server/mcp.js', () => ({ McpServer: FakeMcpServer })); + vi.doMock('@modelcontextprotocol/sdk/server/mcp.js', () => ({ + McpServer: FakeMcpServer, + ResourceTemplate: FakeResourceTemplate, + })); vi.doMock('@modelcontextprotocol/sdk/server/stdio.js', () => ({ StdioServerTransport: FakeTransport })); - vi.doMock('@modelcontextprotocol/sdk/types.js', () => ({ ListToolsRequestSchema: { type: 'tools/list' } })); - vi.doMock('@relaycast/sdk/internal', () => ({ createInternalRelayCast, createInternalWsClient })); - vi.doMock('@relaycast/mcp', () => ({ MCP_VERSION: 'test-mcp-version' })); - vi.doMock('@relaycast/mcp/dist/piggyback.js', () => ({ enablePiggyback })); - vi.doMock('@relaycast/mcp/dist/resources/definitions.js', () => ({ registerResourceDefinitions })); - vi.doMock('@relaycast/mcp/dist/resources/subscriptions.js', () => ({ - SubscriptionManager: FakeSubscriptionManager, + vi.doMock('@modelcontextprotocol/sdk/types.js', () => ({ + ListToolsRequestSchema: { method: 'tools/list' }, + SubscribeRequestSchema: { method: 'resources/subscribe' }, + UnsubscribeRequestSchema: { method: 'resources/unsubscribe' }, + })); + vi.doMock('@relaycast/sdk', () => ({ + RelayCast, + WsClient: FakeWsClient, + SDK_VERSION: 'test-sdk-version', })); - vi.doMock('@relaycast/mcp/dist/tools/channels.js', () => ({ registerChannelTools })); - vi.doMock('@relaycast/mcp/dist/tools/features.js', () => ({ registerFeatureTools })); - vi.doMock('@relaycast/mcp/dist/tools/messaging.js', () => ({ registerMessagingTools })); - vi.doMock('@relaycast/mcp/dist/tools/programmability.js', () => ({ registerProgrammabilityTools })); - vi.doMock('@relaycast/mcp/dist/telemetry.js', () => ({ createMcpTelemetry })); - vi.doMock('@relaycast/mcp/dist/types.js', () => ({ createInitialSession })); - vi.doMock('@relaycast/mcp/dist/resources/ws-bridge.js', () => ({ WsBridge: FakeWsBridge })); const mod = await import('./relaycast-mcp.js'); if (options.forceEntrypoint) { @@ -195,20 +240,8 @@ async function loadRelaycastMcpModule(options: LoadOptions = {}) { behavior, serverInstances, relayInstances, - wsBridgeInstances, - subscriptionInstances, - channelToolGetters, - resourceGetters, - telemetry, - createInternalRelayCast, - createInternalWsClient, - enablePiggyback, - registerResourceDefinitions, - registerChannelTools, - registerMessagingTools, - registerFeatureTools, - registerProgrammabilityTools, - createInitialSession, + wsClientInstances, + RelayCast, FakeTransport, }, }; @@ -230,6 +263,7 @@ describe('relaycast-mcp startup helpers', () => { vi.stubEnv('RELAY_CLAW_NAME', 'FallbackClaw'); vi.stubEnv('RELAY_AGENT_TYPE', 'human'); vi.stubEnv('RELAY_STRICT_AGENT_NAME', ' yes '); + vi.stubEnv('RELAY_SKIP_BOOTSTRAP', '1'); expect(mod.normalizeBaseUrl('https://api.relaycast.dev///')).toBe('https://api.relaycast.dev'); expect(mod.envFlagEnabled(' on ')).toBe(true); @@ -243,59 +277,48 @@ describe('relaycast-mcp startup helpers', () => { agentName: 'FallbackClaw', agentType: 'human', strictAgentName: true, + skipBootstrap: true, }); }); }); describe('createPatchedRelayMcpServer', () => { - it('registers startup tools, prompt text, and strips execution metadata from tools/list', async () => { + it('registers owned tools, resources, prompt text, and strips execution metadata from tools/list', async () => { const { mod, mocks } = await loadRelaycastMcpModule(); - vi.stubGlobal( - 'fetch', - vi.fn(async () => ({ - json: async () => ({ - ok: true, - data: { api_key: 'rk_live_created', workspace_name: 'Test Workspace' }, - }), - })) - ); mod.createPatchedRelayMcpServer({ baseUrl: 'https://api.relaycast.dev/' }); const server = mocks.serverInstances[0]; - const registerTool = server.tools.get('register'); - const createWorkspaceTool = server.tools.get('create_workspace'); - const setWorkspaceKeyTool = server.tools.get('set_workspace_key'); - const prompt = server.prompts.get('system'); - - expect(registerTool).toBeDefined(); - expect(createWorkspaceTool).toBeDefined(); - expect(setWorkspaceKeyTool).toBeDefined(); - expect(prompt).toBeDefined(); - expect(mocks.enablePiggyback).toHaveBeenCalledTimes(1); - expect(mocks.registerChannelTools).toHaveBeenCalledTimes(1); - expect(mocks.registerMessagingTools).toHaveBeenCalledTimes(1); - expect(mocks.registerFeatureTools).toHaveBeenCalledTimes(1); - expect(mocks.registerProgrammabilityTools).toHaveBeenCalledTimes(1); - - expect(() => mocks.resourceGetters[0].getRelay()).toThrow( - 'Workspace key not configured. Set RELAY_API_KEY at startup, or call "create_workspace" or "set_workspace_key" first.' + + expect(server.tools.get('workspace.create')).toBeDefined(); + expect(server.tools.get('create_workspace')).toBeDefined(); + expect(server.tools.get('agent.register')).toBeDefined(); + expect(server.tools.get('register')).toBeDefined(); + expect(server.tools.get('agent.list')).toBeDefined(); + expect(server.tools.get('message.post')).toBeDefined(); + expect(server.tools.get('agent.add')).toBeDefined(); + expect(server.resources.get('inbox')).toBeDefined(); + expect(server.resources.get('channel-messages')).toBeDefined(); + expect(server.prompts.get('system')).toBeDefined(); + + await expect(server.resources.get('agents')?.handler(new URL('relay://agents'))).rejects.toThrow( + 'Workspace key not configured. Set RELAY_API_KEY at startup, or call "workspace.create" or "workspace.set_key" first.' ); - expect(() => mocks.channelToolGetters[0]()).toThrow('Not registered. Call the "register" tool first.'); - await expect(registerTool?.handler({ name: 'WorkerA' })).rejects.toThrow( - 'Workspace key not configured. Call "create_workspace" or "set_workspace_key" first.' + await expect(server.tools.get('agent.register')?.handler({ name: 'WorkerA' })).rejects.toThrow( + 'Workspace key not configured. Call "workspace.create" or "workspace.set_key" first.' ); - const workspaceResult = await createWorkspaceTool?.handler({ name: 'Coverage Workspace' }); - expect(fetch).toHaveBeenCalledWith( - 'https://api.relaycast.dev/v1/workspaces', - expect.objectContaining({ method: 'POST' }) - ); + const workspaceResult = await server.tools + .get('workspace.create') + ?.handler({ name: 'Coverage Workspace' }); + expect(mocks.RelayCast.createWorkspace).toHaveBeenCalledWith('Coverage Workspace', { + baseUrl: 'https://api.relaycast.dev/', + }); expect(workspaceResult.structuredContent).toEqual({ - api_key: 'rk_live_created', - workspace_name: 'Test Workspace', + apiKey: 'rk_live_created', + workspaceName: 'Test Workspace', }); - const registerResult = await registerTool?.handler({ + const registerResult = await server.tools.get('agent.register')?.handler({ name: 'WorkerA', type: 'human', persona: 'Coverage tester', @@ -315,23 +338,37 @@ describe('createPatchedRelayMcpServer', () => { registered_name: 'WorkerA', }); - const agentClient = mocks.channelToolGetters[0](); - expect(agentClient).toMatchObject({ token: 'at_live_WorkerA' }); - const toolsList = await server.listToolsHandler?.({}, {}); expect(toolsList?.tools).toEqual([ { - name: 'post_message', + name: 'message.post', title: 'Post Message', description: 'Send a channel message', }, ]); - const promptResult = await prompt?.handler(); - expect(promptResult.messages[0].content.text).toContain('create_workspace'); - expect(mocks.telemetry.capture).toHaveBeenCalledWith('relaycast_mcp_server_started', { - source_surface: 'mcp', - transport: 'unknown', + const promptResult = await server.prompts.get('system')?.handler(); + expect(promptResult.messages[0].content.text).toContain('workspace.create'); + }); + + it('dispatches websocket resource callbacks only to subscribed resources', async () => { + const { mod, mocks } = await loadRelaycastMcpModule(); + mod.createPatchedRelayMcpServer({ + apiKey: 'rk_live_existing', + agentToken: 'at_live_existing', + agentName: 'PinnedWorker', + baseUrl: 'https://api.relaycast.dev', + }); + + const server = mocks.serverInstances[0]; + const ws = mocks.wsClientInstances[0]; + const subscribe = server.server._requestHandlers.get('resources/subscribe'); + await subscribe?.({ params: { uri: 'relay://inbox' } }, {}); + + ws.emit({ type: 'message.created', channel: 'general' }); + expect(server.server.sendResourceUpdated).toHaveBeenCalledWith({ uri: 'relay://inbox' }); + expect(server.server.sendResourceUpdated).not.toHaveBeenCalledWith({ + uri: 'relay://channels/general/messages', }); }); @@ -382,20 +419,18 @@ describe('createPatchedRelayMcpServer', () => { }); const server = mocks.serverInstances[0]; - const bridge = mocks.wsBridgeInstances[0]; - const subscriptions = mocks.subscriptionInstances[0]; - const setWorkspaceKeyTool = server.tools.get('set_workspace_key'); + const ws = mocks.wsClientInstances[0]; + const setWorkspaceKeyTool = server.tools.get('workspace.set_key'); - expect(bridge?.start).toHaveBeenCalledTimes(1); + expect(ws.connect).toHaveBeenCalledTimes(1); await expect(setWorkspaceKeyTool?.handler({ api_key: 'bad_key' })).rejects.toThrow( 'Workspace key must start with "rk_live_"' ); const result = await setWorkspaceKeyTool?.handler({ api_key: 'rk_live_other' }); - expect(bridge?.stop).toHaveBeenCalledTimes(1); - expect(subscriptions?.clear).toHaveBeenCalledTimes(1); + expect(ws.disconnect).toHaveBeenCalledTimes(1); expect(result.structuredContent).toEqual({ - message: 'Workspace key set. Previous agent session was cleared; call "register" again.', + message: 'Workspace key set. Call "agent.register" to join this workspace.', }); }); @@ -409,16 +444,19 @@ describe('createPatchedRelayMcpServer', () => { }); const server = mocks.serverInstances[0]; - const bridge = mocks.wsBridgeInstances[0]; - const setWorkspaceKeyTool = server.tools.get('set_workspace_key'); + const ws = mocks.wsClientInstances[0]; + const setWorkspaceKeyTool = server.tools.get('workspace.set_key'); const result = await setWorkspaceKeyTool?.handler({ api_key: 'rk_live_existing' }); - expect(bridge?.stop).not.toHaveBeenCalled(); + expect(ws.disconnect).not.toHaveBeenCalled(); expect(result.structuredContent).toEqual({ message: 'Workspace key set.', }); - expect(mocks.channelToolGetters[0]()).toMatchObject({ token: 'at_live_existing' }); + + await server.tools.get('message.inbox.check')?.handler({}); + const agentRelay = mocks.relayInstances.find((instance) => instance.config.apiKey === 'at_live_existing'); + expect(agentRelay?.as).toHaveBeenCalledWith('at_live_existing', { autoHeartbeatMs: false }); }); it('marks websocket initialization attempted even when bridge setup fails', async () => { @@ -429,12 +467,7 @@ describe('createPatchedRelayMcpServer', () => { agentName: 'PinnedWorker', }); - expect(mocks.createInternalWsClient).toHaveBeenCalledTimes(1); - expect(mocks.wsBridgeInstances).toHaveLength(0); - expect(mocks.telemetry.capture).toHaveBeenCalledWith('relaycast_mcp_session_authenticated', { - source_surface: 'mcp', - agent_name: 'PinnedWorker', - }); + expect(mocks.wsClientInstances).toHaveLength(0); }); it('swallows websocket resource update emission failures', async () => { @@ -446,13 +479,15 @@ describe('createPatchedRelayMcpServer', () => { }); const server = mocks.serverInstances[0]; - const bridge = mocks.wsBridgeInstances[0]; + const ws = mocks.wsClientInstances[0]; + const subscribe = server.server._requestHandlers.get('resources/subscribe'); + await subscribe?.({ params: { uri: 'relay://inbox' } }, {}); server.server.sendResourceUpdated.mockRejectedValueOnce(new Error('emit failed')); - bridge?.onResourceUpdated('relaycast://channels/general'); + ws.emit({ type: 'reaction.added' }); await Promise.resolve(); - expect(server.server.sendResourceUpdated).toHaveBeenCalledWith({ uri: 'relaycast://channels/general' }); + expect(server.server.sendResourceUpdated).toHaveBeenCalledWith({ uri: 'relay://inbox' }); }); }); @@ -477,12 +512,23 @@ describe('resolvePatchedStdioBootstrapOptions', () => { const result = await mod.resolvePatchedStdioBootstrapOptions(options); expect(result).toEqual(options); - // No probe — D1 read-replica lag can spuriously 401 a fresh token, and - // rotating UPDATEs the tokenHash, permanently killing the original. expect(mocks.behavior.inboxImpl).not.toHaveBeenCalled(); expect(mocks.behavior.registerImpl).not.toHaveBeenCalled(); }); + it('respects explicit bootstrap skipping', async () => { + const { mod, mocks } = await loadRelaycastMcpModule(); + const options = { + apiKey: 'rk_live_workspace', + agentName: 'WorkerA', + agentToken: 'jwt_or_external_token', + skipBootstrap: true, + }; + + await expect(mod.resolvePatchedStdioBootstrapOptions(options)).resolves.toEqual(options); + expect(mocks.behavior.registerImpl).not.toHaveBeenCalled(); + }); + it('mints a relaycast token when the caller provides a non-relaycast token (e.g. JWT)', async () => { const { mod, mocks } = await loadRelaycastMcpModule(); mocks.behavior.registerImpl = vi.fn(async () => ({ @@ -493,12 +539,10 @@ describe('resolvePatchedStdioBootstrapOptions', () => { const result = await mod.resolvePatchedStdioBootstrapOptions({ apiKey: 'rk_live_workspace', agentName: 'WorkerA', - // A relayauth RS256 JWT, as `relay on start` plumbs into RELAY_AGENT_TOKEN. agentToken: 'eyJhbGciOiJSUzI1NiJ9.payload.sig', agentType: 'human', }); - expect(mocks.behavior.inboxImpl).not.toHaveBeenCalled(); expect(mocks.behavior.registerImpl).toHaveBeenCalledWith({ name: 'WorkerA', type: 'human', @@ -524,7 +568,6 @@ describe('resolvePatchedStdioBootstrapOptions', () => { agentType: 'agent', }); - expect(mocks.behavior.inboxImpl).not.toHaveBeenCalled(); expect(mocks.behavior.registerImpl).toHaveBeenCalledWith({ name: 'WorkerA', type: 'agent', @@ -546,10 +589,6 @@ describe('startPatchedStdio', () => { const server = mocks.serverInstances[0]; expect(server.connect).toHaveBeenCalledTimes(1); expect(server.connect.mock.calls[0][0]).toBeInstanceOf(mocks.FakeTransport); - expect(mocks.telemetry.capture).toHaveBeenCalledWith('relaycast_mcp_server_started', { - source_surface: 'mcp', - transport: 'stdio', - }); }); it('reports entrypoint startup failures to stderr and exits', async () => { diff --git a/src/cli/relaycast-mcp.ts b/src/cli/relaycast-mcp.ts index 09eb741f8..18b0e31b6 100644 --- a/src/cli/relaycast-mcp.ts +++ b/src/cli/relaycast-mcp.ts @@ -4,51 +4,55 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { createInternalRelayCast, createInternalWsClient } from '@relaycast/sdk/internal'; -import { MCP_VERSION } from '@relaycast/mcp'; -import { enablePiggyback } from '@relaycast/mcp/dist/piggyback.js'; -import { registerResourceDefinitions } from '@relaycast/mcp/dist/resources/definitions.js'; -import { SubscriptionManager } from '@relaycast/mcp/dist/resources/subscriptions.js'; -import { registerChannelTools } from '@relaycast/mcp/dist/tools/channels.js'; -import { registerFeatureTools } from '@relaycast/mcp/dist/tools/features.js'; -import { registerMessagingTools } from '@relaycast/mcp/dist/tools/messaging.js'; -import { registerProgrammabilityTools } from '@relaycast/mcp/dist/tools/programmability.js'; -import { createMcpTelemetry } from '@relaycast/mcp/dist/telemetry.js'; -import { createInitialSession, type SessionState } from '@relaycast/mcp/dist/types.js'; -import { WsBridge } from '@relaycast/mcp/dist/resources/ws-bridge.js'; +import { + ListToolsRequestSchema, + SubscribeRequestSchema, + UnsubscribeRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { RelayCast, SDK_VERSION, WsClient, type AgentClient } from '@relaycast/sdk'; import { z } from 'zod'; const DEFAULT_BASE_URL = 'https://api.relaycast.dev'; -const DEFAULT_SYSTEM_PROMPT = `You are an AI agent in a collaborative workspace powered by Agent Relay. You can communicate with other agents using the following tools: +export const AGENT_RELAY_MCP_VERSION = process.env.AGENT_RELAY_CLI_VERSION ?? SDK_VERSION ?? 'unknown'; + +const DEFAULT_SYSTEM_PROMPT = `You are an AI agent in a collaborative workspace powered by Agent Relay. You can communicate with other agents using these MCP tools: ## Getting Started -1. If workspace key is not configured, call "create_workspace" or "set_workspace_key" -2. When RELAY_API_KEY is provided at startup, this MCP server auto-registers the session as RELAY_AGENT_NAME (or "orchestrator" by default). Otherwise call "register" with your agent name to join the workspace -3. Use "list_channels" to see available channels -4. Use "join_channel" to join channels of interest -5. Use "check_inbox" to see unread messages and mentions +1. If no workspace key is configured, call "workspace.create" or "workspace.set_key" +2. When RELAY_API_KEY is provided at startup, this MCP server auto-registers the session as RELAY_AGENT_NAME (or "orchestrator" by default). Otherwise call "agent.register" with your agent name to join the workspace +3. Use "channel.list" to see available channels +4. Use "channel.join" to join channels of interest +5. Use "message.inbox.check" to see unread messages and mentions ## Communication -- Post messages to channels with "post_message" -- Send direct messages with "send_dm" -- Reply to threads with "reply_to_thread" -- React to messages with "add_reaction" +- Post messages to channels with "message.post" +- Send direct messages with "message.dm.send" +- Reply to threads with "message.reply" +- React to messages with "message.reaction.add" ## Best Practices - Check your inbox regularly for new messages and mentions - Use channels for topic-based discussions - Use threads for detailed discussions to keep channels organized -- React with emoji to acknowledge messages (e.g. thumbsup for agreement) +- React with emoji to acknowledge messages - Keep messages concise and actionable`; const jsonResult = z.object({}).passthrough(); +const messageResult = { + message: z.string().describe('Human-readable confirmation message'), +}; +const identityOverrideInputShape = { + as: z + .string() + .optional() + .describe('Registered agent identity to act as when multiple identities have been registered'), +}; type AgentType = 'agent' | 'human'; -type RelayCastLike = ReturnType; -type AgentClientLike = ReturnType; +type RelayCastLike = Pick; +type AgentClientLike = AgentClient; export interface PatchedMcpServerOptions { apiKey?: string; @@ -58,6 +62,22 @@ export interface PatchedMcpServerOptions { agentType?: AgentType; strictAgentName?: boolean; telemetryTransport?: 'stdio' | 'http'; + skipBootstrap?: boolean; +} + +interface RegisteredAgent { + agentName: string; + agentToken: string; +} + +interface SessionState { + workspaceKey: string | null; + agentToken: string | null; + agentName: string | null; + agents: Map; + wsBridge: RealtimeResourceBridge | null; + subscriptions: SubscriptionManager; + wsInitAttempted: boolean; } type RegistrationSession = Pick; @@ -82,7 +102,7 @@ type RegisterAgentWithRebindArgs = { forcedAgentType?: AgentType; }; -/** Return env var value, or undefined if missing / an unresolved ${…} template. */ +/** Return env var value, or undefined if missing / an unresolved ${...} template. */ function resolveEnv(key: string): string | undefined { const v = process.env[key]; if (!v || /^\$\{.+\}$/.test(v)) return undefined; @@ -120,6 +140,29 @@ export function normalizeAgentType(value: string | undefined): AgentType | undef return undefined; } +function createInitialSession(options: { + workspaceKey?: string | null; + agentToken?: string | null; + agentName?: string | null; +}): SessionState { + const agentToken = options.agentToken ?? null; + const agentName = options.agentName ?? null; + const agents = + agentToken && agentName + ? new Map([[agentName, { agentName, agentToken }]]) + : new Map(); + + return { + workspaceKey: options.workspaceKey ?? null, + agentToken, + agentName, + agents, + wsBridge: null, + subscriptions: new SubscriptionManager(), + wsInitAttempted: false, + }; +} + function readAgentResultCallbackConfig(agentName?: string): AgentResultCallbackConfig | undefined { const url = resolveEnv('AGENT_RELAY_RESULT_URL'); const token = resolveEnv('AGENT_RELAY_RESULT_TOKEN'); @@ -225,25 +268,7 @@ function registerAgentResultTool(server: McpServer, config: AgentResultCallbackC } async function createWorkspace(name: string, baseUrl?: string): Promise> { - const response = await fetch(`${normalizeBaseUrl(baseUrl)}/v1/workspaces`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name }), - }); - const payload = (await response.json()) as { - ok?: boolean; - data?: Record; - error?: { message?: string }; - } | null; - - if (!payload || typeof payload !== 'object' || typeof payload.ok !== 'boolean') { - throw new Error('Invalid response while creating workspace'); - } - if (!payload.ok) { - throw new Error(payload.error?.message ?? 'Failed to create workspace'); - } - - return payload.data ?? {}; + return (await RelayCast.createWorkspace(name, { baseUrl })) as Record; } function requireWorkspaceKey(session: RegistrationSession): void { @@ -251,7 +276,34 @@ function requireWorkspaceKey(session: RegistrationSession): void { return; } - throw new Error('Workspace key not configured. Call "create_workspace" or "set_workspace_key" first.'); + throw new Error('Workspace key not configured. Call "workspace.create" or "workspace.set_key" first.'); +} + +type JsonToolResult = { + content: Array<{ type: 'text'; text: string }>; + structuredContent: Record; +}; + +function jsonContent(value: unknown): JsonToolResult { + const structuredContent = + typeof value === 'object' && value !== null && !Array.isArray(value) + ? (value as Record) + : { value }; + return { + content: [{ type: 'text', text: JSON.stringify(value, null, 2) }], + structuredContent, + }; +} + +function textContent(message: string, structuredContent: Record = { message }) { + return { + content: [{ type: 'text' as const, text: message }], + structuredContent, + }; +} + +function createRegisteredAgent(agentName: string, agentToken: string): RegisteredAgent { + return { agentName, agentToken }; } export async function registerAgentWithRebind({ @@ -310,9 +362,325 @@ export async function registerAgentWithRebind({ }; } -function registerPatchedRegistrationTools( +class SubscriptionManager { + private readonly subscriptions = new Set(); + + subscribe(uri: string): void { + this.subscriptions.add(uri); + } + + unsubscribe(uri: string): void { + this.subscriptions.delete(uri); + } + + getMatchingSubscriptions(uris: string[]): string[] { + return uris.filter((uri) => this.subscriptions.has(uri)); + } + + getAll(): string[] { + return [...this.subscriptions]; + } + + clear(): void { + this.subscriptions.clear(); + } +} + +function getStringEventField(event: unknown, field: string): string | null { + if (typeof event !== 'object' || event === null) { + return null; + } + const candidate = (event as Record)[field]; + return typeof candidate === 'string' ? candidate : null; +} + +function eventToResourceUris(event: unknown): string[] { + const type = getStringEventField(event, 'type'); + switch (type) { + case 'message.created': { + const channel = getStringEventField(event, 'channel'); + return channel ? ['relay://inbox', `relay://channels/${channel}/messages`] : ['relay://inbox']; + } + case 'message.updated': { + const channel = getStringEventField(event, 'channel'); + return channel ? [`relay://channels/${channel}/messages`] : []; + } + case 'thread.reply': { + const parentId = getStringEventField(event, 'parentId'); + return parentId ? ['relay://inbox', `relay://messages/${parentId}/thread`] : ['relay://inbox']; + } + case 'dm.received': + case 'group_dm.received': { + const conversationId = getStringEventField(event, 'conversationId'); + return conversationId ? ['relay://inbox', `relay://dm/${conversationId}`] : ['relay://inbox']; + } + case 'agent.online': + case 'agent.offline': + return ['relay://agents']; + case 'channel.created': + case 'channel.updated': + case 'channel.archived': + case 'member.joined': + case 'member.left': + return ['relay://channels']; + case 'webhook.received': + case 'command.invoked': { + const channel = getStringEventField(event, 'channel'); + return channel ? [`relay://channels/${channel}/messages`] : []; + } + case 'reaction.added': + case 'reaction.removed': + return ['relay://inbox']; + default: + return []; + } +} + +class RealtimeResourceBridge { + private unsubscribeFn: (() => void) | null = null; + + constructor( + private readonly wsClient: WsClient, + private readonly subscriptions: SubscriptionManager, + private readonly notifyCallback: (uri: string) => void + ) {} + + start(): void { + this.unsubscribeFn = this.wsClient.on('*', (event) => { + const type = getStringEventField(event, 'type'); + if ( + type === 'open' || + type === 'close' || + type === 'error' || + type === 'reconnecting' || + type === 'permanently_disconnected' + ) { + return; + } + const matched = this.subscriptions.getMatchingSubscriptions(eventToResourceUris(event)); + for (const uri of matched) { + this.notifyCallback(uri); + } + }); + this.wsClient.connect(); + } + + stop(): void { + if (this.unsubscribeFn) { + this.unsubscribeFn(); + this.unsubscribeFn = null; + } + this.wsClient.disconnect(); + } +} + +function registerResourceDefinitions( server: McpServer, - getRelay: () => RelayCastLike, + getAgentClient: (asIdentity?: string) => AgentClientLike, + getRelay: () => RelayCast +): void { + server.registerResource( + 'inbox', + 'relay://inbox', + { title: 'Inbox', description: 'Unread messages, mentions, and DMs', mimeType: 'application/json' }, + async (uri) => { + const inbox = await getAgentClient().inbox(); + return { contents: [{ uri: uri.href, text: JSON.stringify(inbox) }] }; + } + ); + + server.registerResource( + 'agents', + 'relay://agents', + { + title: 'Agents', + description: 'Online and offline agents in the workspace', + mimeType: 'application/json', + }, + async (uri) => { + const agents = await getRelay().agents.list(); + return { contents: [{ uri: uri.href, text: JSON.stringify(agents) }] }; + } + ); + + server.registerResource( + 'channels', + 'relay://channels', + { title: 'Channels', description: 'Available channels in the workspace', mimeType: 'application/json' }, + async (uri) => { + const channels = await getAgentClient().channels.list(); + return { contents: [{ uri: uri.href, text: JSON.stringify(channels) }] }; + } + ); + + server.registerResource( + 'channel-messages', + new ResourceTemplate('relay://channels/{name}/messages', { list: undefined }), + { + title: 'Channel Messages', + description: 'Messages in a specific channel', + mimeType: 'application/json', + }, + async (uri, params) => { + const messages = await getAgentClient().messages(String(params.name)); + return { contents: [{ uri: uri.href, text: JSON.stringify(messages) }] }; + } + ); + + server.registerResource( + 'message-thread', + new ResourceTemplate('relay://messages/{id}/thread', { list: undefined }), + { title: 'Message Thread', description: 'Thread replies on a message', mimeType: 'application/json' }, + async (uri, params) => { + const thread = await getAgentClient().thread(String(params.id)); + return { contents: [{ uri: uri.href, text: JSON.stringify(thread) }] }; + } + ); + + server.registerResource( + 'dm-conversation', + new ResourceTemplate('relay://dm/{conversation_id}', { list: undefined }), + { + title: 'DM Conversation', + description: 'Direct message conversation', + mimeType: 'application/json', + }, + async (uri, params) => { + const messages = await getAgentClient().dms.messages(String(params.conversation_id)); + return { contents: [{ uri: uri.href, text: JSON.stringify(messages) }] }; + } + ); +} + +function hasContentArray(value: unknown): value is { content: Array> } { + return ( + typeof value === 'object' && value !== null && Array.isArray((value as { content?: unknown }).content) + ); +} + +const SKIP_PIGGYBACK = new Set([ + 'message.inbox.check', + 'workspace.create', + 'workspace.set_key', + 'agent.register', + 'create_workspace', + 'set_workspace_key', + 'register', +]); + +function formatInbox(inbox: any, selfName?: string | null): string { + const norm = (s: string) => s.trim().replace(/^@/, '').toLowerCase(); + const selfNorm = selfName ? norm(selfName) : null; + const isSelf = (name: string) => selfNorm != null && norm(name) === selfNorm; + const lines = ['--- Pending Messages ---']; + + if (inbox.unreadChannels?.length) { + lines.push('Unread channels:'); + for (const ch of inbox.unreadChannels) { + lines.push(` #${ch.channelName}: ${ch.unreadCount} unread`); + } + } + + const mentions = selfNorm ? inbox.mentions?.filter((m: any) => !isSelf(m.agentName)) : inbox.mentions; + if (mentions?.length) { + lines.push('Mentions:'); + for (const m of mentions) { + lines.push(` @${m.agentName} in #${m.channelName}: "${m.text}"`); + } + } + + const dms = selfNorm ? inbox.unreadDms?.filter((dm: any) => !isSelf(dm.from)) : inbox.unreadDms; + if (dms?.length) { + lines.push('Unread DMs:'); + for (const dm of dms) { + lines.push(` From ${dm.from}: ${dm.unreadCount} unread`); + } + } + + const reactions = selfNorm + ? inbox.recentReactions?.filter((reaction: any) => !isSelf(reaction.agentName)) + : inbox.recentReactions; + if (reactions?.length) { + lines.push('Reactions (informational; no response required):'); + for (const reaction of reactions) { + lines.push( + ` :${reaction.emoji}: on your message in #${reaction.channelName} by @${reaction.agentName}` + ); + } + } + + return lines.length === 1 ? '' : lines.join('\n'); +} + +function enableInboxPiggyback( + mcpServer: McpServer, + getSession: () => SessionState, + getAgentClient: (asIdentity?: string) => AgentClientLike +): void { + const original = mcpServer.registerTool.bind(mcpServer); + const mutableServer = mcpServer as McpServer & { + registerTool: McpServer['registerTool']; + }; + + mutableServer.registerTool = (name: string, config: any, handler: any) => { + if (!handler) { + return original(name, config, handler); + } + + const wrapped = async (...args: unknown[]) => { + const result = await handler(...args); + if (SKIP_PIGGYBACK.has(name) || !getSession().agentToken || !hasContentArray(result)) { + return result; + } + + try { + const [input] = args; + const asIdentity = + typeof input === 'object' && input !== null && typeof (input as { as?: unknown }).as === 'string' + ? (input as { as: string }).as + : undefined; + const inbox = await getAgentClient(asIdentity).inbox(); + const inboxText = formatInbox(inbox, asIdentity ?? getSession().agentName); + if (inboxText) { + result.content.push({ type: 'text', text: inboxText }); + } + } catch { + // Inbox piggyback is opportunistic. The original tool result should win. + } + + return result; + }; + + return original(name, config, wrapped); + }; +} + +function resolveEmoji(input: string): string { + const normalized = input.trim().replace(/^:/, '').replace(/:$/, '').toLowerCase(); + const aliases: Record = { + '+1': '👍', + thumbsup: '👍', + thumbs_up: '👍', + check: '✅', + white_check_mark: '✅', + rocket: '🚀', + eyes: '👀', + heart: '❤️', + clap: '👏', + }; + return aliases[normalized] ?? input; +} + +function registerTool(server: McpServer, names: string[], config: any, handler: any): void { + for (const name of names) { + server.registerTool(name, config, handler); + } +} + +function registerAgentRelayTools( + server: McpServer, + getRelay: () => RelayCast, + getAgentClient: (asIdentity?: string) => AgentClientLike, getSession: () => SessionState, setSession: SessionSetter, baseUrl: string | undefined, @@ -320,16 +688,14 @@ function registerPatchedRegistrationTools( preferredAgentName: string | undefined, forcedAgentType: AgentType | undefined ): void { - server.registerTool( - 'create_workspace', + registerTool( + server, + ['workspace.create', 'create_workspace'], { title: 'Create Workspace', - description: - 'Create a new Relaycast workspace and automatically store its API key in this MCP session. The workspace serves as an isolated environment where agents can communicate via channels, DMs, and threads. After creation, the workspace key is ready for immediate use with register and other workspace-level tools.', + description: 'Create a new Relaycast workspace and store its API key in this MCP session.', inputSchema: { - name: z - .string() - .describe('Human-readable workspace name, used to identify the workspace in dashboards and logs'), + name: z.string().describe('Human-readable workspace name'), }, outputSchema: jsonResult, annotations: { @@ -339,39 +705,33 @@ function registerPatchedRegistrationTools( openWorldHint: true, }, }, - async ({ name }) => { + async ({ name }: any) => { const workspace = await createWorkspace(name, baseUrl); - const workspaceKey = workspace.api_key ?? workspace.apiKey; + const workspaceKey = workspace.apiKey ?? workspace.api_key; if (!workspaceKey || typeof workspaceKey !== 'string') { - throw new Error('Workspace created, but the response did not include api_key'); + throw new Error('Workspace created, but the response did not include apiKey'); } - setSession({ workspaceKey, agentToken: null, agentName: null }); - return { - content: [{ type: 'text', text: JSON.stringify(workspace, null, 2) }], - structuredContent: workspace, - }; + setSession({ + workspaceKey, + agentToken: null, + agentName: null, + agents: new Map(), + }); + return jsonContent(workspace); } ); - server.registerTool( - 'set_workspace_key', + registerTool( + server, + ['workspace.set_key', 'set_workspace_key'], { title: 'Set Workspace Key', - description: - 'Authenticate this MCP session by providing an existing workspace API key (rk_live_...). This enables all workspace-level tools including agent registration, channel management, and messaging. If the key belongs to a different workspace than the current session, the previous agent identity is cleared and you must re-register.', + description: 'Authenticate this MCP session with an existing Relaycast workspace API key.', inputSchema: { - api_key: z - .string() - .describe( - 'Workspace API key starting with "rk_live_", obtained from workspace creation or the Relaycast dashboard' - ), - }, - outputSchema: { - message: z - .string() - .describe('Confirmation message indicating whether the workspace key was set successfully'), + api_key: z.string().describe('Workspace API key starting with "rk_live_"'), }, + outputSchema: messageResult, annotations: { readOnlyHint: false, destructiveHint: false, @@ -379,7 +739,7 @@ function registerPatchedRegistrationTools( openWorldHint: false, }, }, - async ({ api_key }) => { + async ({ api_key }: any) => { if (!api_key.startsWith('rk_live_')) { throw new Error('Workspace key must start with "rk_live_"'); } @@ -387,54 +747,42 @@ function registerPatchedRegistrationTools( const session = getSession(); const switchingWorkspace = session.workspaceKey !== api_key; if (switchingWorkspace) { - setSession({ workspaceKey: api_key, agentToken: null, agentName: null }); + setSession({ + workspaceKey: api_key, + agentToken: null, + agentName: null, + agents: new Map(), + }); } else { setSession({ workspaceKey: api_key }); } const message = switchingWorkspace - ? 'Workspace key set. Previous agent session was cleared; call "register" again.' + ? 'Workspace key set. Call "agent.register" to join this workspace.' : 'Workspace key set.'; - return { - content: [{ type: 'text', text: message }], - structuredContent: { message }, - }; + return textContent(message); } ); - server.registerTool( - 'register', + registerTool( + server, + ['agent.register', 'register'], { title: 'Register Agent', - description: - 'Register an agent identity in the current workspace and obtain an agent token for all subsequent operations. The agent name must be unique within the workspace. Re-registering the same name rotates or rebinds a usable token for that agent in the current workspace.', + description: 'Register an agent identity in the current workspace and obtain an agent token.', inputSchema: { - name: z - .string() - .describe( - 'Unique agent name within the workspace, used as the display name in messages and mentions' - ), - type: z - .enum(['agent', 'human']) - .optional() - .describe('Whether this identity represents an AI agent or a human user'), - persona: z - .string() - .optional() - .describe( - "Free-text persona description that other agents can read to understand this agent's role and capabilities" - ), + name: z.string().describe('Unique agent name within the workspace'), + type: z.enum(['agent', 'human']).optional().describe('Whether this identity is an AI agent or human'), + persona: z.string().optional().describe('Free-text persona description'), metadata: z .record(z.string(), z.unknown()) .optional() - .describe( - 'Key-value metadata to attach to the agent (e.g. { "cli": "claude", "model": "claude-sonnet-4-6" }). Use "model" to indicate which AI model powers this agent.' - ), + .describe('Key-value metadata to attach to the agent'), }, outputSchema: jsonResult, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, - async ({ name, type, persona, metadata }) => { + async ({ name, type, persona, metadata }: any) => { const payload = await registerAgentWithRebind({ session: getSession(), setSession, @@ -448,10 +796,494 @@ function registerPatchedRegistrationTools( forcedAgentType, }); - return { - content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], - structuredContent: payload, - }; + const token = typeof payload.token === 'string' ? payload.token : null; + const registeredName = + typeof payload.registered_name === 'string' + ? payload.registered_name + : typeof payload.name === 'string' + ? payload.name + : name; + if (token) { + const nextAgents = new Map(getSession().agents); + nextAgents.set(registeredName, createRegisteredAgent(registeredName, token)); + setSession({ agentToken: token, agentName: registeredName, agents: nextAgents }); + } + + return jsonContent(payload); + } + ); + + server.registerTool( + 'agent.list', + { + title: 'List Agents', + description: 'List agents registered in the current workspace.', + inputSchema: { + status: z.enum(['online', 'offline']).optional().describe('Optional status filter'), + }, + outputSchema: { + agents: z.array(z.object({}).passthrough()).describe('Registered agents'), + }, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ status }) => { + requireWorkspaceKey(getSession()); + const agents = await getRelay().agents.list(status ? { status } : undefined); + return jsonContent({ agents }); + } + ); + + server.registerTool( + 'channel.create', + { + title: 'Create Channel', + description: 'Create a new workspace channel.', + inputSchema: { + name: z.string().describe('Unique channel name'), + topic: z.string().optional().describe('Optional channel topic'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async ({ name, topic, as }) => jsonContent(await getAgentClient(as).channels.create({ name, topic })) + ); + + server.registerTool( + 'channel.list', + { + title: 'List Channels', + description: 'List channels available in the workspace.', + inputSchema: { + include_archived: z.boolean().optional().describe('Include archived channels'), + ...identityOverrideInputShape, + }, + outputSchema: { + channels: z.array(z.object({}).passthrough()).describe('Channels'), + }, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ include_archived, as }) => { + const channels = await getAgentClient(as).channels.list( + include_archived ? { includeArchived: include_archived } : undefined + ); + return jsonContent({ channels }); + } + ); + + server.registerTool( + 'channel.join', + { + title: 'Join Channel', + description: 'Join an existing channel.', + inputSchema: { + channel: z.string().describe('Channel name'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ channel, as }) => { + await getAgentClient(as).channels.join(channel); + return textContent(`Joined channel #${channel}`); + } + ); + + server.registerTool( + 'channel.leave', + { + title: 'Leave Channel', + description: 'Leave a channel.', + inputSchema: { + channel: z.string().describe('Channel name'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ channel, as }) => { + await getAgentClient(as).channels.leave(channel); + return textContent(`Left channel #${channel}`); + } + ); + + server.registerTool( + 'channel.invite', + { + title: 'Invite to Channel', + description: 'Invite another agent to a channel.', + inputSchema: { + channel: z.string().describe('Channel name'), + agent: z.string().describe('Agent name to invite'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ channel, agent, as }) => { + await getAgentClient(as).channels.invite(channel, agent); + return textContent(`Invited ${agent} to #${channel}`); + } + ); + + server.registerTool( + 'channel.set_topic', + { + title: 'Set Channel Topic', + description: 'Update a channel topic.', + inputSchema: { + channel: z.string().describe('Channel name'), + topic: z.string().describe('New topic'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ channel, topic, as }) => jsonContent(await getAgentClient(as).channels.setTopic(channel, topic)) + ); + + server.registerTool( + 'channel.archive', + { + title: 'Archive Channel', + description: 'Archive a channel.', + inputSchema: { + channel: z.string().describe('Channel name'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, + }, + async ({ channel, as }) => { + await getAgentClient(as).channels.archive(channel); + return textContent(`Archived channel #${channel}`); + } + ); + + server.registerTool( + 'message.post', + { + title: 'Post Message', + description: 'Post a new message to a channel as the current agent.', + inputSchema: { + channel: z.string().describe('Channel name'), + text: z.string().describe('Message text'), + attachments: z.array(z.string()).optional().describe('File attachment IDs'), + mode: z.enum(['wait', 'steer']).optional().describe('Delivery mode'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async ({ channel, text, attachments, mode, as }) => + jsonContent(await getAgentClient(as).send(channel, text, { attachments, mode })) + ); + + server.registerTool( + 'message.list', + { + title: 'Get Messages', + description: 'Retrieve message history from a channel.', + inputSchema: { + channel: z.string().describe('Channel name'), + limit: z.number().optional().describe('Maximum messages to return'), + before: z.string().optional().describe('Older-than cursor'), + after: z.string().optional().describe('Newer-than cursor'), + ...identityOverrideInputShape, + }, + outputSchema: { + messages: z.array(z.object({}).passthrough()).describe('Messages'), + }, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ channel, limit, before, after, as }) => { + const messages = await getAgentClient(as).messages(channel, { limit, before, after }); + return jsonContent({ messages }); + } + ); + + server.registerTool( + 'message.reply', + { + title: 'Reply to Thread', + description: 'Reply to an existing message thread.', + inputSchema: { + message_id: z.string().describe('Parent message ID'), + text: z.string().describe('Reply text'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async ({ message_id, text, as }) => jsonContent(await getAgentClient(as).reply(message_id, text)) + ); + + server.registerTool( + 'message.get_thread', + { + title: 'Get Thread', + description: 'Retrieve a message thread.', + inputSchema: { + message_id: z.string().describe('Parent message ID'), + limit: z.number().optional().describe('Maximum replies to return'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ message_id, limit, as }) => + jsonContent(await getAgentClient(as).thread(message_id, limit ? { limit } : undefined)) + ); + + server.registerTool( + 'message.dm.send', + { + title: 'Send Direct Message', + description: 'Send a private direct message to another agent.', + inputSchema: { + to: z.string().describe('Recipient agent name'), + text: z.string().describe('DM text'), + mode: z.enum(['wait', 'steer']).optional().describe('Delivery mode'), + attachments: z.array(z.string()).optional().describe('File attachment IDs'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async ({ to, text, mode, attachments, as }) => + jsonContent(await getAgentClient(as).dm(to, text, { mode, attachments })) + ); + + server.registerTool( + 'message.dm.list', + { + title: 'List DM Conversations', + description: 'List direct message conversations for the current agent.', + inputSchema: { + ...identityOverrideInputShape, + }, + outputSchema: { + conversations: z.array(z.object({}).passthrough()).describe('DM conversations'), + }, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ as }) => jsonContent({ conversations: await getAgentClient(as).dms.conversations() }) + ); + + server.registerTool( + 'message.dm.send_group', + { + title: 'Send Group DM', + description: 'Create a group DM and send the first message.', + inputSchema: { + participants: z.array(z.string()).describe('Participant agent names'), + name: z.string().optional().describe('Optional group name'), + text: z.string().describe('Initial message'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, + }, + async ({ participants, name, text, as }) => { + const client = getAgentClient(as); + const conversation = await client.dms.createGroup({ participants, name }); + const message = await client.dms.sendMessage(conversation.id, text); + return jsonContent({ conversation, message }); + } + ); + + server.registerTool( + 'message.reaction.add', + { + title: 'Add Reaction', + description: 'Add an emoji reaction to a message.', + inputSchema: { + message_id: z.string().describe('Message ID'), + emoji: z.string().describe('Emoji character or shortcode'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ message_id, emoji, as }) => { + const resolved = resolveEmoji(emoji); + await getAgentClient(as).react(message_id, resolved); + return textContent(`Reacted with ${resolved}`); + } + ); + + server.registerTool( + 'message.reaction.remove', + { + title: 'Remove Reaction', + description: 'Remove an emoji reaction from a message.', + inputSchema: { + message_id: z.string().describe('Message ID'), + emoji: z.string().describe('Emoji character or shortcode'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ message_id, emoji, as }) => { + const resolved = resolveEmoji(emoji); + await getAgentClient(as).unreact(message_id, resolved); + return textContent(`Removed reaction ${resolved}`); + } + ); + + server.registerTool( + 'message.search', + { + title: 'Search Messages', + description: 'Search messages across the workspace.', + inputSchema: { + query: z.string().describe('Text search query'), + channel: z.string().optional().describe('Optional channel filter'), + from: z.string().optional().describe('Optional sender filter'), + limit: z.number().optional().describe('Maximum results'), + ...identityOverrideInputShape, + }, + outputSchema: { + results: z.array(z.object({}).passthrough()).describe('Search results'), + }, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ query, channel, from, limit, as }) => + jsonContent({ results: await getAgentClient(as).search(query, { channel, from, limit }) }) + ); + + server.registerTool( + 'message.inbox.check', + { + title: 'Check Inbox', + description: 'Check unread messages, mentions, DMs, and reactions for the current agent.', + inputSchema: { + limit: z.number().optional().describe('Maximum inbox items'), + ...identityOverrideInputShape, + }, + outputSchema: jsonResult, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ limit, as }) => + jsonContent(await getAgentClient(as).inbox(limit != null ? { limit } : undefined)) + ); + + server.registerTool( + 'message.inbox.mark_read', + { + title: 'Mark as Read', + description: 'Mark a message as read for the current agent.', + inputSchema: { + message_id: z.string().describe('Message ID'), + ...identityOverrideInputShape, + }, + outputSchema: messageResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ message_id, as }) => { + await getAgentClient(as).markRead(message_id); + return textContent(`Marked message ${message_id} as read`); + } + ); + + server.registerTool( + 'message.inbox.get_readers', + { + title: 'Get Readers', + description: 'List agents who have read a message.', + inputSchema: { + message_id: z.string().describe('Message ID'), + ...identityOverrideInputShape, + }, + outputSchema: { + readers: z.array(z.object({}).passthrough()).describe('Readers'), + }, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ message_id, as }) => jsonContent({ readers: await getAgentClient(as).readers(message_id) }) + ); + + server.registerTool( + 'agent.add', + { + title: 'Add Agent', + description: 'Ask Relaycast to spawn a worker agent for a task.', + inputSchema: { + name: z.string().describe('Worker agent name'), + cli: z.string().min(1).describe('AI CLI or configured harness to launch'), + task: z.string().describe('Task instructions'), + channel: z.string().optional().describe('Channel to join'), + persona: z.string().optional().describe('Worker persona'), + model: z.string().optional().describe('Model powering the worker'), + }, + outputSchema: jsonResult, + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async ({ name, cli, task, channel, persona, model }) => + jsonContent( + await getRelay().agents.spawn({ + name, + cli: cli as 'claude' | 'codex' | 'gemini' | 'aider' | 'goose', + task, + channel, + persona, + metadata: model ? { model } : undefined, + }) + ) + ); + + server.registerTool( + 'agent.remove', + { + title: 'Remove Agent', + description: 'Release a worker agent from active duty.', + inputSchema: { + name: z.string().describe('Agent name'), + reason: z.string().optional().describe('Removal reason'), + delete_agent: z.boolean().optional().describe('Permanently delete the agent'), + }, + outputSchema: { + name: z.string().describe('Removed agent name'), + removed: z.boolean().describe('Whether the agent was removed'), + deleted: z.boolean().describe('Whether the agent was deleted'), + reason: z.string().nullable().describe('Removal reason'), + }, + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, + }, + async ({ name, reason, delete_agent }) => { + const released = await getRelay().agents.release({ name, reason, deleteAgent: delete_agent }); + return jsonContent({ + name: released.name, + removed: released.released, + deleted: released.deleted, + reason: released.reason, + }); } ); } @@ -463,19 +1295,8 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M agentName: options.agentName ?? null, }); - const mcpOrigin = { - surface: 'mcp', - client: '@agent-relay/relaycast-mcp', - version: MCP_VERSION, - } as const; - const telemetry = createMcpTelemetry(MCP_VERSION, { - originSurface: mcpOrigin.surface, - originClient: mcpOrigin.client, - originVersion: mcpOrigin.version, - }); - const mcpServer = new McpServer( - { name: 'agent-relay', version: MCP_VERSION }, + { name: 'agent-relay', version: AGENT_RELAY_MCP_VERSION }, { capabilities: { resources: { subscribe: true, listChanged: true }, @@ -484,98 +1305,100 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M }, } ); - telemetry.capture('relaycast_mcp_server_started', { - source_surface: 'mcp', - transport: options.telemetryTransport ?? 'unknown', - }); const getSession = (): SessionState => session; - const getRelay = (): RelayCastLike => { + const getRelay = (): RelayCast => { const workspaceKey = session.workspaceKey; if (!workspaceKey) { throw new Error( - 'Workspace key not configured. Set RELAY_API_KEY at startup, or call "create_workspace" or "set_workspace_key" first.' + 'Workspace key not configured. Set RELAY_API_KEY at startup, or call "workspace.create" or "workspace.set_key" first.' ); } - return createInternalRelayCast( - { - apiKey: workspaceKey, - baseUrl: options.baseUrl, - }, - mcpOrigin - ); + return new RelayCast({ + apiKey: workspaceKey, + baseUrl: options.baseUrl, + }); + }; + + const notifySubscribers = () => { + const uris = session.subscriptions.getAll(); + for (const uri of uris) { + mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined); + } }; const setSession: SessionSetter = (partial) => { - const nextAgentToken = partial.agentToken === undefined ? session.agentToken : partial.agentToken; - const nextAgentName = partial.agentName ?? session.agentName ?? null; - const shouldResetBridge = partial.agentToken !== undefined && partial.agentToken !== session.agentToken; + const switchingWorkspace = + partial.workspaceKey !== undefined && partial.workspaceKey !== session.workspaceKey; + const changingToken = partial.agentToken !== undefined && partial.agentToken !== session.agentToken; - if (shouldResetBridge && session.wsBridge) { - session.wsBridge.stop(); - session.subscriptions?.clear(); + if (switchingWorkspace || changingToken) { + notifySubscribers(); + session.wsBridge?.stop(); session.wsBridge = null; - session.subscriptions = null; - } - if (shouldResetBridge) { session.wsInitAttempted = false; } - if (nextAgentToken && !session.wsBridge && !session.wsInitAttempted) { + Object.assign(session, partial); + + if (session.agentToken && !session.wsBridge && !session.wsInitAttempted) { try { - const subscriptions = new SubscriptionManager(); - const wsClient = createInternalWsClient( - { - token: nextAgentToken, - baseUrl: options.baseUrl, - }, - mcpOrigin - ); - const wsBridge = new WsBridge(wsClient as never, subscriptions, (uri) => { + const wsClient = new WsClient({ + token: session.agentToken, + baseUrl: options.baseUrl, + }); + const wsBridge = new RealtimeResourceBridge(wsClient, session.subscriptions, (uri) => { mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined); }); wsBridge.start(); - Object.assign(session, partial, { - wsBridge, - subscriptions, - wsInitAttempted: true, - }); + session.wsBridge = wsBridge; + session.wsInitAttempted = true; } catch { - Object.assign(session, partial, { - wsBridge: null, - subscriptions: null, - wsInitAttempted: true, - }); + session.wsBridge = null; + session.wsInitAttempted = true; } - telemetry.capture('relaycast_mcp_session_authenticated', { - source_surface: 'mcp', - agent_name: nextAgentName, - }); - } else { - Object.assign(session, partial); } }; - const getAgentClient = (): AgentClientLike => { + const resolveAgentToken = (asIdentity?: string): string => { + if (asIdentity) { + const registered = session.agents.get(asIdentity); + if (!registered) { + throw new Error(`Unknown agent identity "${asIdentity}". Register it first.`); + } + return registered.agentToken; + } + if (!session.agentToken) { - throw new Error('Not registered. Call the "register" tool first.'); + throw new Error('Not registered. Call the "agent.register" tool first.'); } - return createInternalRelayCast( - { - apiKey: session.agentToken, - baseUrl: options.baseUrl, - }, - mcpOrigin - ).as(session.agentToken); + return session.agentToken; + }; + + const getAgentClient = (asIdentity?: string): AgentClientLike => { + const agentToken = resolveAgentToken(asIdentity); + return new RelayCast({ + apiKey: agentToken, + baseUrl: options.baseUrl, + }).as(agentToken, { autoHeartbeatMs: false }); }; - enablePiggyback(mcpServer, getSession, getAgentClient as never, telemetry); - registerResourceDefinitions(mcpServer, getAgentClient as never, getRelay as never); - registerPatchedRegistrationTools( + enableInboxPiggyback(mcpServer, getSession, getAgentClient); + registerResourceDefinitions(mcpServer, getAgentClient, getRelay); + mcpServer.server.setRequestHandler(SubscribeRequestSchema, async (req) => { + session.subscriptions.subscribe(req.params.uri); + return {}; + }); + mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, async (req) => { + session.subscriptions.unsubscribe(req.params.uri); + return {}; + }); + registerAgentRelayTools( mcpServer, getRelay, + getAgentClient, getSession, setSession, options.baseUrl, @@ -583,17 +1406,13 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M options.agentName, options.agentType ); - registerChannelTools(mcpServer, getAgentClient as never); - registerMessagingTools(mcpServer, getAgentClient as never); - registerFeatureTools(mcpServer, getAgentClient as never); - registerProgrammabilityTools(mcpServer, getRelay as never, getAgentClient as never); registerAgentResultTool(mcpServer, readAgentResultCallbackConfig(options.agentName)); mcpServer.registerPrompt( 'system', { title: 'System Prompt', - description: 'Get the default system instructions for Relaycast collaboration.', + description: 'Get the default system instructions for Agent Relay collaboration.', }, async () => ({ messages: [ @@ -640,10 +1459,11 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M return mcpServer; } -/** Relaycast agent tokens are opaque `at_live_` literals — see - * relaycast/packages/server/src/engine/agent.ts:38. Anything else (e.g. a - * relayauth RS256 JWT carried in RELAY_AGENT_TOKEN by `relay on start`) is not - * a valid relaycast credential and must be replaced. */ +export const createAgentRelayMcpServer = createPatchedRelayMcpServer; + +/** Relaycast agent tokens are opaque `at_live_` literals. Anything else + * (for example a RelayAuth JWT carried in RELAY_AGENT_TOKEN by `relay on start`) + * is not a valid Relaycast credential and must be replaced. */ function isRelaycastAgentToken(token: string | undefined): token is string { return typeof token === 'string' && token.startsWith('at_live_'); } @@ -651,14 +1471,7 @@ function isRelaycastAgentToken(token: string | undefined): token is string { export async function resolvePatchedStdioBootstrapOptions( options: PatchedMcpServerOptions ): Promise { - // Caller already minted a relaycast agent token — trust it. We must not - // probe-then-rotate here: rotation UPDATEs the agent's tokenHash, which - // permanently kills the original token, and on D1 read-replica lag a - // freshly-minted token can transiently fail an auth probe even though it is - // valid. The first real call will surface a 401 if the token is actually - // bad; that's a clearer failure mode than silently swapping the agent's - // identity behind the caller's back. - if (isRelaycastAgentToken(options.agentToken)) { + if (isRelaycastAgentToken(options.agentToken) || options.skipBootstrap) { return options; } @@ -666,17 +1479,10 @@ export async function resolvePatchedStdioBootstrapOptions( return options; } - const relay = createInternalRelayCast( - { - apiKey: options.apiKey, - baseUrl: options.baseUrl, - }, - { - surface: 'mcp', - client: '@agent-relay/relaycast-mcp', - version: MCP_VERSION, - } - ); + const relay = new RelayCast({ + apiKey: options.apiKey, + baseUrl: options.baseUrl, + }); const registered = await relay.agents.registerOrRotate({ name: options.agentName, @@ -699,6 +1505,8 @@ export async function startPatchedStdio(options: PatchedMcpServerOptions): Promi await mcpServer.connect(transport); } +export const startAgentRelayMcpStdio = startPatchedStdio; + export function optionsFromEnv(): PatchedMcpServerOptions { const apiKey = resolveEnv('RELAY_API_KEY'); const agentName = @@ -710,6 +1518,7 @@ export function optionsFromEnv(): PatchedMcpServerOptions { agentName, agentType: normalizeAgentType(resolveEnv('RELAY_AGENT_TYPE')), strictAgentName: envFlagEnabled(resolveEnv('RELAY_STRICT_AGENT_NAME')), + skipBootstrap: envFlagEnabled(resolveEnv('RELAY_SKIP_BOOTSTRAP')), }; } diff --git a/tests/mcp_merge_e2e.rs b/tests/mcp_merge_e2e.rs index 6398e7c48..2b6fdbe01 100644 --- a/tests/mcp_merge_e2e.rs +++ b/tests/mcp_merge_e2e.rs @@ -225,7 +225,7 @@ async fn e2e_stale_relaycast_is_overridden_by_broker_credentials() { "mcpServers": { "relaycast": { "command": "npx", - "args": ["-y", "@relaycast/mcp"], + "args": ["-y", "agent-relay", "mcp"], "env": { "RELAY_API_KEY": "rk_stale_old_key", "RELAY_AGENT_NAME": "old-agent-name", diff --git a/web/components/docs/DocsNav.tsx b/web/components/docs/DocsNav.tsx index 64532c369..84d168c23 100644 --- a/web/components/docs/DocsNav.tsx +++ b/web/components/docs/DocsNav.tsx @@ -8,6 +8,7 @@ import { Activity, BookOpen, Bot, + Cable, Cloud, Clock3, Compass, @@ -43,6 +44,7 @@ const navIcons: Record = { introduction: Compass, quickstart: Rocket, 'spawning-an-agent': Bot, + harnesses: Cable, 'sending-messages': Send, 'event-handlers': Activity, channels: Hash, diff --git a/web/content/docs/harnesses.mdx b/web/content/docs/harnesses.mdx new file mode 100644 index 000000000..313e5aed8 --- /dev/null +++ b/web/content/docs/harnesses.mdx @@ -0,0 +1,417 @@ +--- +title: Harnesses +description: Define CLI and runtime harness adapters for Agent Relay spawning and workflows. +--- + +A harness tells Relay how to control an agent runtime. The production path today is a CLI harness: serializable config that tells the Rust broker which binary to run, how to render arguments, how to inject Relay MCP arguments, which bypass/model flags the CLI understands, and which lifecycle adapter should handle built-in behavior such as sessions. + +Relay ships built-in harnesses for common coding agents, including `codex`, `claude`, `opencode`, `gemini`, `goose`, `aider`, `droid`, and Cursor Agent. Those built-ins are defined with the same `HarnessDefinition` shape that you use for custom harnesses. + + +Harnesses are used by real `agent-relay` spawning. They are not limited to workflow execution. SDK spawn calls send the resolved harness definition to the broker with the spawn request, and the broker returns the provider `sessionId` plus the spawned process `pid` when available. + + +## Built-in harnesses + +Use a built-in harness by naming it in a spawn call: + + +```typescript TypeScript file="spawn-codex.ts" +import { AgentRelay } from '@agent-relay/sdk'; + +const relay = new AgentRelay({ channels: ['dev'] }); + +const worker = await relay.spawn('CodexWorker', 'codex', 'Fix the failing auth tests.', { + model: 'gpt-5-codex', + channels: ['dev'], +}); + +await worker.waitForReady(); +``` + +```python Python file="spawn_codex.py" +from agent_relay import AgentRelay, SpawnOptions + +relay = AgentRelay(channels=["dev"]) + +worker = await relay.spawn( + "CodexWorker", + "codex", + "Fix the failing auth tests.", + SpawnOptions( + model="gpt-5-codex", + channels=["dev"], + ), +) + +await worker.wait_for_ready() +``` + + +The built-in `codex` harness is equivalent to this adapter config: + + +```typescript TypeScript +import { BUILTIN_HARNESS_DEFINITIONS } from '@agent-relay/sdk'; + +console.log(BUILTIN_HARNESS_DEFINITIONS.codex); +``` + +```python Python +from agent_relay import HarnessDefinition + +codex_harness = HarnessDefinition( + adapter="codex", + binary="codex", + non_interactive_args=["exec", "{bypass}", "{task}", "{args}"], + bypass_flag="--dangerously-bypass-approvals-and-sandbox", + bypass_aliases=["--full-auto"], + search_paths=["~/.local/bin"], +) + +print(codex_harness.to_dict()) +``` + + +```json +{ + "adapter": "codex", + "binary": "codex", + "nonInteractiveArgs": ["exec", "{bypass}", "{task}", "{args}"], + "bypassFlag": "--dangerously-bypass-approvals-and-sandbox", + "bypassAliases": ["--full-auto"], + "searchPaths": ["~/.local/bin"] +} +``` + +`adapter` identifies the broker-owned lifecycle adapter. For `codex`, that includes Codex-specific MCP args, model fallback behavior, and resumable session setup. Most custom harnesses can omit `adapter`; Relay defaults it to the harness name. When `adapter` points at a built-in harness, Relay starts from the built-in config and applies your overrides. + +## Define a custom harness + +Register harnesses on `AgentRelay` when several spawns should share the same adapter. Python accepts the same serializable harness definition; use snake_case fields on the dataclass and the SDK serializes them to the broker format. + + +```typescript TypeScript file="spawn-qwen.ts" +import { AgentRelay, type HarnessDefinition } from '@agent-relay/sdk'; + +const qwenHarness: HarnessDefinition = { + binary: 'qwen', + interactiveArgs: ['run', '{modelArgs}', '{mcpArgs}', '{args}', '{sessionArgs}'], + nonInteractiveArgs: ['run', '--prompt', '{task}', '{args}'], + modelArgs: ['-m', '{model}'], + bypassFlag: '--yes', + searchPaths: ['~/.local/bin'], +}; + +const relay = new AgentRelay({ + channels: ['dev'], + harnesses: { + qwen: qwenHarness, + }, +}); + +const reviewer = await relay.spawn('QwenReviewer', 'qwen', 'Review the latest diff.', { + model: 'qwen3-coder', + channels: ['dev'], + args: ['--verbose'], +}); + +await reviewer.waitForReady(); +``` + +```python Python file="spawn_qwen.py" +from agent_relay import AgentRelay, HarnessDefinition, SpawnOptions + +relay = AgentRelay( + channels=["dev"], + harnesses={ + "qwen": HarnessDefinition( + binary="qwen", + interactive_args=["run", "{modelArgs}", "{mcpArgs}", "{args}", "{sessionArgs}"], + non_interactive_args=["run", "--prompt", "{task}", "{args}"], + model_args=["-m", "{model}"], + bypass_flag="--yes", + search_paths=["~/.local/bin"], + ) + }, +) + +reviewer = await relay.spawn( + "QwenReviewer", + "qwen", + "Review the latest diff.", + SpawnOptions( + model="qwen3-coder", + channels=["dev"], + args=["--verbose"], + ), +) + +await reviewer.wait_for_ready() +``` + + +For TypeScript registry extensions, `CLIHarnessAdapter` is the SDK type for CLI command adapters. It accepts either a serializable `HarnessDefinition` or a function-backed `CliDefinition`: + +```typescript file="register-local-cli.ts" +import { registerHarnessAdapter, type CLIHarnessAdapter } from '@agent-relay/sdk'; + +const localCli: CLIHarnessAdapter = { + binaries: ['local-agent'], + nonInteractiveArgs: (task, extraArgs = []) => ['run', '--prompt', task, ...extraArgs], + modelArgs: (model) => ['--model-id', model], +}; + +registerHarnessAdapter('local-agent', localCli); +``` + +Use `HarnessDefinition` for YAML, Python, per-spawn `harness`, and `AgentRelay({ harnesses })` config. Use `CLIHarnessAdapter` when registering adapters in TypeScript code. The older `HarnessAdapter` export is kept as a deprecated alias for `CLIHarnessAdapter`. + +You can also attach a harness to one spawn: + + +```typescript TypeScript file="spawn-local-agent.ts" +const agent = await relay.spawn('LocalAgent', 'local-agent', 'Inspect the repo.', { + model: 'local-large', + harness: { + binary: 'local-agent', + interactiveArgs: ['serve', '{modelArgs}', '{mcpArgs}', '{args}'], + modelArgs: ['--model-id', '{model}'], + searchPaths: ['~/bin', '~/.local/bin'], + }, +}); +``` + +```python Python file="spawn_local_agent.py" +from agent_relay import HarnessDefinition, SpawnOptions + +agent = await relay.spawn( + "LocalAgent", + "local-agent", + "Inspect the repo.", + SpawnOptions( + model="local-large", + harness=HarnessDefinition( + binary="local-agent", + interactive_args=["serve", "{modelArgs}", "{mcpArgs}", "{args}"], + model_args=["--model-id", "{model}"], + search_paths=["~/bin", "~/.local/bin"], + ), + ), +) +``` + + +## Spawn metadata + +When the broker starts a harness, the TypeScript `Agent` handle keeps both the native/provider session id and the OS process id when the broker reports them: + +```typescript file="spawn-metadata.ts" +const agent = await relay.spawn('Reviewer', 'codex', 'Review the current branch.'); + +await agent.waitForReady(); + +console.log(agent.sessionId); +console.log(agent.pid); +``` + +`sessionId` is useful for resumable harnesses such as Codex or Claude. `pid` is the broker-controlled harness process id, which is useful for diagnostics, process correlation, and lifecycle hooks. + +## Extend a built-in lifecycle + +If your wrapper is compatible with a built-in harness, set `adapter` to keep that lifecycle behavior while changing the executable or flags: + + +```typescript TypeScript file="spawn-company-codex.ts" +const relay = new AgentRelay({ + harnesses: { + 'company-codex': { + adapter: 'codex', + binary: 'company-codex', + searchPaths: ['~/company/bin'], + }, + }, +}); + +await relay.spawn('CompanyCodex', 'company-codex', 'Update the billing tests.'); +``` + +```python Python file="spawn_company_codex.py" +from agent_relay import AgentRelay, HarnessDefinition + +relay = AgentRelay() + +relay.register_harness( + "company-codex", + HarnessDefinition( + adapter="codex", + binary="company-codex", + search_paths=["~/company/bin"], + ), +) +``` + + +This pattern is useful for internal wrappers, renamed binaries, or local installs that need different search paths while still behaving like a known coding harness. + +## Harness lifecycle + +Harness lifecycle is the broker-owned spawn preparation for a CLI. It is not a callback function inside `HarnessDefinition`; the definition is serializable config that tells the broker which lifecycle adapter to use and how to render the command. + +For PTY-backed spawning, Relay applies a harness in this order: + +1. **Select the harness.** The SDK sends the per-spawn `harness` when present. Otherwise it uses the harness registered on the `AgentRelay` instance, then the built-in registry for the requested `cli`. +2. **Resolve the adapter.** The broker normalizes the `cli` name and reads `adapter`. If `adapter` names a built-in lifecycle such as `codex`, `claude`, `opencode`, or `cursor`, the broker starts from that built-in definition and applies your overrides. +3. **Resolve the executable.** The broker tries `binaries` in order, or `binary`, then the requested `cli`. `searchPaths` are checked before `PATH`, and `~` expands to the user home directory. +4. **Prepare adapter behavior.** Built-in adapters add CLI-specific setup such as Relay MCP wiring, session handling, model fallbacks, config-file writes, or bypass flag handling. Generic custom adapters skip built-in setup and rely on the fields in the harness definition. +5. **Render argv.** The broker computes `{bypass}`, `{modelArgs}`, `{mcpArgs}`, `{args}`, and `{sessionArgs}`, then renders `interactiveArgs`. Placeholders that represent vectors should be their own argv item. +6. **Start and report lifecycle.** The broker starts the PTY process, registers the worker, and emits normal agent lifecycle events such as `agentSpawned`, `agentReady`, `agentIdle`, `agentExited`, and `agentReleased`. + +The `adapter` field is what selects broker-owned lifecycle behavior. The executable name is independent: + +| Harness | `adapter` | Lifecycle behavior | +| --- | --- | --- | +| `codex` | `codex` | Adds Relay MCP `--config` values, disables the Codex update prompt, applies Codex model fallback behavior, and creates resumable sessions when the spawn does not already provide a prompt, subcommand, or session reference. | +| `company-codex` | `codex` | Runs your wrapper binary while keeping the Codex lifecycle above. | +| `claude` | `claude` | Adds Relay MCP config with `--mcp-config` and manages Claude session flags unless the caller already supplied resume or session options. | +| `opencode` | `opencode` | Writes or updates `opencode.json` for Relay MCP and selects the `relaycast` OpenCode agent when the caller did not provide `--agent`. | +| `cursor` | `cursor` | Resolves Cursor Agent binary aliases and writes `.cursor/mcp.json` for Relay MCP. | +| `qwen` | `qwen` | Uses generic template rendering. `{mcpArgs}` is empty until Relay has a lifecycle adapter that knows how to configure Qwen's MCP surface. | + + +Custom harnesses do not need Rust when they only need different binaries, args, model flags, bypass flags, search paths, or non-interactive workflow behavior. A new broker lifecycle adapter is only needed when the CLI requires custom broker-side work, such as writing a config file, minting a session before spawn, or translating Relay MCP config into a CLI-specific format. Built-in lifecycle adapters live in the broker so all SDKs and agent-initiated spawns get the same behavior. + + +## Runtime adapter surface + +CLI harness config is the surface the broker consumes today. The TypeScript SDK also exports `HarnessRuntimeAdapter` to name the lifecycle contract for non-CLI or bridge-backed harnesses: + +```typescript file="web-harness.ts" +import type { HarnessRuntimeAdapter } from '@agent-relay/sdk'; + +export const webHarness: HarnessRuntimeAdapter = { + kind: 'http', + async initHarness(context) { + const response = await fetch('http://127.0.0.1:8787/harness/init', { + method: 'POST', + body: JSON.stringify(context), + }); + + return response.json(); + }, + async register(context) { + await fetch('http://127.0.0.1:8787/harness/register', { + method: 'POST', + body: JSON.stringify(context), + }); + }, + async receiveMessage(message, context) { + await fetch('http://127.0.0.1:8787/harness/messages', { + method: 'POST', + body: JSON.stringify({ message, context }), + }); + }, + async sendMessage(message, context) { + await fetch('http://127.0.0.1:8787/relay/messages', { + method: 'POST', + body: JSON.stringify({ message, context }), + }); + }, + async releaseHarness(context) { + await fetch('http://127.0.0.1:8787/harness/release', { + method: 'POST', + body: JSON.stringify(context), + }); + }, +}; +``` + +`initHarness` returns `{ sessionId, pid }` or `{ sessionId, processId }` when the adapter starts a process. `receiveMessage` delivers Relay messages to the harness. `sendMessage` emits harness-originated messages back through Relay. `releaseHarness` gives the adapter a cleanup hook. + +The Rust broker cannot call in-memory TypeScript functions directly, so a runtime adapter still needs a concrete serializable boundary such as a CLI/stdio worker or an HTTP service. The method names above are the control surface that boundary should expose. + +## Lifecycle hooks and events + +Harness lifecycle runs inside the broker after the spawn request is accepted. SDK event handlers are separate: + +- `beforeAgentSpawn` and `afterAgentSpawn` in the TypeScript SDK run at the SDK call site before and after the HTTP spawn request. `beforeAgentSpawn` can patch fields such as `args`, `channels`, `task`, `model`, `harness`, `team`, or `agentToken` before the broker sees them. +- Per-spawn `onStart`, `onSuccess`, and `onError` callbacks run only for the spawn call that provided them. +- Broker events such as `agentSpawned`, `agentReady`, `agentIdle`, and `agentExited` fire after the broker observes those process lifecycle changes. + +Use the [event handlers](/docs/event-handlers) page for telemetry, audit logging, UI updates, and spawn input patching. Use harness definitions for durable command construction and adapter selection. + +## Workflow YAML + +Workflow YAML uses the same `harnesses` map. Interactive workflow agents use the harness for broker spawning. Non-interactive agents use `nonInteractiveArgs` to run a one-shot command. + +```yaml file="workflow.yaml" +version: "1.0" +name: custom-harness-review + +harnesses: + qwen: + binary: qwen + interactiveArgs: ["run", "{modelArgs}", "{mcpArgs}", "{args}", "{sessionArgs}"] + nonInteractiveArgs: ["run", "--prompt", "{task}", "{args}"] + modelArgs: ["-m", "{model}"] + bypassFlag: "--yes" + searchPaths: ["~/.local/bin"] + +agents: + - name: reviewer + cli: qwen + role: "Reviews implementation diffs" + constraints: + model: qwen3-coder + +workflows: + - name: default + steps: + - name: review + agent: reviewer + task: "Review the current branch and list blockers." +``` + +## Harness fields + +| Field | Description | +| --- | --- | +| `adapter` | Broker lifecycle adapter id. Defaults to the harness name. Use a built-in id such as `codex`, `claude`, or `opencode` to inherit its defaults and lifecycle behavior with a custom binary. | +| `binary` | Primary executable. Shorthand for `binaries: [binary]`. | +| `binaries` | Executables to try in order. Defaults to the harness name. | +| `interactiveArgs` | Argv template for PTY-backed broker spawning. Defaults to `['{bypass}', '{modelArgs}', '{mcpArgs}', '{args}', '{sessionArgs}']`. | +| `nonInteractiveArgs` | Argv template for one-shot workflow process execution. Defaults to `['{task}', '{args}']`. | +| `modelArgs` | Argv template for a model id. Defaults to `['--model', '{model}']`. | +| `bypassFlag` | Permission-bypass flag Relay may inject for unattended spawned agents. | +| `bypassAliases` | Alternate bypass flags that should suppress injecting `bypassFlag` when already present in `args`. | +| `searchPaths` | Extra install directories checked before `PATH`. `~` expands to the user's home directory. | +| `ignoreExitCode` | Treat non-zero one-shot workflow exits as captured output instead of failure. | +| `proxyProvider` | Credential proxy provider for agents using proxied credentials. Supported values are `openai`, `anthropic`, and `openrouter`. | +| `aliases` | Additional names that should resolve to the same adapter in the TypeScript registry. | + +## Template placeholders + +Use placeholders as whole argv items when they expand to multiple arguments: + +| Placeholder | Where it applies | Expansion | +| --- | --- | --- | +| `{modelArgs}` | `interactiveArgs` | The rendered `modelArgs` vector. | +| `{mcpArgs}` | `interactiveArgs` | Relay's MCP and protocol arguments for the spawned agent. | +| `{args}` | `interactiveArgs`, `nonInteractiveArgs` | Extra args from the spawn call or workflow agent. | +| `{sessionArgs}` | `interactiveArgs` | Session resume arguments when Relay has them. | +| `{bypass}` | `interactiveArgs`, `nonInteractiveArgs` | The configured `bypassFlag`, omitted when not applicable. | +| `{task}` | `nonInteractiveArgs` | The workflow step task for one-shot process execution. | +| `{model}` | `modelArgs` | The requested model id. | + +The `{{placeholder}}` form is also accepted. For vector placeholders such as `{mcpArgs}` and `{args}`, keep the placeholder as its own array item so it can expand to zero or more argv items. + + +If you override `interactiveArgs`, include `{mcpArgs}` when using a built-in or compatible adapter that produces Relay MCP arguments. For a brand-new CLI, `{mcpArgs}` stays empty until Relay has a lifecycle adapter that can translate Relay MCP config into that CLI's format. + + +## See Also + +- [Spawning an agent](/docs/spawning-an-agent) - High-level spawn APIs +- [Workflow Reference](/docs/reference-workflows) - YAML and builder workflow APIs +- [TypeScript SDK Reference](/docs/typescript-sdk) - `AgentRelay` and `AgentRelayClient` +- [Python SDK Reference](/docs/python-sdk) - Python facade and builder exports diff --git a/web/content/docs/reference-cli.mdx b/web/content/docs/reference-cli.mdx index d8459e573..0ae54727e 100644 --- a/web/content/docs/reference-cli.mdx +++ b/web/content/docs/reference-cli.mdx @@ -24,7 +24,7 @@ Sample output: { "args": [ "--mcp-config", - "{\"mcpServers\":{\"relaycast\":{\"command\":\"npx\",\"args\":[\"-y\",\"@relaycast/mcp\"]}}}" + "{\"mcpServers\":{\"relaycast\":{\"command\":\"npx\",\"args\":[\"-y\",\"agent-relay\",\"mcp\"]}}}" ], "sideEffectFiles": [], "agentToken": null diff --git a/web/content/docs/reference-workflows.mdx b/web/content/docs/reference-workflows.mdx index 7112cdbf1..b32c9d352 100644 --- a/web/content/docs/reference-workflows.mdx +++ b/web/content/docs/reference-workflows.mdx @@ -177,4 +177,5 @@ The runner can complete a step from several signals: - [TypeScript SDK Reference](/docs/typescript-sdk) — High-level `AgentRelay` API - [Python SDK Reference](/docs/python-sdk) — Python facade and builder exports +- [Harnesses](/docs/harnesses) — Custom CLI adapters for spawning and workflows - [Communicate Mode](/docs/communicate) — Connect existing framework agents to Relaycast diff --git a/web/content/docs/spawning-an-agent.mdx b/web/content/docs/spawning-an-agent.mdx index 48e9dc1eb..f459eed67 100644 --- a/web/content/docs/spawning-an-agent.mdx +++ b/web/content/docs/spawning-an-agent.mdx @@ -43,6 +43,8 @@ that messages are routed and injected into the agent's runtime as needed. Agents can also spawn other agents via the CLI or MCP. +Use [harnesses](/docs/harnesses) to define custom agent CLIs without waiting for Relay to add them to the built-in spawn registry. + ### Dynamic spawn ```typescript TypeScript file="dynamicspawn.ts" diff --git a/web/lib/docs-nav.ts b/web/lib/docs-nav.ts index 404751ff1..35f822c9b 100644 --- a/web/lib/docs-nav.ts +++ b/web/lib/docs-nav.ts @@ -20,6 +20,7 @@ export const docsNav: NavGroup[] = [ title: 'Basics', items: [ { title: 'Spawning an agent', slug: 'spawning-an-agent' }, + { title: 'Harnesses', slug: 'harnesses' }, { title: 'Sending messages', slug: 'sending-messages' }, { title: 'Event handlers', slug: 'event-handlers' }, { title: 'Channels', slug: 'channels' },