<!-- nornir:class: design -->
# dwarves — the parity spec (server · thin/fat client · UI · funnel)
> **What this is.** The concrete, buildable spec that turned `dwarves` from "a forge
> library + a stub CLI + a demo window" into a **first-class nordisk app at parity
> with nornir**. As of 2026-07-13 the server core has LANDED: its own `dwarf.v1`
> gRPC server (D1 ✅), a thin/fat client (D2 ✅), the shared jera ledger (D3 ✅), its
> own repo (D4 ✅), and the skidbladnir systemd seam (L1 ✅ for serve/daemonize; the
> deploy/rollback RPCs remain open). The live **funnel** is wired for read *and*
> write. Remaining: promote the demo window into the real UI, and the L1 lifecycle
> RPCs. It consolidates the decisions already filed and makes them actionable:
> - the symmetry model + D/L backlog — [workspace_nordisk/.nornir/nornir-dwarf-symmetry.md](../../workspace_nordisk/.nornir/nornir-dwarf-symmetry.md)
> - standalone service + cross-embed + wasm — [ui-embedding-and-standalone.md](ui-embedding-and-standalone.md)
> - the four shared seams (skidbladnir/jera/draupnir/facett-embed) — [workspace_nordisk/.nornir/consolidation-panels-maps-design.md](../../workspace_nordisk/.nornir/consolidation-panels-maps-design.md)
> - the thin/fat rule to copy verbatim — [nornir/.nornir/cli-client-modes.md](../../nornir/.nornir/cli-client-modes.md)
> - CI/CD market inspiration (what dwarves borrows; jera multi-machine sketch) — [cicd-market-inspiration.md](cicd-market-inspiration.md)
>
> **Naming:** the project is `dwarves` everywhere (crate, lib, `dwarves` command, repo,
> docs). Only the physical **host** is still called `dwarfs`. **DWARF** debug-format
> code (`Dwarf*` gRPC service in nornir) is unrelated — never touch it here.
## Suite map — shared framework, nornir menu inventory, and the edda extraction
### The suite (JetBrains-style apps, not libraries)
**nornir · dwarves · holger · korp · ordning** — five apps, each with a **CLI *and* an egui UI** (native + wasm). **facett** is the shared UI *platform* (not an app). Everything else (skade, znippy, znippy-zoomies, tunnr, draupnir, edda, modgunn, Skidbladnir, knut) is library/infra. **Njord** ⛔ off-limits.
### What the suite apps MUST share (the "JetBrains common framework")
One crate per concern — never re-implemented per app:
| Concern | THE crate / seam | note |
|---|---|---|
| **thin/fat client** | `--server`/`--token`/`--workspace`; fat=in-proc, thin=gRPC | copy `nornir/.nornir/cli-client-modes.md` verbatim |
| **service + systemd lifecycle** | **skidbladnir** (`ServiceSpec`/`Lifecycle`) | no hand-rolled daemon |
| **jobs (process/VM/container), multi-machine** | **jera** (edda) — jobs cross machines over gRPC (worker groups) | shared by nornir+dwarves+holger |
| **job-list pane** | **`facett-jobview`** | Windows-Task-Manager-style; agent-aware |
| **About pane** | shared | version/git-sha/host/endpoint |
| **Tools menu** | demos launched **as jera jobs** (bidirectional kill) | |
| **CLI mirrors UI** | every UI action has a CLI verb (text-UI) | same gRPC underneath |
| **cross-app panes** | `facett-<guest>` component + `<guest>-embed` adapter | two surface modes below |
### Cross-app panes — the two surface modes (the structure you described)
An app can **show/drive another app's functionality in a pane**, either compiled-in or over the wire — this goes both ways:
| Host → guest | crate | surface mode |
|---|---|---|
| **nornir hosts holger** | `nornir/src/holger_embed.rs` | **compiled-in / direct Rust** (in-process, `read_registry` — no gRPC server started) |
| **dwarves hosts nornir's funnel** | `dwarves/crates/nornir-funnel-embed` | **thin gRPC** (`Funnel.Show` → reconstruct client-side) |
Rule: `facett-<guest>` holds the pane's Elm/Facet body once; the `<guest>-embed` adapter wires data either **direct (fat)** or **gRPC (thin)**. CLI verbs ride the same seam.
### ⚠️ nornir is being ripped into `nordisk/edda` (dedup — first-class fact)
Functionality is actively moving OUT of nornir into the shared **edda** crate collection so all apps consume one copy. **edda crates today:** `jera` · `nornir-testmatrix` · `nornir-robotui` · `nornir-bench` · `autonom` · `nornir-coverage` · `nornir-git` · `nornir-cli-outcome` · `nornir-saga`. **Left in nornir:** `nornir-build-thing` · `nornir-guard` · `nornir-introspect` · `nornir-jobs` (the *internal* ledger — superseded by edda `jera`; dwarves uses jera) · `nornir-selftest` · `nornir-viz-web`. **Implication for dwarves:** depend on the **edda** crates, never on nornir-internal twins.
### nornir viz — top-level tabs (the "menus") + ownership
nornir's UI is a flat tab bar; the tabs ARE the top menus. Ownership: **nornir** (warehouse/introspection) · **⚒ dwarves** (CI/CD forge) · **⇄ shared/cross-app** · **edda-backed** (engine moved to edda).
| Tab | Owner | Cross-app / note |
|---|---|---|
| Nornir (root/server ops) | ⇄ shared | workspace + server lifecycle → shared framework |
| Timeline · DepGraph · CallGraph · Architecture · Metro · Helix · Bloodstream | nornir | warehouse/introspection views |
| **Funnel** | ⇄ shared | **already embedded in dwarves** (nornir-funnel-embed, thin gRPC) |
| TimeTravel · Knowledge · Warehouse · WarehouseDeck · Search · Manual | nornir | |
| LiveRun · Release · Gates | nornir | (LiveRun/Release candidates to share w/ dwarves) |
| **Bench · Test · Leaderboard** | edda-backed | engines in edda (`nornir-bench`/`nornir-testmatrix`); tab stays nornir |
| Mcp | nornir | MCP server status |
| **Security** | modgunn | hosts modgunn VERDICT (cross-app candidate) |
| **⚒ Dwarves** (build + brokkr forge) | ⚒ **dwarves** | THIS is the pipeline — belongs to dwarves; nornir should embed it (thin gRPC to dwarves-server) once dwarves daemonizes |
| **📦 Holger** | 📦 holger | nornir hosts holger — **compiled-in/direct** (`holger_embed.rs`) |
| **🤖 Claude** | ⇄ shared | direct Claude prompt — needs **`facett-claude`** extraction to embed in dwarves |
| 🧠 Models | nornir/shared | LLM/embedding model download (as jobs) |
**dwarves' three panes** = **⚒ Pipeline** (owns it) · **Funnel** (guest of nornir) · **🤖 Claude** (shared, needs facett-claude). Small on purpose.
## nornir keeps vs dwarves takes — the CI/CD partition
**The principle.** **nornir = the warehouse** — code · time · space, time-travelled: you *read and understand* what exists. **dwarves = the CI/CD that ACTS on the warehouse** — its runs *produce* warehouse rows, which nornir then shows. So partition every surface by **read/understand (stays nornir)** vs **run/act (moves to dwarves)**.
**One-code-place still holds** (Law #1): a surface "moves" by **dwarves owning it and nornir embedding the *view*** via the `<guest>-embed` adapter (thin gRPC), **not** by copying code. And dwarves gets **no warehouse of its own** — it reads nornir's over thin gRPC, and writes its run results *back* into nornir's warehouse (`bench_runs` · `test_results` · `release_events` · `brokkr_runs/steps`).
### ⬛ STAY in nornir (warehouse / introspection / time-travel)
- **Time:** Timeline · TimeTravel
- **Space (system-DAG):** DepGraph · CallGraph · Architecture · Metro · Helix · Bloodstream
- **Browse / query:** Knowledge · Warehouse · WarehouseDeck · Search
- **Owner-surfaces:** Nornir (server ops) · **Funnel** (plan-DAG — nornir owns; dwarves embeds) · Mcp · Models · Manual (docs)
### ⚒ MOVE → dwarves (CI/CD run surfaces — the *act*)
- **Dwarves / Brokkr** — the pipeline (already dwarves; nornir hosts the view via gRPC once dwarves daemonizes)
- **LiveRun** — live pipeline-run monitor
- **Release** — release/publish cascade (the CD half)
- **Gates** — release/quality gates that a pipeline must pass
- **run-side of Bench · Test** — *running* them is a pipeline stage (the results VIEW stays nornir)
### ⇄ SHARED / cross-embed (view in nornir, act in dwarves)
- **Bench · Test · Leaderboard** — view = nornir warehouse; run = dwarves (engines already in **edda**)
- **Security** — dwarves *invokes* the gate; **modgunn** owns the VERDICT
- **Holger** (holger owns, nornir embeds — direct) · **Funnel** (nornir owns, dwarves embeds — gRPC) · **Claude** (shared, needs `facett-claude`)
> Super-graph name **"Yggdrasil"** is a **(working name)** — Rickard is not settled on it.
## 0. North star
dwarves is the constellation's **TeamCity-in-Rust**: a CI/CD forge you drive from a
UI and a CLI, backed by a long-running server. It must feel like the *same app family*
as nornir/holger/korp — the JetBrains-style **shared framework** (prompt §13): one
lifecycle, one thin/fat rule, an **About**, a **Tools menu** whose demos launch as
**jera** jobs, shared **facett** panels. No re-implementation: reuse the crate that
already owns each concern.
**Acyclicity (the load-bearing constraint).** dwarves is a *leaf that nornir depends
on*. So dwarves must **never** crate-depend on `nornir`. Every dwarves→nornir need
(e.g. reading/submitting the funnel) is a **runtime gRPC call**, compiling only a
trimmed proto — exactly the pattern `nornir-funnel-embed` already proves. Runtime
gRPC ≠ compile cycle.
## 1. Current state (2026-07-13, post-rename)
| Piece | State |
|---|---|
| `brokkr` forge (descriptor→DAG→plan→engine, 5 Elm facets) | ✅ in `crates/dwarves/src/brokkr/` |
| `sindri` scheduler (`tick`: cron→poll→drain→dispatch; `serve` sleep-loop) | ✅ `src/sindri.rs` |
| host backends (`RealRunner`/`DemoRunner`, action cache, incremental, JSONL store) | ✅ |
| job ledger | ✅ via **jera** (`edda`), not a local twin |
| `dwarves` CLI | ✅ real clap verbs — `validate / plan / run / trigger / history / serve / agent / doctor / pipelines` (fat + thin, `--server`) |
| gRPC server (`dwarves-server`) | ✅ `dwarf.v1` service (`crates/dwarves/proto/dwarf.proto`): `Health / Show / History / RegisterPipeline / Trigger / Webhook / RunStatus / GetRunReport / Badge`; `serve` is now the sindri loop **and** the tonic surface; bearer auth via shared `nornir_service::auth_interceptor` (constant-time). Handler-level tests cover the whole surface (`server::rpc_handlers`) |
| thin client (`--server`) | ✅ every verb routes to `dwarves-server` over gRPC when `--server <url>` / `$DWARVES_SERVER` is set; `--token`→`$DWARVES_SERVER_TOKEN`→`~/.dwarves/token`; else FAT against a local `brokkr.toml` |
| systemd / lifecycle | ✅ skidbladnir `ServiceSpec` + `Lifecycle` seam (`dwarves serve` daemonizes; durable redb-backed server-side cron); L1 deploy/rollback RPCs still open |
| Funnel panel | ✅ `nornir-funnel-embed` — thin gRPC to nornir `Funnel.Show` (read) **+ `Funnel.SubmitIdea` (write)**; renders `facett-plandag` DAG, carries a submit strip (idea\|error → grow the DAG); mounted in `dwarves-gui` (native + wasm) |
| CI results panel | ✅ `facett-brokkrview`; neutral `facett-cicd` component exists |
| Jobs pane | ✅ shared `facett-jobview` over a jera-shaped **agent-aware** fixture (`dwarves-gui/src/jobs.rs`, [`jobs-pane.md`](jobs-pane.md)); robot+testmatrix in code; live-data pull tracked (D1/D3) |
| UI host | ⚠️ `dwarves-gui` only (tabbed **Forge \| Funnel \| Claude \| Jobs** on `facett-wasm-demo`); not the "real" app |
## 2. Target architecture — the D/L points, made concrete
### D1 — `dwarf.proto` + `dwarves-server` ✅ LANDED
A gRPC service mirroring `nornir-server`, exposing the sindri/brokkr core. Shipped
surface: `Health / Show / History / RegisterPipeline / Trigger / Webhook / RunStatus /
GetRunReport / Badge / AgentPool` (`DrainStatus` from the original wishlist remains open —
see the backlog). Bearer auth is the shared constant-time `nornir_service::auth_interceptor`
(no local `!=` twin). Original target:
- `service Dwarf { RegisterPipeline · Trigger · RunStatus · DrainStatus · History · Badge · AgentPool · Health }` (+ a read-only `Show`-style dump for the embed panel).
- `dwarves serve` becomes **both** the sindri loop **and** the gRPC surface. Bearer-token auth + a `dwarves-workspace` metadata header, identical convention to nornir.
- Proto lives at `crates/dwarves/proto/dwarf.proto`, package `dwarf.v1`, so the method path is stable for embedders.
- **Server scaffold = reuse, not copy.** Per `ui-embedding-and-standalone.md` §3, factor nornir's server+client+agent bootstrap into a shared edda crate (`nornir-service`) that nornir *and* dwarves consume. Interim: a thin hand-rolled tonic server is acceptable **only** if it's the same shape and slated to fold into the shared crate.
### D2 — thin/fat `dwarves` CLI ✅ LANDED
Mirrors `cli-client-modes.md`:
- **FAT** (default, no server): CLI drives the local brokkr/sindri + local JSONL/warehouse.
- **THIN** (`--server <url>` or `$DWARVES_SERVER`): every verb routes to `dwarves-server` over gRPC.
- Global `--server` / `--token` / `--workspace` (any position); `--token` falls back to `$DWARVES_SERVER_TOKEN` then `~/.dwarves/token`; cwd-`workspace_<name>` pickup; non-TTY → error listing served names, never a hang.
- Real verbs behind clap — shipped: `validate / plan / run / serve / trigger / history / badge / release / deps / agents / agent / doctor / pipelines` (`badge` renders a pipeline's latest-run status SVG, fat + thin over the `Badge` RPC; `release`/`deps` CONSUME the shared edda leaves `nornir-release` + `nornir-depgraph` — deps-first publish waves + `can_skip_republish`, and the cross-repo graph + build order — behind `--features release`; the warehouse-coupled publish/doctor legs stay in nornir, reached over gRPC; `agents` lists the worker fleet fat/thin over the `AgentPool` RPC, reusing jera's `Scheduler`).
### D3 — shared fat/thin job ledger
dwarves already uses **jera** for `CI_BUILD` jobs. Mirror nornir's `jobs.redb` fat/thin split: FAT writes the local `jobs.redb`; THIN submits lifecycle records to `dwarves-server` and reads them back paginated. One ledger, one widget (`facett-jobview`) in the UI.
### D4 — own repo ✅ DONE
`codeberg.org/nordisk/dwarves`; nornir path-deps the crate. Nothing else depends on dwarves (leaf).
### L1 — lifecycle / systemd (shared, also missing in nornir)
Reuse **skidbladnir** (`ServiceSpec` + `Lifecycle`, already green) — no hand-rolled daemon/systemd. Add the lifecycle-RPC family to *both* servers:
- `server deploy --binary <path>` (checksum-verify → atomic swap → restart → keep previous) — the right primitive for the toolchain-less `dwarfs` host; replaces the manual `scp`+`install`+`restart`.
- `server restart | stop | start | rollback | health` (health = version, git-sha, uptime, queue depth, warehouse-lock — must not lie about version).
### Funnel — read → **read/write** (the SUPER-PRIO)
The funnel data lives in **nornir's** warehouse; dwarves is its client over gRPC (no cycle).
- **Read (done):** `nornir-funnel-embed` fetches `Funnel.Show`, reconstructs `funnel-core`, renders the `facett-plandag` DAG (nodes light up, cost-heat as data). Offline → clean "unavailable".
- **Write (✅ DONE):** the embed adapter carries `submit_idea` (thin gRPC → nornir-server's `Funnel.SubmitIdea`, idea **and** error kinds — both first-class per `funnel-intake`) plus a **submit strip** in the pane (kind combo + text field + Submit/Enter): **type an idea/error into dwarves' funnel pane** and the auto-poll `Show` re-renders the grown DAG. Offline → clean `error` outcome, draft preserved, no panic. Robot + testmatrix in code (`funnel_write_submit_idea_offline_safe`, `funnel_submit_leg_drivable`). **Next:** the mother→child **prompt-planner** decomposition + **history** side-panel (already exist server-side).
- **Claude token (prompt §14 gap):** the prompt-planner's decompose step needs an LLM token; it is a **nornir-server** config (`~/.nornir/…` / env). dwarves only triggers submit/plan over gRPC — surface a clear "planner unconfigured" state if the server lacks the token, never a silent no-op.
## 3. The shared framework (the "JetBrains common framework" law)
Every nordisk app — dwarves included — reuses ONE crate per concern (no duplicates,
even thin). This is the consolidation law:
| Concern | THE crate | dwarves adopts by |
|---|---|---|
| service + systemd lifecycle | **skidbladnir** | declaring a `ServiceSpec`, driving a `Lifecycle` (L1) |
| jobs (process/VM/container) + ledger | **jera** (edda) | already; route *all* real work + Tool-menu demos through it |
| boot / provision | **draupnir** | only via jera — never directly |
| UI panes | **facett-\*** component + `<guest>-embed` adapter | `facett-brokkrview`/`facett-cicd` + `nornir-funnel-embed` (native, no wasm for cross-embed) |
Plus the per-app UI conventions the whole fleet shares: **thin/fat** toggle, an
**About**, a **Tools menu** whose demo entries **start containers/VMs as jera jobs**,
and a **job list** (`facett-jobview`). wasm is the *browser delivery* target, not the
cross-embed mechanism (`wasm-ui-panels-design.md`).
## 4. The CI/CD model — Rickard's headlines
> Captured live. Each headline is a top-level product feature; the UI shell (§4b)
> and phasing (§5) follow from these.
### H1 — Pipelines are Rust `xtask`s, the pipeline is a DAG, jobs run distributed on many agent machines
The identity: a build/test/release forge, driven from UI + CLI, backed by the server.
Three commitments:
- **Pipeline-as-code in Rust — zero YAML, zero shell.** A repo declares its pipeline
as a **Rust `xtask`** (not `.gitlab-ci.yml` / `.github/workflows`, not bash). Steps
are Rust (cargo/API calls, argv-only — Brokkr's zero-shell law). Because it's code,
a pipeline can **generate its own DAG at runtime** (the GitLab parent-child dynamic
idea, but native). `brokkr.toml` stays as the declarative *seed*; the xtask is the
programmable form.
- **The pipeline IS a DAG** (`will be` — today the model has DAG bones: `brokkr::dag`,
`[[pipeline.depends]]`). Steps/stages declare upstream `needs`; the scheduler runs
the **ready frontier**, out-of-order, not rigid stages; **matrix fan-out** expands a
step over params (target × feature × profile — reuse `BuildVariantSet`).
- **jera runs the jobs across MANY agent machines.** Every pipeline step is a **jera
job**, and jera must dispatch to a **fleet of remote agents**, not just in-process.
**The "few good ones" borrowed (Rickard: pick from TeamCity / GitLab / GitHub):**
| Borrowed idea | From | In dwarves |
|---|---|---|
| Pipeline-as-code, **no YAML/shell** | (all — inverted: they use YAML; we use Rust) | the pipeline is a **Rust xtask** |
| DAG `needs:` — out-of-order, run the ready frontier | GitLab CI, GHA | steps declare upstream deps; `brokkr::dag` + sindri drain |
| **Matrix** fan-out over params | GitHub Actions, TeamCity | one step → target×feature×profile (`BuildVariantSet`) |
| **Distributed agents + capability/tag matching** | TeamCity agent requirements · GitLab runner tags · GHA runner labels | steps declare required caps; jera dispatches to a matching remote agent |
| **Build chains** — snapshot + artifact deps between pipelines | TeamCity, GitLab artifacts | cross-pipeline `[[pipeline.depends]]`; artifacts flow along DAG edges, **content-addressed** |
| **Dynamic / generated** pipelines | GitLab parent-child dynamic | the xtask *emits* the DAG at runtime |
| Content-addressed **cache** + incremental skip | GHA / GitLab cache | already: `ActionCache` + `incremental` (skip unchanged inputs) |
| **Pre-tested / personal builds** (green-gate before merge) | TeamCity | run the DAG on a worktree/branch before landing |
**Distributed agents + worker groups — the jera work this requires:**
> **Rollout = LAZY (Rickard).** Ship **single-machine first** (one local agent,
> in-process), but design the seam now so **distributed-over-gRPC is a clean drop-in
> later**. Distributed multi-machine execution is a **tracked to-develop feature**,
> not day-1 — it must not block the funnel/server/CLI work.
>
> **SHARED, not dwarves-only (Rickard).** Multi-machine awareness is a **common jera
> feature** (in `edda`): **nornir, dwarves, AND holger** all get "jobs can run across
> multiple machines over gRPC" from the one jera crate — never re-implemented per app.
> So the `Agent` trait + `GrpcAgent` transport + worker-groups below live in **jera**,
> and each app consumes them (consolidation law).
- Today: jera is a **ledger + local runners** (process / VM / container); brokkr's
`AgentPool` dispatch is **in-process** (`dwarfs-design` names this the open gap) —
though the pool is now READABLE over the wire (`AgentPool` RPC + `dwarves agents`,
reusing jera's `Scheduler` fleet-as-data); the remaining gap is remote *dispatch*.
- **Worker groups** (GitHub "runner groups" model): agent machines join into **named
groups of workers**; a pipeline/step targets a **group** (plus capability/label
match within it). One group can be a single box (the lazy start) or a fleet.
- The **remote-agent transport is gRPC** (same convention as `dwarves-server`):
- **Agent registration** — each machine runs `dwarves agent --group <name>` and
registers `{id, host, group, capabilities/labels, cores, mem, capacity}` with the
server (mirrors the bench machine-roster discipline: Loki/Odin/NUC — record
machine+cores+mem on every job).
- **Scheduling** — sindri's drain matches each ready step to a **capable idle agent
in the target group** (labels/caps + the exclusive gate), then dispatches over gRPC.
- **Cross-machine lifecycle** — tracked in the ONE ledger with **bidirectional kill**:
cancel the job → kill the remote work; agent/work dies → job → `Killed` (never a
silent stall — RAGNARÖK).
- **Artifacts** flow between agents along DAG edges, content-addressed (reuse
`artifact::content_id`) so a downstream agent pulls exactly the produced inputs.
- **The seam that keeps "lazy" honest:** define the `Agent` dispatch as a trait with a
**`LocalAgent` (in-process, single-machine)** impl shipped first and a **`GrpcAgent`
(remote, worker-group)** impl added later — sindri/jera schedule against the trait, so
going distributed is *adding an impl*, not a rewrite. This is the same dep-light
stand-in discipline as `MonitorReport`.
### H2 … _(next headlines — Rickard, keep them coming)_
## 4b. The UI / client shell (follows from the headlines)
Fleet-consistent shared-framework scaffold (adjusted as headlines land):
- a tabbed app (promoted from `dwarves-gui` into the real `dwarves` UI, native + wasm on `facett-wasm-demo`);
- **thin/fat** aware (a server field + connection status; fat when empty);
- **About** pane (version, git-sha, host, server endpoint, the Brokkr/Sindri story);
- **Tools menu** — selectable **demos that launch as jera jobs** (bidirectional kill);
- **Jobs** tab (`facett-jobview`) — now **agent-aware** (which machine ran each step); ✅ **mounted** (`dwarves-gui` Jobs tab over a jera-shaped fixture; live-data pull tracked — see [`jobs-pane.md`](jobs-pane.md));
- **Agents** view — the live fleet (per H1: id/host/caps/capacity/online);
**The three headline panes (Rickard):** dwarves must carry **Pipeline · Funnel · direct Claude prompt**.
| Pane | Component | State today |
|---|---|---|
| **Pipeline** (CI/CD) | `facett-brokkrview` (results) + `facett-brokkr` (builder) over `dwarves::brokkr` | ✅ mounted (`dwarves-gui` Forge tab) |
| **Funnel** (plan-DAG) | `nornir-funnel-embed` → `facett-plandag`, thin gRPC to nornir `Funnel.Show` + `Funnel.SubmitIdea` | ✅ mounted, **read + write** (submit strip: idea\|error → DAG grows); LLM-decompose planner is server-side next |
| **Claude prompt** (direct chat) | **`facett-claude` — DOES NOT EXIST YET** | ⚠️ lives only in `nornir/src/viz/claude_tab.rs` (nornir-native `ClaudeTab`, not a facett component) |
**The one extraction to close it:** pull the conversation pane out of `nornir/src/viz/claude_tab.rs` into a reusable **`facett-claude`** component (Elm/Facet body + `state_json`), mirroring how `facett-brokkrview` was extracted from the brokkr Elm core. The Anthropic client (`ClaudeTransport`, `POST /v1/messages`, `claude-opus-4-8`, `$ANTHROPIC_API_KEY`) is already reusable in `nornir/src/viz/claude.rs` (and dwarves already has a `ClaudeTransport` twin in `brokkr/notify.rs`). Once `facett-claude` exists, dwarves mounts three tabs: **Pipeline | Funnel | Claude**.
## 5. Phasing (each phase ends green: builds + one robot scenario + one testmatrix row, wire-once/verified-in-both-hosts)
1. **P1 — funnel write-path** ✅ **DONE** (SUPER-PRIO): `submit_idea` (idea\|error) added to `nornir-funnel-embed` over `Funnel.SubmitIdea`; a submit strip in the pane (kind combo + field + Submit/Enter); post-submit DAG refresh; offline-safe (clean error, draft kept). Robot scenario (`funnel_submit_leg_drivable`) + testmatrix (`funnel_write_submit_idea_offline_safe`) drive submit headless. *(Remaining P1 tail: the History side-list — server-side already.)*
2. **P2 — `dwarf.proto` + `dwarves serve` gRPC** (D1): read surface first (`Show`/`History`/`Health`), then write (`Trigger`/`RegisterPipeline`).
3. **P3 — thin/fat CLI** (D2 + D3): global flags, `*_remote` verbs, shared `jobs.redb` split.
4. **P4 — lifecycle** (L1): skidbladnir `ServiceSpec`; `server deploy/health/rollback`.
5. **P5 — real UI app**: promote the shell (§4) with Rickard's feature list; wasm build + Pages deploy at `/dwarves`.
6. **P6 — shared server scaffold**: fold the interim server into the edda `nornir-service` crate; nornir consumes it too.
## 6. Laws (verbatim in every subagent prompt)
- 🥇 **Code lives in exactly ONE place.** Functionality overlaps across the suite (apps share panes/features), but each concern is implemented **once** in a shared crate and *consumed* — never re-implemented per app, **not even "thin."** Close a gap by depending on the shared crate and **deleting the hand-rolled twin** after the shared path is green + tested. Overlap is expected at the *surface*; duplication in the *code* is forbidden.
- 🥈 **Reuse-first — extend the common infrastructure.** Before writing ANY code, search the suite for an existing component and **read it from source**, then extend it; develop only genuinely *unique* logic. Never hand-roll a twin of a solved concern (jobs→**jera**, concurrency→**gatling** in znippy-zoomies, compression→znippy-zoomies codecs, service/systemd→**skidbladnir**, provisioning→**draupnir**, panes/maps→**facett**, warehouse→**skade**). If a shared component lacks a capability, add it *there*. Full registry: `workspace_nordisk/.nornir/consolidation-panels-maps-design.md` § Shared-component registry.
- 🥉 **Every UI atom is robot-tested.** Every UI atom/feature ships **ONE `nornir-robotui` scenario** — runnable *both* headless (matrix) *and* visible via `watch` on `DISPLAY=:0` — **plus ONE `nornir-testmatrix` cell**. Wire-once, verified-everywhere; **green ⟺ nothing untested**. Reuse edda's `nornir-robotui` + `nornir-testmatrix`; never hand-roll a UI-test harness.
- ⛔ **NO AI attribution** in commits (mega-prompt §0 Law 4). Keep human `Co-Authored-By: Rickard`.
- **Zero shell** (Brokkr's Software Criteria) — cargo/bollard/tunnr Rust APIs only, argv, no `Command`-to-bash.
- **Common infrastructure = one crate each, audited** — service/systemd → skidbladnir · jobs/multi-machine → jera · provisioning → draupnir · panes → facett-\* + `<guest>-embed` · maps → facett-map. See the no-dup audit in [`../../workspace_nordisk/.nornir/consolidation-panels-maps-design.md`](../../workspace_nordisk/.nornir/consolidation-panels-maps-design.md).
- **Plain numbers** — no thousands separators in the UI.
- **Never touch Njord.**
- **SHIPPED ≠ TESTED** — for wasm, test the *deployed* `.wasm`, not just the native build.
## Planner / Executor split — Claude drives the plan-DAG
Rickard's decision: **two SEPARATE Claude roles** — one builds the plan-DAG, one drains
it. Kept separate until Rickard says to unify.
| Role | Job | Status | Machinery |
|---|---|---|---|
| **Planner Claude** | idea → plan-DAG | ✅ **DONE** | `nornir funnel prompt --llm` → `funnel::decompose::{llm_decompose_prompt, decomposition_from_llm_reply}` → `funnel::run_prompt_with_decomposition` |
| **Executor Claude** | "sucks off" the grand plan-DAG and *executes* it | ❌ **NOT BUILT** (next, gated on Rickard's go) | reuses `funnel::run_subtask_loop` (below) |
### Executor loop (the drain)
1. **Ready frontier** — `Funnel::promote_ready()` (funnel-core) promotes nodes whose deps are all `Done`.
2. For each ready node → an **executor agent (Claude) does that one node's task**.
3. **Oracle checks it** — the existing `run_subtask_loop(task, sub, generator, judge, max_rounds)` already runs *generate → judge → retry* with the compiler-as-oracle: `AnswerJudge` trait, `CodegenJudge` impl (apply patch → `cargo build`+test → accept/reject). **The executor Claude simply replaces the mock `Generator`** in that loop; the compiler-judge oracle stays. A **separate verifier Claude** can be layered on later.
4. **Record** — `Funnel.SetStatus` RPC (thin) / `NodeStatusChanged` event (fat) → `Done | Failed`.
5. **Re-promote → repeat** until the DAG drains (all `Done`) or **blocks** (`Blocked`/`Failed`) — never a silent stall (RAGNARÖK).
### Why separate (until told otherwise)
- **Distinct role/context** — "decompose the whole thing" vs "do exactly this one node."
- **Independent verification** — a fresh executor/verifier catches the planner's blind spots (the DAG author shouldn't grade its own plan).
- **Asymmetric compute** — one high-effort planner; many cheap **parallel** executors.
### Distribution
Each node execution = a **jera job** → executors fan out across **worker machines** (H1
distributed agents). One planner, N executors on the fleet.
**Symbol note:** the judge trait is `AnswerJudge` (`CodegenJudge` is its impl); all other cited symbols verified present (`promote_ready`, `run_subtask_loop`, `Generator` seam, `SetStatus` RPC, `NodeStatusChanged` event, the three planner fns).