@nurix/etna 0.19.83 → 0.19.84

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.
@@ -8,13 +8,16 @@
8
8
  import { existsSync, cpSync, rmSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
 
11
- export function projectSkills({ targetDir, projectHasGit }) {
11
+ // `snapshot: false` skips the no-git safety copy. The harness installer passes it because it has
12
+ // already cleared .agents/skills and rewritten it from the bundle, so the directory holds only
13
+ // bytes that install just produced — snapshotting those protects nothing and leaves .bak litter.
14
+ export function projectSkills({ targetDir, projectHasGit, snapshot = true }) {
12
15
  const src = join(targetDir, '.claude', 'skills');
13
16
  if (!existsSync(src)) return null;
14
17
  const dest = join(targetDir, '.agents', 'skills');
15
18
  let warning = null;
16
19
  if (existsSync(dest)) {
17
- if (!projectHasGit) {
20
+ if (!projectHasGit && snapshot) {
18
21
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
19
22
  cpSync(dest, `${dest}.bak.${ts}`, { recursive: true });
20
23
  warning = 'No .git here — snapshotted existing .agents/skills/ before rebuild.';
@@ -30,6 +30,7 @@ import {
30
30
  } from 'node:fs';
31
31
  import { join, dirname } from 'node:path';
32
32
  import { planHarnessFiles, PRUNABLE_SUBDIRS, CODEX_REBUILT_DIRS } from './plan.mjs';
33
+ import { projectSkills } from '../codex/skills.mjs';
33
34
 
34
35
  // Hook scripts shipped by earlier harness versions (the legacy memory-ledger tier) and since retired.
35
36
  // mergeHooks is additive, so an upgrade must explicitly remove both the stale settings.json
@@ -153,6 +154,27 @@ function retireOldHooks(targetDir, bundledHarnessDir) {
153
154
  return removed;
154
155
  }
155
156
 
157
+ // Artifacts a prior install leaves behind, in the order they are cheapest to trust. `.claude/` is
158
+ // deliberately NOT the only one: a repo that gitignores it never commits `harness.json`, so a fresh
159
+ // CI checkout of an already-harnessed repo carries no marker there and would read as first contact
160
+ // on EVERY roll — which seeds pristine scaffolds over the project's real docs, backs the originals
161
+ // up as .bkp, and does it again next time, since the gitignore is what stops the marker persisting.
162
+ // The Codex artifacts and AGENTS.md are written by the same install and are conventionally tracked,
163
+ // so any one of them present is proof this repo has been harnessed before.
164
+ const PRIOR_INSTALL_MARKERS = [
165
+ join('.claude', 'harness.json'),
166
+ join('.codex', 'hooks.json'),
167
+ join('.agents', 'skills'),
168
+ 'AGENTS.md',
169
+ ];
170
+
171
+ // First contact means "this repo has never been harnessed", which is what licenses the installer to
172
+ // move a colliding file aside as .bkp and write the pristine seed over it. Getting it wrong in the
173
+ // permissive direction is destructive, so any evidence of a prior install settles it.
174
+ function isFirstContact(targetDir) {
175
+ return !PRIOR_INSTALL_MARKERS.some((rel) => existsSync(join(targetDir, rel)));
176
+ }
177
+
156
178
  // Read the `owned` manifest the PREVIOUS install recorded in the project's harness.json.
157
179
  // Absent (first install, or pre-pruning version) ⇒ {} ⇒ nothing is pruned this round.
158
180
  function readInstalledOwned(targetDir) {
@@ -310,7 +332,7 @@ export function materializeHarness({ bundledHarnessDir, targetDir, pluginName, p
310
332
  // to the human). Git is not the distinction: history protects bytes, but the .bkp file is what
311
333
  // makes the fold duty visible to the next session. The harness is deliberately unapologetic
312
334
  // about its own process; the backup is how it stays honest about yours.
313
- const firstContact = !existsSync(join(targetDir, '.claude', 'harness.json'));
335
+ const firstContact = isFirstContact(targetDir);
314
336
  void projectHasGit; // kept for caller compatibility; the .bkp rule does not branch on it
315
337
 
316
338
  // The Codex mirrors are rebuilt wholesale, not merged: they are byte projections of the Claude
@@ -381,6 +403,14 @@ export function materializeHarness({ bundledHarnessDir, targetDir, pluginName, p
381
403
  }
382
404
  }
383
405
 
406
+ // Re-project the Codex skill mirror from the INSTALLED tree, after both prunes. The write pass
407
+ // above only emits what the bundle ships, so a project-local skill — one installed by
408
+ // `--skill=<name>` and deliberately absent from `owned`, which is what makes it survive pruning —
409
+ // was present for Claude and missing for Codex after every roll. Ordering is load-bearing: run
410
+ // before the prunes and a skill this bundle dropped would be mirrored straight back.
411
+ const mirrored = projectSkills({ targetDir, projectHasGit, snapshot: false });
412
+ if (mirrored?.installed) installed.push(mirrored.installed);
413
+
384
414
  // Retire hooks earlier harness versions shipped, before merging, so the additive merge never
385
415
  // resurrects an entry pointing at a script this bundle no longer carries.
386
416
  for (const r of retireOldHooks(targetDir, bundledHarnessDir)) notices.push(`removed ${r}`);
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
1
  {
2
- "name": "@nurix/etna",
3
- "version": "0.19.83",
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
- "type": "module",
6
- "bin": {
7
- "etna": "bin/etna.js"
8
- },
9
- "dependencies": {
10
- "@clack/prompts": "^0.11.0"
11
- },
12
- "files": [
13
- "bin/",
14
- "lib/",
15
- "data/",
16
- "README.md"
17
- ],
18
- "engines": {
19
- "node": ">=18.0.0"
20
- },
21
- "scripts": {
22
- "test": "node --test 'tests/**/*.test.mjs'",
23
- "build:data": "node scripts/build-data.js",
24
- "prepack": "node scripts/build-data.js",
25
- "validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
26
- },
27
- "repository": {
28
- "type": "git",
29
- "url": "git+https://github.com/nurixlabs/etna.git",
30
- "directory": "packages/cli"
31
- },
32
- "homepage": "https://github.com/nurixlabs/etna/tree/dev/packages/cli",
33
- "license": "UNLICENSED",
34
- "publishConfig": {
35
- "access": "public"
36
- },
37
- "keywords": [
38
- "claude",
39
- "claude-code",
40
- "harness",
41
- "nurix",
42
- "agent",
43
- "plugin"
44
- ],
45
- "etnaFingerprint": "574f23e45199"
2
+ "name": "@nurix/etna",
3
+ "version": "0.19.84",
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
+ "type": "module",
6
+ "bin": {
7
+ "etna": "bin/etna.js"
8
+ },
9
+ "dependencies": {
10
+ "@clack/prompts": "^0.11.0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "data/",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "scripts": {
22
+ "test": "node --test 'tests/**/*.test.mjs'",
23
+ "build:data": "node scripts/build-data.js",
24
+ "prepack": "node scripts/build-data.js",
25
+ "validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/nurixlabs/etna.git",
30
+ "directory": "packages/cli"
31
+ },
32
+ "homepage": "https://github.com/nurixlabs/etna/tree/dev/packages/cli",
33
+ "license": "UNLICENSED",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "keywords": [
38
+ "claude",
39
+ "claude-code",
40
+ "harness",
41
+ "nurix",
42
+ "agent",
43
+ "plugin"
44
+ ],
45
+ "etnaFingerprint": "574f23e45199"
46
46
  }