ADR-0031: One Cairn Binary — The Server Is cairn serve
Context and Problem Statement
Cairn builds two Go binaries: cairn (./cmd/cairn, the CLI, a pure REST
client) and cairnd (./cmd/cairnd, the server). Releases publish both, to a
separate public repo (stump-wtf/cairn-cli) that exists only because the
binaries do, and the Homebrew tap renders its formula from that repo's assets.
Every other service in the fleet — switchboard serve plus operator
subcommands, harness daemon plus its CLI — is one binary.
Joe, 09/29: "Cairn shouldn't have a separate CLI. Go is nice because it's a single binary paradigm."
The costs of the split are real and recurring: a second release destination
whose only content is cairn's own binaries; a tap formula that must be told
which of two repos to render from; a verify-cli gate (scripts/
verify-cli-artifact.sh) whose whole job is keeping the server out of the CLI
binary — a dependency invariant that becomes false the moment the split ends;
and an image entrypoint (cairnd) that is one more name operators must know.
The benefit was decoupling the CLI's download size and release cadence from the
server. With the source MIT and the whole product one repository, that benefit
no longer buys enough to pay for the machinery.
Decision
One binary: cairn. The server becomes a subcommand, cairn serve,
matching switchboard serve. Every existing CLI command stays exactly where it
is — cairn add, cairn login, and the root pipe-and-file ingest are
untouched.
- Command surface.
cairn serveruns the server.cairn serve --helpdocuments it. Operator subcommands that the specs design forcairnd(search reindex, retention admin) arecairn <subcommand>under the same tree; the specs are amended to say so. - Config and env compatibility (the hard requirement). Every
CAIRN_*environment variable and every flagcairndaccepts works undercairn serveunchanged. Configuration continues to come from the environment only;cairn servetakes no flags of its own beyond the standard--help/--version, so a deployment's env is the contract and does not move. - Exit behaviour.
cairn servekeeps cairnd's exit contract: startup failures exit non-zero with the same error text, SIGTERM/SIGINT shut down gracefully, and the process logs the same structured JSON lines. - Image. The container image ships the single
cairnbinary. The image also ships acairndshim — a tiny executable thatexecscairn serve "$@"— and keepscairndas the default ENTRYPOINT, so every existing deployment (the StumpCloud edge stack runs the image with no command override; self-hosters' compose files name the service, not the command) continues to start the server without a change. Removing the shim is a later, separately-announced breaking change. - Release destination. goreleaser publishes one build (
cairn) fromstump-wtf/cairnitself — the same repo the source lives in — and the tap formula renders from those assets. Thecairnd_<ver>_<os>_<arch>archives stop being published.stump-wtf/cairn-cliis retired afterwards (by Joe, not the release pipeline). - The import-graph CLI gate is retired with the split. scripts/verify-cli-artifact.sh enforced "the shipped CLI contains no server code". That invariant is not merely relaxed — it is reversed: the shipped binary is supposed to contain the server. The gate is removed in the same change that folds the binaries in, and the archive-composition check (scripts/verify-release-archives.sh) is narrowed to the one remaining archive shape.
Consequences
- Easier to get. One download, one tap formula source, one name. The release notes stop needing a "which download do I want" header with two answers.
- Larger CLI download. The binary now carries the server (embedded docs, migrations, the HTTP stack) — a few MB. This is the accepted trade; the CLI is no longer pretending to be separable from the product.
- The
cairndname deprecates, in stages: (1) this release, the image keeps thecairndshim and entrypoint so deploys are untouched; (2) after the v0.3.0 image has shipped everywhere, StumpCloud's Ansible flips to an explicitcommand: ["serve"]-style invocation and the shim is removed; (3)stump-wtf/cairn-cliis archived. Nativecairndarchive users move tocairn servewith no config change — the env contract is identical. - One ADR-0020 promise is deliberately broken. ADR-0020 chose to embed the
docs in the server binary and explicitly kept the CLI separate, on the
release-cadence argument. This ADR supersedes that clause; the docs
embedding itself stands, now served by
cairn serve. - ADR-0003's "pure network client" clause narrows. The CLI's commands
remain pure REST clients with no domain logic (SPEC-0008 stands); what
changes is that the binary also carries the server behind
serve. Surface parity is unaffected — the parity claim was always about semantics, not linkage. - Specs move with the decision. Every spec that names
cairndas the process or as a command prefix is amended in the same change that lands this: the server process is "the cairn server (cairn serve)" and the operator surface iscairn <subcommand>.