tamperward 2.7.1 → 2.8.0

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +526 -355
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2,9 +2,376 @@
2
2
 
3
3
  // src/git/build.ts
4
4
  import { execFileSync } from "node:child_process";
5
- import { readFileSync } from "node:fs";
5
+
6
+ // src/disk.ts
7
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readlinkSync, realpathSync } from "node:fs";
6
8
  import { join } from "node:path";
7
9
 
10
+ // src/policy.ts
11
+ import picomatch from "picomatch";
12
+ var POLICY_FILE = ".tamperward.yml";
13
+ var BLOCK_SINCE = {};
14
+ function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
15
+ const out2 = { ...rules };
16
+ for (const [rule, since] of Object.entries(gates)) {
17
+ if (since > version && out2[rule]?.severity === "block") {
18
+ out2[rule] = { ...out2[rule], severity: "warn" };
19
+ }
20
+ }
21
+ return out2;
22
+ }
23
+ function normalizeGlob(glob) {
24
+ return glob.startsWith("/") ? glob.replace(/^\/+/, "") : glob;
25
+ }
26
+ function mergeRules(base, user) {
27
+ const out2 = { ...base };
28
+ for (const [name, cfg] of Object.entries(user ?? {})) {
29
+ out2[name] = { ...out2[name] ?? {}, ...cfg ?? {} };
30
+ }
31
+ return out2;
32
+ }
33
+ var CONTROL = /[\u0000-\u001f\u007f]/;
34
+ function hasControlChar(path) {
35
+ return CONTROL.test(path);
36
+ }
37
+ function escapeControl(path) {
38
+ return path.replace(/[\u0000-\u001f\u007f]/g, (ch) => {
39
+ const c = ch.charCodeAt(0);
40
+ if (c === 10) return "\\n";
41
+ if (c === 13) return "\\r";
42
+ if (c === 9) return "\\t";
43
+ return `\\x${c.toString(16).padStart(2, "0")}`;
44
+ });
45
+ }
46
+ var listCache = /* @__PURE__ */ new WeakMap();
47
+ function listMatcher(globs4) {
48
+ let re = listCache.get(globs4);
49
+ if (re === void 0) {
50
+ re = globs4.length === 0 ? null : new RegExp(globs4.map((g) => `(?:${picomatch.makeRe(g, { dot: true }).source})`).join("|"));
51
+ listCache.set(globs4, re);
52
+ }
53
+ return re;
54
+ }
55
+ function matchesAny(path, globs4) {
56
+ if (!globs4) return false;
57
+ const re = listMatcher(globs4);
58
+ return re !== null && re.test(path);
59
+ }
60
+ function protectedCategory(path, policy) {
61
+ const cats = Object.entries(policy.protected ?? {});
62
+ if (hasControlChar(path)) return cats[0]?.[0] ?? "tests";
63
+ for (const [cat, globs4] of cats) {
64
+ if (matchesAny(path, globs4)) return cat;
65
+ }
66
+ return null;
67
+ }
68
+ var anyCache = /* @__PURE__ */ new WeakMap();
69
+ function allProtectedGlobs(policy) {
70
+ const p = policy.protected ?? {};
71
+ let all = anyCache.get(p);
72
+ if (!all) {
73
+ all = Object.values(p).flat();
74
+ anyCache.set(p, all);
75
+ }
76
+ return all;
77
+ }
78
+ function isProtected(path, policy, category) {
79
+ if (hasControlChar(path)) return true;
80
+ if (category) return matchesAny(path, policy.protected?.[category]);
81
+ return matchesAny(path, allProtectedGlobs(policy));
82
+ }
83
+ function isPolicyFile(path) {
84
+ return path === POLICY_FILE || path.endsWith("/" + POLICY_FILE);
85
+ }
86
+ function isIgnored(path, policy) {
87
+ return matchesAny(path, policy.ignore);
88
+ }
89
+ function mergeProtected(base, user) {
90
+ const out2 = { ...base };
91
+ for (const [cat, globs4] of Object.entries(user ?? {})) {
92
+ const have = new Set(out2[cat] ?? []);
93
+ out2[cat] = [...out2[cat] ?? [], ...(globs4 ?? []).filter((g) => !have.has(g))];
94
+ }
95
+ return out2;
96
+ }
97
+ function defaultPolicy(version = 1) {
98
+ const p = {
99
+ version,
100
+ protected: {
101
+ // Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
102
+ // .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
103
+ // instead of warning. Kept ADDITIVE (originals + new extensions) so the policy-diff sees a
104
+ // strengthening, not a narrowing. picomatch expands the brace.
105
+ // test-skip, test-deletion and test-content-removal all gate on this
106
+ // class, so a repo it does not match gets rules that can NEVER fire —
107
+ // protection that is silently absent rather than visibly off. The JS/TS
108
+ // list shipped alone; these are the conventional layouts of the other
109
+ // ecosystems `init` is run in. (P2-13, external review.)
110
+ tests: [
111
+ // JavaScript / TypeScript
112
+ "**/*.test.ts",
113
+ "**/*.spec.ts",
114
+ "**/*.{test,spec}.{tsx,cts,mts,js,jsx,cjs,mjs}",
115
+ "**/__tests__/**",
116
+ // Python (pytest / unittest layouts)
117
+ "**/test_*.py",
118
+ "**/*_test.py",
119
+ "**/conftest.py",
120
+ // Go
121
+ "**/*_test.go",
122
+ // Rust (integration tests live in tests/, unit tests inline)
123
+ "**/tests/**/*.rs",
124
+ // Ruby (RSpec / minitest)
125
+ "**/*_spec.rb",
126
+ "**/*_test.rb",
127
+ // Java / Kotlin / Scala (Maven & Gradle convention)
128
+ "**/src/test/**",
129
+ // PHP (PHPUnit)
130
+ "**/*Test.php",
131
+ // C# (.NET)
132
+ "**/*Tests.cs",
133
+ "**/*Test.cs"
134
+ ],
135
+ config: [
136
+ "**/jest.config.*",
137
+ "**/vitest.config.*",
138
+ // A `vitest.workspace.*` file REPLACES the root config's selection with the
139
+ // union of its projects, and a `vite.config.*` carrying a `test:` key is the
140
+ // vitest config when no vitest.config exists — both are where a spec can be
141
+ // dropped from the suite without a line of it changing. ADDITIVE, like every
142
+ // baseline widening (the policy-diff reads it as a strengthening).
143
+ "**/vitest.workspace.*",
144
+ "**/vite.config.*",
145
+ "**/tsconfig*.json",
146
+ "**/.eslintrc*",
147
+ "**/eslint.config.*",
148
+ "**/package.json",
149
+ // Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
150
+ // see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
151
+ // codecov `target` lived in files no glob protected, so lowering them fired
152
+ // nothing. ADDITIVE, like every baseline widening (the policy-diff reads it
153
+ // as a strengthening). pyproject.toml carries [tool.coverage.report].
154
+ "**/.coveragerc",
155
+ "**/pyproject.toml",
156
+ "**/.nycrc",
157
+ "**/.nycrc.*",
158
+ "**/nyc.config.*",
159
+ "**/codecov.yml",
160
+ "**/.codecov.yml"
161
+ ],
162
+ ci: [".github/workflows/**"],
163
+ // Recorded expected outputs. An assertion stored as data is still an assertion;
164
+ // rewriting it from current output is the snapshot-update move the affordance
165
+ // experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
166
+ // tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
167
+ // live there as .cjs and were rewritten unseen (PHASE3.md §4).
168
+ snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
169
+ // The wiring IS the enforcement: settings.json carries the PreToolUse and
170
+ // Stop hooks, and a pre-commit config carries the commit-time gate. These
171
+ // were installed by `init` and protected by nothing, so editing them out
172
+ // fired no rule at all. (.git/hooks/** is outside every git view and so
173
+ // cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
174
+ hooks: [
175
+ ".husky/**",
176
+ "**/lefthook.*",
177
+ // lefthook merges `lefthook-local.yml` OVER `lefthook.yml`: a `skip: true`
178
+ // there switches the gate off without touching the file init pointed at.
179
+ "**/lefthook-local.*",
180
+ ".tamperward.yml",
181
+ "**/.tamperward.yml",
182
+ ".claude/settings.json",
183
+ ".claude/settings.local.json",
184
+ "**/.pre-commit-config.yaml",
185
+ // CODEOWNERS is enforcement wiring, not documentation: it is the only
186
+ // thing that can require a human on a change to the workflow that
187
+ // decides whether the gate runs at all. GitHub reads it from any of
188
+ // these three locations. (H2.)
189
+ ".github/CODEOWNERS",
190
+ "CODEOWNERS",
191
+ "docs/CODEOWNERS"
192
+ ]
193
+ },
194
+ rules: {
195
+ "test-deletion": { severity: "block" },
196
+ // content gutted from a surviving spec — the class Phase 3 measured walking
197
+ // through the block-count rule (rows, assertion arguments, setup). Severity
198
+ // backed by the real-commit corpus measurement recorded in the rule's SPEC row.
199
+ "test-content-removal": { severity: "block" },
200
+ "test-skip": { severity: "block" },
201
+ "ts-any-cast": { severity: "block" },
202
+ // the unambiguous explicit casts + ts-suppression directives
203
+ // broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
204
+ // block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
205
+ "ts-any-launder": { severity: "warn" },
206
+ "lint-suppression": { severity: "block" },
207
+ "coverage-lowering": { severity: "block" },
208
+ "ci-tampering": { severity: "block" },
209
+ "hook-tampering": { severity: "block" },
210
+ "no-verify": { severity: "block" },
211
+ // mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
212
+ // when intended output changes. WARN until the §7 evidence path earns block.
213
+ "snapshot-rewrite": { severity: "warn" },
214
+ // the watcher's transient class (modified-and-restored within a turn). WARN by
215
+ // default: a Stop block already forces restores, and a compliant restore reads
216
+ // as churn — block-by-default would deadlock the honest path. Harnesses that own
217
+ // that semantics raise it via TAMPERWARD_TRANSIENT=block.
218
+ "transient-protected-mutation": { severity: "warn" },
219
+ // the narrow graduation candidate: snapshot moved with NO accompanying change.
220
+ // ~0.06% FP on 1,652 audited mainline commits, 7/7 TP on observed tampers
221
+ // (harness/fp-study/). warn until a version: gate graduates it (BLOCK_SINCE).
222
+ "snapshot-only-rewrite": { severity: "warn" },
223
+ // heuristic — warn until precision is measured (SPEC §7)
224
+ "assertion-weakening": { severity: "warn" },
225
+ "guard-removal": { severity: "warn" }
226
+ },
227
+ ignore: [],
228
+ signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
229
+ };
230
+ p.rules = applyVersionGates(p.rules, version);
231
+ return p;
232
+ }
233
+
234
+ // src/disk.ts
235
+ var READ_CAP = 64 * 1024 * 1024;
236
+ var NO_META = { mode: 0, size: 0, mtimeMs: 0 };
237
+ function errText(e) {
238
+ return e instanceof Error ? e.message : String(e);
239
+ }
240
+ function errCode(e) {
241
+ return String(e?.code ?? "");
242
+ }
243
+ function irregularName(st) {
244
+ if (st.isFIFO()) return "fifo";
245
+ if (st.isSocket()) return "socket";
246
+ if (st.isCharacterDevice()) return "character device";
247
+ if (st.isBlockDevice()) return "block device";
248
+ return "unknown file type";
249
+ }
250
+ function meta(st) {
251
+ return { mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
252
+ }
253
+ var O_READ = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
254
+ function readRegular(abs2, st) {
255
+ let fd;
256
+ try {
257
+ fd = openSync(abs2, O_READ);
258
+ } catch (e) {
259
+ if (errCode(e) === "ELOOP") return inspectOnce(abs2);
260
+ return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
261
+ }
262
+ try {
263
+ const now = fstatSync(fd);
264
+ if (!now.isFile()) return { kind: "irregular", content: null, ...meta(now), detail: irregularName(now) };
265
+ if (now.size > READ_CAP) return { kind: "oversize", content: null, ...meta(now), detail: `${now.size} bytes` };
266
+ const buf = Buffer.allocUnsafe(now.size);
267
+ let off2 = 0;
268
+ while (off2 < now.size) {
269
+ const n = readSync(fd, buf, off2, now.size - off2, off2);
270
+ if (n === 0) break;
271
+ off2 += n;
272
+ }
273
+ return { kind: "file", content: off2 === now.size ? buf : buf.subarray(0, off2), mode: now.mode, size: off2, mtimeMs: now.mtimeMs, detail: "" };
274
+ } catch (e) {
275
+ return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
276
+ } finally {
277
+ closeSync(fd);
278
+ }
279
+ }
280
+ function classify(abs2, st) {
281
+ if (st.isSymbolicLink()) {
282
+ try {
283
+ const target = readlinkSync(abs2);
284
+ return { kind: "symlink", content: Buffer.from(target, "utf8"), ...meta(st), detail: target };
285
+ } catch (e) {
286
+ return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
287
+ }
288
+ }
289
+ if (st.isDirectory()) return { kind: "directory", content: null, ...meta(st), detail: "directory" };
290
+ if (!st.isFile()) return { kind: "irregular", content: null, ...meta(st), detail: irregularName(st) };
291
+ if (st.size > READ_CAP) return { kind: "oversize", content: null, ...meta(st), detail: `${st.size} bytes` };
292
+ return readRegular(abs2, st);
293
+ }
294
+ function inspectOnce(abs2) {
295
+ let st;
296
+ try {
297
+ st = lstatSync(abs2);
298
+ } catch (e) {
299
+ const code = errCode(e);
300
+ if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
301
+ return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
302
+ }
303
+ if (st.isSymbolicLink() || !st.isFile()) return classify(abs2, st);
304
+ return { kind: "unreadable", content: null, ...meta(st), detail: "the path changed shape while it was being read" };
305
+ }
306
+ function inspectPath(abs2) {
307
+ let st;
308
+ try {
309
+ st = lstatSync(abs2);
310
+ } catch (e) {
311
+ const code = errCode(e);
312
+ if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
313
+ return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
314
+ }
315
+ return classify(abs2, st);
316
+ }
317
+ function inspectRel(cwd, rel) {
318
+ return inspectPath(join(cwd, rel));
319
+ }
320
+ function inspectResolved(abs2) {
321
+ let real;
322
+ try {
323
+ real = realpathSync(abs2);
324
+ } catch (e) {
325
+ const code = errCode(e);
326
+ if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
327
+ return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
328
+ }
329
+ return inspectPath(real);
330
+ }
331
+ function textOf(e) {
332
+ return e.content == null ? null : e.content.toString("utf8");
333
+ }
334
+ function describe(e, shown) {
335
+ switch (e.kind) {
336
+ case "symlink":
337
+ return `Protected path ${shown} is a symbolic link to ${escapeControl(e.detail)}. The gate never follows a link, so what the runner will read there cannot be judged.`;
338
+ case "directory":
339
+ return `Protected path ${shown} is a directory where a file is expected, so it cannot be judged.`;
340
+ case "irregular":
341
+ return `Protected path ${shown} is a ${e.detail}, not a regular file, so it cannot be read to judge it.`;
342
+ case "oversize":
343
+ return `Protected file ${shown} is ${e.detail}, above the ${READ_CAP / (1024 * 1024)} MiB the gate reads, so it is judged by name.`;
344
+ default:
345
+ return `Protected file ${shown} cannot be read to judge it (${e.detail}).`;
346
+ }
347
+ }
348
+ function unjudgeableFinding(cwd, rel) {
349
+ const e = inspectRel(cwd, rel);
350
+ if (e.kind === "file" || e.kind === "absent") return null;
351
+ const shown = escapeControl(rel);
352
+ return {
353
+ rule: "hidden-drift",
354
+ severity: "block",
355
+ file: rel,
356
+ message: describe(e, shown),
357
+ evidence: `${shown}: ${e.kind}${e.detail ? ` (${escapeControl(e.detail)})` : ""}`,
358
+ remediation: "Put a regular file the gate can read at the path \u2014 no symbolic link, FIFO, socket or device, and under 64 MiB \u2014 through a tool call the gate can see, or remove it.",
359
+ signoff: { required: true, command: `tamperward allow hidden-drift --file ${shown} --reason "..."` }
360
+ };
361
+ }
362
+ function unjudgeableProtected(cwd, policy, changes) {
363
+ const out2 = [];
364
+ const seen = /* @__PURE__ */ new Set();
365
+ for (const c of changes) {
366
+ if (c.kind !== "file" || c.op === "delete" || seen.has(c.path)) continue;
367
+ seen.add(c.path);
368
+ if (!isProtected(c.path, policy)) continue;
369
+ const f = unjudgeableFinding(cwd, c.path);
370
+ if (f) out2.push(f);
371
+ }
372
+ return out2;
373
+ }
374
+
8
375
  // src/diff/parse.ts
9
376
  var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
10
377
  var C_ESCAPES = { a: 7, b: 8, f: 12, n: 10, r: 13, t: 9, v: 11, "\\": 92, '"': 34 };
@@ -251,11 +618,7 @@ function blobAt(rev, path, cwd) {
251
618
  }
252
619
  }
