tamperward 2.37.1 → 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
@@ -5300,13 +5300,13 @@ function merge(a, b) {
5300
5300
  function literals(e) {
5301
5301
  if (ts.isStringLiteral(e)) return { items: [e.text], opaque: false };
5302
5302
  if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5303
- const items = [];
5303
+ const items2 = [];
5304
5304
  let opaque = false;
5305
5305
  for (const el of e.elements) {
5306
- if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
5306
+ if (ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el)) items2.push(el.text);
5307
5307
  else opaque = true;
5308
5308
  }
5309
- return { items, opaque };
5309
+ return { items: items2, opaque };
5310
5310
  }
5311
5311
  function parseLists(src) {
5312
5312
  const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], ignoreRe: [], opaqueCollect: false, opaqueIgnore: false };
@@ -5319,13 +5319,13 @@ function parseLists(src) {
5319
5319
  const k = keyName(node.name);
5320
5320
  const vitestCoverage = ownerKey(node) === "coverage";
5321
5321
  if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
5322
- const { items, opaque } = literals(resolve23(node.initializer));
5323
- res.collectFrom = items;
5322
+ const { items: items2, opaque } = literals(resolve23(node.initializer));
5323
+ res.collectFrom = items2;
5324
5324
  res.collectKey = k === "include" ? "coverage.include" : k;
5325
5325
  if (opaque) res.opaqueCollect = true;
5326
5326
  } else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
5327
- const { items, opaque } = literals(resolve23(node.initializer));
5328
- (k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
5327
+ const { items: items2, opaque } = literals(resolve23(node.initializer));
5328
+ (k === "exclude" ? res.ignore : res.ignoreRe).push(...items2);
5329
5329
  if (opaque) res.opaqueIgnore = true;
5330
5330
  }
5331
5331
  }
@@ -5923,15 +5923,15 @@ function keyName2(name) {
5923
5923
  function literals2(e) {
5924
5924
  if (isStr(e)) return { items: [e.text], opaque: false };
5925
5925
  if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
5926
- const items = [];
5926
+ const items2 = [];
5927
5927
  let opaque = false;
5928
5928
  for (const el of e.elements) {
5929
- if (isStr(el)) items.push(el.text);
5929
+ if (isStr(el)) items2.push(el.text);
5930
5930
  else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
5931
- items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5931
+ items2.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
5932
5932
  } else opaque = true;
5933
5933
  }
5934
- return { items, opaque };
5934
+ return { items: items2, opaque };
5935
5935
  }
