tamperward 2.37.2 → 2.38.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
@@ -14395,6 +14395,10 @@ jobs:
14395
14395
  # Requires a \`verify:\` block in .tamperward.yml naming the suite command;
14396
14396
  # without one this step fails closed (exit 2) rather than passing quietly.
14397
14397
  - name: Tamperward verify (pristine re-execution)
14398
+ # bash so \`set -o pipefail\` propagates verify's exit through the tee \u2014 the
14399
+ # default GitHub shell does not enable pipefail, and tee's own 0 would
14400
+ # otherwise mask a red verify.
14401
+ shell: bash
14398
14402
  env:
14399
14403
  # The same channel as the gate. A compact token generated for \`verify\`
14400
14404
  # (or legacy \`tamperward:allow:verify@<head-sha>\`) accepts a MASKED_FAILURE \u2014
@@ -14404,7 +14408,53 @@ jobs:
14404
14408
  # stays red whatever the labels say.
14405
14409
  TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
14406
14410
  TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
14407
- run: tamperward verify --require-ancestor --base "\${{ github.event.pull_request.base.sha }}"
14411
+ # --json to a file for the reconciliation step below; the tee keeps the
14412
+ # verdict visible in the log. This step remains the ENFORCEMENT authority:
14413
+ # its own exit gates the job.
14414
+ run: tamperward verify --require-ancestor --base "\${{ github.event.pull_request.base.sha }}" --json | tee "$RUNNER_TEMP/tw-verify.json"
14415
+ # EVIDENCE, not authority (#601). CI has now rerun the canonical verification
14416
+ # itself; this step reconciles a claimed LOCAL verification receipt against
14417
+ # THAT result and writes a job summary separating the local claim, the CI
14418
+ # result and their agreement/divergence. A stale/mismatched/tampered/missing/
14419
+ # unknown-schema receipt can NEVER promote a CI result \u2014 the reconciled
14420
+ # verdict is CI's own (consumed from the verify --json above), recomputed from
14421
+ # trusted inputs. Set TAMPERWARD_RECEIPT (e.g. from a download-artifact step)
14422
+ # to a receipt exported by \`tamperward receipt export\` on the BRANCH TIP;
14423
+ # with none, reconciliation reports NO_CLAIM and CI's verdict stands.
14424
+ # \`if: always()\` re-asserts CI's verdict even when the verify step failed.
14425
+ #
14426
+ # The reconcile computes CI's candidate identity from pull_request.head.sha \u2014
14427
+ # the branch tip a developer verifies \u2014 NOT this job's merge-ref HEAD, so a
14428
+ # genuine receipt's \`head\`/\`tree\` line up (a receipt binds the branch tip;
14429
+ # the merge ref is a synthetic commit that no receipt can match). A linked
14430
+ # worktree reuses this checkout's object store (base, head and the merge
14431
+ # commit are all present from fetch-depth: 0), so merge-base resolves without
14432
+ # a second fetch. reconcile with --ci-result runs NO candidate code here \u2014 it
14433
+ # only computes identity (git/filesystem reads) and consumes the verdict the
14434
+ # verify step already produced. The enforcement authority remains the verify
14435
+ # step, which runs over the MERGE result as before.
14436
+ - name: Tamperward receipt reconciliation (evidence)
14437
+ if: always()
14438
+ shell: bash
14439
+ env:
14440
+ # The reconcile step's exit re-asserts CI's verdict, and a MASKED_FAILURE
14441
+ # a reviewer has signed off must exit 0 here just as the verify step does.
14442
+ # reconcile recomputes that sign-off from this SAME trusted channel \u2014 never
14443
+ # from the --ci-result document \u2014 so a forged oob_signoff field inside the
14444
+ # file it validates can never flip the exit (#601 re-review).
14445
+ TAMPERWARD_OOB_SIGNOFF: \${{ steps.oob.outputs.rules }}
14446
+ TAMPERWARD_OOB_HEAD: \${{ github.event.pull_request.head.sha }}
14447
+ run: |
14448
+ set -euo pipefail
14449
+ HEAD_TREE="$RUNNER_TEMP/tw-head"
14450
+ rm -rf "$HEAD_TREE"
14451
+ git worktree add --detach "$HEAD_TREE" "\${{ github.event.pull_request.head.sha }}"
14452
+ args=(receipt reconcile --require-ancestor --cwd "$HEAD_TREE" --base "\${{ github.event.pull_request.base.sha }}" --ci-result "$RUNNER_TEMP/tw-verify.json")
14453
+ if [ -n "\${TAMPERWARD_RECEIPT:-}" ] && [ -f "\${TAMPERWARD_RECEIPT:-}" ]; then
14454
+ args+=(--receipt "$TAMPERWARD_RECEIPT")
14455
+ fi
14456
+ tamperward "\${args[@]}"
14457
+ git worktree remove --force "$HEAD_TREE" || true
14408
14458
  `;
14409
14459
  WORKFLOW_MARK = "# tamperward:generated";
14410
14460
  WORKFLOW_MARK_RE = /^# tamperward:generated v(\S+) sha256:([0-9a-f]+)\n/m;
@@ -15752,6 +15802,14 @@ function resolveBaseRev(baseRef, cwd) {
15752
15802
  return rev2;
15753
15803
  }
15754
15804
  }
15805
+ function resolveConcreteCommit(cwd, ref) {
15806
+ try {
15807
+ assertRev(ref);
15808
+ return git2(["rev-parse", "--verify", `${ref}^{commit}`], repoRoot(cwd)).trim();
15809
+ } catch {
15810
+ return null;
15811
+ }
15812
+ }
15755
15813
  function canonicalJson(value) {
15756
15814
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
15757
15815
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -16001,6 +16059,15 @@ function firstMismatch(recorded, live) {
16001
16059
  }
16002
16060
  return null;
16003
16061
  }
16062
+ function firstCandidateIdentityMismatch(recorded, live) {
16063
+ for (const input of CANDIDATE_MISMATCH_PRIORITY) {
16064
+ if (recorded[input] !== live[input]) return input;
16065
+ }
16066
+ return null;
16067
+ }
16068
+ function environmentInputDivergences(recorded, live) {
16069
+ return ENVIRONMENT_INPUTS.filter((input) => recorded[input] !== live[input]);
16070
+ }
16004
16071
  function evaluateVerificationState(cwd) {
16005
16072
  if (readVerifyingMarker(cwd)) {
16006
16073
  return { state: "VERIFYING", reason: "a verification is in progress" };
@@ -16047,7 +16114,7 @@ function evaluateVerificationState(cwd) {
16047
16114
  record
16048
16115
  };
16049
16116
  }
16050
- var VERIFICATION_STATE_SCHEMA_VERSION, BINDING_INPUTS, STALE_REASON, MISMATCH_PRIORITY;
16117
+ var VERIFICATION_STATE_SCHEMA_VERSION, BINDING_INPUTS, STALE_REASON, MISMATCH_PRIORITY, CANDIDATE_IDENTITY_INPUTS, ENVIRONMENT_INPUTS, CANDIDATE_IDENTITY_SET, CANDIDATE_MISMATCH_PRIORITY;
16051
16118
  var init_verification_state = __esm({
16052
16119
  "src/verification-state.ts"() {
16053
16120
  "use strict";
@@ -16090,15 +16157,257 @@ var init_verification_state = __esm({
16090
16157
  "policy",
16091
16158
  "tree"
16092
16159
  ];
16160
+ CANDIDATE_IDENTITY_INPUTS = [
16161
+ "tree",
16162
+ "head",
16163
+ "base",
16164
+ "policy",
16165
+ "verifier",
16166
+ "surface"
16167
+ ];
16168
+ ENVIRONMENT_INPUTS = ["intervention", "dependencies"];
16169
+ CANDIDATE_IDENTITY_SET = new Set(CANDIDATE_IDENTITY_INPUTS);
16170
+ CANDIDATE_MISMATCH_PRIORITY = MISMATCH_PRIORITY.filter(
16171
+ (i) => CANDIDATE_IDENTITY_SET.has(i)
16172
+ );
16173
+ }
16174
+ });
16175
+
16176
+ // src/verification-receipt.ts
16177
+ import { createHash as createHash13 } from "node:crypto";
16178
+ import { mkdirSync as mkdirSync9, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
16179
+ import { dirname as dirname9, join as join20 } from "node:path";
16180
+ function isRecord5(v) {
16181
+ return typeof v === "object" && v !== null && !Array.isArray(v);
16182
+ }
16183
+ function canonicalJson2(value) {
16184
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
16185
+ if (Array.isArray(value)) return `[${value.map(canonicalJson2).join(",")}]`;
16186
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
16187
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson2(v)}`).join(",")}}`;
16188
+ }
16189
+ function receiptEvidenceDigest(fields2) {
16190
+ const canonical2 = {
16191
+ schema_version: fields2.schema_version,
16192
+ verdict: fields2.verdict,
16193
+ binding: BINDING_INPUTS.reduce((acc2, k) => {
16194
+ acc2[k] = fields2.binding[k];
16195
+ return acc2;
16196
+ }, {}),
16197
+ stages: fields2.stages
16198
+ };
16199
+ return "sha256:" + createHash13("sha256").update(canonicalJson2(canonical2)).digest("hex");
16200
+ }
16201
+ function receiptFromRecord(record) {
16202
+ const stages = { candidate: "PASS", pristine: "PASS", integrity: "CLEAN" };
16203
+ const core = {
16204
+ schema_version: RECEIPT_SCHEMA_VERSION,
16205
+ verdict: "VERIFIED",
16206
+ binding: record.binding,
16207
+ stages
16208
+ };
16209
+ return {
16210
+ ...core,
16211
+ tw_version: record.tw_version,
16212
+ verified_at: record.verified_at,
16213
+ evidence_digest: receiptEvidenceDigest(core)
16214
+ };
16215
+ }
16216
+ function receiptPath(cwd) {
16217
+ const ctx = repoContext(cwd);
16218
+ return ctx ? join20(ctx.gitDir, "tamperward", "verification-receipt.json") : null;
16219
+ }
16220
+ function storeReceipt(cwd, receipt) {
16221
+ const path = receiptPath(cwd);
16222
+ if (!path) return false;
16223
+ try {
16224
+ mkdirSync9(dirname9(path), { recursive: true });
16225
+ writeFileSync10(path, JSON.stringify(receipt) + "\n");
16226
+ return true;
16227
+ } catch {
16228
+ return false;
16229
+ }
16230
+ }
16231
+ function removeStoredReceipt(cwd) {
16232
+ const path = receiptPath(cwd);
16233
+ if (!path) return;
16234
+ try {
16235
+ rmSync8(path, { force: true });
16236
+ } catch {
16237
+ }
16238
+ }
16239
+ function isOneOf(list3, v) {
16240
+ return typeof v === "string" && list3.includes(v);
16241
+ }
16242
+ function isStages(v) {
16243
+ return isRecord5(v) && isOneOf(RECEIPT_STAGE_RESULTS, v.candidate) && isOneOf(RECEIPT_STAGE_RESULTS, v.pristine) && isOneOf(RECEIPT_INTEGRITY_RESULTS, v.integrity);
16244
+ }
16245
+ function isBinding2(v) {
16246
+ if (!isRecord5(v)) return false;
16247
+ return BINDING_INPUTS.every((k) => {
16248
+ const val = v[k];
16249
+ return typeof val === "string" && val.length > 0;
16250
+ });
16251
+ }
16252
+ function classifyReceipt(value) {
16253
+ if (value === null || value === void 0) {
16254
+ return { disposition: "ABSENT", detail: "no local verification receipt was provided" };
16255
+ }
16256
+ if (!isRecord5(value)) {
16257
+ return { disposition: "UNKNOWN_SCHEMA", detail: "receipt is not a JSON object" };
16258
+ }
16259
+ if (value.schema_version !== RECEIPT_SCHEMA_VERSION) {
16260
+ return {
16261
+ disposition: "UNKNOWN_SCHEMA",
16262
+ detail: `unrecognised receipt schema_version ${JSON.stringify(value.schema_version)} (this build understands ${RECEIPT_SCHEMA_VERSION})`
16263
+ };
16264
+ }
16265
+ if (value.verdict !== "VERIFIED") {
16266
+ return { disposition: "MALFORMED", detail: "receipt verdict is not VERIFIED" };
16267
+ }
16268
+ if (typeof value.tw_version !== "string" || typeof value.verified_at !== "string") {
16269
+ return { disposition: "MALFORMED", detail: "receipt is missing tw_version or verified_at" };
16270
+ }
16271
+ if (!isBinding2(value.binding)) {
16272
+ return { disposition: "MALFORMED", detail: "receipt binding is incomplete or malformed" };
16273
+ }
16274
+ if (!isStages(value.stages)) {
16275
+ return { disposition: "MALFORMED", detail: "receipt stages are incomplete or malformed" };
16276
+ }
16277
+ if (value.stages.candidate !== "PASS" || value.stages.pristine !== "PASS" || value.stages.integrity !== "CLEAN") {
16278
+ return {
16279
+ disposition: "MALFORMED",
16280
+ detail: "receipt verdict is VERIFIED but a stage result is not a clean pass"
16281
+ };
16282
+ }
16283
+ if (typeof value.evidence_digest !== "string") {
16284
+ return { disposition: "MALFORMED", detail: "receipt evidence_digest is missing" };
16285
+ }
16286
+ const b = value.binding;
16287
+ const binding = {
16288
+ tree: b.tree,
16289
+ head: b.head,
16290
+ base: b.base,
16291
+ policy: b.policy,
16292
+ verifier: b.verifier,
16293
+ surface: b.surface,
16294
+ intervention: b.intervention,
16295
+ dependencies: b.dependencies
16296
+ };
16297
+ const stages = {
16298
+ candidate: value.stages.candidate,
16299
+ pristine: value.stages.pristine,
16300
+ integrity: value.stages.integrity
16301
+ };
16302
+ const expected = receiptEvidenceDigest({
16303
+ schema_version: RECEIPT_SCHEMA_VERSION,
16304
+ verdict: "VERIFIED",
16305
+ binding,
16306
+ stages
16307
+ });
16308
+ if (expected !== value.evidence_digest) {
16309
+ return { disposition: "MALFORMED", detail: "receipt evidence_digest does not match its own contents (tampered)" };
16310
+ }
16311
+ return {
16312
+ disposition: "PRESENT",
16313
+ receipt: {
16314
+ schema_version: RECEIPT_SCHEMA_VERSION,
16315
+ verdict: "VERIFIED",
16316
+ tw_version: value.tw_version,
16317
+ verified_at: value.verified_at,
16318
+ binding,
16319
+ stages,
16320
+ evidence_digest: value.evidence_digest
16321
+ }
16322
+ };
16323
+ }
16324
+ function reconcile(ci, claimed) {
16325
+ const base = {
16326
+ result: ci.verdict,
16327
+ // CI is the sole authority for the verdict — always.
16328
+ agreement: "NO_CLAIM",
16329
+ applicable: false,
16330
+ local: { disposition: claimed.disposition },
16331
+ ci: { verdict: ci.verdict },
16332
+ divergence: [],
16333
+ environment_divergence: []
16334
+ };
16335
+ if (claimed.disposition !== "PRESENT") {
16336
+ base.local.detail = claimed.detail;
16337
+ if (claimed.disposition === "ABSENT") {
16338
+ base.agreement = "NO_CLAIM";
16339
+ } else {
16340
+ base.agreement = "NON_APPLICABLE";
16341
+ base.divergence.push(`local receipt ${claimed.disposition === "UNKNOWN_SCHEMA" ? "has an unknown schema/version" : "is malformed or tampered"} \u2014 ignored as evidence`);
16342
+ }
16343
+ return base;
16344
+ }
16345
+ const receipt = claimed.receipt;
16346
+ base.local.verdict = receipt.verdict;
16347
+ base.local.verified_at = receipt.verified_at;
16348
+ if (!ci.binding) {
16349
+ base.agreement = "NON_APPLICABLE";
16350
+ base.divergence.push(`CI could not independently compute the candidate identity${ci.binding_error ? ` (${ci.binding_error})` : ""}; the receipt cannot be applied`);
16351
+ return base;
16352
+ }
16353
+ base.environment_divergence = environmentInputDivergences(receipt.binding, ci.binding);
16354
+ const mismatch = firstCandidateIdentityMismatch(receipt.binding, ci.binding);
16355
+ if (mismatch) {
16356
+ base.agreement = "NON_APPLICABLE";
16357
+ base.mismatched_input = mismatch;
16358
+ base.divergence.push(`receipt binds a different \`${mismatch}\` than CI computed \u2014 the receipt describes another state`);
16359
+ return base;
16360
+ }
16361
+ if (ci.adjudicated_tree === void 0) {
16362
+ base.agreement = "NON_APPLICABLE";
16363
+ base.mismatched_input = "tree";
16364
+ base.divergence.push(
16365
+ "CI verify result predates adjudicated-tree reporting \u2014 cannot confirm CI ran this tree; treating as non-applicable"
16366
+ );
16367
+ return base;
16368
+ }
16369
+ if (ci.adjudicated_tree !== receipt.binding.tree) {
16370
+ base.agreement = "NON_APPLICABLE";
16371
+ base.mismatched_input = "tree";
16372
+ base.divergence.push(
16373
+ "CI adjudicated the merge result, not this tree \u2014 bring the branch up to date to reconcile"
16374
+ );
16375
+ return base;
16376
+ }
16377
+ for (const env of base.environment_divergence) {
16378
+ base.divergence.push(`informational: ${STALE_REASON[env]} (machine-local; not load-bearing for cross-machine applicability)`);
16379
+ }
16380
+ base.applicable = true;
16381
+ if (ci.verdict === "VERIFIED") {
16382
+ base.agreement = "AGREE";
16383
+ return base;
16384
+ }
16385
+ base.agreement = "DIVERGENCE";
16386
+ base.divergence.push(`local claim VERIFIED, CI ${ci.verdict}`);
16387
+ if (receipt.stages.pristine === "PASS" && ci.verdict === "MASKED_FAILURE") {
16388
+ base.divergence.push("local pristine: PASS / CI pristine: FAIL");
16389
+ }
16390
+ return base;
16391
+ }
16392
+ var RECEIPT_SCHEMA_VERSION, RECEIPT_STAGE_RESULTS, RECEIPT_INTEGRITY_RESULTS;
16393
+ var init_verification_receipt = __esm({
16394
+ "src/verification-receipt.ts"() {
16395
+ "use strict";
16396
+ init_repo_context();
16397
+ init_verification_state();
16398
+ init_machine_output();
16399
+ RECEIPT_SCHEMA_VERSION = 1;
16400
+ RECEIPT_STAGE_RESULTS = ["PASS", "FAIL"];
16401
+ RECEIPT_INTEGRITY_RESULTS = ["CLEAN", "CHANGED"];
16093
16402
  }
16094
16403
  });
