@nurix/etna 0.19.45 → 0.19.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "8a30d3130f63",
2
+ "fingerprint": "fa53d6fe5dfc",
3
3
  "harnesses": {
4
4
  "apollo": {
5
5
  "plugin": "apollo",
@@ -15,8 +15,8 @@ Ground every edit in this ladder, in order. Never invent content; a fact you can
15
15
 
16
16
  1. **`auto:` rows are recomputable from code + docs on any machine — fix directly.** A `dead-glob` row: read the owning doc, find where the files went (`git log --follow`, glob search), update its `owns:` frontmatter. A `header-missing` row: add the file-path header comment in the file's native comment syntax. A `doc-lint` row: fix the finding, or add `<!-- doc-lint-disable: id -->` with a reason beside it when the flagged text is deliberate. An `index-row-missing` row: add the doc's row to `docs/README.md`. A `doc-implicated` row: read the diff of the named source file (`git diff` / `git log -p`), and update the owning doc **only where the diff contradicts documented behavior**; if nothing documented changed, the row's resolution is deleting it.
17
17
  2. **`note:` rows — expand strictly from the stated context.** The one-line context after the area is everything the session chose to hand you; land it in the destination the row names (`lands-in:` an owning doc, the backlog, or memory), written in that document's genre.
18
- 3. **A thin note, same machine only** — read a timestamp-anchored window of the transcript JSONL at the row's `tp=` path around its `ts=` stamp (never the whole file). The stamp is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
19
- 4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original ts}`) and stop working that row.
18
+ 3. **A thin note, same machine only** — derive the transcript from the row's `sid=` tail: the session JSONL lives at `~/.claude/projects/{project-dir-slug}/{sid}.jsonl` (the slug is the project path with `/` → `-`). Search it for the row's file path or area and read only that window — never the whole file. The sid is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
19
+ 4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original row}`) and stop working that row.
20
20
 
21
21
  ## Rules
22
22
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "apollo",
3
3
  "name": "apollo",
4
- "version": "1.0.3",
4
+ "version": "1.0.4",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -14,5 +14,5 @@
14
14
  "docs/history/backlog.md",
15
15
  "docs/memory/main.md"
16
16
  ],
17
- "digest": "544ba9e97120"
17
+ "digest": "7b3bfe5d305e"
18
18
  }
@@ -123,6 +123,14 @@ function driftBanner(projectDir) {
123
123
  const waivers = unexpiredWaivers(queueRows, "dead-glob", today());
124
124
  const unwaivedDead = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
125
125
 
126
+ // A queued dead-glob row covered by an unexpired waiver is resolved history — drop it,
127
+ // so waiving a finding class actually empties the rows it supersedes.
128
+ resolveRows(projectDir, (row) => {
129
+ if (row.kind !== "auto" || !row.body.includes("dead-glob")) return false;
130
+ const m = /`([^`]+)`/.exec(row.body);
131
+ return !!m && waivers.some((w) => w.body.includes(`\`${m[1]}\``));
132
+ });
133
+
126
134
  // Docs older than the code they own (doc mtime < newest owned-file mtime).
127
135
  const staleDocs = [];
