# ⚒ dwarves — the CI/build-server (continuum) — design
> **📍 THIS DOC LIVES IN `dwarves` (moved from `nornir/.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:** `f0` the umbrella (forge + scheduler → a running CI server) · `f1` the `brokkr` forge (re-exported) · `f2` the `sindri` scheduler (cron / poll→enqueue / drain) · `f3` the host backends dwarves owns (RealRunner / DemoRunner / engine / store) · `f4` standalone tool + callable lib + server embed · `f5` the read surface (5 facets + `Tab::Brokkr` + CLI + badge + uiplane cell).
> Strict: this file states *only what dwarves is*. Sibling how-to: [`dwarves-guide.md`](dwarves-guide.md); zero-knowledge walkthrough: [`dwarves-for-idiots.md`](dwarves-for-idiots.md). The FORGE half is documented in depth in [`brokkr-design.md`](brokkr-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`](../crates/brokkr/) (the forge)
and adds [`sindri`](../crates/dwarves/src/sindri.rs) (the scheduler);
- a **standalone tool** — the `dwarves` binary (`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 `modgunn` line)
and delegates to — its `nornir::brokkr` host wiring re-exports dwarves, and
`nornir-server` embeds 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`](../crates/brokkr/) (re-exported `dwarves::brokkr`) | pipeline-as-data model, descriptor, DAG, plan, run engine, 5 Elm facet cores |
| `f2` scheduler | [`sindri.rs`](../crates/dwarves/src/sindri.rs) `Sindri::tick` | cron eval → poll enqueue → drain the queue against the pool → dispatch |
| `f3` backends | [`runner.rs`](../crates/dwarves/src/runner.rs) `RealRunner`/`DemoRunner`; [`engine.rs`](../crates/dwarves/src/engine.rs) `run_pipeline_job`; [`store.rs`](../crates/dwarves/src/store.rs) | real cargo build (zero shell) / demo; the `CI_BUILD` job wrap; JSONL history |
| `f4` tool + embed | [`main.rs`](../crates/dwarves/src/main.rs); `nornir-server.rs` `sindri_sweep` | standalone CLI; the server's poll→run CI leg |
| `f5` read surface | [`src/viz/brokkr_tab.rs`](../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)`](../crates/dwarves/src/sindri.rs)
does, in order:
1. **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-field `min hour dom mon dow`
evaluator supporting `*`, `*/n`, `a-b`, `a,b`), unit-tested without a clock.
2. **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. `MonitorReport` is a dep-light stand-in for
nornir's `monitor::FetchReport` so sindri never depends back on nornir.
3. **drain** — `drain(runner, sink, warehouse)`: walk the FIFO queue, ask brokkr's
pure [`admit`](../crates/brokkr/src/queue.rs) verdict whether each item may
start now (a capable idle agent, past the exclusive gate), **dispatch** the
admitted ones through [`run_pipeline_job`] (a tracked `CI_BUILD` job) 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`](../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_sweep` runs the toolchain-free
`DemoRunner`; swap in `RealRunner` (cargo/container) behind the same seam once
agent isolation is wired.
- **Cron in the server.** The server embed drives poll→drain; standalone `dwarves
serve` drives 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_flow` waves + **gate-steps**
(`[[pipeline.depends]]` is modelled + resolved in `brokkr::artifact` but not yet
scheduled across pipelines).
- **Distributed remote agents** — the `AgentPool` is in-process; a real fleet
needs a remote agent transport behind the pool.
- **Iceberg history** — swap the JSONL store for the `brokkr_runs`/`brokkr_steps`
tables (seam above).