Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/creating-a-workspace.md

Creating a workspace (workspace_<name>) from scratch

How to stand up a new nornir-monitored workspace end-to-end. The pieces are documented in fragments across mode-matrix.md (the SOURCE vs DERIVED model) and running-as-a-server.md (the monitor flags); this is the single recipe that ties them together. Worked example: workspace_znippy (created 2026-06-09), modelled on workspace_holger.

What a workspace repo is — and is not

A workspace_<name>/ repo is the control plane, not the code. It holds:

File / dir Role
nornir-workspace.toml SOURCE descriptor — the member repos + their git URLs
release/nornir.toml per-repo publish order, gates, and the [guard] table
release/SPEC.md the declarative release-gate contract (human/LLM reads it)
WORKSPACE.md coordination doc — tooling policy, guard rails, bench pattern
.nornir/warehouse/ iceberg bench_runs table (gitignored; LFS for parquet)
.nornir/logs/ release-run logs (gitignored)

No source code lives here. The members are cloned into <home>/.nornir/workspaces/<name>/git/<member>/ — at register time (register is eager by default; see step 7) and thereafter refreshed by the server's monitor poll loop on a git HEAD change.

Where the data lives (not in this repo). The workspace_<name>/ repo is the control plane (config + descriptor only). The data — warehouse, funnel, cloned members — lives under the home-derived root ~/.nornir/workspaces/<name>/{git,builds/warehouse,funnel} (the server uses /home/nornir/.nornir/...; same code, same rule). Run nornir root to see it. The old in-repo .nornir/warehouse/ is legacy; new workspaces let the warehouse default to the per-workspace home path. See data-root.md.

Steps

1. Scaffold the repo (sibling of the code checkouts)

git/
├── <name>/                 # the code (e.g. znippy/)
├── nornir/                 # the tooling (sibling checkout)
└── workspace_<name>/       # ← what you are creating
mkdir -p workspace_<name>/release workspace_<name>/.nornir/logs

2. .gitignore + .gitattributes

# .gitignore — local iceberg/redb state, rebuilt by `nornir bench run`
.nornir/warehouse/
.nornir/logs/
# .gitattributes — warehouse parquet + cache dbs go through LFS
.nornir/warehouse/**/*.parquet filter=lfs diff=lfs merge=lfs -text
.nornir/cache/**/*.db filter=lfs diff=lfs merge=lfs -text

3. nornir-workspace.toml — the SOURCE descriptor

One [repos.<member>] per repo. Public repos clone over pure-Rust HTTPS (no credential); private repos clone over SSH with the server's deploy key (add nornir key show on the host to the codeberg deploy keys).

[workspace]
name = "<name>"
# deep_scan = true   # optional: republish also scans the FULL transitive dep
                     # closure (gatling-fanned) into the warehouse under repo `deps`

[repos.<primary>]
git = "https://codeberg.org/<org>/<primary>"     # public → HTTPS

[repos.nornir]
git = "git@codeberg.org:<org>/nornir.git"        # private → SSH deploy key

A repo may belong to more than one workspace (e.g. znippy is a dependency member of workspace_holger and the primary member of workspace_znippy). Each workspace gets its own warehouse + MCP scope.

4. release/nornir.toml — recipes + guard rails

Append one [repo.<name>] table per member with remote, history, readme, publish_order (inner lists publish-parallel, outer sequential), a [.gates] table, and a [.bench] table listing the required_results one bench run must produce. The top-level [guard].forbidden lists workspace-root-relative paths agents must not edit (append-only histories, generated docs, the contract itself). See workspace_znippy/release/nornir.toml for a filled example.

5. release/SPEC.md + WORKSPACE.md

Copy from a sibling workspace and adapt: drop gates that don't apply (e.g. nexus-floor is holger-only), fix the bench-result shapes, set the publish order to match the repo's CLAUDE.md §Release procedure.

6. Commit

cd workspace_<name>
git init -q
git remote add origin git@codeberg.org:<org>/workspace_<name>.git
git add -A && git commit -m "workspace_<name>: nornir control plane"
# create the codeberg repo, then: git push -u origin main

7. Register with the server (on the host, oden)

register is EAGER by default: it records the workspace AND clones its members + builds the warehouse at once (the same pipeline the poll loop runs on a change), so the workspace is populated immediately — no waiting for the first tick. The poll loop then handles UPDATES on a git HEAD change.

nornir workspace register <name> \
  --descriptor /abs/path/workspace_<name>/nornir-workspace.toml \
  --monitored --poll 60s            # eager: clones + builds at once

# opt OUT (record only; population deferred to the poll loop) with --lazy:
nornir workspace register <name> --descriptor … --monitored --poll 60s --lazy

(nornir workspace add <name> … is the explicit alias for the eager path — identical behaviour. The MCP workspace_register tool is eager by default too, returning the populated members; pass lazy: true for record-only.)

There is no install-time workspace flag. The systemd unit launches the server with bind + auth only (it has no config ingress); you register against the running server instead. Install the service once…

sudo nornir install add --addr 0.0.0.0:7878   # bind + auth only; serves 0 until you register

…then register the workspace at runtime as above (NORNIR_SERVER + token set). It persists in the registry and is re-served on the next boot. (The descriptor's TOML content is stored in the registry row — no descriptors/*.toml file. install add has no --monitor/--poll flags and writes no NORNIR_MONITOR/NORNIR_POLL env lines — removed in 0c240f9/9ea97c4; unit tests on render_server_unit assert the generated unit carries neither. Register at runtime instead.)

After registration the workspace joins the monitored set and its name becomes a valid NORNIR_WORKSPACE scope for the MCP server (see mcp-from-claude-code.md).

8. First bench run establishes the baseline

nornir bench run <name>      # spawns each member's xtask/examples/nornir-bench.rs

The first run on a new machine sets the regression baseline (no regression fires). Results land in .nornir/warehouse and feed the README's <!-- nornir:gen --> tables.

Checklist