@openthink/stamp 2.0.0 → 2.0.2

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
@@ -59,8 +59,8 @@ 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";
@@ -1567,11 +1567,31 @@ function parseResponseJson(raw) {
1567
1567
  `review_server response: top-level verdict (${verdict}) and approval.verdict (${a.verdict}) disagree`
1568
1568
  );
1569
1569
  }
1570
+ let prAttestationV3 = null;
1571
+ const payloadB64 = obj["pr_attestation_v3_payload_b64"];
1572
+ const sigB64 = obj["pr_attestation_v3_signature_b64"];
1573
+ if (payloadB64 !== void 0 || sigB64 !== void 0) {
1574
+ if (typeof payloadB64 !== "string" || !payloadB64) {
1575
+ throw new Error(
1576
+ `review_server response.pr_attestation_v3_payload_b64 must be a non-empty string when present`
1577
+ );
1578
+ }
1579
+ if (typeof sigB64 !== "string" || !sigB64) {
1580
+ throw new Error(
1581
+ `review_server response.pr_attestation_v3_signature_b64 must be a non-empty string when present`
1582
+ );
1583
+ }
1584
+ prAttestationV3 = {
1585
+ payloadBytes: Buffer.from(payloadB64, "base64"),
1586
+ signatureB64: sigB64
1587
+ };
1588
+ }
1570
1589
  return {
1571
1590
  verdict,
1572
1591
  prose: obj.prose,
1573
1592
  approval,
1574
- signature: obj.signature
1593
+ signature: obj.signature,
1594
+ prAttestationV3
1575
1595
  };
1576
1596
  }
1577
1597
  function verifyServerSignature(opts) {
@@ -1709,12 +1729,26 @@ async function requestServerReview(input) {
1709
1729
  `review_server returned a signed approval for diff_sha256 ${parsed.approval.diff_sha256} but we sent diff_sha256 ${diffSha256} \u2014 refusing.`
1710
1730
  );
1711
1731
  }
1732
+ if (parsed.prAttestationV3) {
1733
+ const localCanonical = canonicalSerializeApproval(parsed.approval);
1734
+ if (!parsed.prAttestationV3.payloadBytes.equals(localCanonical)) {
1735
+ throw new Error(
1736
+ `review_server returned pr_attestation_v3_payload_b64 bytes that do not match canonicalSerializeApproval(approval) recomputed locally \u2014 refusing. This indicates a server/client canonicalizer drift (server stamp version mismatch?). Server bytes (first 80): ${JSON.stringify(parsed.prAttestationV3.payloadBytes.toString("utf8").slice(0, 80))}; client bytes (first 80): ${JSON.stringify(localCanonical.toString("utf8").slice(0, 80))}.`
1737
+ );
1738
+ }
1739
+ if (parsed.prAttestationV3.signatureB64 !== parsed.signature) {
1740
+ throw new Error(
1741
+ `review_server returned pr_attestation_v3_signature_b64 that disagrees with the top-level signature \u2014 refusing.`
1742
+ );
1743
+ }
1744
+ }
1712
1745
  return {
1713
1746
  verdict: parsed.verdict,
1714
1747
  prose: parsed.prose,
1715
1748
  approval: parsed.approval,
1716
1749
  signature: parsed.signature,
1717
- approvalJson: JSON.stringify(parsed.approval)
1750
+ approvalJson: JSON.stringify(parsed.approval),
1751
+ prAttestationV3: parsed.prAttestationV3
1718
1752
  };
1719
1753
  }
