@nurix/etna 0.17.0 → 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
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // packages/cli/bin/etna.js
3
- // `npx @nurix/etna <harness-name>` — copy a Nurix harness into the current
4
- // directory. Drops the harness's CLAUDE.md at the project root and copies its
5
- // skills, agents, rules, and scripts into <project>/.claude/. No marketplace,
6
- // no plugin enablement — files live in the project, version-controlled and
7
- // editable per project. Re-run `etna` to refresh. Harnesses are addressed by
8
- // their harness/<folder> name; several harnesses may share one plugin.
3
+ // `npx @nurix/etna [harness-name]` — copy a Nurix harness into the current
4
+ // directory. Given a name it installs straight away; with no name (in a TTY) it
5
+ // prompts you to pick one. Drops the harness's CLAUDE.md + project-root config
6
+ // files (.prettierrc.json, .vscode/settings.json, …) at the project root, and
7
+ // copies its skills, agents, rules, scripts, and hooks into <project>/.claude/.
8
+ // No marketplace, no plugin enablement — files live in the project,
9
+ // version-controlled and editable per project. Re-run `etna` to refresh.
10
+ // Harnesses are addressed by their harness/<folder> name; several harnesses may
11
+ // share one plugin.
9
12
 
10
13
  import {
11
14
  existsSync,
@@ -20,6 +23,7 @@ import {
20
23
  } from 'node:fs';
21
24
  import { join, dirname, resolve } from 'node:path';
22
25
  import { fileURLToPath } from 'node:url';
26
+ import * as p from '@clack/prompts';
23
27
 
24
28
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
29
  const pkgRoot = resolve(__dirname, '..');
@@ -31,29 +35,38 @@ const dataDir = join(pkgRoot, 'data');
31
35
  // the settings merge reads it from the bundle, not the project copy).
32
36
  const PLUGIN_SUBDIRS = ['skills', 'agents', 'rules', 'scripts', 'hooks'];
33
37
 
34
- function usage() {
35
- const idx = loadHarnessIndex();
36
- console.log(`\nUsage: npx @nurix/etna <harness-name>\n`);
37
- const entries = Object.entries(idx.harnesses || {});
38
- if (entries.length) {
39
- console.log(`Available harnesses:`);
40
- for (const [folder, plugin] of entries) {
41
- console.log(` ${folder}${plugin && plugin !== folder ? ` (plugin: ${plugin})` : ''}`);
42
- }
38
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
39
+
40
+ // True when both stdin and stdout are a terminal — clack prompts can't resolve
41
+ // otherwise (CI, piped stdin), so the no-arg pick must fall back to an error.
42
+ function isInteractive() {
43
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
44
+ }
45
+
46
+ // Boxed list of installable harnesses (name + description), for --help and error paths.
47
+ function printHarnessList(harnesses) {
48
+ const entries = Object.entries(harnesses);
49
+ if (!entries.length) {
50
+ p.log.warn('No harnesses are bundled. Run `npm run build:data` from the package root.');
51
+ return;
43
52
  }
44
- console.log();
53
+ const width = Math.max(...entries.map(([folder]) => folder.length));
54
+ const lines = entries.map(
55
+ ([folder, meta]) => `${folder.padEnd(width)}${meta?.description ? ` ${meta.description}` : ''}`,
56
+ );
57
+ p.note(lines.join('\n'), 'Available harnesses');
45
58
  }
46
59
 
47
60
  function loadHarnessIndex() {
48
61
  const path = join(dataDir, 'harness-index.json');
49
62
  if (!existsSync(path)) {
50
- die(`harness-index.json missing — run \`npm run build:data\` from the package root.`);
63
+ die('harness-index.json missing — run `npm run build:data` from the package root.');
51
64
  }
52
65
  return JSON.parse(readFileSync(path, 'utf8'));
53
66
  }
54
67
 
55
68
  function die(msg, code = 1) {
56
- console.error(`error: ${msg}`);
69
+ p.log.error(msg);
57
70
  process.exit(code);
58
71
  }
59
72
 
@@ -204,22 +217,44 @@ function mergeHooks(targetDir, bundledHooksJson) {
204
217
  return added;
205
218
  }
206
219
 
207
- async function main(args) {
208
- if (args.includes('-h') || args.includes('--help') || args.length === 0) {
209
- usage();
210
- process.exit(args.length === 0 ? 1 : 0);
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) {
223
+ const names = Object.keys(harnesses);
224
+ if (name !== null) {
225
+ if (!name || !harnesses[name]) {
226
+ p.log.error(name ? `Unknown harness '${name}'.` : 'Empty --name value.');
227
+ printHarnessList(harnesses);
228
+ p.outro('Pass --name=<harness> with one of the names above.');
229
+ process.exit(1);
230
+ }
231
+ return name;
211
232
  }
212
-
213
- const harnessName = args[0];
214
- const idx = loadHarnessIndex();
215
- const harnesses = idx.harnesses || {};
216
- if (!harnesses[harnessName]) {
217
- die(
218
- `Unknown harness '${harnessName}'. Available: ${Object.keys(harnesses).join(', ') || '(none)'}`,
219
- );
233
+ if (!isInteractive()) {
234
+ p.log.error('No --name given, and not running in an interactive terminal.');
235
+ printHarnessList(harnesses);
236
+ p.outro('Re-run as `npx @nurix/etna --name=<harness>`.');
237
+ process.exit(1);
220
238
  }
221
- const harnessFolder = harnessName;
239
+ const choice = await p.select({
240
+ message: 'Which harness do you want to install here?',
241
+ options: names.map((folder) => ({
242
+ value: folder,
243
+ label: folder,
244
+ hint: harnesses[folder]?.description || undefined,
245
+ })),
246
+ });
247
+ if (p.isCancel(choice)) {
248
+ p.cancel('Cancelled — nothing installed.');
249
+ return null;
250
+ }
251
+ return choice;
252
+ }
222
253
 
254
+ // Copy one harness's bundled payload into the current directory. All file ops run
255
+ // inside a spinner; the per-item detail + any warnings are emitted after it stops.
256
+ async function installHarness(harnessName, harnesses) {
257
+ const harnessFolder = harnessName;
223
258
  const bundledHarnessDir = join(dataDir, 'harnesses', harnessFolder);
224
259
  const bundledClaudeMd = join(bundledHarnessDir, 'CLAUDE.md');
225
260
  if (!existsSync(bundledClaudeMd)) {
@@ -229,40 +264,44 @@ async function main(args) {
229
264
  const targetDir = process.cwd();
230
265
  const claudeMdDest = join(targetDir, 'CLAUDE.md');
231
266
 
232
- // Read the bundled epoch marker up front so the install banner can name the
233
- // harness version; the file itself is copied into the project further down.
267
+ // Bundled epoch marker — names the version for the banner and carries the
268
+ // `files` list (project-root config shipped outside the plugin route).
234
269
  const bundledHarnessJson = join(bundledHarnessDir, 'harness.json');
235
270
  const harnessMeta = existsSync(bundledHarnessJson) ? readJson(bundledHarnessJson) : {};
236
271
  const versionTag = harnessMeta.version ? ` v${harnessMeta.version}` : '';
237
272
  // The underlying plugin's name — may differ from the harness name, and several
238
273
  // harnesses may share it. Used only for legacy marketplace-entry cleanup.
239
- const pluginName = harnessMeta.name || harnesses[harnessName] || harnessName;
240
-
241
- console.log(`\nInstalling ${harnessName}${versionTag} into ${targetDir}`);
274
+ const pluginName = harnessMeta.name || harnesses[harnessName]?.plugin || harnessName;
242
275
 
243
- const claudeBackup = backupIfExists(claudeMdDest);
244
- if (claudeBackup) console.log(` backed up existing CLAUDE.md → ${claudeBackup}`);
276
+ const spin = p.spinner();
277
+ spin.start(`Installing ${harnessName}${versionTag} into ${targetDir}`);
245
278
 
246
- // CLAUDE.md ships frontmatter-free (the harness's metadata lives in its index.md and the
247
- // plugin's definition.md), so it installs as a verbatim copy — no stripping.
248
- // Tracks the categories actually present in this harness, for the closing
249
- // summary — so a harness that ships no skills or hooks reads as an explicit,
250
- // honest list rather than a silently missing line.
251
279
  const installed = [];
252
-
280
+ const notices = [];
281
+ const warnings = [];
282
+
283
+ // CLAUDE.md ships frontmatter-free, so it installs as a verbatim copy — no stripping.
284
+ // It's harness-owned and overwritten in place. If the project is a git repo the old copy
285
+ // is recoverable from history, so we skip the backup; if it isn't, there's no safety net,
286
+ // so we back it up and tell the user. (Backup applies to CLAUDE.md only.)
287
+ const claudeExisted = existsSync(claudeMdDest) || isSymlink(claudeMdDest);
288
+ const projectHasGit = existsSync(join(targetDir, '.git'));
289
+ if (claudeExisted) {
290
+ if (projectHasGit) {
291
+ notices.push('Overwrote existing CLAUDE.md (tracked by git — `git checkout CLAUDE.md` to restore).');
292
+ } else {
293
+ const claudeBackup = backupIfExists(claudeMdDest);
294
+ warnings.push(
295
+ `No .git here, so no version-control safety net — backed up existing CLAUDE.md → ${claudeBackup}`,
296
+ );
297
+ }
298
+ }
253
299
  cpSync(bundledClaudeMd, claudeMdDest, { force: true });
254
- console.log(` installed CLAUDE.md → ${claudeMdDest}`);
255
- installed.push('CLAUDE.md');
300
+ installed.push('CLAUDE.md → ./');
256
301
 
257
302
  for (const sub of PLUGIN_SUBDIRS) {
258
- const count = copyHarnessSubdir(
259
- join(bundledHarnessDir, sub),
260
- join(targetDir, '.claude', sub),
261
- );
262
- if (count > 0) {
263
- console.log(` copied ${count} ${sub} → ${targetDir}/.claude/${sub}/`);
264
- installed.push(`${count} ${sub}`);
265
- }
303
+ const count = copyHarnessSubdir(join(bundledHarnessDir, sub), join(targetDir, '.claude', sub));
304
+ if (count > 0) installed.push(`${count} ${sub} → .claude/${sub}/`);
266
305
  }
267
306
 
268
307
  // Copy the epoch marker so harness-manager can resolve the installed epoch.
@@ -270,33 +309,103 @@ async function main(args) {
270
309
  const harnessJsonDest = join(targetDir, '.claude', 'harness.json');
271
310
  mkdirSync(dirname(harnessJsonDest), { recursive: true });
272
311
  cpSync(bundledHarnessJson, harnessJsonDest, { force: true });
273
- const tag = [harnessMeta.version && `v${harnessMeta.version}`, harnessMeta.epoch && `epoch ${harnessMeta.epoch}`]
274
- .filter(Boolean)
275
- .join(', ');
276
- console.log(` installed harness.json → ${harnessJsonDest}${tag ? ` (${tag})` : ''}`);
312
+ }
313
+
314
+ // Verbatim project-root files bundled outside the plugin route (.prettierrc.json,
315
+ // .vscode/settings.json, …), listed in harness.json's `files`. They land at the project
316
+ // root at their relative path — NOT under .claude/ — and overwrite unconditionally with no
317
+ // backup: the harness owns this config, and re-running etna refreshes it.
318
+ for (const rel of harnessMeta.files || []) {
319
+ const src = join(bundledHarnessDir, rel);
320
+ if (!existsSync(src)) continue;
321
+ const dest = join(targetDir, rel);
322
+ mkdirSync(dirname(dest), { recursive: true });
323
+ cpSync(src, dest, { recursive: true, force: true });
324
+ installed.push(`${rel} → ./${rel}`);
277
325
  }
278
326
 
279
327
  // Retire hooks earlier epochs shipped before merging, so the additive merge
280
328
  // never resurrects an entry pointing at a script this bundle no longer carries.
281
- const retiredHooks = retireOldHooks(targetDir);
282
- for (const r of retiredHooks) console.log(` removed ${r}`);
329
+ for (const r of retireOldHooks(targetDir)) notices.push(`removed ${r}`);
283
330
 
284
331
  // Merge the bundle's hooks.json into the project's settings.json (idempotent).
285
332
  const added = mergeHooks(targetDir, join(bundledHarnessDir, 'hooks', 'hooks.json'));
286
- if (added > 0) {
287
- console.log(` merged ${added} hook(s) → ${targetDir}/.claude/settings.json`);
333
+ if (added > 0) installed.push(`${added} hook(s) → .claude/settings.json`);
334
+
335
+ for (const entry of migrateFromPluginInstall(targetDir, pluginName)) {
336
+ notices.push(`removed stale ${entry}`);
288
337
  }
289
338
 
290
- const migrated = migrateFromPluginInstall(targetDir, pluginName);
291
- for (const entry of migrated) console.log(` removed stale ${entry}`);
339
+ spin.stop(`Installed ${harnessName}${versionTag}`);
340
+
341
+ p.note(installed.join('\n'), 'Written');
342
+ for (const notice of notices) p.log.info(notice);
343
+ for (const warning of warnings) p.log.warn(warning);
292
344
 
293
- console.log(`\nDone — ${harnessName}${versionTag} installed: ${installed.join(', ')}.`);
294
- console.log(
295
- `Open Claude Code in ${targetDir}. Re-run \`npx @nurix/etna ${harnessName}\` anytime to refresh harness content.\n`,
345
+ p.outro(
346
+ `${harnessName}${versionTag} ready in ${targetDir}. ` +
347
+ `Re-run \`npx @nurix/etna --name=${harnessName}\` anytime to refresh harness content.`,
296
348
  );
297
349
  }
298
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
+
367
+ async function main(args) {
368
+ const idx = loadHarnessIndex();
369
+ const harnesses = idx.harnesses || {};
370
+ const { name, help, positional } = parseArgs(args);
371
+
372
+ if (help) {
373
+ p.intro('@nurix/etna — Nurix harness installer');
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
+ );
379
+ printHarnessList(harnesses);
380
+ p.outro('No args → interactive menu. --name=<harness> → direct install.');
381
+ return;
382
+ }
383
+
384
+ p.intro('@nurix/etna — Nurix harness installer');
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
+
397
+ const boot = p.spinner();
398
+ boot.start('Starting up');
399
+ await sleep(2000);
400
+ boot.stop('Ready');
401
+
402
+ const harnessName = await resolveHarnessName(name, harnesses);
403
+ if (!harnessName) return; // user cancelled the interactive pick
404
+
405
+ await installHarness(harnessName, harnesses);
406
+ }
407
+
299
408
  main(process.argv.slice(2)).catch((e) => {
300
- console.error(e?.stack || e);
409
+ p.log.error(e?.stack || String(e));
301
410
  process.exit(1);
302
411
  });
@@ -1,10 +1,25 @@
1
1
  {
2
- "fingerprint": "b51d4ebd2a1e",
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
  }
@@ -0,0 +1,12 @@
1
+ # Dependencies & build output
2
+ node_modules
3
+ dist
4
+ build
5
+
6
+ # Lockfiles
7
+ pnpm-lock.yaml
8
+ package-lock.json
9
+ yarn.lock
10
+
11
+ # Harness-managed agent/skill definitions (installed under .claude/)
12
+ .claude
@@ -0,0 +1,3 @@
1
+ {
2
+ "proseWrap": "never"
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ // Markdown: Prettier unwraps prose to one logical line per paragraph
3
+ // (see .prettierrc → proseWrap:never) on save; the editor soft-wraps for
4
+ // reading, so no hard line breaks get baked into the source.
5
+ "[markdown]": {
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "editor.formatOnSave": true,
8
+ "editor.wordWrap": "on"
9
+ }
10
+ }
@@ -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.
@@ -3,5 +3,10 @@
3
3
  "name": "code-fable",
4
4
  "version": "0.17.0",
5
5
  "epoch": 14,
6
- "digest": "adc67e13b079"
6
+ "files": [
7
+ ".prettierrc.json",
8
+ ".prettierignore",
9
+ ".vscode/settings.json"
10
+ ],
11
+ "digest": "a897772819ce"
7
12
  }
@@ -0,0 +1,12 @@
1
+ # Dependencies & build output
2
+ node_modules
3
+ dist
4
+ build
5
+
6
+ # Lockfiles
7
+ pnpm-lock.yaml
8
+ package-lock.json
9
+ yarn.lock
10
+
11
+ # Harness-managed agent/skill definitions (installed under .claude/)
12
+ .claude
@@ -0,0 +1,3 @@
1
+ {
2
+ "proseWrap": "never"
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ // Markdown: Prettier unwraps prose to one logical line per paragraph
3
+ // (see .prettierrc → proseWrap:never) on save; the editor soft-wraps for
4
+ // reading, so no hard line breaks get baked into the source.
5
+ "[markdown]": {
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "editor.formatOnSave": true,
8
+ "editor.wordWrap": "on"
9
+ }
10
+ }
@@ -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.
@@ -3,5 +3,10 @@
3
3
  "name": "code-fable",
4
4
  "version": "0.17.0",
5
5
  "epoch": 14,
6
- "digest": "6e3420a69727"
6
+ "files": [
7
+ ".prettierrc.json",
8
+ ".prettierignore",
9
+ ".vscode/settings.json"
10
+ ],
11
+ "digest": "7ce2d78b5ead"
7
12
  }
@@ -0,0 +1,12 @@
1
+ # Dependencies & build output
2
+ node_modules
3
+ dist
4
+ build
5
+
6
+ # Lockfiles
7
+ pnpm-lock.yaml
8
+ package-lock.json
9
+ yarn.lock
10
+
11
+ # Harness-managed agent/skill definitions (installed under .claude/)
12
+ .claude
@@ -0,0 +1,3 @@
1
+ {
2
+ "proseWrap": "never"
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ // Markdown: Prettier unwraps prose to one logical line per paragraph
3
+ // (see .prettierrc → proseWrap:never) on save; the editor soft-wraps for
4
+ // reading, so no hard line breaks get baked into the source.
5
+ "[markdown]": {
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "editor.formatOnSave": true,
8
+ "editor.wordWrap": "on"
9
+ }
10
+ }
@@ -3,5 +3,10 @@
3
3
  "name": "discover",
4
4
  "version": "0.1.7",
5
5
  "epoch": null,
6
- "digest": "459be63d0534"
6
+ "files": [
7
+ ".prettierrc.json",
8
+ ".prettierignore",
9
+ ".vscode/settings.json"
10
+ ],
11
+ "digest": "bd18173c697a"
7
12
  }
@@ -0,0 +1,12 @@
1
+ # Dependencies & build output
2
+ node_modules
3
+ dist
4
+ build
5
+
6
+ # Lockfiles
7
+ pnpm-lock.yaml
8
+ package-lock.json
9
+ yarn.lock
10
+
11
+ # Harness-managed agent/skill definitions (installed under .claude/)
12
+ .claude
@@ -0,0 +1,3 @@
1
+ {
2
+ "proseWrap": "never"
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ // Markdown: Prettier unwraps prose to one logical line per paragraph
3
+ // (see .prettierrc → proseWrap:never) on save; the editor soft-wraps for
4
+ // reading, so no hard line breaks get baked into the source.
5
+ "[markdown]": {
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "editor.formatOnSave": true,
8
+ "editor.wordWrap": "on"
9
+ }
10
+ }
@@ -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
@@ -3,5 +3,10 @@
3
3
  "name": "hr",
4
4
  "version": "0.1.1",
5
5
  "epoch": null,
6
- "digest": "dfc7c529bbec"
6
+ "files": [
7
+ ".prettierrc.json",
8
+ ".prettierignore",
9
+ ".vscode/settings.json"
10
+ ],
11
+ "digest": "c1632f70fcdb"
7
12
  }
@@ -0,0 +1,12 @@
1
+ # Dependencies & build output
2
+ node_modules
3
+ dist
4
+ build
5
+
6
+ # Lockfiles
7
+ pnpm-lock.yaml
8
+ package-lock.json
9
+ yarn.lock
10
+
11
+ # Harness-managed agent/skill definitions (installed under .claude/)
12
+ .claude
@@ -0,0 +1,3 @@
1
+ {
2
+ "proseWrap": "never"
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ // Markdown: Prettier unwraps prose to one logical line per paragraph
3
+ // (see .prettierrc → proseWrap:never) on save; the editor soft-wraps for
4
+ // reading, so no hard line breaks get baked into the source.
5
+ "[markdown]": {
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "editor.formatOnSave": true,
8
+ "editor.wordWrap": "on"
9
+ }
10
+ }
@@ -3,5 +3,10 @@
3
3
  "name": "kb-agi",
4
4
  "version": "0.10.1",
5
5
  "epoch": null,
6
- "digest": "856a9f4e297a"
6
+ "files": [
7
+ ".prettierrc.json",
8
+ ".prettierignore",
9
+ ".vscode/settings.json"
10
+ ],
11
+ "digest": "25e1cb993a7f"
7
12
  }
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.17.0",
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": {
7
7
  "etna": "bin/etna.js"
8
8
  },
9
+ "dependencies": {
10
+ "@clack/prompts": "^0.11.0"
11
+ },
9
12
  "files": [
10
13
  "bin/",
11
14
  "data/",