5936
5936
  function ownerKey2(node) {
5937
5937
  const obj = node.parent;
@@ -6054,11 +6054,11 @@ function collect(root, runner) {
6054
6054
  const sel = empty();
6055
6055
  const seen = /* @__PURE__ */ new Set();
6056
6056
  const take = (key2, e) => {
6057
- const { items, opaque } = literals2(e);
6057
+ const { items: items2, opaque } = literals2(e);
6058
6058
  if (opaque || seen.has(key2)) sel.opaque = true;
6059
6059
  seen.add(key2);
6060
6060
  sel.present = true;
6061
- return items.map(stripRootDir);
6061
+ return items2.map(stripRootDir);
6062
6062
  };
6063
6063
  const ignores = (key2, e) => {
6064
6064
  const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
@@ -10701,10 +10701,10 @@ function validate(r, where) {
10701
10701
  throw new PolicyError(`${where}: ${msg}`);
10702
10702
  }
10703
10703
  const show = (v) => JSON.stringify(v) ?? String(v);
10704
- function refuseNegated(field, list3) {
10704
+ function refuseNegated(field2, list3) {
10705
10705
  const neg = list3.find(isNegatedGlob);
10706
10706
  if (neg !== void 0) {
10707
- bad2(`${field}: negated glob ${show(neg)} is not allowed \u2014 a "!" pattern matches every path, so it would ${field === "ignore" ? "ignore every file" : field.startsWith("protected.") ? `put every file in ${field}` : "blind the rule on every file"}; list the paths to include instead`);
10707
+ bad2(`${field2}: negated glob ${show(neg)} is not allowed \u2014 a "!" pattern matches every path, so it would ${field2 === "ignore" ? "ignore every file" : field2.startsWith("protected.") ? `put every file in ${field2}` : "blind the rule on every file"}; list the paths to include instead`);
10708
10708
  }
10709
10709
  }
10710
10710
  const out3 = {};
@@ -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({
@@ -20577,7 +20898,52 @@ function pairRecordFrom(raw, where = "record") {
20577
20898
  }
20578
20899
  };
20579
20900
  }
20580
- var SHA_RE, SHA256_RE;
20901
+ function onlyFields(raw, allowed, where) {
20902
+ if (!isRecord(raw)) return;
20903
+ for (const key2 of Object.keys(raw)) {
20904
+ if (!allowed.includes(key2)) bad(`${where}.${key2} is not a field of the v1 pair record; refusing to carry it as portable evidence`);
20905
+ }
20906
+ }
20907
+ function portableObject(raw, spec2, where) {
20908
+ if (!isRecord(raw)) bad(`${where} is not an object`);
20909
+ for (const [key2, value] of Object.entries(raw)) {
20910
+ const field2 = spec2.get(key2);
20911
+ const at = `${where}.${key2}`;
20912
+ if (field2 === void 0) bad(`${at} is not a field of the v1 run document; refusing to carry it as portable evidence`);
20913
+ if (field2 === "scalar") {
20914
+ if (value !== null && typeof value === "object") bad(`${at} is not a scalar; refusing to carry it as portable evidence`);
20915
+ } else if ("items" in field2) {
20916
+ if (!Array.isArray(value)) bad(`${at} is not an array`);
20917
+ value.forEach((item, i) => portableObject(item, field2.items, `${at}[${i}]`));
20918
+ } else {
20919
+ portableObject(value, field2.fields, at);
20920
+ }
20921
+ }
20922
+ }
20923
+ function portablePairRecordFrom(raw, where = "record") {
20924
+ onlyFields(raw, PAIR_FIELDS, where);
20925
+ if (isRecord(raw)) {
20926
+ onlyFields(raw.adapter, ADAPTER_FIELDS, `${where}.adapter`);
20927
+ onlyFields(raw.arms, ARMS_FIELDS, `${where}.arms`);
20928
+ if (isRecord(raw.arms)) {
20929
+ for (const arm of ARMS_FIELDS) {
20930
+ const trajectory = raw.arms[arm];
20931
+ const at = `${where}.arms.${arm}`;
20932
+ onlyFields(trajectory, TRAJECTORY_FIELDS, at);
20933
+ if (isRecord(trajectory)) {
20934
+ onlyFields(trajectory.agent, AGENT_FIELDS, `${at}.agent`);
20935
+ onlyFields(trajectory.treatment, TREATMENT_FIELDS, `${at}.treatment`);
20936
+ if (isRecord(trajectory.treatment) && trajectory.treatment.envelope !== null && trajectory.treatment.envelope !== void 0) {
20937
+ portableObject(trajectory.treatment.envelope, RUN_DOCUMENT_FIELDS, `${at}.treatment.envelope`);
20938
+ }
20939
+ onlyFields(trajectory.outcome, OUTCOME_FIELDS, `${at}.outcome`);
20940
+ }
20941
+ }
20942
+ }
20943
+ }
20944
+ return pairRecordFrom(raw, where);
20945
+ }
20946
+ var SHA_RE, SHA256_RE, PAIR_FIELDS, ADAPTER_FIELDS, ARMS_FIELDS, TRAJECTORY_FIELDS, AGENT_FIELDS, TREATMENT_FIELDS, OUTCOME_FIELDS, fields, items, RUN_DOCUMENT_FIELDS;
20581
20947
  var init_record = __esm({
20582
20948
  "src/research/record.ts"() {
20583
20949
  "use strict";
@@ -20587,23 +20953,99 @@ var init_record = __esm({
20587
20953
  init_derive();
20588
20954
  SHA_RE = /^[0-9a-f]{40,64}$/;
20589
20955
  SHA256_RE = /^[0-9a-f]{64}$/;
20956
+ PAIR_FIELDS = [
20957
+ "schema_version",
20958
+ "command",
20959
+ "document",
20960
+ "task",
20961
+ "pair",
20962
+ "adapter",
20963
+ "model",
20964
+ "tamperward_version",
20965
+ "agent_argv",
20966
+ "agent_budget",
20967
+ "manifest_sha256",
20968
+ "verify_command",
20969
+ "arms"
20970
+ ];
20971
+ ADAPTER_FIELDS = ["name", "layers"];
20972
+ ARMS_FIELDS = ["ungated", "gated"];
20973
+ TRAJECTORY_FIELDS = [
20974
+ "arm",
20975
+ "workspace",
20976
+ "base",
20977
+ "head",
20978
+ "started_at",
20979
+ "finished_at",
20980
+ "agent",
20981
+ "treatment",
20982
+ "outcome",
20983
+ "released_green",
20984
+ "measured",
20985
+ "unmeasurable"
20986
+ ];
20987
+ AGENT_FIELDS = ["exit_code", "signal", "timed_out", "failure"];
20988
+ TREATMENT_FIELDS = ["verdict", "exit_code", "complete", "disposition", "envelope"];
20989
+ OUTCOME_FIELDS = [
20990
+ "verify_verdict",
20991
+ "visible_exit",
20992
+ "pristine_exit",
20993
+ "visible_green",
20994
+ "pristine_green",
20995
+ "masked_failure",
20996
+ "surviving_protected_mutations",
20997
+ "warn_findings",
20998
+ "rules",
20999
+ "honest_completion"
21000
+ ];
21001
+ fields = (entries) => ({ fields: new Map(entries) });
21002
+ items = (entries) => ({ items: new Map(entries) });
21003
+ RUN_DOCUMENT_FIELDS = /* @__PURE__ */ new Map([
21004
+ ["schema_version", "scalar"],
21005
+ ["verdict", "scalar"],
21006
+ ["exit_code", "scalar"],
21007
+ ["complete", "scalar"],
21008
+ ["base", "scalar"],
21009
+ ["head", "scalar"],
21010
+ ["reason", "scalar"],
21011
+ ["agent", fields([["exit_code", "scalar"], ["timed_out", "scalar"], ["lifecycle_owned", "scalar"], ["budget_secs", "scalar"]])],
21012
+ ["checks", fields([["diff", "scalar"], ["worktree", "scalar"], ["verify", "scalar"]])],
21013
+ ["verifier_backend", fields([
21014
+ ["kind", "scalar"],
21015
+ ["trust", "scalar"],
21016
+ ["available", "scalar"],
21017
+ ["image", "scalar"],
21018
+ ["engine", "scalar"],
21019
+ ["reason", "scalar"],
21020
+ ["resources", fields([["memory_bytes", "scalar"], ["memory_swap_bytes", "scalar"], ["cpus", "scalar"], ["pids", "scalar"]])]
21021
+ ])],
21022
+ ["dependency_environment", fields([
21023
+ ["status", "scalar"],
21024
+ ["fingerprint", "scalar"],
21025
+ ["reason", "scalar"],
21026
+ ["image", "scalar"],
21027
+ ["roots", items([["kind", "scalar"], ["path", "scalar"]])],
21028
+ ["diagnostics", fields([["full_snapshots", "scalar"], ["reused_snapshots", "scalar"], ["total_ms", "scalar"]])]
21029
+ ])],
21030
+ ["observer", fields([["enabled", "scalar"], ["blocking", "scalar"]])]
21031
+ ]);
20590
21032
  }
20591
21033
  });
20592
21034
 
20593
21035
  // src/research/run.ts
20594
21036
  import { randomUUID as randomUUID2 } from "node:crypto";
20595
21037
  import { execFileSync as execFileSync14 } from "node:child_process";
20596
- 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";
20597
21039
  import { hostname } from "node:os";
20598
- import { join as join26, resolve as resolve16 } from "node:path";
21040
+ import { join as join27, resolve as resolve16 } from "node:path";
20599
21041
  function git7(args, cwd) {
20600
21042
  return execFileSync14("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
20601
21043
  }
20602
21044
  function pairRecordPath(ledger, task, pair) {
20603
- return join26(ledger, "pairs", `${task}--${pair}.json`);
21045
+ return join27(ledger, "pairs", `${task}--${pair}.json`);
20604
21046
  }
20605
21047
  function researchLockPath(ledger) {
20606
- return join26(ledger, "run.lock");
21048
+ return join27(ledger, "run.lock");
20607
21049
  }
20608
21050
  function readLockOwner(path) {
20609
21051
  try {
@@ -20628,7 +21070,7 @@ function lockHolder(path) {
20628
21070
  return owner ? `pid ${owner.pid} on ${owner.host}, started ${owner.started_at}` : "an unreadable lock file";
20629
21071
  }
20630
21072
  function acquireResearchLock(ledger, breakStaleLock = false) {
20631
- mkdirSync10(ledger, { recursive: true });
21073
+ mkdirSync11(ledger, { recursive: true });
20632
21074
  const path = researchLockPath(ledger);
20633
21075
  if (breakStaleLock && existsSync18(path)) {
20634
21076
  const owner2 = readLockOwner(path);
@@ -20738,16 +21180,16 @@ function resumableRecord(path, expected) {
20738
21180
  }
20739
21181
  function writeRecordAtomically(path, text) {
20740
21182
  const tmp = `${path}.${process.pid}.tmp`;
20741
- writeFileSync12(tmp, text);
21183
+ writeFileSync13(tmp, text);
20742
21184
  renameSync4(tmp, path);
20743
21185
  }
20744
21186
  function cloneArgs(repo, ws3) {
20745
21187
  return ["-c", "protocol.ext.allow=never", "clone", "-q", "--no-hardlinks", "--", repo, ws3];
20746
21188
  }
20747
21189
  function freshWorkspace(ledger, task, pair, arm, sourceBase) {
20748
- const ws3 = join26(ledger, "workspaces", `${task.id}--${pair}--${arm}`);
20749
- rmSync12(ws3, { recursive: true, force: true });
20750
- 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 });
20751
21193
  try {
20752
21194
  execFileSync14("git", cloneArgs(task.repo, ws3), { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
20753
21195
  } catch (e) {
@@ -20996,7 +21438,7 @@ function runResearch(opts) {
20996
21438
  }
20997
21439
  try {
20998
21440
  const pairs = opts.pairs ?? 1;
20999
- mkdirSync10(join26(ledger, "pairs"), { recursive: true });
21441
+ mkdirSync11(join27(ledger, "pairs"), { recursive: true });
21000
21442
  for (const task of tasks) {
21001
21443
  const existingRecords = /* @__PURE__ */ new Map();
21002
21444
  let sourceBase = null;
@@ -21122,27 +21564,95 @@ var init_run2 = __esm({
21122
21564
  }
21123
21565
  });
21124
21566
 
21125
- // src/research/summarize.ts
21126
- import { readdirSync as readdirSync7, readFileSync as readFileSync24 } from "node:fs";
21127
- import { join as join27, resolve as resolve17 } from "node:path";
21128
- function readLedger2(dir) {
21129
- const pairsDir = join27(resolve17(dir), "pairs");
21567
+ // src/research/ledger-files.ts
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";
21569
+ import { join as join28, resolve as resolve17 } from "node:path";
21570
+ function lstatOrNull(path) {
21571
+ try {
21572
+ return lstatSync11(path);
21573
+ } catch {
21574
+ return null;
21575
+ }
21576
+ }
21577
+ function kindOf(st) {
21578
+ if (st.isSymbolicLink()) return "a symlink";
21579
+ if (st.isDirectory()) return "a directory";
21580
+ if (st.isFIFO()) return "a FIFO";
21581
+ if (st.isSocket()) return "a socket";
21582
+ if (st.isCharacterDevice() || st.isBlockDevice()) return "a device";
21583
+ return "not a regular file";
21584
+ }
21585
+ function readRegularFile(path, realPairsDir, name) {
21586
+ const listed = lstatOrNull(path);
21587
+ if (listed === null) throw new ResearchError(`ledger record ${path} vanished while the ledger was being read`);
21588
+ if (!listed.isFile()) {
21589
+ throw new ResearchError(`ledger record ${path} is ${kindOf(listed)}; only a regular file placed directly in pairs/ is read`);
21590
+ }
21591
+ if (listed.nlink !== 1) {
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`);
21593
+ }
21594
+ const real = realpathSync11.native(path);
21595
+ if (real !== join28(realPairsDir, name)) {
21596
+ throw new ResearchError(`ledger record ${path} resolves outside the pairs directory (${real}); refusing to read it`);
21597
+ }
21598
+ const flags = constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0);
21599
+ let fd;
21600
+ try {
21601
+ fd = openSync5(path, flags);
21602
+ } catch (e) {
21603
+ throw new ResearchError(`cannot open ledger record ${path}: ${errorMessage(e)}`);
21604
+ }
21605
+ try {
21606
+ const opened = fstatSync2(fd);
21607
+ const sameInode = process.platform === "win32" || opened.dev === listed.dev && opened.ino === listed.ino;
21608
+ if (!opened.isFile() || !sameInode || opened.nlink !== 1) {
21609
+ throw new ResearchError(`ledger record ${path} changed between listing and reading; refusing to read it`);
21610
+ }
21611
+ return readFileSync24(fd);
21612
+ } finally {
21613
+ closeSync5(fd);
21614
+ }
21615
+ }
21616
+ function readPairFiles(ledger) {
21617
+ const root = resolve17(ledger);
21618
+ const pairsDir = join28(root, "pairs");
21619
+ const dirStat = lstatOrNull(pairsDir);
21620
+ if (dirStat !== null && dirStat.isSymbolicLink()) {
21621
+ throw new ResearchError(`ledger pairs directory ${pairsDir} is a symlink; only a real directory is read`);
21622
+ }
21623
+ if (dirStat === null || !dirStat.isDirectory()) throw new ResearchError(`ledger ${root} has no pairs directory`);
21624
+ const realPairsDir = realpathSync11.native(pairsDir);
21130
21625
  let names;
21131
21626
  try {
21132
21627
  names = readdirSync7(pairsDir).filter((n2) => n2.endsWith(".json")).sort();
21133
21628
  } catch (e) {
21134
21629
  throw new ResearchError(`cannot read ledger ${pairsDir}: ${errorMessage(e)}`);
21135
21630
  }
21136
- if (names.length === 0) throw new ResearchError(`ledger ${pairsDir} holds no pair records`);
21137
- return names.map((n2) => {
21138
- const path = join27(pairsDir, n2);
21631
+ return names.map((name) => {
21632
+ const path = join28(pairsDir, name);
21633
+ return { name, path, bytes: readRegularFile(path, realPairsDir, name) };
21634
+ });
21635
+ }
21636
+ var init_ledger_files = __esm({
21637
+ "src/research/ledger-files.ts"() {
21638
+ "use strict";
21639
+ init_narrow();
21640
+ init_adapter();
21641
+ }
21642
+ });
21643
+
21644
+ // src/research/summarize.ts
21645
+ function readLedger2(dir) {
21646
+ const files = readPairFiles(dir);
21647
+ if (files.length === 0) throw new ResearchError(`ledger ${dir} holds no pair records`);
21648
+ return files.map((f) => {
21139
21649
  let raw;
21140
21650
  try {
21141
- raw = JSON.parse(readFileSync24(path, "utf8"));
21651
+ raw = JSON.parse(f.bytes.toString("utf8"));
21142
21652
  } catch (e) {
21143
- throw new ResearchError(`ledger record ${path} is not valid JSON: ${errorMessage(e)}`);
21653
+ throw new ResearchError(`ledger record ${f.path} is not valid JSON: ${errorMessage(e)}`);
21144
21654
  }
21145
- return pairRecordFrom(raw, path);
21655
+ return pairRecordFrom(raw, f.path);
21146
21656
  });
21147
21657
  }
21148
21658
  function weakened(t) {
@@ -21263,15 +21773,16 @@ var init_summarize = __esm({
21263
21773
  "use strict";
21264
21774
  init_machine_output();
21265
21775
  init_narrow();
21776
+ init_ledger_files();
21266
21777
  init_adapter();
21267
21778
  init_record();
21268
21779
  }
21269
21780
  });
21270
21781
 
21271
21782
  // src/research/init.ts
21272
- import { createHash as createHash15 } from "node:crypto";
21273
- import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
21274
- 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";
21275
21786
  function idFrom(repo) {
21276
21787
  const clean = repo.replace(/[\\/]$/, "").split(/[\\/]/).pop() || "task";
21277
21788
  return (basename5(clean, ".git").replace(/[^A-Za-z0-9._-]+/g, "-") || "task").slice(0, 80);
@@ -21299,8 +21810,8 @@ function createResearchManifest(opts) {
21299
21810
  }]
21300
21811
  };
21301
21812
  const text = JSON.stringify(doc, null, 2) + "\n";
21302
- mkdirSync11(dirname12(out3), { recursive: true });
21303
- writeFileSync13(out3, text, { flag: "wx", mode: 384 });
21813
+ mkdirSync12(dirname13(out3), { recursive: true });
21814
+ writeFileSync14(out3, text, { flag: "wx", mode: 384 });
21304
21815
  const parsed = readManifest(out3);
21305
21816
  if (parsed.tasks.length !== 1 || parsed.tasks[0].id !== id) throw new ResearchError(`research init wrote an invalid manifest ${out3}`);
21306
21817
  return out3;
@@ -21322,7 +21833,7 @@ function runResearchInit(opts) {
21322
21833
  }
21323
21834
  }
21324
21835
  function requireHash(path) {
21325
- return createHash15("sha256").update(readFileSync25(path)).digest("hex");
21836
+ return createHash16("sha256").update(readFileSync25(path)).digest("hex");
21326
21837
  }
21327
21838
  var init_init2 = __esm({
21328
21839
  "src/research/init.ts"() {
@@ -21387,65 +21898,107 @@ var init_report2 = __esm({
21387
21898
  });
21388
21899
 
21389
21900
  // src/research/bundle.ts
21390
- import { createHash as createHash16 } from "node:crypto";
21901
+ import { createHash as createHash17 } from "node:crypto";
21391
21902
  import { gzipSync, gunzipSync } from "node:zlib";
21392
- import { existsSync as existsSync20, mkdirSync as mkdirSync12, readdirSync as readdirSync8, readFileSync as readFileSync26, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
21393
- import { dirname as dirname13, join as join28, 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";
21394
21905
  function octal(value, width) {
21395
21906
  return value.toString(8).padStart(width - 1, "0") + "\0";
21396
21907
  }
21397
- function tarEntry(name, bytes) {
21908
+ function tarHeader(name, size) {
21398
21909
  const header = Buffer.alloc(BLOCK, 0);
21399
- const safe = name.replace(/^\/+/, "").slice(0, 99);
21400
- header.write(safe, 0, "utf8");
21910
+ header.write(name, 0, "utf8");
21401
21911
  header.write(octal(420, 8), 100, "ascii");
21402
21912
  header.write(octal(0, 8), 108, "ascii");
21403
21913
  header.write(octal(0, 8), 116, "ascii");
21404
- header.write(octal(bytes.length, 12), 124, "ascii");
21914
+ header.write(octal(size, 12), 124, "ascii");
21405
21915
  header.write(octal(0, 12), 136, "ascii");
21406
21916
  header.fill(32, 148, 156);
21407
- header[156] = 48;
21917
+ header[TYPE_OFFSET] = 48;
21408
21918
  header.write("ustar", 257, "ascii");
21409
21919
  header.write("00", 263, "ascii");
21410
21920
  const checksum = header.reduce((sum, b) => sum + b, 0);
21411
21921
  header.write(octal(checksum, 8), 148, "ascii");
21922
+ return header;
21923
+ }
21924
+ function tarEntry(name, bytes) {
21925
+ if (name.startsWith("/") || Buffer.byteLength(name, "utf8") > TAR_NAME_MAX) {
21926
+ throw new ResearchError(`archive entry name ${name} cannot be stored byte for byte in a ustar header`);
21927
+ }
21412
21928
  const padding = Buffer.alloc((BLOCK - bytes.length % BLOCK) % BLOCK, 0);
21413
- return Buffer.concat([header, bytes, padding]);
21929
+ return Buffer.concat([tarHeader(name, bytes.length), bytes, padding]);
21414
21930
  }
21415
21931
  function makeTar(entries) {
21416
21932
  return Buffer.concat([...entries.map((e) => tarEntry(e.name, e.bytes)), Buffer.alloc(BLOCK * 2, 0)]);
21417
21933
  }
21934
+ function field(header, start, length) {
21935
+ const raw = header.subarray(start, start + length);
21936
+ const end = raw.indexOf(0);
21937
+ return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
21938
+ }
21418
21939
  function parseTar(bytes) {
21419
21940
  const files = /* @__PURE__ */ new Map();
21420
- for (let offset = 0; offset + BLOCK <= bytes.length; ) {
21941
+ let offset = 0;
21942
+ for (; ; ) {
21943
+ if (offset + BLOCK > bytes.length) throw new ResearchError("research bundle is missing its end-of-archive blocks");
21421
21944
  const header = bytes.subarray(offset, offset + BLOCK);
21422
- if (header.every((b) => b === 0)) break;
21423
- const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
21424
- const sizeText = header.subarray(124, 136).toString("ascii").replace(/\0.*$/, "").trim();
21425
- const size = Number.parseInt(sizeText || "0", 8);
21945
+ if (header.every((b) => b === 0)) {
21946
+ const second = bytes.subarray(offset + BLOCK, offset + 2 * BLOCK);
21947
+ if (second.length !== BLOCK || !second.every((b) => b === 0) || offset + 2 * BLOCK !== bytes.length) {
21948
+ throw new ResearchError("research bundle carries bytes after its end-of-archive blocks");
21949
+ }
21950
+ return files;
21951
+ }
21952
+ const name = field(header, 0, NAME_FIELD);
21953
+ const prefix = field(header, PREFIX_OFFSET, PREFIX_FIELD);
21954
+ if (prefix) {
21955
+ throw new ResearchError(`research bundle entry ${prefix}/${name} uses a ustar prefix; this bundle format stores every name in the name field`);
21956
+ }
21957
+ const type = header[TYPE_OFFSET];
21958
+ if (type !== 48) {
21959
+ throw new ResearchError(`research bundle entry ${name} is not a regular file entry (type flag ${JSON.stringify(String.fromCharCode(type))})`);
21960
+ }
21961
+ const size = Number.parseInt(field(header, 124, 12).trim() || "0", 8);
21426
21962
  if (!name || name.startsWith("/") || name.split("/").includes("..") || !Number.isSafeInteger(size) || size < 0) {
21427
21963
  throw new ResearchError("research bundle contains an unsafe or malformed archive entry");
21428
21964
  }
21965
+ if (Buffer.byteLength(name, "utf8") > TAR_NAME_MAX || !header.equals(tarHeader(name, size))) {
21966
+ throw new ResearchError(`research bundle entry ${name} has a header this bundle format does not write`);
21967
+ }
21429
21968
  const start = offset + BLOCK;
21430
21969
  const end = start + size;
21431
21970
  if (end > bytes.length) throw new ResearchError("research bundle is truncated");
21971
+ const padded = start + Math.ceil(size / BLOCK) * BLOCK;
21972
+ if (padded > bytes.length || !bytes.subarray(end, padded).every((b) => b === 0)) {
21973
+ throw new ResearchError(`research bundle entry ${name} carries bytes in its padding`);
21974
+ }
21975
+ if (files.has(name)) throw new ResearchError(`research bundle contains a duplicate archive entry ${name}`);
21432
21976
  files.set(name, Buffer.from(bytes.subarray(start, end)));
21433
- offset = start + Math.ceil(size / BLOCK) * BLOCK;
21977
+ offset = padded;
21434
21978
  }
21435
- return files;
21436
21979
  }
21437
- function walkLedger(ledger) {
21980
+ function packagedRecords(ledger) {
21981
+ const files = readPairFiles(ledger);
21982
+ if (files.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
21983
+ const records = [];
21438
21984
  const entries = [];
21439
- const pairs = join28(ledger, "pairs");
21440
- if (!existsSync20(pairs) || !statSync6(pairs).isDirectory()) throw new ResearchError(`ledger ${ledger} has no pairs directory`);
21441
- const names = readdirSync8(pairs).filter((n2) => n2.endsWith(".json")).sort();
21442
- if (names.length === 0) throw new ResearchError(`ledger ${ledger} holds no pair records`);
21443
- for (const name of names) {
21444
- const path = join28(pairs, name);
21445
- if (!statSync6(path).isFile()) continue;
21446
- entries.push({ name: `ledger/pairs/${name}`, bytes: readFileSync26(path) });
21985
+ for (const f of files) {
21986
+ let raw;
21987
+ try {
21988
+ raw = JSON.parse(f.bytes.toString("utf8"));
21989
+ } catch (e) {
21990
+ throw new ResearchError(`ledger record ${f.path} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
21991
+ }
21992
+ const record = portablePairRecordFrom(raw, f.path);
21993
+ const archiveName = `ledger/pairs/${f.name}`;
21994
+ const nameBytes = Buffer.byteLength(archiveName, "utf8");
21995
+ if (nameBytes > TAR_NAME_MAX) {
21996
+ throw new ResearchError(`pair record name ${f.name} is too long for the archive (${nameBytes} bytes as ${archiveName}; the limit is ${TAR_NAME_MAX})`);
21997
+ }
21998
+ records.push(record);
21999
+ entries.push({ name: archiveName, bytes: jsonBytes(record) });
21447
22000
  }
21448
- return entries;
22001
+ return { records, entries };
21449
22002
  }
21450
22003
  function jsonBytes(value) {
21451
22004
  return Buffer.from(JSON.stringify(value, null, 2) + "\n", "utf8");
@@ -21457,6 +22010,11 @@ function validateResearchBundle(path) {
21457
22010
  } catch (e) {
21458
22011
  throw new ResearchError(`cannot read research bundle ${path}: ${e instanceof Error ? e.message : String(e)}`);
21459
22012
  }
22013
+ for (const name of files.keys()) {
22014
+ if (!TOP_LEVEL_ENTRIES.has(name) && !PAIR_ENTRY_RE.test(name)) {
22015
+ throw new ResearchError(`research bundle carries an entry that is not evidence: ${name}`);
22016
+ }
22017
+ }
21460
22018
  const provenance = files.get("provenance.json");
21461
22019
  const summaryBytes = files.get("summary.json");
21462
22020
  const report2 = files.get("report.txt");
@@ -21476,21 +22034,23 @@ function validateResearchBundle(path) {
21476
22034
  if (p.manifest_included !== Boolean(manifest) || p.prompts_included !== Boolean(manifest)) {
21477
22035
  throw new ResearchError("research bundle manifest inclusion metadata is inconsistent");
21478
22036
  }
21479
- if (manifest && createHash16("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
22037
+ if (manifest && createHash17("sha256").update(manifest).digest("hex") !== p.manifest_sha256) {
21480
22038
  throw new ResearchError("research bundle manifest does not match its recorded sha256");
21481
22039
  }
21482
22040
  const parsedSummary = summary;
21483
22041
  if (parsedSummary.document !== "summary" || parsedSummary.command !== "research") throw new ResearchError("research bundle summary is not a research summary");
21484
22042
  const records = [];
21485
22043
  for (const [name, bytes] of files) {
21486
- if (!name.startsWith("ledger/pairs/") || !name.endsWith(".json")) continue;
22044
+ if (!PAIR_ENTRY_RE.test(name)) continue;
21487
22045
  let raw;
21488
22046
  try {
21489
22047
  raw = JSON.parse(bytes.toString("utf8"));
21490
22048
  } catch {
21491
22049
  throw new ResearchError(`invalid JSON in ${name}`);
21492
22050
  }
21493
- records.push(pairRecordFrom(raw, name));
22051
+ const record = portablePairRecordFrom(raw, name);
22052
+ if (!bytes.equals(jsonBytes(record))) throw new ResearchError(`${name} is not the canonical serialization of its pair record`);
22053
+ records.push(record);
21494
22054
  }
21495
22055
  if (records.length === 0) throw new ResearchError("research bundle contains no pair records");
21496
22056
  const derived = summarizeRecords(records);
@@ -21504,11 +22064,11 @@ function createResearchBundle(opts) {
21504
22064
  if (!opts.ledger) throw new ResearchError("research bundle requires --ledger");
21505
22065
  if (!opts.out) throw new ResearchError("research bundle requires --out");
21506
22066
  const ledger = resolve19(opts.ledger);
21507
- const summary = summarizeLedger(ledger);
21508
- const entries = walkLedger(ledger);
22067
+ const { records, entries } = packagedRecords(ledger);
22068
+ const summary = summarizeRecords(records);
21509
22069
  if (opts.manifest) {
21510
22070
  const manifest = readFileSync26(resolve19(opts.manifest));
21511
- const sha3 = createHash16("sha256").update(manifest).digest("hex");
22071
+ const sha3 = createHash17("sha256").update(manifest).digest("hex");
21512
22072
  if (sha3 !== summary.manifest_sha256) throw new ResearchError(`manifest sha256 ${sha3} does not match ledger ${summary.manifest_sha256}`);
21513
22073
  entries.push({ name: "manifest.json", bytes: manifest });
21514
22074
  }
@@ -21530,8 +22090,8 @@ function createResearchBundle(opts) {
21530
22090
  entries.push({ name: "report.txt", bytes: Buffer.from(renderResearchReport(summary), "utf8") });
21531
22091
  entries.push({ name: "provenance.json", bytes: jsonBytes(provenance) });
21532
22092
  const out3 = resolve19(opts.out);
21533
- mkdirSync12(dirname13(out3), { recursive: true });
21534
- 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 });
21535
22095
  return out3;
21536
22096
  }
21537
22097
  function runResearchBundle(opts) {
@@ -21555,17 +22115,25 @@ function runResearchBundle(opts) {
21555
22115
  throw e;
21556
22116
  }
21557
22117
  }
21558
- var BLOCK, BUNDLE_VERSION;
22118
+ var BLOCK, BUNDLE_VERSION, TOP_LEVEL_ENTRIES, PAIR_ENTRY_RE, TAR_NAME_MAX, NAME_FIELD, TYPE_OFFSET, PREFIX_OFFSET, PREFIX_FIELD;
21559
22119
  var init_bundle = __esm({
21560
22120
  "src/research/bundle.ts"() {
21561
22121
  "use strict";
21562
22122
  init_machine_output();
21563
22123
  init_adapter();
22124
+ init_ledger_files();
21564
22125
  init_record();
21565
22126
  init_report2();
21566
22127
  init_summarize();
21567
22128
  BLOCK = 512;
21568
22129
  BUNDLE_VERSION = 1;
22130
+ TOP_LEVEL_ENTRIES = /* @__PURE__ */ new Set(["summary.json", "report.txt", "provenance.json", "manifest.json"]);
22131
+ PAIR_ENTRY_RE = /^ledger\/pairs\/[^/]+\.json$/;
22132
+ TAR_NAME_MAX = 99;
22133
+ NAME_FIELD = 100;
22134
+ TYPE_OFFSET = 156;
22135
+ PREFIX_OFFSET = 345;
22136
+ PREFIX_FIELD = 155;
21569
22137
  }
21570
22138
  });
21571
22139
 
@@ -22535,9 +23103,9 @@ var init_adapter4 = __esm({
22535
23103
 
22536
23104
  // src/adapters/copilot-sdk/changes.ts
22537
23105
  import { execFileSync as execFileSync15 } from "node:child_process";
22538
- import { mkdtempSync as mkdtempSync7, writeFileSync as writeFileSync15, readFileSync as readFileSync27, rmSync as rmSync13, existsSync as existsSync21, realpathSync as realpathSync11 } 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";
22539
23107
  import { tmpdir as tmpdir8 } from "node:os";
22540
- 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";
22541
23109
  function asStr4(v) {
22542
23110
  return typeof v === "string" ? v : "";
22543
23111
  }
@@ -22548,25 +23116,25 @@ function relForDisplay5(path, cwd) {
22548
23116
  function canonicalContainedTarget(abs, root) {
22549
23117
  let realRoot;
22550
23118
  try {
22551
- realRoot = realpathSync11(root);
23119
+ realRoot = realpathSync12(root);
22552
23120
  } catch (e) {
22553
23121
  throw new Error(`trusted repository root ${root} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22554
23122
  }
22555
23123
  let dir = abs;
22556
23124
  const tail = [];
22557
- while (!existsSync21(dir)) {
22558
- const parent = dirname14(dir);
23125
+ while (!existsSync20(dir)) {
23126
+ const parent = dirname15(dir);
22559
23127
  if (parent === dir) break;
22560
23128
  tail.unshift(basename6(dir));
22561
23129
  dir = parent;
22562
23130
  }
22563
23131
  let realDir;
22564
23132
  try {
22565
- realDir = realpathSync11(dir);
23133
+ realDir = realpathSync12(dir);
22566
23134
  } catch (e) {
22567
23135
  throw new Error(`write target ${abs} cannot be resolved: ${e instanceof Error ? e.message : String(e)}`);
22568
23136
  }
22569
- const real = tail.length ? join29(realDir, ...tail) : realDir;
23137
+ const real = tail.length ? join30(realDir, ...tail) : realDir;
22570
23138
  const rel = relative12(realRoot, real);
22571
23139
  if (rel === "" || rel.startsWith("..") || isAbsolute17(rel)) {
22572
23140
  throw new Error(`write target ${abs} resolves outside the trusted repository root ${realRoot} \u2014 refusing to judge a write beyond the repository`);
@@ -22609,12 +23177,12 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
22609
23177
  const bytes = Buffer.byteLength(rawDiff);
22610
23178
  if (bytes > DIFF_MAX_BYTES()) throw new Error(`write diff for ${display2} is ${bytes} bytes (over the ${DIFF_MAX_BYTES()}-byte reconstruction budget)`);
22611
23179
  const { op, patch } = canonicalPatch(rawDiff, display2, isCreate);
22612
- const dir = mkdtempSync7(join29(tmpdir8(), "hf-sdk-apply-"));
23180
+ const dir = mkdtempSync7(join30(tmpdir8(), "hf-sdk-apply-"));
22613
23181
  try {
22614
- const targetPath = join29(dir, TARGET);
22615
- if (op !== "create") writeFileSync15(targetPath, before ?? "");
22616
- const patchPath = join29(dir, "change.patch");
22617
- 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);
22618
23186
  const git8 = (extra) => execFileSync15("git", ["apply", ...extra, patchPath], {
22619
23187
  cwd: dir,
22620
23188
  encoding: "utf8",
@@ -22633,7 +23201,7 @@ function reconstructAfterViaGit(rawDiff, display2, before, isCreate) {
22633
23201
  } catch (e) {
22634
23202
  throw new Error(`could not reconstruct the write to ${display2} from its diff via git apply: ${e instanceof Error ? e.message : String(e)}`);
22635
23203
  } finally {
22636
- rmSync13(dir, { recursive: true, force: true });
23204
+ rmSync14(dir, { recursive: true, force: true });
22637
23205
  }
22638
23206
  }
22639
23207
  function canonicalPatch(raw, display2, isCreate) {
@@ -23082,7 +23650,7 @@ var init_evidence = __esm({
23082
23650
  });
23083
23651
 
23084
23652
  // src/runtime-qualification.ts
23085
- import { createHash as createHash17 } from "node:crypto";
23653
+ import { createHash as createHash18 } from "node:crypto";
23086
23654
  function includesKind(set, kind) {
23087
23655
  return set.includes(kind);
23088
23656
  }
@@ -23253,7 +23821,7 @@ function aggregateInLoop(assessments) {
23253
23821
  return "PARTIAL";
23254
23822
  }
23255
23823
  function sha16(s) {
23256
- return createHash17("sha256").update(s).digest("hex").slice(0, 16);
23824
+ return createHash18("sha256").update(s).digest("hex").slice(0, 16);
23257
23825
  }
23258
23826
  function capabilityHash(caps) {
23259
23827
  const stable = JSON.stringify({
@@ -23373,15 +23941,15 @@ var init_runtime_qualification = __esm({
23373
23941
 
23374
23942
  // src/cli/runtime.ts
23375
23943
  import { execFileSync as execFileSync16 } from "node:child_process";
23376
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
23377
- 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";
23378
23946
  function storePath(cwd) {
23379
23947
  const dir = gitDir(cwd);
23380
- return dir ? join30(dir, "tamperward", "runtime-qualification.json") : null;
23948
+ return dir ? join31(dir, "tamperward", "runtime-qualification.json") : null;
23381
23949
  }
23382
23950
  function readStore(cwd) {
23383
23951
  const path = storePath(cwd);
23384
- if (!path || !existsSync22(path)) return null;
23952
+ if (!path || !existsSync21(path)) return null;
23385
23953
  try {
23386
23954
  const parsed = JSON.parse(readFileSync28(path, "utf8"));
23387
23955
  if (parsed && typeof parsed === "object" && parsed.records && typeof parsed.records === "object") return parsed;
@@ -23504,8 +24072,8 @@ function writeRecord(cwd, id, report2) {
23504
24072
  store.records[id] = report2;
23505
24073
  store.updated_at = report2.timestamp;
23506
24074
  try {
23507
- mkdirSync13(dirname15(path), { recursive: true });
23508
- 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 });
23509
24077
  return true;
23510
24078
  } catch {
23511
24079
  return false;
@@ -23540,10 +24108,10 @@ function resolveHookConfigHash(canonicalId, cwd) {
23540
24108
  return sha16(JSON.stringify(interventionWiring(cwd)));
23541
24109
  }
23542
24110
  const files = {
23543
- "github-copilot-cli": join30(cwd, ".github", "hooks", "tamperward.json")
24111
+ "github-copilot-cli": join31(cwd, ".github", "hooks", "tamperward.json")
23544
24112
  };
23545
24113
  const file = files[canonicalId];
23546
- if (!file || !existsSync22(file)) return null;
24114
+ if (!file || !existsSync21(file)) return null;
23547
24115
  try {
23548
24116
  const raw = readFileSync28(file, "utf8");
23549
24117
  let hooks = raw;
@@ -23810,6 +24378,352 @@ var init_runtime = __esm({
23810
24378
  }
23811
24379
  });
23812
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
+
23813
24727
  // src/cli/signoff-label.ts
23814
24728
  function runSignoffLabel(opts) {
23815
24729
  if (!opts.rule) {
@@ -24267,6 +25181,29 @@ function validateCliArgs(cmd, args) {
24267
25181
  }
24268
25182
  return `unknown research subcommand "${sub}" (${RESEARCH_SUBCOMMANDS.join(" | ")})`;
24269
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
+ }
24270
25207
  return void 0;
24271
25208
  }
24272
25209
  function printHelp() {
@@ -24402,6 +25339,23 @@ Formats:
24402
25339
  emits the versioned status document for
24403
25340
  editors, CI and dashboards. Local CURRENT
24404
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.
24405
25359
  tamperward runtime verify [--runtime ID] qualify the in-loop runtime: an operation-
24406
25360
  [--mode headless|interactive] specific capability matrix (pre-deny:shell,
24407
25361
  [--model M] [--json] [--cwd D] native-edit, mcp, end-of-turn, transport:*,
@@ -24492,6 +25446,8 @@ function main(argv) {
24492
25446
  const { sub, opts } = parseRuntime(rest);
24493
25447
  return runRuntime(sub, opts);
24494
25448
  }
25449
+ case "receipt":
25450
+ return runReceipt(rest);
24495
25451
  case "run":
24496
25452
  return runEnvelope({
24497
25453
  ...parseRun(rest),
@@ -24543,6 +25499,7 @@ var init_main = __esm({
24543
25499
  init_onboard();
24544
25500
  init_research();
24545
25501
  init_runtime();
25502
+ init_receipt();
24546
25503
  init_audit();
24547
25504
  init_signoff_label();
24548
25505
  }
@@ -24551,7 +25508,7 @@ var init_main = __esm({
24551
25508
  // src/cli/index.ts
24552
25509
  init_hook_client();
24553
25510
  init_exit();
24554
- import { readFileSync as readFileSync29 } from "node:fs";
25511
+ import { readFileSync as readFileSync30 } from "node:fs";
24555
25512
  function loadMain() {
24556
25513
  return Promise.resolve().then(() => (init_main(), main_exports));
24557
25514
  }
@@ -24564,7 +25521,7 @@ async function launch(argv) {
24564
25521
  if (kind && hookServiceEnabled()) {
24565
25522
  let raw = null;
24566
25523
  try {
24567
- raw = readFileSync29(0, "utf8");
25524
+ raw = readFileSync30(0, "utf8");
24568
25525
  } catch {
24569
25526
  raw = null;
24570
25527
  }