@nurix/etna 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,17 +6,26 @@ A second install path also exists and is fully supported: `/plugin install <plug
6
6
 
7
7
  ## Usage
8
8
 
9
- From the root of the project you want to bootstrap:
9
+ From the root of the project you want to bootstrap, pick a harness from the interactive menu:
10
10
 
11
11
  ```sh
12
- npx @nurix/etna code
12
+ npx @nurix/etna
13
+ ```
14
+
15
+ …or install one directly by name:
16
+
17
+ ```sh
18
+ npx @nurix/etna --name=code
13
19
  ```
14
20
 
15
21
  After this completes:
16
22
 
17
23
  ```
18
24
  your-project/
19
- ├── CLAUDE.md ← harness policy (your existing file → CLAUDE.md.bak.<ISO>)
25
+ ├── CLAUDE.md ← harness policy (non-git repo: existing file → CLAUDE.md.bak.<ISO>)
26
+ ├── .prettierrc.json ← markdown formatting (proseWrap: never)
27
+ ├── .prettierignore
28
+ ├── .vscode/settings.json ← editor settings (markdown formatter on save)
20
29
  └── .claude/
21
30
  ├── skills/ ← documentation, boundary, distiller, brainstorm, workflow-mapper
22
31
  ├── agents/ ← harness-manager, spec-mode, brainstorm, maintenance, change-review, performance-review
@@ -28,7 +37,7 @@ your-project/
28
37
 
29
38
  That's the entire footprint. Nothing is written outside the project. No marketplace registration, no `enabledPlugins` entry, no global cache dependency — Claude Code picks up the project-scoped agents/skills/rules on its next session start, and the hooks wire in through `.claude/settings.json`.
30
39
 
31
- Re-run `npx @nurix/etna code` anytime to refresh the harness content with what the latest published package ships. Existing same-named files are overwritten; files you've added that aren't part of the harness are left alone. The previous `CLAUDE.md` is backed up to `CLAUDE.md.bak.<ISO-timestamp>`. Because the new `harness.json` carries the installed `{version, epoch, digest}`, a re-run that's ahead of your repo's stamped `harness-version` is what the reconcile (below) detects as an upgrade.
40
+ Re-run `npx @nurix/etna --name=code` anytime to refresh the harness content with what the latest published package ships. Existing same-named files are overwritten; files you've added that aren't part of the harness are left alone. In a git repo the previous `CLAUDE.md` is overwritten in place (recover it with `git checkout CLAUDE.md`); outside a git repo it's backed up to `CLAUDE.md.bak.<ISO-timestamp>`. Because the new `harness.json` carries the installed `{version, epoch, digest}`, a re-run that's ahead of your repo's stamped `harness-version` is what the reconcile (below) detects as an upgrade.
32
41
 
33
42
  ## After install: bootstrap the harness
34
43
 
package/bin/etna.js CHANGED
@@ -43,15 +43,16 @@ function isInteractive() {
43
43
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
44
44
  }
45
45
 
46
- // Boxed list of installable harnesses, for --help and the non-interactive no-arg path.
46
+ // Boxed list of installable harnesses (name + description), for --help and error paths.
47
47
  function printHarnessList(harnesses) {
48
48
  const entries = Object.entries(harnesses);
49
49
  if (!entries.length) {
50
50
  p.log.warn('No harnesses are bundled. Run `npm run build:data` from the package root.');
51
51
  return;
52
52
  }
53
+ const width = Math.max(...entries.map(([folder]) => folder.length));
53
54
  const lines = entries.map(
54
- ([folder, plugin]) => `${folder}${plugin && plugin !== folder ? ` (plugin: ${plugin})` : ''}`,
55
+ ([folder, meta]) => `${folder.padEnd(width)}${meta?.description ? ` ${meta.description}` : ''}`,
55
56
  );
56
57
  p.note(lines.join('\n'), 'Available harnesses');
57
58
  }
@@ -216,35 +217,32 @@ function mergeHooks(targetDir, bundledHooksJson) {
216
217
  return added;
217
218
  }
218
219
 
