@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 +13 -4
- package/bin/etna.js +177 -68
- package/data/harness-index.json +21 -6
- package/data/harnesses/code/.prettierignore +12 -0
- package/data/harnesses/code/.prettierrc.json +3 -0
- package/data/harnesses/code/.vscode/settings.json +10 -0
- package/data/harnesses/code/agents/harness-manager.md +3 -3
- package/data/harnesses/code/harness.json +6 -1
- package/data/harnesses/code-fable/.prettierignore +12 -0
- package/data/harnesses/code-fable/.prettierrc.json +3 -0
- package/data/harnesses/code-fable/.vscode/settings.json +10 -0
- package/data/harnesses/code-fable/agents/harness-manager.md +3 -3
- package/data/harnesses/code-fable/harness.json +6 -1
- package/data/harnesses/discover/.prettierignore +12 -0
- package/data/harnesses/discover/.prettierrc.json +3 -0
- package/data/harnesses/discover/.vscode/settings.json +10 -0
- package/data/harnesses/discover/harness.json +6 -1
- package/data/harnesses/hr/.prettierignore +12 -0
- package/data/harnesses/hr/.prettierrc.json +3 -0
- package/data/harnesses/hr/.vscode/settings.json +10 -0
- package/data/harnesses/hr/CLAUDE.md +1 -1
- package/data/harnesses/hr/harness.json +6 -1
- package/data/harnesses/kb-agi/.prettierignore +12 -0
- package/data/harnesses/kb-agi/.prettierrc.json +3 -0
- package/data/harnesses/kb-agi/.vscode/settings.json +10 -0
- package/data/harnesses/kb-agi/harness.json +6 -1
- package/package.json +4 -1
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
|
|
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 (
|
|
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.
|
|
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
|
|
4
|
-
// directory.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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
|
-
//
|
|
233
|
-
//
|
|
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
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
409
|
+
p.log.error(e?.stack || String(e));
|
|
301
410
|
process.exit(1);
|
|
302
411
|
});
|
package/data/harness-index.json
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"fingerprint": "
|
|
2
|
+
"fingerprint": "2a8b6a3ea74d",
|
|
3
3
|
"harnesses": {
|
|
4
|
-
"code":
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
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,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.
|
|
@@ -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.
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "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/",
|