Skip to content

Building

The monorepo is a single dub project; every sub-package lives under packages/: the agentcore library, the controller, initializer, and supervisor executables, and the crdgen / tsgen code generators.

A Makefile at the repository root is the task surface, and CI runs these same targets — a green make test build drift itest locally reproduces the main CI job. make help lists them:

TargetDoes
make testUnit tests for every package that has them.
make buildBuilds all runtime and codegen binaries.
make itestHost-level integration tests (no cluster, no docker).
make itest-controllerController integration tests (needs kind/minikube + docker).
make ctestCross-distro container tests (needs docker).
make regenRegenerates deploy/crds and the TypeScript contracts from the D model.
make driftFails if either generated artifact drifted from the model.
make contractsTests and builds the @re-cinq/agent-contracts npm package.
make docsBuilds this documentation site.
make hooksInstalls the git pre-push hook that runs the drift checks.

CONTRIBUTING.md covers the pinned toolchain and the contribution workflow. The rest of this page is what the targets do underneath, and why.

  • dub: the D package manager and build tool.
  • A D compiler: dmd works out of the box; LDC (ldc2) is used for optimized release builds and for fully-static (musl) builds.
  • Node: only for make contracts and make docs (the npm package and this site).

From the repository root, make build builds everything. Individually:

Terminal window
dub build :controller # -> packages/controller/ai-agent-controller
dub build :initializer # -> packages/initializer/ai-agent-init
dub build :supervisor # -> packages/supervisor/ai-agent-supervisor
dub build :crdgen # -> packages/crdgen/ai-agent-crdgen
dub build :tsgen # -> packages/tsgen/ai-agent-tsgen
dub build :controller --build=static --compiler=ldc2 # optimized release

The executables depend on ai-agent-subsystem:agentcore, which dub resolves locally as a sub-package, no separate install step.

Two artifacts are generated from the annotated structs in packages/agentcore/source/agentcore/crds, not hand-written — the CRD manifests in deploy/crds (via crdgen, which introspects the model with describe-d and emits OpenAPI schemas using open-api’s vocabulary) and the TypeScript types the @re-cinq/agent-contracts npm package publishes (via tsgen). One model, two consumers, so a Kubernetes schema and a TypeScript caller can never disagree about a field.

Terminal window
make regen # both, from the D model

To change either, edit the struct and its attributes (@Description, @wire, @Required, @Minimum, @PrinterColumn, …) and regenerate. make drift runs both checks (scripts/check-crd-drift.sh and scripts/check-contracts-drift.sh); each regenerates into a temp dir and diffs against what is committed, failing if it drifted. CI runs them, and make hooks installs a pre-push hook that runs them before you can push.

A third check, scripts/check-contracts-version.sh, runs on a v* tag and fails it when packages/agent-contracts/package.json does not match the tag — the npm version is committed rather than derived, so without it a release PR that forgot the bump republishes an existing version and npm rejects it with an error that reads like an auth failure.

The goal is binaries that ship with no D runtime dependency — only the system C library stays dynamic, and any glibc-based image (which the injected runtime already requires) provides it. The default dub + LDC build links the D runtime (druntime + Phobos) shared, so the static link is requested explicitly. Built on an old glibc base (e.g. debian:bullseye, glibc 2.31) with

Terminal window
DFLAGS="-link-defaultlib-shared=false -L-lz" dub build :initializer --compiler=ldc2

the binary runs unchanged on every glibc-based Kubernetes distro (Debian, Ubuntu, the RHEL family, Amazon Linux) because it only needs a baseline glibc (and libz/libgcc_s, present everywhere). Verify the D runtime is statically linked in:

Terminal window
ldd packages/controller/ai-agent-controller
# libm.so.6, libgcc_s.so.1, libc.so.6, ld-linux, and no libphobos / libdruntime

Alpine is musl, not glibc, so a glibc binary can’t run there; it is built natively on Alpine instead. A fully-static musl binary is not used: LDC’s musl static link drags in libunwindliblzma and is brittle, so the portable-glibc + native-Alpine split is the CI strategy (see Cross-distro init-container tests).

The pure logic in agentcore (prompt rendering, the reconcile state machine, the CRD model and its attribute metadata) is unit-tested with D’s built-in unittest blocks, asserting via fluent-asserts (value.should.equal(…)). It is scoped to a unittest dub configuration, so the shipped binaries link none of it:

Terminal window
make test # every package that has tests
dub test :agentcore # just the library

The supervisor’s end-to-end behaviour (streaming, file/http sinks, signal forwarding, exit-code passthrough, and robustness against an agent that leaves a child holding stdout) is covered by an integration suite that runs the real binary against a configurable mock agent (ai-agent-mock):

Terminal window
./scripts/itest-supervisor.sh

The initializer’s host suite runs the real ai-agent-init against a local repo, covering the clone, idempotent re-runs, lifecycle notifications, and private-repo token auth (asserting the token never leaks to a sink):

Terminal window
./scripts/itest-initializer.sh

The init container self-bootstraps its prerequisites through the distro package manager, so it is also exercised inside real minimal images where git is absent, proving it installs git via the detected package manager and clones for real:

Terminal window
./scripts/ctest-initializer.sh # Debian/apt (default)
BUILDER_IMAGE=fedora:40 RUNTIME_IMAGE=fedora:40 ./scripts/ctest-initializer.sh # dnf

CI runs this in the Init container workflow (.github/workflows/init-container.yml) across the top Kubernetes base distros: Debian, Ubuntu, Rocky, and Amazon Linux (one shared glibc build) plus Alpine (built natively on musl).

The supervisor runs inside the Station’s (glibc) image, so its integration suite is also run inside each glibc base distro. Because the supervisor links vibe-d (and so libssl.so.3), the stack is built once on Rocky 9: the oldest glibc (2.34) and openssl 3 common to every glibc distro, and fanned out, carrying the ldc runtime libs and installing libssl3 where a base image lacks it. Alpine is not a target: the supervisor requires glibc.

Terminal window
./scripts/ctest-supervisor.sh # all four glibc distros
TARGETS="debian:bookworm-slim" ./scripts/ctest-supervisor.sh # a subset

CI runs this on Rocky, Amazon Linux, Debian, and Ubuntu in the Supervisor container workflow (.github/workflows/supervisor-container.yml).

The controller is exercised end to end on a real cluster, hermetically (no API key, no network in the run pod), with the agent CLI swapped for a deterministic mock. It drives Agents through two scenarios: (A) controller wiring — Job creation with an owner reference, status moving Pending → Running → Succeeded, status.output/exitCode enrichment, and owner-reference garbage collection; and (B) the credential path — an agent-secrets Secret injected as ANTHROPIC_API_KEY via secretKeyRef reaches the agent child (kubelet → pod → supervisor → child), proven with a fake key the mock asserts before it succeeds:

Terminal window
./scripts/itest-controller.sh # kind (default)
CLUSTER_TOOL=minikube CLUSTER=minikube ./scripts/itest-controller.sh # minikube

CI runs this on kind in the Controller container workflow (.github/workflows/controller.yml). On some very new host kernels kind’s containerd balloons the init container’s memory and OOM-kills it before the run starts (it does not reproduce under plain Docker or in CI); on such hosts run it with CLUSTER_TOOL=minikube (the docker runtime), which is unaffected. The real-CLI half of the credential path (an actual Claude authentication) is a manual repro documented under Launch an agent, kept out of CI so no job spends real API credits.