Gunnar

nordisk/nornir

.nornir/creating-a-workspace.md

<!-- 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