@nurix/etna 0.19.29 → 0.19.31
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/bin/etna.js +53 -0
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/.prettierignore +12 -1
- package/data/harnesses/code/agents/maintenance.md +20 -13
- package/data/harnesses/code/codex/agents/maintenance.toml +20 -13
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/skills/boundary/SKILL.md +1 -1
- package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +29 -8
- package/data/harnesses/code/skills/documentation/templates/docs-readme.md +2 -0
- package/data/harnesses/discover/.prettierignore +12 -1
- package/data/harnesses/discover/harness.json +1 -1
- package/data/harnesses/discussion/.prettierignore +12 -1
- package/data/harnesses/discussion/harness.json +2 -2
- package/data/harnesses/etna/.prettierignore +12 -1
- package/data/harnesses/etna/CLAUDE.md +1 -1
- package/data/harnesses/etna/agents/maintenance.md +20 -13
- package/data/harnesses/etna/codex/agents/maintenance.toml +20 -13
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/boundary/SKILL.md +1 -1
- package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +29 -8
- package/data/harnesses/etna/skills/documentation/templates/docs-readme.md +2 -0
- package/data/harnesses/hr/.prettierignore +12 -1
- package/data/harnesses/hr/harness.json +1 -1
- package/data/harnesses/kb-agi/.prettierignore +12 -1
- package/data/harnesses/kb-agi/harness.json +1 -1
- package/package.json +1 -1
package/bin/etna.js
CHANGED
|
@@ -242,6 +242,49 @@ function pruneOrphanedComponents(targetDir, priorOwned, newOwned) {
|
|
|
242
242
|
return removed;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// Every file inside dir, as paths relative to dir.
|
|
246
|
+
function listFilesRecursive(dir, prefix = '') {
|
|
247
|
+
const out = [];
|
|
248
|
+
if (!existsSync(dir)) return out;
|
|
249
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
250
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
251
|
+
if (entry.isDirectory()) out.push(...listFilesRecursive(join(dir, entry.name), rel));
|
|
252
|
+
else out.push(rel);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Remove directories left empty by a prune, depth-first.
|
|
258
|
+
function removeEmptyDirs(dir) {
|
|
259
|
+
if (!existsSync(dir)) return;
|
|
260
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }))
|
|
261
|
+
if (entry.isDirectory()) removeEmptyDirs(join(dir, entry.name));
|
|
262
|
+
if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Remove files inside an installed component that this bundle no longer ships. copyHarnessSubdir
|
|
266
|
+
// merges, so a component that SURVIVES an upgrade while shedding a file keeps the stale copy
|
|
267
|
+
// forever — that is how a retired reference file goes on being read as live doctrine. Scoped to
|
|
268
|
+
// components the bundle still ships (a project's own components are never in that set, so they
|
|
269
|
+
// are never touched), and needs no prior manifest, so one refresh clears orphans left by any
|
|
270
|
+
// earlier version. Whole components that disappear are pruneOrphanedComponents' job.
|
|
271
|
+
function pruneStaleComponentFiles(bundledSubdir, projectSubdir) {
|
|
272
|
+
const removed = [];
|
|
273
|
+
if (!existsSync(bundledSubdir)) return removed;
|
|
274
|
+
for (const entry of readdirSync(bundledSubdir, { withFileTypes: true })) {
|
|
275
|
+
if (!entry.isDirectory()) continue;
|
|
276
|
+
const shipped = new Set(listFilesRecursive(join(bundledSubdir, entry.name)));
|
|
277
|
+
const installedDir = join(projectSubdir, entry.name);
|
|
278
|
+
for (const rel of listFilesRecursive(installedDir)) {
|
|
279
|
+
if (shipped.has(rel)) continue;
|
|
280
|
+
rmSync(join(installedDir, rel), { force: true });
|
|
281
|
+
removed.push(`${entry.name}/${rel}`);
|
|
282
|
+
}
|
|
283
|
+
removeEmptyDirs(installedDir);
|
|
284
|
+
}
|
|
285
|
+
return removed;
|
|
286
|
+
}
|
|
287
|
+
|
|
245
288
|
// True if an event's matcher-group already carries a hook with this command string.
|
|
246
289
|
function groupHasCommand(group, command) {
|
|
247
290
|
return (group.hooks || []).some((h) => h.command === command);
|
|
@@ -395,6 +438,16 @@ async function installHarness(harnessName, harnesses) {
|
|
|
395
438
|
const newOwned = bundledOwned(bundledHarnessDir);
|
|
396
439
|
for (const r of pruneOrphanedComponents(targetDir, priorOwned, newOwned)) notices.push(`removed ${r}`);
|
|
397
440
|
|
|
441
|
+
// Then prune stale files INSIDE components the bundle still ships — the case the
|
|
442
|
+
// name-level diff above cannot see, since the component itself is still present.
|
|
443
|
+
for (const sub of PRUNABLE_SUBDIRS) {
|
|
444
|
+
const stale = pruneStaleComponentFiles(
|
|
445
|
+
join(bundledHarnessDir, sub),
|
|
446
|
+
join(targetDir, '.claude', sub),
|
|
447
|
+
);
|
|
448
|
+
for (const r of stale) notices.push(`removed stale ${sub.replace(/s$/, '')} file ${r}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
398
451
|
// Dual-runtime: derive the repo's Codex artifacts (AGENTS.md, .agents/skills/,
|
|
399
452
|
// .codex/agents/, .codex/hooks.json) from the just-installed Claude tree + bundle, so the
|
|
400
453
|
// same project works under Codex too. Pure-derived — no MCP, no settings/config.toml
|
package/data/harness-index.json
CHANGED
|
@@ -8,8 +8,19 @@ pnpm-lock.yaml
|
|
|
8
8
|
package-lock.json
|
|
9
9
|
yarn.lock
|
|
10
10
|
|
|
11
|
-
# Harness-managed
|
|
11
|
+
# Harness-managed — the installer overwrites all of these, so formatting them is churn
|
|
12
|
+
# that comes straight back on the next `npx @nurix/etna`.
|
|
12
13
|
.claude
|
|
14
|
+
.codex
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE.md
|
|
17
|
+
AGENTS.md
|
|
18
|
+
|
|
19
|
+
# Frozen records — a plan, changelog entry, or migration runbook is a point-in-time
|
|
20
|
+
# artifact that is never edited after it freezes. Reformatting one is an edit, and the
|
|
21
|
+
# `maintenance` agent is forbidden from making it — so a format gate over this tree is
|
|
22
|
+
# a demand nothing in the repo is allowed to satisfy.
|
|
23
|
+
docs/history
|
|
13
24
|
|
|
14
25
|
# Source catalogs — authored markdown; prettier collapses template/example blocks
|
|
15
26
|
skills
|
|
@@ -27,7 +27,9 @@ Decide the mode once, up front, from the version marker — then run.
|
|
|
27
27
|
2. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter (a semver).
|
|
28
28
|
- **Absent** ⇒ the repo is unharnessed ⇒ **baseline mode** (reconcile from nothing), then audit.
|
|
29
29
|
- **Behind on major** ⇒ **baseline mode** (reconcile across the gap), then audit.
|
|
30
|
-
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** the target tree is missing
|
|
30
|
+
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/index.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
|
|
31
|
+
|
|
32
|
+
> **Never probe on a missing folder.** Git does not track empty directories, so an unused folder is absent on every clone — reading that as drift puts the repo in baseline mode *forever* and contradicts this agent's own idempotency guarantee. Only the five scaffolded files above are shape signals; every folder in the target tree is created on first use.
|
|
31
33
|
- **Equal to installed** ⇒ **audit only** (the default spine).
|
|
32
34
|
|
|
33
35
|
> **The version digit is a hint; the fold table is the authority.** A shape change ships in a minor whenever the capability behind it is additive — `docs/history/ledger/` dissolving into `docs/history/changelog/` arrived in a minor, as did `docs/history/migrations/`. Never conclude "minor ⇒ nothing to reshape" without running the probe: that reading silently strands dissolved-shape artifacts in the repo forever.
|
|
@@ -113,7 +115,7 @@ For unowned code paths:
|
|
|
113
115
|
|
|
114
116
|
### Step 6: Report & update tracking
|
|
115
117
|
|
|
116
|
-
1. Update `docs/structure.md` Health Log with today's date and summary.
|
|
118
|
+
1. Update `docs/structure.md` Health Log with today's date and summary — **append to the end**, never date-sorted into the middle. Rows are positional: one may say "the row above is…", and an inserted row silently breaks that reference.
|
|
117
119
|
2. Update `docs/memory/maintenance.md` with last review date, stale docs fixed, unowned paths, backlog items created.
|
|
118
120
|
3. Report findings: stale docs, broken ownership, unowned code, architecture drift, backlog health.
|
|
119
121
|
|
|
@@ -127,27 +129,31 @@ Baseline mode converges the repo's `docs/` to the **installed** harness shape, t
|
|
|
127
129
|
|
|
128
130
|
```
|
|
129
131
|
docs/
|
|
130
|
-
├── README.md
|
|
131
|
-
├── structure.md
|
|
132
|
+
├── README.md ✱ # Documentation root — quick links, section index
|
|
133
|
+
├── structure.md ✱ # Authority map + frontmatter `harness-version` (you own this field)
|
|
132
134
|
├── architecture/ # Cross-module design + steering layer
|
|
133
135
|
│ │ # domain-model, glossary, non-goals, references, open-questions
|
|
134
136
|
│ └── decisions/ # Architecture Decisions (ADs — one per subject, updated in place; journey in each AD's Decision Changelog)
|
|
135
137
|
├── features/ # Feature specs — the only first-class doc unit (flat; ownership in each doc's `owns:` frontmatter)
|
|
136
138
|
│ └── {group}/ # browse-only subfolder for grouping; NEVER a module; may carry a Readme.md
|
|
137
139
|
├── operations/ # Setup, development, deployment, CI/CD
|
|
138
|
-
├── research/ #
|
|
139
|
-
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill;
|
|
140
|
+
├── research/ # In-progress brainstorm sessions ({topic}-brainstorm/; legacy -brainstorming/) + standing research; Readme.md is the index
|
|
141
|
+
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill; moves to history/brainstorms/ on a passed harvest
|
|
140
142
|
├── history/
|
|
141
|
-
│ ├── CHANGELOG.md
|
|
142
|
-
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time
|
|
143
|
+
│ ├── CHANGELOG.md ✱ # Master changelog index
|
|
144
|
+
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time)
|
|
143
145
|
│ ├── plans/ # Implementation plans (Status field required)
|
|
144
|
-
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen
|
|
146
|
+
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen)
|
|
145
147
|
│ ├── brainstorms/ # Harvested brainstorm sessions ({topic}/) — user decides kept (committed) or deleted; Readme.md is the index
|
|
146
|
-
│
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
│ ├── research/ # Deep-research records ({topic}/ — README.md synthesis + findings.md + sources.md); frozen, cited, owns no code
|
|
149
|
+
│ └── backlog.md ✱ # Tracked work items by priority (P0-P3)
|
|
150
|
+
├── memory/
|
|
151
|
+
│ └── index.md ✱ # plus main.md, {agent}.md — project knowledge + per-agent strategy memory
|
|
152
|
+
└── tests/ # Behavioral test checklists ({feature}.md, step → **Assert**), derived at plan Completion via the `documentation` skill; author-maintained
|
|
149
153
|
```
|
|
150
154
|
|
|
155
|
+
**✱ marks the five scaffolded files — the only paths a baseline creates, and the only shape signals.** Every **folder** in this tree is created on first use and is simply absent until it holds a file; git cannot track an empty directory, so a missing folder is never evidence of drift and is never scaffolded to satisfy the shape. Record its role in `docs/structure.md` instead.
|
|
156
|
+
|
|
151
157
|
There is **no `docs/routing.md`**, **no `docs/modules/{m}/`**, **no `docs/pillars/`**, **no `docs/boundaries/`**. Code ownership lives in feature `owns:` frontmatter; a module is a `module:` label, not a folder; boundaries are diagnosed from source; brainstorm records live in `docs/research/` (active) and `docs/history/brainstorms/` (harvested).
|
|
152
158
|
|
|
153
159
|
> Brainstorm records are **author-created, never scaffolded** — the `brainstorm` skill writes an active session at `docs/research/{topic}-brainstorm/` (legacy folders carry the older `-brainstorming/` suffix; treat both as canonical) and moves it to `docs/history/brainstorms/{topic}/` on a **passed harvest** — the move is the completion marker, and the moved folder's fate is the **user's decision: delete or commit**. Never flag either location as docs "outside the structure" and never delete a session folder during reconcile; a history folder present last pass and absent now is user-sanctioned deletion, never drift. Each folder carries a `Readme.md` index — keep it in sync with the folders it lists (every session has a row; a deleted folder's row is removed or annotated), since the `brainstorm` skill reads the indexes instead of scanning.
|
|
@@ -173,7 +179,8 @@ This is what makes baseline more than a fresh scaffold. Detect old-shape artifac
|
|
|
173
179
|
| `docs/history/ledger/YYYY-MM.md` (the Refinement Ledger, now merged into the changelog) | split each `### ` block into its own `docs/history/changelog/YYYY-MM-DD-{three-words}.md` entry — content preserved **verbatim**, heading promoted to `#`, three words naming the work; append one row per entry to `docs/history/CHANGELOG.md`; delete the `docs/history/ledger/` folder. Records are moved, never rewritten |
|
|
174
180
|
| `docs/operations/guardrails.md` | fold durable contract notes into `docs/architecture/` |
|
|
175
181
|
| `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
|
|
176
|
-
|
|
|
182
|
+
| `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
|
|
183
|
+
| a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
|
|
177
184
|
| no `harness-version` in `structure.md` | stamp it (Step B4) |
|
|
178
185
|
|
|
179
186
|
### Step B3 — Present the reconcile plan, then execute
|
|
@@ -25,7 +25,9 @@ Decide the mode once, up front, from the version marker — then run.
|
|
|
25
25
|
2. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter (a semver).
|
|
26
26
|
- **Absent** ⇒ the repo is unharnessed ⇒ **baseline mode** (reconcile from nothing), then audit.
|
|
27
27
|
- **Behind on major** ⇒ **baseline mode** (reconcile across the gap), then audit.
|
|
28
|
-
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** the target tree is missing
|
|
28
|
+
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/index.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
|
|
29
|
+
|
|
30
|
+
> **Never probe on a missing folder.** Git does not track empty directories, so an unused folder is absent on every clone — reading that as drift puts the repo in baseline mode *forever* and contradicts this agent's own idempotency guarantee. Only the five scaffolded files above are shape signals; every folder in the target tree is created on first use.
|
|
29
31
|
- **Equal to installed** ⇒ **audit only** (the default spine).
|
|
30
32
|
|
|
31
33
|
> **The version digit is a hint; the fold table is the authority.** A shape change ships in a minor whenever the capability behind it is additive — `docs/history/ledger/` dissolving into `docs/history/changelog/` arrived in a minor, as did `docs/history/migrations/`. Never conclude "minor ⇒ nothing to reshape" without running the probe: that reading silently strands dissolved-shape artifacts in the repo forever.
|
|
@@ -111,7 +113,7 @@ For unowned code paths:
|
|
|
111
113
|
|
|
112
114
|
### Step 6: Report & update tracking
|
|
113
115
|
|
|
114
|
-
1. Update `docs/structure.md` Health Log with today's date and summary.
|
|
116
|
+
1. Update `docs/structure.md` Health Log with today's date and summary — **append to the end**, never date-sorted into the middle. Rows are positional: one may say "the row above is…", and an inserted row silently breaks that reference.
|
|
115
117
|
2. Update `docs/memory/maintenance.md` with last review date, stale docs fixed, unowned paths, backlog items created.
|
|
116
118
|
3. Report findings: stale docs, broken ownership, unowned code, architecture drift, backlog health.
|
|
117
119
|
|
|
@@ -125,27 +127,31 @@ Baseline mode converges the repo's `docs/` to the **installed** harness shape, t
|
|
|
125
127
|
|
|
126
128
|
```
|
|
127
129
|
docs/
|
|
128
|
-
├── README.md
|
|
129
|
-
├── structure.md
|
|
130
|
+
├── README.md ✱ # Documentation root — quick links, section index
|
|
131
|
+
├── structure.md ✱ # Authority map + frontmatter `harness-version` (you own this field)
|
|
130
132
|
├── architecture/ # Cross-module design + steering layer
|
|
131
133
|
│ │ # domain-model, glossary, non-goals, references, open-questions
|
|
132
134
|
│ └── decisions/ # Architecture Decisions (ADs — one per subject, updated in place; journey in each AD's Decision Changelog)
|
|
133
135
|
├── features/ # Feature specs — the only first-class doc unit (flat; ownership in each doc's `owns:` frontmatter)
|
|
134
136
|
│ └── {group}/ # browse-only subfolder for grouping; NEVER a module; may carry a Readme.md
|
|
135
137
|
├── operations/ # Setup, development, deployment, CI/CD
|
|
136
|
-
├── research/ #
|
|
137
|
-
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill;
|
|
138
|
+
├── research/ # In-progress brainstorm sessions ({topic}-brainstorm/; legacy -brainstorming/) + standing research; Readme.md is the index
|
|
139
|
+
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill; moves to history/brainstorms/ on a passed harvest
|
|
138
140
|
├── history/
|
|
139
|
-
│ ├── CHANGELOG.md
|
|
140
|
-
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time
|
|
141
|
+
│ ├── CHANGELOG.md ✱ # Master changelog index
|
|
142
|
+
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time)
|
|
141
143
|
│ ├── plans/ # Implementation plans (Status field required)
|
|
142
|
-
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen
|
|
144
|
+
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen)
|
|
143
145
|
│ ├── brainstorms/ # Harvested brainstorm sessions ({topic}/) — user decides kept (committed) or deleted; Readme.md is the index
|
|
144
|
-
│
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
│ ├── research/ # Deep-research records ({topic}/ — README.md synthesis + findings.md + sources.md); frozen, cited, owns no code
|
|
147
|
+
│ └── backlog.md ✱ # Tracked work items by priority (P0-P3)
|
|
148
|
+
├── memory/
|
|
149
|
+
│ └── index.md ✱ # plus main.md, {agent}.md — project knowledge + per-agent strategy memory
|
|
150
|
+
└── tests/ # Behavioral test checklists ({feature}.md, step → **Assert**), derived at plan Completion via the `documentation` skill; author-maintained
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
**✱ marks the five scaffolded files — the only paths a baseline creates, and the only shape signals.** Every **folder** in this tree is created on first use and is simply absent until it holds a file; git cannot track an empty directory, so a missing folder is never evidence of drift and is never scaffolded to satisfy the shape. Record its role in `docs/structure.md` instead.
|
|
154
|
+
|
|
149
155
|
There is **no `docs/routing.md`**, **no `docs/modules/{m}/`**, **no `docs/pillars/`**, **no `docs/boundaries/`**. Code ownership lives in feature `owns:` frontmatter; a module is a `module:` label, not a folder; boundaries are diagnosed from source; brainstorm records live in `docs/research/` (active) and `docs/history/brainstorms/` (harvested).
|
|
150
156
|
|
|
151
157
|
> Brainstorm records are **author-created, never scaffolded** — the `brainstorm` skill writes an active session at `docs/research/{topic}-brainstorm/` (legacy folders carry the older `-brainstorming/` suffix; treat both as canonical) and moves it to `docs/history/brainstorms/{topic}/` on a **passed harvest** — the move is the completion marker, and the moved folder's fate is the **user's decision: delete or commit**. Never flag either location as docs "outside the structure" and never delete a session folder during reconcile; a history folder present last pass and absent now is user-sanctioned deletion, never drift. Each folder carries a `Readme.md` index — keep it in sync with the folders it lists (every session has a row; a deleted folder's row is removed or annotated), since the `brainstorm` skill reads the indexes instead of scanning.
|
|
@@ -171,7 +177,8 @@ This is what makes baseline more than a fresh scaffold. Detect old-shape artifac
|
|
|
171
177
|
| `docs/history/ledger/YYYY-MM.md` (the Refinement Ledger, now merged into the changelog) | split each `### ` block into its own `docs/history/changelog/YYYY-MM-DD-{three-words}.md` entry — content preserved **verbatim**, heading promoted to `#`, three words naming the work; append one row per entry to `docs/history/CHANGELOG.md`; delete the `docs/history/ledger/` folder. Records are moved, never rewritten |
|
|
172
178
|
| `docs/operations/guardrails.md` | fold durable contract notes into `docs/architecture/` |
|
|
173
179
|
| `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
|
|
174
|
-
|
|
|
180
|
+
| `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
|
|
181
|
+
| a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
|
|
175
182
|
| no `harness-version` in `structure.md` | stamp it (Step B4) |
|
|
176
183
|
|
|
177
184
|
### Step B3 — Present the reconcile plan, then execute
|
|
@@ -17,7 +17,7 @@ Three layers tell you what to look for; the code tells you what exists.
|
|
|
17
17
|
|
|
18
18
|
- **The mechanisms** — [`references/mechanisms.md`](references/mechanisms.md) defines the seven failure mechanisms every boundary instantiates (rendezvous-string, serialized-shape, generated-artifact, dual-authority, config-elsewhere, trust-invariant, lifecycle-protocol), each with its trace strategy, universal safe-change pattern, and severity heuristic, plus the two stack-independent domain contracts (entity-model, tenancy). Load it on every check.
|
|
19
19
|
- **The technology packs** — [`references/index.md`](references/index.md) routes to the packs under `references/packs/` by the signals in the touched code. Each pack carries the non-derivable payload for one technology area: exact detect patterns, expensive-to-learn gotchas, and safe-change sequences. Load **only the packs the touch list exhibits**; a technology no pack covers is still in scope — diagnose it from its mechanism.
|
|
20
|
-
- **Repo doctrine** — `.claude/rules/`, `docs/architecture/` (including `
|
|
20
|
+
- **Repo doctrine** — `.claude/rules/`, `docs/architecture/` (including `decisions/`), and any contracts package name the repo's bespoke contracts (a documented response envelope, a schema-parity rule, an entity model). A documented contract is a real side of a boundary: a plan that contradicts one is a finding **even when no code consumer exists yet** — this is how greenfield plans, whose modules have no code to trace, still get checked.
|
|
21
21
|
|
|
22
22
|
The packs are a floor, not a fence: report any contract that passes the ships-green litmus, pack entry or not.
|
|
23
23
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Usage: node doc-lint.mjs <file.md ...> | node doc-lint.mjs --changed (git-changed + untracked .md)
|
|
3
3
|
// Exit 1 when any block-severity finding survives. Escape hatch: <!-- doc-lint-disable: id[, id] --> (file-wide, or on the offending line).
|
|
4
4
|
|
|
5
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
|
|
8
8
|
const RULES = [
|
|
@@ -10,10 +10,14 @@ const RULES = [
|
|
|
10
10
|
{ id: 'history-narration', severity: 'block', scope: 'truth', pattern: /\bno longer\b|\bwas (renamed|removed|extracted|folded|migrated|replaced)\b|\bmoved (to|from|into) [\w`./-]+ \(?20\d{2}/i, message: 'history narration — the journey belongs in a dated record' },
|
|
11
11
|
{ id: 'progress-marker', severity: 'block', scope: 'truth', pattern: /\bphases?\s+\d[^.\n]{0,40}\b(complete|done)\b|\b(build|implementation) (is )?(\d+%|partially) (complete|done)\b/i, message: 'progress marker — progress lives in the plan/backlog record' },
|
|
12
12
|
{ id: 'temporal-hedge', severity: 'warn', scope: 'truth', pattern: /\bnot yet\b|\bfor now\b|\bcurrently\b|\bstill (unsupported|missing|pending|unimplemented|in progress)\b/i, message: 'temporal hedge — state the fact without "currently / not yet / for now"' },
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// "previously-fetched" is a hyphenated adjective naming a thing, not a predecessor design — don't fire on it.
|
|
14
|
+
{ id: 'predecessor-frame', severity: 'warn', scope: 'truth', pattern: /\bpreviously\b(?!-)|\bthe old \w+\b|\blegacy \w+ (was|is)\b/i, message: 'predecessor framing — describe the one current design' },
|
|
15
|
+
// docs/tests/ behavioral checklists use `- [ ]` as test-step notation (step → **Assert**) and docs/operations/
|
|
16
|
+
// procedures use it for operator verification steps — neither is progress tracking. Every other truth doc still bans checkboxes.
|
|
17
|
+
{ id: 'status-glyph', severity: 'warn', scope: 'truth', exempt: /(^|\/)docs\/(tests|operations)\//, pattern: /(^|\s)(✅|⏳|- \[[ x]\])/u, message: 'status glyph / checklist in a truth doc — tracking belongs in a record' },
|
|
18
|
+
// Truth docs are supposed to cite records, and record filenames are dated — so a date inside a link
|
|
19
|
+
// target or a code span is a path, not narration. Strip those before matching.
|
|
20
|
+
{ id: 'date-in-truth-doc', severity: 'warn', scope: 'truth', strip: /\[[^\]]*\]\([^)]*\)|`[^`]*`/g, pattern: /\b20\d{2}-\d{2}-\d{2}\b/, message: 'date in a truth doc — only the AD Decision Changelog and structure.md Health Log tails carry dates' },
|
|
17
21
|
];
|
|
18
22
|
|
|
19
23
|
// Sections inside a truth doc where dated history is sanctioned (the two bridges).
|
|
@@ -74,13 +78,16 @@ function lintFile(path, findings) {
|
|
|
74
78
|
if (rule.scope !== scope || disabled.has(rule.id)) continue;
|
|
75
79
|
if (rule.exempt && rule.exempt.test(path)) continue;
|
|
76
80
|
for (const { n, line } of lines) {
|
|
77
|
-
|
|
81
|
+
const subject = rule.strip ? line.replace(rule.strip, '') : line;
|
|
82
|
+
if (!rule.pattern.test(subject)) continue;
|
|
78
83
|
if (line.includes(`doc-lint-disable: ${rule.id}`) || new RegExp(`doc-lint-disable:[^>]*\\b${rule.id}\\b`).test(line)) continue;
|
|
79
84
|
findings.push({ path, n, severity: rule.severity, id: rule.id, message: rule.message, excerpt: line.trim().slice(0, 90) });
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
// A folder index (Readme.md/README.md) is a browse page, not a feature doc — it owns no code.
|
|
89
|
+
const isFolderIndex = /(^|\/)(readme)\.md$/i.test(path);
|
|
90
|
+
if (scope === 'truth' && !isFolderIndex && /(^|\/)docs\/features\/[^/]+\.md$/.test(path)) {
|
|
84
91
|
const fm = text.startsWith('---') ? text.slice(0, text.indexOf('\n---', 3)) : '';
|
|
85
92
|
for (const key of ['owns:', 'keywords:', 'status:'])
|
|
86
93
|
if (!fm.includes(key)) findings.push({ path, n: 1, severity: 'block', id: 'feature-frontmatter', message: `feature doc missing \`${key}\` frontmatter` });
|
|
@@ -106,7 +113,21 @@ if (changedMode) {
|
|
|
106
113
|
const out = execSync('git diff --name-only HEAD -- "*.md"; git ls-files --others --exclude-standard -- "*.md"', { encoding: 'utf8', shell: '/bin/bash' });
|
|
107
114
|
files = [...new Set([...files, ...out.split('\n').filter(Boolean)])];
|
|
108
115
|
}
|
|
109
|
-
|
|
116
|
+
// Expand any directory argument into the .md files beneath it, so `doc-lint.mjs docs/` works.
|
|
117
|
+
function expandDirs(paths) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const p of paths) {
|
|
120
|
+
if (!existsSync(p)) { out.push(p); continue; }
|
|
121
|
+
if (!statSync(p).isDirectory()) { out.push(p); continue; }
|
|
122
|
+
for (const e of readdirSync(p, { withFileTypes: true })) {
|
|
123
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
124
|
+
out.push(...expandDirs([`${p.replace(/\/$/, '')}/${e.name}`]));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
files = expandDirs(files).filter((f) => f.endsWith('.md'));
|
|
130
|
+
if (!files.length) { console.error('usage: node doc-lint.mjs <file.md | dir ...> | --changed'); process.exit(2); }
|
|
110
131
|
|
|
111
132
|
const findings = [];
|
|
112
133
|
files.forEach((f) => lintFile(f, findings));
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Truth document. Use for: the entry point for all documentation in a repo. Created during baseline audit.
|
|
4
4
|
|
|
5
|
+
**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.
|
|
6
|
+
|
|
5
7
|
```
|
|
6
8
|
# {Project Name} Documentation
|
|
7
9
|
|
|
@@ -8,8 +8,19 @@ pnpm-lock.yaml
|
|
|
8
8
|
package-lock.json
|
|
9
9
|
yarn.lock
|
|
10
10
|
|
|
11
|
-
# Harness-managed
|
|
11
|
+
# Harness-managed — the installer overwrites all of these, so formatting them is churn
|
|
12
|
+
# that comes straight back on the next `npx @nurix/etna`.
|
|
12
13
|
.claude
|
|
14
|
+
.codex
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE.md
|
|
17
|
+
AGENTS.md
|
|
18
|
+
|
|
19
|
+
# Frozen records — a plan, changelog entry, or migration runbook is a point-in-time
|
|
20
|
+
# artifact that is never edited after it freezes. Reformatting one is an edit, and the
|
|
21
|
+
# `maintenance` agent is forbidden from making it — so a format gate over this tree is
|
|
22
|
+
# a demand nothing in the repo is allowed to satisfy.
|
|
23
|
+
docs/history
|
|
13
24
|
|
|
14
25
|
# Source catalogs — authored markdown; prettier collapses template/example blocks
|
|
15
26
|
skills
|
|
@@ -8,8 +8,19 @@ pnpm-lock.yaml
|
|
|
8
8
|
package-lock.json
|
|
9
9
|
yarn.lock
|
|
10
10
|
|
|
11
|
-
# Harness-managed
|
|
11
|
+
# Harness-managed — the installer overwrites all of these, so formatting them is churn
|
|
12
|
+
# that comes straight back on the next `npx @nurix/etna`.
|
|
12
13
|
.claude
|
|
14
|
+
.codex
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE.md
|
|
17
|
+
AGENTS.md
|
|
18
|
+
|
|
19
|
+
# Frozen records — a plan, changelog entry, or migration runbook is a point-in-time
|
|
20
|
+
# artifact that is never edited after it freezes. Reformatting one is an edit, and the
|
|
21
|
+
# `maintenance` agent is forbidden from making it — so a format gate over this tree is
|
|
22
|
+
# a demand nothing in the repo is allowed to satisfy.
|
|
23
|
+
docs/history
|
|
13
24
|
|
|
14
25
|
# Source catalogs — authored markdown; prettier collapses template/example blocks
|
|
15
26
|
skills
|
|
@@ -8,8 +8,19 @@ pnpm-lock.yaml
|
|
|
8
8
|
package-lock.json
|
|
9
9
|
yarn.lock
|
|
10
10
|
|
|
11
|
-
# Harness-managed
|
|
11
|
+
# Harness-managed — the installer overwrites all of these, so formatting them is churn
|
|
12
|
+
# that comes straight back on the next `npx @nurix/etna`.
|
|
12
13
|
.claude
|
|
14
|
+
.codex
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE.md
|
|
17
|
+
AGENTS.md
|
|
18
|
+
|
|
19
|
+
# Frozen records — a plan, changelog entry, or migration runbook is a point-in-time
|
|
20
|
+
# artifact that is never edited after it freezes. Reformatting one is an edit, and the
|
|
21
|
+
# `maintenance` agent is forbidden from making it — so a format gate over this tree is
|
|
22
|
+
# a demand nothing in the repo is allowed to satisfy.
|
|
23
|
+
docs/history
|
|
13
24
|
|
|
14
25
|
# Source catalogs — authored markdown; prettier collapses template/example blocks
|
|
15
26
|
skills
|
|
@@ -44,7 +44,7 @@ etna authors the components it also runs and ships, so **three copies of every c
|
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| **Source** (canonical) | the shared catalogs — `skills/`, `agents/`, `rules/`, plus `harness/*/` (each harness — its `readme.md` manifest, `CLAUDE.md` policy, and owned trees in one folder) | Edit here. Etna is the source of truth. |
|
|
46
46
|
| **Dogfood** | `.claude/skills/`, `.claude/agents/`, `.claude/rules/`, `.claude/hooks/` (git-tracked) | The subset the `etna` harness installs on itself — the copy this repo actually runs this session. |
|
|
47
|
-
| **Shipped bundle** | `packages/cli/data/harnesses/*/` (
|
|
47
|
+
| **Shipped bundle** | `packages/cli/data/harnesses/*/` (generated by `packages/cli/scripts/build-data.js`; gitignored except each harness `CLAUDE.md`, and rebuilt by `prepack` on every publish) | Generated — never hand-edit. |
|
|
48
48
|
|
|
49
49
|
- **Keep source and dogfood in sync.** When a dogfooded component's source changes, mirror the edit into its `.claude/` copy in the same change — otherwise the skill this session runs diverges from the authored one.
|
|
50
50
|
- **A component edit ships.** On push to `dev`, CI fingerprints the payload, patch-bumps, and publishes to the marketplace and `@nurix/etna` npm — reaching every consumer repo. Weigh catalog edits with that blast radius in mind. The pipeline authority is **[`docs/architecture/distribution.md`](docs/architecture/distribution.md)** — link, don't restate.
|
|
@@ -27,7 +27,9 @@ Decide the mode once, up front, from the version marker — then run.
|
|
|
27
27
|
2. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter (a semver).
|
|
28
28
|
- **Absent** ⇒ the repo is unharnessed ⇒ **baseline mode** (reconcile from nothing), then audit.
|
|
29
29
|
- **Behind on major** ⇒ **baseline mode** (reconcile across the gap), then audit.
|
|
30
|
-
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** the target tree is missing
|
|
30
|
+
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/index.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
|
|
31
|
+
|
|
32
|
+
> **Never probe on a missing folder.** Git does not track empty directories, so an unused folder is absent on every clone — reading that as drift puts the repo in baseline mode *forever* and contradicts this agent's own idempotency guarantee. Only the five scaffolded files above are shape signals; every folder in the target tree is created on first use.
|
|
31
33
|
- **Equal to installed** ⇒ **audit only** (the default spine).
|
|
32
34
|
|
|
33
35
|
> **The version digit is a hint; the fold table is the authority.** A shape change ships in a minor whenever the capability behind it is additive — `docs/history/ledger/` dissolving into `docs/history/changelog/` arrived in a minor, as did `docs/history/migrations/`. Never conclude "minor ⇒ nothing to reshape" without running the probe: that reading silently strands dissolved-shape artifacts in the repo forever.
|
|
@@ -113,7 +115,7 @@ For unowned code paths:
|
|
|
113
115
|
|
|
114
116
|
### Step 6: Report & update tracking
|
|
115
117
|
|
|
116
|
-
1. Update `docs/structure.md` Health Log with today's date and summary.
|
|
118
|
+
1. Update `docs/structure.md` Health Log with today's date and summary — **append to the end**, never date-sorted into the middle. Rows are positional: one may say "the row above is…", and an inserted row silently breaks that reference.
|
|
117
119
|
2. Update `docs/memory/maintenance.md` with last review date, stale docs fixed, unowned paths, backlog items created.
|
|
118
120
|
3. Report findings: stale docs, broken ownership, unowned code, architecture drift, backlog health.
|
|
119
121
|
|
|
@@ -127,27 +129,31 @@ Baseline mode converges the repo's `docs/` to the **installed** harness shape, t
|
|
|
127
129
|
|
|
128
130
|
```
|
|
129
131
|
docs/
|
|
130
|
-
├── README.md
|
|
131
|
-
├── structure.md
|
|
132
|
+
├── README.md ✱ # Documentation root — quick links, section index
|
|
133
|
+
├── structure.md ✱ # Authority map + frontmatter `harness-version` (you own this field)
|
|
132
134
|
├── architecture/ # Cross-module design + steering layer
|
|
133
135
|
│ │ # domain-model, glossary, non-goals, references, open-questions
|
|
134
136
|
│ └── decisions/ # Architecture Decisions (ADs — one per subject, updated in place; journey in each AD's Decision Changelog)
|
|
135
137
|
├── features/ # Feature specs — the only first-class doc unit (flat; ownership in each doc's `owns:` frontmatter)
|
|
136
138
|
│ └── {group}/ # browse-only subfolder for grouping; NEVER a module; may carry a Readme.md
|
|
137
139
|
├── operations/ # Setup, development, deployment, CI/CD
|
|
138
|
-
├── research/ #
|
|
139
|
-
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill;
|
|
140
|
+
├── research/ # In-progress brainstorm sessions ({topic}-brainstorm/; legacy -brainstorming/) + standing research; Readme.md is the index
|
|
141
|
+
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill; moves to history/brainstorms/ on a passed harvest
|
|
140
142
|
├── history/
|
|
141
|
-
│ ├── CHANGELOG.md
|
|
142
|
-
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time
|
|
143
|
+
│ ├── CHANGELOG.md ✱ # Master changelog index
|
|
144
|
+
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time)
|
|
143
145
|
│ ├── plans/ # Implementation plans (Status field required)
|
|
144
|
-
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen
|
|
146
|
+
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen)
|
|
145
147
|
│ ├── brainstorms/ # Harvested brainstorm sessions ({topic}/) — user decides kept (committed) or deleted; Readme.md is the index
|
|
146
|
-
│
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
│ ├── research/ # Deep-research records ({topic}/ — README.md synthesis + findings.md + sources.md); frozen, cited, owns no code
|
|
149
|
+
│ └── backlog.md ✱ # Tracked work items by priority (P0-P3)
|
|
150
|
+
├── memory/
|
|
151
|
+
│ └── index.md ✱ # plus main.md, {agent}.md — project knowledge + per-agent strategy memory
|
|
152
|
+
└── tests/ # Behavioral test checklists ({feature}.md, step → **Assert**), derived at plan Completion via the `documentation` skill; author-maintained
|
|
149
153
|
```
|
|
150
154
|
|
|
155
|
+
**✱ marks the five scaffolded files — the only paths a baseline creates, and the only shape signals.** Every **folder** in this tree is created on first use and is simply absent until it holds a file; git cannot track an empty directory, so a missing folder is never evidence of drift and is never scaffolded to satisfy the shape. Record its role in `docs/structure.md` instead.
|
|
156
|
+
|
|
151
157
|
There is **no `docs/routing.md`**, **no `docs/modules/{m}/`**, **no `docs/pillars/`**, **no `docs/boundaries/`**. Code ownership lives in feature `owns:` frontmatter; a module is a `module:` label, not a folder; boundaries are diagnosed from source; brainstorm records live in `docs/research/` (active) and `docs/history/brainstorms/` (harvested).
|
|
152
158
|
|
|
153
159
|
> Brainstorm records are **author-created, never scaffolded** — the `brainstorm` skill writes an active session at `docs/research/{topic}-brainstorm/` (legacy folders carry the older `-brainstorming/` suffix; treat both as canonical) and moves it to `docs/history/brainstorms/{topic}/` on a **passed harvest** — the move is the completion marker, and the moved folder's fate is the **user's decision: delete or commit**. Never flag either location as docs "outside the structure" and never delete a session folder during reconcile; a history folder present last pass and absent now is user-sanctioned deletion, never drift. Each folder carries a `Readme.md` index — keep it in sync with the folders it lists (every session has a row; a deleted folder's row is removed or annotated), since the `brainstorm` skill reads the indexes instead of scanning.
|
|
@@ -173,7 +179,8 @@ This is what makes baseline more than a fresh scaffold. Detect old-shape artifac
|
|
|
173
179
|
| `docs/history/ledger/YYYY-MM.md` (the Refinement Ledger, now merged into the changelog) | split each `### ` block into its own `docs/history/changelog/YYYY-MM-DD-{three-words}.md` entry — content preserved **verbatim**, heading promoted to `#`, three words naming the work; append one row per entry to `docs/history/CHANGELOG.md`; delete the `docs/history/ledger/` folder. Records are moved, never rewritten |
|
|
174
180
|
| `docs/operations/guardrails.md` | fold durable contract notes into `docs/architecture/` |
|
|
175
181
|
| `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
|
|
176
|
-
|
|
|
182
|
+
| `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
|
|
183
|
+
| a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
|
|
177
184
|
| no `harness-version` in `structure.md` | stamp it (Step B4) |
|
|
178
185
|
|
|
179
186
|
### Step B3 — Present the reconcile plan, then execute
|
|
@@ -25,7 +25,9 @@ Decide the mode once, up front, from the version marker — then run.
|
|
|
25
25
|
2. Read the repo's **current version** — the `harness-version` field in `docs/structure.md` frontmatter (a semver).
|
|
26
26
|
- **Absent** ⇒ the repo is unharnessed ⇒ **baseline mode** (reconcile from nothing), then audit.
|
|
27
27
|
- **Behind on major** ⇒ **baseline mode** (reconcile across the gap), then audit.
|
|
28
|
-
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** the target tree is missing
|
|
28
|
+
- **Behind on minor/patch** ⇒ **probe before deciding.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/index.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
|
|
29
|
+
|
|
30
|
+
> **Never probe on a missing folder.** Git does not track empty directories, so an unused folder is absent on every clone — reading that as drift puts the repo in baseline mode *forever* and contradicts this agent's own idempotency guarantee. Only the five scaffolded files above are shape signals; every folder in the target tree is created on first use.
|
|
29
31
|
- **Equal to installed** ⇒ **audit only** (the default spine).
|
|
30
32
|
|
|
31
33
|
> **The version digit is a hint; the fold table is the authority.** A shape change ships in a minor whenever the capability behind it is additive — `docs/history/ledger/` dissolving into `docs/history/changelog/` arrived in a minor, as did `docs/history/migrations/`. Never conclude "minor ⇒ nothing to reshape" without running the probe: that reading silently strands dissolved-shape artifacts in the repo forever.
|
|
@@ -111,7 +113,7 @@ For unowned code paths:
|
|
|
111
113
|
|
|
112
114
|
### Step 6: Report & update tracking
|
|
113
115
|
|
|
114
|
-
1. Update `docs/structure.md` Health Log with today's date and summary.
|
|
116
|
+
1. Update `docs/structure.md` Health Log with today's date and summary — **append to the end**, never date-sorted into the middle. Rows are positional: one may say "the row above is…", and an inserted row silently breaks that reference.
|
|
115
117
|
2. Update `docs/memory/maintenance.md` with last review date, stale docs fixed, unowned paths, backlog items created.
|
|
116
118
|
3. Report findings: stale docs, broken ownership, unowned code, architecture drift, backlog health.
|
|
117
119
|
|
|
@@ -125,27 +127,31 @@ Baseline mode converges the repo's `docs/` to the **installed** harness shape, t
|
|
|
125
127
|
|
|
126
128
|
```
|
|
127
129
|
docs/
|
|
128
|
-
├── README.md
|
|
129
|
-
├── structure.md
|
|
130
|
+
├── README.md ✱ # Documentation root — quick links, section index
|
|
131
|
+
├── structure.md ✱ # Authority map + frontmatter `harness-version` (you own this field)
|
|
130
132
|
├── architecture/ # Cross-module design + steering layer
|
|
131
133
|
│ │ # domain-model, glossary, non-goals, references, open-questions
|
|
132
134
|
│ └── decisions/ # Architecture Decisions (ADs — one per subject, updated in place; journey in each AD's Decision Changelog)
|
|
133
135
|
├── features/ # Feature specs — the only first-class doc unit (flat; ownership in each doc's `owns:` frontmatter)
|
|
134
136
|
│ └── {group}/ # browse-only subfolder for grouping; NEVER a module; may carry a Readme.md
|
|
135
137
|
├── operations/ # Setup, development, deployment, CI/CD
|
|
136
|
-
├── research/ #
|
|
137
|
-
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill;
|
|
138
|
+
├── research/ # In-progress brainstorm sessions ({topic}-brainstorm/; legacy -brainstorming/) + standing research; Readme.md is the index
|
|
139
|
+
│ └── {topic}-brainstorm/ # created by the `brainstorm` skill; moves to history/brainstorms/ on a passed harvest
|
|
138
140
|
├── history/
|
|
139
|
-
│ ├── CHANGELOG.md
|
|
140
|
-
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time
|
|
141
|
+
│ ├── CHANGELOG.md ✱ # Master changelog index
|
|
142
|
+
│ ├── changelog/ # Individual entries (YYYY-MM-DD-{three-words}.md; written at commit time)
|
|
141
143
|
│ ├── plans/ # Implementation plans (Status field required)
|
|
142
|
-
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen
|
|
144
|
+
│ ├── migrations/ # One-shot migration/upgrade runbooks (YYYY-MM-DD-{slug}.md; outcome-stamped then frozen)
|
|
143
145
|
│ ├── brainstorms/ # Harvested brainstorm sessions ({topic}/) — user decides kept (committed) or deleted; Readme.md is the index
|
|
144
|
-
│
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
│ ├── research/ # Deep-research records ({topic}/ — README.md synthesis + findings.md + sources.md); frozen, cited, owns no code
|
|
147
|
+
│ └── backlog.md ✱ # Tracked work items by priority (P0-P3)
|
|
148
|
+
├── memory/
|
|
149
|
+
│ └── index.md ✱ # plus main.md, {agent}.md — project knowledge + per-agent strategy memory
|
|
150
|
+
└── tests/ # Behavioral test checklists ({feature}.md, step → **Assert**), derived at plan Completion via the `documentation` skill; author-maintained
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
**✱ marks the five scaffolded files — the only paths a baseline creates, and the only shape signals.** Every **folder** in this tree is created on first use and is simply absent until it holds a file; git cannot track an empty directory, so a missing folder is never evidence of drift and is never scaffolded to satisfy the shape. Record its role in `docs/structure.md` instead.
|
|
154
|
+
|
|
149
155
|
There is **no `docs/routing.md`**, **no `docs/modules/{m}/`**, **no `docs/pillars/`**, **no `docs/boundaries/`**. Code ownership lives in feature `owns:` frontmatter; a module is a `module:` label, not a folder; boundaries are diagnosed from source; brainstorm records live in `docs/research/` (active) and `docs/history/brainstorms/` (harvested).
|
|
150
156
|
|
|
151
157
|
> Brainstorm records are **author-created, never scaffolded** — the `brainstorm` skill writes an active session at `docs/research/{topic}-brainstorm/` (legacy folders carry the older `-brainstorming/` suffix; treat both as canonical) and moves it to `docs/history/brainstorms/{topic}/` on a **passed harvest** — the move is the completion marker, and the moved folder's fate is the **user's decision: delete or commit**. Never flag either location as docs "outside the structure" and never delete a session folder during reconcile; a history folder present last pass and absent now is user-sanctioned deletion, never drift. Each folder carries a `Readme.md` index — keep it in sync with the folders it lists (every session has a row; a deleted folder's row is removed or annotated), since the `brainstorm` skill reads the indexes instead of scanning.
|
|
@@ -171,7 +177,8 @@ This is what makes baseline more than a fresh scaffold. Detect old-shape artifac
|
|
|
171
177
|
| `docs/history/ledger/YYYY-MM.md` (the Refinement Ledger, now merged into the changelog) | split each `### ` block into its own `docs/history/changelog/YYYY-MM-DD-{three-words}.md` entry — content preserved **verbatim**, heading promoted to `#`, three words naming the work; append one row per entry to `docs/history/CHANGELOG.md`; delete the `docs/history/ledger/` folder. Records are moved, never rewritten |
|
|
172
178
|
| `docs/operations/guardrails.md` | fold durable contract notes into `docs/architecture/` |
|
|
173
179
|
| `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
|
|
174
|
-
|
|
|
180
|
+
| `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
|
|
181
|
+
| a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
|
|
175
182
|
| no `harness-version` in `structure.md` | stamp it (Step B4) |
|
|
176
183
|
|
|
177
184
|
### Step B3 — Present the reconcile plan, then execute
|
|
@@ -17,7 +17,7 @@ Three layers tell you what to look for; the code tells you what exists.
|
|
|
17
17
|
|
|
18
18
|
- **The mechanisms** — [`references/mechanisms.md`](references/mechanisms.md) defines the seven failure mechanisms every boundary instantiates (rendezvous-string, serialized-shape, generated-artifact, dual-authority, config-elsewhere, trust-invariant, lifecycle-protocol), each with its trace strategy, universal safe-change pattern, and severity heuristic, plus the two stack-independent domain contracts (entity-model, tenancy). Load it on every check.
|
|
19
19
|
- **The technology packs** — [`references/index.md`](references/index.md) routes to the packs under `references/packs/` by the signals in the touched code. Each pack carries the non-derivable payload for one technology area: exact detect patterns, expensive-to-learn gotchas, and safe-change sequences. Load **only the packs the touch list exhibits**; a technology no pack covers is still in scope — diagnose it from its mechanism.
|
|
20
|
-
- **Repo doctrine** — `.claude/rules/`, `docs/architecture/` (including `
|
|
20
|
+
- **Repo doctrine** — `.claude/rules/`, `docs/architecture/` (including `decisions/`), and any contracts package name the repo's bespoke contracts (a documented response envelope, a schema-parity rule, an entity model). A documented contract is a real side of a boundary: a plan that contradicts one is a finding **even when no code consumer exists yet** — this is how greenfield plans, whose modules have no code to trace, still get checked.
|
|
21
21
|
|
|
22
22
|
The packs are a floor, not a fence: report any contract that passes the ships-green litmus, pack entry or not.
|
|
23
23
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Usage: node doc-lint.mjs <file.md ...> | node doc-lint.mjs --changed (git-changed + untracked .md)
|
|
3
3
|
// Exit 1 when any block-severity finding survives. Escape hatch: <!-- doc-lint-disable: id[, id] --> (file-wide, or on the offending line).
|
|
4
4
|
|
|
5
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
|
|
8
8
|
const RULES = [
|
|
@@ -10,10 +10,14 @@ const RULES = [
|
|
|
10
10
|
{ id: 'history-narration', severity: 'block', scope: 'truth', pattern: /\bno longer\b|\bwas (renamed|removed|extracted|folded|migrated|replaced)\b|\bmoved (to|from|into) [\w`./-]+ \(?20\d{2}/i, message: 'history narration — the journey belongs in a dated record' },
|
|
11
11
|
{ id: 'progress-marker', severity: 'block', scope: 'truth', pattern: /\bphases?\s+\d[^.\n]{0,40}\b(complete|done)\b|\b(build|implementation) (is )?(\d+%|partially) (complete|done)\b/i, message: 'progress marker — progress lives in the plan/backlog record' },
|
|
12
12
|
{ id: 'temporal-hedge', severity: 'warn', scope: 'truth', pattern: /\bnot yet\b|\bfor now\b|\bcurrently\b|\bstill (unsupported|missing|pending|unimplemented|in progress)\b/i, message: 'temporal hedge — state the fact without "currently / not yet / for now"' },
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// "previously-fetched" is a hyphenated adjective naming a thing, not a predecessor design — don't fire on it.
|
|
14
|
+
{ id: 'predecessor-frame', severity: 'warn', scope: 'truth', pattern: /\bpreviously\b(?!-)|\bthe old \w+\b|\blegacy \w+ (was|is)\b/i, message: 'predecessor framing — describe the one current design' },
|
|
15
|
+
// docs/tests/ behavioral checklists use `- [ ]` as test-step notation (step → **Assert**) and docs/operations/
|
|
16
|
+
// procedures use it for operator verification steps — neither is progress tracking. Every other truth doc still bans checkboxes.
|
|
17
|
+
{ id: 'status-glyph', severity: 'warn', scope: 'truth', exempt: /(^|\/)docs\/(tests|operations)\//, pattern: /(^|\s)(✅|⏳|- \[[ x]\])/u, message: 'status glyph / checklist in a truth doc — tracking belongs in a record' },
|
|
18
|
+
// Truth docs are supposed to cite records, and record filenames are dated — so a date inside a link
|
|
19
|
+
// target or a code span is a path, not narration. Strip those before matching.
|
|
20
|
+
{ id: 'date-in-truth-doc', severity: 'warn', scope: 'truth', strip: /\[[^\]]*\]\([^)]*\)|`[^`]*`/g, pattern: /\b20\d{2}-\d{2}-\d{2}\b/, message: 'date in a truth doc — only the AD Decision Changelog and structure.md Health Log tails carry dates' },
|
|
17
21
|
];
|
|
18
22
|
|
|
19
23
|
// Sections inside a truth doc where dated history is sanctioned (the two bridges).
|
|
@@ -74,13 +78,16 @@ function lintFile(path, findings) {
|
|
|
74
78
|
if (rule.scope !== scope || disabled.has(rule.id)) continue;
|
|
75
79
|
if (rule.exempt && rule.exempt.test(path)) continue;
|
|
76
80
|
for (const { n, line } of lines) {
|
|
77
|
-
|
|
81
|
+
const subject = rule.strip ? line.replace(rule.strip, '') : line;
|
|
82
|
+
if (!rule.pattern.test(subject)) continue;
|
|
78
83
|
if (line.includes(`doc-lint-disable: ${rule.id}`) || new RegExp(`doc-lint-disable:[^>]*\\b${rule.id}\\b`).test(line)) continue;
|
|
79
84
|
findings.push({ path, n, severity: rule.severity, id: rule.id, message: rule.message, excerpt: line.trim().slice(0, 90) });
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
// A folder index (Readme.md/README.md) is a browse page, not a feature doc — it owns no code.
|
|
89
|
+
const isFolderIndex = /(^|\/)(readme)\.md$/i.test(path);
|
|
90
|
+
if (scope === 'truth' && !isFolderIndex && /(^|\/)docs\/features\/[^/]+\.md$/.test(path)) {
|
|
84
91
|
const fm = text.startsWith('---') ? text.slice(0, text.indexOf('\n---', 3)) : '';
|
|
85
92
|
for (const key of ['owns:', 'keywords:', 'status:'])
|
|
86
93
|
if (!fm.includes(key)) findings.push({ path, n: 1, severity: 'block', id: 'feature-frontmatter', message: `feature doc missing \`${key}\` frontmatter` });
|
|
@@ -106,7 +113,21 @@ if (changedMode) {
|
|
|
106
113
|
const out = execSync('git diff --name-only HEAD -- "*.md"; git ls-files --others --exclude-standard -- "*.md"', { encoding: 'utf8', shell: '/bin/bash' });
|
|
107
114
|
files = [...new Set([...files, ...out.split('\n').filter(Boolean)])];
|
|
108
115
|
}
|
|
109
|
-
|
|
116
|
+
// Expand any directory argument into the .md files beneath it, so `doc-lint.mjs docs/` works.
|
|
117
|
+
function expandDirs(paths) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const p of paths) {
|
|
120
|
+
if (!existsSync(p)) { out.push(p); continue; }
|
|
121
|
+
if (!statSync(p).isDirectory()) { out.push(p); continue; }
|
|
122
|
+
for (const e of readdirSync(p, { withFileTypes: true })) {
|
|
123
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
124
|
+
out.push(...expandDirs([`${p.replace(/\/$/, '')}/${e.name}`]));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
files = expandDirs(files).filter((f) => f.endsWith('.md'));
|
|
130
|
+
if (!files.length) { console.error('usage: node doc-lint.mjs <file.md | dir ...> | --changed'); process.exit(2); }
|
|
110
131
|
|
|
111
132
|
const findings = [];
|
|
112
133
|
files.forEach((f) => lintFile(f, findings));
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Truth document. Use for: the entry point for all documentation in a repo. Created during baseline audit.
|
|
4
4
|
|
|
5
|
+
**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.
|
|
6
|
+
|
|
5
7
|
```
|
|
6
8
|
# {Project Name} Documentation
|
|
7
9
|
|
|
@@ -8,8 +8,19 @@ pnpm-lock.yaml
|
|
|
8
8
|
package-lock.json
|
|
9
9
|
yarn.lock
|
|
10
10
|
|
|
11
|
-
# Harness-managed
|
|
11
|
+
# Harness-managed — the installer overwrites all of these, so formatting them is churn
|
|
12
|
+
# that comes straight back on the next `npx @nurix/etna`.
|
|
12
13
|
.claude
|
|
14
|
+
.codex
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE.md
|
|
17
|
+
AGENTS.md
|
|
18
|
+
|
|
19
|
+
# Frozen records — a plan, changelog entry, or migration runbook is a point-in-time
|
|
20
|
+
# artifact that is never edited after it freezes. Reformatting one is an edit, and the
|
|
21
|
+
# `maintenance` agent is forbidden from making it — so a format gate over this tree is
|
|
22
|
+
# a demand nothing in the repo is allowed to satisfy.
|
|
23
|
+
docs/history
|
|
13
24
|
|
|
14
25
|
# Source catalogs — authored markdown; prettier collapses template/example blocks
|
|
15
26
|
skills
|
|
@@ -8,8 +8,19 @@ pnpm-lock.yaml
|
|
|
8
8
|
package-lock.json
|
|
9
9
|
yarn.lock
|
|
10
10
|
|
|
11
|
-
# Harness-managed
|
|
11
|
+
# Harness-managed — the installer overwrites all of these, so formatting them is churn
|
|
12
|
+
# that comes straight back on the next `npx @nurix/etna`.
|
|
12
13
|
.claude
|
|
14
|
+
.codex
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE.md
|
|
17
|
+
AGENTS.md
|
|
18
|
+
|
|
19
|
+
# Frozen records — a plan, changelog entry, or migration runbook is a point-in-time
|
|
20
|
+
# artifact that is never edited after it freezes. Reformatting one is an edit, and the
|
|
21
|
+
# `maintenance` agent is forbidden from making it — so a format gate over this tree is
|
|
22
|
+
# a demand nothing in the repo is allowed to satisfy.
|
|
23
|
+
docs/history
|
|
13
24
|
|
|
14
25
|
# Source catalogs — authored markdown; prettier collapses template/example blocks
|
|
15
26
|
skills
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.31",
|
|
4
4
|
"description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|