Skip to content

feat(sdk): expose the flows CLI as a mountable relay CLI surface - #451

Merged
AgentRelayBot merged 9 commits into
mainfrom
feat/relay-cli-surface
Sep 18, 2026
Merged

AgentRelayBot merged 9 commits into
mainfrom
feat/relay-cli-surface

Conversation

@AgentRelayBot

@AgentRelayBot AgentRelayBot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Part of a cross-repo change mounting every Relay product into the agent-relay
CLI. This repo's half: @relayflows/sdk gains a ./relay-cli export that
agent-relay mounts as agent-relay flows, without relay reimplementing a
single flows command.

What this does

  • @relayflows/sdk/relay-cli exports createRelayCliSurface() returning a
    contract-v1 surface. run delegates straight to the runCli this package
    already ships, and commands is projected from the same CLI_VERBS table
    parseArgs dispatches on — so the declared tree and the dispatcher read from
    one source rather than two that can rot apart. A drift test walks both.
  • runCli no longer touches process state. Output routes through the
    injected CliIo, and it returns an exit code rather than exiting.
    packages/relayflows/bin/flows.js keeps working unchanged on the same path.
  • Structurally typed against @agent-relay/cli-surface without importing
    it, so the published package gains no runtime dependency on relay.

Why the declared tree matters

agent-relay renders help from this spec rather than forwarding --help to the
product — forwarding would print flows run ... when the user typed
agent-relay flows run. That makes commands user-visible output, not an
internal detail, so descriptions and flags are written to be read.

v1 → v2 mapping

relay is deprecating its v1 relayflows surface against this one. The scoping
that came out of it:

  • agent-relay cloud run --relayflow-version v1 → agent-relay flows run --cloud
  • agent-relay node workflow run|logs|sync (pinned to @relayflows/cli 1.0.1)
    → agent-relay flows run|replay|sync
  • cloud schedule/schedules have no v2 equivalent — tick is a local
    cadence, not hosted scheduling — so relay keeps them supported and visible
    rather than pointing at a command that cannot do the job.

Verification

  • tests/relay-cli-surface.test.ts — 29 tests.
  • Verified mounted: agent-relay flows --help lists all 14 commands, and
    agent-relay flows check <fixture> runs the real compiler to CHECK PASSED.

Release

Nothing is published. agent-relay pins the published version and currently
reports "upgrade @relayflows/sdk" until this ships.

🤖 Generated with Claude Code


Note

Medium Risk
Changes cloud sync patch application (exclusions, multi-path, dry-run) and embeddable CLI cancellation; mistakes could skip or mis-apply agent output in local trees, though behavior is heavily tested.

Overview
Adds @relayflows/sdk/relay-cli so agent-relay can mount flows as agent-relay flows: createRelayCliSurface() projects CLI_VERBS into the host command tree and delegates run to runCli, with drift and contract tests so declared help and parsing stay aligned. runCli now accepts an optional AbortSignal (no global SIGINT/SIGTERM when embedded); the standalone flows binary behavior is unchanged.

