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.
The make targets
Section titled “The make targets”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:
| Target | Does |
|---|---|
make test | Unit tests for every package that has them. |
make build | Builds all runtime and codegen binaries. |
make itest | Host-level integration tests (no cluster, no docker). |
make itest-controller | Controller integration tests (needs kind/minikube + docker). |
make ctest | Cross-distro container tests (needs docker). |
make regen | Regenerates deploy/crds and the TypeScript contracts from the D model. |
make drift | Fails if either generated artifact drifted from the model. |
make contracts | Tests and builds the @re-cinq/agent-contracts npm package. |
make docs | Builds this documentation site. |
make hooks | Installs 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.
Prerequisites
Section titled “Prerequisites”- dub: the D package manager and build tool.
- A D compiler:
dmdworks out of the box; LDC (ldc2) is used for optimized release builds and for fully-static (musl) builds. - Node: only for
make contractsandmake docs(the npm package and this site).
From the repository root, make build builds everything. Individually:
dub build :controller # -> packages/controller/ai-agent-controllerdub build :initializer # -> packages/initializer/ai-agent-initdub build :supervisor # -> packages/supervisor/ai-agent-supervisordub build :crdgen # -> packages/crdgen/ai-agent-crdgendub build :tsgen # -> packages/tsgen/ai-agent-tsgendub build :controller --build=static --compiler=ldc2 # optimized releaseThe executables depend on ai-agent-subsystem:agentcore, which dub resolves locally as a
sub-package, no separate install step.
Generated artifacts
Section titled “Generated artifacts”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.
make regen # both, from the D modelTo 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.
Static linking
Section titled “Static linking”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
DFLAGS="-link-defaultlib-shared=false -L-lz" dub build :initializer --compiler=ldc2the 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:
ldd packages/controller/ai-agent-controller# libm.so.6, libgcc_s.so.1, libc.so.6, ld-linux, and no libphobos / libdruntimeAlpine 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:
make test # every package that has testsdub test :agentcore # just the libraryThe 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):
./scripts/itest-supervisor.shThe 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):
./scripts/itest-initializer.shCross-distro init-container tests
Section titled “Cross-distro init-container tests”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:
./scripts/ctest-initializer.sh # Debian/apt (default)BUILDER_IMAGE=fedora:40 RUNTIME_IMAGE=fedora:40 ./scripts/ctest-initializer.sh # dnfCI 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).
Cross-distro supervisor tests
Section titled “Cross-distro supervisor tests”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.
./scripts/ctest-supervisor.sh # all four glibc distrosTARGETS="debian:bookworm-slim" ./scripts/ctest-supervisor.sh # a subsetCI runs this on Rocky, Amazon Linux, Debian, and Ubuntu in the Supervisor container workflow
(.github/workflows/supervisor-container.yml).
Controller integration test
Section titled “Controller integration test”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:
./scripts/itest-controller.sh # kind (default)CLUSTER_TOOL=minikube CLUSTER=minikube ./scripts/itest-controller.sh # minikubeCI 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.