@nurix/etna 0.19.90 → 0.19.92

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.
@@ -0,0 +1,68 @@
1
+ // harness/code/hooks/lib/repo.mjs — the shared floor every hook module stands on: safe reads,
2
+ // path normalization, git, session ids, and the one definition of "source file". Each of these
3
+ // existed as 2–5 diverging private copies before being hoisted here.
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ export function readSafe(file) {
10
+ try {
11
+ return fs.readFileSync(file, "utf8");
12
+ } catch (e) {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ // Repo-relative /-separated path, or null when the target lies outside the project.
18
+ export function relIfInside(projectDir, filePath) {
19
+ if (!filePath) return null;
20
+ const rel = path.relative(projectDir, path.resolve(projectDir, filePath));
21
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
22
+ return rel.split(path.sep).join("/");
23
+ }
24
+
25
+ export function git(cwd, args) {
26
+ const res = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5000 });
27
+ return res.status === 0 ? (res.stdout || "").trim() : null;
28
+ }
29
+
30
+ export const today = () => new Date().toISOString().slice(0, 10);
31
+
32
+ export function sid8(sessionId) {
33
+ return String(sessionId || "unknown").replace(/[^a-zA-Z0-9-]/g, "").slice(0, 8) || "unknown";
34
+ }
35
+
36
+ // One definition of "source file" — the plan gate and the drift detector must agree on it.
37
+ export const SOURCE_EXT = /\.(m?[jt]sx?|py|rs|go|rb|java|kt|swift|c|h|cpp|hpp|cs|sh|sql|css|scss|vue|svelte)$/;
38
+
39
+ // Space-free backticked tokens in a markdown chunk, `./` prefix stripped — the shared grammar
40
+ // for path-carrying prose (protected lists, plan scopes).
41
+ export function backtickTokens(text) {
42
+ const out = [];
43
+ for (const m of String(text || "").matchAll(/`([^`\n]+)`/g)) {
44
+ const candidate = m[1].trim().replace(/^\.\//, "");
45
+ if (candidate && !candidate.includes(" ")) out.push(candidate);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ // The repo a Bash command acts on: an explicit `cd <dir>` in the command wins, then the
51
+ // payload's cwd, then the session's home project — a session may operate across repos
52
+ // (forge + consumer), and gating a commit against the wrong repo blocks or passes falsely.
53
+ // A candidate is normalized to its git toplevel so a subdirectory cwd still resolves to the
54
+ // repo root. Claim and release both route here — two private copies of this rule once
55
+ // disagreed, and the divergence leaked stale claims.
56
+ export function commandRoot(projectDir, payload, command) {
57
+ const cdMatch = /(?:^|\n|&&|;)\s*cd\s+"?([^"\s;&]+)"?/.exec(command || "");
58
+ for (const candidate of [cdMatch && cdMatch[1], payload && payload.cwd]) {
59
+ if (!candidate || !path.isAbsolute(candidate)) continue;
60
+ try {
61
+ if (!fs.statSync(candidate).isDirectory()) continue;
62
+ } catch (e) {
63
+ continue;
64
+ }
65
+ return git(candidate, ["rev-parse", "--show-toplevel"]) || candidate;
66
+ }
67
+ return projectDir;
68
+ }
@@ -1,6 +1,6 @@
1
1
  // harness/code/hooks/lib/session-log.mjs — the per-session gate log: enforcement state, not a record.
2
2
  // It lives in .claude/state/sessions/ (machine-local, self-ignored) because its load-bearing role
3
- // is synchronous: machinery appends `denied` and `hook-crash` events, the session appends
3
+ // is synchronous: machinery appends `denied` events, the session appends
4
4
  // `resolved` lines, and the walls read PAIRS — a `denied` with no later `resolved` for the same
5
5
  // gate·target is an open failure, and silence about it is what the Stop and commit walls refuse.
6
6
  // Within a session resolution appends rather than deletes (the retry story is what the walls
@@ -10,10 +10,7 @@
10
10
  import fs from "node:fs";
11
11
  import path from "node:path";
12
12
  import { stateDir } from "./state.mjs";
13
-
14
- function sid8(sessionId) {
15
- return String(sessionId || "unknown").replace(/[^a-zA-Z0-9-]/g, "").slice(0, 8) || "unknown";
16
- }
13
+ import { sid8 } from "./repo.mjs";
17
14
 
18
15
  export function sessionLogPath(projectDir, sessionId) {
19
16
  const dir = path.join(stateDir(projectDir), "sessions");
@@ -9,15 +9,20 @@
9
9
  // only while its expires= date is in the future; past it, the deferral is over and the wall returns.
10
10
  // Rows are also re-verified against the repo before the walls read them (awareness.mjs's sweep),
11
11
  // so a row whose condition died is deleted rather than waiting for its date.
12
+ //
13
+ // The three walls read the queue at three widths: the commit wall takes only the rows implicating
14
+ // the commit's staged set (§ What a row implicates), the Stop wall takes the ending session's own
15
+ // rows, and CI takes every row. Accountability is per session; a commit is per set of files.
12
16
 
13
17
  import fs from "node:fs";
14
18
  import path from "node:path";
19
+ import { globToRegExp } from "./ownership.mjs";
15
20
 
16
21
  export const QUEUE_PATH = ["docs", "operations", "bookkeeping.md"];
17
22
 
18
23
  export const QUEUE_HEADER = `# Bookkeeping
19
24
 
20
- The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to \`waived:\`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the \`documentation\` skill's \`templates/bookkeeping.md\`.\n\n**Every row is a cache of a condition, not a record of an event.** A session-start sweep re-tests each row against the repo and deletes the ones whose condition has died — \`waived:\` rows included, because a waiver defers a finding and never makes one immortal. What the sweep cannot mechanically re-test ages out instead: a \`waived:\` row carries \`expires=\`, is PR-reviewable history until that date, and counts as **unresolved** past it — which returns the wall and re-summons the bookkeeper to re-judge whether the reason still holds.
25
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to \`waived:\`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the \`documentation\` skill's \`templates/bookkeeping.md\`.\n\n**Every row is a cache of a condition, not a record of an event.** A session-start sweep re-tests each row against the repo and deletes the ones whose condition has died — \`waived:\` rows included, because a waiver defers a finding and never makes one immortal. What the sweep cannot mechanically re-test ages out instead: a \`waived:\` row carries \`expires=\`, is PR-reviewable history until that date, and counts as **unresolved** past it — which returns the wall and re-summons the bookkeeper to re-judge whether the reason still holds.
21
26
  `;
22
27
 
23
28
  export function queueFile(projectDir) {
@@ -143,6 +148,85 @@ export function countRows(rows, today = new Date().toISOString().slice(0, 10)) {
143
148
  return c;
144
149
  }
145
150
 
151
+ // The unresolved rows themselves — the same set countRows() tallies, as rows rather than a number.
152
+ export function unresolvedRows(rows, today = new Date().toISOString().slice(0, 10)) {
153
+ return rows.filter((row) => {
154
+ if (row.kind !== "waived") return true;
155
+ const expires = waiverExpiry(row);
156
+ return !expires || expires < today;
157
+ });
158
+ }
159
+
160
+ // --- What a row implicates -------------------------------------------------------------------
161
+ //
162
+ // THE COMMIT WALL IS SCOPED BY PATH. Every mechanical finding names the file it is about, so a
163
+ // commit can be walled by the rows implicating ITS staged set rather than by the whole queue.
164
+ // A global wall is unattributable the moment agents share a checkout: sibling agents also share a
165
+ // session id, so an agent ready to commit was held behind rows a concurrent sibling raised — rows
166
+ // the ownership etiquette (drain only what your own edits raised) forbids it from draining, which
167
+ // makes the wall a wait with no action that clears it. Measured on the fleet: a 15-minute stall
168
+ // behind a sibling's ten rows, and two agents in 5–6-minute mutual politeness loops.
169
+ //
170
+ // The shapes below mirror the ones awareness.mjs's ROW_VERIFIERS re-test — one grammar, two
171
+ // readers. Each rule captures every path the row names: the file the finding is about AND the doc
172
+ // that owns or records it, since staging either one is a commit answering for that row.
173
+ //
174
+ // A ROW THIS CANNOT PARSE IMPLICATES EVERYTHING. Scoping may narrow a wall on evidence, never on
175
+ // ignorance — so an unrecognized row, a judgment row, and a commit with nothing staged to compare
176
+ // against all keep the old repo-wide block.
177
+ const ROW_PATH_RULES = [
178
+ /·\s*doc-implicated\s*·\s*(\S+)\s+changed;\s+owning doc\s+(\S+?)\s+may need/,
179
+ /·\s*doc-lint\s*·\s*(\S+)\s+has\s/,
180
+ /·\s*header-missing\s*·\s*(\S+)\s+lacks\s/,
181
+ /·\s*index-row-missing\s*·\s*(\S+)\s+is not linked from\s+(\S+)/,
182
+ /·\s*package-rejected\s*·\s*(\S+)\s+declares\s/,
183
+ /·\s*frozen-record-edited\s*·\s*(\S+)\s+is a frozen record/,
184
+ /·\s*dead-glob\s*·\s*owns: glob\s+`([^`]+)`\s+in\s+(\S+)\s+matches/,
185
+ /→\s*lands-in:\s*(\S+\/\S+)/, // a note: row names the doc its context lands in
186
+ ];
187
+
188
+ // The paths a row implicates, or null when its grammar yields none.
189
+ export function rowPaths(row) {
190
+ for (const re of ROW_PATH_RULES) {
191
+ const m = re.exec(row.body);
192
+ if (!m) continue;
193
+ const paths = m.slice(1).filter(Boolean);
194
+ if (paths.length > 0) return paths;
195
+ }
196
+ return null;
197
+ }
198
+
199
+ // True when one of a row's tokens names a staged file. A token carrying `*` — the glob a dead-glob
200
+ // row quotes — compiles through the ownership index's engine; a plain token matches a staged file
201
+ // exactly, or as the directory it sits in.
202
+ function tokenHitsStaged(token, staged) {
203
+ if (token.includes("*")) {
204
+ const re = globToRegExp(token);
205
+ return staged.some((f) => re.test(f));
206
+ }
207
+ return staged.some((f) => f === token || f.startsWith(token + "/"));
208
+ }
209
+
210
+ // The unresolved rows a commit staging `staged` must answer for, each with the staged paths it
211
+ // implicates. `needs-session:` rows and expired waivers are judgments rather than mechanical
212
+ // findings — they carry no reliable path and stay global by kind, which is the semantics they had
213
+ // before scoping. Returns { blocking, unrelated }: `unrelated` counts the rows this commit is
214
+ // walked past, which the block message reports so scoping is visible rather than silent.
215
+ export function rowsBlockingCommit(rows, staged, today = new Date().toISOString().slice(0, 10)) {
216
+ const blocking = [];
217
+ let unrelated = 0;
218
+ for (const row of unresolvedRows(rows, today)) {
219
+ const paths = row.kind === "auto" || row.kind === "note" ? rowPaths(row) : null;
220
+ const hits = paths && staged.length > 0 ? paths.filter((p) => tokenHitsStaged(p, staged)) : null;
221
+ if (hits && hits.length === 0) {
222
+ unrelated += 1;
223
+ continue;
224
+ }
225
+ blocking.push({ row, hits: hits || [] });
226
+ }
227
+ return { blocking, unrelated };
228
+ }
229
+
146
230
  // Format one auto: row per the grammar; the sid tail is the only provenance kept —
147
231
  // a transcript path or timestamp is machine noise in a PR-reviewed queue.
148
232
  export function formatAutoRow({ date, kind, description, action, sessionId }) {
@@ -219,18 +303,26 @@ export function resolveRows(projectDir, predicate) {
219
303
  return res.result;
220
304
  }
221
305
 
306
+ export function waiverExpiry(row) {
307
+ return (/expires=(\d{4}-\d{2}-\d{2})/.exec(row.body) || [])[1] || null;
308
+ }
309
+
222
310
  // Unexpired waived rows for a finding kind (the dead-glob allowlist CI honors).
223
- // A waived row covers a glob when its body names it verbatim.
224
311
  export function unexpiredWaivers(rows, kind, today) {
225
312
  return rows.filter((row) => {
226
313
  if (row.kind !== "waived") return false;
227
314
  if (kind && !row.body.includes(kind)) return false;
228
- const expires = (/expires=(\d{4}-\d{2}-\d{2})/.exec(row.body) || [])[1];
229
- if (!expires) return false;
230
- return expires >= today;
315
+ const expires = waiverExpiry(row);
316
+ return !!expires && expires >= today;
231
317
  });
232
318
  }
233
319
 
320
+ // True when an unexpired waiver of `kind` names the finding's subject. The subject is matched
321
+ // BACKTICKED — a bare substring match let a waiver for `src/dead1.ts` silently cover `src/dead`.
322
+ export function waiverCovers(rows, kind, subject, today) {
323
+ return unexpiredWaivers(rows, kind, today).some((w) => w.body.includes(`\`${subject}\``));
324
+ }
325
+
234
326
  // Ensure .claude/state/ exists and is self-ignoring (a `*` .gitignore inside it),
