tamperward 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.js +392 -121
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -153,7 +153,7 @@ function parseHunk(lines, start) {
153
153
  const oldLines = m[2] !== void 0 ? Number(m[2]) : 1;
154
154
  const newStart = Number(m[3]);
155
155
  const newLines = m[4] !== void 0 ? Number(m[4]) : 1;
156
- const out = [];
156
+ const out2 = [];
157
157
  let oldLine = oldStart;
158
158
  let newLine = newStart;
159
159
  let i = start + 1;
@@ -164,20 +164,20 @@ function parseHunk(lines, start) {
164
164
  const tag = l[0];
165
165
  const content = l.slice(1);
166
166
  if (tag === "+") {
167
- out.push({ type: "add", content, oldLine: null, newLine });
167
+ out2.push({ type: "add", content, oldLine: null, newLine });
168
168
  newLine++;
169
169
  } else if (tag === "-") {
170
- out.push({ type: "del", content, oldLine, newLine: null });
170
+ out2.push({ type: "del", content, oldLine, newLine: null });
171
171
  oldLine++;
172
172
  } else if (tag === " ") {
173
- out.push({ type: "context", content, oldLine, newLine });
173
+ out2.push({ type: "context", content, oldLine, newLine });
174
174
  oldLine++;
175
175
  newLine++;
176
176
  } else {
177
177
  break;
178
178
  }
179
179
  }
180
- return { hunk: { oldStart, oldLines, newStart, newLines, lines: out }, next: i };
180
+ return { hunk: { oldStart, oldLines, newStart, newLines, lines: out2 }, next: i };
181
181
  }
182
182
 
183
183
  // src/git/build.ts
@@ -190,9 +190,9 @@ function git(args, cwd) {
190
190
  stdio: ["ignore", "pipe", "pipe"]
191
191
  });
192
192
  } catch (e) {
193
- const err = e;
194
- const detail = String(err.stderr ?? "").trim();
195
- throw new Error(detail ? `git ${args[0]}: ${detail}` : err.message ?? `git ${args[0]} failed`);
193
+ const err2 = e;
194
+ const detail = String(err2.stderr ?? "").trim();
195
+ throw new Error(detail ? `git ${args[0]}: ${detail}` : err2.message ?? `git ${args[0]} failed`);
196
196
  }
197
197
  }
