@nurix/etna 0.19.45 → 0.19.46

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": "efebb48c460d",
3
3
  "harnesses": {
4
4
  "apollo": {
5
5
  "plugin": "apollo",
@@ -14,5 +14,5 @@
14
14
  "docs/history/backlog.md",
15
15
  "docs/memory/main.md"
16
16
  ],
17
- "digest": "544ba9e97120"
17
+ "digest": "25bb37bef74c"
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.
@@ -8,5 +8,5 @@
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "132494301fd0"
11
+ "digest": "9074f75982ad"
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.46",
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": "efebb48c460d"
45
46
  }