16095
16404
 
16096
16405
  // src/cli/verify.ts
16097
16406
  import { execFileSync as execFileSync9, spawnSync as spawnSync3 } from "node:child_process";
16098
- import { createHash as createHash13 } from "node:crypto";
16099
- import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync8, symlinkSync, writeFileSync as writeFileSync10, existsSync as existsSync14 } from "node:fs";
16407
+ import { createHash as createHash14 } from "node:crypto";
16408
+ import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync9, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, readlinkSync as readlinkSync3, rmSync as rmSync9, symlinkSync, writeFileSync as writeFileSync11, existsSync as existsSync14 } from "node:fs";
16100
16409
  import { tmpdir as tmpdir4 } from "node:os";
16101
- import { dirname as dirname9, isAbsolute as isAbsolute11, join as join20, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
16410
+ import { dirname as dirname10, isAbsolute as isAbsolute11, join as join21, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
16102
16411
  function oracleAssuranceReport() {
16103
16412
  return {
16104
16413
  level: "suite-exit-only",
@@ -16162,15 +16471,15 @@ function rootedLinkTarget(target) {
16162
16471
  return isAbsolute11(target) || driveRelativeLinkTarget(target);
16163
16472
  }
16164
16473
  function safeSymlink(target, out3, root, label) {
16165
- if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname9(out3), target))) {
16474
+ if (rootedLinkTarget(target) || !inside2(root, resolve9(dirname10(out3), target))) {
16166
16475
  throw linkEscape(label, target);
16167
16476
  }
16168
- rmSync8(out3, { force: true });
16477
+ rmSync9(out3, { force: true });
16169
16478
  symlinkSync(target, out3);
16170
16479
  }
16171
16480
  function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
16172
16481
  let domain = "tree";
16173
- let stack = linkParts(relative6(root, dirname9(out3)));
16482
+ let stack = linkParts(relative6(root, dirname10(out3)));
16174
16483
  const pending = linkParts(target);
16175
16484
  const seen = /* @__PURE__ */ new Set();
16176
16485
  let hops = 0;
@@ -16187,7 +16496,7 @@ function validateSymlinkGraph(target, out3, root, dependencyRoot, label) {
16187
16496
  continue;
16188
16497
  }
16189
16498
  const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
16190
- const current = join20(base, ...stack);
16499
+ const current = join21(base, ...stack);
16191
16500
  let st;
16192
16501
  try {
16193
16502
  st = lstatSync9(current);
@@ -16229,7 +16538,7 @@ function rejectLinkedParent(cwd, rel) {
16229
16538
  const parts = rel.split("/").slice(0, -1);
16230
16539
  let current = cwd;
16231
16540
  for (const part of parts) {
16232
- current = join20(current, part);
16541
+ current = join21(current, part);
16233
16542
  let st;
16234
16543
  try {
16235
16544
  st = lstatSync9(current);
@@ -16254,7 +16563,7 @@ function materialize(cwd, dest, dependencyRoot) {
16254
16563
  }
16255
16564
  const links = [];
16256
16565
  for (const rel of listed) {
16257
- const src = join20(cwd, rel);
16566
+ const src = join21(cwd, rel);
16258
16567
  rejectLinkedParent(cwd, rel);
16259
16568
  let st;
16260
16569
  try {
@@ -16262,8 +16571,8 @@ function materialize(cwd, dest, dependencyRoot) {
16262
16571
  } catch {
16263
16572
  continue;
16264
16573
  }
16265
- const out3 = join20(dest, rel);
16266
- mkdirSync9(dirname9(out3), { recursive: true });
16574
+ const out3 = join21(dest, rel);
16575
+ mkdirSync10(dirname10(out3), { recursive: true });
16267
16576
  dropSymlink(out3);
16268
16577
  if (st.isSymbolicLink()) {
16269
16578
  const target = readlinkSync3(src);
@@ -16282,19 +16591,19 @@ function materialize(cwd, dest, dependencyRoot) {
16282
16591
  for (const link of links) {
16283
16592
  validateSymlinkGraph(link.target, link.out, dest, dependencyRoot, link.rel);
16284
16593
  }
16285
- if (dependencyRoot) symlinkSync(dependencyRoot, join20(dest, "node_modules"), "dir");
16594
+ if (dependencyRoot) symlinkSync(dependencyRoot, join21(dest, "node_modules"), "dir");
16286
16595
  }
16287
16596
  function workingTreePaths(cwd) {
16288
16597
  return git3(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
16289
16598
  }
16290
16599
  function probeFilesystemCaseSensitivity(dir) {
16291
- const upper = join20(dir, ".tw-case-probe-A");
16292
- const lower = join20(dir, ".tw-case-probe-a");
16600
+ const upper = join21(dir, ".tw-case-probe-A");
16601
+ const lower = join21(dir, ".tw-case-probe-a");
16293
16602
  try {
16294
- writeFileSync10(upper, "", { flag: "wx" });
16603
+ writeFileSync11(upper, "", { flag: "wx" });
16295
16604
  return !existsSync14(lower);
16296
16605
  } finally {
16297
- rmSync8(upper, { force: true });
16606
+ rmSync9(upper, { force: true });
16298
16607
  }
16299
16608
  }
16300
16609
  function foldCase(p) {
@@ -16312,7 +16621,7 @@ function caseFoldCollision(paths) {
16312
16621
  }
16313
16622
  function dropSymlink(p) {
16314
16623
  try {
16315
- if (lstatSync9(p).isSymbolicLink()) rmSync8(p, { force: true });
16624
+ if (lstatSync9(p).isSymbolicLink()) rmSync9(p, { force: true });
16316
16625
  } catch {
16317
16626
  }
16318
16627
  }
@@ -16348,11 +16657,11 @@ function baseEntries(base, cwd) {
16348
16657
  });
16349
16658
  }
16350
16659
  function overlayDigest(dest, paths) {
16351
- const h = createHash13("sha256");
16660
+ const h = createHash14("sha256");
16352
16661
  for (const rel of [...paths].sort()) {
16353
16662
  h.update(rel);
16354
16663
  h.update("\0");
16355
- const p = join20(dest, rel);
16664
+ const p = join21(dest, rel);
16356
16665
  try {
16357
16666
  const st = lstatSync9(p);
16358
16667
  h.update(String(st.mode));
@@ -16386,15 +16695,15 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
16386
16695
  throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
16387
16696
  }
16388
16697
  const content = execFileSync9("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
16389
- const out3 = join20(dest, e.path);
16390
- mkdirSync9(dirname9(out3), { recursive: true });
16391
- rmSync8(out3, { force: true });
16698
+ const out3 = join21(dest, e.path);
16699
+ mkdirSync10(dirname10(out3), { recursive: true });
16700
+ rmSync9(out3, { force: true });
16392
16701
  if (e.mode === "120000") {
16393
16702
  const target = content.toString("utf8");
16394
16703
  safeSymlink(target, out3, dest, `the base's ${e.path}`);
16395
16704
  restoredLinks.push({ path: e.path, out: out3, target });
16396
16705
  } else {
16397
- writeFileSync10(out3, content);
16706
+ writeFileSync11(out3, content);
16398
16707
  chmodSync2(out3, parseInt(e.mode.slice(-4), 8) & 511);
16399
16708
  }
16400
16709
  restored.push(e.path);
@@ -16403,7 +16712,7 @@ function overlayPristine(cwd, base, dest, policy, cmd, dependencyRoot, caseSensi
16403
16712
  const inCopy = workingTreePaths(cwd);
16404
16713
  for (const rel of inCopy) {
16405
16714
  if (!isOverlay(rel) || baseProtected.has(fold(rel))) continue;
16406
- rmSync8(join20(dest, rel), { force: true });
16715
+ rmSync9(join21(dest, rel), { force: true });
16407
16716
  removed++;
16408
16717
  }
16409
16718
  for (const link of restoredLinks) {
@@ -16417,9 +16726,9 @@ function suiteEnv(scratch) {
16417
16726
  if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
16418
16727
  env[k] = v;
16419
16728
  }
16420
- const userRc = join20(scratch, "npmrc-user");
16421
- const globalRc = join20(scratch, "npmrc-global");
16422
- for (const f of [userRc, globalRc]) writeFileSync10(f, "", { mode: 292 });
16729
+ const userRc = join21(scratch, "npmrc-user");
16730
+ const globalRc = join21(scratch, "npmrc-global");
16731
+ for (const f of [userRc, globalRc]) writeFileSync11(f, "", { mode: 292 });
16423
16732
  env.npm_config_userconfig = userRc;
16424
16733
  env.npm_config_globalconfig = globalRc;
16425
16734
  env.npm_config_node_options = " ";
@@ -16450,7 +16759,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
16450
16759
  reason: `checkpointed-local verifier is unsupported on ${process.platform}; no trusted local shell contract is defined`
16451
16760
  };
16452
16761
  }
16453
- const scratch = mkdtempSync3(join20(tmpdir4(), "tw-verify-run-"));
16762
+ const scratch = mkdtempSync3(join21(tmpdir4(), "tw-verify-run-"));
16454
16763
  try {
16455
16764
  const r = runCapturedProcessSync(shell.executable, shell.args, {
16456
16765
  cwd: dir,
@@ -16495,7 +16804,7 @@ function runLocalSuite(dir, cmd, budgetSecs) {
16495
16804
  diagnostics: r.diagnostics
16496
16805
  };
16497
16806
  } finally {
16498
- rmSync8(scratch, { recursive: true, force: true });
16807
+ rmSync9(scratch, { recursive: true, force: true });
16499
16808
  }
16500
16809
  }
16501
16810
  function diagnosticsJson(diagnostics, includeTail) {
@@ -16696,17 +17005,17 @@ function runVerifyImpl(opts) {
16696
17005
  } catch {
16697
17006
  return cannotVerify("BASE_UNRESOLVABLE", `cannot resolve base rev "${opts.base ?? "HEAD"}"`);
16698
17007
  }
16699
- const visRoot = mkdtempSync3(join20(tmpdir4(), "tw-verify-vis-"));
16700
- const visDir = join20(visRoot, "t");
17008
+ const visRoot = mkdtempSync3(join21(tmpdir4(), "tw-verify-vis-"));
17009
+ const visDir = join21(visRoot, "t");
16701
17010
  const cleanup = (dirs) => {
16702
17011
  if (opts.keep) return;
16703
- for (const d of dirs) rmSync8(d, { recursive: true, force: true });
17012
+ for (const d of dirs) rmSync9(d, { recursive: true, force: true });
16704
17013
  };
16705
17014
  filesystemCaseSensitive = (opts.probeCaseSensitivity ?? probeFilesystemCaseSensitivity)(visRoot);
16706
17015
  if (!filesystemCaseSensitive) {
16707
17016
  const present = (rel) => {
16708
17017
  try {
16709
- lstatSync9(join20(cwd, rel));
17018
+ lstatSync9(join21(cwd, rel));
16710
17019
  return true;
16711
17020
  } catch {
16712
17021
  return false;
@@ -16738,7 +17047,7 @@ function runVerifyImpl(opts) {
16738
17047
  );
16739
17048
  }
16740
17049
  try {
16741
- mkdirSync9(visDir);
17050
+ mkdirSync10(visDir);
16742
17051
  materialize(cwd, visDir, frozenNodeModules);
16743
17052
  } catch (e) {
16744
17053
  cleanup([visRoot]);
@@ -16839,12 +17148,12 @@ function runVerifyImpl(opts) {
16839
17148
  { stage: "visible", dependency_environment: dependencyReport(), verifier_backend: backendReport(), oracle_assurance: oracleAssuranceReport() }
16840
17149
  );
16841
17150
  }
16842
- const priRoot = mkdtempSync3(join20(tmpdir4(), "tw-verify-pri-"));
16843
- const priDir = join20(priRoot, "t");
17151
+ const priRoot = mkdtempSync3(join21(tmpdir4(), "tw-verify-pri-"));
17152
+ const priDir = join21(priRoot, "t");
16844
17153
  let restored = [];
16845
17154
  let removedAdded = 0;
16846
17155
  try {
16847
- mkdirSync9(priDir);
17156
+ mkdirSync10(priDir);
16848
17157
  materialize(cwd, priDir, frozenNodeModules);
16849
17158
  ({ restored, removed: removedAdded } = overlayPristine(
16850
17159
  cwd,
@@ -16941,11 +17250,13 @@ function runVerifyImpl(opts) {
16941
17250
  budget
16942
17251
  };
16943
17252
  recordVerification(cwd, inputs, treeBefore);
17253
+ const written = readVerificationRecord(cwd);
17254
+ if (written) storeReceipt(cwd, receiptFromRecord(written));
16944
17255
  } catch {
16945
17256
  }
16946
17257
  } else {
16947
17258
  try {
16948
- invalidateVerificationRecordIfCurrent(cwd);
17259
+ if (invalidateVerificationRecordIfCurrent(cwd)) removeStoredReceipt(cwd);
16949
17260
  } catch {
16950
17261
  }
16951
17262
  }
@@ -16957,6 +17268,15 @@ function runVerifyImpl(opts) {
16957
17268
  schema_version: MACHINE_SCHEMA_VERSION,
16958
17269
  verdict: verdict2,
16959
17270
  base,
17271
+ // The candidate tree fingerprint this run actually ADJUDICATED — the same
17272
+ // identity `verify` binds into the #600 record (`treeBefore`). Additive
17273
+ // (verify-v1 is `additionalProperties: true`), it lets `receipt reconcile`
17274
+ // tell whether CI's verdict is about the receipt's tree or a different one:
17275
+ // in a `pull_request` run the enforcement verify runs over the MERGE result,
17276
+ // so when the branch is behind its base this differs from the branch-tip
17277
+ // tree a receipt binds, and reconcile must report NON_APPLICABLE rather than
17278
+ // attribute CI's verdict to a tree CI never ran (#601 re-review).
17279
+ adjudicated_tree: treeBefore,
16960
17280
  command: cmd,
16961
17281
  budget_secs: budget,
16962
17282
  visible: stageJson(visible),
@@ -17055,6 +17375,7 @@ var init_verify = __esm({
17055
17375
  init_machine_output();
17056
17376
  init_signoff();
17057
17377
  init_verification_state();
17378
+ init_verification_receipt();
17058
17379
  init_suite_diagnostics();
17059
17380
  init_text();
17060
17381
  init_status();
@@ -17153,9 +17474,9 @@ var init_verify = __esm({
17153
17474
  // src/cli/run.ts
17154
17475
  import { execFileSync as execFileSync10, spawn, spawnSync as spawnSync4 } from "node:child_process";
17155
17476
  import { randomBytes as randomBytes3 } from "node:crypto";
17156
- import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync9, statSync as statSync5, writeFileSync as writeFileSync11 } from "node:fs";
17477
+ import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync8, rmSync as rmSync10, statSync as statSync5, writeFileSync as writeFileSync12 } from "node:fs";
17157
17478
  import { tmpdir as tmpdir5 } from "node:os";
17158
- import { dirname as dirname10, join as join21, resolve as resolve10 } from "node:path";
17479
+ import { dirname as dirname11, join as join22, resolve as resolve10 } from "node:path";
17159
17480
  function authoritativeRunLifecyclePlatform(platform = process.platform) {
17160
17481
  return platform === "linux";
17161
17482
  }
@@ -17192,7 +17513,7 @@ function writableByCaller2(path) {
17192
17513
  return true;
17193
17514
  } catch {
17194
17515
  }
17195
- const parent = dirname10(cur);
17516
+ const parent = dirname11(cur);
17196
17517
  if (parent === cur) break;
17197
17518
  cur = parent;
17198
17519
  }
@@ -17231,9 +17552,9 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
17231
17552
  };
17232
17553
  }
17233
17554
  function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecycleTestMode, machineMode = false) {
17234
- const stateDir2 = mkdtempSync4(join21(tmpdir5(), "tw-agent-supervisor-"));
17235
- const resultFile = join21(stateDir2, "result.json");
17236
- const agentEnvFile = join21(stateDir2, "agent-env.json");
17555
+ const stateDir2 = mkdtempSync4(join22(tmpdir5(), "tw-agent-supervisor-"));
17556
+ const resultFile = join22(stateDir2, "result.json");
17557
+ const agentEnvFile = join22(stateDir2, "agent-env.json");
17237
17558
  try {
17238
17559
  const linux = process.platform === "linux";
17239
17560
  let executable;
@@ -17250,7 +17571,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
17250
17571
  failure: trustedPython.reason ?? "trusted Linux python3 interpreter is unavailable"
17251
17572
  };
17252
17573
  }
17253
- writeFileSync11(agentEnvFile, JSON.stringify(process.env), { mode: 384 });
17574
+ writeFileSync12(agentEnvFile, JSON.stringify(process.env), { mode: 384 });
17254
17575
  executable = trustedPython.path;
17255
17576
  args = [
17256
17577
  "-I",
@@ -17332,7 +17653,7 @@ function runAgentSupervised(argv, cwd, budgetSecs, linuxPythonCandidates, lifecy
17332
17653
  ...state.failure ? { failure: state.failure } : {}
17333
17654
  };
17334
17655
  } finally {
17335
- rmSync9(stateDir2, { recursive: true, force: true });
17656
+ rmSync10(stateDir2, { recursive: true, force: true });
17336
17657
  }
17337
17658
  }
17338
17659
  function waitMs(ms) {
@@ -17359,8 +17680,8 @@ function observerExited(pid) {
17359
17680
  }
17360
17681
  }
17361
17682
  function supervisedObserverLog(cwd) {
17362
- const gd = gitDir(cwd) ?? join21(cwd, ".git");
17363
- return join21(
17683
+ const gd = gitDir(cwd) ?? join22(cwd, ".git");
17684
+ return join22(
17364
17685
  gd,
17365
17686
  "tamperward",
17366
17687
  `run-observer-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}.jsonl`
@@ -18070,7 +18391,7 @@ raise SystemExit(0)
18070
18391
  // src/cli/doctor.ts
18071
18392
  import { execFileSync as execFileSync11 } from "node:child_process";
18072
18393
  import { existsSync as existsSync15, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
18073
- import { join as join22, resolve as resolve11 } from "node:path";
18394
+ import { join as join23, resolve as resolve11 } from "node:path";
18074
18395
  function lifecyclePlatformCheck(platform = process.platform, linuxPython = platform === "linux" ? trustedLinuxPython() : null) {
18075
18396
  if (platform === "linux") {
18076
18397
  if (linuxPython?.path) {
@@ -18268,9 +18589,9 @@ function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow =
18268
18589
  addWiring("codeowners", "codeowners", true);
18269
18590
  checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
18270
18591
  const pins = [
18271
- ...pinsInFile(join22(cwd, ".claude", "settings.json")),
18272
- ...pinsInFile(join22(cwd, ".git", "hooks", "pre-commit")),
18273
- ...pinsInFile(join22(cwd, ".husky", "pre-commit"))
18592
+ ...pinsInFile(join23(cwd, ".claude", "settings.json")),
18593
+ ...pinsInFile(join23(cwd, ".git", "hooks", "pre-commit")),
18594
+ ...pinsInFile(join23(cwd, ".husky", "pre-commit"))
18274
18595
  ];
18275
18596
  const below = pins.filter((pin) => (compareVersions(pin, TW_VERSION) ?? 0) < 0);
18276
18597
  const above = pins.filter((pin) => (compareVersions(TW_VERSION, pin) ?? 0) < 0);
@@ -18566,7 +18887,7 @@ function diagnose(opts = {}) {
18566
18887
  `${workflowDirRel}: could not enumerate workflows (${e instanceof Error ? e.message : String(e)})`
18567
18888
  );
18568
18889
  }
18569
- for (const name of entries) workflowRels.push(join22(workflowDirRel, name));
18890
+ for (const name of entries) workflowRels.push(join23(workflowDirRel, name));
18570
18891
  }
18571
18892
  const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
18572
18893
  const requiredMinutes = Math.ceil(requiredSecs / 60);
@@ -18895,11 +19216,11 @@ import {
18895
19216
  readFileSync as readFileSync19,
18896
19217
  readdirSync as readdirSync6,
18897
19218
  realpathSync as realpathSync9,
18898
- rmSync as rmSync10,
19219
+ rmSync as rmSync11,
18899
19220
  symlinkSync as symlinkSync2
18900
19221
  } from "node:fs";
18901
19222
  import { tmpdir as tmpdir6 } from "node:os";
18902
- import { isAbsolute as isAbsolute12, join as join23, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
19223
+ import { isAbsolute as isAbsolute12, join as join24, relative as relative7, resolve as resolve12, sep as sep5 } from "node:path";
18903
19224
  function unescapeStraceString(s) {
18904
19225
  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, "\\");
18905
19226
  }
@@ -19147,8 +19468,8 @@ function materializeBase(base, cwd, dest) {
19147
19468
  `tar extraction failed${untar.stderr?.length ? `: ${String(untar.stderr).trim()}` : ""}`
19148
19469
  );
19149
19470
  }
19150
- const hostModules = join23(cwd, "node_modules");
19151
- const tracedModules = join23(dest, "node_modules");
19471
+ const hostModules = join24(cwd, "node_modules");
19472
+ const tracedModules = join24(dest, "node_modules");
19152
19473
  if (existsSync16(hostModules) && !existsSync16(tracedModules)) {
19153
19474
  symlinkSync2(resolve12(hostModules), tracedModules, "dir");
19154
19475
  }
@@ -19156,7 +19477,7 @@ function materializeBase(base, cwd, dest) {
19156
19477
  function traceFiles(prefix) {
19157
19478
  const dir = resolve12(prefix, "..");
19158
19479
  const base = prefix.split(sep5).at(-1) ?? prefix;
19159
- return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join23(dir, name)).sort();
19480
+ return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join24(dir, name)).sort();
19160
19481
  }
19161
19482
  function rewriteTraceRoot(accesses, actualRoot) {
19162
19483
  return accesses.map((item) => {
@@ -19171,13 +19492,13 @@ function rewriteTraceRoot(accesses, actualRoot) {
19171
19492
  if (!inside3(actualRoot, real)) return { ...item, path: real };
19172
19493
  } catch {
19173
19494
  }
19174
- return { ...item, path: join23(TRACE_ROOT, ...rel.split("/")) };
19495
+ return { ...item, path: join24(TRACE_ROOT, ...rel.split("/")) };
19175
19496
  });
19176
19497
  }
19177
19498
  function straceOnce(root, command, budget) {
19178
- const traceDir = mkdtempSync5(join23(tmpdir6(), "tw-trace-log-"));
19499
+ const traceDir = mkdtempSync5(join24(tmpdir6(), "tw-trace-log-"));
19179
19500
  try {
19180
- const prefix = join23(traceDir, "trace");
19501
+ const prefix = join24(traceDir, "trace");
19181
19502
  const traced = spawnSync5(
19182
19503
  "strace",
19183
19504
  [
@@ -19217,26 +19538,26 @@ function straceOnce(root, command, budget) {
19217
19538
  };
19218
19539
  return { raw, accesses };
19219
19540
  } finally {
19220
- rmSync10(traceDir, { recursive: true, force: true });
19541
+ rmSync11(traceDir, { recursive: true, force: true });
19221
19542
  }
19222
19543
  }
19223
19544
  function runOneTrace(cwd, base, command, budget) {
19224
- const root = mkdtempSync5(join23(tmpdir6(), "tw-trace-base-"));
19545
+ const root = mkdtempSync5(join24(tmpdir6(), "tw-trace-base-"));
19225
19546
  try {
19226
19547
  materializeBase(base, cwd, root);
19227
19548
  const { raw, accesses } = straceOnce(root, command, budget);
19228
19549
  return { outcome: classifyTraceRun(raw), accesses: rewriteTraceRoot(accesses, root) };
19229
19550
  } finally {
19230
- rmSync10(root, { recursive: true, force: true });
19551
+ rmSync11(root, { recursive: true, force: true });
19231
19552
  }
19232
19553
  }
19233
19554
  function tracerPreflight(budget = 10) {
19234
- const probeRoot = mkdtempSync5(join23(tmpdir6(), "tw-trace-probe-"));
19555
+ const probeRoot = mkdtempSync5(join24(tmpdir6(), "tw-trace-probe-"));
19235
19556
  try {
19236
19557
  const { raw } = straceOnce(probeRoot, "true", Math.max(1, Math.min(budget, 10)));
19237
19558
  return classifyTraceRun(raw);
19238
19559
  } finally {
19239
- rmSync10(probeRoot, { recursive: true, force: true });
19560
+ rmSync11(probeRoot, { recursive: true, force: true });
19240
19561
  }
19241
19562
  }
19242
19563
  function renderText2(report2) {
@@ -19439,25 +19760,25 @@ var init_trace_verify = __esm({
19439
19760
 
19440
19761
  // src/runtimes.ts
19441
19762
  import { existsSync as existsSync17, readFileSync as readFileSync20 } from "node:fs";
19442
- import { join as join24 } from "node:path";
19443
- function isRecord5(x) {
19763
+ import { join as join25 } from "node:path";
19764
+ function isRecord6(x) {
19444
19765
  return typeof x === "object" && x !== null && !Array.isArray(x);
19445
19766
  }
19446
19767
  function hooksBeyondTamperward(hooks) {
19447
- if (!isRecord5(hooks)) return hooks != null;
19768
+ if (!isRecord6(hooks)) return hooks != null;
19448
19769
  for (const entries of Object.values(hooks)) {
19449
19770
  if (!Array.isArray(entries)) {
19450
19771
  if (entries != null) return true;
19451
19772
  continue;
19452
19773
  }
19453
19774
  for (const entry of entries) {
19454
- const hs = isRecord5(entry) ? entry.hooks : null;
19775
+ const hs = isRecord6(entry) ? entry.hooks : null;
19455
19776
  if (!Array.isArray(hs)) {
19456
19777
  if (hs != null) return true;
19457
19778
  continue;
19458
19779
  }
19459
19780
  for (const h of hs) {
19460
- const cmd = isRecord5(h) ? String(h.command ?? "") : "";
19781
+ const cmd = isRecord6(h) ? String(h.command ?? "") : "";
19461
19782
  if (!/tamperward/.test(cmd)) return true;
19462
19783
  }
19463
19784
  }
@@ -19465,7 +19786,7 @@ function hooksBeyondTamperward(hooks) {
19465
19786
  return false;
19466
19787
  }
19467
19788
  function claudeSettingsBeyondTamperward(cwd) {
19468
- const path = join24(cwd, ".claude", "settings.json");
19789
+ const path = join25(cwd, ".claude", "settings.json");
19469
19790
  let raw;
19470
19791
  try {
19471
19792
  if (!existsSync17(path)) return false;
@@ -19479,7 +19800,7 @@ function claudeSettingsBeyondTamperward(cwd) {
19479
19800
  } catch {
19480
19801
  return raw.trim().length > 0;
19481
19802
  }
19482
- if (!isRecord5(parsed)) return raw.trim().length > 0;
19803
+ if (!isRecord6(parsed)) return raw.trim().length > 0;
19483
19804
  for (const [key2, value] of Object.entries(parsed)) {
19484
19805
  if (key2 === "disableAllHooks") continue;
19485
19806
  if (key2 === "hooks") {
@@ -19495,7 +19816,7 @@ function detectRuntimes(cwd) {
19495
19816
  for (const rt of KNOWN_RUNTIMES) {
19496
19817
  const marker = rt.markers.find((m) => {
19497
19818
  try {
19498
- return existsSync17(join24(cwd, m));
19819
+ return existsSync17(join25(cwd, m));
19499
19820
  } catch {
19500
19821
  return false;
19501
19822
  }
@@ -19576,9 +19897,9 @@ var init_runtimes = __esm({
19576
19897
 
19577
19898
  // src/cli/onboard.ts
19578
19899
  import { execFileSync as execFileSync13 } from "node:child_process";
19579
- import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as rmSync11 } from "node:fs";
19900
+ import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync21, realpathSync as realpathSync10, rmSync as rmSync12 } from "node:fs";
19580
19901
  import { tmpdir as tmpdir7 } from "node:os";
19581
- import { join as join25, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
19902
+ import { join as join26, relative as relative8, resolve as resolve13, sep as sep6 } from "node:path";
19582
19903
  import { createInterface } from "node:readline";
19583
19904
  import { parseDocument } from "yaml";
19584
19905
  function readlineAsker(input, output) {
@@ -19643,7 +19964,7 @@ function explainVerdict(v, code2) {
19643
19964
  }
19644
19965
  }
19645
19966
  function writeVerifyCommand(cwd, command) {
19646
- const path = join25(cwd, POLICY_FILE);
19967
+ const path = join26(cwd, POLICY_FILE);
19647
19968
  const kind = writeTargetKind(path);
19648
19969
  if (kind === "symlink" || kind === "irregular") {
19649
19970
  return `${POLICY_FILE} is not a regular file; refusing to follow or replace a symlink or special file`;
@@ -19666,7 +19987,7 @@ function writeVerifyCommand(cwd, command) {
19666
19987
  if (loaded2.verify?.command !== command) throw new Error("verify.command did not round-trip");
19667
19988
  return null;
19668
19989
  } catch (e) {
19669
- if (original === null) rmSync11(path, { force: true });
19990
+ if (original === null) rmSync12(path, { force: true });
19670
19991
  else atomicReplaceFile(path, original, mode);
19671
19992
  return `the updated ${POLICY_FILE} does not load (${errorMessage(e)}); restored the previous file`;
19672
19993
  }
@@ -20036,13 +20357,13 @@ function runDemo(cwd, head, runners, out3) {
20036
20357
  }
20037
20358
  const before = treeFingerprint(cwd);
20038
20359
  out3(`working tree fingerprint before: ${before.slice(0, 16)}`);
20039
- const wt = mkdtempSync6(join25(tmpdir7(), "tw-onboard-demo-"));
20360
+ const wt = mkdtempSync6(join26(tmpdir7(), "tw-onboard-demo-"));
20040
20361
  try {
20041
20362
  if (git6(cwd, ["worktree", "add", "--detach", wt, head]) === null) {
20042
20363
  out3("Demo skipped: could not create a temporary worktree (git worktree add failed).");
20043
20364
  return;
20044
20365
  }
20045
- const targetPath = join25(wt, target.path);
20366
+ const targetPath = join26(wt, target.path);
20046
20367
  const targetStat = lstatSync10(targetPath, { throwIfNoEntry: false });
20047
20368
  if (!targetStat || !targetStat.isFile() || targetStat.isSymbolicLink()) {
20048
20369
  out3("Demo skipped: the selected test is not a regular file in the disposable worktree.");
@@ -20073,7 +20394,7 @@ function runDemo(cwd, head, runners, out3) {
20073
20394
  } finally {
20074
20395
  git6(cwd, ["worktree", "remove", "--force", wt]);
20075
20396
  git6(cwd, ["worktree", "prune"]);
20076
- rmSync11(wt, { recursive: true, force: true });
20397
+ rmSync12(wt, { recursive: true, force: true });
20077
20398
  }
20078
20399
  const after = treeFingerprint(cwd);
20079
20400
  out3(`working tree fingerprint after: ${after.slice(0, 16)}`);
@@ -20260,9 +20581,9 @@ var init_capture = __esm({
20260
20581
  });
20261
20582
 
20262
20583
  // src/research/manifest.ts
20263
- import { createHash as createHash14 } from "node:crypto";
20584
+ import { createHash as createHash15 } from "node:crypto";
20264
20585
  import { readFileSync as readFileSync22 } from "node:fs";
20265
- import { dirname as dirname11, resolve as resolve15 } from "node:path";
20586
+ import { dirname as dirname12, resolve as resolve15 } from "node:path";
20266
20587
  function taskFrom(raw, index, manifestDir) {
20267
20588
  const where = `task #${index + 1}`;
20268
20589
  if (!isRecord(raw)) throw new ResearchError(`task manifest: ${where} is not an object`);
@@ -20312,13 +20633,13 @@ function readManifest(path) {
20312
20633
  if (!Array.isArray(raw.tasks) || raw.tasks.length === 0) {
20313
20634
  throw new ResearchError(`task manifest ${abs}: no tasks (tasks must be a non-empty array)`);
20314
20635
  }
20315
- const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname11(abs)));
20636
+ const tasks = raw.tasks.map((t, i) => taskFrom(t, i, dirname12(abs)));
20316
20637
  const seen = /* @__PURE__ */ new Set();
20317
20638
  for (const t of tasks) {
20318
20639
  if (seen.has(t.id)) throw new ResearchError(`task manifest ${abs}: duplicate task id "${t.id}"`);
20319
20640
  seen.add(t.id);
20320
20641
  }
20321
- return { path: abs, sha256: createHash14("sha256").update(bytes).digest("hex"), tasks };
20642
+ return { path: abs, sha256: createHash15("sha256").update(bytes).digest("hex"), tasks };
20322
20643
  }
20323
20644
  var MANIFEST_VERSION;
20324
20645
  var init_manifest = __esm({
@@ -20714,17 +21035,17 @@ var init_record = __esm({
20714
21035
  // src/research/run.ts
20715
21036
  import { randomUUID as randomUUID2 } from "node:crypto";
20716
21037
  import { execFileSync as execFileSync14 } from "node:child_process";
20717
- import { closeSync as closeSync4, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync10, openSync as openSync4, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync12, unlinkSync, writeFileSync as writeFileSync12, writeSync } from "node:fs";
21038
+ import { closeSync as closeSync4, existsSync as existsSync18, linkSync, mkdirSync as mkdirSync11, openSync as openSync4, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync13, unlinkSync, writeFileSync as writeFileSync13, writeSync } from "node:fs";
20718
21039
  import { hostname } from "node:os";
20719
- import { join as join26, resolve as resolve16 } from "node:path";
21040
+ import { join as join27, resolve as resolve16 } from "node:path";
20720
21041
  function git7(args, cwd) {
20721
21042
  return execFileSync14("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
20722
21043
  }
20723
21044
  function pairRecordPath(ledger, task, pair) {
20724
- return join26(ledger, "pairs", `${task}--${pair}.json`);
21045
+ return join27(ledger, "pairs", `${task}--${pair}.json`);
20725
21046
  }
20726
21047
  function researchLockPath(ledger) {
20727
- return join26(ledger, "run.lock");
21048
+ return join27(ledger, "run.lock");
20728
21049
  }
20729
21050
  function readLockOwner(path) {
20730
21051
  try {
@@ -20749,7 +21070,7 @@ function lockHolder(path) {
20749
21070
  return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
20750
21071
  }
20751
21072
  function acquireResearchLock(ledger, breakStaleLock = false) {
20752
- mkdirSync10(ledger, { recursive: true });
21073
+ mkdirSync11(ledger, { recursive: true });
20753
21074
  const path = researchLockPath(ledger);
20754
21075
  if (breakStaleLock && existsSync18(path)) {
20755
21076
  const owner2 = readLockOwner(path);
@@ -20859,16 +21180,16 @@ function resumableRecord(path, expected) {
20859
21180
  }
20860
21181
  function writeRecordAtomically(path, text) {
20861
21182
  const tmp = `${path}.${process.pid}.tmp`;
20862
- writeFileSync12(tmp, text);
21183
+ writeFileSync13(tmp, text);
20863
21184
  renameSync4(tmp, path);
20864
21185
  }
20865
21186
  function cloneArgs(repo, ws3) {
20866
21187
  return ["-c", "protocol.ext.allow=never", "clone", "-q", "--no-hardlinks", "--", repo, ws3];
20867
21188
  }
20868
21189
  function freshWorkspace(ledger, task, pair, arm, sourceBase) {
20869
- const ws3 = join26(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
20870
- rmSync12(ws3, { recursive: true, force: true });
20871
- mkdirSync10(join26(ledger, "workspaces"), { recursive: true });
21190
+ const ws3 = join27(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
21191
+ rmSync13(ws3, { recursive: true, force: true });
21192
+ mkdirSync11(join27(ledger, "workspaces"), { recursive: true });
20872
21193
  try {
20873
21194
  execFileSync14("git", cloneArgs(task.repo, ws3), { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
20874
21195
  } catch (e) {
@@ -21117,7 +21438,7 @@ function runResearch(opts) {
21117
21438
  }
21118
21439
  try {
21119
21440
  const pairs = opts.pairs ?? 1;
21120
- mkdirSync10(join26(ledger, "pairs"), { recursive: true });
21441
+ mkdirSync11(join27(ledger, "pairs"), { recursive: true });
21121
21442
  for (const task of tasks) {
21122
21443
  const existingRecords = /* @__PURE__ */ new Map();
21123
21444
  let sourceBase = null;
@@ -21245,7 +21566,7 @@ var init_run2 = __esm({
21245
21566
 
21246
21567
  // src/research/ledger-files.ts
21247
21568
  import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync2, lstatSync as lstatSync11, openSync as openSync5, readFileSync as readFileSync24, readdirSync as readdirSync7, realpathSync as realpathSync11 } from "node:fs";
21248
- import { join as join27, resolve as resolve17 } from "node:path";
21569
+ import { join as join28, resolve as resolve17 } from "node:path";
21249
21570
  function lstatOrNull(path) {
21250
21571
  try {
21251
21572
  return lstatSync11(path);
@@ -21271,7 +21592,7 @@ function readRegularFile(path, realPairsDir, name) {
21271
21592
  throw new ResearchError(`ledger record ${path} has ${listed.nlink} links; a hard link can name a file outside pairs/, so only a singly linked regular file is read`);
21272
21593
  }
21273
21594
  const real = realpathSync11.native(path);
21274
- if (real !== join27(realPairsDir, name)) {
21595
+ if (real !== join28(realPairsDir, name)) {
21275
21596
  throw new ResearchError(`ledger record ${path} resolves outside the pairs directory (${real}); refusing to read it`);
21276
21597
  }
21277
21598
  const flags = constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0);
@@ -21294,7 +21615,7 @@ function readRegularFile(path, realPairsDir, name) {
21294
21615
  }
21295
21616
  function readPairFiles(ledger) {
21296
21617
  const root = resolve17(ledger);
21297
- const pairsDir = join27(root, "pairs");
21618
+ const pairsDir = join28(root, "pairs");
21298
21619
  const dirStat = lstatOrNull(pairsDir);
21299
21620
  if (dirStat !== null && dirStat.isSymbolicLink()) {
21300
21621
  throw new ResearchError(`ledger pairs directory ${pairsDir} is a symlink; only a real directory is read`);
@@ -21308,7 +21629,7 @@ function readPairFiles(ledger) {
21308
21629
  throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
21309
21630
  }
21310
21631
  return names.map((name) => {
21311
- const path = join27(pairsDir, name);
21632
+ const path = join28(pairsDir, name);
21312
21633
  return { name, path, bytes: readRegularFile(path, realPairsDir, name) };
21313
21634
  });
21314
21635
  }
@@ -21459,9 +21780,9 @@ var init_summarize = __esm({
21459
21780
  });
21460
21781
 
21461
21782
  // src/research/init.ts
21462
- import { createHash as createHash15 } from "node:crypto";
21463
- import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
21464
- import { basename as basename5, dirname as dirname12, resolve as resolve18 } from "node:path";
21783
+ import { createHash as createHash16 } from "node:crypto";
21784
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync25, writeFileSync as writeFileSync14 } from "node:fs";
21785
+ import { basename as basename5, dirname as dirname13, resolve as resolve18 } from "node:path";
21465
21786
  function idFrom(repo) {
21466
21787
  const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
21467
21788
  return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
@@ -21489,8 +21810,8 @@ function createResearchManifest(opts) {
21489
21810
  }]
21490
21811
  };
21491
21812
  const text = JSON.stringify(doc, null, 2) + "\n";
21492
- mkdirSync11(dirname12(out3), { recursive: true });
21493
- writeFileSync13(out3, text, { flag: "wx", mode: 384 });
21813
+ mkdirSync12(dirname13(out3), { recursive: true });
21814
+ writeFileSync14(out3, text, { flag: "wx", mode: 384 });
21494
21815
  const parsed = readManifest(out3);
21495
21816
  if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
21496
21817
  return out3;
@@ -21512,7 +21833,7 @@ function runResearchInit(opts) {
21512
21833
  }
21513
21834
  }
21514
21835
  function requireHash(path) {
21515
- return createHash15("sha256").update(readFileSync25(path)).digest("hex");
21836
+ return createHash16("sha256").update(readFileSync25(path)).digest("hex");
21516
21837
  }
21517
21838
  var init_init2 = __esm({
21518
21839
  "src/research/init.ts"() {
@@ -21577,10 +21898,10 @@ var init_report2 = __esm({
21577
21898
  });
21578
21899
 
21579
21900
  // src/research/bundle.ts
21580
- import { createHash as createHash16 } from "node:crypto";
21901
+ import { createHash as createHash17 } from "node:crypto";
21581
21902
  import { gzipSync, gunzipSync } from "node:zlib";
21582
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "node:fs";
21583
- import { dirname as dirname13, resolve as resolve19 } from "node:path";
21903
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
21904
+ import { dirname as dirname14, resolve as resolve19 } from "node:path";
21584
21905
  function octal(value, width) {
21585
21906
  return value.toString(8).padStart(width - 1, "0") + "\0";
21586
21907
  }
@@ -21713,7 +22034,7 @@ function validateResearchBundle(path) {
21713
22034
  if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
21714
22035
  throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
21715
22036
  }
21716
- if (manifest && createHash16("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
22037
+ if (manifest && createHash17("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
21717
22038
  throw new ResearchError("research bundle manifest does not match its recorded sha256");
21718
22039
  }
21719
22040
  const parsedSummary = summary;
@@ -21747,7 +22068,7 @@ function createResearchBundle(opts) {
21747
22068
  const summary = summarizeRecords(records);
21748
22069
  if (opts.manifest) {
21749
22070
  const manifest = readFileSync26(resolve19(opts.manifest));
21750
- const sha3 = createHash16("sha256").update(manifest).digest("hex");
22071
+ const sha3 = createHash17("sha256").update(manifest).digest("hex");
21751
22072
  if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
21752
22073
  entries.push({ name: "manifest.json", bytes: manifest });
21753
22074
  }
@@ -21769,8 +22090,8 @@ function createResearchBundle(opts) {
21769
22090
  entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
21770
22091
  entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
21771
22092
  const out3 = resolve19(opts.out);
21772
- mkdirSync12(dirname13(out3), { recursive: true });
21773
- writeFileSync14(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
22093
+ mkdirSync13(dirname14(out3), { recursive: true });
22094
+ writeFileSync15(out3, gzipSync(makeTar(entries)), { flag: "wx", mode: 384 });
21774
22095
  return out3;
21775
22096
  }
21776
22097
  function runResearchBundle(opts) {
@@ -22782,9 +23103,9 @@ var init_adapter4 = __esm({
22782
23103
 
22783
23104
  // src/adapters/copilot-sdk/changes.ts
22784
23105
  import { execFileSync as execFileSync15 } from "node:child_process";
22785
- import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync27, rmSync as rmSync13, existsSync as existsSync20, realpathSync as realpathSync12 } from "node:fs";
23106
+ import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync16, readFileSync as readFileSync27, rmSync as rmSync14, existsSync as existsSync20, realpathSync as realpathSync12 } from "node:fs";
22786
23107
  import { tmpdir as tmpdir8 } from "node:os";
22787
- import { basename as basename6, dirname as dirname14, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join29 } from "node:path";
23108
+ import { basename as basename6, dirname as dirname15, isAbsolute as isAbsolute17, relative as relative12, resolve as resolve22, join as join30 } from "node:path";
22788
23109
  function asStr4(v) {
22789
23110
  return typeof v === "string" ? v : "";
22790
23111
  }
@@ -22802,7 +23123,7 @@ function canonicalContainedTarget(abs, root) {
22802
23123
  let dir = abs;
22803
23124
  const tail = [];
22804
23125
  while (!existsSync20(dir)) {
22805
- const parent = dirname14(dir);
23126
+ const parent = dirname15(dir);
22806
23127
  if (parent === dir) break;
22807
23128
  tail.unshift(basename6(dir));
22808
23129
  dir = parent;
@@ -22813,7 +23134,7 @@ function canonicalContainedTarget(abs, root) {
22813
23134
  } catch (e) {
22814
23135
  throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22815
23136
  }
22816
- const real = tail.length ? join29(realDir, ...tail) : realDir;
23137
+ const real = tail.length ? join30(realDir, ...tail) : realDir;
22817
23138
  const rel = relative12(realRoot, real);
22818
23139
  if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
22819
23140
  throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
@@ -22856,12 +23177,12 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
22856
23177
  const bytes = Buffer.byteLength(rawDiff);
22857
23178
  if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
22858
23179
  const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
22859
- const dir = mkdtempSync7(join29(tmpdir8(), "hf-sdk-apply-"));
23180
+ const dir = mkdtempSync7(join30(tmpdir8(), "hf-sdk-apply-"));
22860
23181
  try {
22861
- const targetPath = join29(dir, TARGET);
22862
- if (op !== "create") writeFileSync15(targetPath, before ?? "");
22863
- const patchPath = join29(dir, "change.patch");
22864
- writeFileSync15(patchPath, patch);
23182
+ const targetPath = join30(dir, TARGET);
23183
+ if (op !== "create") writeFileSync16(targetPath, before ?? "");
23184
+ const patchPath = join30(dir, "change.patch");
23185
+ writeFileSync16(patchPath, patch);
22865
23186
  const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
22866
23187
  cwd: dir,
22867
23188
  encoding: "utf8",
@@ -22880,7 +23201,7 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
22880
23201
  } catch (e) {
22881
23202
  throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
22882
23203
  } finally {
22883
- rmSync13(dir, { recursive: true, force: true });
23204
+ rmSync14(dir, { recursive: true, force: true });
22884
23205
  }
22885
23206
  }
22886
23207
  function canonicalPatch(raw, display2, isCreate) {
@@ -23329,7 +23650,7 @@ var init_evidence = __esm({
23329
23650
  });
23330
23651
 
23331
23652
  // src/runtime-qualification.ts
23332
- import { createHash as createHash17 } from "node:crypto";
23653
+ import { createHash as createHash18 } from "node:crypto";
23333
23654
  function includesKind(set, kind) {
23334
23655
  return set.includes(kind);
23335
23656
  }
@@ -23500,7 +23821,7 @@ function aggregateInLoop(assessments) {
23500
23821
  return "PARTIAL";
23501
23822
  }
23502
23823
  function sha16(s) {
23503
- return createHash17("sha256").update(s).digest("hex").slice(0, 16);
23824
+ return createHash18("sha256").update(s).digest("hex").slice(0, 16);
23504
23825
  }
23505
23826
  function capabilityHash(caps) {
23506
23827
  const stable = JSON.stringify({
@@ -23620,11 +23941,11 @@ var init_runtime_qualification = __esm({
23620
23941
 
23621
23942
  // src/cli/runtime.ts
23622
23943
  import { execFileSync as execFileSync16 } from "node:child_process";
23623
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
23624
- import { join as join30, dirname as dirname15 } from "node:path";
23944
+ import { existsSync as existsSync21, mkdirSync as mkdirSync14, readFileSync as readFileSync28, writeFileSync as writeFileSync17 } from "node:fs";
23945
+ import { join as join31, dirname as dirname16 } from "node:path";
23625
23946
  function storePath(cwd) {
23626
23947
  const dir = gitDir(cwd);
23627
- return dir ? join30(dir, "tamperward", "runtime-qualification.json") : null;
23948
+ return dir ? join31(dir, "tamperward", "runtime-qualification.json") : null;
23628
23949
  }
23629
23950
  function readStore(cwd) {
23630
23951
  const path = storePath(cwd);
@@ -23751,8 +24072,8 @@ function writeRecord(cwd, id, report2) {
23751
24072
  store.records[id] = report2;
23752
24073
  store.updated_at = report2.timestamp;
23753
24074
  try {
23754
- mkdirSync13(dirname15(path), { recursive: true });
23755
- writeFileSync16(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
24075
+ mkdirSync14(dirname16(path), { recursive: true });
24076
+ writeFileSync17(path, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
23756
24077
  return true;
23757
24078
  } catch {
23758
24079
  return false;
@@ -23787,7 +24108,7 @@ function resolveHookConfigHash(canonicalId, cwd) {
23787
24108
  return sha16(JSON.stringify(interventionWiring(cwd)));
23788
24109
  }
23789
24110
  const files = {
23790
- "github-copilot-cli": join30(cwd, ".github", "hooks", "tamperward.json")
24111
+ "github-copilot-cli": join31(cwd, ".github", "hooks", "tamperward.json")
23791
24112
  };
23792
24113
  const file = files[canonicalId];
23793
24114
  if (!file || !existsSync21(file)) return null;
@@ -24057,6 +24378,352 @@ var init_runtime = __esm({
24057
24378
  }
24058
24379
  });
24059
24380
 
24381
+ // src/cli/receipt.ts
24382
+ import { appendFileSync as appendFileSync6, readFileSync as readFileSync29, writeFileSync as writeFileSync18 } from "node:fs";
24383
+ function parseReceipt(args) {
24384
+ const [sub, ...rest] = args;
24385
+ const exportOpts = {};
24386
+ const reconcileOpts = {};
24387
+ for (let i = 0; i < rest.length; i++) {
24388
+ const a = rest[i];
24389
+ const next = () => rest[++i];
24390
+ switch (a) {
24391
+ case "--json":
24392
+ reconcileOpts.json = true;
24393
+ break;
24394
+ case "--require-ancestor":
24395
+ reconcileOpts.requireAncestor = true;
24396
+ break;
24397
+ case "--out":
24398
+ exportOpts.out = next();
24399
+ break;
24400
+ case "--cwd": {
24401
+ const v = next();
24402
+ exportOpts.cwd = v;
24403
+ reconcileOpts.cwd = v;
24404
+ break;
24405
+ }
24406
+ case "--base":
24407
+ reconcileOpts.base = next();
24408
+ break;
24409
+ case "--cmd":
24410
+ reconcileOpts.cmd = next();
24411
+ break;
24412
+ case "--budget": {
24413
+ const v = next();
24414
+ if (v !== void 0) reconcileOpts.budget = Number(v);
24415
+ break;
24416
+ }
24417
+ case "--receipt":
24418
+ reconcileOpts.receipt = next();
24419
+ break;
24420
+ case "--ci-result":
24421
+ reconcileOpts.ciResult = next();
24422
+ break;
24423
+ }
24424
+ }
24425
+ return {
24426
+ sub: isOneOf(RECEIPT_SUBCOMMANDS, sub) ? sub : void 0,
24427
+ exportOpts,
24428
+ reconcileOpts
24429
+ };
24430
+ }
24431
+ function runReceipt(args) {
24432
+ const { sub, exportOpts, reconcileOpts } = parseReceipt(args);
24433
+ if (sub === "export") return runReceiptExport(exportOpts);
24434
+ if (sub === "reconcile") return runReceiptReconcile(reconcileOpts);
24435
+ process.stderr.write(`tamperward: receipt requires a subcommand (${RECEIPT_SUBCOMMANDS.join(" | ")})
24436
+ `);
24437
+ return 2;
24438
+ }
24439
+ function reconcileInputs(opts) {
24440
+ return {
24441
+ base_ref: opts.base ?? "HEAD",
24442
+ explicit_base: opts.base !== void 0,
24443
+ command_source: opts.cmd !== void 0 ? "flag" : "policy",
24444
+ command: opts.cmd ?? "",
24445
+ budget_source: opts.budget !== void 0 ? "flag" : "policy",
24446
+ budget: opts.budget ?? 300
24447
+ };
24448
+ }
24449
+ function verdictExit(verdict2, signedOff) {
24450
+ switch (verdict2) {
24451
+ case "VERIFIED":
24452
+ return 0;
24453
+ case "MASKED_FAILURE":
24454
+ return signedOff ? 0 : 1;
24455
+ case "SUITE_RED":
24456
+ return 1;
24457
+ default:
24458
+ return 2;
24459
+ }
24460
+ }
24461
+ function needRepo(cwd, what) {
24462
+ if (repoContext(cwd)) return null;
24463
+ const why = outsideRepository(cwd) ?? "cwd is not inside a repository the gate can read";
24464
+ process.stderr.write(`tamperward: receipt ${what} needs a git repository (${why})
24465
+ `);
24466
+ return 2;
24467
+ }
24468
+ function runReceiptExport(opts = {}) {
24469
+ const cwd = opts.cwd ?? process.cwd();
24470
+ const guard = needRepo(cwd, "export");
24471
+ if (guard !== null) return guard;
24472
+ const evaluation = evaluateVerificationState(cwd);
24473
+ const record = readVerificationRecord(cwd);
24474
+ if (evaluation.state !== "CURRENT" || !record) {
24475
+ const detail = evaluation.state === "CURRENT" ? "no verification record is available to export" : `no CURRENT verification to export (state: ${evaluation.state}) \u2014 run \`tamperward verify\` first`;
24476
+ process.stderr.write(`tamperward: ${detail}
24477
+ `);
24478
+ return 2;
24479
+ }
24480
+ const receipt = receiptFromRecord(record);
24481
+ const serialized = JSON.stringify(receipt) + "\n";
24482
+ if (opts.out) {
24483
+ try {
24484
+ writeFileSync18(opts.out, serialized);
24485
+ } catch (e) {
24486
+ process.stderr.write(`tamperward: could not write receipt to ${opts.out} (${e instanceof Error ? e.message : String(e)})
24487
+ `);
24488
+ return 2;
24489
+ }
24490
+ process.stdout.write(`receipt: exported VERIFIED receipt for tree ${receipt.binding.tree.slice(0, 10)} to ${opts.out}
24491
+ `);
24492
+ } else {
24493
+ process.stdout.write(serialized);
24494
+ }
24495
+ return 0;
24496
+ }
24497
+ function ciAdjudicate(cwd, opts) {
24498
+ const inputs = reconcileInputs(opts);
24499
+ let binding = null;
24500
+ let bindingError;
24501
+ try {
24502
+ binding = computeBinding(cwd, inputs);
24503
+ } catch (e) {
24504
+ bindingError = e instanceof Error ? e.message : String(e);
24505
+ }
24506
+ if (opts.ciResult !== void 0) {
24507
+ let doc;
24508
+ try {
24509
+ doc = JSON.parse(readFileSync29(opts.ciResult, "utf8"));
24510
+ } catch (e) {
24511
+ return {
24512
+ ci: { verdict: "CANNOT_VERIFY", binding, binding_error: `unreadable CI verify result: ${e instanceof Error ? e.message : String(e)}` },
24513
+ exit: 2
24514
+ };
24515
+ }
24516
+ const check2 = readCiVerify(doc, binding, cwd, opts);
24517
+ if (check2.reason !== void 0) {
24518
+ return { ci: { verdict: "CANNOT_VERIFY", binding, binding_error: check2.reason }, exit: 2 };
24519
+ }
24520
+ const signedOff = check2.verdict === "MASKED_FAILURE" && oobToken("verify", oobFromEnv(), oobHeadFromEnv()) !== null;
24521
+ return {
24522
+ ci: {
24523
+ verdict: check2.verdict,
24524
+ binding,
24525
+ ...check2.adjudicated_tree !== void 0 ? { adjudicated_tree: check2.adjudicated_tree } : {},
24526
+ ...bindingError ? { binding_error: bindingError } : {}
24527
+ },
24528
+ exit: verdictExit(check2.verdict, signedOff)
24529
+ };
24530
+ }
24531
+ const impl = opts.runVerifyImpl ?? runVerify;
24532
+ let summary;
24533
+ const exit = impl({
24534
+ cwd,
24535
+ base: opts.base,
24536
+ cmd: opts.cmd,
24537
+ budget: opts.budget,
24538
+ requireAncestor: opts.requireAncestor,
24539
+ silent: true,
24540
+ // reconcile prints its own summary; suppress verify's stdout
24541
+ onVerdict: (s) => {
24542
+ summary = s;
24543
+ }
24544
+ });
24545
+ const verdict2 = summary?.verdict ?? "CANNOT_VERIFY";
24546
+ return {
24547
+ ci: {
24548
+ verdict: verdict2,
24549
+ binding,
24550
+ ...binding ? { adjudicated_tree: binding.tree } : {},
24551
+ ...bindingError ? { binding_error: bindingError } : {}
24552
+ },
24553
+ exit
24554
+ };
24555
+ }
24556
+ function isCommitId(v) {
24557
+ return typeof v === "string" && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(v);
24558
+ }
24559
+ function readCiVerify(doc, binding, cwd, opts) {
24560
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
24561
+ return { verdict: "CANNOT_VERIFY", reason: "CI verify result is not a JSON object" };
24562
+ }
24563
+ const d = doc;
24564
+ if (d.schema_version !== 1) {
24565
+ return { verdict: "CANNOT_VERIFY", reason: `CI verify result is not a verify --json document (schema_version ${JSON.stringify(d.schema_version)})` };
24566
+ }
24567
+ if (!isCommitId(d.base)) {
24568
+ return { verdict: "CANNOT_VERIFY", reason: "CI verify result has no resolved base commit \u2014 not a verify --json document (a receipt is not a verify result)" };
24569
+ }
24570
+ if (!isRecordObject(d.visible) || !isRecordObject(d.pristine)) {
24571
+ return { verdict: "CANNOT_VERIFY", reason: "CI verify result is missing the visible/pristine stage objects \u2014 not a verify --json document" };
24572
+ }
24573
+ if (!isOneOf(VERIFY_VERDICTS, d.verdict)) {
24574
+ return { verdict: "CANNOT_VERIFY", reason: `CI verify result has an unknown verdict ${JSON.stringify(d.verdict)}` };
24575
+ }
24576
+ if (binding) {
24577
+ const concrete = resolveConcreteCommit(cwd, opts.base ?? "HEAD");
24578
+ const accepted = /* @__PURE__ */ new Set([binding.base, ...concrete ? [concrete] : []]);
24579
+ if (!accepted.has(d.base)) {
24580
+ return {
24581
+ verdict: "CANNOT_VERIFY",
24582
+ reason: `CI verify result binds base ${d.base.slice(0, 10)} but CI computed ${binding.base.slice(0, 10)} \u2014 the document describes a different base`
24583
+ };
24584
+ }
24585
+ }
24586
+ const adjudicated_tree = typeof d.adjudicated_tree === "string" && d.adjudicated_tree.length > 0 ? d.adjudicated_tree : void 0;
24587
+ return { verdict: d.verdict, adjudicated_tree };
24588
+ }
24589
+ function isRecordObject(v) {
24590
+ return typeof v === "object" && v !== null && !Array.isArray(v);
24591
+ }
24592
+ function reconcileDocument(r) {
24593
+ return machineOutput({
24594
+ command: "reconcile",
24595
+ result: r.result,
24596
+ ci: r.ci,
24597
+ local: r.local,
24598
+ reconciliation: {
24599
+ agreement: r.agreement,
24600
+ applicable: r.applicable,
24601
+ ...r.mismatched_input ? { mismatched_input: r.mismatched_input } : {},
24602
+ divergence: r.divergence,
24603
+ environment_divergence: r.environment_divergence
24604
+ }
24605
+ });
24606
+ }
24607
+ function renderReconcile(r, claimedReceipt, ciBinding, adjudicatedTree, colour) {
24608
+ const tick = (s) => paint(s, severityColour("ok"), colour);
24609
+ const cross = (s) => paint(s, BOLD + severityColour("bad"), colour);
24610
+ const stageMark = (value, pass) => value === pass ? tick("\u2713") : cross("\u2717");
24611
+ const lines = [];
24612
+ lines.push("LOCAL");
24613
+ if (claimedReceipt) {
24614
+ lines.push(` ${stageMark(claimedReceipt.stages.candidate, "PASS")} candidate ${claimedReceipt.binding.tree.slice(0, 10)} locally verified`);
24615
+ lines.push(` ${stageMark(claimedReceipt.stages.pristine, "PASS")} pristine ${claimedReceipt.stages.pristine}`);
24616
+ lines.push(` ${stageMark(claimedReceipt.stages.integrity, "CLEAN")} integrity ${claimedReceipt.stages.integrity}`);
24617
+ } else if (r.local.disposition === "ABSENT") {
24618
+ lines.push(" (no receipt provided)");
24619
+ } else {
24620
+ lines.push(` ${cross("\u2717")} receipt ${r.local.disposition}${r.local.detail ? ` \u2014 ${r.local.detail}` : ""}`);
24621
+ }
24622
+ lines.push("");
24623
+ lines.push("CI");
24624
+ if (ciBinding) {
24625
+ const mark = r.ci.verdict === "VERIFIED" ? tick("\u2713") : cross("\u2717");
24626
+ const shownTree = adjudicatedTree ?? ciBinding.tree;
24627
+ lines.push(` ${mark} independently ${r.ci.verdict} ${shownTree.slice(0, 10)}`);
24628
+ } else {
24629
+ lines.push(` ${cross("\u2717")} ${r.ci.verdict} (identity not computable)`);
24630
+ }
24631
+ lines.push("");
24632
+ lines.push("RESULT");
24633
+ lines.push(` ${paint(r.result, (r.result === "VERIFIED" ? "" : BOLD) + severityColour(r.result === "VERIFIED" ? "ok" : "bad"), colour)}`);
24634
+ const agreementLine = {
24635
+ AGREE: "Local and CI evidence agree",
24636
+ DIVERGENCE: "EVIDENCE DIVERGENCE \u2014 local claim not confirmed by CI",
24637
+ NON_APPLICABLE: "Local receipt does not apply to the CI-adjudicated state \u2014 ignored",
24638
+ NO_CLAIM: "No local receipt to reconcile \u2014 CI verdict stands"
24639
+ };
24640
+ lines.push(` ${paint(agreementLine[r.agreement], severityColour(AGREEMENT_SEVERITY[r.agreement]), colour)}`);
24641
+ for (const d of r.divergence) lines.push(` ${d}`);
24642
+ return lines.join("\n") + "\n";
24643
+ }
24644
+ function loadClaimedReceipt(path) {
24645
+ if (path === void 0) {
24646
+ return { disposition: "ABSENT", detail: "no local verification receipt was provided (pass one with --receipt)" };
24647
+ }
24648
+ let raw;
24649
+ try {
24650
+ raw = readFileSync29(path, "utf8");
24651
+ } catch (e) {
24652
+ return { disposition: "MALFORMED", detail: `receipt file could not be read (${e instanceof Error ? e.message : String(e)})` };
24653
+ }
24654
+ let parsed;
24655
+ try {
24656
+ parsed = JSON.parse(raw);
24657
+ } catch (e) {
24658
+ return { disposition: "MALFORMED", detail: `receipt file could not be parsed as JSON (${e instanceof Error ? e.message : String(e)})` };
24659
+ }
24660
+ return classifyReceipt(parsed);
24661
+ }
24662
+ function runReceiptReconcile(opts = {}) {
24663
+ const cwd = opts.cwd ?? process.cwd();
24664
+ const guard = needRepo(cwd, "reconcile");
24665
+ if (guard !== null) return guard;
24666
+ const { ci, exit } = ciAdjudicate(cwd, opts);
24667
+ const claimed = loadClaimedReceipt(opts.receipt);
24668
+ const r = reconcile(ci, claimed);
24669
+ const claimedReceipt = claimed.disposition === "PRESENT" ? claimed.receipt : null;
24670
+ if (opts.json) {
24671
+ process.stdout.write(JSON.stringify(reconcileDocument(r)) + "\n");
24672
+ } else {
24673
+ process.stdout.write(renderReconcile(r, claimedReceipt, ci.binding, ci.adjudicated_tree, colourEnabled(process.env, process.stdout)));
24674
+ }
24675
+ writeJobSummary(r, claimedReceipt, ci.binding, ci.adjudicated_tree);
24676
+ return exit;
24677
+ }
24678
+ function writeJobSummary(r, receipt, ciBinding, adjudicatedTree) {
24679
+ const path = process.env.GITHUB_STEP_SUMMARY;
24680
+ if (!path) return;
24681
+ const md = ["## TamperWard receipt reconciliation", ""];
24682
+ const mark = (value, pass) => value === pass ? "\u2713" : "\u2717";
24683
+ md.push("### LOCAL");
24684
+ if (receipt) {
24685
+ md.push(`- ${mark(receipt.stages.candidate, "PASS")} candidate \`${receipt.binding.tree.slice(0, 10)}\` locally verified`);
24686
+ md.push(`- ${mark(receipt.stages.pristine, "PASS")} pristine ${receipt.stages.pristine}`);
24687
+ md.push(`- ${mark(receipt.stages.integrity, "CLEAN")} integrity ${receipt.stages.integrity}`);
24688
+ } else if (r.local.disposition === "ABSENT") {
24689
+ md.push("- _no receipt provided_");
24690
+ } else {
24691
+ md.push(`- receipt **${r.local.disposition}**${r.local.detail ? ` \u2014 ${r.local.detail}` : ""}`);
24692
+ }
24693
+ md.push("", "### CI");
24694
+ const shownTree = adjudicatedTree ?? ciBinding?.tree;
24695
+ md.push(`- independently **${r.ci.verdict}**${shownTree ? ` \`${shownTree.slice(0, 10)}\`` : " (identity not computable)"}`);
24696
+ md.push("", "### RESULT");
24697
+ md.push(`**${r.result}** \u2014 ${r.agreement}`);
24698
+ for (const d of r.divergence) md.push(`- ${d}`);
24699
+ md.push("");
24700
+ try {
24701
+ appendFileSync6(path, md.join("\n") + "\n");
24702
+ } catch {
24703
+ }
24704
+ }
24705
+ var RECEIPT_SUBCOMMANDS, AGREEMENT_SEVERITY;
24706
+ var init_receipt = __esm({
24707
+ "src/cli/receipt.ts"() {
24708
+ "use strict";
24709
+ init_machine_output();
24710
+ init_repo_context();
24711
+ init_verification_state();
24712
+ init_verification_receipt();
24713
+ init_verify();
24714
+ init_signoff();
24715
+ init_text();
24716
+ init_status();
24717
+ RECEIPT_SUBCOMMANDS = ["export", "reconcile"];
24718
+ AGREEMENT_SEVERITY = {
24719
+ AGREE: "ok",
24720
+ DIVERGENCE: "bad",
24721
+ NON_APPLICABLE: "warn",
24722
+ NO_CLAIM: "info"
24723
+ };
24724
+ }
24725
+ });
24726
+
24060
24727
  // src/cli/signoff-label.ts
24061
24728
  function runSignoffLabel(opts) {
24062
24729
  if (!opts.rule) {
@@ -24514,6 +25181,29 @@ function validateCliArgs(cmd, args) {
24514
25181
  }
24515
25182
  return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
24516
25183
  }
25184
+ if (cmd === "receipt") {
25185
+ const [sub, ...rest] = args;
25186
+ if (sub === void 0) return `receipt requires a subcommand (${RECEIPT_SUBCOMMANDS.join(" | ")})`;
25187
+ if (sub === "export") {
25188
+ return validateFlatArgs(rest, {
25189
+ values: { "--out": "string", "--cwd": "string" }
25190
+ }).error;
25191
+ }
25192
+ if (sub === "reconcile") {
25193
+ return validateFlatArgs(rest, {
25194
+ flags: ["--json", "--require-ancestor"],
25195
+ values: {
25196
+ "--base": "string",
25197
+ "--cmd": "string",
25198
+ "--budget": "positive",
25199
+ "--cwd": "string",
25200
+ "--receipt": "string",
25201
+ "--ci-result": "string"
25202
+ }
25203
+ }).error;
25204
+ }
25205
+ return `unknown receipt subcommand "${sub}" (${RECEIPT_SUBCOMMANDS.join(" | ")})`;
25206
+ }
24517
25207
  return void 0;
24518
25208
  }
24519
25209
  function printHelp() {
@@ -24649,6 +25339,23 @@ Formats:
24649
25339
  emits the versioned status document for
24650
25340
  editors, CI and dashboards. Local CURRENT
24651
25341
  is posture, never repository/CI authority.
25342
+ tamperward receipt export [--out F] emit the bounded, transportable verification
25343
+ [--cwd D] receipt for the CURRENT verified state (to F,
25344
+ else stdout). Refuses when the state is not
25345
+ CURRENT. Raw evidence otherwise stays under
25346
+ .git/tamperward/, never in the tracked tree.
25347
+ tamperward receipt reconcile [--base R] CI reruns the canonical verification FIRST,
25348
+ [--cmd C] [--budget S] then reconciles a claimed receipt against its
25349
+ [--receipt F] [--ci-result F] own adjudication. --receipt is the claimed
25350
+ [--require-ancestor] [--json] local receipt (absent \u2192 NO_CLAIM, never a
25351
+ [--cwd D] failure by itself); --ci-result consumes a
25352
+ preceding "verify --json" so the suite runs
25353
+ once. Prints LOCAL / CI / RESULT and (--json)
25354
+ a machine-readable reconciliation document,
25355
+ plus a GitHub job summary. The receipt is
25356
+ evidence, never authority: a stale, mismatched,
25357
+ tampered, missing or unknown-schema receipt can
25358
+ NEVER promote a CI result. Exit is CI's verdict.
24652
25359
  tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
24653
25360
  [--mode headless|interactive] specific capability matrix (pre-deny:shell,
24654
25361
  [--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
@@ -24739,6 +25446,8 @@ function main(argv) {
24739
25446
  const { sub, opts } = parseRuntime(rest);
24740
25447
  return runRuntime(sub, opts);
24741
25448
  }
25449
+ case "receipt":
25450
+ return runReceipt(rest);
24742
25451
  case "run":
24743
25452
  return runEnvelope({
24744
25453
  ...parseRun(rest),
@@ -24790,6 +25499,7 @@ var init_main = __esm({
24790
25499
  init_onboard();
24791
25500
  init_research();
24792
25501
  init_runtime();
25502
+ init_receipt();
24793
25503
  init_audit();
24794
25504
  init_signoff_label();
24795
25505
  }
@@ -24798,7 +25508,7 @@ var init_main = __esm({
24798
25508
  // src/cli/index.ts
24799
25509
  init_hook_client();
24800
25510
  init_exit();
24801
- import { readFileSync as readFileSync29 } from "node:fs";
25511
+ import { readFileSync as readFileSync30 } from "node:fs";
24802
25512
  function loadMain() {
24803
25513
  return Promise.resolve().then(() => (init_main(), main_exports));
24804
25514
  }
@@ -24811,7 +25521,7 @@ async function launch(argv) {
24811
25521
  if (kind && hookServiceEnabled()) {
24812
25522
  let raw = null;
24813
25523
  try {
24814
- raw = readFileSync29(0, "utf8");
25524
+ raw = readFileSync30(0, "utf8");
24815
25525
  } catch {
24816
25526
  raw = null;
24817
25527
  }