From 79311e9ff3b10c353b41282639179f1252dbf7e3 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Thu, 21 May 2026 22:05:18 -0400 Subject: [PATCH 01/14] fix(web): restore legacy production origin hostname --- web/sst.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/sst.config.ts b/web/sst.config.ts index 3a936a4fd..a76915f92 100644 --- a/web/sst.config.ts +++ b/web/sst.config.ts @@ -8,7 +8,7 @@ export default $config({ }, run() { const isProd = $app.stage === 'production'; - const domain = isProd ? 'origin.agentrelay.net' : `${$app.stage}.agentrelay.net`; + const domain = isProd ? 'orgin.agentrelay.net' : `${$app.stage}.agentrelay.net`; const NEXT_PUBLIC_POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://i.agentrelay.com'; const NEXT_PUBLIC_POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY ?? ''; @@ -19,7 +19,7 @@ export default $config({ NEXT_PUBLIC_POSTHOG_HOST, NEXT_PUBLIC_POSTHOG_KEY, }, - // Production deploys land on origin.agentrelay.net; SEO canonicals are set in Next metadata. + // Production deploys land on orgin.agentrelay.net; SEO canonicals are set in Next metadata. domain: { name: domain, dns: sst.cloudflare.dns({ proxy: true }) }, }); }, From e5db219cf71a957d47c6208a7a9dc68497915d20 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Thu, 21 May 2026 23:20:05 -0400 Subject: [PATCH 02/14] fix(web): restore corrected production origin hostname --- web/sst.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/sst.config.ts b/web/sst.config.ts index a76915f92..3a936a4fd 100644 --- a/web/sst.config.ts +++ b/web/sst.config.ts @@ -8,7 +8,7 @@ export default $config({ }, run() { const isProd = $app.stage === 'production'; - const domain = isProd ? 'orgin.agentrelay.net' : `${$app.stage}.agentrelay.net`; + const domain = isProd ? 'origin.agentrelay.net' : `${$app.stage}.agentrelay.net`; const NEXT_PUBLIC_POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://i.agentrelay.com'; const NEXT_PUBLIC_POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY ?? ''; @@ -19,7 +19,7 @@ export default $config({ NEXT_PUBLIC_POSTHOG_HOST, NEXT_PUBLIC_POSTHOG_KEY, }, - // Production deploys land on orgin.agentrelay.net; SEO canonicals are set in Next metadata. + // Production deploys land on origin.agentrelay.net; SEO canonicals are set in Next metadata. domain: { name: domain, dns: sst.cloudflare.dns({ proxy: true }) }, }); }, From 28ec8236a6f1461a1ecac839cde82112fb1e8489 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 24 May 2026 17:09:48 -0400 Subject: [PATCH 03/14] Update relay workflow guidance --- .claude/scheduled_tasks.lock | 1 + .github/workflows/publish.yml | 155 +- .trajectories/active/traj_4b2d63f6ljvh.json | 150 + .../completed/2026-05/traj_dqgg2q4scsvt.json | 53 + .../completed/2026-05/traj_dqgg2q4scsvt.md | 31 + .../completed/2026-05/traj_ft1pwdlcrmcn.json | 60 + .../completed/2026-05/traj_ft1pwdlcrmcn.md | 38 + .../2026-05/traj_ft1pwdlcrmcn.trace.json | 30 + .../completed/2026-05/traj_pjadgfw0mtw4.json | 61 + .../completed/2026-05/traj_pjadgfw0mtw4.md | 38 + .../2026-05/traj_pjadgfw0mtw4.trace.json | 101 + .../completed/2026-05/traj_r3eic6rt84pq.json | 25 + .../completed/2026-05/traj_r3eic6rt84pq.md | 14 + .trajectories/index.json | 38 +- AGENTS.md | 20 +- CHANGELOG.md | 3010 ++++------------- crates/broker/src/cli/mod.rs | 12 +- crates/broker/src/cli_mcp_args.rs | 6 +- crates/broker/src/codex_session.rs | 241 ++ crates/broker/src/lib.rs | 1 + crates/broker/src/listen_api.rs | 33 +- crates/broker/src/protocol.rs | 54 + crates/broker/src/relaycast/mod.rs | 2 +- crates/broker/src/runtime/api.rs | 4 + crates/broker/src/runtime/init.rs | 1 - crates/broker/src/runtime/mod.rs | 9 +- crates/broker/src/runtime/relaycast_events.rs | 8 +- crates/broker/src/runtime/session.rs | 14 - crates/broker/src/runtime/spawn_spec.rs | 3 + crates/broker/src/runtime/tests.rs | 34 + crates/broker/src/runtime/worker_events.rs | 6 +- crates/broker/src/snippets.rs | 64 +- crates/broker/src/supervisor.rs | 2 + crates/broker/src/worker.rs | 769 ++++- crates/broker/src/wrap.rs | 1 - docs/cli-command-tree.md | 236 ++ package-lock.json | 34 - package.json | 5 +- packages/openclaw/skill/SKILL.md | 4 +- packages/openclaw/src/setup.ts | 6 +- packages/sdk-py/src/agent_relay/__init__.py | 4 + packages/sdk-py/src/agent_relay/builder.py | 20 + packages/sdk-py/src/agent_relay/client.py | 4 + packages/sdk-py/src/agent_relay/relay.py | 86 +- packages/sdk-py/src/agent_relay/types.py | 64 +- packages/sdk-py/tests/test_builder.py | 25 + packages/sdk-py/tests/test_relay_harness.py | 28 + .../Sources/AgentRelaySDK/RelayTypes.swift | 8 +- packages/sdk/README.md | 29 + .../sdk/src/__tests__/spawn-harness.test.ts | 106 + packages/sdk/src/cli-registry.ts | 197 +- packages/sdk/src/cli-resolver.ts | 5 +- packages/sdk/src/client.ts | 24 +- packages/sdk/src/index.ts | 1 + packages/sdk/src/lifecycle-hooks.ts | 7 +- packages/sdk/src/protocol.ts | 7 + packages/sdk/src/relay-adapter.ts | 3 + packages/sdk/src/relay.ts | 100 +- packages/sdk/src/types.ts | 6 + packages/sdk/src/workflows/README.md | 27 + .../__tests__/harness-adapters.test.ts | 78 + packages/sdk/src/workflows/builder.ts | 45 + packages/sdk/src/workflows/index.ts | 8 + .../src/workflows/process-backend-executor.ts | 3 +- packages/sdk/src/workflows/process-spawner.ts | 10 +- packages/sdk/src/workflows/run.ts | 5 +- packages/sdk/src/workflows/runner.ts | 88 +- packages/sdk/src/workflows/schema.json | 37 +- packages/sdk/src/workflows/types.ts | 4 + packages/workflow-types/src/index.ts | 45 + plugins/codex-relay-skill/README.md | 2 +- .../codex-config/config.toml | 2 +- plugins/codex-relay-skill/scripts/setup.sh | 2 +- .../gemini-relay-extension/package-lock.json | 1158 ------- plugins/gemini-relay-extension/package.json | 3 - .../gemini-relay-extension/relay-server.js | 28 +- scripts/watch-cli-tools.sh | 2 +- src/cli/bootstrap.ts | 12 +- src/cli/commands/agent-management.ts | 19 +- src/cli/relaycast-mcp.startup.test.ts | 395 ++- src/cli/relaycast-mcp.ts | 1234 +++++-- tests/mcp_merge_e2e.rs | 2 +- web/content/docs/reference-cli.mdx | 2 +- web/sst.config.ts | 7 +- 84 files changed, 5165 insertions(+), 4151 deletions(-) create mode 100644 .claude/scheduled_tasks.lock create mode 100644 .trajectories/active/traj_4b2d63f6ljvh.json create mode 100644 .trajectories/completed/2026-05/traj_dqgg2q4scsvt.json create mode 100644 .trajectories/completed/2026-05/traj_dqgg2q4scsvt.md create mode 100644 .trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json create mode 100644 .trajectories/completed/2026-05/traj_ft1pwdlcrmcn.md create mode 100644 .trajectories/completed/2026-05/traj_ft1pwdlcrmcn.trace.json create mode 100644 .trajectories/completed/2026-05/traj_pjadgfw0mtw4.json create mode 100644 .trajectories/completed/2026-05/traj_pjadgfw0mtw4.md create mode 100644 .trajectories/completed/2026-05/traj_pjadgfw0mtw4.trace.json create mode 100644 .trajectories/completed/2026-05/traj_r3eic6rt84pq.json create mode 100644 .trajectories/completed/2026-05/traj_r3eic6rt84pq.md create mode 100644 crates/broker/src/codex_session.rs create mode 100644 docs/cli-command-tree.md create mode 100644 packages/sdk-py/tests/test_relay_harness.py create mode 100644 packages/sdk/src/__tests__/spawn-harness.test.ts create mode 100644 packages/sdk/src/workflows/__tests__/harness-adapters.test.ts 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/publish.yml b/.github/workflows/publish.yml index 9d2815da4..b88e125af 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1950,101 +1950,100 @@ jobs: }; }); - function extractPR(subject, body) { - const m = (subject + ' ' + body).match(/#(\d+)/); - return m ? `(#${m[1]})` : ''; + function parseSubject(subject) { + const conventional = subject.match( + /^(feat|fix|refactor|perf|chore|test|ci|docs|build|style|security|deprecate|deprecated|remove|removed)(\(([^)]+)\))?(!)?:\s*(.*)$/i + ); + + if (!conventional) { + return { + type: 'changed', + scope: '', + title: cleanTitle(subject), + breaking: false, + }; + } + + const [, typeRaw, , scopeRaw = '', bang = '', titleRaw] = conventional; + const type = typeRaw.toLowerCase(); + return { + type, + scope: scopeRaw.toLowerCase(), + title: cleanTitle(titleRaw), + breaking: bang === '!', + }; } - function formatTitle(subject) { - const cleaned = subject.replace( - /^(feat|fix|refactor|perf|chore|test|ci|docs|build|style)(\([^)]+\))?!?:\s*/i, - '' - ); + function cleanTitle(title) { + const cleaned = title + .replace(/\s*\(#[0-9]+(?:[^)]*)?\)/g, '') + .replace(/\s+#\d+\b/g, '') + .replace(/\s+/g, ' ') + .trim(); return cleaned.charAt(0).toUpperCase() + cleaned.slice(1); } - function getType(subject) { - const m = subject.match(/^(feat|fix|refactor|perf|chore|test|ci|docs|build|style)(\([^)]+\))?(!)?:/i); - if (!m) return 'other'; - const type = m[1].toLowerCase(); - const scope = (m[2] || '').replace(/[()]/g, ''); - const breaking = m[3] === '!'; - if (breaking) return 'breaking'; - if (type === 'feat') return 'feat'; - if (type === 'fix') return 'fix'; - if (type === 'refactor' || type === 'perf' || type === 'build') return 'arch'; - if (type === 'test' || type === 'ci') return 'reliability'; - if (type === 'chore' && scope === 'deps') return 'deps'; - if (type === 'chore' && scope === 'release') return 'release'; - if (type === 'chore') return 'deps'; - return 'other'; + function shouldSkip({ type, scope, title }) { + const text = title.toLowerCase(); + if (type === 'chore' && (scope === 'release' || scope === 'prerelease')) return true; + if (scope === 'trajectories' || scope === 'comments') return true; + if (text.includes('compact trajectories')) return true; + if (text.includes('record ') && text.includes('trajectory')) return true; + if (text.includes('address pr review')) return true; + if (text.includes('review feedback')) return true; + if (text.includes('retrigger flaky')) return true; + if (text === 'clean up skills' || text === 'bump skills') return true; + if (text.startsWith('auto-format ') || text.startsWith('format ')) return true; + if (text.startsWith('revert version bump')) return true; + return title.length === 0; } - const cats = { breaking: [], feat: [], fix: [], arch: [], reliability: [], deps: [], release: [] }; + function sectionFor(commit) { + if (commit.breaking) return 'Breaking Changes'; + if (commit.type === 'feat') return 'Added'; + if (commit.type === 'fix') return 'Fixed'; + if (commit.type === 'security') return 'Security'; + if (commit.type === 'deprecate' || commit.type === 'deprecated') return 'Deprecated'; + if (commit.type === 'remove' || commit.type === 'removed') return 'Removed'; + return 'Changed'; + } + + const sections = new Map([ + ['Breaking Changes', []], + ['Added', []], + ['Changed', []], + ['Deprecated', []], + ['Removed', []], + ['Fixed', []], + ['Security', []], + ]); for (const c of commits) { - const type = getType(c.subject); - const title = formatTitle(c.subject); - const pr = extractPR(c.subject, c.body); - if (cats[type]) cats[type].push({ title, pr }); + const parsed = parseSubject(c.subject); + if (shouldSkip(parsed)) continue; + const section = sectionFor(parsed); + const entries = sections.get(section); + if (!entries.includes(parsed.title)) entries.push(parsed.title); + } + + if ([...sections.values()].every(entries => entries.length === 0)) { + console.log('No changelog-worthy commits since last tag, skipping changelog'); + process.exit(0); } const lines = []; lines.push(`## [${newVersion}] - ${today}`); lines.push(''); - const hasProd = cats.breaking.length + cats.feat.length + cats.fix.length > 0; - const hasTech = cats.arch.length + cats.reliability.length + cats.deps.length > 0; - - if (hasProd) { - lines.push('### Product Perspective'); - if (cats.breaking.length > 0) { - lines.push('#### Breaking Changes'); - for (const c of cats.breaking) lines.push(`- **${c.title}** ${c.pr}`.trimEnd()); - lines.push(''); - } - if (cats.feat.length > 0) { - lines.push('#### User-Facing Features & Improvements'); - for (const c of cats.feat) lines.push(`- **${c.title}** ${c.pr}`.trimEnd()); - lines.push(''); - } - if (cats.fix.length > 0) { - lines.push('#### User-Impacting Fixes'); - for (const c of cats.fix) lines.push(`- ${c.title} ${c.pr}`.trimEnd()); - lines.push(''); - } + for (const [section, entries] of sections) { + if (entries.length === 0) continue; + lines.push(`### ${section}`); + lines.push(''); + for (const entry of entries) lines.push(`- ${entry}`); + lines.push(''); } - if (hasTech) { - lines.push('### Technical Perspective'); - if (cats.arch.length > 0) { - lines.push('#### Architecture & API Changes'); - for (const c of cats.arch) lines.push(`- ${c.title} ${c.pr}`.trimEnd()); - lines.push(''); - } - if (cats.reliability.length > 0) { - lines.push('#### Performance & Reliability'); - for (const c of cats.reliability) lines.push(`- ${c.title} ${c.pr}`.trimEnd()); - lines.push(''); - } - if (cats.deps.length > 0) { - lines.push('#### Dependencies & Tooling'); - for (const c of cats.deps) lines.push(`- ${c.title} ${c.pr}`.trimEnd()); - lines.push(''); - } - } - - if (!hasTech) { - lines.push('### Technical Perspective'); - } - lines.push('#### Releases'); - lines.push(`- v${newVersion}`); - lines.push(''); - lines.push('---'); - lines.push(''); - lines.push(''); - - const newEntry = lines.join('\n'); + const newEntry = lines.join('\n').trimEnd() + '\n\n'; const changelog = readFileSync('CHANGELOG.md', 'utf-8'); // Insert after the [Unreleased] block, before the first versioned entry diff --git a/.trajectories/active/traj_4b2d63f6ljvh.json b/.trajectories/active/traj_4b2d63f6ljvh.json new file mode 100644 index 000000000..ac32a7ff4 --- /dev/null +++ b/.trajectories/active/traj_4b2d63f6ljvh.json @@ -0,0 +1,150 @@ +{ + "id": "traj_4b2d63f6ljvh", + "version": 1, + "task": { + "title": "Fix CI run 26263517444" + }, + "status": "active", + "startedAt": "2026-05-22T01:47:08.312Z", + "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", + "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" + } + ] + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "898b8ee37197075913578fdb1c2fe4139b2ac562", + "endRef": "898b8ee37197075913578fdb1c2fe4139b2ac562" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json new file mode 100644 index 000000000..38a91cd6c --- /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": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b", + "endRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b" + } +} \ No newline at end of file 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..ef67b9074 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json @@ -0,0 +1,60 @@ +{ + "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": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "0d716e468654c8d98b867df09298aafce0b23604", + "endRef": "898b8ee37197075913578fdb1c2fe4139b2ac562", + "traceId": "672b9fc7-be82-421b-8103-a18cdd0f914f" + } +} \ No newline at end of file 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_pjadgfw0mtw4.json b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json new file mode 100644 index 000000000..67930703f --- /dev/null +++ b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json @@ -0,0 +1,61 @@ +{ + "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": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "23e07f08a24c2913a918aee2a0c9af9c54e4d40d", + "endRef": "c13ee3189b10c83fc52c4d017e268eedb5119f48", + "traceId": "7daa40cf-b98c-4790-b1d4-06c1fc4607ff" + } +} \ No newline at end of file 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_r3eic6rt84pq.json b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json new file mode 100644 index 000000000..4b97af738 --- /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": "/Users/will/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b", + "endRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md new file mode 100644 index 000000000..adc6c2859 --- /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/index.json b/.trajectories/index.json index 6197864b8..04ad92756 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-21T04:14:45.258Z", + "lastUpdated": "2026-05-24T17:02:55.026Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1131,6 +1131,40 @@ "startedAt": "2026-05-21T04:14:44.815Z", "completedAt": "2026-05-21T04:14:45.063Z", "path": "/private/tmp/relay-quiet-broker-logs/.trajectories/completed/2026-05/traj_u3loicehnwb4.json" + }, + "traj_pjadgfw0mtw4": { + "title": "Review package dependencies", + "status": "completed", + "startedAt": "2026-05-21T20:27:44.911Z", + "completedAt": "2026-05-21T20:31:49.784Z", + "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_r3eic6rt84pq.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json" + }, + "traj_4b2d63f6ljvh": { + "title": "Fix CI run 26263517444", + "status": "active", + "startedAt": "2026-05-22T01:47:08.312Z", + "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/active/traj_4b2d63f6ljvh.json" } } -} +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 7b1ff02d2..acc9b19a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,13 +26,23 @@ git push origin main # NO! This ensures the user maintains control over what goes into the main branch. -## Changelog Style +## Changelog + +Curate `[Unreleased]` in `CHANGELOG.md` as you land PRs. The root changelog is +the cross-package, user-facing release narrative for Relay. It follows +[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and Semantic +Versioning. Changelog entries should be concise and impact-first. Prefer one short bullet -per user-visible change: name the command, API, or schema touched and the -practical effect. Drop issue/PR links, internal review notes, implementation -backstory, and "foundation for..." phrasing unless that text clearly explains -the shipped impact. +per user-visible change: name the command, API, schema, or package touched and +the practical effect. Drop issue/PR links, internal review notes, +implementation backstory, release-only entries, and "foundation for..." phrasing +unless that text clearly explains the shipped impact. + +Use Keep a Changelog sections (`Added`, `Changed`, `Deprecated`, `Removed`, +`Fixed`, `Security`), plus `Breaking Changes` and `Migration Guidance` when a +SemVer-major change needs explicit callouts. Do not use generated perspective +sections such as "Product Perspective", "Technical Perspective", or "Releases". ## .trajectories Must Be Tracked diff --git a/CHANGELOG.md b/CHANGELOG.md index 8183b4a41..35ea22d34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,2805 +7,1257 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Breaking Changes +### Added -- `@agent-relay/sdk`: `AgentRelay` events move to a multi-listener registry. Use `relay.addListener('x', handler)` / `removeListener` in place of `relay.onX = handler` — the 13 `on*` fields (`onMessageReceived`, `onMessageSent`, `onAgentSpawned`, `onAgentReleased`, `onAgentExited`, `onAgentReady`, `onWorkerOutput`, `onDeliveryUpdate`, `onAgentExitRequested`, `onAgentIdle`, `onAgentActivityChanged`, `onChannelSubscribed`, `onChannelUnsubscribed`) are removed. -- `@agent-relay/sdk`: `channelSubscribed` / `channelUnsubscribed` handlers receive a single `{ agent, channels }` object instead of positional `(agent, channels)` args. -- `@agent-relay/sdk`: new `beforeAgentSpawn` / `afterAgentSpawn` / `beforeAgentRelease` / `afterAgentRelease` call-site hooks. `beforeAgentSpawn` listeners may return a `SpawnPatch` (shallow-merged in registration order) to mutate the spawn input before the broker POST. -- Broker/SDK wire protocol is now version 2 for delivery terminal events and lifecycle event shape changes. -- `relay.spawn({ task })` now returns `success: false` and terminates the agent when task delivery fails after retries. -- `agent-relay send` now uses the orchestrator identity by default so `agent-relay replies ` can correlate worker DMs. -- The `relay_broker` Rust crate now exposes only `protocol`, `snippets`, and `run_cli`; broker implementation modules are crate-private. +- `@agent-relay/sdk`: spawn calls and workflow configs can declare harness adapters so custom agent CLIs define their own binaries, interactive/non-interactive argument templates, model flags, and process behavior without Relay changes. -### Migration Guidance +### Changed -- Pass `--from` to `agent-relay send` when a script requires a specific sender identity. -- Handle `success: false` from `relay.spawn()` calls that pass `task`; spawns without a task are unchanged. -- Set `POSTHOG_PROJECT_KEY` in GitHub Actions repository variables before publishing telemetry-enabled artifacts. -- Update relay event handlers from field-assignment to `addListener`: +- `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. +- Release workflow changelog generation now writes concise Keep a Changelog sections and skips release-only, trajectory, PR-review, and placeholder entries. +- `agent-relay spawn` and SDK spawn calls now return harness `sessionId` metadata for resumable Claude and Codex PTY sessions. - ```ts - // Before - relay.onAgentSpawned = (agent) => log(agent.name); +### Fixed - // After - const off = relay.addListener('agentSpawned', (agent) => log(agent.name)); - // ...later: off(); // unsubscribe - ``` +- `web`: PR preview SST deploys reuse AWS's managed disabled cache policy instead of creating one custom CloudFront cache policy per preview stage. +- `web`: production SST deploys target `origin.agentrelay.net` again. - Channel subscribe/unsubscribe handlers receive an object: `({ agent, channels }) => ...`. +## [7.0.1] - 2026-05-22 ### Added -- `agent-relay activity` tails broker-wide message, delivery, lifecycle, and worker output events in a human-readable stream with filters and JSON Lines output. -- `agent-relay view ` streams a running agent's PTY without taking control or stopping the agent. -- `agent-relay drive ` attaches interactively and queues inbound relay messages until the user flushes them. -- `agent-relay passthrough ` attaches interactively while inbound relay messages continue to auto-inject. -- `agent-relay new NAME CLI [args...]` starts broker-owned agents, with `--attach`, `--ephemeral`, and `-n` / `--name` spawn-and-attach forms. -- `agent-relay rm ` releases broker-owned agents. -- Broker `/api/spawned/{name}/delivery-mode`, `/pending`, and `/flush` routes manage per-agent inbound queues. -- Broker `/api/input/{name}/stream` and SDK `openInputStream()` provide ordered websocket PTY input without one HTTP request per keystroke. -- TypeScript SDK clients can read snapshots, stream worker output, set delivery mode, inspect pending queues, and flush queued messages. -- `agent-relay replies ` reads worker DM replies with JSON, unread, mark-read, sender identity, and cursor options. -- `agent-relay history` and `agent-relay replies` accept message-id `--since` cursors for incremental reads. -- `agent-relay who --json` returns structured status, PID, uptime, and memory fields for scripts. -- `agent-relay agents:logs --plain` and `--json` return sanitized agent logs for scripts. -- `packages/personas` includes a `nextjs-web-steward` persona and workforce v3 persona schema. -- Preview cleanup tooling removes stale preview environments before CloudFront quota failures. -- Docs navigation shows icons for the CLI reference and Broker HTTP / WS API pages. -- `@agent-relay/cloud` exposes provider auth helpers for Daytona-backed `agent-relay cloud connect` flows. -- `@agent-relay/sdk/workflows` exposes `runScriptWorkflow()` for running workflow scripts in-process. -- CLI local auth supports SSH-based authentication. -- Agent spawning supports multiple repositories in one operation. -- Agents can switch provider or model at runtime. -- Prerelease publishing supports staging releases. -- Broker `--api-bind` configures the HTTP/WS bind address. -- PTY workers accept `write_pty` messages and report bytes written or worker errors. -- Broker events include delivery confirmation/failure and agent lifecycle health signals for subscribed orchestrators. +- `agent-relay log {path,list,view,rotate,clear}` inspects and prunes broker diagnostic logs, with rotated platform-standard log files. +- `AgentRelayClient.onBrokerExit()` notifies SDK consumers when a spawned broker exits, including code, signal, PID, and recent stderr. +- `AgentRelay.addListener()` accepts `BeforeAgentSpawnHandler` directly. ### Changed -- Broker inbound delivery now uses one per-agent queue so `auto_inject` and `manual_flush` handle ordering consistently. -- Inbound delivery APIs use `/delivery-mode` with `auto_inject` and `manual_flush` names before their first release. -- CLI attach commands share SDK-backed broker snapshots, delivery mode changes, streams, and flushes. -- PTY terminal query replies use the live VT grid, so cursor-position responses reflect the actual screen. -- PTY writes from user input and terminal query replies now pass through one FIFO writer. -- Broker snapshot requests return consistent worker timeout and error-envelope responses. -- Rust and TypeScript telemetry disable PostHog reporting when no `AGENT_RELAY_POSTHOG_KEY` is configured. -- `agent-relay inbox` shows unread DM content and `direction` metadata, with terminal controls stripped from text summaries. -- `agent-relay who` reports last activity, context budget, and working/idle/blocked-on-send state. -- `agent-relay doctor` reports broker Relaycast auth state and stuck outbound delivery queues. -- Relaycast MCP auto-registers workspace-key sessions as an orchestrator so read tools work without a manual register step. - -### Fixed - -- Broker worker teardown now emits `message_delivery_failed` for dropped pending deliveries so SDK delivery waiters terminate. -- SDK `sendAndWaitForDelivery` waits for `message_delivery_confirmed` or `message_delivery_failed` instead of treating `delivery_ack` as final. -- Relaycast MCP startup ignores unresolved `${RELAY_*}` environment placeholders before auto-registering. -- PTY context budget detection uses the latest percentage in output and can re-emit after the budget rises. -- `agent-relay agents:logs` now cooks PTY redraws into line-oriented output by default and keeps raw terminal bytes behind `--raw`. -- `agent-relay agents:logs --raw` preserves non-UTF-8 bytes, and follow mode keeps split escape/codepoint sequences intact. -- `agent-relay up --no-dashboard` and `agent-relay down --force` now recover half-started brokers that stayed alive without readable connection metadata. -- `agent-relay who` and `agent-relay agents` now fail clearly when broker queries fail instead of printing an empty agent list. -- `agent-relay history` and `agent-relay replies` now resolve the project broker session even when `AGENT_RELAY_STATE_DIR` points elsewhere. -- `agent-relay doctor` now fails with an actionable diagnostic for half-started, stale-connection, and unresolved-API-key-template brokers instead of reporting "healthy". -- CLI readiness checks use the live VT grid and cursor position to avoid false ready states in alternate screens and menus. -- Broker diagnostic logs write to a platform-standard directory with daily rotation: macOS `~/Library/Logs/agent-relay/{brokerId}.log.YYYY-MM-DD`, Linux `~/.local/state/agent-relay/logs/...`, Windows `%LOCALAPPDATA%\agent-relay\Logs\...`. Set `AGENT_RELAY_BROKER_LOG=off` to disable, `AGENT_RELAY_BROKER_LOG=stderr` to print to stderr, and use `RUST_LOG=...` for finer-grained filters. -- `agent-relay log {path,list,view,rotate,clear}` inspects and prunes broker tracing logs. `log rotate --keep-days 7` removes rotated files older than the retention window; `log clear --broker-id ` wipes a single broker's logs. -- `agent-relay history --from ` returns the newest messages after chronological sorting. -- `agent-relay replies --unread` prints nothing when there are no unread messages. -- Messaging `--limit` values clamp invalid negative inputs. -- Task delivery failures return an error, retry automatically, and clean up agents that never received their task. -- Agent initialization no longer fails because of stale cache state. -- Tests treat `better-sqlite3` as optional, improving CI reliability. -- `agent-relay doctor` validates partial driver availability correctly. -- SDK `sendInput` routes through the PTY worker protocol so input reaches the agent PTY. -- DM delivery retries now end in a surfaced `message_delivery_failed` event instead of silently retrying forever. -- The PTY watchdog marks agents with pending delivery work as blocked-on-send instead of idle. -- PTY `worker_stream` events preserve multi-byte UTF-8 characters split across read chunks instead of emitting `U+FFFD` replacement glyphs. +- `web`: deploy tooling can repair stale SST ACM certificate validation state before continuing. +- `web`: `@posthog/next` upgraded to the current 0.4.x line. +- Relay self-termination guidance now points agents at direct process exit instead of broker shutdown paths. -## [6.3.6] - 2026-05-21 +### Fixed -### Product Perspective -#### User-Facing Features & Improvements -- **Typed multi-listener registry replaces on* callback fields** +- `web`: Next is pinned through overrides so deploy builds do not install duplicate Next versions. -#### User-Impacting Fixes -- Address PR #936 review feedback (#936) +## [7.0.0] - 2026-05-21 -### Technical Perspective -#### Dependencies & Tooling -- Revert version bump — release workflow handles versioning +### Changed -#### Releases -- v6.3.6 +- `agent-relay` and `@agent-relay/*` packages moved to the 7.x release line for the SDK listener API changes. The code-level changes also appeared in the immediately preceding 6.3.6 build, so 7.0.0 is the corrected major-version release line. ---- +## [6.3.6] - 2026-05-21 -## [6.3.5] - 2026-05-21 +### Breaking Changes -### Product Perspective -#### User-Facing Features & Improvements -- **Add --broker-name override to `agent-relay up` (#939)** (#939) +- `@agent-relay/sdk`: `AgentRelay` event callbacks moved from `relay.on* = handler` fields to `relay.addListener(type, handler)` / `removeListener`; the old callback fields are removed. +- `@agent-relay/sdk`: channel subscribe and unsubscribe listeners now receive `{ agent, channels }` instead of positional arguments. +- `@agent-relay/sdk`: spawn and release lifecycle hooks can observe call sites, and `beforeAgentSpawn` listeners can return shallow spawn-input patches. +- Broker/SDK wire protocol moved to v2 for terminal delivery events and lifecycle event shape changes. -### Technical Perspective -#### Releases -- v6.3.5 +### Migration Guidance ---- +- Use `relay.addListener(...)` and retain the returned unsubscribe function instead of assigning `relay.onAgentSpawned = ...`. +- Update channel subscribe and unsubscribe handlers to destructure `({ agent, channels })`. -## [6.3.4] - 2026-05-21 +## [6.3.5] - 2026-05-21 -### Product Perspective -#### User-Facing Features & Improvements -- **Upload workflow code through cloud storage API (#938)** (#938) -- **Pass env vars to scheduled workflows (#935)** (#935) +### Added -### Technical Perspective -#### Releases -- v6.3.4 +- `agent-relay up --broker-name` overrides the local broker identity instead of deriving it from the project directory. ---- +## [6.3.4] - 2026-05-21 -## [6.3.3] - 2026-05-21 +### Added -### Product Perspective +- `agent-relay cloud`: workflow code uploads through the cloud storage API. +- Scheduled workflows can receive environment variables. -#### User-Impacting Fixes +## [6.3.3] - 2026-05-21 -- Detect opencode api key completion (#934) (#934) +### Fixed -### Technical Perspective +- `agent-relay config`: OpenCode API-key completion is detected correctly. -#### Releases +## [6.3.2] - 2026-05-20 -- v6.3.3 +### Fixed ---- +- Broker worker stderr no longer renders inside the agent xterm. -## [6.3.2] - 2026-05-20 +## [6.3.1] - 2026-05-20 -### Product Perspective +### Fixed -#### User-Impacting Fixes +- Claude PTY workers pre-register so Relaycast MCP boots faster. -- Stop worker stderr from rendering inside agent xterm (#931) (#931) +## [6.3.0] - 2026-05-20 -### Technical Perspective +### Added -#### Releases +- `agent-relay activity` tails broker-wide message, delivery, lifecycle, and worker-output events with filters and JSON Lines output. +- Broker `/api/input/{name}/stream` and SDK `openInputStream()` provide ordered websocket PTY input without one HTTP request per keystroke. -- v6.3.2 +### Changed ---- +- CLI attach modes use the SDK PTY input stream for interactive input. -## [6.3.1] - 2026-05-20 +## [6.2.8] - 2026-05-20 -### Product Perspective +### Fixed -#### User-Impacting Fixes +- Workflow runtime PTY chrome scrubbing is stricter, stale-state warnings are quieter, and idle override behavior is documented. -- Pre-register Claude PTY workers so Relaycast MCP boots fast (#926) +## [6.2.7] - 2026-05-20 -### Technical Perspective +### Fixed -#### Dependencies & Tooling +- `agent-relay up --no-dashboard` and `agent-relay down --force` recover half-started brokers that stayed alive without readable connection metadata. +- `agent-relay who` and `agent-relay agents` fail clearly when broker queries fail instead of printing empty agent lists. +- `agent-relay doctor` reports half-started, stale-connection, unresolved-template, and stuck outbound-delivery states directly. -- Retrigger flaky macOS Rust Tests -- Drop change-implying framing from PTY pre-register note +## [6.2.6] - 2026-05-20 -#### Releases +### Fixed -- v6.3.1 +- PTY `worker_stream` events preserve multi-byte UTF-8 characters split across read chunks. +- The broker flushes UTF-8 decoder state on the normal `pty_closed` path. ---- +## [6.2.5] - 2026-05-19 -## [6.3.0] - 2026-05-20 +### Changed -### Technical Perspective +- `web`: Next moved from 15.5.14 to 15.5.18. +- Deprecated `uuid` usage was removed from install-time dependencies. -#### Releases +### Fixed -- v6.3.0 +- PTY workers handle `write_pty` frames. ---- +## [6.2.4] - 2026-05-19 -## [6.2.8] - 2026-05-20 +### Changed -### Product Perspective +- Broker Relaycast integration uses the Relaycast SDK 1.1 helper APIs. -#### User-Impacting Fixes +## [6.2.3] - 2026-05-19 -- Tighten PTY chrome scrubbing, document idle override, tame stale-state warning (#930) (#930) +### Added -### Technical Perspective +- Broker status reports the product release line instead of an internal crate version. -#### Releases +### Changed -- v6.2.8 +- Broker runtime code was split into focused modules and the public Rust crate API was narrowed. +- `agent-relay agents:logs` returns readable, line-oriented output by default. ---- +### Fixed -## [6.2.7] - 2026-05-20 +- Spawned workers receive idle thresholds consistently. +- Docs navigation uses Next links. +- Broker runtime review issues in request handling and stale-state reporting were addressed. -### Technical Perspective +## [6.2.2] - 2026-05-18 -#### Releases +### Changed -- v6.2.7 +- CLI attach and drive sessions share preparation helpers; behavior is unchanged. ---- +## [6.2.1] - 2026-05-18 -## [6.2.6] - 2026-05-20 +### Fixed -### Product Perspective +- Removed an out-of-scope preview configuration change from the 6.2.0 line. -#### User-Impacting Fixes +## [6.2.0] - 2026-05-18 -- Flush UTF-8 decoder on normal pty_closed path -- Preserve split multi-byte UTF-8 in worker_stream (#922) (#922) +### Added -### Technical Perspective +- `agent-relay view ` streams a running agent PTY without taking control or stopping the agent. +- `agent-relay drive ` attaches interactively and queues inbound relay messages until the user flushes them. +- `agent-relay passthrough ` attaches interactively while inbound relay messages continue to auto-inject. +- `agent-relay new NAME CLI [args...]` starts broker-owned agents, with `--attach`, `--ephemeral`, and spawn-and-attach forms. +- `agent-relay rm ` releases broker-owned agents. +- Broker per-agent delivery-mode, pending-queue, and flush routes manage inbound queues. +- TypeScript SDK clients can read snapshots, stream worker output, set delivery mode, inspect pending queues, and flush queued messages. +- `agent-relay replies ` reads worker direct-message replies with JSON, unread, mark-read, sender identity, and cursor options. +- `agent-relay history` and `agent-relay replies` accept message-id `--since` cursors for incremental reads. +- `agent-relay who --json` returns structured status, PID, uptime, and memory fields for scripts. +- `packages/personas` includes a `nextjs-web-steward` persona and workforce v3 persona schema. +- Docs include broker HTTP / WebSocket API reference pages and CLI reference navigation icons. -#### Releases +### Changed -- v6.2.6 +- Broker inbound delivery uses one per-agent queue so `auto_inject` and `manual_flush` preserve ordering consistently. +- CLI attach commands share SDK-backed broker snapshots, delivery mode changes, streams, and flushes. +- PTY readiness checks use the live VT grid and cursor position to avoid false ready states in alternate screens and menus. +- PTY writes from user input and terminal-query replies pass through one FIFO writer. +- Rust and TypeScript telemetry disable PostHog reporting when no `AGENT_RELAY_POSTHOG_KEY` is configured. +- `agent-relay send` uses the orchestrator identity by default so `agent-relay replies ` can correlate worker direct messages. ---- +### Fixed -## [6.2.5] - 2026-05-19 +- `relay.spawn({ task })` returns `success: false` and terminates the agent when task delivery fails after retries. +- Broker worker teardown emits `message_delivery_failed` for dropped pending deliveries so SDK delivery waiters terminate. +- SDK `sendAndWaitForDelivery` waits for terminal delivery confirmation or failure instead of treating `delivery_ack` as final. +- Relaycast MCP startup ignores unresolved `RELAY_*` environment placeholders before auto-registering. +- `agent-relay history --from ` returns the newest messages after chronological sorting. +- `agent-relay replies --unread` prints nothing when there are no unread messages. +- Messaging `--limit` values clamp invalid negative inputs. +- SDK `sendInput` routes through the PTY worker protocol so input reaches the agent PTY. -### Product Perspective +## [6.0.22] - 2026-05-15 -#### User-Impacting Fixes +### Fixed -- Handle write_pty frames in PTY worker (#920) +- Bump agent-relay-workflow writer timeouts -### Technical Perspective +## [6.0.21] - 2026-05-14 -#### Dependencies & Tooling +### Added -- Sync package-lock.json for next 15.5.18 bump -- Bump next from 15.5.14 to 15.5.18 in /web +- Add pr_url verification check -#### Releases +## [6.0.20] - 2026-05-13 -- v6.2.5 +### Fixed ---- +- Persist spawned agents across cwd -## [6.2.4] - 2026-05-19 +## [6.0.19] - 2026-05-13 -### Technical Perspective +### Added -#### Architecture & API Changes +- Export createContextFactory + its option/return interfaces -- Use relaycast sdk 1.1 helpers +## [6.0.18] - 2026-05-12 -#### Releases +### Added -- v6.2.4 +- Proactive-runtime — agent-relay CLI bootstrap + DLQ + cloud SDK ---- +## [6.0.17] - 2026-05-12 -## [6.2.3] - 2026-05-19 +### Added -### Product Perspective +- Host @agent-relay/events + @agent-relay/agent in relay -#### User-Facing Features & Improvements +## [6.0.16] - 2026-05-11 -- **Align reported version with product release line** (#904) +### Fixed -#### User-Impacting Fixes +- Drain broker stderr alongside stdout after startup +- Replace blocking stdout writer task with tokio::io -- Address coderabbit review on version handling -- Use next/link for docs navigation -- Pass idle threshold to spawned workers -- Address runtime review findings +## [6.0.14] - 2026-05-10 -### Technical Perspective +### Fixed -#### Architecture & API Changes +- Reclaim agent on 409 instead of crashing the broker -- Narrow public crate API -- Group relaycast broker integration -- Extract broker runtime event handlers -- Split broker runtime modules -- Split broker main entrypoint -- Move broker crate under crates +## [6.0.13] - 2026-05-09 -#### Dependencies & Tooling +### Added -- Record runtime split trajectory -- Complete issue 875 trajectory file list -- Update issue 875 trajectory metadata +- Re-export github primitive from root entry +- Make reliability repair-aware by default -#### Releases +### Fixed -- v6.2.3 +- Wait for matching broker tarball before install ---- +## [6.0.12] - 2026-05-09 -## [6.2.2] - 2026-05-18 +### Fixed -### Technical Perspective +- Finish agentToken doc cleanup in types.ts -#### Architecture & API Changes +## [6.0.10] - 2026-05-08 -- Share interactive-attach prep helpers via attach.ts -- Split runDriveSession to drop below complexity 15 (#897) +### Added -#### Dependencies & Tooling +- Spawn agents from named AgentWorkforce personas +- Add @agentrelay/personas pack -- Align trajectory title with retrospective scope -- Sanitize absolute paths in metadata (#899) +### Changed -#### Releases +- Skip personas package in dist-files check +- Align with @agent-relay scope and lockstep versioning -- v6.2.2 +### Fixed ---- +- Stop stamping default_workspace_id into RELAYFILE_WORKSPACE +- Stop stamping relaycast workspace id into RELAYFILE_WORKSPACE +- Trust at*live*\* agent tokens, drop probe-then-rotate +- Address PR review (Windows paths, TOCTOU, harness validation) +- Tighten validator robustness +- Regenerate lockfile and address review nits -## [6.2.1] - 2026-05-18 +## [6.0.9] - 2026-05-05 -### Technical Perspective +### Added -#### Releases +- Add WorkflowBuilder.paths() for multi-repo cloud workflows -- v6.2.1 +### Fixed ---- +- Align communicate transport with current Relaycast API -## [6.2.0] - 2026-05-18 +## [6.0.8] - 2026-05-04 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- Surface phase C multi-repo push results in cloud CLI +- Phase B multi-path tarball upload for cloud workflows -- **`new` / `relay` / `run` / `rm` verbs + `-n` silent alias (#864 sub-4)** (#864) -- **`agent-relay drive ` interactive take-over client (#864 sub-3)** (#864) -- **Per-agent session mode + pending-queue routes (#864 sub-2)** (#864) -- **`agent-relay view ` read-only PTY stream client (#864 sub-1)** (#864) +### Fixed -#### User-Impacting Fixes +- Exclude volatile workflow files when applying sync patches -- Defer spawn-and-attach import until --attach is set -- Surface drainer write failures from pty write_all -- Resolve #800 — broker: composable wait-conditions for CLI readiness (steal from ht) (#800) -- Resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty) (#802) -- Resolve #802 — broker: add VT grid via alacritty_terminal (steal from ht, don't use libghostty) (#802) +## [6.0.6] - 2026-04-30 -### Technical Perspective +### Fixed -#### Architecture & API Changes +- Add repository metadata for workflow types +- Publish SDK internal deps before sdk -- Rename session-mode `relay` → `passthrough` across all surfaces -- `new` takes positional NAME (drop `-n` flag) + scrub PR refs (#864) -- Drop `run` verb, fold spawn-and-attach into `new --attach` (#889) -- Unify worker request/response correlation (#871) (#871) +## [6.0.4] - 2026-04-30 -#### Performance & Reliability +### Fixed -- Assert X-API-Key on every broker request -- Actually assert on the API-key header in the harness -- Cover drainer flush failure ack propagation -- Alias slack-primitive / github-primitive / workflow-types in vitest -- Add 'view' to expected leaf command list (#880) -- Add stale preview environment cleanup +- Publish SDK workflow types before SDK +- Pack github-primitive + workflow-types in smoke; publish workflow-types -#### Dependencies & Tooling +## [6.0.3] - 2026-04-29 -- Drop PR references and legacy framing from code comments (#864) -- Record `run` -> `new --attach` refactor decision -- Record decisions for sub-PR 4 (#864) (#864) -- Normalize projectId in merged trajectory -- Source PostHog key from `vars.POSTHOG_PROJECT_KEY` -- Inject PostHog key at build time (P0.5 of #881) (#881) +### Added -#### Releases +- Expose connectProvider() in @agent-relay/cloud SDK +- Expose runScriptWorkflow() in @agent-relay/sdk/workflows +- Bundle @agent-relay/github-primitive at /github subpath -- v6.2.0 +### Fixed ---- +- Update codegen-models workflow to use new Python output path -## [6.0.22] - 2026-05-15 +## [6.0.2] - 2026-04-25 -### Product Perspective +### Fixed -#### User-Impacting Fixes +- Drop darwin-x64 verify leg (macos-13 queue stuck again) +- Re-add @agent-relay/cloud to publish-packages matrix -- Bump agent-relay-workflow writer timeouts (#857) (#857) +## [6.0.1] - 2026-04-25 -### Technical Perspective +### Breaking Changes -#### Releases +- Drop legacy agent-relay/broker\* exports and shipped workspace dirs -- v6.0.22 +### Added ---- +- Restore agent-relay/\* subpath exports via shim re-exports -## [6.0.21] - 2026-05-14 +### Changed -### Product Perspective +- Fix stale broker checks and PyPI retry -#### User-Facing Features & Improvements +### Fixed -- **Add pr_url verification check (#852)** (#852) +- Drop dead linkResult reference +- Allow shipped workspace packages declared as regular deps +- Unbundle @agent-relay/\* to restore optional-dep broker resolution +- Walk ancestor node_modules for shadowed broker packages +- Install broker optional-deps for CLI users -### Technical Perspective +## [6.0.0] - 2026-04-24 -#### Releases +### Added -- v6.0.21 +- ApplySiblingLinks — link sibling-repo packages during workflow setup +- Split broker binaries into per-platform optional-dep packages ---- +### Changed -## [6.0.20] - 2026-05-13 +- Drop darwin-x64 smoke test +- Cross-platform post-publish verification of @agent-relay/sdk +- Skip dist check for broker-\* packages in package-validation +- Add cross-platform smoke test for broker optional-deps +- Update Cursor models to latest -### Product Perspective +### Fixed -#### User-Impacting Fixes +- Keep SIGWINCH on unix, background-thread poll on Windows +- Unbreak Windows build +- Convert rewrites to direct redirects +- Verify-publish-sdk must accept publish-sdk-only too +- Pack @agent-relay/config alongside SDK for smoke test +- Address PR review feedback on broker optional-deps +- Keep broker packages as workspaces so npm ci passes -- Persist spawned agents across cwd (#846) (#846) +## [5.0.0] - 2026-04-22 -### Technical Perspective +### Changed -#### Releases +- Include publish-sdk-py in summary job -- v6.0.20 +### Fixed ---- +- Repair pre-existing test failures on main +- Address Copilot review on broker resolution +- Ship per-platform wheels with embedded broker (drop runtime download) -## [6.0.19] - 2026-05-13 +## [4.0.40] - 2026-04-22 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- Add browser and github workflow primitives -- **Export createContextFactory + its option/return interfaces (#845)** (#845) +## [4.0.38] - 2026-04-22 -### Technical Perspective +### Fixed -#### Releases +- Retry get_session on 503 + correct quickstart idle wait -- v6.0.19 +## [4.0.37] - 2026-04-22 ---- +### Added -## [6.0.18] - 2026-05-12 +- Send workflowPath so the launcher can skip the $HOME upload -### Product Perspective +## [4.0.36] - 2026-04-22 -#### User-Facing Features & Improvements +### Added -- **Proactive-runtime — agent-relay CLI bootstrap + DLQ + cloud SDK (#843)** (#843) +- Add credential proxy workflows runtime stack -### Technical Perspective +### Fixed -#### Releases +- Bootstrap for first publish -- v6.0.18 +## [4.0.35] - 2026-04-21 ---- +### Added -## [6.0.17] - 2026-05-12 +- Widen @relayfile/sdk dep range to allow 0.2.x + 0.3.x -### Product Perspective +## [4.0.34] - 2026-04-21 -#### User-Facing Features & Improvements +### Fixed -- **Host @agent-relay/events + @agent-relay/agent in relay (#844)** (#844) +- Mark run failed under continue-on-error when steps fail -### Technical Perspective +## [4.0.33] - 2026-04-20 -#### Releases +### Added -- v6.0.17 +- Add --register flag to mcp-args subcommand ---- +### Fixed -## [6.0.16] - 2026-05-11 +- Bundle local mount package -### Product Perspective +## [4.0.32] - 2026-04-20 -#### User-Impacting Fixes +### Added -- Drain broker stderr alongside stdout after startup (#842) (#842) -- Replace blocking stdout writer task with tokio::io (#841) (#841) +- Add agent-relay mcp-args subcommand +- Add agent activity hook -### Technical Perspective +### Fixed -#### Releases +- Ignore late delivery ack activity -- v6.0.16 +## [4.0.31] - 2026-04-20 ---- +### Added -## [6.0.15] - 2026-05-11 +- Align Rust AgentSpawn/AgentRelease with TS schema +- Per-component version properties on every event +- Instrument all CLI commands with rich events -### Technical Perspective +### Fixed -#### Releases +- FileDb in-memory cache authoritative — fixes stale status after disk write failures +- Extract runSignalHandler helper; apply in monitoring +- Is_tty should check stdin, not stdout +- Plug two CliExit regressions flagged by Devin +- Flush queue before process exit; schema cleanup +- Upgrade posthog-node from v4 to v5 -- v6.0.15 +## [4.0.30] - 2026-04-19 ---- +### Fixed -## [6.0.14] - 2026-05-10 +- Export A2A communicate subpaths -### Product Perspective +## [4.0.29] - 2026-04-17 -#### User-Impacting Fixes +### Added -- Reclaim agent on 409 instead of crashing the broker (#797) (#830) (#797) +- Add ProcessBackend workflow for cloud sandbox execution -### Technical Perspective +## [4.0.28] - 2026-04-15 -#### Releases +### Fixed -- v6.0.14 +- Bundle ssh2 in release pipeline, not just scripts/build-bun.sh ---- +## [4.0.27] - 2026-04-15 -## [6.0.13] - 2026-05-09 +### Fixed -### Product Perspective +- Bundle ssh2 into Bun binary so cloud connect exercises the ssh2 path -#### User-Facing Features & Improvements +## [4.0.26] - 2026-04-15 -- **Re-export github primitive from root entry (#823)** (#823) -- **Make reliability repair-aware by default (#827)** (#827) +### Fixed -#### User-Impacting Fixes +- Add visible launch checkpoint for cloud connect -- Wait for matching broker tarball before install (#829) (#829) +## [4.0.25] - 2026-04-15 -### Technical Perspective +### Fixed -#### Releases +- Stop cloud connect hangs and re-auth loops -- v6.0.13 +## [4.0.24] - 2026-04-15 ---- +### Fixed -## [6.0.12] - 2026-05-09 +- Prefer native Node TS stripping over tsx fallback -### Product Perspective +## [4.0.23] - 2026-04-14 -#### User-Impacting Fixes +### Added -- Finish agentToken doc cleanup in types.ts (#822) (#822) +- Show workspace key and observer URL in agent-relay status -### Technical Perspective +## [4.0.22] - 2026-04-14 -#### Releases +### Added -- v6.0.12 +- Cloud-connect fix workflows (claude hang + utils bundling) ---- +## [4.0.21] - 2026-04-13 -## [6.0.10] - 2026-05-08 +### Added -### Product Perspective +- Env-var auth fallback for headless consumers -#### User-Facing Features & Improvements +### Fixed -- **Spawn agents from named AgentWorkforce personas** -- **Add @agentrelay/personas pack (#816)** (#816) +- Inbox --agent flag, history DM support, history --from DM context -#### User-Impacting Fixes +## [4.0.20] - 2026-04-13 -- Stop stamping default_workspace_id into RELAYFILE_WORKSPACE (#821) (#821) -- Stop stamping relaycast workspace id into RELAYFILE_WORKSPACE (#820) (#820) -- Trust at*live*\* agent tokens, drop probe-then-rotate (#819) (#819) -- Address PR review (Windows paths, TOCTOU, harness validation) -- Tighten validator robustness -- Regenerate lockfile and address review nits +### Changed -### Technical Perspective +- Unify WorkflowTrajectory on agent-trajectories SDK -#### Performance & Reliability +### Fixed -- Skip personas package in dist-files check +- Replace esbuild pre-parse with tsx stderr post-processing -#### Dependencies & Tooling +## [4.0.19] - 2026-04-13 -- Align with @agent-relay scope and lockstep versioning +### Fixed -#### Releases +- Make preParseWorkflowFile async to avoid Bun-compiled CLI hang -- v6.0.10 +## [4.0.18] - 2026-04-13 ---- +### Fixed -## [6.0.9] - 2026-05-05 +- Add progress diagnostics and spawnSync to runScriptFile +- History/inbox fetch workspace_key via broker HTTP API -### Product Perspective +## [4.0.17] - 2026-04-13 -#### User-Facing Features & Improvements +### Added -- **Add WorkflowBuilder.paths() for multi-repo cloud workflows (#814)** (#814) +- Workerd export condition + narrow entry + workers-safety probe -#### User-Impacting Fixes +### Fixed -- Align communicate transport with current Relaycast API (#813) (#813) +- Restore packages/sdk vitest suite to green +- Pre-parse workflow script files with actionable error hints +- Make --resume work for script workflows -### Technical Perspective +## [4.0.16] - 2026-04-12 -#### Releases +### Fixed -- v6.0.9 +- Wire relaycast MCP for headless opencode spawner ---- +## [4.0.15] - 2026-04-12 -## [6.0.8] - 2026-05-04 +### Fixed -### Product Perspective +- History and inbox work without RELAY_API_KEY env var -#### User-Facing Features & Improvements +## [4.0.14] - 2026-04-11 -- **Surface phase C multi-repo push results in cloud CLI (#775)** (#775) -- **Phase B multi-path tarball upload for cloud workflows (#774)** (#774) +### Added -#### User-Impacting Fixes +- Add cloud cancel CLI + fix opencode headless spawn -- Exclude volatile workflow files when applying sync patches (#811) (#811) +## [4.0.13] - 2026-04-11 -### Technical Perspective +### Fixed -#### Releases +- Retry real install paths in verify-publish -- v6.0.8 +## [4.0.12] - 2026-04-11 ---- +### Added -## [6.0.7] - 2026-05-01 +- Add workflow for relay bootstrap and messaging fixes +- Add meta and clean-room relay validation workflows -### Technical Perspective +## [4.0.11] - 2026-04-10 -#### Releases +### Fixed -- v6.0.7 +- Log full deterministic step output on failure for cloud visibility ---- +## [4.0.10] - 2026-04-10 -## [6.0.6] - 2026-04-30 +### Changed -### Product Perspective +- Harden macos binary verification -#### User-Impacting Fixes +### Fixed -- Add repository metadata for workflow types (#809) (#809) -- Publish SDK internal deps before sdk (#806) (#806) +- Skip in-sandbox provisioning when cloud launcher already seeded ACLs +- Harden macos binary smoke checks -### Technical Perspective +## [4.0.9] - 2026-04-10 -#### Releases +### Fixed -- v6.0.6 +- Harden npm publish packaging +- Use bun built-in TS validation, remove esbuild dependency +- Npm tarball propagation race in verify-publish and install.sh ---- +## [4.0.6] - 2026-04-10 -## [6.0.4] - 2026-04-30 +### Added -### Product Perspective +- Complete implementation + fix Supermemory adapter -#### User-Impacting Fixes +## [4.0.5] - 2026-04-08 -- Publish SDK workflow types before SDK (#807) (#807) -- Pack github-primitive + workflow-types in smoke; publish workflow-types (#804) (#804) +### Changed -### Technical Perspective +- Route waitlist signups to cloud -#### Releases +## [4.0.4] - 2026-04-07 -- v6.0.4 +### Fixed ---- +- Use local workspace session for symlink/solo mode to avoid 405 on cloud API -## [6.0.3] - 2026-04-29 +## [4.0.3] - 2026-04-07 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- Fast workspace seeding — symlink mount + tar bulk upload +- 30 workflows to wire relayauth/relayfile permissions into workflow runner -- **Expose connectProvider() in @agent-relay/cloud SDK (#798)** (#798) -- **Expose runScriptWorkflow() in @agent-relay/sdk/workflows (#799)** (#799) -- **Bundle @agent-relay/github-primitive at /github subpath (#782)** (#782) +### Fixed -#### User-Impacting Fixes +- Only prefer sibling relay-dashboard dev build when RELAY_LOCAL_DEV=1 +- Install broker binary to BIN_DIR so it's on PATH -- Update codegen-models workflow to use new Python output path (#780) (#780) +## [4.0.1] - 2026-04-06 -### Technical Perspective +### Added -#### Releases +- TDD refactoring workflows for runner.ts + main.rs decomposition +- /schedule — RelayCron landing page +- Auto-download relayfile-mount binary on first use -- v6.0.3 +### Changed ---- +- Gitignore .trajectories/ (automated run artifacts) -## [6.0.2] - 2026-04-25 +### Fixed -### Product Perspective +- Allow anonymous workspace creation in agent-relay on +- Wire .agentignore/.agentreadonly enforcement into agent-relay on -#### User-Impacting Fixes +## [4.0.0] - 2026-03-31 -- Drop darwin-x64 verify leg (macos-13 queue stuck again) -- Re-add @agent-relay/cloud to publish-packages matrix (#788) +### Added -### Technical Perspective +- Default agent-relay on to production cloud endpoints +- Unified workspace ID across relay services -#### Releases +## [3.2.21] - 2026-03-27 -- v6.0.2 +### Fixed ---- +- Avoid E2BIG spawn failure and verification token double-count +- Queue outbound messages during RelayObserver reconnect -## [6.0.1] - 2026-04-25 +## [3.2.18] - 2026-03-25 -### Product Perspective +### Fixed -#### Breaking Changes +- Remove unused dm_drops_total function to fix clippy dead-code warning -- **Drop legacy agent-relay/broker\* exports and shipped workspace dirs** +## [3.2.17] - 2026-03-25 -#### User-Facing Features & Improvements +### Added -- **Restore agent-relay/\* subpath exports via shim re-exports** +- Add dry-run support and stream CLI output to terminal -#### User-Impacting Fixes +### Fixed -- Drop dead linkResult reference -- Allow shipped workspace packages declared as regular deps -- Unbundle @agent-relay/\* to restore optional-dep broker resolution -- Walk ancestor node_modules for shadowed broker packages -- Install broker optional-deps for CLI users +- Resolve DM participants for correct routing -### Technical Perspective +## [3.2.16] - 2026-03-25 -#### Performance & Reliability +### Added -- Fix stale broker checks and PyPI retry +- Add http and broker-path subpath exports for Electron apps +- PTY output streaming workflow +- Add integration step type for external services +- Add dynamic channel subscribe/unsubscribe to broker +- Cloud endpoints, API executor, and Communicate SDK v2 protocol +- Communicate Mode SDK (on_relay) for Python and TypeScript +- Add wait/steer message injection modes -#### Releases +### Changed -- v6.0.1 +- Assert injection mode defaults to wait when omitted +- Fix missing MessageInjectionMode imports in test modules +- Bump relaycast crate to v1 for injection mode support ---- +### Fixed -## [6.0.0] - 2026-04-24 +- Add RELAY_SKIP_PROMPT and self-echo filtering +- Ignore failing relaycast DM tests pending relaycast 1.0 API investigation +- Cargo fmt corrections +- Sync lockfile for new UI deps +- Validate channel names at build time and dry-run +- Forward steer mode through relaycast DMs +- Unblock fork PR checks and enforce steer rejection for relaycast DM +- Propagate inbound injection mode on relay_inbound events +- Allow relaycast delivery path to accept steer mode +- Reject steer mode on relaycast-only send path +- Validate send mode and harden steer delivery semantics +- Satisfy rust fmt/clippy for injection mode changes +- Don't block steer injections behind autosuggest gate -### Product Perspective +## [3.2.15] - 2026-03-23 -#### User-Facing Features & Improvements +### Added -- **ApplySiblingLinks — link sibling-repo packages during workflow setup (#776)** (#776) -- **Split broker binaries into per-platform optional-dep packages** (#770) +- Add RelayObserver proxy client for UI consumers -#### User-Impacting Fixes +### Fixed -- Keep SIGWINCH on unix, background-thread poll on Windows -- Unbreak Windows build -- Convert rewrites to direct redirects -- Verify-publish-sdk must accept publish-sdk-only too -- Pack @agent-relay/config alongside SDK for smoke test -- Address PR review feedback on broker optional-deps -- Keep broker packages as workspaces so npm ci passes +- Add bypass flag to codex non-interactive spawns -### Technical Perspective +## [3.2.14] - 2026-03-23 -#### Performance & Reliability +### Added -- Drop darwin-x64 smoke test -- Cross-platform post-publish verification of @agent-relay/sdk -- Skip dist check for broker-\* packages in package-validation -- Add cross-platform smoke test for broker optional-deps +- Add initial Swift SDK and harden workflow output -#### Dependencies & Tooling +### Changed -- Update Cursor models to latest (#777) (#777) +- Rename SST app to relay-web -#### Releases +### Fixed -- v6.0.0 +- Make og image compatible with OpenNext +- Track generated SST resource types +- Avoid generated SST type dependency ---- +## [3.2.13] - 2026-03-20 -## [5.0.0] - 2026-04-22 +### Fixed -### Product Perspective +- Ignore non-zero exit codes for opencode non-interactive agents -#### User-Impacting Fixes +## [3.2.12] - 2026-03-20 -- Repair pre-existing test failures on main -- Address Copilot review on broker resolution (#769) -- Ship per-platform wheels with embedded broker (drop runtime download) (#769) +### Added -### Technical Perspective +- Add Codex relay skill for sub-agent communication -#### Performance & Reliability +## [3.2.11] - 2026-03-20 -- Include publish-sdk-py in summary job +### Added -#### Releases +- Add workflow defaults abstraction -- v5.0.0 +### Fixed ---- +- Detect Codex boot marker format in PTY startup gate +- Consolidate CLI path resolution +- Reduce WS spawn pre-registration timeout from 15s to 3s -## [4.0.40] - 2026-04-22 +## [3.2.10] - 2026-03-20 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- Workflow to polish CLI output with listr2 + chalk +- CLI session collectors, step-level cwd, and run summary table -- **Add browser and github workflow primitives (#718)** (#718) +### Fixed -### Technical Perspective +- Auto-build local sdk workflows runtime +- MCP tools unavailable for agents spawned via agent_add -#### Releases +## [3.2.8] - 2026-03-18 -- v4.0.40 +### Fixed ---- +- Detect claude CLI with inline args for MCP injection -## [4.0.38] - 2026-04-22 +## [3.2.7] - 2026-03-18 -### Product Perspective +### Fixed -#### User-Impacting Fixes +- Forward RELAY_WORKSPACES_JSON and RELAY_DEFAULT_WORKSPACE to spawned agent MCP config -- Retry get_session on 503 + correct quickstart idle wait +## [3.2.6] - 2026-03-17 -### Technical Perspective +### Added -#### Releases +- Add reasoning effort metadata to model registry +- Add resize_pty protocol message for remote PTY resize -- v4.0.38 +### Fixed ---- +- Ensure spawned Claude agents get proper MCP config +- Address PR review feedback for resize_pty -## [4.0.37] - 2026-04-22 +## [3.2.4] - 2026-03-17 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- StartFrom + deterministic/worktree step parity +- A2A protocol transport layer — Python (89 tests ✅) + TypeScript +- Add OpenClaw orchestrator skill for headless multi-agent sessions +- Add TS adapters for OpenAI Agents, LangGraph, Google ADK, CrewAI + review fixes +- Add Pi RPC adapter for Python SDK + verify TS Pi adapter exports +- Add Communicate Mode SDK (on_relay) for Python and TypeScript -- **Send workflowPath so the launcher can skip the $HOME upload (#766)** (#766) +### Changed -### Technical Perspective +- Add 13 e2e tests for all TS + Python adapters against live Relaycast +- Hide communicate pages from public docs until tested +- Sync package-lock.json after config version bump -#### Releases +### Fixed -- v4.0.37 +- Address latest Devin review findings +- Move framework adapters from dependencies to optional peerDependencies +- Update TS test mock servers to match actual Relaycast API paths +- Address remaining Devin review findings +- Exclude all test files from SDK tsconfig.json too +- Exclude all test files from SDK build config +- Address Devin review findings on Communicate SDK +- Address Barry review feedback on Communicate SDK +- Address Will + Devin review feedback on Communicate SDK +- Address PR review — remove onRelay auto-detect, fix ReDoS regex +- RegisterOrRotate for 409, ws.close timeout, add @sinclair/typebox dep for Pi adapter +- Align Python SDK transport with real Relaycast API surface +- Address Devin review findings +- Exclude vitest test files from SDK build config +- Add @sinclair/typebox to root dependencies for global install +- Address PR review feedback +- Communicate mode spec compliance — adapters, tests, infra +- Critical spec compliance issues from deep review +- Spec compliance — ping/pong, auto-detect module matching +- Add per-adapter subpath exports and withRelay alias +- Sync package-lock.json with package.json ---- +## [3.2.3] - 2026-03-15 -## [4.0.36] - 2026-04-22 +### Added -### Product Perspective +- Add HTTP transport mode; route all CLI commands through SDK -#### User-Facing Features & Improvements +### Changed -- **Add credential proxy workflows runtime stack (#717)** (#717) +- Add tests for droid/opencode auto-accept permission detection -#### User-Impacting Fixes +### Fixed -- Bootstrap for first publish (#764) (#764) +- Use correct broker init subcommand and --api-port flag +- Use broker binary path instead of process.argv[1] for auto-start +- Add RELAY_SKIP_BOOTSTRAP to Codex, Opencode, and Gemini/Droid config paths +- Auto-accept droid/opencode permission prompts with --cwd +- Set RELAY_SKIP_BOOTSTRAP when agent token is pre-registered +- Address review feedback on HTTP client and listing commands +- Auto-accept Claude Code folder trust prompt for spawned agents -### Technical Perspective +## [3.2.2] - 2026-03-14 -#### Releases +### Added -- v4.0.36 +- Package plugins as proper platform formats and PRPM collections +- Implement CLI native plugins for OpenCode, Claude Code, and Gemini CLI +- Add deterministic step support to WorkflowBuilder ---- +### Changed -## [4.0.35] - 2026-04-21 +- Update MCP tool name references to 3-level hierarchy -### Product Perspective +### Fixed -#### User-Facing Features & Improvements +- Suppress codex update prompt in spawned workers +- Remove relay.shutdown() that killed the running broker in status command +- Add jq availability check in before-model-inject.sh +- Make broker API port discovery injectable for testability +- Status command spawns new broker instead of connecting to existing one +- Address Devin review round 2 — error handling, state mutation order, message limit +- Address Devin PR review comments +- Address minor verification gaps across all 3 plugins +- Idle verification loop handles single-fire agent_idle events +- Idle verification loop mirrors runVerification double-occurrence guard +- Non-lead agents in hub-spoke should use idle-as-complete +- Address Devin review feedback on PR +- Use ref-counted Map for activeReviewers instead of Set +- WorkflowBuilder drops preset field and reviewer double-booking -- **Widen @relayfile/sdk dep range to allow 0.2.x + 0.3.x (#763)** (#763) +## [3.2.1] - 2026-03-13 -### Technical Perspective +### Added -#### Releases +- Point-person-led completion pipeline -- v4.0.35 +## [3.2.0] - 2026-03-13 ---- +### Added -## [4.0.34] - 2026-04-21 +- Deterministic workspace key from user + directory -### Product Perspective +### Changed -#### User-Impacting Fixes +- Move skills to dedicated directory with symlinks +- Add workflow smoke matrix for codex and gemini -- Mark run failed under continue-on-error when steps fail (#762) (#762) +### Fixed -### Technical Perspective +- Pass --model flag to spawned CLI processes +- Rebind relaycast tokens after workspace switch +- Update MCP tool name references to dot-notation hierarchy +- Inject inter-agent DMs via workspace WebSocket +- Exact flag matching for --mcp-config guard -#### Releases +## [3.1.22] - 2026-03-11 -- v4.0.34 +### Fixed ---- +- Install parity and spawn deserialization fallback +- Preserve user MCP servers when spawning Claude from dashboard +- Codex bypass flag → --dangerously-bypass-approvals-and-sandbox -## [4.0.33] - 2026-04-20 +## [3.1.21] - 2026-03-11 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- Wire workspaceName/relaycastBaseUrl options in AgentRelay +- Add multi-workspace support to OpenClaw bridge +- Add skipRelayPrompt flag to skip MCP config injection on spawn +- Wire multi-workspace runtime flows +- Add multi-workspace auth plumbing -- **Add --register flag to mcp-args subcommand (#760)** (#760) +### Changed -#### User-Impacting Fixes +- Record multi-workspace implementation trail -- Bundle local mount package +### Fixed -### Technical Perspective +- SwitchWorkspace clawName, stale alias default, and corrupt JSON handling +- Preserve skip_relay_prompt on restart +- Reset exit info per retry + preserve exit code on spawn failure +- Avoid wiping workspace alias/id when add-workspace updates without flags +- Use timeoutMs directly in nudge loop timeout guard +- Forward skip_relay_prompt in Python SDK and skip pre-registration in broker +- Workspace default handling in add-workspace +- Harden multi-workspace add-workspace default and logging behavior +- Distinguish force-released (nudge exhaustion) from released (idle-complete) +- Address PR review feedback in workflow runner +- Always record failed attempt output for workflow retries +- Pass skipRelayPrompt through spawner headless path and simplify Rust type +- Include exitCode and exitSignal in step events +- Escape TOML string values for codex --config workspace env vars +- Treat force-released agent as step failure, not success +- Correct error message for default workspace lookup failure and forward workspace env vars in MCP snippets +- Use workspace-scoped dedup keys for MCP self-echo pre-seeding +- Allow clippy too_many_arguments on MultiWorkspaceSession::new +- Address multi-workspace code review bugs from PR +- Restore carriage return in wrap retry PTY injection -#### Releases +## [3.1.19] - 2026-03-10 -- v4.0.33 +### Fixed ---- +- Resolve install binary verification, uninstall, and version prefix bugs -## [4.0.32] - 2026-04-20 +## [3.1.18] - 2026-03-10 -### Product Perspective +### Added -#### User-Facing Features & Improvements +- Multi-workspace runtime support +- Harden handoffs with auto step owners + per-step reviews -- **Add agent-relay mcp-args subcommand (#759)** (#759) -- **Add agent activity hook** +### Fixed -#### User-Impacting Fixes +- Rebase release commit on latest main before pushing +- Guard specialist promise in executor supervised path +- Avoid rotating relay agent token on setup -- Ignore late delivery ack activity +## [3.1.14] - 2026-03-09 -### Technical Perspective +### Fixed -#### Releases +- Prevent race condition in relay WS handler binding -- v4.0.32 +## [3.1.13] - 2026-03-09 ---- +### Fixed -## [4.0.31] - 2026-04-20 +- Bind relay event handlers after WS connect +- Expose all workspace DM conversations in dashboard -### Product Perspective +## [3.1.10] - 2026-03-05 -#### User-Facing Features & Improvements +### Fixed -- **Align Rust AgentSpawn/AgentRelease with TS schema** -- **Per-component version properties on every event** -- **Instrument all CLI commands with rich events** +- Quote make_latest to prevent openclaw release from hijacking latest -#### User-Impacting Fixes +## [3.1.1] - 2026-03-04 -- FileDb in-memory cache authoritative — fixes stale status after disk write failures (#757) (#757) -- Extract runSignalHandler helper; apply in monitoring -- Is_tty should check stdin, not stdout -- Plug two CliExit regressions flagged by Devin -- Flush queue before process exit; schema cleanup -- Upgrade posthog-node from v4 to v5 +### Added -### Technical Perspective +- Add openclaw-relaycast package -#### Releases +### Fixed -- v4.0.31 +- Remove unsupported dashboard flag from dev script ---- +## [3.1.0] - 2026-03-04 -## [4.0.30] - 2026-04-19 +### Added -### Product Perspective +- Make provider spawn transport-driven +- Add direct spawn/message API -#### User-Impacting Fixes +### Changed -- Export A2A communicate subpaths (#753) (#753) +- Switch runtime contract to provider-driven headless +- Align contract fixture checks with broker event shapes -### Technical Perspective +### Fixed -#### Releases +- Make SDK lifecycle release test more robust -- v4.0.30 +## [3.0.2] - 2026-03-02 ---- +### Changed -## [4.0.29] - 2026-04-17 +- Stabilize macOS CLI agents timeout +- Allow SDK broker fallback in macOS npx verify +- Accept SDK broker fallback in npx resolution check +- Fix verify-publish PR package resolution +- Accept both relaycast workspace key field shapes +- Restore coverage threshold and fix sdk integration type +- Retrigger checks +- Use published relaycast 0.3.0 crate -### Product Perspective +### Fixed -#### User-Facing Features & Improvements +- Resolve platform-specific broker binary in SDK +- Use SDK join_channel API for broker channel joins +- Remove relay-pty references from postinstall.js +- Update verify-install to check for agent-relay-broker instead of relay-pty +- Remove redundant registration map_err conversion -- **Add ProcessBackend workflow for cloud sandbox execution (#747)** (#747) +## [2.3.16] - 2026-03-02 -### Technical Perspective +### Changed -#### Releases +- Stabilize macOS CLI agents timeout +- Allow SDK broker fallback in macOS npx verify +- Accept SDK broker fallback in npx resolution check +- Fix verify-publish PR package resolution +- Accept both relaycast workspace key field shapes +- Restore coverage threshold and fix sdk integration type +- Retrigger checks +- Use published relaycast 0.3.0 crate -- v4.0.29 +### Fixed ---- - -## [4.0.28] - 2026-04-15 - -### Product Perspective - -#### User-Impacting Fixes - -- Bundle ssh2 in release pipeline, not just scripts/build-bun.sh (#746) (#746) - -### Technical Perspective - -#### Releases - -- v4.0.28 - ---- - -## [4.0.27] - 2026-04-15 - -### Product Perspective - -#### User-Impacting Fixes - -- Bundle ssh2 into Bun binary so cloud connect exercises the ssh2 path (#745) (#745) - -### Technical Perspective - -#### Releases - -- v4.0.27 - ---- - -## [4.0.26] - 2026-04-15 - -### Product Perspective - -#### User-Impacting Fixes - -- Add visible launch checkpoint for cloud connect (#744) (#744) - -### Technical Perspective - -#### Releases - -- v4.0.26 - ---- - -## [4.0.25] - 2026-04-15 - -### Product Perspective - -#### User-Impacting Fixes - -- Stop cloud connect hangs and re-auth loops (#743) (#743) - -### Technical Perspective - -#### Releases - -- v4.0.25 - ---- - -## [4.0.24] - 2026-04-15 - -### Product Perspective - -#### User-Impacting Fixes - -- Prefer native Node TS stripping over tsx fallback (#741) (#741) - -### Technical Perspective - -#### Releases - -- v4.0.24 - ---- - -## [4.0.23] - 2026-04-14 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Show workspace key and observer URL in agent-relay status (#740)** (#740) - -### Technical Perspective - -#### Releases - -- v4.0.23 - ---- - -## [4.0.22] - 2026-04-14 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Cloud-connect fix workflows (claude hang + utils bundling) (#738)** (#738) - -### Technical Perspective - -#### Releases - -- v4.0.22 - ---- - -## [4.0.21] - 2026-04-13 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Env-var auth fallback for headless consumers (#734)** (#734) - -#### User-Impacting Fixes - -- Inbox --agent flag, history DM support, history --from DM context (#737) (#737) - -### Technical Perspective - -#### Releases - -- v4.0.21 - ---- - -## [4.0.20] - 2026-04-13 - -### Product Perspective - -#### User-Impacting Fixes - -- Replace esbuild pre-parse with tsx stderr post-processing (#735) (#735) - -### Technical Perspective - -#### Architecture & API Changes - -- Unify WorkflowTrajectory on agent-trajectories SDK (#732) (#732) - -#### Releases - -- v4.0.20 - ---- - -## [4.0.19] - 2026-04-13 - -### Product Perspective - -#### User-Impacting Fixes - -- Make preParseWorkflowFile async to avoid Bun-compiled CLI hang (#733) (#733) - -### Technical Perspective - -#### Releases - -- v4.0.19 - ---- - -## [4.0.18] - 2026-04-13 - -### Product Perspective - -#### User-Impacting Fixes - -- Add progress diagnostics and spawnSync to runScriptFile (#731) (#731) -- History/inbox fetch workspace_key via broker HTTP API (#729) (#729) - -### Technical Perspective - -#### Releases - -- v4.0.18 - ---- - -## [4.0.17] - 2026-04-13 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Workerd export condition + narrow entry + workers-safety probe (#726)** (#726) - -#### User-Impacting Fixes - -- Restore packages/sdk vitest suite to green (#728) (#728) -- Pre-parse workflow script files with actionable error hints (#727) (#727) -- Make --resume work for script workflows (#725) (#725) - -### Technical Perspective - -#### Releases - -- v4.0.17 - ---- - -## [4.0.16] - 2026-04-12 - -### Product Perspective - -#### User-Impacting Fixes - -- Wire relaycast MCP for headless opencode spawner (#723) (#723) - -### Technical Perspective - -#### Releases - -- v4.0.16 - ---- - -## [4.0.15] - 2026-04-12 - -### Product Perspective - -#### User-Impacting Fixes - -- History and inbox work without RELAY_API_KEY env var (#722) (#722) - -### Technical Perspective - -#### Releases - -- v4.0.15 - ---- - -## [4.0.14] - 2026-04-11 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add cloud cancel CLI + fix opencode headless spawn (#721)** (#721) - -### Technical Perspective - -#### Releases - -- v4.0.14 - ---- - -## [4.0.13] - 2026-04-11 - -### Product Perspective - -#### User-Impacting Fixes - -- Retry real install paths in verify-publish (#719) (#719) - -### Technical Perspective - -#### Releases - -- v4.0.13 - ---- - -## [4.0.12] - 2026-04-11 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add workflow for relay bootstrap and messaging fixes (#708)** (#708) -- **Add meta and clean-room relay validation workflows (#713)** (#713) - -### Technical Perspective - -#### Releases - -- v4.0.12 - ---- - -## [4.0.11] - 2026-04-10 - -### Product Perspective - -#### User-Impacting Fixes - -- Log full deterministic step output on failure for cloud visibility (#716) (#716) - -### Technical Perspective - -#### Releases - -- v4.0.11 - ---- - -## [4.0.10] - 2026-04-10 - -### Product Perspective - -#### User-Impacting Fixes - -- Skip in-sandbox provisioning when cloud launcher already seeded ACLs (#711) (#711) -- Harden macos binary smoke checks (#710) (#710) - -### Technical Perspective - -#### Performance & Reliability - -- Harden macos binary verification (#709) (#709) - -#### Releases - -- v4.0.10 - ---- - -## [4.0.9] - 2026-04-10 - -### Product Perspective - -#### User-Impacting Fixes - -- Harden npm publish packaging (#707) (#707) -- Use bun built-in TS validation, remove esbuild dependency (#706) (#706) -- Npm tarball propagation race in verify-publish and install.sh (#705) (#705) - -### Technical Perspective - -#### Releases - -- v4.0.9 - ---- - -## [4.0.6] - 2026-04-10 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Complete implementation + fix Supermemory adapter (#700)** (#700) - -### Technical Perspective - -#### Releases - -- v4.0.6 - ---- - -## [4.0.5] - 2026-04-08 - -### Technical Perspective - -#### Architecture & API Changes - -- Route waitlist signups to cloud - -#### Releases - -- v4.0.5 - ---- - -## [4.0.4] - 2026-04-07 - -### Product Perspective - -#### User-Impacting Fixes - -- Use local workspace session for symlink/solo mode to avoid 405 on cloud API (#692) (#692) - -### Technical Perspective - -#### Releases - -- v4.0.4 - ---- - -## [4.0.3] - 2026-04-07 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Fast workspace seeding — symlink mount + tar bulk upload (#691)** (#691) -- **30 workflows to wire relayauth/relayfile permissions into workflow runner (#673)** (#673) - -#### User-Impacting Fixes - -- Only prefer sibling relay-dashboard dev build when RELAY_LOCAL_DEV=1 (#690) (#690) -- Install broker binary to BIN_DIR so it's on PATH (#689) (#689) - -### Technical Perspective - -#### Releases - -- v4.0.3 - ---- - -## [4.0.2] - 2026-04-07 - -### Technical Perspective - -#### Releases - -- v4.0.2 - ---- - -## [4.0.1] - 2026-04-06 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **TDD refactoring workflows for runner.ts + main.rs decomposition (#675)** (#675) -- **/schedule — RelayCron landing page** -- **Auto-download relayfile-mount binary on first use (#670)** (#670) - -#### User-Impacting Fixes - -- Allow anonymous workspace creation in agent-relay on (#683) (#683) -- Wire .agentignore/.agentreadonly enforcement into agent-relay on (#671) (#671) - -### Technical Perspective - -#### Dependencies & Tooling - -- Gitignore .trajectories/ (automated run artifacts) (#676) (#676) - -#### Releases - -- v4.0.1 - ---- - -## [4.0.0] - 2026-03-31 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Default agent-relay on to production cloud endpoints (#667)** (#667) -- **Unified workspace ID across relay services (#664)** (#664) - -### Technical Perspective - -#### Releases - -- v4.0.0 - ---- - -## [3.2.22] - 2026-03-27 - -### Technical Perspective - -#### Releases - -- v3.2.22 - ---- - -## [3.2.21] - 2026-03-27 - -### Product Perspective - -#### User-Impacting Fixes - -- Avoid E2BIG spawn failure and verification token double-count (#655) (#655) -- Queue outbound messages during RelayObserver reconnect (#646) (#646) - -### Technical Perspective - -#### Releases - -- v3.2.21 - ---- - -## [3.2.18] - 2026-03-25 - -### Product Perspective - -#### User-Impacting Fixes - -- Remove unused dm_drops_total function to fix clippy dead-code warning (#645) (#645) - -### Technical Perspective - -#### Releases - -- v3.2.18 - ---- - -## [3.2.17] - 2026-03-25 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add dry-run support and stream CLI output to terminal (#643)** (#643) - -#### User-Impacting Fixes - -- Resolve DM participants for correct routing (#644) (#644) - -### Technical Perspective - -#### Releases - -- v3.2.17 - ---- - -## [3.2.16] - 2026-03-25 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add http and broker-path subpath exports for Electron apps (#640)** (#640) -- **PTY output streaming workflow (#390) (#528)** (#390) -- **Add integration step type for external services (#631)** (#631) -- **Add dynamic channel subscribe/unsubscribe to broker (#630)** (#630) -- **Cloud endpoints, API executor, and Communicate SDK v2 protocol (#632)** (#632) -- **Communicate Mode SDK (on_relay) for Python and TypeScript (#618)** (#618) -- **Add wait/steer message injection modes** - -#### User-Impacting Fixes - -- Add RELAY_SKIP_PROMPT and self-echo filtering (#641) (#641) -- Ignore failing relaycast DM tests pending relaycast 1.0 API investigation -- Cargo fmt corrections -- Sync lockfile for new UI deps -- Validate channel names at build time and dry-run (#638) (#638) -- Forward steer mode through relaycast DMs -- Unblock fork PR checks and enforce steer rejection for relaycast DM -- Propagate inbound injection mode on relay_inbound events -- Allow relaycast delivery path to accept steer mode -- Reject steer mode on relaycast-only send path -- Validate send mode and harden steer delivery semantics -- Satisfy rust fmt/clippy for injection mode changes -- Don't block steer injections behind autosuggest gate - -### Technical Perspective - -#### Performance & Reliability - -- Assert injection mode defaults to wait when omitted -- Fix missing MessageInjectionMode imports in test modules - -#### Dependencies & Tooling - -- Bump relaycast crate to v1 for injection mode support - -#### Releases - -- v3.2.16 - ---- - -## [3.2.15] - 2026-03-23 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add RelayObserver proxy client for UI consumers (#627)** (#627) - -#### User-Impacting Fixes - -- Add bypass flag to codex non-interactive spawns (#628) (#628) - -### Technical Perspective - -#### Releases - -- v3.2.15 - ---- - -## [3.2.14] - 2026-03-23 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add initial Swift SDK and harden workflow output (#589)** (#589) - -#### User-Impacting Fixes - -- Make og image compatible with OpenNext -- Track generated SST resource types -- Avoid generated SST type dependency - -### Technical Perspective - -#### Dependencies & Tooling - -- Rename SST app to relay-web - -#### Releases - -- v3.2.14 - ---- - -## [3.2.13] - 2026-03-20 - -### Product Perspective - -#### User-Impacting Fixes - -- Ignore non-zero exit codes for opencode non-interactive agents (#602) (#602) - -### Technical Perspective - -#### Releases - -- v3.2.13 - ---- - -## [3.2.12] - 2026-03-20 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add Codex relay skill for sub-agent communication (#595)** (#595) - -### Technical Perspective - -#### Releases - -- v3.2.12 - ---- - -## [3.2.11] - 2026-03-20 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add workflow defaults abstraction (#599)** (#599) - -#### User-Impacting Fixes - -- Detect Codex boot marker format in PTY startup gate (#600) (#600) -- Consolidate CLI path resolution (#598) (#598) -- Reduce WS spawn pre-registration timeout from 15s to 3s (#597) (#597) - -### Technical Perspective - -#### Releases - -- v3.2.11 - ---- - -## [3.2.10] - 2026-03-20 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Workflow to polish CLI output with listr2 + chalk (#585)** (#585) -- **CLI session collectors, step-level cwd, and run summary table (#592)** (#592) - -#### User-Impacting Fixes - -- Auto-build local sdk workflows runtime (#588) (#588) -- MCP tools unavailable for agents spawned via agent_add (#591) (#591) - -### Technical Perspective - -#### Releases - -- v3.2.10 - ---- - -## [3.2.9] - 2026-03-19 - -### Technical Perspective - -#### Releases - -- v3.2.9 - ---- - -## [3.2.8] - 2026-03-18 - -### Product Perspective - -#### User-Impacting Fixes - -- Detect claude CLI with inline args for MCP injection (#584) (#584) - -### Technical Perspective - -#### Releases - -- v3.2.8 - ---- - -## [3.2.7] - 2026-03-18 - -### Product Perspective - -#### User-Impacting Fixes - -- Forward RELAY_WORKSPACES_JSON and RELAY_DEFAULT_WORKSPACE to spawned agent MCP config (#583) (#583) - -### Technical Perspective - -#### Releases - -- v3.2.7 - ---- - -## [3.2.6] - 2026-03-17 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add reasoning effort metadata to model registry (#579)** (#579) -- **Add resize_pty protocol message for remote PTY resize** - -#### User-Impacting Fixes - -- Ensure spawned Claude agents get proper MCP config (#581) (#581) -- Address PR review feedback for resize_pty - -### Technical Perspective - -#### Releases - -- v3.2.6 - ---- - -## [3.2.5] - 2026-03-17 - -### Technical Perspective - -#### Releases - -- v3.2.5 - ---- - -## [3.2.4] - 2026-03-17 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **StartFrom + deterministic/worktree step parity (#574)** (#574) -- **A2A protocol transport layer — Python (89 tests ✅) + TypeScript** -- **Add OpenClaw orchestrator skill for headless multi-agent sessions** -- **Add TS adapters for OpenAI Agents, LangGraph, Google ADK, CrewAI + review fixes** -- **Add Pi RPC adapter for Python SDK + verify TS Pi adapter exports** -- **Add Communicate Mode SDK (on_relay) for Python and TypeScript** - -#### User-Impacting Fixes - -- Address latest Devin review findings -- Move framework adapters from dependencies to optional peerDependencies -- Update TS test mock servers to match actual Relaycast API paths -- Address remaining Devin review findings -- Exclude all test files from SDK tsconfig.json too -- Exclude all test files from SDK build config -- Address Devin review findings on Communicate SDK -- Address Barry review feedback on Communicate SDK -- Address Will + Devin review feedback on Communicate SDK -- Address PR #565 review — remove onRelay auto-detect, fix ReDoS regex (#565) -- RegisterOrRotate for 409, ws.close timeout, add @sinclair/typebox dep for Pi adapter -- Align Python SDK transport with real Relaycast API surface -- Address Devin review findings -- Exclude vitest test files from SDK build config -- Add @sinclair/typebox to root dependencies for global install -- Address PR #565 review feedback (#565) -- Communicate mode spec compliance — adapters, tests, infra -- Critical spec compliance issues from deep review -- Spec compliance — ping/pong, auto-detect module matching -- Add per-adapter subpath exports and withRelay alias -- Sync package-lock.json with package.json - -### Technical Perspective - -#### Performance & Reliability - -- Add 13 e2e tests for all TS + Python adapters against live Relaycast - -#### Dependencies & Tooling - -- Hide communicate pages from public docs until tested -- Sync package-lock.json after config version bump - -#### Releases - -- v3.2.4 - ---- - -## [3.2.3] - 2026-03-15 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add HTTP transport mode; route all CLI commands through SDK** - -#### User-Impacting Fixes - -- Use correct broker init subcommand and --api-port flag (#569) -- Use broker binary path instead of process.argv[1] for auto-start (#569) -- Add RELAY_SKIP_BOOTSTRAP to Codex, Opencode, and Gemini/Droid config paths -- Auto-accept droid/opencode permission prompts with --cwd -- Set RELAY_SKIP_BOOTSTRAP when agent token is pre-registered (#85) -- Auto-accept droid/opencode permission prompts with --cwd -- Address review feedback on HTTP client and listing commands -- Auto-accept Claude Code folder trust prompt for spawned agents - -### Technical Perspective - -#### Performance & Reliability - -- Add tests for droid/opencode auto-accept permission detection -- Add tests for droid/opencode auto-accept permission detection - -#### Releases - -- v3.2.3 - ---- - -## [3.2.2] - 2026-03-14 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Package plugins as proper platform formats and PRPM collections** -- **Implement CLI native plugins for OpenCode, Claude Code, and Gemini CLI** -- **Add deterministic step support to WorkflowBuilder** - -#### User-Impacting Fixes - -- Suppress codex update prompt in spawned workers -- Remove relay.shutdown() that killed the running broker in status command -- Add jq availability check in before-model-inject.sh -- Make broker API port discovery injectable for testability -- Status command spawns new broker instead of connecting to existing one -- Address Devin review round 2 — error handling, state mutation order, message limit -- Address Devin PR review comments -- Address minor verification gaps across all 3 plugins -- Idle verification loop handles single-fire agent_idle events -- Idle verification loop mirrors runVerification double-occurrence guard -- Non-lead agents in hub-spoke should use idle-as-complete -- Address Devin review feedback on PR #566 (#566) -- Use ref-counted Map for activeReviewers instead of Set -- WorkflowBuilder drops preset field and reviewer double-booking - -### Technical Perspective - -#### Dependencies & Tooling - -- Update MCP tool name references to 3-level hierarchy (#564) (#564) - -#### Releases - -- v3.2.2 - ---- - -## [3.2.1] - 2026-03-13 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Point-person-led completion pipeline (#552)** (#552) - -### Technical Perspective - -#### Releases - -- v3.2.1 - ---- - -## [3.2.0] - 2026-03-13 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Deterministic workspace key from user + directory (#549)** (#549) - -#### User-Impacting Fixes - -- Pass --model flag to spawned CLI processes (#559) (#559) -- Rebind relaycast tokens after workspace switch (#558) (#558) -- Update MCP tool name references to dot-notation hierarchy (#555) (#555) -- Inject inter-agent DMs via workspace WebSocket (#553) (#553) -- Exact flag matching for --mcp-config guard (#550) (#550) - -### Technical Perspective - -#### Architecture & API Changes - -- Move skills to dedicated directory with symlinks (#561) (#561) - -#### Performance & Reliability - -- Add workflow smoke matrix for codex and gemini (#544) (#544) - -#### Releases - -- v3.2.0 - ---- - -## [3.1.23] - 2026-03-12 - -### Technical Perspective - -#### Releases - -- v3.1.23 - ---- - -## [3.1.22] - 2026-03-11 - -### Product Perspective - -#### User-Impacting Fixes - -- Install parity and spawn deserialization fallback (#541) (#541) -- Preserve user MCP servers when spawning Claude from dashboard (#542) (#542) -- Codex bypass flag → --dangerously-bypass-approvals-and-sandbox (#540) (#540) - -### Technical Perspective - -#### Releases - -- v3.1.22 - ---- - -## [3.1.21] - 2026-03-11 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Wire workspaceName/relaycastBaseUrl options in AgentRelay (#538)** (#538) -- **Add multi-workspace support to OpenClaw bridge** -- **Add skipRelayPrompt flag to skip MCP config injection on spawn** (#419) -- **Wire multi-workspace runtime flows** -- **Add multi-workspace auth plumbing** - -#### User-Impacting Fixes - -- SwitchWorkspace clawName, stale alias default, and corrupt JSON handling -- Preserve skip_relay_prompt on restart -- Reset exit info per retry + preserve exit code on spawn failure -- Avoid wiping workspace alias/id when add-workspace updates without flags -- Use timeoutMs directly in nudge loop timeout guard -- Forward skip_relay_prompt in Python SDK and skip pre-registration in broker -- Workspace default handling in add-workspace -- Harden multi-workspace add-workspace default and logging behavior -- Distinguish force-released (nudge exhaustion) from released (idle-complete) -- Address PR #531 review feedback in workflow runner (#531) -- Always record failed attempt output for workflow retries -- Pass skipRelayPrompt through spawner headless path and simplify Rust type -- Include exitCode and exitSignal in step events (#499) (#499) -- Escape TOML string values for codex --config workspace env vars -- Treat force-released agent as step failure, not success (#498) -- Correct error message for default workspace lookup failure and forward workspace env vars in MCP snippets -- Use workspace-scoped dedup keys for MCP self-echo pre-seeding -- Allow clippy too_many_arguments on MultiWorkspaceSession::new -- Address multi-workspace code review bugs from PR #519 (#519) -- Restore carriage return in wrap retry PTY injection - -### Technical Perspective - -#### Dependencies & Tooling - -- Record multi-workspace implementation trail - -#### Releases - -- v3.1.21 - ---- - -## [3.1.19] - 2026-03-10 - -### Product Perspective - -#### User-Impacting Fixes - -- Resolve install binary verification, uninstall, and version prefix bugs (#535) (#535) - -### Technical Perspective - -#### Releases - -- v3.1.19 - ---- - -## [3.1.18] - 2026-03-10 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Multi-workspace runtime support (#519)** (#519) -- **Harden handoffs with auto step owners + per-step reviews (#511)** (#511) - -#### User-Impacting Fixes - -- Rebase release commit on latest main before pushing (#533) (#533) -- Guard specialist promise in executor supervised path (#525) (#525) -- Avoid rotating relay agent token on setup (#520) (#520) - -### Technical Perspective - -#### Releases - -- v3.1.18 - ---- - -## [3.1.15] - 2026-03-09 - -### Technical Perspective - -#### Releases - -- v3.1.15 - ---- - -## [3.1.14] - 2026-03-09 - -### Product Perspective - -#### User-Impacting Fixes - -- Prevent race condition in relay WS handler binding (#515) (#515) - -### Technical Perspective - -#### Releases - -- v3.1.14 - ---- - -## [3.1.13] - 2026-03-09 - -### Product Perspective - -#### User-Impacting Fixes - -- Bind relay event handlers after WS connect (#513) (#513) -- Expose all workspace DM conversations in dashboard (#510) (#510) - -### Technical Perspective - -#### Releases - -- v3.1.13 - ---- - -## [3.1.12] - 2026-03-07 - -### Technical Perspective - -#### Releases - -- v3.1.12 - ---- - -## [3.1.11] - 2026-03-07 - -### Technical Perspective - -#### Releases - -- v3.1.11 - ---- - -## [3.1.10] - 2026-03-05 - -### Product Perspective - -#### User-Impacting Fixes - -- Quote make_latest to prevent openclaw release from hijacking latest (#496) (#496) - -### Technical Perspective - -#### Releases - -- v3.1.10 - ---- - -## [3.1.9] - 2026-03-05 - -### Technical Perspective - -#### Releases - -- v3.1.9 - ---- - -## [3.1.8] - 2026-03-05 - -### Technical Perspective - -#### Releases - -- v3.1.8 - ---- - -## [3.1.7] - 2026-03-05 - -### Technical Perspective - -#### Releases - -- v3.1.7 - ---- - -## [3.1.5] - 2026-03-04 - -### Technical Perspective - -#### Releases - -- v3.1.5 - ---- - -## [3.1.4] - 2026-03-04 - -### Technical Perspective - -#### Releases - -- v3.1.4 - ---- - -## [3.1.3] - 2026-03-04 - -### Technical Perspective - -#### Releases - -- v3.1.3 - ---- - -## [3.1.2] - 2026-03-04 - -### Technical Perspective - -#### Releases - -- v3.1.2 - ---- - -## [3.1.1] - 2026-03-04 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Add openclaw-relaycast package (#474)** (#474) - -#### User-Impacting Fixes - -- Remove unsupported dashboard flag from dev script - -### Technical Perspective - -#### Releases - -- v3.1.1 - ---- - -## [3.1.0] - 2026-03-04 - -### Product Perspective - -#### User-Facing Features & Improvements - -- **Make provider spawn transport-driven** -- **Add direct spawn/message API (#473)** (#473) - -#### User-Impacting Fixes - -- Make SDK lifecycle release test more robust (#471) (#471) - -### Technical Perspective - -#### Architecture & API Changes - -- Switch runtime contract to provider-driven headless - -#### Performance & Reliability - -- Align contract fixture checks with broker event shapes - -#### Releases - -- v3.1.0 - ---- - -## [3.0.2] - 2026-03-02 - -### Product Perspective - -#### User-Impacting Fixes - -- Resolve platform-specific broker binary in SDK (#464) (#464) -- Use SDK join_channel API for broker channel joins -- Remove relay-pty references from postinstall.js -- Update verify-install to check for agent-relay-broker instead of relay-pty -- Remove redundant registration map_err conversion - -### Technical Perspective - -#### Performance & Reliability - -- Stabilize macOS CLI agents timeout -- Allow SDK broker fallback in macOS npx verify -- Accept SDK broker fallback in npx resolution check -- Fix verify-publish PR package resolution -- Accept both relaycast workspace key field shapes -- Restore coverage threshold and fix sdk integration type -- Retrigger checks - -#### Dependencies & Tooling - -- Use published relaycast 0.3.0 crate - -#### Releases - -- v3.0.2 - ---- - -## [2.3.16] - 2026-03-02 - -### Product Perspective - -#### User-Impacting Fixes - -- Resolve platform-specific broker binary in SDK (#464) (#464) -- Use SDK join_channel API for broker channel joins -- Remove relay-pty references from postinstall.js -- Update verify-install to check for agent-relay-broker instead of relay-pty -- Remove redundant registration map_err conversion - -### Technical Perspective - -#### Performance & Reliability - -- Stabilize macOS CLI agents timeout -- Allow SDK broker fallback in macOS npx verify -- Accept SDK broker fallback in npx resolution check -- Fix verify-publish PR package resolution -- Accept both relaycast workspace key field shapes -- Restore coverage threshold and fix sdk integration type -- Retrigger checks - -#### Dependencies & Tooling - -- Use published relaycast 0.3.0 crate - -#### Releases - -- v2.3.16 - ---- +- Resolve platform-specific broker binary in SDK +- Use SDK join_channel API for broker channel joins +- Remove relay-pty references from postinstall.js +- Update verify-install to check for agent-relay-broker instead of relay-pty +- Remove redundant registration map_err conversion ## [2.3.14] - 2026-02-19 -### Technical Perspective - -#### Dependencies & Tooling - -- Auto-generate CHANGELOG on stable release (#447) (#447) - -#### Releases - -- v2.3.14 +### Changed ---- +- Auto-generate CHANGELOG on stable release ## [2.1.5] - 2026-01-30 -### Product Perspective - -#### User-Facing Features & Improvements - -- **Task injection retries**: Spawning agents with tasks now automatically retries delivery up to 3 times, preventing silent failures that left agents without their initial instructions. - -#### User-Impacting Fixes - -- Auto-suggestion injection and cursor-agent reconciliation fixed — agents now correctly receive suggestions and cursor state stays in sync (#347). +### Added -### Technical Perspective +- Task injection retries: Spawning agents with tasks now automatically retries delivery up to 3 times, preventing silent failures that left agents without their initial instructions. -#### Architecture & API Changes +### Changed -- Injection retry logic added to spawn flow with configurable attempts and backoff (#349). +- Injection retry logic added to spawn flow with configurable attempts and backoff. - Cursor-agent reconciliation ensures agent state matches the editor's cursor position after reconnects. -#### Releases - -- v2.1.4, v2.1.5 +### Fixed ---- +- Auto-suggestion injection and cursor-agent reconciliation fixed — agents now correctly receive suggestions and cursor state stays in sync. ## [2.1.3] - 2026-01-29 -### Product Perspective - -#### User-Facing Features & Improvements - -- **Agent-to-agent JSONL watch**: Agents can now observe each other's activity streams via JSONL watch, enabling real-time coordination (#346). -- **Onboarding improvements**: Smoother first-run experience with better prompts and flow handling (#345). -- **SQLite dependency removed**: Storage layer switched from SQLite to JSONL, reducing native binary requirements and simplifying installation (#343). - -#### User-Impacting Fixes - -- Relay-pty binary resolution fixed for `npx` usage — no longer requires postinstall scripts, making global installs more reliable (#344). -- Messages path routing corrected for dashboard storage (#341). +### Added -### Technical Perspective +- Agent-to-agent JSONL watch: Agents can now observe each other's activity streams via JSONL watch, enabling real-time coordination. +- Onboarding improvements: Smoother first-run experience with better prompts and flow handling. +- SQLite dependency removed: Storage layer switched from SQLite to JSONL, reducing native binary requirements and simplifying installation. -#### Architecture & API Changes +### Changed - Storage backend migrated from SQLite to JSONL flat files, eliminating the native `better-sqlite3` dependency. - Relay-pty binary resolution rewritten with comprehensive edge case handling for npx, global installs, and monorepo setups. - Agent-to-agent JSONL watch enables streaming observation of peer agent activity. - -#### Performance & Reliability - - Comprehensive test suite added for relay-pty binary path resolution across install scenarios. -- Bundled dependency audit added to CI (#339). -- Timeout and skip logic for x64 macOS verification on PRs (#340). - -#### Dependencies & Tooling - +- Bundled dependency audit added to CI. +- Timeout and skip logic for x64 macOS verification on PRs. - Removed `better-sqlite3` native dependency in favor of JSONL storage. - macOS x64 verification job removed from CI (slow, low value). -#### Releases - -- v2.1.0, v2.1.1, v2.1.2, v2.1.3 (plus v2.0.34–v2.0.37) +### Fixed ---- +- Relay-pty binary resolution fixed for `npx` usage — no longer requires postinstall scripts, making global installs more reliable. +- Messages path routing corrected for dashboard storage. ## [2.0.37] - 2026-01-28 -### Product Perspective - -#### User-Facing Features & Improvements - -- **OpenCode HTTP API integration**: Full OpenCode provider support via HTTP API, enabling OpenCode as a first-class agent backend (#337). -- **File-based continuity**: Agents can now save and restore session state through file-based continuity commands, surviving restarts and long operations (#331). -- **Performance benchmarking**: New benchmarking package for comparing agent configurations and measuring swarm performance (#326). -- **MCP client parity**: MCP client now aligned with SDK for consistent behavior across both integration paths (#323). - -#### User-Impacting Fixes - -- **Unbounded output buffer crash fixed**: `RangeError` from large agent output no longer crashes the process (#338). -- Storage health reporting and doctor CLI now correctly handle JSONL storage (#334, #335). -- Stale agents cleaned up automatically when their process dies without a clean disconnect (#319). -- CJS exports fixed for `agent-relay` and `@agent-relay/utils` — CommonJS consumers can now `require()` the packages (#325, #328). +### Added -### Technical Perspective +- OpenCode HTTP API integration: Full OpenCode provider support via HTTP API, enabling OpenCode as a first-class agent backend. +- File-based continuity: Agents can now save and restore session state through file-based continuity commands, surviving restarts and long operations. +- Performance benchmarking: New benchmarking package for comparing agent configurations and measuring swarm performance. +- MCP client parity: MCP client now aligned with SDK for consistent behavior across both integration paths. -#### Architecture & API Changes +### Changed - OpenCode HTTP API integration adds a new provider adapter for the OpenCode backend. - File-based continuity command handling added to orchestrator for session persistence. - New `listConnectedAgents()` and `removeAgent()` APIs for programmatic agent management. - Shared client helpers extracted to `@agent-relay/utils` for SDK/MCP consistency. - MCP client aligned with SDK: `sendAndWait` return types updated to `AckPayload`, `PROTOCOL_VERSION` imported consistently. -- Agent capacity increased to support 10,000 concurrent agents (#318). - -#### Performance & Reliability - +- Agent capacity increased to support 10,000 concurrent agents. - Output buffer bounds enforced to prevent `RangeError` crashes from large payloads. - Storage reliability and security fixes: health checks, doctor diagnostics, and JSONL handling hardened. - Stale agent cleanup on process death prevents ghost entries in connected agent lists. -- Relay-pty binary fallback logic improved for cross-platform resolution (#324). - -#### Dependencies & Tooling - -- Post-publish verification workflow added for npm packages with npx, Docker, and macOS tests (#323). +- Relay-pty binary fallback logic improved for cross-platform resolution. +- Post-publish verification workflow added for npm packages with npx, Docker, and macOS tests. - CJS build artifacts generated during `npm pack` for dual ESM/CJS support. - Bundled dependencies ensure tarball includes all `@agent-relay` packages. - macOS CI runners updated (macos-13 → macos-15-large, macos-12 for Intel x64). - Dashboard publishing removed from relay monorepo (moved to relay-cloud). -- PostHog analytics added to docs site (#321). +- PostHog analytics added to docs site. -#### Releases - -- v2.0.21–v2.0.32, plus numerous CI and packaging fixes. +### Fixed ---- +- Unbounded output buffer crash fixed: `RangeError` from large agent output no longer crashes the process. +- Storage health reporting and doctor CLI now correctly handle JSONL storage. +- Stale agents cleaned up automatically when their process dies without a clean disconnect. +- CJS exports fixed for `agent-relay` and `@agent-relay/utils` — CommonJS consumers can now `require()` the packages. ## [2.0.25] - 2026-01-27 -### Product Perspective - -#### User-Facing Features & Improvements - -- **Dashboard moved to relay-cloud**: Dashboard package removed from the relay monorepo and migrated to the dedicated relay-cloud repository, simplifying the core package. -- **CLI dashboard startup**: `--dashboard` flag now launches the dashboard via npx fallback when not locally available (#322). -- **Socket length handling**: Long socket messages no longer truncated or malformed (#317). -- **Stale agent cleanup**: Agents whose processes die without clean disconnect are now automatically removed (#319). -- **10K agent capacity**: Relay server now supports up to 10,000 concurrent connected agents (#318). - -#### User-Impacting Fixes - -- Dashboard references cleaned up after package removal to prevent broken imports. -- Socket.rs `warn!` macro indentation corrected for proper Rust compilation. -- CLI tests isolated from running daemon to prevent interference. - -### Technical Perspective - -#### Architecture & API Changes +### Added -- Dashboard package fully removed; CI updated to test daemon via socket instead of HTTP (#315, #316). -- `listConnectedAgents()` and `removeAgent()` APIs added for agent lifecycle management (#319). -- Agent capacity limit raised to 10,000 (#318). -- Socket length handling improved in Rust relay-pty core (#317). +- Dashboard moved to relay-cloud: Dashboard package removed from the relay monorepo and migrated to the dedicated relay-cloud repository, simplifying the core package. +- CLI dashboard startup: `--dashboard` flag now launches the dashboard via npx fallback when not locally available. +- Socket length handling: Long socket messages no longer truncated or malformed. +- Stale agent cleanup: Agents whose processes die without clean disconnect are now automatically removed. +- 10K agent capacity: Relay server now supports up to 10,000 concurrent connected agents. -#### Performance & Reliability +### Changed +- Dashboard package fully removed; CI updated to test daemon via socket instead of HTTP. +- `listConnectedAgents()` and `removeAgent()` APIs added for agent lifecycle management. +- Agent capacity limit raised to 10,000. +- Socket length handling improved in Rust relay-pty core. - Stale agent cleanup prevents ghost entries when processes exit uncleanly. - CLI tests no longer conflict with a running local daemon. - -#### Dependencies & Tooling - -- Dashboard publishing workflow removed; package cleanup across workspaces (#315, #320). -- PostHog analytics added to documentation site (#321). +- Dashboard publishing workflow removed; package cleanup across workspaces. +- PostHog analytics added to documentation site. - npx fallback added for dashboard startup in CLI. -#### Releases - -- v2.0.21–v2.0.25 +### Fixed ---- +- Dashboard references cleaned up after package removal to prevent broken imports. +- Socket.rs `warn!` macro indentation corrected for proper Rust compilation. +- CLI tests isolated from running daemon to prevent interference. ## [2.0.20] - 2026-01-26 -### Overview - -- Major SDK expansion with swarm primitives, logs API, and protocol types. -- New CLI auth testing package with Dockerized workflows and scripts. -- Relay-pty and wrapper improvements focused on reliability and orchestration. -- Expanded documentation for swarm primitives and testing guides. - -### Product Perspective - -#### User-Facing Features & Improvements +### Added - Swarm primitives added to SDK with full documentation and examples. - CLI auth testing tooling introduced with repeatable scripts and Docker workflows. - Provider connection UI copy refreshed (OpenCode/Droid messaging updates). - Improved onboarding reliability for OAuth flows in cloud workspaces. +- `@agent-relay/mcp` package with MCP tools/resources and one-command install. +- Swarm primitives SDK API and examples (`SWARM_CAPABILITIES`, `SWARM_PATTERNS`). +- CLI auth testing package with Docker and scripted flows. +- New roadmap/spec documentation for primitives and multi-server architecture. -#### User-Impacting Fixes - -- Spawner registration timeouts in cloud workspaces resolved. -- Idle detection behavior made more robust to avoid false positives. -- OAuth URL parsing now handles line-wrapped output from CLI. - -#### Deprecations - -- None noted for this release. - -#### Breaking Changes & Migration Guidance - -- None noted for this release. - -### Technical Perspective - -#### Architecture & API Changes +### Changed +- Major SDK expansion with swarm primitives, logs API, and protocol types. +- New CLI auth testing package with Dockerized workflows and scripts. +- Relay-pty and wrapper improvements focused on reliability and orchestration. +- Expanded documentation for swarm primitives and testing guides. - New SDK client capabilities (`client`, `logs`, and protocol types) and expanded test coverage. - Spawner logic updated for more reliable agent registration and routing. - Relay-pty orchestration updated in Rust core with supporting wrapper changes. - -#### Performance & Reliability - - Idle detection strengthened in wrapper layer (logic + tests). - Relay-pty orchestration hardened; additional tests for injection handling. - -#### Dependencies & Tooling - - Workspace package updates and lockfile refresh. - New hooks scripts (`scripts/hooks/install.sh`, `scripts/hooks/pre-commit`) for developer workflows. - Dockerfiles updated for workspace and CLI testing images. - -#### Implementation Details (For Developers) - - Added `packages/cli-tester` with auth credential checks and socket client utilities. - New CLI tester scripts for spawn/registration/auth flows. - `packages/config` gains CLI auth config updates for cloud onboarding. - `relay-pty` binary updated for macOS arm64. - -### Added - -- `@agent-relay/mcp` package with MCP tools/resources and one-command install. -- Swarm primitives SDK API and examples (`SWARM_CAPABILITIES`, `SWARM_PATTERNS`). -- CLI auth testing package with Docker and scripted flows. -- New roadmap/spec documentation for primitives and multi-server architecture. - -### Fixed - -- Cloud spawner timeout in agent registration. -- OAuth URL parsing for line-wrapped output in CLI auth flows. -- Idle detection stability in wrapper layer. -- Relay-pty postinstall and codesign handling for macOS builds. -- Minor CI/test issues in relay-pty orchestrator tests. - -### Changed - - Dynamic import for MCP commands in CLI. - Spawner and daemon routing adjustments for improved registration and diagnostics. - Wrapper base class behavior and tests for relay-pty orchestration. - -### Infrastructure & Refactors - - Updates to workspace Dockerfiles and publish workflow tweaks. - Package metadata alignment across SDK, dashboard, wrapper, spawner, and api-types. - Additional instrumentation in relay-pty and orchestrator to support reliability. - -### Documentation - - Swarm primitives guide and comprehensive roadmap specification. - CLI auth testing guide. -### Recent Daily Breakdown - -#### 2026-01-27 - -- Merged swarm primitives and channels work into mainline (#314). -- Relay and orchestrator fixes: relay-pty updates, wrapper base changes, and new dev hooks. - -#### 2026-01-26 - -- Added CLI auth testing package with Docker workflow and scripts. -- Added swarm primitives SDK APIs, examples, and documentation. -- Added primitives roadmap spec and beads/trajectory artifacts. -- Fixed spawner registration timeout in cloud workspaces. -- Improved onboarding behavior for OAuth URL wrapping and bypass permissions. -- Hardened idle detection and relay-pty orchestration; added tests. -- Updated package-lock and workspace package metadata; release tags v2.0.18–v2.0.20. - -### Commit Activity (Past 3 Weeks) - -- 23 commits across Jan 26–27, 2026 (21 on Jan 26; 2 on Jan 27). -- Authors: Khaliq (18), GitHub Actions (3), Agent Relay (2). -- Top scopes: `feat`, `fix`, `docs`, `chore`. - ---- - -## [Three-Week Retrospective: Jan 3–24, 2026] - -## [Week 1: January 3-10, 2026] - -### Product Perspective - -**Core Messaging & Communication** - -- First-class channels and direct messages as core features. -- Direct message routing improvements and message store integration. - -**Cloud & Workspace Management** - -- Cloud link logic for workspace connectivity. -- Workspace persistence across container restarts and dynamic repo management. -- Workspace deployment fixes. - -**Developer Experience** - -- CLI patterns for agent visibility and log tailing. -- Codex state management and XTerm display improvements. - -**Billing & Authentication** - -- Billing bridge fixes and GitHub CLI auth support. -- Authentication tightening and token fetch improvements. - -### Technical Perspective - -**Architecture & Infrastructure** - -- Multi-server architecture documentation and scalability adjustments. -- WebSocket ping/pong keepalive for main and bridge connections. - -**Cloud Infrastructure** - -- Cloud link migrations and update-workspaces workflow fixes. - -**State Management** - -- Message delivery fixes and Codex state persistence improvements. - -**Deployment & Operations** - -- Container entrypoint updates and deployment fixes. - -**Documentation** - -- Trail snippet bump and competitive analysis additions. - ---- - -## [Week 2: January 10-17, 2026] - -### Product Perspective - -**Mobile & UI Improvements** - -- Mobile scrolling fixes for XTermLogViewer and viewport stability. -- Dashboard UI restrictions/restore and agent list labeling cleanup. - -**Channels & Messaging** - -- Channel creation logging improvements. -- Message routing, duplication, and attribution fixes in cloud dashboard. - -**Workspace & User Management** - -- Workspace selector and user filtering fixes. -- Workspace proxy query parameter preservation. - -**Agent Profiles & Coordination** - -- Added agent profiles for multi-project support and Mega coordinator command. -- Trajectory viewer race condition fixes. - -**Authentication & Providers** - -- Gemini API key validation fixes and Claude login flow improvements. - -### Technical Perspective - -**Relay-PTY System Migration** - -- Node-pty to Rust relay-pty migration with hybrid orchestrator. -- Relay-pty infrastructure tests and Rust 1.83 Cargo.lock v4 fixes. - -**Performance & Reliability** - -- Injection reliability improvements and duplicate terminal message fixes. -- Workspace ID sync fixes to avoid routing race conditions. - -**State & Continuity** - -- Continuity parsing and workspace path handling. - -**Fallback Logic** - -- Proper fallback logic and protocol prompt updates. - ---- - -## [Week 3: January 17-24, 2026] - -### Product Perspective - -**Channels & Team Collaboration** - -- Channel invites, endpoints, and message delivery fixes. -- Mobile channel scrolling and DM filtering in sidebar. -- Unified threading between channels and DMs. - -**Performance & User Experience** - -- 5x faster relay message injection latency. -- Mobile scrolling improvements and unified markdown rendering. -- Agent pin-to-top for agents panel. - -**Developer Experience** - -- Model selection dropdown sync and mapping consolidation. -- CLI tool bumps and SDK fixes. - -**Workspace & Credentials** - -- Workspace-scoped provider credentials. -- Workspace switching fixes and force-update workflow. - -**Pricing & Documentation** - -- Pricing updates and TASKS/protocol documentation refresh. -- Clarified agent roles (devops vs infrastructure). - -### Technical Perspective - -**Build System & CI/CD** - -- Turborepo integration and concurrent Docker builds. -- Turbo/TypeScript build fixes and publish error remediation. - -**Sync Messaging Protocol** - -- Turn-based sync messaging with `[await]` syntax and ACK tracking. - -**Daemon & Spawning** - -- Daemon-based spawning with improved diagnostics and membership restore. -- Spawn timing race condition fixes. - -**Cloud Infrastructure** - -- Static file serving fixes, new `/api/bridge` endpoint, and routing fixes. -- Cloud sync heartbeat timeout handling and queue monitor fixes. - -**Authentication & Git Operations** - -- GitHub token fallback and GH_TOKEN injection fixes. -- Custom GitHub credential helper with improved retry logic. - -**Workspace & Path Management** +### Fixed -- Workspace inbox namespacing and continuity parsing improvements. +- Spawner registration timeouts in cloud workspaces resolved. +- Idle detection behavior made more robust to avoid false positives. +- OAuth URL parsing now handles line-wrapped output from CLI. +- Cloud spawner timeout in agent registration. +- OAuth URL parsing for line-wrapped output in CLI auth flows. +- Idle detection stability in wrapper layer. +- Relay-pty postinstall and codesign handling for macOS builds. +- Minor CI/test issues in relay-pty orchestrator tests. 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..91e5cd6ee --- /dev/null +++ b/crates/broker/src/codex_session.rs @@ -0,0 +1,241 @@ +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()); + 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 d8562fc93..17c46601f 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}, @@ -38,6 +38,7 @@ pub enum ListenApiRequest { cli: String, transport: Option, model: Option, + harness: Option, args: Vec, task: Option, channels: Vec, @@ -611,6 +612,23 @@ async fn listen_api_spawn( .or_else(|| body.get("restartPolicy")) .cloned(), ); + let harness = match body + .get("harness") + .cloned() + .map(serde_json::from_value::) + .transpose() + { + Ok(value) => 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")) @@ -632,6 +650,7 @@ async fn listen_api_spawn( cli, transport, model, + harness, args, task, channels, @@ -2265,6 +2284,7 @@ mod auth_tests { cli, transport, model, + harness, args, task, channels, @@ -2283,6 +2303,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!( @@ -2316,6 +2342,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 7ad80fcf8..80f53e5ea 100644 --- a/crates/broker/src/protocol.rs +++ b/crates/broker/src/protocol.rs @@ -19,6 +19,53 @@ 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 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 +78,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")] @@ -157,6 +208,8 @@ pub enum BrokerEvent { parent: Option, cli: Option, model: Option, + #[serde(default, rename = "sessionId")] + session_id: Option, pid: Option, source: Option, }, @@ -445,6 +498,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 42c460224..b8267da33 100644 --- a/crates/broker/src/relaycast/mod.rs +++ b/crates/broker/src/relaycast/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod dm_participants; pub(crate) mod workspace; pub(crate) mod ws; -pub(crate) use crate::snippets::{configure_relaycast_mcp_with_token, ensure_relaycast_mcp_config}; +pub(crate) use crate::snippets::configure_relaycast_mcp_with_token; pub(crate) use auth::AuthClient; pub(crate) use bridge::{map_ws_broker_command, map_ws_event}; pub(crate) use dm_participants::{resolve_dm_participants_cached, DmParticipantsCache}; diff --git a/crates/broker/src/runtime/api.rs b/crates/broker/src/runtime/api.rs index 945885971..506aba287 100644 --- a/crates/broker/src/runtime/api.rs +++ b/crates/broker/src/runtime/api.rs @@ -33,6 +33,7 @@ impl BrokerRuntime { cli, transport, model, + harness, args, task, channels, @@ -57,6 +58,7 @@ impl BrokerRuntime { cli.clone(), transport, model.clone(), + harness, args, effective_channels.clone(), cwd, @@ -253,6 +255,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(), @@ -272,6 +275,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 f8b679b67..8ea579d92 100644 --- a/crates/broker/src/runtime/init.rs +++ b/crates/broker/src/runtime/init.rs @@ -185,7 +185,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 a39655365..f7fe89895 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 d11f75afa..5a9648f96 100644 --- a/crates/broker/src/runtime/relaycast_events.rs +++ b/crates/broker/src/runtime/relaycast_events.rs @@ -221,6 +221,8 @@ impl BrokerRuntime { cli: Some(cli.clone()), model: None, cwd: None, + session_id: None, + harness: None, team: None, shadow_of: None, shadow_mode: None, @@ -231,7 +233,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 @@ -332,6 +334,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(), @@ -430,6 +433,8 @@ impl BrokerRuntime { cli: Some(cli.clone()), model: None, cwd: None, + session_id: None, + harness: None, team: None, shadow_of: None, shadow_mode: None, @@ -525,6 +530,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 e3332cfa2..ce1c694f3 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, @@ -1916,6 +1918,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()), @@ -1932,6 +1935,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( @@ -1939,6 +1971,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, @@ -1999,6 +2032,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..85f487896 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) = workers .workers .get(&name) .map(|h| { @@ -324,9 +324,10 @@ impl BrokerRuntime { h.spec.provider.clone(), h.spec.cli.clone(), h.spec.model.clone(), + h.spec.session_id.clone(), ) }) - .unwrap_or((None, None, None)); + .unwrap_or((None, None, None, None)); let _ = send_event( sdk_out_tx, json!({ @@ -336,6 +337,7 @@ impl BrokerRuntime { "provider": provider_val, "cli": cli_val, "model": model_val, + "sessionId": session_id_val, }), ) .await; diff --git a/crates/broker/src/snippets.rs b/crates/broker/src/snippets.rs index 9baaf92a3..e74526a1e 100644 --- a/crates/broker/src/snippets.rs +++ b/crates/broker/src/snippets.rs @@ -9,7 +9,8 @@ use anyhow::{Context, Result}; use serde_json::{Map, Value}; use tokio::process::Command; -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"; @@ -239,7 +240,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() { @@ -260,7 +261,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()), ]), ); } @@ -370,7 +372,8 @@ pub fn ensure_opencode_config( 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(); @@ -680,7 +683,7 @@ pub async fn configure_relaycast_mcp_with_token( "--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([ @@ -848,7 +851,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" ) } @@ -903,7 +906,8 @@ fn gemini_droid_mcp_add_args( } 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 } @@ -1001,12 +1005,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")); } #[test] @@ -1026,19 +1029,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] @@ -1174,7 +1165,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] @@ -1353,11 +1345,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] @@ -1394,11 +1383,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] @@ -1594,7 +1583,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"]; @@ -2056,7 +2046,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 20a2c2cff..dbf560b37 100644 --- a/crates/broker/src/supervisor.rs +++ b/crates/broker/src/supervisor.rs @@ -225,6 +225,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 059011061..41377a7d5 100644 --- a/crates/broker/src/worker.rs +++ b/crates/broker/src/worker.rs @@ -1,5 +1,7 @@ use std::{ collections::HashMap, + env, + ffi::OsString, path::{Path, PathBuf}, process::Stdio, time::{Duration, Instant}, @@ -7,7 +9,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_token, supervisor::Supervisor, }; @@ -132,6 +137,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, @@ -217,8 +223,10 @@ 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 harness = spec.harness.clone(); + let (parsed_cli, inline_cli_args) = parse_cli_command(cli) .with_context(|| format!("invalid CLI command '{cli}'"))?; + let resolved_cli = resolve_harness_command(&parsed_cli, harness.as_ref()); let normalized_cli = normalize_cli_name(&resolved_cli); let mut effective_args = inline_cli_args; effective_args.extend(spec.args.clone()); @@ -244,28 +252,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, @@ -284,35 +341,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); } } @@ -676,12 +764,450 @@ 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 resolve_command_with_paths(command: &str, search_paths: &[String]) -> String { + if command.contains('/') || command.contains('\\') || command.starts_with('.') { + return canonicalize_display(Path::new(command)); + } + + for dir in search_paths { + let candidate = expand_home_path(dir).join(command); + if candidate.is_file() { + 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 candidate.is_file() { + 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 || Path::new(&resolved).is_file() { + return resolved; + } + } + + candidates + .first() + .copied() + .unwrap_or(default_command) + .to_string() +} + +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 + .map(|fallback| { + tracing::warn!( + worker = %worker_name, + requested_model = %requested, + fallback_model = %fallback, + "local Codex CLI model catalog does not confirm requested model; using fallback" + ); + 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, @@ -991,6 +1517,84 @@ fn spawn_worker_reader( }); } +#[cfg(test)] +mod harness_adapter_tests { + use super::*; + + #[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::*; @@ -1039,6 +1643,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 fac401f5c..61f192a71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "@modelcontextprotocol/sdk": "^1.0.0", "@relayauth/core": "^0.1.2", "@relayauth/sdk": "^0.1.2", - "@relaycast/mcp": "1.0.0", "@relaycast/sdk": "^1.1.0", "@relayfile/local-mount": "^0.2.2", "@relayfile/sdk": "^0.6.0", @@ -3963,39 +3962,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 0a54c4e24..a0f9e3a4d 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": { @@ -144,7 +148,6 @@ "@modelcontextprotocol/sdk": "^1.0.0", "@relayauth/core": "^0.1.2", "@relayauth/sdk": "^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..b57fe9181 100644 --- a/packages/openclaw/src/setup.ts +++ b/packages/openclaw/src/setup.ts @@ -397,7 +397,8 @@ export async function setup(options: SetupOptions): Promise { ...mcp.prefix, 'config', 'add', 'relaycast', '--command', 'npx', - '--arg', '@relaycast/mcp', + '--arg', 'agent-relay', + '--arg', 'mcp', ...envArgs, '--scope', 'home', '--description', 'Relaycast messaging MCP server', @@ -444,7 +445,8 @@ export async function setup(options: SetupOptions): Promise { ...mcp.prefix, 'config', 'add', 'relaycast', '--command', 'npx', - '--arg', '@relaycast/mcp', + '--arg', 'agent-relay', + '--arg', 'mcp', ...envArgs, '--env', `RELAY_AGENT_TOKEN=${agentToken}`, '--scope', 'home', 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..bdf81a780 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, @@ -348,6 +349,7 @@ async def spawn_pty( } 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 @@ -368,6 +370,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 +395,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..2dec37b24 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', ) @@ -596,6 +653,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 +680,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 +813,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 +850,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 +905,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..697ebb4d4 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,49 @@ def to_dict(self) -> dict[str, Any]: return result +@dataclass +class HarnessDefinition: + """Serializable harness adapter config for spawning and workflows.""" + + 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.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 +373,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 +391,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..5c83c9414 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? } 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? } 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 e3d43bfcf..9bd5989b7 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -58,6 +58,23 @@ 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', +}); + // Wait for agent to finish (go idle or exit) const result = await agent.waitForIdle(120_000); @@ -102,6 +119,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__/spawn-harness.test.ts b/packages/sdk/src/__tests__/spawn-harness.test.ts new file mode 100644 index 000000000..eac9d9cc3 --- /dev/null +++ b/packages/sdk/src/__tests__/spawn-harness.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { AgentRelayClient } from '../client.js'; +import { registerHarnessAdapter } from '../cli-registry.js'; +import { AgentRelay } from '../relay.js'; + +describe('spawn harness adapters', () => { + 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('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..34ea64190 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,7 +54,10 @@ 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; + +const CLI_REGISTRY: Record = { claude: { binaries: ['claude'], nonInteractiveArgs: (task, extra = []) => ['-p', '--dangerously-skip-permissions', task, ...extra], @@ -111,18 +118,198 @@ const CLI_REGISTRY: Record = { }, }; +const USER_CLI_REGISTRY = new Map(); +const USER_HARNESS_CONFIGS = new Map(); + +function normalizeCliKey(cli: string): string { + const trimmed = cli.trim(); + if (!trimmed) { + throw new Error('Harness name must be a non-empty string'); + } + return trimmed.includes(':') ? trimmed.split(':')[0] : trimmed; +} + +function lookupCliKey(cli: string): string | undefined { + const trimmed = cli.trim(); + if (!trimmed) return undefined; + return trimmed.includes(':') ? trimmed.split(':')[0] : trimmed; +} + +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.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 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 expandTemplateArg( + template: string, + context: { task: string; extraArgs: string[]; model?: string } +): string[] { + if (template === '{args}' || template === '{{args}}') { + return [...context.extraArgs]; + } + if ((template === '{model}' || template === '{{model}}') && context.model === undefined) { + return []; + } + return [ + template + .replace(/\{\{\s*task\s*\}\}|\{task\}/g, context.task) + .replace(/\{\{\s*model\s*\}\}|\{model\}/g, context.model ?? ''), + ]; +} + +function renderArgTemplate( + template: readonly string[], + context: { task: string; extraArgs?: string[]; model?: string } +): string[] { + const args: string[] = []; + for (const entry of template) { + args.push( + ...expandTemplateArg(entry, { + task: context.task, + extraArgs: context.extraArgs ?? [], + 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 }), + 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 HarnessAdapter = CliDefinition | HarnessDefinition; + +function isCliDefinition(adapter: HarnessAdapter): adapter is CliDefinition { + return typeof (adapter as { nonInteractiveArgs?: unknown }).nonInteractiveArgs === 'function'; +} + +export function defineHarnessAdapter(name: string, adapter: HarnessAdapter): CliDefinition { + if (isCliDefinition(adapter)) { + return { + ...adapter, + binaries: [...adapter.binaries], + bypassAliases: adapter.bypassAliases ? [...adapter.bypassAliases] : undefined, + searchPaths: adapter.searchPaths ? [...adapter.searchPaths] : undefined, + }; + } + return adapterFromConfig(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: HarnessAdapter): void { + const key = normalizeCliKey(name); + const definition = defineHarnessAdapter(key, adapter); + USER_CLI_REGISTRY.set(key, definition); + const serializableConfig = isCliDefinition(adapter) + ? harnessConfigFromCliDefinition(definition) + : cloneHarnessDefinition(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); + } +} + /** * 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 getHarnessDefinition(cli: string): HarnessDefinition | undefined { + const baseCli = lookupCliKey(cli); + if (!baseCli) return undefined; + const config = USER_HARNESS_CONFIGS.get(baseCli); + return config ? cloneHarnessDefinition(config) : 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; +} + export interface SessionInfo { broker_version: string; protocol_version: number; @@ -152,6 +158,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 ?? [], @@ -175,6 +182,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 ?? [], @@ -523,7 +531,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, @@ -534,7 +542,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)), }); @@ -546,7 +554,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( @@ -564,7 +572,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)), }); @@ -576,19 +584,19 @@ 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 }> { + ): Promise { return this.spawnProvider({ ...input, provider: 'claude' }); } async spawnOpencode( input: Omit - ): Promise<{ name: string; runtime: AgentRuntime }> { + ): Promise { return this.spawnProvider({ ...input, provider: 'opencode' }); } @@ -625,7 +633,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/index.ts b/packages/sdk/src/index.ts index 98bd78cfc..824e6a480 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -15,6 +15,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'; diff --git a/packages/sdk/src/lifecycle-hooks.ts b/packages/sdk/src/lifecycle-hooks.ts index ed06dee0b..1b21bb180 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 }; /** 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 edbace5b7..b948cca19 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,7 @@ export type BrokerEvent = provider?: HeadlessProvider; cli?: string; model?: string; + sessionId?: string; } | { kind: 'worker_error'; diff --git a/packages/sdk/src/relay-adapter.ts b/packages/sdk/src/relay-adapter.ts index 189fc7c70..ef16be692 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, diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index b32b64da2..ca476c60f 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -29,7 +29,12 @@ import path from 'node:path'; import { RelayCast } from '@relaycast/sdk'; -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 } from './lifecycle-hooks.js'; import { @@ -43,7 +48,8 @@ import { type ResolvedPersona, } from './personas.js'; import { AgentRelayProtocolError } from './transport.js'; -import type { SendMessageInput, SpawnPtyInput } from './types.js'; +import type { HarnessDefinition, SendMessageInput, SpawnPtyInput } from './types.js'; +import { getHarnessDefinition, registerHarnessAdapter, registerHarnessAdapters } from './cli-registry.js'; import type { AgentRuntime, BrokerEvent, @@ -120,6 +126,26 @@ 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, cloneHarnessDefinition(definition)]) + ); +} + function toWorkspaceRegistryEntry(value: unknown): WorkspaceRegistryEntry { if (!value || typeof value !== 'object') { return {}; @@ -208,6 +234,7 @@ export interface SpawnLifecycleContext { export interface SpawnLifecycleSuccessContext extends SpawnLifecycleContext { runtime: AgentRuntime; + sessionId?: string; } export interface SpawnLifecycleErrorContext extends SpawnLifecycleContext { @@ -243,6 +270,7 @@ export interface SpawnOptions extends SpawnLifecycleHooks { args?: string[]; channels?: string[]; model?: string; + harness?: HarnessDefinition; cwd?: string; team?: string; shadowOf?: string; @@ -301,6 +329,7 @@ type AgentOutputCallback = ((chunk: string) => void) | ((data: AgentOutputPayloa export interface Agent { readonly name: string; readonly runtime: AgentRuntime; + readonly sessionId?: string; readonly channels: string[]; /** Current lifecycle status of the agent. */ readonly status: AgentStatus; @@ -362,6 +391,7 @@ export interface SpawnerSpawnOptions extends SpawnLifecycleHooks { channels?: string[]; task?: string; model?: string; + harness?: HarnessDefinition; cwd?: string; idleThresholdSecs?: number; /** Optional pre-minted relaycast agent token (`at_live_`, from @@ -387,6 +417,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` / @@ -428,6 +460,7 @@ type OutputListener = { type InternalAgent = Agent & { _setChannels: (channels: string[]) => void; + _setSessionId: (sessionId: string) => void; }; interface AgentActivityState { @@ -507,6 +540,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; @@ -537,6 +571,8 @@ export class AgentRelay { this.defaultChannels = options.channels ?? ['general']; this.requestedWorkspaceId = requestedWorkspaceId; this.workspaceName = options.workspaceName; + this.harnesses = cloneHarnessMap(options.harnesses); + registerHarnessAdapters(this.harnesses); if (options.workspaceName && !options.workspaceId) { console.warn( '[AgentRelay] workspaceName without workspaceId is deprecated and will be removed in a future major version. ' + @@ -561,6 +597,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'); + } + this.harnesses[key] = cloneHarnessDefinition(definition); + registerHarnessAdapter(key, definition); + 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'); } @@ -708,7 +762,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; try { result = await client.spawnPty({ name: input.name, @@ -717,6 +771,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, @@ -738,7 +793,7 @@ export class AgentRelay { throw error; } this.resetAgentLifecycleState(result.name); - const agent = this.makeAgent(result.name, result.runtime, channels); + const agent = this.makeAgent(result.name, result.runtime, channels, result.sessionId); this.knownAgents.set(agent.name, agent); await this.invokeLifecycleHook( input.onSuccess, @@ -746,6 +801,7 @@ export class AgentRelay { ...lifecycleContext, name: result.name, runtime: result.runtime, + sessionId: result.sessionId, }, `spawnPty("${input.name}") onSuccess` ); @@ -760,6 +816,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, @@ -835,6 +892,7 @@ export class AgentRelay { ...(task !== undefined ? { task } : {}), channels: options.channels, model: spec.model, + harness: options.harness, cwd: spawnCwd, team: options.team, agentToken: options.agentToken, @@ -991,7 +1049,7 @@ export class AgentRelay { 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); + const agent = this.makeAgent(entry.name, entry.runtime, entry.channels, entry.sessionId); this.knownAgents.set(agent.name, agent); return agent; }); @@ -1268,12 +1326,20 @@ 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 + ): Agent { const existing = this.knownAgents.get(name); if (existing) { + if (sessionId) { + (existing as InternalAgent)._setSessionId(sessionId); + } return existing; } - const agent = this.makeAgent(name, runtime, channels); + const agent = this.makeAgent(name, runtime, channels, sessionId); this.knownAgents.set(name, agent); return agent; } @@ -1556,7 +1622,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); this.readyAgents.delete(event.name); this.messageReadyAgents.delete(event.name); this.exitedAgents.delete(event.name); @@ -1609,7 +1675,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); this.readyAgents.add(event.name); this.exitedAgents.delete(event.name); this.idleAgents.delete(event.name); @@ -1716,13 +1782,17 @@ export class AgentRelay { }); } - private makeAgent(name: string, runtime: AgentRuntime, channels: string[]): Agent { + private makeAgent(name: string, runtime: AgentRuntime, channels: string[], sessionId?: string): Agent { // eslint-disable-next-line @typescript-eslint/no-this-alias const relay = this; let agentChannels = [...channels]; + let agentSessionId = sessionId; const agent: InternalAgent = { name, runtime, + get sessionId() { + return agentSessionId; + }, get channels() { return [...agentChannels]; }, @@ -1924,6 +1994,9 @@ export class AgentRelay { _setChannels(nextChannels: string[]) { agentChannels = [...nextChannels]; }, + _setSessionId(nextSessionId: string) { + agentSessionId = nextSessionId; + }, }; return agent; } @@ -1944,6 +2017,7 @@ export class AgentRelay { channels, task, model: options?.model, + harness: this.resolveHarnessForSpawn(cli, options?.harness), cwd: options?.cwd, idleThresholdSecs: options?.idleThresholdSecs, agentToken: options?.agentToken, @@ -1962,7 +2036,7 @@ export class AgentRelay { task, }; await this.invokeLifecycleHook(options?.onStart, lifecycleContext, `spawn("${name}") onStart`); - let result: { name: string; runtime: AgentRuntime }; + let result: SpawnAgentResult; try { result = await client.spawnProvider({ name, @@ -1972,6 +2046,7 @@ export class AgentRelay { channels, task, model: options?.model, + harness: this.resolveHarnessForSpawn(cli, options?.harness), cwd: options?.cwd, idleThresholdSecs: options?.idleThresholdSecs, agentToken: options?.agentToken, @@ -1990,7 +2065,7 @@ export class AgentRelay { } this.resetAgentLifecycleState(result.name); - const agent = this.makeAgent(result.name, result.runtime, channels); + const agent = this.makeAgent(result.name, result.runtime, channels, result.sessionId); this.knownAgents.set(agent.name, agent); await this.invokeLifecycleHook( options?.onSuccess, @@ -1998,6 +2073,7 @@ export class AgentRelay { ...lifecycleContext, name: result.name, runtime: result.runtime, + sessionId: result.sessionId, }, `spawn("${name}") onSuccess` ); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 32f23bdb3..9bf596587 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 interface SpawnPtyInput { name: string; @@ -17,6 +20,7 @@ export interface SpawnPtyInput { channels?: string[]; task?: string; model?: string; + harness?: HarnessDefinition; cwd?: string; team?: string; shadowOf?: string; @@ -62,6 +66,7 @@ export interface SpawnProviderInput { channels?: string[]; task?: string; model?: string; + harness?: HarnessDefinition; cwd?: string; team?: string; shadowOf?: string; @@ -98,6 +103,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..63e45e7a3 100644 --- a/packages/sdk/src/workflows/README.md +++ b/packages/sdk/src/workflows/README.md @@ -234,6 +234,33 @@ 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: + +```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: 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..4553042bc --- /dev/null +++ b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; + +import { buildModelArgs, registerHarnessAdapter } from '../../cli-registry.js'; +import { WorkflowBuilder } from '../builder.js'; +import { buildCommand } from '../process-spawner.js'; +import { WorkflowRunner } from '../runner.js'; + +describe('workflow harness adapters', () => { + 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('registers harnesses declared in parsed YAML', () => { + 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(buildCommand('unit-harness-c', buildModelArgs('unit-harness-c', 'model-c'), 'do it')).toEqual([ + 'unit-c', + 'exec', + 'do it', + '--m', + 'model-c', + ]); + }); +}); 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..c6c2bf9f9 100644 --- a/packages/sdk/src/workflows/index.ts +++ b/packages/sdk/src/workflows/index.ts @@ -47,6 +47,14 @@ 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 HarnessAdapter, +} from '../cli-registry.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..d1f530d95 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 { @@ -48,7 +49,7 @@ export function createProcessBackendExecutor( ); } - const extraArgs = agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : []; + const extraArgs = buildModelArgs(agentDef.cli, agentDef.constraints?.model); const argv = buildCommand(agentDef.cli, extraArgs, resolvedTask); const commandString = commandToShell(argv); 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..61b0102d4 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 { buildModelArgs, getCliDefinition, registerHarnessAdapters } 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,7 @@ function resolveCursorCli(): 'cursor-agent' | 'agent' { return (resolved?.binary as 'cursor-agent' | 'agent') ?? 'agent'; } -function getWorkflowSdkSpawner(relay: AgentRelay, cli: AgentCli): AgentSpawner | null { +function getWorkflowSdkSpawner(relay: AgentRelay, cli: string): AgentSpawner | null { switch (cli) { case 'claude': return relay.claude; @@ -555,6 +557,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,6 +568,8 @@ 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; + registerHarnessAdapters(this.harnesses); this.envSecrets = options.envSecrets; if (!this.executor && this.processBackend) { this.executor = createProcessBackendExecutor(this.processBackend, { @@ -1746,6 +1751,15 @@ export class WorkflowRunner { } } + const harnessProxyProvider = getCliDefinition(agentDef.cli)?.proxyProvider; + if ( + harnessProxyProvider === 'openai' || + harnessProxyProvider === 'anthropic' || + harnessProxyProvider === 'openrouter' + ) { + return harnessProxyProvider; + } + switch (agentDef.cli) { case 'claude': return 'anthropic'; @@ -2072,9 +2086,15 @@ export class WorkflowRunner { this.validateConfig(parsed, source); const config = this.normalizeLegacyPermissionConfig(parsed as RelayYamlConfig); config.agents ??= []; + this.registerConfigHarnesses(config); return config; } + private registerConfigHarnesses(config?: RelayYamlConfig): void { + registerHarnessAdapters(this.harnesses); + registerHarnessAdapters(config?.harnesses); + } + private normalizeLegacyPermissionConfig(config: RelayYamlConfig): RelayYamlConfig { const legacyPermissions = ( config as RelayYamlConfig & { @@ -2182,6 +2202,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) { @@ -2220,6 +2287,7 @@ export class WorkflowRunner { let resolved: RelayYamlConfig; try { this.validateConfig(config); + this.registerConfigHarnesses(config); resolved = vars ? this.resolveVariables(config, vars) : config; resolved = this.applyPermissionProfiles(resolved); } catch (err) { @@ -2839,10 +2907,12 @@ export class WorkflowRunner { this.paused = false; const resolved = this.applyPermissionProfiles(vars ? this.resolveVariables(config, vars) : config); + this.registerConfigHarnesses(resolved); // Validate config (catches cycles, missing deps, invalid steps, etc.) this.validateConfig(resolved); const runtimeConfig = this.applyReliabilityDefaults(resolved); + this.registerConfigHarnesses(runtimeConfig); const permissionResult = this.validatePermissions( runtimeConfig.agents, @@ -3012,6 +3082,7 @@ export class WorkflowRunner { const resolvedConfig = this.applyReliabilityDefaults( vars ? this.resolveVariables(run.config, vars) : run.config ); + this.registerConfigHarnesses(resolvedConfig); // Resolve path definitions (same as execute()) so workdir lookups work on resume const pathResult = this.resolvePathDefinitions(resolvedConfig.paths, this.cwd); @@ -3146,6 +3217,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 +6396,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 +6413,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 +6467,7 @@ export class WorkflowRunner { timeoutMs?: number ): Promise { const agentName = `${step.name}-${this.generateShortId()}`; - const modelArgs = agentDef.constraints?.model ? ['--model', agentDef.constraints.model] : []; + const modelArgs = buildModelArgs(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. diff --git a/packages/sdk/src/workflows/schema.json b/packages/sdk/src/workflows/schema.json index c04325384..1f0b91987 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,24 @@ }, "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": { + "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..b5caa0a3e 100644 --- a/packages/workflow-types/src/index.ts +++ b/packages/workflow-types/src/index.ts @@ -85,6 +85,10 @@ export interface AgentDefinition { } export type AgentCli = + | KnownAgentCli + | (string & {}); + +export type KnownAgentCli = | 'claude' | 'codex' | 'gemini' @@ -97,6 +101,47 @@ 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 { + /** 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..132a0e7f1 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 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.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 573bc3c99..b9cfe2bd6 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 FakeSubscriptionManager { - clear = vi.fn(); + class FakeResourceTemplate { + constructor( + public readonly template: string, + public readonly options: unknown + ) {} + } - constructor() { - subscriptionInstances.push(this); + class FakeWsClient { + readonly handlers = new Map void>>(); + readonly connect = vi.fn(); + readonly disconnect = vi.fn(); + + 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', }); }); @@ -345,20 +382,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.', }); }); @@ -372,16 +407,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 () => { @@ -392,12 +430,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 () => { @@ -409,13 +442,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' }); }); }); @@ -440,12 +475,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 () => ({ @@ -456,12 +502,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', @@ -487,7 +531,6 @@ describe('resolvePatchedStdioBootstrapOptions', () => { agentType: 'agent', }); - expect(mocks.behavior.inboxImpl).not.toHaveBeenCalled(); expect(mocks.behavior.registerImpl).toHaveBeenCalledWith({ name: 'WorkerA', type: 'agent', @@ -509,10 +552,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 bee1d40a1..a7a647e1c 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 | null; + wsInitAttempted: boolean; } type RegistrationSession = Pick; @@ -76,7 +96,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; @@ -114,26 +134,31 @@ export function normalizeAgentType(value: string | undefined): AgentType | undef return undefined; } -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'); - } +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: null, + wsInitAttempted: false, + }; +} - return payload.data ?? {}; +async function createWorkspace(name: string, baseUrl?: string): Promise> { + return (await RelayCast.createWorkspace(name, { baseUrl })) as Record; } function requireWorkspaceKey(session: RegistrationSession): void { @@ -141,7 +166,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({ @@ -200,9 +252,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, + 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: () => RelayCastLike, + getRelay: () => RelayCast, + getAgentClient: (asIdentity?: string) => AgentClientLike, getSession: () => SessionState, setSession: SessionSetter, baseUrl: string | undefined, @@ -210,16 +578,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: { @@ -229,39 +595,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, @@ -269,7 +629,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_"'); } @@ -277,54 +637,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, @@ -338,10 +686,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, + }); } ); } @@ -353,19 +1185,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 }, @@ -374,98 +1195,105 @@ 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(); + if (switchingWorkspace || changingToken) { + notifySubscribers(); + session.wsBridge?.stop(); session.subscriptions?.clear(); 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, subscriptions, (uri) => { mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined); }); wsBridge.start(); - Object.assign(session, partial, { - wsBridge, - subscriptions, - wsInitAttempted: true, - }); + session.wsBridge = wsBridge; + session.subscriptions = subscriptions; + session.wsInitAttempted = true; } catch { - Object.assign(session, partial, { - wsBridge: null, - subscriptions: null, - wsInitAttempted: true, - }); + session.wsBridge = null; + session.subscriptions = 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, @@ -473,16 +1301,12 @@ 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); 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: [ @@ -529,10 +1353,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_'); } @@ -540,14 +1365,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; } @@ -555,17 +1373,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, @@ -588,6 +1399,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 = @@ -599,6 +1412,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/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/sst.config.ts b/web/sst.config.ts index 3a936a4fd..eaf710a0f 100644 --- a/web/sst.config.ts +++ b/web/sst.config.ts @@ -1,3 +1,5 @@ +const AWS_MANAGED_CACHING_DISABLED_CACHE_POLICY_ID = '4135ea2d-6df8-44a3-9df3-4b5a84be39ad'; + export default $config({ app(input) { return { @@ -6,8 +8,9 @@ export default $config({ removal: input?.stage === 'production' ? 'retain' : 'remove', }; }, - run() { + async run() { const isProd = $app.stage === 'production'; + const isPreview = $app.stage.startsWith('pr-'); const domain = isProd ? 'origin.agentrelay.net' : `${$app.stage}.agentrelay.net`; const NEXT_PUBLIC_POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://i.agentrelay.com'; const NEXT_PUBLIC_POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY ?? ''; @@ -21,6 +24,8 @@ export default $config({ }, // Production deploys land on origin.agentrelay.net; SEO canonicals are set in Next metadata. domain: { name: domain, dns: sst.cloudflare.dns({ proxy: true }) }, + // PR previews should not allocate one custom CloudFront cache policy per stage. + ...(isPreview ? { cachePolicy: AWS_MANAGED_CACHING_DISABLED_CACHE_POLICY_ID } : {}), }); }, }); From 7a265d5a9880642fd887796627ce75a24fa978ee Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sun, 24 May 2026 21:46:25 -0400 Subject: [PATCH 04/14] Make built-in harnesses config-backed adapters --- .trajectories/active/traj_4b2d63f6ljvh.json | 150 - .../completed/2026-05/traj_4b2d63f6ljvh.json | 381 ++ .../completed/2026-05/traj_4b2d63f6ljvh.md | 89 + .../2026-05/traj_4b2d63f6ljvh.trace.json | 5268 +++++++++++++++++ .trajectories/index.json | 7 +- CHANGELOG.md | 2 +- crates/broker/src/protocol.rs | 2 + crates/broker/src/worker.rs | 251 +- packages/sdk-py/src/agent_relay/types.py | 3 + .../sdk/src/__tests__/spawn-harness.test.ts | 72 +- packages/sdk/src/cli-registry.ts | 180 +- packages/sdk/src/relay.ts | 14 +- .../__tests__/harness-adapters.test.ts | 9 + packages/sdk/src/workflows/schema.json | 1 + packages/workflow-types/src/index.ts | 10 +- web/content/docs/harnesses.mdx | 95 +- 16 files changed, 6303 insertions(+), 231 deletions(-) delete mode 100644 .trajectories/active/traj_4b2d63f6ljvh.json create mode 100644 .trajectories/completed/2026-05/traj_4b2d63f6ljvh.json create mode 100644 .trajectories/completed/2026-05/traj_4b2d63f6ljvh.md create mode 100644 .trajectories/completed/2026-05/traj_4b2d63f6ljvh.trace.json diff --git a/.trajectories/active/traj_4b2d63f6ljvh.json b/.trajectories/active/traj_4b2d63f6ljvh.json deleted file mode 100644 index ac32a7ff4..000000000 --- a/.trajectories/active/traj_4b2d63f6ljvh.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "id": "traj_4b2d63f6ljvh", - "version": 1, - "task": { - "title": "Fix CI run 26263517444" - }, - "status": "active", - "startedAt": "2026-05-22T01:47:08.312Z", - "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", - "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" - } - ] - } - ], - "commits": [], - "filesChanged": [], - "projectId": "/Users/will/Projects/AgentWorkforce/relay", - "tags": [], - "_trace": { - "startRef": "898b8ee37197075913578fdb1c2fe4139b2ac562", - "endRef": "898b8ee37197075913578fdb1c2fe4139b2ac562" - } -} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json new file mode 100644 index 000000000..69ef3d624 --- /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": "/Users/will/Projects/AgentWorkforce/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/index.json b/.trajectories/index.json index f84c117d0..7952ed538 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-24T17:02:55.026Z", + "lastUpdated": "2026-05-25T01:45:42.089Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1229,9 +1229,10 @@ }, "traj_4b2d63f6ljvh": { "title": "Fix CI run 26263517444", - "status": "active", + "status": "completed", "startedAt": "2026-05-22T01:47:08.312Z", - "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/active/traj_4b2d63f6ljvh.json" + "completedAt": "2026-05-25T01:45:41.710Z", + "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json" } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8363249ea..dad2b7cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ 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 harness adapters so custom agent CLIs define their own binaries, interactive/non-interactive argument templates, model flags, and process behavior without Relay changes. +- `@agent-relay/sdk`: spawn calls and workflow configs can declare harness adapters 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. ### Changed diff --git a/crates/broker/src/protocol.rs b/crates/broker/src/protocol.rs index 3b0eda21c..81a7a8b79 100644 --- a/crates/broker/src/protocol.rs +++ b/crates/broker/src/protocol.rs @@ -22,6 +22,8 @@ pub enum HeadlessProvider { #[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")] diff --git a/crates/broker/src/worker.rs b/crates/broker/src/worker.rs index be049cbf9..2d13123e9 100644 --- a/crates/broker/src/worker.rs +++ b/crates/broker/src/worker.rs @@ -234,11 +234,12 @@ impl WorkerRegistry { match spec.runtime { AgentRuntime::Pty => { let cli = spec.cli.as_deref().context("pty runtime requires `cli`")?; - let harness = spec.harness.clone(); let (parsed_cli, inline_cli_args) = parse_cli_command(cli) .with_context(|| format!("invalid CLI command '{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 normalized_cli = normalize_cli_name(&resolved_cli); + let adapter_cli = harness_adapter_key(&resolved_cli, harness.as_ref()); let mut effective_args = inline_cli_args; effective_args.extend(spec.args.clone()); @@ -249,7 +250,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"; @@ -343,7 +344,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(".")), @@ -954,6 +955,213 @@ fn fallback_path_env() -> OsString { } } +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 { + HarnessDefinition { + adapter: override_definition.adapter.or(base.adapter), + binary: override_definition.binary.or(base.binary), + binaries: if override_definition.binaries.is_empty() { + 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('.') { return canonicalize_display(Path::new(command)); @@ -1539,6 +1747,41 @@ fn spawn_worker_reader( 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 harness_interactive_args_expand_vector_placeholders() { let harness = HarnessDefinition { diff --git a/packages/sdk-py/src/agent_relay/types.py b/packages/sdk-py/src/agent_relay/types.py index 697ebb4d4..a1f63d708 100644 --- a/packages/sdk-py/src/agent_relay/types.py +++ b/packages/sdk-py/src/agent_relay/types.py @@ -198,6 +198,7 @@ def to_dict(self) -> dict[str, Any]: 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 @@ -212,6 +213,8 @@ class HarnessDefinition: 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: diff --git a/packages/sdk/src/__tests__/spawn-harness.test.ts b/packages/sdk/src/__tests__/spawn-harness.test.ts index eac9d9cc3..01efdda8d 100644 --- a/packages/sdk/src/__tests__/spawn-harness.test.ts +++ b/packages/sdk/src/__tests__/spawn-harness.test.ts @@ -1,10 +1,20 @@ import { describe, expect, it, vi } from 'vitest'; import { AgentRelayClient } from '../client.js'; -import { registerHarnessAdapter } from '../cli-registry.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 @@ -76,6 +86,66 @@ describe('spawn harness adapters', () => { ); }); + 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'], diff --git a/packages/sdk/src/cli-registry.ts b/packages/sdk/src/cli-registry.ts index 34ea64190..6dc4c5a77 100644 --- a/packages/sdk/src/cli-registry.ts +++ b/packages/sdk/src/cli-registry.ts @@ -57,61 +57,79 @@ export const COMMON_SEARCH_PATHS = [ const DEFAULT_NON_INTERACTIVE_TEMPLATE = ['{task}', '{args}'] as const; const DEFAULT_MODEL_ARGS_TEMPLATE = ['--model', '{model}'] as const; -const CLI_REGISTRY: Record = { +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], @@ -146,6 +164,7 @@ function validateStringArray(value: readonly string[] | undefined, label: string 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] } : {}), @@ -156,6 +175,63 @@ function cloneHarnessDefinition(config: HarnessDefinition): HarnessDefinition { }; } +function mergeHarnessDefinitions(base: HarnessDefinition, override: HarnessDefinition): HarnessDefinition { + return { + ...cloneHarnessDefinition(base), + ...cloneHarnessDefinition(override), + adapter: override.adapter ?? base.adapter, + binary: override.binary ?? base.binary, + ...(override.binaries + ? { binaries: [...override.binaries] } + : 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], @@ -167,26 +243,44 @@ function harnessConfigFromCliDefinition(definition: CliDefinition): HarnessDefin }; } +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[]; model?: 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[]; model?: string } + context: { task: string; extraArgs?: string[]; bypass?: string; model?: string } ): string[] { const args: string[] = []; for (const entry of template) { @@ -194,6 +288,7 @@ function renderArgTemplate( ...expandTemplateArg(entry, { task: context.task, extraArgs: context.extraArgs ?? [], + bypass: context.bypass, model: context.model, }) ); @@ -209,17 +304,22 @@ function adapterFromConfig(name: string, config: HarnessDefinition): CliDefiniti 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]; + 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 }), + renderArgTemplate(nonInteractiveTemplate, { + task, + extraArgs, + bypass: resolveTemplateBypass(config, extraArgs), + }), modelArgs: (model) => renderArgTemplate(modelTemplate, { task: '', model }), bypassFlag: config.bypassFlag, bypassAliases: validateStringArray(config.bypassAliases, `harness "${name}".bypassAliases`), @@ -244,7 +344,7 @@ export function defineHarnessAdapter(name: string, adapter: HarnessAdapter): Cli searchPaths: adapter.searchPaths ? [...adapter.searchPaths] : undefined, }; } - return adapterFromConfig(name, adapter as HarnessDefinition); + return adapterFromConfig(name, resolveHarnessConfig(name, adapter as HarnessDefinition)); } /** @@ -260,7 +360,7 @@ export function registerHarnessAdapter(name: string, adapter: HarnessAdapter): v USER_CLI_REGISTRY.set(key, definition); const serializableConfig = isCliDefinition(adapter) ? harnessConfigFromCliDefinition(definition) - : cloneHarnessDefinition(adapter); + : resolveHarnessConfig(key, adapter); USER_HARNESS_CONFIGS.set(key, serializableConfig); const aliases = 'aliases' in adapter ? adapter.aliases : undefined; @@ -295,11 +395,17 @@ export function getCliDefinition(cli: string): CliDefinition | undefined { 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); - return config ? cloneHarnessDefinition(config) : undefined; + 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[] { diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index bc577cd8f..860c841c6 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -51,7 +51,12 @@ import { } from './personas.js'; import { AgentRelayProtocolError } from './transport.js'; import type { HarnessDefinition, JsonSchema, SendMessageInput, SpawnPtyInput } from './types.js'; -import { getHarnessDefinition, registerHarnessAdapter, registerHarnessAdapters } from './cli-registry.js'; +import { + defineHarnessDefinition, + getHarnessDefinition, + registerHarnessAdapter, + registerHarnessAdapters, +} from './cli-registry.js'; import type { AgentRuntime, BrokerEvent, @@ -146,7 +151,7 @@ function cloneHarnessMap( ): Record { if (!harnesses) return {}; return Object.fromEntries( - Object.entries(harnesses).map(([name, definition]) => [name, cloneHarnessDefinition(definition)]) + Object.entries(harnesses).map(([name, definition]) => [name, defineHarnessDefinition(name, definition)]) ); } @@ -674,8 +679,9 @@ export class AgentRelay { if (!key) { throw new Error('registerHarness() expects a non-empty harness name'); } - this.harnesses[key] = cloneHarnessDefinition(definition); - registerHarnessAdapter(key, definition); + const resolved = defineHarnessDefinition(key, definition); + this.harnesses[key] = cloneHarnessDefinition(resolved); + registerHarnessAdapter(key, resolved); return this; } diff --git a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts index 4553042bc..712c430f9 100644 --- a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts +++ b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts @@ -6,6 +6,15 @@ import { buildCommand } from '../process-spawner.js'; import { WorkflowRunner } from '../runner.js'; describe('workflow harness adapters', () => { + 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('lets SDK callers register a harness command adapter', () => { registerHarnessAdapter('unit-harness-a', { binaries: ['unit-agent'], diff --git a/packages/sdk/src/workflows/schema.json b/packages/sdk/src/workflows/schema.json index 1f0b91987..e56f7988e 100644 --- a/packages/sdk/src/workflows/schema.json +++ b/packages/sdk/src/workflows/schema.json @@ -258,6 +258,7 @@ "type": "object", "additionalProperties": false, "properties": { + "adapter": { "type": "string" }, "binary": { "type": "string" }, "binaries": { "type": "array", "items": { "type": "string" } }, "interactiveArgs": { "type": "array", "items": { "type": "string" } }, diff --git a/packages/workflow-types/src/index.ts b/packages/workflow-types/src/index.ts index b5caa0a3e..77b11ae68 100644 --- a/packages/workflow-types/src/index.ts +++ b/packages/workflow-types/src/index.ts @@ -84,9 +84,7 @@ export interface AgentDefinition { skills?: string; } -export type AgentCli = - | KnownAgentCli - | (string & {}); +export type AgentCli = KnownAgentCli | (string & {}); export type KnownAgentCli = | 'claude' @@ -112,6 +110,12 @@ export type KnownAgentCli = * 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. */ diff --git a/web/content/docs/harnesses.mdx b/web/content/docs/harnesses.mdx index 7bd2f7f87..5e0d3410d 100644 --- a/web/content/docs/harnesses.mdx +++ b/web/content/docs/harnesses.mdx @@ -1,17 +1,55 @@ --- title: Harnesses -description: Define custom agent CLI adapters for Agent Relay spawning and workflows. +description: Define agent CLI adapters for Agent Relay spawning and workflows. --- -A harness tells Relay how to start an agent CLI: which binary to execute, how to render model flags, which Relay MCP arguments to inject, and which extra paths to search when the command is not on `PATH`. +A harness is Relay's adapter for an agent CLI. It tells the 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 includes built-in harnesses for the common CLIs. Define a custom harness when you want to spawn a CLI that Relay does not ship yet, or when your local install needs different binary names, flags, or search paths. +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 to the broker with the spawn request. +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. -## TypeScript spawn +## 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(); +``` + +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); +``` + +```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: @@ -57,26 +95,26 @@ const agent = await relay.spawn('LocalAgent', 'local-agent', 'Inspect the repo.' }); ``` -For direct broker control, pass the same shape to the low-level client: +## Extend a built-in lifecycle -```typescript TypeScript file="client-spawn.ts" -import { AgentRelayClient } from '@agent-relay/sdk'; +If your wrapper is compatible with a built-in harness, set `adapter` to keep that lifecycle behavior while changing the executable or flags: -const client = AgentRelayClient.connect({ cwd: '/repo' }); - -await client.spawnPty({ - name: 'CustomWorker', - cli: 'custom-agent', - model: 'fast', - task: 'Summarize the worktree state.', - harness: { - binaries: ['custom-agent', 'custom'], - modelArgs: ['--model', '{model}'], - searchPaths: ['~/.custom/bin'], +```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.'); ``` +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. + ## Python spawn Python accepts the same serializable harness definition. Use snake_case fields on the dataclass; the SDK serializes them to the broker format. @@ -112,16 +150,16 @@ reviewer = await relay.spawn( await reviewer.wait_for_ready() ``` -Use `register_harness()` when you want to add an adapter after constructing the relay: +Use `adapter` when a Python-defined harness should use a built-in lifecycle: -```python Python file="register_harness.py" +```python Python relay.register_harness( - "local-agent", - { - "binary": "local-agent", - "interactiveArgs": ["serve", "{modelArgs}", "{mcpArgs}", "{args}"], - "modelArgs": ["--model-id", "{model}"], - }, + "company-codex", + HarnessDefinition( + adapter="codex", + binary="company-codex", + search_paths=["~/company/bin"], + ), ) ``` @@ -161,6 +199,7 @@ workflows: | 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}']`. | @@ -183,7 +222,7 @@ Use placeholders as whole argv items when they expand to multiple arguments: | `{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` | The configured `bypassFlag`, omitted when not applicable. | +| `{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. | From eb28b3a1d65e97248378a79be459ca62f792433a Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 07:44:21 -0400 Subject: [PATCH 05/14] Address harness adapter review comments --- .../completed/2026-05/traj_4b2d63f6ljvh.json | 2 +- .../completed/2026-05/traj_dqgg2q4scsvt.json | 4 +- .../completed/2026-05/traj_ft1pwdlcrmcn.json | 14 +-- .../completed/2026-05/traj_pjadgfw0mtw4.json | 8 +- .../completed/2026-05/traj_q97ei72svf2f.json | 53 +++++++++++ .../completed/2026-05/traj_q97ei72svf2f.md | 33 +++++++ .../completed/2026-05/traj_r3eic6rt84pq.json | 6 +- .../completed/2026-05/traj_r3eic6rt84pq.md | 2 +- .trajectories/index.json | 19 ++-- crates/broker/src/codex_session.rs | 3 +- crates/broker/src/listen_api.rs | 30 +++---- crates/broker/src/protocol.rs | 2 + crates/broker/src/worker.rs | 88 +++++++++++++++++-- packages/openclaw/src/setup.ts | 2 + packages/sdk-py/src/agent_relay/client.py | 33 ++++--- packages/sdk-py/src/agent_relay/relay.py | 3 + .../Sources/AgentRelaySDK/RelayTypes.swift | 4 +- .../sdk/src/__tests__/spawn-harness.test.ts | 12 +++ packages/sdk/src/cli-registry.ts | 55 ++++++++++-- packages/sdk/src/relay.ts | 16 ++-- .../__tests__/harness-adapters.test.ts | 47 ++++++++-- packages/sdk/src/workflows/runner.ts | 50 +++++++---- scripts/watch-cli-tools.sh | 2 +- src/cli/bootstrap.test.ts | 2 + src/cli/relaycast-mcp.ts | 17 ++-- 25 files changed, 389 insertions(+), 118 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_q97ei72svf2f.json create mode 100644 .trajectories/completed/2026-05/traj_q97ei72svf2f.md diff --git a/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json index 69ef3d624..8739a0d82 100644 --- a/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json +++ b/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json @@ -371,7 +371,7 @@ "web/lib/docs-nav.ts", "web/sst.config.ts" ], - "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "projectId": "relay", "tags": [], "_trace": { "startRef": "898b8ee37197075913578fdb1c2fe4139b2ac562", diff --git a/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json index 38a91cd6c..440ecb93b 100644 --- a/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json +++ b/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json @@ -44,10 +44,10 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "projectId": "relay", "tags": [], "_trace": { "startRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b", "endRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b" } -} \ No newline at end of file +} diff --git a/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json index ef67b9074..2ca107d52 100644 --- a/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json +++ b/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json @@ -42,19 +42,13 @@ "approach": "Standard approach", "confidence": 0.82 }, - "commits": [ - "898b8ee3", - "adb6d6b9", - "e5554b50" - ], - "filesChanged": [ - ".github/scripts/repair-failed-sst-acm-cert.sh" - ], - "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "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" } -} \ No newline at end of file +} diff --git a/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json index 67930703f..c2b790aef 100644 --- a/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json +++ b/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json @@ -42,20 +42,18 @@ "approach": "Standard approach", "confidence": 0.78 }, - "commits": [ - "c13ee318" - ], + "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": "/Users/will/Projects/AgentWorkforce/relay", + "projectId": "relay", "tags": [], "_trace": { "startRef": "23e07f08a24c2913a918aee2a0c9af9c54e4d40d", "endRef": "c13ee3189b10c83fc52c4d017e268eedb5119f48", "traceId": "7daa40cf-b98c-4790-b1d4-06c1fc4607ff" } -} \ No newline at end of file +} 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_r3eic6rt84pq.json b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json index 4b97af738..5387d1831 100644 --- a/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json +++ b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json @@ -10,16 +10,16 @@ "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.", + "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": "/Users/will/Projects/AgentWorkforce/relay", + "projectId": "relay", "tags": [], "_trace": { "startRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b", "endRef": "866d36df85d6f84ccfb1d84e116d4ca51e2a4a0b" } -} \ No newline at end of file +} diff --git a/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md index adc6c2859..017d74d03 100644 --- a/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md +++ b/.trajectories/completed/2026-05/traj_r3eic6rt84pq.md @@ -9,6 +9,6 @@ ## 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. +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/index.json b/.trajectories/index.json index 7952ed538..4dffd9f0d 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-25T01:45:42.089Z", + "lastUpdated": "2026-05-25T11:42:29.659Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1204,35 +1204,42 @@ "status": "completed", "startedAt": "2026-05-21T20:27:44.911Z", "completedAt": "2026-05-21T20:31:49.784Z", - "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json" + "path": ".trajectories/completed/2026-05/traj_pjadgfw0mtw4.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json" + "path": ".trajectories/completed/2026-05/traj_dqgg2q4scsvt.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json" + "path": ".trajectories/completed/2026-05/traj_r3eic6rt84pq.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json" + "path": ".trajectories/completed/2026-05/traj_ft1pwdlcrmcn.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json" + "path": ".trajectories/completed/2026-05/traj_4b2d63f6ljvh.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" } } } diff --git a/crates/broker/src/codex_session.rs b/crates/broker/src/codex_session.rs index 91e5cd6ee..3dda6b039 100644 --- a/crates/broker/src/codex_session.rs +++ b/crates/broker/src/codex_session.rs @@ -39,7 +39,8 @@ async fn create_resumable_codex_thread_inner( .current_dir(&thread_cwd) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); + .stderr(Stdio::piped()) + .kill_on_drop(true); for (key, value) in env { command.env(key, value); } diff --git a/crates/broker/src/listen_api.rs b/crates/broker/src/listen_api.rs index f210ecc45..fcdd3cb48 100644 --- a/crates/broker/src/listen_api.rs +++ b/crates/broker/src/listen_api.rs @@ -649,22 +649,20 @@ async fn listen_api_spawn( .or_else(|| body.get("restartPolicy")) .cloned(), ); - let harness = match body - .get("harness") - .cloned() - .map(serde_json::from_value::) - .transpose() - { - Ok(value) => value, - Err(err) => { - return ( - axum::http::StatusCode::BAD_REQUEST, - axum::Json(json!({ - "success": false, - "error": format!("Invalid field: harness ({err})") - })), - ); - } + 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") diff --git a/crates/broker/src/protocol.rs b/crates/broker/src/protocol.rs index 81a7a8b79..1e8294dc0 100644 --- a/crates/broker/src/protocol.rs +++ b/crates/broker/src/protocol.rs @@ -137,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, @@ -365,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, diff --git a/crates/broker/src/worker.rs b/crates/broker/src/worker.rs index 2d13123e9..5f15977d0 100644 --- a/crates/broker/src/worker.rs +++ b/crates/broker/src/worker.rs @@ -1,7 +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}, @@ -1076,11 +1079,16 @@ fn merge_harness_definitions( base: HarnessDefinition, override_definition: HarnessDefinition, ) -> HarnessDefinition { + let overrides_binary = override_definition.binary.is_some(); HarnessDefinition { adapter: override_definition.adapter.or(base.adapter), binary: override_definition.binary.or(base.binary), binaries: if override_definition.binaries.is_empty() { - base.binaries + if overrides_binary { + Vec::new() + } else { + base.binaries + } } else { override_definition.binaries }, @@ -1164,12 +1172,16 @@ fn harness_adapter_key(resolved_cli: &str, harness: Option<&HarnessDefinition>) fn resolve_command_with_paths(command: &str, search_paths: &[String]) -> String { if command.contains('/') || command.contains('\\') || command.starts_with('.') { - return canonicalize_display(Path::new(command)); + 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 candidate.is_file() { + if is_executable_file(&candidate) { return canonicalize_display(&candidate); } } @@ -1179,7 +1191,7 @@ fn resolve_command_with_paths(command: &str, search_paths: &[String]) -> String .unwrap_or_else(fallback_path_env); for dir in env::split_paths(&path_env) { let candidate = dir.join(command); - if candidate.is_file() { + if is_executable_file(&candidate) { return canonicalize_display(&candidate); } } @@ -1204,7 +1216,7 @@ fn resolve_harness_command(default_command: &str, harness: Option<&HarnessDefini for candidate in candidates.iter().copied() { let resolved = resolve_command_with_paths(candidate, &harness.search_paths); - if resolved != candidate || Path::new(&resolved).is_file() { + if resolved != candidate || is_executable_file(Path::new(&resolved)) { return resolved; } } @@ -1216,6 +1228,23 @@ fn resolve_harness_command(default_command: &str, harness: Option<&HarnessDefini .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 @@ -1417,14 +1446,13 @@ async fn resolve_harness_model_args( let model = if normalized_cli.eq_ignore_ascii_case("codex") { codex_local_fallback_model(resolved_cli, requested) .await - .map(|fallback| { + .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" ); - fallback }) .unwrap_or(requested) } else { @@ -1782,6 +1810,52 @@ mod harness_adapter_tests { ); } + #[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()); + } + + #[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 { diff --git a/packages/openclaw/src/setup.ts b/packages/openclaw/src/setup.ts index b57fe9181..ee1962401 100644 --- a/packages/openclaw/src/setup.ts +++ b/packages/openclaw/src/setup.ts @@ -397,6 +397,7 @@ export async function setup(options: SetupOptions): Promise { ...mcp.prefix, 'config', 'add', 'relaycast', '--command', 'npx', + '--arg', '-y', '--arg', 'agent-relay', '--arg', 'mcp', ...envArgs, @@ -445,6 +446,7 @@ export async function setup(options: SetupOptions): Promise { ...mcp.prefix, 'config', 'add', 'relaycast', '--command', 'npx', + '--arg', '-y', '--arg', 'agent-relay', '--arg', 'mcp', ...envArgs, diff --git a/packages/sdk-py/src/agent_relay/client.py b/packages/sdk-py/src/agent_relay/client.py index bdf81a780..a79721966 100644 --- a/packages/sdk-py/src/agent_relay/client.py +++ b/packages/sdk-py/src/agent_relay/client.py @@ -347,17 +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 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 + 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( diff --git a/packages/sdk-py/src/agent_relay/relay.py b/packages/sdk-py/src/agent_relay/relay.py index 2dec37b24..c9ed9a9b1 100644 --- a/packages/sdk-py/src/agent_relay/relay.py +++ b/packages/sdk-py/src/agent_relay/relay.py @@ -646,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( diff --git a/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift b/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift index 5c83c9414..a788bdef2 100644 --- a/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift +++ b/packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift @@ -386,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 sessionId: 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? } @@ -396,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 var sessionId: 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/src/__tests__/spawn-harness.test.ts b/packages/sdk/src/__tests__/spawn-harness.test.ts index 01efdda8d..775829e24 100644 --- a/packages/sdk/src/__tests__/spawn-harness.test.ts +++ b/packages/sdk/src/__tests__/spawn-harness.test.ts @@ -86,6 +86,18 @@ describe('spawn harness adapters', () => { ); }); + 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, diff --git a/packages/sdk/src/cli-registry.ts b/packages/sdk/src/cli-registry.ts index 6dc4c5a77..752fec9c5 100644 --- a/packages/sdk/src/cli-registry.ts +++ b/packages/sdk/src/cli-registry.ts @@ -139,18 +139,23 @@ const CLI_REGISTRY: Record = { const USER_CLI_REGISTRY = new Map(); const USER_HARNESS_CONFIGS = new Map(); -function normalizeCliKey(cli: string): string { +function normalizedBaseCliKey(cli: string): string | undefined { const trimmed = cli.trim(); - if (!trimmed) { + 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 trimmed.includes(':') ? trimmed.split(':')[0] : trimmed; + return base; } function lookupCliKey(cli: string): string | undefined { - const trimmed = cli.trim(); - if (!trimmed) return undefined; - return trimmed.includes(':') ? trimmed.split(':')[0] : trimmed; + return normalizedBaseCliKey(cli); } function validateStringArray(value: readonly string[] | undefined, label: string): string[] | undefined { @@ -183,7 +188,7 @@ function mergeHarnessDefinitions(base: HarnessDefinition, override: HarnessDefin binary: override.binary ?? base.binary, ...(override.binaries ? { binaries: [...override.binaries] } - : base.binaries + : base.binaries && override.binary === undefined ? { binaries: [...base.binaries] } : {}), ...(override.interactiveArgs @@ -383,6 +388,42 @@ export function registerHarnessAdapters(adapters: Record } } +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. diff --git a/packages/sdk/src/relay.ts b/packages/sdk/src/relay.ts index 860c841c6..bb6bc0e1a 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -51,12 +51,7 @@ import { } from './personas.js'; import { AgentRelayProtocolError } from './transport.js'; import type { HarnessDefinition, JsonSchema, SendMessageInput, SpawnPtyInput } from './types.js'; -import { - defineHarnessDefinition, - getHarnessDefinition, - registerHarnessAdapter, - registerHarnessAdapters, -} from './cli-registry.js'; +import { defineHarnessDefinition, getHarnessDefinition } from './cli-registry.js'; import type { AgentRuntime, BrokerEvent, @@ -649,7 +644,6 @@ export class AgentRelay { this.requestedWorkspaceId = requestedWorkspaceId; this.workspaceName = options.workspaceName; this.harnesses = cloneHarnessMap(options.harnesses); - registerHarnessAdapters(this.harnesses); if (options.workspaceName && !options.workspaceId) { console.warn( '[AgentRelay] workspaceName without workspaceId is deprecated and will be removed in a future major version. ' + @@ -681,7 +675,6 @@ export class AgentRelay { } const resolved = defineHarnessDefinition(key, definition); this.harnesses[key] = cloneHarnessDefinition(resolved); - registerHarnessAdapter(key, resolved); return this; } @@ -1160,7 +1153,12 @@ export class AgentRelay { const list = await client.listAgents(); return list.map((entry) => { const existing = this.knownAgents.get(entry.name); - if (existing) return existing; + if (existing) { + if (entry.sessionId) { + (existing as InternalAgent)._setSessionId(entry.sessionId); + } + return existing; + } const agent = this.makeAgent(entry.name, entry.runtime, entry.channels, entry.sessionId); this.knownAgents.set(agent.name, agent); return agent; diff --git a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts index 712c430f9..0fd3ab519 100644 --- a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts +++ b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts @@ -1,11 +1,22 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; -import { buildModelArgs, registerHarnessAdapter } from '../../cli-registry.js'; +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'; +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', @@ -52,7 +63,7 @@ describe('workflow harness adapters', () => { }); }); - it('registers harnesses declared in parsed YAML', () => { + it('keeps harnesses declared in parsed YAML scoped to workflow execution', () => { const runner = new WorkflowRunner({ cwd: process.cwd() }); runner.parseYamlString(` version: "1.0" @@ -76,12 +87,32 @@ workflows: task: do it `); - expect(buildCommand('unit-harness-c', buildModelArgs('unit-harness-c', 'model-c'), 'do it')).toEqual([ - 'unit-c', + 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('lets binary override inherited adapter binaries', () => { + registerHarnessAdapter('company-codex-wrapper', { + adapter: 'codex', + binary: 'company-codex', + searchPaths: ['~/company/bin'], + }); + + expect(buildCommand('company-codex-wrapper', [], 'do the work')).toEqual([ + 'company-codex', 'exec', - 'do it', - '--m', - 'model-c', + '--dangerously-bypass-approvals-and-sandbox', + '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/runner.ts b/packages/sdk/src/workflows/runner.ts index 61b0102d4..f7371d354 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -28,7 +28,14 @@ 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 { buildModelArgs, getCliDefinition, registerHarnessAdapters } from '../cli-registry.js'; +import { + buildModelArgs, + getCliDefinition, + registerHarnessAdapters, + restoreHarnessAdapters, + snapshotHarnessAdapters, + type HarnessRegistrySnapshot, +} from '../cli-registry.js'; import { resolveCliSync } from '../cli-resolver.js'; import { buildNormalizedProxyEnv, @@ -569,7 +576,6 @@ export class WorkflowRunner { this.executor = options.executor; this.processBackend = options.processBackend; this.harnesses = options.harnesses; - registerHarnessAdapters(this.harnesses); this.envSecrets = options.envSecrets; if (!this.executor && this.processBackend) { this.executor = createProcessBackendExecutor(this.processBackend, { @@ -1744,13 +1750,6 @@ export class WorkflowRunner { return 'openai'; } - if (configuredProviders.length === 1) { - const [onlyProvider] = configuredProviders; - if (onlyProvider === 'openai' || onlyProvider === 'anthropic' || onlyProvider === 'openrouter') { - return onlyProvider; - } - } - const harnessProxyProvider = getCliDefinition(agentDef.cli)?.proxyProvider; if ( harnessProxyProvider === 'openai' || @@ -1760,6 +1759,13 @@ export class WorkflowRunner { return harnessProxyProvider; } + if (configuredProviders.length === 1) { + const [onlyProvider] = configuredProviders; + if (onlyProvider === 'openai' || onlyProvider === 'anthropic' || onlyProvider === 'openrouter') { + return onlyProvider; + } + } + switch (agentDef.cli) { case 'claude': return 'anthropic'; @@ -2086,13 +2092,23 @@ export class WorkflowRunner { this.validateConfig(parsed, source); const config = this.normalizeLegacyPermissionConfig(parsed as RelayYamlConfig); config.agents ??= []; - this.registerConfigHarnesses(config); return config; } - private registerConfigHarnesses(config?: RelayYamlConfig): void { - registerHarnessAdapters(this.harnesses); - registerHarnessAdapters(config?.harnesses); + private installConfigHarnesses(config?: RelayYamlConfig): HarnessRegistrySnapshot { + const snapshot = snapshotHarnessAdapters(); + try { + registerHarnessAdapters(this.harnesses); + registerHarnessAdapters(config?.harnesses); + return snapshot; + } catch (err) { + restoreHarnessAdapters(snapshot); + throw err; + } + } + + private restoreConfigHarnesses(snapshot: HarnessRegistrySnapshot): void { + restoreHarnessAdapters(snapshot); } private normalizeLegacyPermissionConfig(config: RelayYamlConfig): RelayYamlConfig { @@ -2287,7 +2303,6 @@ export class WorkflowRunner { let resolved: RelayYamlConfig; try { this.validateConfig(config); - this.registerConfigHarnesses(config); resolved = vars ? this.resolveVariables(config, vars) : config; resolved = this.applyPermissionProfiles(resolved); } catch (err) { @@ -2907,12 +2922,11 @@ export class WorkflowRunner { this.paused = false; const resolved = this.applyPermissionProfiles(vars ? this.resolveVariables(config, vars) : config); - this.registerConfigHarnesses(resolved); // Validate config (catches cycles, missing deps, invalid steps, etc.) this.validateConfig(resolved); const runtimeConfig = this.applyReliabilityDefaults(resolved); - this.registerConfigHarnesses(runtimeConfig); + this.validateConfig(runtimeConfig); const permissionResult = this.validatePermissions( runtimeConfig.agents, @@ -3082,7 +3096,7 @@ export class WorkflowRunner { const resolvedConfig = this.applyReliabilityDefaults( vars ? this.resolveVariables(run.config, vars) : run.config ); - this.registerConfigHarnesses(resolvedConfig); + this.validateConfig(resolvedConfig); // Resolve path definitions (same as execute()) so workdir lookups work on resume const pathResult = this.resolvePathDefinitions(resolvedConfig.paths, this.cwd); @@ -3151,6 +3165,7 @@ export class WorkflowRunner { // Initialize trajectory recording this.trajectory = new WorkflowTrajectory(config.trajectories, runId, this.cwd); + const harnessSnapshot = this.installConfigHarnesses(config); try { await this.updateRunStatus(runId, 'running'); @@ -3532,6 +3547,7 @@ export class WorkflowRunner { }); } } finally { + this.restoreConfigHarnesses(harnessSnapshot); this.lastFailedStepOutput.clear(); this.lastCustomVerificationFailure.clear(); for (const stream of this.ptyLogStreams.values()) stream.end(); diff --git a/scripts/watch-cli-tools.sh b/scripts/watch-cli-tools.sh index 132a0e7f1..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 dist/src/cli/relaycast-mcp.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/relaycast-mcp.ts b/src/cli/relaycast-mcp.ts index 1c7307033..18b0e31b6 100644 --- a/src/cli/relaycast-mcp.ts +++ b/src/cli/relaycast-mcp.ts @@ -76,7 +76,7 @@ interface SessionState { agentName: string | null; agents: Map; wsBridge: RealtimeResourceBridge | null; - subscriptions: SubscriptionManager | null; + subscriptions: SubscriptionManager; wsInitAttempted: boolean; } @@ -158,7 +158,7 @@ function createInitialSession(options: { agentName, agents, wsBridge: null, - subscriptions: null, + subscriptions: new SubscriptionManager(), wsInitAttempted: false, }; } @@ -1322,7 +1322,7 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M }; const notifySubscribers = () => { - const uris = session.subscriptions?.getAll() ?? []; + const uris = session.subscriptions.getAll(); for (const uri of uris) { mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined); } @@ -1336,9 +1336,7 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M if (switchingWorkspace || changingToken) { notifySubscribers(); session.wsBridge?.stop(); - session.subscriptions?.clear(); session.wsBridge = null; - session.subscriptions = null; session.wsInitAttempted = false; } @@ -1346,21 +1344,18 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M if (session.agentToken && !session.wsBridge && !session.wsInitAttempted) { try { - const subscriptions = new SubscriptionManager(); const wsClient = new WsClient({ token: session.agentToken, baseUrl: options.baseUrl, }); - const wsBridge = new RealtimeResourceBridge(wsClient, subscriptions, (uri) => { + const wsBridge = new RealtimeResourceBridge(wsClient, session.subscriptions, (uri) => { mcpServer.server.sendResourceUpdated({ uri }).catch(() => undefined); }); wsBridge.start(); session.wsBridge = wsBridge; - session.subscriptions = subscriptions; session.wsInitAttempted = true; } catch { session.wsBridge = null; - session.subscriptions = null; session.wsInitAttempted = true; } } @@ -1393,11 +1388,11 @@ export function createPatchedRelayMcpServer(options: PatchedMcpServerOptions): M enableInboxPiggyback(mcpServer, getSession, getAgentClient); registerResourceDefinitions(mcpServer, getAgentClient, getRelay); mcpServer.server.setRequestHandler(SubscribeRequestSchema, async (req) => { - session.subscriptions?.subscribe(req.params.uri); + session.subscriptions.subscribe(req.params.uri); return {}; }); mcpServer.server.setRequestHandler(UnsubscribeRequestSchema, async (req) => { - session.subscriptions?.unsubscribe(req.params.uri); + session.subscriptions.unsubscribe(req.params.uri); return {}; }); registerAgentRelayTools( From f01afb7103d718b54e5a92390f527f52710de002 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 07:59:50 -0400 Subject: [PATCH 06/14] Guard workflow harness registry install --- packages/sdk/src/workflows/runner.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/workflows/runner.ts b/packages/sdk/src/workflows/runner.ts index f7371d354..ec745a3b8 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -3165,9 +3165,10 @@ export class WorkflowRunner { // Initialize trajectory recording this.trajectory = new WorkflowTrajectory(config.trajectories, runId, this.cwd); - const harnessSnapshot = this.installConfigHarnesses(config); + let harnessSnapshot: HarnessRegistrySnapshot | undefined; try { + harnessSnapshot = this.installConfigHarnesses(config); await this.updateRunStatus(runId, 'running'); if (!isResume) { this.emit({ type: 'run:started', runId }); @@ -3547,7 +3548,9 @@ export class WorkflowRunner { }); } } finally { - this.restoreConfigHarnesses(harnessSnapshot); + if (harnessSnapshot) { + this.restoreConfigHarnesses(harnessSnapshot); + } this.lastFailedStepOutput.clear(); this.lastCustomVerificationFailure.clear(); for (const stream of this.ptyLogStreams.values()) stream.end(); From b5387872b677403c2d4d0d0346ea574e27b71900 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 08:19:27 -0400 Subject: [PATCH 07/14] Address final harness review comments --- .../completed/2026-05/traj_60dr7ojudhfu.json | 25 + .../completed/2026-05/traj_60dr7ojudhfu.md | 14 + .trajectories/index.json | 1168 +++++++++++++++++ crates/broker/src/worker.rs | 30 +- packages/openclaw/src/setup.ts | 168 ++- packages/sdk/src/cli-registry.ts | 11 +- .../__tests__/harness-adapters.test.ts | 79 +- .../src/workflows/process-backend-executor.ts | 7 +- packages/sdk/src/workflows/runner.ts | 110 +- 9 files changed, 1506 insertions(+), 106 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_60dr7ojudhfu.json create mode 100644 .trajectories/completed/2026-05/traj_60dr7ojudhfu.md create mode 100644 .trajectories/index.json diff --git a/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json new file mode 100644 index 000000000..0e96ee20d --- /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": "/Users/will/Projects/AgentWorkforce/relay", + "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/index.json b/.trajectories/index.json new file mode 100644 index 000000000..99a99907d --- /dev/null +++ b/.trajectories/index.json @@ -0,0 +1,1168 @@ +{ + "version": 1, + "lastUpdated": "2026-05-25T12:18:38.134Z", + "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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json" + } + } +} diff --git a/crates/broker/src/worker.rs b/crates/broker/src/worker.rs index 5f15977d0..4df6c0dda 100644 --- a/crates/broker/src/worker.rs +++ b/crates/broker/src/worker.rs @@ -1079,10 +1079,17 @@ fn merge_harness_definitions( base: HarnessDefinition, override_definition: HarnessDefinition, ) -> HarnessDefinition { - let overrides_binary = override_definition.binary.is_some(); + 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_definition.binary.or(base.binary), + binary: override_binary.or(base.binary), binaries: if override_definition.binaries.is_empty() { if overrides_binary { Vec::new() @@ -1826,6 +1833,25 @@ mod harness_adapter_tests { 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() { diff --git a/packages/openclaw/src/setup.ts b/packages/openclaw/src/setup.ts index ee1962401..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,29 +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', '-y', - '--arg', 'agent-relay', - '--arg', '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; @@ -440,27 +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', '-y', - '--arg', 'agent-relay', - '--arg', '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)}`); @@ -482,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/src/cli-registry.ts b/packages/sdk/src/cli-registry.ts index 752fec9c5..09adfb9df 100644 --- a/packages/sdk/src/cli-registry.ts +++ b/packages/sdk/src/cli-registry.ts @@ -181,16 +181,19 @@ function cloneHarnessDefinition(config: HarnessDefinition): HarnessDefinition { } 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: override.binary ?? base.binary, + binary: overrideBinary ?? base.binary, ...(override.binaries ? { binaries: [...override.binaries] } - : base.binaries && override.binary === undefined - ? { binaries: [...base.binaries] } - : {}), + : overrideBinary !== undefined + ? { binaries: undefined } + : base.binaries + ? { binaries: [...base.binaries] } + : {}), ...(override.interactiveArgs ? { interactiveArgs: [...override.interactiveArgs] } : base.interactiveArgs diff --git a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts index 0fd3ab519..194915909 100644 --- a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts +++ b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts @@ -9,6 +9,7 @@ import { import { WorkflowBuilder } from '../builder.js'; import { buildCommand } from '../process-spawner.js'; import { WorkflowRunner } from '../runner.js'; +import type { ProcessBackend } from '../types.js'; const registrySnapshot = snapshotHarnessAdapters(); @@ -93,17 +94,81 @@ workflows: ); }); + 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-codex-wrapper', { - adapter: 'codex', - binary: 'company-codex', + registerHarnessAdapter('company-cursor-wrapper', { + adapter: 'cursor', + binary: 'company-cursor', searchPaths: ['~/company/bin'], }); - expect(buildCommand('company-codex-wrapper', [], 'do the work')).toEqual([ - 'company-codex', - 'exec', - '--dangerously-bypass-approvals-and-sandbox', + 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', ]); }); diff --git a/packages/sdk/src/workflows/process-backend-executor.ts b/packages/sdk/src/workflows/process-backend-executor.ts index d1f530d95..8ca26a005 100644 --- a/packages/sdk/src/workflows/process-backend-executor.ts +++ b/packages/sdk/src/workflows/process-backend-executor.ts @@ -27,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( @@ -49,8 +51,9 @@ export function createProcessBackendExecutor( ); } - const extraArgs = buildModelArgs(agentDef.cli, 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/runner.ts b/packages/sdk/src/workflows/runner.ts index ec745a3b8..3882965f7 100644 --- a/packages/sdk/src/workflows/runner.ts +++ b/packages/sdk/src/workflows/runner.ts @@ -28,14 +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 { - buildModelArgs, - getCliDefinition, - registerHarnessAdapters, - restoreHarnessAdapters, - snapshotHarnessAdapters, - type HarnessRegistrySnapshot, -} from '../cli-registry.js'; +import { defineHarnessAdapter, getCliDefinition, type CliDefinition } from '../cli-registry.js'; import { resolveCliSync } from '../cli-resolver.js'; import { buildNormalizedProxyEnv, @@ -449,6 +442,13 @@ function resolveCursorCli(): 'cursor-agent' | 'agent' { return (resolved?.binary as 'cursor-agent' | 'agent') ?? 'agent'; } +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': @@ -580,6 +580,12 @@ export class WorkflowRunner { 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(); @@ -1750,7 +1756,7 @@ export class WorkflowRunner { return 'openai'; } - const harnessProxyProvider = getCliDefinition(agentDef.cli)?.proxyProvider; + const harnessProxyProvider = this.getWorkflowCliDefinition(agentDef.cli)?.proxyProvider; if ( harnessProxyProvider === 'openai' || harnessProxyProvider === 'anthropic' || @@ -2095,20 +2101,72 @@ export class WorkflowRunner { return config; } - private installConfigHarnesses(config?: RelayYamlConfig): HarnessRegistrySnapshot { - const snapshot = snapshotHarnessAdapters(); - try { - registerHarnessAdapters(this.harnesses); - registerHarnessAdapters(config?.harnesses); - return snapshot; - } catch (err) { - restoreHarnessAdapters(snapshot); - throw err; + 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 restoreConfigHarnesses(snapshot: HarnessRegistrySnapshot): void { - restoreHarnessAdapters(snapshot); + 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 { @@ -3165,10 +3223,7 @@ export class WorkflowRunner { // Initialize trajectory recording this.trajectory = new WorkflowTrajectory(config.trajectories, runId, this.cwd); - let harnessSnapshot: HarnessRegistrySnapshot | undefined; - try { - harnessSnapshot = this.installConfigHarnesses(config); await this.updateRunStatus(runId, 'running'); if (!isResume) { this.emit({ type: 'run:started', runId }); @@ -3548,9 +3603,6 @@ export class WorkflowRunner { }); } } finally { - if (harnessSnapshot) { - this.restoreConfigHarnesses(harnessSnapshot); - } this.lastFailedStepOutput.clear(); this.lastCustomVerificationFailure.clear(); for (const stream of this.ptyLogStreams.values()) stream.end(); @@ -6486,7 +6538,7 @@ export class WorkflowRunner { timeoutMs?: number ): Promise { const agentName = `${step.name}-${this.generateShortId()}`; - const modelArgs = buildModelArgs(agentDef.cli, 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. @@ -6511,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 @@ -6664,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( From 7383705eddfda237cded8247490d59fbe0409a1e Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 08:25:04 -0400 Subject: [PATCH 08/14] Refresh package validation dependency cache --- .github/workflows/package-validation.yml | 2 +- .../completed/2026-05/traj_g35xvgo8fbqq.json | 25 +++++++++++++++++++ .../completed/2026-05/traj_g35xvgo8fbqq.md | 14 +++++++++++ .trajectories/index.json | 9 ++++++- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_g35xvgo8fbqq.json create mode 100644 .trajectories/completed/2026-05/traj_g35xvgo8fbqq.md diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index b720b51df..6456ceec7 100644 --- a/.github/workflows/package-validation.yml +++ b/.github/workflows/package-validation.yml @@ -79,7 +79,7 @@ jobs: path: | node_modules packages/*/node_modules - key: modules-v2-${{ hashFiles('package-lock.json') }} + key: modules-v3-${{ hashFiles('package-lock.json') }} - name: Install dependencies if: steps.cache-modules.outputs.cache-hit != 'true' diff --git a/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json new file mode 100644 index 000000000..e163f41d4 --- /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": "/Users/will/Projects/AgentWorkforce/relay", + "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/index.json b/.trajectories/index.json index 99a99907d..72ea59caf 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-25T12:18:38.134Z", + "lastUpdated": "2026-05-25T12:24:47.307Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1163,6 +1163,13 @@ "startedAt": "2026-05-25T12:12:15.123Z", "completedAt": "2026-05-25T12:18:37.956Z", "path": "/Users/will/Projects/AgentWorkforce/relay/.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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json" } } } From 522f78f8d2f3e1da2d1f38e9b8831d95fa40645f Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 08:39:26 -0400 Subject: [PATCH 09/14] Sanitize trajectory metadata paths --- .../completed/2026-05/traj_60dr7ojudhfu.json | 2 +- .../completed/2026-05/traj_g35xvgo8fbqq.json | 2 +- .trajectories/index.json | 334 +++++++++--------- 3 files changed, 169 insertions(+), 169 deletions(-) diff --git a/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json index 0e96ee20d..c08f4a946 100644 --- a/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json +++ b/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json @@ -16,7 +16,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "dbbd13fdd5c37535f1782e2234d7e8ee514ed32a", diff --git a/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json index e163f41d4..f78c7e8e1 100644 --- a/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json +++ b/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json @@ -16,7 +16,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "b5387872b677403c2d4d0d0346ea574e27b71900", diff --git a/.trajectories/index.json b/.trajectories/index.json index 72ea59caf..e7605231a 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -7,1169 +7,1169 @@ "status": "completed", "startedAt": "2026-04-10T14:56:33.229Z", "completedAt": "2026-04-10T15:05:14.660Z", - "path": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_05xg7j388bc4.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_1776105620545_9dcebb3d.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_222ha5671idc.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_3b3p1z4y7qlo.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_530xmbfeljyb.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_703m7sqyq89t.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_9tt55is74dq5.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_abjovknvcijv.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_qb54w47qwod6.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_tjadoebpscps.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_ui5omrgz819d.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_0e8i20oitwvz.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_0o6gb2wvk59t.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_0z98tkaigaxg.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1775914133873_35667beb.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1778873209642_c70e32ab.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1778873211616_6db3b2cd.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1rrpe2r7fyem.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_2gpglosdsq7s.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_2tqxnib25omk.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_2yicjxgajt0a.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_34b1u84b19gz.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_3gjtcykvybt5.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_47akjihewlow.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_4b2d63f6ljvh.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_4chzkm724ufo.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_4t07itef99ug.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_4vucir4qvqa2.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_5k0jtc1g5l33.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_5nzj6v56id4z.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_5q8i0iz4klpo.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_5qbla7w4kzoi.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_60qc24ufr96g.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_6sjeohtm3php.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_78ytpicts778.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_7uznwzoxbao6.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_7zu7et53ph3l.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_81kobstnzzwk.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_8ljgydz61do5.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_90jmd9z27oap.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_947wzpddsg9j.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_9fdv7hxm0b60.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_9gq96irkj00s.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_aw7stgf4qau0.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_bdrlknyl8twj.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_bz1a1o15p7px.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_cbmwd07phhm2.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ceo5q9bh2od3.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_d89s38ddu7cj.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_dbsnr453nxjw.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_dcl9hgoiuac5.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_dpgn0am1jq1c.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_dqgg2q4scsvt.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_e1b7ww3un1u3.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_elx0fcwgs37x.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_erzd7j9nto9r.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_f1iac9ngymlj.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_f3arvbmmlomn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_f9wxa8ujeg78.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_fh8oosbijpwc.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ft1pwdlcrmcn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_gh05rj5gwsap.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_gnqvtoxtc8dy.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_hfkww5z7trxn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_hrsndfzk0qay.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_hysw5o7idqas.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ij5b3kcatvwn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_iole5zdt9orr.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_irafiyk6wpw0.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_j9k10fez3e81.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_jbo2x14y7ovt.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_jmf9pyt3zikn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_k7njijv51iq4.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_lhyrcib40kao.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_lieyyspidhfj.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_mi9eqd4rjfea.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_mytnzgfayj3d.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_mz5m5ysjj31e.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_n8duofq5vq1a.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_o251whkvy9rl.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ootb5rt3tozd.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_oyc528j7suvo.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_piik8r6zu3i7.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_pjadgfw0mtw4.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_pmrcfj6or3pz.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_q97ei72svf2f.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_qtmid2nzz0kz.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_r3eic6rt84pq.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ryf5sstno6p3.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_s5ojo1f4srz4.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_sh2ahp9z2xg6.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_sqerp89tc436.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_t5uknesn2fcw.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_tavtex0db4b0.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_tgism98me5na.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_u33qn99ijbh4.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_u3loicehnwb4.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_u4ixmbqqm2y1.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_uf8y40ewrfh0.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_v1wexlfur5zr.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_v87cyrs8dke9.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_v9x3o92ag682.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_vfa1jr6otnjn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_vkozdglobkyg.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_wbn62q4cq16h.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_whd40oxptlhn.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_wx00tjvpptvg.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_wzzboitm85ee.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_x37bhga2j5ph.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_ybcrij9wg8m1.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_z171lng2fbbi.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_zfa6skfr32vy.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_zqwco4gl76g3.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_zu3252hxzoqh.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1775914296101_a4397efe.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1776024661304_cfc829b9.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873052429_03a4dacb.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873197540_01102ade.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873199489_f2ce4060.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873201502_0dacf7c5.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873203502_4c225b7e.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873205470_a4e5f0cb.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778873207471_b7def991.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874205797_81e92307.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874216773_c6b12ab2.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874218579_a0225559.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874224855_9c722c4b.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874226983_3367d527.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874229373_9cce9465.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874240339_51b823cd.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874241076_caa675a9.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874248966_e29c4c54.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874249983_12a98df3.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874258229_0bdc53d8.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874261453_55f49624.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874261608_48fb9bf5.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874269139_d7d7485a.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874274412_70843e0e.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874274581_71efa470.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874282200_39ad11db.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874283570_ce3585b8.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874289674_e3f868c8.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874291950_0b1b5c1f.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874295927_4083d181.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/traj_1778874296362_bdf727ff.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_60dr7ojudhfu.json" + "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": "/Users/will/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json" + "path": "/.trajectories/completed/2026-05/traj_g35xvgo8fbqq.json" } } } From bffae82614f439546dab4d4be4e195b59e2575c6 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 08:45:20 -0400 Subject: [PATCH 10/14] Always install package validation dependencies --- .github/workflows/package-validation.yml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/package-validation.yml b/.github/workflows/package-validation.yml index 6456ceec7..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-v3-${{ 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 From 2b405d09bd018f5d395a5c07acfc71c1bd3b1488 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 09:20:16 -0400 Subject: [PATCH 11/14] Fix harness docs language examples --- .../completed/2026-05/traj_5kytmhye9atg.json | 25 ++++ .../completed/2026-05/traj_5kytmhye9atg.md | 14 ++ .trajectories/index.json | 9 +- web/components/docs/DocsNav.tsx | 2 + web/content/docs/harnesses.mdx | 138 +++++++++++++----- 5 files changed, 148 insertions(+), 40 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_5kytmhye9atg.json create mode 100644 .trajectories/completed/2026-05/traj_5kytmhye9atg.md 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/index.json b/.trajectories/index.json index e7605231a..c376591ce 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-25T12:24:47.307Z", + "lastUpdated": "2026-05-25T13:19:41.214Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1170,6 +1170,13 @@ "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" } } } 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 index 5e0d3410d..be4a9224f 100644 --- a/web/content/docs/harnesses.mdx +++ b/web/content/docs/harnesses.mdx @@ -15,6 +15,7 @@ Harnesses are used by real `agent-relay` spawning. They are not limited to workf Use a built-in harness by naming it in a spawn call: + ```typescript TypeScript file="spawn-codex.ts" import { AgentRelay } from '@agent-relay/sdk'; @@ -28,14 +29,50 @@ const worker = await relay.spawn('CodexWorker', 'codex', 'Fix the failing auth t 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", @@ -51,8 +88,9 @@ console.log(BUILTIN_HARNESS_DEFINITIONS.codex); ## Define a custom harness -Register harnesses on `AgentRelay` when several spawns should share the same adapter: +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'; @@ -81,8 +119,41 @@ const reviewer = await relay.spawn('QwenReviewer', 'qwen', 'Review the latest di 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() +``` + + 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', @@ -95,10 +166,31 @@ const agent = await relay.spawn('LocalAgent', 'local-agent', 'Inspect the repo.' }); ``` +```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"], + ), + ), +) +``` + + ## 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: { @@ -113,46 +205,11 @@ const relay = new AgentRelay({ await relay.spawn('CompanyCodex', 'company-codex', 'Update the billing tests.'); ``` -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. - -## Python spawn - -Python accepts the same serializable harness definition. Use snake_case fields on the dataclass; the SDK serializes them to the broker format. - -```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() -``` +```python Python file="spawn_company_codex.py" +from agent_relay import AgentRelay, HarnessDefinition -Use `adapter` when a Python-defined harness should use a built-in lifecycle: +relay = AgentRelay() -```python Python relay.register_harness( "company-codex", HarnessDefinition( @@ -162,6 +219,9 @@ relay.register_harness( ), ) ``` + + +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. ## Workflow YAML From 77dc58139ff2674c08aee13bdb2d3ae0db2201a8 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 09:36:46 -0400 Subject: [PATCH 12/14] Document harness lifecycle --- .../completed/2026-05/traj_qpefcnn38jnx.json | 53 +++++++++++++++++++ .../completed/2026-05/traj_qpefcnn38jnx.md | 33 ++++++++++++ .trajectories/index.json | 9 +++- web/content/docs/harnesses.mdx | 40 +++++++++++++- 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_qpefcnn38jnx.json create mode 100644 .trajectories/completed/2026-05/traj_qpefcnn38jnx.md 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/index.json b/.trajectories/index.json index c376591ce..6156ba520 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-25T13:19:41.214Z", + "lastUpdated": "2026-05-25T13:36:05.306Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1177,6 +1177,13 @@ "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" } } } diff --git a/web/content/docs/harnesses.mdx b/web/content/docs/harnesses.mdx index be4a9224f..b85aa4ed0 100644 --- a/web/content/docs/harnesses.mdx +++ b/web/content/docs/harnesses.mdx @@ -223,6 +223,44 @@ relay.register_harness( 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. + + +### 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. @@ -289,7 +327,7 @@ Use placeholders as whole argv items when they expand to multiple arguments: 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}` unless the CLI does not need Relay messaging. Omitting it prevents Relay from passing the MCP configuration and protocol prompt into the spawned process. +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 From 8d30ceceac8e9464c2cfb8ba54fb47620bf278ab Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 10:26:41 -0400 Subject: [PATCH 13/14] Add harness runtime adapter surface --- .../completed/2026-05/traj_ps68dydvgfuz.json | 53 +++ .../completed/2026-05/traj_ps68dydvgfuz.md | 33 ++ .trajectories/index.json | 9 +- CHANGELOG.md | 4 +- crates/broker/src/runtime/worker_events.rs | 6 +- packages/sdk/README.md | 4 + .../sdk/src/__tests__/lifecycle-hooks.test.ts | 18 + .../sdk/src/__tests__/spawn-harness.test.ts | 28 ++ packages/sdk/src/cli-registry.ts | 17 +- packages/sdk/src/client.ts | 1 + packages/sdk/src/harness-runtime.ts | 82 ++++ packages/sdk/src/index.ts | 1 + packages/sdk/src/lifecycle-hooks.ts | 2 +- packages/sdk/src/protocol.ts | 3 +- packages/sdk/src/relay-adapter.ts | 8 +- packages/sdk/src/relay.ts | 38 +- packages/sdk/src/workflows/README.md | 450 +++++++++--------- .../__tests__/harness-adapters.test.ts | 20 +- packages/sdk/src/workflows/index.ts | 11 + 19 files changed, 542 insertions(+), 246 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_ps68dydvgfuz.json create mode 100644 .trajectories/completed/2026-05/traj_ps68dydvgfuz.md create mode 100644 packages/sdk/src/harness-runtime.ts 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/index.json b/.trajectories/index.json index 6156ba520..d64633b82 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-25T13:36:05.306Z", + "lastUpdated": "2026-05-25T14:19:17.134Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1184,6 +1184,13 @@ "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" } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 59284505c..5ddc2bb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +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 harness adapters 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`: 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/runtime/worker_events.rs b/crates/broker/src/runtime/worker_events.rs index 85f487896..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, session_id_val) = workers + let (provider_val, cli_val, model_val, session_id_val, pid_val) = workers .workers .get(&name) .map(|h| { @@ -325,9 +325,10 @@ impl BrokerRuntime { h.spec.cli.clone(), h.spec.model.clone(), h.spec.session_id.clone(), + h.child.id(), ) }) - .unwrap_or((None, None, None, None)); + .unwrap_or((None, None, None, None, None)); let _ = send_event( sdk_out_tx, json!({ @@ -338,6 +339,7 @@ impl BrokerRuntime { "cli": cli_val, "model": model_val, "sessionId": session_id_val, + "pid": pid_val, }), ) .await; diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 16148a1ef..2df87e2b0 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -75,6 +75,10 @@ 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); 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 index 775829e24..77ede0c82 100644 --- a/packages/sdk/src/__tests__/spawn-harness.test.ts +++ b/packages/sdk/src/__tests__/spawn-harness.test.ts @@ -86,6 +86,34 @@ describe('spawn harness adapters', () => { ); }); + 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: { diff --git a/packages/sdk/src/cli-registry.ts b/packages/sdk/src/cli-registry.ts index 09adfb9df..b1ea06aee 100644 --- a/packages/sdk/src/cli-registry.ts +++ b/packages/sdk/src/cli-registry.ts @@ -337,13 +337,20 @@ function adapterFromConfig(name: string, config: HarnessDefinition): CliDefiniti }; } -export type HarnessAdapter = CliDefinition | HarnessDefinition; +export type CLIHarnessAdapter = CliDefinition | HarnessDefinition; -function isCliDefinition(adapter: HarnessAdapter): adapter is CliDefinition { +/** + * @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: HarnessAdapter): CliDefinition { +export function defineHarnessAdapter(name: string, adapter: CLIHarnessAdapter): CliDefinition { if (isCliDefinition(adapter)) { return { ...adapter, @@ -362,7 +369,7 @@ export function defineHarnessAdapter(name: string, adapter: HarnessAdapter): Cli * Programmatic adapters can provide functions; YAML/workflow configs should * use the serializable {@link HarnessDefinition} shape. */ -export function registerHarnessAdapter(name: string, adapter: HarnessAdapter): void { +export function registerHarnessAdapter(name: string, adapter: CLIHarnessAdapter): void { const key = normalizeCliKey(name); const definition = defineHarnessAdapter(key, adapter); USER_CLI_REGISTRY.set(key, definition); @@ -384,7 +391,7 @@ export function registerHarnessAdapter(name: string, adapter: HarnessAdapter): v /** Backward-compatible name for callers that think in CLI definitions. */ export const registerCliDefinition = registerHarnessAdapter; -export function registerHarnessAdapters(adapters: Record | undefined): void { +export function registerHarnessAdapters(adapters: Record | undefined): void { if (!adapters) return; for (const [name, adapter] of Object.entries(adapters)) { registerHarnessAdapter(name, adapter); diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 339faa559..86d267f81 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -120,6 +120,7 @@ export interface SpawnAgentResult { name: string; runtime: AgentRuntime; sessionId?: string; + pid?: number; } export interface SessionInfo { 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 037563a71..761326798 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -43,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 edad5e094..bba84f173 100644 --- a/packages/sdk/src/lifecycle-hooks.ts +++ b/packages/sdk/src/lifecycle-hooks.ts @@ -81,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; sessionId?: string }; + 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 a612c3f38..22ad4dca0 100644 --- a/packages/sdk/src/protocol.ts +++ b/packages/sdk/src/protocol.ts @@ -404,6 +404,7 @@ export type BrokerEvent = cli?: string; model?: string; sessionId?: string; + pid?: number; } | { kind: 'worker_error'; @@ -511,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 ef16be692..577ecdf30 100644 --- a/packages/sdk/src/relay-adapter.ts +++ b/packages/sdk/src/relay-adapter.ts @@ -208,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 bb6bc0e1a..864c10cf9 100644 --- a/packages/sdk/src/relay.ts +++ b/packages/sdk/src/relay.ts @@ -270,6 +270,7 @@ export interface SpawnLifecycleContext { export interface SpawnLifecycleSuccessContext extends SpawnLifecycleContext { runtime: AgentRuntime; sessionId?: string; + pid?: number; } export interface SpawnLifecycleErrorContext extends SpawnLifecycleContext { @@ -370,6 +371,7 @@ 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; @@ -502,6 +504,7 @@ type OutputListener = { type InternalAgent = Agent & { _setChannels: (channels: string[]) => void; _setSessionId: (sessionId: string) => void; + _setPid: (pid: number) => void; }; type InternalAgentResultContract = { @@ -882,7 +885,8 @@ export class AgentRelay { result.name, result.runtime, channels, - result.sessionId + result.sessionId, + result.pid ) as Agent; this.knownAgents.set(agent.name, agent); await this.invokeLifecycleHook( @@ -892,6 +896,7 @@ export class AgentRelay { name: result.name, runtime: result.runtime, sessionId: result.sessionId, + pid: result.pid, }, `spawnPty("${input.name}") onSuccess` ); @@ -1157,9 +1162,12 @@ export class AgentRelay { 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); + const agent = this.makeAgent(entry.name, entry.runtime, entry.channels, entry.sessionId, entry.pid); this.knownAgents.set(agent.name, agent); return agent; }); @@ -1449,16 +1457,20 @@ export class AgentRelay { name: string, runtime: AgentRuntime = 'pty', channels: string[] = [], - sessionId?: 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, sessionId); + const agent = this.makeAgent(name, runtime, channels, sessionId, pid); this.knownAgents.set(name, agent); return agent; } @@ -1741,7 +1753,7 @@ export class AgentRelay { break; } case 'agent_spawned': { - const agent = this.ensureAgentHandle(event.name, event.runtime, [], event.sessionId); + 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); @@ -1802,7 +1814,7 @@ export class AgentRelay { break; } case 'worker_ready': { - const agent = this.ensureAgentHandle(event.name, event.runtime, [], event.sessionId); + 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); @@ -2047,18 +2059,23 @@ export class AgentRelay { name: string, runtime: AgentRuntime, channels: string[], - sessionId?: 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]; }, @@ -2271,6 +2288,9 @@ export class AgentRelay { _setSessionId(nextSessionId: string) { agentSessionId = nextSessionId; }, + _setPid(nextPid: number) { + agentPid = nextPid; + }, }; return agent; } @@ -2356,7 +2376,8 @@ export class AgentRelay { result.name, result.runtime, channels, - result.sessionId + result.sessionId, + result.pid ) as Agent; this.knownAgents.set(agent.name, agent); await this.invokeLifecycleHook( @@ -2366,6 +2387,7 @@ export class AgentRelay { name: result.name, runtime: result.runtime, sessionId: result.sessionId, + pid: result.pid, }, `spawn("${name}") onSuccess` ); diff --git a/packages/sdk/src/workflows/README.md b/packages/sdk/src/workflows/README.md index 63e45e7a3..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 @@ -238,16 +242,19 @@ verification: 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: +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"] + interactiveArgs: ['run', '{modelArgs}', '{args}'] + nonInteractiveArgs: ['run', '--prompt', '{task}', '{args}'] + modelArgs: ['-m', '{model}'] + searchPaths: ['~/.local/bin'] agents: - name: reviewer @@ -270,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. @@ -288,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 @@ -371,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 @@ -404,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(); @@ -424,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 @@ -442,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(); ``` @@ -541,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) @@ -555,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 @@ -572,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); }, @@ -595,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 @@ -604,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 @@ -631,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 @@ -736,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. @@ -754,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 index 194915909..6f346cb4b 100644 --- a/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts +++ b/packages/sdk/src/workflows/__tests__/harness-adapters.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, expectTypeOf, it } from 'vitest'; import { buildModelArgs, @@ -9,7 +9,9 @@ import { 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(); @@ -27,6 +29,22 @@ describe('workflow harness adapters', () => { ]); }); + 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'], diff --git a/packages/sdk/src/workflows/index.ts b/packages/sdk/src/workflows/index.ts index c6c2bf9f9..d78780ff8 100644 --- a/packages/sdk/src/workflows/index.ts +++ b/packages/sdk/src/workflows/index.ts @@ -53,8 +53,19 @@ export { 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 { From 0bb01e01fcb20e76e97c75face594dad23f34440 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 25 May 2026 10:36:39 -0400 Subject: [PATCH 14/14] Update harnesses docs for runtime adapters --- .../completed/2026-05/traj_yh2ml7b0fze1.json | 25 ++++++ .../completed/2026-05/traj_yh2ml7b0fze1.md | 14 +++ .trajectories/index.json | 9 +- web/content/docs/harnesses.mdx | 87 ++++++++++++++++++- 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 .trajectories/completed/2026-05/traj_yh2ml7b0fze1.json create mode 100644 .trajectories/completed/2026-05/traj_yh2ml7b0fze1.md 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 index d64633b82..a3674eccc 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-25T14:19:17.134Z", + "lastUpdated": "2026-05-25T14:35:52.143Z", "trajectories": { "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", @@ -1191,6 +1191,13 @@ "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/web/content/docs/harnesses.mdx b/web/content/docs/harnesses.mdx index b85aa4ed0..313e5aed8 100644 --- a/web/content/docs/harnesses.mdx +++ b/web/content/docs/harnesses.mdx @@ -1,14 +1,14 @@ --- title: Harnesses -description: Define agent CLI adapters for Agent Relay spawning and workflows. +description: Define CLI and runtime harness adapters for Agent Relay spawning and workflows. --- -A harness is Relay's adapter for an agent CLI. It tells the 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. +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. +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 @@ -151,6 +151,22 @@ 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: @@ -186,6 +202,21 @@ agent = await relay.spawn( ``` +## 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: @@ -251,7 +282,55 @@ The `adapter` field is what selects broker-owned lifecycle behavior. The executa 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. -### Lifecycle hooks and events +## 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: