@openthink/stamp 2.0.1 → 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,15 +52,15 @@ 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";
59
59
 
60
60
  // src/commands/bootstrap.ts
61
61
  import { execFileSync as execFileSync4 } from "child_process";
62
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync, statSync, writeFileSync as writeFileSync5 } from "fs";
63
- import { dirname as dirname3, join as join3 } from "path";
62
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync, statSync, writeFileSync as writeFileSync5 } from "fs";
63
+ import { dirname as dirname4, join as join4 } from "path";
64
64
 
65
65
  // src/lib/agentsMd.ts
66
66
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -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}`]);
@@ -3166,9 +3145,38 @@ function readLineSync() {
3166
3145
  return out;
3167
3146
  }
3168
3147
 
3148
+ // src/lib/version.ts
3149
+ import { readFileSync as readFileSync4 } from "fs";
3150
+ import { dirname, join as join3 } from "path";
3151
+ import { fileURLToPath as fileURLToPath2 } from "url";
3152
+ function readPackageVersion() {
3153
+ const here = dirname(fileURLToPath2(import.meta.url));
3154
+ for (let dir = here, i = 0; i < 6; i++) {
3155
+ try {
3156
+ const raw = readFileSync4(join3(dir, "package.json"), "utf8");
3157
+ const pkg = JSON.parse(raw);
3158
+ if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
3159
+ } catch {
3160
+ }
3161
+ const parent = dirname(dir);
3162
+ if (parent === dir) break;
3163
+ dir = parent;
3164
+ }
3165
+ throw new Error("could not locate @openthink/stamp package.json to read version");
3166
+ }
3167
+
3169
3168
  // src/lib/deprecationNotice.ts
3170
- function maybePrintDeprecationNotice() {
3169
+ function maybePrintDeprecationNotice(versionOverride) {
3171
3170
  if (process.env.STAMP_SUPPRESS_DEPRECATION === "1") return;
3171
+ let major = null;
3172
+ try {
3173
+ const version = versionOverride ?? readPackageVersion();
3174
+ const m = /^(\d+)\./.exec(version);
3175
+ if (m) major = Number.parseInt(m[1], 10);
3176
+ } catch {
3177
+ return;
3178
+ }
3179
+ if (major === null || major >= 2) return;
3172
3180
  process.stderr.write(
3173
3181
  "warning: stamp 1.x is in maintenance \u2014 the server-attested 2.x line is active. See docs/migration-1.x-to-2.x.md. Suppress: STAMP_SUPPRESS_DEPRECATION=1.\n"
3174
3182
  );
@@ -3374,6 +3382,11 @@ function buildV3Trailers(input) {
3374
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.`
3375
3383
  );
3376
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
+ }
3377
3390
  const promptText = showAtRef(input.baseSha, def.prompt, input.repoRoot);
3378
3391
  const source = readReviewerSource(a.reviewer, input.repoRoot);
3379
3392
  return {
@@ -3460,7 +3473,6 @@ function buildV4Trailers(input) {
3460
3473
  "diff_sha256",
3461
3474
  "base_sha",
3462
3475
  "head_sha",
3463
- "trusted_keys_snapshot_sha256",
3464
3476
  "issued_at",
3465
3477
  "server_key_id"
3466
3478
  ]) {
@@ -3540,12 +3552,14 @@ function buildV4Trailers(input) {
3540
3552
  exit_code: c.exit_code,
3541
3553
  output_sha: c.output_sha
3542
3554
  }));
3555
+ const manifestSnapshot = snapshotSha256(manifest);
3543
3556
  const trustAnchorSigs = collectTrustAnchorSignatures({
3544
3557
  repoRoot: input.repoRoot,
3545
3558
  baseSha: input.baseSha,
3546
3559
  headSha: input.headSha,
3547
3560
  targetBranch: input.targetBranch,
3548
3561
  diffSha256,
3562
+ manifestSnapshotSha256: manifestSnapshot,
3549
3563
  approvals: entries,
3550
3564
  checks: v4Checks,
3551
3565
  operatorFingerprint: input.operatorFingerprint,
@@ -3558,6 +3572,7 @@ function buildV4Trailers(input) {
3558
3572
  head_sha: input.headSha,
3559
3573
  target_branch: input.targetBranch,
3560
3574
  diff_sha256: diffSha256,
3575
+ manifest_snapshot_sha256: manifestSnapshot,
3561
3576
  approvals: entries,
3562
3577
  checks: v4Checks,
3563
3578
  trust_anchor_signatures: trustAnchorSigs,
@@ -3609,6 +3624,7 @@ function collectTrustAnchorSignatures(input) {
3609
3624
  headSha: input.headSha,
3610
3625
  targetBranch: input.targetBranch,
3611
3626
  diffSha256: input.diffSha256,
3627
+ manifestSnapshotSha256: input.manifestSnapshotSha256,
3612
3628
  approvals: input.approvals,
3613
3629
  checks: input.checks,
3614
3630
  signerKeyId: input.operatorFingerprint
@@ -3763,12 +3779,12 @@ ${close}`;
3763
3779
  import {
3764
3780
  existsSync as existsSync3,
3765
3781
  mkdirSync,
3766
- readFileSync as readFileSync4,
3782
+ readFileSync as readFileSync5,
3767
3783
  renameSync,
3768
3784
  unlinkSync,
3769
3785
  writeFileSync as writeFileSync2
3770
3786
  } from "fs";
3771
- import { dirname } from "path";
3787
+ import { dirname as dirname2 } from "path";
3772
3788
  import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
3773
3789
  var DEFAULT_REVIEWER_MODELS = {
3774
3790
  security: "claude-sonnet-4-6",
@@ -3788,7 +3804,7 @@ function loadUserConfig() {
3788
3804
  if (!existsSync3(path2)) return null;
3789
3805
  let raw;
3790
3806
  try {
3791
- raw = readFileSync4(path2, "utf8");
3807
+ raw = readFileSync5(path2, "utf8");
3792
3808
  } catch (err) {
3793
3809
  throw new Error(
3794
3810
  `failed to read ${path2}: ${err instanceof Error ? err.message : String(err)}`
@@ -3848,7 +3864,7 @@ function stringifyUserConfig(cfg) {
3848
3864
  }
3849
3865
  function writeUserConfig(cfg) {
3850
3866
  const path2 = userConfigPath();
3851
- const dir = dirname(path2);
3867
+ const dir = dirname2(path2);
3852
3868
  if (!existsSync3(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
3853
3869
  const tmp = `${path2}.tmp.${process.pid}`;
3854
3870
  writeFileSync2(tmp, stringifyUserConfig(cfg), { mode: 384 });
@@ -4659,7 +4675,7 @@ function stripLastLineVerdict(text) {
4659
4675
 
4660
4676
  // src/lib/llmNotice.ts
4661
4677
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
4662
- import { dirname as dirname2 } from "path";
4678
+ import { dirname as dirname3 } from "path";
4663
4679
  function maybePrintLlmNotice(repoRoot) {
4664
4680
  if (process.env.STAMP_SUPPRESS_LLM_NOTICE === "1") return;
4665
4681
  const marker = stampLlmNoticeMarkerPath(repoRoot);
@@ -4672,7 +4688,7 @@ function maybePrintLlmNotice(repoRoot) {
4672
4688
  `
4673
4689
  );
4674
4690
  try {
4675
- mkdirSync3(dirname2(marker), { recursive: true });
4691
+ mkdirSync3(dirname3(marker), { recursive: true });
4676
4692
  writeFileSync4(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
4677
4693
  `);
4678
4694
  } catch {
@@ -4702,6 +4718,11 @@ function buildReviewPlan(opts) {
4702
4718
  const reviewers2 = [];
4703
4719
  for (const name of reviewerNames) {
4704
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
+ }
4705
4726
  let prompt2;
4706
4727
  try {
4707
4728
  prompt2 = showAtRef(resolved.base_sha, def.prompt, opts.repoRoot);
@@ -4912,7 +4933,7 @@ import { spawnSync as spawnSync4 } from "child_process";
4912
4933
  import { createInterface } from "readline";
4913
4934
 
4914
4935
  // src/lib/serverConfig.ts
4915
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
4936
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
4916
4937
  import { parse as parseYaml7 } from "yaml";
4917
4938
  var DEFAULT_USER = "git";
4918
4939
  var DEFAULT_REPO_ROOT = "/srv/git";
@@ -4942,7 +4963,7 @@ function loadServerConfig() {
4942
4963
  if (!existsSync5(path2)) return null;
4943
4964
  let raw;
4944
4965
  try {
4945
- raw = readFileSync5(path2, "utf8");
4966
+ raw = readFileSync6(path2, "utf8");
4946
4967
  } catch (err) {
4947
4968
  throw new Error(
4948
4969
  `failed to read ${path2}: ${err instanceof Error ? err.message : String(err)}`
@@ -5311,19 +5332,6 @@ async function runReview(opts) {
5311
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.`
5312
5333
  );
5313
5334
  }
5314
- const promptBytesByReviewer = /* @__PURE__ */ new Map();
5315
- for (const name of reviewerNames) {
5316
- const def = config2.reviewers[name];
5317
- let bytes;
5318
- try {
5319
- bytes = showAtRef(resolved.base_sha, def.prompt, repoRoot);
5320
- } catch (err) {
5321
- throw new Error(
5322
- `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.)`
5323
- );
5324
- }
5325
- promptBytesByReviewer.set(name, bytes);
5326
- }
5327
5335
  const targetBranch = opts.into ?? inferTargetBranch(opts.diff);
5328
5336
  const branchRule = targetBranch ? findBranchRule(config2.branches, targetBranch) : void 0;
5329
5337
  if (branchRule?.review_server) {
@@ -5331,7 +5339,7 @@ async function runReview(opts) {
5331
5339
  opts,
5332
5340
  config: config2,
5333
5341
  reviewerNames,
5334
- promptBytesByReviewer,
5342
+ promptBytesByReviewer: /* @__PURE__ */ new Map(),
5335
5343
  resolved,
5336
5344
  repoRoot,
5337
5345
  reviewServerUrl: branchRule.review_server,
@@ -5339,6 +5347,24 @@ async function runReview(opts) {
5339
5347
  });
5340
5348
  return;
5341
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
+ }
5342
5368
  maybePrintLlmNotice(repoRoot);
5343
5369
  const userCfg = loadOrCreateUserConfig();
5344
5370
  if (userCfg.created) {
@@ -5867,6 +5893,7 @@ function buildPlan(current, targetBranch, targetRule, opts) {
5867
5893
  }
5868
5894
  newReviewersConfig = seed.config.reviewers;
5869
5895
  for (const [name, def] of Object.entries(seed.config.reviewers)) {
5896
+ if (def.prompt === void 0) continue;
5870
5897
  const promptBody = seed.reviewerFiles.get(def.prompt);
5871
5898
  if (promptBody === void 0) {
5872
5899
  throw new Error(
@@ -5921,27 +5948,27 @@ function readSeedDir(seedDir) {
5921
5948
  if (!existsSync7(seedDir) || !statSync(seedDir).isDirectory()) {
5922
5949
  throw new Error(`--from path is not a directory: ${seedDir}`);
5923
5950
  }
5924
- const configPath = join3(seedDir, "config.yml");
5951
+ const configPath = join4(seedDir, "config.yml");
5925
5952
  if (!existsSync7(configPath)) {
5926
5953
  throw new Error(`--from dir missing config.yml: ${configPath}`);
5927
5954
  }
5928
- const reviewersDir = join3(seedDir, "reviewers");
5955
+ const reviewersDir = join4(seedDir, "reviewers");
5929
5956
  if (!existsSync7(reviewersDir) || !statSync(reviewersDir).isDirectory()) {
5930
5957
  throw new Error(`--from dir missing reviewers/ subdirectory: ${reviewersDir}`);
5931
5958
  }
5932
- const yaml = readFileSync6(configPath, "utf8");
5959
+ const yaml = readFileSync7(configPath, "utf8");
5933
5960
  const config2 = parseConfigFromYaml(yaml);
5934
5961
  const reviewerFiles = /* @__PURE__ */ new Map();
5935
5962
  for (const entry of readdirSync(reviewersDir)) {
5936
- const full = join3(reviewersDir, entry);
5963
+ const full = join4(reviewersDir, entry);
5937
5964
  if (statSync(full).isFile()) {
5938
- reviewerFiles.set(`.stamp/reviewers/${entry}`, readFileSync6(full, "utf8"));
5965
+ reviewerFiles.set(`.stamp/reviewers/${entry}`, readFileSync7(full, "utf8"));
5939
5966
  }
5940
5967
  }
5941
5968
  let mirrorYml;
5942
- const mirrorPath = join3(seedDir, "mirror.yml");
5969
+ const mirrorPath = join4(seedDir, "mirror.yml");
5943
5970
  if (existsSync7(mirrorPath)) {
5944
- mirrorYml = readFileSync6(mirrorPath, "utf8");
5971
+ mirrorYml = readFileSync7(mirrorPath, "utf8");
5945
5972
  }
5946
5973
  return { config: config2, reviewerFiles, mirrorYml };
5947
5974
  }
@@ -5949,12 +5976,12 @@ function writeBootstrapFiles(repoRoot, plan) {
5949
5976
  ensureDir(stampConfigDir(repoRoot));
5950
5977
  ensureDir(stampReviewersDir(repoRoot));
5951
5978
  for (const { path: path2, content } of plan.reviewerFiles.values()) {
5952
- const full = join3(repoRoot, path2);
5953
- ensureDir(dirname3(full));
5979
+ const full = join4(repoRoot, path2);
5980
+ ensureDir(dirname4(full));
5954
5981
  writeFileSync5(full, content);
5955
5982
  }
5956
5983
  if (plan.mirrorYml !== void 0) {
5957
- writeFileSync5(join3(repoRoot, ".stamp/mirror.yml"), plan.mirrorYml);
5984
+ writeFileSync5(join4(repoRoot, ".stamp/mirror.yml"), plan.mirrorYml);
5958
5985
  }
5959
5986
  writeFileSync5(stampConfigFile(repoRoot), stringifyConfig(plan.newConfig));
5960
5987
  }
@@ -5997,8 +6024,8 @@ function branchExists(name, cwd) {
5997
6024
  }
5998
6025
 
5999
6026
  // src/commands/init.ts
6000
- import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
6001
- import { dirname as dirname4, join as join5 } from "path";
6027
+ import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
6028
+ import { dirname as dirname5, join as join6 } from "path";
6002
6029
 
6003
6030
  // src/lib/ghRuleset.ts
6004
6031
  import { spawnSync as spawnSync5 } from "child_process";
@@ -6334,14 +6361,14 @@ function computeDesiredBypassActors(current, deployKeyId, flags) {
6334
6361
  }
6335
6362
 
6336
6363
  // src/lib/oteamConfig.ts
6337
- import { existsSync as existsSync8, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
6364
+ import { existsSync as existsSync8, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
6338
6365
  import { homedir } from "os";
6339
- import { join as join4 } from "path";
6340
- var OTEAM_CONFIG_PATH = join4(homedir(), ".open-team", "config.json");
6366
+ import { join as join5 } from "path";
6367
+ var OTEAM_CONFIG_PATH = join5(homedir(), ".open-team", "config.json");
6341
6368
  function readOteamConfig(configPath = OTEAM_CONFIG_PATH) {
6342
6369
  if (!existsSync8(configPath)) return null;
6343
6370
  try {
6344
- const parsed = JSON.parse(readFileSync7(configPath, "utf8"));
6371
+ const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
6345
6372
  if (Array.isArray(parsed)) {
6346
6373
  throw new Error("config must be a JSON object, not an array");
6347
6374
  }
@@ -6356,7 +6383,7 @@ function patchStampHost(host, configPath = OTEAM_CONFIG_PATH) {
6356
6383
  let config2 = {};
6357
6384
  if (existsSync8(configPath)) {
6358
6385
  try {
6359
- const parsed = JSON.parse(readFileSync7(configPath, "utf8"));
6386
+ const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
6360
6387
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
6361
6388
  config2 = parsed;
6362
6389
  }
@@ -6382,6 +6409,7 @@ function patchStampHost(host, configPath = OTEAM_CONFIG_PATH) {
6382
6409
  }
6383
6410
 
6384
6411
  // src/commands/init.ts
6412
+ var DEFAULT_ACTION_SOURCE = "OpenThinkAi/stamp-cli";
6385
6413
  function runInit(opts = {}) {
6386
6414
  maybePrintDeprecationNotice();
6387
6415
  const repoRoot = findRepoRoot();
@@ -6412,26 +6440,26 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
6412
6440
  if (!alreadyHasConfig) {
6413
6441
  if (opts.minimal) {
6414
6442
  writeFileSync7(configFile, stringifyConfig(MINIMAL_CONFIG));
6415
- writeFileSync7(join5(reviewersDir, "example.md"), EXAMPLE_REVIEWER_PROMPT);
6443
+ writeFileSync7(join6(reviewersDir, "example.md"), EXAMPLE_REVIEWER_PROMPT);
6416
6444
  } else {
6417
6445
  writeFileSync7(configFile, stringifyConfig(DEFAULT_CONFIG));
6418
6446
  writeFileSync7(
6419
- join5(reviewersDir, "security.md"),
6447
+ join6(reviewersDir, "security.md"),
6420
6448
  DEFAULT_SECURITY_PROMPT
6421
6449
  );
6422
6450
  writeFileSync7(
6423
- join5(reviewersDir, "standards.md"),
6451
+ join6(reviewersDir, "standards.md"),
6424
6452
  DEFAULT_STANDARDS_PROMPT
6425
6453
  );
6426
6454
  writeFileSync7(
6427
- join5(reviewersDir, "product.md"),
6455
+ join6(reviewersDir, "product.md"),
6428
6456
  DEFAULT_PRODUCT_PROMPT
6429
6457
  );
6430
6458
  }
6431
6459
  }
6432
6460
  const { keypair, created: keyCreated } = ensureUserKeypair();
6433
6461
  const userCfg = loadOrCreateUserConfig();
6434
- const pubKeyPath = join5(
6462
+ const pubKeyPath = join6(
6435
6463
  trustedKeysDir,
6436
6464
  publicKeyFingerprintFilename(keypair.fingerprint)
6437
6465
  );
@@ -6445,7 +6473,8 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
6445
6473
  const prCheckResult = maybeWriteVerifyWorkflow(
6446
6474
  repoRoot,
6447
6475
  opts.prCheck,
6448
- effectiveMode
6476
+ effectiveMode,
6477
+ opts.actionSource ?? DEFAULT_ACTION_SOURCE
6449
6478
  );
6450
6479
  const prModeResult = opts.prMode ? maybeWritePrModeMirrorWorkflow(repoRoot, { force: opts.prModeForce }) : { action: "skipped", path: PR_MODE_WORKFLOW_PATH, substitution: null };
6451
6480
  const agentsMdAction = opts.agentsMd === false ? "skipped" : ensureAgentsMd(repoRoot, effectiveMode);
@@ -6597,8 +6626,8 @@ function runBootstrapCommit(repoRoot, scaffoldOrSync) {
6597
6626
  return { kind: "skipped-already-tracked" };
6598
6627
  }
6599
6628
  const toAdd = [".stamp"];
6600
- if (existsSync9(join5(repoRoot, "AGENTS.md"))) toAdd.push("AGENTS.md");
6601
- if (existsSync9(join5(repoRoot, "CLAUDE.md"))) toAdd.push("CLAUDE.md");
6629
+ if (existsSync9(join6(repoRoot, "AGENTS.md"))) toAdd.push("AGENTS.md");
6630
+ if (existsSync9(join6(repoRoot, "CLAUDE.md"))) toAdd.push("CLAUDE.md");
6602
6631
  runGit(["add", ...toAdd], repoRoot);
6603
6632
  let hasStagedChanges = false;
6604
6633
  try {
@@ -6803,27 +6832,27 @@ function resolveMode(userMode, remoteClass) {
6803
6832
  }
6804
6833
  }
6805
6834
  var VERIFY_ACTION_REF = "v1.6.1";
6806
- function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode) {
6835
+ function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode, actionSource = DEFAULT_ACTION_SOURCE) {
6807
6836
  const path2 = ".github/workflows/stamp-verify.yml";
6808
- const fullPath = join5(repoRoot, path2);
6837
+ const fullPath = join6(repoRoot, path2);
6809
6838
  const defaultForMode = effectiveMode !== "server-gated";
6810
6839
  const shouldWrite = prCheckOpt ?? defaultForMode;
6811
6840
  if (!shouldWrite) return { action: "skipped", path: path2 };
6812
6841
  if (existsSync9(fullPath)) {
6813
6842
  return { action: "exists", path: path2 };
6814
6843
  }
6815
- ensureDir(dirname4(fullPath));
6816
- writeFileSync7(fullPath, renderVerifyWorkflow());
6844
+ ensureDir(dirname5(fullPath));
6845
+ writeFileSync7(fullPath, renderVerifyWorkflow(actionSource));
6817
6846
  return { action: "wrote", path: path2 };
6818
6847
  }
6819
6848
  var PR_MODE_WORKFLOW_PATH = ".github/workflows/stamp-mirror.yml";
6820
6849
  function maybeWritePrModeMirrorWorkflow(repoRoot, opts = {}) {
6821
- const fullPath = join5(repoRoot, PR_MODE_WORKFLOW_PATH);
6850
+ const fullPath = join6(repoRoot, PR_MODE_WORKFLOW_PATH);
6822
6851
  const substitution = derivePrModeSubstitution(repoRoot);
6823
6852
  if (existsSync9(fullPath) && !opts.force) {
6824
6853
  return { action: "exists", path: PR_MODE_WORKFLOW_PATH, substitution };
6825
6854
  }
6826
- ensureDir(dirname4(fullPath));
6855
+ ensureDir(dirname5(fullPath));
6827
6856
  writeFileSync7(fullPath, renderMirrorWorkflow(substitution));
6828
6857
  return { action: "wrote", path: PR_MODE_WORKFLOW_PATH, substitution };
6829
6858
  }
@@ -6831,9 +6860,9 @@ function derivePrModeSubstitution(repoRoot) {
6831
6860
  let host = null;
6832
6861
  let port = null;
6833
6862
  try {
6834
- const configFile = join5(repoRoot, ".stamp", "config.yml");
6863
+ const configFile = join6(repoRoot, ".stamp", "config.yml");
6835
6864
  if (existsSync9(configFile)) {
6836
- const cfg = parseConfigFromYaml(readFileSync8(configFile, "utf8"));
6865
+ const cfg = parseConfigFromYaml(readFileSync9(configFile, "utf8"));
6837
6866
  for (const rule of Object.values(cfg.branches)) {
6838
6867
  if (rule.review_server) {
6839
6868
  try {
@@ -7073,7 +7102,7 @@ function printPrModeWalkthrough(result) {
7073
7102
  );
7074
7103
  console.log();
7075
7104
  }
7076
- function renderVerifyWorkflow() {
7105
+ function renderVerifyWorkflow(actionSource = DEFAULT_ACTION_SOURCE) {
7077
7106
  return [
7078
7107
  "name: stamp verify",
7079
7108
  "",
@@ -7106,18 +7135,18 @@ function renderVerifyWorkflow() {
7106
7135
  " # would force per-step refetches.",
7107
7136
  " fetch-depth: 0",
7108
7137
  " - name: stamp/verify-attestation",
7109
- ` uses: OpenThinkAi/stamp-cli/.github/actions/verify-attestation@${VERIFY_ACTION_REF}`,
7138
+ ` uses: ${actionSource}/.github/actions/verify-attestation@${VERIFY_ACTION_REF}`,
7110
7139
  ""
7111
7140
  ].join("\n");
7112
7141
  }
7113
7142
 
7114
7143
  // src/commands/migrateServerAttested.ts
7115
- import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
7116
- import { dirname as dirname5, join as join7, relative } from "path";
7144
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
7145
+ import { dirname as dirname6, join as join8, relative } from "path";
7117
7146
 
7118
7147
  // src/lib/migrateServerAttested.ts
7119
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
7120
- import { join as join6 } from "path";
7148
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
7149
+ import { join as join7 } from "path";
7121
7150
  function detectExistingKeys(repoRoot, onSkip) {
7122
7151
  const dir = stampTrustedKeysDir(repoRoot);
7123
7152
  let entries;
@@ -7131,7 +7160,7 @@ function detectExistingKeys(repoRoot, onSkip) {
7131
7160
  const out = [];
7132
7161
  for (const filename of entries.sort()) {
7133
7162
  if (!filename.endsWith(".pub")) continue;
7134
- const full = join6(dir, filename);
7163
+ const full = join7(dir, filename);
7135
7164
  try {
7136
7165
  const st = statSync2(full);
7137
7166
  if (!st.isFile()) continue;
@@ -7140,7 +7169,7 @@ function detectExistingKeys(repoRoot, onSkip) {
7140
7169
  }
7141
7170
  let pem;
7142
7171
  try {
7143
- pem = readFileSync9(full, "utf8");
7172
+ pem = readFileSync10(full, "utf8");
7144
7173
  } catch (err) {
7145
7174
  onSkip?.(filename, err instanceof Error ? err.message : String(err));
7146
7175
  continue;
@@ -7328,7 +7357,7 @@ function runMigrateToServerAttested(opts = {}) {
7328
7357
  console.error(`note: skipping .stamp/trusted-keys/${filename} \u2014 ${reason}`);
7329
7358
  })
7330
7359
  );
7331
- const manifestPath = join7(repoRoot, MANIFEST_RELATIVE_PATH);
7360
+ const manifestPath = join8(repoRoot, MANIFEST_RELATIVE_PATH);
7332
7361
  const manifestExists = existsSync10(manifestPath);
7333
7362
  let adminFingerprints;
7334
7363
  if (manifestExists) {
@@ -7361,7 +7390,7 @@ function runMigrateToServerAttested(opts = {}) {
7361
7390
  `expected .stamp/config.yml at ${cfgPath} but found none. Run \`stamp init\` first.`
7362
7391
  );
7363
7392
  }
7364
- const cfgInput = readFileSync10(cfgPath, "utf8");
7393
+ const cfgInput = readFileSync11(cfgPath, "utf8");
7365
7394
  const rewrite = rewriteConfigForMigration(cfgInput);
7366
7395
  if (dryRun) {
7367
7396
  printDryRun({
@@ -7385,10 +7414,10 @@ function runMigrateToServerAttested(opts = {}) {
7385
7414
  console.error(`warning: ${w}`);
7386
7415
  }
7387
7416
  let manifestAction;
7388
- if (manifestExists && readFileSync10(manifestPath, "utf8") === manifestText) {
7417
+ if (manifestExists && readFileSync11(manifestPath, "utf8") === manifestText) {
7389
7418
  manifestAction = "unchanged";
7390
7419
  } else {
7391
- ensureDir(dirname5(manifestPath));
7420
+ ensureDir(dirname6(manifestPath));
7392
7421
  writeFileSync8(manifestPath, manifestText);
7393
7422
  manifestAction = "wrote";
7394
7423
  }
@@ -7454,7 +7483,7 @@ function readExistingAdminFingerprints(manifestPath) {
7454
7483
  const out = /* @__PURE__ */ new Set();
7455
7484
  let text;
7456
7485
  try {
7457
- text = readFileSync10(manifestPath, "utf8");
7486
+ text = readFileSync11(manifestPath, "utf8");
7458
7487
  } catch {
7459
7488
  return out;
7460
7489
  }
@@ -7529,9 +7558,9 @@ function indent(text, prefix) {
7529
7558
  import { spawnSync as spawnSync6 } from "child_process";
7530
7559
  import { request as httpRequest } from "http";
7531
7560
  import { request as httpsRequest } from "https";
7532
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
7561
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
7533
7562
  import { homedir as homedir2, hostname, userInfo } from "os";
7534
- import { join as join8 } from "path";
7563
+ import { join as join9 } from "path";
7535
7564
  import { createInterface as createInterface2 } from "readline/promises";
7536
7565
 
7537
7566
  // src/lib/inviteUrl.ts
@@ -7698,10 +7727,10 @@ function runInvitesMint(opts) {
7698
7727
  }
7699
7728
  }
7700
7729
  function defaultSshPubkeyPath() {
7701
- return join8(homedir2(), ".ssh", "id_ed25519.pub");
7730
+ return join9(homedir2(), ".ssh", "id_ed25519.pub");
7702
7731
  }
7703
7732
  function defaultStampPubkeyPath() {
7704
- return join8(homedir2(), ".stamp", "keys", "ed25519.pub");
7733
+ return join9(homedir2(), ".stamp", "keys", "ed25519.pub");
7705
7734
  }
7706
7735
  function defaultShortName() {
7707
7736
  const u = userInfo().username || "user";
@@ -7714,13 +7743,13 @@ function loadSshPubkey(path2) {
7714
7743
  `SSH pubkey not found at ${path2}. Generate one with \`ssh-keygen -t ed25519\` or pass --ssh-pubkey <path>.`
7715
7744
  );
7716
7745
  }
7717
- const raw = readFileSync11(path2, "utf8");
7746
+ const raw = readFileSync12(path2, "utf8");
7718
7747
  const parsed = parseSshPubkey(raw);
7719
7748
  return { path: path2, full: parsed.full, fingerprint: parsed.fingerprint };
7720
7749
  }
7721
7750
  function loadStampPubkey(path2) {
7722
7751
  if (!existsSync11(path2)) return null;
7723
- const pem = readFileSync11(path2, "utf8");
7752
+ const pem = readFileSync12(path2, "utf8");
7724
7753
  try {
7725
7754
  const fp = fingerprintFromPem(pem);
7726
7755
  return { path: path2, pem, fingerprint: fp };
@@ -7865,15 +7894,15 @@ async function runInvitesAccept(opts) {
7865
7894
 
7866
7895
  // src/commands/provision.ts
7867
7896
  import { spawnSync as spawnSync8 } from "child_process";
7868
- import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync12, rmSync, writeFileSync as writeFileSync10 } from "fs";
7897
+ import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
7869
7898
  import { tmpdir } from "os";
7870
- import { join as join9, resolve as resolvePath } from "path";
7899
+ import { join as join10, resolve as resolvePath } from "path";
7871
7900
  import { parse as parseYaml8 } from "yaml";
7872
7901
 
7873
7902
  // src/commands/server.ts
7874
7903
  import { spawnSync as spawnSync7 } from "child_process";
7875
7904
  import { existsSync as existsSync12, mkdirSync as mkdirSync4, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
7876
- import { dirname as dirname6 } from "path";
7905
+ import { dirname as dirname7 } from "path";
7877
7906
  import { stringify as stringifyYaml2 } from "yaml";
7878
7907
 
7879
7908
  // src/lib/perRepoKey.ts
@@ -8032,7 +8061,7 @@ function writeConfig(opts) {
8032
8061
  repoRootPrefix: opts.repoRootPrefix
8033
8062
  });
8034
8063
  const path2 = userServerConfigPath();
8035
- const dir = dirname6(path2);
8064
+ const dir = dirname7(path2);
8036
8065
  if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
8037
8066
  const tmp = `${path2}.tmp.${process.pid}`;
8038
8067
  writeFileSync9(tmp, yaml, { mode: 384 });
@@ -8345,9 +8374,9 @@ async function runMigrateExisting(opts, server2) {
8345
8374
  console.log("\n(dry run \u2014 no changes made)");
8346
8375
  return;
8347
8376
  }
8348
- const stagingDir = mkdtempSync(join9(tmpdir(), "stamp-migrate-"));
8349
- const bareCloneDir = join9(stagingDir, `${opts.name}.git`);
8350
- const tarballPath = join9(stagingDir, `${opts.name}.tar.gz`);
8377
+ const stagingDir = mkdtempSync(join10(tmpdir(), "stamp-migrate-"));
8378
+ const bareCloneDir = join10(stagingDir, `${opts.name}.git`);
8379
+ const tarballPath = join10(stagingDir, `${opts.name}.tar.gz`);
8351
8380
  try {
8352
8381
  console.log(`
8353
8382
  Building bare-clone tarball of existing repo`);
@@ -8383,9 +8412,9 @@ function ensureCwdIsGitRepo(cwd) {
8383
8412
  }
8384
8413
  }
8385
8414
  function ensureStampInitDone(cwd) {
8386
- if (!existsSync13(join9(cwd, ".stamp", "config.yml"))) {
8415
+ if (!existsSync13(join10(cwd, ".stamp", "config.yml"))) {
8387
8416
  throw new Error(
8388
- `--migrate-existing expects this repo to already be stamp-init'd (${join9(cwd, ".stamp/config.yml")} not found). Run \`stamp init --mode local-only\` first, calibrate your reviewers, then re-run with --migrate-existing.`
8417
+ `--migrate-existing expects this repo to already be stamp-init'd (${join10(cwd, ".stamp/config.yml")} not found). Run \`stamp init --mode local-only\` first, calibrate your reviewers, then re-run with --migrate-existing.`
8389
8418
  );
8390
8419
  }
8391
8420
  }
@@ -8510,7 +8539,7 @@ mirror.yml was added to .stamp/. Commit it through the normal stamp flow:`);
8510
8539
  }
8511
8540
  }
8512
8541
  function readMirrorYmlGithubRepo(repoRoot) {
8513
- const path2 = join9(repoRoot, ".stamp", "mirror.yml");
8542
+ const path2 = join10(repoRoot, ".stamp", "mirror.yml");
8514
8543
  if (!existsSync13(path2)) {
8515
8544
  throw new Error(
8516
8545
  `${path2} not found \u2014 --migrate-bypass operates on an already-server-gated repo, but this cwd has no .stamp/mirror.yml. If the repo is not yet server-gated, provision it first with \`stamp provision --migrate-existing\`.`
@@ -8518,7 +8547,7 @@ function readMirrorYmlGithubRepo(repoRoot) {
8518
8547
  }
8519
8548
  let raw;
8520
8549
  try {
8521
- raw = readFileSync12(path2, "utf8");
8550
+ raw = readFileSync13(path2, "utf8");
8522
8551
  } catch (err) {
8523
8552
  throw new Error(
8524
8553
  `could not read ${path2}: ${err instanceof Error ? err.message : String(err)}`
@@ -8758,10 +8787,10 @@ import {
8758
8787
  import {
8759
8788
  existsSync as existsSync14,
8760
8789
  readdirSync as readdirSync3,
8761
- readFileSync as readFileSync13,
8790
+ readFileSync as readFileSync14,
8762
8791
  writeFileSync as writeFileSync11
8763
8792
  } from "fs";
8764
- import { join as join10, resolve } from "path";
8793
+ import { join as join11, resolve } from "path";
8765
8794
  var USERS_EXIT = {
8766
8795
  OK: 0,
8767
8796
  CONFIG: 1,
@@ -8820,10 +8849,10 @@ function findExistingTrustedKey(repoRoot, fingerprint) {
8820
8849
  if (!existsSync14(dir)) return null;
8821
8850
  for (const f of readdirSync3(dir)) {
8822
8851
  if (!f.endsWith(".pub")) continue;
8823
- const fullPath = join10(dir, f);
8852
+ const fullPath = join11(dir, f);
8824
8853
  let pem;
8825
8854
  try {
8826
- pem = readFileSync13(fullPath, "utf8");
8855
+ pem = readFileSync14(fullPath, "utf8");
8827
8856
  } catch {
8828
8857
  continue;
8829
8858
  }
@@ -8875,7 +8904,7 @@ function runTrustGrant(opts) {
8875
8904
  );
8876
8905
  }
8877
8906
  const repoRoot = resolve(opts.repoPath ?? process.cwd());
8878
- if (!existsSync14(join10(repoRoot, ".git"))) {
8907
+ if (!existsSync14(join11(repoRoot, ".git"))) {
8879
8908
  throw new UsageError(`${repoRoot} is not a git repository`);
8880
8909
  }
8881
8910
  if (!existsSync14(stampConfigDir(repoRoot))) {
@@ -8917,7 +8946,7 @@ function runTrustGrant(opts) {
8917
8946
  runGit2(["checkout", "-b", branch], repoRoot);
8918
8947
  const keysDir = stampTrustedKeysDir(repoRoot);
8919
8948
  ensureDir(keysDir, 493);
8920
- const keyFile = join10(keysDir, `${opts.shortName}.pub`);
8949
+ const keyFile = join11(keysDir, `${opts.shortName}.pub`);
8921
8950
  writeFileSync11(keyFile, pem, { mode: 420 });
8922
8951
  runGit2(["add", keyFile], repoRoot);
8923
8952
  runGit2(
@@ -9115,8 +9144,8 @@ function runUsersRemove(opts) {
9115
9144
  }
9116
9145
 
9117
9146
  // src/commands/keys.ts
9118
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
9119
- import { basename, join as join11 } from "path";
9147
+ import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
9148
+ import { basename, join as join12 } from "path";
9120
9149
  function keysGenerate() {
9121
9150
  const existing = loadUserKeypair();
9122
9151
  if (existing) {
@@ -9160,7 +9189,7 @@ function keysList() {
9160
9189
  }
9161
9190
  for (const file of pubFiles.sort()) {
9162
9191
  try {
9163
- const pem = readFileSync14(join11(trustedDir, file), "utf8");
9192
+ const pem = readFileSync15(join12(trustedDir, file), "utf8");
9164
9193
  const fp = fingerprintFromPem(pem);
9165
9194
  const marker = local && fp === local.fingerprint ? " (you)" : "";
9166
9195
  console.log(` ${fp}${marker} [${file}]`);
@@ -9187,7 +9216,7 @@ function keysTrust(pubFile) {
9187
9216
  if (!existsSync15(pubFile)) {
9188
9217
  throw new Error(`public key file not found: ${pubFile}`);
9189
9218
  }
9190
- const pem = readFileSync14(pubFile, "utf8");
9219
+ const pem = readFileSync15(pubFile, "utf8");
9191
9220
  let fingerprint;
9192
9221
  try {
9193
9222
  fingerprint = fingerprintFromPem(pem);
@@ -9197,7 +9226,7 @@ function keysTrust(pubFile) {
9197
9226
  );
9198
9227
  }
9199
9228
  const filename = publicKeyFingerprintFilename(fingerprint);
9200
- const dest = join11(trustedDir, filename);
9229
+ const dest = join12(trustedDir, filename);
9201
9230
  if (existsSync15(dest)) {
9202
9231
  console.log(`${fingerprint} is already trusted (${basename(dest)})`);
9203
9232
  return;
@@ -9456,11 +9485,13 @@ function signPending(repoRoot, rawSha, opts) {
9456
9485
  }
9457
9486
  predictedSigner = opts.signerKeyId;
9458
9487
  }
9488
+ const manifestSnapshotSha256 = snapshotSha256(manifest);
9459
9489
  const signingBytes = trustAnchorSigningBytes({
9460
9490
  baseSha,
9461
9491
  headSha,
9462
9492
  targetBranch,
9463
9493
  diffSha256,
9494
+ manifestSnapshotSha256,
9464
9495
  approvals,
9465
9496
  checks: [],
9466
9497
  // see trustAnchorPayload.ts "Operational caveat"
@@ -9657,14 +9688,14 @@ function gitDiffBetween(repoRoot, base, head) {
9657
9688
 
9658
9689
  // src/commands/adminRotate.ts
9659
9690
  import { execFileSync as execFileSync6 } from "child_process";
9660
- import { copyFileSync, existsSync as existsSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
9661
- import { join as join12, relative as relative2 } from "path";
9691
+ import { copyFileSync, existsSync as existsSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
9692
+ import { join as join13, relative as relative2 } from "path";
9662
9693
  var NAME_PATTERN2 = /^[A-Za-z0-9_.-]+$/;
9663
9694
  var FINGERPRINT_PATTERN2 = /^sha256:[0-9a-f]{64}$/;
9664
9695
  var KNOWN_CAPABILITIES2 = ["admin", "operator", "server"];
9665
9696
  function runAdminAddKey(opts) {
9666
9697
  const repoRoot = findRepoRoot();
9667
- const manifestPath = join12(repoRoot, MANIFEST_RELATIVE_PATH);
9698
+ const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
9668
9699
  const trustedDir = stampTrustedKeysDir(repoRoot);
9669
9700
  if (!NAME_PATTERN2.test(opts.name)) {
9670
9701
  throw new Error(
@@ -9675,7 +9706,7 @@ function runAdminAddKey(opts) {
9675
9706
  if (!existsSync16(opts.pubkeyPath)) {
9676
9707
  throw new Error(`pubkey file not found: ${opts.pubkeyPath}`);
9677
9708
  }
9678
- const pem = readFileSync15(opts.pubkeyPath, "utf8");
9709
+ const pem = readFileSync16(opts.pubkeyPath, "utf8");
9679
9710
  if (!pem.includes("-----BEGIN PUBLIC KEY-----")) {
9680
9711
  throw new Error(
9681
9712
  `${opts.pubkeyPath} does not look like a public key PEM (no "-----BEGIN PUBLIC KEY-----" header). Refusing to import \u2014 did you accidentally pass the private key path? Public keys live at ~/.stamp/keys/ed25519.pub (note the .pub suffix).`
@@ -9694,7 +9725,7 @@ function runAdminAddKey(opts) {
9694
9725
  `no manifest at ${MANIFEST_RELATIVE_PATH}. Run \`stamp init --migrate-to-server-attested\` to bootstrap one, then re-run \`stamp admin add-key\`.`
9695
9726
  );
9696
9727
  }
9697
- const existing = parseManifest(readFileSync15(manifestPath, "utf8"));
9728
+ const existing = parseManifest(readFileSync16(manifestPath, "utf8"));
9698
9729
  if (!existing) {
9699
9730
  throw new Error(
9700
9731
  `${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file (see \`stamp admin list-keys\` for the current state) before adding a new key.`
@@ -9729,7 +9760,7 @@ function runAdminAddKey(opts) {
9729
9760
  }
9730
9761
  writeFileSync13(manifestPath, yamlText);
9731
9762
  const pubDestFilename = publicKeyFingerprintFilename(fingerprint);
9732
- const pubDestPath = join12(trustedDir, pubDestFilename);
9763
+ const pubDestPath = join13(trustedDir, pubDestFilename);
9733
9764
  if (!existsSync16(pubDestPath)) {
9734
9765
  copyFileSync(opts.pubkeyPath, pubDestPath);
9735
9766
  }
@@ -9754,7 +9785,7 @@ function runAdminAddKey(opts) {
9754
9785
  }
9755
9786
  function runAdminRevoke(opts) {
9756
9787
  const repoRoot = findRepoRoot();
9757
- const manifestPath = join12(repoRoot, MANIFEST_RELATIVE_PATH);
9788
+ const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
9758
9789
  if (!FINGERPRINT_PATTERN2.test(opts.fingerprint)) {
9759
9790
  throw new Error(
9760
9791
  `fingerprint ${JSON.stringify(opts.fingerprint)} is not in the expected sha256:<64-hex> form. Run \`stamp admin list-keys\` to copy a fingerprint exactly.`
@@ -9765,7 +9796,7 @@ function runAdminRevoke(opts) {
9765
9796
  `no manifest at ${MANIFEST_RELATIVE_PATH} \u2014 nothing to revoke.`
9766
9797
  );
9767
9798
  }
9768
- const existing = parseManifest(readFileSync15(manifestPath, "utf8"));
9799
+ const existing = parseManifest(readFileSync16(manifestPath, "utf8"));
9769
9800
  if (!existing) {
9770
9801
  throw new Error(
9771
9802
  `${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file before revoking.`
@@ -9826,7 +9857,7 @@ function runAdminRevoke(opts) {
9826
9857
  }
9827
9858
  function runAdminListKeys(opts = {}) {
9828
9859
  const repoRoot = findRepoRoot();
9829
- const manifestPath = join12(repoRoot, MANIFEST_RELATIVE_PATH);
9860
+ const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
9830
9861
  if (!existsSync16(manifestPath)) {
9831
9862
  if (opts.json) {
9832
9863
  process.stdout.write(JSON.stringify({ entries: [] }, null, 2) + "\n");
@@ -9838,7 +9869,7 @@ function runAdminListKeys(opts = {}) {
9838
9869
  );
9839
9870
  return;
9840
9871
  }
9841
- const manifest = parseManifest(readFileSync15(manifestPath, "utf8"));
9872
+ const manifest = parseManifest(readFileSync16(manifestPath, "utf8"));
9842
9873
  if (!manifest) {
9843
9874
  throw new Error(
9844
9875
  `${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file by hand (see \`git show HEAD:${MANIFEST_RELATIVE_PATH}\` for the last good version) before continuing.`
@@ -10184,6 +10215,7 @@ function parseEnvelope(bytes) {
10184
10215
  return null;
10185
10216
  }
10186
10217
  if (typeof p.diff_sha256 !== "string") return null;
10218
+ if (typeof p.manifest_snapshot_sha256 !== "string") return null;
10187
10219
  if (!Array.isArray(p.trust_anchor_signatures)) return null;
10188
10220
  if (typeof p.target_branch_tip_sha !== "string") return null;
10189
10221
  return env;
@@ -10405,7 +10437,6 @@ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re
10405
10437
  "diff_sha256",
10406
10438
  "base_sha",
10407
10439
  "head_sha",
10408
- "trusted_keys_snapshot_sha256",
10409
10440
  "issued_at",
10410
10441
  "server_key_id"
10411
10442
  ]) {
@@ -10480,6 +10511,7 @@ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re
10480
10511
  db.close();
10481
10512
  }
10482
10513
  const trustAnchorSignatures = [];
10514
+ const manifestSnapshot = snapshotSha256(manifest);
10483
10515
  const payload = {
10484
10516
  schema_version: PR_ATTESTATION_SCHEMA_VERSION,
10485
10517
  patch_id: input.patchId,
@@ -10488,6 +10520,7 @@ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re
10488
10520
  target_branch: input.targetBranch,
10489
10521
  target_branch_tip_sha: input.targetBranchTipSha,
10490
10522
  diff_sha256: diffSha256,
10523
+ manifest_snapshot_sha256: manifestSnapshot,
10491
10524
  approvals: entries,
10492
10525
  checks: [],
10493
10526
  // Phase-1 deliberate omission — see file-level comment.
@@ -10548,6 +10581,11 @@ function buildV2Envelope(input) {
10548
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.`
10549
10582
  );
10550
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
+ }
10551
10589
  const promptText = showAtRef(input.baseSha, def.prompt, input.repoRoot);
10552
10590
  const source = readReviewerSource2(a.reviewer, input.repoRoot);
10553
10591
  return {
@@ -10613,7 +10651,7 @@ function readReviewerSource2(reviewerName, repoRoot) {
10613
10651
 
10614
10652
  // src/commands/prune.ts
10615
10653
  import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
10616
- import { join as join13 } from "path";
10654
+ import { join as join14 } from "path";
10617
10655
 
10618
10656
  // src/lib/duration.ts
10619
10657
  function parseRetentionDuration(input) {
@@ -10642,8 +10680,8 @@ function runPrune(opts) {
10642
10680
  );
10643
10681
  const repoRoot = findRepoRoot();
10644
10682
  const dbPath = stampStateDbPath(repoRoot);
10645
- const parsesDir = join13(gitCommonDir(repoRoot), "stamp", "failed-parses");
10646
- const runsDir = join13(gitCommonDir(repoRoot), "stamp", "failed-runs");
10683
+ const parsesDir = join14(gitCommonDir(repoRoot), "stamp", "failed-parses");
10684
+ const runsDir = join14(gitCommonDir(repoRoot), "stamp", "failed-runs");
10647
10685
  const spoolCutoffMs = Date.now() - durationMs;
10648
10686
  if (!existsSync18(dbPath) && !existsSync18(parsesDir) && !existsSync18(runsDir)) {
10649
10687
  console.log(
@@ -10724,7 +10762,7 @@ function peekSpools(spoolDir, cutoffMs) {
10724
10762
  if (!existsSync18(spoolDir)) return [];
10725
10763
  const out = [];
10726
10764
  for (const entry of readdirSync5(spoolDir)) {
10727
- const filepath = join13(spoolDir, entry);
10765
+ const filepath = join14(spoolDir, entry);
10728
10766
  let stat;
10729
10767
  try {
10730
10768
  stat = statSync3(filepath);
@@ -10879,27 +10917,27 @@ function loadOrEmpty() {
10879
10917
  import { spawnSync as spawnSync15 } from "child_process";
10880
10918
  import {
10881
10919
  existsSync as existsSync21,
10882
- readFileSync as readFileSync17,
10920
+ readFileSync as readFileSync18,
10883
10921
  statSync as statSync4,
10884
10922
  unlinkSync as unlinkSync4,
10885
10923
  writeFileSync as writeFileSync15
10886
10924
  } from "fs";
10887
- import { join as join15, relative as relative3, resolve as resolve2 } from "path";
10925
+ import { join as join16, relative as relative3, resolve as resolve2 } from "path";
10888
10926
  import { parse as parseYaml10, stringify as stringifyYaml3 } from "yaml";
10889
10927
 
10890
10928
  // src/lib/reviewerLock.ts
10891
- import { existsSync as existsSync20, readFileSync as readFileSync16, writeFileSync as writeFileSync14 } from "fs";
10892
- import { join as join14 } from "path";
10929
+ import { existsSync as existsSync20, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "fs";
10930
+ import { join as join15 } from "path";
10893
10931
  var LOCK_FILE_VERSION = 1;
10894
10932
  var LOCK_DRIFT_EXIT = 3;
10895
10933
  function lockFilePath(repoRoot, reviewerName) {
10896
- return join14(repoRoot, ".stamp", "reviewers", `${reviewerName}.lock.json`);
10934
+ return join15(repoRoot, ".stamp", "reviewers", `${reviewerName}.lock.json`);
10897
10935
  }
10898
10936
  function readLockFile(repoRoot, reviewerName) {
10899
10937
  const path2 = lockFilePath(repoRoot, reviewerName);
10900
10938
  if (!existsSync20(path2)) return null;
10901
10939
  try {
10902
- const raw = readFileSync16(path2, "utf8");
10940
+ const raw = readFileSync17(path2, "utf8");
10903
10941
  const parsed = JSON.parse(raw);
10904
10942
  if (typeof parsed.version !== "number" || typeof parsed.source !== "string" || typeof parsed.ref !== "string" || typeof parsed.reviewer !== "string" || typeof parsed.prompt_sha256 !== "string" || typeof parsed.tools_sha256 !== "string" || typeof parsed.mcp_sha256 !== "string") {
10905
10943
  throw new Error(`malformed lock file at ${path2}`);
@@ -10920,13 +10958,18 @@ function checkReviewerDrift(repoRoot, reviewerName, def) {
10920
10958
  if (!lock) {
10921
10959
  return unpinnedResult();
10922
10960
  }
10923
- const promptPath = join14(repoRoot, def.prompt);
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
+ }
10966
+ const promptPath = join15(repoRoot, def.prompt);
10924
10967
  if (!existsSync20(promptPath)) {
10925
10968
  throw new Error(
10926
10969
  `reviewer "${reviewerName}" has a lock file but its prompt "${def.prompt}" does not exist on disk. Re-run 'stamp reviewers fetch ${reviewerName} --from ${lock.source}@${lock.ref}' to restore it, or delete the lock file to un-pin the reviewer.`
10927
10970
  );
10928
10971
  }
10929
- const promptBytes = readFileSync16(promptPath);
10972
+ const promptBytes = readFileSync17(promptPath);
10930
10973
  const observedPrompt = hashPromptBytes(promptBytes);
10931
10974
  const observedTools = hashTools(def.tools);
10932
10975
  const observedMcp = hashMcpServers(def.mcp_servers);
@@ -11005,6 +11048,10 @@ function reviewersList() {
11005
11048
  const maxNameLen = Math.max(...names.map((n) => n.length));
11006
11049
  for (const name of names) {
11007
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
+ }
11008
11055
  const abs = resolve2(repoRoot, def.prompt);
11009
11056
  let annotation = "";
11010
11057
  if (!existsSync21(abs)) {
@@ -11031,6 +11078,11 @@ function reviewersEdit(name) {
11031
11078
  `reviewer "${name}" is not configured. Run \`stamp reviewers list\` to see available reviewers.`
11032
11079
  );
11033
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
+ }
11034
11086
  const target = resolve2(repoRoot, def.prompt);
11035
11087
  launchEditor(target);
11036
11088
  }
@@ -11095,6 +11147,10 @@ function reviewersRemove(name, opts = {}) {
11095
11147
  delete config2.reviewers[name];
11096
11148
  writeFileSync15(configPath, stringifyConfig(config2));
11097
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
+ }
11098
11154
  if (opts.deleteFile) {
11099
11155
  const promptAbs = resolve2(repoRoot, def.prompt);
11100
11156
  if (existsSync21(promptAbs)) {
@@ -11128,8 +11184,13 @@ async function reviewersTest(name, diff) {
11128
11184
  console.log(` prompt sourced from working tree (test/iteration use case)`);
11129
11185
  console.log();
11130
11186
  const def = config2.reviewers[name];
11131
- const promptPath = join15(repoRoot, def.prompt);
11132
- const systemPrompt = readFileSync17(promptPath, "utf8");
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
+ }
11192
+ const promptPath = join16(repoRoot, def.prompt);
11193
+ const systemPrompt = readFileSync18(promptPath, "utf8");
11133
11194
  const result = await invokeReviewer({
11134
11195
  reviewer: name,
11135
11196
  config: config2,
@@ -11172,7 +11233,9 @@ function reviewersShow(name, opts) {
11172
11233
  const bar = "\u2500".repeat(72);
11173
11234
  console.log(bar);
11174
11235
  console.log(`reviewer: ${name}`);
11175
- console.log(`prompt: ${config2.reviewers[name].prompt}`);
11236
+ console.log(
11237
+ `prompt: ${config2.reviewers[name].prompt ?? "(server-bundled \u2014 no local prompt)"}`
11238
+ );
11176
11239
  console.log(bar);
11177
11240
  if (stats.total === 0) {
11178
11241
  console.log(" no verdicts recorded yet");
@@ -11267,7 +11330,7 @@ async function reviewersFetch(reviewerName, opts) {
11267
11330
  mcpServers = validateMcpServersFromSource(parsed.mcp_servers, source, ref);
11268
11331
  }
11269
11332
  }
11270
- const promptPath = join15(reviewersDir, `${reviewerName}.md`);
11333
+ const promptPath = join16(reviewersDir, `${reviewerName}.md`);
11271
11334
  const promptBytes = Buffer.from(promptText, "utf8");
11272
11335
  const promptSha = hashPromptBytes(promptBytes);
11273
11336
  const toolsSha = hashTools(tools);
@@ -11605,28 +11668,6 @@ function printGate(result, base_sha, head_sha) {
11605
11668
 
11606
11669
  // src/commands/update.ts
11607
11670
  import { spawnSync as spawnSync16 } from "child_process";
11608
-
11609
- // src/lib/version.ts
11610
- import { readFileSync as readFileSync18 } from "fs";
11611
- import { dirname as dirname7, join as join16 } from "path";
11612
- import { fileURLToPath as fileURLToPath2 } from "url";
11613
- function readPackageVersion() {
11614
- const here = dirname7(fileURLToPath2(import.meta.url));
11615
- for (let dir = here, i = 0; i < 6; i++) {
11616
- try {
11617
- const raw = readFileSync18(join16(dir, "package.json"), "utf8");
11618
- const pkg = JSON.parse(raw);
11619
- if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
11620
- } catch {
11621
- }
11622
- const parent = dirname7(dir);
11623
- if (parent === dir) break;
11624
- dir = parent;
11625
- }
11626
- throw new Error("could not locate @openthink/stamp package.json to read version");
11627
- }
11628
-
11629
- // src/commands/update.ts
11630
11671
  var PKG_NAME = "@openthink/stamp";
11631
11672
  function compareSemver(a, b) {
11632
11673
  const parse2 = (v) => (v.split("-")[0] ?? "0.0.0").split(".").map((n) => parseInt(n, 10) || 0);
@@ -11840,6 +11881,12 @@ function verifyReviewerHashes(sha, payload, repoRoot, config2) {
11840
11881
  `v2 attestation: reviewer "${approval.reviewer}" is in payload but not defined in config.reviewers at the merge commit`
11841
11882
  );
11842
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
+ }
11843
11890
  const promptBytes = tryGitShow(`${sha}:${def.prompt}`, repoRoot);
11844
11891
  if (promptBytes === null) {
11845
11892
  fail(
@@ -11972,6 +12019,7 @@ function verifyV3Envelope(envelope, opts, resolved, patch_id, repoRoot) {
11972
12019
  head_sha: envelope.payload.head_sha,
11973
12020
  target_branch: envelope.payload.target_branch,
11974
12021
  diff_sha256: envelope.payload.diff_sha256,
12022
+ manifest_snapshot_sha256: envelope.payload.manifest_snapshot_sha256,
11975
12023
  approvals: envelope.payload.approvals,
11976
12024
  checks: envelope.payload.checks,
11977
12025
  trust_anchor_signatures: envelope.payload.trust_anchor_signatures,
@@ -12228,6 +12276,9 @@ program.command("init").description(
12228
12276
  ).option(
12229
12277
  "--pr-mode-force",
12230
12278
  "with --pr-mode, overwrite an existing .github/workflows/stamp-mirror.yml (useful when re-running after configuring review_server so the host/port placeholders fill in)"
12279
+ ).option(
12280
+ "--action-source <org/repo>",
12281
+ "GitHub repo that hosts the stamp/verify-attestation action used by .github/workflows/stamp-verify.yml. Default 'OpenThinkAi/stamp-cli'. Override when consuming a fork (e.g. 'Anglepoint-Inc/anglepoint-stamp-server') so the workflow tracks your fork's updates instead of the upstream."
12231
12282
  ).action(
12232
12283
  (opts) => {
12233
12284
  try {
@@ -12235,6 +12286,11 @@ program.command("init").description(
12235
12286
  runMigrateToServerAttested({ dryRun: opts.dryRun === true });
12236
12287
  return;
12237
12288
  }
12289
+ if (opts.dryRun === true) {
12290
+ throw new Error(
12291
+ "--dry-run is supported only with --migrate-to-server-attested. Re-run with --migrate-to-server-attested to preview the migration scaffold, or drop --dry-run to run the requested init operation."
12292
+ );
12293
+ }
12238
12294
  let mode;
12239
12295
  if (opts.mode === void 0) {
12240
12296
  mode = void 0;
@@ -12245,6 +12301,13 @@ program.command("init").description(
12245
12301
  `--mode must be 'server-gated' or 'local-only' (got "${opts.mode}")`
12246
12302
  );
12247
12303
  }
12304
+ if (opts.actionSource !== void 0) {
12305
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(opts.actionSource)) {
12306
+ throw new Error(
12307
+ `--action-source must be of the form 'org/repo' (got "${opts.actionSource}")`
12308
+ );
12309
+ }
12310
+ }
12248
12311
  runInit({
12249
12312
  minimal: opts.minimal,
12250
12313
  agentsMd: opts.agentsMd,
@@ -12260,7 +12323,8 @@ program.command("init").description(
12260
12323
  // default fires when the operator hasn't opted out.
12261
12324
  prCheck: opts.prCheck === false ? false : void 0,
12262
12325
  prMode: opts.prMode === true,
12263
- prModeForce: opts.prModeForce === true
12326
+ prModeForce: opts.prModeForce === true,
12327
+ actionSource: opts.actionSource
12264
12328
  });
12265
12329
  } catch (err) {
12266
12330
  const message = err instanceof Error ? err.message : String(err);
@@ -12593,7 +12657,7 @@ program.command("prune").description(
12593
12657
  });
12594
12658
  program.command("ui").description("launch the interactive terminal UI").action(async () => {
12595
12659
  try {
12596
- const { runUi } = await import("./ui-P5DRAT3P.js");
12660
+ const { runUi } = await import("./ui-67BDQPER.js");
12597
12661
  runUi();
12598
12662
  } catch (err) {
12599
12663
  handleCliError(err);