128
136
  for (const entry of scanFeatureDocs(projectDir)) {
@@ -313,7 +321,9 @@ function detectorForFile(projectDir, rel, meta, rows) {
313
321
 
314
322
  function detectRenames(projectDir, command, meta, rows) {
315
323
  if (!/\bgit mv\b|\bmv\b/.test(command || "")) return;
324
+ const waivers = unexpiredWaivers(readQueue(projectDir), "dead-glob", today());
316
325
  for (const { glob, doc } of deadGlobs(projectDir)) {
326
+ if (waivers.some((w) => w.body.includes(`\`${glob}\``))) continue;
317
327
  rows.push(
318
328
  formatAutoRow({
319
329
  ...meta,
@@ -232,7 +232,8 @@ function bashGate(projectDir, command, sessionId) {
232
232
  return commitGate(projectDir);
233
233
  }
234
234
 
235
- if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+\S+|worktree\s+add)\b`).test(command)) {
235
+ // `git branch <name>` creates; flag-led forms (--show-current, --list, -d, -vv) query or manage
236
+ if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+(?!-)\S+|worktree\s+add)`).test(command)) {
236
237
  return askOnce(
237
238
  projectDir,
238
239
  sessionId,
@@ -290,12 +291,17 @@ function plansCompletedThisSession(projectDir) {
290
291
  function stopGate(projectDir, payload) {
291
292
  if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
292
293
 
293
- const counts = countRows(readQueue(projectDir));
294
+ // The Stop wall is scoped to this session's own rows (plus sid-less ones) — a concurrent
295
+ // session's open work must not block this one's stop. The commit wall stays global.
296
+ const mine = readQueue(projectDir).filter(
297
+ (row) => !/\bsid=/.test(row.body) || row.body.includes(`sid=${payload.session_id}`)
298
+ );
299
+ const counts = countRows(mine);
294
300
  if (counts.unresolved > 0 && !bookkeeperDispatched(payload.transcript_path)) {
295
301
  return {
296
302
  decision: "block",
297
303
  reason:
298
- `${counts.unresolved} unresolved bookkeeping rows (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
304
+ `${counts.unresolved} unresolved bookkeeping rows from this session (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
299
305
  "Dispatch the `bookkeeper` agent to drain them, resolve them now, or convert to `waived:` rows with reason + expiry. " +
300
306
  "If implementation of the open plan is complete, also report it and offer the verification gate — never run it unbidden.",
301
307
  };
@@ -1,8 +1,8 @@
1
1
  // harness/code/hooks/lib/state.mjs — the bookkeeping queue (docs/operations/bookkeeping.md) and the
2
2
  // machine-local state dir (.claude/state/). The row grammar is the contract between the awareness
3
3
  // detector (writer), the bookkeeper agent (drain), the compliance gates (walls), and CI (backstop):
4
- // - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
5
- // - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
4
+ // - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id}
5
+ // - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=…
6
6
  // - needs-session: {YYYY-MM-DD} · {what could not be grounded} → main session resolves | refs={ts}
7
7
  // - waived: {YYYY-MM-DD} · {what is deferred} · reason: {why} → expires={YYYY-MM-DD}
8
8
  // Unresolved = auto + note + needs-session. waived: rows are resolved history until expires= passes.
@@ -57,9 +57,10 @@ export function countRows(rows) {
57
57
  return c;
58
58
  }
59
59
 
60
- // Format one auto: row per the grammar; sid/tp/ts come from the hook payload.
61
- export function formatAutoRow({ date, kind, description, action, sessionId, transcriptPath, ts }) {
62
- return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"} tp=${transcriptPath || "-"} ts=${ts || "-"}`;
60
+ // Format one auto: row per the grammar; the sid tail is the only provenance kept —
61
+ // a transcript path or timestamp is machine noise in a PR-reviewed queue.
62
+ export function formatAutoRow({ date, kind, description, action, sessionId }) {
63
+ return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"}`;
63
64
  }
64
65
 
65
66
  // Append rows to the queue, creating the file with its genre header when absent.
@@ -15,8 +15,8 @@ Ground every edit in this ladder, in order. Never invent content; a fact you can
15
15
 
16
16
  1. **`auto:` rows are recomputable from code + docs on any machine — fix directly.** A `dead-glob` row: read the owning doc, find where the files went (`git log --follow`, glob search), update its `owns:` frontmatter. A `header-missing` row: add the file-path header comment in the file's native comment syntax. A `doc-lint` row: fix the finding, or add `<!-- doc-lint-disable: id -->` with a reason beside it when the flagged text is deliberate. An `index-row-missing` row: add the doc's row to `docs/README.md`. A `doc-implicated` row: read the diff of the named source file (`git diff` / `git log -p`), and update the owning doc **only where the diff contradicts documented behavior**; if nothing documented changed, the row's resolution is deleting it.
17
17
  2. **`note:` rows — expand strictly from the stated context.** The one-line context after the area is everything the session chose to hand you; land it in the destination the row names (`lands-in:` an owning doc, the backlog, or memory), written in that document's genre.
18
- 3. **A thin note, same machine only** — read a timestamp-anchored window of the transcript JSONL at the row's `tp=` path around its `ts=` stamp (never the whole file). The stamp is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
19
- 4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original ts}`) and stop working that row.
18
+ 3. **A thin note, same machine only** — derive the transcript from the row's `sid=` tail: the session JSONL lives at `~/.claude/projects/{project-dir-slug}/{sid}.jsonl` (the slug is the project path with `/` → `-`). Search it for the row's file path or area and read only that window — never the whole file. The sid is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
19
+ 4. **Still ungrounded** — replace the row with a `needs-session:` row (`- needs-session: {date} · {what you could not ground} → main session resolves | refs={original row}`) and stop working that row.
20
20
 
21
21
  ## Rules
22
22
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.5.11",
4
+ "version": "3.5.12",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "132494301fd0"
11
+ "digest": "ddfeda99a408"
12
12
  }
@@ -123,6 +123,14 @@ function driftBanner(projectDir) {
123
123
  const waivers = unexpiredWaivers(queueRows, "dead-glob", today());
124
124
  const unwaivedDead = dead.filter((d) => !waivers.some((w) => w.body.includes(d.glob)));
125
125
 
126
+ // A queued dead-glob row covered by an unexpired waiver is resolved history — drop it,
127
+ // so waiving a finding class actually empties the rows it supersedes.
128
+ resolveRows(projectDir, (row) => {
129
+ if (row.kind !== "auto" || !row.body.includes("dead-glob")) return false;
130
+ const m = /`([^`]+)`/.exec(row.body);
131
+ return !!m && waivers.some((w) => w.body.includes(`\`${m[1]}\``));
132
+ });
133
+
126
134
  // Docs older than the code they own (doc mtime < newest owned-file mtime).
127
135
  const staleDocs = [];
128
136
  for (const entry of scanFeatureDocs(projectDir)) {
@@ -313,7 +321,9 @@ function detectorForFile(projectDir, rel, meta, rows) {
313
321
 
314
322
  function detectRenames(projectDir, command, meta, rows) {
315
323
  if (!/\bgit mv\b|\bmv\b/.test(command || "")) return;
324
+ const waivers = unexpiredWaivers(readQueue(projectDir), "dead-glob", today());
316
325
  for (const { glob, doc } of deadGlobs(projectDir)) {
326
+ if (waivers.some((w) => w.body.includes(`\`${glob}\``))) continue;
317
327
  rows.push(
318
328
  formatAutoRow({
319
329
  ...meta,
@@ -232,7 +232,8 @@ function bashGate(projectDir, command, sessionId) {
232
232
  return commitGate(projectDir);
233
233
  }
234
234
 
235
- if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+\S+|worktree\s+add)\b`).test(command)) {
235
+ // `git branch <name>` creates; flag-led forms (--show-current, --list, -d, -vv) query or manage
236
+ if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+(?!-)\S+|worktree\s+add)`).test(command)) {
236
237
  return askOnce(
237
238
  projectDir,
238
239
  sessionId,
@@ -290,12 +291,17 @@ function plansCompletedThisSession(projectDir) {
290
291
  function stopGate(projectDir, payload) {
291
292
  if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
292
293
 
293
- const counts = countRows(readQueue(projectDir));
294
+ // The Stop wall is scoped to this session's own rows (plus sid-less ones) — a concurrent
295
+ // session's open work must not block this one's stop. The commit wall stays global.
296
+ const mine = readQueue(projectDir).filter(
297
+ (row) => !/\bsid=/.test(row.body) || row.body.includes(`sid=${payload.session_id}`)
298
+ );
299
+ const counts = countRows(mine);
294
300
  if (counts.unresolved > 0 && !bookkeeperDispatched(payload.transcript_path)) {
295
301
  return {
296
302
  decision: "block",
297
303
  reason:
298
- `${counts.unresolved} unresolved bookkeeping rows (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
304
+ `${counts.unresolved} unresolved bookkeeping rows from this session (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
299
305
  "Dispatch the `bookkeeper` agent to drain them, resolve them now, or convert to `waived:` rows with reason + expiry. " +
300
306
  "If implementation of the open plan is complete, also report it and offer the verification gate — never run it unbidden.",
301
307
  };
@@ -1,8 +1,8 @@
1
1
  // harness/code/hooks/lib/state.mjs — the bookkeeping queue (docs/operations/bookkeeping.md) and the
2
2
  // machine-local state dir (.claude/state/). The row grammar is the contract between the awareness
3
3
  // detector (writer), the bookkeeper agent (drain), the compliance gates (walls), and CI (backstop):
4
- // - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
5
- // - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
4
+ // - auto: {YYYY-MM-DD} · {finding-kind} · {description} → {action} | sid={session_id}
5
+ // - note: {YYYY-MM-DD} · {area} · {context} → lands-in: {owning doc | backlog | memory} | sid=…
6
6
  // - needs-session: {YYYY-MM-DD} · {what could not be grounded} → main session resolves | refs={ts}
7
7
  // - waived: {YYYY-MM-DD} · {what is deferred} · reason: {why} → expires={YYYY-MM-DD}
8
8
  // Unresolved = auto + note + needs-session. waived: rows are resolved history until expires= passes.
@@ -57,9 +57,10 @@ export function countRows(rows) {
57
57
  return c;
58
58
  }
59
59
 
60
- // Format one auto: row per the grammar; sid/tp/ts come from the hook payload.
61
- export function formatAutoRow({ date, kind, description, action, sessionId, transcriptPath, ts }) {
62
- return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"} tp=${transcriptPath || "-"} ts=${ts || "-"}`;
60
+ // Format one auto: row per the grammar; the sid tail is the only provenance kept —
61
+ // a transcript path or timestamp is machine noise in a PR-reviewed queue.
62
+ export function formatAutoRow({ date, kind, description, action, sessionId }) {
63
+ return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"}`;
63
64
  }
64
65
 
65
66
  // Append rows to the queue, creating the file with its genre header when absent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.45",
3
+ "version": "0.19.47",
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": {
@@ -19,6 +19,7 @@
19
19
  "node": ">=18.0.0"
20
20
  },
21
21
  "scripts": {
22
+ "test": "node --test 'tests/**/*.test.mjs'",
22
23
  "build:data": "node scripts/build-data.js",
23
24
  "prepack": "node scripts/build-data.js",
24
25
  "validate:codex-agents": "node lib/codex/validate-codex-agents.mjs"
@@ -41,5 +42,5 @@
41
42
  "agent",
42
43
  "plugin"
43
44
  ],
44
- "etnaFingerprint": "8a30d3130f63"
45
+ "etnaFingerprint": "fa53d6fe5dfc"
45
46
  }