@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.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/hooks/mount.mjs +12 -2
- package/data/harnesses/code/skills/documentation/references/frontmatter.md +1 -1
- package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +4 -2
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +1 -1
- package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +4 -2
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/references/frontmatter.md +1 -1
- package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +4 -2
- package/lib/install/mount.mjs +12 -2
- package/package.json +2 -2
package/data/harness-index.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"harness": "code",
|
|
3
3
|
"name": "code",
|
|
4
|
-
"version": "3.20.
|
|
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": "
|
|
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
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
|
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);
|
package/lib/install/mount.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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": "
|
|
49
|
+
"etnaFingerprint": "748e1019e5d8"
|
|
50
50
|
}
|