From 68bdb0be608a84bd25190cac848cb87525a3bcf3 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 6 Mar 2026 13:48:51 -0800 Subject: [PATCH 1/2] Add openclaw package split workflow and skill updates - Add openclaw-package-split.yaml workflow (12-step DAG with 5 agents) - Add BOUNDARY.md and PACKAGE_SPLIT_PLAN.md workflow outputs - Update workflow skill with scrub step pattern and verification guidance - Update openclaw skill with inbound message testing section Co-Authored-By: Claude Opus 4.6 --- .../writing-agent-relay-workflows/SKILL.md | 83 ++++ packages/openclaw/BOUNDARY.md | 136 +++++++ packages/openclaw/PACKAGE_SPLIT_PLAN.md | 123 ++++++ packages/openclaw/skill/SKILL.md | 30 ++ .../workflows/openclaw-package-split.yaml | 377 ++++++++++++++++++ 5 files changed, 749 insertions(+) create mode 100644 packages/openclaw/BOUNDARY.md create mode 100644 packages/openclaw/PACKAGE_SPLIT_PLAN.md create mode 100644 packages/sdk/src/examples/workflows/openclaw-package-split.yaml diff --git a/.claude/skills/writing-agent-relay-workflows/SKILL.md b/.claude/skills/writing-agent-relay-workflows/SKILL.md index 4c9f6808b..bff796689 100644 --- a/.claude/skills/writing-agent-relay-workflows/SKILL.md +++ b/.claude/skills/writing-agent-relay-workflows/SKILL.md @@ -344,6 +344,87 @@ Even if a wave has 10 ready steps, the runner will only start 5 at a time and pi | 5–10 | 5 | | 10+ | 6–8 max | +## Scrub Steps: Taming Interactive Lead Output + +Interactive agents (PTY mode) produce massive output — TUI chrome, progress indicators, tool call +traces, and verbose reasoning. A single interactive lead step can produce 100KB–10MB of captured +output. When this gets chained via `{{steps.lead-step.output}}` into downstream steps, it: + +1. **Overwhelms downstream agents** — they get a 240KB prompt and spiral +2. **Causes timeouts** — the next lead step tries to process megabytes of PTY noise +3. **Breaks verification** — the token exists somewhere in 10MB of output but the agent never exits + +### The fix: deterministic scrub steps + +After every interactive lead step that produces structured output, add a **deterministic scrub step** +that extracts the clean artifact. Have the lead write its output to a file, then the scrub step +reads that file. Downstream steps chain from the scrub step, not the lead. + +```yaml +# Lead writes structured output to a file +- name: propose-boundary + type: agent + agent: lead + dependsOn: [analyze] + task: | + Write your boundary proposal to packages/BOUNDARY.md with this structure: + # Package Boundary + ## Package A owns + - ... + ## Package B owns + - ... + When done, run: echo "BOUNDARY_PROPOSED" + verification: + type: output_contains + value: BOUNDARY_PROPOSED + +# Deterministic step reads the clean file — no PTY noise +- name: scrub-proposal + type: deterministic + dependsOn: [propose-boundary] + command: | + if [ -f packages/BOUNDARY.md ]; then + cat packages/BOUNDARY.md + else + echo "File not found" + fi + captureOutput: true + +# Downstream steps chain from the scrub, not the lead +- name: review + agent: reviewer + dependsOn: [scrub-proposal] + task: | + Review this boundary: + {{steps.scrub-proposal.output}} +``` + +### Rules + +- **Every interactive lead step should write its deliverable to a file** (markdown, JSON, etc.) +- **Add a scrub step after each lead step** that `cat`s the file +- **Downstream steps depend on the scrub step**, never directly on the lead step's output +- **Keep lead tasks focused**: one deliverable per step, 10–15 line prompts +- **Non-interactive agents don't need scrub steps** — their stdout is already clean + +### When NOT to scrub + +- Non-interactive agents (`interactive: false`, presets `worker`/`reviewer`/`analyst`) produce clean + stdout. Their output is safe to chain directly. +- If the lead's output is only consumed by a non-interactive worker (which will read a file anyway), + you can skip the scrub and have the worker read the file directly in its task. + +## Verification: Interactive vs Non-Interactive + +| Agent type | Verification | Rationale | +|---|---|---| +| Non-interactive (codex workers, `interactive: false`) | None needed | They exit when done — exit code 0 = success | +| Interactive lead (claude PTY) | `output_contains` with `echo "TOKEN"` | Must signal completion explicitly since they stay alive | + +Non-interactive agents just die when their task is finished. The runner already treats a clean +exit as success. Adding `output_contains` verification to non-interactive agents triggers the +double-occurrence rule and causes spurious failures. Only use verification on interactive agents. + ## Common Mistakes | Mistake | Fix | @@ -361,6 +442,8 @@ Even if a wave has 10 ready steps, the runner will only start 5 at a time and pi | Workers depending on the lead step (deadlock) | Workers and lead both depend on a shared context step | | Omitting `agents` field for deterministic-only workflows | Field is now optional — pure shell pipelines work without it | | Verification token buried at end of long codex task | Use `REQUIRED: The very last line you print must be exactly: TOKEN` | +| Chaining interactive lead output to downstream steps | Add a scrub step that reads the lead's file output (see above) | +| Adding `output_contains` to non-interactive agents | Don't — they exit when done, no verification needed | ## Verification Tokens with Non-Interactive Workers diff --git a/packages/openclaw/BOUNDARY.md b/packages/openclaw/BOUNDARY.md new file mode 100644 index 000000000..839bc5036 --- /dev/null +++ b/packages/openclaw/BOUNDARY.md @@ -0,0 +1,136 @@ +# Package Boundary + +## @relaycast/openclaw owns + +- `config.ts` — detection and config I/O primitives +- `types.ts` — shared types and constants +- `inject.ts` — lightweight bridge delivery +- `openclawHome()` — detect OpenClaw home directory (env vars, variant probing) +- `detectOpenClaw()` — full installation detection (paths, config, variant) +- `hasValidConfig()` (internal) — sync config file validation helper +- `loadGatewayConfig(configPath?: string)` — read gateway config from `.env`; path-agnostic I/O +- `saveGatewayConfig(config, configPath?: string)` — atomic write of gateway config; path-agnostic I/O +- `deliverMessage(message, clawName, sender)` — stateless single-send into OpenClaw skill entrypoint (no fallback chain, no retry, no thread routing) +- `type OpenClawDetection` — detection result (paths, variant, config) +- `type GatewayConfig` — Relaycast workspace connection config +- `type InboundMessage` — normalized inbound message shape +- `type DeliveryResult` — delivery outcome (`ok`, `method`, `error`) +- `type RelaySenderLike` — minimal sender interface (`sendMessage()` contract, decouples from full SDK) +- `DEFAULT_OPENCLAW_GATEWAY_PORT` constant + +## @agent-relay/openclaw owns + +- `gateway.ts` — `InboundGateway` class, `OpenClawGatewayClient`, `GatewayOptions`, `RelaySender`, device identity persistence/pairing, realtime routing, delivery strategy (fallback chain), embedded control HTTP server (`/spawn`, `/list`, `/release`) +- `resolveGatewayConfigPath()` (new local helper) — derives Relaycast-specific config path from `detectOpenClaw()` result; encapsulates `workspace/relaycast/.env` convention +- `setup.ts` — full setup orchestration (workspace create/join, mcporter wiring, gateway bootstrap, background start), `SetupOptions`, `SetupResult` +- `cli.ts` — `relay-openclaw` CLI commands (`setup`, `gateway`, `spawn`, `list`, `release`, `mcp-server`, `runtime-setup`) +- `control.ts` — `spawnOpenClaw()`, `listOpenClaws()`, `releaseOpenClaw()`, `ClawRunnerControlConfig`, `SpawnOpenClawInput`, `ReleaseOpenClawInput` +- `mcp/server.ts` — `startMcpServer()` exposing spawn/list/release tools +- `mcp/tools.ts` — MCP tool definitions +- `spawn/manager.ts` — `SpawnManager`, `SpawnMode` +- `spawn/docker.ts` — `DockerSpawnProvider`, `DockerSpawnProviderOptions` +- `spawn/process.ts` — `ProcessSpawnProvider` +- `spawn/types.ts` — `SpawnOptions`, `SpawnHandle`, `SpawnProvider` +- `runtime/setup.ts` — `runtimeSetup()`, `RuntimeSetupOptions` +- `runtime/openclaw-config.ts` — `writeOpenClawConfig()`, `OpenClawConfigOptions` +- `runtime/patch.ts` — `patchOpenClawDist()`, `clearJitCache()` +- `identity/*` — `normalizeModelRef()`, `buildAgentName()`, `buildIdentityTask()`, `buildRuntimeIdentityPreamble()`, `renderSoulTemplate()`, `generateSoulMd()`, `generateIdentityMd()`, `writeRuntimeIdentityJson()`, `ensureWorkspace()`, `EnsureWorkspaceOptions` +- `auth/converter.ts` — `convertCodexAuth()`, `ConvertResult`, `CodexAuth` +- `index.ts` — orchestration exports (+ temporary deprecated re-exports of migrated symbols during transition) + +## @agent-relay/openclaw imports from @relaycast/openclaw + +- `openclawHome` (used in `gateway.ts`) +- `detectOpenClaw` (used in `setup.ts`) +- `loadGatewayConfig` (used in `cli.ts`, with explicit path from `resolveGatewayConfigPath()`) +- `saveGatewayConfig` (used in `setup.ts`, with explicit path from `resolveGatewayConfigPath()`) +- `deliverMessage` (used in `gateway.ts`) +- `DEFAULT_OPENCLAW_GATEWAY_PORT` (used in `gateway.ts`, `spawn/docker.ts`) +- `type GatewayConfig` (used in `gateway.ts`, `setup.ts`) +- `type OpenClawDetection` (used in `setup.ts`) +- `type InboundMessage` (used in `gateway.ts`) +- `type DeliveryResult` (used in `gateway.ts`) +- `type RelaySenderLike` (used in `gateway.ts`) + +## Breaking changes + +- `detectOpenClaw`, `openclawHome`, `loadGatewayConfig`, `saveGatewayConfig`, `deliverMessage` move from `@agent-relay/openclaw` to `@relaycast/openclaw` — import paths change for all consumers +- `GatewayConfig`, `OpenClawDetection`, `InboundMessage`, `DeliveryResult`, `DEFAULT_OPENCLAW_GATEWAY_PORT` move from `@agent-relay/openclaw` to `@relaycast/openclaw` +- `deliverMessage` signature changes: requires `RelaySenderLike` sender parameter (no optional `AgentRelayClient`, no internal fallback chain); fallback logic moves to `InboundGateway` +- `loadGatewayConfig`/`saveGatewayConfig` accept optional explicit `configPath` parameter; Relaycast-specific default path resolution moves to `@agent-relay/openclaw` via `resolveGatewayConfigPath()` +- `@relaycast/openclaw` removes orchestration exports: `InboundGateway`, `setup`, control API, `spawn/*`, `runtime/*`, `identity/*`, `auth/*`, `mcp/*`, CLI +- `@relaycast/openclaw` drops heavy dependencies: `@agent-relay/sdk`, `dockerode`, runtime/spawn deps +- `@agent-relay/openclaw` adds `@relaycast/openclaw` as a dependency (one-way only, no reverse) +- `@agent-relay/openclaw` re-exports migrated symbols with `@deprecated` for two minor versions; removal in next major + +--- + +## Review feedback resolutions + +### BLOCKER 1: Breaking-change rollout underspecified for `@relaycast/openclaw` consumers + +**Finding**: `@relaycast/openclaw` drops orchestration exports immediately. Existing consumers hard-break without a safe migration window. + +**Resolution**: The boundary is correct — `@relaycast/openclaw` should not carry orchestration exports long-term. However, the rollout needs staging: + +1. **Phase 1 (split release)**: `@relaycast/openclaw` adds deprecated re-exports of removed orchestration symbols that proxy to `@agent-relay/openclaw`. This requires `@agent-relay/openclaw` as an optional peer dependency during transition only. +2. **Phase 2 (next minor)**: Ship codemod (`npx @relaycast/openclaw-migrate`) that rewrites imports. Console warnings on deprecated re-exports. +3. **Phase 3 (next major)**: Remove deprecated re-exports, drop optional peer dependency. Clean break. + +Updated in Breaking changes: "`@relaycast/openclaw` removes orchestration exports" now reads "staged removal over two minor versions with deprecated re-exports and codemod." + +### BLOCKER 2: Device identity behavior across Docker + `OPENCLAW_HOME` override + +**Finding**: Identity persistence key may change with runtime path/container mount, causing duplicate identities, broken pairing, and routing instability. + +**Resolution**: Device identity is owned by `@agent-relay/openclaw` (`gateway.ts` Ed25519 key management). The boundary is correct — identity stays in the orchestration layer. To prevent the stated failure modes, `@agent-relay/openclaw` must define: + +1. **Identity scope**: Workspace-level. Key stored at `{openclawHome}/workspace/relaycast/.device-identity.json`. +2. **Stable key components**: Device ID derived from `clawName` + key fingerprint only — explicitly excludes filesystem path so identity survives mount changes. +3. **`OPENCLAW_HOME` changes**: If `OPENCLAW_HOME` changes and the old identity file isn't at the new path, the gateway generates a new identity and logs a warning. Previous pairings become stale (expected — this is an explicit reconfiguration). +4. **Container ephemeral fallback**: If identity file is unwritable (read-only mount), generate ephemeral in-memory identity per session. Log at `warn` level. No persistence attempted. + +This does not change the boundary — it specifies behavior that `@agent-relay/openclaw` must implement during the split. + +### WARNING 1: Orchestration-flavored names in `@relaycast/openclaw` + +**Finding**: `InboundMessage`, `RelaySenderLike`, `GatewayConfig` couple the primitives package to relay gateway semantics. + +**Acknowledged**: The names reflect the actual domain — these types describe Relaycast-to-OpenClaw bridge primitives, not generic transport. Renaming to abstract terms (`BridgeMessage`, `MessageSink`) would obscure intent without adding safety. Instead, document `@relaycast/openclaw` as "Relaycast/OpenClaw integration primitives" in its `package.json` description field. The `RelaySenderLike` name stays — it accurately describes the contract. + +### WARNING 2: Circular dependency risk via barrels and deprecated re-exports + +**Finding**: One-way dependency is intended but easy to accidentally violate. + +**Acknowledged**: During migration, enforce with: +- `dependency-cruiser` rule: `@relaycast/openclaw` must never import from `@agent-relay/openclaw` (except the transitional deprecated re-exports in Phase 1, which proxy the other direction). +- All deprecated re-exports use `import type` where possible to minimize runtime coupling. +- CI check added to fail on circular imports between the two packages. + +### WARNING 3: `OPENCLAW_HOME` + variant probing precedence edge cases + +**Finding**: Ambiguity around invalid paths, symlinks, permissions, non-deterministic behavior. + +**Acknowledged**: The boundary owns this in `@relaycast/openclaw` (`config.ts`). During migration, refactor to: + +1. Extract shared `resolveHome()` internal helper (dedup between `openclawHome()` and `detectOpenClaw()`). +2. Explicit precedence: `OPENCLAW_CONFIG_PATH` > `OPENCLAW_HOME` > probe. If env var is set but path is invalid, throw (no silent fallback). +3. Probe tie-break rules documented in JSDoc and tested. + +### WARNING 4: `saveGatewayConfig` atomic write not guaranteed + +**Finding**: `writeFile()` is not atomic on Docker bind mounts or networked filesystems. + +**Acknowledged**: During migration, implement temp-file-in-same-dir + `rename` pattern. Single-writer assumption is valid (config written at setup/CLI time only, not during gateway runtime). Document this assumption in JSDoc. + +### WARNING 5: `gateway.ts` monolith risk + +**Finding**: `InboundGateway` (1450 LOC) may become a god object post-split. + +**Acknowledged**: This is a valid concern but does not affect the package boundary. Post-split backlog item: decompose `gateway.ts` into `delivery-strategy.ts`, `control-server.ts`, `device-identity.ts`, `realtime-router.ts`. Not a blocker for the split itself. + +### NOTEs (confirmed, no action required) + +- **NOTE 1**: One-way dependency (`@agent-relay/openclaw` -> `@relaycast/openclaw`) confirmed as strong architectural improvement. +- **NOTE 2**: Stateless `deliverMessage` is the right separation; fallback/retry belongs in orchestration. +- **NOTE 3**: `resolveGatewayConfigPath()` in `@agent-relay/openclaw` is a good boundary guard for Relaycast-specific conventions. diff --git a/packages/openclaw/PACKAGE_SPLIT_PLAN.md b/packages/openclaw/PACKAGE_SPLIT_PLAN.md new file mode 100644 index 000000000..9f690120a --- /dev/null +++ b/packages/openclaw/PACKAGE_SPLIT_PLAN.md @@ -0,0 +1,123 @@ +# Package Split Plan + +## Overview + +Split the current `@agent-relay/openclaw` monolith (2780 LOC across 8 source files) into two packages with a clean one-way dependency: `@relaycast/openclaw` owns SDK/bridge primitives (config detection, types, stateless delivery) while `@agent-relay/openclaw` retains orchestration (gateway, spawn management, MCP server, CLI, identity, setup). The new primitives package lives at `packages/relaycast-openclaw`. This eliminates circular dependency risk, reduces install weight for SDK consumers, and establishes a clear architectural boundary. The migration uses a staged 3-phase rollout with deprecated re-exports to avoid hard breaks. + +## Package Ownership + +### `@relaycast/openclaw` (SDK/bridge primitives — `packages/relaycast-openclaw`) +- `config.ts` — `openclawHome()`, `detectOpenClaw()`, `hasValidConfig()` (internal), `loadGatewayConfig(configPath?)`, `saveGatewayConfig(config, configPath?)`, internal `resolveHome()` helper +- `types.ts` — `GatewayConfig`, `OpenClawDetection`, `InboundMessage`, `DeliveryResult`, `RelaySenderLike`, `DEFAULT_OPENCLAW_GATEWAY_PORT` +- `inject.ts` — `deliverMessage(message, clawName, sender)` (stateless single-send, no fallback chain) +- No heavy dependencies (`@agent-relay/sdk`, `dockerode`, `ws` removed from this package) + +### `@agent-relay/openclaw` (orchestration layer — `packages/openclaw`) +- `gateway.ts` — `InboundGateway`, `OpenClawGatewayClient`, device identity, realtime routing, delivery strategy with fallback chain, embedded control HTTP server +- `resolve-gateway-config-path.ts` — new helper deriving Relaycast-specific config path (`workspace/relaycast/.env`) from `detectOpenClaw()` result +- `setup.ts` — full setup orchestration (workspace create/join, mcporter wiring, gateway bootstrap) +- `cli.ts` — `relay-openclaw` CLI commands +- `control.ts` — `spawnOpenClaw()`, `listOpenClaws()`, `releaseOpenClaw()` +- `mcp/` — MCP server and tool definitions +- `spawn/` — `SpawnManager`, Docker/process providers +- `runtime/` — `runtimeSetup()`, config writing, dist patching +- `identity/` — model refs, agent names, soul/identity generation +- `auth/` — `convertCodexAuth()` +- Depends on `@relaycast/openclaw` (one-way only) + +## Migration Phases + +### Phase 1: Prepare `@relaycast/openclaw` — Size: **M** + +| Path | Action | What happens | +|---|---|---| +| `packages/relaycast-openclaw/package.json` | create | New package manifest, description "Relaycast/OpenClaw integration primitives", optional peer dep on `@agent-relay/openclaw` for transitional re-exports | +| `packages/relaycast-openclaw/tsconfig.json` | create | Standard TS build config (`rootDir: src`, `outDir: dist`) | +| `packages/relaycast-openclaw/src/types.ts` | create | `GatewayConfig`, `InboundMessage`, `DeliveryResult`, `RelaySenderLike`, `DEFAULT_OPENCLAW_GATEWAY_PORT` | +| `packages/relaycast-openclaw/src/config.ts` | create | `openclawHome`, `detectOpenClaw`, `hasValidConfig`, internal `resolveHome()`; explicit precedence (`OPENCLAW_CONFIG_PATH` > `OPENCLAW_HOME` > probe); throw on invalid env path; optional `configPath` on load/save; atomic temp-file+rename save | +| `packages/relaycast-openclaw/src/inject.ts` | create | `deliverMessage(message, clawName, sender)` with required `RelaySenderLike`; no fallback chain | +| `packages/relaycast-openclaw/src/index.ts` | create | Export primitives; add transitional `@deprecated` orchestration re-exports proxying to `@agent-relay/openclaw` | +| `packages/relaycast-openclaw/src/__tests__/config.test.ts` | create | Coverage for env precedence, invalid env-path throw, probe tie-breaks, explicit `configPath`, atomic write | +| `packages/relaycast-openclaw/src/__tests__/inject.test.ts` | create | Coverage for required sender contract and single-send behavior | +| `.dependency-cruiser.cjs` | create | Enforce no import from `@relaycast/openclaw` -> `@agent-relay/openclaw` (except transitional proxy) | +| `.github/workflows/` (CI files) | modify | Add boundary check command; include `packages/relaycast-openclaw/**` in path filters | + +**Version bump**: `@relaycast/openclaw` minor bump (e.g. `3.2.0`). No changes to `@agent-relay/openclaw` in this phase. + +### Phase 2: Update `@agent-relay/openclaw` — Size: **L** + +**Files deleted** (ownership moved to `@relaycast/openclaw`): + +| Path | Action | +|---|---| +| `packages/openclaw/src/config.ts` | delete | +| `packages/openclaw/src/types.ts` | delete | +| `packages/openclaw/src/inject.ts` | delete | + +**Files created/modified**: + +| Path | Action | What happens | +|---|---|---| +| `packages/openclaw/package.json` | modify | Add `@relaycast/openclaw` as direct dependency | +| `packages/openclaw/src/resolve-gateway-config-path.ts` | create | Encapsulates `workspace/relaycast/.env` derivation from `detectOpenClaw()` | +| `packages/openclaw/src/gateway.ts` | modify | Import primitives from `@relaycast/openclaw`; keep fallback strategy in `InboundGateway`; implement identity behavior (workspace-level key at `{openclawHome}/workspace/relaycast/.device-identity.json`, stable ID from `clawName+fingerprint`, OPENCLAW_HOME change warning, read-only ephemeral fallback) | +| `packages/openclaw/src/setup.ts` | modify | Import `detectOpenClaw`, `saveGatewayConfig`, types from `@relaycast/openclaw`; pass explicit path via `resolveGatewayConfigPath()` | +| `packages/openclaw/src/cli.ts` | modify | Import `loadGatewayConfig` from `@relaycast/openclaw`; use explicit config path | +| `packages/openclaw/src/spawn/docker.ts` | modify | Import `DEFAULT_OPENCLAW_GATEWAY_PORT` from `@relaycast/openclaw` | +| `packages/openclaw/src/index.ts` | modify | Remove local exports of moved symbols; re-export from `@relaycast/openclaw` with `@deprecated` (2 minor versions) | + +**Import rewrites**: +- `gateway.ts`: `./config.js` + `./types.js` -> `@relaycast/openclaw`; add `deliverMessage`, `RelaySenderLike` +- `setup.ts`: `./config.js` + `./types.js` -> `@relaycast/openclaw`; add `./resolve-gateway-config-path.js` +- `cli.ts`: `./config.js` -> `@relaycast/openclaw`; add `./resolve-gateway-config-path.js` +- `spawn/docker.ts`: `../types.js` -> `@relaycast/openclaw` + +**New tests**: +- `__tests__/resolve-gateway-config-path.test.ts` — path derivation for both variants +- `__tests__/gateway-delivery-strategy.test.ts` — relay single-send + fallback-to-WS +- `__tests__/device-identity.test.ts` — persistence path, OPENCLAW_HOME change, read-only fallback + +**Modified tests**: `gateway-control.test.ts`, `ws-client.test.ts` — update mocks for new import boundaries. + +**Verification**: `npm --workspace packages/openclaw run test` passes; `tsc --noEmit` clean for both packages. + +### Phase 3: Clean break (next major) — Size: **S** +- Remove deprecated orchestration re-exports from `@relaycast/openclaw` +- Remove deprecated primitive re-exports from `@agent-relay/openclaw` +- Drop optional peer dependency +- Ship codemod (`npx @relaycast/openclaw-migrate`) for remaining consumers + +**Verification checklist**: +1. `OPENCLAW_CONFIG_PATH` invalid path throws immediately (no silent fallback) +2. `OPENCLAW_CONFIG_PATH` valid file overrides `OPENCLAW_HOME`; `OPENCLAW_HOME` overrides probe +3. `saveGatewayConfig(..., configPath)` writes via temp+rename and preserves parseability +4. `relay-openclaw status` loads config via explicit `resolveGatewayConfigPath()` +5. Gateway delivery order: relay sender first; on failure, OpenClaw WS fallback +6. Identity file at `{openclawHome}/workspace/relaycast/.device-identity.json`; device ID stable by `clawName + key fingerprint` +7. Changing `OPENCLAW_HOME` regenerates identity and logs warning +8. Read-only identity path triggers warn-level ephemeral in-memory fallback +9. Deprecated re-exports compile from both packages during transition period + +## Risks and Mitigations + +| Risk | Severity | Mitigation | +|------|----------|------------| +| Breaking imports for existing consumers | High | Deprecated re-exports in both packages for 2 minor versions; codemod for automated migration | +| `deliverMessage` behavior drift during signature change | High | Fallback logic explicitly moved to `InboundGateway`; stateless primitive tested in isolation | +| Config path mismatch after `resolveGatewayConfigPath()` split | High | Explicit `configPath` parameter on load/save; `resolveGatewayConfigPath()` encapsulates convention | +| Device identity churn in Docker | Medium | Identity keyed on `clawName + fingerprint` (not filesystem path); ephemeral fallback for read-only mounts | +| Circular dependency accidentally introduced | Medium | `dependency-cruiser` rule + CI enforcement; `import type` for deprecated re-exports | +| `gateway.ts` monolith (1450 LOC) accumulates more logic | Medium | Post-split backlog: decompose into `delivery-strategy.ts`, `control-server.ts`, `device-identity.ts`, `realtime-router.ts` | +| Version skew between packages | Medium | Peer dependency version range; integration tests spanning both packages | + +## Recommended First PR + +**Phase 1 only**: Create `packages/relaycast-openclaw` with the primitives package (`config.ts`, `types.ts`, `inject.ts`). This PR: + +1. Adds the refined primitives with the new API surface (`resolveHome()` helper, explicit precedence, atomic write, `configPath` parameter, `RelaySenderLike` signature) +2. Includes unit tests for config detection, load/save, and stateless delivery +3. Adds deprecated re-exports of orchestration symbols (temporary, removed in Phase 3) +4. Does **not** touch `@agent-relay/openclaw` yet — that package continues working as-is +5. Adds `dependency-cruiser` config to enforce one-way dependency from the start + +This is the lowest-risk entry point: it publishes the new package without breaking existing consumers, and Phase 2 can proceed incrementally once Phase 1 is validated in production. diff --git a/packages/openclaw/skill/SKILL.md b/packages/openclaw/skill/SKILL.md index 59948e450..025a736f9 100644 --- a/packages/openclaw/skill/SKILL.md +++ b/packages/openclaw/skill/SKILL.md @@ -121,6 +121,36 @@ mcporter call relaycast.get_thread message_id=MSG_ID mcporter call relaycast.search_messages query="keyword" limit=10 ``` +### Testing Inbound Messages (Manual vs Auto-injection) + +Inbound can work in two modes. This distinction matters during bring-up. + +**1) Manual polling (works immediately once API is configured)** + +```bash +mcporter call relaycast.check_inbox +``` + +If this returns messages, Relaycast transport is working. + +**2) Auto-injection (requires continuously running gateway process)** + +Run the inbound gateway daemon so messages are pushed into your local OpenClaw session/UI stream: + +```bash +npx -y @agent-relay/openclaw@latest gateway +``` + +Run it under a supervisor/systemd/nohup in real deployments. + +Verify it is running: + +```bash +ps aux | grep 'relay-openclaw gateway' | grep -v grep +``` + +If manual polling works but auto-injection does not, focus on gateway process lifecycle/auth/profile alignment. + --- ## 6) Channels, Reactions, Agent Discovery diff --git a/packages/sdk/src/examples/workflows/openclaw-package-split.yaml b/packages/sdk/src/examples/workflows/openclaw-package-split.yaml new file mode 100644 index 000000000..f453a02f3 --- /dev/null +++ b/packages/sdk/src/examples/workflows/openclaw-package-split.yaml @@ -0,0 +1,377 @@ +version: "1.0" +name: openclaw-package-split +description: > + Multi-agent workflow to split OpenClaw integration into two packages with clear + boundaries: @relaycast/openclaw (SDK/bridge layer) and @agent-relay/openclaw + (orchestration layer). Uses reflection and consensus to validate the split design. +paths: + - name: relaycast + path: ../relaycast + description: "@relaycast/openclaw — SDK/bridge layer (config, types, bridge)" +swarm: + pattern: dag + maxConcurrency: 4 + timeoutMs: 5400000 + channel: wf-openclaw-split + idleNudge: + nudgeAfterMs: 120000 + escalateAfterMs: 120000 + maxNudges: 1 +agents: + - name: lead + cli: claude + role: "Owns the split design, sequences work, and resolves conflicts between reviewers" + - name: relaycast-analyst + cli: codex + workdir: relaycast + role: "Analyzes @relaycast/openclaw — what it owns today and what it should own" + interactive: false + - name: relay-analyst + cli: codex + role: "Analyzes @agent-relay/openclaw — identifies what should stay vs migrate to @relaycast/openclaw" + interactive: false + - name: boundary-reviewer + cli: codex + role: "Reviews proposed API boundary for leaky abstractions, circular deps, and breaking changes" + additionalPaths: ["../relaycast"] + interactive: false + - name: migration-planner + cli: codex + role: "Produces concrete file-level migration plan with dependency graph" + additionalPaths: ["../relaycast"] + interactive: false +workflows: + - name: package-split-design + description: "Analyze both packages, design boundary, review with reflection, produce migration plan" + onError: retry + preflight: + - command: test -d packages/openclaw/src && echo "relay openclaw found" || echo "MISSING" + description: "Verify @agent-relay/openclaw exists (project: relay)" + - command: test -d packages/openclaw/src && echo "relaycast openclaw found" || echo "MISSING" + description: "Verify @relaycast/openclaw exists (project: relaycast)" + steps: + # ── Wave 1: Deterministic context capture ────────────────────────── + + - name: capture-relay-openclaw + type: deterministic + command: | + echo "=== @agent-relay/openclaw package.json ===" + cat packages/openclaw/package.json + echo "" + echo "=== Source files ===" + find packages/openclaw/src -name '*.ts' -not -path '*/node_modules/*' -not -path '*/__tests__/*' | sort + echo "" + echo "=== Exports (index.ts) ===" + cat packages/openclaw/src/index.ts + echo "" + echo "=== Types ===" + cat packages/openclaw/src/types.ts + echo "" + echo "=== Config ===" + cat packages/openclaw/src/config.ts + echo "" + echo "=== LOC per file ===" + wc -l packages/openclaw/src/*.ts + captureOutput: true + + - name: capture-relaycast-openclaw + type: deterministic + workdir: relaycast + command: | + echo "=== @relaycast/openclaw package.json ===" + cat packages/openclaw/package.json + echo "" + echo "=== Source files ===" + find packages/openclaw/src -name '*.ts' -not -path '*/node_modules/*' -not -path '*/__tests__/*' | sort + echo "" + echo "=== Exports (index.ts) ===" + cat packages/openclaw/src/index.ts + echo "" + echo "=== Config ===" + cat packages/openclaw/src/config.ts + echo "" + echo "=== Bridge ===" + cat packages/openclaw/src/bridge.ts + echo "" + echo "=== LOC per file ===" + wc -l packages/openclaw/src/*.ts + captureOutput: true + + - name: capture-shared-imports + type: deterministic + command: | + echo "=== @agent-relay/openclaw imports from @relaycast/* ===" + grep -r '@relaycast/' packages/openclaw/src/ --include='*.ts' || echo "none" + echo "" + echo "=== @relaycast/openclaw imports from @agent-relay/* ===" + grep -r '@agent-relay/' ../relaycast/packages/openclaw/src/ --include='*.ts' || echo "none" + echo "" + echo "=== Duplicate function names across both ===" + grep -roh 'export.*function [a-zA-Z]*' packages/openclaw/src/ --include='*.ts' | sed 's/.*function //' | sort -u > /tmp/relay-fns.txt + grep -roh 'export.*function [a-zA-Z]*' ../relaycast/packages/openclaw/src/ --include='*.ts' | sed 's/.*function //' | sort -u > /tmp/relaycast-fns.txt + comm -12 /tmp/relay-fns.txt /tmp/relaycast-fns.txt || echo "none" + echo "" + echo "=== Duplicate interface/type names across both ===" + grep -roh 'export \(interface\|type\) [a-zA-Z]*' packages/openclaw/src/ --include='*.ts' | sed 's/.*\(interface\|type\) //' | sort -u > /tmp/relay-types.txt + grep -roh 'export \(interface\|type\) [a-zA-Z]*' ../relaycast/packages/openclaw/src/ --include='*.ts' | sed 's/.*\(interface\|type\) //' | sort -u > /tmp/relaycast-types.txt + comm -12 /tmp/relay-types.txt /tmp/relaycast-types.txt || echo "none" + rm -f /tmp/relay-fns.txt /tmp/relaycast-fns.txt /tmp/relay-types.txt /tmp/relaycast-types.txt + captureOutput: true + + # ── Wave 2: Parallel analysis ────────────────────────────────────── + + - name: analyze-relaycast + type: agent + agent: relaycast-analyst + dependsOn: [capture-relaycast-openclaw, capture-shared-imports] + task: | + Analyze the @relaycast/openclaw package and determine what it should own + from a separation-of-concerns perspective. + + @relaycast/openclaw should be the SDK/bridge layer: + - Config detection (openclawHome, detectOpenClaw) + - .env read/write for relaycast settings + - Basic OpenClaw bridge (lightweight, no orchestration) + - Shared types (GatewayConfig, OpenClawDetection) + + Current package contents: + {{steps.capture-relaycast-openclaw.output}} + + Cross-package imports and duplicates: + {{steps.capture-shared-imports.output}} + + Produce: + 1. What @relaycast/openclaw currently does well + 2. What's missing that @agent-relay/openclaw duplicates + 3. Proposed public API surface (exported functions, types, interfaces) + 4. Breaking changes required in the current @relaycast/openclaw API + + - name: analyze-relay + type: agent + agent: relay-analyst + dependsOn: [capture-relay-openclaw, capture-shared-imports] + task: | + Analyze the @agent-relay/openclaw package and determine what should stay + vs what should migrate to @relaycast/openclaw. + + @agent-relay/openclaw should be the orchestration layer: + - Gateway WS client with device identity persistence and pairing + - Spawn manager (Docker, process lifecycle) + - MCP server (spawn/list/release tools) + - Control API (HTTP server for spawn commands) + - Setup flow that imports config primitives from @relaycast/openclaw + + Current package contents: + {{steps.capture-relay-openclaw.output}} + + Cross-package imports and duplicates: + {{steps.capture-shared-imports.output}} + + Produce: + 1. Files/functions that should STAY in @agent-relay/openclaw + 2. Files/functions that should MIGRATE to @relaycast/openclaw + 3. New imports @agent-relay/openclaw would need from @relaycast/openclaw + 4. Risk assessment: what breaks during migration + + # ── Wave 3: Lead proposes ownership boundary ───────────────────── + # Focused task: just assign modules to packages. No migration plan yet. + + - name: propose-boundary + type: agent + agent: lead + dependsOn: [analyze-relaycast, analyze-relay] + task: | + Based on both analyses, assign each module/function/type to a package. + + @relaycast/openclaw analysis: + {{steps.analyze-relaycast.output}} + + @agent-relay/openclaw analysis: + {{steps.analyze-relay.output}} + + Constraints: + - @relaycast/openclaw = SDK/bridge layer (npm-published, used by relaycast.dev) + - @agent-relay/openclaw = orchestration layer (depends on @relaycast/openclaw) + - No circular deps. Shared types/config live in @relaycast/openclaw. + + Write a file packages/openclaw/BOUNDARY.md with exactly this structure: + + # Package Boundary + + ## @relaycast/openclaw owns + - [module/function/type per line] + + ## @agent-relay/openclaw owns + - [module/function/type per line] + + ## @agent-relay/openclaw imports from @relaycast/openclaw + - [specific import per line] + + ## Breaking changes + - [change per line, or "none"] + + Do NOT produce a migration plan. Just the boundary assignment. + When done, run: echo "BOUNDARY_PROPOSED" + verification: + type: output_contains + value: BOUNDARY_PROPOSED + + # ── Wave 3b: Scrub lead PTY output for downstream chaining ─────── + + - name: scrub-proposal + type: deterministic + dependsOn: [propose-boundary] + command: | + if [ -f packages/openclaw/BOUNDARY.md ]; then + cat packages/openclaw/BOUNDARY.md + else + echo "ERROR: BOUNDARY.md not found. The propose-boundary step must write this file." + exit 1 + fi + captureOutput: true + + # ── Wave 4: Independent boundary review ────────────────────────── + + - name: review-boundary + type: agent + agent: boundary-reviewer + dependsOn: [scrub-proposal] + task: | + Review this package boundary proposal for: + - Leaky abstractions (does @relaycast/openclaw expose orchestration details?) + - Circular dependency risk + - Breaking change impact on existing consumers + - Missing edge cases (Docker environments, OPENCLAW_HOME overrides, device identity) + - Whether the split actually simplifies both packages or just moves complexity + + Boundary proposal: + {{steps.scrub-proposal.output}} + + For each concern found, classify as: + - BLOCKER: must fix before migration + - WARNING: should fix but can defer + - NOTE: observation for future consideration + + If the boundary is sound, state what makes it solid. + If it has problems, propose specific fixes. + + # ── Wave 5: Lead resolves review feedback ──────────────────────── + # Small task: just address blockers/warnings against the clean boundary doc. + + - name: resolve-feedback + type: agent + agent: lead + dependsOn: [review-boundary, scrub-proposal] + task: | + Address the review feedback and update the boundary document. + + Current boundary (from BOUNDARY.md): + {{steps.scrub-proposal.output}} + + Review feedback: + {{steps.review-boundary.output}} + + For each BLOCKER: fix the boundary or explain why it's correct. + For each WARNING: acknowledge and note for migration. + + Update packages/openclaw/BOUNDARY.md with the resolved version. + Keep the same structure. Do not add a migration plan. + + When done, run: echo "BOUNDARY_FINALIZED" + verification: + type: output_contains + value: BOUNDARY_FINALIZED + + # ── Wave 5b: Scrub resolved boundary for downstream ───────────── + + - name: scrub-resolved + type: deterministic + dependsOn: [resolve-feedback] + command: | + if [ -f packages/openclaw/BOUNDARY.md ]; then + cat packages/openclaw/BOUNDARY.md + else + echo "ERROR: BOUNDARY.md not found. The resolve-feedback step must update this file." + exit 1 + fi + captureOutput: true + + # ── Wave 6: Concrete migration plan ────────────────────────────── + + - name: migration-plan + type: agent + agent: migration-planner + dependsOn: [scrub-resolved] + task: | + Produce a concrete, file-level migration plan based on this finalized + package boundary: + + {{steps.scrub-resolved.output}} + + The plan must include: + + 1. **Phase 1: Prepare @relaycast/openclaw** (no breaking changes yet) + - New files to create + - Functions/types to add or modify + - Version bump strategy + + 2. **Phase 2: Update @agent-relay/openclaw** (switch imports) + - Files that get deleted (moved to @relaycast/openclaw) + - Import statements that change + - package.json dependency addition + - Test updates + + 3. **Phase 3: Verify** (no new code) + - Type-check commands for both packages + - Test commands for both packages + - Manual verification steps + + 4. **Dependency graph** showing migration order + + For each file change, show the exact path and what happens to it + (create/move/delete/modify). + + # ── Wave 7: Lead writes final summary ─────────────────────────── + # Small task: read BOUNDARY.md, combine with migration plan, write summary file. + + - name: handoff + type: agent + agent: lead + dependsOn: [migration-plan, scrub-resolved] + task: | + You have two tasks: + 1. Read packages/openclaw/BOUNDARY.md (already exists on disk) + 2. Write packages/openclaw/PACKAGE_SPLIT_PLAN.md with this structure: + + # Package Split Plan + ## Overview (one paragraph) + ## Package Ownership (bullet points for each package) + ## Migration Phases (Phase 1/2/3 with S/M/L complexity) + ## Risks and Mitigations + ## Recommended First PR + + Use the boundary doc and this migration context: + {{steps.migration-plan.output}} + + IMPORTANT: You MUST create the file packages/openclaw/PACKAGE_SPLIT_PLAN.md. + After writing the file, run this exact command: echo "HANDOFF_DONE" + verification: + type: output_contains + value: HANDOFF_DONE +coordination: + barriers: + - name: analyses-complete + waitFor: [analyze-relaycast, analyze-relay] + timeoutMs: 600000 + - name: boundary-finalized + waitFor: [resolve-feedback] + timeoutMs: 900000 +state: + backend: memory + ttlMs: 86400000 + namespace: openclaw-split +errorHandling: + strategy: retry + maxRetries: 2 + retryDelayMs: 10000 + notifyChannel: wf-openclaw-split From f772a380c16c75a8bcc272c3287ef6539b536247 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:43:56 +0100 Subject: [PATCH 2/2] fix: correct preflight check path for relaycast project Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/examples/workflows/openclaw-package-split.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/examples/workflows/openclaw-package-split.yaml b/packages/sdk/src/examples/workflows/openclaw-package-split.yaml index f453a02f3..8cda5dc0c 100644 --- a/packages/sdk/src/examples/workflows/openclaw-package-split.yaml +++ b/packages/sdk/src/examples/workflows/openclaw-package-split.yaml @@ -47,7 +47,7 @@ workflows: preflight: - command: test -d packages/openclaw/src && echo "relay openclaw found" || echo "MISSING" description: "Verify @agent-relay/openclaw exists (project: relay)" - - command: test -d packages/openclaw/src && echo "relaycast openclaw found" || echo "MISSING" + - command: test -d ../relaycast/packages/openclaw/src && echo "relaycast openclaw found" || echo "MISSING" description: "Verify @relaycast/openclaw exists (project: relaycast)" steps: # ── Wave 1: Deterministic context capture ──────────────────────────