# @nurix/etna

CLI to bootstrap a project with a Nurix harness. It writes the harness in two layers. The **contract** is committed with your code: `CLAUDE.md`, `AGENTS.md`, `.claude/harness.json` (which harness, which version, where it comes from), `.claude/settings.json` and the one session-start hook. The **mounted harness** (skills, agents, rules, hooks) is gitignored, and every session start keeps it current from the harness CDN.

A second install path also exists and is fully supported: `/plugin install <plugin>@nurixlabs` from inside Claude Code, which installs the harness as a plugin from the `nurixlabs/skills` marketplace. The two paths are first-class equals — pick the one that matches how you want to manage the harness. See [`docs/playbooks/harness/Readme.md`](../../docs/playbooks/harness/Readme.md#the-install-model) for the comparison.

## Usage

From the root of the project you want to bootstrap, pick a harness from the interactive menu:

```sh
npx --yes @nurix/etna
```

…or install one directly by name:

```sh
npx --yes @nurix/etna --name=code
```

The `--yes` flag skips npx's install confirmation, so the command runs unattended. On startup the CLI also **self-updates**: it checks the registry and, if the copy npx resolved is behind the latest published version, re-execs the latest before installing — so `npx --yes @nurix/etna` always runs the newest harness, even when npx has an older copy cached. The check is quick but synchronous; set `ETNA_NO_SELFUPDATE=1` to skip it (CI, offline, or a pinned local build).

After this completes (the `code` harness shown — the component folders differ per harness, and one a harness ships nothing for is simply absent):

```
your-project/
├── CLAUDE.md              ← committed: harness policy (a pre-existing file moves aside as CLAUDE.bkp.md on first install)
├── AGENTS.md              ← committed: Codex — CLAUDE.md byte-for-byte + a generated Codex runtime adapter
├── .gitignore             ← gains the managed `# etna:mount:start` … `# etna:mount:end` block
├── .prettierrc.json, .prettierignore, .vscode/settings.json   ← committed project config
├── .codex/hooks.json      ← committed: Codex's SessionStart hook (runs mount.mjs)
├── .codex/agents/         ← mounted: Codex subagents
├── .agents/skills/        ← mounted: mirror of .claude/skills/ for Codex
└── .claude/
    ├── harness.json       ← committed: {harness, version, digest, docsRoot, mount: {origin, track}, owned, …}
    ├── settings.json      ← committed: the hooks deep-merged in (your existing keys preserved)
    ├── hooks/mount.mjs    ← committed: the SessionStart hook that mounts everything else
    ├── skills/ agents/ rules/ hooks/…   ← mounted, gitignored by name (your own components stay tracked)
    └── state/             ← machine-local: mount.json and the hooks' state, self-ignored
```

**At every session start** `.claude/hooks/mount.mjs` mounts the version `harness.json` pins, from the machine cache (`~/.etna/cache`) or the origin (`https://cdn.nustack.io` by default). It then checks `latest.json` for a newer version. When it finds one, it downloads the version beside the running one and prints *Harness code {new} is ready (mounted {old}); restart the session to run on it*. With `--track=latest` (the default) it also moves `version` in `harness.json`; commit that change like a lockfile bump. With `--track=pinned` it only prints the notice. `--origin=<url>` points a private network at its own origin. Offline with a warm cache, the session mounts from the cache. Offline with a cold cache, it prints one line and runs on what is on disk.

`npx --yes @nurix/etna --name=code` refreshes the contract and remounts from the bundle. A component absent from the manifest is pruned; files you've added that aren't part of the harness are left alone. **On a repository whose tracked set includes mounted paths**, the CLI prints the exact `git rm -r --cached -- <paths>` that adopts the mount; the files stay on disk, and you run and commit it yourself. Because `harness.json` carries the installed `{version, digest}`, a re-run whose **major** version is ahead of your repo's stamped `harness-version` is what the reconcile (below) detects as a structural upgrade.

## After install: bootstrap the harness

Installing only _drops the files_ — the harness isn't active for your repo until you **baseline** it. Open Claude Code in the project and drive the reconcile chain once (the installed `CLAUDE.md` carries the same steps under its `## Maintenance` heading, for the agent):

1. **"baseline this repo"** (or "reconcile the harness") — runs the **`maintenance`** agent, which auto-selects **baseline mode** from the missing/behind `harness-version`. It builds the `docs/` tree to the harness shape, stamps `docs/structure.md` with the installed `harness-version`, proposes **modules** (as `module:` labels, derived from your code's bounded contexts), then hands back.
2. If it hands back with a follow-up (typically the module proposal), re-invoke it to finish.

Then work normally. Boundaries aren't generated as files — the `boundary` skill diagnoses cross-cutting contracts from source at plan time. This brownfield path reads your _actual_ code, not a blank slate, so it's safe to run on an existing project with real history.

One thing worth knowing on an existing repo:

- **Your root `CLAUDE.md` is replaced** with the harness policy file (recoverable from git, or from `CLAUDE.md.bak.<ISO>` outside a git repo). If it held project-specific instructions, fold them back in after install — the harness file is the policy skeleton; your domain specifics sit alongside it.

## Migrating from 0.1.x

`0.1.x` registered the harness as a Claude Code plugin via the `nurixlabs/skills` marketplace. `0.2.0` switches to direct file installation. When you re-run `etna` on a project that has a `0.1.x` install, the CLI removes the stale `enabledPlugins["<plugin>@nurixlabs"]` entry from `<project>/.claude/settings.json` so Claude Code doesn't double-load the same content from the global plugin cache.

The `nurixlabs` marketplace entry in `~/.claude/settings.json` is harmless to leave (it doesn't load anything by itself) — remove it manually if you want.

## Available harnesses

Listed by `npx --yes @nurix/etna` (no args) using the bundled `data/harness-index.json`.

| Harness | Source in etna | What it ships |
| --- | --- | --- |
| `code` | [`harness/code/`](../../harness/code/) | The canonical Nurix code-project harness — 14 agents, 10 skills, 5 rules, the ownership-index `SessionStart` hook |
| `discover` | [`harness/discover/`](../../harness/discover/) | The product-discovery harness — eight owned skills plus `fetch`, six owned lane and review agents, and one tool-loop hook that enforces the evidence rules. No rules |
| `discussion` | [`harness/discussion/`](../../harness/discussion/) | The deliberation-workspace harness — 2 owned agents, 2 owned rules, and the `update-topics` hook that keeps the topic index current. No skills |
| `etna` | [`harness/etna/`](../../harness/etna/) | The forge's own governance harness — a deliberate fork of `code` reframed for authoring artifacts: 7 agents, 3 skills, 5 rules, the ownership-index hook |
| `hr` | [`harness/hr/`](../../harness/hr/) | The hiring / AI-steering evaluation harness — the `evaluator` skill (session, project and batch modes). No agents, no hooks |
| `kb-agi` | [`harness/kb-agi/`](../../harness/kb-agi/) | The self-improving knowledge-base harness — 3 owned vault agents (`kb-init`, `kb-audit`, `kb-apply`) and the owned `kb-export.mjs` script. No skills, no rules |

## How it works

Each harness lives in a single folder in the etna repo — `harness/<name>/`:

- `readme.md` — the **manifest**: identity (name, description, version, keywords) plus the shared `skills:`/`rules:`/`agents:` lists it pulls in from the top-level catalogs. Its body becomes the harness's published README.
- `CLAUDE.md` — the **policy**: pure policy, no frontmatter, installed as a verbatim copy at the consumer repo's root.
- Any owned `agents/`, `rules/`, `hooks/`, `scripts/` — purpose-built components that belong to this harness alone.

Shared root config common to every harness (`.prettierrc.json`, `.prettierignore`, `.vscode/settings.json`) lives once, in `harness/common/`, instead of duplicated per harness.

Two kinds of membership:

- **Shared, listed** — skills, rules, and agents live in the flat top-level `skills/`/`rules/`/`agents/` catalogs, reusable across harnesses. The manifest names the ones it pulls in:

```yaml
# harness/code/readme.md
---
name: code
skills:
  [boundary, testing, brainstorm, documentation, design]
rules: [general, frontend, express, nomenclature, database, testing, pnpm]
agents:
  [brainstorm, change-review, lld-mode, maintenance, performance-review, retro]
---
```

- **Owned, by location** — hooks, scripts (and any unlisted, harness-specific agents) live inside the harness folder (`harness/<name>/{agents,hooks,scripts}/`). No list — membership is presence in the folder, exactly how a Claude Code plugin ships them. Shared governance agents are listed instead (above) and resolved from the `agents/` catalog, local-first; a harness can still drop its own copy beside it to override one by name.

`scripts/build-data.js` (run by `prepack`) resolves the listed skills/rules/agents from the catalogs (local-first), copies any unlisted harness-owned agents plus the harness's `hooks/` and `scripts/` wholesale into `data/harnesses/<folder>/`, adds the `harness/common/` root files and each bundled agent's `*.codex.toml` twin, and writes the `harness.json` marker (version, `files` list, kit digest); a listed skill/rule/agent that doesn't exist is a hard error. The marketplace channel (`.github/scripts/publish.mjs`) reads the same harness folder and resolves the listed skills/rules/agents the same local-first way — but the two channels' **footprints differ**: the marketplace plugin carries no root policy, and its Codex surface is skills + hooks only (Codex custom agents ship through this CLI). The installer (`bin/etna.js`) reads from the bundle at install time; no GitHub fetch at runtime.

## Building / contributing

`data/` is generated. Don't edit by hand. To regenerate after editing the etna catalogs or manifests:

```sh
cd packages/cli
npm run build:data
```

`prepack` runs `build:data` automatically before `npm publish`, so the bundled data always matches the published package's commit.

## Publishing

CI auto-publishes on push to `dev` when `packages/cli/**` or `harness/**` change — see [`.github/workflows/publish-etna-to-npm.yml`](../../.github/workflows/publish-etna-to-npm.yml). The `publishConfig.access` is `public`, so `npx @nurix/etna` installs and self-updates without any npm token. Publishing still requires auth: the `NPM_PUBLISH_TOKEN` GitHub Actions secret must be a granular access token with **Bypass 2FA** enabled — classic automation tokens are rejected by the npmjs.com 2FA policy on the `nurix-tools` account.
