@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/{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 +269 -205
- 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,15 +52,15 @@ 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";
|
|
59
59
|
|
|
60
60
|
// src/commands/bootstrap.ts
|
|
61
61
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
62
|
-
import { existsSync as existsSync7, readFileSync as
|
|
63
|
-
import { dirname as
|
|
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
|
-
|
|
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}`]);
|
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5963
|
+
const full = join4(reviewersDir, entry);
|
|
5937
5964
|
if (statSync(full).isFile()) {
|
|
5938
|
-
reviewerFiles.set(`.stamp/reviewers/${entry}`,
|
|
5965
|
+
reviewerFiles.set(`.stamp/reviewers/${entry}`, readFileSync7(full, "utf8"));
|
|
5939
5966
|
}
|
|
5940
5967
|
}
|
|
5941
5968
|
let mirrorYml;
|
|
5942
|
-
const mirrorPath =
|
|
5969
|
+
const mirrorPath = join4(seedDir, "mirror.yml");
|
|
5943
5970
|
if (existsSync7(mirrorPath)) {
|
|
5944
|
-
mirrorYml =
|
|
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 =
|
|
5953
|
-
ensureDir(
|
|
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(
|
|
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
|
|
6001
|
-
import { dirname as
|
|
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
|
|
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
|
|
6340
|
-
var OTEAM_CONFIG_PATH =
|
|
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(
|
|
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(
|
|
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(
|
|
6443
|
+
writeFileSync7(join6(reviewersDir, "example.md"), EXAMPLE_REVIEWER_PROMPT);
|
|
6416
6444
|
} else {
|
|
6417
6445
|
writeFileSync7(configFile, stringifyConfig(DEFAULT_CONFIG));
|
|
6418
6446
|
writeFileSync7(
|
|
6419
|
-
|
|
6447
|
+
join6(reviewersDir, "security.md"),
|
|
6420
6448
|
DEFAULT_SECURITY_PROMPT
|
|
6421
6449
|
);
|
|
6422
6450
|
writeFileSync7(
|
|
6423
|
-
|
|
6451
|
+
join6(reviewersDir, "standards.md"),
|
|
6424
6452
|
DEFAULT_STANDARDS_PROMPT
|
|
6425
6453
|
);
|
|
6426
6454
|
writeFileSync7(
|
|
6427
|
-
|
|
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 =
|
|
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(
|
|
6601
|
-
if (existsSync9(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
6863
|
+
const configFile = join6(repoRoot, ".stamp", "config.yml");
|
|
6835
6864
|
if (existsSync9(configFile)) {
|
|
6836
|
-
const cfg = parseConfigFromYaml(
|
|
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:
|
|
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
|
|
7116
|
-
import { dirname as
|
|
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
|
|
7120
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 &&
|
|
7417
|
+
if (manifestExists && readFileSync11(manifestPath, "utf8") === manifestText) {
|
|
7389
7418
|
manifestAction = "unchanged";
|
|
7390
7419
|
} else {
|
|
7391
|
-
ensureDir(
|
|
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 =
|
|
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
|
|
7561
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
7533
7562
|
import { homedir as homedir2, hostname, userInfo } from "os";
|
|
7534
|
-
import { join as
|
|
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
|
|
7730
|
+
return join9(homedir2(), ".ssh", "id_ed25519.pub");
|
|
7702
7731
|
}
|
|
7703
7732
|
function defaultStampPubkeyPath() {
|
|
7704
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
8349
|
-
const bareCloneDir =
|
|
8350
|
-
const tarballPath =
|
|
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(
|
|
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 (${
|
|
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 =
|
|
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 =
|
|
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
|
|
8790
|
+
readFileSync as readFileSync14,
|
|
8762
8791
|
writeFileSync as writeFileSync11
|
|
8763
8792
|
} from "fs";
|
|
8764
|
-
import { join as
|
|
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 =
|
|
8852
|
+
const fullPath = join11(dir, f);
|
|
8824
8853
|
let pem;
|
|
8825
8854
|
try {
|
|
8826
|
-
pem =
|
|
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(
|
|
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 =
|
|
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
|
|
9119
|
-
import { basename, join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
9661
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
10646
|
-
const runsDir =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
10892
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
11132
|
-
|
|
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(
|
|
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 =
|
|
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-
|
|
12660
|
+
const { runUi } = await import("./ui-67BDQPER.js");
|
|
12597
12661
|
runUi();
|
|
12598
12662
|
} catch (err) {
|
|
12599
12663
|
handleCliError(err);
|