feat(sdk): expose the flows CLI as a mountable relay CLI surface - #451
Conversation
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
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 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. Comment |
Review swarm: maintainabilityNo fresh transcript was produced for run |
Review swarm: historyNo fresh transcript was produced for run |
Review swarm: structureNo fresh transcript was produced for run |
Review swarm: FAILED
Cloud run: |
`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
`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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
`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

Part of a cross-repo change mounting every Relay product into the
agent-relayCLI. This repo's half:
@relayflows/sdkgains a./relay-cliexport thatagent-relaymounts asagent-relay flows, without relay reimplementing asingle flows command.
What this does
@relayflows/sdk/relay-cliexportscreateRelayCliSurface()returning acontract-v1 surface.
rundelegates straight to therunClithis packagealready ships, and
commandsis projected from the sameCLI_VERBStableparseArgsdispatches on — so the declared tree and the dispatcher read fromone source rather than two that can rot apart. A drift test walks both.
runClino longer touches process state. Output routes through theinjected
CliIo, and it returns an exit code rather than exiting.packages/relayflows/bin/flows.jskeeps working unchanged on the same path.@agent-relay/cli-surfacewithout importingit, so the published package gains no runtime dependency on relay.
Why the declared tree matters
agent-relayrenders help from this spec rather than forwarding--helpto theproduct — forwarding would print
flows run ...when the user typedagent-relay flows run. That makescommandsuser-visible output, not aninternal 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 --cloudagent-relay node workflow run|logs|sync(pinned to@relayflows/cli1.0.1)→
agent-relay flows run|replay|synccloud schedule/scheduleshave no v2 equivalent —tickis a localcadence, 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.agent-relay flows --helplists all 14 commands, andagent-relay flows check <fixture>runs the real compiler toCHECK PASSED.Release
Nothing is published.
agent-relaypins the published version and currentlyreports "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-clisoagent-relaycan mount flows asagent-relay flows:createRelayCliSurface()projectsCLI_VERBSinto the host command tree and delegatesruntorunCli, with drift and contract tests so declared help and parsing stay aligned.runClinow accepts an optionalAbortSignal(no global SIGINT/SIGTERM when embedded); the standaloneflowsbinary behavior is unchanged.flows syncis extended to match documented Cloud behavior: runtime paths (.agent-bin/**, trajectories, relayfile state, etc.) are skipped viagit apply --excludeon both check and apply;--dry-runprints or JSON-embeds the patch; multi-path runs are inspectable in dry-run but applying stayssync_unsupported. Parser/help fixes includebuild --verify+--json, bundledeploy --json, andserve-webhookdefaulting--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 turnsflows syncinto a drop-in replacement foragent-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;rundelegates torunCli, andcommandsprojects from the sameCLI_VERBStable that gatesparseArgs.ParsedArgsvariant to be claimed by a declared verb, and vice versa.answer,schedule,schedules,unscheduleverbs and the--no-connectflag on the cloud-handoff verbs.runCliaccepts an optionalAbortSignal; with one supplied it installs no process signal handlers, while the standaloneflowsbinary keeps its SIGINT/SIGTERM behavior unchanged.@agent-relay/cli-surfacewithout importing it, so the published package gains no runtime dependency on relay.@agent-relay/cli-surfaceis a devDependency pinned to^12.2.4; the sibling-checkout alias was dropped so tests resolve the published package.Bug Fixes
flows syncexcludes the agent runtime's own bookkeeping paths from applied patches, reports them as skipped, and uses identical exclusions on bothgit applyinvocations.flows sync --dry-runprints the patch and applies nothing; under--jsonthe diff travels in thepatchfield.--dry-run; applying stays refused (sync_unsupported) since they target different repositories.serve-webhooknow defaults--data-dirlike the other verbs;build --verifyis an ordinary flag usable with--json(--outstays refused);deploy --jsonworks on the bundle form.run --cloudpasses the caller's signal through to the connect preflight, so submissions there no longer die with a ReferenceError and aborted submissions surface assubmission_aborted.patch_conflict.Written for commit d5f987c. Summary will update on new commits.