relay
A TypeScript monorepo that ships a CLI and a library for running multi-step Claude Code workflows that resume after crashes, bill through a subscription, and produce the same artifact every time.
1Overview
What this repository is and how it is organized at a glance.
Relay is a pnpm workspace organized into three library packages
(@relay/core, @relay/cli,
@relay/generator), one reference race package
(@ganderbite/relay-codebase-discovery), and two
example races (hello-world,
hello-world-mocked). The core library exposes a
typed DSL (defineRace, runner) and an
Orchestrator that executes races against a pluggable
Provider. The CLI is a thin orchestrator around the
library; the generator is a Claude Code skill that scaffolds new
race packages. The project targets Node 20.10+ and ships as
ESM-only.
2Inventory
Every package in the repository, its primary language, and its entry points.
| Package | Path | Language | Entry points |
|---|---|---|---|
@relay/core |
packages/core |
TypeScript | src/index.ts, src/testing/index.ts |
@relay/cli |
packages/cli |
TypeScript | src/cli.ts, bin/relay.js |
@relay/generator |
packages/generator |
TypeScript | src/install.ts, src/scaffold.ts, bin/install.js |
@ganderbite/flow-codebase-discovery |
packages/flows/codebase-discovery |
TypeScript | flow.ts |
hello-world |
examples/hello-world |
TypeScript | race.ts |
hello-world-mocked |
examples/hello-world-mocked |
TypeScript | race.ts, run-mocked.ts |
3Entities
Top-level named constructs — models, services, controllers, and utilities — grouped across packages.
| Name | Kind | Package | Description |
|---|---|---|---|
defineRace |
util | @relay/core |
Compiles a RaceSpec into an executable Race and validates the runner graph. Returns a Result with a RaceDefinitionError on failure. |
Runner |
service | @relay/core |
Orchestrates execution of a compiled race — resolves the DAG, executes runners against a Provider, writes checkpoints, and emits runner-level events. |
ClaudeProvider |
service | @relay/core |
Default provider. Wraps @anthropic-ai/claude-agent-sdk, translates SDK messages into typed InvocationEvents, and enforces the subscription-billing safety guard. |
MockProvider |
service | @relay/core |
Provider for tests and the hello-world-mocked example. Returns canned responses keyed by runner id. No network, no Claude binary, no credentials. |
BatonStore |
service | @relay/core |
Atomic reader and writer for the per-run batons/*.json files that carry structured context between runners. |
StateMachine |
model | @relay/core |
The serialized run-state (state.json) schema and the legal-transition rules that govern runner status (pending, running, succeeded, failed). |
PipelineError |
model | @relay/core |
Root of the typed error hierarchy. Concrete subclasses include ProviderAuthError, BatonSchemaError, StateCorruptError, and AtomicWriteError — each mapped to a distinct CLI exit code. |
relay command |
controller | @relay/cli |
Commander-based CLI entry. Subcommands live in src/commands/: run, resume, list, search, install, new, test, runs, doctor, upgrade. |
4Services
Cross-cutting runtime concerns that more than one package depends on or participates in.
| Service | Description | Used by |
|---|---|---|
Provider abstraction |
A single interface (Provider) with an async-iterator invoke contract. Lets the Runner stay unaware of whether Claude is being reached via the SDK, a subprocess, or a mock. The registry (defaultRegistry) binds provider ids to implementations. |
@relay/core, @relay/cli, hello-world-mocked |
Subscription-billing guard |
A pre-run check that refuses to start if ANTHROPIC_API_KEY is present in the environment without an explicit opt-in (--api-key or RELAY_ALLOW_API_KEY=1). Prevents a Max subscriber from silently routing tokens to a paid API account. Enforced in ClaudeProvider and surfaced by relay doctor. |
@relay/core, @relay/cli |
State persistence |
Atomic writes (atomicWriteJson, atomicWriteText) for state.json, batons/*.json, metrics.json, and the live/ directory. Tempfile-plus-rename under the hood so a crashed writer never leaves a partially written file a later resume would read. |
@relay/core, @relay/cli |
5Design review
Observations about the architecture: what patterns are present, where coupling is concentrated, which boundaries hold.
-
Layering
Clean dependency direction.
The CLI depends on core; core depends on nothing in the workspace. Race packages declare core as a
peerDependency, which is the right choice for distribution — a race installed against a newer core picks up fixes without re-publishing. -
Errors
Results over exceptions across core.
Fallible functions return
Result<T, PipelineError>vianeverthrowrather than throwing. The error hierarchy is domain-generic (ProviderAuthError,AtomicWriteError) and each subclass maps to a distinct exit code. Downstream code can pattern-match withouttry/catchceremony. -
Provider
The Provider boundary is the high-leverage seam.
ClaudeProviderandMockProvidersatisfy the same contract, which is what makeshello-world-mockedpossible and what keeps the test suite off the Claude binary. The translation layer from SDK messages to typedInvocationEvents is the one part of core that would need care when the SDK version changes. -
Coupling
CLI commands share conventions, not a framework.
Each file in
packages/cli/src/commands/owns its argument parsing, banner, and exit-code mapping. That keeps any one command easy to read, at the cost of some duplicated shape. The shared pieces that matter (banner rendering, progress display, input parsing, exit codes) already live in their own modules, which is the right place for the repetition to collapse if it gets expensive. -
Schemas
Batons are Zod-validated at every boundary.
Reference-race baton schemas live next to the race (
schemas/inventory.ts,schemas/entities.ts) and run against each runner's output before the next runner reads it. A malformed baton surfaces as a typedBatonSchemaErrorwith the exact issue path, not a downstreamundefined.
6Recommendations
Concrete, actionable changes worth considering — ordered by estimated payoff against effort.
-
High
Pin the Claude Agent SDK version tested in CI.
The SDK is declared as a
peerDependencyat0.2.112, which is correct for consumers but means the translation layer inClaudeProvideronly ever sees that exact version in practice. Add a scheduled CI job that installs the latest SDK patch against core's test suite so regressions surface before a user hits them. -
Medium
Extract a
CommandBasefor CLI subcommands.Each file in
packages/cli/src/commands/repeats the same shape: parse input, render the banner, run the operation, map errors to exit codes. A thin shared helper that accepts a handler and a banner config would remove about a third of the duplication without giving up per-command ownership of argument parsing. Worth doing oncenewandteststabilize. -
Low
Ship a race-package linter as part of the
relayCLI.The §7 format is currently enforced by convention. A
relay lint <dir>subcommand that checks therelaymetadata block, required README sections, and thedist/race.jsentry point would make the catalog onboarding story less fragile, and the same logic can back the registry generator for the static catalog site.