235
327
  // so consumers never see machine-local state as untracked noise. Returns the dir.
236
328
  export function stateDir(projectDir) {
@@ -1,13 +1,15 @@
1
1
  # Bookkeeping ledger (docs/operations/bookkeeping.md)
2
2
 
3
- Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop and commit — a commit is possible only when every row is resolved (deleted) or converted to `waived:`. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
3
+ Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop, at commit, and in CI. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
4
+
5
+ **Each wall reads the queue at its own width.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
4
6
 
5
7
  The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `waived:` rows whose finding kind is `dead-glob` double as the dead-glob allowlist.
6
8
 
7
9
  ```
8
10
  # Bookkeeping
9
11
 
10
- The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
12
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
13
 
12
14
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
15
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -15,4 +17,4 @@ The shared work queue keeping documentation and code in sync — a queue, never
15
17
  - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
16
18
  ```
17
19
 
18
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `boundary-scan-suggested` · `doc-placement`.
20
+ Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.2.18",
4
+ "version": "0.2.20",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "a11ca1e20bd4"
11
+ "digest": "3bb0c6640019"
12
12
  }
@@ -1,13 +1,15 @@
1
1
  # Bookkeeping ledger (docs/operations/bookkeeping.md)
2
2
 
3
- Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop and commit — a commit is possible only when every row is resolved (deleted) or converted to `waived:`. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
3
+ Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop, at commit, and in CI. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
4
+
5
+ **Each wall reads the queue at its own width.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
4
6
 