1720
1754
  function exitCodeHint(exitCode) {
@@ -3132,9 +3166,38 @@ function readLineSync() {
3132
3166
  return out;
3133
3167
  }
3134
3168
 
3169
+ // src/lib/version.ts
3170
+ import { readFileSync as readFileSync4 } from "fs";
3171
+ import { dirname, join as join3 } from "path";
3172
+ import { fileURLToPath as fileURLToPath2 } from "url";
3173
+ function readPackageVersion() {
3174
+ const here = dirname(fileURLToPath2(import.meta.url));
3175
+ for (let dir = here, i = 0; i < 6; i++) {
3176
+ try {
3177
+ const raw = readFileSync4(join3(dir, "package.json"), "utf8");
3178
+ const pkg = JSON.parse(raw);
3179
+ if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
3180
+ } catch {
3181
+ }
3182
+ const parent = dirname(dir);
3183
+ if (parent === dir) break;
3184
+ dir = parent;
3185
+ }
3186
+ throw new Error("could not locate @openthink/stamp package.json to read version");
3187
+ }
3188
+
3135
3189
  // src/lib/deprecationNotice.ts
3136
- function maybePrintDeprecationNotice() {
3190
+ function maybePrintDeprecationNotice(versionOverride) {
3137
3191
  if (process.env.STAMP_SUPPRESS_DEPRECATION === "1") return;
3192
+ let major = null;
3193
+ try {
3194
+ const version = versionOverride ?? readPackageVersion();
3195
+ const m = /^(\d+)\./.exec(version);
3196
+ if (m) major = Number.parseInt(m[1], 10);
3197
+ } catch {
3198
+ return;
3199
+ }
3200
+ if (major === null || major >= 2) return;
3138
3201
  process.stderr.write(
3139
3202
  "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"
3140
3203
  );
@@ -3729,12 +3792,12 @@ ${close}`;
3729
3792
  import {
3730
3793
  existsSync as existsSync3,
3731
3794
  mkdirSync,
3732
- readFileSync as readFileSync4,
3795
+ readFileSync as readFileSync5,
3733
3796
  renameSync,
3734
3797
  unlinkSync,
3735
3798
  writeFileSync as writeFileSync2
3736
3799
  } from "fs";
3737
- import { dirname } from "path";
3800
+ import { dirname as dirname2 } from "path";
3738
3801
  import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
3739
3802
  var DEFAULT_REVIEWER_MODELS = {
3740
3803
  security: "claude-sonnet-4-6",
@@ -3754,7 +3817,7 @@ function loadUserConfig() {
3754
3817
  if (!existsSync3(path2)) return null;
3755
3818
  let raw;
3756
3819
  try {
3757
- raw = readFileSync4(path2, "utf8");
3820
+ raw = readFileSync5(path2, "utf8");
3758
3821
  } catch (err) {
3759
3822
  throw new Error(
3760
3823
  `failed to read ${path2}: ${err instanceof Error ? err.message : String(err)}`
@@ -3814,7 +3877,7 @@ function stringifyUserConfig(cfg) {
3814
3877
  }
3815
3878
  function writeUserConfig(cfg) {
3816
3879
  const path2 = userConfigPath();
3817
- const dir = dirname(path2);
3880
+ const dir = dirname2(path2);
3818
3881
  if (!existsSync3(dir)) mkdirSync(dir, { recursive: true, mode: 448 });
3819
3882
  const tmp = `${path2}.tmp.${process.pid}`;
3820
3883
  writeFileSync2(tmp, stringifyUserConfig(cfg), { mode: 384 });
@@ -4625,7 +4688,7 @@ function stripLastLineVerdict(text) {
4625
4688
 
4626
4689
  // src/lib/llmNotice.ts
4627
4690
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
4628
- import { dirname as dirname2 } from "path";
4691
+ import { dirname as dirname3 } from "path";
4629
4692
  function maybePrintLlmNotice(repoRoot) {
4630
4693
  if (process.env.STAMP_SUPPRESS_LLM_NOTICE === "1") return;
4631
4694
  const marker = stampLlmNoticeMarkerPath(repoRoot);
@@ -4638,7 +4701,7 @@ function maybePrintLlmNotice(repoRoot) {
4638
4701
  `
4639
4702
  );
4640
4703
  try {
4641
- mkdirSync3(dirname2(marker), { recursive: true });
4704
+ mkdirSync3(dirname3(marker), { recursive: true });
4642
4705
  writeFileSync4(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
4643
4706
  `);
4644
4707
  } catch {
@@ -4878,7 +4941,7 @@ import { spawnSync as spawnSync4 } from "child_process";
4878
4941
  import { createInterface } from "readline";
4879
4942
 
4880
4943
  // src/lib/serverConfig.ts
4881
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
4944
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
4882
4945
  import { parse as parseYaml7 } from "yaml";
4883
4946
  var DEFAULT_USER = "git";
4884
4947
  var DEFAULT_REPO_ROOT = "/srv/git";
@@ -4908,7 +4971,7 @@ function loadServerConfig() {
4908
4971
  if (!existsSync5(path2)) return null;
4909
4972
  let raw;
4910
4973
  try {
4911
- raw = readFileSync5(path2, "utf8");
4974
+ raw = readFileSync6(path2, "utf8");
4912
4975
  } catch (err) {
4913
4976
  throw new Error(
4914
4977
  `failed to read ${path2}: ${err instanceof Error ? err.message : String(err)}`
@@ -5887,27 +5950,27 @@ function readSeedDir(seedDir) {
5887
5950
  if (!existsSync7(seedDir) || !statSync(seedDir).isDirectory()) {
5888
5951
  throw new Error(`--from path is not a directory: ${seedDir}`);
5889
5952
  }
5890
- const configPath = join3(seedDir, "config.yml");
5953
+ const configPath = join4(seedDir, "config.yml");
5891
5954
  if (!existsSync7(configPath)) {
5892
5955
  throw new Error(`--from dir missing config.yml: ${configPath}`);
5893
5956
  }
5894
- const reviewersDir = join3(seedDir, "reviewers");
5957
+ const reviewersDir = join4(seedDir, "reviewers");
5895
5958
  if (!existsSync7(reviewersDir) || !statSync(reviewersDir).isDirectory()) {
5896
5959
  throw new Error(`--from dir missing reviewers/ subdirectory: ${reviewersDir}`);
5897
5960
  }
5898
- const yaml = readFileSync6(configPath, "utf8");
5961
+ const yaml = readFileSync7(configPath, "utf8");
5899
5962
  const config2 = parseConfigFromYaml(yaml);
5900
5963
  const reviewerFiles = /* @__PURE__ */ new Map();
5901
5964
  for (const entry of readdirSync(reviewersDir)) {
5902
- const full = join3(reviewersDir, entry);
5965
+ const full = join4(reviewersDir, entry);
5903
5966
  if (statSync(full).isFile()) {
5904
- reviewerFiles.set(`.stamp/reviewers/${entry}`, readFileSync6(full, "utf8"));
5967
+ reviewerFiles.set(`.stamp/reviewers/${entry}`, readFileSync7(full, "utf8"));
5905
5968
  }
5906
5969
  }
5907
5970
  let mirrorYml;
5908
- const mirrorPath = join3(seedDir, "mirror.yml");
5971
+ const mirrorPath = join4(seedDir, "mirror.yml");
5909
5972
  if (existsSync7(mirrorPath)) {
5910
- mirrorYml = readFileSync6(mirrorPath, "utf8");
5973
+ mirrorYml = readFileSync7(mirrorPath, "utf8");
5911
5974
  }
5912
5975
  return { config: config2, reviewerFiles, mirrorYml };
5913
5976
  }
@@ -5915,12 +5978,12 @@ function writeBootstrapFiles(repoRoot, plan) {
5915
5978
  ensureDir(stampConfigDir(repoRoot));
5916
5979
  ensureDir(stampReviewersDir(repoRoot));
5917
5980
  for (const { path: path2, content } of plan.reviewerFiles.values()) {
5918
- const full = join3(repoRoot, path2);
5919
- ensureDir(dirname3(full));
5981
+ const full = join4(repoRoot, path2);
5982
+ ensureDir(dirname4(full));
5920
5983
  writeFileSync5(full, content);
5921
5984
  }
5922
5985
  if (plan.mirrorYml !== void 0) {
5923
- writeFileSync5(join3(repoRoot, ".stamp/mirror.yml"), plan.mirrorYml);
5986
+ writeFileSync5(join4(repoRoot, ".stamp/mirror.yml"), plan.mirrorYml);
5924
5987
  }
5925
5988
  writeFileSync5(stampConfigFile(repoRoot), stringifyConfig(plan.newConfig));
5926
5989
  }
@@ -5963,8 +6026,8 @@ function branchExists(name, cwd) {
5963
6026
  }
5964
6027
 
5965
6028
  // src/commands/init.ts
5966
- import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
5967
- import { dirname as dirname4, join as join5 } from "path";
6029
+ import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
6030
+ import { dirname as dirname5, join as join6 } from "path";
5968
6031
 
5969
6032
  // src/lib/ghRuleset.ts
5970
6033
  import { spawnSync as spawnSync5 } from "child_process";
@@ -6300,14 +6363,14 @@ function computeDesiredBypassActors(current, deployKeyId, flags) {
6300
6363
  }
6301
6364
 
6302
6365
  // src/lib/oteamConfig.ts
6303
- import { existsSync as existsSync8, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
6366
+ import { existsSync as existsSync8, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
6304
6367
  import { homedir } from "os";
6305
- import { join as join4 } from "path";
6306
- var OTEAM_CONFIG_PATH = join4(homedir(), ".open-team", "config.json");
6368
+ import { join as join5 } from "path";
6369
+ var OTEAM_CONFIG_PATH = join5(homedir(), ".open-team", "config.json");
6307
6370
  function readOteamConfig(configPath = OTEAM_CONFIG_PATH) {
6308
6371
  if (!existsSync8(configPath)) return null;
6309
6372
  try {
6310
- const parsed = JSON.parse(readFileSync7(configPath, "utf8"));
6373
+ const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
6311
6374
  if (Array.isArray(parsed)) {
6312
6375
  throw new Error("config must be a JSON object, not an array");
6313
6376
  }
@@ -6322,7 +6385,7 @@ function patchStampHost(host, configPath = OTEAM_CONFIG_PATH) {
6322
6385
  let config2 = {};
6323
6386
  if (existsSync8(configPath)) {
6324
6387
  try {
6325
- const parsed = JSON.parse(readFileSync7(configPath, "utf8"));
6388
+ const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
6326
6389
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
6327
6390
  config2 = parsed;
6328
6391
  }
@@ -6348,6 +6411,7 @@ function patchStampHost(host, configPath = OTEAM_CONFIG_PATH) {
6348
6411
  }
6349
6412
 
6350
6413
  // src/commands/init.ts
6414
+ var DEFAULT_ACTION_SOURCE = "OpenThinkAi/stamp-cli";
6351
6415
  function runInit(opts = {}) {
6352
6416
  maybePrintDeprecationNotice();
6353
6417
  const repoRoot = findRepoRoot();
@@ -6378,26 +6442,26 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
6378
6442
  if (!alreadyHasConfig) {
6379
6443
  if (opts.minimal) {
6380
6444
  writeFileSync7(configFile, stringifyConfig(MINIMAL_CONFIG));
6381
- writeFileSync7(join5(reviewersDir, "example.md"), EXAMPLE_REVIEWER_PROMPT);
6445
+ writeFileSync7(join6(reviewersDir, "example.md"), EXAMPLE_REVIEWER_PROMPT);
6382
6446
  } else {
6383
6447
  writeFileSync7(configFile, stringifyConfig(DEFAULT_CONFIG));
6384
6448
  writeFileSync7(
6385
- join5(reviewersDir, "security.md"),
6449
+ join6(reviewersDir, "security.md"),
6386
6450
  DEFAULT_SECURITY_PROMPT
6387
6451
  );
6388
6452
  writeFileSync7(
6389
- join5(reviewersDir, "standards.md"),
6453
+ join6(reviewersDir, "standards.md"),
6390
6454
  DEFAULT_STANDARDS_PROMPT
6391
6455
  );
6392
6456
  writeFileSync7(
6393
- join5(reviewersDir, "product.md"),
6457
+ join6(reviewersDir, "product.md"),
6394
6458
  DEFAULT_PRODUCT_PROMPT
6395
6459
  );
6396
6460
  }
6397
6461
  }
6398
6462
  const { keypair, created: keyCreated } = ensureUserKeypair();
6399
6463
  const userCfg = loadOrCreateUserConfig();
6400
- const pubKeyPath = join5(
6464
+ const pubKeyPath = join6(
6401
6465
  trustedKeysDir,
6402
6466
  publicKeyFingerprintFilename(keypair.fingerprint)
6403
6467
  );
@@ -6411,7 +6475,8 @@ For local-only / advisory use against this GitHub repo: re-run with \`stamp init
6411
6475
  const prCheckResult = maybeWriteVerifyWorkflow(
6412
6476
  repoRoot,
6413
6477
  opts.prCheck,
6414
- effectiveMode
6478
+ effectiveMode,
6479
+ opts.actionSource ?? DEFAULT_ACTION_SOURCE
6415
6480
  );
6416
6481
  const prModeResult = opts.prMode ? maybeWritePrModeMirrorWorkflow(repoRoot, { force: opts.prModeForce }) : { action: "skipped", path: PR_MODE_WORKFLOW_PATH, substitution: null };
6417
6482
  const agentsMdAction = opts.agentsMd === false ? "skipped" : ensureAgentsMd(repoRoot, effectiveMode);
@@ -6563,8 +6628,8 @@ function runBootstrapCommit(repoRoot, scaffoldOrSync) {
6563
6628
  return { kind: "skipped-already-tracked" };
6564
6629
  }
6565
6630
  const toAdd = [".stamp"];
6566
- if (existsSync9(join5(repoRoot, "AGENTS.md"))) toAdd.push("AGENTS.md");
6567
- if (existsSync9(join5(repoRoot, "CLAUDE.md"))) toAdd.push("CLAUDE.md");
6631
+ if (existsSync9(join6(repoRoot, "AGENTS.md"))) toAdd.push("AGENTS.md");
6632
+ if (existsSync9(join6(repoRoot, "CLAUDE.md"))) toAdd.push("CLAUDE.md");
6568
6633
  runGit(["add", ...toAdd], repoRoot);
6569
6634
  let hasStagedChanges = false;
6570
6635
  try {
@@ -6769,27 +6834,27 @@ function resolveMode(userMode, remoteClass) {
6769
6834
  }
6770
6835
  }
6771
6836
  var VERIFY_ACTION_REF = "v1.6.1";
6772
- function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode) {
6837
+ function maybeWriteVerifyWorkflow(repoRoot, prCheckOpt, effectiveMode, actionSource = DEFAULT_ACTION_SOURCE) {
6773
6838
  const path2 = ".github/workflows/stamp-verify.yml";
6774
- const fullPath = join5(repoRoot, path2);
6839
+ const fullPath = join6(repoRoot, path2);
6775
6840
  const defaultForMode = effectiveMode !== "server-gated";
6776
6841
  const shouldWrite = prCheckOpt ?? defaultForMode;
6777
6842
  if (!shouldWrite) return { action: "skipped", path: path2 };
6778
6843
  if (existsSync9(fullPath)) {
6779
6844
  return { action: "exists", path: path2 };
6780
6845
  }
6781
- ensureDir(dirname4(fullPath));
6782
- writeFileSync7(fullPath, renderVerifyWorkflow());
6846
+ ensureDir(dirname5(fullPath));
6847
+ writeFileSync7(fullPath, renderVerifyWorkflow(actionSource));
6783
6848
  return { action: "wrote", path: path2 };
6784
6849
  }
6785
6850
  var PR_MODE_WORKFLOW_PATH = ".github/workflows/stamp-mirror.yml";
6786
6851
  function maybeWritePrModeMirrorWorkflow(repoRoot, opts = {}) {
6787
- const fullPath = join5(repoRoot, PR_MODE_WORKFLOW_PATH);
6852
+ const fullPath = join6(repoRoot, PR_MODE_WORKFLOW_PATH);
6788
6853
  const substitution = derivePrModeSubstitution(repoRoot);
6789
6854
  if (existsSync9(fullPath) && !opts.force) {
6790
6855
  return { action: "exists", path: PR_MODE_WORKFLOW_PATH, substitution };
6791
6856
  }
6792
- ensureDir(dirname4(fullPath));
6857
+ ensureDir(dirname5(fullPath));
6793
6858
  writeFileSync7(fullPath, renderMirrorWorkflow(substitution));
6794
6859
  return { action: "wrote", path: PR_MODE_WORKFLOW_PATH, substitution };
6795
6860
  }
@@ -6797,9 +6862,9 @@ function derivePrModeSubstitution(repoRoot) {
6797
6862
  let host = null;
6798
6863
  let port = null;
6799
6864
  try {
6800
- const configFile = join5(repoRoot, ".stamp", "config.yml");
6865
+ const configFile = join6(repoRoot, ".stamp", "config.yml");
6801
6866
  if (existsSync9(configFile)) {
6802
- const cfg = parseConfigFromYaml(readFileSync8(configFile, "utf8"));
6867
+ const cfg = parseConfigFromYaml(readFileSync9(configFile, "utf8"));
6803
6868
  for (const rule of Object.values(cfg.branches)) {
6804
6869
  if (rule.review_server) {
6805
6870
  try {
@@ -7039,7 +7104,7 @@ function printPrModeWalkthrough(result) {
7039
7104
  );
7040
7105
  console.log();
7041
7106
  }
7042
- function renderVerifyWorkflow() {
7107
+ function renderVerifyWorkflow(actionSource = DEFAULT_ACTION_SOURCE) {
7043
7108
  return [
7044
7109
  "name: stamp verify",
7045
7110
  "",
@@ -7072,18 +7137,18 @@ function renderVerifyWorkflow() {
7072
7137
  " # would force per-step refetches.",
7073
7138
  " fetch-depth: 0",
7074
7139
  " - name: stamp/verify-attestation",
7075
- ` uses: OpenThinkAi/stamp-cli/.github/actions/verify-attestation@${VERIFY_ACTION_REF}`,
7140
+ ` uses: ${actionSource}/.github/actions/verify-attestation@${VERIFY_ACTION_REF}`,
7076
7141
  ""
7077
7142
  ].join("\n");
7078
7143
  }
7079
7144
 
7080
7145
  // src/commands/migrateServerAttested.ts
7081
- import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
7082
- import { dirname as dirname5, join as join7, relative } from "path";
7146
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
7147
+ import { dirname as dirname6, join as join8, relative } from "path";
7083
7148
 
7084
7149
  // src/lib/migrateServerAttested.ts
7085
- import { readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
7086
- import { join as join6 } from "path";
7150
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
7151
+ import { join as join7 } from "path";
7087
7152
  function detectExistingKeys(repoRoot, onSkip) {
7088
7153
  const dir = stampTrustedKeysDir(repoRoot);
7089
7154
  let entries;
@@ -7097,7 +7162,7 @@ function detectExistingKeys(repoRoot, onSkip) {
7097
7162
  const out = [];
7098
7163
  for (const filename of entries.sort()) {
7099
7164
  if (!filename.endsWith(".pub")) continue;
7100
- const full = join6(dir, filename);
7165
+ const full = join7(dir, filename);
7101
7166
  try {
7102
7167
  const st = statSync2(full);
7103
7168
  if (!st.isFile()) continue;
@@ -7106,7 +7171,7 @@ function detectExistingKeys(repoRoot, onSkip) {
7106
7171
  }
7107
7172
  let pem;
7108
7173
  try {
7109
- pem = readFileSync9(full, "utf8");
7174
+ pem = readFileSync10(full, "utf8");
7110
7175
  } catch (err) {
7111
7176
  onSkip?.(filename, err instanceof Error ? err.message : String(err));
7112
7177
  continue;
@@ -7294,7 +7359,7 @@ function runMigrateToServerAttested(opts = {}) {
7294
7359
  console.error(`note: skipping .stamp/trusted-keys/${filename} \u2014 ${reason}`);
7295
7360
  })
7296
7361
  );
7297
- const manifestPath = join7(repoRoot, MANIFEST_RELATIVE_PATH);
7362
+ const manifestPath = join8(repoRoot, MANIFEST_RELATIVE_PATH);
7298
7363
  const manifestExists = existsSync10(manifestPath);
7299
7364
  let adminFingerprints;
7300
7365
  if (manifestExists) {
@@ -7327,7 +7392,7 @@ function runMigrateToServerAttested(opts = {}) {
7327
7392
  `expected .stamp/config.yml at ${cfgPath} but found none. Run \`stamp init\` first.`
7328
7393
  );
7329
7394
  }
7330
- const cfgInput = readFileSync10(cfgPath, "utf8");
7395
+ const cfgInput = readFileSync11(cfgPath, "utf8");
7331
7396
  const rewrite = rewriteConfigForMigration(cfgInput);
7332
7397
  if (dryRun) {
7333
7398
  printDryRun({
@@ -7351,10 +7416,10 @@ function runMigrateToServerAttested(opts = {}) {
7351
7416
  console.error(`warning: ${w}`);
7352
7417
  }
7353
7418
  let manifestAction;
7354
- if (manifestExists && readFileSync10(manifestPath, "utf8") === manifestText) {
7419
+ if (manifestExists && readFileSync11(manifestPath, "utf8") === manifestText) {
7355
7420
  manifestAction = "unchanged";
7356
7421
  } else {
7357
- ensureDir(dirname5(manifestPath));
7422
+ ensureDir(dirname6(manifestPath));
7358
7423
  writeFileSync8(manifestPath, manifestText);
7359
7424
  manifestAction = "wrote";
7360
7425
  }
@@ -7420,7 +7485,7 @@ function readExistingAdminFingerprints(manifestPath) {
7420
7485
  const out = /* @__PURE__ */ new Set();
7421
7486
  let text;
7422
7487
  try {
7423
- text = readFileSync10(manifestPath, "utf8");
7488
+ text = readFileSync11(manifestPath, "utf8");
7424
7489
  } catch {
7425
7490
  return out;
7426
7491
  }
@@ -7495,9 +7560,9 @@ function indent(text, prefix) {
7495
7560
  import { spawnSync as spawnSync6 } from "child_process";
7496
7561
  import { request as httpRequest } from "http";
7497
7562
  import { request as httpsRequest } from "https";
7498
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
7563
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
7499
7564
  import { homedir as homedir2, hostname, userInfo } from "os";
7500
- import { join as join8 } from "path";
7565
+ import { join as join9 } from "path";
7501
7566
  import { createInterface as createInterface2 } from "readline/promises";
7502
7567
 
7503
7568
  // src/lib/inviteUrl.ts
@@ -7664,10 +7729,10 @@ function runInvitesMint(opts) {
7664
7729
  }
7665
7730
  }
7666
7731
  function defaultSshPubkeyPath() {
7667
- return join8(homedir2(), ".ssh", "id_ed25519.pub");
7732
+ return join9(homedir2(), ".ssh", "id_ed25519.pub");
7668
7733
  }
7669
7734
  function defaultStampPubkeyPath() {
7670
- return join8(homedir2(), ".stamp", "keys", "ed25519.pub");
7735
+ return join9(homedir2(), ".stamp", "keys", "ed25519.pub");
7671
7736
  }
7672
7737
  function defaultShortName() {
7673
7738
  const u = userInfo().username || "user";
@@ -7680,13 +7745,13 @@ function loadSshPubkey(path2) {
7680
7745
  `SSH pubkey not found at ${path2}. Generate one with \`ssh-keygen -t ed25519\` or pass --ssh-pubkey <path>.`
7681
7746
  );
7682
7747
  }
7683
- const raw = readFileSync11(path2, "utf8");
7748
+ const raw = readFileSync12(path2, "utf8");
7684
7749
  const parsed = parseSshPubkey(raw);
7685
7750
  return { path: path2, full: parsed.full, fingerprint: parsed.fingerprint };
7686
7751
  }
7687
7752
  function loadStampPubkey(path2) {
7688
7753
  if (!existsSync11(path2)) return null;
7689
- const pem = readFileSync11(path2, "utf8");
7754
+ const pem = readFileSync12(path2, "utf8");
7690
7755
  try {
7691
7756
  const fp = fingerprintFromPem(pem);
7692
7757
  return { path: path2, pem, fingerprint: fp };
@@ -7831,15 +7896,15 @@ async function runInvitesAccept(opts) {
7831
7896
 
7832
7897
  // src/commands/provision.ts
7833
7898
  import { spawnSync as spawnSync8 } from "child_process";
7834
- import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync12, rmSync, writeFileSync as writeFileSync10 } from "fs";
7899
+ import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
7835
7900
  import { tmpdir } from "os";
7836
- import { join as join9, resolve as resolvePath } from "path";
7901
+ import { join as join10, resolve as resolvePath } from "path";
7837
7902
  import { parse as parseYaml8 } from "yaml";
7838
7903
 
7839
7904
  // src/commands/server.ts
7840
7905
  import { spawnSync as spawnSync7 } from "child_process";
7841
7906
  import { existsSync as existsSync12, mkdirSync as mkdirSync4, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "fs";
7842
- import { dirname as dirname6 } from "path";
7907
+ import { dirname as dirname7 } from "path";
7843
7908
  import { stringify as stringifyYaml2 } from "yaml";
7844
7909
 
7845
7910
  // src/lib/perRepoKey.ts
@@ -7998,7 +8063,7 @@ function writeConfig(opts) {
7998
8063
  repoRootPrefix: opts.repoRootPrefix
7999
8064
  });
8000
8065
  const path2 = userServerConfigPath();
8001
- const dir = dirname6(path2);
8066
+ const dir = dirname7(path2);
8002
8067
  if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true, mode: 448 });
8003
8068
  const tmp = `${path2}.tmp.${process.pid}`;
8004
8069
  writeFileSync9(tmp, yaml, { mode: 384 });
@@ -8311,9 +8376,9 @@ async function runMigrateExisting(opts, server2) {
8311
8376
  console.log("\n(dry run \u2014 no changes made)");
8312
8377
  return;
8313
8378
  }
8314
- const stagingDir = mkdtempSync(join9(tmpdir(), "stamp-migrate-"));
8315
- const bareCloneDir = join9(stagingDir, `${opts.name}.git`);
8316
- const tarballPath = join9(stagingDir, `${opts.name}.tar.gz`);
8379
+ const stagingDir = mkdtempSync(join10(tmpdir(), "stamp-migrate-"));
8380
+ const bareCloneDir = join10(stagingDir, `${opts.name}.git`);
8381
+ const tarballPath = join10(stagingDir, `${opts.name}.tar.gz`);
8317
8382
  try {
8318
8383
  console.log(`
8319
8384
  Building bare-clone tarball of existing repo`);
@@ -8349,9 +8414,9 @@ function ensureCwdIsGitRepo(cwd) {
8349
8414
  }
8350
8415
  }
8351
8416
  function ensureStampInitDone(cwd) {
8352
- if (!existsSync13(join9(cwd, ".stamp", "config.yml"))) {
8417
+ if (!existsSync13(join10(cwd, ".stamp", "config.yml"))) {
8353
8418
  throw new Error(
8354
- `--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.`
8419
+ `--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.`
8355
8420
  );
8356
8421
  }
8357
8422
  }
@@ -8476,7 +8541,7 @@ mirror.yml was added to .stamp/. Commit it through the normal stamp flow:`);
8476
8541
  }
8477
8542
  }
8478
8543
  function readMirrorYmlGithubRepo(repoRoot) {
8479
- const path2 = join9(repoRoot, ".stamp", "mirror.yml");
8544
+ const path2 = join10(repoRoot, ".stamp", "mirror.yml");
8480
8545
  if (!existsSync13(path2)) {
8481
8546
  throw new Error(
8482
8547
  `${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\`.`
@@ -8484,7 +8549,7 @@ function readMirrorYmlGithubRepo(repoRoot) {
8484
8549
  }
8485
8550
  let raw;
8486
8551
  try {
8487
- raw = readFileSync12(path2, "utf8");
8552
+ raw = readFileSync13(path2, "utf8");
8488
8553
  } catch (err) {
8489
8554
  throw new Error(
8490
8555
  `could not read ${path2}: ${err instanceof Error ? err.message : String(err)}`
@@ -8724,10 +8789,10 @@ import {
8724
8789
  import {
8725
8790
  existsSync as existsSync14,
8726
8791
  readdirSync as readdirSync3,
8727
- readFileSync as readFileSync13,
8792
+ readFileSync as readFileSync14,
8728
8793
  writeFileSync as writeFileSync11
8729
8794
  } from "fs";
8730
- import { join as join10, resolve } from "path";
8795
+ import { join as join11, resolve } from "path";
8731
8796
  var USERS_EXIT = {
8732
8797
  OK: 0,
8733
8798
  CONFIG: 1,
@@ -8786,10 +8851,10 @@ function findExistingTrustedKey(repoRoot, fingerprint) {
8786
8851
  if (!existsSync14(dir)) return null;
8787
8852
  for (const f of readdirSync3(dir)) {
8788
8853
  if (!f.endsWith(".pub")) continue;
8789
- const fullPath = join10(dir, f);
8854
+ const fullPath = join11(dir, f);
8790
8855
  let pem;
8791
8856
  try {
8792
- pem = readFileSync13(fullPath, "utf8");
8857
+ pem = readFileSync14(fullPath, "utf8");
8793
8858
  } catch {
8794
8859
  continue;
8795
8860
  }
@@ -8841,7 +8906,7 @@ function runTrustGrant(opts) {
8841
8906
  );
8842
8907
  }
8843
8908
  const repoRoot = resolve(opts.repoPath ?? process.cwd());
8844
- if (!existsSync14(join10(repoRoot, ".git"))) {
8909
+ if (!existsSync14(join11(repoRoot, ".git"))) {
8845
8910
  throw new UsageError(`${repoRoot} is not a git repository`);
8846
8911
  }
8847
8912
  if (!existsSync14(stampConfigDir(repoRoot))) {
@@ -8883,7 +8948,7 @@ function runTrustGrant(opts) {
8883
8948
  runGit2(["checkout", "-b", branch], repoRoot);
8884
8949
  const keysDir = stampTrustedKeysDir(repoRoot);
8885
8950
  ensureDir(keysDir, 493);
8886
- const keyFile = join10(keysDir, `${opts.shortName}.pub`);
8951
+ const keyFile = join11(keysDir, `${opts.shortName}.pub`);
8887
8952
  writeFileSync11(keyFile, pem, { mode: 420 });
8888
8953
  runGit2(["add", keyFile], repoRoot);
8889
8954
  runGit2(
@@ -9081,8 +9146,8 @@ function runUsersRemove(opts) {
9081
9146
  }
9082
9147
 
9083
9148
  // src/commands/keys.ts
9084
- import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
9085
- import { basename, join as join11 } from "path";
9149
+ import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
9150
+ import { basename, join as join12 } from "path";
9086
9151
  function keysGenerate() {
9087
9152
  const existing = loadUserKeypair();
9088
9153
  if (existing) {
@@ -9126,7 +9191,7 @@ function keysList() {
9126
9191
  }
9127
9192
  for (const file of pubFiles.sort()) {
9128
9193
  try {
9129
- const pem = readFileSync14(join11(trustedDir, file), "utf8");
9194
+ const pem = readFileSync15(join12(trustedDir, file), "utf8");
9130
9195
  const fp = fingerprintFromPem(pem);
9131
9196
  const marker = local && fp === local.fingerprint ? " (you)" : "";
9132
9197
  console.log(` ${fp}${marker} [${file}]`);
@@ -9153,7 +9218,7 @@ function keysTrust(pubFile) {
9153
9218
  if (!existsSync15(pubFile)) {
9154
9219
  throw new Error(`public key file not found: ${pubFile}`);
9155
9220
  }
9156
- const pem = readFileSync14(pubFile, "utf8");
9221
+ const pem = readFileSync15(pubFile, "utf8");
9157
9222
  let fingerprint;
9158
9223
  try {
9159
9224
  fingerprint = fingerprintFromPem(pem);
@@ -9163,7 +9228,7 @@ function keysTrust(pubFile) {
9163
9228
  );
9164
9229
  }
9165
9230
  const filename = publicKeyFingerprintFilename(fingerprint);
9166
- const dest = join11(trustedDir, filename);
9231
+ const dest = join12(trustedDir, filename);
9167
9232
  if (existsSync15(dest)) {
9168
9233
  console.log(`${fingerprint} is already trusted (${basename(dest)})`);
9169
9234
  return;
@@ -9623,14 +9688,14 @@ function gitDiffBetween(repoRoot, base, head) {
9623
9688
 
9624
9689
  // src/commands/adminRotate.ts
9625
9690
  import { execFileSync as execFileSync6 } from "child_process";
9626
- import { copyFileSync, existsSync as existsSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
9627
- 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";
9628
9693
  var NAME_PATTERN2 = /^[A-Za-z0-9_.-]+$/;
9629
9694
  var FINGERPRINT_PATTERN2 = /^sha256:[0-9a-f]{64}$/;
9630
9695
  var KNOWN_CAPABILITIES2 = ["admin", "operator", "server"];
9631
9696
  function runAdminAddKey(opts) {
9632
9697
  const repoRoot = findRepoRoot();
9633
- const manifestPath = join12(repoRoot, MANIFEST_RELATIVE_PATH);
9698
+ const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
9634
9699
  const trustedDir = stampTrustedKeysDir(repoRoot);
9635
9700
  if (!NAME_PATTERN2.test(opts.name)) {
9636
9701
  throw new Error(
@@ -9641,7 +9706,7 @@ function runAdminAddKey(opts) {
9641
9706
  if (!existsSync16(opts.pubkeyPath)) {
9642
9707
  throw new Error(`pubkey file not found: ${opts.pubkeyPath}`);
9643
9708
  }
9644
- const pem = readFileSync15(opts.pubkeyPath, "utf8");
9709
+ const pem = readFileSync16(opts.pubkeyPath, "utf8");
9645
9710
  if (!pem.includes("-----BEGIN PUBLIC KEY-----")) {
9646
9711
  throw new Error(
9647
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).`
@@ -9660,7 +9725,7 @@ function runAdminAddKey(opts) {
9660
9725
  `no manifest at ${MANIFEST_RELATIVE_PATH}. Run \`stamp init --migrate-to-server-attested\` to bootstrap one, then re-run \`stamp admin add-key\`.`
9661
9726
  );
9662
9727
  }
9663
- const existing = parseManifest(readFileSync15(manifestPath, "utf8"));
9728
+ const existing = parseManifest(readFileSync16(manifestPath, "utf8"));
9664
9729
  if (!existing) {
9665
9730
  throw new Error(
9666
9731
  `${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file (see \`stamp admin list-keys\` for the current state) before adding a new key.`
@@ -9695,7 +9760,7 @@ function runAdminAddKey(opts) {
9695
9760
  }
9696
9761
  writeFileSync13(manifestPath, yamlText);
9697
9762
  const pubDestFilename = publicKeyFingerprintFilename(fingerprint);
9698
- const pubDestPath = join12(trustedDir, pubDestFilename);
9763
+ const pubDestPath = join13(trustedDir, pubDestFilename);
9699
9764
  if (!existsSync16(pubDestPath)) {
9700
9765
  copyFileSync(opts.pubkeyPath, pubDestPath);
9701
9766
  }
@@ -9720,7 +9785,7 @@ function runAdminAddKey(opts) {
9720
9785
  }
9721
9786
  function runAdminRevoke(opts) {
9722
9787
  const repoRoot = findRepoRoot();
9723
- const manifestPath = join12(repoRoot, MANIFEST_RELATIVE_PATH);
9788
+ const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
9724
9789
  if (!FINGERPRINT_PATTERN2.test(opts.fingerprint)) {
9725
9790
  throw new Error(
9726
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.`
@@ -9731,7 +9796,7 @@ function runAdminRevoke(opts) {
9731
9796
  `no manifest at ${MANIFEST_RELATIVE_PATH} \u2014 nothing to revoke.`
9732
9797
  );
9733
9798
  }
9734
- const existing = parseManifest(readFileSync15(manifestPath, "utf8"));
9799
+ const existing = parseManifest(readFileSync16(manifestPath, "utf8"));
9735
9800
  if (!existing) {
9736
9801
  throw new Error(
9737
9802
  `${MANIFEST_RELATIVE_PATH} failed to parse. Fix the file before revoking.`
@@ -9792,7 +9857,7 @@ function runAdminRevoke(opts) {
9792
9857
  }
9793
9858
  function runAdminListKeys(opts = {}) {
9794
9859
  const repoRoot = findRepoRoot();
9795
- const manifestPath = join12(repoRoot, MANIFEST_RELATIVE_PATH);
9860
+ const manifestPath = join13(repoRoot, MANIFEST_RELATIVE_PATH);
9796
9861
  if (!existsSync16(manifestPath)) {
9797
9862
  if (opts.json) {
9798
9863
  process.stdout.write(JSON.stringify({ entries: [] }, null, 2) + "\n");
@@ -9804,7 +9869,7 @@ function runAdminListKeys(opts = {}) {
9804
9869
  );
9805
9870
  return;
9806
9871
  }
9807
- const manifest = parseManifest(readFileSync15(manifestPath, "utf8"));
9872
+ const manifest = parseManifest(readFileSync16(manifestPath, "utf8"));
9808
9873
  if (!manifest) {
9809
9874
  throw new Error(
9810
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.`
@@ -10123,6 +10188,7 @@ function patchIdForSpan(base_sha, head_sha, repoRoot) {
10123
10188
 
10124
10189
  // src/lib/prAttestation.ts
10125
10190
  import { spawnSync as spawnSync13 } from "child_process";
10191
+ var PR_ATTESTATION_SCHEMA_VERSION = 3;
10126
10192
  var LEGACY_CLIENT_PR_ATTESTATION_SCHEMA_VERSION = 2;
10127
10193
  var MIN_ACCEPTED_PR_ATTESTATION_VERSION = 3;
10128
10194
  var MAX_PR_ATTESTATION_BYTES = 64 * 1024;
@@ -10236,22 +10302,256 @@ function runAttest(opts) {
10236
10302
  const branchRef = opts.branch ?? "HEAD";
10237
10303
  const revspec = `${opts.into}..${branchRef}`;
10238
10304
  const resolved = resolveDiff(revspec, repoRoot);
10239
- const db = openDb(stampStateDbPath(repoRoot));
10305
+ const patch_id = patchIdForSpan(resolved.base_sha, resolved.head_sha, repoRoot);
10306
+ const target_branch_tip_sha = runGit(
10307
+ ["rev-parse", `${opts.into}^{commit}`],
10308
+ repoRoot
10309
+ ).trim();
10310
+ const { keypair } = ensureUserKeypair();
10311
+ const result = rule.review_server ? buildV3Envelope({
10312
+ repoRoot,
10313
+ revspec,
10314
+ baseSha: resolved.base_sha,
10315
+ headSha: resolved.head_sha,
10316
+ diff: resolved.diff,
10317
+ targetBranch: opts.into,
10318
+ targetBranchTipSha: target_branch_tip_sha,
10319
+ patchId: patch_id,
10320
+ requiredReviewers: rule.required,
10321
+ operatorPrivateKeyPem: keypair.privateKeyPem,
10322
+ operatorFingerprint: keypair.fingerprint
10323
+ }) : buildV2Envelope({
10324
+ repoRoot,
10325
+ revspec,
10326
+ baseSha: resolved.base_sha,
10327
+ headSha: resolved.head_sha,
10328
+ targetBranch: opts.into,
10329
+ targetBranchTipSha: target_branch_tip_sha,
10330
+ patchId: patch_id,
10331
+ requiredReviewers: rule.required,
10332
+ operatorPrivateKeyPem: keypair.privateKeyPem,
10333
+ operatorFingerprint: keypair.fingerprint
10334
+ });
10335
+ const { ref, blob_sha } = writeAttestationRef(
10336
+ { payload: result.payload, signature: result.signature },
10337
+ repoRoot
10338
+ );
10339
+ const bar = "\u2500".repeat(72);
10340
+ console.log(bar);
10341
+ console.log(`attested ${branchRef} for merge into '${opts.into}'`);
10342
+ console.log(bar);
10343
+ console.log(` patch-id: ${patch_id}`);
10344
+ console.log(
10345
+ ` base\u2192head: ${resolved.base_sha.slice(0, 8)} \u2192 ${resolved.head_sha.slice(0, 8)}`
10346
+ );
10347
+ console.log(` signed by: ${keypair.fingerprint}`);
10348
+ console.log(` approvals: ${result.reviewerNames.join(", ")}`);
10349
+ console.log(
10350
+ ` schema: v${result.payload.schema_version}${result.payload.schema_version === PR_ATTESTATION_SCHEMA_VERSION ? " (server-attested)" : " (legacy)"}`
10351
+ );
10352
+ console.log(` ref: ${ref}`);
10353
+ console.log(` blob: ${blob_sha.slice(0, 12)}`);
10354
+ console.log(bar);
10355
+ if (opts.pushTo) {
10356
+ pushBranchAndAttestation(opts.pushTo, ref, repoRoot);
10357
+ console.log(
10358
+ `
10359
+ \u2713 pushed branch + attestation ref to ${opts.pushTo}. Open the PR; stamp/verify-attestation@v1 will look up refs/stamp/attestations/<patch-id> from your head SHA's diff against the base.`
10360
+ );
10361
+ } else {
10362
+ console.log(
10363
+ `
10364
+ Next: push the branch + attestation ref to your remote, open a PR, and let stamp/verify-attestation@v1 (the GH Action) confirm it. To do both pushes in one shot:
10365
+
10366
+ git push <remote> HEAD ${ref}
10367
+
10368
+ Or re-run with --push <remote> next time.`
10369
+ );
10370
+ }
10371
+ }
10372
+ function buildV3Envelope(input) {
10373
+ const diffBytes = Buffer.from(input.diff, "utf8");
10374
+ const diffSha256 = createHash12("sha256").update(diffBytes).digest("hex");
10375
+ let manifestYaml;
10376
+ try {
10377
+ manifestYaml = showAtRef(
10378
+ input.baseSha,
10379
+ ".stamp/trusted-keys/manifest.yml",
10380
+ input.repoRoot
10381
+ );
10382
+ } catch (err) {
10383
+ throw new Error(
10384
+ `review_server is configured but .stamp/trusted-keys/manifest.yml is missing at base ${input.baseSha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}. Server-attested PR mode requires the manifest in the merge-base tree so each approval's server signature can be checked against the keys the repo trusted at attestation time. Commit a manifest with capabilities: [server] entries for the review server before attesting.`
10385
+ );
10386
+ }
10387
+ const manifest = parseManifest(manifestYaml);
10388
+ if (!manifest) {
10389
+ throw new Error(
10390
+ `.stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)} failed to parse as a valid trusted-keys manifest. Fix the YAML (syntax error, duplicate fingerprint, unknown capability, etc.) and re-run \`stamp attest\`.`
10391
+ );
10392
+ }
10393
+ const pubFilenames = listFilesAtRef(
10394
+ input.baseSha,
10395
+ ".stamp/trusted-keys",
10396
+ input.repoRoot
10397
+ );
10398
+ const pubkeyByFingerprint = buildPubkeyMap(
10399
+ pubFilenames,
10400
+ (relPath) => showAtRef(input.baseSha, relPath, input.repoRoot)
10401
+ );
10402
+ const db = openDb(stampStateDbPath(input.repoRoot));
10403
+ let entries;
10404
+ try {
10405
+ const rows = serverApprovalsFor(db, input.baseSha, input.headSha);
10406
+ const byReviewer = new Map(rows.map((r) => [r.reviewer, r]));
10407
+ entries = input.requiredReviewers.map((reviewerName) => {
10408
+ const row = byReviewer.get(reviewerName);
10409
+ if (!row) {
10410
+ throw new Error(
10411
+ `missing server signature for reviewer "${reviewerName}" at base\u2192head ${input.baseSha.slice(0, 8)}\u2192${input.headSha.slice(0, 8)}. Server-attested PR mode requires every required reviewer to have a stamp-server-signed approval in the local DB. Possible causes:
10412
+ \u2022 the stamp-server is older than 2.0.1 and doesn't produce PR-attestation v3 payloads (upgrade the server)
10413
+ \u2022 \`stamp review --diff ${input.revspec}\` hasn't been run against this exact (base, head) pair yet
10414
+ \u2022 the review was run in local-mode (no \`review_server\` configured at review time)
10415
+ Run \`stamp review --diff ${input.revspec}\` to populate the signed row, then re-run \`stamp attest\`.`
10416
+ );
10417
+ }
10418
+ let parsedJson;
10419
+ try {
10420
+ parsedJson = JSON.parse(row.approval_json);
10421
+ } catch (err) {
10422
+ throw new Error(
10423
+ `server approval row for reviewer "${reviewerName}" has malformed JSON in server_approval_json \u2014 DB corruption or a writer-side bug. Re-run \`stamp review --diff ${input.revspec}\` to write a fresh row. (parse error: ${err instanceof Error ? err.message : String(err)})`
10424
+ );
10425
+ }
10426
+ if (!parsedJson || typeof parsedJson !== "object" || Array.isArray(parsedJson)) {
10427
+ throw new Error(
10428
+ `server approval row for reviewer "${reviewerName}" parsed to a non-object value \u2014 DB corruption or a writer-side bug. Re-run \`stamp review --diff ${input.revspec}\` to write a fresh row.`
10429
+ );
10430
+ }
10431
+ const obj = parsedJson;
10432
+ for (const field of [
10433
+ "reviewer",
10434
+ "verdict",
10435
+ "prompt_sha256",
10436
+ "diff_sha256",
10437
+ "base_sha",
10438
+ "head_sha",
10439
+ "trusted_keys_snapshot_sha256",
10440
+ "issued_at",
10441
+ "server_key_id"
10442
+ ]) {
10443
+ if (typeof obj[field] !== "string") {
10444
+ throw new Error(
10445
+ `server approval row for reviewer "${reviewerName}" is missing required field "${field}" (or it isn't a string) \u2014 DB corruption or a writer-side bug. Re-run \`stamp review --diff ${input.revspec}\` to write a fresh row.`
10446
+ );
10447
+ }
10448
+ }
10449
+ const approval = parsedJson;
10450
+ if (approval.reviewer !== reviewerName) {
10451
+ throw new Error(
10452
+ `server approval row for reviewer "${reviewerName}" carries approval.reviewer="${approval.reviewer}" \u2014 DB row drifted. Re-run \`stamp review --diff ${input.revspec}\`.`
10453
+ );
10454
+ }
10455
+ if (approval.base_sha !== input.baseSha) {
10456
+ throw new Error(
10457
+ `server approval for "${reviewerName}" was signed against base_sha ${approval.base_sha.slice(0, 8)} but we're attesting from ${input.baseSha.slice(0, 8)} \u2014 stale signature. Re-run \`stamp review --diff ${input.revspec}\` to refresh.`
10458
+ );
10459
+ }
10460
+ if (approval.head_sha !== input.headSha) {
10461
+ throw new Error(
10462
+ `server approval for "${reviewerName}" was signed against head_sha ${approval.head_sha.slice(0, 8)} but we're attesting head ${input.headSha.slice(0, 8)} \u2014 stale signature. Re-run \`stamp review --diff ${input.revspec}\` to refresh.`
10463
+ );
10464
+ }
10465
+ if (approval.diff_sha256 !== diffSha256) {
10466
+ throw new Error(
10467
+ `server approval for "${reviewerName}" was signed against diff_sha256 ${approval.diff_sha256.slice(0, 12)}\u2026 but the current diff hashes to ${diffSha256.slice(0, 12)}\u2026 \u2014 stale signature. The diff content drifted between review and attest (rebased base, modified head). Re-run \`stamp review --diff ${input.revspec}\`.`
10468
+ );
10469
+ }
10470
+ if (approval.verdict !== "approved") {
10471
+ throw new Error(
10472
+ `server approval for "${reviewerName}" carries verdict "${approval.verdict}", not "approved". Re-run \`stamp review --diff ${input.revspec}\` so the server signs the current approved verdict.`
10473
+ );
10474
+ }
10475
+ const caps = resolveCapability(manifest, approval.server_key_id);
10476
+ if (caps === null) {
10477
+ throw new Error(
10478
+ `server approval for "${reviewerName}" was signed by ${approval.server_key_id}, but that key isn't listed in .stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)}. Either the server's signing key changed (commit the new fingerprint to the manifest with capabilities: [server]) or this row was written by a server the repo no longer trusts. Re-run \`stamp review --diff ${input.revspec}\` after fixing the manifest.`
10479
+ );
10480
+ }
10481
+ if (!caps.includes("server")) {
10482
+ throw new Error(
10483
+ `server approval for "${reviewerName}" was signed by ${approval.server_key_id}, but that key's capabilities in .stamp/trusted-keys/manifest.yml at base ${input.baseSha.slice(0, 8)} are [${caps.join(", ")}] \u2014 missing the required 'server' capability. Update the manifest entry and re-attest.`
10484
+ );
10485
+ }
10486
+ const serverPubPem = pubkeyByFingerprint.get(approval.server_key_id);
10487
+ if (!serverPubPem) {
10488
+ throw new Error(
10489
+ `server approval for "${reviewerName}" was signed by ${approval.server_key_id}, but no .pub file in .stamp/trusted-keys/ at base ${input.baseSha.slice(0, 8)} matches that fingerprint. Commit the server's public key alongside its manifest entry and re-attest.`
10490
+ );
10491
+ }
10492
+ const sigOk = verifyBytes(
10493
+ serverPubPem,
10494
+ canonicalSerializeApproval(approval),
10495
+ row.signature_b64
10496
+ );
10497
+ if (!sigOk) {
10498
+ throw new Error(
10499
+ `server signature for "${reviewerName}" failed Ed25519 verification against key ${approval.server_key_id}. The DB row's signature does not match the canonical bytes of its approval body \u2014 either the row was tampered with or the writer was buggy. Re-run \`stamp review --diff ${input.revspec}\` to refresh the signed row.`
10500
+ );
10501
+ }
10502
+ return {
10503
+ approval,
10504
+ server_attestation: {
10505
+ server_key_id: approval.server_key_id,
10506
+ signature: row.signature_b64
10507
+ }
10508
+ };
10509
+ });
10510
+ } finally {
10511
+ db.close();
10512
+ }
10513
+ const trustAnchorSignatures = [];
10514
+ const payload = {
10515
+ schema_version: PR_ATTESTATION_SCHEMA_VERSION,
10516
+ patch_id: input.patchId,
10517
+ base_sha: input.baseSha,
10518
+ head_sha: input.headSha,
10519
+ target_branch: input.targetBranch,
10520
+ target_branch_tip_sha: input.targetBranchTipSha,
10521
+ diff_sha256: diffSha256,
10522
+ approvals: entries,
10523
+ checks: [],
10524
+ // Phase-1 deliberate omission — see file-level comment.
10525
+ trust_anchor_signatures: trustAnchorSignatures,
10526
+ signer_key_id: input.operatorFingerprint
10527
+ };
10528
+ const signature = signBytes(
10529
+ input.operatorPrivateKeyPem,
10530
+ serializePayload2(payload)
10531
+ );
10532
+ return {
10533
+ payload,
10534
+ signature,
10535
+ reviewerNames: input.requiredReviewers
10536
+ };
10537
+ }
10538
+ function buildV2Envelope(input) {
10539
+ const db = openDb(stampStateDbPath(input.repoRoot));
10240
10540
  let approvals;
10241
10541
  try {
10242
- const reviews = latestReviews(db, resolved.base_sha, resolved.head_sha);
10542
+ const reviews = latestReviews(db, input.baseSha, input.headSha);
10243
10543
  const byReviewer = new Map(reviews.map((r) => [r.reviewer, r]));
10244
10544
  const missing = [];
10245
- for (const r of rule.required) {
10545
+ for (const r of input.requiredReviewers) {
10246
10546
  const rev = byReviewer.get(r);
10247
10547
  if (!rev || rev.verdict !== "approved") missing.push(r);
10248
10548
  }
10249
10549
  if (missing.length > 0) {
10250
10550
  throw new Error(
10251
- `gate CLOSED: missing approved verdicts for: ${missing.join(", ")}. Run \`stamp status --diff ${revspec}\` to inspect, then \`stamp review --diff ${revspec}\` to review.`
10551
+ `gate CLOSED: missing approved verdicts for: ${missing.join(", ")}. Run \`stamp status --diff ${input.revspec}\` to inspect, then \`stamp review --diff ${input.revspec}\` to review.`
10252
10552
  );
10253
10553
  }
10254
- approvals = rule.required.map((name) => {
10554
+ approvals = input.requiredReviewers.map((name) => {
10255
10555
  const rev = byReviewer.get(name);
10256
10556
  const toolCalls = redactToolCallsForAttestation(
10257
10557
  parseToolCalls(rev.tool_calls)
@@ -10267,20 +10567,20 @@ function runAttest(opts) {
10267
10567
  db.close();
10268
10568
  }
10269
10569
  const baseConfigYaml = showAtRef(
10270
- resolved.base_sha,
10570
+ input.baseSha,
10271
10571
  ".stamp/config.yml",
10272
- repoRoot
10572
+ input.repoRoot
10273
10573
  );
10274
10574
  const baseReviewers = readReviewersFromYaml(baseConfigYaml);
10275
10575
  approvals = approvals.map((a) => {
10276
10576
  const def = baseReviewers[a.reviewer];
10277
10577
  if (!def) {
10278
10578
  throw new Error(
10279
- `reviewer "${a.reviewer}" approved the diff but is not defined in .stamp/config.yml at base ${resolved.base_sha.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.`
10579
+ `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.`
10280
10580
  );
10281
10581
  }
10282
- const promptText = showAtRef(resolved.base_sha, def.prompt, repoRoot);
10283
- const source = readReviewerSource2(a.reviewer, repoRoot);
10582
+ const promptText = showAtRef(input.baseSha, def.prompt, input.repoRoot);
10583
+ const source = readReviewerSource2(a.reviewer, input.repoRoot);
10284
10584
  return {
10285
10585
  ...a,
10286
10586
  prompt_sha256: hashPromptBytes(Buffer.from(promptText, "utf8")),
@@ -10289,58 +10589,27 @@ function runAttest(opts) {
10289
10589
  ...source ? { reviewer_source: source } : {}
10290
10590
  };
10291
10591
  });
10292
- const patch_id = patchIdForSpan(resolved.base_sha, resolved.head_sha, repoRoot);
10293
- const target_branch_tip_sha = runGit(
10294
- ["rev-parse", `${opts.into}^{commit}`],
10295
- repoRoot
10296
- ).trim();
10297
- const { keypair } = ensureUserKeypair();
10298
10592
  const payload = {
10299
10593
  schema_version: LEGACY_CLIENT_PR_ATTESTATION_SCHEMA_VERSION,
10300
- patch_id,
10301
- base_sha: resolved.base_sha,
10302
- head_sha: resolved.head_sha,
10303
- target_branch: opts.into,
10304
- target_branch_tip_sha,
10594
+ patch_id: input.patchId,
10595
+ base_sha: input.baseSha,
10596
+ head_sha: input.headSha,
10597
+ target_branch: input.targetBranch,
10598
+ target_branch_tip_sha: input.targetBranchTipSha,
10305
10599
  approvals,
10306
10600
  checks: [],
10307
10601
  // Phase-1 deliberate omission — see file-level comment.
10308
- signer_key_id: keypair.fingerprint
10602
+ signer_key_id: input.operatorFingerprint
10309
10603
  };
10310
- const signature = signBytes(keypair.privateKeyPem, serializePayload2(payload));
10311
- const { ref, blob_sha } = writeAttestationRef(
10312
- { payload, signature },
10313
- repoRoot
10604
+ const signature = signBytes(
10605
+ input.operatorPrivateKeyPem,
10606
+ serializePayload2(payload)
10314
10607
  );
10315
- const bar = "\u2500".repeat(72);
10316
- console.log(bar);
10317
- console.log(`attested ${branchRef} for merge into '${opts.into}'`);
10318
- console.log(bar);
10319
- console.log(` patch-id: ${patch_id}`);
10320
- console.log(
10321
- ` base\u2192head: ${resolved.base_sha.slice(0, 8)} \u2192 ${resolved.head_sha.slice(0, 8)}`
10322
- );
10323
- console.log(` signed by: ${keypair.fingerprint}`);
10324
- console.log(` approvals: ${approvals.map((a) => a.reviewer).join(", ")}`);
10325
- console.log(` ref: ${ref}`);
10326
- console.log(` blob: ${blob_sha.slice(0, 12)}`);
10327
- console.log(bar);
10328
- if (opts.pushTo) {
10329
- pushBranchAndAttestation(opts.pushTo, ref, repoRoot);
10330
- console.log(
10331
- `
10332
- \u2713 pushed branch + attestation ref to ${opts.pushTo}. Open the PR; stamp/verify-attestation@v1 will look up refs/stamp/attestations/<patch-id> from your head SHA's diff against the base.`
10333
- );
10334
- } else {
10335
- console.log(
10336
- `
10337
- Next: push the branch + attestation ref to your remote, open a PR, and let stamp/verify-attestation@v1 (the GH Action) confirm it. To do both pushes in one shot:
10338
-
10339
- git push <remote> HEAD ${ref}
10340
-
10341
- Or re-run with --push <remote> next time.`
10342
- );
10343
- }
10608
+ return {
10609
+ payload,
10610
+ signature,
10611
+ reviewerNames: approvals.map((a) => a.reviewer)
10612
+ };
10344
10613
  }
10345
10614
  function pushBranchAndAttestation(remote, attestationRef, repoRoot) {
10346
10615
  const result = spawnSync14(
@@ -10375,7 +10644,7 @@ function readReviewerSource2(reviewerName, repoRoot) {
10375
10644
 
10376
10645
  // src/commands/prune.ts
10377
10646
  import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
10378
- import { join as join13 } from "path";
10647
+ import { join as join14 } from "path";
10379
10648
 
10380
10649
  // src/lib/duration.ts
10381
10650
  function parseRetentionDuration(input) {
@@ -10404,8 +10673,8 @@ function runPrune(opts) {
10404
10673
  );
10405
10674
  const repoRoot = findRepoRoot();
10406
10675
  const dbPath = stampStateDbPath(repoRoot);
10407
- const parsesDir = join13(gitCommonDir(repoRoot), "stamp", "failed-parses");
10408
- const runsDir = join13(gitCommonDir(repoRoot), "stamp", "failed-runs");
10676
+ const parsesDir = join14(gitCommonDir(repoRoot), "stamp", "failed-parses");
10677
+ const runsDir = join14(gitCommonDir(repoRoot), "stamp", "failed-runs");
10409
10678
  const spoolCutoffMs = Date.now() - durationMs;
10410
10679
  if (!existsSync18(dbPath) && !existsSync18(parsesDir) && !existsSync18(runsDir)) {
10411
10680
  console.log(
@@ -10486,7 +10755,7 @@ function peekSpools(spoolDir, cutoffMs) {
10486
10755
  if (!existsSync18(spoolDir)) return [];
10487
10756
  const out = [];
10488
10757
  for (const entry of readdirSync5(spoolDir)) {
10489
- const filepath = join13(spoolDir, entry);
10758
+ const filepath = join14(spoolDir, entry);
10490
10759
  let stat;
10491
10760
  try {
10492
10761
  stat = statSync3(filepath);
@@ -10641,27 +10910,27 @@ function loadOrEmpty() {
10641
10910
  import { spawnSync as spawnSync15 } from "child_process";
10642
10911
  import {
10643
10912
  existsSync as existsSync21,
10644
- readFileSync as readFileSync17,
10913
+ readFileSync as readFileSync18,
10645
10914
  statSync as statSync4,
10646
10915
  unlinkSync as unlinkSync4,
10647
10916
  writeFileSync as writeFileSync15
10648
10917
  } from "fs";
10649
- import { join as join15, relative as relative3, resolve as resolve2 } from "path";
10918
+ import { join as join16, relative as relative3, resolve as resolve2 } from "path";
10650
10919
  import { parse as parseYaml10, stringify as stringifyYaml3 } from "yaml";
10651
10920
 
10652
10921
  // src/lib/reviewerLock.ts
10653
- import { existsSync as existsSync20, readFileSync as readFileSync16, writeFileSync as writeFileSync14 } from "fs";
10654
- import { join as join14 } from "path";
10922
+ import { existsSync as existsSync20, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "fs";
10923
+ import { join as join15 } from "path";
10655
10924
  var LOCK_FILE_VERSION = 1;
10656
10925
  var LOCK_DRIFT_EXIT = 3;
10657
10926
  function lockFilePath(repoRoot, reviewerName) {
10658
- return join14(repoRoot, ".stamp", "reviewers", `${reviewerName}.lock.json`);
10927
+ return join15(repoRoot, ".stamp", "reviewers", `${reviewerName}.lock.json`);
10659
10928
  }
10660
10929
  function readLockFile(repoRoot, reviewerName) {
10661
10930
  const path2 = lockFilePath(repoRoot, reviewerName);
10662
10931
  if (!existsSync20(path2)) return null;
10663
10932
  try {
10664
- const raw = readFileSync16(path2, "utf8");
10933
+ const raw = readFileSync17(path2, "utf8");
10665
10934
  const parsed = JSON.parse(raw);
10666
10935
  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") {
10667
10936
  throw new Error(`malformed lock file at ${path2}`);
@@ -10682,13 +10951,13 @@ function checkReviewerDrift(repoRoot, reviewerName, def) {
10682
10951
  if (!lock) {
10683
10952
  return unpinnedResult();
10684
10953
  }
10685
- const promptPath = join14(repoRoot, def.prompt);
10954
+ const promptPath = join15(repoRoot, def.prompt);
10686
10955
  if (!existsSync20(promptPath)) {
10687
10956
  throw new Error(
10688
10957
  `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.`
10689
10958
  );
10690
10959
  }
10691
- const promptBytes = readFileSync16(promptPath);
10960
+ const promptBytes = readFileSync17(promptPath);
10692
10961
  const observedPrompt = hashPromptBytes(promptBytes);
10693
10962
  const observedTools = hashTools(def.tools);
10694
10963
  const observedMcp = hashMcpServers(def.mcp_servers);
@@ -10890,8 +11159,8 @@ async function reviewersTest(name, diff) {
10890
11159
  console.log(` prompt sourced from working tree (test/iteration use case)`);
10891
11160
  console.log();
10892
11161
  const def = config2.reviewers[name];
10893
- const promptPath = join15(repoRoot, def.prompt);
10894
- const systemPrompt = readFileSync17(promptPath, "utf8");
11162
+ const promptPath = join16(repoRoot, def.prompt);
11163
+ const systemPrompt = readFileSync18(promptPath, "utf8");
10895
11164
  const result = await invokeReviewer({
10896
11165
  reviewer: name,
10897
11166
  config: config2,
@@ -11029,7 +11298,7 @@ async function reviewersFetch(reviewerName, opts) {
11029
11298
  mcpServers = validateMcpServersFromSource(parsed.mcp_servers, source, ref);
11030
11299
  }
11031
11300
  }
11032
- const promptPath = join15(reviewersDir, `${reviewerName}.md`);
11301
+ const promptPath = join16(reviewersDir, `${reviewerName}.md`);
11033
11302
  const promptBytes = Buffer.from(promptText, "utf8");
11034
11303
  const promptSha = hashPromptBytes(promptBytes);
11035
11304
  const toolsSha = hashTools(tools);
@@ -11367,28 +11636,6 @@ function printGate(result, base_sha, head_sha) {
11367
11636
 
11368
11637
  // src/commands/update.ts
11369
11638
  import { spawnSync as spawnSync16 } from "child_process";
11370
-
11371
- // src/lib/version.ts
11372
- import { readFileSync as readFileSync18 } from "fs";
11373
- import { dirname as dirname7, join as join16 } from "path";
11374
- import { fileURLToPath as fileURLToPath2 } from "url";
11375
- function readPackageVersion() {
11376
- const here = dirname7(fileURLToPath2(import.meta.url));
11377
- for (let dir = here, i = 0; i < 6; i++) {
11378
- try {
11379
- const raw = readFileSync18(join16(dir, "package.json"), "utf8");
11380
- const pkg = JSON.parse(raw);
11381
- if (pkg.name === "@openthink/stamp" && pkg.version) return pkg.version;
11382
- } catch {
11383
- }
11384
- const parent = dirname7(dir);
11385
- if (parent === dir) break;
11386
- dir = parent;
11387
- }
11388
- throw new Error("could not locate @openthink/stamp package.json to read version");
11389
- }
11390
-
11391
- // src/commands/update.ts
11392
11639
  var PKG_NAME = "@openthink/stamp";
11393
11640
  function compareSemver(a, b) {
11394
11641
  const parse2 = (v) => (v.split("-")[0] ?? "0.0.0").split(".").map((n) => parseInt(n, 10) || 0);
@@ -11990,6 +12237,9 @@ program.command("init").description(
11990
12237
  ).option(
11991
12238
  "--pr-mode-force",
11992
12239
  "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)"
12240
+ ).option(
12241
+ "--action-source <org/repo>",
12242
+ "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."
11993
12243
  ).action(
11994
12244
  (opts) => {
11995
12245
  try {
@@ -11997,6 +12247,11 @@ program.command("init").description(
11997
12247
  runMigrateToServerAttested({ dryRun: opts.dryRun === true });
11998
12248
  return;
11999
12249
  }
12250
+ if (opts.dryRun === true) {
12251
+ throw new Error(
12252
+ "--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."
12253
+ );
12254
+ }
12000
12255
  let mode;
12001
12256
  if (opts.mode === void 0) {
12002
12257
  mode = void 0;
@@ -12007,6 +12262,13 @@ program.command("init").description(
12007
12262
  `--mode must be 'server-gated' or 'local-only' (got "${opts.mode}")`
12008
12263
  );
12009
12264
  }
12265
+ if (opts.actionSource !== void 0) {
12266
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(opts.actionSource)) {
12267
+ throw new Error(
12268
+ `--action-source must be of the form 'org/repo' (got "${opts.actionSource}")`
12269
+ );
12270
+ }
12271
+ }
12010
12272
  runInit({
12011
12273
  minimal: opts.minimal,
12012
12274
  agentsMd: opts.agentsMd,
@@ -12022,7 +12284,8 @@ program.command("init").description(
12022
12284
  // default fires when the operator hasn't opted out.
12023
12285
  prCheck: opts.prCheck === false ? false : void 0,
12024
12286
  prMode: opts.prMode === true,
12025
- prModeForce: opts.prModeForce === true
12287
+ prModeForce: opts.prModeForce === true,
12288
+ actionSource: opts.actionSource
12026
12289
  });
12027
12290
  } catch (err) {
12028
12291
  const message = err instanceof Error ? err.message : String(err);