198
198
  function blobAt(rev, path, cwd) {
@@ -324,7 +324,7 @@ var noVerify = {
324
324
  surface: ["command"],
325
325
  certainty: "mechanical",
326
326
  run(changes, policy) {
327
- const out = [];
327
+ const out2 = [];
328
328
  for (const c of changes) {
329
329
  if (c.kind !== "command") continue;
330
330
  for (const seg of segments(c.raw)) {
@@ -341,7 +341,7 @@ var noVerify = {
341
341
  }
342
342
  }
343
343
  if (why) {
344
- out.push(
344
+ out2.push(
345
345
  makeFinding(RULE, policy, {
346
346
  message: `Command bypasses git hooks: ${why}.`,
347
347
  evidence: seg,
@@ -351,7 +351,7 @@ var noVerify = {
351
351
  }
352
352
  }
353
353
  }
354
- return out;
354
+ return out2;
355
355
  }
356
356
  };
357
357
 
@@ -373,13 +373,13 @@ import picomatch from "picomatch";
373
373
  var POLICY_FILE = ".tamperward.yml";
374
374
  var BLOCK_SINCE = {};
375
375
  function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
376
- const out = { ...rules };
376
+ const out2 = { ...rules };
377
377
  for (const [rule, since] of Object.entries(gates)) {
378
- if (since > version && out[rule]?.severity === "block") {
379
- out[rule] = { ...out[rule], severity: "warn" };
378
+ if (since > version && out2[rule]?.severity === "block") {
379
+ out2[rule] = { ...out2[rule], severity: "warn" };
380
380
  }
381
381
  }
382
- return out;
382
+ return out2;
383
383
  }
384
384
  var cache = /* @__PURE__ */ new Map();
385
385
  function matcher(glob) {
@@ -410,12 +410,12 @@ function isIgnored(path, policy) {
410
410
  return matchesAny(path, policy.ignore);
411
411
  }
412
412
  function mergeProtected(base, user) {
413
- const out = { ...base };
413
+ const out2 = { ...base };
414
414
  for (const [cat, globs] of Object.entries(user ?? {})) {
415
- const have = new Set(out[cat] ?? []);
416
- out[cat] = [...out[cat] ?? [], ...(globs ?? []).filter((g) => !have.has(g))];
415
+ const have = new Set(out2[cat] ?? []);
416
+ out2[cat] = [...out2[cat] ?? [], ...(globs ?? []).filter((g) => !have.has(g))];
417
417
  }
418
- return out;
418
+ return out2;
419
419
  }
420
420
  function defaultPolicy(version = 1) {
421
421
  const p = {
@@ -522,7 +522,7 @@ var tsAnyCast = {
522
522
  surface: ["file"],
523
523
  certainty: "mechanical",
524
524
  run(changes, policy) {
525
- const out = [];
525
+ const out2 = [];
526
526
  for (const c of changes) {
527
527
  if (c.kind !== "file" || !isCodeFile(c.path)) continue;
528
528
  const inTest = protectedCategory(c.path, policy) === "tests";
@@ -539,7 +539,7 @@ var tsAnyCast = {
539
539
  if (dDouble > 0) blockReasons.push("`as unknown as` double cast");
540
540
  if (dSuppr > 0) blockReasons.push("@ts-ignore/@ts-expect-error/@ts-nocheck suppression");
541
541
  if (blockReasons.length) {
542
- out.push(
542
+ out2.push(
543
543
  makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
544
544
  file: c.path,
545
545
  message: inTest ? `Type-checker escape in a test file: ${blockReasons.join("; ")} (test infrastructure \u2014 flagged, not blocked).` : `Type safety discarded: ${blockReasons.join("; ")}.`,
@@ -550,7 +550,7 @@ var tsAnyCast = {
550
550
  );
551
551
  }
552
552
  if (dBroad > 0) {
553
- out.push(
553
+ out2.push(
554
554
  makeFinding(WARN_RULE, policy, {
555
555
  file: c.path,
556
556
  message: `Type laundered to \`any\`: introduces ${dBroad} new \`any\`-typed value(s) in a type/generic position (e.g. : any, Record<string, any>).`,
@@ -564,7 +564,7 @@ var tsAnyCast = {
564
564
  }
565
565
  for (const l of addedLines(c)) {
566
566
  if (NARROW_LINE.test(l.content)) {
567
- out.push(
567
+ out2.push(
568
568
  makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
569
569
  file: c.path,
570
570
  line: l.newLine ?? void 0,
@@ -575,7 +575,7 @@ var tsAnyCast = {
575
575
  })
576
576
  );
577
577
  } else if (BROAD_LINE.test(l.content)) {
578
- out.push(
578
+ out2.push(
579
579
  makeFinding(WARN_RULE, policy, {
580
580
  file: c.path,
581
581
  line: l.newLine ?? void 0,
@@ -588,7 +588,7 @@ var tsAnyCast = {
588
588
  }
589
589
  }
590
590
  }
591
- return out;
591
+ return out2;
592
592
  }
593
593
  };
594
594
 
@@ -604,13 +604,13 @@ var lintSuppression = {
604
604
  surface: ["file"],
605
605
  certainty: "mechanical",
606
606
  run(changes, policy) {
607
- const out = [];
607
+ const out2 = [];
608
608
  for (const c of changes) {
609
609
  if (c.kind !== "file" || !isCodeFile(c.path)) continue;
610
610
  for (const l of addedLines(c)) {
611
611
  for (const p of PATTERNS) {
612
612
  if (p.re.test(l.content)) {
613
- out.push(
613
+ out2.push(
614
614
  makeFinding(RULE2, policy, {
615
615
  file: c.path,
616
616
  line: l.newLine ?? void 0,
@@ -624,7 +624,7 @@ var lintSuppression = {
624
624
  }
625
625
  }
626
626
  }
627
- return out;
627
+ return out2;
628
628
  }
629
629
  };
630
630
 
@@ -647,14 +647,14 @@ var testSkip = {
647
647
  surface: ["file"],
648
648
  certainty: "mechanical",
649
649
  run(changes, policy) {
650
- const out = [];
650
+ const out2 = [];
651
651
  for (const c of changes) {
652
652
  if (c.kind !== "file") continue;
653
653
  if (!isProtected(c.path, policy, "tests")) continue;
654
654
  for (const l of addedLines(c)) {
655
655
  for (const p of PATTERNS2) {
656
656
  if (p.re.test(l.content)) {
657
- out.push(
657
+ out2.push(
658
658
  makeFinding(RULE3, policy, {
659
659
  file: c.path,
660
660
  line: l.newLine ?? void 0,
@@ -668,7 +668,7 @@ var testSkip = {
668
668
  }
669
669
  }
670
670
  }
671
- return out;
671
+ return out2;
672
672
  }
673
673
  };
674
674
 
@@ -691,7 +691,7 @@ function numericOf(e) {
691
691
  }
692
692
  function metricsOf(obj) {
693
693
  if (!ts2.isObjectLiteralExpression(obj)) return void 0;
694
- const out = {};
694
+ const out2 = {};
695
695
  let any = false;
696
696
  for (const p of obj.properties) {
697
697
  if (!ts2.isPropertyAssignment(p)) continue;
@@ -699,10 +699,10 @@ function metricsOf(obj) {
699
699
  if (k === null || !isMetric(k)) continue;
700
700
  const n = numericOf(p.initializer);
701
701
  if (n === void 0) continue;
702
- out[k] = n;
702
+ out2[k] = n;
703
703
  any = true;
704
704
  }
705
- return any ? out : void 0;
705
+ return any ? out2 : void 0;
706
706
  }
707
707
  function underCoverageKey(node) {
708
708
  for (let p = node.parent; p; p = p.parent) {
@@ -744,41 +744,41 @@ function parseThresholds(src) {
744
744
  return res;
745
745
  }
746
746
  var eff = (t, path) => t.paths.get(norm(path)) ?? t.global;
747
- function compareMetrics(scope, before, after, out) {
747
+ function compareMetrics(scope, before, after, out2) {
748
748
  if (!before) return;
749
749
  for (const m of METRICS) {
750
750
  const bv = before[m];
751
751
  if (bv === void 0) continue;
752
752
  const av = after?.[m];
753
- if (av === void 0) out.push(`${scope} ${m} threshold removed (was ${bv})`);
754
- else if (av < bv) out.push(`${scope} ${m} threshold lowered ${bv} \u2192 ${av}`);
753
+ if (av === void 0) out2.push(`${scope} ${m} threshold removed (was ${bv})`);
754
+ else if (av < bv) out2.push(`${scope} ${m} threshold lowered ${bv} \u2192 ${av}`);
755
755
  }
756
756
  }
757
757
  function weakenings(before, after) {
758
- const out = [];
758
+ const out2 = [];
759
759
  if (before.present && !after.present) {
760
- out.push("the coverage threshold gate was removed");
761
- return out;
760
+ out2.push("the coverage threshold gate was removed");
761
+ return out2;
762
762
  }
763
- compareMetrics("global", before.global, after.global, out);
763
+ compareMetrics("global", before.global, after.global, out2);
764
764
  const paths = /* @__PURE__ */ new Set([...before.paths.keys(), ...after.paths.keys()]);
765
765
  for (const p of paths) {
766
- compareMetrics(`required coverage for ${p}:`, eff(before, p), eff(after, p), out);
766
+ compareMetrics(`required coverage for ${p}:`, eff(before, p), eff(after, p), out2);
767
767
  }
768
- return out;
768
+ return out2;
769
769
  }
770
770
  var coverageLowering = {
771
771
  id: RULE4,
772
772
  surface: ["file"],
773
773
  certainty: "mechanical",
774
774
  run(changes, policy) {
775
- const out = [];
775
+ const out2 = [];
776
776
  for (const c of changes) {
777
777
  if (c.kind !== "file") continue;
778
778
  if (!isProtected(c.path, policy, "config")) continue;
779
779
  if (c.after != null && c.op !== "delete") {
780
780
  for (const reason of weakenings(parseThresholds(c.before ?? ""), parseThresholds(c.after))) {
781
- out.push(
781
+ out2.push(
782
782
  makeFinding(RULE4, policy, {
783
783
  file: c.path,
784
784
  message: `Coverage gate weakened: ${reason}.`,
@@ -789,7 +789,7 @@ var coverageLowering = {
789
789
  }
790
790
  } else if (c.op === "delete" || c.after === "" && (c.before ?? "").length > 0) {
791
791
  if (parseThresholds(c.before ?? "").present) {
792
- out.push(
792
+ out2.push(
793
793
  makeFinding(RULE4, policy, {
794
794
  file: c.path,
795
795
  message: "Coverage gate weakened: the coverage config was deleted.",
@@ -803,7 +803,7 @@ var coverageLowering = {
803
803
  const removed = removedLines(c);
804
804
  const removedCoverage = removed.find((l) => /--coverage\b/.test(l.content));
805
805
  if (removedCoverage && !added.some((l) => /--coverage\b/.test(l.content))) {
806
- out.push(
806
+ out2.push(
807
807
  makeFinding(RULE4, policy, {
808
808
  file: c.path,
809
809
  message: "The --coverage flag was removed from a test script.",
@@ -814,7 +814,7 @@ var coverageLowering = {
814
814
  }
815
815
  for (const l of added) {
816
816
  if (/--passWithNoTests\b/.test(l.content)) {
817
- out.push(
817
+ out2.push(
818
818
  makeFinding(RULE4, policy, {
819
819
  file: c.path,
820
820
  line: l.newLine ?? void 0,
@@ -837,7 +837,7 @@ var coverageLowering = {
837
837
  if (!m) continue;
838
838
  const ov = oldNums.get(m[1]);
839
839
  if (ov !== void 0 && Number(m[2]) < ov) {
840
- out.push(
840
+ out2.push(
841
841
  makeFinding(RULE4, policy, {
842
842
  file: c.path,
843
843
  line: l.newLine ?? void 0,
@@ -851,7 +851,7 @@ var coverageLowering = {
851
851
  }
852
852
  }
853
853
  const seen = /* @__PURE__ */ new Set();
854
- return out.filter((f) => {
854
+ return out2.filter((f) => {
855
855
  const k = `${f.rule}|${f.message}`;
856
856
  if (seen.has(k)) return false;
857
857
  seen.add(k);
@@ -894,7 +894,7 @@ var ciTampering = {
894
894
  surface: ["file"],
895
895
  certainty: "mechanical",
896
896
  run(changes, policy) {
897
- const out = [];
897
+ const out2 = [];
898
898
  for (const c of changes) {
899
899
  if (c.kind !== "file") continue;
900
900
  if (!isProtected(c.path, policy, "ci")) continue;
@@ -902,7 +902,7 @@ var ciTampering = {
902
902
  const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
903
903
  const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
904
904
  if (coe && isTruthy(coe[1])) {
905
- out.push(
905
+ out2.push(
906
906
  makeFinding(RULE5, policy, {
907
907
  file: c.path,
908
908
  line: l.newLine ?? void 0,
@@ -912,7 +912,7 @@ var ciTampering = {
912
912
  })
913
913
  );
914
914
  } else if (cond && isAlwaysFalse(cond[1])) {
915
- out.push(
915
+ out2.push(
916
916
  makeFinding(RULE5, policy, {
917
917
  file: c.path,
918
918
  line: l.newLine ?? void 0,
@@ -931,7 +931,7 @@ var ciTampering = {
931
931
  const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
932
932
  if (!isUsesLine && !isCheckCommand) continue;
933
933
  if (afterCores.has(commandCore(l.content))) continue;
934
- out.push(
934
+ out2.push(
935
935
  makeFinding(RULE5, policy, {
936
936
  file: c.path,
937
937
  message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
@@ -941,7 +941,7 @@ var ciTampering = {
941
941
  );
942
942
  }
943
943
  }
944
- return out;
944
+ return out2;
945
945
  }
946
946
  };
947
947
 
@@ -1014,6 +1014,24 @@ function policyWeakening(before, after) {
1014
1014
  if (be.requiredFor.includes("block") && !new Set(ae.requiredFor).has("block")) {
1015
1015
  reasons.push("sign-off no longer required for blocking findings");
1016
1016
  }
1017
+ const bv = b.verify;
1018
+ const av = a.verify;
1019
+ if (bv?.command) {
1020
+ if (!av?.command) {
1021
+ reasons.push("verify block removed \u2014 pristine-suite re-execution disabled");
1022
+ } else {
1023
+ if (av.command !== bv.command) {
1024
+ reasons.push(
1025
+ `verify.command changed ("${bv.command}" \u2192 "${av.command}") \u2014 the re-executed suite is no longer the one that was configured`
1026
+ );
1027
+ }
1028
+ const bb = bv.budget ?? 300;
1029
+ const ab = av.budget ?? 300;
1030
+ if (ab < bb) {
1031
+ reasons.push(`verify.budget lowered ${bb}s \u2192 ${ab}s \u2014 starving the budget turns "cannot verify" into a pressure point`);
1032
+ }
1033
+ }
1034
+ }
1017
1035
  return reasons;
1018
1036
  }
1019
1037
 
@@ -1026,13 +1044,13 @@ var hookTampering = {
1026
1044
  surface: ["file", "command"],
1027
1045
  certainty: "mechanical",
1028
1046
  run(changes, policy) {
1029
- const out = [];
1047
+ const out2 = [];
1030
1048
  for (const c of changes) {
1031
1049
  if (c.kind === "file") {
1032
1050
  const targetsHook = isProtected(c.path, policy, "hooks") || (c.oldPath ? isProtected(c.oldPath, policy, "hooks") : false);
1033
1051
  if (!targetsHook) continue;
1034
1052
  if (c.op === "delete") {
1035
- out.push(
1053
+ out2.push(
1036
1054
  makeFinding(RULE6, policy, {
1037
1055
  file: c.path,
1038
1056
  message: "A protected hook/policy file was deleted.",
@@ -1041,7 +1059,7 @@ var hookTampering = {
1041
1059
  })
1042
1060
  );
1043
1061
  } else if (c.op === "rename") {
1044
- out.push(
1062
+ out2.push(
1045
1063
  makeFinding(RULE6, policy, {
1046
1064
  file: c.path,
1047
1065
  message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
@@ -1051,7 +1069,7 @@ var hookTampering = {
1051
1069
  );
1052
1070
  } else if (c.path.endsWith(POLICY_FILE) && c.before != null && c.after != null && policyWeakening(c.before, c.after) !== null) {
1053
1071
  for (const reason of policyWeakening(c.before, c.after) ?? []) {
1054
- out.push(
1072
+ out2.push(
1055
1073
  makeFinding(RULE6, policy, {
1056
1074
  file: c.path,
1057
1075
  message: `The policy was weakened: ${reason}.`,
@@ -1066,7 +1084,7 @@ var hookTampering = {
1066
1084
  const live = added.filter((l) => !isShellComment(l.content));
1067
1085
  const earlyExit = added.find((l) => /^\s*exit\s+0\s*$/.test(l.content));
1068
1086
  if (earlyExit) {
1069
- out.push(
1087
+ out2.push(
1070
1088
  makeFinding(RULE6, policy, {
1071
1089
  file: c.path,
1072
1090
  line: earlyExit.newLine ?? void 0,
@@ -1081,7 +1099,7 @@ var hookTampering = {
1081
1099
  const m = l.content.match(HOOK_INVOCATION);
1082
1100
  if (!m) continue;
1083
1101
  if (live.some((a) => a.content.includes(m[0]))) continue;
1084
- out.push(
1102
+ out2.push(
1085
1103
  makeFinding(RULE6, policy, {
1086
1104
  file: c.path,
1087
1105
  message: `A check invocation (\`${m[0]}\`) was removed from a protected hook.`,
@@ -1101,7 +1119,7 @@ var hookTampering = {
1101
1119
  for (const l of addedLines(c)) {
1102
1120
  const m = l.content.match(INLINE);
1103
1121
  if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
1104
- out.push(
1122
+ out2.push(
1105
1123
  makeFinding(RULE6, policy, {
1106
1124
  file: c.path,
1107
1125
  line: l.newLine ?? void 0,
@@ -1111,7 +1129,7 @@ var hookTampering = {
1111
1129
  })
1112
1130
  );
1113
1131
  } else if (/enabled:\s*false\b/.test(l.content)) {
1114
- out.push(
1132
+ out2.push(
1115
1133
  makeFinding(RULE6, policy, {
1116
1134
  file: c.path,
1117
1135
  line: l.newLine ?? void 0,
@@ -1140,7 +1158,7 @@ var hookTampering = {
1140
1158
  why = "a shell rewrite (tee/sed/cp/dd/install/mv) replaces a protected hook in place";
1141
1159
  }
1142
1160
  if (why) {
1143
- out.push(
1161
+ out2.push(
1144
1162
  makeFinding(RULE6, policy, {
1145
1163
  message: `Hook tampering via shell: ${why}.`,
1146
1164
  evidence: seg,
@@ -1151,7 +1169,7 @@ var hookTampering = {
1151
1169
  }
1152
1170
  }
1153
1171
  }
1154
- return out;
1172
+ return out2;
1155
1173
  }
1156
1174
  };
1157
1175
 
@@ -1179,19 +1197,19 @@ function countTestBlocks(src) {
1179
1197
  return n;
1180
1198
  }
1181
1199
  function significantLines(src) {
1182
- const out = /* @__PURE__ */ new Set();
1200
+ const out2 = /* @__PURE__ */ new Set();
1183
1201
  for (const raw of src.split("\n")) {
1184
1202
  const l = raw.trim();
1185
- if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out.add(l);
1203
+ if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out2.add(l);
1186
1204
  }
1187
- return out;
1205
+ return out2;
1188
1206
  }
1189
1207
  var testDeletion = {
1190
1208
  id: RULE7,
1191
1209
  surface: ["file", "command"],
1192
1210
  certainty: "mechanical",
1193
1211
  run(changes, policy) {
1194
- const out = [];
1212
+ const out2 = [];
1195
1213
  const addedTestLines = /* @__PURE__ */ new Set();
1196
1214
  let addedTestBlocks = 0;
1197
1215
  for (const c of changes) {
@@ -1212,7 +1230,7 @@ var testDeletion = {
1212
1230
  const isTest = isProtected(c.path, policy, "tests");
1213
1231
  if (c.op === "delete" && isTest) {
1214
1232
  if (c.before != null && isRelocation(c.before)) continue;
1215
- out.push(
1233
+ out2.push(
1216
1234
  makeFinding(RULE7, policy, {
1217
1235
  file: c.path,
1218
1236
  message: "A test file was deleted.",
@@ -1221,7 +1239,7 @@ var testDeletion = {
1221
1239
  })
1222
1240
  );
1223
1241
  } else if (c.op === "rename" && c.oldPath && isProtected(c.oldPath, policy, "tests") && !isTest) {
1224
- out.push(
1242
+ out2.push(
1225
1243
  makeFinding(RULE7, policy, {
1226
1244
  file: c.path,
1227
1245
  message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
@@ -1233,7 +1251,7 @@ var testDeletion = {
1233
1251
  const before = countTestBlocks(c.before);
1234
1252
  const after = countTestBlocks(c.after);
1235
1253
  if (after < before) {
1236
- out.push(
1254
+ out2.push(
1237
1255
  makeFinding(RULE7, policy, {
1238
1256
  file: c.path,
1239
1257
  message: `Test blocks removed: ${before} \u2192 ${after} it()/test() in this spec.`,
@@ -1268,7 +1286,7 @@ var testDeletion = {
1268
1286
  }
1269
1287
  }
1270
1288
  if (why) {
1271
- out.push(
1289
+ out2.push(
1272
1290
  makeFinding(RULE7, policy, {
1273
1291
  message: `Test removed via shell: ${why}.`,
1274
1292
  evidence,
@@ -1279,7 +1297,7 @@ var testDeletion = {
1279
1297
  }
1280
1298
  }
1281
1299
  }
1282
- return out;
1300
+ return out2;
1283
1301
  }
1284
1302
  };
1285
1303
 
@@ -1288,19 +1306,19 @@ var RULE8 = "test-content-removal";
1288
1306
  var MIN_REMOVED_LINES = 3;
1289
1307
  var ws = (s) => s.replace(/\s+/g, "");
1290
1308
  function significantLinesOrdered(src) {
1291
- const out = [];
1309
+ const out2 = [];
1292
1310
  for (const raw of src.split("\n")) {
1293
1311
  const l = raw.trim();
1294
- if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out.push(l);
1312
+ if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out2.push(l);
1295
1313
  }
1296
- return out;
1314
+ return out2;
1297
1315
  }
1298
1316
  var testContentRemoval = {
1299
1317
  id: RULE8,
1300
1318
  surface: ["file"],
1301
1319
  certainty: "mechanical",
1302
1320
  run(changes, policy) {
1303
- const out = [];
1321
+ const out2 = [];
1304
1322
  let keptPool = "";
1305
1323
  for (const c of changes) {
1306
1324
  if (c.kind === "file" && c.after != null && isProtected(c.path, policy, "tests")) {
@@ -1323,7 +1341,7 @@ var testContentRemoval = {
1323
1341
  gone.push(line);
1324
1342
  }
1325
1343
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
1326
- out.push(
1344
+ out2.push(
1327
1345
  makeFinding(RULE8, policy, {
1328
1346
  file: c.path,
1329
1347
  message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
@@ -1333,7 +1351,7 @@ var testContentRemoval = {
1333
1351
  );
1334
1352
  }
1335
1353
  }
1336
- return out;
1354
+ return out2;
1337
1355
  }
1338
1356
  };
1339
1357
 
@@ -1356,8 +1374,8 @@ var snapshotRewrite = {
1356
1374
  surface: ["command", "file"],
1357
1375
  certainty: "mechanical",
1358
1376
  run(changes, policy) {
1359
- const out = [];
1360
- const warn = (input) => out.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
1377
+ const out2 = [];
1378
+ const warn = (input) => out2.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
1361
1379
  for (const c of changes) {
1362
1380
  if (c.kind === "command") {
1363
1381
  for (const seg of segments(c.raw)) {
@@ -1390,7 +1408,7 @@ var snapshotRewrite = {
1390
1408
  remediation: "If the intended output genuinely changed, a human should confirm the new expectation against the requirement; if the test was failing, fix the code instead of re-recording the expectation from it."
1391
1409
  });
1392
1410
  }
1393
- return out;
1411
+ return out2;
1394
1412
  }
1395
1413
  };
1396
1414
 
@@ -1456,18 +1474,18 @@ function activeChanges(changes, policy) {
1456
1474
  }
1457
1475
  function evaluate(changes, policy, detectors = allDetectors, view) {
1458
1476
  const active = activeChanges(changes, policy);
1459
- const out = [];
1477
+ const out2 = [];
1460
1478
  for (const d of detectors) {
1461
1479
  if (!isEnabled(d.id, policy)) continue;
1462
1480
  try {
1463
- out.push(...d.run(active, policy, view));
1481
+ out2.push(...d.run(active, policy, view));
1464
1482
  } catch (e) {
1465
1483
  process.stderr.write(`tamperward: detector "${d.id}" errored and was skipped: ${String(e)}
1466
1484
  `);
1467
1485
  }
1468
1486
  }
1469
1487
  const seen = /* @__PURE__ */ new Set();
1470
- const deduped = out.filter((f) => {
1488
+ const deduped = out2.filter((f) => {
1471
1489
  const k = key(f);
1472
1490
  if (seen.has(k)) return false;
1473
1491
  seen.add(k);
@@ -1511,7 +1529,8 @@ function parsePolicy(raw) {
1511
1529
  signoff: {
1512
1530
  requiredFor: r.signoff?.required_for ?? r.signoff?.requiredFor ?? base.signoff.requiredFor,
1513
1531
  ledger: r.signoff?.ledger ?? base.signoff.ledger
1514
- }
1532
+ },
1533
+ ...r.verify?.command ? { verify: { command: r.verify.command, budget: r.verify.budget ?? 300 } } : {}
1515
1534
  };
1516
1535
  }
1517
1536
  function parseOrThrow(src, where) {
@@ -1552,16 +1571,16 @@ function ledgerPath(cwd, policy) {
1552
1571
  function readLedger(cwd, policy) {
1553
1572
  const p = ledgerPath(cwd, policy);
1554
1573
  if (!existsSync2(p)) return [];
1555
- const out = [];
1574
+ const out2 = [];
1556
1575
  for (const line of readFileSync3(p, "utf8").split("\n")) {
1557
1576
  if (!line.trim()) continue;
1558
1577
  try {
1559
1578
  const e = JSON.parse(line);
1560
- if (e && typeof e.fingerprint === "string") out.push(e);
1579
+ if (e && typeof e.fingerprint === "string") out2.push(e);
1561
1580
  } catch {
1562
1581
  }
1563
1582
  }
1564
- return out;
1583
+ return out2;
1565
1584
  }
1566
1585
  function appendEntry(cwd, policy, e) {
1567
1586
  const p = ledgerPath(cwd, policy);
@@ -1620,18 +1639,18 @@ function paint(s, code, on) {
1620
1639
  return on ? code + s + RESET : s;
1621
1640
  }
1622
1641
  function wrap(text, width) {
1623
- const out = [];
1642
+ const out2 = [];
1624
1643
  let cur = "";
1625
1644
  for (const w of text.split(/\s+/).filter(Boolean)) {
1626
1645
  if (cur === "") cur = w;
1627
1646
  else if (cur.length + 1 + w.length <= width) cur += " " + w;
1628
1647
  else {
1629
- out.push(cur);
1648
+ out2.push(cur);
1630
1649
  cur = w;
1631
1650
  }
1632
1651
  }
1633
- if (cur) out.push(cur);
1634
- return out.length ? out : [""];
1652
+ if (cur) out2.push(cur);
1653
+ return out2.length ? out2 : [""];
1635
1654
  }
1636
1655
  function clip(s, max) {
1637
1656
  const one = s.replace(/\s+/g, " ").trim();
@@ -1663,15 +1682,15 @@ function renderText(input, opts) {
1663
1682
  const blocks = findings.filter((f) => f.severity === "block");
1664
1683
  const warns = findings.filter((f) => f.severity === "warn");
1665
1684
  const c = opts.colour;
1666
- const out = [];
1685
+ const out2 = [];
1667
1686
  if (findings.length === 0) {
1668
1687
  return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
1669
1688
  `;
1670
1689
  }
1671
1690
  const headColour = blocks.length ? RED : YELLOW;
1672
- out.push(paint(`tamperward: ${countsPhrase(blocks.length, warns.length)}`, BOLD + headColour, c));
1673
- out.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
1674
- out.push("");
1691
+ out2.push(paint(`tamperward: ${countsPhrase(blocks.length, warns.length)}`, BOLD + headColour, c));
1692
+ out2.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
1693
+ out2.push("");
1675
1694
  const LABEL = "sign-off".length;
1676
1695
  const pad = (k) => (k + " ".repeat(LABEL)).slice(0, LABEL);
1677
1696
  const body = Math.max(20, opts.width - LABEL - 6);
@@ -1679,22 +1698,22 @@ function renderText(input, opts) {
1679
1698
  const isBlock = f.severity === "block";
1680
1699
  const mark = paint(isBlock ? "BLOCK" : "warn ", isBlock ? BOLD + RED : YELLOW, c);
1681
1700
  const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "(command)";
1682
- out.push(` ${mark} ${paint(f.rule, BOLD, c)} ${paint(loc, DIM, c)}`);
1683
- for (const l of wrap(f.message, opts.width - 4)) out.push(` ${l}`);
1701
+ out2.push(` ${mark} ${paint(f.rule, BOLD, c)} ${paint(loc, DIM, c)}`);
1702
+ for (const l of wrap(f.message, opts.width - 4)) out2.push(` ${l}`);
1684
1703
  const row = (key2, lines) => {
1685
- lines.forEach((l, i) => out.push(` ${paint(pad(i === 0 ? key2 : ""), DIM, c)} ${l}`));
1704
+ lines.forEach((l, i) => out2.push(` ${paint(pad(i === 0 ? key2 : ""), DIM, c)} ${l}`));
1686
1705
  };
1687
1706
  row("evidence", [paint(clip(f.evidence, body), DIM, c)]);
1688
1707
  row("instead", wrap(f.remediation, body));
1689
1708
  if (f.signoff.required) row("sign-off", [f.signoff.command]);
1690
- out.push("");
1709
+ out2.push("");
1691
1710
  }
1692
1711
  if (blocks.length > 0) {
1693
- out.push(
1712
+ out2.push(
1694
1713
  wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
1695
1714
  );
1696
1715
  }
1697
- return out.join("\n").replace(/\n+$/, "") + "\n";
1716
+ return out2.join("\n").replace(/\n+$/, "") + "\n";
1698
1717
  }
1699
1718
 
1700
1719
  // src/cli/render/github.ts
@@ -1742,7 +1761,7 @@ function renderSummary(input, env = process.env) {
1742
1761
  No integrity findings (${scope}).
1743
1762
  `;
1744
1763
  }
1745
- const out = [
1764
+ const out2 = [
1746
1765
  `## Tamperward: ${countsPhrase(blocks.length, warns.length)}`,
1747
1766
  "",
1748
1767
  `${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
@@ -1752,21 +1771,21 @@ No integrity findings (${scope}).
1752
1771
  ];
1753
1772
  for (const f of findings) {
1754
1773
  const sev = f.severity === "block" ? "**BLOCK**" : "warn";
1755
- out.push(
1774
+ out2.push(
1756
1775
  `| ${sev} | \`${cell(f.rule)}\` | ${locationCell(f, env)} | ${cell(f.message)} | ${cell(f.remediation)} |`
1757
1776
  );
1758
1777
  }
1759
1778
  const needSignoff = findings.filter((f) => f.signoff.required);
1760
1779
  if (needSignoff.length) {
1761
- out.push("", "### Clearing a blocking finding", "", SIGNOFF_NOTE, "");
1780
+ out2.push("", "### Clearing a blocking finding", "", SIGNOFF_NOTE, "");
1762
1781
  const seen = /* @__PURE__ */ new Set();
1763
1782
  for (const f of needSignoff) {
1764
1783
  if (seen.has(f.signoff.command)) continue;
1765
1784
  seen.add(f.signoff.command);
1766
- out.push(`- \`${cell(f.signoff.command)}\``);
1785
+ out2.push(`- \`${cell(f.signoff.command)}\``);
1767
1786
  }
1768
1787
  }
1769
- return out.join("\n") + "\n";
1788
+ return out2.join("\n") + "\n";
1770
1789
  }
1771
1790
  function writeSummary(md, env = process.env) {
1772
1791
  const path = env.GITHUB_STEP_SUMMARY;
@@ -1920,8 +1939,8 @@ function synthFileChange(displayPath, before, after) {
1920
1939
  try {
1921
1940
  raw = execFileSync2("git", ["diff", "--no-index", "--no-color", a, b], { encoding: "utf8" });
1922
1941
  } catch (e) {
1923
- const err = e;
1924
- raw = err.stdout ? String(err.stdout) : "";
1942
+ const err2 = e;
1943
+ raw = err2.stdout ? String(err2.stdout) : "";
1925
1944
  }
1926
1945
  } finally {
1927
1946
  rmSync(dir, { recursive: true, force: true });
@@ -2055,7 +2074,7 @@ function ptreePath(cwd, sessionId) {
2055
2074
  }
2056
2075
  var sha = (buf) => createHash2("sha256").update(buf).digest("hex").slice(0, 16);
2057
2076
  function snapshotProtected(cwd, policy, prev) {
2058
- const out = {};
2077
+ const out2 = {};
2059
2078
  const walk = (rel) => {
2060
2079
  const abs2 = rel ? join6(cwd, rel) : cwd;
2061
2080
  let names;
@@ -2077,7 +2096,7 @@ function snapshotProtected(cwd, policy, prev) {
2077
2096
  else if (st.isFile() && isProtected(r, policy)) {
2078
2097
  const p = prev?.[r];
2079
2098
  if (p && p.mtimeMs === st.mtimeMs && p.size === st.size && p.mode === st.mode) {
2080
- out[r] = p;
2099
+ out2[r] = p;
2081
2100
  } else {
2082
2101
  let hash = "";
2083
2102
  try {
@@ -2085,13 +2104,13 @@ function snapshotProtected(cwd, policy, prev) {
2085
2104
  } catch {
2086
2105
  hash = "unreadable";
2087
2106
  }
2088
- out[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
2107
+ out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
2089
2108
  }
2090
2109
  }
2091
2110
  }
2092
2111
  };
2093
2112
  walk("");
2094
- return out;
2113
+ return out2;
2095
2114
  }
2096
2115
  function driftBetween(expected, current) {
2097
2116
  const changed = [];
@@ -2218,9 +2237,9 @@ function runWatch(args) {
2218
2237
  else if (args[i] === "--log" && args[i + 1]) log = args[++i];
2219
2238
  }
2220
2239
  const policy = loadPolicy(dir);
2221
- const out = log ?? defaultEventLog(dir);
2222
- startWatcher(dir, out, policy);
2223
- process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${out}
2240
+ const out2 = log ?? defaultEventLog(dir);
2241
+ startWatcher(dir, out2, policy);
2242
+ process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${out2}
2224
2243
  `);
2225
2244
  return -1;
2226
2245
  }
@@ -2255,7 +2274,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
2255
2274
  arr.push(e);
2256
2275
  byPath.set(e.path, arr);
2257
2276
  }
2258
- const out = [];
2277
+ const out2 = [];
2259
2278
  const strict = process.env.TAMPERWARD_TRANSIENT === "block";
2260
2279
  for (const [path, evs] of byPath) {
2261
2280
  if (persistentPaths.has(path)) continue;
@@ -2278,9 +2297,9 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
2278
2297
  f.severity = "block";
2279
2298
  f.signoff.required = true;
2280
2299
  }
2281
- out.push(f);
2300
+ out2.push(f);
2282
2301
  }
2283
- return out;
2302
+ return out2;
2284
2303
  }
2285
2304
 
2286
2305
  // src/cli/hook.ts
@@ -2728,6 +2747,241 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
2728
2747
  return 0;
2729
2748
  }
2730
2749
 
2750
+ // src/cli/verify.ts
2751
+ import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
2752
+ import { chmodSync as chmodSync2, cpSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, rmSync as rmSync2, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "node:fs";
2753
+ import { tmpdir as tmpdir2 } from "node:os";
2754
+ import { dirname as dirname4, join as join9 } from "node:path";
2755
+ var OVERLAY_CLASSES = ["tests", "snapshots", "config"];
2756
+ function git2(args, cwd) {
2757
+ return execFileSync4("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
2758
+ }
2759
+ function resolveBase(base, cwd) {
2760
+ const rev = git2(["rev-parse", "--verify", `${base}^{commit}`], cwd).trim();
2761
+ try {
2762
+ return git2(["merge-base", rev, "HEAD"], cwd).trim();
2763
+ } catch {
2764
+ return rev;
2765
+ }
2766
+ }
2767
+ function materialize(cwd, dest) {
2768
+ const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
2769
+ for (const rel of listed) {
2770
+ const src = join9(cwd, rel);
2771
+ let st;
2772
+ try {
2773
+ st = statSync5(src);
2774
+ } catch {
2775
+ continue;
2776
+ }
2777
+ if (!st.isFile()) continue;
2778
+ const out2 = join9(dest, rel);
2779
+ mkdirSync6(dirname4(out2), { recursive: true });
2780
+ cpSync(src, out2);
2781
+ chmodSync2(out2, st.mode);
2782
+ }
2783
+ const nm = join9(cwd, "node_modules");
2784
+ if (existsSync8(nm)) symlinkSync(nm, join9(dest, "node_modules"), "dir");
2785
+ }
2786
+ function overlayPristine(cwd, base, dest, policy) {
2787
+ const atBase = git2(["ls-tree", "-r", "--name-only", "-z", base], cwd).split("\0").filter(Boolean);
2788
+ const isOverlay = (p) => OVERLAY_CLASSES.some((c) => isProtected(p, policy, c));
2789
+ let restored = 0;
2790
+ const baseProtected = /* @__PURE__ */ new Set();
2791
+ for (const rel of atBase) {
2792
+ if (!isOverlay(rel)) continue;
2793
+ baseProtected.add(rel);
2794
+ const content = execFileSync4("git", ["show", `${base}:${rel}`], { cwd, maxBuffer: 1 << 28 });
2795
+ const out2 = join9(dest, rel);
2796
+ mkdirSync6(dirname4(out2), { recursive: true });
2797
+ writeFileSync6(out2, content);
2798
+ restored++;
2799
+ }
2800
+ let kept = 0;
2801
+ const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
2802
+ for (const rel of inCopy) if (isOverlay(rel) && !baseProtected.has(rel)) kept++;
2803
+ return [restored, kept];
2804
+ }
2805
+ function runSuite(dir, cmd, budgetSecs) {
2806
+ const t0 = Date.now();
2807
+ const r = spawnSync("sh", ["-c", cmd], {
2808
+ cwd: dir,
2809
+ stdio: "ignore",
2810
+ timeout: budgetSecs * 1e3,
2811
+ killSignal: "SIGKILL"
2812
+ });
2813
+ const secs = Math.round((Date.now() - t0) / 1e3);
2814
+ if (r.error && r.error.code === "ETIMEDOUT") return { exit: null, secs };
2815
+ if (r.status === null && r.signal) return { exit: null, secs };
2816
+ return { exit: r.status ?? 2, secs };
2817
+ }
2818
+ function runVerify(opts) {
2819
+ const cwd = opts.cwd ?? process.cwd();
2820
+ const out2 = (s) => void process.stdout.write(s + "\n");
2821
+ let policy;
2822
+ try {
2823
+ policy = loadPolicy(cwd);
2824
+ } catch (e) {
2825
+ out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
2826
+ return 2;
2827
+ }
2828
+ const cmd = opts.cmd ?? policy.verify?.command;
2829
+ if (!cmd) {
2830
+ out2("verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd");
2831
+ return 2;
2832
+ }
2833
+ const budget = opts.budget ?? policy.verify?.budget ?? 300;
2834
+ let base;
2835
+ try {
2836
+ base = resolveBase(opts.base ?? "HEAD", cwd);
2837
+ } catch {
2838
+ out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
2839
+ return 2;
2840
+ }
2841
+ const work = mkdtempSync2(join9(tmpdir2(), "tw-verify-"));
2842
+ const visDir = join9(work, "visible");
2843
+ const priDir = join9(work, "pristine");
2844
+ let restored = 0;
2845
+ let kept = 0;
2846
+ try {
2847
+ mkdirSync6(visDir);
2848
+ mkdirSync6(priDir);
2849
+ materialize(cwd, visDir);
2850
+ materialize(cwd, priDir);
2851
+ [restored, kept] = overlayPristine(cwd, base, priDir, policy);
2852
+ } catch (e) {
2853
+ if (!opts.keep) rmSync2(work, { recursive: true, force: true });
2854
+ out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
2855
+ return 2;
2856
+ }
2857
+ const visible = runSuite(visDir, cmd, budget);
2858
+ const pristine = runSuite(priDir, cmd, budget);
2859
+ if (!opts.keep) rmSync2(work, { recursive: true, force: true });
2860
+ let verdict2;
2861
+ let code;
2862
+ if (visible.exit === null || pristine.exit === null) {
2863
+ verdict2 = "BUDGET_EXCEEDED";
2864
+ code = 2;
2865
+ } else if (visible.exit === 0 && pristine.exit === 0) {
2866
+ verdict2 = "VERIFIED";
2867
+ code = 0;
2868
+ } else if (visible.exit === 0) {
2869
+ verdict2 = "MASKED_FAILURE";
2870
+ code = 1;
2871
+ } else {
2872
+ verdict2 = "SUITE_RED";
2873
+ code = 1;
2874
+ }
2875
+ if (opts.json) {
2876
+ out2(
2877
+ JSON.stringify({
2878
+ verdict: verdict2,
2879
+ base,
2880
+ command: cmd,
2881
+ budget_secs: budget,
2882
+ visible: { exit: visible.exit, secs: visible.secs },
2883
+ pristine: { exit: pristine.exit, secs: pristine.secs },
2884
+ protected_restored: restored,
2885
+ added_protected_kept: kept,
2886
+ ...opts.keep ? { workdir: work } : {}
2887
+ })
2888
+ );
2889
+ } else {
2890
+ const lines = {
2891
+ VERIFIED: `verified: the suite passes, and still passes with all ${restored} protected files restored from ${base.slice(0, 10)}.`,
2892
+ MASKED_FAILURE: `MASKED FAILURE: the visible suite passes, but with the ${restored} protected files restored from ${base.slice(0, 10)} it FAILS (exit ${pristine.exit}). Something weakened the checks; the code does not pass the original suite.`,
2893
+ SUITE_RED: `suite red: the visible suite fails (exit ${visible.exit}) \u2014 fix the code first (pristine exit ${pristine.exit}).`,
2894
+ BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
2895
+ };
2896
+ out2(`tamperward verify \u2014 ${lines[verdict2]}`);
2897
+ if (kept > 0) out2(`(${kept} protected file(s) added since ${base.slice(0, 10)} were kept in the pristine run.)`);
2898
+ }
2899
+ return code;
2900
+ }
2901
+ function parseVerify(args) {
2902
+ const o = {};
2903
+ for (let i = 0; i < args.length; i++) {
2904
+ const a = args[i];
2905
+ if (a === "--base" && args[i + 1]) o.base = args[++i];
2906
+ else if (a === "--cmd" && args[i + 1]) o.cmd = args[++i];
2907
+ else if (a === "--budget" && args[i + 1]) o.budget = Number(args[++i]);
2908
+ else if (a === "--json") o.json = true;
2909
+ else if (a === "--keep") o.keep = true;
2910
+ else if (a === "--cwd" && args[i + 1]) o.cwd = args[++i];
2911
+ }
2912
+ return o;
2913
+ }
2914
+
2915
+ // src/cli/run.ts
2916
+ import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
2917
+ var out = (s) => process.stdout.write(s + "\n");
2918
+ var err = (s) => process.stderr.write(s + "\n");
2919
+ function git3(args, cwd) {
2920
+ return execFileSync5("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
2921
+ }
2922
+ function runEnvelope(opts) {
2923
+ const cwd = opts.cwd ?? process.cwd();
2924
+ if (!opts.argv.length) {
2925
+ err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
2926
+ return 2;
2927
+ }
2928
+ let base;
2929
+ try {
2930
+ base = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
2931
+ } catch {
2932
+ err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
2933
+ return 2;
2934
+ }
2935
+ if (!opts.allowDirty) {
2936
+ const dirty = git3(["status", "--porcelain"], cwd).trim();
2937
+ if (dirty) {
2938
+ err("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
2939
+ err("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
2940
+ return 2;
2941
+ }
2942
+ }
2943
+ out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
2944
+ const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
2945
+ const agentExit = agent.status ?? 1;
2946
+ const head = git3(["rev-parse", "HEAD"], cwd).trim();
2947
+ let diffCode = 0;
2948
+ if (head !== base) {
2949
+ out(`
2950
+ [envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
2951
+ diffCode = runCheck({ diff: `${base}...${head}`, cwd });
2952
+ }
2953
+ out("\n[envelope] policy check over the working tree:");
2954
+ const workCode = runCheck({ worktree: true, cwd });
2955
+ out("\n[envelope] pristine verification against the trusted base:");
2956
+ const verifyCode = runVerify({ cwd, base, cmd: opts.cmd, budget: opts.budget });
2957
+ const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
2958
+ const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1;
2959
+ const enforcement = cannot ? 2 : blocked ? 1 : 0;
2960
+ const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
2961
+ out(`
2962
+ tamperward run \u2014 agent exit ${agentExit}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
2963
+ return enforcement !== 0 ? enforcement : agentExit;
2964
+ }
2965
+ function parseRun(args) {
2966
+ const o = { argv: [] };
2967
+ for (let i = 0; i < args.length; i++) {
2968
+ const a = args[i];
2969
+ if (a === "--") {
2970
+ o.argv = args.slice(i + 1);
2971
+ break;
2972
+ } else if (a === "--base") o.base = args[++i];
2973
+ else if (a === "--cmd") o.cmd = args[++i];
2974
+ else if (a === "--budget") o.budget = Number(args[++i]);
2975
+ else if (a === "--allow-dirty") o.allowDirty = true;
2976
+ else if (a === "--cwd") o.cwd = args[++i];
2977
+ else {
2978
+ o.argv = args.slice(i);
2979
+ break;
2980
+ }
2981
+ }
2982
+ return o;
2983
+ }
2984
+
2731
2985
  // src/cli/index.ts
2732
2986
  function parseAllow(args) {
2733
2987
  const o = {};
@@ -2803,6 +3057,19 @@ Formats:
2803
3057
  tamperward watch [--dir D] [--log F] filesystem-event observer daemon: records
2804
3058
  protected-file events so the sweep can
2805
3059
  observe supported transient effects
3060
+ tamperward verify [--base R] [--cmd C] pristine-suite re-execution: run the suite
3061
+ [--budget S] [--json] [--keep] as-is AND with protected files restored
3062
+ from the trusted base; a visible-green /
3063
+ pristine-red result is a MASKED FAILURE
3064
+ (exit 1); cannot-verify fails closed (2)
3065
+ tamperward run [opts] -- <agent cmd...> enforcement envelope: record the trusted
3066
+ [--base R] [--cmd C] base, run the agent, treat its exit as
3067
+ [--budget S] [--allow-dirty] untrusted, then re-adjudicate the tree it
3068
+ left (policy over base...HEAD and the
3069
+ worktree, plus verify). Exit: agent's code
3070
+ when clean; 1 on any blocking finding or
3071
+ masked failure; 2 when it cannot
3072
+ adjudicate (fails closed)
2806
3073
  tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
2807
3074
  tamperward init [--dry-run] wire the policy file plus every
2808
3075
  enforcement point: Claude Code hooks,
@@ -2826,6 +3093,10 @@ function main(argv2) {
2826
3093
  return runAllow(parseAllow(rest));
2827
3094
  case "init":
2828
3095
  return runInit(parseInit(rest));
3096
+ case "verify":
3097
+ return runVerify(parseVerify(rest));
3098
+ case "run":
3099
+ return runEnvelope(parseRun(rest));
2829
3100
  case void 0:
2830
3101
  case "-h":
2831
3102
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",