Local development
The full development loop: the compose stack, the fake fleet, a kind cluster, the console and CLI offline, checks, and the measurement tools.
This is the full loop for working on Wingsmith. For the shortest path to a running stack, use the quickstart.
Prerequisites
You need just, Docker, uv, pnpm, Go and Python 3.12. kind, kubectl and helm are only needed for the cluster and chart recipes. Running just on its own lists every recipe.
git config core.hooksPath .githooks # once per clone: the pre-commit and commit-msg hooks
just setup # console packages, Python virtualenvs, Go modules
The local stack
just dev starts Postgres, migrations, the api and the hub in Docker Compose, then runs the console dev server beside them.
| Recipe | What it does |
|---|---|
just dev | The whole stack: compose services plus the console dev server. Needs just setup first. |
just dev-up | The compose services only (Postgres, migrations, api, hub), in the background. |
just logs | Follows the api and hub logs. Pass service names to follow others. |
just dev-down | Stops the stack and keeps the data volume. |
just seed | Creates the tenants every other flow starts from, over the API. --demo adds clusters, pools, quotas, workloads and an endpoint on the fake fleet. |
just reseed | Drops every table, migrates from scratch, and seeds. |
just fleet | Runs three fake clusters waiting to be enrolled. Name one to run just that cluster. --register skips the registration screen. |
| Process | Address |
|---|---|
| API | http://localhost:8000 |
| Hub | ws://localhost:8001/v1/hub/sync |
| Console | http://localhost:5273 |
| Postgres | localhost:5442 (hangar/hangar) |
How compose is set up
HANGAR_APP_ENV=local. Compose sets it, which turns admin authentication off and makes the admin password and session secret optional at boot. Anyone who can reach the api is an administrator. All ports are published on127.0.0.1only. Never expose this stack to a network. See Security.- Source is mounted. The plane’s
app,migrationsand the contract directory are mounted into the containers. The api runs with--reload, so an edit reloads it. A dependency change needs a rebuild, whichjust dev-updoes. - The hub does not reload. It holds long-lived operator sessions. Run
docker compose restart hubto pick up an edit. - The reactor runs inside the hub. It has no service of its own.
- The api serves the API only. The console comes from the dev server on
:5273, which proxies/v1to the api.
The fake fleet
The plane does not know what Kubernetes is, so a cluster is a socket that speaks the wire protocol and a device is a device.observed frame. just fleet starts three such clusters. Each waits for a cluster of its name to be registered, mints an enrolment token the way the console’s registration screen does, connects to the hub, and reports its hardware. It also answers desired deployments, so a workload goes ready locally.
Keep just fleet running: a cluster is only reachable while its session is up, and placement refuses an unreachable cluster. just seed --demo registers the fleet’s clusters and waits for them to report before it creates workloads, so start the fleet in another terminal first or right after.
The fleet renders nothing. To test whether a real Deployment comes out right, run the real operator against kind.
A kind cluster for the operator
just cluster-up # create the kind cluster "hangar" from dev/kind-cluster.yaml
just cluster-down # delete it
A kind cluster runs the operator but not a model, because it has no GPUs. From libs/operator, the Makefile builds and loads the operator:
| Command | What it does |
|---|---|
make all | Generate, manifests, build, test. |
make test | go test -race ./.... |
make vet | go vet, including the e2e and interop suites. |
make verify-manifests | Regenerates and fails if deploy/crd, deploy/rbac or the generated code changed. |
make kind-load deploy | Builds the image, loads it into the kind cluster, and applies the kustomize bundle. |
The CRDs and RBAC under deploy/ are generated from the kubebuilder markers by make manifests. Edit the markers, not the YAML. The chart keeps its own copy of the CRDs: run just chart-sync-crds after they change.
just e2e runs the reconciler against a real API server with a real garbage collector. It creates and deletes namespaces and briefly patches one node, so it only runs against a kind cluster unless you name another context in HANGAR_E2E_CONTEXT.
Point just e2e at a local kind cluster, never at one serving traffic.
The console without a backend
The console can run with no plane at all. Set VITE_API_MODE=fixtures (for example in libs/console/.env.local, copied from .env.example) and start the dev server:
cd libs/console
pnpm dev
An in-browser offline plane answers every request, and a Fixtures badge shows in the sidebar. Sign-in accepts any admin password without calling anything, and tenant sign-in needs a real plane. A screen that calls an endpoint the offline plane does not know fails with a not-found error, so add the fixture when you add the screen.
In the default http mode, the dev server proxies /v1 and /health to VITE_API_PROXY (default http://localhost:8000). Other console commands (pnpm test, pnpm lint, pnpm typecheck, pnpm build) are listed in Console.
The CLI without a plane
hgr --fake answers from an offline stand-in for the plane, seeded with four clusters in different phases. You can also select it with HANGAR_FAKE=1.
cd libs/cli && go build -o bin/hgr ./cmd/hgr
./bin/hgr --fake cluster ls
Set HANGAR_FAKE_STATE=/path/to/state.json to persist changes between commands, and HANGAR_FAKE_FAIL=NOT_FOUND (any code from hgr explain) to make every call fail with that code. To use the CLI against the compose stack instead:
./bin/hgr config set context.server http://localhost:8000
See hgr CLI for the commands.
Checks
| Command | What it runs | Needs |
|---|---|---|
just fmt | ruff, gofmt, prettier. --check verifies without writing. | Nothing |
just lint | ruff, go vet, eslint, tsc | Nothing |
just test | pytest, go test -race, vitest, plus the tests for hgrbench, hgrconform and dev/ | Nothing: no database, no cluster |
just test-db | The database-backed plane tests | Docker, or HANGAR_TEST_DATABASE_URL |
just interop | The real Go operator client against the real Python hub | uv, after just setup |
just chart-lint | Renders the chart offline and checks each required value is refused when empty | helm, uv |
just check | fmt --check, lint, test, test-db, chart-lint, interop, in order | Docker, helm |
Run just check before you open a pull request. CI runs the same checks on every pull request, including the database suite against a Postgres service. If you cannot run that suite locally, say so in the pull request and rely on CI.
A few more recipes are not part of just check, and CI runs them:
| Command | What it does |
|---|---|
just check-docs | Fails on a dead relative link, anchor or backticked path in the repository’s Markdown. |
just conformance-gateway | Runs the conformance probes through a locally built gateway against a stub engine. |
just e2e | The reconciler against a real API server. See above. |
just cov | Test coverage per component. --html writes browsable reports. |
Measurement tools
Three tools measure what you run. None of them needs the plane. For how to read their numbers, see Measurement method.
| Recipe | Tool | What it does |
|---|---|---|
just transit | hgrtransit | Ramps load through the real gateway, tunnel and agent on this host, with no GPU and no cluster. |
just transit-smoke | hgrtransit | One short ramp, JSON out. Fails if the path cannot carry it. |
just transit-live <url> <model> | hgrtransit | Ramps against a deployed gateway. Small by default. |
just bench | hgrbench | One AIPerf run from this machine against an endpoint, with an HTML report. |
just bench-suite smoke | hgrbench | A whole plan: four harnesses, one comparison report. smoke or deep. |
just bench-plans, just bench-harnesses | hgrbench | List the plans and the harnesses. |
just bench-report | hgrbench | Re-renders a run’s report from disk. Latest by default. |
just conformance <profile> | hgrconform | Observes an endpoint, optionally diffed against a reference with --reference <profile>. |
just conformance-probes | hgrconform | Lists every probe. |
just conformance-diff | hgrconform | Re-diffs a stored run without touching the endpoint. |
just conformance-test | hgrconform | Runs every probe against two noisy-but-correct stubs and fails on any difference. Run it after you write a probe. |
just bench takes its settings from BENCH_* environment variables, for example:
BENCH_MODEL=openai/gpt-oss-20b just bench
BENCH_MODEL=org/model just bench --dry-run
The load generator runs on your machine, so every number includes your own path to the endpoint. Read a local run for shape and for whether the path works, not as the cluster’s latency. just bench-suite deep --hard includes a saturation search that climbs load until the SLA breaks, so on a live endpoint it is a decision, not a default.
Images and the chart
just build <target> builds one image (plane, gateway, operator, agent or evalscope) and just push <target> pushes it. Both refuse a dirty working tree and tag the image with the commit unless you pass a tag. HANGAR_REGISTRY sets the registry, HANGAR_CHART_OCI sets where just chart-push publishes the chart, and HANGAR_PLATFORM sets the target platform (default linux/amd64). Nothing is published for you, so see Build the images for the install-side procedure.