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.
  • lld: only for LDC release and static builds, which link with --linker=lld. The DMD dev loop does not need it.
  • 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 --linker=lld" 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). The link needs the lld package: ld.bfd older than binutils 2.36 (bullseye’s and Rocky 9’s 2.35) can split .tbss into two sections at the same address, so two thread-locals share storage. Verify the D runtime is statically linked in and the binary has a single .tbss:

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

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 libunwind → liblzma 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 pull request delivery suite drives the init, supervisor and courier through one delivering run against a local bare origin and a fake GitHub API (the mock agent’s serve mode). It asserts the clone keeps no push auth, the branch is pushed and the pull request opened, and a failed agent delivers nothing:

Terminal window
./scripts/itest-delivery.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

Two opt-in checks install a real agent CLI and need network:

SettingScriptWhat it adds
CTEST_CLAUDE=1scripts/ctest-initializer.shRuns the real Claude installer and checks claude lands in ~/.local/bin; skipped when downloads.claude.ai is unreachable.
CTEST_CODEX=1scripts/ctest-init-portable.shInstalls the pinned Codex CLI (codexCliVersion) on each target and checks codex --version reports codex-cli <codexCliVersion>.
Terminal window
CTEST_CODEX=1 TARGETS="ubuntu:24.04" ./scripts/ctest-init-portable.sh

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.