@nurix/etna 0.20.32 → 0.20.34

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "1b90a96b3812",
2
+ "fingerprint": "748e1019e5d8",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.20.3",
4
+ "version": "3.20.5",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -29,5 +29,5 @@
29
29
  "origin": "https://cdn.nustack.io",
30
30
  "track": "latest"
31
31
  },
32
- "digest": "0bde4238378a"
32
+ "digest": "00beb7af3c87"
33
33
  }
@@ -184,6 +184,14 @@ function prunableFiles(projectDir, owned) {
184
184
  return out.filter((rel) => rel !== '.claude/hooks');
185
185
  }
186
186
 
187
+ // Whether the owned skills and agents a mount record names are still on disk.
188
+ function ownedOnDisk(projectDir, owned) {
189
+ if (!owned) return true;
190
+ for (const name of owned.skills || []) if (!existsSync(join(projectDir, '.claude', 'skills', name))) return false;
191
+ for (const name of owned.agents || []) if (!existsSync(join(projectDir, '.claude', 'agents', name))) return false;
192
+ return true;
193
+ }
194
+
187
195
  function unionOwned(...maps) {
188
196
  const out = {};
189
197
  for (const map of maps) {
@@ -305,7 +313,7 @@ function mountSettings(marker) {
305
313
  };
306
314
  }
307
315
 
308
- // Mounts the pinned version unless the mount record already names it.
316
+ // Mounts the pinned version unless the mount record already names it and its owned files are on disk.
309
317
  async function mountHarness({ projectDir, cacheDir = cacheRoot(), fetchImpl = globalThis.fetch, timeoutMs = 5000, force = false }) {
310
318
  const marker = readJsonFile(join(projectDir, '.claude', 'harness.json'));
311
319
  if (!marker || !marker.harness || !marker.version) return { status: 'unmarked' };
@@ -313,7 +321,9 @@ async function mountHarness({ projectDir, cacheDir = cacheRoot(), fetchImpl = gl
313
321
  const slug = marker.harness;
314
322
  const version = marker.version;
315
323
  const prior = readJsonFile(join(projectDir, '.claude', 'state', 'mount.json'));
316
- if (!force && prior && prior.version === version) return { status: 'current', slug, version, digest: prior.digest };
324
+ if (!force && prior && prior.version === version && ownedOnDisk(projectDir, prior.owned)) {
325
+ return { status: 'current', slug, version, digest: prior.digest };
326
+ }
317
327
  let resolved;
318
328
  try {
319
329
  resolved = await resolveManifest({ origin, slug, version, cacheDir, fetchImpl, timeoutMs });
@@ -37,7 +37,7 @@ Tooling keys sit beside these on any genre: `rev:` (a generator's generation sta
37
37
  ### `description:`
38
38
 
39
39
  - **Values:** one plain string of at most 300 characters on one line: what the document settles and where its boundary sits, in one or two third-person, present-tense sentences. "How a person signs in to NuStack — the web, command-line and desktop doors, the session each mints, and what a refused sign-in shows; enterprise single sign-on is its own document." The description does not open with "This document"; it carries no file list, no date and no status.
40
- - **Required on:** every truth document and every folder index; a record carries it wherever its template has frontmatter (a changelog entry's one-line summary is its row in `CHANGELOG.md`). A frozen record that predates the key is never edited to add it. Exempt: `docs/structure.md`, which the installer writes whole, and a tree a generator owns (`docs/dna/`, the product-dna skill's).
40
+ - **Required on:** every truth document and every folder index; a record carries it wherever its template has frontmatter (a changelog entry's one-line summary is its row in `CHANGELOG.md`). A frozen record that predates the key is never edited to add it. Exempt: `docs/structure.md`, which the installer writes whole; `docs/operations/bookkeeping.md`, which the hooks write from a fixed header; and a tree a generator owns (`docs/dna/`, the product-dna skill's). A release note under `docs/operations/release-notes/` is a record, frozen at publication.
41
41
  - **Reading:** it is how a folder is browsed without an index row per file: `grep -m1 '^description:' docs/features/*.md`. Tooling that lists documents shows it beside the path.
42
42
  - **Upkeep:** an edit that changes what a document settles updates its description in the same edit.
43
43
 
@@ -28,7 +28,7 @@ const SKIP = /^(\.claude|\.agents|\.codex|skills|rules|agents|harness)\/|(^|\/)(
28
28
  // `operations/issues/YYYY-MM-DD-{slug}.md` is a sanctioned record genre (`type: issue`, the
29
29
  // `issue-bridge` skill's local pack) living outside the record folders, so it is named here too —
30
30
  // graded as a truth document it fails the date rule for carrying the date its filename already does.
31
- const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\//;
31
+ const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\/|(^|\/)docs\/operations\/release-notes\/(?!readme\.md$)[^/]+\.md$/i;
32
32
 
33
33
  const RECORD_TYPES = /^(changelog-entry|plan|migration|migration-runbook|research|brainstorm|journal|record|ledger|issue|triage|project-memory|agent-memory)$/;
34
34
  const TRUTH_LOCATIONS = /(^|\/)docs\/(features|architecture|tests|acceptance)\//;
@@ -152,6 +152,8 @@ function lintFile(path, findings) {
152
152
  // A file or record named without a link is the same per-file row as a link (SKILL.md § Every document describes itself).
153
153
  // docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
154
154
  const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
155
+ // The bookkeeping queue is written by the harness hooks from a fixed header (hooks/lib/state.mjs); it carries no frontmatter.
156
+ const isHookQueue = /(^|\/)docs\/operations\/bookkeeping\.md$/.test(path);
155
157
  if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
156
158
  const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
157
159
  for (const { n, line } of lines) {
@@ -162,7 +164,7 @@ function lintFile(path, findings) {
162
164
  }
163
165
  }
164
166
  // Every truth document and folder index under docs/ describes itself in at most 300 characters (references/frontmatter.md § description:).
165
- if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !disabled.has('description-missing')) {
167
+ if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !isHookQueue && !disabled.has('description-missing')) {
166
168
  const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
167
169
  const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
168
170
  const m = /^description:[ \t]*(.*)$/m.exec(fmText);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.40",
4
+ "version": "0.2.41",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "5a9f81fff98a"
17
+ "digest": "0b5c0a49d7ba"
18
18
  }
@@ -37,7 +37,7 @@ Tooling keys sit beside these on any genre: `rev:` (a generator's generation sta
37
37
  ### `description:`
38
38
 
39
39
  - **Values:** one plain string of at most 300 characters on one line: what the document settles and where its boundary sits, in one or two third-person, present-tense sentences. "How a person signs in to NuStack — the web, command-line and desktop doors, the session each mints, and what a refused sign-in shows; enterprise single sign-on is its own document." The description does not open with "This document"; it carries no file list, no date and no status.
40
- - **Required on:** every truth document and every folder index; a record carries it wherever its template has frontmatter (a changelog entry's one-line summary is its row in `CHANGELOG.md`). A frozen record that predates the key is never edited to add it. Exempt: `docs/structure.md`, which the installer writes whole, and a tree a generator owns (`docs/dna/`, the product-dna skill's).
40
+ - **Required on:** every truth document and every folder index; a record carries it wherever its template has frontmatter (a changelog entry's one-line summary is its row in `CHANGELOG.md`). A frozen record that predates the key is never edited to add it. Exempt: `docs/structure.md`, which the installer writes whole; `docs/operations/bookkeeping.md`, which the hooks write from a fixed header; and a tree a generator owns (`docs/dna/`, the product-dna skill's). A release note under `docs/operations/release-notes/` is a record, frozen at publication.
41
41
  - **Reading:** it is how a folder is browsed without an index row per file: `grep -m1 '^description:' docs/features/*.md`. Tooling that lists documents shows it beside the path.
42
42
  - **Upkeep:** an edit that changes what a document settles updates its description in the same edit.
43
43
 
@@ -28,7 +28,7 @@ const SKIP = /^(\.claude|\.agents|\.codex|skills|rules|agents|harness)\/|(^|\/)(
28
28
  // `operations/issues/YYYY-MM-DD-{slug}.md` is a sanctioned record genre (`type: issue`, the
29
29
  // `issue-bridge` skill's local pack) living outside the record folders, so it is named here too —
30
30
  // graded as a truth document it fails the date rule for carrying the date its filename already does.
31
- const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\//;
31
+ const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\/|(^|\/)docs\/operations\/release-notes\/(?!readme\.md$)[^/]+\.md$/i;
32
32
 
33
33
  const RECORD_TYPES = /^(changelog-entry|plan|migration|migration-runbook|research|brainstorm|journal|record|ledger|issue|triage|project-memory|agent-memory)$/;
34
34
  const TRUTH_LOCATIONS = /(^|\/)docs\/(features|architecture|tests|acceptance)\//;
@@ -152,6 +152,8 @@ function lintFile(path, findings) {
152
152
  // A file or record named without a link is the same per-file row as a link (SKILL.md § Every document describes itself).
153
153
  // docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
154
154
  const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
155
+ // The bookkeeping queue is written by the harness hooks from a fixed header (hooks/lib/state.mjs); it carries no frontmatter.
156
+ const isHookQueue = /(^|\/)docs\/operations\/bookkeeping\.md$/.test(path);
155
157
  if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
156
158
  const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
157
159
  for (const { n, line } of lines) {
@@ -162,7 +164,7 @@ function lintFile(path, findings) {
162
164
  }
163
165
  }
164
166
  // Every truth document and folder index under docs/ describes itself in at most 300 characters (references/frontmatter.md § description:).
165
- if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !disabled.has('description-missing')) {
167
+ if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !isHookQueue && !disabled.has('description-missing')) {
166
168
  const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
167
169
  const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
168
170
  const m = /^description:[ \t]*(.*)$/m.exec(fmText);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.30",
4
+ "version": "1.5.31",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "origin": "https://cdn.nustack.io",
15
15
  "track": "latest"
16
16
  },
17
- "digest": "02bf4a5e9588"
17
+ "digest": "2c3760e696b9"
18
18
  }
@@ -37,7 +37,7 @@ Tooling keys sit beside these on any genre: `rev:` (a generator's generation sta
37
37
  ### `description:`
38
38
 
39
39
  - **Values:** one plain string of at most 300 characters on one line: what the document settles and where its boundary sits, in one or two third-person, present-tense sentences. "How a person signs in to NuStack — the web, command-line and desktop doors, the session each mints, and what a refused sign-in shows; enterprise single sign-on is its own document." The description does not open with "This document"; it carries no file list, no date and no status.
40
- - **Required on:** every truth document and every folder index; a record carries it wherever its template has frontmatter (a changelog entry's one-line summary is its row in `CHANGELOG.md`). A frozen record that predates the key is never edited to add it. Exempt: `docs/structure.md`, which the installer writes whole, and a tree a generator owns (`docs/dna/`, the product-dna skill's).
40
+ - **Required on:** every truth document and every folder index; a record carries it wherever its template has frontmatter (a changelog entry's one-line summary is its row in `CHANGELOG.md`). A frozen record that predates the key is never edited to add it. Exempt: `docs/structure.md`, which the installer writes whole; `docs/operations/bookkeeping.md`, which the hooks write from a fixed header; and a tree a generator owns (`docs/dna/`, the product-dna skill's). A release note under `docs/operations/release-notes/` is a record, frozen at publication.
41
41
  - **Reading:** it is how a folder is browsed without an index row per file: `grep -m1 '^description:' docs/features/*.md`. Tooling that lists documents shows it beside the path.
42
42
  - **Upkeep:** an edit that changes what a document settles updates its description in the same edit.
43
43
 
@@ -28,7 +28,7 @@ const SKIP = /^(\.claude|\.agents|\.codex|skills|rules|agents|harness)\/|(^|\/)(
28
28
  // `operations/issues/YYYY-MM-DD-{slug}.md` is a sanctioned record genre (`type: issue`, the
29
29
  // `issue-bridge` skill's local pack) living outside the record folders, so it is named here too —
30
30
  // graded as a truth document it fails the date rule for carrying the date its filename already does.
31
- const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\//;
31
+ const RECORD = /(^|\/)docs\/(history|research|memory)\/|(^|\/)docs\/operations\/issues\/|(^|\/)docs\/operations\/release-notes\/(?!readme\.md$)[^/]+\.md$/i;
32
32
 
33
33
  const RECORD_TYPES = /^(changelog-entry|plan|migration|migration-runbook|research|brainstorm|journal|record|ledger|issue|triage|project-memory|agent-memory)$/;
34
34
  const TRUTH_LOCATIONS = /(^|\/)docs\/(features|architecture|tests|acceptance)\//;
@@ -152,6 +152,8 @@ function lintFile(path, findings) {
152
152
  // A file or record named without a link is the same per-file row as a link (SKILL.md § Every document describes itself).
153
153
  // docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
154
154
  const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
155
+ // The bookkeeping queue is written by the harness hooks from a fixed header (hooks/lib/state.mjs); it carries no frontmatter.
156
+ const isHookQueue = /(^|\/)docs\/operations\/bookkeeping\.md$/.test(path);
155
157
  if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
156
158
  const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
157
159
  for (const { n, line } of lines) {
@@ -162,7 +164,7 @@ function lintFile(path, findings) {
162
164
  }
163
165
  }
164
166
  // Every truth document and folder index under docs/ describes itself in at most 300 characters (references/frontmatter.md § description:).
165
- if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !disabled.has('description-missing')) {
167
+ if (scope === 'truth' && /(^|\/)docs\//.test(path) && !isGeneratedTree && !isAuthorityMap && !isHookQueue && !disabled.has('description-missing')) {
166
168
  const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
167
169
  const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
168
170
  const m = /^description:[ \t]*(.*)$/m.exec(fmText);
@@ -182,6 +182,14 @@ function prunableFiles(projectDir, owned) {
182
182
  return out.filter((rel) => rel !== '.claude/hooks');
183
183
  }
184
184
 
185
+ // Whether the owned skills and agents a mount record names are still on disk.
186
+ function ownedOnDisk(projectDir, owned) {
187
+ if (!owned) return true;
188
+ for (const name of owned.skills || []) if (!existsSync(join(projectDir, '.claude', 'skills', name))) return false;
189
+ for (const name of owned.agents || []) if (!existsSync(join(projectDir, '.claude', 'agents', name))) return false;
190
+ return true;
191
+ }
192
+
185
193
  function unionOwned(...maps) {
186
194
  const out = {};
187
195
  for (const map of maps) {
@@ -303,7 +311,7 @@ function mountSettings(marker) {
303
311
  };
304
312
  }
305
313
 
306
- // Mounts the pinned version unless the mount record already names it.
314
+ // Mounts the pinned version unless the mount record already names it and its owned files are on disk.
307
315
  async function mountHarness({ projectDir, cacheDir = cacheRoot(), fetchImpl = globalThis.fetch, timeoutMs = 5000, force = false }) {
308
316
  const marker = readJsonFile(join(projectDir, '.claude', 'harness.json'));
309
317
  if (!marker || !marker.harness || !marker.version) return { status: 'unmarked' };
@@ -311,7 +319,9 @@ async function mountHarness({ projectDir, cacheDir = cacheRoot(), fetchImpl = gl
311
319
  const slug = marker.harness;
312
320
  const version = marker.version;
313
321
  const prior = readJsonFile(join(projectDir, '.claude', 'state', 'mount.json'));
314
- if (!force && prior && prior.version === version) return { status: 'current', slug, version, digest: prior.digest };
322
+ if (!force && prior && prior.version === version && ownedOnDisk(projectDir, prior.owned)) {
323
+ return { status: 'current', slug, version, digest: prior.digest };
324
+ }
315
325
  let resolved;
316
326
  try {
317
327
  resolved = await resolveManifest({ origin, slug, version, cacheDir, fetchImpl, timeoutMs });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.32",
3
+ "version": "0.20.34",
4
4
  "description": "Bootstrap a project with a Nurix harness — writes the committed contract (CLAUDE.md, AGENTS.md, .claude/harness.json, settings.json, the mount hook) and mounts the harness itself, gitignored, from a bundled payload; sessions keep it current from the CDN.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,5 +46,5 @@
46
46
  "agent",
47
47
  "plugin"
48
48
  ],
49
- "etnaFingerprint": "1b90a96b3812"
49
+ "etnaFingerprint": "748e1019e5d8"
50
50
  }