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