@nurix/etna 0.19.90 → 0.19.91

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");
@@ -219,18 +219,26 @@ export function resolveRows(projectDir, predicate) {
219
219
  return res.result;
220
220
  }
221
221
 
222
+ export function waiverExpiry(row) {
223
+ return (/expires=(\d{4}-\d{2}-\d{2})/.exec(row.body) || [])[1] || null;
224
+ }
225
+
222
226
  // 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
227
  export function unexpiredWaivers(rows, kind, today) {
225
228
  return rows.filter((row) => {
226
229
  if (row.kind !== "waived") return false;
227
230
  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;
231
+ const expires = waiverExpiry(row);
232
+ return !!expires && expires >= today;
231
233
  });
232
234
  }
233
235
 
236
+ // True when an unexpired waiver of `kind` names the finding's subject. The subject is matched
237
+ // BACKTICKED — a bare substring match let a waiver for `src/dead1.ts` silently cover `src/dead`.
238
+ export function waiverCovers(rows, kind, subject, today) {
239
+ return unexpiredWaivers(rows, kind, today).some((w) => w.body.includes(`\`${subject}\``));
240
+ }
241
+
234
242
  // Ensure .claude/state/ exists and is self-ignoring (a `*` .gitignore inside it),
235
243
  // so consumers never see machine-local state as untracked noise. Returns the dir.
236
244
  export function stateDir(projectDir) {
@@ -15,4 +15,4 @@ The shared work queue keeping documentation and code in sync — a queue, never
15
15
  - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
16
16
  ```
17
17
 
18
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `boundary-scan-suggested` · `doc-placement`.
18
+ 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.19",
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": "7358b638bff2"
12
12
  }
@@ -15,4 +15,4 @@ The shared work queue keeping documentation and code in sync — a queue, never
15
15
  - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
16
16
  ```
17
17
 
18
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `boundary-scan-suggested` · `doc-placement`.
18
+ 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.8",
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": "f2edee92fac2"
12
12
  }
@@ -15,4 +15,4 @@ The shared work queue keeping documentation and code in sync — a queue, never
15
15
  - waived: YYYY-MM-DD · {what is deferred} · reason: {why} → expires=YYYY-MM-DD
16
16
  ```
17
17
 
18
- Finding kinds for `auto:`: `dead-glob` · `doc-implicated` · `doc-lint` · `header-missing` · `index-row-missing` · `boundary-scan-suggested` · `doc-placement`.
18
+ 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.91",
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": "b52694cf6295"
46
46
  }