Software dark factory written in Elixir/Phoenix LiveView. Given a spec, Kiln ships working software end-to-end with no human intervention — safely, visibly, and durably.
See .planning/PROJECT.md for the full vision and constraints.
Requires: Docker with Compose v2. No Elixir install needed.
docker compose upOpen http://localhost:4000 — you'll land on the onboarding flow. Before your first live run visit /settings to connect providers and GitHub auth.
First start takes 2–3 min to fetch and compile deps; subsequent starts are fast (deps are cached in a named volume).
Host Elixir path (faster inner loop): if you have Elixir/OTP installed (
asdf installfrom.tool-versions), you can skip Docker for the app and runbash script/dev_up.shinstead — Postgres still runs in Docker, Phoenix runs on your machine.
Kiln’s parallelism grain for factory work is per active run at the
Kiln.Runs.RunDirector scan: runs are ordered with round-robin among the
current active set, with a stable tie-break on inserted_at then
run_id (lexicographic on the UUID string). A fair_cursor in the
director remembers the last successfully spawned run so the next scan starts
after it — this is admission order, not a global multi-node scheduler.
Telemetry — run queued dwell
When a run successfully leaves :queued, the app emits a single Telemetry
measurement on [:kiln, :run, :scheduling, :queued, :stop] (the
run_queued dwell signal) with
duration in integer milliseconds of wall-clock time since
inserted_at (v1 uses inserted_at as the queued-start proxy). Metadata is
whitelisted (run_id, next_state, correlation_id). Do not attach
run_id as a Prometheus / Telemetry.Metrics tag in KilnWeb.Telemetry
— that would explode cardinality; keep this signal event-first for
operators and tests.
Three different “waits” (D-16)
- Run queued dwell — time the row spends in
:queuedbefore a successful transition out (signal above). - Oban queue time — time a job waits between insert and execution (Oban’s own telemetry / job timestamps).
- Ecto pool queue time — time a caller waits to checkout a DB connection from the pool (repo query telemetry).
Weighted fair-share and cross-node scheduling charts are out of scope for v1 (deferred to later milestones).
Operator docs and landing page (Astro + Starlight) are built from site/ and published to https://szTheory.github.io/kiln/ when GitHub Pages is enabled. See CONTRIBUTING.md for how to edit the site and optional DOCS=1 mix docs.verify checks.
- Elixir
~> 1.19and OTP~> 28(see.tool-versionsfor exact pins used in development). - Docker with the Compose v2 plugin (Docker Desktop or Docker Engine +
docker compose). - asdf (optional) — only if you manage Erlang/Elixir through
.tool-versions. If you install Elixir another way, ensuremixis on yourPATH; the integration script does not runasdf installfor you. - direnv (optional) — convenient for loading
.env; you canset -a; source .env; set +ainstead.
Fastest path (one command): from the repo root, run bash script/dev_up.sh (or just dev if you use just) — starts Compose Postgres, runs mix setup, then mix phx.server in the foreground (Ctrl+C stops the server). Uses .env (creates from .env.sample if missing). Same host-port rules as below if 5432 is taken (KILN_DB_HOST_PORT + matching DATABASE_URL).
Compose vs host app: docker compose brings up Postgres (and optionally DTU, OTel/Jaeger — see Traces). The Phoenix app runs on your machine via mix phx.server (Elixir/OTP per .tool-versions). There is no Kiln app service in Compose. For v0.2.0, Phase 12 ships an optional checked-in justfile that names the same primitives as this quick start — host Phoenix plus Compose for the data plane only (see Optional: Just recipes below and .planning/research/LOCAL-DX-AUDIT.md). Phase 21 adds an optional .devcontainer/ path (same Compose data plane; BEAM may run inside the container) — see Optional: Dev Container below. No Compose-hosted Kiln app service is required for either path.
- Environment —
cp .env.sample .envthen load it (direnv allowor export vars manually). See Environment below for required keys. - Database —
docker compose up -d dband wait until Postgres is healthy. - Migrations (owner role) —
KILN_DB_ROLE=kiln_owner mix setup(runsecto.create,ecto.migrate, seeds, assets). Runtime sessions use the restrictedkiln_approle by default. - Run the app —
mix phx.server. - Open the app —
http://localhost:4000/onboardingfor the demo-first orientation, thenhttp://localhost:4000/settingsbefore your first real live run./settingsis the authoritative readiness checklist and remediation surface for local live mode.
Sandbox stages talk to DTU (mock HTTP) on Compose’s internal kiln-sandbox network (internal: true in compose.yaml — no egress to the public internet from that bridge). The quick start above only starts Postgres so you can reach the UI quickly.
Before your first sandbox-backed stage, start DTU and wait until it is healthy:
docker compose up -d dtu
docker compose ps dtuSee service definitions in compose.yaml (db, dtu, otel-collector, jaeger). Optional sandbox-net-anchor profile exists for advanced local networking — not required for the default README path.
Pull requests targeting main need green GitHub Actions. Full tier table, boot checks, integration smoke, and optional local commands: .planning/PROJECT.md#merge-authority. Local mix check may read PARTIAL vs CI when Postgres, Docker, Dialyzer PLTs, or env differ — see .planning/phases/12-local-docker-dx/12-01-SUMMARY.md.
http://localhost:4000/ops/dashboard— Phoenix LiveDashboardhttp://localhost:4000/ops/oban— Oban.Webhttp://localhost:4000/health— JSON health probe (Plan 06 contract)
Use this as a cold-clone sanity pass (order matches the happy path above):
- Tooling — Elixir/OTP per
.tool-versions, Docker with Compose v2,mixonPATH(see Prerequisites). - Secrets file —
cp .env.sample .env; fill at leastSECRET_KEY_BASE,DATABASE_URL,PORT/PHX_HOSTas in Environment below. - Database —
docker compose up -d db; Postgres shows healthy indocker compose ps. - Port 5432 — If another Postgres or container holds host
5432, setKILN_DB_HOST_PORT(e.g.5434) in.envand pointDATABASE_URLat the same host port; see.env.sample(seetest/integration/first_run.sherror text). - Migrations —
KILN_DB_ROLE=kiln_owner mix setuponce (creates DB if needed, migrates, seeds, assets). Day-to-day runs leaveKILN_DB_ROLEunset (kiln_app). - App —
mix phx.serverwithoutKILN_SKIP_BOOTCHECKS(BootChecks must pass). - Orientation — Open
/onboardingfirst if you want the demo-first tour and scenario framing. - Live readiness — Open
/settingsbefore any real local live attempt; this is the canonical checklist for provider refs,gh auth, and Docker readiness. - Sandbox work — Before stages that hit mocks:
docker compose up -d dtu(subsection above). - Machine smoke (optional) —
bash test/integration/first_run.shormix integration.first_run— DB + migrate + boot +/healthJSON (does not prove browser onboarding). - Traces (optional) — See Traces (local); set
OTEL_EXPORTER_OTLP_ENDPOINTonly when collector/Jaeger are up.
Why Compose does not start Kiln: shipped layout is Postgres + DTU (+ optional OTel) in Compose, Phoenix on the host — see .planning/research/LOCAL-DX-AUDIT.md. Optional just orchestration lives in the repo root justfile (same contracts as this checklist).
Longer-form operator docs (architecture, configuration) live in the Starlight site — Operator docs — built from site/ per Documentation above.
Use this when you want a reproducible Linux toolchain inside Docker and are fine with bind-mount + volume tradeoffs (often slower file sync than host Phoenix — prefer Docker Desktop VirtioFS or the fastest file-sharing mode your engine offers; if live reload misbehaves, run mix phx.server on the host instead).
Prerequisites: Docker Desktop or Colima with the Compose v2 plugin, the Dev Containers extension (VS Code / compatible editors), and the same .env contract as the host path (cp .env.sample .env).
Same logical sequence as the quick start (only where BEAM runs changes):
- On the host, bring up the data plane:
docker compose up -d db(adddtubefore any sandbox-backed stage:docker compose up -d dtu). - Open the repo in the dev container (see
.devcontainer/— image pin matches.tool-versions). - Inside the container:
KILN_DB_ROLE=kiln_owner mix setup(Postgres must be reachable; on macOS Docker Desktop the defaultDATABASE_URLin the devcontainer useshost.docker.internalas the DB host). Colima / Podman: setDOCKER_HOSTto your engine’s socket (see your engine docs); keepkiln-sandbox+dtuon the same daemon Kiln targets or you will see “network not found” / unreachable DTU. KILN_DEV_BIND_ALL=1is preset in the devcontainer so Bandit listens on0.0.0.0andhttp://localhost:4000from the host reaches the app.- Run
mix phx.server, then openhttp://localhost:4000/onboarding(same as host).
DooD (Docker outside of Docker): the orchestrator may talk to the host Docker daemon (DOCKER_HOST or a socket visible only to the devcontainer) so System.cmd("docker", …) and DTU-backed sandboxes keep using kiln-sandbox on that daemon. Never mount the Docker socket (or inject DOCKER_HOST) into Kiln-spawned stage/sandbox containers — sandboxes stay isolated per CLAUDE.md.
If you use just (brew install just on macOS), the checked-in justfile wraps the same Compose + setup primitives as the numbered quick start above. just dev runs script/dev_up.sh (Postgres + mix setup + mix phx.server in one foreground process). Other recipes still assume Phoenix on the host unless you use just dev.
| Command | What it runs |
|---|---|
just dev |
script/dev_up.sh — Postgres + mix setup + mix phx.server (foreground) |
just db-up |
docker compose up -d db |
just dtu-up |
docker compose up -d dtu |
just otel-up |
docker compose up -d otel-collector jaeger (see Traces (local)) |
just setup |
KILN_DB_ROLE=kiln_owner mix setup |
just smoke |
bash test/integration/first_run.sh (same SSOT as Integration smoke) |
just dev-deps |
db-up, then prints a one-line reminder to start mix phx.server in another shell |
just planning-gates |
script/planning_gates.sh — CI-parity mix check only (defaults match .github/workflows/ci.yml; Postgres must be reachable) |
just shift-left |
script/shift_left_verify.sh — mix check, test/integration/first_run.sh, then mix kiln.e2e (full local mirror of CI acceptance) |
just precommit |
script/precommit.sh — same env defaults as CI when .env is missing; then mix precommit (templates.verify + mix check) |
just before-plan-phase 12 |
Runs shift-left, then prints /gsd-plan-phase 12 --gaps for GSD gap closure |
config/runtime.exs reads all environment variables (T-02). .env.sample lists the keys required for a normal dev boot:
DATABASE_URL,SECRET_KEY_BASE,PHX_HOST,PORT- Optional providers:
ANTHROPIC_API_KEY,OPENAI_API_KEY,GOOGLE_API_KEY,OLLAMA_HOST - Optional GitHub automation:
GH_TOKEN, dogfood vars (KILN_DOGFOOD_*) - Optional observability:
OTEL_EXPORTER_OTLP_ENDPOINT(see Traces (local)) KILN_DB_ROLE— leave unset for day-to-day app runs (kiln_app). Set tokiln_owneronly for migrations / DDL (KILN_DB_ROLE=kiln_owner mix ecto.migrate).
- Workspace: export
KILN_DOGFOOD_WORKSPACE=/absolute/path/to/your/clonebefore running shell scenarios whosecwdtargets the external Rust repo (not required while scenarios still usemixagainst this tree). - Workflow on disk:
priv/workflows/rust_gb_dogfood_v1.yaml— idrust_gb_dogfood_v1(load viaKiln.Workflows.load/1; the/workflowsLiveView lists workflows discovered from disk when that path is wired in your deploy). - Spec:
priv/dogfood/gb_vertical_slice_spec.md— threekiln-scenarioentries with argv-onlymixsteps today; swapargvtocargo test --workspace --locked(andcwdunderKILN_DOGFOOD_WORKSPACE) once the throwaway repo exists so CI matches the operator clone (D-1105).
| Step | Human | Automated in CI / scripts |
|---|---|---|
Create .env from .env.sample |
Yes | No |
gh auth login / GitHub App install for private automation |
Yes (when using GH features) | No |
Vendor API keys (ANTHROPIC_*, etc.) |
Yes | No (CI uses placeholders) |
docker compose up -d db |
Yes (local) | No (Actions uses a service container instead of compose) |
mix check on push / PR |
N/A | Yes (.github/workflows/ci.yml) |
mix check + boot checks |
N/A | Yes on main; tag pushes run the tag vs mix.exs version gate |
bash test/integration/first_run.sh / mix integration.first_run |
N/A | Yes (integration-smoke job) |
mix kiln.e2e / mix shift_left.verify UI path |
N/A | Yes (e2e job; local shift-left mirrors CI) |
With the stack running:
docker compose up -d otel-collector jaeger
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
mix phx.serverOpen Jaeger UI at http://localhost:16686. Omit OTEL_EXPORTER_OTLP_ENDPOINT to keep the SDK in noop mode.
Kiln's remote profile keeps the dashboard private over Tailscale and leaves the default local compose path unchanged.
- Create a Tailscale auth key in the admin console.
- Put it in
.envasTS_AUTHKEY(see.env.sample). - Start your host Phoenix app as usual (
mix phx.server). - Start the tunnel sidecar:
docker compose --profile remote up -d tailscaleBy default the sidecar serves http://host.docker.internal:4000 on your tailnet MagicDNS name. If you run Kiln on a different local port, set TAILSCALE_TUNNEL_TARGET in .env before starting the remote profile.
mix check # format + compile + test + credo + dialyzer + sobelow + mix_audit + xref + boot checks
mix test --stale # fast inner loopSSOT command (DB + migrate + host boot + /health JSON — does not hit /onboarding; see Human-required vs automated):
bash test/integration/first_run.shMix-discoverable alias (same script, no duplicated orchestration):
mix integration.first_runHeader comments in test/integration/first_run.sh match this README: asdf is not invoked — the script assumes docker, jq, curl, lsof, and mix are already on PATH per the prerequisites above.
Three layers of UI verification ship in CI on every PR. For UI flows covered here, this stack is the acceptance oracle; routine human UAT is not part of phase closure:
mix check— includestest/kiln_web/live/route_smoke_test.exs(every LiveView route boots + no retired Phase-reskin tokens in rendered HTML) andmix kiln.ui.lint(static grep gate onlib/kiln_web/**andassets/css/app.css).bash test/integration/first_run.sh— Compose DB + host Phoenix +/healthcontract.mix kiln.e2e— Playwright: all 14 LiveView routes x light/dark x mobile/desktop + axe-core a11y. Runs locally against the same boot script CI uses.
Local one-liners:
mix shift_left.verify # steps 1 + 2 + 3
just shift-left # same, Just recipe
just e2e # just the Playwright step (boots Phoenix for you)
just e2e-ui # Playwright watch UIEnv escape hatches: SHIFT_LEFT_SKIP_INTEGRATION=1 (step 1 only), SHIFT_LEFT_SKIP_E2E=1 (steps 1+2).
Use $gsd-verify-work only for typed exceptions the automation cannot cover yet: first-time auth, credentials, budget approvals, third-party blockers, or other explicitly documented human-only checks.
Plan 03 introduces a two-role Postgres model (kiln_owner owns DDL, kiln_app is the runtime role). Migrations must run as kiln_owner:
KILN_DB_ROLE=kiln_owner mix ecto.migrateSee config/runtime.exs for the KILN_DB_ROLE switch.
Kiln.BootChecks.run!/0 asserts durability-floor invariants at boot. For local debugging only:
KILN_SKIP_BOOTCHECKS=1 mix phx.serverNever use in production.
Licensed under the Apache License, Version 2.0 — see LICENSE and NOTICE.