@nurix/etna 0.20.30 → 0.20.32

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.
Files changed (49) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/agents/maintenance.md +1 -0
  3. package/data/harnesses/code/harness.json +2 -2
  4. package/data/harnesses/code/hooks/mount.mjs +17 -3
  5. package/data/harnesses/code/skills/documentation/SKILL.md +1 -0
  6. package/data/harnesses/code/skills/documentation/references/frontmatter.md +18 -9
  7. package/data/harnesses/code/skills/documentation/references/style-guide.md +1 -1
  8. package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +31 -0
  9. package/data/harnesses/code/skills/documentation/templates/acceptance-criteria.md +1 -0
  10. package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +1 -0
  11. package/data/harnesses/code/skills/documentation/templates/docs-readme.md +6 -1
  12. package/data/harnesses/code/skills/documentation/templates/doctrine.md +1 -0
  13. package/data/harnesses/code/skills/documentation/templates/feature.md +1 -0
  14. package/data/harnesses/code/skills/documentation/templates/handover.md +1 -0
  15. package/data/harnesses/code/skills/documentation/templates/package.md +1 -0
  16. package/data/harnesses/code/skills/documentation/templates/setup.md +1 -0
  17. package/data/harnesses/code/skills/documentation/templates/system.md +1 -0
  18. package/data/harnesses/delivery/agents/maintenance.md +1 -0
  19. package/data/harnesses/delivery/harness.json +2 -2
  20. package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -0
  21. package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +18 -9
  22. package/data/harnesses/delivery/skills/documentation/references/style-guide.md +1 -1
  23. package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +31 -0
  24. package/data/harnesses/delivery/skills/documentation/templates/acceptance-criteria.md +1 -0
  25. package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +1 -0
  26. package/data/harnesses/delivery/skills/documentation/templates/docs-readme.md +6 -1
  27. package/data/harnesses/delivery/skills/documentation/templates/doctrine.md +1 -0
  28. package/data/harnesses/delivery/skills/documentation/templates/feature.md +1 -0
  29. package/data/harnesses/delivery/skills/documentation/templates/handover.md +1 -0
  30. package/data/harnesses/delivery/skills/documentation/templates/package.md +1 -0
  31. package/data/harnesses/delivery/skills/documentation/templates/setup.md +1 -0
  32. package/data/harnesses/delivery/skills/documentation/templates/system.md +1 -0
  33. package/data/harnesses/etna/agents/maintenance.md +1 -0
  34. package/data/harnesses/etna/harness.json +2 -2
  35. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -0
  36. package/data/harnesses/etna/skills/documentation/references/frontmatter.md +18 -9
  37. package/data/harnesses/etna/skills/documentation/references/style-guide.md +1 -1
  38. package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +31 -0
  39. package/data/harnesses/etna/skills/documentation/templates/acceptance-criteria.md +1 -0
  40. package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +1 -0
  41. package/data/harnesses/etna/skills/documentation/templates/docs-readme.md +6 -1
  42. package/data/harnesses/etna/skills/documentation/templates/doctrine.md +1 -0
  43. package/data/harnesses/etna/skills/documentation/templates/feature.md +1 -0
  44. package/data/harnesses/etna/skills/documentation/templates/handover.md +1 -0
  45. package/data/harnesses/etna/skills/documentation/templates/package.md +1 -0
  46. package/data/harnesses/etna/skills/documentation/templates/setup.md +1 -0
  47. package/data/harnesses/etna/skills/documentation/templates/system.md +1 -0
  48. package/lib/install/mount.mjs +17 -3
  49. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "687486a21483",
2
+ "fingerprint": "1b90a96b3812",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -64,6 +64,7 @@ Across the header pointers and the transitional globs:
64
64
  - **Retired keys:** `applies:` and `keywords:` are removed wherever found (remediation); a doctrine the removed `applies:` named stays named in the document's Scope section.
65
65
  - Flag **equal-specificity overlaps** — two feature docs whose transitional `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
66
66
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
67
+ - **Write `description:` where missing.** Every truth document and folder index carries a `description:` of at most 300 characters saying what it settles and where its boundary sits (`documentation` skill, `references/frontmatter.md` § `description:`). Each one is written from a read of the document, never from its filename — many documents ⇒ parallel sub-agents on the cheapest capable tier, one batch each. A folder index that names its files or records is rewritten to describe the folder, after confirming every per-file fact it carried lives in that file.
67
68
 
68
69
  **Then validate the doc→doc edge.** `node .claude/hooks/check-ownership.mjs` and the session-injected **doc edge index** carry `verifies:` and its generated inverse; read the edge index and check two things:
69
70
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.20.1",
4
+ "version": "3.20.3",
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": "f52d24285ab6"
32
+ "digest": "0bde4238378a"
33
33
  }
@@ -206,7 +206,7 @@ function removeEmptyParents(projectDir, rel) {
206
206
  }
207
207
  }
208
208
 
209
- // Writes the manifest's mounted files that differ on disk, then deletes owned names the manifest omits.
209
+ // Fetches the manifest's mounted files that differ on disk, writes them deepest path first, then deletes owned names the manifest omits.
210
210
  async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetchImpl, timeoutMs, priorOwned }) {
211
211
  const all = mountedEntries(manifest);
212
212
  const entries = admittedEntries(projectDir, all);
@@ -218,14 +218,28 @@ async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetch
218
218
  const dest = join(projectDir, entry.path);
219
219
  return !(existsSync(dest) && sha256Of(readFileSync(dest)) === entry.sha256);
220
220
  });
221
+ // Booby trap: every live session in the checkout runs these hooks while the mount rewrites them.
222
+ // Writing as each fetch lands let `hooks/compliance.mjs` arrive seconds before a new `hooks/lib/`
223
+ // module it imports, and every hook call in that window died with ERR_MODULE_NOT_FOUND (3.20.1
224
+ // rollout, 2026-10-05). Fetch everything, then write locally, imports (deeper paths) before importers.
225
+ const fetched = [];
221
226
  await eachLimited(stale, async (entry) => {
222
227
  try {
223
- writeAtomic(join(projectDir, entry.path), await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }));
224
- written.push(entry.path);
228
+ fetched.push({ entry, bytes: await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }) });
225
229
  } catch {
226
230
  failed.push(entry.path);
227
231
  }
228
232
  });
233
+ const depth = (path) => path.split('/').length;
234
+ fetched.sort((a, b) => depth(b.entry.path) - depth(a.entry.path) || a.entry.path.localeCompare(b.entry.path));
235
+ for (const { entry, bytes } of fetched) {
236
+ try {
237
+ writeAtomic(join(projectDir, entry.path), bytes);
238
+ written.push(entry.path);
239
+ } catch {
240
+ failed.push(entry.path);
241
+ }
242
+ }
229
243
  for (const rel of prunableFiles(projectDir, unionOwned(manifest.owned, priorOwned))) {
230
244
  if (listed.has(rel)) continue;
231
245
  rmSync(join(projectDir, rel), { force: true });
@@ -72,6 +72,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
72
72
  - **Records are never edited to match the present.** A completed plan stays wrong-in-hindsight; a changelog entry describes close-time state; a scope change gets a *new* dated record that links back, never an edit to a frozen one.
73
73
  - **Two sanctioned bridges — head is truth, tail is provenance.** An **Architecture Decision (AD)** ends with a **Decision Changelog** (for a decision, the journey — what each revision reversed and what triggered it — is part of the reasoning), and the `maintenance` agent's **Health Log** lives in `docs/history/health-log.md`, an append-only record. The AD body stays current-truth-only; the tail carries the dates. No other truth document carries dated history. `docs/structure.md` is harness-owned — written whole by the installer, never edited by hand or by a skill; only its `harness-version` frontmatter is stamped, by the `maintenance` agent.
74
74
  - **Indexes describe directories, never files.** `docs/README.md` — the top-level index of the documentation folder — is a description of its directories: one entry per folder saying what the folder holds, who reads it and how its files are named. It carries no file list and no file tree: not a per-file row, not a `├──` tree of files, not a tree at all — `docs/structure.md` is the one place the tree lives, as the authority on placement. The same holds for a `README.md` / `Readme.md` at any level of `docs/`. `ls` is the roster, and the ownership index is how a document is found for a path. A per-file row has to be edited on every add, rename and retirement, so the index's history records roster churn instead of what the index said, and a reader cannot tell when the index itself changed. The only files an index may link are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`, `memory/main.md`) and a sibling folder's own `Readme.md`. `scripts/doc-lint.mjs` blocks the rest as `index-lists-files`, and blocks a tree drawn in a folder index as `index-file-tree`. An inverse (which checklists verify a feature, which docs a folder holds) is generated, never written.