5
7
  The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `waived:` rows whose finding kind is `dead-glob` double as the dead-glob allowlist.
6
8
 
7
9
  ```
8
10
  # Bookkeeping
9
11
 
10
- The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
12
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
13
 
12
14
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
15
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -15,4 +17,4 @@ The shared work queue keeping documentation and code in sync — a queue, never
15
17
  - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
16
18
  ```
17
19
 
18
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `boundary-scan-suggested` · `doc-placement`.
20
+ Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.5.7",
4
+ "version": "1.5.9",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "5843b3511178"
11
+ "digest": "269767b3abdf"
12
12
  }
@@ -1,13 +1,15 @@
1
1
  # Bookkeeping ledger (docs/operations/bookkeeping.md)
2
2
 
3
- Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop and commit — a commit is possible only when every row is resolved (deleted) or converted to `waived:`. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
3
+ Truth document with an unusual lifecycle: a **work queue**, not an archive. Rows are appended mechanically by the awareness hook (`auto:`) and deliberately by working sessions (`note:`), drained by the `bookkeeper` agent, and walled at Stop, at commit, and in CI. What survives in git — `waived:` and `needs-session:` rows — is exactly what a PR reviewer should see.
4
+
5
+ **Each wall reads the queue at its own width.** Stop takes the ending session's own rows, because accountability for an arc is per session. The commit gate takes only the rows **implicating that commit's staged files** — a row names the file its finding is about, and holding a commit behind rows raised by somebody else's edits is a wait nothing the committer may do can clear. CI takes every row on the branch, where there is no staged set to scope by. Fail-closed cases stay repo-wide at the commit gate: a row whose grammar names no path, a `needs-session:` row, an expired waiver, and a commit staging nothing at all.
4
6
 
5
7
  The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:` row's `expires=` date is enforced: CI honors the waiver until the date, then fails. `waived:` rows whose finding kind is `dead-glob` double as the dead-glob allowlist.
6
8
 
7
9
  ```
8
10
  # Bookkeeping
9
11
 
10
- The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
12
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit goes through only when every row **implicating one of its staged files** is either **resolved, which deletes it**, or converted to `waived:`. A row about files the commit does not touch belongs to whoever raised it; a row naming no file, and a commit staging none, block everything. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
13
 
12
14
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
15
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -15,4 +17,4 @@ The shared work queue keeping documentation and code in sync — a queue, never
15
17
  - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
16
18
  ```
17
19
 
18
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `boundary-scan-suggested` · `doc-placement`.
20
+ Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `package-rejected` · `frozen-record-edited`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.90",
3
+ "version": "0.19.92",
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": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "8d6f9b80d64d"
45
+ "etnaFingerprint": "a1cf8eb35f57"
46
46
  }