219
- // Resolve the harness to install: the positional arg, or — in a TTY — an interactive
220
- // pick. Returns the chosen folder name, or null if the user cancels / can't be prompted.
221
- async function resolveHarnessName(positional, harnesses) {
220
+ // Resolve the harness to install: the --name value, or — in a TTY — an interactive pick
221
+ // showing each harness's description. Returns the chosen folder, or null on cancel.
222
+ async function resolveHarnessName(name, harnesses) {
222
223
  const names = Object.keys(harnesses);
223
- if (positional) {
224
- if (!harnesses[positional]) {
225
- p.log.error(`Unknown harness '${positional}'.`);
224
+ if (name !== null) {
225
+ if (!name || !harnesses[name]) {
226
+ p.log.error(name ? `Unknown harness '${name}'.` : 'Empty --name value.');
226
227
  printHarnessList(harnesses);
227
- p.outro('Pick one of the harnesses above.');
228
+ p.outro('Pass --name=<harness> with one of the names above.');
228
229
  process.exit(1);
229
230
  }
230
- return positional;
231
+ return name;
231
232
  }
232
233
  if (!isInteractive()) {
233
- p.log.error('No harness specified, and not running in an interactive terminal.');
234
+ p.log.error('No --name given, and not running in an interactive terminal.');
234
235
  printHarnessList(harnesses);
235
- p.outro('Re-run as `npx @nurix/etna <harness-name>`.');
236
+ p.outro('Re-run as `npx @nurix/etna --name=<harness>`.');
236
237
  process.exit(1);
237
238
  }
238
239
  const choice = await p.select({
239
240
  message: 'Which harness do you want to install here?',
240
- options: names.map((folder) => {
241
- const plugin = harnesses[folder];
242
- return {
243
- value: folder,
244
- label: folder,
245
- hint: plugin && plugin !== folder ? `plugin: ${plugin}` : undefined,
246
- };
247
- }),
241
+ options: names.map((folder) => ({
242
+ value: folder,
243
+ label: folder,
244
+ hint: harnesses[folder]?.description || undefined,
245
+ })),
248
246
  });
249
247
  if (p.isCancel(choice)) {
250
248
  p.cancel('Cancelled — nothing installed.');
@@ -273,7 +271,7 @@ async function installHarness(harnessName, harnesses) {
273
271
  const versionTag = harnessMeta.version ? ` v${harnessMeta.version}` : '';
274
272
  // The underlying plugin's name — may differ from the harness name, and several
275
273
  // harnesses may share it. Used only for legacy marketplace-entry cleanup.
276
- const pluginName = harnessMeta.name || harnesses[harnessName] || harnessName;
274
+ const pluginName = harnessMeta.name || harnesses[harnessName]?.plugin || harnessName;
277
275
 
278
276
  const spin = p.spinner();
279
277
  spin.start(`Installing ${harnessName}${versionTag} into ${targetDir}`);
@@ -346,32 +344,62 @@ async function installHarness(harnessName, harnesses) {
346
344
 
347
345
  p.outro(
348
346
  `${harnessName}${versionTag} ready in ${targetDir}. ` +
349
- `Re-run \`npx @nurix/etna ${harnessName}\` anytime to refresh harness content.`,
347
+ `Re-run \`npx @nurix/etna --name=${harnessName}\` anytime to refresh harness content.`,
350
348
  );
351
349
  }
352
350
 
351
+ // Parse argv. `--name=<harness>` / `--name <harness>` selects a harness; -h/--help shows
352
+ // help; any other bare token is the dropped positional form (a breaking change in 0.18.0).
353
+ function parseArgs(args) {
354
+ let name = null;
355
+ let help = false;
356
+ let positional = null;
357
+ for (let i = 0; i < args.length; i++) {
358
+ const a = args[i];
359
+ if (a === '-h' || a === '--help') help = true;
360
+ else if (a.startsWith('--name=')) name = a.slice('--name='.length);
361
+ else if (a === '--name') name = args[++i] ?? '';
362
+ else if (!a.startsWith('-') && positional === null) positional = a;
363
+ }
364
+ return { name, help, positional };
365
+ }
366
+
353
367
  async function main(args) {
354
368
  const idx = loadHarnessIndex();
355
369
  const harnesses = idx.harnesses || {};
370
+ const { name, help, positional } = parseArgs(args);
356
371
 
357
- if (args.includes('-h') || args.includes('--help')) {
372
+ if (help) {
358
373
  p.intro('@nurix/etna — Nurix harness installer');
359
- p.log.message('Usage: npx @nurix/etna [harness-name]');
374
+ p.log.message(
375
+ 'Usage:\n' +
376
+ ' npx @nurix/etna pick a harness from the menu\n' +
377
+ ' npx @nurix/etna --name=<harness> install that harness directly',
378
+ );
360
379
  printHarnessList(harnesses);
361
- p.outro('Run with a name to install, or with no name to pick interactively.');
380
+ p.outro('No args → interactive menu. --name=<harness> → direct install.');
362
381
  return;
363
382
  }
364
383
 
365
- const positional = args.find((a) => !a.startsWith('-'));
366
-
367
384
  p.intro('@nurix/etna — Nurix harness installer');
368
385
 
386
+ // Breaking change (0.18.0): the positional form `etna <harness>` was replaced by
387
+ // `--name=<harness>`. Fail loudly with the new syntax rather than silently misfiring.
388
+ if (positional && name === null) {
389
+ p.log.error(
390
+ `Positional arguments are no longer supported. Install with \`npx @nurix/etna --name=${positional}\`.`,
391
+ );
392
+ printHarnessList(harnesses);
393
+ p.outro('Use --name=<harness> to install, or no args to pick from the menu.');
394
+ process.exit(1);
395
+ }
396
+
369
397
  const boot = p.spinner();
370
398
  boot.start('Starting up');
371
399
  await sleep(2000);
372
400
  boot.stop('Ready');
373
401
 
374
- const harnessName = await resolveHarnessName(positional, harnesses);
402
+ const harnessName = await resolveHarnessName(name, harnesses);
375
403
  if (!harnessName) return; // user cancelled the interactive pick
376
404
 
377
405
  await installHarness(harnessName, harnesses);
@@ -1,10 +1,25 @@
1
1
  {
2
- "fingerprint": "50dc8734b7ee",
2
+ "fingerprint": "2a8b6a3ea74d",
3
3
  "harnesses": {
4
- "code": "code-fable",
5
- "code-fable": "code-fable",
6
- "discover": "discover",
7
- "hr": "hr",
8
- "kb-agi": "kb-agi"
4
+ "code": {
5
+ "plugin": "code-fable",
6
+ "description": "Nurix code-project harness"
7
+ },
8
+ "code-fable": {
9
+ "plugin": "code-fable",
10
+ "description": "Nurix code-project harness"
11
+ },
12
+ "discover": {
13
+ "plugin": "discover",
14
+ "description": "Product Discovery harness"
15
+ },
16
+ "hr": {
17
+ "plugin": "hr",
18
+ "description": "Hiring / AI-Steering Evaluation harness"
19
+ },
20
+ "kb-agi": {
21
+ "plugin": "kb-agi",
22
+ "description": "self-improving knowledge-base harness"
23
+ }
9
24
  }
10
25
  }
@@ -15,7 +15,7 @@ Reconcile is **declarative, not a migration ladder.** You converge to the *curre
15
15
 
16
16
  ## The two version layers (don't confuse them)
17
17
 
18
- - The **CLI** (`npx @nurix/etna code`) refreshes the *engine* — `CLAUDE.md` + `.claude/{agents,skills,rules,scripts}/`. That is not your job; the install does it.
18
+ - The **CLI** (`npx @nurix/etna --name=code`) refreshes the *engine* — `CLAUDE.md` + `.claude/{agents,skills,rules,scripts}/`. That is not your job; the install does it.
19
19
  - **You** reconcile the *repo's authored data* — the `docs/` tree — to match that engine.
20
20
 
21
21
  The upgrade story is the two run back-to-back: install refreshes the engine → you reconcile the docs to it.
@@ -37,7 +37,7 @@ You never edit a doc for content fidelity (that is `maintenance`); `maintenance`
37
37
  ## Before starting
38
38
 
39
39
  1. Verify a **root `CLAUDE.md` exists** before any reconcile. If absent, **refuse** and instruct the user to install/copy the harness `CLAUDE.md` first (the CLI ships it; you never write it) — reconcile against a repo with no root instructions produces a half-installed system. Detect-and-instruct, not a gate.
40
- 2. Resolve the **installed harness epoch** — read it from `.claude/harness.json`, shipped by the CLI at install. Never a number baked into this file. If `.claude/harness.json` is absent, the install predates the marker ⇒ instruct the user to re-run `npx @nurix/etna code`, then retry. The resolved epoch is the target.
40
+ 2. Resolve the **installed harness epoch** — read it from `.claude/harness.json`, shipped by the CLI at install. Never a number baked into this file. If `.claude/harness.json` is absent, the install predates the marker ⇒ instruct the user to re-run `npx @nurix/etna --name=code`, then retry. The resolved epoch is the target.
41
41
  3. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter. Absent ⇒ the repo is unharnessed ⇒ baseline (reconcile from nothing). Equal to installed ⇒ report and stop, **except** when re-invoked for the Step 6 module proposal: the version is already current from your first pass, so skip the stop and go straight to Step 6. (Health asked ⇒ run Health.) Behind ⇒ reconcile forward.
42
42
  4. If `docs/memory/harness-manager.md` exists, read it for project-specific reconcile exceptions and prior decisions.
43
43
 
@@ -158,7 +158,7 @@ Confirm all mandated files exist; `structure.md` carries the current `harness-ve
158
158
  Conformance, not fidelity. Report a table; fix nothing destructively without asking.
159
159
 
160
160
  - **Version currency** — `harness-version` present in `structure.md` and equal to installed? If behind/absent → recommend Reconcile. (This is the *repo-vs-installed* gap — it self-heals once the consumer reinstalls and re-runs you. The next two checks catch what reinstall *hasn't* happened yet, or was bypassed by a hand-edit.)
161
- - **Published-version currency** *(advisory, network)* — the installed `.claude/harness.json` records the kit's `version`; the published kit may have moved past it. Attempt `npm view @nurix/etna version` (the kit is a restricted package — needs npm auth + network). **Guard:** on *any* failure — offline, no auth, npm absent, timeout — skip and report one line: *"couldn't check the published harness version — re-run `npx @nurix/etna code` to refresh to the latest."* On success, compare the published version against `.claude/harness.json`'s `version`; if published is ahead, recommend re-running the CLI to pull the newer kit. Never a gate, never blocks.
161
+ - **Published-version currency** *(advisory, network)* — the installed `.claude/harness.json` records the kit's `version`; the published kit may have moved past it. Attempt `npm view @nurix/etna version` (the kit is a restricted package — needs npm auth + network). **Guard:** on *any* failure — offline, no auth, npm absent, timeout — skip and report one line: *"couldn't check the published harness version — re-run `npx @nurix/etna --name=code` to refresh to the latest."* On success, compare the published version against `.claude/harness.json`'s `version`; if published is ahead, recommend re-running the CLI to pull the newer kit. Never a gate, never blocks.
162
162
  - **Kit build fingerprint (digest)** *(informational)* — `.claude/harness.json` carries a `digest`: a deterministic fingerprint of the exact kit **build** the consumer installed (`build-data` computes it over the shipped bundle). Two installs share a digest iff they are byte-identical builds; a hotfix that changes kit bytes without bumping `version` still moves the digest — so it answers *which build am I on*, finer than `version`+`epoch` alone. **Don't recompute it over `.claude/` to self-check:** the installed layout is a superset of the build (the project `CLAUDE.md` sits at the repo root, `.claude/` adds `settings.json`/`harness.json`/local rule files), so paths and file sets differ **by design** — a recompute would never match. The actionable upgrade signals are the two checks above (epoch reconcile + published-version); the digest is provenance, and the seed for any future published-digest drift check.
163
163
  - **Scaffold completeness** — every mandated file present.
164
164
  - **Dissolved-version residue** — any `operations/guardrails.md`, repo-wide `docs/pillars/`, `architecture/overview.md`, a persisted `docs/boundaries/` tree, or other old-shape artifact lingering.
@@ -8,5 +8,5 @@
8
8
  ".prettierignore",
9
9
  ".vscode/settings.json"
10
10
  ],
11
- "digest": "da02e3f9ca11"
11
+ "digest": "a897772819ce"
12
12
  }
@@ -15,7 +15,7 @@ Reconcile is **declarative, not a migration ladder.** You converge to the *curre
15
15
 
16
16
  ## The two version layers (don't confuse them)
17
17
 
18
- - The **CLI** (`npx @nurix/etna code`) refreshes the *engine* — `CLAUDE.md` + `.claude/{agents,skills,rules,scripts}/`. That is not your job; the install does it.
18
+ - The **CLI** (`npx @nurix/etna --name=code`) refreshes the *engine* — `CLAUDE.md` + `.claude/{agents,skills,rules,scripts}/`. That is not your job; the install does it.
19
19
  - **You** reconcile the *repo's authored data* — the `docs/` tree — to match that engine.
20
20
 
21
21
  The upgrade story is the two run back-to-back: install refreshes the engine → you reconcile the docs to it.
@@ -37,7 +37,7 @@ You never edit a doc for content fidelity (that is `maintenance`); `maintenance`
37
37
  ## Before starting
38
38
 
39
39
  1. Verify a **root `CLAUDE.md` exists** before any reconcile. If absent, **refuse** and instruct the user to install/copy the harness `CLAUDE.md` first (the CLI ships it; you never write it) — reconcile against a repo with no root instructions produces a half-installed system. Detect-and-instruct, not a gate.
40
- 2. Resolve the **installed harness epoch** — read it from `.claude/harness.json`, shipped by the CLI at install. Never a number baked into this file. If `.claude/harness.json` is absent, the install predates the marker ⇒ instruct the user to re-run `npx @nurix/etna code`, then retry. The resolved epoch is the target.
40
+ 2. Resolve the **installed harness epoch** — read it from `.claude/harness.json`, shipped by the CLI at install. Never a number baked into this file. If `.claude/harness.json` is absent, the install predates the marker ⇒ instruct the user to re-run `npx @nurix/etna --name=code`, then retry. The resolved epoch is the target.
41
41
  3. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter. Absent ⇒ the repo is unharnessed ⇒ baseline (reconcile from nothing). Equal to installed ⇒ report and stop, **except** when re-invoked for the Step 6 module proposal: the version is already current from your first pass, so skip the stop and go straight to Step 6. (Health asked ⇒ run Health.) Behind ⇒ reconcile forward.
42
42
  4. If `docs/memory/harness-manager.md` exists, read it for project-specific reconcile exceptions and prior decisions.
43
43
 
@@ -158,7 +158,7 @@ Confirm all mandated files exist; `structure.md` carries the current `harness-ve
158
158
  Conformance, not fidelity. Report a table; fix nothing destructively without asking.
159
159
 
160
160
  - **Version currency** — `harness-version` present in `structure.md` and equal to installed? If behind/absent → recommend Reconcile. (This is the *repo-vs-installed* gap — it self-heals once the consumer reinstalls and re-runs you. The next two checks catch what reinstall *hasn't* happened yet, or was bypassed by a hand-edit.)
161
- - **Published-version currency** *(advisory, network)* — the installed `.claude/harness.json` records the kit's `version`; the published kit may have moved past it. Attempt `npm view @nurix/etna version` (the kit is a restricted package — needs npm auth + network). **Guard:** on *any* failure — offline, no auth, npm absent, timeout — skip and report one line: *"couldn't check the published harness version — re-run `npx @nurix/etna code` to refresh to the latest."* On success, compare the published version against `.claude/harness.json`'s `version`; if published is ahead, recommend re-running the CLI to pull the newer kit. Never a gate, never blocks.
161
+ - **Published-version currency** *(advisory, network)* — the installed `.claude/harness.json` records the kit's `version`; the published kit may have moved past it. Attempt `npm view @nurix/etna version` (the kit is a restricted package — needs npm auth + network). **Guard:** on *any* failure — offline, no auth, npm absent, timeout — skip and report one line: *"couldn't check the published harness version — re-run `npx @nurix/etna --name=code` to refresh to the latest."* On success, compare the published version against `.claude/harness.json`'s `version`; if published is ahead, recommend re-running the CLI to pull the newer kit. Never a gate, never blocks.
162
162
  - **Kit build fingerprint (digest)** *(informational)* — `.claude/harness.json` carries a `digest`: a deterministic fingerprint of the exact kit **build** the consumer installed (`build-data` computes it over the shipped bundle). Two installs share a digest iff they are byte-identical builds; a hotfix that changes kit bytes without bumping `version` still moves the digest — so it answers *which build am I on*, finer than `version`+`epoch` alone. **Don't recompute it over `.claude/` to self-check:** the installed layout is a superset of the build (the project `CLAUDE.md` sits at the repo root, `.claude/` adds `settings.json`/`harness.json`/local rule files), so paths and file sets differ **by design** — a recompute would never match. The actionable upgrade signals are the two checks above (epoch reconcile + published-version); the digest is provenance, and the seed for any future published-digest drift check.
163
163
  - **Scaffold completeness** — every mandated file present.
164
164
  - **Dissolved-version residue** — any `operations/guardrails.md`, repo-wide `docs/pillars/`, `architecture/overview.md`, a persisted `docs/boundaries/` tree, or other old-shape artifact lingering.
@@ -8,5 +8,5 @@
8
8
  ".prettierignore",
9
9
  ".vscode/settings.json"
10
10
  ],
11
- "digest": "c62c79b3aff1"
11
+ "digest": "7ce2d78b5ead"
12
12
  }
@@ -24,7 +24,7 @@ mistake, the redirect that pulled it off a bad path. Turn count, prompt length,
24
24
  ## Step 0 — first run
25
25
 
26
26
  If the `interview-eval` skill isn't resolvable (no `.claude/skills/interview-eval/`), this project isn't set
27
- up — tell the user to run `npx @nurix/etna hr` (or point you at the skill) before continuing. Confirm
27
+ up — tell the user to run `npx @nurix/etna --name=hr` (or point you at the skill) before continuing. Confirm
28
28
  `python3` is on PATH; the extractor is stdlib-only and needs nothing else.
29
29
 
30
30
  ## Step 1 — intake interview
@@ -8,5 +8,5 @@
8
8
  ".prettierignore",
9
9
  ".vscode/settings.json"
10
10
  ],
11
- "digest": "95bc2ce3024d"
11
+ "digest": "c1632f70fcdb"
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {