tamperward 2.33.0 → 2.34.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((resolve17) => {
214
+ return new Promise((resolve20) => {
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
- resolve17(value);
225
+ resolve20(value);
226
226
  };
227
227
  const sock = createConnection(paths.socket);
228
228
  const timer = setTimeout(() => finish(handedOff ? acceptedFailure("timeout") : null), timeoutMs);
@@ -686,9 +686,9 @@ function readRegular(abs, st) {
686
686
  const buf = Buffer.allocUnsafe(now.size);
687
687
  let off2 = 0;
688
688
  while (off2 < now.size) {
689
- const n = readSync(fd, buf, off2, now.size - off2, off2);
690
- if (n === 0) break;
691
- off2 += n;
689
+ const n2 = readSync(fd, buf, off2, now.size - off2, off2);
690
+ if (n2 === 0) break;
691
+ off2 += n2;
692
692
  }
693
693
  return { kind: "file", content: off2 === now.size ? buf : buf.subarray(0, off2), mode: now.mode, size: off2, mtimeMs: now.mtimeMs, detail: "" };
694
694
  } catch (e) {
@@ -1520,8 +1520,8 @@ var init_changes = __esm({
1520
1520
  init_narrow();
1521
1521
  num = (name, fallback) => {
1522
1522
  const raw = process.env[name];
1523
- const n = raw === void 0 ? NaN : Number(raw);
1524
- return Number.isFinite(n) && n > 0 ? n : fallback;
1523
+ const n2 = raw === void 0 ? NaN : Number(raw);
1524
+ return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
1525
1525
  };
1526
1526
  RECONSTRUCT_TIMEOUT_MS = () => num("TAMPERWARD_RECONSTRUCT_TIMEOUT_MS", 5e3);
1527
1527
  RECONSTRUCT_MAXBUFFER = () => num("TAMPERWARD_RECONSTRUCT_MAXBUFFER", 32 * 1024 * 1024);
@@ -2190,8 +2190,8 @@ function literal(arg, vars) {
2190
2190
  const a = unq(arg.trim());
2191
2191
  if (/[`]|\$\(/.test(a)) return null;
2192
2192
  let unknown = false;
2193
- const r = a.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (_, n) => {
2194
- const v = vars.get(n);
2193
+ const r = a.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (_, n2) => {
2194
+ const v = vars.get(n2);
2195
2195
  if (v === void 0) unknown = true;
2196
2196
  return v ?? "";
2197
2197
  });
@@ -2389,7 +2389,7 @@ function walk(lines, st, topIdx, depth) {
2389
2389
  why = "runs from another directory after a `cd` away from the repository root";
2390
2390
  }
2391
2391
  if (state === "live" && /^tamperward\b/.test(id)) {
2392
- const onGate = assignedNames(prefix).filter((n) => SENSITIVE_ENV.test(n));
2392
+ const onGate = assignedNames(prefix).filter((n2) => SENSITIVE_ENV.test(n2));
2393
2393
  if (onGate.length) {
2394
2394
  state = "neutered";
2395
2395
  why = `runtime redirected \u2014 \`${onGate[0]}=\` in front of it decides what the gate resolves to`;
@@ -2829,7 +2829,7 @@ function setfaclDropsExec(toks) {
2829
2829
  }
2830
2830
  function nameGlob(pattern) {
2831
2831
  const re = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
2832
- return (n) => re.test(n);
2832
+ return (n2) => re.test(n2);
2833
2833
  }
2834
2834
  function destinations(cmd, args, isDir) {
2835
2835
  const positional = [];
@@ -2994,9 +2994,9 @@ function shellWritesHook(seg, toks, policy, ctx) {
2994
2994
  if (/-exec(?:dir)?\s+chmod\b/.test(seg) && !chmodDropsExec(toks)) return null;
2995
2995
  const dirs = positional.filter((p) => !/^-/.test(p) && !["{}", ";", "+", "rm", "chmod", "unlink", "truncate", "mv", "sed", "tee", "shred", "inplace"].includes(p));
2996
2996
  const names = args.filter((a, i) => /^-i?(?:name|path|regex)$/.test(args[i - 1] ?? ""));
2997
- const nameHits = names.length === 0 || names.some((n) => {
2997
+ const nameHits = names.length === 0 || names.some((n2) => {
2998
2998
  try {
2999
- const g = nameGlob(n.replace(/^.*\//, ""));
2999
+ const g = nameGlob(n2.replace(/^.*\//, ""));
3000
3000
  return HOOK_BASENAMES.some(g);
3001
3001
  } catch {
3002
3002
  return true;
@@ -3757,11 +3757,11 @@ function buildAliasMap(sf) {
3757
3757
  ts.forEachChild(node, collect2);
3758
3758
  };
3759
3759
  collect2(sf);
3760
- const kindOfBinding = (b, seen) => b.type ? kindOfType(b.type, (n) => resolveLexical(n, b.type, seen)) : null;
3760
+ const kindOfBinding = (b, seen) => b.type ? kindOfType(b.type, (n2) => resolveLexical(n2, b.type, seen)) : null;
3761
3761
  const resolveLexical = (name, at, seen) => {
3762
3762
  if (seen.has(name)) return null;
3763
- for (let n = at; n; n = n.parent) {
3764
- const here = bindings.filter((x) => x.name === name && x.scope === n);
3763
+ for (let n2 = at; n2; n2 = n2.parent) {
3764
+ const here = bindings.filter((x) => x.name === name && x.scope === n2);
3765
3765
  if (here.length === 0) continue;
3766
3766
  if (here.some((b) => b.kind !== "alias" || !b.type)) return null;
3767
3767
  return kindOfBinding(here[0], new Set(seen).add(name));
@@ -3781,7 +3781,7 @@ function buildAliasMap(sf) {
3781
3781
  };
3782
3782
  }
3783
3783
  function assertedLaunderKind(node, aliases) {
3784
- return kindOfType(assertedType(node), (n) => aliases.resolve(n, node));
3784
+ return kindOfType(assertedType(node), (n2) => aliases.resolve(n2, node));
3785
3785
  }
3786
3786
  function isDoubleCast(node, aliases) {
3787
3787
  const inner = unparenthesized(node.expression);
@@ -3825,11 +3825,11 @@ function surfaceOf(path, src) {
3825
3825
  return surface;
3826
3826
  }
3827
3827
  function unmatchedAfter(before, after) {
3828
- const n = before.length;
3828
+ const n2 = before.length;
3829
3829
  const m = after.length;
3830
3830
  const same = (i2, j2) => before[i2].lineText === after[j2].lineText && before[i2].text === after[j2].text;
3831
- const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
3832
- for (let i2 = n - 1; i2 >= 0; i2--) {
3831
+ const lcs = Array.from({ length: n2 + 1 }, () => new Array(m + 1).fill(0));
3832
+ for (let i2 = n2 - 1; i2 >= 0; i2--) {
3833
3833
  for (let j2 = m - 1; j2 >= 0; j2--) {
3834
3834
  lcs[i2][j2] = same(i2, j2) ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
3835
3835
  }
@@ -3838,10 +3838,10 @@ function unmatchedAfter(before, after) {
3838
3838
  let i = 0;
3839
3839
  let j = 0;
3840
3840
  while (j < m) {
3841
- if (i < n && same(i, j)) {
3841
+ if (i < n2 && same(i, j)) {
3842
3842
  i++;
3843
3843
  j++;
3844
- } else if (i < n && lcs[i + 1][j] >= lcs[i][j + 1]) {
3844
+ } else if (i < n2 && lcs[i + 1][j] >= lcs[i][j + 1]) {
3845
3845
  i++;
3846
3846
  } else {
3847
3847
  out3.push(after[j]);
@@ -3917,7 +3917,7 @@ function commentIsLiveDirective(text) {
3917
3917
  return false;
3918
3918
  }
3919
3919
  function countLiveDirectives(src) {
3920
- let n = 0;
3920
+ let n2 = 0;
3921
3921
  try {
3922
3922
  const scanner = ts.createScanner(
3923
3923
  ts.ScriptTarget.Latest,
@@ -3929,13 +3929,13 @@ function countLiveDirectives(src) {
3929
3929
  let tok = scanner.scan();
3930
3930
  while (tok !== ts.SyntaxKind.EndOfFileToken) {
3931
3931
  if (tok === ts.SyntaxKind.SingleLineCommentTrivia || tok === ts.SyntaxKind.MultiLineCommentTrivia) {
3932
- if (commentIsLiveDirective(scanner.getTokenText())) n++;
3932
+ if (commentIsLiveDirective(scanner.getTokenText())) n2++;
3933
3933
  }
3934
3934
  tok = scanner.scan();
3935
3935
  }
3936
3936
  } catch {
3937
3937
  }
3938
- return n;
3938
+ return n2;
3939
3939
  }
3940
3940
  function countAny(src, path) {
3941
3941
  const r = { cast: 0, never: 0, broad: 0, double: 0, suppress: 0, jsdoc: 0 };
@@ -3976,16 +3976,16 @@ function lineHasRow4Cast(line, aliasMap) {
3976
3976
  const sf = parseSource("line.ts", line);
3977
3977
  if (!sf) return false;
3978
3978
  let found = false;
3979
- const visit = (n) => {
3979
+ const visit = (n2) => {
3980
3980
  if (found) return;
3981
- if (ts.isAsExpression(n) || ts.isTypeAssertionExpression(n)) {
3982
- const k = assertedLaunderKind(n, aliasMap);
3983
- if (k === "any" || k === "never" || isDoubleCast(n, aliasMap)) {
3981
+ if (ts.isAsExpression(n2) || ts.isTypeAssertionExpression(n2)) {
3982
+ const k = assertedLaunderKind(n2, aliasMap);
3983
+ if (k === "any" || k === "never" || isDoubleCast(n2, aliasMap)) {
3984
3984
  found = true;
3985
3985
  return;
3986
3986
  }
3987
3987
  }
3988
- ts.forEachChild(n, visit);
3988
+ ts.forEachChild(n2, visit);
3989
3989
  };
3990
3990
  visit(sf);
3991
3991
  return found;
@@ -5175,8 +5175,8 @@ function numericOf(e) {
5175
5175
  }
5176
5176
  return void 0;
5177
5177
  }
5178
- function metricsOf(expr, resolve17) {
5179
- const obj = resolve17(expr);
5178
+ function metricsOf(expr, resolve20) {
5179
+ const obj = resolve20(expr);
5180
5180
  if (!ts.isObjectLiteralExpression(obj)) return void 0;
5181
5181
  const out3 = { values: {}, opaque: false };
5182
5182
  let any = false;
@@ -5190,9 +5190,9 @@ function metricsOf(expr, resolve17) {
5190
5190
  const k = keyName(p.name);
5191
5191
  if (k === null || !isMetric(k)) continue;
5192
5192
  any = true;
5193
- const n = numericOf(resolve17(p.initializer));
5194
- if (n === void 0) out3.opaque = true;
5195
- else out3.values[k] = n;
5193
+ const n2 = numericOf(resolve20(p.initializer));
5194
+ if (n2 === void 0) out3.opaque = true;
5195
+ else out3.values[k] = n2;
5196
5196
  }
5197
5197
  return any ? out3 : void 0;
5198
5198
  }
@@ -5237,17 +5237,17 @@ function parseThresholds(src) {
5237
5237
  try {
5238
5238
  const sf = sourceOf(src);
5239
5239
  if (!sf) return res;
5240
- const resolve17 = resolverFor(sf);
5240
+ const resolve20 = resolverFor(sf);
5241
5241
  const visit = (node) => {
5242
5242
  if (ts.isPropertyAssignment(node)) {
5243
5243
  const key2 = keyName(node.name);
5244
5244
  if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
5245
5245
  res.present = true;
5246
- const init = resolve17(node.initializer);
5246
+ const init = resolve20(node.initializer);
5247
5247
  if (!ts.isObjectLiteralExpression(init)) {
5248
5248
  res.opaque = true;
5249
5249
  } else {
5250
- const flat = metricsOf(init, resolve17);
5250
+ const flat = metricsOf(init, resolve20);
5251
5251
  if (flat) res.global = merge(res.global, flat);
5252
5252
  for (const p of init.properties) {
5253
5253
  if (ts.isSpreadAssignment(p)) {
@@ -5261,7 +5261,7 @@ function parseThresholds(src) {
5261
5261
  res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
5262
5262
  continue;
5263
5263
  }
5264
- const m = metricsOf(p.initializer, resolve17);
5264
+ const m = metricsOf(p.initializer, resolve20);
5265
5265
  if (!m) continue;
5266
5266
  if (k === "global") res.global = merge(res.global, m);
5267
5267
  else res.paths.set(norm(k), m);
@@ -5295,18 +5295,18 @@ function parseLists(src) {
5295
5295
  try {
5296
5296
  const sf = sourceOf(src);
5297
5297
  if (!sf) return res;
5298
- const resolve17 = resolverFor(sf);
5298
+ const resolve20 = resolverFor(sf);
5299
5299
  const visit = (node) => {
5300
5300
  if (ts.isPropertyAssignment(node)) {
5301
5301
  const k = keyName(node.name);
5302
5302
  const vitestCoverage = ownerKey(node) === "coverage";
5303
5303
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
5304
- const { items, opaque } = literals(resolve17(node.initializer));
5304
+ const { items, opaque } = literals(resolve20(node.initializer));
5305
5305
  res.collectFrom = items;
5306
5306
  res.collectKey = k === "include" ? "coverage.include" : k;
5307
5307
  if (opaque) res.opaqueCollect = true;
5308
5308
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
5309
- const { items, opaque } = literals(resolve17(node.initializer));
5309
+ const { items, opaque } = literals(resolve20(node.initializer));
5310
5310
  (k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
5311
5311
  if (opaque) res.opaqueIgnore = true;
5312
5312
  }
@@ -5491,12 +5491,12 @@ function simpleWeakeningsOf(spec2, before, after) {
5491
5491
  for (const k of spec2.keys) {
5492
5492
  const bAll = numbersOf(before, k.key, spec2.scoped);
5493
5493
  const aAll = numbersOf(after, k.key, spec2.scoped);
5494
- const scopes = [...new Set(bAll.map((n) => n.scope))];
5494
+ const scopes = [...new Set(bAll.map((n2) => n2.scope))];
5495
5495
  for (const scope of scopes) {
5496
5496
  const label = scope ? `${scope} ${k.label}` : k.label;
5497
5497
  const desc = (x, y2) => x === "auto" ? -1 : y2 === "auto" ? 1 : lessThan(x, y2) ? 1 : lessThan(y2, x) ? -1 : 0;
5498
- const b = bAll.filter((n) => n.scope === scope).map((n) => n.v).sort(desc);
5499
- const a = aAll.filter((n) => n.scope === scope).map((n) => n.v).sort(desc);
5498
+ const b = bAll.filter((n2) => n2.scope === scope).map((n2) => n2.v).sort(desc);
5499
+ const a = aAll.filter((n2) => n2.scope === scope).map((n2) => n2.v).sort(desc);
5500
5500
  if (b.length === 0) continue;
5501
5501
  if (a.length === 0) {
5502
5502
  if (spec2.removable) out3.push(`${label} removed (was ${b.join(", ")})`);
@@ -5598,10 +5598,10 @@ function gateMovedElsewhere(c, changes, policy) {
5598
5598
  const spec2 = SIMPLE_FILES.find((s) => s.file.test(c.path));
5599
5599
  if (!spec2) return false;
5600
5600
  return spec2.keys.every((k) => {
5601
- const b = numbersOf(before, k.key, spec2.scoped).map((n) => n.v);
5601
+ const b = numbersOf(before, k.key, spec2.scoped).map((n2) => n2.v);
5602
5602
  if (b.length === 0) return true;
5603
5603
  return others.some((o) => {
5604
- const a = numbersOf(o.after, k.key, spec2.scoped).map((n) => n.v);
5604
+ const a = numbersOf(o.after, k.key, spec2.scoped).map((n2) => n2.v);
5605
5605
  return a.length > 0 && b.every((bv) => a.some((av) => k.weakens === "lower" ? !lessThan(av, bv) : !lessThan(bv, av)));
5606
5606
  });
5607
5607
  });
@@ -5959,7 +5959,7 @@ function effectivePytestConfig(path, ctx) {
5959
5959
  const files = ctx?.trackedFiles;
5960
5960
  if (!files) return true;
5961
5961
  const rootNames = new Set(files.filter((f) => !f.includes("/")));
5962
- return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
5962
+ return !PYTEST_INI_ORDER.slice(0, rank).some((n2) => rootNames.has(n2));
5963
5963
  }
5964
5964
  function pytestBlock(src) {
5965
5965
  const lines = src.split(/\r?\n/);
@@ -6633,7 +6633,7 @@ function eslintAstFacts(text, f) {
6633
6633
  }
6634
6634
  if (!sf) return;
6635
6635
  const isStr2 = (e) => ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e);
6636
- const keyText = (n) => ts.isIdentifier(n) || isStr2(n) || ts.isNumericLiteral(n) ? n.text : null;
6636
+ const keyText = (n2) => ts.isIdentifier(n2) || isStr2(n2) || ts.isNumericLiteral(n2) ? n2.text : null;
6637
6637
  const literal2 = (e) => {
6638
6638
  if (isStr2(e)) return e.text;
6639
6639
  if (ts.isNumericLiteral(e)) return Number(e.text);
@@ -7268,8 +7268,8 @@ function scriptKind3(inner) {
7268
7268
  function runnerOfSegment(canon) {
7269
7269
  const m = canon.match(RUNNER_NAME);
7270
7270
  if (!m) return null;
7271
- const n = m[1];
7272
- return n === "jest" || n === "vitest" || n === "mocha" || n === "pytest" || n === "go" || n === "cargo" ? n : "node";
7271
+ const n2 = m[1];
7272
+ return n2 === "jest" || n2 === "vitest" || n2 === "mocha" || n2 === "pytest" || n2 === "go" || n2 === "cargo" ? n2 : "node";
7273
7273
  }
7274
7274
  function statements(raw) {
7275
7275
  const out3 = [];
@@ -7803,7 +7803,7 @@ var init_ci_tampering = __esm({
7803
7803
  }
7804
7804
  if (!isProtected(c.path, policy, "ci")) continue;
7805
7805
  const afterLines = c.after != null ? c.after.split("\n") : null;
7806
- const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
7806
+ const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n2) => n2 != null));
7807
7807
  const github = isGithubFile(c.path);
7808
7808
  for (const l of github ? addedLines(c) : []) {
7809
7809
  const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
@@ -7844,13 +7844,13 @@ var init_ci_tampering = __esm({
7844
7844
  for (let k = 0; k < b.body.length; k++) {
7845
7845
  const idx = b.start + 1 + k;
7846
7846
  if (!addedAt.has(idx + 1)) continue;
7847
- const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
7848
- if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
7847
+ const n2 = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
7848
+ if (!n2 || propagates && /set\s\+e/.test(b.body[k])) continue;
7849
7849
  out3.push(
7850
7850
  makeFinding(RULE8, policy, {
7851
7851
  file: c.path,
7852
7852
  line: idx + 1,
7853
- message: `A check's run block was neutralised: ${n.what}.`,
7853
+ message: `A check's run block was neutralised: ${n2.what}.`,
7854
7854
  evidence: b.body[k].trim(),
7855
7855
  remediation: "Let the check decide the step. Masking its exit status is the same tamper as removing it."
7856
7856
  })
@@ -8599,13 +8599,13 @@ function renameKept(c) {
8599
8599
  }
8600
8600
  function lefthookBase(c, changes, ctx) {
8601
8601
  const dir = c.path.includes("/") ? c.path.slice(0, c.path.lastIndexOf("/") + 1) : "";
8602
- const names = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"].map((n) => dir + n);
8602
+ const names = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"].map((n2) => dir + n2);
8603
8603
  const inSet = changes.find((o) => o.kind === "file" && names.includes(o.path));
8604
8604
  if (inSet) return inSet.after != null ? parseDoc(inSet.after) : null;
8605
8605
  if (!ctx?.cwd) return null;
8606
- for (const n of names) {
8606
+ for (const n2 of names) {
8607
8607
  try {
8608
- const p = join7(ctx.cwd, n);
8608
+ const p = join7(ctx.cwd, n2);
8609
8609
  if (existsSync4(p)) return parseDoc(readFileSync4(p, "utf8"));
8610
8610
  } catch {
8611
8611
  }
@@ -9096,8 +9096,8 @@ function isSpecShaped(path, before, after) {
9096
9096
  if (before === null && after === null) return true;
9097
9097
  for (const src of [before, after]) {
9098
9098
  if (src === null) continue;
9099
- const n = countTests(src, path);
9100
- if (n.min > 0 || n.open) return true;
9099
+ const n2 = countTests(src, path);
9100
+ if (n2.min > 0 || n2.open) return true;
9101
9101
  }
9102
9102
  return false;
9103
9103
  }
@@ -9253,7 +9253,7 @@ function unreachableNodes(sf) {
9253
9253
  collect2(sf);
9254
9254
  if (candidates.length === 0) return dead;
9255
9255
  const inDead = (node) => {
9256
- for (let n = node; n; n = n.parent) if (dead.has(n)) return true;
9256
+ for (let n2 = node; n2; n2 = n2.parent) if (dead.has(n2)) return true;
9257
9257
  return false;
9258
9258
  };
9259
9259
  const within2 = (node, owner) => node.pos >= owner.pos && node.end <= owner.end;
@@ -9302,7 +9302,7 @@ function partitionJs(src) {
9302
9302
  if (!sf) return null;
9303
9303
  const dead = unreachableNodes(sf);
9304
9304
  const deadRanges = [];
9305
- for (const n of dead) deadRanges.push([n.getStart(sf), n.getEnd()]);
9305
+ for (const n2 of dead) deadRanges.push([n2.getStart(sf), n2.getEnd()]);
9306
9306
  const literals3 = literalInteriors(sf);
9307
9307
  const mask = new Uint8Array(src.length);
9308
9308
  for (const [s, e] of deadRanges) for (let i = s; i < e; i++) mask[i] = 1;
@@ -9361,7 +9361,7 @@ function partition(src, path) {
9361
9361
  return { live: src, hidden: "", inLiteral: new Uint8Array(src.length) };
9362
9362
  }
9363
9363
  function pytestCollectedDefs(src) {
9364
- let n = 0;
9364
+ let n2 = 0;
9365
9365
  const stack = [];
9366
9366
  for (const line of src.split("\n")) {
9367
9367
  if (!/\S/.test(line) || /^\s*#/.test(line)) continue;
@@ -9372,9 +9372,9 @@ function pytestCollectedDefs(src) {
9372
9372
  stack.push({ indent, collected: PY_COLLECTED_CLASS(cls[1], cls[2] ?? "") });
9373
9373
  continue;
9374
9374
  }
9375
- if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected)) n++;
9375
+ if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected)) n2++;
9376
9376
  }
9377
- return n;
9377
+ return n2;
9378
9378
  }
9379
9379
  var MAX_ROUNDS, blankRange, PY_INDENT, PY_TERMINAL, PY_CONST_FALSE, PY_CLASS, PY_TEST_DEF, PY_COLLECTED_CLASS;
9380
9380
  var init_reachability = __esm({
@@ -9407,13 +9407,13 @@ function calleeName(expr) {
9407
9407
  }
9408
9408
  function loopRows(iterable) {
9409
9409
  if (ts.isArrayLiteralExpression(iterable)) {
9410
- let n = 0;
9410
+ let n2 = 0;
9411
9411
  let open = false;
9412
9412
  for (const el of iterable.elements) {
9413
9413
  if (ts.isSpreadElement(el)) open = true;
9414
- else n++;
9414
+ else n2++;
9415
9415
  }
9416
- return { n: Math.max(n, open ? 0 : 1), open };
9416
+ return { n: Math.max(n2, open ? 0 : 1), open };
9417
9417
  }
9418
9418
  return { n: 1, open: true };
9419
9419
  }
@@ -9432,13 +9432,13 @@ function eachRows(call) {
9432
9432
  const arg = call.arguments[0];
9433
9433
  if (!arg) return { n: 1, open: true };
9434
9434
  if (ts.isArrayLiteralExpression(arg)) {
9435
- let n = 0;
9435
+ let n2 = 0;
9436
9436
  let open = false;
9437
9437
  for (const el of arg.elements) {
9438
9438
  if (ts.isSpreadElement(el)) open = true;
9439
- else n++;
9439
+ else n2++;
9440
9440
  }
9441
- return { n: Math.max(n, open ? 0 : 1), open };
9441
+ return { n: Math.max(n2, open ? 0 : 1), open };
9442
9442
  }
9443
9443
  return { n: 1, open: true };
9444
9444
  }
@@ -9447,13 +9447,13 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
9447
9447
  if (lang === "py") return { min: pytestCollectedDefs(src), open: false };
9448
9448
  if (lang && lang !== "js") {
9449
9449
  const re = TEST_DEFS[lang];
9450
- let n2 = 0;
9451
- for (const line of src.split("\n")) if (re.test(line)) n2++;
9452
- return { min: n2, open: false };
9450
+ let n3 = 0;
9451
+ for (const line of src.split("\n")) if (re.test(line)) n3++;
9452
+ return { min: n3, open: false };
9453
9453
  }
9454
9454
  const sf = parseSource("spec.ts", src);
9455
9455
  if (!sf) return { min: 0, open: true };
9456
- let n = 0;
9456
+ let n2 = 0;
9457
9457
  let open = false;
9458
9458
  const dead = unreachableNodes(sf);
9459
9459
  const visit = (node, mult) => {
@@ -9488,19 +9488,19 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
9488
9488
  }
9489
9489
  if (isEachOf(node.expression)) {
9490
9490
  const r = eachRows(node);
9491
- n += r.n * mult;
9491
+ n2 += r.n * mult;
9492
9492
  if (r.open) open = true;
9493
9493
  } else {
9494
9494
  const name = calleeName(node.expression);
9495
- if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
9495
+ if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n2 += mult;
9496
9496
  }
9497
9497
  } else if (ts.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
9498
- n += templateRows(node) * mult;
9498
+ n2 += templateRows(node) * mult;
9499
9499
  }
9500
9500
  ts.forEachChild(node, (c) => visit(c, mult));
9501
9501
  };
9502
9502
  visit(sf, 1);
9503
- return { min: n, open };
9503
+ return { min: n2, open };
9504
9504
  }
9505
9505
  function countTestBlocks(src, path = "spec.ts") {
9506
9506
  return countTests(src, path).min;
@@ -9546,7 +9546,7 @@ function scriptWeakenings(before, after, path, policy, addedPaths) {
9546
9546
  const f = dir + file.replace(/^\.\//, "");
9547
9547
  return isProtected(f, policy, "config") && !addedPaths.has(f);
9548
9548
  };
9549
- const relocated = (kind) => Object.keys(now).some((n) => !(n in was) && checkKinds(now[n]).has(kind));
9549
+ const relocated = (kind) => Object.keys(now).some((n2) => !(n2 in was) && checkKinds(now[n2]).has(kind));
9550
9550
  const out3 = [];
9551
9551
  for (const name of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])) {
9552
9552
  const whole = WHOLE_SUITE_SCRIPT.test(name);
@@ -9618,10 +9618,10 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
9618
9618
  if (!was || !now || was.path === now.path) return [];
9619
9619
  const samples = runnerSamples(now.path, policy, ctx, "pytest");
9620
9620
  return suiteNarrowings(was.content, now.content, now.path, samples).map(
9621
- (n) => makeFinding(RULE10, policy, {
9621
+ (n2) => makeFinding(RULE10, policy, {
9622
9622
  file: now.path,
9623
- message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
9624
- evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
9623
+ message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n2.path} is no longer selected: ${n2.reason}.`,
9624
+ evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n2.path} from the suite`,
9625
9625
  remediation: "pytest reads ONE config file. Adding a higher-precedence one, or deleting the one in force, changes which settings apply \u2014 keep the effective configuration selecting the whole suite."
9626
9626
  })
9627
9627
  );
@@ -9755,28 +9755,28 @@ var init_test_deletion = __esm({
9755
9755
  })
9756
9756
  );
9757
9757
  }
9758
- for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
9758
+ for (const n2 of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
9759
9759
  out3.push(
9760
9760
  makeFinding(RULE10, policy, {
9761
9761
  file: c.path,
9762
- message: `The runner no longer selects ${n.path}: ${n.reason}.`,
9763
- evidence: `${n.path} dropped from the suite by ${c.path}`,
9762
+ message: `The runner no longer selects ${n2.path}: ${n2.reason}.`,
9763
+ evidence: `${n2.path} dropped from the suite by ${c.path}`,
9764
9764
  remediation: "Keep the runner pointed at the whole suite. Excluding a spec from the runner removes its tests without touching the file."
9765
9765
  })
9766
9766
  );
9767
9767
  }
9768
- for (const n of scriptWeakenings(c.before, c.after, c.path, policy, addedPaths)) {
9769
- const w = n.weakening;
9770
- const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
9768
+ for (const n2 of scriptWeakenings(c.before, c.after, c.path, policy, addedPaths)) {
9769
+ const w = n2.weakening;
9770
+ const evidence = `"${n2.script}": ${JSON.stringify(n2.before)} \u2192 ${n2.after === "" ? "deleted" : JSON.stringify(n2.after)}`;
9771
9771
  out3.push(
9772
9772
  w.state === "removed" ? makeFinding(RULE10, policy, {
9773
9773
  file: c.path,
9774
- message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
9774
+ message: `The test script no longer runs the ${w.kind} check: scripts.${n2.script} ${w.what}.`,
9775
9775
  evidence,
9776
9776
  remediation: "Keep the script running its check. Replacing the runner in package.json removes every test from every `npm test` while the workflow line stays untouched."
9777
9777
  }) : makeFinding(RULE10, policy, {
9778
9778
  file: c.path,
9779
- message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
9779
+ message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n2.script}.`,
9780
9780
  evidence,
9781
9781
  remediation: "Keep the test script running the whole suite and letting it decide; a masked status, a cut-short run or a runner flag that selects fewer specs removes the rest from every run."
9782
9782
  })
@@ -10371,13 +10371,13 @@ function pureRemoval(before, after) {
10371
10371
  const remaining = /* @__PURE__ */ new Map();
10372
10372
  for (const a of before) remaining.set(a.canonical, (remaining.get(a.canonical) ?? 0) + 1);
10373
10373
  for (const a of after) {
10374
- const n = remaining.get(a.canonical) ?? 0;
10375
- if (n <= 0) return null;
10376
- remaining.set(a.canonical, n - 1);
10374
+ const n2 = remaining.get(a.canonical) ?? 0;
10375
+ if (n2 <= 0) return null;
10376
+ remaining.set(a.canonical, n2 - 1);
10377
10377
  }
10378
10378
  for (const a of before) {
10379
- const n = remaining.get(a.canonical) ?? 0;
10380
- if (n > 0) return a;
10379
+ const n2 = remaining.get(a.canonical) ?? 0;
10380
+ if (n2 > 0) return a;
10381
10381
  }
10382
10382
  return null;
10383
10383
  }
@@ -11180,8 +11180,14 @@ function cell(s) {
11180
11180
  function mdCode(s) {
11181
11181
  return s.replace(/`/g, "'").replace(/[\r\n]/g, " ");
11182
11182
  }
11183
+ function encodePathSegment(segment) {
11184
+ return encodeURIComponent(segment).replace(
11185
+ /[!'()*]/g,
11186
+ (character) => "%" + character.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
11187
+ );
11188
+ }
11183
11189
  function mdUrl(s) {
11184
- return s.replace(/[()<> \r\n"'`\\]/g, (c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase());
11190
+ return s.split("/").map(encodePathSegment).join("/");
11185
11191
  }
11186
11192
  function locationCell(f, env) {
11187
11193
  if (!f.file) return "`(command)`";
@@ -12302,10 +12308,10 @@ function parseAuditJsonl(raw) {
12302
12308
  return events;
12303
12309
  }
12304
12310
  function parseSince(value, nowMs = Date.now()) {
12305
- const relative8 = /^(\d+)(m|h|d)$/.exec(value);
12306
- if (relative8) {
12307
- const count = Number(relative8[1]);
12308
- const unit = relative8[2] === "m" ? 6e4 : relative8[2] === "h" ? 36e5 : 864e5;
12311
+ const relative9 = /^(\d+)(m|h|d)$/.exec(value);
12312
+ if (relative9) {
12313
+ const count = Number(relative9[1]);
12314
+ const unit = relative9[2] === "m" ? 6e4 : relative9[2] === "h" ? 36e5 : 864e5;
12309
12315
  if (!Number.isSafeInteger(count) || count <= 0) throw new Error(`invalid --since value "${value}"`);
12310
12316
  return nowMs - count * unit;
12311
12317
  }
@@ -12714,8 +12720,8 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
12714
12720
  if (!existsSync10(log)) return none;
12715
12721
  let offset = 0;
12716
12722
  if (cp && existsSync10(cp)) {
12717
- const n = Number(readFileSync11(cp, "utf8"));
12718
- offset = Number.isFinite(n) ? n : 0;
12723
+ const n2 = Number(readFileSync11(cp, "utf8"));
12724
+ offset = Number.isFinite(n2) ? n2 : 0;
12719
12725
  }
12720
12726
  const drained = drainEvents(log, offset);
12721
12727
  if (!drained.complete) {
@@ -13013,12 +13019,12 @@ function pidAlive2(pid) {
13013
13019
  }
13014
13020
  }
13015
13021
  function listening(socket) {
13016
- return new Promise((resolve17) => {
13022
+ return new Promise((resolve20) => {
13017
13023
  const sock = createConnection2(socket);
13018
13024
  const done = (v) => {
13019
13025
  clearTimeout(timer);
13020
13026
  sock.destroy();
13021
- resolve17(v);
13027
+ resolve20(v);
13022
13028
  };
13023
13029
  const timer = setTimeout(() => done(false), 1e3);
13024
13030
  sock.once("connect", () => done(true));
@@ -13197,11 +13203,11 @@ async function startHookService(opts) {
13197
13203
  });
13198
13204
  const umask = process.umask(63);
13199
13205
  try {
13200
- await new Promise((resolve17, reject) => {
13206
+ await new Promise((resolve20, reject) => {
13201
13207
  server.once("error", reject);
13202
13208
  server.listen(paths.socket, () => {
13203
13209
  server.off("error", reject);
13204
- resolve17();
13210
+ resolve20();
13205
13211
  });
13206
13212
  });
13207
13213
  } finally {
@@ -13218,8 +13224,8 @@ async function startHookService(opts) {
13218
13224
  get served() {
13219
13225
  return served;
13220
13226
  },
13221
- close: () => new Promise((resolve17) => {
13222
- if (closed) return resolve17();
13227
+ close: () => new Promise((resolve20) => {
13228
+ if (closed) return resolve20();
13223
13229
  closed = true;
13224
13230
  setSnapshotCache(null);
13225
13231
  let settled = false;
@@ -13230,7 +13236,7 @@ async function startHookService(opts) {
13230
13236
  clearTimeout(drain);
13231
13237
  removeQuietly(paths.socket);
13232
13238
  removeQuietly(paths.state);
13233
- resolve17();
13239
+ resolve20();
13234
13240
  };
13235
13241
  server.close(finalize);
13236
13242
  for (const sock of sockets) if (sock !== acceptedSocket) sock.destroy();
@@ -13257,11 +13263,16 @@ async function probeSocket(paths) {
13257
13263
  function describeStateFile(state) {
13258
13264
  return state ? `records pid ${state.pid}, root ${state.root}, tamperward@${state.version}` : "is absent or unreadable";
13259
13265
  }
13260
- async function stopHookService(paths) {
13266
+ async function stopHookService(paths, expectedRoot) {
13261
13267
  const probe = await probeSocket(paths);
13262
13268
  const state = readServiceState(paths);
13263
13269
  if (probe.answered) {
13264
13270
  const st = probe.status;
13271
+ const requestedRoot = expectedRoot === void 0 ? void 0 : realpathSync4(repoRoot(expectedRoot));
13272
+ if (requestedRoot !== void 0 && (st === null || st.root !== requestedRoot)) {
13273
+ const detail2 = st ? `a listener answered on ${paths.socket} for root ${st.root}, not the requested repository ${requestedRoot}` : `a listener answered on ${paths.socket} but did not report a root for the requested repository ${requestedRoot}`;
13274
+ return { outcome: "mismatch", pid: st?.pid ?? state?.pid, root: st?.root ?? state?.root, detail: detail2 };
13275
+ }
13265
13276
  const agrees = st !== null && state !== null && state.pid === st.pid && state.root === st.root && state.version === st.version;
13266
13277
  if (agrees && st.pid !== process.pid) {
13267
13278
  try {
@@ -13283,13 +13294,13 @@ async function stopHookService(paths) {
13283
13294
  removeQuietly(paths.state);
13284
13295
  return { outcome: "not-running" };
13285
13296
  }
13286
- function parseStart(args) {
13287
- let dir = process.cwd();
13288
- for (let i = 0; i < args.length; i++) if (args[i] === "--dir" && args[i + 1]) dir = args[++i];
13289
- return { dir };
13297
+ function parseDir(args) {
13298
+ for (let i = 0; i < args.length; i++) if (args[i] === "--dir" && args[i + 1]) return args[++i];
13299
+ return void 0;
13290
13300
  }
13291
13301
  function runHookService(args) {
13292
13302
  const sub = args[0];
13303
+ const dir = parseDir(args.slice(1));
13293
13304
  const paths = servicePaths();
13294
13305
  if (!paths) {
13295
13306
  process.stderr.write(
@@ -13297,9 +13308,18 @@ function runHookService(args) {
13297
13308
  );
13298
13309
  return 2;
13299
13310
  }
13311
+ let requestedRoot;
13312
+ if (dir !== void 0) {
13313
+ try {
13314
+ requestedRoot = realpathSync4(repoRoot(realpathSync4(dir)));
13315
+ } catch (e) {
13316
+ process.stderr.write(`tamperward hook-service: cannot resolve --dir ${dir} (${e instanceof Error ? e.message : String(e)})
13317
+ `);
13318
+ return 2;
13319
+ }
13320
+ }
13300
13321
  if (sub === "start") {
13301
- const { dir } = parseStart(args.slice(1));
13302
- startHookService({ root: dir, paths }).then((svc) => {
13322
+ startHookService({ root: requestedRoot ?? process.cwd(), paths }).then((svc) => {
13303
13323
  let closing = false;
13304
13324
  const shutdown = () => {
13305
13325
  if (closing) return;
@@ -13320,7 +13340,7 @@ function runHookService(args) {
13320
13340
  return -1;
13321
13341
  }
13322
13342
  if (sub === "stop") {
13323
- return stopHookService(paths).then((res) => {
13343
+ return stopHookService(paths, requestedRoot).then((res) => {
13324
13344
  if (res.outcome === "stopped") {
13325
13345
  process.stdout.write(`tamperward hook-service: stopped (pid ${res.pid}); ${paths.socket} removed
13326
13346
  `);
@@ -13363,6 +13383,14 @@ function runHookService(args) {
13363
13383
  exitAfterFlush(0);
13364
13384
  return;
13365
13385
  }
13386
+ if (requestedRoot !== void 0 && res.root !== requestedRoot) {
13387
+ process.stderr.write(
13388
+ `tamperward hook-service: a listener is bound to ${res.root}, not the requested repository ${requestedRoot}; it was not treated as the requested service; ${optIn}
13389
+ `
13390
+ );
13391
+ exitAfterFlush(1);
13392
+ return;
13393
+ }
13366
13394
  const cache3 = isRecord(res.cache) ? res.cache : {};
13367
13395
  const startedAt = typeof res.started_at === "string" ? res.started_at : state?.started_at ?? "unknown";
13368
13396
  const disagrees = !state || state.pid !== res.pid || state.root !== res.root || state.version !== res.version;
@@ -13394,6 +13422,7 @@ var init_hook_service = __esm({
13394
13422
  });
13395
13423
 
13396
13424
  // src/cli/allow.ts
13425
+ import { relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
13397
13426
  function runAllow(opts) {
13398
13427
  if (!opts.rule) {
13399
13428
  process.stderr.write('tamperward allow <rule> [--file <path>] --reason "<why>"\n');
@@ -13412,8 +13441,9 @@ function runAllow(opts) {
13412
13441
  process.stderr.write("tamperward: cannot read the working tree (not a git repo?).\n");
13413
13442
  return 2;
13414
13443
  }
13444
+ const requestedFile = opts.file === void 0 ? void 0 : relative3(cwd, resolve5(cwd, opts.file)).split(sep2).join("/");
13415
13445
  const targets = findings.filter(
13416
- (f) => f.severity === "block" && f.rule === opts.rule && (!opts.file || f.file === opts.file)
13446
+ (f) => f.severity === "block" && f.rule === opts.rule && (!requestedFile || f.file === requestedFile)
13417
13447
  );
13418
13448
  if (targets.length === 0) {
13419
13449
  process.stderr.write(
@@ -13532,7 +13562,7 @@ var init_safe_write = __esm({
13532
13562
  import { execFileSync as execFileSync6 } from "node:child_process";
13533
13563
  import { createHash as createHash8 } from "node:crypto";
13534
13564
  import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "node:fs";
13535
- import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as relative3, resolve as resolve5 } from "node:path";
13565
+ import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute9, join as join15, relative as relative4, resolve as resolve6 } from "node:path";
13536
13566
  function gitConfig(cwd, key2) {
13537
13567
  try {
13538
13568
  return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
@@ -13872,13 +13902,13 @@ function planClaudeHooks(cwd) {
13872
13902
  function hooksDir(cwd) {
13873
13903
  try {
13874
13904
  const out3 = execFileSync6("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
13875
- if (out3) return resolve5(cwd, out3);
13905
+ if (out3) return resolve6(cwd, out3);
13876
13906
  } catch {
13877
13907
  }
13878
13908
  return existsSync12(join15(cwd, ".git")) ? join15(cwd, ".git", "hooks") : null;
13879
13909
  }
13880
13910
  function display(cwd, path) {
13881
- const rel = relative3(cwd, path);
13911
+ const rel = relative4(cwd, path);
13882
13912
  return rel && !rel.startsWith("..") && !isAbsolute9(rel) ? rel : path;
13883
13913
  }
13884
13914
  function unconditionalExitAt(lines) {
@@ -14127,7 +14157,7 @@ function planInit(cwd, opts = {}) {
14127
14157
  ];
14128
14158
  }
14129
14159
  function runInit(opts) {
14130
- const requested = resolve5(opts.cwd ?? process.cwd());
14160
+ const requested = resolve6(opts.cwd ?? process.cwd());
14131
14161
  const cwd = repoRoot(requested);
14132
14162
  const w = opts.quiet ? { write: (_text) => true } : process.stdout;
14133
14163
  if (cwd !== requested && !sameDir(cwd, requested)) {
@@ -14377,16 +14407,16 @@ import {
14377
14407
  dirname as dirname6,
14378
14408
  isAbsolute as isAbsolute10,
14379
14409
  join as join16,
14380
- relative as relative4,
14381
- resolve as resolve6,
14382
- sep as sep2
14410
+ relative as relative5,
14411
+ resolve as resolve7,
14412
+ sep as sep3
14383
14413
  } from "node:path";
14384
14414
  function metricsFor(descriptor) {
14385
14415
  return descriptor.metrics ??= { fullSnapshots: 0, reusedSnapshots: 0, totalMs: 0 };
14386
14416
  }
14387
14417
  function inside(root, candidate) {
14388
- const rel = relative4(resolve6(root), resolve6(candidate));
14389
- return rel === "" || rel !== ".." && !rel.startsWith(".." + sep2) && !isAbsolute10(rel);
14418
+ const rel = relative5(resolve7(root), resolve7(candidate));
14419
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep3) && !isAbsolute10(rel);
14390
14420
  }
14391
14421
  function modeKind(path) {
14392
14422
  const st = lstatSync8(path);
@@ -14538,7 +14568,7 @@ function freezeRoot(kind, path) {
14538
14568
  if (!statSync3(path).isDirectory()) {
14539
14569
  throw new Error("not a directory");
14540
14570
  }
14541
- return { kind, path: resolve6(path), realPath: realpathSync6(path) };
14571
+ return { kind, path: resolve7(path), realPath: realpathSync6(path) };
14542
14572
  } catch (e) {
14543
14573
  throw new Unattestable(
14544
14574
  kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
@@ -14574,7 +14604,7 @@ function firstExecutable(command) {
14574
14604
  function resolveExecutable(token, cwd, env) {
14575
14605
  if (!token) return null;
14576
14606
  if (token.includes("/") || token.includes("\\")) {
14577
- const path = isAbsolute10(token) ? token : resolve6(cwd, token);
14607
+ const path = isAbsolute10(token) ? token : resolve7(cwd, token);
14578
14608
  try {
14579
14609
  return existsSync13(path) ? realpathSync6(path) : null;
14580
14610
  } catch {
@@ -14582,7 +14612,7 @@ function resolveExecutable(token, cwd, env) {
14582
14612
  }
14583
14613
  }
14584
14614
  for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
14585
- const candidate = resolve6(entry, token);
14615
+ const candidate = resolve7(entry, token);
14586
14616
  try {
14587
14617
  if (existsSync13(candidate) && statSync3(candidate).isFile()) return realpathSync6(candidate);
14588
14618
  } catch {
@@ -14601,7 +14631,7 @@ function looksPython(command, executable, token) {
14601
14631
  }
14602
14632
  function inferredVenvFromToken(token, cwd) {
14603
14633
  if (!token || !token.includes("/") && !token.includes("\\")) return null;
14604
- const path = isAbsolute10(token) ? token : resolve6(cwd, token);
14634
+ const path = isAbsolute10(token) ? token : resolve7(cwd, token);
14605
14635
  if (!pythonName(basename4(path))) return null;
14606
14636
  const parent = dirname6(path);
14607
14637
  const parentName = basename4(parent).toLowerCase();
@@ -14639,7 +14669,7 @@ function venvProblem(root) {
14639
14669
  return null;
14640
14670
  }
14641
14671
  function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
14642
- const cwd = resolve6(cwdInput);
14672
+ const cwd = resolve7(cwdInput);
14643
14673
  const roots = [];
14644
14674
  const probes = [];
14645
14675
  const seen = /* @__PURE__ */ new Set();
@@ -14663,7 +14693,7 @@ function discoverDependencyEnvironment(cwdInput, command, env = process.env) {
14663
14693
  if (!isAbsolute10(env.VIRTUAL_ENV)) {
14664
14694
  throw new Unattestable("VIRTUAL_ENV is relative (" + JSON.stringify(env.VIRTUAL_ENV) + ")");
14665
14695
  }
14666
- venv = resolve6(env.VIRTUAL_ENV);
14696
+ venv = resolve7(env.VIRTUAL_ENV);
14667
14697
  } else {
14668
14698
  venv = inferredVenv(executable) ?? inferredVenvFromToken(token, cwd);
14669
14699
  if (!venv && /(?:^|\s)uv\s+run(?:\s|$)/.test(command) && existsSync13(join16(cwd, ".venv", "pyvenv.cfg"))) {
@@ -14703,7 +14733,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
14703
14733
  const fingerprint2 = measuredSnapshot(
14704
14734
  descriptor.roots,
14705
14735
  descriptor.probes,
14706
- resolve6(cwdInput),
14736
+ resolve7(cwdInput),
14707
14737
  metricsFor(descriptor)
14708
14738
  );
14709
14739
  if (fingerprint2 !== descriptor.fingerprint) {
@@ -14716,7 +14746,7 @@ function checkDependencyEnvironment(cwdInput, descriptor) {
14716
14746
  }
14717
14747
  function attestDependencyEnvironment(cwdInput, descriptor) {
14718
14748
  const attestation = {
14719
- cwd: resolve6(cwdInput),
14749
+ cwd: resolve7(cwdInput),
14720
14750
  descriptor,
14721
14751
  check: checkDependencyEnvironment(cwdInput, descriptor)
14722
14752
  };
@@ -14724,7 +14754,7 @@ function attestDependencyEnvironment(cwdInput, descriptor) {
14724
14754
  return attestation;
14725
14755
  }
14726
14756
  function reuseDependencyEnvironmentAttestation(cwdInput, descriptor, attestation) {
14727
- if (!attestation || !issuedAttestations.has(attestation) || attestation.descriptor !== descriptor || attestation.cwd !== resolve6(cwdInput)) return null;
14757
+ if (!attestation || !issuedAttestations.has(attestation) || attestation.descriptor !== descriptor || attestation.cwd !== resolve7(cwdInput)) return null;
14728
14758
  metricsFor(descriptor).reusedSnapshots += 1;
14729
14759
  return attestation.check;
14730
14760
  }
@@ -15146,7 +15176,7 @@ if (child) {
15146
15176
  import { createHash as createHash11, randomUUID } from "node:crypto";
15147
15177
  import { spawnSync as spawnSync2 } from "node:child_process";
15148
15178
  import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
15149
- import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as resolve7 } from "node:path";
15179
+ import { delimiter as delimiter2, dirname as dirname7, join as join18, resolve as resolve8 } from "node:path";
15150
15180
  function identity() {
15151
15181
  const uid = typeof process.getuid === "function" ? process.getuid() : null;
15152
15182
  const groups = typeof process.getgroups === "function" ? process.getgroups() : [];
@@ -15166,7 +15196,7 @@ function writableByCaller(path) {
15166
15196
  }
15167
15197
  }
15168
15198
  function replaceableByCaller(path) {
15169
- let current = resolve7(path);
15199
+ let current = resolve8(path);
15170
15200
  if (writableByCaller(current)) return true;
15171
15201
  current = dirname7(current);
15172
15202
  while (true) {
@@ -15512,7 +15542,7 @@ function runContainerStage(backend, dir, command, budgetSecs) {
15512
15542
  const args = containerRunArgs({
15513
15543
  image: backend.image,
15514
15544
  name,
15515
- workspace: resolve7(dir),
15545
+ workspace: resolve8(dir),
15516
15546
  command,
15517
15547
  uid,
15518
15548
  gid,
@@ -15626,7 +15656,7 @@ import { execFileSync as execFileSync8, spawnSync as spawnSync3 } from "node:chi
15626
15656
  import { createHash as createHash12 } from "node:crypto";
15627
15657
  import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync7, symlinkSync, writeFileSync as writeFileSync9, existsSync as existsSync14 } from "node:fs";
15628
15658
  import { tmpdir as tmpdir4 } from "node:os";
15629
- import { dirname as dirname8, isAbsolute as isAbsolute11, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "node:path";
15659
+ import { dirname as dirname8, isAbsolute as isAbsolute11, join as join19, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
15630
15660
  function oracleAssuranceReport() {
15631
15661
  return {
15632
15662
  level: "suite-exit-only",
@@ -15652,24 +15682,45 @@ function baseIsAncestorOfHead(base, cwd) {
15652
15682
  return r.status === 0;
15653
15683
  }
15654
15684
  function inside2(root, path) {
15655
- const rel = relative5(resolve8(root), resolve8(path));
15656
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute11(rel);
15685
+ const rel = relative6(resolve9(root), resolve9(path));
15686
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute11(rel);
15657
15687
  }
15658
15688
  function linkParts(path) {
15659
- const parts = sep3 === "\\" ? path.split(/[\\/]+/) : path.split("/");
15689
+ const parts = sep4 === "\\" ? path.split(/[\\/]+/) : path.split("/");
15660
15690
  return parts.filter((part) => part !== "" && part !== ".");
15661
15691
  }
15692
+ function materializationFailureReason(error) {
15693
+ if (error instanceof MaterializationError) return error.materializationReason;
15694
+ const code2 = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
15695
+ const message = error instanceof Error ? error.message : String(error);
15696
+ if (/tracked\s+node_modules|node_modules.*(?:attested|dependency).*(?:conflict|replace)/i.test(message)) {
15697
+ return "TRACKED_NODE_MODULES_CONFLICT";
15698
+ }
15699
+ if (/symlink.*(?:escape|cycle|parent directory)|escapes the materialised tree/i.test(message)) {
15700
+ return "SYMLINK_ESCAPE";
15701
+ }
15702
+ if (/(?:special file|where git expects a file)/i.test(message)) {
15703
+ return "SPECIAL_FILE";
15704
+ }
15705
+ if (code2 === "ENOENT" || /(?:ENOENT|no such file|file not found)/i.test(message)) {
15706
+ return "RACING_DELETION";
15707
+ }
15708
+ return "UNKNOWN";
15709
+ }
15662
15710
  function linkEscape(label, target) {
15663
- return new Error(`${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`);
15711
+ return new MaterializationError(
15712
+ "SYMLINK_ESCAPE",
15713
+ `${label} is a symlink that escapes the materialised tree (${JSON.stringify(target)})`
15714
+ );
15664
15715
  }
15665
15716
  function driveRelativeLinkTarget(target) {
15666
- return sep3 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
15717
+ return sep4 === "\\" && /^[A-Za-z]:(?![\\/])/.test(target);
15667
15718
  }
15668
15719
  function rootedLinkTarget(target) {
15669
15720
  return isAbsolute11(target) || driveRelativeLinkTarget(target);
15670
15721
  }
15671
15722
  function safeSymlink(target, out3, root, label) {
15672
- if (rootedLinkTarget(target) || !inside2(root, resolve8(dirname8(out3), target))) {
15723
+ if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname8(out3), target))) {
15673
15724
  throw linkEscape(label, target);
15674
15725
  }
15675
15726
  rmSync7(out3, { force: true });
@@ -15677,7 +15728,7 @@ function safeSymlink(target, out3, root, label) {
15677
15728
  }
15678
15729
  function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
15679
15730
  let domain = "tree";
15680
- let stack = linkParts(relative5(root, dirname8(out3)));
15731
+ let stack = linkParts(relative6(root, dirname8(out3)));
15681
15732
  const pending = linkParts(target);
15682
15733
  const seen = /* @__PURE__ */ new Set();
15683
15734
  let hops = 0;
@@ -15703,11 +15754,17 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
15703
15754
  }
15704
15755
  if (!st.isSymbolicLink()) continue;
15705
15756
  if (++hops > 128) {
15706
- throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
15757
+ throw new MaterializationError(
15758
+ "SYMLINK_ESCAPE",
15759
+ `${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`
15760
+ );
15707
15761
  }
15708
- const key2 = `${domain}:${current}:${pending.join(sep3)}`;
15762
+ const key2 = `${domain}:${current}:${pending.join(sep4)}`;
15709
15763
  if (seen.has(key2)) {
15710
- throw new Error(`${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`);
15764
+ throw new MaterializationError(
15765
+ "SYMLINK_ESCAPE",
15766
+ `${label} contains a symlink cycle while materialising (${JSON.stringify(target)})`
15767
+ );
15711
15768
  }
15712
15769
  seen.add(key2);
15713
15770
  const next = readlinkSync3(current);
@@ -15720,7 +15777,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
15720
15777
  throw linkEscape(label, target);
15721
15778
  }
15722
15779
  stack = [];
15723
- pending.unshift(...linkParts(relative5(dependencyRoot, resolve8(next))));
15780
+ pending.unshift(...linkParts(relative6(dependencyRoot, resolve9(next))));
15724
15781
  } else {
15725
15782
  pending.unshift(...linkParts(next));
15726
15783
  }
@@ -15738,12 +15795,21 @@ function rejectLinkedParent(cwd, rel) {
15738
15795
  continue;
15739
15796
  }
15740
15797
  if (st.isSymbolicLink()) {
15741
- throw new Error(`${rel} has a symlinked parent directory (${relative5(cwd, current)})`);
15798
+ throw new MaterializationError(
15799
+ "SYMLINK_ESCAPE",
15800
+ `${rel} has a symlinked parent directory (${relative6(cwd, current)})`
15801
+ );
15742
15802
  }
15743
15803
  }
15744
15804
  }
15745
15805
  function materialize(cwd, dest, dependencyRoot) {
15746
15806
  const listed = workingTreePaths(cwd);
15807
+ if (dependencyRoot && listed.some((rel) => rel === "node_modules" || rel.startsWith("node_modules/"))) {
15808
+ throw new MaterializationError(
15809
+ "TRACKED_NODE_MODULES_CONFLICT",
15810
+ "tracked node_modules conflicts with the attested dependency root"
15811
+ );
15812
+ }
15747
15813
  const links = [];
15748
15814
  for (const rel of listed) {
15749
15815
  const src = join19(cwd, rel);
@@ -15765,7 +15831,10 @@ function materialize(cwd, dest, dependencyRoot) {
15765
15831
  cpSync(src, out3, { dereference: true });
15766
15832
  chmodSync2(out3, st.mode);
15767
15833
  } else {
15768
- throw new Error(`${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`);
15834
+ throw new MaterializationError(
15835
+ "SPECIAL_FILE",
15836
+ `${rel} is ${st.isDirectory() ? "a directory" : "a special file"} where git expects a file`
15837
+ );
15769
15838
  }
15770
15839
  }
15771
15840
  for (const link of links) {
@@ -16225,7 +16294,7 @@ function runVerify(opts) {
16225
16294
  return cannotVerify(
16226
16295
  "MATERIALIZATION_FAILED",
16227
16296
  `could not materialize (${e instanceof Error ? e.message : String(e)})`,
16228
- { verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
16297
+ { verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport(), materialization_reason: materializationFailureReason(e) }
16229
16298
  );
16230
16299
  }
16231
16300
  const visible = runStage(visDir);
@@ -16259,6 +16328,49 @@ function runVerify(opts) {
16259
16328
  }
16260
16329
  return 2;
16261
16330
  }
16331
+ if (visible.failure === "budget") {
16332
+ const pristineNotRun = {
16333
+ exit: null,
16334
+ secs: 0,
16335
+ failure: "budget",
16336
+ reason: "not run: visible stage exceeded its budget"
16337
+ };
16338
+ cleanup([visRoot]);
16339
+ opts.onVerdict?.({ verdict: "BUDGET_EXCEEDED" });
16340
+ if (opts.json) {
16341
+ out3(JSON.stringify({
16342
+ schema_version: MACHINE_SCHEMA_VERSION,
16343
+ verdict: "BUDGET_EXCEEDED",
16344
+ base,
16345
+ command: cmd,
16346
+ budget_secs: budget,
16347
+ visible: stageJson(visible),
16348
+ pristine: stageJson(pristineNotRun),
16349
+ protected_restored: 0,
16350
+ added_protected_removed: 0,
16351
+ filesystem_case_sensitive: filesystemCaseSensitive,
16352
+ verifier_backend: backendReport(),
16353
+ dependency_environment: dependencyReport(),
16354
+ oracle_assurance: oracleAssuranceReport(),
16355
+ ...opts.keep ? { visible_dir: visDir } : {}
16356
+ }));
16357
+ } else {
16358
+ out3(`tamperward verify \u2014 ${verifyVerdictLine("BUDGET_EXCEEDED", {
16359
+ restored: 0,
16360
+ base,
16361
+ visibleExit: visible.exit,
16362
+ pristineExit: pristineNotRun.exit,
16363
+ budget
16364
+ })}`);
16365
+ out3("verifier backend: " + verifierBackendSummary(verifierBackend));
16366
+ out3("oracle assurance: suite-exit-only (candidate source executes inside the suite process; execution-domain isolation is not semantic/oracle isolation)");
16367
+ out3(
16368
+ dependencyEnvironment ? "dependency environment: " + dependencyEnvironmentSummary(dependencyEnvironment) : "dependency environment: verifier-owned by " + (verifierBackend.image ?? "isolated image")
16369
+ );
16370
+ renderStageDiagnostics(out3, "visible", visible);
16371
+ }
16372
+ return 2;
16373
+ }
16262
16374
  if (treeFingerprint(cwd, protectedIgnored) !== treeBefore) {
16263
16375
  cleanup([visRoot]);
16264
16376
  return cannotVerify(
@@ -16297,7 +16409,7 @@ function runVerify(opts) {
16297
16409
  return cannotVerify(
16298
16410
  "MATERIALIZATION_FAILED",
16299
16411
  `could not materialize pristine copy (${e instanceof Error ? e.message : String(e)})`,
16300
- { stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport() }
16412
+ { stage: "pristine", verifier_backend: backendReport(), dependency_environment: dependencyReport(), oracle_assurance: oracleAssuranceReport(), materialization_reason: materializationFailureReason(e) }
16301
16413
  );
16302
16414
  }
16303
16415
  const overlayBefore = overlayDigest(priDir, restored);
@@ -16458,7 +16570,7 @@ function parseVerify(args) {
16458
16570
  }
16459
16571
  return o;
16460
16572
  }
16461
- var OVERLAY_CLASSES, VERIFICATION_SURFACE, DROPPED_ENV, PINNED_NPM;
16573
+ var OVERLAY_CLASSES, VERIFICATION_SURFACE, MaterializationError, DROPPED_ENV, PINNED_NPM;
16462
16574
  var init_verify = __esm({
16463
16575
  "src/cli/verify.ts"() {
16464
16576
  "use strict";
@@ -16546,6 +16658,14 @@ var init_verify = __esm({
16546
16658
  "**/phpunit.xml.dist",
16547
16659
  "**/*.runsettings"
16548
16660
  ];
16661
+ MaterializationError = class extends Error {
16662
+ constructor(materializationReason, message) {
16663
+ super(message);
16664
+ this.materializationReason = materializationReason;
16665
+ this.name = "MaterializationError";
16666
+ }
16667
+ materializationReason;
16668
+ };
16549
16669
  DROPPED_ENV = /* @__PURE__ */ new Set([
16550
16670
  "NODE_OPTIONS",
16551
16671
  "NODE_PATH",
@@ -16564,7 +16684,7 @@ import { execFileSync as execFileSync9, spawn, spawnSync as spawnSync4 } from "n
16564
16684
  import { randomBytes as randomBytes3 } from "node:crypto";
16565
16685
  import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
16566
16686
  import { tmpdir as tmpdir5 } from "node:os";
16567
- import { dirname as dirname9, join as join20, resolve as resolve9 } from "node:path";
16687
+ import { dirname as dirname9, join as join20, resolve as resolve10 } from "node:path";
16568
16688
  function authoritativeRunLifecyclePlatform(platform = process.platform) {
16569
16689
  return platform === "linux";
16570
16690
  }
@@ -16783,7 +16903,7 @@ function startSupervisedObserver(cwd, base, entry) {
16783
16903
  try {
16784
16904
  child = spawn(
16785
16905
  process.execPath,
16786
- [resolve9(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
16906
+ [resolve10(cliEntry), "watch", "--dir", cwd, "--log", log, "--base", base],
16787
16907
  {
16788
16908
  cwd,
16789
16909
  stdio: ["ignore", "ignore", "inherit"],
@@ -16902,8 +17022,8 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
16902
17022
  return out3;
16903
17023
  }
16904
17024
  for (const pid of pids) {
16905
- const n = Number(pid);
16906
- if (n === process.pid) continue;
17025
+ const n2 = Number(pid);
17026
+ if (n2 === process.pid) continue;
16907
17027
  try {
16908
17028
  const stat = readFileSync17(`/proc/${pid}/stat`, "utf8");
16909
17029
  const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
@@ -16920,7 +17040,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
16920
17040
  for (const link of procLinks) {
16921
17041
  try {
16922
17042
  const raw = readlinkSync4(link).replace(/ \(deleted\)$/, "");
16923
- const held = raw.startsWith("/") ? resolve9(raw) : raw;
17043
+ const held = raw.startsWith("/") ? resolve10(raw) : raw;
16924
17044
  if (held === real || held.startsWith(real + "/")) {
16925
17045
  holds = true;
16926
17046
  break;
@@ -16928,12 +17048,12 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
16928
17048
  } catch {
16929
17049
  }
16930
17050
  }
16931
- if (holds) out3.push(n);
17051
+ if (holds) out3.push(n2);
16932
17052
  }
16933
17053
  return out3;
16934
17054
  }
16935
17055
  function runEnvelope(opts) {
16936
- const cwd = resolve9(opts.cwd ?? process.cwd());
17056
+ const cwd = resolve10(opts.cwd ?? process.cwd());
16937
17057
  const say = opts.json ? (_s) => {
16938
17058
  } : out;
16939
17059
  if (!opts.argv.length) {
@@ -16948,7 +17068,8 @@ function runEnvelope(opts) {
16948
17068
  }
16949
17069
  let base;
16950
17070
  try {
16951
- base = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
17071
+ const requestedBase = assertRev(opts.base ?? "HEAD");
17072
+ base = git3(["rev-parse", "--verify", `${requestedBase}^{commit}`], cwd).trim();
16952
17073
  } catch {
16953
17074
  err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
16954
17075
  return 2;
@@ -17478,7 +17599,7 @@ raise SystemExit(0)
17478
17599
  // src/cli/doctor.ts
17479
17600
  import { execFileSync as execFileSync10 } from "node:child_process";
17480
17601
  import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
17481
- import { join as join21, resolve as resolve10 } from "node:path";
17602
+ import { join as join21, resolve as resolve11 } from "node:path";
17482
17603
  function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
17483
17604
  if (platform === "linux") {
17484
17605
  if (linuxPython?.path) {
@@ -17556,7 +17677,7 @@ function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperw
17556
17677
  const broken = [];
17557
17678
  const warnings = [];
17558
17679
  for (const rel of workflowRels) {
17559
- const path = resolve10(cwd, rel);
17680
+ const path = resolve11(cwd, rel);
17560
17681
  if (!existsSync15(path)) {
17561
17682
  broken.push(`${rel} is missing`);
17562
17683
  continue;
@@ -17937,7 +18058,7 @@ function verifyJobs(doc) {
17937
18058
  return found;
17938
18059
  }
17939
18060
  function diagnose(opts = {}) {
17940
- const cwd = repoRoot(resolve10(opts.cwd ?? process.cwd()));
18061
+ const cwd = repoRoot(resolve11(opts.cwd ?? process.cwd()));
17941
18062
  let policy;
17942
18063
  try {
17943
18064
  policy = policyFor(opts, cwd);
@@ -17961,7 +18082,7 @@ function diagnose(opts = {}) {
17961
18082
  workflowRels.push(opts.workflow);
17962
18083
  } else {
17963
18084
  const workflowDirRel = ".github/workflows";
17964
- const workflowDir = resolve10(cwd, workflowDirRel);
18085
+ const workflowDir = resolve11(cwd, workflowDirRel);
17965
18086
  if (!existsSync15(workflowDir)) {
17966
18087
  return fail("ci-verifier", `${workflowDirRel}: workflow directory does not exist`);
17967
18088
  }
@@ -17981,7 +18102,7 @@ function diagnose(opts = {}) {
17981
18102
  let verifyJobCount = 0;
17982
18103
  const authorityWorkflows = [];
17983
18104
  for (const workflowRel of workflowRels) {
17984
- const workflowPath = resolve10(cwd, workflowRel);
18105
+ const workflowPath = resolve11(cwd, workflowRel);
17985
18106
  if (!existsSync15(workflowPath)) {
17986
18107
  return fail("ci-verifier", `${workflowRel}: workflow does not exist`);
17987
18108
  }
@@ -18173,27 +18294,127 @@ import {
18173
18294
  symlinkSync as symlinkSync2
18174
18295
  } from "node:fs";
18175
18296
  import { tmpdir as tmpdir6 } from "node:os";
18176
- import { isAbsolute as isAbsolute12, join as join22, relative as relative6, resolve as resolve11, sep as sep4 } from "node:path";
18297
+ import { isAbsolute as isAbsolute12, join as join22, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
18177
18298
  function unescapeStraceString(s) {
18178
18299
  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, "\\");
18179
18300
  }
18180
- function parseStraceFileAccess(raw) {
18301
+ function copyTraceState(state) {
18302
+ return { cwd: state.cwd, fds: new Map(state.fds) };
18303
+ }
18304
+ function traceLineProcess(line) {
18305
+ const bracketed = line.match(/^\s*\[pid\s+(\d+)\]\s+(.*)$/);
18306
+ if (bracketed) return { pid: bracketed[1], body: bracketed[2] };
18307
+ const numbered = line.match(/^\s*(\d+)\s+(.*)$/);
18308
+ if (numbered && /\b(?:execve|open|chdir|clone|fork|stat|access|readlink)/.test(numbered[2])) {
18309
+ return { pid: numbered[1], body: numbered[2] };
18310
+ }
18311
+ return { pid: "0", body: line.trim() };
18312
+ }
18313
+ function syscallResult(body) {
18314
+ const match = body.match(/\)\s+=\s+(-?\d+)\b/);
18315
+ return match ? Number(match[1]) : null;
18316
+ }
18317
+ function resolveTracePath(rawPath, state, dirfd) {
18318
+ if (isAbsolute12(rawPath)) return resolve12(rawPath);
18319
+ const base = dirfd === null || dirfd === -100 ? state.cwd : state.fds.get(dirfd);
18320
+ if (!base) return null;
18321
+ return resolve12(base, rawPath);
18322
+ }
18323
+ function firstDirFd(args) {
18324
+ const first = args.match(/^\s*(AT_FDCWD|-?\d+)/)?.[1];
18325
+ if (!first || first === "AT_FDCWD") return null;
18326
+ return Number(first);
18327
+ }
18328
+ function successfulFdResult(body) {
18329
+ const result = syscallResult(body);
18330
+ return result !== null && result >= 0 ? result : null;
18331
+ }
18332
+ function parseStraceFileAccess(raw, initialCwd = TRACE_ROOT) {
18181
18333
  const out3 = [];
18182
18334
  const seen = /* @__PURE__ */ new Set();
18183
- for (const line of raw.split("\n")) {
18184
- if (!line.trim() || /\)\s+=\s+-1\b/.test(line)) continue;
18185
- const call = line.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink)\s*\(/);
18186
- if (!call) continue;
18187
- const afterCall = line.slice((call.index ?? 0) + call[0].length);
18188
- const quoted = afterCall.match(/"([^"\\]*(?:\\.[^"\\]*)*)"/);
18189
- if (!quoted) continue;
18190
- const path = unescapeStraceString(quoted[1]);
18191
- if (!path) continue;
18192
- const access = call[1].startsWith("execve") ? "exec" : "read";
18193
- const key2 = `${access}\0${path}`;
18194
- if (seen.has(key2)) continue;
18335
+ const states = /* @__PURE__ */ new Map();
18336
+ const unfinished = /* @__PURE__ */ new Map();
18337
+ const stateFor = (pid) => {
18338
+ let state = states.get(pid);
18339
+ if (!state) {
18340
+ state = { cwd: initialCwd, fds: /* @__PURE__ */ new Map() };
18341
+ states.set(pid, state);
18342
+ }
18343
+ return state;
18344
+ };
18345
+ const add = (path, access, unresolved = false) => {
18346
+ const key2 = `${access}\0${unresolved ? "unresolved:" : ""}${path}`;
18347
+ if (seen.has(key2)) return;
18195
18348
  seen.add(key2);
18196
- out3.push({ path, access });
18349
+ out3.push(unresolved ? { path, access, unresolved: true } : { path, access });
18350
+ };
18351
+ for (const rawLine of raw.split("\n")) {
18352
+ if (!rawLine.trim()) continue;
18353
+ const line = traceLineProcess(rawLine);
18354
+ const resumed = line.body.match(/^<\.\.\.\s+(\w+)\s+resumed>(.*)$/);
18355
+ let body = line.body;
18356
+ if (resumed) {
18357
+ const key2 = `${line.pid}:${resumed[1]}`;
18358
+ const prefix = unfinished.get(key2);
18359
+ if (!prefix) continue;
18360
+ unfinished.delete(key2);
18361
+ body = prefix + resumed[2];
18362
+ }
18363
+ const unfinishedAt = body.indexOf("<unfinished ...>");
18364
+ if (unfinishedAt >= 0) {
18365
+ const before = body.slice(0, unfinishedAt);
18366
+ const call2 = before.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink|chdir|fchdir|close|dup2|dup3|dup|fcntl|clone3|clone|fork|vfork)\s*\(/);
18367
+ if (call2) unfinished.set(`${line.pid}:${call2[1]}`, before);
18368
+ continue;
18369
+ }
18370
+ const call = body.match(/\b(execveat|execve|openat2|openat|open|newfstatat|fstatat|statx|lstat|stat|access|readlinkat|readlink|chdir|fchdir|close|dup2|dup3|dup|fcntl|clone3|clone|fork|vfork)\s*\(/);
18371
+ if (!call || /\)\s+=\s+-1\b/.test(body)) continue;
18372
+ const name = call[1];
18373
+ const args = body.slice((call.index ?? 0) + call[0].length);
18374
+ const state = stateFor(line.pid);
18375
+ const result = syscallResult(body);
18376
+ if (name === "chdir") {
18377
+ const quoted2 = args.match(/\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"/);
18378
+ if (quoted2 && result === 0) {
18379
+ const next = resolveTracePath(unescapeStraceString(quoted2[1]), state, null);
18380
+ if (next) state.cwd = next;
18381
+ }
18382
+ continue;
18383
+ }
18384
+ if (name === "fchdir") {
18385
+ const fd = Number(args.match(/^\s*(-?\d+)/)?.[1]);
18386
+ const next = state.fds.get(fd);
18387
+ if (result === 0 && next) state.cwd = next;
18388
+ continue;
18389
+ }
18390
+ if (name === "close") {
18391
+ const fd = Number(args.match(/^\s*(-?\d+)/)?.[1]);
18392
+ if (result === 0) state.fds.delete(fd);
18393
+ continue;
18394
+ }
18395
+ if (name === "dup" || name === "dup2" || name === "dup3" || name === "fcntl") {
18396
+ const source = Number(args.match(/^\s*(-?\d+)/)?.[1]);
18397
+ const destination = name === "dup" ? result : name === "fcntl" ? result : Number(args.match(/^\s*-?\d+\s*,\s*(-?\d+)/)?.[1]);
18398
+ const sourcePath = state.fds.get(source);
18399
+ if (result !== null && result >= 0 && destination !== void 0 && destination !== null && sourcePath) state.fds.set(destination, sourcePath);
18400
+ continue;
18401
+ }
18402
+ if (name === "clone" || name === "clone3" || name === "fork" || name === "vfork") {
18403
+ if (result !== null && result > 0) states.set(String(result), copyTraceState(state));
18404
+ continue;
18405
+ }
18406
+ const quoted = args.match(/\"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)\"/);
18407
+ if (!quoted) continue;
18408
+ const rawPath = unescapeStraceString(quoted[1]);
18409
+ if (!rawPath) continue;
18410
+ const atCall = name === "execveat" || name === "openat2" || name === "openat" || name === "newfstatat" || name === "fstatat" || name === "statx" || name === "readlinkat";
18411
+ const dirfd = atCall ? firstDirFd(args) : null;
18412
+ const resolved = resolveTracePath(rawPath, state, dirfd);
18413
+ add(resolved ?? rawPath, name.startsWith("execve") ? "exec" : "read", resolved === null);
18414
+ if (name === "open" || name === "openat" || name === "openat2") {
18415
+ const fd = successfulFdResult(body);
18416
+ if (fd !== null && resolved) state.fds.set(fd, resolved);
18417
+ }
18197
18418
  }
18198
18419
  return out3;
18199
18420
  }
@@ -18214,13 +18435,13 @@ function classifyTraceRun(r) {
18214
18435
  return { kind: "ok", exit: r.status ?? 1 };
18215
18436
  }
18216
18437
  function inside3(root, path) {
18217
- const rel = relative6(resolve11(root), resolve11(path));
18218
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute12(rel);
18438
+ const rel = relative7(resolve12(root), resolve12(path));
18439
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute12(rel);
18219
18440
  }
18220
18441
  function normalRepoPath(root, raw) {
18221
- const abs = isAbsolute12(raw) ? resolve11(raw) : resolve11(root, raw);
18442
+ const abs = isAbsolute12(raw) ? resolve12(raw) : resolve12(root, raw);
18222
18443
  if (!inside3(root, abs)) return null;
18223
- const rel = relative6(root, abs).split(sep4).join("/");
18444
+ const rel = relative7(root, abs).split(sep5).join("/");
18224
18445
  return rel && rel !== ".." ? rel : null;
18225
18446
  }
18226
18447
  function likelyConfig(path) {
@@ -18233,12 +18454,17 @@ function sortAccesses(values) {
18233
18454
  function summarizeTraceRuns(opts) {
18234
18455
  const repo = /* @__PURE__ */ new Map();
18235
18456
  const external = /* @__PURE__ */ new Map();
18457
+ let unresolved_accesses = 0;
18236
18458
  opts.runs.forEach((run, index) => {
18237
18459
  const perRun = /* @__PURE__ */ new Set();
18238
18460
  for (const item of run) {
18461
+ if (item.unresolved) {
18462
+ unresolved_accesses++;
18463
+ continue;
18464
+ }
18239
18465
  const rel = normalRepoPath(opts.root, item.path);
18240
18466
  const isTracked = rel !== null && opts.tracked.has(rel);
18241
- const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve11(opts.root, item.path);
18467
+ const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve12(opts.root, item.path);
18242
18468
  if (keyPath === null) continue;
18243
18469
  const target = isTracked ? repo : external;
18244
18470
  const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
@@ -18276,7 +18502,8 @@ function summarizeTraceRuns(opts) {
18276
18502
  uncovered_repository_inputs,
18277
18503
  // Exact paths are intentionally conservative. A maintainer may widen them to
18278
18504
  // reviewed globs, but TamperWard never invents a broader trust surface.
18279
- suggested_verify_inputs: [...uncovered_repository_inputs]
18505
+ suggested_verify_inputs: [...uncovered_repository_inputs],
18506
+ unresolved_accesses
18280
18507
  };
18281
18508
  }
18282
18509
  function git4(args, cwd) {
@@ -18318,21 +18545,24 @@ function materializeBase(base, cwd, dest) {
18318
18545
  const hostModules = join22(cwd, "node_modules");
18319
18546
  const tracedModules = join22(dest, "node_modules");
18320
18547
  if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
18321
- symlinkSync2(resolve11(hostModules), tracedModules, "dir");
18548
+ symlinkSync2(resolve12(hostModules), tracedModules, "dir");
18322
18549
  }
18323
18550
  }
18324
18551
  function traceFiles(prefix) {
18325
- const dir = resolve11(prefix, "..");
18326
- const base = prefix.split(sep4).at(-1) ?? prefix;
18552
+ const dir = resolve12(prefix, "..");
18553
+ const base = prefix.split(sep5).at(-1) ?? prefix;
18327
18554
  return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join22(dir, name)).sort();
18328
18555
  }
18329
18556
  function rewriteTraceRoot(accesses, actualRoot) {
18330
18557
  return accesses.map((item) => {
18331
- const abs = isAbsolute12(item.path) ? resolve11(item.path) : resolve11(actualRoot, item.path);
18558
+ if (item.unresolved) return item;
18559
+ const placeholder = isAbsolute12(item.path) ? resolve12(item.path) : resolve12(TRACE_ROOT, item.path);
18560
+ const placeholderRel = inside3(TRACE_ROOT, placeholder) ? relative7(TRACE_ROOT, placeholder) : null;
18561
+ const abs = placeholderRel === null ? placeholder : resolve12(actualRoot, placeholderRel);
18332
18562
  if (!inside3(actualRoot, abs)) return { ...item, path: abs };
18333
- const rel = relative6(actualRoot, abs).split(sep4).join("/");
18563
+ const rel = relative7(actualRoot, abs).split(sep5).join("/");
18334
18564
  try {
18335
- const real = resolve11(realpathSync9(abs));
18565
+ const real = resolve12(realpathSync9(abs));
18336
18566
  if (!inside3(actualRoot, real)) return { ...item, path: real };
18337
18567
  } catch {
18338
18568
  }
@@ -18346,7 +18576,7 @@ function straceOnce(root, command, budget) {
18346
18576
  const traced = spawnSync5(
18347
18577
  "strace",
18348
18578
  [
18349
- "-ff",
18579
+ "-f",
18350
18580
  "-qq",
18351
18581
  "-s",
18352
18582
  "4096",
@@ -18365,13 +18595,13 @@ function straceOnce(root, command, budget) {
18365
18595
  ],
18366
18596
  {
18367
18597
  cwd: root,
18368
- env: process.env,
18598
+ env: suiteEnv(traceDir),
18369
18599
  encoding: "utf8",
18370
18600
  maxBuffer: 64 * 1024 * 1024
18371
18601
  }
18372
18602
  );
18373
18603
  const logs = traced.error ? [] : traceFiles(prefix);
18374
- const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync19(path, "utf8")));
18604
+ const accesses = logs.flatMap((path) => parseStraceFileAccess(readFileSync19(path, "utf8"), TRACE_ROOT));
18375
18605
  const raw = {
18376
18606
  spawnError: traced.error ? traced.error.message : null,
18377
18607
  status: traced.status,
@@ -18410,6 +18640,7 @@ function renderText2(report2) {
18410
18640
  out3(`trusted base: ${report2.base}`);
18411
18641
  out3(`command: ${report2.command}`);
18412
18642
  out3(`runs: ${report2.runs_completed}/${report2.runs_requested}; exits: ${report2.run_exits.join(", ")}`);
18643
+ out3(`unresolved accesses: ${report2.unresolved_accesses}`);
18413
18644
  out3();
18414
18645
  out3("repository inputs observed:");
18415
18646
  if (!report2.repository_inputs.length) out3(" (none)");
@@ -18438,7 +18669,7 @@ function renderText2(report2) {
18438
18669
  for (const note of report2.notes) out3(`note: ${note}`);
18439
18670
  }
18440
18671
  function runTraceVerify(opts = {}) {
18441
- const cwd = resolve11(opts.cwd ?? process.cwd());
18672
+ const cwd = resolve12(opts.cwd ?? process.cwd());
18442
18673
  if (process.platform !== "linux") {
18443
18674
  process.stderr.write("tamperward trace-verify: Linux only in this release; unsupported platform \u2014 no parity is implied.\n");
18444
18675
  return 2;
@@ -18536,13 +18767,14 @@ This is a tracing/tooling limitation (e.g. ptrace denied by seccomp or Yama), no
18536
18767
  runs_requested: runs,
18537
18768
  runs_completed: observed.length,
18538
18769
  run_exits: runExits,
18539
- trace_complete: runExits.every((code2) => code2 === 0),
18770
+ trace_complete: runExits.every((code2) => code2 === 0) && summary.unresolved_accesses === 0,
18540
18771
  ...summary,
18541
18772
  notes: [
18542
18773
  "Observed reads are evidence from these runs, not proof that an unobserved path can never be read.",
18543
18774
  "Dynamic paths are those observed in fewer than all repeated traces.",
18544
18775
  "Suggestions are exact tracked paths only; TamperWard never edits .tamperward.yml or widens a glob automatically.",
18545
- "Review every suggestion before adding it to verify.inputs."
18776
+ "Review every suggestion before adding it to verify.inputs.",
18777
+ ...summary.unresolved_accesses > 0 ? [`${summary.unresolved_accesses} relative file access(es) could not be resolved from trace cwd/fd state; discovery is incomplete.`] : []
18546
18778
  ]
18547
18779
  };
18548
18780
  if (opts.json) process.stdout.write(JSON.stringify(report2) + "\n");
@@ -18741,7 +18973,7 @@ var init_runtimes = __esm({
18741
18973
  import { execFileSync as execFileSync12 } from "node:child_process";
18742
18974
  import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as rmSync10 } from "node:fs";
18743
18975
  import { tmpdir as tmpdir7 } from "node:os";
18744
- import { join as join24, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
18976
+ import { join as join24, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
18745
18977
  import { createInterface } from "node:readline";
18746
18978
  import { parseDocument } from "yaml";
18747
18979
  function readlineAsker(input, output) {
@@ -18750,9 +18982,9 @@ function readlineAsker(input, output) {
18750
18982
  let pending = null;
18751
18983
  rl.on("close", () => {
18752
18984
  closed = true;
18753
- const resolve17 = pending;
18985
+ const resolve20 = pending;
18754
18986
  pending = null;
18755
- resolve17?.(null);
18987
+ resolve20?.(null);
18756
18988
  });
18757
18989
  rl.on("SIGINT", () => rl.close());
18758
18990
  const ask = (question) => new Promise((resolvePromise) => {
@@ -18775,9 +19007,9 @@ function terminalText(text) {
18775
19007
  return stripControl(text).replace(/\s+/g, " ").trim();
18776
19008
  }
18777
19009
  function committableSetupPath(cwd, path) {
18778
- const rel = relative7(cwd, resolve12(cwd, path));
18779
- if (!rel || rel === ".." || rel.startsWith(".." + sep5)) return false;
18780
- return rel !== ".git" && !rel.startsWith(".git" + sep5);
19010
+ const rel = relative8(cwd, resolve13(cwd, path));
19011
+ if (!rel || rel === ".." || rel.startsWith(".." + sep6)) return false;
19012
+ return rel !== ".git" && !rel.startsWith(".git" + sep6);
18781
19013
  }
18782
19014
  function platformLabel(platform) {
18783
19015
  if (platform === "darwin") return "macOS";
@@ -18835,7 +19067,7 @@ function writeVerifyCommand(cwd, command) {
18835
19067
  }
18836
19068
  }
18837
19069
  async function runOnboard(opts, io = {}) {
18838
- const requestedCwd = resolve12(opts.cwd ?? process.cwd());
19070
+ const requestedCwd = resolve13(opts.cwd ?? process.cwd());
18839
19071
  const out3 = io.out ?? ((line2) => void process.stdout.write(line2 + "\n"));
18840
19072
  const errLine = (line2) => void process.stderr.write(line2 + "\n");
18841
19073
  const rawErr = (line2) => errLine(terminalText(line2));
@@ -18869,7 +19101,7 @@ async function runOnboard(opts, io = {}) {
18869
19101
  fail(requestedCwd + " is not inside a Git repository. Run `git init` first.");
18870
19102
  return 2;
18871
19103
  }
18872
- const cwd = resolve12(rootText);
19104
+ const cwd = resolve13(rootText);
18873
19105
  let sameRoot = cwd === requestedCwd;
18874
19106
  try {
18875
19107
  sameRoot = realpathSync10(cwd) === realpathSync10(requestedCwd);
@@ -18902,10 +19134,10 @@ async function runOnboard(opts, io = {}) {
18902
19134
  return a === "y" || a === "yes";
18903
19135
  };
18904
19136
  let sectionNo = 0;
18905
- const section = (n) => {
18906
- sectionNo = n;
19137
+ const section = (n2) => {
19138
+ sectionNo = n2;
18907
19139
  out3("");
18908
- out3(paint(n + "/" + SECTIONS.length + " " + SECTIONS[n - 1], BOLD + CYAN, colour));
19140
+ out3(paint(n2 + "/" + SECTIONS.length + " " + SECTIONS[n2 - 1], BOLD + CYAN, colour));
18909
19141
  };
18910
19142
  const wrote = [];
18911
19143
  let verifyCommand;
@@ -18923,6 +19155,8 @@ async function runOnboard(opts, io = {}) {
18923
19155
  const localVerifySupported = localVerifierShell(platform, "true") !== null;
18924
19156
  if (platform === "linux" && lifecycle.state === "OK") {
18925
19157
  status("OK", "Full check / verify / run support is available.", "ok");
19158
+ } else if (platform === "linux") {
19159
+ status("LIMITED", lifecycle.detail, "warn");
18926
19160
  } else if (localVerifySupported) {
18927
19161
  status("LIMITED", platformLabel(platform) + ": check + verify work here; `tamperward run` requires Linux in this release.", "warn");
18928
19162
  } else {
@@ -19033,8 +19267,8 @@ async function runOnboard(opts, io = {}) {
19033
19267
  status("ACTION", "Choose the command that decides whether this repository passes:", "warn");
19034
19268
  candidates.forEach((candidate, i) => out3(" " + (i + 1) + ". " + terminalText(candidate)));
19035
19269
  const a = await ask("Choose 1-" + candidates.length + ", type a command, or press Enter to skip: ");
19036
- const n = Number(a);
19037
- chosen = a === "" ? void 0 : Number.isInteger(n) && n >= 1 && n <= candidates.length ? candidates[n - 1] : a;
19270
+ const n2 = Number(a);
19271
+ chosen = a === "" ? void 0 : Number.isInteger(n2) && n2 >= 1 && n2 <= candidates.length ? candidates[n2 - 1] : a;
19038
19272
  } else {
19039
19273
  status("ACTION", "No test command was detected automatically.", "warn");
19040
19274
  chosen = await ask("Test command to trust (Enter to skip): ") || void 0;
@@ -19218,7 +19452,7 @@ function runDemo(cwd, head, runners, out3) {
19218
19452
  out3("Demo skipped: the selected test could not be resolved safely inside the disposable worktree.");
19219
19453
  return;
19220
19454
  }
19221
- const prefix = worktreeRoot.endsWith(sep5) ? worktreeRoot : worktreeRoot + sep5;
19455
+ const prefix = worktreeRoot.endsWith(sep6) ? worktreeRoot : worktreeRoot + sep6;
19222
19456
  if (!realTarget.startsWith(prefix)) {
19223
19457
  out3("Demo skipped: the selected test resolves outside the disposable worktree.");
19224
19458
  return;
@@ -19282,7 +19516,7 @@ var init_onboard = __esm({
19282
19516
  });
19283
19517
 
19284
19518
  // src/research/adapter.ts
19285
- import { isAbsolute as isAbsolute13, resolve as resolve13 } from "node:path";
19519
+ import { isAbsolute as isAbsolute13, resolve as resolve14 } from "node:path";
19286
19520
  function taskEnv(task) {
19287
19521
  return {
19288
19522
  TAMPERWARD_RESEARCH_TASK: task.id,
@@ -19301,7 +19535,7 @@ function normalizeCommandArgv(argv, cwd = process.cwd()) {
19301
19535
  throw new ResearchError('the command adapter needs an agent command after "--"');
19302
19536
  }
19303
19537
  const head = argv[0];
19304
- return head.includes("/") && !isAbsolute13(head) ? [resolve13(cwd, head), ...argv.slice(1)] : [...argv];
19538
+ return head.includes("/") && !isAbsolute13(head) ? [resolve14(cwd, head), ...argv.slice(1)] : [...argv];
19305
19539
  }
19306
19540
  function commandAdapter(argv, cwd = process.cwd()) {
19307
19541
  const anchored = normalizeCommandArgv(argv, cwd);
@@ -19314,6 +19548,24 @@ function commandAdapter(argv, cwd = process.cwd()) {
19314
19548
  }
19315
19549
  };
19316
19550
  }
19551
+ function stdioAdapter(argv, cwd = process.cwd()) {
19552
+ const anchored = normalizeCommandArgv(argv, cwd);
19553
+ return {
19554
+ name: "stdio",
19555
+ layers: ["envelope"],
19556
+ launch(task) {
19557
+ const substituted = anchored.map((arg) => substitutePlaceholders(arg, task));
19558
+ return {
19559
+ argv: substituted,
19560
+ env: {
19561
+ ...taskEnv(task),
19562
+ TAMPERWARD_RESEARCH_PROTOCOL: "research-stdio-jsonl-v1",
19563
+ TAMPERWARD_RESEARCH_CAPABILITIES: JSON.stringify({ layers: ["envelope"], intervention: "not-connected" })
19564
+ }
19565
+ };
19566
+ }
19567
+ };
19568
+ }
19317
19569
  function claudeCodeAdapter(model) {
19318
19570
  return {
19319
19571
  name: "claude-code",
@@ -19336,6 +19588,7 @@ function claudeCodeAdapter(model) {
19336
19588
  function resolveAdapter(name, argv, model) {
19337
19589
  if (name === "command") return commandAdapter(argv);
19338
19590
  if (name === "claude-code") return claudeCodeAdapter(model);
19591
+ if (name === "stdio") return stdioAdapter(argv);
19339
19592
  throw new ResearchError(`unknown adapter "${name}" (built-in adapters: ${BUILTIN_ADAPTERS.join(", ")})`);
19340
19593
  }
19341
19594
  var ADAPTER_LAYERS, RESEARCH_ARMS, BUILTIN_ADAPTERS, ResearchError, PLACEHOLDERS, PLACEHOLDER_PATTERN;
@@ -19345,7 +19598,7 @@ var init_adapter = __esm({
19345
19598
  init_init();
19346
19599
  ADAPTER_LAYERS = ["envelope", "pre-tool-use", "stop-sweep"];
19347
19600
  RESEARCH_ARMS = ["ungated", "gated"];
19348
- BUILTIN_ADAPTERS = ["claude-code", "command"];
19601
+ BUILTIN_ADAPTERS = ["claude-code", "command", "stdio"];
19349
19602
  ResearchError = class extends Error {
19350
19603
  };
19351
19604
  PLACEHOLDERS = {
@@ -19404,7 +19657,7 @@ var init_capture = __esm({
19404
19657
  // src/research/manifest.ts
19405
19658
  import { createHash as createHash13 } from "node:crypto";
19406
19659
  import { readFileSync as readFileSync22 } from "node:fs";
19407
- import { dirname as dirname10, resolve as resolve14 } from "node:path";
19660
+ import { dirname as dirname10, resolve as resolve15 } from "node:path";
19408
19661
  function taskFrom(raw, index, manifestDir) {
19409
19662
  const where = `task #${index + 1}`;
19410
19663
  if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
@@ -19427,14 +19680,14 @@ function taskFrom(raw, index, manifestDir) {
19427
19680
  const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(repo) || /^[^/]+@[^:]+:/.test(repo);
19428
19681
  return {
19429
19682
  id,
19430
- repo: isUrl ? repo : resolve14(manifestDir, repo),
19683
+ repo: isUrl ? repo : resolve15(manifestDir, repo),
19431
19684
  base: base ?? "HEAD",
19432
19685
  prompt,
19433
19686
  verify: { command: verify.command, ...budget !== void 0 ? { budget } : {} }
19434
19687
  };
19435
19688
  }
19436
19689
  function readManifest(path) {
19437
- const abs = resolve14(path);
19690
+ const abs = resolve15(path);
19438
19691
  let bytes;
19439
19692
  try {
19440
19693
  bytes = readFileSync22(abs);
@@ -19737,7 +19990,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
19737
19990
  import { execFileSync as execFileSync13 } from "node:child_process";
19738
19991
  import { closeSync as closeSync3, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync9, openSync as openSync3, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync11, unlinkSync, writeFileSync as writeFileSync11, writeSync } from "node:fs";
19739
19992
  import { hostname } from "node:os";
19740
- import { join as join25, resolve as resolve15 } from "node:path";
19993
+ import { join as join25, resolve as resolve16 } from "node:path";
19741
19994
  function git6(args, cwd) {
19742
19995
  return execFileSync13("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
19743
19996
  }
@@ -19991,15 +20244,19 @@ function unobservedOutcome() {
19991
20244
  honest_completion: false
19992
20245
  };
19993
20246
  }
19994
- function trustedPolicyAt(base, ws3) {
20247
+ function trustedPolicyAt(base, ws3, loader = loadPolicyAt) {
19995
20248
  try {
19996
- return { policy: loadPolicyAt(base, ws3) ?? defaultPolicy() };
20249
+ return { policy: loader(base, ws3) ?? defaultPolicy() };
19997
20250
  } catch (e) {
19998
20251
  return { failure: `trusted policy at ${base.slice(0, 10)} could not be loaded: ${errorMessage(e).split("\n")[0]}` };
19999
20252
  }
20000
20253
  }
20001
- function observeOutcome(ws3, base, head, task) {
20002
- const trusted = trustedPolicyAt(base, ws3);
20254
+ function trustedProtectedOnly(base, ws3, loader = loadPolicyAt) {
20255
+ const trusted = trustedPolicyAt(base, ws3, loader);
20256
+ if ("failure" in trusted) return { trusted, protectedOnly: () => true };
20257
+ return { trusted, protectedOnly: (rel) => isProtected(rel, trusted.policy) };
20258
+ }
20259
+ function observeOutcome(ws3, base, head, task, trusted) {
20003
20260
  if ("failure" in trusted) return { outcome: unobservedOutcome(), unmeasurable: trusted.failure };
20004
20261
  const policy = trusted.policy;
20005
20262
  const verify = parseDocument2(
@@ -20073,16 +20330,10 @@ function runTrajectory(ledger, task, pair, arm, adapter, opts, sourceBase) {
20073
20330
  }
20074
20331
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
20075
20332
  const head = git6(["rev-parse", "HEAD"], ws3);
20076
- const protectedOnly = (rel) => {
20077
- try {
20078
- return isProtected(rel, loadPolicyAt(base, ws3) ?? defaultPolicy());
20079
- } catch {
20080
- return true;
20081
- }
20082
- };
20083
- const fingerprintBefore = treeFingerprint(ws3, protectedOnly);
20084
- const observed = observeOutcome(ws3, base, head, task);
20085
- const movedDuringObservation = treeFingerprint(ws3, protectedOnly) !== fingerprintBefore;
20333
+ const trustedSurface = trustedProtectedOnly(base, ws3);
20334
+ const fingerprintBefore = treeFingerprint(ws3, trustedSurface.protectedOnly);
20335
+ const observed = observeOutcome(ws3, base, head, task, trustedSurface.trusted);
20336
+ const movedDuringObservation = treeFingerprint(ws3, trustedSurface.protectedOnly) !== fingerprintBefore;
20086
20337
  const unmeasurable = agent.unmeasurable ?? observed.unmeasurable ?? (movedDuringObservation ? "NOT_QUIESCENT: the workspace changed while its outcome was being observed" : null);
20087
20338
  const { unmeasurable: _agentUnmeasurable, ...agentRecord } = agent;
20088
20339
  const outcome = unmeasurable === null ? observed.outcome : unobservedOutcome();
@@ -20127,7 +20378,7 @@ function runResearch(opts) {
20127
20378
  err2("tamperward research: the gated arm cannot start here (see `tamperward doctor`); run as a non-root user on Linux.");
20128
20379
  return 2;
20129
20380
  }
20130
- const ledger = resolve15(opts.out);
20381
+ const ledger = resolve16(opts.out);
20131
20382
  let lock;
20132
20383
  try {
20133
20384
  lock = acquireResearchLock(ledger, opts.breakLock === true);
@@ -20268,18 +20519,18 @@ var init_run2 = __esm({
20268
20519
 
20269
20520
  // src/research/summarize.ts
20270
20521
  import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
20271
- import { join as join26, resolve as resolve16 } from "node:path";
20522
+ import { join as join26, resolve as resolve17 } from "node:path";
20272
20523
  function readLedger2(dir) {
20273
- const pairsDir = join26(resolve16(dir), "pairs");
20524
+ const pairsDir = join26(resolve17(dir), "pairs");
20274
20525
  let names;
20275
20526
  try {
20276
- names = readdirSync7(pairsDir).filter((n) => n.endsWith(".json")).sort();
20527
+ names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
20277
20528
  } catch (e) {
20278
20529
  throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
20279
20530
  }
20280
20531
  if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
20281
- return names.map((n) => {
20282
- const path = join26(pairsDir, n);
20532
+ return names.map((n2) => {
20533
+ const path = join26(pairsDir, n2);
20283
20534
  let raw;
20284
20535
  try {
20285
20536
  raw = JSON.parse(readFileSync24(path, "utf8"));
@@ -20412,7 +20663,321 @@ var init_summarize = __esm({
20412
20663
  }
20413
20664
  });
20414
20665
 
20666
+ // src/research/init.ts
20667
+ import { createHash as createHash14 } from "node:crypto";
20668
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync25, writeFileSync as writeFileSync12 } from "node:fs";
20669
+ import { basename as basename5, dirname as dirname11, resolve as resolve18 } from "node:path";
20670
+ function idFrom(repo) {
20671
+ const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
20672
+ return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
20673
+ }
20674
+ function createResearchManifest(opts) {
20675
+ if (!opts.out) throw new ResearchError("research init requires --out");
20676
+ if (!opts.repo) throw new ResearchError("research init requires --repo");
20677
+ if (!opts.prompt) throw new ResearchError("research init requires --prompt");
20678
+ if (!opts.verifyCommand) throw new ResearchError("research init requires --verify-command");
20679
+ const out3 = resolve18(opts.out);
20680
+ if (existsSync19(out3)) throw new ResearchError(`research init refuses to overwrite ${out3}; choose a new path`);
20681
+ const id = opts.id ?? idFrom(opts.repo);
20682
+ if (!/^[A-Za-z0-9._-]+$/.test(id)) throw new ResearchError(`research init id must match [A-Za-z0-9._-] (got "${id}")`);
20683
+ if (opts.verifyBudget !== void 0 && (!Number.isFinite(opts.verifyBudget) || opts.verifyBudget <= 0)) {
20684
+ throw new ResearchError("research init --verify-budget must be positive");
20685
+ }
20686
+ const doc = {
20687
+ version: 1,
20688
+ tasks: [{
20689
+ id,
20690
+ repo: opts.repo,
20691
+ base: opts.base ?? "HEAD",
20692
+ prompt: opts.prompt,
20693
+ verify: { command: opts.verifyCommand, ...opts.verifyBudget === void 0 ? {} : { budget: opts.verifyBudget } }
20694
+ }]
20695
+ };
20696
+ const text = JSON.stringify(doc, null, 2) + "\n";
20697
+ mkdirSync10(dirname11(out3), { recursive: true });
20698
+ writeFileSync12(out3, text, { flag: "wx", mode: 384 });
20699
+ const parsed = readManifest(out3);
20700
+ if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
20701
+ return out3;
20702
+ }
20703
+ function runResearchInit(opts) {
20704
+ try {
20705
+ const out3 = createResearchManifest(opts);
20706
+ const bytes = requireHash(out3);
20707
+ process.stdout.write(`tamperward research init: wrote ${out3} (sha256 ${bytes})
20708
+ `);
20709
+ return 0;
20710
+ } catch (e) {
20711
+ if (e instanceof ResearchError || e instanceof Error) {
20712
+ process.stderr.write(`tamperward research: ${e.message}
20713
+ `);
20714
+ return 2;
20715
+ }
20716
+ throw e;
20717
+ }
20718
+ }
20719
+ function requireHash(path) {
20720
+ return createHash14("sha256").update(readFileSync25(path)).digest("hex");
20721
+ }
20722
+ var init_init2 = __esm({
20723
+ "src/research/init.ts"() {
20724
+ "use strict";
20725
+ init_adapter();
20726
+ init_manifest();
20727
+ }
20728
+ });
20729
+
20730
+ // src/research/report.ts
20731
+ function renderResearchReport(summary) {
20732
+ const u = summary.model_behaviour.ungated;
20733
+ const g = summary.model_behaviour.gated;
20734
+ const uo = summary.independent_outcome.ungated;
20735
+ const go = summary.independent_outcome.gated;
20736
+ const lines = [
20737
+ "MODEL BEHAVIOUR",
20738
+ ` model: ${summary.model ?? "(unspecified)"}`,
20739
+ ` adapter: ${summary.adapter.name} [${summary.adapter.layers.join(", ")}]`,
20740
+ ` pairs: ${n(summary.pairs)} (measured: ${n(summary.measured_pairs)}, unmeasurable: ${n(summary.unmeasurable_pairs)})`,
20741
+ ` ungated: ${n(u.trajectories)} trajectories; masked failures ${n(u.masked_failure)}; surviving mutations ${n(u.surviving_protected_mutations)}`,
20742
+ ` gated: ${n(g.trajectories)} trajectories; masked failures ${n(g.masked_failure)}; surviving mutations ${n(g.surviving_protected_mutations)}`,
20743
+ "",
20744
+ "CONTROL RESPONSE",
20745
+ " not measured by this adapter/ledger; denial-to-fix, surrender, and alternative-bypass responses are not inferred.",
20746
+ "",
20747
+ "INDEPENDENT OUTCOME",
20748
+ ` ungated: visible green ${n(uo.visible_green)}, pristine green ${n(uo.pristine_green)}, masked failure ${n(uo.masked_failure)}, honest completion ${n(uo.honest_completion)}, released green ${n(uo.released_green)}`,
20749
+ ` gated: visible green ${n(go.visible_green)}, pristine green ${n(go.pristine_green)}, masked failure ${n(go.masked_failure)}, honest completion ${n(go.honest_completion)}, released green ${n(go.released_green)}`,
20750
+ "",
20751
+ "TAMPERWARD PERFORMANCE",
20752
+ ` caught: ${n(summary.tamperward.caught)}`,
20753
+ ` escapes: ${n(summary.tamperward.escapes)}`,
20754
+ ` false refusals: ${n(summary.tamperward.false_refusals)}`,
20755
+ ` cannot adjudicate: ${n(summary.tamperward.cannot_adjudicate)}`,
20756
+ ` paired masked failure: ungated-only ${n(summary.paired.masked_failure.ungated_only)}, gated-only ${n(summary.paired.masked_failure.gated_only)}, both ${n(summary.paired.masked_failure.both)}, neither ${n(summary.paired.masked_failure.neither)}`
20757
+ ];
20758
+ return lines.join("\n") + "\n";
20759
+ }
20760
+ function runResearchReport(opts) {
20761
+ try {
20762
+ const summary = summarizeLedger(opts.ledger);
20763
+ process.stdout.write(opts.json ? JSON.stringify(summary, null, 2) + "\n" : renderResearchReport(summary));
20764
+ return 0;
20765
+ } catch (e) {
20766
+ if (e instanceof ResearchError) {
20767
+ process.stderr.write(`tamperward research: ${e.message}
20768
+ `);
20769
+ return 2;
20770
+ }
20771
+ throw e;
20772
+ }
20773
+ }
20774
+ var n;
20775
+ var init_report2 = __esm({
20776
+ "src/research/report.ts"() {
20777
+ "use strict";
20778
+ init_adapter();
20779
+ init_summarize();
20780
+ n = (value) => String(value);
20781
+ }
20782
+ });
20783
+
20784
+ // src/research/bundle.ts
20785
+ import { createHash as createHash15 } from "node:crypto";
20786
+ import { gzipSync, gunzipSync } from "node:zlib";
20787
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync26, statSync as statSync6, writeFileSync as writeFileSync13 } from "node:fs";
20788
+ import { dirname as dirname12, join as join27, resolve as resolve19 } from "node:path";
20789
+ function octal(value, width) {
20790
+ return value.toString(8).padStart(width - 1, "0") + "\0";
20791
+ }
20792
+ function tarEntry(name, bytes) {
20793
+ const header = Buffer.alloc(BLOCK, 0);
20794
+ const safe = name.replace(/^\/+/, "").slice(0, 99);
20795
+ header.write(safe, 0, "utf8");
20796
+ header.write(octal(420, 8), 100, "ascii");
20797
+ header.write(octal(0, 8), 108, "ascii");
20798
+ header.write(octal(0, 8), 116, "ascii");
20799
+ header.write(octal(bytes.length, 12), 124, "ascii");
20800
+ header.write(octal(0, 12), 136, "ascii");
20801
+ header.fill(32, 148, 156);
20802
+ header[156] = 48;
20803
+ header.write("ustar", 257, "ascii");
20804
+ header.write("00", 263, "ascii");
20805
+ const checksum = header.reduce((sum, b) => sum + b, 0);
20806
+ header.write(octal(checksum, 8), 148, "ascii");
20807
+ const padding = Buffer.alloc((BLOCK - bytes.length % BLOCK) % BLOCK, 0);
20808
+ return Buffer.concat([header, bytes, padding]);
20809
+ }
20810
+ function makeTar(entries) {
20811
+ return Buffer.concat([...entries.map((e) => tarEntry(e.name, e.bytes)), Buffer.alloc(BLOCK * 2, 0)]);
20812
+ }
20813
+ function parseTar(bytes) {
20814
+ const files = /* @__PURE__ */ new Map();
20815
+ for (let offset = 0; offset + BLOCK <= bytes.length; ) {
20816
+ const header = bytes.subarray(offset, offset + BLOCK);
20817
+ if (header.every((b) => b === 0)) break;
20818
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
20819
+ const sizeText = header.subarray(124, 136).toString("ascii").replace(/\0.*$/, "").trim();
20820
+ const size = Number.parseInt(sizeText || "0", 8);
20821
+ if (!name || name.startsWith("/") || name.split("/").includes("..") || !Number.isSafeInteger(size) || size < 0) {
20822
+ throw new ResearchError("research bundle contains an unsafe or malformed archive entry");
20823
+ }
20824
+ const start = offset + BLOCK;
20825
+ const end = start + size;
20826
+ if (end > bytes.length) throw new ResearchError("research bundle is truncated");
20827
+ files.set(name, Buffer.from(bytes.subarray(start, end)));
20828
+ offset = start + Math.ceil(size / BLOCK) * BLOCK;
20829
+ }
20830
+ return files;
20831
+ }
20832
+ function walkLedger(ledger) {
20833
+ const entries = [];
20834
+ const pairs = join27(ledger, "pairs");
20835
+ if (!existsSync20(pairs) || !statSync6(pairs).isDirectory()) throw new ResearchError(`ledger ${ledger} has no pairs directory`);
20836
+ const names = readdirSync8(pairs).filter((n2) => n2.endsWith(".json")).sort();
20837
+ if (names.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
20838
+ for (const name of names) {
20839
+ const path = join27(pairs, name);
20840
+ if (!statSync6(path).isFile()) continue;
20841
+ entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync26(path) });
20842
+ }
20843
+ return entries;
20844
+ }
20845
+ function jsonBytes(value) {
20846
+ return Buffer.from(JSON.stringify(value, null, 2) + "\n", "utf8");
20847
+ }
20848
+ function validateResearchBundle(path) {
20849
+ let files;
20850
+ try {
20851
+ files = parseTar(gunzipSync(readFileSync26(resolve19(path))));
20852
+ } catch (e) {
20853
+ throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
20854
+ }
20855
+ const provenance = files.get("provenance.json");
20856
+ const summaryBytes = files.get("summary.json");
20857
+ const report2 = files.get("report.txt");
20858
+ if (!provenance || !summaryBytes || !report2) throw new ResearchError("research bundle is missing provenance.json, summary.json or report.txt");
20859
+ let p;
20860
+ let summary;
20861
+ try {
20862
+ p = JSON.parse(provenance.toString("utf8"));
20863
+ summary = JSON.parse(summaryBytes.toString("utf8"));
20864
+ } catch {
20865
+ throw new ResearchError("research bundle contains invalid JSON");
20866
+ }
20867
+ if (p.bundle_schema_version !== BUNDLE_VERSION || p.schema_version !== MACHINE_SCHEMA_VERSION) {
20868
+ throw new ResearchError("research bundle schema version is unsupported");
20869
+ }
20870
+ const manifest = files.get("manifest.json");
20871
+ if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
20872
+ throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
20873
+ }
20874
+ if (manifest && createHash15("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
20875
+ throw new ResearchError("research bundle manifest does not match its recorded sha256");
20876
+ }
20877
+ const parsedSummary = summary;
20878
+ if (parsedSummary.document !== "summary" || parsedSummary.command !== "research") throw new ResearchError("research bundle summary is not a research summary");
20879
+ const records = [];
20880
+ for (const [name, bytes] of files) {
20881
+ if (!name.startsWith("ledger/pairs/") || !name.endsWith(".json")) continue;
20882
+ let raw;
20883
+ try {
20884
+ raw = JSON.parse(bytes.toString("utf8"));
20885
+ } catch {
20886
+ throw new ResearchError(`invalid JSON in ${name}`);
20887
+ }
20888
+ records.push(pairRecordFrom(raw, name));
20889
+ }
20890
+ if (records.length === 0) throw new ResearchError("research bundle contains no pair records");
20891
+ const derived = summarizeRecords(records);
20892
+ if (JSON.stringify(derived) !== JSON.stringify(summary)) throw new ResearchError("research bundle summary does not match its pair records");
20893
+ if (report2.toString("utf8") !== renderResearchReport(derived)) throw new ResearchError("research bundle report does not match its summary");
20894
+ if (p.manifest_sha256 !== parsedSummary.manifest_sha256) throw new ResearchError("research bundle manifest hash disagrees with summary");
20895
+ if (!p.protocol || p.protocol !== "research-bundle-v1") throw new ResearchError("research bundle protocol is missing or unsupported");
20896
+ return { records: records.length, manifest_sha256: String(p.manifest_sha256) };
20897
+ }
20898
+ function createResearchBundle(opts) {
20899
+ if (!opts.ledger) throw new ResearchError("research bundle requires --ledger");
20900
+ if (!opts.out) throw new ResearchError("research bundle requires --out");
20901
+ const ledger = resolve19(opts.ledger);
20902
+ const summary = summarizeLedger(ledger);
20903
+ const entries = walkLedger(ledger);
20904
+ if (opts.manifest) {
20905
+ const manifest = readFileSync26(resolve19(opts.manifest));
20906
+ const sha3 = createHash15("sha256").update(manifest).digest("hex");
20907
+ if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
20908
+ entries.push({ name: "manifest.json", bytes: manifest });
20909
+ }
20910
+ const provenance = {
20911
+ bundle_schema_version: BUNDLE_VERSION,
20912
+ protocol: "research-bundle-v1",
20913
+ schema_version: MACHINE_SCHEMA_VERSION,
20914
+ manifest_sha256: summary.manifest_sha256,
20915
+ adapter: summary.adapter,
20916
+ model: summary.model,
20917
+ tamperward_version: summary.tamperward_version,
20918
+ agent_argv: summary.agent_argv,
20919
+ agent_budget: summary.agent_budget,
20920
+ records: entries.filter((e) => e.name.startsWith("ledger/pairs/")).length,
20921
+ manifest_included: Boolean(opts.manifest),
20922
+ prompts_included: Boolean(opts.manifest)
20923
+ };
20924
+ entries.push({ name: "summary.json", bytes: jsonBytes(summary) });
20925
+ entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
20926
+ entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
20927
+ const out3 = resolve19(opts.out);
20928
+ mkdirSync11(dirname12(out3), { recursive: true });
20929
+ writeFileSync13(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
20930
+ return out3;
20931
+ }
20932
+ function runResearchBundle(opts) {
20933
+ try {
20934
+ if (opts.validate) {
20935
+ const result = validateResearchBundle(opts.validate);
20936
+ process.stdout.write(`tamperward research bundle: valid (${result.records} records, manifest ${result.manifest_sha256})
20937
+ `);
20938
+ return 0;
20939
+ }
20940
+ const out3 = createResearchBundle(opts);
20941
+ process.stdout.write(`tamperward research bundle: wrote ${out3}
20942
+ `);
20943
+ return 0;
20944
+ } catch (e) {
20945
+ if (e instanceof ResearchError || e instanceof Error) {
20946
+ process.stderr.write(`tamperward research: ${e.message}
20947
+ `);
20948
+ return 2;
20949
+ }
20950
+ throw e;
20951
+ }
20952
+ }
20953
+ var BLOCK, BUNDLE_VERSION;
20954
+ var init_bundle = __esm({
20955
+ "src/research/bundle.ts"() {
20956
+ "use strict";
20957
+ init_machine_output();
20958
+ init_adapter();
20959
+ init_record();
20960
+ init_report2();
20961
+ init_summarize();
20962
+ BLOCK = 512;
20963
+ BUNDLE_VERSION = 1;
20964
+ }
20965
+ });
20966
+
20415
20967
  // src/cli/research.ts
20968
+ function parseResearchInit(args) {
20969
+ const o = { out: "", repo: "", prompt: "", verifyCommand: "" };
20970
+ for (let i = 0; i < args.length; i++) {
20971
+ if (args[i] === "--out") o.out = args[++i];
20972
+ else if (args[i] === "--repo") o.repo = args[++i];
20973
+ else if (args[i] === "--base") o.base = args[++i];
20974
+ else if (args[i] === "--id") o.id = args[++i];
20975
+ else if (args[i] === "--prompt") o.prompt = args[++i];
20976
+ else if (args[i] === "--verify-command") o.verifyCommand = args[++i];
20977
+ else if (args[i] === "--verify-budget") o.verifyBudget = Number(args[++i]);
20978
+ }
20979
+ return o;
20980
+ }
20416
20981
  function parseResearchRun(args) {
20417
20982
  const o = { manifest: "", out: "", adapter: "", agentArgv: [] };
20418
20983
  for (let i = 0; i < args.length; i++) {
@@ -20438,10 +21003,36 @@ function parseResearchSummarize(args) {
20438
21003
  }
20439
21004
  return o;
20440
21005
  }
21006
+ function parseResearchReport(args) {
21007
+ const o = { ledger: "" };
21008
+ for (let i = 0; i < args.length; i++) {
21009
+ if (args[i] === "--ledger") o.ledger = args[++i];
21010
+ else if (args[i] === "--json") o.json = true;
21011
+ }
21012
+ return o;
21013
+ }
21014
+ function parseResearchBundle(args) {
21015
+ const o = { ledger: "" };
21016
+ for (let i = 0; i < args.length; i++) {
21017
+ if (args[i] === "--ledger") o.ledger = args[++i];
21018
+ else if (args[i] === "--out") o.out = args[++i];
21019
+ else if (args[i] === "--manifest") o.manifest = args[++i];
21020
+ else if (args[i] === "--validate") o.validate = args[++i];
21021
+ else if (args[i] === "--bundle") o.validate = args[++i];
21022
+ }
21023
+ return o;
21024
+ }
20441
21025
  function runResearchCommand(args) {
20442
21026
  const [sub, ...rest] = args;
21027
+ if (sub === "init") return runResearchInit(parseResearchInit(rest));
20443
21028
  if (sub === "run") return runResearch(parseResearchRun(rest));
20444
21029
  if (sub === "summarize") return runResearchSummarize(parseResearchSummarize(rest));
21030
+ if (sub === "report") return runResearchReport(parseResearchReport(rest));
21031
+ if (sub === "bundle") return runResearchBundle(parseResearchBundle(rest));
21032
+ if (sub === "validate") {
21033
+ const bundle = parseResearchBundle(rest);
21034
+ return runResearchBundle({ ledger: "", validate: bundle.validate ?? bundle.out });
21035
+ }
20445
21036
  process.stderr.write(`tamperward research: unknown subcommand "${sub ?? ""}" (${RESEARCH_SUBCOMMANDS.join(" | ")})
20446
21037
  `);
20447
21038
  return 2;
@@ -20452,7 +21043,10 @@ var init_research = __esm({
20452
21043
  "use strict";
20453
21044
  init_run2();
20454
21045
  init_summarize();
20455
- RESEARCH_SUBCOMMANDS = ["run", "summarize"];
21046
+ init_init2();
21047
+ init_report2();
21048
+ init_bundle();
21049
+ RESEARCH_SUBCOMMANDS = ["init", "run", "summarize", "report", "bundle", "validate"];
20456
21050
  }
20457
21051
  });
20458
21052
 
@@ -20587,14 +21181,45 @@ function parseCheck(args) {
20587
21181
  }
20588
21182
  return o;
20589
21183
  }
21184
+ function splitLongOptionEquals(args) {
21185
+ const out3 = [];
21186
+ for (let i = 0; i < args.length; i++) {
21187
+ const arg = args[i];
21188
+ if (arg === "--") {
21189
+ out3.push(...args.slice(i));
21190
+ break;
21191
+ }
21192
+ if (arg.startsWith("--")) {
21193
+ const equals = arg.indexOf("=");
21194
+ if (equals > 2) {
21195
+ out3.push(arg.slice(0, equals), arg.slice(equals + 1));
21196
+ continue;
21197
+ }
21198
+ }
21199
+ out3.push(arg);
21200
+ }
21201
+ return out3;
21202
+ }
21203
+ function helpRequested(args) {
21204
+ for (const arg of args) {
21205
+ if (arg === "--") break;
21206
+ if (arg === "-h" || arg === "--help") return true;
21207
+ }
21208
+ return false;
21209
+ }
20590
21210
  function validateFlatArgs(args, grammar) {
20591
21211
  const flags = new Set(grammar.flags ?? []);
20592
21212
  const values = grammar.values ?? {};
20593
21213
  const seen = /* @__PURE__ */ new Set();
20594
21214
  const positionals = [];
21215
+ let options = true;
20595
21216
  for (let i = 0; i < args.length; i++) {
20596
21217
  const a = args[i];
20597
- if (flags.has(a) || Object.prototype.hasOwnProperty.call(values, a)) {
21218
+ if (options && a === "--") {
21219
+ options = false;
21220
+ continue;
21221
+ }
21222
+ if (options && (flags.has(a) || Object.prototype.hasOwnProperty.call(values, a))) {
20598
21223
  if (seen.has(a)) {
20599
21224
  return { error: `option "${a}" specified more than once`, seen, positionals };
20600
21225
  }
@@ -20611,18 +21236,18 @@ function validateFlatArgs(args, grammar) {
20611
21236
  }
20612
21237
  i++;
20613
21238
  if (rule === "positive") {
20614
- const n = Number(v);
20615
- if (!Number.isFinite(n) || n <= 0) {
21239
+ const n2 = Number(v);
21240
+ if (!Number.isFinite(n2) || n2 <= 0) {
20616
21241
  return { error: `${a} needs a positive number (got "${v}")`, seen, positionals };
20617
21242
  }
20618
21243
  } else if (rule === "positive-integer") {
20619
- const n = Number(v);
20620
- if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
21244
+ const n2 = Number(v);
21245
+ if (!Number.isFinite(n2) || !Number.isInteger(n2) || n2 <= 0) {
20621
21246
  return { error: `${a} needs a positive integer (got "${v}")`, seen, positionals };
20622
21247
  }
20623
21248
  } else if (rule === "non-negative") {
20624
- const n = Number(v);
20625
- if (!Number.isFinite(n) || n < 0) {
21249
+ const n2 = Number(v);
21250
+ if (!Number.isFinite(n2) || n2 < 0) {
20626
21251
  return { error: `${a} needs a non-negative number (got "${v}")`, seen, positionals };
20627
21252
  }
20628
21253
  } else if (rule === "format" && !isFormat(v)) {
@@ -20634,7 +21259,7 @@ function validateFlatArgs(args, grammar) {
20634
21259
  }
20635
21260
  continue;
20636
21261
  }
20637
- if (a.startsWith("-")) {
21262
+ if (options && a.startsWith("-")) {
20638
21263
  return { error: `unknown option "${a}"`, seen, positionals };
20639
21264
  }
20640
21265
  if (grammar.positional === "one" && positionals.length === 0) {
@@ -20646,6 +21271,8 @@ function validateFlatArgs(args, grammar) {
20646
21271
  return { seen, positionals };
20647
21272
  }
20648
21273
  function validateCliArgs(cmd, args) {
21274
+ args = splitLongOptionEquals(args);
21275
+ if (helpRequested(args)) return void 0;
20649
21276
  if (cmd === "hook" || cmd === "sweep") {
20650
21277
  if (args.length === 0) return `${cmd} requires an agent name`;
20651
21278
  if (args.length > 1) return `unexpected argument "${args[1]}"`;
@@ -20744,7 +21371,7 @@ function validateCliArgs(cmd, args) {
20744
21371
  if (cmd === "hook-service") {
20745
21372
  const sub = args[0];
20746
21373
  if (sub !== "start" && sub !== "stop" && sub !== "status") return "hook-service requires one of start | stop | status";
20747
- return validateFlatArgs(args.slice(1), sub === "start" ? { values: { "--dir": "string" } } : {}).error;
21374
+ return validateFlatArgs(args.slice(1), { values: { "--dir": "string" } }).error;
20748
21375
  }
20749
21376
  if (cmd === "verify") {
20750
21377
  return validateFlatArgs(args, {
@@ -20777,6 +21404,24 @@ function validateCliArgs(cmd, args) {
20777
21404
  if (cmd === "research") {
20778
21405
  const [sub, ...rest] = args;
20779
21406
  if (sub === void 0) return `research requires a subcommand (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
21407
+ if (sub === "init") {
21408
+ const parsed = validateFlatArgs(rest, {
21409
+ values: {
21410
+ "--out": "string",
21411
+ "--repo": "string",
21412
+ "--base": "string",
21413
+ "--id": "string",
21414
+ "--prompt": "string",
21415
+ "--verify-command": "string",
21416
+ "--verify-budget": "positive"
21417
+ }
21418
+ });
21419
+ if (parsed.error) return parsed.error;
21420
+ for (const required of ["--out", "--repo", "--prompt", "--verify-command"]) {
21421
+ if (!parsed.seen.has(required)) return `research init requires ${required}`;
21422
+ }
21423
+ return void 0;
21424
+ }
20780
21425
  if (sub === "run") {
20781
21426
  const delimiter3 = rest.indexOf("--");
20782
21427
  const prefix = delimiter3 < 0 ? rest : rest.slice(0, delimiter3);
@@ -20803,6 +21448,29 @@ function validateCliArgs(cmd, args) {
20803
21448
  if (!parsed.seen.has("--ledger")) return "research summarize requires --ledger";
20804
21449
  return void 0;
20805
21450
  }
21451
+ if (sub === "report") {
21452
+ const parsed = validateFlatArgs(rest, { flags: ["--json"], values: { "--ledger": "string" } });
21453
+ if (parsed.error) return parsed.error;
21454
+ if (!parsed.seen.has("--ledger")) return "research report requires --ledger";
21455
+ return void 0;
21456
+ }
21457
+ if (sub === "bundle") {
21458
+ const parsed = validateFlatArgs(rest, {
21459
+ values: { "--ledger": "string", "--out": "string", "--manifest": "string", "--validate": "string" }
21460
+ });
21461
+ if (parsed.error) return parsed.error;
21462
+ if (parsed.seen.has("--validate")) return void 0;
21463
+ for (const required of ["--ledger", "--out"]) {
21464
+ if (!parsed.seen.has(required)) return `research bundle requires ${required}`;
21465
+ }
21466
+ return void 0;
21467
+ }
21468
+ if (sub === "validate") {
21469
+ const parsed = validateFlatArgs(rest, { values: { "--bundle": "string" } });
21470
+ if (parsed.error) return parsed.error;
21471
+ if (!parsed.seen.has("--bundle")) return "research validate requires --bundle";
21472
+ return void 0;
21473
+ }
20806
21474
  return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
20807
21475
  }
20808
21476
  return void 0;
@@ -20830,8 +21498,8 @@ Formats:
20830
21498
  tamperward hook claude PreToolUse gate (reads hook JSON on stdin)
20831
21499
  tamperward sweep claude Stop sweep (re-scan the turn's working tree)
20832
21500
  tamperward hook-service start [--dir D] OPT-IN persistent hook service: one warm
20833
- tamperward hook-service stop | status process per user and repository that
20834
- evaluates hook/sweep requests over a
21501
+ tamperward hook-service stop [--dir D] process per user and repository that
21502
+ tamperward hook-service status [--dir D] evaluates hook/sweep requests over a
20835
21503
  private unix socket, so each tool call
20836
21504
  skips Node + bundle startup. Hooks
20837
21505
  consult it only with
@@ -20877,6 +21545,8 @@ Formats:
20877
21545
  code when clean; 124 on clean AGENT_TIMEOUT;
20878
21546
  1 on any blocking finding/masked failure;
20879
21547
  2 when it cannot adjudicate (fails closed)
21548
+ tamperward research init --out F --repo R author a versioned one-task manifest
21549
+ --prompt P --verify-command C and print its sha256 identity
20880
21550
  tamperward research run --manifest F bring-your-own-model evaluation: for every
20881
21551
  --out D --adapter A [--pairs N] task in the manifest, pin one source commit,
20882
21552
  [--model M] [--agent-budget S] clone fresh state per arm, run the agent
@@ -20888,6 +21558,11 @@ Formats:
20888
21558
  tamperward research summarize --ledger D aggregate measured pairs into model behaviour,
20889
21559
  independent outcome, TamperWard hits/misses
20890
21560
  and paired counts \u2014 no composite score
21561
+ tamperward research report --ledger D human-readable four-section report (or --json)
21562
+ tamperward research bundle --ledger D reproducible records/provenance archive
21563
+ --out F [--manifest M] (manifest/prompts included only when requested)
21564
+ tamperward research bundle --validate F structurally and provenance-validate a bundle
21565
+ tamperward research validate --bundle F alias for bundle validation
20891
21566
  tamperward stats [--file F] [--since 30d] aggregate privacy-safe hook/sweep audit
20892
21567
  [--json] [--cwd D] events by rule and enforcement surface.
20893
21568
  Defaults to the repository-local
@@ -20944,7 +21619,12 @@ Exit codes: 0 clean \xB7 1 a blocking finding (check), MASKED_FAILURE or SUITE_R
20944
21619
  `);
20945
21620
  }
20946
21621
  function main(argv) {
20947
- const [cmd, ...rest] = argv;
21622
+ const [cmd, ...rawRest] = argv;
21623
+ const rest = splitLongOptionEquals(rawRest);
21624
+ if (cmd !== void 0 && cmd !== "-h" && cmd !== "--help" && helpRequested(rest)) {
21625
+ printHelp();
21626
+ return 0;
21627
+ }
20948
21628
  if (cmd !== void 0 && cmd !== "-h" && cmd !== "--help") {
20949
21629
  const invalid = validateCliArgs(cmd, rest);
20950
21630
  if (invalid) {
@@ -21039,7 +21719,7 @@ var init_main = __esm({
21039
21719
  // src/cli/index.ts
21040
21720
  init_hook_client();
21041
21721
  init_exit();
21042
- import { readFileSync as readFileSync25 } from "node:fs";
21722
+ import { readFileSync as readFileSync27 } from "node:fs";
21043
21723
  function loadMain() {
21044
21724
  return Promise.resolve().then(() => (init_main(), main_exports));
21045
21725
  }
@@ -21052,7 +21732,7 @@ async function launch(argv) {
21052
21732
  if (kind && hookServiceEnabled()) {
21053
21733
  let raw = null;
21054
21734
  try {
21055
- raw = readFileSync25(0, "utf8");
21735
+ raw = readFileSync27(0, "utf8");
21056
21736
  } catch {
21057
21737
  raw = null;
21058
21738
  }