253
620
  function fromDisk(path, cwd) {
254
- try {
255
- return readFileSync(join(cwd ?? process.cwd(), path), "utf8");
256
- } catch {
257
- return null;
258
- }
621
+ return textOf(inspectRel(cwd ?? process.cwd(), path));
259
622
  }
260
623
  function enrich(c, beforeReader, afterReader) {
261
624
  if (c.kind !== "file") return c;
@@ -353,6 +716,9 @@ function underSkippedDir(rel) {
353
716
  return rel.split("/").some((seg) => SKIP_DIRS.has(seg));
354
717
  }
355
718
  function ignoredPaths(opts = {}, keep) {
719
+ return ignoredTree(opts, keep).files;
720
+ }
721
+ function ignoredTree(opts = {}, keep) {
356
722
  const files = [];
357
723
  const dirs = [];
358
724
  for (const entry of git([...IGNORED_LS, "--directory"], opts.cwd).split("\0").filter(Boolean)) {
@@ -364,7 +730,7 @@ function ignoredPaths(opts = {}, keep) {
364
730
  const expanded = git([...IGNORED_LS, "--", ...dirs.map((d) => `:(literal)${d}`)], opts.cwd).split("\0").filter(Boolean);
365
731
  for (const rel of expanded) if (!underSkippedDir(rel)) files.push(rel);
366
732
  }
367
- return files.filter(keep);
733
+ return { dirs, files: files.filter(keep) };
368
734
  }
369
735
  function ignoredAdds(opts = {}, keep, exclude) {
370
736
  return ignoredPaths(opts, keep).filter((rel) => !exclude?.has(rel)).map((rel) => addOf(rel, opts.cwd));
@@ -588,202 +954,6 @@ function removedLines(c) {
588
954
  return c.hunks.flatMap((h) => h.lines.filter((l) => l.type === "del"));
589
955
  }
590
956
 
591
- // src/policy.ts
592
- import picomatch from "picomatch";
593
- var POLICY_FILE = ".tamperward.yml";
594
- var BLOCK_SINCE = {};
595
- function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
596
- const out2 = { ...rules };
597
- for (const [rule, since] of Object.entries(gates)) {
598
- if (since > version && out2[rule]?.severity === "block") {
599
- out2[rule] = { ...out2[rule], severity: "warn" };
600
- }
601
- }
602
- return out2;
603
- }
604
- function normalizeGlob(glob) {
605
- return glob.startsWith("/") ? glob.replace(/^\/+/, "") : glob;
606
- }
607
- function mergeRules(base, user) {
608
- const out2 = { ...base };
609
- for (const [name, cfg] of Object.entries(user ?? {})) {
610
- out2[name] = { ...out2[name] ?? {}, ...cfg ?? {} };
611
- }
612
- return out2;
613
- }
614
- var cache = /* @__PURE__ */ new Map();
615
- function matcher(glob) {
616
- let m = cache.get(glob);
617
- if (!m) {
618
- m = picomatch(glob, { dot: true });
619
- cache.set(glob, m);
620
- }
621
- return m;
622
- }
623
- function matchesAny(path, globs4) {
624
- return !!globs4 && globs4.some((g) => matcher(g)(path));
625
- }
626
- function protectedCategory(path, policy) {
627
- for (const [cat, globs4] of Object.entries(policy.protected ?? {})) {
628
- if (matchesAny(path, globs4)) return cat;
629
- }
630
- return null;
631
- }
632
- function isProtected(path, policy, category) {
633
- if (category) return matchesAny(path, policy.protected?.[category]);
634
- return protectedCategory(path, policy) !== null;
635
- }
636
- function isPolicyFile(path) {
637
- return path === POLICY_FILE || path.endsWith("/" + POLICY_FILE);
638
- }
639
- function isIgnored(path, policy) {
640
- return matchesAny(path, policy.ignore);
641
- }
642
- function mergeProtected(base, user) {
643
- const out2 = { ...base };
644
- for (const [cat, globs4] of Object.entries(user ?? {})) {
645
- const have = new Set(out2[cat] ?? []);
646
- out2[cat] = [...out2[cat] ?? [], ...(globs4 ?? []).filter((g) => !have.has(g))];
647
- }
648
- return out2;
649
- }
650
- function defaultPolicy(version = 1) {
651
- const p = {
652
- version,
653
- protected: {
654
- // Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
655
- // .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
656
- // instead of warning. Kept ADDITIVE (originals + new extensions) so the policy-diff sees a
657
- // strengthening, not a narrowing. picomatch expands the brace.
658
- // test-skip, test-deletion and test-content-removal all gate on this
659
- // class, so a repo it does not match gets rules that can NEVER fire —
660
- // protection that is silently absent rather than visibly off. The JS/TS
661
- // list shipped alone; these are the conventional layouts of the other
662
- // ecosystems `init` is run in. (P2-13, external review.)
663
- tests: [
664
- // JavaScript / TypeScript
665
- "**/*.test.ts",
666
- "**/*.spec.ts",
667
- "**/*.{test,spec}.{tsx,cts,mts,js,jsx,cjs,mjs}",
668
- "**/__tests__/**",
669
- // Python (pytest / unittest layouts)
670
- "**/test_*.py",
671
- "**/*_test.py",
672
- "**/conftest.py",
673
- // Go
674
- "**/*_test.go",
675
- // Rust (integration tests live in tests/, unit tests inline)
676
- "**/tests/**/*.rs",
677
- // Ruby (RSpec / minitest)
678
- "**/*_spec.rb",
679
- "**/*_test.rb",
680
- // Java / Kotlin / Scala (Maven & Gradle convention)
681
- "**/src/test/**",
682
- // PHP (PHPUnit)
683
- "**/*Test.php",
684
- // C# (.NET)
685
- "**/*Tests.cs",
686
- "**/*Test.cs"
687
- ],
688
- config: [
689
- "**/jest.config.*",
690
- "**/vitest.config.*",
691
- // A `vitest.workspace.*` file REPLACES the root config's selection with the
692
- // union of its projects, and a `vite.config.*` carrying a `test:` key is the
693
- // vitest config when no vitest.config exists — both are where a spec can be
694
- // dropped from the suite without a line of it changing. ADDITIVE, like every
695
- // baseline widening (the policy-diff reads it as a strengthening).
696
- "**/vitest.workspace.*",
697
- "**/vite.config.*",
698
- "**/tsconfig*.json",
699
- "**/.eslintrc*",
700
- "**/eslint.config.*",
701
- "**/package.json",
702
- // Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
703
- // see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
704
- // codecov `target` lived in files no glob protected, so lowering them fired
705
- // nothing. ADDITIVE, like every baseline widening (the policy-diff reads it
706
- // as a strengthening). pyproject.toml carries [tool.coverage.report].
707
- "**/.coveragerc",
708
- "**/pyproject.toml",
709
- "**/.nycrc",
710
- "**/.nycrc.*",
711
- "**/nyc.config.*",
712
- "**/codecov.yml",
713
- "**/.codecov.yml"
714
- ],
715
- ci: [".github/workflows/**"],
716
- // Recorded expected outputs. An assertion stored as data is still an assertion;
717
- // rewriting it from current output is the snapshot-update move the affordance
718
- // experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
719
- // tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
720
- // live there as .cjs and were rewritten unseen (PHASE3.md §4).
721
- snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
722
- // The wiring IS the enforcement: settings.json carries the PreToolUse and
723
- // Stop hooks, and a pre-commit config carries the commit-time gate. These
724
- // were installed by `init` and protected by nothing, so editing them out
725
- // fired no rule at all. (.git/hooks/** is outside every git view and so
726
- // cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
727
- hooks: [
728
- ".husky/**",
729
- "**/lefthook.*",
730
- // lefthook merges `lefthook-local.yml` OVER `lefthook.yml`: a `skip: true`
731
- // there switches the gate off without touching the file init pointed at.
732
- "**/lefthook-local.*",
733
- ".tamperward.yml",
734
- "**/.tamperward.yml",
735
- ".claude/settings.json",
736
- ".claude/settings.local.json",
737
- "**/.pre-commit-config.yaml",
738
- // CODEOWNERS is enforcement wiring, not documentation: it is the only
739
- // thing that can require a human on a change to the workflow that
740
- // decides whether the gate runs at all. GitHub reads it from any of
741
- // these three locations. (H2.)
742
- ".github/CODEOWNERS",
743
- "CODEOWNERS",
744
- "docs/CODEOWNERS"
745
- ]
746
- },
747
- rules: {
748
- "test-deletion": { severity: "block" },
749
- // content gutted from a surviving spec — the class Phase 3 measured walking
750
- // through the block-count rule (rows, assertion arguments, setup). Severity
751
- // backed by the real-commit corpus measurement recorded in the rule's SPEC row.
752
- "test-content-removal": { severity: "block" },
753
- "test-skip": { severity: "block" },
754
- "ts-any-cast": { severity: "block" },
755
- // the unambiguous explicit casts + ts-suppression directives
756
- // broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
757
- // block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
758
- "ts-any-launder": { severity: "warn" },
759
- "lint-suppression": { severity: "block" },
760
- "coverage-lowering": { severity: "block" },
761
- "ci-tampering": { severity: "block" },
762
- "hook-tampering": { severity: "block" },
763
- "no-verify": { severity: "block" },
764
- // mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
765
- // when intended output changes. WARN until the §7 evidence path earns block.
766
- "snapshot-rewrite": { severity: "warn" },
767
- // the watcher's transient class (modified-and-restored within a turn). WARN by
768
- // default: a Stop block already forces restores, and a compliant restore reads
769
- // as churn — block-by-default would deadlock the honest path. Harnesses that own
770
- // that semantics raise it via TAMPERWARD_TRANSIENT=block.
771
- "transient-protected-mutation": { severity: "warn" },
772
- // the narrow graduation candidate: snapshot moved with NO accompanying change.
773
- // ~0.06% FP on 1,652 audited mainline commits, 7/7 TP on observed tampers
774
- // (harness/fp-study/). warn until a version: gate graduates it (BLOCK_SINCE).
775
- "snapshot-only-rewrite": { severity: "warn" },
776
- // heuristic — warn until precision is measured (SPEC §7)
777
- "assertion-weakening": { severity: "warn" },
778
- "guard-removal": { severity: "warn" }
779
- },
780
- ignore: [],
781
- signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
782
- };
783
- p.rules = applyVersionGates(p.rules, version);
784
- return p;
785
- }
786
-
787
957
  // src/detectors/files.ts
788
958
  var CODE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
789
959
  function isCodeFile(path) {
@@ -1164,12 +1334,12 @@ import ts2 from "typescript";
1164
1334
 
1165
1335
  // src/detectors/repo.ts
1166
1336
  import { execFileSync as execFileSync2 } from "node:child_process";
1167
- var cache2 = /* @__PURE__ */ new Map();
1337
+ var cache = /* @__PURE__ */ new Map();
1168
1338
  function trackedFiles(ctx) {
1169
1339
  if (!ctx) return null;
1170
1340
  if (ctx.trackedFiles) return ctx.trackedFiles;
1171
1341
  if (!ctx.cwd) return null;
1172
- const hit = cache2.get(ctx.cwd);
1342
+ const hit = cache.get(ctx.cwd);
1173
1343
  if (hit !== void 0) return hit;
1174
1344
  let files = null;
1175
1345
  try {
@@ -1182,7 +1352,7 @@ function trackedFiles(ctx) {
1182
1352
  } catch {
1183
1353
  files = null;
1184
1354
  }
1185
- cache2.set(ctx.cwd, files);
1355
+ cache.set(ctx.cwd, files);
1186
1356
  return files;
1187
1357
  }
1188
1358
  var branchCache = /* @__PURE__ */ new Map();
@@ -2490,11 +2660,11 @@ var ciTampering = {
2490
2660
  };
2491
2661
 
2492
2662
  // src/detectors/hook-tampering.ts
2493
- import { existsSync, readFileSync as readFileSync3 } from "node:fs";
2663
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
2494
2664
  import { isAbsolute, join as join3, relative } from "node:path";
2495
2665
 
2496
2666
  // src/wiring.ts
2497
- import { readFileSync as readFileSync2 } from "node:fs";
2667
+ import { readFileSync } from "node:fs";
2498
2668
  import { dirname, join as join2 } from "node:path";
2499
2669
  import { fileURLToPath } from "node:url";
2500
2670
  function shippedVersion() {
@@ -2502,7 +2672,7 @@ function shippedVersion() {
2502
2672
  const here = dirname(fileURLToPath(import.meta.url));
2503
2673
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
2504
2674
  try {
2505
- const pkg = JSON.parse(readFileSync2(join2(here, rel), "utf8"));
2675
+ const pkg = JSON.parse(readFileSync(join2(here, rel), "utf8"));
2506
2676
  if (pkg.name === "tamperward" && pkg.version) return pkg.version;
2507
2677
  } catch {
2508
2678
  }
@@ -4151,7 +4321,7 @@ function lefthookBase(c, changes, ctx) {
4151
4321
  for (const n of names) {
4152
4322
  try {
4153
4323
  const p = join3(ctx.cwd, n);
4154
- if (existsSync(p)) return parseDoc(readFileSync3(p, "utf8"));
4324
+ if (existsSync(p)) return parseDoc(readFileSync2(p, "utf8"));
4155
4325
  } catch {
4156
4326
  }
4157
4327
  }
@@ -5386,7 +5556,7 @@ function hasBlocking(findings) {
5386
5556
  }
5387
5557
 
5388
5558
  // src/policy-load.ts
5389
- import { readFileSync as readFileSync4, existsSync as existsSync2 } from "node:fs";
5559
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
5390
5560
  import { isAbsolute as isAbsolute2, join as join4, posix } from "node:path";
5391
5561
  import { parse as parse2 } from "yaml";
5392
5562
  var PolicyError = class extends Error {
@@ -5513,7 +5683,7 @@ function parseOrThrow(src, where) {
5513
5683
  function loadPolicy(cwd = process.cwd()) {
5514
5684
  const path = join4(cwd, POLICY_FILE);
5515
5685
  if (!existsSync2(path)) return defaultPolicy();
5516
- return parseOrThrow(readFileSync4(path, "utf8"), POLICY_FILE);
5686
+ return parseOrThrow(readFileSync3(path, "utf8"), POLICY_FILE);
5517
5687
  }
5518
5688
  function loadPolicyAt(rev, cwd) {
5519
5689
  const src = fileAt(rev, POLICY_FILE, { cwd });
@@ -5523,7 +5693,7 @@ function loadPolicyAt(rev, cwd) {
5523
5693
 
5524
5694
  // src/signoff.ts
5525
5695
  import { createHash } from "node:crypto";
5526
- import { appendFileSync, mkdirSync, readFileSync as readFileSync5, existsSync as existsSync3 } from "node:fs";
5696
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
5527
5697
  import { dirname as dirname2, join as join5 } from "node:path";
5528
5698
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
5529
5699
  function fingerprint(rule, file, evidence) {
@@ -5541,7 +5711,7 @@ function readLedger(cwd, policy) {
5541
5711
  const p = ledgerPath(cwd, policy);
5542
5712
  if (!existsSync3(p)) return [];
5543
5713
  const out2 = [];
5544
- for (const line of readFileSync5(p, "utf8").split("\n")) {
5714
+ for (const line of readFileSync4(p, "utf8").split("\n")) {
5545
5715
  if (!line.trim()) continue;
5546
5716
  try {
5547
5717
  const e = JSON.parse(line);
@@ -5694,7 +5864,7 @@ function renderText(input, opts) {
5694
5864
  for (const f of findings) {
5695
5865
  const isBlock = f.severity === "block";
5696
5866
  const mark = paint(isBlock ? "BLOCK" : "warn ", isBlock ? BOLD + RED : YELLOW, c);
5697
- const loc = f.file ? `${stripControl(f.file)}${f.line ? `:${f.line}` : ""}` : "(command)";
5867
+ const loc = f.file ? `${stripControl(escapeControl(f.file))}${f.line ? `:${f.line}` : ""}` : "(command)";
5698
5868
  out2.push(` ${mark} ${paint(stripControl(f.rule), BOLD, c)} ${paint(loc, DIM, c)}`);
5699
5869
  for (const l of wrap(f.message, opts.width - 4)) out2.push(` ${l}`);
5700
5870
  const row = (key2, lines) => {
@@ -5886,6 +6056,7 @@ function check(opts) {
5886
6056
  }
5887
6057
  const ignoredFiles = changes.filter((c) => isSuppressed(c, policy)).length;
5888
6058
  let findings = evaluate(changes, policy, void 0, view, { cwd });
6059
+ if (view === "worktree") findings = findings.concat(unjudgeableProtected(cwd, policy, changes));
5889
6060
  const { findings: remaining, cleared } = layer === "local" ? applyLocalSignoffs(findings, cwd, policy) : applyOobSignoffs(findings, oobFromEnv(), oobHeadFromEnv());
5890
6061
  findings = remaining;
5891
6062
  if (cleared.length) {
@@ -5910,24 +6081,19 @@ function runCheck(opts) {
5910
6081
  }
5911
6082
 
5912
6083
  // src/cli/hook.ts
5913
- import { readFileSync as readFileSync11, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, statSync as statSync4 } from "node:fs";
6084
+ import { readFileSync as readFileSync9, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
5914
6085
  import { createHash as createHash4 } from "node:crypto";
5915
- import { join as join10 } from "node:path";
5916
6086
 
5917
6087
  // src/adapters/claude/changes.ts
5918
6088
  import { execFileSync as execFileSync3 } from "node:child_process";
5919
- import { readFileSync as readFileSync6, existsSync as existsSync4, writeFileSync, mkdtempSync, rmSync } from "node:fs";
6089
+ import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
5920
6090
  import { tmpdir } from "node:os";
5921
6091
  import { isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve } from "node:path";
5922
6092
  function asStr(v) {
5923
6093
  return typeof v === "string" ? v : "";
5924
6094
  }
5925
6095
  function readDisk(path) {
5926
- try {
5927
- return existsSync4(path) ? readFileSync6(path, "utf8") : null;
5928
- } catch {
5929
- return null;
5930
- }
6096
+ return textOf(inspectResolved(path));
5931
6097
  }
5932
6098
  function abs(path, cwd) {
5933
6099
  return resolve(cwd, path);
@@ -6021,7 +6187,7 @@ function formatDenial(blocks) {
6021
6187
  const lead = blocks[0];
6022
6188
  const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
6023
6189
  for (const f of blocks) {
6024
- const loc = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
6190
+ const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
6025
6191
  lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
6026
6192
  }
6027
6193
  lines.push("");
@@ -6036,7 +6202,7 @@ function formatDenial(blocks) {
6036
6202
  }
6037
6203
 
6038
6204
  // src/session.ts
6039
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
6205
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
6040
6206
  import { dirname as dirname3, join as join7 } from "node:path";
6041
6207
  var UNSAFE = /[^A-Za-z0-9_-]/g;
6042
6208
  var SHA = /^[0-9a-f]{7,40}$/;
@@ -6050,8 +6216,8 @@ function turnBaseline(cwd, sessionId) {
6050
6216
  try {
6051
6217
  const p = baselinePath(cwd, sessionId);
6052
6218
  if (!p) return null;
6053
- if (existsSync5(p)) {
6054
- const v = readFileSync7(p, "utf8").trim();
6219
+ if (existsSync4(p)) {
6220
+ const v = readFileSync5(p, "utf8").trim();
6055
6221
  if (SHA.test(v)) return v;
6056
6222
  }
6057
6223
  const head = headSha(cwd);
@@ -6077,7 +6243,7 @@ function advanceTurnBaseline(cwd, sessionId) {
6077
6243
 
6078
6244
  // src/effect.ts
6079
6245
  import { createHash as createHash2 } from "node:crypto";
6080
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
6246
+ import { existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
6081
6247
  import { isAbsolute as isAbsolute4, join as join8 } from "node:path";
6082
6248
  import { execFileSync as execFileSync4 } from "node:child_process";
6083
6249
  var UNSAFE2 = /[^A-Za-z0-9._-]/g;
@@ -6112,8 +6278,22 @@ var sha = (buf) => createHash2("sha256").update(buf).digest("hex").slice(0, 16);
6112
6278
  function contentHash(content) {
6113
6279
  return sha(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"));
6114
6280
  }
6281
+ function entryOf(e) {
6282
+ const hash = e.content != null ? sha(e.content) : e.kind === "unreadable" ? "unreadable" : `${e.kind}:${e.detail}`;
6283
+ return { hash, mode: e.mode, size: e.size, mtimeMs: e.mtimeMs };
6284
+ }
6115
6285
  function snapshotProtected(cwd, policy, prev) {
6286
+ void prev;
6116
6287
  const out2 = {};
6288
+ const keep = (rel) => isProtected(rel, policy);
6289
+ let pruned = /* @__PURE__ */ new Set();
6290
+ let listed = [];
6291
+ try {
6292
+ const t = ignoredTree({ cwd }, keep);
6293
+ pruned = new Set(t.dirs.map((d) => d.replace(/\/$/, "")));
6294
+ listed = t.files;
6295
+ } catch {
6296
+ }
6117
6297
  const walk2 = (rel) => {
6118
6298
  const abs2 = rel ? join8(cwd, rel) : cwd;
6119
6299
  let names;
@@ -6125,25 +6305,19 @@ function snapshotProtected(cwd, policy, prev) {
6125
6305
  for (const name of names) {
6126
6306
  if (SKIP_DIRS2.has(name)) continue;
6127
6307
  const r = rel ? `${rel}/${name}` : name;
6308
+ if (pruned.has(r)) continue;
6128
6309
  let st;
6129
6310
  try {
6130
- st = statSync(join8(cwd, r));
6311
+ st = lstatSync2(join8(cwd, r));
6131
6312
  } catch {
6132
6313
  continue;
6133
6314
  }
6134
6315
  if (st.isDirectory()) walk2(r);
6135
- else if (st.isFile() && isProtected(r, policy)) {
6136
- let hash = "";
6137
- try {
6138
- hash = sha(readFileSync8(join8(cwd, r)));
6139
- } catch {
6140
- hash = "unreadable";
6141
- }
6142
- out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
6143
- }
6316
+ else if (keep(r)) out2[r] = entryOf(inspectRel(cwd, r));
6144
6317
  }
6145
6318
  };
6146
6319
  walk2("");
6320
+ for (const rel of listed) if (out2[rel] === void 0) out2[rel] = entryOf(inspectRel(cwd, rel));
6147
6321
  return out2;
6148
6322
  }
6149
6323
  function driftBetween(expected, current) {
@@ -6161,9 +6335,9 @@ function driftBetween(expected, current) {
6161
6335
  return { changed, deleted, added };
6162
6336
  }
6163
6337
  function loadTree(p) {
6164
- if (!p || !existsSync6(p)) return null;
6338
+ if (!p || !existsSync5(p)) return null;
6165
6339
  try {
6166
- return JSON.parse(readFileSync8(p, "utf8"));
6340
+ return JSON.parse(readFileSync6(p, "utf8"));
6167
6341
  } catch {
6168
6342
  return null;
6169
6343
  }
@@ -6202,19 +6376,16 @@ function dropTurnTree(cwd, sessionId) {
6202
6376
  }
6203
6377
 
6204
6378
  // src/cli/watch.ts
6205
- import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync2, watch } from "node:fs";
6379
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
6206
6380
  import { join as join9 } from "node:path";
6207
6381
  import { createHash as createHash3 } from "node:crypto";
6208
6382
  var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
6209
6383
  function snap(abs2) {
6210
- try {
6211
- const st = statSync2(abs2);
6212
- if (!st.isFile()) return { mode: st.mode, size: null, hash: null };
6213
- const hash = createHash3("sha256").update(readFileSync9(abs2)).digest("hex").slice(0, 16);
6214
- return { mode: st.mode, size: st.size, hash };
6215
- } catch {
6216
- return { mode: null, size: null, hash: null };
6217
- }
6384
+ const e = inspectPath(abs2);
6385
+ if (e.kind === "absent" || e.kind === "unreadable") return { mode: null, size: null, hash: null };
6386
+ if (e.kind !== "file" || e.content == null) return { mode: e.mode, size: null, hash: null };
6387
+ const hash = createHash3("sha256").update(e.content).digest("hex").slice(0, 16);
6388
+ return { mode: e.mode, size: e.size, hash };
6218
6389
  }
6219
6390
  function defaultEventLog(cwd) {
6220
6391
  if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
@@ -6240,7 +6411,7 @@ function watchTree(dir, cb) {
6240
6411
  const child = rel ? `${rel}/${String(fname)}` : String(fname);
6241
6412
  cb(kind, child);
6242
6413
  try {
6243
- if (statSync2(join9(dir, child)).isDirectory()) addDir(child);
6414
+ if (statSync(join9(dir, child)).isDirectory()) addDir(child);
6244
6415
  } catch {
6245
6416
  }
6246
6417
  });
@@ -6261,7 +6432,7 @@ function watchTree(dir, cb) {
6261
6432
  const child = rel ? `${rel}/${name}` : name;
6262
6433
  if (SKIP2.test(child + "/")) continue;
6263
6434
  try {
6264
- if (statSync2(join9(dir, child)).isDirectory()) walk2(child);
6435
+ if (statSync(join9(dir, child)).isDirectory()) walk2(child);
6265
6436
  } catch {
6266
6437
  }
6267
6438
  }
@@ -6306,17 +6477,17 @@ function runWatch(args) {
6306
6477
  }
6307
6478
 
6308
6479
  // src/detectors/fs-events.ts
6309
- import { readFileSync as readFileSync10, statSync as statSync3 } from "node:fs";
6480
+ import { readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
6310
6481
  var RULE11 = "transient-protected-mutation";
6311
6482
  function readEvents(log, offset) {
6312
6483
  let size = 0;
6313
6484
  try {
6314
- size = statSync3(log).size;
6485
+ size = statSync2(log).size;
6315
6486
  } catch {
6316
6487
  return { events: [], newOffset: offset };
6317
6488
  }
6318
6489
  if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
6319
- const raw = readFileSync10(log, "utf8").slice(offset);
6490
+ const raw = readFileSync8(log, "utf8").slice(offset);
6320
6491
  const events = [];
6321
6492
  for (const line of raw.split("\n")) {
6322
6493
  if (!line.trim()) continue;
@@ -6369,7 +6540,7 @@ var HookInputError = class extends Error {
6369
6540
  };
6370
6541
  function readStdin() {
6371
6542
  try {
6372
- return readFileSync11(0, "utf8");
6543
+ return readFileSync9(0, "utf8");
6373
6544
  } catch (e) {
6374
6545
  throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
6375
6546
  }
@@ -6395,7 +6566,7 @@ function recordDenylog(blocks) {
6395
6566
  } catch {
6396
6567
  }
6397
6568
  }
6398
- function errText(e) {
6569
+ function errText2(e) {
6399
6570
  const m = e instanceof Error ? e.message : String(e);
6400
6571
  return m.length > 200 ? m.slice(0, 200) + "\u2026" : m;
6401
6572
  }
@@ -6427,11 +6598,8 @@ function fileChanges(changes) {
6427
6598
  return changes.filter((c) => c.kind === "file");
6428
6599
  }
6429
6600
  function hashOnDisk(cwd, rel) {
6430
- try {
6431
- return contentHash(readFileSync11(join10(cwd, rel)));
6432
- } catch {
6433
- return null;
6434
- }
6601
+ const e = inspectRel(cwd, rel);
6602
+ return e.content == null ? null : contentHash(e.content);
6435
6603
  }
6436
6604
  function gitMode(mode) {
6437
6605
  return (mode & 64) !== 0 ? "100755" : "100644";
@@ -6478,6 +6646,11 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
6478
6646
  const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
6479
6647
  for (const rel of [...d.changed, ...d.deleted]) {
6480
6648
  if (seen.has(rel)) continue;
6649
+ const unjudgeable = unjudgeableFinding(cwd, rel);
6650
+ if (unjudgeable) {
6651
+ blocks.push(unjudgeable);
6652
+ continue;
6653
+ }
6481
6654
  const after = fileOnDisk(rel, { cwd });
6482
6655
  const trustedBlob = fileAt(trusted, rel, { cwd });
6483
6656
  const atGitState = after != null && trustedBlob != null && contentHash(after) === contentHash(trustedBlob);
@@ -6526,6 +6699,11 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
6526
6699
  }
6527
6700
  for (const rel of d.added) {
6528
6701
  if (seen.has(rel)) continue;
6702
+ const unjudgeable = unjudgeableFinding(cwd, rel);
6703
+ if (unjudgeable) {
6704
+ blocks.push(unjudgeable);
6705
+ continue;
6706
+ }
6529
6707
  const after = fileOnDisk(rel, { cwd });
6530
6708
  if (after == null) {
6531
6709
  blocks.push({
@@ -6565,7 +6743,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
6565
6743
  (c) => c.kind === "file" && (drifted.has(c.path) || c.oldPath != null && drifted.has(c.oldPath))
6566
6744
  );
6567
6745
  const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
6568
- const blocks = evaluate([...scoped, ...gap.changes], policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks);
6746
+ const judged = [...scoped, ...gap.changes];
6747
+ const blocks = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
6569
6748
  if (blocks.length > 0) return blocks;
6570
6749
  savePtree(cwd, sessionId, current);
6571
6750
  return null;
@@ -6602,7 +6781,7 @@ function preToolUseVerdict(input) {
6602
6781
  if (blocks.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
6603
6782
  return verdict(blocks, "PreToolUse");
6604
6783
  } catch (e) {
6605
- return failClosed("PreToolUse", errText(e));
6784
+ return failClosed("PreToolUse", errText2(e));
6606
6785
  }
6607
6786
  }
6608
6787
  function cursorPath(cwd, sessionId) {
@@ -6614,10 +6793,10 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
6614
6793
  } };
6615
6794
  const log = defaultEventLog(cwd);
6616
6795
  const cp = cursorPath(cwd, sessionId);
6617
- if (!existsSync7(log)) return none;
6796
+ if (!existsSync6(log)) return none;
6618
6797
  let offset = 0;
6619
- if (cp && existsSync7(cp)) {
6620
- const n = Number(readFileSync11(cp, "utf8"));
6798
+ if (cp && existsSync6(cp)) {
6799
+ const n = Number(readFileSync9(cp, "utf8"));
6621
6800
  offset = Number.isFinite(n) ? n : 0;
6622
6801
  }
6623
6802
  const { events, newOffset } = readEvents(log, offset);
@@ -6629,12 +6808,8 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
6629
6808
  } };
6630
6809
  const persistent = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
6631
6810
  const finalHash = (path) => {
6632
- try {
6633
- if (!statSync4(`${cwd}/${path}`).isFile()) return null;
6634
- return createHash4("sha256").update(readFileSync11(`${cwd}/${path}`)).digest("hex").slice(0, 16);
6635
- } catch {
6636
- return null;
6637
- }
6811
+ const e = inspectRel(cwd, path);
6812
+ return e.kind === "file" && e.content != null ? contentHash(e.content) : null;
6638
6813
  };
6639
6814
  const findings = transientFindings(events, persistent, policy, finalHash);
6640
6815
  recordWarns(findings.filter((f) => f.severity !== "block"));
@@ -6675,7 +6850,7 @@ function stopVerdict(input) {
6675
6850
  changes = changes.concat(gap.changes);
6676
6851
  hiddenBlocks = gap.blocks;
6677
6852
  }
6678
- blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks);
6853
+ blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
6679
6854
  const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
6680
6855
  blocks = blocks.concat(transient.blocks);
6681
6856
  commitCursor = transient.commit;
@@ -6684,7 +6859,7 @@ function stopVerdict(input) {
6684
6859
  saveTurnTree(cwd, input.session_id, current);
6685
6860
  }
6686
6861
  } catch (e) {
6687
- return failClosed("Stop", errText(e));
6862
+ return failClosed("Stop", errText2(e));
6688
6863
  }
6689
6864
  if (blocks.length === 0) {
6690
6865
  advanceTurnBaseline(cwd, input.session_id);
@@ -6700,28 +6875,28 @@ function preToolUseFromRaw(raw) {
6700
6875
  try {
6701
6876
  return preToolUseVerdict(parseInput(raw));
6702
6877
  } catch (e) {
6703
- return failClosed("PreToolUse", errText(e));
6878
+ return failClosed("PreToolUse", errText2(e));
6704
6879
  }
6705
6880
  }
6706
6881
  function stopFromRaw(raw) {
6707
6882
  try {
6708
6883
  return stopVerdict(parseInput(raw));
6709
6884
  } catch (e) {
6710
- return failClosed("Stop", errText(e));
6885
+ return failClosed("Stop", errText2(e));
6711
6886
  }
6712
6887
  }
6713
6888
  function runHookClaude() {
6714
6889
  try {
6715
6890
  return emit(preToolUseFromRaw(readStdin()));
6716
6891
  } catch (e) {
6717
- return emit(failClosed("PreToolUse", errText(e)));
6892
+ return emit(failClosed("PreToolUse", errText2(e)));
6718
6893
  }
6719
6894
  }
6720
6895
  function runSweepClaude() {
6721
6896
  try {
6722
6897
  return emit(stopFromRaw(readStdin()));
6723
6898
  } catch (e) {
6724
- return emit(failClosed("Stop", errText(e)));
6899
+ return emit(failClosed("Stop", errText2(e)));
6725
6900
  }
6726
6901
  }
6727
6902
 
@@ -6774,9 +6949,9 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
6774
6949
  // src/cli/init.ts
6775
6950
  import { execFileSync as execFileSync5 } from "node:child_process";
6776
6951
  import { createHash as createHash5 } from "node:crypto";
6777
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5, chmodSync, statSync as statSync5 } from "node:fs";
6778
- import { basename, dirname as dirname4, isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve2 } from "node:path";
6779
- var errText2 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
6952
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
6953
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve2 } from "node:path";
6954
+ var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
6780
6955
  var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6781
6956
  function gitConfig(cwd, key2) {
6782
6957
  try {
@@ -6925,13 +7100,13 @@ function readWorkflowMark(src) {
6925
7100
  if (!m) return null;
6926
7101
  return { version: m[1], hash: m[2], body: src.replace(WORKFLOW_MARK_RE, "") };
6927
7102
  }
6928
- function toolSet(matcher2) {
6929
- const m = String(matcher2 ?? "").trim();
7103
+ function toolSet(matcher) {
7104
+ const m = String(matcher ?? "").trim();
6930
7105
  if (m === "" || m === "*") return null;
6931
7106
  return new Set(m.split("|").map((t) => t.trim()).filter(Boolean));
6932
7107
  }
6933
- function missingTools(matcher2) {
6934
- const have = toolSet(matcher2);
7108
+ function missingTools(matcher) {
7109
+ const have = toolSet(matcher);
6935
7110
  if (have === null) return [];
6936
7111
  return PRE_MATCHER.split("|").filter((t) => !have.has(t));
6937
7112
  }
@@ -6951,12 +7126,12 @@ function hooksShapeError(hooks) {
6951
7126
  return null;
6952
7127
  }
6953
7128
  function planPolicy(cwd) {
6954
- const path = join11(cwd, POLICY_FILE);
6955
- if (existsSync8(path)) {
7129
+ const path = join10(cwd, POLICY_FILE);
7130
+ if (existsSync7(path)) {
6956
7131
  try {
6957
7132
  loadPolicy(cwd);
6958
7133
  } catch (e) {
6959
- return { item: "policy", path: POLICY_FILE, status: "error", detail: `present but does not load \u2014 ${errText2(e)}; fix it, then re-run init (left untouched)` };
7134
+ return { item: "policy", path: POLICY_FILE, status: "error", detail: `present but does not load \u2014 ${errText3(e)}; fix it, then re-run init (left untouched)` };
6960
7135
  }
6961
7136
  return { item: "policy", path: POLICY_FILE, status: "ok", detail: "already present and loads \u2014 left untouched" };
6962
7137
  }
@@ -6970,12 +7145,12 @@ function planPolicy(cwd) {
6970
7145
  }
6971
7146
  function planClaudeHooks(cwd) {
6972
7147
  const rel = ".claude/settings.json";
6973
- const path = join11(cwd, rel);
7148
+ const path = join10(cwd, rel);
6974
7149
  let settings = {};
6975
- if (existsSync8(path)) {
7150
+ if (existsSync7(path)) {
6976
7151
  let parsed;
6977
7152
  try {
6978
- parsed = JSON.parse(readFileSync12(path, "utf8"));
7153
+ parsed = JSON.parse(readFileSync10(path, "utf8"));
6979
7154
  } catch {
6980
7155
  return {
6981
7156
  item: "agent",
@@ -7009,7 +7184,7 @@ function planClaudeHooks(cwd) {
7009
7184
  return {
7010
7185
  item: "agent",
7011
7186
  path: rel,
7012
- status: existsSync8(path) ? "update" : "create",
7187
+ status: existsSync7(path) ? "update" : "create",
7013
7188
  detail: [
7014
7189
  needPre && "wire PreToolUse deny",
7015
7190
  needStop && "wire Stop sweep",
@@ -7068,7 +7243,7 @@ function hooksDir(cwd) {
7068
7243
  if (out2) return resolve2(cwd, out2);
7069
7244
  } catch {
7070
7245
  }
7071
- return existsSync8(join11(cwd, ".git")) ? join11(cwd, ".git", "hooks") : null;
7246
+ return existsSync7(join10(cwd, ".git")) ? join10(cwd, ".git", "hooks") : null;
7072
7247
  }
7073
7248
  function display(cwd, path) {
7074
7249
  const rel = relative3(cwd, path);
@@ -7095,30 +7270,30 @@ function planPreCommit(cwd) {
7095
7270
  const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
7096
7271
  let path;
7097
7272
  let note;
7098
- if (existsSync8(join11(cwd, ".husky"))) {
7099
- path = join11(cwd, ".husky", "pre-commit");
7273
+ if (existsSync7(join10(cwd, ".husky"))) {
7274
+ path = join10(cwd, ".husky", "pre-commit");
7100
7275
  note = "husky";
7101
7276
  } else {
7102
- const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(join11(cwd, c))));
7277
+ const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync7(join10(cwd, c))));
7103
7278
  if (managed) {
7104
7279
  return skip(`${managed.name} manages this repository's hooks and regenerates them on install, so a line in the hooks directory would not last \u2014 ${managed.hint}`);
7105
7280
  }
7106
7281
  const dir = hooksDir(cwd);
7107
7282
  if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
7108
7283
  if (basename(dir) === "_" && basename(dirname4(dir)) === ".husky") {
7109
- path = join11(dirname4(dir), "pre-commit");
7284
+ path = join10(dirname4(dir), "pre-commit");
7110
7285
  note = "husky";
7111
7286
  } else {
7112
- path = join11(dir, "pre-commit");
7287
+ path = join10(dir, "pre-commit");
7113
7288
  const rel2 = display(cwd, dir);
7114
- note = rel2 === join11(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
7289
+ note = rel2 === join10(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
7115
7290
  }
7116
7291
  }
7117
7292
  const rel = display(cwd, path);
7118
- if (existsSync8(path) && !statSync5(path).isFile()) {
7293
+ if (existsSync7(path) && !statSync3(path).isFile()) {
7119
7294
  return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
7120
7295
  }
7121
- const existing = existsSync8(path) ? readFileSync12(path, "utf8") : null;
7296
+ const existing = existsSync7(path) ? readFileSync10(path, "utf8") : null;
7122
7297
  if (existing === null) {
7123
7298
  return {
7124
7299
  item: "pre-commit",
@@ -7207,10 +7382,10 @@ ${block.join("\n")}
7207
7382
  function planCodeowners(cwd) {
7208
7383
  const rel = ".github/CODEOWNERS";
7209
7384
  const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
7210
- (r) => existsSync8(join11(cwd, r))
7385
+ (r) => existsSync7(join10(cwd, r))
7211
7386
  );
7212
- const path = join11(cwd, existingRel ?? rel);
7213
- const existing = existingRel ? readFileSync12(path, "utf8") : null;
7387
+ const path = join10(cwd, existingRel ?? rel);
7388
+ const existing = existingRel ? readFileSync10(path, "utf8") : null;
7214
7389
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
7215
7390
  if (missing.length === 0) {
7216
7391
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
@@ -7243,12 +7418,12 @@ ${CODEOWNERS_MARK}
7243
7418
  }
7244
7419
  function planWorkflow(cwd, force) {
7245
7420
  const rel = ".github/workflows/tamperward.yml";
7246
- const path = join11(cwd, rel);
7421
+ const path = join10(cwd, rel);
7247
7422
  const write = () => {
7248
7423
  mkdirSync5(dirname4(path), { recursive: true });
7249
7424
  writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
7250
7425
  };
7251
- if (!existsSync8(path)) {
7426
+ if (!existsSync7(path)) {
7252
7427
  return {
7253
7428
  item: "ci",
7254
7429
  path: rel,
@@ -7259,7 +7434,7 @@ function planWorkflow(cwd, force) {
7259
7434
  }
7260
7435
  let src;
7261
7436
  try {
7262
- src = readFileSync12(path, "utf8");
7437
+ src = readFileSync10(path, "utf8");
7263
7438
  } catch {
7264
7439
  return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
7265
7440
  }
@@ -7297,7 +7472,7 @@ function planned(item, path, plan) {
7297
7472
  try {
7298
7473
  return plan();
7299
7474
  } catch (e) {
7300
- return { item, path, status: "error", detail: `cannot plan this item \u2014 ${errText2(e)}` };
7475
+ return { item, path, status: "error", detail: `cannot plan this item \u2014 ${errText3(e)}` };
7301
7476
  }
7302
7477
  }
7303
7478
  function planInit(cwd, opts = {}) {
@@ -7321,7 +7496,7 @@ function runInit(opts) {
7321
7496
  applied++;
7322
7497
  } catch (e) {
7323
7498
  a.status = "error";
7324
- a.detail = `could not be written \u2014 ${errText2(e)}`;
7499
+ a.detail = `could not be written \u2014 ${errText3(e)}`;
7325
7500
  a.apply = void 0;
7326
7501
  }
7327
7502
  }
@@ -7358,15 +7533,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
7358
7533
  // src/cli/verify.ts
7359
7534
  import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
7360
7535
  import { createHash as createHash8 } from "node:crypto";
7361
- import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, readlinkSync as readlinkSync2, rmSync as rmSync3, statSync as statSync7, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
7536
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "node:fs";
7362
7537
  import { tmpdir as tmpdir2 } from "node:os";
7363
- import { dirname as dirname5, join as join14, resolve as resolve3 } from "node:path";
7538
+ import { dirname as dirname5, join as join13, resolve as resolve3 } from "node:path";
7364
7539
 
7365
7540
  // src/git/trusted.ts
7366
7541
  import { execFileSync as execFileSync6 } from "node:child_process";
7367
7542
  import { createHash as createHash6 } from "node:crypto";
7368
- import { existsSync as existsSync9, readFileSync as readFileSync13 } from "node:fs";
7369
- import { isAbsolute as isAbsolute6, join as join12 } from "node:path";
7543
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
7544
+ import { isAbsolute as isAbsolute6, join as join11 } from "node:path";
7370
7545
  function trustedGitEnv() {
7371
7546
  return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
7372
7547
  }
@@ -7379,7 +7554,7 @@ function gitDirOf(cwd) {
7379
7554
  env: trustedGitEnv()
7380
7555
  }).trim();
7381
7556
  if (!d) return null;
7382
- return isAbsolute6(d) ? d : join12(cwd, d);
7557
+ return isAbsolute6(d) ? d : join11(cwd, d);
7383
7558
  } catch {
7384
7559
  return null;
7385
7560
  }
@@ -7404,9 +7579,9 @@ function objectRewriteState(cwd) {
7404
7579
  parts.push(`${rel}:<no-git-dir>`);
7405
7580
  continue;
7406
7581
  }
7407
- const p = join12(dir, rel);
7582
+ const p = join11(dir, rel);
7408
7583
  try {
7409
- parts.push(`${rel}:` + (existsSync9(p) ? readFileSync13(p, "utf8").trim() : ""));
7584
+ parts.push(`${rel}:` + (existsSync8(p) ? readFileSync11(p, "utf8").trim() : ""));
7410
7585
  } catch {
7411
7586
  parts.push(`${rel}:<unreadable>`);
7412
7587
  }
@@ -7417,8 +7592,8 @@ function objectRewriteState(cwd) {
7417
7592
  // src/fingerprint.ts
7418
7593
  import { execFileSync as execFileSync7 } from "node:child_process";
7419
7594
  import { createHash as createHash7 } from "node:crypto";
7420
- import { readdirSync as readdirSync3, readFileSync as readFileSync14, readlinkSync, statSync as statSync6 } from "node:fs";
7421
- import { join as join13 } from "node:path";
7595
+ import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
7596
+ import { join as join12 } from "node:path";
7422
7597
  function treeFingerprint(cwd) {
7423
7598
  const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
7424
7599
  cwd,
@@ -7430,17 +7605,13 @@ function treeFingerprint(cwd) {
7430
7605
  for (const rel of files) {
7431
7606
  h.update(rel);
7432
7607
  h.update("\0");
7433
- try {
7434
- h.update(readFileSync14(join13(cwd, rel)));
7435
- } catch {
7436
- h.update("<unreadable>");
7437
- }
7608
+ h.update(inspectRel(cwd, rel).content ?? "<unreadable>");
7438
7609
  h.update("\0");
7439
7610
  }
7440
7611
  return h.digest("hex");
7441
7612
  }
7442
7613
  function depsFingerprint(cwd) {
7443
- const root = join13(cwd, "node_modules");
7614
+ const root = join12(cwd, "node_modules");
7444
7615
  const h = createHash7("sha256");
7445
7616
  const walk2 = (dir) => {
7446
7617
  let entries;
@@ -7450,20 +7621,20 @@ function depsFingerprint(cwd) {
7450
7621
  return;
7451
7622
  }
7452
7623
  for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
7453
- const p = join13(dir, e.name);
7624
+ const p = join12(dir, e.name);
7454
7625
  if (e.isDirectory()) walk2(p);
7455
7626
  else if (e.isFile()) {
7456
7627
  h.update(p);
7457
7628
  try {
7458
- h.update(readFileSync14(p));
7459
- h.update(String(statSync6(p).mode));
7629
+ h.update(readFileSync12(p));
7630
+ h.update(String(statSync4(p).mode));
7460
7631
  } catch {
7461
7632
  h.update("<unreadable>");
7462
7633
  }
7463
7634
  } else if (e.isSymbolicLink()) {
7464
7635
  h.update(p);
7465
7636
  try {
7466
- h.update(readlinkSync(p));
7637
+ h.update(readlinkSync2(p));
7467
7638
  } catch {
7468
7639
  h.update("<unreadable-link>");
7469
7640
  }
@@ -7471,7 +7642,7 @@ function depsFingerprint(cwd) {
7471
7642
  }
7472
7643
  };
7473
7644
  try {
7474
- if (!statSync6(root).isDirectory()) return "none";
7645
+ if (!statSync4(root).isDirectory()) return "none";
7475
7646
  } catch {
7476
7647
  return "none";
7477
7648
  }
@@ -7570,30 +7741,30 @@ function baseIsAncestorOfHead(base, cwd) {
7570
7741
  function materialize(cwd, dest) {
7571
7742
  const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
7572
7743
  for (const rel of listed) {
7573
- const src = join14(cwd, rel);
7744
+ const src = join13(cwd, rel);
7574
7745
  let st;
7575
7746
  try {
7576
- st = statSync7(src);
7747
+ st = statSync5(src);
7577
7748
  } catch {
7578
7749
  continue;
7579
7750
  }
7580
7751
  if (!st.isFile()) continue;
7581
- const out2 = join14(dest, rel);
7752
+ const out2 = join13(dest, rel);
7582
7753
  mkdirSync6(dirname5(out2), { recursive: true });
7583
7754
  dropSymlink(out2);
7584
- if (lstatSync(src).isSymbolicLink()) {
7585
- writeFileSync6(out2, readFileSync15(src));
7755
+ if (lstatSync3(src).isSymbolicLink()) {
7756
+ writeFileSync6(out2, readFileSync13(src));
7586
7757
  } else {
7587
7758
  cpSync(src, out2, { dereference: true });
7588
7759
  }
7589
7760
  chmodSync2(out2, st.mode);
7590
7761
  }
7591
7762
  const nm = resolve3(cwd, "node_modules");
7592
- if (existsSync10(nm)) symlinkSync(nm, join14(dest, "node_modules"), "dir");
7763
+ if (existsSync9(nm)) symlinkSync(nm, join13(dest, "node_modules"), "dir");
7593
7764
  }
7594
7765
  function dropSymlink(p) {
7595
7766
  try {
7596
- if (lstatSync(p).isSymbolicLink()) rmSync3(p, { force: true });
7767
+ if (lstatSync3(p).isSymbolicLink()) rmSync3(p, { force: true });
7597
7768
  } catch {
7598
7769
  }
7599
7770
  }
@@ -7622,11 +7793,11 @@ function overlayDigest(dest, paths) {
7622
7793
  for (const rel of [...paths].sort()) {
7623
7794
  h.update(rel);
7624
7795
  h.update("\0");
7625
- const p = join14(dest, rel);
7796
+ const p = join13(dest, rel);
7626
7797
  try {
7627
- const st = lstatSync(p);
7798
+ const st = lstatSync3(p);
7628
7799
  h.update(String(st.mode));
7629
- h.update(st.isSymbolicLink() ? readlinkSync2(p) : readFileSync15(p));
7800
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync13(p));
7630
7801
  } catch {
7631
7802
  h.update("<gone>");
7632
7803
  }
@@ -7649,7 +7820,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
7649
7820
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
7650
7821
  }
7651
7822
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
7652
- const out2 = join14(dest, e.path);
7823
+ const out2 = join13(dest, e.path);
7653
7824
  mkdirSync6(dirname5(out2), { recursive: true });
7654
7825
  rmSync3(out2, { force: true });
7655
7826
  if (e.mode === "120000") {
@@ -7664,7 +7835,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
7664
7835
  const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
7665
7836
  for (const rel of inCopy) {
7666
7837
  if (!isOverlay(rel) || baseProtected.has(rel)) continue;
7667
- rmSync3(join14(dest, rel), { force: true });
7838
+ rmSync3(join13(dest, rel), { force: true });
7668
7839
  removed++;
7669
7840
  }
7670
7841
  return { restored, removed };
@@ -7706,8 +7877,8 @@ function suiteEnv(scratch) {
7706
7877
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
7707
7878
  env[k] = v;
7708
7879
  }
7709
- const userRc = join14(scratch, "npmrc-user");
7710
- const globalRc = join14(scratch, "npmrc-global");
7880
+ const userRc = join13(scratch, "npmrc-user");
7881
+ const globalRc = join13(scratch, "npmrc-global");
7711
7882
  for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
7712
7883
  env.npm_config_userconfig = userRc;
7713
7884
  env.npm_config_globalconfig = globalRc;
@@ -7717,8 +7888,8 @@ function suiteEnv(scratch) {
7717
7888
  }
7718
7889
  function runSuite(dir, cmd, budgetSecs) {
7719
7890
  const t0 = Date.now();
7720
- const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
7721
- const outFile = join14(outDir, "result.json");
7891
+ const outDir = mkdtempSync2(join13(tmpdir2(), "tw-verify-run-"));
7892
+ const outFile = join13(outDir, "result.json");
7722
7893
  try {
7723
7894
  spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
7724
7895
  cwd: dir,
@@ -7731,7 +7902,7 @@ function runSuite(dir, cmd, budgetSecs) {
7731
7902
  const secs = Math.round((Date.now() - t0) / 1e3);
7732
7903
  let r;
7733
7904
  try {
7734
- r = JSON.parse(readFileSync15(outFile, "utf8"));
7905
+ r = JSON.parse(readFileSync13(outFile, "utf8"));
7735
7906
  } catch {
7736
7907
  return { exit: null, secs };
7737
7908
  }
@@ -7782,8 +7953,8 @@ function runVerify(opts) {
7782
7953
  out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
7783
7954
  return 2;
7784
7955
  }
7785
- const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
7786
- const visDir = join14(visRoot, "t");
7956
+ const visRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-vis-"));
7957
+ const visDir = join13(visRoot, "t");
7787
7958
  const cleanup = (dirs) => {
7788
7959
  if (opts.keep) return;
7789
7960
  for (const d of dirs) rmSync3(d, { recursive: true, force: true });
@@ -7811,8 +7982,8 @@ function runVerify(opts) {
7811
7982
  out2("pristine run executes through it too. Failing closed, not open.");
7812
7983
  return 2;
7813
7984
  }
7814
- const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
7815
- const priDir = join14(priRoot, "t");
7985
+ const priRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-pri-"));
7986
+ const priDir = join13(priRoot, "t");
7816
7987
  let restored = [];
7817
7988
  let removedAdded = 0;
7818
7989
  try {
@@ -7924,7 +8095,7 @@ function parseVerify(args) {
7924
8095
 
7925
8096
  // src/cli/run.ts
7926
8097
  import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
7927
- import { readdirSync as readdirSync4, readFileSync as readFileSync16, realpathSync } from "node:fs";
8098
+ import { readdirSync as readdirSync4, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "node:fs";
7928
8099
  import { resolve as resolve4 } from "node:path";
7929
8100
  var out = (s) => process.stdout.write(s + "\n");
7930
8101
  var err = (s) => process.stderr.write(s + "\n");
@@ -7933,7 +8104,7 @@ function git3(args, cwd) {
7933
8104
  }
7934
8105
  function nowTicks() {
7935
8106
  try {
7936
- return Math.floor(parseFloat(readFileSync16("/proc/uptime", "utf8").split(" ")[0]) * 100);
8107
+ return Math.floor(parseFloat(readFileSync14("/proc/uptime", "utf8").split(" ")[0]) * 100);
7937
8108
  } catch {
7938
8109
  return Number.POSITIVE_INFINITY;
7939
8110
  }
@@ -7942,7 +8113,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
7942
8113
  const out2 = [];
7943
8114
  let real;
7944
8115
  try {
7945
- real = realpathSync(cwd);
8116
+ real = realpathSync2(cwd);
7946
8117
  } catch {
7947
8118
  return out2;
7948
8119
  }
@@ -7957,13 +8128,13 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
7957
8128
  if (n === process.pid) continue;
7958
8129
  let held;
7959
8130
  try {
7960
- held = realpathSync(`/proc/${pid}/cwd`);
8131
+ held = realpathSync2(`/proc/${pid}/cwd`);
7961
8132
  } catch {
7962
8133
  continue;
7963
8134
  }
7964
8135
  if (held !== real && !held.startsWith(real + "/")) continue;
7965
8136
  try {
7966
- const stat = readFileSync16(`/proc/${pid}/stat`, "utf8");
8137
+ const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
7967
8138
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
7968
8139
  if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
7969
8140
  } catch {