⚒ dwarves — the CI/build-server (continuum) — design
📍 THIS DOC LIVES IN
dwarves(moved fromnornir/.nornir/, 2026-07-22, move=delete). The code it documents is dwarves' own (crates/dwarves/src/…), so the doc follows the code. nornir keeps only the read-only viz surface that renders this engine's output. Source links below are dwarves-repo-relative.
Class:
design· Function axis:f0the umbrella (forge + scheduler → a running CI server) ·f1thebrokkrforge (re-exported) ·f2thesindrischeduler (cron / poll→enqueue / drain) ·f3the host backends dwarves owns (RealRunner / DemoRunner / engine / store) ·f4standalone tool + callable lib + server embed ·f5the read surface (5 facets +Tab::Brokkr+ CLI + badge + uiplane cell). Strict: this file states only what dwarves is. Sibling how-to:dwarves-guide.md; zero-knowledge walkthrough:dwarves-for-idiots.md. The FORGE half is documented in depth inbrokkr-design.md.
Named for the two dwarf-smith brothers who forged Mjölnir — Brokkr worked the bellows while Sindri directed the forge. Here brokkr owns the pipeline model and sindri schedules it; dwarves is the umbrella over both.
f0 — dwarves, the whole (crates/dwarves)
brokkr wrote and unit-tested every pure CI model — trigger firing, the build queue admission verdict, the agent pool, cron-shaped triggers, the poll bridge — but nothing ever drove them: there was no scheduler and no standalone binary. BuildQueue, AgentPool, deliver_with_retries, run_matrix, runs_for_report had no live caller. dwarves closes that gap. It is:
- the umbrella crate that re-exports
brokkr(the forge) and addssindri(the scheduler); - a standalone tool — the
dwarvesbinary (validate / plan / run / serve / trigger / history / badge / agent) that builds a pipeline without the full nornir binary; - a callable library nornir depends on by path (mirroring the
modgunnline) and delegates to — itsnornir::brokkrhost wiring re-exports dwarves, andnornir-serverembeds the sindri loop.
dwarves may depend on the dep-light backbones (brokkr, nornir-build-thing, nornir-jobs) but never on the full nornir crate — no cycle. Zero shell (LAW 7): every runner is a cargo/bollard/tunnr Rust API, argv-only.
| part | lives in | what it is |
f1 forge |
brokkr (re-exported dwarves::brokkr) |
pipeline-as-data model, descriptor, DAG, plan, run engine, 5 Elm facet cores |
f2 scheduler |
sindri.rs Sindri::tick |
cron eval → poll enqueue → drain the queue against the pool → dispatch |
f3 backends |
runner.rs RealRunner/DemoRunner; engine.rs run_pipeline_job; store.rs |
real cargo build (zero shell) / demo; the CI_BUILD job wrap; JSONL history |
f4 tool + embed |
main.rs; nornir-server.rs sindri_sweep |
standalone CLI; the server's poll→run CI leg |
f5 read surface |
src/viz/brokkr_tab.rs; nornir brokkr … |
5 facets, Tab::Brokkr (now a uiplane cell), CLI, badge |
f2 — sindri, the scheduler (the missing host driver)
sindri is the live loop that DRIVES brokkr's pure models. One Sindri::tick(now, report, runner, sink, warehouse) does, in order:
- cron —
enqueue_cron(now): every registered pipeline whose cron trigger matches this minute (and has not already fired it) enqueues a run. The pure core is [cron_matches] / [cron_next_fire] (a 5-fieldmin hour dom mon dowevaluator supporting*,*/n,a-b,a,b), unit-tested without a clock. - poll → enqueue —
enqueue_poll(report): given a [MonitorReport] (which projects' HEAD moved), [runs_for_report] enqueues one run per changed project that carries a poll trigger.MonitorReportis a dep-light stand-in for nornir'smonitor::FetchReportso sindri never depends back on nornir. - drain —
drain(runner, sink, warehouse): walk the FIFO queue, ask brokkr's pureadmitverdict whether each item may start now (a capable idle agent, past the exclusive gate), dispatch the admitted ones through [run_pipeline_job] (a trackedCI_BUILDjob) and historize them. Blocked items stay queued with a reason (RAGNARÖK: never a silent stall); the exclusive gate admits one exclusive build per drain.
Every tick returns a [TickReport] (cron_fired, enqueued, dispatched, blocked) — the observable outcome that drives the facets, the server log line, and the tests. Sindri::serve is the thin sleep-loop over tick; it is synchronous by design (no async runtime), so the standalone tool and the embedded server drive it identically.
The pure decisions (cron-next-fire, drain admission order, the exclusive-gate serialization) are unit-tested separately from the loop.
f4 — standalone + callable + embedded (the modgunn shape)
Exactly modgunn's split: src/lib.rs is the callable API (dwarves::…) and a [[bin]] dwarves (src/main.rs) is the standalone tool, with clap behind the default cli feature so a lean embedder (nornir) pulls the lib clap-free (default-features = false). nornir depends on it via a path dep (nornir/Cargo.toml) beside brokkr.
Server embed (Phase 0). nornir-server's poll sweep already computes which members' HEAD moved (server_sweep → rep.changed). The new sindri_sweep helper discovers each changed member's brokkr.toml, registers its pipeline, and ticks a Sindri once with a MonitorReport for the changed set — poll → enqueue → drain → dispatch through the job-tracked engine, historized to the served warehouse. This is what makes the running nornir-server a CI server.
f5 — the read surface
The five facett Elm cores (results / tree / DAG-as-MetroView / agents / log) live in the brokkr leaf and render in Tab::Brokkr; each keeps a state_json slice so the whole tab is drivable headless. Tab::Brokkr is now a declared uiplane cell (TAB_PANE_BLOCK → ("Brokkr","brokkr")) so the LAW-9 walk runs a green row for it instead of a silent hole. A self-driving robot scenario (tests/viz_brokkr_robot.rs robot_dwarves_run_lights_the_dag_stations_green) drives a run through the tab and asserts the DAG stations transition dark → lit green (visible movement).
Warehouse seam
History is JSONL under <warehouse>/brokkr/ (store.rs), the dep-light fallback. The rows are already Iceberg-shaped (PipelineRunRow / BuildRow), so the brokkr_runs / brokkr_steps Iceberg tables are a drop-in swap behind append_history / read_runs with no caller change.
Phase 2/3 seams left as TODO (deliberately out of Phase-0 scope)
Documented here so they are not silently forgotten:
- Real runner in the server loop.
sindri_sweepruns the toolchain-freeDemoRunner; swap inRealRunner(cargo/container) behind the same seam once agent isolation is wired. - Cron in the server. The server embed drives poll→drain; standalone
dwarves servedrives cron+drain. A persistent server-side cron evaluator (with durable last-fired state) is a follow-up. - Webhook receiver. Only poll + cron + manual are driven; an inbound webhook endpoint (
TriggerKind::Webhook) has no host receiver yet. - Change-based incremental builds + a content-addressed action cache (reuse
artifact::content_id) — skip steps whose inputs are unchanged. - Cross-project chains via
funnel-core::simulate_flowwaves + gate-steps ([[pipeline.depends]]is modelled + resolved inbrokkr::artifactbut not yet scheduled across pipelines). - Distributed remote agents — the
AgentPoolis in-process; a real fleet needs a remote agent transport behind the pool. - Iceberg history — swap the JSONL store for the
brokkr_runs/brokkr_stepstables (seam above).