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). Runnornir rootto 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
nornir-workspace.tomllists every member with the right transport- private members resolve via the server's deploy key (
nornir key show) release/nornir.tomlpublish order matches each repo'sCLAUDE.md[guard].forbiddencovers histories + generated docs + the contract- gates trimmed to what actually applies to this constellation
- codeberg
workspace_<name>repo created and pushed - registered at runtime with
nornir workspace register(no install-time seeding) - first
nornir bench run <name>is green and sets the baseline