dwarves — the parity spec (server · thin/fat client · UI · funnel)
What this is. The concrete, buildable spec that turned
dwarvesfrom "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 owndwarf.v1gRPC 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
- standalone service + cross-embed + wasm — ui-embedding-and-standalone.md
- the four shared seams (skidbladnir/jera/draupnir/facett-embed) — workspace_nordisk/.nornir/consolidation-panels-maps-design.md
- the thin/fat rule to copy verbatim — nornir/.nornir/cli-client-modes.md
- CI/CD market inspiration (what dwarves borrows; jera multi-machine sketch) — cicd-market-inspiration.md
Naming: the project is
dwarveseverywhere (crate, lib,dwarvescommand, repo, docs). Only the physical host is still calleddwarfs. 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); 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-onlyShow-style dump for the embed panel).dwarves servebecomes both the sindri loop and the gRPC surface. Bearer-token auth + adwarves-workspacemetadata header, identical convention to nornir.- Proto lives at
crates/dwarves/proto/dwarf.proto, packagedwarf.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 todwarves-serverover gRPC. - Global
--server/--token/--workspace(any position);--tokenfalls back to$DWARVES_SERVER_TOKENthen~/.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(badgerenders a pipeline's latest-run status SVG, fat + thin over theBadgeRPC;release/depsCONSUME the shared edda leavesnornir-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;agentslists the worker fleet fat/thin over theAgentPoolRPC, reusing jera'sScheduler).
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-lessdwarfshost; replaces the manualscp+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-embedfetchesFunnel.Show, reconstructsfunnel-core, renders thefacett-plandagDAG (nodes light up, cost-heat as data). Offline → clean "unavailable". - Write (✅ DONE): the embed adapter carries
submit_idea(thin gRPC → nornir-server'sFunnel.SubmitIdea, idea and error kinds — both first-class perfunnel-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-pollShowre-renders the grown DAG. Offline → cleanerroroutcome, 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 xtasks, 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.tomlstays 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 upstreamneeds; the scheduler runs the ready frontier, out-of-order, not rigid stages; matrix fan-out expands a step over params (target × feature × profile — reuseBuildVariantSet). - 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 theAgenttrait +GrpcAgenttransport + 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
AgentPooldispatch is in-process (dwarfs-designnames this the open gap) — though the pool is now READABLE over the wire (AgentPoolRPC +dwarves agents, reusing jera'sSchedulerfleet-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.
- Agent registration — each machine runs
-
The seam that keeps "lazy" honest: define the
Agentdispatch as a trait with aLocalAgent(in-process, single-machine) impl shipped first and aGrpcAgent(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 asMonitorReport.
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-guiinto the realdwarvesUI, native + wasm onfacett-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-guiJobs tab over a jera-shaped fixture; live-data pull tracked — seejobs-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)
- P1 — funnel write-path ✅ DONE (SUPER-PRIO):
submit_idea(idea|error) added tonornir-funnel-embedoverFunnel.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.) - P2 —
dwarf.proto+dwarves servegRPC (D1): read surface first (Show/History/Health), then write (Trigger/RegisterPipeline). - P3 — thin/fat CLI (D2 + D3): global flags,
*_remoteverbs, sharedjobs.redbsplit. - P4 — lifecycle (L1): skidbladnir
ServiceSpec;server deploy/health/rollback. - P5 — real UI app: promote the shell (§4) with Rickard's feature list; wasm build + Pages deploy at
/dwarves. - P6 — shared server scaffold: fold the interim server into the edda
nornir-servicecrate; 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-robotuiscenario — runnable both headless (matrix) and visible viawatchonDISPLAY=:0— plus ONEnornir-testmatrixcell. Wire-once, verified-everywhere; green ⟺ nothing untested. Reuse edda'snornir-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. - 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)
- Ready frontier —
Funnel::promote_ready()(funnel-core) promotes nodes whose deps are allDone. - For each ready node → an executor agent (Claude) does that one node's task.
- Oracle checks it — the existing
run_subtask_loop(task, sub, generator, judge, max_rounds)already runs generate → judge → retry with the compiler-as-oracle:AnswerJudgetrait,CodegenJudgeimpl (apply patch →cargo build+test → accept/reject). The executor Claude simply replaces the mockGeneratorin that loop; the compiler-judge oracle stays. A separate verifier Claude can be layered on later. - Record —
Funnel.SetStatusRPC (thin) /NodeStatusChangedevent (fat) →Done | Failed. - 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).