tamperward 2.27.0 → 2.29.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.
package/dist/cli/index.js CHANGED
@@ -211,7 +211,7 @@ function forwardedEnv(env) {
211
211
  return out3;
212
212
  }
213
213
  function exchange(paths, req2, timeoutMs) {
214
- return new Promise((resolve16) => {
214
+ return new Promise((resolve17) => {
215
215
  let done = false;
216
216
  let handedOff = false;
217
217
  let accepted = false;
@@ -222,7 +222,7 @@ function exchange(paths, req2, timeoutMs) {
222
222
  done = true;
223
223
  clearTimeout(timer);
224
224
  sock.destroy();
225
- resolve16(value);
225
+ resolve17(value);
226
226
  };
227
227
  const sock = createConnection(paths.socket);
228
228
  const timer = setTimeout(() => finish(handedOff ? acceptedFailure("timeout") : null), timeoutMs);
@@ -298,6 +298,7 @@ var init_hook_client = __esm({
298
298
  MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
299
299
  FORWARDED_ENV = [
300
300
  "TAMPERWARD_DENYLOG",
301
+ "TAMPERWARD_AUDIT_LOG",
301
302
  "TAMPERWARD_FSEVENTS",
302
303
  "TAMPERWARD_TRANSIENT",
303
304
  "CLAUDE_CONFIG_DIR",
@@ -3538,15 +3539,15 @@ function buildAliasMap(sf) {
3538
3539
  ts.forEachChild(node, collect2);
3539
3540
  };
3540
3541
  collect2(sf);
3541
- const resolve16 = (name, seen) => {
3542
+ const resolve17 = (name, seen) => {
3542
3543
  if (seen.has(name)) return null;
3543
3544
  seen.add(name);
3544
3545
  const t = decls.get(name);
3545
- return t ? kindOfType(t, (n) => resolve16(n, seen)) : null;
3546
+ return t ? kindOfType(t, (n) => resolve17(n, seen)) : null;
3546
3547
  };
3547
3548
  const resolved = /* @__PURE__ */ new Map();
3548
3549
  for (const name of decls.keys()) {
3549
- const k = resolve16(name, /* @__PURE__ */ new Set());
3550
+ const k = resolve17(name, /* @__PURE__ */ new Set());
3550
3551
  if (k) resolved.set(name, k);
3551
3552
  }
3552
3553
  return resolved;
@@ -4933,8 +4934,8 @@ function numericOf(e) {
4933
4934
  }
4934
4935
  return void 0;
4935
4936
  }
4936
- function metricsOf(expr, resolve16) {
4937
- const obj = resolve16(expr);
4937
+ function metricsOf(expr, resolve17) {
4938
+ const obj = resolve17(expr);
4938
4939
  if (!ts.isObjectLiteralExpression(obj)) return void 0;
4939
4940
  const out3 = { values: {}, opaque: false };
4940
4941
  let any = false;
@@ -4948,7 +4949,7 @@ function metricsOf(expr, resolve16) {
4948
4949
  const k = keyName(p.name);
4949
4950
  if (k === null || !isMetric(k)) continue;
4950
4951
  any = true;
4951
- const n = numericOf(resolve16(p.initializer));
4952
+ const n = numericOf(resolve17(p.initializer));
4952
4953
  if (n === void 0) out3.opaque = true;
4953
4954
  else out3.values[k] = n;
4954
4955
  }
@@ -4995,17 +4996,17 @@ function parseThresholds(src) {
4995
4996
  try {
4996
4997
  const sf = sourceOf(src);
4997
4998
  if (!sf) return res;
4998
- const resolve16 = resolverFor(sf);
4999
+ const resolve17 = resolverFor(sf);
4999
5000
  const visit = (node) => {
5000
5001
  if (ts.isPropertyAssignment(node)) {
5001
5002
  const key2 = keyName(node.name);
5002
5003
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
5003
5004
  res.present = true;
5004
- const init = resolve16(node.initializer);
5005
+ const init = resolve17(node.initializer);
5005
5006
  if (!ts.isObjectLiteralExpression(init)) {
5006
5007
  res.opaque = true;
5007
5008
  } else {
5008
- const flat = metricsOf(init, resolve16);
5009
+ const flat = metricsOf(init, resolve17);
5009
5010
  if (flat) res.global = merge(res.global, flat);
5010
5011
  for (const p of init.properties) {
5011
5012
  if (ts.isSpreadAssignment(p)) {
@@ -5019,7 +5020,7 @@ function parseThresholds(src) {
5019
5020
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
5020
5021
  continue;
5021
5022
  }
5022
- const m = metricsOf(p.initializer, resolve16);
5023
+ const m = metricsOf(p.initializer, resolve17);
5023
5024
  if (!m) continue;
5024
5025
  if (k === "global") res.global = merge(res.global, m);
5025
5026
  else res.paths.set(norm(k), m);
@@ -5053,18 +5054,18 @@ function parseLists(src) {
5053
5054
  try {
5054
5055
  const sf = sourceOf(src);
5055
5056
  if (!sf) return res;
5056
- const resolve16 = resolverFor(sf);
5057
+ const resolve17 = resolverFor(sf);
5057
5058
  const visit = (node) => {
5058
5059
  if (ts.isPropertyAssignment(node)) {
5059
5060
  const k = keyName(node.name);
5060
5061
  const vitestCoverage = ownerKey(node) === "coverage";
5061
5062
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
5062
- const { items, opaque } = literals(resolve16(node.initializer));
5063
+ const { items, opaque } = literals(resolve17(node.initializer));
5063
5064
  res.collectFrom = items;
5064
5065
  res.collectKey = k === "include" ? "coverage.include" : k;
5065
5066
  if (opaque) res.opaqueCollect = true;
5066
5067
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
5067
- const { items, opaque } = literals(resolve16(node.initializer));
5068
+ const { items, opaque } = literals(resolve17(node.initializer));
5068
5069
  (k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
5069
5070
  if (opaque) res.opaqueIgnore = true;
5070
5071
  }
@@ -11829,15 +11830,251 @@ var init_fs_events = __esm({
11829
11830
  }
11830
11831
  });
11831
11832
 
11833
+ // src/cli/audit.ts
11834
+ import { createHash as createHash5, randomBytes } from "node:crypto";
11835
+ import { appendFileSync as appendFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10 } from "node:fs";
11836
+ import { dirname as dirname4, isAbsolute as isAbsolute8, join as join13, resolve as resolve4 } from "node:path";
11837
+ function sessionHash(sessionId) {
11838
+ if (!sessionId) return void 0;
11839
+ return "sha256:" + createHash5("sha256").update(sessionId).digest("hex").slice(0, 24);
11840
+ }
11841
+ function eventId() {
11842
+ return "sha256:" + createHash5("sha256").update(randomBytes(32)).digest("hex").slice(0, 32);
11843
+ }
11844
+ function configuredAuditPath(cwd, env = process.env) {
11845
+ const value = env[AUDIT_LOG_ENV];
11846
+ if (!value) return null;
11847
+ if (value === "auto") {
11848
+ const ctx = repoContext(cwd);
11849
+ return ctx ? join13(ctx.gitDir, "tamperward", "audit.jsonl") : null;
11850
+ }
11851
+ return isAbsolute8(value) ? value : resolve4(cwd, value);
11852
+ }
11853
+ function defaultAuditPath(cwd) {
11854
+ const ctx = repoContext(cwd);
11855
+ return ctx ? join13(ctx.gitDir, "tamperward", "audit.jsonl") : null;
11856
+ }
11857
+ function recordAuditFindings(findings, context) {
11858
+ if (findings.length === 0) return;
11859
+ const path = configuredAuditPath(context.cwd);
11860
+ if (!path) return;
11861
+ try {
11862
+ mkdirSync5(dirname4(path), { recursive: true });
11863
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11864
+ const session = sessionHash(context.sessionId);
11865
+ const lines = findings.map((finding) => {
11866
+ const severity = finding.severity === "block" ? "block" : "warn";
11867
+ const event = {
11868
+ schema_version: AUDIT_SCHEMA_VERSION,
11869
+ id: eventId(),
11870
+ timestamp,
11871
+ surface: context.surface,
11872
+ agent: "claude-code",
11873
+ rule: finding.rule,
11874
+ severity,
11875
+ decision: severity === "block" ? "deny" : "warn",
11876
+ ...session ? { session } : {}
11877
+ };
11878
+ return JSON.stringify(event);
11879
+ });
11880
+ appendFileSync4(path, lines.join("\n") + "\n");
11881
+ } catch {
11882
+ }
11883
+ }
11884
+ function recordAuditSignal(rule, severity, context) {
11885
+ const finding = {
11886
+ rule,
11887
+ severity,
11888
+ message: "",
11889
+ evidence: "",
11890
+ remediation: "",
11891
+ signoff: { required: severity === "block", command: "" }
11892
+ };
11893
+ recordAuditFindings([finding], context);
11894
+ }
11895
+ function isRecord3(v) {
11896
+ return typeof v === "object" && v !== null && !Array.isArray(v);
11897
+ }
11898
+ function parseAuditEvent(value, line = 0) {
11899
+ const where = line > 0 ? `audit line ${line}` : "audit event";
11900
+ if (!isRecord3(value)) throw new Error(`${where} must be a JSON object`);
11901
+ const unknown = Object.keys(value).filter((key2) => !ALLOWED_KEYS.has(key2));
11902
+ if (unknown.length) throw new Error(`${where} contains unsupported field "${unknown[0]}"`);
11903
+ if (value.schema_version !== AUDIT_SCHEMA_VERSION) {
11904
+ throw new Error(`${where} has unsupported schema_version ${String(value.schema_version)}`);
11905
+ }
11906
+ if (typeof value.id !== "string" || !/^sha256:[0-9a-f]{32}$/.test(value.id)) {
11907
+ throw new Error(`${where} has an invalid id`);
11908
+ }
11909
+ if (typeof value.timestamp !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value.timestamp) || !Number.isFinite(Date.parse(value.timestamp))) {
11910
+ throw new Error(`${where} has an invalid timestamp`);
11911
+ }
11912
+ if (value.surface !== "pretooluse" && value.surface !== "stop") {
11913
+ throw new Error(`${where} has an invalid surface`);
11914
+ }
11915
+ if (typeof value.agent !== "string" || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(value.agent)) {
11916
+ throw new Error(`${where} has an invalid agent`);
11917
+ }
11918
+ if (typeof value.rule !== "string" || !/^[a-z0-9][a-z0-9-]{0,79}$/.test(value.rule)) {
11919
+ throw new Error(`${where} has an invalid rule`);
11920
+ }
11921
+ if (value.severity !== "block" && value.severity !== "warn") {
11922
+ throw new Error(`${where} has an invalid severity`);
11923
+ }
11924
+ if (value.decision !== "deny" && value.decision !== "warn") {
11925
+ throw new Error(`${where} has an invalid decision`);
11926
+ }
11927
+ if (value.severity === "block" && value.decision !== "deny" || value.severity === "warn" && value.decision !== "warn") {
11928
+ throw new Error(`${where} has inconsistent severity and decision`);
11929
+ }
11930
+ if (value.session !== void 0 && (typeof value.session !== "string" || !/^sha256:[0-9a-f]{24}$/.test(value.session))) {
11931
+ throw new Error(`${where} has an invalid session`);
11932
+ }
11933
+ return {
11934
+ schema_version: AUDIT_SCHEMA_VERSION,
11935
+ id: value.id,
11936
+ timestamp: value.timestamp,
11937
+ surface: value.surface,
11938
+ agent: value.agent,
11939
+ rule: value.rule,
11940
+ severity: value.severity,
11941
+ decision: value.decision,
11942
+ ...typeof value.session === "string" ? { session: value.session } : {}
11943
+ };
11944
+ }
11945
+ function parseAuditJsonl(raw) {
11946
+ const events = [];
11947
+ raw.split(/\r?\n/).forEach((line, index) => {
11948
+ if (!line.trim()) return;
11949
+ let value;
11950
+ try {
11951
+ value = JSON.parse(line);
11952
+ } catch {
11953
+ throw new Error(`audit line ${index + 1} is not valid JSON`);
11954
+ }
11955
+ events.push(parseAuditEvent(value, index + 1));
11956
+ });
11957
+ return events;
11958
+ }
11959
+ function parseSince(value, nowMs = Date.now()) {
11960
+ const relative8 = /^(\d+)(m|h|d)$/.exec(value);
11961
+ if (relative8) {
11962
+ const count = Number(relative8[1]);
11963
+ const unit = relative8[2] === "m" ? 6e4 : relative8[2] === "h" ? 36e5 : 864e5;
11964
+ if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
11965
+ return nowMs - count * unit;
11966
+ }
11967
+ const parsed = Date.parse(value);
11968
+ if (!Number.isFinite(parsed)) {
11969
+ throw new Error(`invalid --since value "${value}" (use e.g. 30d, 12h, 90m, or an ISO timestamp)`);
11970
+ }
11971
+ return parsed;
11972
+ }
11973
+ function summarizeAudit(events) {
11974
+ const ordered = [...events].sort((a, b) => a.timestamp.localeCompare(b.timestamp) || a.id.localeCompare(b.id));
11975
+ const rules = /* @__PURE__ */ new Map();
11976
+ const surfaces = /* @__PURE__ */ new Map();
11977
+ const sessions = /* @__PURE__ */ new Set();
11978
+ for (const event of ordered) {
11979
+ const bucket = rules.get(event.rule) ?? { events: 0, blocked: 0, warnings: 0 };
11980
+ bucket.events++;
11981
+ if (event.severity === "block") bucket.blocked++;
11982
+ else bucket.warnings++;
11983
+ rules.set(event.rule, bucket);
11984
+ surfaces.set(event.surface, (surfaces.get(event.surface) ?? 0) + 1);
11985
+ if (event.session) sessions.add(event.session);
11986
+ }
11987
+ const by_rule = [...rules.entries()].map(([rule, counts]) => ({ rule, ...counts })).sort((a, b) => b.events - a.events || a.rule.localeCompare(b.rule));
11988
+ const by_surface = [...surfaces.entries()].map(([surface, count]) => ({ surface, events: count })).sort((a, b) => b.events - a.events || a.surface.localeCompare(b.surface));
11989
+ return {
11990
+ schema_version: AUDIT_SCHEMA_VERSION,
11991
+ events: ordered.length,
11992
+ blocked: ordered.filter((event) => event.severity === "block").length,
11993
+ warnings: ordered.filter((event) => event.severity === "warn").length,
11994
+ sessions: sessions.size,
11995
+ first_event: ordered[0]?.timestamp ?? null,
11996
+ last_event: ordered.at(-1)?.timestamp ?? null,
11997
+ by_rule,
11998
+ by_surface,
11999
+ interpretation: "finding-is-not-proof-of-intent"
12000
+ };
12001
+ }
12002
+ function renderAuditStats(summary) {
12003
+ const out3 = [
12004
+ "TamperWard audit stats",
12005
+ "",
12006
+ `Events ${summary.events}`,
12007
+ `Blocked ${summary.blocked}`,
12008
+ `Warnings ${summary.warnings}`,
12009
+ `Sessions ${summary.sessions}`
12010
+ ];
12011
+ if (summary.first_event && summary.last_event) {
12012
+ out3.push(`First ${summary.first_event}`, `Last ${summary.last_event}`);
12013
+ }
12014
+ if (summary.by_rule.length) {
12015
+ out3.push("", "By rule");
12016
+ const width = Math.max(...summary.by_rule.map((row) => row.rule.length));
12017
+ for (const row of summary.by_rule) out3.push(` ${row.rule.padEnd(width)} ${row.events}`);
12018
+ }
12019
+ if (summary.by_surface.length) {
12020
+ out3.push("", "By surface");
12021
+ const width = Math.max(...summary.by_surface.map((row) => row.surface.length));
12022
+ for (const row of summary.by_surface) out3.push(` ${row.surface.padEnd(width)} ${row.events}`);
12023
+ }
12024
+ out3.push("", "Note: an integrity finding is a signal, not proof of agent intent; legitimate refactors can trigger findings.");
12025
+ return out3.join("\n") + "\n";
12026
+ }
12027
+ function runStats(opts = {}) {
12028
+ const cwd = resolve4(opts.cwd ?? process.cwd());
12029
+ const explicitFile = opts.file !== void 0;
12030
+ const configured = process.env[AUDIT_LOG_ENV];
12031
+ const file = opts.file ? isAbsolute8(opts.file) ? opts.file : resolve4(cwd, opts.file) : configured && configured !== "auto" ? isAbsolute8(configured) ? configured : resolve4(cwd, configured) : defaultAuditPath(cwd);
12032
+ const cutoff = opts.since ? parseSince(opts.since) : null;
12033
+ if (!file) throw new Error("stats needs --file outside a Git repository");
12034
+ if (!existsSync9(file)) {
12035
+ if (explicitFile) throw new Error(`audit file not found: ${file}`);
12036
+ const empty2 = summarizeAudit([]);
12037
+ process.stdout.write(opts.json ? JSON.stringify(empty2) + "\n" : renderAuditStats(empty2));
12038
+ return 0;
12039
+ }
12040
+ let events = parseAuditJsonl(readFileSync10(file, "utf8"));
12041
+ if (cutoff !== null) {
12042
+ events = events.filter((event) => Date.parse(event.timestamp) >= cutoff);
12043
+ }
12044
+ const summary = summarizeAudit(events);
12045
+ process.stdout.write(opts.json ? JSON.stringify(summary) + "\n" : renderAuditStats(summary));
12046
+ return 0;
12047
+ }
12048
+ var AUDIT_SCHEMA_VERSION, AUDIT_LOG_ENV, ALLOWED_KEYS;
12049
+ var init_audit = __esm({
12050
+ "src/cli/audit.ts"() {
12051
+ "use strict";
12052
+ init_repo_context();
12053
+ AUDIT_SCHEMA_VERSION = 1;
12054
+ AUDIT_LOG_ENV = "TAMPERWARD_AUDIT_LOG";
12055
+ ALLOWED_KEYS = /* @__PURE__ */ new Set([
12056
+ "schema_version",
12057
+ "id",
12058
+ "timestamp",
12059
+ "surface",
12060
+ "agent",
12061
+ "rule",
12062
+ "severity",
12063
+ "decision",
12064
+ "session"
12065
+ ]);
12066
+ }
12067
+ });
12068
+
11832
12069
  // src/cli/hook.ts
11833
- import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "node:fs";
11834
- import { createHash as createHash5 } from "node:crypto";
12070
+ import { readFileSync as readFileSync11, appendFileSync as appendFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
12071
+ import { createHash as createHash6 } from "node:crypto";
11835
12072
  function setSnapshotCache(cache3) {
11836
12073
  snapshotCache = cache3;
11837
12074
  }
11838
12075
  function readStdin() {
11839
12076
  try {
11840
- return readFileSync10(0, "utf8");
12077
+ return readFileSync11(0, "utf8");
11841
12078
  } catch (e) {
11842
12079
  throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
11843
12080
  }
@@ -11865,7 +12102,7 @@ function recordDenylog(blocks2) {
11865
12102
  const log = process.env.TAMPERWARD_DENYLOG;
11866
12103
  if (!log) return;
11867
12104
  try {
11868
- appendFileSync4(log, blocks2.map((b) => b.rule).join(",") + "\n");
12105
+ appendFileSync5(log, blocks2.map((b) => b.rule).join(",") + "\n");
11869
12106
  } catch {
11870
12107
  }
11871
12108
  }
@@ -11873,7 +12110,7 @@ function errText2(e) {
11873
12110
  const m = e instanceof Error ? e.message : String(e);
11874
12111
  return m.length > 200 ? m.slice(0, 200) + "\u2026" : m;
11875
12112
  }
11876
- function failClosed(kind, detail) {
12113
+ function failClosed(kind, detail, audit) {
11877
12114
  const f = {
11878
12115
  rule: "tamperward-unavailable",
11879
12116
  severity: "block",
@@ -11882,7 +12119,7 @@ function failClosed(kind, detail) {
11882
12119
  remediation: "Repair the Tamperward setup \u2014 most often an unparseable .tamperward.yml (check for merge-conflict markers) \u2014 then retry. Do not work around the gate while it is down.",
11883
12120
  signoff: { required: true, command: 'tamperward allow --reason "..."' }
11884
12121
  };
11885
- return verdict([f], kind);
12122
+ return verdict([f], kind, audit);
11886
12123
  }
11887
12124
  function denyWire(reason, kind) {
11888
12125
  const payload = kind === "PreToolUse" ? {
@@ -11894,9 +12131,16 @@ function denyWire(reason, kind) {
11894
12131
  } : { decision: "block", reason };
11895
12132
  return JSON.stringify(payload) + "\n";
11896
12133
  }
11897
- function verdict(blocks2, kind) {
12134
+ function verdict(blocks2, kind, audit) {
11898
12135
  if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
11899
12136
  recordDenylog(blocks2);
12137
+ if (audit) {
12138
+ recordAuditFindings(blocks2, {
12139
+ cwd: audit.cwd,
12140
+ surface: kind === "PreToolUse" ? "pretooluse" : "stop",
12141
+ sessionId: audit.sessionId
12142
+ });
12143
+ }
11900
12144
  const reason = formatDenial(blocks2);
11901
12145
  return { exitCode: 0, stdout: denyWire(reason, kind) };
11902
12146
  }
@@ -12062,7 +12306,7 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
12062
12306
  let dirty = false;
12063
12307
  for (const c of changes) {
12064
12308
  if (c.kind !== "file" || c.after == null || !isProtected(c.path, policy)) continue;
12065
- const hash = createHash5("sha256").update(c.after).digest("hex").slice(0, 16);
12309
+ const hash = createHash6("sha256").update(c.after).digest("hex").slice(0, 16);
12066
12310
  const prev = tree[c.path];
12067
12311
  tree[c.path] = {
12068
12312
  hash,
@@ -12077,8 +12321,8 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
12077
12321
  if (dirty) savePtree(cwd, sessionId, tree);
12078
12322
  }
12079
12323
  function preToolUseVerdict(input, defaultCwd, trustedRoot) {
12324
+ const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
12080
12325
  try {
12081
- const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
12082
12326
  if (trustedRoot !== void 0) {
12083
12327
  const v = validateClaimAgainstRoot(input.cwd, trustedRoot, defaultCwd ?? process.cwd());
12084
12328
  if (!v.ok) return failClosed("PreToolUse", `repository identity claim rejected: ${v.rejected}`);
@@ -12087,13 +12331,13 @@ function preToolUseVerdict(input, defaultCwd, trustedRoot) {
12087
12331
  turnBaseline(cwd, input.session_id);
12088
12332
  const policy = loadPolicy(cwd);
12089
12333
  const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
12090
- if (driftBlocks) return verdict(driftBlocks, "PreToolUse");
12334
+ if (driftBlocks) return verdict(driftBlocks, "PreToolUse", { cwd, sessionId: input.session_id });
12091
12335
  const changes = changesFromClaudeHook(input, cwd, sessionCwd);
12092
12336
  const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
12093
12337
  if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
12094
- return verdict(blocks2, "PreToolUse");
12338
+ return verdict(blocks2, "PreToolUse", { cwd, sessionId: input.session_id });
12095
12339
  } catch (e) {
12096
- return failClosed("PreToolUse", errText2(e));
12340
+ return failClosed("PreToolUse", errText2(e), { cwd: sessionCwd, sessionId: input.session_id });
12097
12341
  }
12098
12342
  }
12099
12343
  function cursorPath(cwd, sessionId) {
@@ -12117,19 +12361,21 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
12117
12361
  const log = defaultEventLog(cwd);
12118
12362
  const telemetry = watcherTelemetry(log);
12119
12363
  if (telemetry.state !== "healthy") {
12120
- recordObserverHealth(telemetry.state, telemetry.reason);
12364
+ recordObserverHealth(cwd, sessionId, telemetry.state, telemetry.reason);
12121
12365
  }
12122
12366
  const cp = cursorPath(cwd, sessionId);
12123
- if (!existsSync9(log)) return none;
12367
+ if (!existsSync10(log)) return none;
12124
12368
  let offset = 0;
12125
- if (cp && existsSync9(cp)) {
12126
- const n = Number(readFileSync10(cp, "utf8"));
12369
+ if (cp && existsSync10(cp)) {
12370
+ const n = Number(readFileSync11(cp, "utf8"));
12127
12371
  offset = Number.isFinite(n) ? n : 0;
12128
12372
  }
12129
12373
  const drained = drainEvents(log, offset);
12130
12374
  if (!drained.complete) {
12131
12375
  const issue = drained.issue ?? "read-stalled";
12132
12376
  recordObserverHealth(
12377
+ cwd,
12378
+ sessionId,
12133
12379
  "degraded",
12134
12380
  `event telemetry was not fully classified (${issue}); Stop retained the prior cursor`
12135
12381
  );
@@ -12157,7 +12403,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
12157
12403
  return e.kind === "file" && e.content != null ? contentHash(e.content) : null;
12158
12404
  };
12159
12405
  const findings = transientFindings(events, persistent, policy, finalHash);
12160
- recordWarns(findings.filter((f) => f.severity !== "block"));
12406
+ recordWarns(cwd, sessionId, findings.filter((f) => f.severity !== "block"));
12161
12407
  return {
12162
12408
  blocks: findings.filter((f) => f.severity === "block"),
12163
12409
  commit: () => {
@@ -12168,20 +12414,23 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
12168
12414
  }
12169
12415
  };
12170
12416
  }
12171
- function recordWarns(warns) {
12417
+ function recordWarns(cwd, sessionId, warns) {
12418
+ if (warns.length === 0) return;
12419
+ recordAuditFindings(warns, { cwd, surface: "stop", sessionId });
12172
12420
  const log = process.env.TAMPERWARD_DENYLOG;
12173
- if (!log || warns.length === 0) return;
12421
+ if (!log) return;
12174
12422
  try {
12175
- appendFileSync4(log, warns.map((w) => `warn:${w.rule}:${w.file ?? ""}`).join("\n") + "\n");
12423
+ appendFileSync5(log, warns.map((w) => `warn:${w.rule}:${w.file ?? ""}`).join("\n") + "\n");
12176
12424
  } catch {
12177
12425
  }
12178
12426
  }
12179
- function recordObserverHealth(state, reason) {
12427
+ function recordObserverHealth(cwd, sessionId, state, reason) {
12428
+ recordAuditSignal("transient-observer", "warn", { cwd, surface: "stop", sessionId });
12180
12429
  const log = process.env.TAMPERWARD_DENYLOG;
12181
12430
  if (!log) return;
12182
12431
  const detail = (reason ?? "").replace(/[\r\n]+/g, " ").trim();
12183
12432
  try {
12184
- appendFileSync4(
12433
+ appendFileSync5(
12185
12434
  log,
12186
12435
  `warn:transient-observer:${state}${detail ? ":" + detail : ""}
12187
12436
  `
@@ -12226,13 +12475,13 @@ function stopVerdict(input, defaultCwd, trustedRoot) {
12226
12475
  saveTurnTree(cwd, input.session_id, current);
12227
12476
  }
12228
12477
  } catch (e) {
12229
- return failClosed("Stop", errText2(e));
12478
+ return failClosed("Stop", errText2(e), { cwd: sessionCwd, sessionId: input.session_id });
12230
12479
  }
12231
12480
  if (blocks2.length === 0) {
12232
12481
  advanceTurnBaseline(cwd, input.session_id);
12233
12482
  commitCursor();
12234
12483
  }
12235
- return verdict(blocks2, "Stop");
12484
+ return verdict(blocks2, "Stop", { cwd, sessionId: input.session_id });
12236
12485
  }
12237
12486
  function emit(r) {
12238
12487
  if (r.stdout) process.stdout.write(r.stdout);
@@ -12290,6 +12539,7 @@ var init_hook = __esm({
12290
12539
  init_disk();
12291
12540
  init_narrow();
12292
12541
  init_repo_context();
12542
+ init_audit();
12293
12543
  snapshotCache = null;
12294
12544
  HookInputError = class extends Error {
12295
12545
  };
@@ -12298,7 +12548,7 @@ var init_hook = __esm({
12298
12548
 
12299
12549
  // src/ptree-cache.ts
12300
12550
  import { lstatSync as lstatSync5 } from "node:fs";
12301
- import { join as join13 } from "node:path";
12551
+ import { join as join14 } from "node:path";
12302
12552
  function keyOf(st) {
12303
12553
  return `${st.dev}:${st.ino}:${st.size}:${st.mode}:${st.mtimeNs}:${st.ctimeNs}`;
12304
12554
  }
@@ -12338,7 +12588,7 @@ var init_ptree_cache = __esm({
12338
12588
  /** A path the caller has reason to distrust (an fs event, a sanctioned edit):
12339
12589
  * its entry is forgotten so the next walk hashes it whatever its stat says. */
12340
12590
  markDirty(cwd, rel) {
12341
- this.map.delete(join13(cwd, rel));
12591
+ this.map.delete(join14(cwd, rel));
12342
12592
  }
12343
12593
  /**
12344
12594
  * The snapshot entry for `rel`, from the cache when the six-field stat key
@@ -12348,7 +12598,7 @@ var init_ptree_cache = __esm({
12348
12598
  * time, the way the in-process path treats them.
12349
12599
  */
12350
12600
  entry(cwd, rel, compute) {
12351
- const abs = join13(cwd, rel);
12601
+ const abs = join14(cwd, rel);
12352
12602
  let st;
12353
12603
  try {
12354
12604
  st = lstatSync5(abs, { bigint: true });
@@ -12391,13 +12641,13 @@ var init_ptree_cache = __esm({
12391
12641
  });
12392
12642
 
12393
12643
  // src/cli/hook-service.ts
12394
- import { chmodSync, existsSync as existsSync10, lstatSync as lstatSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
12644
+ import { chmodSync, existsSync as existsSync11, lstatSync as lstatSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
12395
12645
  import { createConnection as createConnection2, createServer } from "node:net";
12396
12646
  import { realpathSync as realpathSync4 } from "node:fs";
12397
12647
  import { sep } from "node:path";
12398
12648
  function readServiceState(paths) {
12399
12649
  try {
12400
- const v = JSON.parse(readFileSync11(paths.state, "utf8"));
12650
+ const v = JSON.parse(readFileSync12(paths.state, "utf8"));
12401
12651
  if (!isRecord(v)) return null;
12402
12652
  const { pid, version, root, started_at } = v;
12403
12653
  if (typeof pid !== "number" || typeof version !== "string" || typeof root !== "string" || typeof started_at !== "string") return null;
@@ -12416,12 +12666,12 @@ function pidAlive2(pid) {
12416
12666
  }
12417
12667
  }
12418
12668
  function listening(socket) {
12419
- return new Promise((resolve16) => {
12669
+ return new Promise((resolve17) => {
12420
12670
  const sock = createConnection2(socket);
12421
12671
  const done = (v) => {
12422
12672
  clearTimeout(timer);
12423
12673
  sock.destroy();
12424
- resolve16(v);
12674
+ resolve17(v);
12425
12675
  };
12426
12676
  const timer = setTimeout(() => done(false), 1e3);
12427
12677
  sock.once("connect", () => done(true));
@@ -12479,7 +12729,7 @@ function removeQuietly(p) {
12479
12729
  }
12480
12730
  }
12481
12731
  function ensurePrivateDir(dir, uid) {
12482
- if (!existsSync10(dir)) mkdirSync5(dir, { recursive: true, mode: 448 });
12732
+ if (!existsSync11(dir)) mkdirSync6(dir, { recursive: true, mode: 448 });
12483
12733
  const st = lstatSync6(dir);
12484
12734
  if (!st.isDirectory()) throw new Error(`${dir} is not a directory`);
12485
12735
  if (st.uid !== uid) throw new Error(`${dir} is owned by uid ${st.uid}, not ${uid}; refusing to serve from it`);
@@ -12574,11 +12824,11 @@ async function startHookService(opts) {
12574
12824
  });
12575
12825
  const umask = process.umask(63);
12576
12826
  try {
12577
- await new Promise((resolve16, reject) => {
12827
+ await new Promise((resolve17, reject) => {
12578
12828
  server.once("error", reject);
12579
12829
  server.listen(paths.socket, () => {
12580
12830
  server.off("error", reject);
12581
- resolve16();
12831
+ resolve17();
12582
12832
  });
12583
12833
  });
12584
12834
  } finally {
@@ -12595,14 +12845,14 @@ async function startHookService(opts) {
12595
12845
  get served() {
12596
12846
  return served;
12597
12847
  },
12598
- close: () => new Promise((resolve16) => {
12599
- if (closed) return resolve16();
12848
+ close: () => new Promise((resolve17) => {
12849
+ if (closed) return resolve17();
12600
12850
  closed = true;
12601
12851
  setSnapshotCache(null);
12602
12852
  server.close(() => {
12603
12853
  removeQuietly(paths.socket);
12604
12854
  removeQuietly(paths.state);
12605
- resolve16();
12855
+ resolve17();
12606
12856
  });
12607
12857
  })
12608
12858
  };
@@ -12631,7 +12881,7 @@ async function stopHookService(paths) {
12631
12881
  process.kill(state.pid, "SIGTERM");
12632
12882
  } catch {
12633
12883
  }
12634
- const gone = () => !pidAlive2(state.pid) || !existsSync10(paths.socket);
12884
+ const gone = () => !pidAlive2(state.pid) || !existsSync11(paths.socket);
12635
12885
  const until = Date.now() + STOP_WAIT_MS;
12636
12886
  while (!gone() && Date.now() < until) sleepSync(50);
12637
12887
  if (!gone()) throw new Error(`hook service pid ${state.pid} did not exit within ${STOP_WAIT_MS / 1e3}s`);
@@ -12816,7 +13066,7 @@ var init_verifier_limits = __esm({
12816
13066
  });
12817
13067
 
12818
13068
  // src/safe-write.ts
12819
- import { randomBytes } from "node:crypto";
13069
+ import { randomBytes as randomBytes2 } from "node:crypto";
12820
13070
  import { lstatSync as lstatSync7, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
12821
13071
  function writeTargetKind(path) {
12822
13072
  let st;
@@ -12845,7 +13095,7 @@ function existingMode(path, fallback) {
12845
13095
  return lstatSync7(path).mode & 511;
12846
13096
  }
12847
13097
  function atomicReplaceFile(path, content, mode) {
12848
- const tmp = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
13098
+ const tmp = `${path}.${process.pid}.${randomBytes2(8).toString("hex")}.tmp`;
12849
13099
  try {
12850
13100
  writeFileSync7(tmp, content, { encoding: "utf8", flag: "wx", mode });
12851
13101
  renameSync3(tmp, path);
@@ -12862,9 +13112,9 @@ var init_safe_write = __esm({
12862
13112
 
12863
13113
  // src/cli/init.ts
12864
13114
  import { execFileSync as execFileSync6 } from "node:child_process";
12865
- import { createHash as createHash6 } from "node:crypto";
12866
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
12867
- import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute8, join as join14, relative as relative3, resolve as resolve4 } from "node:path";
13115
+ import { createHash as createHash7 } from "node:crypto";
13116
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "node:fs";
13117
+ import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as relative3, resolve as resolve5 } from "node:path";
12868
13118
  function gitConfig(cwd, key2) {
12869
13119
  try {
12870
13120
  return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
@@ -12912,7 +13162,7 @@ function stalePin(command) {
12912
13162
  return pin !== null && (pin !== TW_VERSION || !OURS.test(command));
12913
13163
  }
12914
13164
  function bodyHash(body) {
12915
- return createHash6("sha256").update(body).digest("hex").slice(0, 16);
13165
+ return createHash7("sha256").update(body).digest("hex").slice(0, 16);
12916
13166
  }
12917
13167
  function workflowFile(body) {
12918
13168
  return `${WORKFLOW_MARK} v${TW_VERSION} sha256:${bodyHash(body)}
@@ -12957,34 +13207,34 @@ function verifierCandidates(cwd) {
12957
13207
  if (!candidates.includes(command)) candidates.push(command);
12958
13208
  };
12959
13209
  try {
12960
- const pkg = JSON.parse(readFileSync12(join14(cwd, "package.json"), "utf8"));
13210
+ const pkg = JSON.parse(readFileSync13(join15(cwd, "package.json"), "utf8"));
12961
13211
  const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
12962
13212
  if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
12963
13213
  } catch {
12964
13214
  }
12965
13215
  try {
12966
- const pyproject = readFileSync12(join14(cwd, "pyproject.toml"), "utf8");
13216
+ const pyproject = readFileSync13(join15(cwd, "pyproject.toml"), "utf8");
12967
13217
  if (/^\s*\[tool\.pytest(?:\.ini_options)?\]\s*$/m.test(pyproject)) add("pytest");
12968
13218
  } catch {
12969
13219
  }
12970
- if (existsSync11(join14(cwd, "pytest.ini"))) add("pytest");
13220
+ if (existsSync12(join15(cwd, "pytest.ini"))) add("pytest");
12971
13221
  try {
12972
- const setupCfg = readFileSync12(join14(cwd, "setup.cfg"), "utf8");
13222
+ const setupCfg = readFileSync13(join15(cwd, "setup.cfg"), "utf8");
12973
13223
  if (/^\s*\[tool:pytest\]\s*$/m.test(setupCfg)) add("pytest");
12974
13224
  } catch {
12975
13225
  }
12976
13226
  try {
12977
- const tox = readFileSync12(join14(cwd, "tox.ini"), "utf8");
13227
+ const tox = readFileSync13(join15(cwd, "tox.ini"), "utf8");
12978
13228
  if (/^\s*\[tox\]\s*$/m.test(tox)) add("tox");
12979
13229
  } catch {
12980
13230
  }
12981
13231
  try {
12982
- const cargo = readFileSync12(join14(cwd, "Cargo.toml"), "utf8");
13232
+ const cargo = readFileSync13(join15(cwd, "Cargo.toml"), "utf8");
12983
13233
  if (/^\s*\[(?:package|workspace)\]\s*(?:#.*)?$/m.test(cargo)) add("cargo test");
12984
13234
  } catch {
12985
13235
  }
12986
13236
  try {
12987
- const goMod = readFileSync12(join14(cwd, "go.mod"), "utf8");
13237
+ const goMod = readFileSync13(join15(cwd, "go.mod"), "utf8");
12988
13238
  if (/^\s*module\s+\S+\s*(?:\/\/.*)?$/m.test(goMod)) add("go test ./...");
12989
13239
  } catch {
12990
13240
  }
@@ -13024,10 +13274,10 @@ function verifierSetupMessage(cwd) {
13024
13274
  return lines.join("\n");
13025
13275
  }
13026
13276
  function planPolicy(cwd) {
13027
- const path = join14(cwd, POLICY_FILE);
13277
+ const path = join15(cwd, POLICY_FILE);
13028
13278
  const refused = refuseNonRegular(path);
13029
13279
  if (refused) return { item: "policy", path: POLICY_FILE, status: "error", detail: refused };
13030
- if (existsSync11(path)) {
13280
+ if (existsSync12(path)) {
13031
13281
  try {
13032
13282
  loadPolicy(cwd);
13033
13283
  } catch (e) {
@@ -13045,8 +13295,8 @@ function planPolicy(cwd) {
13045
13295
  }
13046
13296
  function planGitignore(cwd) {
13047
13297
  const rel = ".gitignore";
13048
- const path = join14(cwd, rel);
13049
- if (!existsSync11(join14(cwd, "node_modules"))) return null;
13298
+ const path = join15(cwd, rel);
13299
+ if (!existsSync12(join15(cwd, "node_modules"))) return null;
13050
13300
  let existing = "";
13051
13301
  let kind;
13052
13302
  try {
@@ -13056,7 +13306,7 @@ function planGitignore(cwd) {
13056
13306
  }
13057
13307
  const refused = refuseNonRegular(path);
13058
13308
  if (refused) return { item: "gitignore", path: rel, status: "error", detail: refused };
13059
- if (kind === "file") existing = readFileSync12(path, "utf8");
13309
+ if (kind === "file") existing = readFileSync13(path, "utf8");
13060
13310
  try {
13061
13311
  const tracked = execFileSync6("git", ["ls-files", "--cached", "--", "node_modules"], {
13062
13312
  cwd,
@@ -13107,14 +13357,14 @@ function gateVerdicts(event, groups, needle) {
13107
13357
  }
13108
13358
  function planClaudeHooks(cwd) {
13109
13359
  const rel = ".claude/settings.json";
13110
- const path = join14(cwd, rel);
13360
+ const path = join15(cwd, rel);
13111
13361
  const refused = refuseNonRegular(path);
13112
13362
  if (refused) return { item: "agent", path: rel, status: "error", detail: refused };
13113
13363
  let settings = {};
13114
- if (existsSync11(path)) {
13364
+ if (existsSync12(path)) {
13115
13365
  let parsed;
13116
13366
  try {
13117
- parsed = JSON.parse(readFileSync12(path, "utf8"));
13367
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
13118
13368
  } catch {
13119
13369
  return {
13120
13370
  item: "agent",
@@ -13163,7 +13413,7 @@ function planClaudeHooks(cwd) {
13163
13413
  return {
13164
13414
  item: "agent",
13165
13415
  path: rel,
13166
- status: existsSync11(path) ? "update" : "create",
13416
+ status: existsSync12(path) ? "update" : "create",
13167
13417
  detail: [
13168
13418
  needPre && "wire PreToolUse deny",
13169
13419
  needStop && "wire Stop sweep",
@@ -13196,7 +13446,7 @@ function planClaudeHooks(cwd) {
13196
13446
  (hooks.Stop ??= []).push({ hooks: [{ type: "command", command: SWEEP_CMD }] });
13197
13447
  }
13198
13448
  if (needDisableFalse) settings.disableAllHooks = false;
13199
- mkdirSync6(dirname4(path), { recursive: true });
13449
+ mkdirSync7(dirname5(path), { recursive: true });
13200
13450
  atomicReplaceFile(path, JSON.stringify(settings, null, 2) + "\n", existingMode(path, 420));
13201
13451
  }
13202
13452
  };
@@ -13204,14 +13454,14 @@ function planClaudeHooks(cwd) {
13204
13454
  function hooksDir(cwd) {
13205
13455
  try {
13206
13456
  const out3 = execFileSync6("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
13207
- if (out3) return resolve4(cwd, out3);
13457
+ if (out3) return resolve5(cwd, out3);
13208
13458
  } catch {
13209
13459
  }
13210
- return existsSync11(join14(cwd, ".git")) ? join14(cwd, ".git", "hooks") : null;
13460
+ return existsSync12(join15(cwd, ".git")) ? join15(cwd, ".git", "hooks") : null;
13211
13461
  }
13212
13462
  function display(cwd, path) {
13213
13463
  const rel = relative3(cwd, path);
13214
- return rel && !rel.startsWith("..") && !isAbsolute8(rel) ? rel : path;
13464
+ return rel && !rel.startsWith("..") && !isAbsolute9(rel) ? rel : path;
13215
13465
  }
13216
13466
  function unconditionalExitAt(lines) {
13217
13467
  let depth = 0;
@@ -13234,29 +13484,29 @@ function planPreCommit(cwd) {
13234
13484
  const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
13235
13485
  let path;
13236
13486
  let note;
13237
- if (existsSync11(join14(cwd, ".husky"))) {
13238
- path = join14(cwd, ".husky", "pre-commit");
13487
+ if (existsSync12(join15(cwd, ".husky"))) {
13488
+ path = join15(cwd, ".husky", "pre-commit");
13239
13489
  note = "husky";
13240
13490
  } else {
13241
- const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync11(join14(cwd, c))));
13491
+ const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync12(join15(cwd, c))));
13242
13492
  if (managed) {
13243
13493
  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}`);
13244
13494
  }
13245
13495
  const dir = hooksDir(cwd);
13246
13496
  if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
13247
- if (basename3(dir) === "_" && basename3(dirname4(dir)) === ".husky") {
13248
- path = join14(dirname4(dir), "pre-commit");
13497
+ if (basename3(dir) === "_" && basename3(dirname5(dir)) === ".husky") {
13498
+ path = join15(dirname5(dir), "pre-commit");
13249
13499
  note = "husky";
13250
13500
  } else {
13251
- path = join14(dir, "pre-commit");
13501
+ path = join15(dir, "pre-commit");
13252
13502
  const rel2 = display(cwd, dir);
13253
- note = rel2 === join14(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
13503
+ note = rel2 === join15(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
13254
13504
  }
13255
13505
  }
13256
13506
  const rel = display(cwd, path);
13257
13507
  const refused = refuseNonRegular(path);
13258
13508
  if (refused) return { item: "pre-commit", path: rel, status: "error", detail: refused };
13259
- const existing = existsSync11(path) ? readFileSync12(path, "utf8") : null;
13509
+ const existing = existsSync12(path) ? readFileSync13(path, "utf8") : null;
13260
13510
  const hookMode = () => existingMode(path, 493) | 73;
13261
13511
  if (existing === null) {
13262
13512
  return {
@@ -13265,7 +13515,7 @@ function planPreCommit(cwd) {
13265
13515
  status: "create",
13266
13516
  detail: `create via ${note}`,
13267
13517
  apply: () => {
13268
- mkdirSync6(dirname4(path), { recursive: true });
13518
+ mkdirSync7(dirname5(path), { recursive: true });
13269
13519
  atomicReplaceFile(path, `#!/bin/sh
13270
13520
  ${block.join("\n")}
13271
13521
  `, hookMode());
@@ -13342,12 +13592,12 @@ ${block.join("\n")}
13342
13592
  function planCodeowners(cwd) {
13343
13593
  const rel = ".github/CODEOWNERS";
13344
13594
  const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
13345
- (r) => writeTargetKind(join14(cwd, r)) !== "absent"
13595
+ (r) => writeTargetKind(join15(cwd, r)) !== "absent"
13346
13596
  );
13347
- const path = join14(cwd, existingRel ?? rel);
13597
+ const path = join15(cwd, existingRel ?? rel);
13348
13598
  const refused = refuseNonRegular(path);
13349
13599
  if (refused) return { item: "codeowners", path: existingRel ?? rel, status: "error", detail: refused };
13350
- const existing = existingRel ? readFileSync12(path, "utf8") : null;
13600
+ const existing = existingRel ? readFileSync13(path, "utf8") : null;
13351
13601
  const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
13352
13602
  if (existingRel !== void 0 && missing.length === 0) {
13353
13603
  return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
@@ -13373,21 +13623,21 @@ ${CODEOWNERS_MARK}
13373
13623
  warning: `${inferred.owner} was inferred from the origin remote and is not confirmed to be a user. If "${inferred.owner.slice(1)}" is an organisation, replace it with ${inferred.owner}/<team> in CODEOWNERS: GitHub rejects a bare organisation as an owner, and a rule with an invalid owner protects nothing.`
13374
13624
  } : {},
13375
13625
  apply: () => {
13376
- mkdirSync6(dirname4(path), { recursive: true });
13626
+ mkdirSync7(dirname5(path), { recursive: true });
13377
13627
  atomicReplaceFile(path, existing === null ? block.replace(/^\n/, "") : existing.replace(/\n?$/, "\n") + block, existingMode(path, 420));
13378
13628
  }
13379
13629
  };
13380
13630
  }
13381
13631
  function planWorkflow(cwd, force) {
13382
13632
  const rel = ".github/workflows/tamperward.yml";
13383
- const path = join14(cwd, rel);
13633
+ const path = join15(cwd, rel);
13384
13634
  const write = () => {
13385
- mkdirSync6(dirname4(path), { recursive: true });
13635
+ mkdirSync7(dirname5(path), { recursive: true });
13386
13636
  atomicReplaceFile(path, workflowFile(WORKFLOW_CONTENT), existingMode(path, 420));
13387
13637
  };
13388
13638
  const refused = refuseNonRegular(path);
13389
13639
  if (refused) return { item: "ci", path: rel, status: "error", detail: refused };
13390
- if (!existsSync11(path)) {
13640
+ if (!existsSync12(path)) {
13391
13641
  return {
13392
13642
  item: "ci",
13393
13643
  path: rel,
@@ -13398,7 +13648,7 @@ function planWorkflow(cwd, force) {
13398
13648
  }
13399
13649
  let src;
13400
13650
  try {
13401
- src = readFileSync12(path, "utf8");
13651
+ src = readFileSync13(path, "utf8");
13402
13652
  } catch {
13403
13653
  return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
13404
13654
  }
@@ -13459,7 +13709,7 @@ function planInit(cwd, opts = {}) {
13459
13709
  ];
13460
13710
  }
13461
13711
  function runInit(opts) {
13462
- const requested = resolve4(opts.cwd ?? process.cwd());
13712
+ const requested = resolve5(opts.cwd ?? process.cwd());
13463
13713
  const cwd = repoRoot(requested);
13464
13714
  const w = opts.quiet ? { write: (_text) => true } : process.stdout;
13465
13715
  if (cwd !== requested && !sameDir(cwd, requested)) {
@@ -13657,7 +13907,7 @@ jobs:
13657
13907
 
13658
13908
  // src/fingerprint.ts
13659
13909
  import { execFileSync as execFileSync7 } from "node:child_process";
13660
- import { createHash as createHash7 } from "node:crypto";
13910
+ import { createHash as createHash8 } from "node:crypto";
13661
13911
  function treeFingerprint(cwd, keepIgnored) {
13662
13912
  const listed = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
13663
13913
  cwd,
@@ -13666,7 +13916,7 @@ function treeFingerprint(cwd, keepIgnored) {
13666
13916
  env: trustedGitEnv()
13667
13917
  }).split("\0").filter(Boolean);
13668
13918
  const files = [.../* @__PURE__ */ new Set([...listed, ...keepIgnored ? ignoredPaths({ cwd }, keepIgnored) : []])].sort();
13669
- const h = createHash7("sha256");
13919
+ const h = createHash8("sha256");
13670
13920
  for (const rel of files) {
13671
13921
  h.update(rel);
13672
13922
  h.update("\0");
@@ -13690,12 +13940,12 @@ var init_fingerprint = __esm({
13690
13940
  });
13691
13941
 
13692
13942
  // src/dependency-env.ts
13693
- import { createHash as createHash8 } from "node:crypto";
13943
+ import { createHash as createHash9 } from "node:crypto";
13694
13944
  import {
13695
- existsSync as existsSync12,
13945
+ existsSync as existsSync13,
13696
13946
  lstatSync as lstatSync8,
13697
13947
  readdirSync as readdirSync3,
13698
- readFileSync as readFileSync13,
13948
+ readFileSync as readFileSync14,
13699
13949
  readlinkSync as readlinkSync2,
13700
13950
  realpathSync as realpathSync6,
13701
13951
  statSync as statSync3
@@ -13703,19 +13953,19 @@ import {
13703
13953
  import {
13704
13954
  basename as basename4,
13705
13955
  delimiter,
13706
- dirname as dirname5,
13707
- isAbsolute as isAbsolute9,
13708
- join as join15,
13956
+ dirname as dirname6,
13957
+ isAbsolute as isAbsolute10,
13958
+ join as join16,
13709
13959
  relative as relative4,
13710
- resolve as resolve5,
13960
+ resolve as resolve6,
13711
13961
  sep as sep2
13712
13962
  } from "node:path";
13713
13963
  function metricsFor(descriptor) {
13714
13964
  return descriptor.metrics ??= { fullSnapshots: 0, reusedSnapshots: 0, totalMs: 0 };
13715
13965
  }
13716
13966
  function inside(root, candidate) {
13717
- const rel = relative4(resolve5(root), resolve5(candidate));
13718
- return rel === "" || rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute9(rel);
13967
+ const rel = relative4(resolve6(root), resolve6(candidate));
13968
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute10(rel);
13719
13969
  }
13720
13970
  function modeKind(path) {
13721
13971
  const st = lstatSync8(path);
@@ -13753,7 +14003,7 @@ function fingerprintRoot(root, cwd, h) {
13753
14003
  st = statSync3(path);
13754
14004
  if (!st.isFile()) throw new Error("not a regular file");
13755
14005
  h.update(label + "\0external-file\0" + path + "\0" + String(st.mode) + "\0");
13756
- h.update(readFileSync13(path));
14006
+ h.update(readFileSync14(path));
13757
14007
  h.update("\0");
13758
14008
  } catch (e) {
13759
14009
  throw new Unattestable(
@@ -13774,7 +14024,7 @@ function fingerprintRoot(root, cwd, h) {
13774
14024
  );
13775
14025
  }
13776
14026
  for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
13777
- const path = join15(dir, entry.name);
14027
+ const path = join16(dir, entry.name);
13778
14028
  const rel = namespace ? namespace + "/" + entry.name : entry.name;
13779
14029
  let ident;
13780
14030
  try {
@@ -13787,7 +14037,7 @@ function fingerprintRoot(root, cwd, h) {
13787
14037
  h.update(rel + "\0" + ident.kind + "\0" + String(ident.mode) + "\0");
13788
14038
  if (ident.kind === "file") {
13789
14039
  try {
13790
- h.update(readFileSync13(path));
14040
+ h.update(readFileSync14(path));
13791
14041
  h.update("\0");
13792
14042
  } catch (e) {
13793
14043
  throw new Unattestable(
@@ -13843,10 +14093,10 @@ function fingerprintRoot(root, cwd, h) {
13843
14093
  walk2(root.realPath, "");
13844
14094
  }
13845
14095
  function snapshot(roots, probes, cwd) {
13846
- const h = createHash8("sha256");
14096
+ const h = createHash9("sha256");
13847
14097
  for (const probe of probes) {
13848
14098
  h.update(
13849
- "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync12(probe.path) ? "present" : "absent") + "\0"
14099
+ "probe\0" + probe.kind + "\0" + probe.path + "\0" + (existsSync13(probe.path) ? "present" : "absent") + "\0"
13850
14100
  );
13851
14101
  }
13852
14102
  for (const root of roots) fingerprintRoot(root, cwd, h);
@@ -13867,7 +14117,7 @@ function freezeRoot(kind, path) {
13867
14117
  if (!statSync3(path).isDirectory()) {
13868
14118
  throw new Error("not a directory");
13869
14119
  }
13870
- return { kind, path: resolve5(path), realPath: realpathSync6(path) };
14120
+ return { kind, path: resolve6(path), realPath: realpathSync6(path) };
13871
14121
  } catch (e) {
13872
14122
  throw new Unattestable(
13873
14123
  kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
@@ -13903,17 +14153,17 @@ function firstExecutable(command) {
13903
14153
  function resolveExecutable(token, cwd, env) {
13904
14154
  if (!token) return null;
13905
14155
  if (token.includes("/") || token.includes("\\")) {
13906
- const path = isAbsolute9(token) ? token : resolve5(cwd, token);
14156
+ const path = isAbsolute10(token) ? token : resolve6(cwd, token);
13907
14157
  try {
13908
- return existsSync12(path) ? realpathSync6(path) : null;
14158
+ return existsSync13(path) ? realpathSync6(path) : null;
13909
14159
  } catch {
13910
14160
  return null;
13911
14161
  }
13912
14162
  }
13913
14163
  for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
13914
- const candidate = resolve5(entry, token);
14164
+ const candidate = resolve6(entry, token);
13915
14165
  try {
13916
- if (existsSync12(candidate) && statSync3(candidate).isFile()) return realpathSync6(candidate);
14166
+ if (existsSync13(candidate) && statSync3(candidate).isFile()) return realpathSync6(candidate);
13917
14167
  } catch {
13918
14168
  }
13919
14169
  }
@@ -13930,13 +14180,13 @@ function looksPython(command, executable, token) {
13930
14180
  }
13931
14181
  function inferredVenvFromToken(token, cwd) {
13932
14182
  if (!token || !token.includes("/") && !token.includes("\\")) return null;
13933
- const path = isAbsolute9(token) ? token : resolve5(cwd, token);
14183
+ const path = isAbsolute10(token) ? token : resolve6(cwd, token);
13934
14184
  if (!pythonName(basename4(path))) return null;
13935
- const parent = dirname5(path);
14185
+ const parent = dirname6(path);
13936
14186
  const parentName = basename4(parent).toLowerCase();
13937
14187
  if (parentName !== "bin" && parentName !== "scripts") return null;
13938
- const root = dirname5(parent);
13939
- return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
14188
+ const root = dirname6(parent);
14189
+ return existsSync13(join16(root, "pyvenv.cfg")) ? root : null;
13940
14190
  }
13941
14191
  function unsupportedEcosystem(command, token) {
13942
14192
  const name = token ? basename4(token).toLowerCase() : "";
@@ -13948,17 +14198,17 @@ function unsupportedEcosystem(command, token) {
13948
14198
  }
13949
14199
  function inferredVenv(executable) {
13950
14200
  if (!executable) return null;
13951
- const parent = dirname5(executable);
14201
+ const parent = dirname6(executable);
13952
14202
  const parentName = basename4(parent).toLowerCase();
13953
14203
  if (parentName !== "bin" && parentName !== "scripts") return null;
13954
- const root = dirname5(parent);
13955
- return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
14204
+ const root = dirname6(parent);
14205
+ return existsSync13(join16(root, "pyvenv.cfg")) ? root : null;
13956
14206
  }
13957
14207
  function venvProblem(root) {
13958
- const cfg = join15(root, "pyvenv.cfg");
13959
- if (!existsSync12(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
14208
+ const cfg = join16(root, "pyvenv.cfg");
14209
+ if (!existsSync13(cfg)) return "VIRTUAL_ENV " + JSON.stringify(root) + " has no pyvenv.cfg";
13960
14210
  try {
13961
- const body = readFileSync13(cfg, "utf8");
14211
+ const body = readFileSync14(cfg, "utf8");
13962
14212
  if (/^\s*include-system-site-packages\s*=\s*true\s*$/im.test(body)) {
13963
14213
  return "Python environment " + JSON.stringify(root) + " enables system site-packages; those external package roots are not bounded";
13964
14214
  }
@@ -13968,7 +14218,7 @@ function venvProblem(root) {
13968
14218
  return null;
13969
14219
  }
13970
14220
  function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
13971
- const cwd = resolve5(cwdInput);
14221
+ const cwd = resolve6(cwdInput);
13972
14222
  const roots = [];
13973
14223
  const probes = [];
13974
14224
  const seen = /* @__PURE__ */ new Set();
@@ -13982,21 +14232,21 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
13982
14232
  }
13983
14233
  };
13984
14234
  try {
13985
- const nodeModules = join15(cwd, "node_modules");
13986
- if (existsSync12(nodeModules)) addRoot("node_modules", nodeModules);
14235
+ const nodeModules = join16(cwd, "node_modules");
14236
+ if (existsSync13(nodeModules)) addRoot("node_modules", nodeModules);
13987
14237
  else probes.push({ kind: "node_modules", path: nodeModules });
13988
14238
  const token = firstExecutable(command);
13989
14239
  const executable = resolveExecutable(token, cwd, env);
13990
14240
  let venv = null;
13991
14241
  if (env.VIRTUAL_ENV?.trim()) {
13992
- if (!isAbsolute9(env.VIRTUAL_ENV)) {
14242
+ if (!isAbsolute10(env.VIRTUAL_ENV)) {
13993
14243
  throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
13994
14244
  }
13995
- venv = resolve5(env.VIRTUAL_ENV);
14245
+ venv = resolve6(env.VIRTUAL_ENV);
13996
14246
  } else {
13997
14247
  venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
13998
- if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync12(join15(cwd, ".venv", "pyvenv.cfg"))) {
13999
- venv = join15(cwd, ".venv");
14248
+ if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync13(join16(cwd, ".venv", "pyvenv.cfg"))) {
14249
+ venv = join16(cwd, ".venv");
14000
14250
  }
14001
14251
  }
14002
14252
  if (venv) {
@@ -14032,7 +14282,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
14032
14282
  const fingerprint2 = measuredSnapshot(
14033
14283
  descriptor.roots,
14034
14284
  descriptor.probes,
14035
- resolve5(cwdInput),
14285
+ resolve6(cwdInput),
14036
14286
  metricsFor(descriptor)
14037
14287
  );
14038
14288
  if (fingerprint2 !== descriptor.fingerprint) {
@@ -14045,7 +14295,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
14045
14295
  }
14046
14296
  function attestDependencyEnvironment(cwdInput, descriptor) {
14047
14297
  const attestation = {
14048
- cwd: resolve5(cwdInput),
14298
+ cwd: resolve6(cwdInput),
14049
14299
  descriptor,
14050
14300
  check: checkDependencyEnvironment(cwdInput, descriptor)
14051
14301
  };
@@ -14053,7 +14303,7 @@ function attestDependencyEnvironment(cwdInput, descriptor) {
14053
14303
  return attestation;
14054
14304
  }
14055
14305
  function reuseDependencyEnvironmentAttestation(cwdInput, descriptor, attestation) {
14056
- if (!attestation || !issuedAttestations.has(attestation) || attestation.descriptor !== descriptor || attestation.cwd !== resolve5(cwdInput)) return null;
14306
+ if (!attestation || !issuedAttestations.has(attestation) || attestation.descriptor !== descriptor || attestation.cwd !== resolve6(cwdInput)) return null;
14057
14307
  metricsFor(descriptor).reusedSnapshots += 1;
14058
14308
  return attestation.check;
14059
14309
  }
@@ -14102,7 +14352,7 @@ var init_dependency_env = __esm({
14102
14352
  import { spawnSync } from "node:child_process";
14103
14353
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
14104
14354
  import { tmpdir as tmpdir3 } from "node:os";
14105
- import { join as join16 } from "node:path";
14355
+ import { join as join17 } from "node:path";
14106
14356
  function emptySuiteDiagnostics() {
14107
14357
  return {
14108
14358
  stdout: { ...EMPTY_STREAM },
@@ -14181,8 +14431,8 @@ function parseCapturedSupervisorResult(stdoutText) {
14181
14431
  };
14182
14432
  }
14183
14433
  function runCapturedProcessSync(executable, args, opts) {
14184
- const stateDir = mkdtempSync2(join16(tmpdir3(), "tw-suite-capture-"));
14185
- const configFile = join16(stateDir, "config.json");
14434
+ const stateDir = mkdtempSync2(join17(tmpdir3(), "tw-suite-capture-"));
14435
+ const configFile = join17(stateDir, "config.json");
14186
14436
  const backstop = opts.backstopMs ?? 3e4;
14187
14437
  try {
14188
14438
  writeFileSync8(
@@ -14417,10 +14667,10 @@ if (child) {
14417
14667
  });
14418
14668
 
14419
14669
  // src/verifier-backend.ts
14420
- import { createHash as createHash9, randomUUID } from "node:crypto";
14670
+ import { createHash as createHash10, randomUUID } from "node:crypto";
14421
14671
  import { spawnSync as spawnSync2 } from "node:child_process";
14422
- import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
14423
- import { delimiter as delimiter2, dirname as dirname6, join as join17, resolve as resolve6 } from "node:path";
14672
+ import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync16, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
14673
+ import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as resolve7 } from "node:path";
14424
14674
  function identity() {
14425
14675
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
14426
14676
  const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
@@ -14440,12 +14690,12 @@ function writableByCaller(path) {
14440
14690
  }
14441
14691
  }
14442
14692
  function replaceableByCaller(path) {
14443
- let current = resolve6(path);
14693
+ let current = resolve7(path);
14444
14694
  if (writableByCaller(current)) return true;
14445
- current = dirname6(current);
14695
+ current = dirname7(current);
14446
14696
  while (true) {
14447
14697
  if (writableByCaller(current)) return true;
14448
- const parent = dirname6(current);
14698
+ const parent = dirname7(current);
14449
14699
  if (parent === current) break;
14450
14700
  current = parent;
14451
14701
  }
@@ -14454,7 +14704,7 @@ function replaceableByCaller(path) {
14454
14704
  function findExecutable(name) {
14455
14705
  for (const raw of (process.env.PATH ?? "").split(delimiter2)) {
14456
14706
  if (!raw) continue;
14457
- const candidate = join17(raw, name);
14707
+ const candidate = join18(raw, name);
14458
14708
  try {
14459
14709
  accessSync2(candidate, constants3.X_OK);
14460
14710
  return realpathSync7(candidate);
@@ -14464,7 +14714,7 @@ function findExecutable(name) {
14464
14714
  return null;
14465
14715
  }
14466
14716
  function fileSha256(path) {
14467
- return createHash9("sha256").update(readFileSync15(path)).digest("hex");
14717
+ return createHash10("sha256").update(readFileSync16(path)).digest("hex");
14468
14718
  }
14469
14719
  function engineClientEnv() {
14470
14720
  return {
@@ -14786,7 +15036,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
14786
15036
  const args = containerRunArgs({
14787
15037
  image: backend.image,
14788
15038
  name,
14789
- workspace: resolve6(dir),
15039
+ workspace: resolve7(dir),
14790
15040
  command,
14791
15041
  uid,
14792
15042
  gid,
@@ -14797,7 +15047,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
14797
15047
  backend.enginePath,
14798
15048
  dockerArgs(backend.daemonHost, args),
14799
15049
  {
14800
- cwd: dirname6(backend.enginePath),
15050
+ cwd: dirname7(backend.enginePath),
14801
15051
  env: engineClientEnv(),
14802
15052
  timeoutMs: budgetSecs * 1e3,
14803
15053
  backstopMs: 3e4
@@ -14897,10 +15147,10 @@ var init_verifier_backend = __esm({
14897
15147
 
14898
15148
  // src/cli/verify.ts
14899
15149
  import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:child_process";
14900
- import { createHash as createHash10 } from "node:crypto";
14901
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync7, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "node:fs";
15150
+ import { createHash as createHash11 } from "node:crypto";
15151
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, readlinkSync as readlinkSync3, rmSync as rmSync7, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync14 } from "node:fs";
14902
15152
  import { tmpdir as tmpdir4 } from "node:os";
14903
- import { dirname as dirname7, isAbsolute as isAbsolute10, join as join18, relative as relative5, resolve as resolve7, sep as sep3 } from "node:path";
15153
+ import { dirname as dirname8, isAbsolute as isAbsolute11, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "node:path";
14904
15154
  function oracleAssuranceReport() {
14905
15155
  return {
14906
15156
  level: "suite-exit-only",
@@ -14926,8 +15176,8 @@ function baseIsAncestorOfHead(base, cwd) {
14926
15176
  return r.status === 0;
14927
15177
  }
14928
15178
  function inside2(root, path) {
14929
- const rel = relative5(resolve7(root), resolve7(path));
14930
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute10(rel);
15179
+ const rel = relative5(resolve8(root), resolve8(path));
15180
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute11(rel);
14931
15181
  }
14932
15182
  function linkParts(path) {
14933
15183
  const parts = sep3 === "\\" ? path.split(/[\\/]+/) : path.split("/");
@@ -14940,10 +15190,10 @@ function driveRelativeLinkTarget(target) {
14940
15190
  return sep3 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
14941
15191
  }
14942
15192
  function rootedLinkTarget(target) {
14943
- return isAbsolute10(target) || driveRelativeLinkTarget(target);
15193
+ return isAbsolute11(target) || driveRelativeLinkTarget(target);
14944
15194
  }
14945
15195
  function safeSymlink(target, out3, root, label) {
14946
- if (rootedLinkTarget(target) || !inside2(root, resolve7(dirname7(out3), target))) {
15196
+ if (rootedLinkTarget(target) || !inside2(root, resolve8(dirname8(out3), target))) {
14947
15197
  throw linkEscape(label, target);
14948
15198
  }
14949
15199
  rmSync7(out3, { force: true });
@@ -14951,7 +15201,7 @@ function safeSymlink(target, out3, root, label) {
14951
15201
  }
14952
15202
  function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
14953
15203
  let domain = "tree";
14954
- let stack = linkParts(relative5(root, dirname7(out3)));
15204
+ let stack = linkParts(relative5(root, dirname8(out3)));
14955
15205
  const pending = linkParts(target);
14956
15206
  const seen = /* @__PURE__ */ new Set();
14957
15207
  let hops = 0;
@@ -14968,7 +15218,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
14968
15218
  continue;
14969
15219
  }
14970
15220
  const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
14971
- const current = join18(base, ...stack);
15221
+ const current = join19(base, ...stack);
14972
15222
  let st;
14973
15223
  try {
14974
15224
  st = lstatSync9(current);
@@ -14989,12 +15239,12 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
14989
15239
  if (driveRelativeLinkTarget(next)) {
14990
15240
  throw linkEscape(label, target);
14991
15241
  }
14992
- if (isAbsolute10(next)) {
15242
+ if (isAbsolute11(next)) {
14993
15243
  if (domain === "tree" || !dependencyRoot || !inside2(dependencyRoot, next)) {
14994
15244
  throw linkEscape(label, target);
14995
15245
  }
14996
15246
  stack = [];
14997
- pending.unshift(...linkParts(relative5(dependencyRoot, resolve7(next))));
15247
+ pending.unshift(...linkParts(relative5(dependencyRoot, resolve8(next))));
14998
15248
  } else {
14999
15249
  pending.unshift(...linkParts(next));
15000
15250
  }
@@ -15004,7 +15254,7 @@ function rejectLinkedParent(cwd, rel) {
15004
15254
  const parts = rel.split("/").slice(0, -1);
15005
15255
  let current = cwd;
15006
15256
  for (const part of parts) {
15007
- current = join18(current, part);
15257
+ current = join19(current, part);
15008
15258
  let st;
15009
15259
  try {
15010
15260
  st = lstatSync9(current);
@@ -15020,7 +15270,7 @@ function materialize(cwd, dest, dependencyRoot) {
15020
15270
  const listed = workingTreePaths(cwd);
15021
15271
  const links = [];
15022
15272
  for (const rel of listed) {
15023
- const src = join18(cwd, rel);
15273
+ const src = join19(cwd, rel);
15024
15274
  rejectLinkedParent(cwd, rel);
15025
15275
  let st;
15026
15276
  try {
@@ -15028,8 +15278,8 @@ function materialize(cwd, dest, dependencyRoot) {
15028
15278
  } catch {
15029
15279
  continue;
15030
15280
  }
15031
- const out3 = join18(dest, rel);
15032
- mkdirSync7(dirname7(out3), { recursive: true });
15281
+ const out3 = join19(dest, rel);
15282
+ mkdirSync8(dirname8(out3), { recursive: true });
15033
15283
  dropSymlink(out3);
15034
15284
  if (st.isSymbolicLink()) {
15035
15285
  const target = readlinkSync3(src);
@@ -15045,17 +15295,17 @@ function materialize(cwd, dest, dependencyRoot) {
15045
15295
  for (const link of links) {
15046
15296
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
15047
15297
  }
15048
- if (dependencyRoot) symlinkSync(dependencyRoot, join18(dest, "node_modules"), "dir");
15298
+ if (dependencyRoot) symlinkSync(dependencyRoot, join19(dest, "node_modules"), "dir");
15049
15299
  }
15050
15300
  function workingTreePaths(cwd) {
15051
15301
  return git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
15052
15302
  }
15053
15303
  function probeFilesystemCaseSensitivity(dir) {
15054
- const upper = join18(dir, ".tw-case-probe-A");
15055
- const lower = join18(dir, ".tw-case-probe-a");
15304
+ const upper = join19(dir, ".tw-case-probe-A");
15305
+ const lower = join19(dir, ".tw-case-probe-a");
15056
15306
  try {
15057
15307
  writeFileSync9(upper, "", { flag: "wx" });
15058
- return !existsSync13(lower);
15308
+ return !existsSync14(lower);
15059
15309
  } finally {
15060
15310
  rmSync7(upper, { force: true });
15061
15311
  }
@@ -15111,15 +15361,15 @@ function baseEntries(base, cwd) {
15111
15361
  });
15112
15362
  }
15113
15363
  function overlayDigest(dest, paths) {
15114
- const h = createHash10("sha256");
15364
+ const h = createHash11("sha256");
15115
15365
  for (const rel of [...paths].sort()) {
15116
15366
  h.update(rel);
15117
15367
  h.update("\0");
15118
- const p = join18(dest, rel);
15368
+ const p = join19(dest, rel);
15119
15369
  try {
15120
15370
  const st = lstatSync9(p);
15121
15371
  h.update(String(st.mode));
15122
- h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync16(p));
15372
+ h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync17(p));
15123
15373
  } catch {
15124
15374
  h.update("<gone>");
15125
15375
  }
@@ -15149,8 +15399,8 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
15149
15399
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
15150
15400
  }
15151
15401
  const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
15152
- const out3 = join18(dest, e.path);
15153
- mkdirSync7(dirname7(out3), { recursive: true });
15402
+ const out3 = join19(dest, e.path);
15403
+ mkdirSync8(dirname8(out3), { recursive: true });
15154
15404
  rmSync7(out3, { force: true });
15155
15405
  if (e.mode === "120000") {
15156
15406
  const target = content.toString("utf8");
@@ -15166,7 +15416,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
15166
15416
  const inCopy = workingTreePaths(cwd);
15167
15417
  for (const rel of inCopy) {
15168
15418
  if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
15169
- rmSync7(join18(dest, rel), { force: true });
15419
+ rmSync7(join19(dest, rel), { force: true });
15170
15420
  removed++;
15171
15421
  }
15172
15422
  for (const link of restoredLinks) {
@@ -15180,8 +15430,8 @@ function suiteEnv(scratch) {
15180
15430
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
15181
15431
  env[k] = v;
15182
15432
  }
15183
- const userRc = join18(scratch, "npmrc-user");
15184
- const globalRc = join18(scratch, "npmrc-global");
15433
+ const userRc = join19(scratch, "npmrc-user");
15434
+ const globalRc = join19(scratch, "npmrc-global");
15185
15435
  for (const f of [userRc, globalRc]) writeFileSync9(f, "", { mode: 292 });
15186
15436
  env.npm_config_userconfig = userRc;
15187
15437
  env.npm_config_globalconfig = globalRc;
@@ -15213,7 +15463,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
15213
15463
  reason: `checkpointed-local verifier is unsupported on ${process.platform}; no trusted local shell contract is defined`
15214
15464
  };
15215
15465
  }
15216
- const scratch = mkdtempSync3(join18(tmpdir4(), "tw-verify-run-"));
15466
+ const scratch = mkdtempSync3(join19(tmpdir4(), "tw-verify-run-"));
15217
15467
  try {
15218
15468
  const r = runCapturedProcessSync(shell.executable, shell.args, {
15219
15469
  cwd: dir,
@@ -15402,8 +15652,8 @@ function runVerify(opts) {
15402
15652
  } catch {
15403
15653
  return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
15404
15654
  }
15405
- const visRoot = mkdtempSync3(join18(tmpdir4(), "tw-verify-vis-"));
15406
- const visDir = join18(visRoot, "t");
15655
+ const visRoot = mkdtempSync3(join19(tmpdir4(), "tw-verify-vis-"));
15656
+ const visDir = join19(visRoot, "t");
15407
15657
  const cleanup = (dirs) => {
15408
15658
  if (opts.keep) return;
15409
15659
  for (const d of dirs) rmSync7(d, { recursive: true, force: true });
@@ -15412,7 +15662,7 @@ function runVerify(opts) {
15412
15662
  if (!filesystemCaseSensitive) {
15413
15663
  const present = (rel) => {
15414
15664
  try {
15415
- lstatSync9(join18(cwd, rel));
15665
+ lstatSync9(join19(cwd, rel));
15416
15666
  return true;
15417
15667
  } catch {
15418
15668
  return false;
@@ -15444,7 +15694,7 @@ function runVerify(opts) {
15444
15694
  );
15445
15695
  }
15446
15696
  try {
15447
- mkdirSync7(visDir);
15697
+ mkdirSync8(visDir);
15448
15698
  materialize(cwd, visDir, frozenNodeModules);
15449
15699
  } catch (e) {
15450
15700
  cleanup([visRoot]);
@@ -15498,12 +15748,12 @@ function runVerify(opts) {
15498
15748
  { stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
15499
15749
  );
15500
15750
  }
15501
- const priRoot = mkdtempSync3(join18(tmpdir4(), "tw-verify-pri-"));
15502
- const priDir = join18(priRoot, "t");
15751
+ const priRoot = mkdtempSync3(join19(tmpdir4(), "tw-verify-pri-"));
15752
+ const priDir = join19(priRoot, "t");
15503
15753
  let restored = [];
15504
15754
  let removedAdded = 0;
15505
15755
  try {
15506
- mkdirSync7(priDir);
15756
+ mkdirSync8(priDir);
15507
15757
  materialize(cwd, priDir, frozenNodeModules);
15508
15758
  ({ restored, removed: removedAdded } = overlayPristine(
15509
15759
  cwd,
@@ -15772,10 +16022,10 @@ var init_verify = __esm({
15772
16022
 
15773
16023
  // src/cli/run.ts
15774
16024
  import { execFileSync as execFileSync9, spawn, spawnSync as spawnSync4 } from "node:child_process";
15775
- import { randomBytes as randomBytes2 } from "node:crypto";
15776
- import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync9, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
16025
+ import { randomBytes as randomBytes3 } from "node:crypto";
16026
+ import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync18, readlinkSync as readlinkSync4, realpathSync as realpathSync9, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
15777
16027
  import { tmpdir as tmpdir5 } from "node:os";
15778
- import { dirname as dirname8, join as join19, resolve as resolve8 } from "node:path";
16028
+ import { dirname as dirname9, join as join20, resolve as resolve9 } from "node:path";
15779
16029
  function authoritativeRunLifecyclePlatform(platform = process.platform) {
15780
16030
  return platform === "linux";
15781
16031
  }
@@ -15812,7 +16062,7 @@ function writableByCaller2(path) {
15812
16062
  return true;
15813
16063
  } catch {
15814
16064
  }
15815
- const parent = dirname8(cur);
16065
+ const parent = dirname9(cur);
15816
16066
  if (parent === cur) break;
15817
16067
  cur = parent;
15818
16068
  }
@@ -15851,9 +16101,9 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
15851
16101
  };
15852
16102
  }
15853
16103
  function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
15854
- const stateDir = mkdtempSync4(join19(tmpdir5(), "tw-agent-supervisor-"));
15855
- const resultFile = join19(stateDir, "result.json");
15856
- const agentEnvFile = join19(stateDir, "agent-env.json");
16104
+ const stateDir = mkdtempSync4(join20(tmpdir5(), "tw-agent-supervisor-"));
16105
+ const resultFile = join20(stateDir, "result.json");
16106
+ const agentEnvFile = join20(stateDir, "agent-env.json");
15857
16107
  try {
15858
16108
  const linux = process.platform === "linux";
15859
16109
  let executable;
@@ -15927,7 +16177,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
15927
16177
  }
15928
16178
  let state = {};
15929
16179
  try {
15930
- state = JSON.parse(readFileSync17(resultFile, "utf8"));
16180
+ state = JSON.parse(readFileSync18(resultFile, "utf8"));
15931
16181
  } catch {
15932
16182
  return {
15933
16183
  exit: 1,
@@ -15971,7 +16221,7 @@ function observerExited(pid) {
15971
16221
  if (!pidAlive3(pid)) return true;
15972
16222
  if (process.platform !== "linux") return false;
15973
16223
  try {
15974
- const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
16224
+ const stat = readFileSync18(`/proc/${pid}/stat`, "utf8");
15975
16225
  const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0];
15976
16226
  return state === "Z" || state === "X";
15977
16227
  } catch (e) {
@@ -15979,11 +16229,11 @@ function observerExited(pid) {
15979
16229
  }
15980
16230
  }
15981
16231
  function supervisedObserverLog(cwd) {
15982
- const gd = gitDir(cwd) ?? join19(cwd, ".git");
15983
- return join19(
16232
+ const gd = gitDir(cwd) ?? join20(cwd, ".git");
16233
+ return join20(
15984
16234
  gd,
15985
16235
  "tamperward",
15986
- `run-observer-${process.pid}-${Date.now()}-${randomBytes2(4).toString("hex")}.jsonl`
16236
+ `run-observer-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}.jsonl`
15987
16237
  );
15988
16238
  }
15989
16239
  function startSupervisedObserver(cwd, base, entry) {
@@ -15994,7 +16244,7 @@ function startSupervisedObserver(cwd, base, entry) {
15994
16244
  try {
15995
16245
  child = spawn(
15996
16246
  process.execPath,
15997
- [resolve8(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
16247
+ [resolve9(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
15998
16248
  {
15999
16249
  cwd,
16000
16250
  stdio: ["ignore", "ignore", "inherit"],
@@ -16093,7 +16343,7 @@ function git3(args, cwd) {
16093
16343
  }
16094
16344
  function nowTicks() {
16095
16345
  try {
16096
- return Math.floor(parseFloat(readFileSync17("/proc/uptime", "utf8").split(" ")[0]) * 100);
16346
+ return Math.floor(parseFloat(readFileSync18("/proc/uptime", "utf8").split(" ")[0]) * 100);
16097
16347
  } catch {
16098
16348
  return Number.POSITIVE_INFINITY;
16099
16349
  }
@@ -16116,7 +16366,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
16116
16366
  const n = Number(pid);
16117
16367
  if (n === process.pid) continue;
16118
16368
  try {
16119
- const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
16369
+ const stat = readFileSync18(`/proc/${pid}/stat`, "utf8");
16120
16370
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
16121
16371
  if (!Number.isFinite(startTicks) || startTicks < spawnedAfterTicks) continue;
16122
16372
  } catch {
@@ -16131,7 +16381,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
16131
16381
  for (const link of procLinks) {
16132
16382
  try {
16133
16383
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
16134
- const held = raw.startsWith("/") ? resolve8(raw) : raw;
16384
+ const held = raw.startsWith("/") ? resolve9(raw) : raw;
16135
16385
  if (held === real || held.startsWith(real + "/")) {
16136
16386
  holds = true;
16137
16387
  break;
@@ -16144,7 +16394,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
16144
16394
  return out3;
16145
16395
  }
16146
16396
  function runEnvelope(opts) {
16147
- const cwd = resolve8(opts.cwd ?? process.cwd());
16397
+ const cwd = resolve9(opts.cwd ?? process.cwd());
16148
16398
  const say = opts.json ? (_s) => {
16149
16399
  } : out;
16150
16400
  if (!opts.argv.length) {
@@ -16688,8 +16938,8 @@ raise SystemExit(0)
16688
16938
 
16689
16939
  // src/cli/doctor.ts
16690
16940
  import { execFileSync as execFileSync10 } from "node:child_process";
16691
- import { existsSync as existsSync14, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
16692
- import { join as join20, resolve as resolve9 } from "node:path";
16941
+ import { existsSync as existsSync15, readFileSync as readFileSync19, readdirSync as readdirSync5 } from "node:fs";
16942
+ import { join as join21, resolve as resolve10 } from "node:path";
16693
16943
  function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
16694
16944
  if (platform === "linux") {
16695
16945
  if (linuxPython?.path) {
@@ -16744,10 +16994,10 @@ function wiringState(status, authority) {
16744
16994
  return authority ? "BROKEN" : "WARN";
16745
16995
  }
16746
16996
  function pinsInFile(path) {
16747
- if (!existsSync14(path)) return [];
16997
+ if (!existsSync15(path)) return [];
16748
16998
  let src = "";
16749
16999
  try {
16750
- src = readFileSync18(path, "utf8");
17000
+ src = readFileSync19(path, "utf8");
16751
17001
  } catch {
16752
17002
  return [];
16753
17003
  }
@@ -16767,14 +17017,14 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
16767
17017
  const broken = [];
16768
17018
  const warnings = [];
16769
17019
  for (const rel of workflowRels) {
16770
- const path = resolve9(cwd, rel);
16771
- if (!existsSync14(path)) {
17020
+ const path = resolve10(cwd, rel);
17021
+ if (!existsSync15(path)) {
16772
17022
  broken.push(`${rel} is missing`);
16773
17023
  continue;
16774
17024
  }
16775
17025
  let doc;
16776
17026
  try {
16777
- doc = yaml.parse(readFileSync18(path, "utf8"));
17027
+ doc = yaml.parse(readFileSync19(path, "utf8"));
16778
17028
  } catch (e) {
16779
17029
  broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
16780
17030
  continue;
@@ -16887,9 +17137,9 @@ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow =
16887
17137
  addWiring("codeowners", "codeowners", true);
16888
17138
  checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
16889
17139
  const pins = [
16890
- ...pinsInFile(join20(cwd, ".claude", "settings.json")),
16891
- ...pinsInFile(join20(cwd, ".git", "hooks", "pre-commit")),
16892
- ...pinsInFile(join20(cwd, ".husky", "pre-commit"))
17140
+ ...pinsInFile(join21(cwd, ".claude", "settings.json")),
17141
+ ...pinsInFile(join21(cwd, ".git", "hooks", "pre-commit")),
17142
+ ...pinsInFile(join21(cwd, ".husky", "pre-commit"))
16893
17143
  ];
16894
17144
  const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
16895
17145
  const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
@@ -17114,7 +17364,7 @@ function verifyJobs(doc) {
17114
17364
  return found;
17115
17365
  }
17116
17366
  function diagnose(opts = {}) {
17117
- const cwd = repoRoot(resolve9(opts.cwd ?? process.cwd()));
17367
+ const cwd = repoRoot(resolve10(opts.cwd ?? process.cwd()));
17118
17368
  let policy;
17119
17369
  try {
17120
17370
  policy = policyFor(opts, cwd);
@@ -17138,8 +17388,8 @@ function diagnose(opts = {}) {
17138
17388
  workflowRels.push(opts.workflow);
17139
17389
  } else {
17140
17390
  const workflowDirRel = ".github/workflows";
17141
- const workflowDir = resolve9(cwd, workflowDirRel);
17142
- if (!existsSync14(workflowDir)) {
17391
+ const workflowDir = resolve10(cwd, workflowDirRel);
17392
+ if (!existsSync15(workflowDir)) {
17143
17393
  return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
17144
17394
  }
17145
17395
  let entries;
@@ -17151,20 +17401,20 @@ function diagnose(opts = {}) {
17151
17401
  `${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
17152
17402
  );
17153
17403
  }
17154
- for (const name of entries) workflowRels.push(join20(workflowDirRel, name));
17404
+ for (const name of entries) workflowRels.push(join21(workflowDirRel, name));
17155
17405
  }
17156
17406
  const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
17157
17407
  const requiredMinutes = Math.ceil(requiredSecs / 60);
17158
17408
  let verifyJobCount = 0;
17159
17409
  const authorityWorkflows = [];
17160
17410
  for (const workflowRel of workflowRels) {
17161
- const workflowPath = resolve9(cwd, workflowRel);
17162
- if (!existsSync14(workflowPath)) {
17411
+ const workflowPath = resolve10(cwd, workflowRel);
17412
+ if (!existsSync15(workflowPath)) {
17163
17413
  return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
17164
17414
  }
17165
17415
  let doc;
17166
17416
  try {
17167
- doc = yaml.parse(readFileSync18(workflowPath, "utf8"));
17417
+ doc = yaml.parse(readFileSync19(workflowPath, "utf8"));
17168
17418
  } catch (e) {
17169
17419
  return fail(
17170
17420
  "ci-verifier",
@@ -17320,16 +17570,16 @@ var init_doctor = __esm({
17320
17570
  // src/cli/trace-verify.ts
17321
17571
  import { execFileSync as execFileSync11, spawnSync as spawnSync5 } from "node:child_process";
17322
17572
  import {
17323
- existsSync as existsSync15,
17573
+ existsSync as existsSync16,
17324
17574
  mkdtempSync as mkdtempSync5,
17325
- readFileSync as readFileSync19,
17575
+ readFileSync as readFileSync20,
17326
17576
  readdirSync as readdirSync6,
17327
17577
  realpathSync as realpathSync10,
17328
17578
  rmSync as rmSync9,
17329
17579
  symlinkSync as symlinkSync2
17330
17580
  } from "node:fs";
17331
17581
  import { tmpdir as tmpdir6 } from "node:os";
17332
- import { isAbsolute as isAbsolute11, join as join21, relative as relative6, resolve as resolve10, sep as sep4 } from "node:path";
17582
+ import { isAbsolute as isAbsolute12, join as join22, relative as relative6, resolve as resolve11, sep as sep4 } from "node:path";
17333
17583
  function unescapeStraceString(s) {
17334
17584
  return s.replace(/\\([0-7]{1,3})/g, (_m, oct) => String.fromCharCode(parseInt(oct, 8))).replace(/\\x([0-9a-fA-F]{2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
17335
17585
  }
@@ -17354,11 +17604,11 @@ function parseStraceFileAccess(raw) {
17354
17604
  return out3;
17355
17605
  }
17356
17606
  function inside3(root, path) {
17357
- const rel = relative6(resolve10(root), resolve10(path));
17358
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute11(rel);
17607
+ const rel = relative6(resolve11(root), resolve11(path));
17608
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute12(rel);
17359
17609
  }
17360
17610
  function normalRepoPath(root, raw) {
17361
- const abs = isAbsolute11(raw) ? resolve10(raw) : resolve10(root, raw);
17611
+ const abs = isAbsolute12(raw) ? resolve11(raw) : resolve11(root, raw);
17362
17612
  if (!inside3(root, abs)) return null;
17363
17613
  const rel = relative6(root, abs).split(sep4).join("/");
17364
17614
  return rel && rel !== ".." ? rel : null;
@@ -17378,7 +17628,7 @@ function summarizeTraceRuns(opts) {
17378
17628
  for (const item of run) {
17379
17629
  const rel = normalRepoPath(opts.root, item.path);
17380
17630
  const isTracked = rel !== null && opts.tracked.has(rel);
17381
- const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve10(opts.root, item.path);
17631
+ const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve11(opts.root, item.path);
17382
17632
  if (keyPath === null) continue;
17383
17633
  const target = isTracked ? repo : external;
17384
17634
  const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
@@ -17455,36 +17705,36 @@ function materializeBase(base, cwd, dest) {
17455
17705
  `tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
17456
17706
  );
17457
17707
  }
17458
- const hostModules = join21(cwd, "node_modules");
17459
- const tracedModules = join21(dest, "node_modules");
17460
- if (existsSync15(hostModules) && !existsSync15(tracedModules)) {
17461
- symlinkSync2(resolve10(hostModules), tracedModules, "dir");
17708
+ const hostModules = join22(cwd, "node_modules");
17709
+ const tracedModules = join22(dest, "node_modules");
17710
+ if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
17711
+ symlinkSync2(resolve11(hostModules), tracedModules, "dir");
17462
17712
  }
17463
17713
  }
17464
17714
  function traceFiles(prefix) {
17465
- const dir = resolve10(prefix, "..");
17715
+ const dir = resolve11(prefix, "..");
17466
17716
  const base = prefix.split(sep4).at(-1) ?? prefix;
17467
- return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join21(dir, name)).sort();
17717
+ return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join22(dir, name)).sort();
17468
17718
  }
17469
17719
  function rewriteTraceRoot(accesses, actualRoot) {
17470
17720
  return accesses.map((item) => {
17471
- const abs = isAbsolute11(item.path) ? resolve10(item.path) : resolve10(actualRoot, item.path);
17721
+ const abs = isAbsolute12(item.path) ? resolve11(item.path) : resolve11(actualRoot, item.path);
17472
17722
  if (!inside3(actualRoot, abs)) return { ...item, path: abs };
17473
17723
  const rel = relative6(actualRoot, abs).split(sep4).join("/");
17474
17724
  try {
17475
- const real = resolve10(realpathSync10(abs));
17725
+ const real = resolve11(realpathSync10(abs));
17476
17726
  if (!inside3(actualRoot, real)) return { ...item, path: real };
17477
17727
  } catch {
17478
17728
  }
17479
- return { ...item, path: join21(TRACE_ROOT, ...rel.split("/")) };
17729
+ return { ...item, path: join22(TRACE_ROOT, ...rel.split("/")) };
17480
17730
  });
17481
17731
  }
17482
17732
  function runOneTrace(cwd, base, command, budget) {
17483
- const root = mkdtempSync5(join21(tmpdir6(), "tw-trace-base-"));
17484
- const traceDir = mkdtempSync5(join21(tmpdir6(), "tw-trace-log-"));
17733
+ const root = mkdtempSync5(join22(tmpdir6(), "tw-trace-base-"));
17734
+ const traceDir = mkdtempSync5(join22(tmpdir6(), "tw-trace-log-"));
17485
17735
  try {
17486
17736
  materializeBase(base, cwd, root);
17487
- const prefix = join21(traceDir, "trace");
17737
+ const prefix = join22(traceDir, "trace");
17488
17738
  const traced = spawnSync5(
17489
17739
  "strace",
17490
17740
  [
@@ -17517,7 +17767,7 @@ function runOneTrace(cwd, base, command, budget) {
17517
17767
  if (logs.length === 0) {
17518
17768
  throw new Error("strace produced no trace files");
17519
17769
  }
17520
- const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync19(path, "utf8")));
17770
+ const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync20(path, "utf8")));
17521
17771
  return {
17522
17772
  exit: traced.status ?? 1,
17523
17773
  accesses: rewriteTraceRoot(accesses, root)
@@ -17561,7 +17811,7 @@ function renderText2(report2) {
17561
17811
  for (const note of report2.notes) out3(`note: ${note}`);
17562
17812
  }
17563
17813
  function runTraceVerify(opts = {}) {
17564
- const cwd = resolve10(opts.cwd ?? process.cwd());
17814
+ const cwd = resolve11(opts.cwd ?? process.cwd());
17565
17815
  if (process.platform !== "linux") {
17566
17816
  process.stderr.write("tamperward trace-verify: Linux only in this release; unsupported platform \u2014 no parity is implied.\n");
17567
17817
  return 2;
@@ -17706,11 +17956,88 @@ var init_trace_verify = __esm({
17706
17956
  }
17707
17957
  });
17708
17958
 
17959
+ // src/runtimes.ts
17960
+ import { existsSync as existsSync17 } from "node:fs";
17961
+ import { join as join23 } from "node:path";
17962
+ function detectRuntimes(cwd) {
17963
+ const found = [];
17964
+ for (const rt of KNOWN_RUNTIMES) {
17965
+ const matchedMarker = rt.markers.find((m) => {
17966
+ try {
17967
+ return existsSync17(join23(cwd, m));
17968
+ } catch {
17969
+ return false;
17970
+ }
17971
+ });
17972
+ if (matchedMarker !== void 0) found.push({ ...rt, matchedMarker });
17973
+ }
17974
+ return found;
17975
+ }
17976
+ function hasInLoopRuntime(detected) {
17977
+ return detected.some((r) => r.steering === "in-loop");
17978
+ }
17979
+ function detectionHeadline(detected) {
17980
+ if (detected.length === 0) {
17981
+ return "No agent runtime detected. Wiring Claude Code in-loop steering plus the agent-neutral layers (pre-commit + CI) that protect any agent or human.";
17982
+ }
17983
+ if (detected.length === 1) {
17984
+ const r = detected[0];
17985
+ return `Detected ${r.label} \u2014 ${r.note}`;
17986
+ }
17987
+ const parts = detected.map((r) => `${r.label} (${r.steering === "in-loop" ? "in-loop" : "neutral layers"})`);
17988
+ return `Detected ${detected.length} agent runtimes: ${parts.join(", ")}.`;
17989
+ }
17990
+ function neutralOnlyCaveat(detected) {
17991
+ if (detected.length === 0 || hasInLoopRuntime(detected)) return null;
17992
+ const names = detected.map((r) => r.label).join(", ");
17993
+ const tracking = [...new Set(detected.map((r) => r.adapterTracking).filter(Boolean))].join(", ");
17994
+ return `In-loop steering (deny-before-execute) ships for Claude Code only today, so on ${names} your live protection is the agent-neutral layers: pre-commit and CI. The Claude hook wiring is still written and is inert unless Claude Code runs in this repository.${tracking ? ` A native in-loop adapter is tracked in ${tracking}.` : ""}`;
17995
+ }
17996
+ var KNOWN_RUNTIMES;
17997
+ var init_runtimes = __esm({
17998
+ "src/runtimes.ts"() {
17999
+ "use strict";
18000
+ KNOWN_RUNTIMES = [
18001
+ {
18002
+ id: "claude-code",
18003
+ label: "Claude Code",
18004
+ markers: [".claude/settings.json", ".claude"],
18005
+ steering: "in-loop",
18006
+ note: "native in-loop steering (PreToolUse deny + Stop sweep) plus the agent-neutral layers."
18007
+ },
18008
+ {
18009
+ id: "cursor",
18010
+ label: "Cursor",
18011
+ markers: [".cursor", ".cursorrules"],
18012
+ steering: "neutral",
18013
+ adapterTracking: "#482",
18014
+ note: "agent-neutral layers (pre-commit + CI) today; a native in-loop adapter is tracked in #482."
18015
+ },
18016
+ {
18017
+ id: "copilot",
18018
+ label: "GitHub Copilot",
18019
+ markers: [".github/copilot-instructions.md", ".github/copilot"],
18020
+ steering: "neutral",
18021
+ adapterTracking: "#482",
18022
+ note: "agent-neutral layers (pre-commit + CI) today; a native in-loop adapter is tracked in #482."
18023
+ },
18024
+ {
18025
+ id: "codex",
18026
+ label: "an AGENTS.md-aware agent (e.g. Codex)",
18027
+ markers: [".codex", "AGENTS.md"],
18028
+ steering: "neutral",
18029
+ adapterTracking: "#482",
18030
+ note: "agent-neutral layers (pre-commit + CI) today; a native in-loop adapter is tracked in #482."
18031
+ }
18032
+ ];
18033
+ }
18034
+ });
18035
+
17709
18036
  // src/cli/onboard.ts
17710
18037
  import { execFileSync as execFileSync12 } from "node:child_process";
17711
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync11, rmSync as rmSync10 } from "node:fs";
18038
+ import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync11, rmSync as rmSync10 } from "node:fs";
17712
18039
  import { tmpdir as tmpdir7 } from "node:os";
17713
- import { join as join22, relative as relative7, resolve as resolve11, sep as sep5 } from "node:path";
18040
+ import { join as join24, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
17714
18041
  import { createInterface } from "node:readline";
17715
18042
  import { parseDocument } from "yaml";
17716
18043
  function readlineAsker(input, output) {
@@ -17719,9 +18046,9 @@ function readlineAsker(input, output) {
17719
18046
  let pending = null;
17720
18047
  rl.on("close", () => {
17721
18048
  closed = true;
17722
- const resolve16 = pending;
18049
+ const resolve17 = pending;
17723
18050
  pending = null;
17724
- resolve16?.(null);
18051
+ resolve17?.(null);
17725
18052
  });
17726
18053
  rl.on("SIGINT", () => rl.close());
17727
18054
  const ask = (question) => new Promise((resolvePromise) => {
@@ -17747,7 +18074,7 @@ function terminalText(text) {
17747
18074
  return stripControl(text).replace(/\s+/g, " ").trim();
17748
18075
  }
17749
18076
  function committableSetupPath(cwd, path) {
17750
- const rel = relative7(cwd, resolve11(cwd, path));
18077
+ const rel = relative7(cwd, resolve12(cwd, path));
17751
18078
  if (!rel || rel === ".." || rel.startsWith(".." + sep5)) return false;
17752
18079
  return rel !== ".git" && !rel.startsWith(".git" + sep5);
17753
18080
  }
@@ -17778,13 +18105,13 @@ function explainVerdict(v, code2) {
17778
18105
  }
17779
18106
  }
17780
18107
  function writeVerifyCommand(cwd, command) {
17781
- const path = join22(cwd, POLICY_FILE);
18108
+ const path = join24(cwd, POLICY_FILE);
17782
18109
  const kind = writeTargetKind(path);
17783
18110
  if (kind === "symlink" || kind === "irregular") {
17784
18111
  return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
17785
18112
  }
17786
18113
  const mode = existingMode(path, 420);
17787
- const original = kind === "file" ? readFileSync20(path, "utf8") : null;
18114
+ const original = kind === "file" ? readFileSync21(path, "utf8") : null;
17788
18115
  let next;
17789
18116
  try {
17790
18117
  const doc = parseDocument(original && original.trim() ? original : "version: 1\n");
@@ -17807,7 +18134,7 @@ function writeVerifyCommand(cwd, command) {
17807
18134
  }
17808
18135
  }
17809
18136
  async function runOnboard(opts, io = {}) {
17810
- const requestedCwd = resolve11(opts.cwd ?? process.cwd());
18137
+ const requestedCwd = resolve12(opts.cwd ?? process.cwd());
17811
18138
  const out3 = io.out ?? ((line) => void process.stdout.write(line + "\n"));
17812
18139
  const errLine = (line) => void process.stderr.write(line + "\n");
17813
18140
  const rawErr = (line) => errLine(terminalText(line));
@@ -17847,7 +18174,7 @@ async function runOnboard(opts, io = {}) {
17847
18174
  fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
17848
18175
  return 2;
17849
18176
  }
17850
- const cwd = resolve11(rootText);
18177
+ const cwd = resolve12(rootText);
17851
18178
  let sameRoot = cwd === requestedCwd;
17852
18179
  try {
17853
18180
  sameRoot = realpathSync11(cwd) === realpathSync11(requestedCwd);
@@ -17932,11 +18259,15 @@ async function runOnboard(opts, io = {}) {
17932
18259
  }
17933
18260
  }
17934
18261
  section(2);
18262
+ const detected = detectRuntimes(cwd);
18263
+ status("RUNTIME", detectionHeadline(detected), "info");
18264
+ const caveat = neutralOnlyCaveat(detected);
18265
+ if (caveat) status("NOTE", caveat, "warn");
17935
18266
  const plan = planInit(cwd);
17936
18267
  const names = {
17937
18268
  policy: "Policy",
17938
18269
  gitignore: "Git ignore",
17939
- agent: "Claude hooks",
18270
+ agent: "In-loop (Claude)",
17940
18271
  "pre-commit": "Pre-commit",
17941
18272
  ci: "CI workflow",
17942
18273
  codeowners: "CODEOWNERS"
@@ -18173,13 +18504,13 @@ function runDemo(cwd, head, runners, out3) {
18173
18504
  }
18174
18505
  const before = treeFingerprint(cwd);
18175
18506
  out3(`working tree fingerprint before: ${before.slice(0, 16)}`);
18176
- const wt = mkdtempSync6(join22(tmpdir7(), "tw-onboard-demo-"));
18507
+ const wt = mkdtempSync6(join24(tmpdir7(), "tw-onboard-demo-"));
18177
18508
  try {
18178
18509
  if (git5(cwd, ["worktree", "add", "--detach", wt, head]) === null) {
18179
18510
  out3("Demo skipped: could not create a temporary worktree (git worktree add failed).");
18180
18511
  return;
18181
18512
  }
18182
- const targetPath = join22(wt, target.path);
18513
+ const targetPath = join24(wt, target.path);
18183
18514
  const targetStat = lstatSync10(targetPath, { throwIfNoEntry: false });
18184
18515
  if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
18185
18516
  out3("Demo skipped: the selected test is not a regular file in the disposable worktree.");
@@ -18234,6 +18565,7 @@ var init_onboard = __esm({
18234
18565
  init_policy();
18235
18566
  init_policy_load();
18236
18567
  init_narrow();
18568
+ init_runtimes();
18237
18569
  init_wiring();
18238
18570
  init_safe_write();
18239
18571
  Aborted = class extends Error {
@@ -18264,7 +18596,7 @@ var init_onboard = __esm({
18264
18596
  });
18265
18597
 
18266
18598
  // src/research/adapter.ts
18267
- import { isAbsolute as isAbsolute12, resolve as resolve12 } from "node:path";
18599
+ import { isAbsolute as isAbsolute13, resolve as resolve13 } from "node:path";
18268
18600
  function taskEnv(task) {
18269
18601
  return {
18270
18602
  TAMPERWARD_RESEARCH_TASK: task.id,
@@ -18280,7 +18612,7 @@ function normalizeCommandArgv(argv, cwd = process.cwd()) {
18280
18612
  throw new ResearchError('the command adapter needs an agent command after "--"');
18281
18613
  }
18282
18614
  const head = argv[0];
18283
- return head.includes("/") && !isAbsolute12(head) ? [resolve12(cwd, head), ...argv.slice(1)] : [...argv];
18615
+ return head.includes("/") && !isAbsolute13(head) ? [resolve13(cwd, head), ...argv.slice(1)] : [...argv];
18284
18616
  }
18285
18617
  function commandAdapter(argv, cwd = process.cwd()) {
18286
18618
  const anchored = normalizeCommandArgv(argv, cwd);
@@ -18379,9 +18711,9 @@ var init_capture = __esm({
18379
18711
  });
18380
18712
 
18381
18713
  // src/research/manifest.ts
18382
- import { createHash as createHash12 } from "node:crypto";
18383
- import { readFileSync as readFileSync21 } from "node:fs";
18384
- import { dirname as dirname9, resolve as resolve13 } from "node:path";
18714
+ import { createHash as createHash13 } from "node:crypto";
18715
+ import { readFileSync as readFileSync22 } from "node:fs";
18716
+ import { dirname as dirname10, resolve as resolve14 } from "node:path";
18385
18717
  function taskFrom(raw, index, manifestDir) {
18386
18718
  const where = `task #${index + 1}`;
18387
18719
  if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
@@ -18404,17 +18736,17 @@ function taskFrom(raw, index, manifestDir) {
18404
18736
  const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(repo) || /^[^/]+@[^:]+:/.test(repo);
18405
18737
  return {
18406
18738
  id,
18407
- repo: isUrl ? repo : resolve13(manifestDir, repo),
18739
+ repo: isUrl ? repo : resolve14(manifestDir, repo),
18408
18740
  base: base ?? "HEAD",
18409
18741
  prompt,
18410
18742
  verify: { command: verify.command, ...budget !== void 0 ? { budget } : {} }
18411
18743
  };
18412
18744
  }
18413
18745
  function readManifest(path) {
18414
- const abs = resolve13(path);
18746
+ const abs = resolve14(path);
18415
18747
  let bytes;
18416
18748
  try {
18417
- bytes = readFileSync21(abs);
18749
+ bytes = readFileSync22(abs);
18418
18750
  } catch (e) {
18419
18751
  throw new ResearchError(`cannot read task manifest ${abs}: ${e instanceof Error ? e.message : String(e)}`);
18420
18752
  }
@@ -18431,13 +18763,13 @@ function readManifest(path) {
18431
18763
  if (!Array.isArray(raw.tasks) || raw.tasks.length === 0) {
18432
18764
  throw new ResearchError(`task manifest ${abs}: no tasks (tasks must be a non-empty array)`);
18433
18765
  }
18434
- const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname9(abs)));
18766
+ const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname10(abs)));
18435
18767
  const seen = /* @__PURE__ */ new Set();
18436
18768
  for (const t of tasks) {
18437
18769
  if (seen.has(t.id)) throw new ResearchError(`task manifest ${abs}: duplicate task id "${t.id}"`);
18438
18770
  seen.add(t.id);
18439
18771
  }
18440
- return { path: abs, sha256: createHash12("sha256").update(bytes).digest("hex"), tasks };
18772
+ return { path: abs, sha256: createHash13("sha256").update(bytes).digest("hex"), tasks };
18441
18773
  }
18442
18774
  var MANIFEST_VERSION;
18443
18775
  var init_manifest = __esm({
@@ -18626,18 +18958,18 @@ var init_record = __esm({
18626
18958
 
18627
18959
  // src/research/run.ts
18628
18960
  import { execFileSync as execFileSync13 } from "node:child_process";
18629
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync22, renameSync as renameSync4, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
18630
- import { join as join23, resolve as resolve14 } from "node:path";
18961
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
18962
+ import { join as join25, resolve as resolve15 } from "node:path";
18631
18963
  function git6(args, cwd) {
18632
18964
  return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
18633
18965
  }
18634
18966
  function pairRecordPath(ledger, task, pair) {
18635
- return join23(ledger, "pairs", `${task}--${pair}.json`);
18967
+ return join25(ledger, "pairs", `${task}--${pair}.json`);
18636
18968
  }
18637
18969
  function resumableRecord(path, expected) {
18638
18970
  let raw;
18639
18971
  try {
18640
- raw = JSON.parse(readFileSync22(path, "utf8"));
18972
+ raw = JSON.parse(readFileSync23(path, "utf8"));
18641
18973
  } catch (e) {
18642
18974
  throw new ResearchError(`ledger record ${path} is malformed (${errorMessage(e).split("\n")[0]}); remove it to regenerate the pair, or use a new --out`);
18643
18975
  }
@@ -18678,9 +19010,9 @@ function writeRecordAtomically(path, text) {
18678
19010
  renameSync4(tmp, path);
18679
19011
  }
18680
19012
  function freshWorkspace(ledger, task, pair, arm, sourceBase) {
18681
- const ws3 = join23(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
19013
+ const ws3 = join25(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
18682
19014
  rmSync11(ws3, { recursive: true, force: true });
18683
- mkdirSync8(join23(ledger, "workspaces"), { recursive: true });
19015
+ mkdirSync9(join25(ledger, "workspaces"), { recursive: true });
18684
19016
  try {
18685
19017
  execFileSync13("git", ["clone", "-q", "--no-hardlinks", task.repo, ws3], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
18686
19018
  } catch (e) {
@@ -18926,16 +19258,16 @@ function runResearch(opts) {
18926
19258
  err2("tamperward research: the gated arm cannot start here (see `tamperward doctor`); run as a non-root user on Linux.");
18927
19259
  return 2;
18928
19260
  }
18929
- const ledger = resolve14(opts.out);
19261
+ const ledger = resolve15(opts.out);
18930
19262
  const pairs = opts.pairs ?? 1;
18931
- mkdirSync8(join23(ledger, "pairs"), { recursive: true });
19263
+ mkdirSync9(join25(ledger, "pairs"), { recursive: true });
18932
19264
  for (const task of tasks) {
18933
19265
  const existingRecords = /* @__PURE__ */ new Map();
18934
19266
  let sourceBase = null;
18935
19267
  try {
18936
19268
  for (let pair = 1; pair <= pairs; pair++) {
18937
19269
  const path = pairRecordPath(ledger, task.id, pair);
18938
- if (!existsSync16(path)) continue;
19270
+ if (!existsSync18(path)) continue;
18939
19271
  const existing = resumableRecord(path, {
18940
19272
  task: task.id,
18941
19273
  pair,
@@ -19060,10 +19392,10 @@ var init_run2 = __esm({
19060
19392
  });
19061
19393
 
19062
19394
  // src/research/summarize.ts
19063
- import { readdirSync as readdirSync7, readFileSync as readFileSync23 } from "node:fs";
19064
- import { join as join24, resolve as resolve15 } from "node:path";
19395
+ import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
19396
+ import { join as join26, resolve as resolve16 } from "node:path";
19065
19397
  function readLedger2(dir) {
19066
- const pairsDir = join24(resolve15(dir), "pairs");
19398
+ const pairsDir = join26(resolve16(dir), "pairs");
19067
19399
  let names;
19068
19400
  try {
19069
19401
  names = readdirSync7(pairsDir).filter((n) => n.endsWith(".json")).sort();
@@ -19072,10 +19404,10 @@ function readLedger2(dir) {
19072
19404
  }
19073
19405
  if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
19074
19406
  return names.map((n) => {
19075
- const path = join24(pairsDir, n);
19407
+ const path = join26(pairsDir, n);
19076
19408
  let raw;
19077
19409
  try {
19078
- raw = JSON.parse(readFileSync23(path, "utf8"));
19410
+ raw = JSON.parse(readFileSync24(path, "utf8"));
19079
19411
  } catch (e) {
19080
19412
  throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
19081
19413
  }
@@ -19316,6 +19648,17 @@ function parseDoctor(args) {
19316
19648
  }
19317
19649
  return o;
19318
19650
  }
19651
+ function parseStats(args) {
19652
+ const o = {};
19653
+ for (let i = 0; i < args.length; i++) {
19654
+ const a = args[i];
19655
+ if (a === "--cwd") o.cwd = args[++i];
19656
+ else if (a === "--file") o.file = args[++i];
19657
+ else if (a === "--since") o.since = args[++i];
19658
+ else if (a === "--json") o.json = true;
19659
+ }
19660
+ return o;
19661
+ }
19319
19662
  function parseCheck(args) {
19320
19663
  const o = {};
19321
19664
  for (let i = 0; i < args.length; i++) {
@@ -19455,6 +19798,16 @@ function validateCliArgs(cmd, args) {
19455
19798
  }
19456
19799
  }).error;
19457
19800
  }
19801
+ if (cmd === "stats") {
19802
+ return validateFlatArgs(args, {
19803
+ flags: ["--json"],
19804
+ values: {
19805
+ "--cwd": "string",
19806
+ "--file": "string",
19807
+ "--since": "string"
19808
+ }
19809
+ }).error;
19810
+ }
19458
19811
  if (cmd === "onboard") {
19459
19812
  const parsed = validateFlatArgs(args, {
19460
19813
  flags: ["--yes", "--skip-demo", "--demo", "--no-github"],
@@ -19615,6 +19968,12 @@ Formats:
19615
19968
  tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
19616
19969
  independent outcome, TamperWard hits/misses
19617
19970
  and paired counts \u2014 no composite score
19971
+ tamperward stats [--file F] [--since 30d] aggregate privacy-safe hook/sweep audit
19972
+ [--json] [--cwd D] events by rule and enforcement surface.
19973
+ Defaults to the repository-local
19974
+ .git/tamperward/audit.jsonl (or
19975
+ TAMPERWARD_AUDIT_LOG). A finding is an
19976
+ integrity signal, not proof of agent intent.
19618
19977
  tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
19619
19978
  [--file F] [--cwd D]
19620
19979
  tamperward onboard [--yes] [--cwd D] guided first-run setup: preflight, the
@@ -19685,6 +20044,8 @@ function main(argv) {
19685
20044
  return runInit(parseInit(rest));
19686
20045
  case "doctor":
19687
20046
  return runDoctor(parseDoctor(rest));
20047
+ case "stats":
20048
+ return runStats(parseStats(rest));
19688
20049
  case "onboard":
19689
20050
  return runOnboard(parseOnboard(rest));
19690
20051
  case "watch":
@@ -19746,13 +20107,14 @@ var init_main = __esm({
19746
20107
  init_watch();
19747
20108
  init_onboard();
19748
20109
  init_research();
20110
+ init_audit();
19749
20111
  }
19750
20112
  });
19751
20113
 
19752
20114
  // src/cli/index.ts
19753
20115
  init_hook_client();
19754
20116
  init_exit();
19755
- import { readFileSync as readFileSync24 } from "node:fs";
20117
+ import { readFileSync as readFileSync25 } from "node:fs";
19756
20118
  function loadMain() {
19757
20119
  return Promise.resolve().then(() => (init_main(), main_exports));
19758
20120
  }
@@ -19765,7 +20127,7 @@ async function launch(argv) {
19765
20127
  if (kind && hookServiceEnabled()) {
19766
20128
  let raw = null;
19767
20129
  try {
19768
- raw = readFileSync24(0, "utf8");
20130
+ raw = readFileSync25(0, "utf8");
19769
20131
  } catch {
19770
20132
  raw = null;
19771
20133
  }