flows sync is extended to match documented Cloud behavior: runtime paths (.agent-bin/**, trajectories, relayfile state, etc.) are skipped via git apply --exclude on both check and apply; --dry-run prints or JSON-embeds the patch; multi-path runs are inspectable in dry-run but applying stays sync_unsupported. Parser/help fixes include build --verify + --json, bundle deploy --json, and serve-webhook defaulting --data-dir.

Reviewed by Cursor Bugbot for commit d5f987c. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Exposes the flows CLI as a mountable relay CLI surface (agent-relay flows) and turns flows sync into a drop-in replacement for agent-relay cloud sync. The parsers now accept what the command surface advertises, so users following the host's help no longer hit exit 2.

New Features

  • createRelayCliSurface() returns a contract-v1 surface; run delegates to runCli, and commands projects from the same CLI_VERBS table that gates parseArgs.
  • Compile-time assertions force every ParsedArgs variant to be claimed by a declared verb, and vice versa.
  • The surface claims the answer, schedule, schedules, unschedule verbs and the --no-connect flag on the cloud-handoff verbs.
  • runCli accepts an optional AbortSignal; with one supplied it installs no process signal handlers, while the standalone flows binary keeps its SIGINT/SIGTERM behavior unchanged.
  • The surface is structurally typed against @agent-relay/cli-surface without importing it, so the published package gains no runtime dependency on relay.
  • @agent-relay/cli-surface is a devDependency pinned to ^12.2.4; the sibling-checkout alias was dropped so tests resolve the published package.

Bug Fixes

  • flows sync excludes the agent runtime's own bookkeeping paths from applied patches, reports them as skipped, and uses identical exclusions on both git apply invocations.
  • flows sync --dry-run prints the patch and applies nothing; under --json the diff travels in the patch field.
  • Multi-path runs are shown per path with --dry-run; applying stays refused (sync_unsupported) since they target different repositories.
  • serve-webhook now defaults --data-dir like the other verbs; build --verify is an ordinary flag usable with --json (--out stays refused); deploy --json works on the bundle form.
  • run --cloud passes the caller's signal through to the connect preflight, so submissions there no longer die with a ReferenceError and aborted submissions surface as submission_aborted.
  • An empty multi-path patch now reports NO CHANGES instead of patch_conflict.

Written for commit d5f987c. Summary will update on new commits.

Review in cubic

Adds `@relayflows/sdk/relay-cli`, the surface `agent-relay` mounts as
`agent-relay flows`. It wraps the CLI this package already ships rather
than reimplementing any of it: `run` delegates to `runCli`, and
`packages/relayflows/bin/flows.js` keeps calling `runCli` on the same
path, unchanged.

Single source for the command tree. `src/cli-commands.ts` holds one
`CLI_VERBS` table that both gates `parseArgs`' verb dispatch and is
projected into `surface.commands`, so the declared tree and the
dispatched tree cannot describe different things. A compile-time
assertion ties the table's declared variants to `ParsedArgs['command']`
in both directions: adding a variant to the union without claiming it
in the table fails `tsc`, not just the test.

Contract audit. `runCli` already returned an exit code and wrote only
through the injected `CliIo`; the one real violation was global signal
handlers, installed in five places (hn-monitor, tick, cloud-run,
serve-webhook, check --watch). Those are now threaded through one
`withInterrupt` helper and an optional `RunCliOptions.signal`. Passing a
signal installs nothing, which is what the surface does; passing none
keeps the standalone binary owning SIGINT exactly as before, and a test
proves that half with a real emitted signal.

The contract package is linked dev-only through tsconfig `paths` and a
vitest alias instead of a `file:` devDependency: npm records a `file:`
path in package-lock.json, and `checkInstalledVersions` in
bundle-typescript.ts rejects non-portable lockfile entries, which would
break every standalone TS bundle build. The tests still run the real
`assertSurfaceConforms`/`walkCommands`, and `dist/relay-cli.d.ts` stays
self-contained, so the published package gains no dependency on relay.

Tests: conformance against the real contract, a drift test covering both
directions, and an E2E that runs a declarative flow and an authored
.flow.ts on the real relayflowd kernel through `surface.run`, asserting
the exit code, the progress written to the injected io, and the file the
run actually wrote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: 318c0079-049c-468e-872e-2878cedbd37a
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 32e4e318-4d0d-4421-9899-e26ca98f9897


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/sdk/src/cli-commands.ts
Comment thread packages/sdk/src/cli-commands.ts
Comment thread packages/sdk/src/cli-commands.ts
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review swarm: maintainability

No fresh transcript was produced for run b55e8bf2-1b69-47f4-bbe2-f5644bdabfcc (MISSING).

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review swarm: history

No fresh transcript was produced for run b55e8bf2-1b69-47f4-bbe2-f5644bdabfcc (MISSING).

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review swarm: structure

No fresh transcript was produced for run b55e8bf2-1b69-47f4-bbe2-f5644bdabfcc (MISSING).

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review swarm: FAILED

  • maintainability: MISSING
  • history: MISSING
  • structure: MISSING

Cloud run: b55e8bf2-1b69-47f4-bbe2-f5644bdabfcc

`agent-relay cloud sync` is to delegate here, and `flows sync` already claims
to replace it. It was not a drop-in: three behaviours lived only in relay, and
one of them is a safety property, so v2 users were exposed too.

Path exclusions. The sandbox commits its baseline before the run, so the
agent runtime's own bookkeeping inside a synced tree -- `.agent-bin/**`,
`.relayfile.acl`, `.relayfile-mount-state.json` and its `.tmp-*` temporaries,
`.trajectories/**`, `.workflow-context/**` -- shows up in the post-run diff.
Applying that verbatim drags trajectory records and agent binaries into the
user's checkout and overwrites the mount state of the tree being synced into.
`CLOUD_SYNC_PATCH_EXCLUDES` is now the single home for the list, and
`applyCloudPatch` takes an `exclude` option that defaults to it. Both `git
apply` invocations carry the identical arguments: a `--check` without them
answers a different question than the apply, passing on an excluded hunk that
is never written or failing on one and refusing a patch whose applied part was
clean. `applyCloudPatch` now returns what it wrote and what it dropped, so
`flows sync` can report both instead of listing paths it did not touch.

Dry run. `flows sync --dry-run` prints the patch and applies nothing. Under
`--json` the diff travels in the payload's `patch` field rather than loose on
stdout beside it, so a consumer still parses one object.

Multi-path patches. The `/patch` route branches on the run's `paths`, not on
`relayflowVersion`, so a v2 `--sync-code` run that submits several paths gets
`{ patches: { <name>: ... } }` too -- this is not a v1 shape. `CloudPatchSet`
and `downloadCloudPatchSet` model both; `downloadCloudPatch` keeps refusing the
multi-path one unchanged. `flows sync --dry-run` shows each path's patch;
applying stays refused, because they target different repositories and no
single `--dir` is the right destination.

Tests are real: patches touching every excluded pattern are applied by actual
`git apply` and the files are asserted absent from disk, the matcher backing
the report is pinned to `git apply --numstat` for the same patterns, and the
excluded-conflict case proves the check and the apply agree. The surface drift
test grew a flag check -- every declared boolean option must be accepted by at
least one declared invocation of its verb -- so a switch advertised to the host
that the parser refuses now fails there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: b45ad9f9-470a-495c-a8bf-e9346524097b

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/sdk/src/cli/cloud-sync.ts
agentrelaybot added 2 commits September 17, 2026 16:01
`tests/relay-cli-surface.test.ts` imports the contract package to assert the
surface satisfies it, but nothing declared the dependency — it resolved only
where a sibling checkout happened to provide it, and CI could not load the test.
Now pinned to the published 12.2.2.

devDependency only: `src/relay-cli.ts` remains structurally typed, so the
published package still has no runtime dependency on relay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916
…rtises

Four behavioural findings from the #451 review. Three are one class: the
declared spec advertises flags the parser refuses, and `agent-relay` renders
help FROM that spec, so each one is a user following help into an exit 2. In
every case the parser was the wrong half -- the combination help promises is
the better UX -- so the parser moved and the spec only narrowed where a
combination genuinely describes nothing.

serve-webhook. `DATA_DIR_OPTION` carries `.relayflowd` as its default and every
other verb honours it; `parseWebhookArgs` alone required the flag. It now
defaults like the rest. `--port` stays required and the spec says so.

build. `--verify` was a leading mode token accepting exactly one positional and
never `--json`, so `build <dir> --verify` and `build --verify --json <dir>` --
both implied by a spec that lists the three as peers -- refused. It is now an
ordinary flag. `--out` with `--verify` is the one pair still refused (a verify
writes nothing, so a destination would be silently ignored) and the option's
description says so rather than leaving the reader to find out at exit 2.

deploy. `--json` is declared on the verb both forms share; only the hosted
listener parser accepted it. The bundle form accepts it now and emits one
object per outcome, refusals included.

cloud sync. `runCloudSyncCli` took a single-tree result's `hasChanges` at face
value. The multi-path branch discounts an empty body and so did `agent-relay
cloud sync`, which this command replaces; without the guard an empty patch
reached `git apply` and failed as `patch_conflict` instead of NO CHANGES.

The drift test grows three assertions so the class cannot recur: every declared
option -- value-taking ones included -- must be carried by an invocation that
parses, every option declaring a default must be omittable from one, and every
command must parse exactly its declared positional arity with each required
positional actually required. Reverting any of the four fixes fails a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: 0156272a-dc1d-44ab-99cf-720666eb754e

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7b610ea. Configure here.

Comment thread packages/sdk/src/cli/build.ts
agentrelaybot added 5 commits September 17, 2026 23:03
`tests/relay-cli-surface.test.ts` imports the contract package but nothing
declared it, so it resolved only where a sibling checkout happened to provide
one. Relay's 12.2.4 release publishes a working tarball (the earlier 12.2.2 held
only package.json), so this pins ^12.2.4 and records it in the lockfile.

devDependency only: src/relay-cli.ts stays structurally typed, so the published
package still carries no runtime dependency on relay.

npm ci clean; 90 tests passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916
`vitest.config.ts` aliased `@agent-relay/cli-surface` to
`../../../relay/packages/cli-surface/dist/index.js` -- a sibling clone of
the relay repo -- and `tsconfig.tests.json` mirrored it in `paths`. Both
predate the package becoming a real registry devDependency, and neither was
removed when it did.

The alias is unconditional, so vitest rewrites the bare specifier to that
absolute path before any node_modules lookup. On a machine that happens to
have the relay repo checked out next door it resolves and the suite is
green; on a GitHub runner the path does not exist, vite's alias plugin
fails to resolve it, and vite-node reports the failure against the original
id:

  Cannot find module '@agent-relay/cli-surface' imported from
    packages/sdk/tests/relay-cli-surface.test.ts

The installed devDependency was never consulted. The tsconfig `paths` entry
was harmless only because tsc falls back to node resolution when a mapping
misses, which is why typecheck:tests stayed green while vitest did not.

Removing both makes the tests resolve the published 12.2.4 devDependency the
lockfile already pins, which is what the registry move was for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916
…urface

# Conflicts:
#	packages/sdk/src/cli.ts

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916
`ensureFlowConnections` was handed `controller.signal`, a name that does not
exist on this path -- the local AbortController it came from belongs to
`runCli`, not to `runCloudCli`, which receives the already-derived `signal`.
A `run --cloud` that reached the connect preflight died with a ReferenceError
instead of submitting, and an aborted submission surfaced as that error rather
than as the `submission_aborted` the classifier below it names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916
…surface

The rebase brought four verbs onto `parseArgs` that `CLI_VERBS` did not claim,
so `_EveryVariantIsDeclared` stopped compiling -- the table's whole point.
Declare them, with the help text `agent-relay flows --help` will render:

- `answer <run-id> <wait-id> yes|no` records a person's decision on a parked
  `f.human`; it continues nothing itself, so the description names the
  `flows resume` that does. `--note`, `--by`, `--data-dir` and `--no-spawn`
  are the parser's, and `--by` documents its OS-user default.
- `schedule <flow>` registers a Cloud cron or `--every` cadence; with neither
  flag the flow's own `schedule.*` handler supplies the cron, which is the
  part of the contract a user cannot guess, so `--cron` says it.
- `schedules` and `unschedule <schedule-id>` mirror the wording of their
  `deployments` / `undeploy` neighbours.

`--no-connect` is declared too, on the three verbs that hand a flow to Cloud
(`deploy`, `run --cloud`, `schedule`). The parser has accepted it since the
integration-connect prompts landed; only help had not caught up, which is the
same drift in the direction the type assertion cannot see.

Every new verb gets sample invocations in the drift test rather than any
narrowing of it: the suite still requires each declared verb to be routable,
each declared flag to be carried by an invocation that parses, each default to
be omittable, and each declared positional to be genuinely required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916

Session-Id: d458bd97-53d8-4f02-be9c-48b67b93c916
@AgentRelayBot
AgentRelayBot merged commit caaa629 into main Sep 18, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant