@openthink/stamp 2.0.2 → 2.1.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/index.js CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  userKeysDir,
53
53
  userServerConfigPath,
54
54
  verifyBytes
55
- } from "./chunk-4PFD2DSY.js";
55
+ } from "./chunk-MJULVH4B.js";
56
56
 
57
57
  // src/index.ts
58
58
  import { Command } from "commander";
@@ -541,8 +541,12 @@ function validateConfig(input) {
541
541
  throw new Error(`config.reviewers.${name} must be an object`);
542
542
  }
543
543
  const d = def;
544
- if (typeof d.prompt !== "string") {
545
- throw new Error(`config.reviewers.${name}.prompt must be a string`);
544
+ let prompt2;
545
+ if (d.prompt !== void 0) {
546
+ if (typeof d.prompt !== "string") {
547
+ throw new Error(`config.reviewers.${name}.prompt must be a string`);
548
+ }
549
+ prompt2 = d.prompt;
546
550
  }
547
551
  const tools = parseTools(d.tools, name);
548
552
  const mcp_servers = parseMcpServers(d.mcp_servers, name);
@@ -564,7 +568,7 @@ function validateConfig(input) {
564
568
  `config.reviewers.${name}.timeout_ms`
565
569
  );
566
570
  reviewers2[name] = {
567
- prompt: d.prompt,
571
+ ...prompt2 !== void 0 ? { prompt: prompt2 } : {},
568
572
  ...tools ? { tools } : {},
569
573
  ...mcp_servers ? { mcp_servers } : {},
570
574
  ...enforce_reads_on_dotstamp !== void 0 ? { enforce_reads_on_dotstamp } : {},
@@ -1412,8 +1416,8 @@ import { createHash as createHash3, createPublicKey, verify } from "crypto";
1412
1416
  import { spawn } from "child_process";
1413
1417
 
1414
1418
  // src/lib/attestationV4.ts
1415
- var CURRENT_V4_SCHEMA_VERSION = 4;
1416
- var MIN_ACCEPTED_V4_SCHEMA_VERSION = 4;
1419
+ var CURRENT_V4_SCHEMA_VERSION = 5;
1420
+ var MIN_ACCEPTED_V4_SCHEMA_VERSION = 5;
1417
1421
  var MAX_V4_ENVELOPE_BYTES = 64 * 1024;
1418
1422
  function sortKeysDeep(value) {
1419
1423
  if (value === null || typeof value !== "object") return value;
@@ -1547,7 +1551,6 @@ function parseResponseJson(raw) {
1547
1551
  "diff_sha256",
1548
1552
  "base_sha",
1549
1553
  "head_sha",
1550
- "trusted_keys_snapshot_sha256",
1551
1554
  "issued_at",
1552
1555
  "server_key_id"
1553
1556
  ]) {
@@ -1945,6 +1948,7 @@ function buildTrustAnchorPayload(input) {
1945
1948
  head_sha: input.headSha,
1946
1949
  target_branch: input.targetBranch,
1947
1950
  diff_sha256: input.diffSha256,
1951
+ manifest_snapshot_sha256: input.manifestSnapshotSha256,
1948
1952
  approvals: input.approvals,
1949
1953
  checks: input.checks,
1950
1954
  trust_anchor_signatures: [],
@@ -2027,6 +2031,11 @@ var COMMIT_PHASES_V4 = [
2027
2031
  { name: "verifyV4TargetBranch", fn: verifyV4TargetBranch },
2028
2032
  { name: "verifyV4SignerTrust", fn: verifyV4SignerTrust },
2029
2033
  { name: "verifyV4OuterSignature", fn: verifyV4OuterSignature },
2034
+ // AGT-370: envelope-level manifest snapshot binding (lifted from
2035
+ // the per-approval slot in v4). Runs once before the per-approval
2036
+ // loop in verifyV4ApprovalSignatures — a single check replaces the
2037
+ // N-checks per envelope the v4 verifier did.
2038
+ { name: "verifyV4ManifestSnapshot", fn: verifyV4ManifestSnapshot },
2030
2039
  { name: "verifyV4Approvals", fn: verifyV4Approvals },
2031
2040
  { name: "verifyV4DiffHash", fn: verifyV4DiffHash },
2032
2041
  { name: "verifyV4ApprovalSignatures", fn: verifyV4ApprovalSignatures },
@@ -2159,10 +2168,19 @@ function verifyV4DiffHash(input) {
2159
2168
  }
2160
2169
  return { ok: true };
2161
2170
  }
2171
+ function verifyV4ManifestSnapshot(input) {
2172
+ const { sha, payload, manifest } = input;
2173
+ const computed = snapshotSha256(manifest);
2174
+ if (payload.manifest_snapshot_sha256 !== computed) {
2175
+ return {
2176
+ ok: false,
2177
+ reason: `commit ${sha.slice(0, 8)}: v4 manifest_snapshot_sha256 (${payload.manifest_snapshot_sha256.slice(0, 16)}\u2026) does not match the manifest at base ${payload.base_sha.slice(0, 8)} (${computed.slice(0, 16)}\u2026). The envelope was signed against a different snapshot of the trust set than the one committed at the merge base. Re-run \`stamp merge\` (or \`stamp attest\`) so the outer signature binds to the current manifest.`
2178
+ };
2179
+ }
2180
+ return { ok: true };
2181
+ }
2162
2182
  function verifyV4ApprovalSignatures(input) {
2163
2183
  const { sha, payload, manifest, pubkeyByFingerprint } = input;
2164
- const manifestSnapshot = snapshotSha256(manifest);
2165
- const reviewerDefs = readReviewerDefsAtRef(payload.base_sha);
2166
2184
  for (const entry of payload.approvals) {
2167
2185
  const a = entry.approval;
2168
2186
  const reviewerLabel = `"${a.reviewer}"`;
@@ -2184,12 +2202,6 @@ function verifyV4ApprovalSignatures(input) {
2184
2202
  reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: server_attestation.server_key_id (${entry.server_attestation.server_key_id}) does not match inner approval.server_key_id (${a.server_key_id}). The inner signed payload is authoritative; one of the two was tampered with after signing.`
2185
2203
  };
2186
2204
  }
2187
- if (a.trusted_keys_snapshot_sha256 !== manifestSnapshot) {
2188
- return {
2189
- ok: false,
2190
- reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: trusted_keys_snapshot_sha256 (${a.trusted_keys_snapshot_sha256.slice(0, 16)}\u2026) does not match the manifest at base ${payload.base_sha.slice(0, 8)} (${manifestSnapshot.slice(0, 16)}\u2026). The server signed against a different snapshot of the trust set than the one committed at the merge base.`
2191
- };
2192
- }
2193
2205
  const caps = resolveCapability(manifest, a.server_key_id);
2194
2206
  if (caps === null) {
2195
2207
  return {
@@ -2229,29 +2241,6 @@ function verifyV4ApprovalSignatures(input) {
2229
2241
  reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: server signature does not verify against ${a.server_key_id} over canonical approval bytes`
2230
2242
  };
2231
2243
  }
2232
- const def = reviewerDefs[a.reviewer];
2233
- if (!def) {
2234
- return {
2235
- ok: false,
2236
- reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: reviewer is not defined in .stamp/config.yml at base ${payload.base_sha.slice(0, 8)}`
2237
- };
2238
- }
2239
- let promptText;
2240
- try {
2241
- promptText = run(["show", `${payload.base_sha}:${def.prompt}`]);
2242
- } catch {
2243
- return {
2244
- ok: false,
2245
- reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: prompt "${def.prompt}" is unreadable at base ${payload.base_sha.slice(0, 8)}`
2246
- };
2247
- }
2248
- const recomputedPromptSha = hashPromptBytes(Buffer.from(promptText, "utf8"));
2249
- if (recomputedPromptSha !== a.prompt_sha256) {
2250
- return {
2251
- ok: false,
2252
- reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: prompt_sha256 mismatch \u2014 server signed ${a.prompt_sha256.slice(0, 12)}\u2026 but prompt file at base hashes to ${recomputedPromptSha.slice(0, 12)}\u2026. The reviewer prompt the server reviewed differs from the one in the merge-base tree.`
2253
- };
2254
- }
2255
2244
  }
2256
2245
  return { ok: true };
2257
2246
  }
@@ -2417,22 +2406,6 @@ function readPubkeyMapAt(ref) {
2417
2406
  }
2418
2407
  return buildPubkeyMap(names, (relPath) => run(["show", `${ref}:${relPath}`]));
2419
2408
  }
2420
- function readReviewerDefsAtRef(ref) {
2421
- let yaml;
2422
- try {
2423
- yaml = run(["show", `${ref}:.stamp/config.yml`]);
2424
- } catch {
2425
- return {};
2426
- }
2427
- const defs = readReviewersFromYaml(yaml);
2428
- const out = {};
2429
- for (const [name, def] of Object.entries(defs)) {
2430
- if (def && typeof def.prompt === "string") {
2431
- out[name] = { prompt: def.prompt };
2432
- }
2433
- }
2434
- return out;
2435
- }
2436
2409
  function readChangedFilesAtRef(baseSha, headSha) {
2437
2410
  let out;
2438
2411
  try {
@@ -2820,6 +2793,12 @@ function verifyReviewerHashesAtMergeBase(input) {
2820
2793
  reason: `${prefix} reviewer "${approval.reviewer}" not defined in .stamp/config.yml at merge-base`
2821
2794
  };
2822
2795
  }
2796
+ if (def.prompt === void 0) {
2797
+ return {
2798
+ ok: false,
2799
+ reason: `${prefix} reviewer "${approval.reviewer}" has no \`prompt:\` in .stamp/config.yml at merge-base; v3 attestation references prompt_sha256 but the producer flow for server-bundled prompts is v4 (server-attested). The attestation envelope and the config shape are inconsistent.`
2800
+ };
2801
+ }
2823
2802
  let promptBytes;
2824
2803
  try {
2825
2804
  promptBytes = run2(["show", `${baseSha}:${def.prompt}`]);
@@ -3403,6 +3382,11 @@ function buildV3Trailers(input) {
3403
3382
  `reviewer "${a.reviewer}" approved the diff but is not defined in .stamp/config.yml at base ${input.baseSha.slice(0, 8)}. This shouldn't happen \u2014 runReview reads from the same base. File a bug at https://github.com/OpenThinkAi/stamp-cli/issues. Merge rolled back.`
3404
3383
  );
3405
3384
  }
3385
+ if (def.prompt === void 0) {
3386
+ throw new Error(
3387
+ `reviewer "${a.reviewer}": no \`prompt:\` configured and no \`review_server:\` on branch rule \u2014 set \`reviewers.${a.reviewer}.prompt\` in .stamp/config.yml or configure a \`review_server:\` for server-attested mode. Merge rolled back.`
3388
+ );
3389
+ }
3406
3390
  const promptText = showAtRef(input.baseSha, def.prompt, input.repoRoot);
3407
3391
  const source = readReviewerSource(a.reviewer, input.repoRoot);
3408
3392
  return {
@@ -3489,7 +3473,6 @@ function buildV4Trailers(input) {
3489
3473
  "diff_sha256",
3490
3474
  "base_sha",
3491
3475
  "head_sha",
3492
- "trusted_keys_snapshot_sha256",
3493
3476
  "issued_at",
3494
3477
  "server_key_id"
3495
3478
  ]) {
@@ -3569,12 +3552,14 @@ function buildV4Trailers(input) {
3569
3552
  exit_code: c.exit_code,
3570
3553
  output_sha: c.output_sha
3571
3554
  }));
3555
+ const manifestSnapshot = snapshotSha256(manifest);
3572
3556
  const trustAnchorSigs = collectTrustAnchorSignatures({
3573
3557
  repoRoot: input.repoRoot,
3574
3558
  baseSha: input.baseSha,
3575
3559
  headSha: input.headSha,
3576
3560
  targetBranch: input.targetBranch,
3577
3561
  diffSha256,
3562
+ manifestSnapshotSha256: manifestSnapshot,
3578
3563
  approvals: entries,
3579
3564
  checks: v4Checks,
3580
3565
  operatorFingerprint: input.operatorFingerprint,
@@ -3587,6 +3572,7 @@ function buildV4Trailers(input) {
3587
3572
  head_sha: input.headSha,
3588
3573
  target_branch: input.targetBranch,
3589
3574
  diff_sha256: diffSha256,
3575
+ manifest_snapshot_sha256: manifestSnapshot,
3590
3576
  approvals: entries,
3591
3577
  checks: v4Checks,
3592
3578
  trust_anchor_signatures: trustAnchorSigs,
@@ -3638,6 +3624,7 @@ function collectTrustAnchorSignatures(input) {
3638
3624
  headSha: input.headSha,
3639
3625
  targetBranch: input.targetBranch,
3640
3626
  diffSha256: input.diffSha256,
3627
+ manifestSnapshotSha256: input.manifestSnapshotSha256,
3641
3628
  approvals: input.approvals,
3642
3629
  checks: input.checks,
3643
3630
  signerKeyId: input.operatorFingerprint
@@ -4731,6 +4718,11 @@ function buildReviewPlan(opts) {
4731
4718
  const reviewers2 = [];
4732
4719
  for (const name of reviewerNames) {
4733
4720
  const def = config2.reviewers[name];
4721
+ if (def.prompt === void 0) {
4722
+ throw new Error(
4723
+ `reviewer "${name}": no \`prompt:\` configured and no \`review_server:\` on branch rule \u2014 set \`reviewers.${name}.prompt\` in .stamp/config.yml or configure a \`review_server:\` for server-attested mode.`
4724
+ );
4725
+ }
4734
4726
  let prompt2;
4735
4727
  try {
4736
4728
  prompt2 = showAtRef(resolved.base_sha, def.prompt, opts.repoRoot);
@@ -5340,19 +5332,6 @@ async function runReview(opts) {
5340
5332
  `no reviewers to run at base ${resolved.base_sha.slice(0, 8)} (config there has ${Object.keys(config2.reviewers).length} configured). If this branch ADDS a new reviewer, the new reviewer cannot review its own introduction \u2014 that's a deliberate security boundary. Land the reviewer in a separate PR first, then it can review subsequent diffs.`
5341
5333
  );
5342
5334
  }
5343
- const promptBytesByReviewer = /* @__PURE__ */ new Map();
5344
- for (const name of reviewerNames) {
5345
- const def = config2.reviewers[name];
5346
- let bytes;
5347
- try {
5348
- bytes = showAtRef(resolved.base_sha, def.prompt, repoRoot);
5349
- } catch (err) {
5350
- throw new Error(
5351
- `failed to read prompt for reviewer "${name}" from base ${resolved.base_sha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}. (The reviewer is configured at the base but its prompt file is missing there.)`
5352
- );
5353
- }
5354
- promptBytesByReviewer.set(name, bytes);
5355
- }
5356
5335
  const targetBranch = opts.into ?? inferTargetBranch(opts.diff);
5357
5336
  const branchRule = targetBranch ? findBranchRule(config2.branches, targetBranch) : void 0;
5358
5337
  if (branchRule?.review_server) {
@@ -5360,7 +5339,7 @@ async function runReview(opts) {
5360
5339
  opts,
5361
5340
  config: config2,
5362
5341
  reviewerNames,
5363
- promptBytesByReviewer,
5342
+ promptBytesByReviewer: /* @__PURE__ */ new Map(),
5364
5343
  resolved,
5365
5344
  repoRoot,
5366
5345
  reviewServerUrl: branchRule.review_server,
@@ -5368,6 +5347,24 @@ async function runReview(opts) {
5368
5347
  });
5369
5348
  return;
5370
5349
  }
5350
+ const promptBytesByReviewer = /* @__PURE__ */ new Map();
5351
+ for (const name of reviewerNames) {
5352
+ const def = config2.reviewers[name];
5353
+ if (def.prompt === void 0) {
5354
+ throw new Error(
5355
+ `reviewer "${name}": no \`prompt:\` configured and no \`review_server:\` on branch rule \u2014 set \`reviewers.${name}.prompt\` in .stamp/config.yml or configure a \`review_server:\` for server-attested mode.`
5356
+ );
5357
+ }
5358
+ let bytes;
5359
+ try {
5360
+ bytes = showAtRef(resolved.base_sha, def.prompt, repoRoot);
5361
+ } catch (err) {
5362
+ throw new Error(
5363
+ `failed to read prompt for reviewer "${name}" from base ${resolved.base_sha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}. (The reviewer is configured at the base but its prompt file is missing there.)`
5364
+ );
5365
+ }
5366
+ promptBytesByReviewer.set(name, bytes);
5367
+ }
5371
5368
  maybePrintLlmNotice(repoRoot);
5372
5369
  const userCfg = loadOrCreateUserConfig();
5373
5370
  if (userCfg.created) {
@@ -5896,6 +5893,7 @@ function buildPlan(current, targetBranch, targetRule, opts) {
5896
5893
  }
5897
5894
  newReviewersConfig = seed.config.reviewers;
5898
5895
  for (const [name, def] of Object.entries(seed.config.reviewers)) {
5896
+ if (def.prompt === void 0) continue;
5899
5897
  const promptBody = seed.reviewerFiles.get(def.prompt);
5900
5898
  if (promptBody === void 0) {
5901
5899
  throw new Error(
@@ -9487,11 +9485,13 @@ function signPending(repoRoot, rawSha, opts) {
9487
9485
  }
9488
9486
  predictedSigner = opts.signerKeyId;
9489
9487
  }
9488
+ const manifestSnapshotSha256 = snapshotSha256(manifest);
9490
9489
  const signingBytes = trustAnchorSigningBytes({
9491
9490
  baseSha,
9492
9491
  headSha,
9493
9492
  targetBranch,
9494
9493
  diffSha256,
9494
+ manifestSnapshotSha256,
9495
9495
  approvals,
9496
9496
  checks: [],
9497
9497
  // see trustAnchorPayload.ts "Operational caveat"
@@ -10215,6 +10215,7 @@ function parseEnvelope(bytes) {
10215
10215
  return null;
10216
10216
  }
10217
10217
  if (typeof p.diff_sha256 !== "string") return null;
10218
+ if (typeof p.manifest_snapshot_sha256 !== "string") return null;
10218
10219
  if (!Array.isArray(p.trust_anchor_signatures)) return null;
10219
10220
  if (typeof p.target_branch_tip_sha !== "string") return null;
10220
10221
  return env;
@@ -10436,7 +10437,6 @@ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re
10436
10437
  "diff_sha256",
10437
10438
  "base_sha",
10438
10439
  "head_sha",
10439
- "trusted_keys_snapshot_sha256",
10440
10440
  "issued_at",
10441
10441
  "server_key_id"
10442
10442
  ]) {
@@ -10511,6 +10511,7 @@ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re
10511
10511
  db.close();
10512
10512
  }
10513
10513
  const trustAnchorSignatures = [];
10514
+ const manifestSnapshot = snapshotSha256(manifest);
10514
10515
  const payload = {
10515
10516
  schema_version: PR_ATTESTATION_SCHEMA_VERSION,
10516
10517
  patch_id: input.patchId,
@@ -10519,6 +10520,7 @@ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re
10519
10520
  target_branch: input.targetBranch,
10520
10521
  target_branch_tip_sha: input.targetBranchTipSha,
10521
10522
  diff_sha256: diffSha256,
10523
+ manifest_snapshot_sha256: manifestSnapshot,
10522
10524
  approvals: entries,
10523
10525
  checks: [],
10524
10526
  // Phase-1 deliberate omission — see file-level comment.
@@ -10579,6 +10581,11 @@ function buildV2Envelope(input) {
10579
10581
  `reviewer "${a.reviewer}" approved the diff but is not defined in .stamp/config.yml at base ${input.baseSha.slice(0, 8)}. This shouldn't happen \u2014 runReview reads from the same base. File a bug at https://github.com/OpenThinkAi/stamp-cli/issues.`
10580
10582
  );
10581
10583
  }
10584
+ if (def.prompt === void 0) {
10585
+ throw new Error(
10586
+ `reviewer "${a.reviewer}": no \`prompt:\` configured and no \`review_server:\` on branch rule \u2014 set \`reviewers.${a.reviewer}.prompt\` in .stamp/config.yml or configure a \`review_server:\` for server-attested PR mode.`
10587
+ );
10588
+ }
10582
10589
  const promptText = showAtRef(input.baseSha, def.prompt, input.repoRoot);
10583
10590
  const source = readReviewerSource2(a.reviewer, input.repoRoot);
10584
10591
  return {
@@ -10951,6 +10958,11 @@ function checkReviewerDrift(repoRoot, reviewerName, def) {
10951
10958
  if (!lock) {
10952
10959
  return unpinnedResult();
10953
10960
  }
10961
+ if (def.prompt === void 0) {
10962
+ throw new Error(
10963
+ `reviewer "${reviewerName}" has a lock file but no \`prompt:\` configured (server-bundled in Shape 4). Lock files pin local prompt bytes and don't apply in server-attested mode. Delete .stamp/reviewers/${reviewerName}.lock.json to un-pin, or set \`reviewers.${reviewerName}.prompt\` in .stamp/config.yml if you intend to author the prompt locally.`
10964
+ );
10965
+ }
10954
10966
  const promptPath = join15(repoRoot, def.prompt);
10955
10967
  if (!existsSync20(promptPath)) {
10956
10968
  throw new Error(
@@ -11036,6 +11048,10 @@ function reviewersList() {
11036
11048
  const maxNameLen = Math.max(...names.map((n) => n.length));
11037
11049
  for (const name of names) {
11038
11050
  const def = config2.reviewers[name];
11051
+ if (def.prompt === void 0) {
11052
+ console.log(` ${name.padEnd(maxNameLen)} (server-bundled \u2014 no local prompt)`);
11053
+ continue;
11054
+ }
11039
11055
  const abs = resolve2(repoRoot, def.prompt);
11040
11056
  let annotation = "";
11041
11057
  if (!existsSync21(abs)) {
@@ -11062,6 +11078,11 @@ function reviewersEdit(name) {
11062
11078
  `reviewer "${name}" is not configured. Run \`stamp reviewers list\` to see available reviewers.`
11063
11079
  );
11064
11080
  }
11081
+ if (def.prompt === void 0) {
11082
+ throw new Error(
11083
+ `reviewer "${name}" has no \`prompt:\` configured (server-bundled in Shape 4). There is no local prompt file to edit. To author the prompt locally, set \`reviewers.${name}.prompt\` to a path under .stamp/reviewers/ in .stamp/config.yml.`
11084
+ );
11085
+ }
11065
11086
  const target = resolve2(repoRoot, def.prompt);
11066
11087
  launchEditor(target);
11067
11088
  }
@@ -11126,6 +11147,10 @@ function reviewersRemove(name, opts = {}) {
11126
11147
  delete config2.reviewers[name];
11127
11148
  writeFileSync15(configPath, stringifyConfig(config2));
11128
11149
  console.log(`reviewer "${name}" removed from .stamp/config.yml`);
11150
+ if (def.prompt === void 0) {
11151
+ console.log(`(no local prompt file \u2014 reviewer was server-bundled)`);
11152
+ return;
11153
+ }
11129
11154
  if (opts.deleteFile) {
11130
11155
  const promptAbs = resolve2(repoRoot, def.prompt);
11131
11156
  if (existsSync21(promptAbs)) {
@@ -11159,6 +11184,11 @@ async function reviewersTest(name, diff) {
11159
11184
  console.log(` prompt sourced from working tree (test/iteration use case)`);
11160
11185
  console.log();
11161
11186
  const def = config2.reviewers[name];
11187
+ if (def.prompt === void 0) {
11188
+ throw new Error(
11189
+ `reviewer "${name}" has no \`prompt:\` configured (server-bundled in Shape 4). \`stamp reviewers test\` is a local prompt-iteration helper and needs a local prompt file to invoke. Set \`reviewers.${name}.prompt\` in .stamp/config.yml to a path under .stamp/reviewers/ to use this command.`
11190
+ );
11191
+ }
11162
11192
  const promptPath = join16(repoRoot, def.prompt);
11163
11193
  const systemPrompt = readFileSync18(promptPath, "utf8");
11164
11194
  const result = await invokeReviewer({
@@ -11203,7 +11233,9 @@ function reviewersShow(name, opts) {
11203
11233
  const bar = "\u2500".repeat(72);
11204
11234
  console.log(bar);
11205
11235
  console.log(`reviewer: ${name}`);
11206
- console.log(`prompt: ${config2.reviewers[name].prompt}`);
11236
+ console.log(
11237
+ `prompt: ${config2.reviewers[name].prompt ?? "(server-bundled \u2014 no local prompt)"}`
11238
+ );
11207
11239
  console.log(bar);
11208
11240
  if (stats.total === 0) {
11209
11241
  console.log(" no verdicts recorded yet");
@@ -11849,6 +11881,12 @@ function verifyReviewerHashes(sha, payload, repoRoot, config2) {
11849
11881
  `v2 attestation: reviewer "${approval.reviewer}" is in payload but not defined in config.reviewers at the merge commit`
11850
11882
  );
11851
11883
  }
11884
+ if (def.prompt === void 0) {
11885
+ fail(
11886
+ sha,
11887
+ `v2 attestation: reviewer "${approval.reviewer}" has no \`prompt:\` in .stamp/config.yml at the merge commit. v2 attestations cite prompt_sha256 over the on-tree prompt file, but the producer flow for server-bundled prompts is v4 (server-attested) \u2014 the envelope and config shape are inconsistent.`
11888
+ );
11889
+ }
11852
11890
  const promptBytes = tryGitShow(`${sha}:${def.prompt}`, repoRoot);
11853
11891
  if (promptBytes === null) {
11854
11892
  fail(
@@ -11981,6 +12019,7 @@ function verifyV3Envelope(envelope, opts, resolved, patch_id, repoRoot) {
11981
12019
  head_sha: envelope.payload.head_sha,
11982
12020
  target_branch: envelope.payload.target_branch,
11983
12021
  diff_sha256: envelope.payload.diff_sha256,
12022
+ manifest_snapshot_sha256: envelope.payload.manifest_snapshot_sha256,
11984
12023
  approvals: envelope.payload.approvals,
11985
12024
  checks: envelope.payload.checks,
11986
12025
  trust_anchor_signatures: envelope.payload.trust_anchor_signatures,
@@ -12618,7 +12657,7 @@ program.command("prune").description(
12618
12657
  });
12619
12658
  program.command("ui").description("launch the interactive terminal UI").action(async () => {
12620
12659
  try {
12621
- const { runUi } = await import("./ui-P5DRAT3P.js");
12660
+ const { runUi } = await import("./ui-67BDQPER.js");
12622
12661
  runUi();
12623
12662
  } catch (err) {
12624
12663
  handleCliError(err);