<!-- nornir:class: history -->
# 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](mode-matrix.md) (the SOURCE vs
DERIVED model) and [running-as-a-server.md](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](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
```
```sh
mkdir -p workspace_<name>/release workspace_<name>/.nornir/logs
```
### 2. `.gitignore` + `.gitattributes`
```gitignore
# .gitignore — local iceberg/redb state, rebuilt by `nornir bench run`
.nornir/warehouse/
.nornir/logs/
```
```gitattributes
# .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).
```toml
[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
```sh
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.
```sh
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…
```sh
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](mcp-from-claude-code.md)).
### 8. First bench run establishes the baseline
```sh
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.toml` lists every member with the right transport
- [ ] private members resolve via the server's deploy key (`nornir key show`)
- [ ] `release/nornir.toml` publish order matches each repo's `CLAUDE.md`
- [ ] `[guard].forbidden` covers 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