# ⚒ dwarves — how-to guide
> **📍 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:** `guide` · how to *use* dwarves. What it is: [`dwarves-design.md`](dwarves-design.md). Zero-knowledge: [`dwarves-for-idiots.md`](dwarves-for-idiots.md). The forge details: [`brokkr-guide.md`](brokkr-guide.md).
dwarves = the **brokkr** pipeline forge + the **sindri** scheduler, shipped as a
standalone tool AND a library nornir embeds.
## Declare a pipeline
A repo declares its CI/build pipeline as data in `brokkr.toml` (schema:
[`brokkr-guide.md`](brokkr-guide.md)):
```toml
[pipeline]
name = "holger-ci"
[[pipeline.trigger]] # fire when the poll sweep sees HEAD move
kind = "poll"
events = ["push"]
[[pipeline.trigger]] # …and nightly
kind = "cron"
cron = "0 3 * * *"
[[pipeline.stage]]
name = "build"
[[pipeline.stage.step]]
name = "compile"
run = { variant = "server" } # real cargo build (zero shell) under --real
[[pipeline.stage]]
name = "test"
[[pipeline.stage.step]]
name = "unit"
needs = ["compile"]
run = { container = { name = "rust:1.85", cmd = ["cargo", "test"] } }
```
## The standalone `dwarves` tool
```bash
cargo build -p dwarves --bin dwarves # build the tool
dwarves # bare: startklar banner + readiness + menu
dwarves doctor # one-shot readiness (store reachable? exit!=0 if not)
dwarves --help # the verbs
dwarves validate # validate ./brokkr.toml
dwarves plan --matrix # topo-ordered step DAG + matrix cells
dwarves run # run through the demo runner, historize
dwarves run --real # `variant` steps build via cargo (zero shell)
dwarves run --matrix # fan over every matrix cell
dwarves trigger push --branch main # which triggers an event would fire
dwarves history --project holger # historized runs (JSONL under ./.nornir)
dwarves badge holger-ci # a status SVG badge for the latest run
dwarves agent ls # the build-agent registry
```
### `dwarves serve` — the scheduler daemon
`serve` runs the sindri loop: cron → poll-enqueue → drain → dispatch.
```bash
# One deterministic tick (demos + CI), injecting a HEAD-move for project "holger":
dwarves serve --once --project holger --poll holger
# sindri tick: 0 cron, 1 enqueued, 1 dispatched (1 green), 0 blocked
# ✓ holger-ci passed — 2/2 steps
# The real daemon: loop every 30s (cron + drain each tick):
dwarves serve --interval 30
```
Add `--json` to any verb for machine-readable output.
## Callable from nornir
nornir depends on dwarves by path and delegates: the `nornir brokkr …` verbs run
through dwarves's re-exported host wiring (`nornir::brokkr::{RealRunner, DemoRunner,
run_pipeline_job, store}` are now `dwarves::…`). Nothing about the `nornir brokkr`
surface changed.
```bash
nornir brokkr validate
nornir brokkr run --real
nornir brokkr history
```
## Embedded in the server (the running server IS a CI server)
`nornir-server`'s poll sweep drives sindri automatically: when a monitored member
that ships a poll-triggered `brokkr.toml` gets a HEAD-move, the server enqueues,
drains and runs its pipeline (`sindri_sweep`), historizing to the served warehouse
and tracking each run as a `CI_BUILD` job. Watch the server log for
`nornir-dwarves: <ws> sindri tick: …`.
## Call it as a library
```rust
use dwarves::{Sindri, RegisteredPipeline, MonitorReport, DemoRunner};
let pipeline = dwarves::discover_pipeline(repo_root)?.expect("a brokkr.toml");
let mut sindri = Sindri::new(vec![RegisteredPipeline::new("holger", pipeline)]);
let report = sindri.tick(
chrono::Utc::now(),
Some(&MonitorReport::changed(["holger"])),
&DemoRunner,
None, // Option<&nornir_jobs::JobSink>
warehouse,
);
assert_eq!(report.dispatched.len(), 1);
```
## Watch the UI move
The Brokkr tab drives a pipeline run and lights the DAG stations green:
```bash
# Headless robot proof (stations transition dark → green):
cargo test --features viz --test viz_brokkr_robot -- --nocapture
# Watch it live on the desktop:
DISPLAY=:0 cargo run --features viz --bin nornir -- viz # open the ⚒ Brokkr tab
```
### Launch the desktop client (`dwarves-ui`) — HOST-ONLY
`dwarves-ui` is a THIN client that knows exactly ONE server address: the dwarves forge.
nornir is embedded in the dwarves server (the host); the UI defers to that host and never
dials nornir itself — the nornir `:7878` port is for EXTERNAL nornir consumers only.
```bash
# The ONE launch command — DWARVES_SERVER is the sole knob (no NORNIR_SERVER):
DWARVES_SERVER=http://oden:7900 dwarves-ui
```
Do NOT set `NORNIR_SERVER` for the UI — the client ignores it. Warehouse/Mímir panes are
served THROUGH the host: the dwarves forge re-serves the `nornir.v1` READ surface
(`Warehouse.{Tables,Scan}` · `Workspaces.*` · the `Mimir.*` lens folds) on its OWN `:7900`
socket and forwards each call to its embedded nornir (`crate::warehouse_proxy`). So the
Warehouse navigator · Data view · Dupes · Coverage · 🔍 Lenses panes render LIVE rows
through the host with no `:7878` anywhere. A standalone (non-fat) server has no embedded
nornir, so a proxied read returns an honest `Unavailable` — never a silent empty, never a
direct `:7878` socket. (Proven end-to-end by `robot_warehouse_proxy_live.rs` /
`robot_lenses_live.rs` and their dead-upstream twin `robot_warehouse_dead_upstream.rs`.)
### The ⚒ Build-Thing tab — select & build anything, and watch korp from Windows
The ⚒ **Build Thing** tab reads every side-by-side checkout's `.nornir/build-variants.toml`
(under `$DWARVES_CONSTELLATION`, else the parent of the repo root) through build-thing's own
model and shows an HONEST roster of build cells + launch cells — never a blank/false-green
pane; an absent constellation reads as a named "NO CHECKOUTS" state.
**The words this tab uses.** *Build Thing* is the SYSTEM — the declarations plus the gate —
and never a name for one artifact. A **product** is what you click first (`gunnar` · `holger`
· `korp`). A **suite** is a tag (`ORIGINAL` · `MVP`) that filters which atoms are real
deliverables. An **atom** is what you click second: ONE declared buildable. Its **kind** is
its shape — `exe` · `service` · `container` · `iso` · `vm` · `installer`. A **param** is a
declared input (`--param hub_key=…`) and renders as a field in the pane — a `kind =
"text"` param landing as a `staged_file` (a hub.conf) renders as a MULTI-LINE editor, and a
param whose cell declares a **fixture** (`[build.installer.fixtures] hub_conf = "wg-hub"`)
gets an "använd fixtur (wg-hub)" checkbox: ticked, the pane supplies the CLI's own
`@fixture` word and the forge MINTS the fixture at build time (a userspace WireGuard hub,
no root, no `/etc/wireguard`); the result head then prints `param hub_conf ← fixture
wg-hub (pub=… endpoint=… key=…)` beside the receipt. The pane knows no fixture by name —
it renders whatever label the recipe declares, and nothing where it declares none. `[[build.variant]]`
stays the TOML section name for the `exe`/`service` kinds — it declares *one kind of atom*,
not the concept, which is why the UI says atom and the declaration says variant.
* **Build an atom.** Pick any declared `ArtifactKind × shape × target × features` atom
and press ▶ Build: it runs `nornir_build_thing::build_variant_observed` on one named worker
thread (never the UI thread) and reports the concrete outcome — artifact path + size, exit
status, elapsed, the compiler's diagnostics verbatim on failure, and the zero-egress verdict.
* **Launch cells + the `bootable_only` filter.** Container / vm / iso launch cells show under
the "endast bootbara" toggle; ▶ Run fires the real launcher and the pane reflects the true
phase (`passed`/`failed`), never a fabricated green.
* **Boot cell → ↻ Reboot + 🖥 RDP viewer.** A booted `rdp-wayland` guest exposes an in-pane
RDP viewer (`rdp_viewer.status`: unavailable → idle → connecting) and a live ↻ Reboot action
— no fabricated frame.
* **Build appliance → watch korp from Windows.** Select tunnr → the appliance picker → the
`rdp-korp-thin-container` cell (the korp-thin-rdp-viewer target: a thin `korp-ui` runs as
`/app` inside an RDP desktop, reaching a REMOTE korp-server on oden). ⚒ Build appliance
composes the cell's cargo payload layers into one OCI image and reports the two-part honest
status — `build: staged` (real, armed) and `boot: needs-runtime` — never a faked boot. From
Windows: `mstsc /v:<host>:3389`.
```bash
# Point the roster at the paved constellation and open the tab:
DWARVES_CONSTELLATION=$HOME/git DWARVES_SERVER=http://oden:7900 dwarves-ui
```
## Test it
```bash
cargo test -p dwarves # the lib (25 tests incl. sindri + cache)
cargo run -p dwarves --bin dwarves -- validate # the standalone tool
cargo test --features viz --test viz_brokkr_robot # the moving robot scenario
cargo build # the whole workspace
```