@openthink/stamp 1.3.1 → 1.4.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/README.md +34 -14
- package/dist/hooks/pre-receive.cjs.map +1 -1
- package/dist/index.js +134 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -520,15 +520,34 @@ function validateConfig(input) {
|
|
|
520
520
|
}
|
|
521
521
|
enforce_reads_on_dotstamp = d.enforce_reads_on_dotstamp;
|
|
522
522
|
}
|
|
523
|
+
const max_turns = parsePositiveInt(
|
|
524
|
+
d.max_turns,
|
|
525
|
+
`config.reviewers.${name}.max_turns`
|
|
526
|
+
);
|
|
527
|
+
const timeout_ms = parsePositiveInt(
|
|
528
|
+
d.timeout_ms,
|
|
529
|
+
`config.reviewers.${name}.timeout_ms`
|
|
530
|
+
);
|
|
523
531
|
reviewers2[name] = {
|
|
524
532
|
prompt: d.prompt,
|
|
525
533
|
...tools ? { tools } : {},
|
|
526
534
|
...mcp_servers ? { mcp_servers } : {},
|
|
527
|
-
...enforce_reads_on_dotstamp !== void 0 ? { enforce_reads_on_dotstamp } : {}
|
|
535
|
+
...enforce_reads_on_dotstamp !== void 0 ? { enforce_reads_on_dotstamp } : {},
|
|
536
|
+
...max_turns !== void 0 ? { max_turns } : {},
|
|
537
|
+
...timeout_ms !== void 0 ? { timeout_ms } : {}
|
|
528
538
|
};
|
|
529
539
|
}
|
|
530
540
|
return { branches, reviewers: reviewers2 };
|
|
531
541
|
}
|
|
542
|
+
function parsePositiveInt(input, path2) {
|
|
543
|
+
if (input === void 0 || input === null) return void 0;
|
|
544
|
+
if (typeof input !== "number" || !Number.isFinite(input) || !Number.isInteger(input) || input <= 0) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`${path2} must be a positive integer (got ${JSON.stringify(input)})`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return input;
|
|
550
|
+
}
|
|
532
551
|
function parseChecks(input, branchName) {
|
|
533
552
|
if (input === void 0 || input === null) return void 0;
|
|
534
553
|
if (!Array.isArray(input)) {
|
|
@@ -2027,14 +2046,14 @@ async function invokeReviewer(params) {
|
|
|
2027
2046
|
...mcpServersResolved ?? {},
|
|
2028
2047
|
"stamp-verdict": verdictServer
|
|
2029
2048
|
};
|
|
2030
|
-
const maxTurns = parseIntEnv("STAMP_REVIEWER_MAX_TURNS", 8);
|
|
2031
|
-
const timeoutMs = parseIntEnv("STAMP_REVIEWER_TIMEOUT_MS", 5 * 60 * 1e3);
|
|
2049
|
+
const maxTurns = def.max_turns ?? parseIntEnv("STAMP_REVIEWER_MAX_TURNS", 8);
|
|
2050
|
+
const timeoutMs = def.timeout_ms ?? parseIntEnv("STAMP_REVIEWER_TIMEOUT_MS", 5 * 60 * 1e3);
|
|
2032
2051
|
const modelOverride = resolveReviewerModel(params.reviewer);
|
|
2033
2052
|
const abortController = new AbortController();
|
|
2034
2053
|
const timeoutHandle = setTimeout(() => {
|
|
2035
2054
|
abortController.abort(
|
|
2036
2055
|
new Error(
|
|
2037
|
-
`reviewer "${params.reviewer}" exceeded ${timeoutMs}ms wall-clock budget
|
|
2056
|
+
`reviewer "${params.reviewer}" exceeded ${timeoutMs}ms wall-clock budget`
|
|
2038
2057
|
)
|
|
2039
2058
|
);
|
|
2040
2059
|
}, timeoutMs);
|
|
@@ -2119,7 +2138,14 @@ async function invokeReviewer(params) {
|
|
|
2119
2138
|
if (msg.subtype === "success") {
|
|
2120
2139
|
finalText = msg.result;
|
|
2121
2140
|
} else {
|
|
2122
|
-
errorMessage =
|
|
2141
|
+
errorMessage = formatRunFailureMessage({
|
|
2142
|
+
reviewer: params.reviewer,
|
|
2143
|
+
subtype: msg.subtype,
|
|
2144
|
+
repoRoot: params.repoRoot,
|
|
2145
|
+
toolCalls,
|
|
2146
|
+
maxTurns,
|
|
2147
|
+
timeoutMs
|
|
2148
|
+
});
|
|
2123
2149
|
}
|
|
2124
2150
|
break;
|
|
2125
2151
|
}
|
|
@@ -2127,7 +2153,16 @@ async function invokeReviewer(params) {
|
|
|
2127
2153
|
} catch (err) {
|
|
2128
2154
|
if (abortController.signal.aborted) {
|
|
2129
2155
|
const reason = abortController.signal.reason instanceof Error ? abortController.signal.reason.message : String(abortController.signal.reason ?? "aborted");
|
|
2130
|
-
throw new Error(
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
formatAbortMessage({
|
|
2158
|
+
reviewer: params.reviewer,
|
|
2159
|
+
reason,
|
|
2160
|
+
repoRoot: params.repoRoot,
|
|
2161
|
+
toolCalls,
|
|
2162
|
+
maxTurns,
|
|
2163
|
+
timeoutMs
|
|
2164
|
+
})
|
|
2165
|
+
);
|
|
2131
2166
|
}
|
|
2132
2167
|
throw err;
|
|
2133
2168
|
} finally {
|
|
@@ -2339,6 +2374,66 @@ function writeFailedParseSpool(repoRoot, reviewer, text) {
|
|
|
2339
2374
|
const lineCount = text === "" ? 0 : text.split("\n").length;
|
|
2340
2375
|
return { path: filepath, lineCount };
|
|
2341
2376
|
}
|
|
2377
|
+
function writeFailedRunSpool(args) {
|
|
2378
|
+
const dir = path.join(gitCommonDir(args.repoRoot), "stamp", "failed-runs");
|
|
2379
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
2380
|
+
chmodSync(dir, 448);
|
|
2381
|
+
const slug = sanitizeReviewerSlug(args.reviewer);
|
|
2382
|
+
const filename = `${Date.now()}-${slug}.log`;
|
|
2383
|
+
const filepath = path.join(dir, filename);
|
|
2384
|
+
const payload = {
|
|
2385
|
+
reviewer: args.reviewer,
|
|
2386
|
+
failure: args.failure,
|
|
2387
|
+
max_turns: args.maxTurns,
|
|
2388
|
+
timeout_ms: args.timeoutMs,
|
|
2389
|
+
tool_call_count: args.toolCalls.length,
|
|
2390
|
+
tool_calls: args.toolCalls,
|
|
2391
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2392
|
+
};
|
|
2393
|
+
writeFileSync3(filepath, JSON.stringify(payload, null, 2) + "\n", {
|
|
2394
|
+
flag: "wx",
|
|
2395
|
+
mode: 384
|
|
2396
|
+
});
|
|
2397
|
+
chmodSync(filepath, 384);
|
|
2398
|
+
return filepath;
|
|
2399
|
+
}
|
|
2400
|
+
function formatRunFailureMessage(args) {
|
|
2401
|
+
const spool = trySpool({
|
|
2402
|
+
repoRoot: args.repoRoot,
|
|
2403
|
+
reviewer: args.reviewer,
|
|
2404
|
+
failure: `subtype=${args.subtype}`,
|
|
2405
|
+
toolCalls: args.toolCalls,
|
|
2406
|
+
maxTurns: args.maxTurns,
|
|
2407
|
+
timeoutMs: args.timeoutMs
|
|
2408
|
+
});
|
|
2409
|
+
const tracePart = formatTracePart(spool, args.toolCalls.length);
|
|
2410
|
+
const hintPart = args.subtype === "error_max_turns" ? ` \u2014 raise STAMP_REVIEWER_MAX_TURNS (currently ${args.maxTurns}) or set reviewers.${args.reviewer}.max_turns in .stamp/config.yml to extend it` : "";
|
|
2411
|
+
return `reviewer "${args.reviewer}" run failed (subtype=${args.subtype})${tracePart}${hintPart}`;
|
|
2412
|
+
}
|
|
2413
|
+
function formatAbortMessage(args) {
|
|
2414
|
+
const spool = trySpool({
|
|
2415
|
+
repoRoot: args.repoRoot,
|
|
2416
|
+
reviewer: args.reviewer,
|
|
2417
|
+
failure: `abort: ${args.reason}`,
|
|
2418
|
+
toolCalls: args.toolCalls,
|
|
2419
|
+
maxTurns: args.maxTurns,
|
|
2420
|
+
timeoutMs: args.timeoutMs
|
|
2421
|
+
});
|
|
2422
|
+
const tracePart = formatTracePart(spool, args.toolCalls.length);
|
|
2423
|
+
const hintPart = ` \u2014 raise STAMP_REVIEWER_TIMEOUT_MS (currently ${args.timeoutMs}ms) or set reviewers.${args.reviewer}.timeout_ms in .stamp/config.yml to extend it`;
|
|
2424
|
+
return `${args.reason}${tracePart}${hintPart}`;
|
|
2425
|
+
}
|
|
2426
|
+
function trySpool(args) {
|
|
2427
|
+
try {
|
|
2428
|
+
return writeFailedRunSpool(args);
|
|
2429
|
+
} catch {
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
function formatTracePart(spool, toolCallCount) {
|
|
2434
|
+
if (spool === null) return "";
|
|
2435
|
+
return ` \u2014 turn trace at ${spool} (${toolCallCount} tool call${toolCallCount === 1 ? "" : "s"} captured)`;
|
|
2436
|
+
}
|
|
2342
2437
|
function parseIntEnv(name, fallback) {
|
|
2343
2438
|
const raw = process.env[name];
|
|
2344
2439
|
if (!raw) return fallback;
|
|
@@ -5091,11 +5186,12 @@ function runPrune(opts) {
|
|
|
5091
5186
|
);
|
|
5092
5187
|
const repoRoot = findRepoRoot();
|
|
5093
5188
|
const dbPath = stampStateDbPath(repoRoot);
|
|
5094
|
-
const
|
|
5189
|
+
const parsesDir = join8(gitCommonDir(repoRoot), "stamp", "failed-parses");
|
|
5190
|
+
const runsDir = join8(gitCommonDir(repoRoot), "stamp", "failed-runs");
|
|
5095
5191
|
const spoolCutoffMs = Date.now() - durationMs;
|
|
5096
|
-
if (!existsSync14(dbPath) && !existsSync14(
|
|
5192
|
+
if (!existsSync14(dbPath) && !existsSync14(parsesDir) && !existsSync14(runsDir)) {
|
|
5097
5193
|
console.log(
|
|
5098
|
-
`note: nothing to prune (
|
|
5194
|
+
`note: nothing to prune (none of ${dbPath}, ${parsesDir}, ${runsDir} exist \u2014 all are created on first \`stamp review\`)`
|
|
5099
5195
|
);
|
|
5100
5196
|
return;
|
|
5101
5197
|
}
|
|
@@ -5113,14 +5209,19 @@ function runPrune(opts) {
|
|
|
5113
5209
|
any2 = true;
|
|
5114
5210
|
}
|
|
5115
5211
|
}
|
|
5116
|
-
const
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
)
|
|
5122
|
-
|
|
5123
|
-
|
|
5212
|
+
for (const [label, dir] of [
|
|
5213
|
+
["failed-parse", parsesDir],
|
|
5214
|
+
["failed-run", runsDir]
|
|
5215
|
+
]) {
|
|
5216
|
+
const peek = peekSpools(dir, spoolCutoffMs);
|
|
5217
|
+
if (peek.length > 0) {
|
|
5218
|
+
if (any2) console.log("");
|
|
5219
|
+
console.log(
|
|
5220
|
+
`would prune ${peek.length} ${label} spool file${peek.length === 1 ? "" : "s"} older than ${humanLabel}:`
|
|
5221
|
+
);
|
|
5222
|
+
for (const f of peek) console.log(` ${f}`);
|
|
5223
|
+
any2 = true;
|
|
5224
|
+
}
|
|
5124
5225
|
}
|
|
5125
5226
|
if (!any2) {
|
|
5126
5227
|
console.log(`note: nothing to prune (no rows or spools older than ${humanLabel})`);
|
|
@@ -5143,13 +5244,18 @@ function runPrune(opts) {
|
|
|
5143
5244
|
any = true;
|
|
5144
5245
|
}
|
|
5145
5246
|
}
|
|
5146
|
-
const
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
)
|
|
5152
|
-
|
|
5247
|
+
for (const [label, dir] of [
|
|
5248
|
+
["failed-parse", parsesDir],
|
|
5249
|
+
["failed-run", runsDir]
|
|
5250
|
+
]) {
|
|
5251
|
+
const deleted = pruneSpools(dir, spoolCutoffMs);
|
|
5252
|
+
if (deleted > 0) {
|
|
5253
|
+
if (any) console.log("");
|
|
5254
|
+
console.log(
|
|
5255
|
+
`${deleted} ${label} spool file${deleted === 1 ? "" : "s"} pruned`
|
|
5256
|
+
);
|
|
5257
|
+
any = true;
|
|
5258
|
+
}
|
|
5153
5259
|
}
|
|
5154
5260
|
if (!any) {
|
|
5155
5261
|
console.log(`note: nothing to prune (no rows or spools older than ${humanLabel})`);
|
|
@@ -5158,7 +5264,7 @@ function runPrune(opts) {
|
|
|
5158
5264
|
db?.close();
|
|
5159
5265
|
}
|
|
5160
5266
|
}
|
|
5161
|
-
function
|
|
5267
|
+
function peekSpools(spoolDir, cutoffMs) {
|
|
5162
5268
|
if (!existsSync14(spoolDir)) return [];
|
|
5163
5269
|
const out = [];
|
|
5164
5270
|
for (const entry of readdirSync3(spoolDir)) {
|
|
@@ -5174,8 +5280,8 @@ function peekFailedParseSpools(spoolDir, cutoffMs) {
|
|
|
5174
5280
|
}
|
|
5175
5281
|
return out.sort();
|
|
5176
5282
|
}
|
|
5177
|
-
function
|
|
5178
|
-
const targets =
|
|
5283
|
+
function pruneSpools(spoolDir, cutoffMs) {
|
|
5284
|
+
const targets = peekSpools(spoolDir, cutoffMs);
|
|
5179
5285
|
let deleted = 0;
|
|
5180
5286
|
for (const filepath of targets) {
|
|
5181
5287
|
try {
|
|
@@ -6629,7 +6735,7 @@ serverRepo.command("restore <name>").description("restore the most recent soft-d
|
|
|
6629
6735
|
}
|
|
6630
6736
|
);
|
|
6631
6737
|
program.command("review").description(
|
|
6632
|
-
"run configured reviewer(s) against a diff. Reviewer config + prompts are sourced from the merge-base tree (security: prevents feature-branch self-review). For lock-file drift checks, use `stamp reviewers verify` (which exits 3 on drift). Reviewer execution budgets
|
|
6738
|
+
"run configured reviewer(s) against a diff. Reviewer config + prompts are sourced from the merge-base tree (security: prevents feature-branch self-review). For lock-file drift checks, use `stamp reviewers verify` (which exits 3 on drift). Reviewer execution budgets resolve narrowest-wins: `.stamp/config.yml` per-reviewer fields (`reviewers.<name>.max_turns` / `timeout_ms`, committed, sourced from the merge-base tree), then env overrides (`STAMP_REVIEWER_MAX_TURNS` default 8, `STAMP_REVIEWER_TIMEOUT_MS` default 300000), then defaults. On failure a structured turn trace is written to `.git/stamp/failed-runs/` \u2014 see docs/troubleshooting.md."
|
|
6633
6739
|
).requiredOption("--diff <revspec>", "git revspec to review, e.g. main..HEAD").option("--only <reviewer>", "run a single reviewer by name").option(
|
|
6634
6740
|
"--allow-large",
|
|
6635
6741
|
"bypass the 200KB diff size cap (raise STAMP_REVIEW_DIFF_CAP_BYTES for a different threshold)"
|