75
+ - **Every document describes itself.** Its frontmatter carries `description:` — at most 300 characters, one or two present-tense sentences saying what the document settles and where its boundary sits (`references/frontmatter.md` § `description:`). That is the per-file account an index would otherwise keep, so an index names no file and no record: not a link, not a filename, not an ID row such as `AD-010`. A folder is browsed with `grep -m1 '^description:' <folder>/*.md`. `doc-lint` blocks a truth document or folder index without one (`description-missing`), one over 300 characters (`description-length`), and an index row that names a file or record (`index-names-records`).
75
76
  - **Truth documents cite records for the _why_, never the _when_.** `Provenance: AD-002` is good; "decided in AD-002 and shipped 2026-06-13" is not. Routing/index docs point only to truth documents as a topic's authority — never a plan or changelog entry as the "read first" source. (An index _of the records themselves_ — a History section listing `plans/`, `changelog/` — is sanctioned.)
76
77
 
77
78
  **A document is intent and promises; facts are the graph's.** An owning document (feature, package, system) answers why the product is shaped this way and what it promises, and names the objects it designs in `objects:`. Every computable fact — a table's columns and vocabularies, a foreign key and its delete rule, which function writes which column, which route sits behind which guard, which code a refusal answers, which file holds what — is the code graph's to compute (`docs/architecture/systems/knowledge-layer.md`), and a hand-typed copy of it is the defect this rule removes: it reads as authoritative and is wrong the day the code moves. Mechanism stays in the code, reasons stay in the document, and the document points by object and symbol name, never by line number. `doc-lint` blocks the retired mechanism sections in an owning document (`mechanism-section`) and an owning document without `objects:`. `keywords:` is not a key: an object's name is its discovery term. **The body is scanned by people** — sharp bullets under noun-phrase headings, each a bold noun-phrase label, a colon, then one or two plain sentences; code named only where a person types or sees it, or at a cross-cutting seam the graph would not lead a reader to (a middleware, a gate, a hook, a shared store); every other pointer in the closing **For the agent** footer. The owning genres read Scope · Flows (Flow for a system, Usage for a package) · Decisions · Promises · Gaps · For the agent (`references/style-guide.md` § Labels, § Owning documents — the body and the footer; `doc-lint` warns `body-identifier`, `clause-label` and `second-person`).
@@ -15,24 +15,32 @@ A document names the product objects it designs. Every other edge between a docu
15
15
  3. **Computed ownership:** the object closure (document → objects → tables → their write, read and refusal sites → files) reaches files nobody listed. Where the header pointer and the closure disagree, the graph prints both. Which document speaks for a path is a query, answered per path on demand.
16
16
  4. **Nothing recomputable stored:** a list of files, a list of consumers, or a list of governing documents typed into frontmatter is a copy of something the graph computes. It is stale the day the code moves, and it fails the stability test (`style-guide.md` § Shape — the model first, the detail after).
17
17
  5. **One declaration, at the consuming end:** the edges a person must author (`objects:`, `verifies:`) are declared once, by the document that knows. Every inverse is generated, never authored.
18
+ 6. **The description as the per-file account:** every document states what it is about in its own `description:`. A folder is browsed by reading its files' descriptions, so no index keeps a row per file.
18
19
 
19
20
  ## Keys by genre
20
21
 
21
22
  | Genre | Keys |
22
23
  | --- | --- |
23
- | Feature | `type`, `status`, `objects`, `module` (optional) · transitional `owns` |
24
- | System | `type`, `status`, `objects` · transitional `owns` |
25
- | Package | `type`, `status`, `objects` · transitional `owns` |
26
- | Doctrine | `type`, `status`, `objects` |
27
- | UX | `type`, `status`, `objects` · transitional `owns` when it speaks for a surface's components |
28
- | Behavioral checklist | `type: checklist`, `verifies` |
29
- | Acceptance criteria | `type`, `status` |
30
- | Every other genre | `type`, plus its template's own keys |
24
+ | Feature | `type`, `description`, `status`, `objects`, `module` (optional) · transitional `owns` |
25
+ | System | `type`, `description`, `status`, `objects` · transitional `owns` |
26
+ | Package | `type`, `description`, `status`, `objects` · transitional `owns` |
27
+ | Doctrine | `type`, `description`, `status`, `objects` |
28
+ | UX | `type`, `description`, `status`, `objects` · transitional `owns` when it speaks for a surface's components |
29
+ | Behavioral checklist | `type: checklist`, `description`, `verifies` |
30
+ | Acceptance criteria | `type`, `description`, `status` |
31
+ | Every other genre | `type`, `description`, plus its template's own keys |
31
32
 
32
33
  Tooling keys sit beside these on any genre: `rev:` (a generator's generation stamp), `slug:` (the identity key on generated documents, minted once, and in the repository the filename), and `last-reviewed:` (stamped on a substantive review, the `maintenance` agent's staleness signal). Sessions preserve them on edits and never narrate or inject them. The `maintenance` agent stamps `type:` where it is missing.
33
34
 
34
35
  ## Keys
35
36
 
37
+ ### `description:`
38
+
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).
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
+ - **Upkeep:** an edit that changes what a document settles updates its description in the same edit.
43
+
36
44
  ### `objects:`
37
45
 
38
46
  - **Values:** the product objects the document designs, by the names the object register uses (`docs/dna/objects/<group>.yaml`, the product-dna skill's). Where a repository has no register yet, the names its product model and its documents already use.
@@ -81,4 +89,5 @@ An edit to a document that carries a retired key removes the key in the same edi
81
89
  - **Edge values:** `verifies:` values are repo-relative document paths, never a bare filename or a display title.
82
90
  - **Dangling edges block:** a `verifies:` naming a document that does not exist, or an `objects:` value no register or product model knows, is a typo or a deletion. Resolve it at authoring time.
83
91
  - **Index-time lints the graph prints:** a header pointing at a document that does not exist, a document no file points at, an object no document designs. They never fail a refresh; a session that sees one resolves it or files it.
84
- - **Baseline and reconcile:** the `maintenance` agent stamps `type:` and `objects:`, reduces any file-level `owns:` list to its region globs, removes retired keys, and reports source files whose header names no document. Many documents ⇒ parallel sub-agents, one document each.
92
+ - **Indexes name no files:** a folder index describes what its folder holds and how its files are named; the per-file account is each file's `description:`. A row per file, record or ID (`AD-010`) in an index is a defect.
93
+ - **Baseline and reconcile:** the `maintenance` agent stamps `type:`, `description:` and `objects:`, reduces any file-level `owns:` list to its region globs, removes retired keys, and reports source files whose header names no document. Many documents ⇒ parallel sub-agents, one document each.
@@ -37,7 +37,7 @@ Every genre takes the same shape (principle 2) and the same labels (principle 5)
37
37
  - **Owning documents:** feature, package and system are the three genres that speak for code: source files name them in their headers, and the ownership scan resolves paths to them (`frontmatter.md`). They take principles 5 and 6 in full, their sections are fixed (§ Owning documents — the body and the footer), and `doc-lint` reads them.
38
38
  - **Doctrine:** the same shape and labels as an owning document, speaking for no code and describing no mechanism. Its model layer may argue in paragraphs; below the model it is bullets like every other explaining document.
39
39
  - **Reference documents and procedures:** the one place a signature, a shape, a variable table, a command list or a file path is the content. They keep the detail the owning genres hand to the graph (§ Reference documents).
40
- - **Indexes:** a folder's `Readme.md` describes directories, never files; the root README follows `templates/root-readme.md`. Both take § Labels.
40
+ - **Indexes:** a folder's `Readme.md` describes directories, never files — no link, filename or record ID per file; each file's own `description:` frontmatter is its account. The root README follows `templates/root-readme.md`. Both take § Labels.
41
41
 
42
42
  ## Shape — the model first, the detail after
43
43
 
@@ -149,6 +149,37 @@ function lintFile(path, findings) {
149
149
  const tree = rawLines.find(({ line }) => /^\s*[│├└]/.test(line));
150
150
  if (tree) findings.push({ path, n: tree.n, severity: 'block', id: 'index-file-tree', message: 'a tree drawn in a folder index — describe each directory in a sentence; the tree is docs/structure.md\'s', excerpt: tree.line.trim().slice(0, 90) });
151
151
  }
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
+ // docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
154
+ const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
155
+ if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
156
+ const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
157
+ for (const { n, line } of lines) {
158
+ if (line.includes('doc-lint-disable: index-names-records')) continue;
159
+ const named = [...line.matchAll(/(?<![\w/{}-])([\w.-]+\.md)\b/g)].map((m) => m[1]).find((f) => !SCAFFOLD_NAME.test(f) && !/YYYY|NNN|[A-Z]\.[A-Z]/.test(f));
160
+ const recordRow = /^\|\s*\[?`?[A-Z]{2,}-\d+/.test(line.trim());
161
+ if (named || recordRow) findings.push({ path, n, severity: 'block', id: 'index-names-records', message: `index names an individual ${recordRow ? 'record' : `file (\`${named}\`)`} — describe the folder; each file's \`description:\` frontmatter is its account`, excerpt: line.trim().slice(0, 90) });
162
+ }
163
+ }
164
+ // 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')) {
166
+ const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
167
+ const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
168
+ const m = /^description:[ \t]*(.*)$/m.exec(fmText);
169
+ if (!m) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: 'no `description:` in the frontmatter — at most 300 characters saying what this document settles and where its boundary sits' });
170
+ else {
171
+ let value = m[1].trim();
172
+ if (/^[>|][-+]?$/.test(value)) {
173
+ const rest = fmText.slice(m.index + m[0].length).split('\n').slice(1);
174
+ const block = [];
175
+ for (const l of rest) { if (/^\s+\S/.test(l)) block.push(l.trim()); else break; }
176
+ value = block.join(' ');
177
+ }
178
+ value = value.replace(/^(["'])(.*)\1$/, '$2');
179
+ if (!value) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: '`description:` is empty — at most 300 characters saying what this document settles and where its boundary sits' });
180
+ else if (value.length > 300 && !disabled.has('description-length')) findings.push({ path, n: 1, severity: 'block', id: 'description-length', message: `\`description:\` is ${value.length} characters — at most 300` });
181
+ }
182
+ }
152
183
  // The repo-root README is an orientation page with a budget (templates/root-readme.md): up to about 140 non-blank lines, plain Markdown, no chrome.
