CLI command map — every nornir verb, its gRPC RPC, and its handler
The complete, code-derived map of the nornir CLI surface: every command and subcommand, whether it can run thin (gRPC to a nornir-server) or only fat (direct warehouse on disk), the exact Service.Rpc it calls in thin mode, and the server handler behind that RPC. This is the CLI↔gRPC↔handler half of the wiring architecture-wiring.md draws for the viz↔table half — together they are the textual form of the EPIC ARCH board.
Source code: src/bin/nornir.rs (the clap Cli/Cmd + every *Op subcommand enum + the *_remote thin paths), proto/nornir.proto (the gRPC services + RPCs), src/bin/nornir-server.rs (the tonic handlers). Companion: cli-client-modes.md (the mode rule), mode-matrix.md, mcp-tools.md (the MCP twin).
1. The thin / fat duality
nornir is one core library with several faces (CLI, nornir-server, nornir-mcp, nornir viz). The CLI itself runs in one of two modes per invocation:
- Fat (embedded): no server target — the verb opens the local Iceberg warehouse files directly under
<home>/.nornir/...and owns the redb lock in-process. This is the default. Every "Fat behavior" cell below is a direct core call. - Thin (client): with a server target the verb talks gRPC to a running
nornir-server, which owns the warehouse lock and runs the same core fn under it. This mirrors exactly how the viz tabs and the MCP tools reach the server — same API, sameworkspace_<name>convention.
The switch is purely the global flags (they work in any position and apply to every subcommand), each falling back to an env var:
| Global flag | Env fallback | Effect |
--server <url> |
$NORNIR_SERVER |
sets thin mode; a bare host:port gets http:// prepended |
--token <tok> |
$NORNIR_SERVER_TOKEN, then <home>/.nornir/token |
bearer token for the server (tonic interceptor) |
--workspace <name> |
$NORNIR_WORKSPACE, then cwd workspace_<name>/ |
picks the workspace; in thin mode it must be one the server serves |
server_target() is just NORNIR_SERVER (the --server flag exports it in main before dispatch); each thin-capable verb checks it and, if set, returns through its *_remote gRPC path instead of the embedded path. Standing in a workspace_<name>/ directory auto-resolves the workspace with no flag.
1a. Version identity — nornir --version
Every nornir build carries one canonical identity line:
nornir v<CARGO_PKG_VERSION> (<git-short-sha>)
e.g. nornir v0.5.4 (9b1a9a8). --version / -V prints exactly this and exits immediately — clap short-circuits before any subcommand dispatch, so it never touches the warehouse, a server, or the MCP boot.
The string is defined once in the library and reused everywhere so the faces can never disagree:
| surface | value |
nornir --version / -V |
nornir <VERSION_LINE> (clap version = nornir::VERSION_LINE) |
Health.Ping.version (gRPC) |
nornir::VERSION_LINE |
| viz ℹ️ About panel | client_version (CARGO_PKG_VERSION) + build_sha (the git hash) |
nornir::VERSION_LINE=concat!("v", CARGO_PKG_VERSION, " (", NORNIR_GIT_HASH, ")")andnornir::GIT_HASH=NORNIR_GIT_HASH— both insrc/lib.rs.NORNIR_GIT_HASH(short SHA) is stamped bybuild.rsat build time by reading the git metadata with purestd::fs— it never runsCommand::new("git"), per the zero-shell law. It resolves a symbolic HEAD through the loose ref thenpacked-refs, a detached HEAD directly, and a linked worktree's.gitpointer file +commondir. Off a git checkout the hash isunknown. The same value is also emitted asNORNIR_BUILD_SHA(the historical env the viz "fresh client?" check reads).
2. Command ↔ gRPC ↔ handler table
Every CLI command and subcommand. Mode is both (has a *_remote thin path and a fat embedded path), fat-only (no server routing — runs locally / spawns cargo / writes local config), or thin-only (no embedded path). For thin-capable verbs the gRPC RPC column names the Service.Rpc; the handler is that service's tonic impl in src/bin/nornir-server.rs (e.g. Release.GateAll → the gate_all method on the Release service impl).
| Command | Mode — gRPC Service.Rpc (thin) |
Fat behavior (direct warehouse / local) | What it does |
commands |
fat-only | recursive clap leaf paths (--json → CommandOutcome) |
list every CLI command (the CLI twin of MCP tools/list) |
repos |
both — Repos.List |
lists nornir.toml repo keys |
list monitored repos (server's vs local config) |
guard status |
both — Guard.Status |
guard::status over local paths |
report AI-agent guard-rail (chmod -w) state |
guard apply |
both — Guard.Apply |
guard::apply |
chmod -w the forbidden paths |
guard release |
both — Guard.Release |
guard::release |
chmod +w (release the guard) |
guard verify |
fat-only (Guard.Verify exists for server self-check) |
guard::verify |
verify guard invariants |
bench run |
fat-only (results submit via Bench.Submit) |
runs benches, query_bench_* + append |
run benches for a repo, persist bench_telemetry/bench_runs |
bench history-show |
both — Bench.History |
bench_history.jsonl + query_bench_runs |
show bench history for a repo |
release trace |
both — Release.Trace |
regression trace over bench_history |
trace a regression to a commit |
release events |
both — Viz.ReleaseEvents |
query_release_events → release_events |
dump release-pipeline events |
release impacted-crates |
fat-only | dep graph + changed_since_last_release |
crates impacted by a change |
| test run | both (results via Telemetry.SubmitTestResults) | nornir-testmatrix detect+run, append test_results | run a repo's tests, persist the matrix | | test all | both (submit per row) | run tests for all workspace repos | full matrix run | | test history | both — Viz.TestResults | query_test_results → test_results | show the test-result history/matrix | | test coverage | fat-only (AUT5; MCP twin test_coverage) | autonom surface − covered → surface_coverage | completeness-gate coverage report | | coverage (top-level) | fat-only | cargo-llvm-cov over a repo → coverage/coverage_fn tables (--boundary view) | source line/region coverage (maven-report style) | | docs init | both — Docs.Init | docs::init scaffolds .nornir/ | scaffold a repo's docs | | docs render | both — Docs.Render | docs::render (typst) | regenerate README/CHANGELOG | | docs check | both — Docs.Check | docs::check freshness | verify docs are current | | docs export | both — Docs.Export | docs::export (PDF/HTML/MD) | export docs in a format | | docs book | both — Docs.Book | docs::book whole-doc book | render the full documentation book | | docs history | both — Docs.History | docs_exports table scan | show doc-export history | | docs index | fat-only | builds docs BM25 index | (re)build the docs search index | | docs search | fat-only (server twin is Search.Query) | BM25 over docs index | search the docs | | introspect depgraph | both — Mimir.Svg | dep_graph → SVG | per-repo dep-graph SVG | | introspect symbols | both — Introspect.Symbols | DWARF symbol extraction | extract symbols from a binary | | introspect symbol-lookup | both — Introspect.SymbolLookup | symbol search | find symbols by name | | introspect defined-in | both — Introspect.DefinedIn | symbol → file/line | where a symbol is defined | | introspect callgraph | fat-only | DWARF/syn call graph | build the call graph | | introspect callers | both — Introspect.Callers | callers_of | who calls a symbol | | introspect callees | both — Introspect.Callees | callees_of | what a symbol calls | | introspect path-between | both — Introspect.PathBetween | path_between | call path between two symbols | | knowledge scan | fat-only (server scans live clones) | knowledge::scan_all → symbol_facts+call_edges+feature_gate_facts | scan source into knowledge facts | | knowledge query (lookup/defined-in/callers/callees/call-path) | both — Knowledge.SymbolLookup/DefinedIn/Callers/Callees/CallPath | query the symbol_facts/call_edges tables | query the knowledge graph | | watch <repo> | fat-only (feature inotify, Linux) | knowledge::watch::watch → debounced knowledge::delta::SymbolDelta::rescan → append_symbol_scan | live-tree knowledge map: kernel file events, not a timer poll. For a tree that changes WITHOUT git; a git-driven server keeps the monitor poll. Without the feature it reports so and exits. | | scip (ingest/…) | fat-only (feature scip) | ingest a rust-analyzer scip index → resolved defs+refs, SHA-keyed | RESOLVED knowledge map (IDE-grade, opt-in) | | map <repo> | fat-only | renders the repo's knowledge map | architecture/knowledge map for a repo | | arch generate <repo> | fat-only | arch::generate → historize architecture_wiring (MCP twin arch_graph) | generate the wiring graph (component·gRPC·core·table) and persist it | | arch svg <repo> | fat-only (MCP twin arch_svg) | arch::generate + to_svg, prints/persists | emit the skade-style wiring SVG | | arch history <repo> | fat-only | arch::warehouse::list_arch_wiring | list historized wiring rows (by git sha) | | funnel submit/idea | both — Funnel.SubmitIdea | funnel::Store append funnel_events | submit an idea into the funnel DAG | | funnel error | both — Funnel.SubmitIdea (error kind) | funnel::Store | record an error/blocker node | | funnel test | both — Funnel.SubmitIdea | funnel::Store | submit a test idea | | funnel plan / plan-from | both — Funnel.CreatePlan | funnel::Store | create a plan from idea(s) | | funnel node | both — Funnel.AddNode | funnel::Store | add a node to a plan | | funnel link | both — Funnel.Link | funnel::Store | link two nodes | | funnel status | both — Funnel.SetStatus | funnel::Store | set a node's status | | funnel next | both — Funnel.Next | funnel::next | what to work on next | | funnel show | both — Funnel.Show | funnel::Store dump | dump the funnel DAG | | funnel history | both — Funnel.History | funnel_events scan | funnel event history | | funnel decompose | fat-only | funnel::decompose | auto-decompose an idea into a plan | | funnel demo | fat-only | seeds demo data | funnel demo seeding | | funnel nuke | fat-only | clears funnel state | reset the funnel | | mimir deps-of | both — Mimir.DepsOf | deps_of over the dep graph | a repo's dependencies | | mimir dependents-of | both — Mimir.DependentsOf | dependents_of | a repo's dependents | | mimir affected | both — Mimir.AffectedByChange | affected_by_change | blast radius of a change | | mimir build-order | both — Mimir.BuildOrder | topo sort | dependency build order | | mimir dep-path | both — Mimir.DepPath | dep_path | path between two repos | | mimir external-users | both — Mimir.ExternalDepUsers | external_dep_users | who uses an external crate | | mimir overview | both — Mimir.RepoOverview | repo_overview | repo summary (deps/release/docs) | | (mimir svg / changed) | both — Mimir.Svg / Mimir.ChangedSinceLastRelease | dep_graph SVG / changed_since_last_release | dep-graph SVG / changed-set | | diagram timeline | both — Viz.Timeline | build_timeline → release_lineage+dep_graph_edges | release/dep timeline diagram | | diagram lane-summary | fat-only | timeline model | per-lane summary diagram | | diagram depgraph | fat-only | dep_graph_edges | dep-graph diagram | | diagram snapshot-edges | fat-only | dep snapshot edges | snapshot-edge diagram | | diagram gate-matrix | fat-only | gate results | gate-matrix diagram | | diagram release-versions | fat-only | release_lineage | release-versions diagram | | diagram bench-history | fat-only | bench_history | bench-history diagram | | diagram bench-compare | fat-only | bench_runs | bench-compare diagram | | bakeoff run | fat-only (submit via Telemetry.SubmitBakeoff) | runs models, append agent_model_runs | run a prompt across local models (H5) | | bakeoff leaderboard | both — Viz.BakeoffResults | query_agent_model_runs → agent_model_runs | model leaderboard | | warehouse import-jsonl | fat-only | append rows from JSONL | import legacy JSONL into the warehouse | | warehouse query | fat-only (read-only open; tolerates a locked live catalog via snapshot) | warehouse::sql::query → DataFusion over native iceberg-datafusion TableProviders | run SQL (SELECT … WHERE / JOIN / GROUP BY) over the warehouse tables; --format json | | warehouse bench-query | both — Warehouse.Tables + Warehouse.Scan | query_bench_runs → bench_runs | typed bench-run reader for a repo (Parquet → JSON) | | warehouse compact | fat-only (operator op; emits WAREHOUSE_COMPACT job) | maintenance::compact_all → replace snapshot | merge small data files → few large SORTED files per partition | | warehouse expire | fat-only (operator op; emits WAREHOUSE_EXPIRE job) | maintenance::expire_all → RemoveSnapshots + orphan GC | drop old snapshots + reclaim their unreferenced files | | index build | fat-only | builds tantivy code index | (re)build the code search index | | index search | both — Search.Query | BM25 over the index | full-text code search | | index stats | both — Index.Stats | index stats | code-index stats | | index snapshot | both — Index.Snapshot | snapshot the index | snapshot the index to the warehouse | | index restore | fat-only (IndexOp::Restore opens the Iceberg warehouse directly — the Index service has no Restore/Get RPC, only Stats/Snapshot/UploadSnapshot) | restore_from_iceberg → blobs back to disk | restore the index | | index upload | both — Index.UploadSnapshot (stream) | upload index blob | push an index snapshot to the server | | vector index | fat-only | embeds into embeddings | build semantic embeddings (feature vector) | | vector search | both — Vector.Search | semantic/hybrid query | search by meaning | | vector stats | fat-only | embedding stats | embedding-store stats | | vector doctor | fat-only | env/HW check | diagnose the vector backend | | vector bench | fat-only | benches embedding | embedding throughput bench | | vector setup-cuda | fat-only | sets up CUDA dir | provision a CUDA build dir | | vector setup-rocm | fat-only | pins a ROCm runtime set (+ ROCm libonnxruntime.so) into /opt/nornir/rocm | provision an AMD ROCm build dir | | robot run | fat-only | drives the viz headlessly (nornir-robotui) | robot-UI test driver (feature robot) | | workspace ls | both — Workspaces.List | local workspace registry | list workspaces (all served, multi-ws) | | workspace show <name> | both — Workspaces.Get | registry record | show a workspace's members | | workspace register | both — Workspaces.Register | record + eager fetch/build | register a workspace | | workspace add | both — Workspaces.Register (member add) | edit descriptor | add a member repo to a workspace | | workspace rm <name> | both — Workspaces.Remove | remove from registry | remove a workspace | | workspace fetch <name> | both — Workspaces.Fetch | clone members + rebuild warehouse | fetch a workspace's remotes now | | workspace populate <name> | both — Workspaces.Fetch (background) | instant clone, async build | populate (fast clone, deferred build) | | jobs | both — Viz.Jobs (server jobs.redb) | local warehouse jobs.redb (--kind/--running/--limit/--json) | the unified job ledger (release/bench/test/docs/coverage/container runs) | | container | fat-only | spawns podman/docker run … AS a tracked job (stdout/stderr+exit captured) | run a container as a tracked nornir job | | cache (run/…) | fat-only | runs the embedded holger proxy (/cache crates.io mirror + /sparring) as a tracked job | local registry cargo can build through | | airgap (pack/verify/unfold/…) | fat-only (MCP twins airgap_pack/airgap_verify/…) | skidbladnir airgap: pack→sign→verify→unfold→install in dep order | offline lake bundle pipeline (EPIC AIRGAP) | | root | fat-only | prints <home>/.nornir (derived from $HOME) | print the resolved nornir data root | | serve | fat-only (this is the server) | runs nornir-server (tonic) | start the gRPC server | | viz | thin-or-fat at launch | execs nornir-ui (egui) | launch the visualizer (21 panes) | | install add/update/remove | fat-only | writes systemd unit + user; remove ALWAYS erases the DATA and ALWAYS keeps the ssh deploy key + token (--wipe-identity is the one opt-in that destroys them; --purge/--keep-identity are deprecated no-ops) | install / refresh-binary / uninstall the server as a service | | key (gen/show/…) | fat-only | manages <home>/.nornir/token | manage the bearer token | | security (scan) | fat-only (server twin Mimir.SecurityScan) | security::scan → vuln_findings+sbom_components | SBOM + vuln + license scan |
The dedicated
Viz.*data RPCs (Timeline,ReleaseEvents,TestResults,BakeoffResults,BenchTelemetry,Knowledge,TestMatrix,CloneEvents,Architecture,Jobs,Coverage,HolgerRegistry— see theservice Vizinproto/nornir.protofrom line 809, the authoritative list) are thin mirrors of the fat query fns — the same data the viz panes use (seearchitecture-wiring.md§2a). The CLI verbs above (release events,test history,bakeoff leaderboard,diagram timeline) ride those same RPCs, so CLI ≡ viz ≡ MCP over one server API.
3. Wiring map — status of the EPIC ARCH generator
The plan item for this is EPIC ARCH in nordisk_plan.md §"living architecture diagrams: UI buttons ↔ gRPC backends, wired (skade-style SVG)" (also tracked as AUT7 under EPIC AUTONOM). It specifies, across ARCH1–ARCH5:
- ARCH1 — enumerate viz surfaces (
Facet ∩ registry()+ tab enum) and the gRPC verb each thin surface calls; emit the wiring as data. - ARCH2 — a coarsened wiring graph (owner-fn quotient: UI-component · gRPC · core fn · table nodes) from
call_edges+ the newfn → tableaccess fact. - ARCH3 — skade-style Typst→SVG render (NOT mermaid), regenerated by
nornir docs, embedded in.nornirdesign docs. - ARCH4 — storage decision:
workspace_*/asset vs warehousearchitecture_wiringtable (historized by git sha). - ARCH5 — CLI + MCP parity:
nornir arch [svg | trace <entrypoint>]+ an MCP tool reusingknowledge_call_path.
Status in code: the generator EXISTS and ships today — the plan checkboxes ([ ]) are stale drift, not an accurate status. Concretely:
- The generator is
src/arch/mod.rs:arch::generatebuilds the coarsenedArchGraph(the owner-fn quotient —impl Facet for T→ Component,*SvcTrait for *Svchandler →Service.VerbgRPC,query_/append_accessor + thefn→tableaccess fact → Table, else CoreFn), andto_svg()renders it as a skade circuit-board SVG (direct-emit, no mermaid, no typst engine — available in every feature set). The<dynamic>table and intra-node self-edges are dropped. - The
fn → tableaccess fact (ARCH2's missing edge) is real:src/warehouse/access_scan.rs→warehouse_access_edges. - CLI verb (the SVG generator the plan asks for):
nornir arch generate <repo>(graph + persist) andnornir arch svg <repo>(emit the SVG);nornir arch history <repo>lists the historized rows. Dispatched atCmd::Arch→run_arch. - Storage (ARCH4): the warehouse table
architecture_wiring(src/warehouse/iceberg_schema.rsarchitecture_wiring()), one row per generated graph keyed bygit_sha(time-travel), carrying both the graph JSON and the SVG. Written byarch::warehouse::record_arch_wiring. - MCP parity (ARCH5):
arch_graph(JSON{nodes,edges}) andarch_svg(the SVG) MCP tools insrc/bin/nornir-mcp.rs, mirroring the CLI.
So: yes, a CLI command produces the wiring SVG today — nornir arch svg <repo>. It covers the CLI/viz↔gRPC↔core-fn↔warehouse-table boundary as a component/gRPC/core/table circuit board (the auto-generated twin of architecture-wiring.md §2). The human-authored doc is the acceptance fixture the generator must reproduce. (For reference, crate-level dep graphs come from a separate path — mimir svg / introspect depgraph / the dep_graph_svg MCP tool — and are not the same as this fn-level wiring board.)
The table in §2 above is the textual form of that map for the CLI surface: each row is one CLI↔gRPC↔handler edge, exactly the enumeration ARCH1 specifies for the UI surface but for the command line.
4. The laws this map reflects
- CLI parity with viz/GUI — every viz pane and MCP tool has a CLI command on the same gRPC API: the
Viz.*data RPCs,Search/Vector/Knowledge/Mimir/Release/Funnel/Docs/Workspaces, andarchall have a CLI verb. The--server/--token/--workspaceflags make any verb thin, so a human at the CLI reaches exactly what the viz and an LLM agent reach. - Layer-boundary discoverability — the wiring is enumerable precisely because the boundaries are type/name-discoverable:
impl Facet for Tmarks UI components,*SvcTrait for *Svchandler methods (e.g.VizSvc::timeline→Viz.Timeline) mark gRPC verbs, andquery_<table>/append_<table>accessors plus thefn→tableaccess scan mark the one edge no call graph yields (which fn touches which table). The arch generator reads exactly these markers — the markers are why both this table and the SVG can be derived mechanically rather than hand-maintained.