153
184
  if (scope === 'truth' && isRootReadme) {
154
185
  const raw = text.split('\n');
@@ -24,6 +24,7 @@ a claim to grade the built product against.
24
24
  ```markdown
25
25
  ---
26
26
  type: acceptance-criteria
27
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
27
28
  status: draft
28
29
  ---
29
30
 
@@ -7,6 +7,7 @@ Truth document. Use for: **how to verify one feature's promised behavior by driv
7
7
  ```yaml
8
8
  ---
9
9
  type: checklist # required tooling key — genre stamp
10
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
10
11
  verifies: [docs/features/auth.md] # required — the doc(s) whose promises this proves
11
12
  ---
12
13
  ```
@@ -2,11 +2,16 @@
2
2
 
3
3
  Truth document. Use for: the entry point for all documentation in a repo. Created during baseline audit.
4
4
 
5
- **This file describes directories, never files.** Each section names one folder under `docs/` and says what the folder holds, who reads it and how its files are named — a description of the directory, not of its contents. No per-file rows, no file tree, no `├──` block: the tree is `structure.md`'s, as the authority on placement, and `ls` is the roster. A row per file forces an index edit on every file added, renamed or retired, and the index's own history then records roster churn instead of what the index said — a repo carried 103 commits on `docs/README.md` before this rule, most of them row edits. Each section below names a folder and says what the folder holds and how its files are named; the only files an index may name are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`) and the root `CLAUDE.md`. The repo-root `README.md` routes the team and agents in by intent; this file describes the folders of the docs tree — each links the other once and neither restates the other. `doc-lint` blocks a folder index that links an individual document (`index-lists-files`) or draws a tree (`index-file-tree`).
5
+ **This file describes directories, never files.** Each section names one folder under `docs/` and says what the folder holds, who reads it and how its files are named — a description of the directory, not of its contents. No per-file rows, no file tree, no `├──` block: the tree is `structure.md`'s, as the authority on placement, and `ls` is the roster. A row per file forces an index edit on every file added, renamed or retired, and the index's own history then records roster churn instead of what the index said — a repo carried 103 commits on `docs/README.md` before this rule, most of them row edits. Each section below names a folder and says what the folder holds and how its files are named; the only files an index may name are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`) and the root `CLAUDE.md`. The repo-root `README.md` routes the team and agents in by intent; this file describes the folders of the docs tree — each links the other once and neither restates the other. Naming a file without a link is the same row: no filename, no record ID (`AD-010`), no per-record table. Each file's own `description:` frontmatter is its account, read with `grep -m1 '^description:' <folder>/*.md`. `doc-lint` blocks a folder index that links an individual document (`index-lists-files`), names one (`index-names-records`) or draws a tree (`index-file-tree`).
6
6
 
7
7
  **Link only what exists.** Every row below is a placeholder — drop any whose target the repo does not have. Most `history/` and `architecture/` folders are created on first use, so linking them unconditionally ships a docs index with dead links on day one. An index is a browse page: a link that resolves to nothing is worse than an absent row.
8
8
 
9
9
  ```
10
+ ---
11
+ type: docs-readme
12
+ description: "{at most 300 characters: what the documentation tree holds and how it is organised}"
13
+ ---
14
+
10
15
  # {Project Name} Documentation
11
16
 
12
17
  Technical documentation for {project description}. {Two or three sentences: what the product is, its planes or packages, who the docs are for.}
@@ -22,6 +22,7 @@ Nothing else: the neighbouring documents live in **For the agent**, the one clos
22
22
  ```yaml
23
23
  ---
24
24
  type: doctrine # required tooling key
25
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
25
26
  status: active # required — active | draft
26
27
  objects: [principal, credential, device grant] # recommended — the concepts this doctrine governs, by their graph names
27
28
  ---
@@ -30,6 +30,7 @@ Nothing else. `doc-lint` blocks the retired mechanism sections (`mechanism-secti
30
30
  ```yaml
31
31
  ---
32
32
  type: feature # required tooling key — preserve, never narrate
33
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
33
34
  status: active # required — active | draft | deprecated
34
35
  objects: [device, credential grant] # required — the product objects this document designs, by their graph names
35
36
  module: identity # optional — one bounded-context label
@@ -9,6 +9,7 @@ Required: **Verdict**, **Decide today**, **Live-world ledger** (even when empty)
9
9
  ````markdown
10
10
  ---
11
11
  type: handover
12
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
12
13
  status: {open | accepted}
13
14
  date: {YYYY-MM-DD}
14
15
  as_of: {YYYY-MM-DDTHH:MM±TZ}
@@ -27,6 +27,7 @@ Nothing else. The feature template's lint rules apply. Forty to a hundred and tw
27
27
  ```yaml
28
28
  ---
29
29
  type: package
30
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
30
31
  status: active
31
32
  objects: [nustack cli, device credential] # required — the objects this package designs or ships
32
33
  owns: ["packages/{name}/**"] # transitional — the residual region glob, read only by the ownership scan until it switches to header pointers
@@ -25,6 +25,7 @@ Required: **Goal**, **Stack**, **Prerequisites**, **Install**, **First run**, **
25
25
  ```markdown
26
26
  ---
27
27
  type: operations
28
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
28
29
  ---
29
30
 
30
31
  # Setup
@@ -26,6 +26,7 @@ Nothing else. The lint rules of the feature template apply. Sixty to a hundred a
26
26
  ```yaml
27
27
  ---
28
28
  type: system # required tooling key
29
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
29
30
  status: active # required — draft = the machine as a whole does not run
30
31
  objects: [run, run ledger, settlement] # required — the objects this machine owns or moves
31
32
  owns: # transitional — region globs, one per subtree; read only by the ownership scan until it switches to header pointers (references/frontmatter.md)
@@ -64,6 +64,7 @@ Across the header pointers and the transitional globs:
64
64
  - **Retired keys:** `applies:` and `keywords:` are removed wherever found (remediation); a doctrine the removed `applies:` named stays named in the document's Scope section.
65
65
  - Flag **equal-specificity overlaps** — two feature docs whose transitional `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
66
66
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
67
+ - **Write `description:` where missing.** Every truth document and folder index carries a `description:` of at most 300 characters saying what it settles and where its boundary sits (`documentation` skill, `references/frontmatter.md` § `description:`). Each one is written from a read of the document, never from its filename — many documents ⇒ parallel sub-agents on the cheapest capable tier, one batch each. A folder index that names its files or records is rewritten to describe the folder, after confirming every per-file fact it carried lives in that file.
67
68
 
68
69
  **Then validate the doc→doc edge.** `node .claude/hooks/check-ownership.mjs` and the session-injected **doc edge index** carry `verifies:` and its generated inverse; read the edge index and check two things:
69
70
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.39",
4
+ "version": "0.2.40",
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": "7837822ea61e"
17
+ "digest": "5a9f81fff98a"
18
18
  }
@@ -72,6 +72,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
72
72
  - **Records are never edited to match the present.** A completed plan stays wrong-in-hindsight; a changelog entry describes close-time state; a scope change gets a *new* dated record that links back, never an edit to a frozen one.
73
73
  - **Two sanctioned bridges — head is truth, tail is provenance.** An **Architecture Decision (AD)** ends with a **Decision Changelog** (for a decision, the journey — what each revision reversed and what triggered it — is part of the reasoning), and the `maintenance` agent's **Health Log** lives in `docs/history/health-log.md`, an append-only record. The AD body stays current-truth-only; the tail carries the dates. No other truth document carries dated history. `docs/structure.md` is harness-owned — written whole by the installer, never edited by hand or by a skill; only its `harness-version` frontmatter is stamped, by the `maintenance` agent.
74
74
  - **Indexes describe directories, never files.** `docs/README.md` — the top-level index of the documentation folder — is a description of its directories: one entry per folder saying what the folder holds, who reads it and how its files are named. It carries no file list and no file tree: not a per-file row, not a `├──` tree of files, not a tree at all — `docs/structure.md` is the one place the tree lives, as the authority on placement. The same holds for a `README.md` / `Readme.md` at any level of `docs/`. `ls` is the roster, and the ownership index is how a document is found for a path. A per-file row has to be edited on every add, rename and retirement, so the index's history records roster churn instead of what the index said, and a reader cannot tell when the index itself changed. The only files an index may link are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`, `memory/main.md`) and a sibling folder's own `Readme.md`. `scripts/doc-lint.mjs` blocks the rest as `index-lists-files`, and blocks a tree drawn in a folder index as `index-file-tree`. An inverse (which checklists verify a feature, which docs a folder holds) is generated, never written.
75
+ - **Every document describes itself.** Its frontmatter carries `description:` — at most 300 characters, one or two present-tense sentences saying what the document settles and where its boundary sits (`references/frontmatter.md` § `description:`). That is the per-file account an index would otherwise keep, so an index names no file and no record: not a link, not a filename, not an ID row such as `AD-010`. A folder is browsed with `grep -m1 '^description:' <folder>/*.md`. `doc-lint` blocks a truth document or folder index without one (`description-missing`), one over 300 characters (`description-length`), and an index row that names a file or record (`index-names-records`).
75
76
  - **Truth documents cite records for the _why_, never the _when_.** `Provenance: AD-002` is good; "decided in AD-002 and shipped 2026-06-13" is not. Routing/index docs point only to truth documents as a topic's authority — never a plan or changelog entry as the "read first" source. (An index _of the records themselves_ — a History section listing `plans/`, `changelog/` — is sanctioned.)
76
77
 
77
78
  **A document is intent and promises; facts are the graph's.** An owning document (feature, package, system) answers why the product is shaped this way and what it promises, and names the objects it designs in `objects:`. Every computable fact — a table's columns and vocabularies, a foreign key and its delete rule, which function writes which column, which route sits behind which guard, which code a refusal answers, which file holds what — is the code graph's to compute (`docs/architecture/systems/knowledge-layer.md`), and a hand-typed copy of it is the defect this rule removes: it reads as authoritative and is wrong the day the code moves. Mechanism stays in the code, reasons stay in the document, and the document points by object and symbol name, never by line number. `doc-lint` blocks the retired mechanism sections in an owning document (`mechanism-section`) and an owning document without `objects:`. `keywords:` is not a key: an object's name is its discovery term. **The body is scanned by people** — sharp bullets under noun-phrase headings, each a bold noun-phrase label, a colon, then one or two plain sentences; code named only where a person types or sees it, or at a cross-cutting seam the graph would not lead a reader to (a middleware, a gate, a hook, a shared store); every other pointer in the closing **For the agent** footer. The owning genres read Scope · Flows (Flow for a system, Usage for a package) · Decisions · Promises · Gaps · For the agent (`references/style-guide.md` § Labels, § Owning documents — the body and the footer; `doc-lint` warns `body-identifier`, `clause-label` and `second-person`).
@@ -15,24 +15,32 @@ A document names the product objects it designs. Every other edge between a docu
15
15
  3. **Computed ownership:** the object closure (document → objects → tables → their write, read and refusal sites → files) reaches files nobody listed. Where the header pointer and the closure disagree, the graph prints both. Which document speaks for a path is a query, answered per path on demand.
16
16
  4. **Nothing recomputable stored:** a list of files, a list of consumers, or a list of governing documents typed into frontmatter is a copy of something the graph computes. It is stale the day the code moves, and it fails the stability test (`style-guide.md` § Shape — the model first, the detail after).
17
17
  5. **One declaration, at the consuming end:** the edges a person must author (`objects:`, `verifies:`) are declared once, by the document that knows. Every inverse is generated, never authored.
18
+ 6. **The description as the per-file account:** every document states what it is about in its own `description:`. A folder is browsed by reading its files' descriptions, so no index keeps a row per file.
18
19
 
19
20
  ## Keys by genre
20
21
 
21
22
  | Genre | Keys |
22
23
  | --- | --- |
23
- | Feature | `type`, `status`, `objects`, `module` (optional) · transitional `owns` |
24
- | System | `type`, `status`, `objects` · transitional `owns` |
25
- | Package | `type`, `status`, `objects` · transitional `owns` |
26
- | Doctrine | `type`, `status`, `objects` |
27
- | UX | `type`, `status`, `objects` · transitional `owns` when it speaks for a surface's components |
28
- | Behavioral checklist | `type: checklist`, `verifies` |
29
- | Acceptance criteria | `type`, `status` |
30
- | Every other genre | `type`, plus its template's own keys |
24
+ | Feature | `type`, `description`, `status`, `objects`, `module` (optional) · transitional `owns` |
25
+ | System | `type`, `description`, `status`, `objects` · transitional `owns` |
26
+ | Package | `type`, `description`, `status`, `objects` · transitional `owns` |
27
+ | Doctrine | `type`, `description`, `status`, `objects` |
28
+ | UX | `type`, `description`, `status`, `objects` · transitional `owns` when it speaks for a surface's components |
29
+ | Behavioral checklist | `type: checklist`, `description`, `verifies` |
30
+ | Acceptance criteria | `type`, `description`, `status` |
31
+ | Every other genre | `type`, `description`, plus its template's own keys |
31
32
 
32
33
  Tooling keys sit beside these on any genre: `rev:` (a generator's generation stamp), `slug:` (the identity key on generated documents, minted once, and in the repository the filename), and `last-reviewed:` (stamped on a substantive review, the `maintenance` agent's staleness signal). Sessions preserve them on edits and never narrate or inject them. The `maintenance` agent stamps `type:` where it is missing.
33
34
 
34
35
  ## Keys
35
36
 
37
+ ### `description:`
38
+
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).
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
+ - **Upkeep:** an edit that changes what a document settles updates its description in the same edit.
43
+
36
44
  ### `objects:`
37
45
 
38
46
  - **Values:** the product objects the document designs, by the names the object register uses (`docs/dna/objects/<group>.yaml`, the product-dna skill's). Where a repository has no register yet, the names its product model and its documents already use.
@@ -81,4 +89,5 @@ An edit to a document that carries a retired key removes the key in the same edi
81
89
  - **Edge values:** `verifies:` values are repo-relative document paths, never a bare filename or a display title.
82
90
  - **Dangling edges block:** a `verifies:` naming a document that does not exist, or an `objects:` value no register or product model knows, is a typo or a deletion. Resolve it at authoring time.
83
91
  - **Index-time lints the graph prints:** a header pointing at a document that does not exist, a document no file points at, an object no document designs. They never fail a refresh; a session that sees one resolves it or files it.
84
- - **Baseline and reconcile:** the `maintenance` agent stamps `type:` and `objects:`, reduces any file-level `owns:` list to its region globs, removes retired keys, and reports source files whose header names no document. Many documents ⇒ parallel sub-agents, one document each.
92
+ - **Indexes name no files:** a folder index describes what its folder holds and how its files are named; the per-file account is each file's `description:`. A row per file, record or ID (`AD-010`) in an index is a defect.
93
+ - **Baseline and reconcile:** the `maintenance` agent stamps `type:`, `description:` and `objects:`, reduces any file-level `owns:` list to its region globs, removes retired keys, and reports source files whose header names no document. Many documents ⇒ parallel sub-agents, one document each.
@@ -37,7 +37,7 @@ Every genre takes the same shape (principle 2) and the same labels (principle 5)
37
37
  - **Owning documents:** feature, package and system are the three genres that speak for code: source files name them in their headers, and the ownership scan resolves paths to them (`frontmatter.md`). They take principles 5 and 6 in full, their sections are fixed (§ Owning documents — the body and the footer), and `doc-lint` reads them.
38
38
  - **Doctrine:** the same shape and labels as an owning document, speaking for no code and describing no mechanism. Its model layer may argue in paragraphs; below the model it is bullets like every other explaining document.
39
39
  - **Reference documents and procedures:** the one place a signature, a shape, a variable table, a command list or a file path is the content. They keep the detail the owning genres hand to the graph (§ Reference documents).
40
- - **Indexes:** a folder's `Readme.md` describes directories, never files; the root README follows `templates/root-readme.md`. Both take § Labels.
40
+ - **Indexes:** a folder's `Readme.md` describes directories, never files — no link, filename or record ID per file; each file's own `description:` frontmatter is its account. The root README follows `templates/root-readme.md`. Both take § Labels.
41
41
 
42
42
  ## Shape — the model first, the detail after
43
43
 
@@ -149,6 +149,37 @@ function lintFile(path, findings) {
149
149
  const tree = rawLines.find(({ line }) => /^\s*[│├└]/.test(line));
150
150
  if (tree) findings.push({ path, n: tree.n, severity: 'block', id: 'index-file-tree', message: 'a tree drawn in a folder index — describe each directory in a sentence; the tree is docs/structure.md\'s', excerpt: tree.line.trim().slice(0, 90) });
151
151
  }
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
+ // docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
154
+ const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
155
+ if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
156
+ const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
157
+ for (const { n, line } of lines) {
158
+ if (line.includes('doc-lint-disable: index-names-records')) continue;
159
+ const named = [...line.matchAll(/(?<![\w/{}-])([\w.-]+\.md)\b/g)].map((m) => m[1]).find((f) => !SCAFFOLD_NAME.test(f) && !/YYYY|NNN|[A-Z]\.[A-Z]/.test(f));
160
+ const recordRow = /^\|\s*\[?`?[A-Z]{2,}-\d+/.test(line.trim());
161
+ if (named || recordRow) findings.push({ path, n, severity: 'block', id: 'index-names-records', message: `index names an individual ${recordRow ? 'record' : `file (\`${named}\`)`} — describe the folder; each file's \`description:\` frontmatter is its account`, excerpt: line.trim().slice(0, 90) });
162
+ }
163
+ }
164
+ // 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')) {
166
+ const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
167
+ const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
168
+ const m = /^description:[ \t]*(.*)$/m.exec(fmText);
169
+ if (!m) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: 'no `description:` in the frontmatter — at most 300 characters saying what this document settles and where its boundary sits' });
170
+ else {
171
+ let value = m[1].trim();
172
+ if (/^[>|][-+]?$/.test(value)) {
173
+ const rest = fmText.slice(m.index + m[0].length).split('\n').slice(1);
174
+ const block = [];
175
+ for (const l of rest) { if (/^\s+\S/.test(l)) block.push(l.trim()); else break; }
176
+ value = block.join(' ');
177
+ }
178
+ value = value.replace(/^(["'])(.*)\1$/, '$2');
179
+ if (!value) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: '`description:` is empty — at most 300 characters saying what this document settles and where its boundary sits' });
180
+ else if (value.length > 300 && !disabled.has('description-length')) findings.push({ path, n: 1, severity: 'block', id: 'description-length', message: `\`description:\` is ${value.length} characters — at most 300` });
181
+ }
182
+ }
152
183
  // The repo-root README is an orientation page with a budget (templates/root-readme.md): up to about 140 non-blank lines, plain Markdown, no chrome.
153
184
  if (scope === 'truth' && isRootReadme) {
154
185
  const raw = text.split('\n');
@@ -24,6 +24,7 @@ a claim to grade the built product against.
24
24
  ```markdown
25
25
  ---
26
26
  type: acceptance-criteria
27
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
27
28
  status: draft
28
29
  ---
29
30
 
@@ -7,6 +7,7 @@ Truth document. Use for: **how to verify one feature's promised behavior by driv
7
7
  ```yaml
8
8
  ---
9
9
  type: checklist # required tooling key — genre stamp
10
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
10
11
  verifies: [docs/features/auth.md] # required — the doc(s) whose promises this proves
11
12
  ---
12
13
  ```
@@ -2,11 +2,16 @@
2
2
 
3
3
  Truth document. Use for: the entry point for all documentation in a repo. Created during baseline audit.
4
4
 
5
- **This file describes directories, never files.** Each section names one folder under `docs/` and says what the folder holds, who reads it and how its files are named — a description of the directory, not of its contents. No per-file rows, no file tree, no `├──` block: the tree is `structure.md`'s, as the authority on placement, and `ls` is the roster. A row per file forces an index edit on every file added, renamed or retired, and the index's own history then records roster churn instead of what the index said — a repo carried 103 commits on `docs/README.md` before this rule, most of them row edits. Each section below names a folder and says what the folder holds and how its files are named; the only files an index may name are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`) and the root `CLAUDE.md`. The repo-root `README.md` routes the team and agents in by intent; this file describes the folders of the docs tree — each links the other once and neither restates the other. `doc-lint` blocks a folder index that links an individual document (`index-lists-files`) or draws a tree (`index-file-tree`).
5
+ **This file describes directories, never files.** Each section names one folder under `docs/` and says what the folder holds, who reads it and how its files are named — a description of the directory, not of its contents. No per-file rows, no file tree, no `├──` block: the tree is `structure.md`'s, as the authority on placement, and `ls` is the roster. A row per file forces an index edit on every file added, renamed or retired, and the index's own history then records roster churn instead of what the index said — a repo carried 103 commits on `docs/README.md` before this rule, most of them row edits. Each section below names a folder and says what the folder holds and how its files are named; the only files an index may name are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`) and the root `CLAUDE.md`. The repo-root `README.md` routes the team and agents in by intent; this file describes the folders of the docs tree — each links the other once and neither restates the other. Naming a file without a link is the same row: no filename, no record ID (`AD-010`), no per-record table. Each file's own `description:` frontmatter is its account, read with `grep -m1 '^description:' <folder>/*.md`. `doc-lint` blocks a folder index that links an individual document (`index-lists-files`), names one (`index-names-records`) or draws a tree (`index-file-tree`).
6
6
 
7
7
  **Link only what exists.** Every row below is a placeholder — drop any whose target the repo does not have. Most `history/` and `architecture/` folders are created on first use, so linking them unconditionally ships a docs index with dead links on day one. An index is a browse page: a link that resolves to nothing is worse than an absent row.
8
8
 
9
9
  ```
10
+ ---
11
+ type: docs-readme
12
+ description: "{at most 300 characters: what the documentation tree holds and how it is organised}"
13
+ ---
14
+
10
15
  # {Project Name} Documentation
11
16
 
12
17
  Technical documentation for {project description}. {Two or three sentences: what the product is, its planes or packages, who the docs are for.}
@@ -22,6 +22,7 @@ Nothing else: the neighbouring documents live in **For the agent**, the one clos
22
22
  ```yaml
23
23
  ---
24
24
  type: doctrine # required tooling key
25
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
25
26
  status: active # required — active | draft
26
27
  objects: [principal, credential, device grant] # recommended — the concepts this doctrine governs, by their graph names
27
28
  ---
@@ -30,6 +30,7 @@ Nothing else. `doc-lint` blocks the retired mechanism sections (`mechanism-secti
30
30
  ```yaml
31
31
  ---
32
32
  type: feature # required tooling key — preserve, never narrate
33
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
33
34
  status: active # required — active | draft | deprecated
34
35
  objects: [device, credential grant] # required — the product objects this document designs, by their graph names
35
36
  module: identity # optional — one bounded-context label
@@ -9,6 +9,7 @@ Required: **Verdict**, **Decide today**, **Live-world ledger** (even when empty)
9
9
  ````markdown
10
10
  ---
11
11
  type: handover
12
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
12
13
  status: {open | accepted}
13
14
  date: {YYYY-MM-DD}
14
15
  as_of: {YYYY-MM-DDTHH:MM±TZ}
@@ -27,6 +27,7 @@ Nothing else. The feature template's lint rules apply. Forty to a hundred and tw
27
27
  ```yaml
28
28
  ---
29
29
  type: package
30
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
30
31
  status: active
31
32
  objects: [nustack cli, device credential] # required — the objects this package designs or ships
32
33
  owns: ["packages/{name}/**"] # transitional — the residual region glob, read only by the ownership scan until it switches to header pointers
@@ -25,6 +25,7 @@ Required: **Goal**, **Stack**, **Prerequisites**, **Install**, **First run**, **
25
25
  ```markdown
26
26
  ---
27
27
  type: operations
28
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
28
29
  ---
29
30
 
30
31
  # Setup
@@ -26,6 +26,7 @@ Nothing else. The lint rules of the feature template apply. Sixty to a hundred a
26
26
  ```yaml
27
27
  ---
28
28
  type: system # required tooling key
29
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
29
30
  status: active # required — draft = the machine as a whole does not run
30
31
  objects: [run, run ledger, settlement] # required — the objects this machine owns or moves
31
32
  owns: # transitional — region globs, one per subtree; read only by the ownership scan until it switches to header pointers (references/frontmatter.md)
@@ -64,6 +64,7 @@ Across the header pointers and the transitional globs:
64
64
  - **Retired keys:** `applies:` and `keywords:` are removed wherever found (remediation); a doctrine the removed `applies:` named stays named in the document's Scope section.
65
65
  - Flag **equal-specificity overlaps** — two feature docs whose transitional `owns:` globs claim the same paths at the same specificity (an ambiguous owner). `node .claude/hooks/check-ownership.mjs` reports these mechanically alongside dead globs (`overlaps()` in `lib/ownership.mjs`); a glob at strictly higher specificity carving out of a broader `**` is the designed layering and is never flagged. Surface a real overlap as a question — narrowing someone's glob is an author's call, not remediation.
66
66
  - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
67
+ - **Write `description:` where missing.** Every truth document and folder index carries a `description:` of at most 300 characters saying what it settles and where its boundary sits (`documentation` skill, `references/frontmatter.md` § `description:`). Each one is written from a read of the document, never from its filename — many documents ⇒ parallel sub-agents on the cheapest capable tier, one batch each. A folder index that names its files or records is rewritten to describe the folder, after confirming every per-file fact it carried lives in that file.
67
68
 
68
69
  **Then validate the doc→doc edge.** `node .claude/hooks/check-ownership.mjs` and the session-injected **doc edge index** carry `verifies:` and its generated inverse; read the edge index and check two things:
69
70
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.29",
4
+ "version": "1.5.30",
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": "26630babb6e4"
17
+ "digest": "02bf4a5e9588"
18
18
  }
@@ -72,6 +72,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
72
72
  - **Records are never edited to match the present.** A completed plan stays wrong-in-hindsight; a changelog entry describes close-time state; a scope change gets a *new* dated record that links back, never an edit to a frozen one.
73
73
  - **Two sanctioned bridges — head is truth, tail is provenance.** An **Architecture Decision (AD)** ends with a **Decision Changelog** (for a decision, the journey — what each revision reversed and what triggered it — is part of the reasoning), and the `maintenance` agent's **Health Log** lives in `docs/history/health-log.md`, an append-only record. The AD body stays current-truth-only; the tail carries the dates. No other truth document carries dated history. `docs/structure.md` is harness-owned — written whole by the installer, never edited by hand or by a skill; only its `harness-version` frontmatter is stamped, by the `maintenance` agent.
74
74
  - **Indexes describe directories, never files.** `docs/README.md` — the top-level index of the documentation folder — is a description of its directories: one entry per folder saying what the folder holds, who reads it and how its files are named. It carries no file list and no file tree: not a per-file row, not a `├──` tree of files, not a tree at all — `docs/structure.md` is the one place the tree lives, as the authority on placement. The same holds for a `README.md` / `Readme.md` at any level of `docs/`. `ls` is the roster, and the ownership index is how a document is found for a path. A per-file row has to be edited on every add, rename and retirement, so the index's history records roster churn instead of what the index said, and a reader cannot tell when the index itself changed. The only files an index may link are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`, `memory/main.md`) and a sibling folder's own `Readme.md`. `scripts/doc-lint.mjs` blocks the rest as `index-lists-files`, and blocks a tree drawn in a folder index as `index-file-tree`. An inverse (which checklists verify a feature, which docs a folder holds) is generated, never written.
75
+ - **Every document describes itself.** Its frontmatter carries `description:` — at most 300 characters, one or two present-tense sentences saying what the document settles and where its boundary sits (`references/frontmatter.md` § `description:`). That is the per-file account an index would otherwise keep, so an index names no file and no record: not a link, not a filename, not an ID row such as `AD-010`. A folder is browsed with `grep -m1 '^description:' <folder>/*.md`. `doc-lint` blocks a truth document or folder index without one (`description-missing`), one over 300 characters (`description-length`), and an index row that names a file or record (`index-names-records`).
75
76
  - **Truth documents cite records for the _why_, never the _when_.** `Provenance: AD-002` is good; "decided in AD-002 and shipped 2026-06-13" is not. Routing/index docs point only to truth documents as a topic's authority — never a plan or changelog entry as the "read first" source. (An index _of the records themselves_ — a History section listing `plans/`, `changelog/` — is sanctioned.)
76
77
 
77
78
  **A document is intent and promises; facts are the graph's.** An owning document (feature, package, system) answers why the product is shaped this way and what it promises, and names the objects it designs in `objects:`. Every computable fact — a table's columns and vocabularies, a foreign key and its delete rule, which function writes which column, which route sits behind which guard, which code a refusal answers, which file holds what — is the code graph's to compute (`docs/architecture/systems/knowledge-layer.md`), and a hand-typed copy of it is the defect this rule removes: it reads as authoritative and is wrong the day the code moves. Mechanism stays in the code, reasons stay in the document, and the document points by object and symbol name, never by line number. `doc-lint` blocks the retired mechanism sections in an owning document (`mechanism-section`) and an owning document without `objects:`. `keywords:` is not a key: an object's name is its discovery term. **The body is scanned by people** — sharp bullets under noun-phrase headings, each a bold noun-phrase label, a colon, then one or two plain sentences; code named only where a person types or sees it, or at a cross-cutting seam the graph would not lead a reader to (a middleware, a gate, a hook, a shared store); every other pointer in the closing **For the agent** footer. The owning genres read Scope · Flows (Flow for a system, Usage for a package) · Decisions · Promises · Gaps · For the agent (`references/style-guide.md` § Labels, § Owning documents — the body and the footer; `doc-lint` warns `body-identifier`, `clause-label` and `second-person`).
@@ -15,24 +15,32 @@ A document names the product objects it designs. Every other edge between a docu
15
15
  3. **Computed ownership:** the object closure (document → objects → tables → their write, read and refusal sites → files) reaches files nobody listed. Where the header pointer and the closure disagree, the graph prints both. Which document speaks for a path is a query, answered per path on demand.
16
16
  4. **Nothing recomputable stored:** a list of files, a list of consumers, or a list of governing documents typed into frontmatter is a copy of something the graph computes. It is stale the day the code moves, and it fails the stability test (`style-guide.md` § Shape — the model first, the detail after).
17
17
  5. **One declaration, at the consuming end:** the edges a person must author (`objects:`, `verifies:`) are declared once, by the document that knows. Every inverse is generated, never authored.
18
+ 6. **The description as the per-file account:** every document states what it is about in its own `description:`. A folder is browsed by reading its files' descriptions, so no index keeps a row per file.
18
19
 
19
20
  ## Keys by genre
20
21
 
21
22
  | Genre | Keys |
22
23
  | --- | --- |
23
- | Feature | `type`, `status`, `objects`, `module` (optional) · transitional `owns` |
24
- | System | `type`, `status`, `objects` · transitional `owns` |
25
- | Package | `type`, `status`, `objects` · transitional `owns` |
26
- | Doctrine | `type`, `status`, `objects` |
27
- | UX | `type`, `status`, `objects` · transitional `owns` when it speaks for a surface's components |
28
- | Behavioral checklist | `type: checklist`, `verifies` |
29
- | Acceptance criteria | `type`, `status` |
30
- | Every other genre | `type`, plus its template's own keys |
24
+ | Feature | `type`, `description`, `status`, `objects`, `module` (optional) · transitional `owns` |
25
+ | System | `type`, `description`, `status`, `objects` · transitional `owns` |
26
+ | Package | `type`, `description`, `status`, `objects` · transitional `owns` |
27
+ | Doctrine | `type`, `description`, `status`, `objects` |
28
+ | UX | `type`, `description`, `status`, `objects` · transitional `owns` when it speaks for a surface's components |
29
+ | Behavioral checklist | `type: checklist`, `description`, `verifies` |
30
+ | Acceptance criteria | `type`, `description`, `status` |
31
+ | Every other genre | `type`, `description`, plus its template's own keys |
31
32
 
32
33
  Tooling keys sit beside these on any genre: `rev:` (a generator's generation stamp), `slug:` (the identity key on generated documents, minted once, and in the repository the filename), and `last-reviewed:` (stamped on a substantive review, the `maintenance` agent's staleness signal). Sessions preserve them on edits and never narrate or inject them. The `maintenance` agent stamps `type:` where it is missing.
33
34
 
34
35
  ## Keys
35
36
 
37
+ ### `description:`
38
+
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).
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
+ - **Upkeep:** an edit that changes what a document settles updates its description in the same edit.
43
+
36
44
  ### `objects:`
37
45
 
38
46
  - **Values:** the product objects the document designs, by the names the object register uses (`docs/dna/objects/<group>.yaml`, the product-dna skill's). Where a repository has no register yet, the names its product model and its documents already use.
@@ -81,4 +89,5 @@ An edit to a document that carries a retired key removes the key in the same edi
81
89
  - **Edge values:** `verifies:` values are repo-relative document paths, never a bare filename or a display title.
82
90
  - **Dangling edges block:** a `verifies:` naming a document that does not exist, or an `objects:` value no register or product model knows, is a typo or a deletion. Resolve it at authoring time.
83
91
  - **Index-time lints the graph prints:** a header pointing at a document that does not exist, a document no file points at, an object no document designs. They never fail a refresh; a session that sees one resolves it or files it.
84
- - **Baseline and reconcile:** the `maintenance` agent stamps `type:` and `objects:`, reduces any file-level `owns:` list to its region globs, removes retired keys, and reports source files whose header names no document. Many documents ⇒ parallel sub-agents, one document each.
92
+ - **Indexes name no files:** a folder index describes what its folder holds and how its files are named; the per-file account is each file's `description:`. A row per file, record or ID (`AD-010`) in an index is a defect.
93
+ - **Baseline and reconcile:** the `maintenance` agent stamps `type:`, `description:` and `objects:`, reduces any file-level `owns:` list to its region globs, removes retired keys, and reports source files whose header names no document. Many documents ⇒ parallel sub-agents, one document each.
@@ -37,7 +37,7 @@ Every genre takes the same shape (principle 2) and the same labels (principle 5)
37
37
  - **Owning documents:** feature, package and system are the three genres that speak for code: source files name them in their headers, and the ownership scan resolves paths to them (`frontmatter.md`). They take principles 5 and 6 in full, their sections are fixed (§ Owning documents — the body and the footer), and `doc-lint` reads them.
38
38
  - **Doctrine:** the same shape and labels as an owning document, speaking for no code and describing no mechanism. Its model layer may argue in paragraphs; below the model it is bullets like every other explaining document.
39
39
  - **Reference documents and procedures:** the one place a signature, a shape, a variable table, a command list or a file path is the content. They keep the detail the owning genres hand to the graph (§ Reference documents).
40
- - **Indexes:** a folder's `Readme.md` describes directories, never files; the root README follows `templates/root-readme.md`. Both take § Labels.
40
+ - **Indexes:** a folder's `Readme.md` describes directories, never files — no link, filename or record ID per file; each file's own `description:` frontmatter is its account. The root README follows `templates/root-readme.md`. Both take § Labels.
41
41
 
42
42
  ## Shape — the model first, the detail after
43
43
 
@@ -149,6 +149,37 @@ function lintFile(path, findings) {
149
149
  const tree = rawLines.find(({ line }) => /^\s*[│├└]/.test(line));
150
150
  if (tree) findings.push({ path, n: tree.n, severity: 'block', id: 'index-file-tree', message: 'a tree drawn in a folder index — describe each directory in a sentence; the tree is docs/structure.md\'s', excerpt: tree.line.trim().slice(0, 90) });
151
151
  }
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
+ // docs/dna/ is the product-dna skill's generated tree, laid out by that skill's own schema.
154
+ const isGeneratedTree = /(^|\/)docs\/dna\//.test(path);
155
+ if (scope === 'truth' && isFolderIndex && /(^|\/)docs\//.test(path) && !isGeneratedTree && !disabled.has('index-names-records')) {
156
+ const SCAFFOLD_NAME = /^(readme|structure|changelog|backlog|main|health-log|journal|claude|agents|setup)\.md$/i;
157
+ for (const { n, line } of lines) {
158
+ if (line.includes('doc-lint-disable: index-names-records')) continue;
159
+ const named = [...line.matchAll(/(?<![\w/{}-])([\w.-]+\.md)\b/g)].map((m) => m[1]).find((f) => !SCAFFOLD_NAME.test(f) && !/YYYY|NNN|[A-Z]\.[A-Z]/.test(f));
160
+ const recordRow = /^\|\s*\[?`?[A-Z]{2,}-\d+/.test(line.trim());
161
+ if (named || recordRow) findings.push({ path, n, severity: 'block', id: 'index-names-records', message: `index names an individual ${recordRow ? 'record' : `file (\`${named}\`)`} — describe the folder; each file's \`description:\` frontmatter is its account`, excerpt: line.trim().slice(0, 90) });
162
+ }
163
+ }
164
+ // 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')) {
166
+ const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
167
+ const fmText = fmEnd === -1 ? '' : text.slice(0, fmEnd);
168
+ const m = /^description:[ \t]*(.*)$/m.exec(fmText);
169
+ if (!m) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: 'no `description:` in the frontmatter — at most 300 characters saying what this document settles and where its boundary sits' });
170
+ else {
171
+ let value = m[1].trim();
172
+ if (/^[>|][-+]?$/.test(value)) {
173
+ const rest = fmText.slice(m.index + m[0].length).split('\n').slice(1);
174
+ const block = [];
175
+ for (const l of rest) { if (/^\s+\S/.test(l)) block.push(l.trim()); else break; }
176
+ value = block.join(' ');
177
+ }
178
+ value = value.replace(/^(["'])(.*)\1$/, '$2');
179
+ if (!value) findings.push({ path, n: 1, severity: 'block', id: 'description-missing', message: '`description:` is empty — at most 300 characters saying what this document settles and where its boundary sits' });
180
+ else if (value.length > 300 && !disabled.has('description-length')) findings.push({ path, n: 1, severity: 'block', id: 'description-length', message: `\`description:\` is ${value.length} characters — at most 300` });
181
+ }
182
+ }
152
183
  // The repo-root README is an orientation page with a budget (templates/root-readme.md): up to about 140 non-blank lines, plain Markdown, no chrome.
153
184
  if (scope === 'truth' && isRootReadme) {
154
185
  const raw = text.split('\n');
@@ -24,6 +24,7 @@ a claim to grade the built product against.
24
24
  ```markdown
25
25
  ---
26
26
  type: acceptance-criteria
27
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
27
28
  status: draft
28
29
  ---
29
30
 
@@ -7,6 +7,7 @@ Truth document. Use for: **how to verify one feature's promised behavior by driv
7
7
  ```yaml
8
8
  ---
9
9
  type: checklist # required tooling key — genre stamp
10
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
10
11
  verifies: [docs/features/auth.md] # required — the doc(s) whose promises this proves
11
12
  ---
12
13
  ```
@@ -2,11 +2,16 @@
2
2
 
3
3
  Truth document. Use for: the entry point for all documentation in a repo. Created during baseline audit.
4
4
 
5
- **This file describes directories, never files.** Each section names one folder under `docs/` and says what the folder holds, who reads it and how its files are named — a description of the directory, not of its contents. No per-file rows, no file tree, no `├──` block: the tree is `structure.md`'s, as the authority on placement, and `ls` is the roster. A row per file forces an index edit on every file added, renamed or retired, and the index's own history then records roster churn instead of what the index said — a repo carried 103 commits on `docs/README.md` before this rule, most of them row edits. Each section below names a folder and says what the folder holds and how its files are named; the only files an index may name are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`) and the root `CLAUDE.md`. The repo-root `README.md` routes the team and agents in by intent; this file describes the folders of the docs tree — each links the other once and neither restates the other. `doc-lint` blocks a folder index that links an individual document (`index-lists-files`) or draws a tree (`index-file-tree`).
5
+ **This file describes directories, never files.** Each section names one folder under `docs/` and says what the folder holds, who reads it and how its files are named — a description of the directory, not of its contents. No per-file rows, no file tree, no `├──` block: the tree is `structure.md`'s, as the authority on placement, and `ls` is the roster. A row per file forces an index edit on every file added, renamed or retired, and the index's own history then records roster churn instead of what the index said — a repo carried 103 commits on `docs/README.md` before this rule, most of them row edits. Each section below names a folder and says what the folder holds and how its files are named; the only files an index may name are the mandated scaffolds (`structure.md`, `operations/setup.md`, `history/CHANGELOG.md`, `history/backlog.md`) and the root `CLAUDE.md`. The repo-root `README.md` routes the team and agents in by intent; this file describes the folders of the docs tree — each links the other once and neither restates the other. Naming a file without a link is the same row: no filename, no record ID (`AD-010`), no per-record table. Each file's own `description:` frontmatter is its account, read with `grep -m1 '^description:' <folder>/*.md`. `doc-lint` blocks a folder index that links an individual document (`index-lists-files`), names one (`index-names-records`) or draws a tree (`index-file-tree`).
6
6
 
7
7
  **Link only what exists.** Every row below is a placeholder — drop any whose target the repo does not have. Most `history/` and `architecture/` folders are created on first use, so linking them unconditionally ships a docs index with dead links on day one. An index is a browse page: a link that resolves to nothing is worse than an absent row.
8
8
 
9
9
  ```
10
+ ---
11
+ type: docs-readme
12
+ description: "{at most 300 characters: what the documentation tree holds and how it is organised}"
13
+ ---
14
+
10
15
  # {Project Name} Documentation
11
16
 
12
17
  Technical documentation for {project description}. {Two or three sentences: what the product is, its planes or packages, who the docs are for.}
@@ -22,6 +22,7 @@ Nothing else: the neighbouring documents live in **For the agent**, the one clos
22
22
  ```yaml
23
23
  ---
24
24
  type: doctrine # required tooling key
25
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
25
26
  status: active # required — active | draft
26
27
  objects: [principal, credential, device grant] # recommended — the concepts this doctrine governs, by their graph names
27
28
  ---
@@ -30,6 +30,7 @@ Nothing else. `doc-lint` blocks the retired mechanism sections (`mechanism-secti
30
30
  ```yaml
31
31
  ---
32
32
  type: feature # required tooling key — preserve, never narrate
33
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
33
34
  status: active # required — active | draft | deprecated
34
35
  objects: [device, credential grant] # required — the product objects this document designs, by their graph names
35
36
  module: identity # optional — one bounded-context label
@@ -9,6 +9,7 @@ Required: **Verdict**, **Decide today**, **Live-world ledger** (even when empty)
9
9
  ````markdown
10
10
  ---
11
11
  type: handover
12
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
12
13
  status: {open | accepted}
13
14
  date: {YYYY-MM-DD}
14
15
  as_of: {YYYY-MM-DDTHH:MM±TZ}
@@ -27,6 +27,7 @@ Nothing else. The feature template's lint rules apply. Forty to a hundred and tw
27
27
  ```yaml
28
28
  ---
29
29
  type: package
30
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
30
31
  status: active
31
32
  objects: [nustack cli, device credential] # required — the objects this package designs or ships
32
33
  owns: ["packages/{name}/**"] # transitional — the residual region glob, read only by the ownership scan until it switches to header pointers
@@ -25,6 +25,7 @@ Required: **Goal**, **Stack**, **Prerequisites**, **Install**, **First run**, **
25
25
  ```markdown
26
26
  ---
27
27
  type: operations
28
+ description: "{at most 300 characters: what this document settles and where its boundary sits}"
28
29
  ---
29
30
 
30
31
  # Setup
@@ -26,6 +26,7 @@ Nothing else. The lint rules of the feature template apply. Sixty to a hundred a
26
26
  ```yaml
27
27
  ---
28
28
  type: system # required tooling key
29
+ description: "{at most 300 characters: what this document settles and where its boundary sits}" # required — the per-file account a folder is browsed by (references/frontmatter.md)
29
30
  status: active # required — draft = the machine as a whole does not run
30
31
  objects: [run, run ledger, settlement] # required — the objects this machine owns or moves
31
32
  owns: # transitional — region globs, one per subtree; read only by the ownership scan until it switches to header pointers (references/frontmatter.md)
@@ -204,7 +204,7 @@ function removeEmptyParents(projectDir, rel) {
204
204
  }
205
205
  }
206
206
 
207
- // Writes the manifest's mounted files that differ on disk, then deletes owned names the manifest omits.
207
+ // Fetches the manifest's mounted files that differ on disk, writes them deepest path first, then deletes owned names the manifest omits.
208
208
  async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetchImpl, timeoutMs, priorOwned }) {
209
209
  const all = mountedEntries(manifest);
210
210
  const entries = admittedEntries(projectDir, all);
@@ -216,14 +216,28 @@ async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetch
216
216
  const dest = join(projectDir, entry.path);
217
217
  return !(existsSync(dest) && sha256Of(readFileSync(dest)) === entry.sha256);
218
218
  });
219
+ // Booby trap: every live session in the checkout runs these hooks while the mount rewrites them.
220
+ // Writing as each fetch lands let `hooks/compliance.mjs` arrive seconds before a new `hooks/lib/`
221
+ // module it imports, and every hook call in that window died with ERR_MODULE_NOT_FOUND (3.20.1
222
+ // rollout, 2026-10-05). Fetch everything, then write locally, imports (deeper paths) before importers.
223
+ const fetched = [];
219
224
  await eachLimited(stale, async (entry) => {
220
225
  try {
221
- writeAtomic(join(projectDir, entry.path), await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }));
222
- written.push(entry.path);
226
+ fetched.push({ entry, bytes: await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }) });
223
227
  } catch {
224
228
  failed.push(entry.path);
225
229
  }
226
230
  });
231
+ const depth = (path) => path.split('/').length;
232
+ fetched.sort((a, b) => depth(b.entry.path) - depth(a.entry.path) || a.entry.path.localeCompare(b.entry.path));
233
+ for (const { entry, bytes } of fetched) {
234
+ try {
235
+ writeAtomic(join(projectDir, entry.path), bytes);
236
+ written.push(entry.path);
237
+ } catch {
238
+ failed.push(entry.path);
239
+ }
240
+ }
227
241
  for (const rel of prunableFiles(projectDir, unionOwned(manifest.owned, priorOwned))) {
228
242
  if (listed.has(rel)) continue;
229
243
  rmSync(join(projectDir, rel), { force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.30",
3
+ "version": "0.20.32",
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": "687486a21483"
49
+ "etnaFingerprint": "1b90a96b3812"
50
50
  }