@jphutchins/code-review 0.1.0-alpha.61 → 0.1.0-alpha.63
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 +2 -1
- package/dist/index.js +637 -300
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/prices.example.json +1 -1
- package/templates/comment.eta +26 -4
package/dist/index.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
|
-
import { resolve
|
|
5
|
+
import { resolve, join, dirname, basename, extname, sep } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
7
7
|
import * as t from 'io-ts';
|
|
8
|
-
import parseDiff from 'parse-diff';
|
|
9
|
-
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
10
|
-
import _addFormats from 'ajv-formats';
|
|
11
8
|
import { execFile } from 'child_process';
|
|
12
9
|
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
|
13
10
|
import { tmpdir } from 'os';
|
|
14
11
|
import { promisify } from 'util';
|
|
12
|
+
import parseDiff from 'parse-diff';
|
|
13
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
14
|
+
import _addFormats from 'ajv-formats';
|
|
15
15
|
import { performance } from 'perf_hooks';
|
|
16
16
|
import { PathReporter } from 'io-ts/lib/PathReporter.js';
|
|
17
17
|
|
|
18
18
|
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
19
19
|
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
20
20
|
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
21
|
-
var
|
|
21
|
+
var CONTEXT_SUFFIX_RE = /\[[12]m\]/i;
|
|
22
|
+
var MODEL_IDENTITY_SUFFIX_RE = new RegExp(CONTEXT_SUFFIX_RE.source, "gi");
|
|
23
|
+
var modelIdentity = (configuredModelId) => configuredModelId.replace(MODEL_IDENTITY_SUFFIX_RE, "");
|
|
24
|
+
var addModelUsage = (prior, entry) => ({
|
|
25
|
+
...prior,
|
|
26
|
+
input_tokens: prior.input_tokens + entry.input_tokens,
|
|
27
|
+
output_tokens: prior.output_tokens + entry.output_tokens,
|
|
28
|
+
cache_read_tokens: (prior.cache_read_tokens ?? 0) + (entry.cache_read_tokens ?? 0),
|
|
29
|
+
cache_write_tokens: (prior.cache_write_tokens ?? 0) + (entry.cache_write_tokens ?? 0)
|
|
30
|
+
});
|
|
22
31
|
var clipText = (body, max) => {
|
|
23
32
|
if (body.length <= max) return body;
|
|
24
33
|
const cut = body.slice(0, max);
|
|
@@ -484,6 +493,9 @@ var incompleteFindings = (summary) => ({
|
|
|
484
493
|
});
|
|
485
494
|
var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
|
|
486
495
|
|
|
496
|
+
// src/released.ts
|
|
497
|
+
var RELEASED = "2026-09-27";
|
|
498
|
+
|
|
487
499
|
// src/cost.ts
|
|
488
500
|
var defaultWarn = (message) => {
|
|
489
501
|
process.stderr.write(`${message}
|
|
@@ -515,7 +527,7 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
515
527
|
if (!("slots" in p)) return p;
|
|
516
528
|
if (pricedAt === void 0) {
|
|
517
529
|
warn(
|
|
518
|
-
`code-review cost: model "${model}" has time-slotted prices but no run instant was supplied to select a slot; cost for this model
|
|
530
|
+
`code-review cost: model "${annotationSafe(model)}" has time-slotted prices but no run instant was supplied to select a slot; cost for this model renders as N/A`
|
|
519
531
|
);
|
|
520
532
|
return null;
|
|
521
533
|
}
|
|
@@ -526,12 +538,11 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
526
538
|
const covering = slots.filter((s) => slotCovers(s, minute));
|
|
527
539
|
if (covering.length === 1) return covering[0] ?? null;
|
|
528
540
|
warn(
|
|
529
|
-
`code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots in \`${useWeekend ? "weekend_slots" : "slots"}\` cover ${hhmmOf(minute)} UTC (expected exactly 1); that array must partition the 24h day with no gap or overlap; cost for this model
|
|
541
|
+
`code-review cost: model "${annotationSafe(model)}" \u2014 ${String(covering.length)} price slots in \`${useWeekend ? "weekend_slots" : "slots"}\` cover ${hhmmOf(minute)} UTC (expected exactly 1); that array must partition the 24h day with no gap or overlap; cost for this model renders as N/A`
|
|
530
542
|
);
|
|
531
543
|
return null;
|
|
532
544
|
};
|
|
533
|
-
var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
534
|
-
const p = prices.models[entry.model];
|
|
545
|
+
var computeModelCost = (entry, prices, pricedAt, warn, colliding = /* @__PURE__ */ new Set()) => {
|
|
535
546
|
const cacheRead = entry.cache_read_tokens ?? 0;
|
|
536
547
|
const cacheWrite = entry.cache_write_tokens ?? 0;
|
|
537
548
|
const zero = {
|
|
@@ -540,30 +551,86 @@ var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
|
540
551
|
outputTokens: entry.output_tokens,
|
|
541
552
|
cacheReadTokens: cacheRead,
|
|
542
553
|
cacheWriteTokens: cacheWrite,
|
|
543
|
-
costUSD: 0
|
|
554
|
+
costUSD: 0,
|
|
555
|
+
known: true
|
|
544
556
|
};
|
|
557
|
+
const canonicalModel = modelIdentity(entry.model);
|
|
558
|
+
if (colliding.has(canonicalModel)) {
|
|
559
|
+
warn(
|
|
560
|
+
`code-review cost: model "${annotationSafe(entry.model)}" is unpriced \u2014 its price map key collides with another key after canonicalization`
|
|
561
|
+
);
|
|
562
|
+
return { ...zero, known: false };
|
|
563
|
+
}
|
|
564
|
+
const p = Object.hasOwn(prices.models, canonicalModel) ? prices.models[canonicalModel] : void 0;
|
|
545
565
|
if (!p) {
|
|
546
566
|
warn(
|
|
547
|
-
|
|
567
|
+
// annotationSafe: a model id is an unvalidated t.string — a CR/LF in it must not emit a
|
|
568
|
+
// second line the Actions runner parses as a workflow command.
|
|
569
|
+
`code-review cost: unknown model "${annotationSafe(entry.model)}" \u2014 no entry in price map; cost for this model renders as N/A`
|
|
548
570
|
);
|
|
549
|
-
return zero;
|
|
571
|
+
return { ...zero, known: false };
|
|
550
572
|
}
|
|
551
573
|
const rate = resolveFlatPrices(entry.model, p, pricedAt, warn);
|
|
552
|
-
if (rate === null) return zero;
|
|
574
|
+
if (rate === null) return { ...zero, known: false };
|
|
553
575
|
const costUSD = (entry.input_tokens * rate.in + entry.output_tokens * rate.out + cacheRead * rate.cache_read + cacheWrite * rate.cache_write) / 1e6;
|
|
554
576
|
return { ...zero, costUSD };
|
|
555
577
|
};
|
|
556
578
|
var computeCost = (models, prices, pricedAt, warn = defaultWarn) => {
|
|
557
|
-
const
|
|
579
|
+
const colliding = /* @__PURE__ */ new Set();
|
|
580
|
+
const seenCanonical = /* @__PURE__ */ new Map();
|
|
581
|
+
const canonicalModels = Object.fromEntries(
|
|
582
|
+
Object.entries(prices.models).map(([key2, rate]) => {
|
|
583
|
+
const canonical = modelIdentity(key2);
|
|
584
|
+
const prior = seenCanonical.get(canonical);
|
|
585
|
+
if (prior !== void 0) {
|
|
586
|
+
colliding.add(canonical);
|
|
587
|
+
warn(
|
|
588
|
+
`code-review cost: the price map keys "${annotationSafe(prior)}" and "${annotationSafe(key2)}" both canonicalize to "${annotationSafe(canonical)}" \u2014 the model is unpriced; fix the map`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
seenCanonical.set(canonical, key2);
|
|
592
|
+
return [canonical, rate];
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
const canonicalPrices = { ...prices, models: canonicalModels };
|
|
596
|
+
const lines = models.map(
|
|
597
|
+
(entry) => computeModelCost(entry, canonicalPrices, pricedAt, warn, colliding)
|
|
598
|
+
);
|
|
558
599
|
return {
|
|
559
600
|
lines,
|
|
560
601
|
totalInputTokens: lines.reduce((s, l) => s + l.inputTokens, 0),
|
|
561
602
|
totalOutputTokens: lines.reduce((s, l) => s + l.outputTokens, 0),
|
|
562
603
|
totalCacheReadTokens: lines.reduce((s, l) => s + l.cacheReadTokens, 0),
|
|
563
604
|
totalCacheWriteTokens: lines.reduce((s, l) => s + l.cacheWriteTokens, 0),
|
|
564
|
-
totalCostUSD: lines.reduce((s, l) => s + l.costUSD, 0)
|
|
605
|
+
totalCostUSD: lines.reduce((s, l) => s + l.costUSD, 0),
|
|
606
|
+
allKnown: lines.every((l) => l.known)
|
|
565
607
|
};
|
|
566
608
|
};
|
|
609
|
+
var parseIsoDate = (stamp) => {
|
|
610
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(stamp)) return void 0;
|
|
611
|
+
const parsed = /* @__PURE__ */ new Date(`${stamp}T00:00:00Z`);
|
|
612
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === stamp ? stamp : void 0;
|
|
613
|
+
};
|
|
614
|
+
var costAxisAnnouncement = (report, markerExists) => {
|
|
615
|
+
if (report === null) return "none";
|
|
616
|
+
if (!report.allKnown) return markerExists ? "none" : "announce";
|
|
617
|
+
return markerExists ? "clear" : "none";
|
|
618
|
+
};
|
|
619
|
+
var warnStalePrices = (prices, warn = defaultWarn) => {
|
|
620
|
+
if (prices === null) return;
|
|
621
|
+
const updated = parseIsoDate(prices._updated);
|
|
622
|
+
if (updated === void 0) return;
|
|
623
|
+
if (updated > (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
624
|
+
warn(
|
|
625
|
+
`::warning:: code-review cost: the price map's _updated (${prices._updated}) is in the future \u2014 a typo'd year silences the staleness check permanently; re-verify and re-stamp`
|
|
626
|
+
);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (updated >= RELEASED) return;
|
|
630
|
+
warn(
|
|
631
|
+
`::warning:: code-review cost: the price map was last verified ${prices._updated}, before this CLI's release (${RELEASED}) \u2014 the map may miss pricing this CLI ships; re-verify against the provider's pricing page`
|
|
632
|
+
);
|
|
633
|
+
};
|
|
567
634
|
|
|
568
635
|
// src/patch.ts
|
|
569
636
|
var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
@@ -1127,6 +1194,167 @@ var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
|
1127
1194
|
const run2 = runUrl ? ` See the [workflow run](${runUrl}) for ${runHasSummary ? "this round's review in full, its job log," : "the job log"} and the findings artifact.` : "";
|
|
1128
1195
|
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${run2}`;
|
|
1129
1196
|
};
|
|
1197
|
+
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
1198
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
1199
|
+
var parseTimeoutMs = (raw, fallback) => {
|
|
1200
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return fallback;
|
|
1201
|
+
const parsed = Number(raw);
|
|
1202
|
+
return parsed > 0 && parsed <= MAX_TIMEOUT_MS ? parsed : fallback;
|
|
1203
|
+
};
|
|
1204
|
+
var SUBPROCESS_TIMEOUT_ENV = "CODE_REVIEW_SUBPROCESS_TIMEOUT_MS";
|
|
1205
|
+
var DEFAULT_SUBPROCESS_TIMEOUT_MS = 12e4;
|
|
1206
|
+
var subprocessTimeoutMs = () => parseTimeoutMs(process.env[SUBPROCESS_TIMEOUT_ENV], DEFAULT_SUBPROCESS_TIMEOUT_MS);
|
|
1207
|
+
var classifyExecError = (err, stderr, timeoutMs) => {
|
|
1208
|
+
const e = err;
|
|
1209
|
+
const stderrStr = stderr.trim();
|
|
1210
|
+
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
1211
|
+
return `output exceeded ${String(MAX_BUFFER)} bytes (killed)`;
|
|
1212
|
+
if (e.killed === true)
|
|
1213
|
+
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
1214
|
+
return stderrStr || errMsg(err);
|
|
1215
|
+
};
|
|
1216
|
+
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
1217
|
+
const child = execFile(
|
|
1218
|
+
spec.command,
|
|
1219
|
+
[...spec.args],
|
|
1220
|
+
{
|
|
1221
|
+
...spec.env ? { env: { ...process.env, ...spec.env } } : {},
|
|
1222
|
+
encoding: "utf-8",
|
|
1223
|
+
maxBuffer: MAX_BUFFER,
|
|
1224
|
+
timeout: spec.timeoutMs,
|
|
1225
|
+
killSignal: "SIGKILL"
|
|
1226
|
+
},
|
|
1227
|
+
(err, stdout, stderr) => {
|
|
1228
|
+
if (err)
|
|
1229
|
+
reject(
|
|
1230
|
+
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
1231
|
+
);
|
|
1232
|
+
else resolve4(stdout);
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
if (spec.stdin !== void 0) {
|
|
1236
|
+
child.stdin?.on("error", () => void 0);
|
|
1237
|
+
child.stdin?.end(spec.stdin);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// src/gh.ts
|
|
1242
|
+
var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
|
|
1243
|
+
var flagFirst;
|
|
1244
|
+
var withEscapeRetry = async (run2, idempotent) => {
|
|
1245
|
+
if (flagFirst) return run2(true);
|
|
1246
|
+
try {
|
|
1247
|
+
return await run2(false);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
if (errMsg(err).includes("--allow-escape-sequences")) {
|
|
1250
|
+
if (!idempotent) throw err;
|
|
1251
|
+
flagFirst = true;
|
|
1252
|
+
return run2(true);
|
|
1253
|
+
}
|
|
1254
|
+
throw err;
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
|
|
1258
|
+
var runGhApi = (args, stdin, env) => withEscapeRetry(
|
|
1259
|
+
(withFlag) => execFileWithTimeout({
|
|
1260
|
+
command: "gh",
|
|
1261
|
+
args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
|
|
1262
|
+
label: `gh api ${describeEndpoint(args)}`,
|
|
1263
|
+
timeoutMs: subprocessTimeoutMs(),
|
|
1264
|
+
env,
|
|
1265
|
+
stdin
|
|
1266
|
+
}),
|
|
1267
|
+
isIdempotentCall(args)
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// src/artifact.ts
|
|
1271
|
+
var run = promisify(execFile);
|
|
1272
|
+
var STEP_TIMEOUT_MS = 6e4;
|
|
1273
|
+
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
1274
|
+
var findingsArtifactUrl = (body) => MARKER_URL.exec(body)?.[1] ?? null;
|
|
1275
|
+
var containedPath = (dir, member) => {
|
|
1276
|
+
const target = resolve(dir, member);
|
|
1277
|
+
return target.startsWith(`${dir}${sep}`) || target === dir ? target : null;
|
|
1278
|
+
};
|
|
1279
|
+
var hasFindingsMarker = (body) => parseFindingsMarker(body) !== null || findingsArtifactUrl(body) !== null;
|
|
1280
|
+
var locateFindingsMember = (listing) => listing.split("\n").map((l) => l.trim()).filter(
|
|
1281
|
+
(l) => l.toLowerCase() === "findings.json" || l.toLowerCase().endsWith("/findings.json")
|
|
1282
|
+
).sort((a, b) => a.length - b.length)[0] ?? null;
|
|
1283
|
+
var readArtifactFindings = (ghApiPath) => {
|
|
1284
|
+
return async (zipUrl) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const dir = await mkdtemp(join(tmpdir(), "code-review-artifact-"));
|
|
1287
|
+
const zip = join(dir, "findings.zip");
|
|
1288
|
+
try {
|
|
1289
|
+
await ghApiPath(zipUrl, zip);
|
|
1290
|
+
const { stdout: listing } = await run("unzip", ["-Z1", zip], {
|
|
1291
|
+
encoding: "utf-8",
|
|
1292
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1293
|
+
timeout: STEP_TIMEOUT_MS
|
|
1294
|
+
});
|
|
1295
|
+
const member = locateFindingsMember(listing);
|
|
1296
|
+
if (member === null) return null;
|
|
1297
|
+
await run("unzip", [zip, "-d", dir], {
|
|
1298
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1299
|
+
timeout: STEP_TIMEOUT_MS
|
|
1300
|
+
});
|
|
1301
|
+
const target = containedPath(dir, member);
|
|
1302
|
+
if (target === null) {
|
|
1303
|
+
process.stderr.write(
|
|
1304
|
+
`::warning::the findings artifact names a member outside the archive directory (${member}) \u2014 the re-review seeds without the prior document`
|
|
1305
|
+
);
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
return await readFile(target, "utf-8");
|
|
1309
|
+
} finally {
|
|
1310
|
+
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
1311
|
+
}
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
process.stderr.write(
|
|
1314
|
+
annotationSafe(
|
|
1315
|
+
`::warning::could not resolve the prior findings artifact ${zipUrl} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
1316
|
+
)
|
|
1317
|
+
);
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
};
|
|
1322
|
+
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
1323
|
+
const { stdout } = await withEscapeRetry(
|
|
1324
|
+
(withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
|
|
1325
|
+
encoding: "buffer",
|
|
1326
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1327
|
+
timeout: STEP_TIMEOUT_MS
|
|
1328
|
+
}),
|
|
1329
|
+
true
|
|
1330
|
+
);
|
|
1331
|
+
await writeFile(outPath, stdout);
|
|
1332
|
+
});
|
|
1333
|
+
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
1334
|
+
var withStampedConvergence = (doc, body) => {
|
|
1335
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
1336
|
+
const stripped = withoutKey(doc, "convergence");
|
|
1337
|
+
const stamped = parseConvergenceMarker(body);
|
|
1338
|
+
return stamped === null ? stripped : { ...stripped, convergence: stamped };
|
|
1339
|
+
};
|
|
1340
|
+
var resolvePriorFindings = async (body, read) => {
|
|
1341
|
+
const embedded = parseFindingsMarker(body);
|
|
1342
|
+
if (embedded !== null) return embedded;
|
|
1343
|
+
const url = findingsArtifactUrl(body);
|
|
1344
|
+
if (url === null) return null;
|
|
1345
|
+
const text = await read(url);
|
|
1346
|
+
if (text === null) return null;
|
|
1347
|
+
try {
|
|
1348
|
+
return withStampedConvergence(JSON.parse(text), body);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
process.stderr.write(
|
|
1351
|
+
annotationSafe(
|
|
1352
|
+
`::warning::could not parse the findings document fetched from ${url} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1130
1358
|
|
|
1131
1359
|
// src/comment-chain.ts
|
|
1132
1360
|
function* ancestors(start, parentOf) {
|
|
@@ -1480,6 +1708,24 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1480
1708
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1481
1709
|
var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
1482
1710
|
var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
|
|
1711
|
+
var shedNote = (count, summaryAvailable, runUrl, findingsDocLinked, thisRunArtifact) => count === 0 ? "" : `<sub>**${String(count)} finding${count === 1 ? "" : "s"} left out of this comment**${summaryAvailable && runUrl !== void 0 ? ` \u2014 the [run summary](<${runUrl}>) carries the whole review` : findingsDocLinked && thisRunArtifact ? " \u2014 the machine findings document carries them" : ""}.</sub>`;
|
|
1712
|
+
var terminalNotice = (i) => [
|
|
1713
|
+
`## ${i.verdictBadge}`,
|
|
1714
|
+
"",
|
|
1715
|
+
`<sub>**Reviewed** \`${escapeCodeBackticks(i.reviewedSha.slice(0, 7))}\`${i.postedAt ? ` at ${i.postedAt}` : ""}${i.route ? ` \xB7 **route:** ${i.route}` : ""}</sub>`,
|
|
1716
|
+
...i.roundsSummary !== "" ? [`<sub>${i.roundsSummary}</sub>`] : [],
|
|
1717
|
+
...i.convergenceSummary !== "" ? [`<sub>${i.convergenceSummary}</sub>`] : [],
|
|
1718
|
+
...i.inlinePosted > 0 ? [
|
|
1719
|
+
`<sub>\u2192 ${String(i.inlinePosted)} inline comment${i.inlinePosted === 1 ? "" : "s"} posted on the diff \u2014 the disposition line the terminal round otherwise skips.</sub>`
|
|
1720
|
+
] : [],
|
|
1721
|
+
"",
|
|
1722
|
+
`> **The review for \`${escapeCodeBackticks(i.reviewedSha.slice(0, 7))}\` exceeds GitHub's comment-size limit.**` + (i.summaryAvailable ? i.runUrl ? ` The whole review lives in the [run summary](<${i.runUrl}>).` : " The whole review lives in the workflow run's summary." : i.findingsDocLinked && i.thisRunArtifact ? " The whole review lives in the machine findings document." : "") + (i.findingsDocLinked && i.thisRunArtifact ? " The machine findings document is linked above." : ""),
|
|
1723
|
+
"",
|
|
1724
|
+
"> [!WARNING]",
|
|
1725
|
+
`> **LLM Disclosure** \u2014 this review was produced by ${i.modelNames || "unknown model"}.`,
|
|
1726
|
+
">",
|
|
1727
|
+
`> _Generated by [code-review](https://github.com/JPHutchins/code-review)${i.runUrl ? ` \xB7 [view the run & traces](<${i.runUrl}>)` : ""}._`
|
|
1728
|
+
].join("\n");
|
|
1483
1729
|
var permalinkFor = (base, f, anchor) => {
|
|
1484
1730
|
if (f.path === "" || f.side === "LEFT") return void 0;
|
|
1485
1731
|
const path = f.path.split("/").map(
|
|
@@ -1604,6 +1850,20 @@ var computeSeverityCounts = (findings) => findings.reduce(
|
|
|
1604
1850
|
);
|
|
1605
1851
|
var isReviewVerdict = (verdict) => verdict !== "error";
|
|
1606
1852
|
var isConvergenceRound = (route, incomplete) => route === "full review" && !incomplete;
|
|
1853
|
+
var verdictBadge = (v) => {
|
|
1854
|
+
switch (v) {
|
|
1855
|
+
case "approve":
|
|
1856
|
+
return "\u2705 approved";
|
|
1857
|
+
case "comment":
|
|
1858
|
+
return "\u{1F4AC} comment";
|
|
1859
|
+
case "changes":
|
|
1860
|
+
return "\u{1F527} changes requested";
|
|
1861
|
+
case "error":
|
|
1862
|
+
return "\u{1F6E0}\uFE0F no review verdict";
|
|
1863
|
+
default:
|
|
1864
|
+
return `\u2753 ${v}`;
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1607
1867
|
var render = (input) => {
|
|
1608
1868
|
const eta = new Eta({ autoTrim: false });
|
|
1609
1869
|
const usageAvailable = input.envelope !== null;
|
|
@@ -1611,6 +1871,7 @@ var render = (input) => {
|
|
|
1611
1871
|
const incomplete = (input.incomplete ?? input.envelope?.incomplete ?? false) || isIncompleteFindings(input.findings);
|
|
1612
1872
|
const pricedAt = parseInstant(input.envelope?.generated_at) ?? input.pricedAt;
|
|
1613
1873
|
const costReport = input.envelope ? computeCost(input.envelope.models, input.prices, pricedAt) : null;
|
|
1874
|
+
const costProvenance = costReport === null || costReport.lines.length === 0 ? "unknown" : costReport.allKnown ? "complete" : costReport.lines.some((line) => line.known) ? "partial" : "unknown";
|
|
1614
1875
|
const pricesProvided = input.pricesProvided ?? true;
|
|
1615
1876
|
const route = input.route ?? input.envelope?.route ?? null;
|
|
1616
1877
|
const effort = input.effort ?? input.envelope?.effort ?? null;
|
|
@@ -1715,12 +1976,53 @@ var render = (input) => {
|
|
|
1715
1976
|
};
|
|
1716
1977
|
return eta.renderString(input.template, {
|
|
1717
1978
|
findings: input.findings,
|
|
1979
|
+
shedCount: input.shedCount ?? 0,
|
|
1980
|
+
terminal: input.terminal ?? false,
|
|
1718
1981
|
envelope: input.envelope,
|
|
1719
1982
|
usageAvailable,
|
|
1720
1983
|
hasUsage,
|
|
1721
1984
|
incomplete,
|
|
1722
1985
|
costReport,
|
|
1723
1986
|
pricesProvided,
|
|
1987
|
+
costProvenance,
|
|
1988
|
+
// The terminal branch's "machine findings document is linked above" claim must only fire when
|
|
1989
|
+
// the pointer actually carries a findings-json marker (the convergence-only marker does not).
|
|
1990
|
+
// Derived from the SAME value the template renders (the jsonUrl fallback pair included).
|
|
1991
|
+
findingsDocLinked: hasFindingsMarker(
|
|
1992
|
+
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
1993
|
+
),
|
|
1994
|
+
summaryAvailable: input.summaryAvailable ?? false,
|
|
1995
|
+
shedNote: shedNote(
|
|
1996
|
+
input.shedCount ?? 0,
|
|
1997
|
+
input.summaryAvailable ?? false,
|
|
1998
|
+
input.runUrl,
|
|
1999
|
+
hasFindingsMarker(
|
|
2000
|
+
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
2001
|
+
),
|
|
2002
|
+
input.jsonUrl !== void 0
|
|
2003
|
+
),
|
|
2004
|
+
terminalNotice: terminalNotice({
|
|
2005
|
+
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
2006
|
+
verdictBadge: verdictBadge(input.findings.verdict),
|
|
2007
|
+
route: route ?? void 0,
|
|
2008
|
+
postedAt: input.postedAt ?? "",
|
|
2009
|
+
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
2010
|
+
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : "",
|
|
2011
|
+
inlinePosted: input.inlineDisposition?.kind === "posted" ? input.inlineDisposition.count : 0,
|
|
2012
|
+
runUrl: input.runUrl,
|
|
2013
|
+
summaryAvailable: input.summaryAvailable ?? false,
|
|
2014
|
+
findingsDocLinked: hasFindingsMarker(
|
|
2015
|
+
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
2016
|
+
),
|
|
2017
|
+
thisRunArtifact: input.jsonUrl !== void 0,
|
|
2018
|
+
modelNames
|
|
2019
|
+
}),
|
|
2020
|
+
// The house sanitizer for untrusted template fields — collapses line breaks AND escapes
|
|
2021
|
+
// backticks, so a hand-edited stamp cannot break the code span or the blockquote.
|
|
2022
|
+
// A nonconforming stamp must not be presented as validated: the template shows a note
|
|
2023
|
+
// instead of the smoothed value when the staleness axis cannot parse it.
|
|
2024
|
+
stampValid: parseIsoDate(input.prices._updated) !== void 0,
|
|
2025
|
+
pricesUpdatedAt: escapeCodeBackticks(input.prices._updated.replace(/\r/g, " ")).trim(),
|
|
1724
2026
|
route,
|
|
1725
2027
|
effort,
|
|
1726
2028
|
modelNames,
|
|
@@ -1764,27 +2066,22 @@ var render = (input) => {
|
|
|
1764
2066
|
answeredReRaiseNote: input.answeredReRaiseNote ?? "",
|
|
1765
2067
|
reviewUrl: input.reviewUrl ?? null,
|
|
1766
2068
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
1767
|
-
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
|
|
1768
|
-
|
|
2069
|
+
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
|
|
2070
|
+
// price them — and N/A for a model that missed the map entirely (issue #221): a misconfigured
|
|
2071
|
+
// row must not look like a free review. Provenance is REQUIRED (no permissive default): a
|
|
2072
|
+
// user-swapped template that drops the argument fails loudly, not silently fail-open.
|
|
2073
|
+
formatCost: (n, provenance) => {
|
|
2074
|
+
if (!pricesProvided || provenance !== "complete" && provenance !== "partial") return "N/A";
|
|
2075
|
+
if (provenance === "partial" && !(n >= 5e-3)) return "N/A";
|
|
2076
|
+
const figure = Number.isFinite(n) ? n > 0 && n.toFixed(2) === "0.00" ? "<$0.01" : `$${n.toFixed(2)}` : "\u2014";
|
|
2077
|
+
return provenance === "partial" ? `\u2265 ${figure}` : figure;
|
|
2078
|
+
},
|
|
1769
2079
|
formatDuration: (ms) => {
|
|
1770
2080
|
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
1771
2081
|
const s = Math.round(ms / 1e3);
|
|
1772
2082
|
return s >= 60 ? `${String(Math.floor(s / 60))}m ${String(s % 60)}s` : `${String(s)}s`;
|
|
1773
2083
|
},
|
|
1774
|
-
verdictBadge
|
|
1775
|
-
switch (v) {
|
|
1776
|
-
case "approve":
|
|
1777
|
-
return "\u2705 approved";
|
|
1778
|
-
case "comment":
|
|
1779
|
-
return "\u{1F4AC} comment";
|
|
1780
|
-
case "changes":
|
|
1781
|
-
return "\u{1F527} changes requested";
|
|
1782
|
-
case "error":
|
|
1783
|
-
return "\u{1F6E0}\uFE0F no review verdict";
|
|
1784
|
-
default:
|
|
1785
|
-
return `\u2753 ${v}`;
|
|
1786
|
-
}
|
|
1787
|
-
},
|
|
2084
|
+
verdictBadge,
|
|
1788
2085
|
severityEmoji,
|
|
1789
2086
|
formatConfidence
|
|
1790
2087
|
});
|
|
@@ -2129,6 +2426,7 @@ var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
|
|
|
2129
2426
|
var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
|
|
2130
2427
|
var priorSuppressedPath = (draftPath) => sidecarPath(draftPath, ".prior-suppressed");
|
|
2131
2428
|
var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
|
|
2429
|
+
var costAxisDisengagedPath = (draftPath) => sidecarPath(draftPath, ".cost-axis-disengaged");
|
|
2132
2430
|
var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and the pre-seeded $DRAFT is a non-review sentinel: it does not count until you have replaced it with your own review. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
|
|
2133
2431
|
var forceBackgroundSpawn = (toolInput) => ({
|
|
2134
2432
|
hookSpecificOutput: {
|
|
@@ -2421,7 +2719,7 @@ var defaultVersion = (kind) => {
|
|
|
2421
2719
|
if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
|
|
2422
2720
|
return latest.defaultVersion;
|
|
2423
2721
|
};
|
|
2424
|
-
var bundledSchemaPath = (relativePath) => resolve
|
|
2722
|
+
var bundledSchemaPath = (relativePath) => resolve(import.meta.dirname, "..", "schema", relativePath);
|
|
2425
2723
|
var schemaPathFor = (kind, version) => {
|
|
2426
2724
|
const table = tableFor(kind);
|
|
2427
2725
|
const entry = version === void 0 ? table.find((v) => v.latest) : table.find((v) => v.minor === majorMinor(version));
|
|
@@ -2454,7 +2752,7 @@ var resolvers = {
|
|
|
2454
2752
|
triage: (raw) => resolveSingleVersion("triage", raw),
|
|
2455
2753
|
prices: (raw) => resolveSingleVersion("prices", raw)
|
|
2456
2754
|
};
|
|
2457
|
-
var
|
|
2755
|
+
var resolve2 = (kind, raw) => resolvers[kind](raw);
|
|
2458
2756
|
var resolveTolerantFindings = (doc) => {
|
|
2459
2757
|
const r = resolveFindings(doc);
|
|
2460
2758
|
if (r.kind === "ok") return r.value;
|
|
@@ -2462,165 +2760,6 @@ var resolveTolerantFindings = (doc) => {
|
|
|
2462
2760
|
const legacy = FindingsCodecV09.decode(doc);
|
|
2463
2761
|
return legacy._tag === "Right" ? normalizeV09(legacy.right) : null;
|
|
2464
2762
|
};
|
|
2465
|
-
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
2466
|
-
var MAX_TIMEOUT_MS = 2147483647;
|
|
2467
|
-
var parseTimeoutMs = (raw, fallback) => {
|
|
2468
|
-
if (raw === void 0 || !/^\d+$/.test(raw)) return fallback;
|
|
2469
|
-
const parsed = Number(raw);
|
|
2470
|
-
return parsed > 0 && parsed <= MAX_TIMEOUT_MS ? parsed : fallback;
|
|
2471
|
-
};
|
|
2472
|
-
var SUBPROCESS_TIMEOUT_ENV = "CODE_REVIEW_SUBPROCESS_TIMEOUT_MS";
|
|
2473
|
-
var DEFAULT_SUBPROCESS_TIMEOUT_MS = 12e4;
|
|
2474
|
-
var subprocessTimeoutMs = () => parseTimeoutMs(process.env[SUBPROCESS_TIMEOUT_ENV], DEFAULT_SUBPROCESS_TIMEOUT_MS);
|
|
2475
|
-
var classifyExecError = (err, stderr, timeoutMs) => {
|
|
2476
|
-
const e = err;
|
|
2477
|
-
const stderrStr = stderr.trim();
|
|
2478
|
-
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
2479
|
-
return `output exceeded ${String(MAX_BUFFER)} bytes (killed)`;
|
|
2480
|
-
if (e.killed === true)
|
|
2481
|
-
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
2482
|
-
return stderrStr || errMsg(err);
|
|
2483
|
-
};
|
|
2484
|
-
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
2485
|
-
const child = execFile(
|
|
2486
|
-
spec.command,
|
|
2487
|
-
[...spec.args],
|
|
2488
|
-
{
|
|
2489
|
-
...spec.env ? { env: { ...process.env, ...spec.env } } : {},
|
|
2490
|
-
encoding: "utf-8",
|
|
2491
|
-
maxBuffer: MAX_BUFFER,
|
|
2492
|
-
timeout: spec.timeoutMs,
|
|
2493
|
-
killSignal: "SIGKILL"
|
|
2494
|
-
},
|
|
2495
|
-
(err, stdout, stderr) => {
|
|
2496
|
-
if (err)
|
|
2497
|
-
reject(
|
|
2498
|
-
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
2499
|
-
);
|
|
2500
|
-
else resolve4(stdout);
|
|
2501
|
-
}
|
|
2502
|
-
);
|
|
2503
|
-
if (spec.stdin !== void 0) {
|
|
2504
|
-
child.stdin?.on("error", () => void 0);
|
|
2505
|
-
child.stdin?.end(spec.stdin);
|
|
2506
|
-
}
|
|
2507
|
-
});
|
|
2508
|
-
|
|
2509
|
-
// src/gh.ts
|
|
2510
|
-
var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
|
|
2511
|
-
var flagFirst;
|
|
2512
|
-
var withEscapeRetry = async (run2, idempotent) => {
|
|
2513
|
-
if (flagFirst) return run2(true);
|
|
2514
|
-
try {
|
|
2515
|
-
return await run2(false);
|
|
2516
|
-
} catch (err) {
|
|
2517
|
-
if (errMsg(err).includes("--allow-escape-sequences")) {
|
|
2518
|
-
if (!idempotent) throw err;
|
|
2519
|
-
flagFirst = true;
|
|
2520
|
-
return run2(true);
|
|
2521
|
-
}
|
|
2522
|
-
throw err;
|
|
2523
|
-
}
|
|
2524
|
-
};
|
|
2525
|
-
var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
|
|
2526
|
-
var runGhApi = (args, stdin, env) => withEscapeRetry(
|
|
2527
|
-
(withFlag) => execFileWithTimeout({
|
|
2528
|
-
command: "gh",
|
|
2529
|
-
args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
|
|
2530
|
-
label: `gh api ${describeEndpoint(args)}`,
|
|
2531
|
-
timeoutMs: subprocessTimeoutMs(),
|
|
2532
|
-
env,
|
|
2533
|
-
stdin
|
|
2534
|
-
}),
|
|
2535
|
-
isIdempotentCall(args)
|
|
2536
|
-
);
|
|
2537
|
-
var run = promisify(execFile);
|
|
2538
|
-
var STEP_TIMEOUT_MS = 6e4;
|
|
2539
|
-
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
2540
|
-
var findingsArtifactUrl = (body) => MARKER_URL.exec(body)?.[1] ?? null;
|
|
2541
|
-
var containedPath = (dir, member) => {
|
|
2542
|
-
const target = resolve$1(dir, member);
|
|
2543
|
-
return target.startsWith(`${dir}${sep}`) || target === dir ? target : null;
|
|
2544
|
-
};
|
|
2545
|
-
var hasFindingsMarker = (body) => parseFindingsMarker(body) !== null || findingsArtifactUrl(body) !== null;
|
|
2546
|
-
var locateFindingsMember = (listing) => listing.split("\n").map((l) => l.trim()).filter(
|
|
2547
|
-
(l) => l.toLowerCase() === "findings.json" || l.toLowerCase().endsWith("/findings.json")
|
|
2548
|
-
).sort((a, b) => a.length - b.length)[0] ?? null;
|
|
2549
|
-
var readArtifactFindings = (ghApiPath) => {
|
|
2550
|
-
return async (zipUrl) => {
|
|
2551
|
-
try {
|
|
2552
|
-
const dir = await mkdtemp(join(tmpdir(), "code-review-artifact-"));
|
|
2553
|
-
const zip = join(dir, "findings.zip");
|
|
2554
|
-
try {
|
|
2555
|
-
await ghApiPath(zipUrl, zip);
|
|
2556
|
-
const { stdout: listing } = await run("unzip", ["-Z1", zip], {
|
|
2557
|
-
encoding: "utf-8",
|
|
2558
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
2559
|
-
timeout: STEP_TIMEOUT_MS
|
|
2560
|
-
});
|
|
2561
|
-
const member = locateFindingsMember(listing);
|
|
2562
|
-
if (member === null) return null;
|
|
2563
|
-
await run("unzip", [zip, "-d", dir], {
|
|
2564
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
2565
|
-
timeout: STEP_TIMEOUT_MS
|
|
2566
|
-
});
|
|
2567
|
-
const target = containedPath(dir, member);
|
|
2568
|
-
if (target === null) {
|
|
2569
|
-
process.stderr.write(
|
|
2570
|
-
`::warning::the findings artifact names a member outside the archive directory (${member}) \u2014 the re-review seeds without the prior document`
|
|
2571
|
-
);
|
|
2572
|
-
return null;
|
|
2573
|
-
}
|
|
2574
|
-
return await readFile(target, "utf-8");
|
|
2575
|
-
} finally {
|
|
2576
|
-
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2577
|
-
}
|
|
2578
|
-
} catch (err) {
|
|
2579
|
-
process.stderr.write(
|
|
2580
|
-
annotationSafe(
|
|
2581
|
-
`::warning::could not resolve the prior findings artifact ${zipUrl} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2582
|
-
)
|
|
2583
|
-
);
|
|
2584
|
-
return null;
|
|
2585
|
-
}
|
|
2586
|
-
};
|
|
2587
|
-
};
|
|
2588
|
-
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
2589
|
-
const { stdout } = await withEscapeRetry(
|
|
2590
|
-
(withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
|
|
2591
|
-
encoding: "buffer",
|
|
2592
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
2593
|
-
timeout: STEP_TIMEOUT_MS
|
|
2594
|
-
}),
|
|
2595
|
-
true
|
|
2596
|
-
);
|
|
2597
|
-
await writeFile(outPath, stdout);
|
|
2598
|
-
});
|
|
2599
|
-
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
2600
|
-
var withStampedConvergence = (doc, body) => {
|
|
2601
|
-
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
2602
|
-
const stripped = withoutKey(doc, "convergence");
|
|
2603
|
-
const stamped = parseConvergenceMarker(body);
|
|
2604
|
-
return stamped === null ? stripped : { ...stripped, convergence: stamped };
|
|
2605
|
-
};
|
|
2606
|
-
var resolvePriorFindings = async (body, read) => {
|
|
2607
|
-
const embedded = parseFindingsMarker(body);
|
|
2608
|
-
if (embedded !== null) return embedded;
|
|
2609
|
-
const url = findingsArtifactUrl(body);
|
|
2610
|
-
if (url === null) return null;
|
|
2611
|
-
const text = await read(url);
|
|
2612
|
-
if (text === null) return null;
|
|
2613
|
-
try {
|
|
2614
|
-
return withStampedConvergence(JSON.parse(text), body);
|
|
2615
|
-
} catch (err) {
|
|
2616
|
-
process.stderr.write(
|
|
2617
|
-
annotationSafe(
|
|
2618
|
-
`::warning::could not parse the findings document fetched from ${url} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2619
|
-
)
|
|
2620
|
-
);
|
|
2621
|
-
return null;
|
|
2622
|
-
}
|
|
2623
|
-
};
|
|
2624
2763
|
|
|
2625
2764
|
// src/pr.ts
|
|
2626
2765
|
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
@@ -2763,6 +2902,9 @@ var checkRun = async (input, ghApi = runGhApi) => {
|
|
|
2763
2902
|
};
|
|
2764
2903
|
|
|
2765
2904
|
// src/post.ts
|
|
2905
|
+
var STICKY_CHAR_LIMIT = 65536;
|
|
2906
|
+
var SHED_RESERVE = 4e3;
|
|
2907
|
+
var SHED_NOTE_BUDGET = 512;
|
|
2766
2908
|
var DEFAULT_MARKER = "<!-- code-review -->";
|
|
2767
2909
|
var EMPTY_MECHANIC_LEAVE_MESSAGE = "The CI-fix pass found no issues and the sticky already reflects a completed full review \u2014 leaving it in place\n";
|
|
2768
2910
|
var MAX_SUGGESTION_LINES = 10;
|
|
@@ -2786,7 +2928,7 @@ var checkLongSuggestions = (comments) => {
|
|
|
2786
2928
|
return { comments: adjusted, longFiles };
|
|
2787
2929
|
};
|
|
2788
2930
|
var decodeFindings = (doc) => {
|
|
2789
|
-
const resolution =
|
|
2931
|
+
const resolution = resolve2("findings", doc);
|
|
2790
2932
|
switch (resolution.kind) {
|
|
2791
2933
|
case "ok":
|
|
2792
2934
|
return { kind: "ok", findings: resolution.value };
|
|
@@ -3400,6 +3542,7 @@ ${dropNote}` : ""}`,
|
|
|
3400
3542
|
if (decodedPrices._tag === "Left") {
|
|
3401
3543
|
throw new Error(`Price map at ${input.pricesPath} does not match the expected shape`);
|
|
3402
3544
|
}
|
|
3545
|
+
warnStalePrices(input.pricesProvided ? decodedPrices.right : null);
|
|
3403
3546
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
3404
3547
|
const inlineRequested = input.inline === true;
|
|
3405
3548
|
const envelope = loadEnvelope(input.envelopePath);
|
|
@@ -3552,6 +3695,74 @@ ${dropNote}` : ""}`,
|
|
|
3552
3695
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
3553
3696
|
const clocDiff = input.clocDiffPath ? loadClocDiff(input.clocDiffPath) : void 0;
|
|
3554
3697
|
const effectiveRoute = input.route ?? envelope?.route;
|
|
3698
|
+
const shedPriority = (f) => SEVERITIES.length - 1 - SEVERITIES.indexOf(f.severity);
|
|
3699
|
+
const shedOrderOf = (list) => list.map((f, index) => ({ f, index })).sort((a, b) => shedPriority(a.f) - shedPriority(b.f) || a.index - b.index).map((entry) => entry.f);
|
|
3700
|
+
const sizeSticky = (renderAt, total, reserve = SHED_RESERVE) => {
|
|
3701
|
+
const target = STICKY_CHAR_LIMIT - reserve - SHED_NOTE_BUDGET;
|
|
3702
|
+
const base = renderAt(0);
|
|
3703
|
+
if (base.length <= target) return { body: base, shedCount: 0, terminal: false };
|
|
3704
|
+
const allShed = renderAt(total);
|
|
3705
|
+
if (allShed.length > STICKY_CHAR_LIMIT - SHED_NOTE_BUDGET)
|
|
3706
|
+
return { body: "", shedCount: total, terminal: true };
|
|
3707
|
+
let low = 0;
|
|
3708
|
+
let high = total;
|
|
3709
|
+
let fitted = "";
|
|
3710
|
+
while (low < high) {
|
|
3711
|
+
const mid = low + high >> 1;
|
|
3712
|
+
const body = renderAt(mid);
|
|
3713
|
+
if (body.length <= target) {
|
|
3714
|
+
high = mid;
|
|
3715
|
+
fitted = body;
|
|
3716
|
+
} else {
|
|
3717
|
+
low = mid + 1;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
return { body: fitted !== "" ? fitted : allShed, shedCount: low, terminal: false };
|
|
3721
|
+
};
|
|
3722
|
+
const minimalTerminalBody = (findingsPointer2, route, incomplete, summaryAvailable2, modelNames2) => [
|
|
3723
|
+
DEFAULT_MARKER,
|
|
3724
|
+
`<!-- reviewed-sha: ${input.headSha} -->`,
|
|
3725
|
+
...!incomplete && route !== void 0 ? [`<!-- reviewed-route: ${route.replace(/--/g, "~~").replace(/\s+/g, " ")} -->`] : [],
|
|
3726
|
+
...!incomplete ? [REVIEW_COMPLETE_MARKER] : [],
|
|
3727
|
+
// The pointer's link form already begins with the directive; the convergence-only marker
|
|
3728
|
+
// does not, and a terminal surface must carry the directive either way.
|
|
3729
|
+
...findingsPointer2 !== "" ? [
|
|
3730
|
+
findingsPointer2.startsWith(AGENTS_STOP_DIRECTIVE) ? findingsPointer2 : `${AGENTS_STOP_DIRECTIVE}
|
|
3731
|
+
${findingsPointer2}`
|
|
3732
|
+
] : [],
|
|
3733
|
+
"",
|
|
3734
|
+
terminalNotice({
|
|
3735
|
+
reviewedSha: input.headSha,
|
|
3736
|
+
verdictBadge: verdictBadge(loadedFindings.verdict),
|
|
3737
|
+
route: route ?? void 0,
|
|
3738
|
+
postedAt: input.postedAt,
|
|
3739
|
+
roundsSummary: "",
|
|
3740
|
+
convergenceSummary: "",
|
|
3741
|
+
inlinePosted: 0,
|
|
3742
|
+
runUrl: input.runUrl,
|
|
3743
|
+
summaryAvailable: summaryAvailable2,
|
|
3744
|
+
findingsDocLinked: hasFindingsMarker(findingsPointer2),
|
|
3745
|
+
thisRunArtifact: input.jsonUrl !== void 0,
|
|
3746
|
+
modelNames: modelNames2
|
|
3747
|
+
})
|
|
3748
|
+
].join("\n");
|
|
3749
|
+
const terminalOrMinimal = (terminalRender, findingsPointer2, route, incomplete, summaryAvailable2, modelNames2) => terminalRender.length <= STICKY_CHAR_LIMIT ? terminalRender : minimalTerminalBody(findingsPointer2, route, incomplete, summaryAvailable2, modelNames2);
|
|
3750
|
+
const summaryAvailable = (process.env["GITHUB_STEP_SUMMARY"] ?? "") !== "";
|
|
3751
|
+
const modelNames = envelope !== null && envelope.models.length > 0 ? envelope.models.map((m) => m.model).join(", ") : "";
|
|
3752
|
+
const withShedNote = (body, shedCount, findingsPointer2) => {
|
|
3753
|
+
if (shedCount === 0) return body;
|
|
3754
|
+
const note = shedNote(
|
|
3755
|
+
shedCount,
|
|
3756
|
+
summaryAvailable,
|
|
3757
|
+
input.runUrl,
|
|
3758
|
+
hasFindingsMarker(findingsPointer2),
|
|
3759
|
+
input.jsonUrl !== void 0
|
|
3760
|
+
);
|
|
3761
|
+
return body.includes(note) ? body : `${body}
|
|
3762
|
+
|
|
3763
|
+
${note}
|
|
3764
|
+
`;
|
|
3765
|
+
};
|
|
3555
3766
|
if (envelope === null) {
|
|
3556
3767
|
const envelopelessIncomplete = isIncompleteFindings(findings);
|
|
3557
3768
|
if (wouldBuryCompleted(envelopelessIncomplete)) {
|
|
@@ -3567,51 +3778,75 @@ ${dropNote}` : ""}`,
|
|
|
3567
3778
|
input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD
|
|
3568
3779
|
) : priorConv
|
|
3569
3780
|
);
|
|
3570
|
-
const
|
|
3781
|
+
const lostInput = {
|
|
3782
|
+
findings: stampedFindings2,
|
|
3783
|
+
envelope: null,
|
|
3784
|
+
incomplete: envelopelessIncomplete,
|
|
3785
|
+
prices: decodedPrices.right,
|
|
3786
|
+
pricesProvided: input.pricesProvided,
|
|
3787
|
+
template,
|
|
3788
|
+
route: effectiveRoute,
|
|
3789
|
+
reviewedSha: input.headSha,
|
|
3790
|
+
repo: input.headRepo || input.repo,
|
|
3791
|
+
effort: input.effort,
|
|
3792
|
+
sameRootNotes: {},
|
|
3793
|
+
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
3794
|
+
// findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
|
|
3795
|
+
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
3796
|
+
// exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
|
|
3797
|
+
// here too (issue #164): below-floor nits are hidden from the human list and shown only in the
|
|
3798
|
+
// collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
|
|
3799
|
+
// must hold on the surface that lists findings without an inline review.
|
|
3800
|
+
suppressedNits,
|
|
3801
|
+
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3802
|
+
answeredNotes: reRaisedNotes,
|
|
3803
|
+
answeredReRaiseNote: answeredDropNote,
|
|
3804
|
+
roundCount: priorRoundCount,
|
|
3805
|
+
convergenceRound: false,
|
|
3806
|
+
testReport,
|
|
3807
|
+
clocDiff,
|
|
3808
|
+
runUrl: input.runUrl,
|
|
3809
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3810
|
+
jsonUrl: input.jsonUrl,
|
|
3811
|
+
findingsPointer: findingsBlob(stampedFindings2),
|
|
3812
|
+
postedAt: input.postedAt,
|
|
3813
|
+
summaryAvailable,
|
|
3814
|
+
discussionByFinding,
|
|
3815
|
+
orphanedDiscussion,
|
|
3816
|
+
orphanedTotal,
|
|
3817
|
+
discussionTruncated,
|
|
3818
|
+
orphanedTruncated,
|
|
3819
|
+
orphanedUnresolvable: orphanResolveFailed
|
|
3820
|
+
};
|
|
3821
|
+
const lostRender = (opts) => formatMarkdown(
|
|
3571
3822
|
render({
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
prices: decodedPrices.right,
|
|
3576
|
-
pricesProvided: input.pricesProvided,
|
|
3577
|
-
template,
|
|
3578
|
-
route: effectiveRoute,
|
|
3579
|
-
reviewedSha: input.headSha,
|
|
3580
|
-
repo: input.headRepo || input.repo,
|
|
3581
|
-
effort: input.effort,
|
|
3582
|
-
sameRootNotes: {},
|
|
3583
|
-
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
3584
|
-
// findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
|
|
3585
|
-
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
3586
|
-
// exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
|
|
3587
|
-
// here too (issue #164): below-floor nits are hidden from the human list and shown only in the
|
|
3588
|
-
// collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
|
|
3589
|
-
// must hold on the surface that lists findings without an inline review.
|
|
3590
|
-
strays: visibleFindings,
|
|
3591
|
-
suppressedNits,
|
|
3592
|
-
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3593
|
-
answeredNotes: reRaisedNotes,
|
|
3594
|
-
answeredReRaiseNote: answeredDropNote,
|
|
3595
|
-
roundCount: priorRoundCount,
|
|
3596
|
-
convergenceRound: false,
|
|
3597
|
-
testReport,
|
|
3598
|
-
clocDiff,
|
|
3599
|
-
inlineDisposition: inlineRequested ? { kind: "no-envelope" } : { kind: "disabled" },
|
|
3600
|
-
runUrl: input.runUrl,
|
|
3601
|
-
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3602
|
-
jsonUrl: input.jsonUrl,
|
|
3603
|
-
findingsPointer: findingsBlob(stampedFindings2),
|
|
3604
|
-
postedAt: input.postedAt,
|
|
3605
|
-
discussionByFinding,
|
|
3606
|
-
orphanedDiscussion,
|
|
3607
|
-
orphanedTotal,
|
|
3608
|
-
discussionTruncated,
|
|
3609
|
-
orphanedTruncated,
|
|
3610
|
-
orphanedUnresolvable: orphanResolveFailed
|
|
3823
|
+
...lostInput,
|
|
3824
|
+
...opts,
|
|
3825
|
+
inlineDisposition: inlineRequested ? { kind: "no-envelope" } : { kind: "disabled" }
|
|
3611
3826
|
})
|
|
3612
3827
|
);
|
|
3828
|
+
const lostShedOrder = shedOrderOf(visibleFindings);
|
|
3829
|
+
const lostRenderAt = (shedCount) => shedCount === 0 ? lostRender({ strays: visibleFindings }) : (() => {
|
|
3830
|
+
const dropped = new Set(lostShedOrder.slice(0, shedCount));
|
|
3831
|
+
return lostRender({
|
|
3832
|
+
strays: visibleFindings.filter((f) => !dropped.has(f)),
|
|
3833
|
+
shedCount
|
|
3834
|
+
});
|
|
3835
|
+
})();
|
|
3836
|
+
const lostSized = sizeSticky(lostRenderAt, visibleFindings.length, 0);
|
|
3837
|
+
const body = lostSized.terminal ? terminalOrMinimal(
|
|
3838
|
+
lostRender({ strays: visibleFindings, terminal: true }),
|
|
3839
|
+
findingsBlob(stampedFindings2),
|
|
3840
|
+
effectiveRoute,
|
|
3841
|
+
envelopelessIncomplete,
|
|
3842
|
+
summaryAvailable,
|
|
3843
|
+
""
|
|
3844
|
+
) : withShedNote(lostSized.body, lostSized.shedCount, findingsBlob(stampedFindings2));
|
|
3613
3845
|
await upsertSticky(input.repo, prNumber, existingSticky, body, ghApi, true);
|
|
3614
|
-
appendRunSummary(
|
|
3846
|
+
appendRunSummary(
|
|
3847
|
+
process.env["GITHUB_STEP_SUMMARY"],
|
|
3848
|
+
() => lostSized.shedCount === 0 ? lostSized.body : lostRender({ strays: visibleFindings })
|
|
3849
|
+
);
|
|
3615
3850
|
if (inlineRequested) {
|
|
3616
3851
|
process.stderr.write(
|
|
3617
3852
|
"Warning: inline: true was requested, but the result envelope is missing \u2014 inline comments cannot be built; the findings are in the sticky and the run summary instead\n"
|
|
@@ -3630,6 +3865,11 @@ ${dropNote}` : ""}`,
|
|
|
3630
3865
|
}
|
|
3631
3866
|
if (emptyMechanicWouldBury(effectiveRoute, thisIncomplete) && existingSticky !== null)
|
|
3632
3867
|
await emptyMechanicLeaveOrNote(existingSticky);
|
|
3868
|
+
if (!input.jsonUrl && existingSticky !== null && parseReviewComplete(existingSticky.body) && parseReviewedRoute(existingSticky.body) === "full review" && hasFindingsMarker(existingSticky.body)) {
|
|
3869
|
+
leaveInPlace(
|
|
3870
|
+
"no --json-url was supplied and the existing sticky still carries the prior findings document's marker (embedded or link) \u2014 leaving it in place rather than severing the seed chain\n"
|
|
3871
|
+
);
|
|
3872
|
+
}
|
|
3633
3873
|
const isRound = isConvergenceRound(effectiveRoute, thisIncomplete) && isReviewVerdict(findings.verdict);
|
|
3634
3874
|
const sameRootNotes = isRound ? computeSameRootNotes(priorTraj, findings.findings, input.headSha.slice(0, 12)) : {};
|
|
3635
3875
|
const {
|
|
@@ -3707,7 +3947,8 @@ ${dropNote}` : ""}`,
|
|
|
3707
3947
|
jsonUrl: input.jsonUrl,
|
|
3708
3948
|
findingsPointer: findingsMarker,
|
|
3709
3949
|
postedAt: input.postedAt,
|
|
3710
|
-
pricedAt: input.pricedAt
|
|
3950
|
+
pricedAt: input.pricedAt,
|
|
3951
|
+
summaryAvailable
|
|
3711
3952
|
};
|
|
3712
3953
|
const longFilesNote = longFiles.length > 0 ? `
|
|
3713
3954
|
|
|
@@ -3715,27 +3956,56 @@ ${dropNote}` : ""}`,
|
|
|
3715
3956
|
|
|
3716
3957
|
> **Note:** ${String(longFiles.length)} suggestion(s) exceeded GitHub's ~10-line inline suggestion limit and were omitted from the inline comments; the affected findings remain in the review.
|
|
3717
3958
|
` : "";
|
|
3718
|
-
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays, discussionOverride) => formatMarkdown(
|
|
3959
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays, discussionOverride, terminal = false, shedCount = 0) => formatMarkdown(
|
|
3719
3960
|
render({
|
|
3720
3961
|
...commonRenderInput,
|
|
3721
3962
|
...straysOverride ? { strays: straysOverride } : {},
|
|
3722
3963
|
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
3723
3964
|
...unanchoredStrays !== void 0 && unanchoredStrays.length > 0 ? { unanchoredStrays } : {},
|
|
3724
3965
|
...discussionOverride ?? {},
|
|
3966
|
+
...terminal ? { terminal } : {},
|
|
3967
|
+
...shedCount > 0 ? { shedCount } : {},
|
|
3725
3968
|
inlineDisposition,
|
|
3726
3969
|
reviewUrl: reviewUrl2
|
|
3727
3970
|
}) + longFilesNote
|
|
3728
3971
|
);
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3972
|
+
const mainShedOrder = shedOrderOf(strays);
|
|
3973
|
+
const mainRenderAt = (shedCount) => shedCount === 0 ? renderBody(initialDisposition) : renderBody(
|
|
3974
|
+
initialDisposition,
|
|
3975
|
+
void 0,
|
|
3976
|
+
strays.filter((f) => !new Set(mainShedOrder.slice(0, shedCount)).has(f)),
|
|
3977
|
+
void 0,
|
|
3978
|
+
void 0,
|
|
3979
|
+
void 0,
|
|
3980
|
+
false,
|
|
3981
|
+
shedCount
|
|
3982
|
+
);
|
|
3983
|
+
const mainSized = sizeSticky(
|
|
3984
|
+
mainRenderAt,
|
|
3985
|
+
strays.length,
|
|
3986
|
+
inlineRequested && rawComments.length > 0 ? SHED_RESERVE : 0
|
|
3987
|
+
);
|
|
3988
|
+
const fitStickyBody = () => mainSized.terminal ? terminalOrMinimal(
|
|
3989
|
+
renderBody(
|
|
3990
|
+
initialDisposition,
|
|
3991
|
+
void 0,
|
|
3992
|
+
void 0,
|
|
3993
|
+
void 0,
|
|
3994
|
+
void 0,
|
|
3995
|
+
void 0,
|
|
3996
|
+
true
|
|
3997
|
+
),
|
|
3998
|
+
findingsMarker,
|
|
3999
|
+
effectiveRoute,
|
|
4000
|
+
thisIncomplete,
|
|
4001
|
+
summaryAvailable,
|
|
4002
|
+
modelNames
|
|
4003
|
+
) : withShedNote(mainSized.body, mainSized.shedCount, findingsMarker);
|
|
3734
4004
|
const stickyRef = await upsertSticky(
|
|
3735
4005
|
input.repo,
|
|
3736
4006
|
prNumber,
|
|
3737
4007
|
existingSticky,
|
|
3738
|
-
|
|
4008
|
+
fitStickyBody(),
|
|
3739
4009
|
ghApi,
|
|
3740
4010
|
true
|
|
3741
4011
|
);
|
|
@@ -3777,7 +4047,6 @@ ${dropNote}` : ""}`,
|
|
|
3777
4047
|
await minimizeComments(prNumber, priorInlineComments, ghApi);
|
|
3778
4048
|
}
|
|
3779
4049
|
const unanchoredCount = unposted.length;
|
|
3780
|
-
const finalStrays = unanchoredCount > 0 ? [...unposted, ...strays] : strays;
|
|
3781
4050
|
const finalDiscussion = unanchoredCount > 0 ? buildStickyDiscussion(
|
|
3782
4051
|
reachable,
|
|
3783
4052
|
[...currentIds, ...unposted.map((f) => f.id).filter((id) => id !== "")],
|
|
@@ -3792,29 +4061,75 @@ ${dropNote}` : ""}`,
|
|
|
3792
4061
|
} : void 0;
|
|
3793
4062
|
if (stickyRef !== null && (inlinePosted > 0 || unanchoredCount > 0)) {
|
|
3794
4063
|
const finalDisposition = inlinePosted > 0 ? { kind: "posted", count: inlinePosted, sha: input.headSha } : { kind: "inline-unavailable" };
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
stickyRef.id,
|
|
3799
|
-
renderBody(
|
|
4064
|
+
if (mainSized.terminal) {
|
|
4065
|
+
try {
|
|
4066
|
+
const terminalPatch = renderBody(
|
|
3800
4067
|
finalDisposition,
|
|
3801
4068
|
reviewUrl,
|
|
3802
|
-
|
|
4069
|
+
void 0,
|
|
3803
4070
|
unanchoredCount,
|
|
3804
4071
|
unposted,
|
|
3805
|
-
finalDiscussionOverride
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
4072
|
+
finalDiscussionOverride,
|
|
4073
|
+
true
|
|
4074
|
+
);
|
|
4075
|
+
if (terminalPatch.length <= STICKY_CHAR_LIMIT) {
|
|
4076
|
+
await patchComment(input.repo, stickyRef.id, terminalPatch, ghApi);
|
|
4077
|
+
process.stderr.write(
|
|
4078
|
+
`Updated sticky comment #${String(stickyRef.id)} to reflect the review
|
|
3811
4079
|
`
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
4080
|
+
);
|
|
4081
|
+
} else {
|
|
4082
|
+
process.stderr.write(
|
|
4083
|
+
`Warning: the terminal sticky patch exceeds GitHub's comment limit \u2014 the sticky stands; the run summary carries the review
|
|
4084
|
+
`
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
} catch (err) {
|
|
4088
|
+
process.stderr.write(
|
|
4089
|
+
`Warning: failed to update the terminal sticky after the review: ${errMsg(err)}
|
|
3816
4090
|
`
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
} else {
|
|
4094
|
+
const patchShedOrder = shedOrderOf(strays);
|
|
4095
|
+
const patchRenderAt = (shedCount) => renderBody(
|
|
4096
|
+
finalDisposition,
|
|
4097
|
+
reviewUrl,
|
|
4098
|
+
[
|
|
4099
|
+
...unposted,
|
|
4100
|
+
...shedCount === 0 ? strays : strays.filter((f) => !new Set(patchShedOrder.slice(0, shedCount)).has(f))
|
|
4101
|
+
],
|
|
4102
|
+
unanchoredCount,
|
|
4103
|
+
unposted,
|
|
4104
|
+
finalDiscussionOverride,
|
|
4105
|
+
false,
|
|
4106
|
+
shedCount
|
|
3817
4107
|
);
|
|
4108
|
+
const patchSized = sizeSticky(patchRenderAt, strays.length, 0);
|
|
4109
|
+
try {
|
|
4110
|
+
if (patchSized.terminal) {
|
|
4111
|
+
process.stderr.write(
|
|
4112
|
+
`Warning: the final sticky patch still exceeds GitHub's comment limit \u2014 the sticky stands as posted${summaryAvailable ? "; the whole review (rejected findings included) is in the run summary" : ""}
|
|
4113
|
+
`
|
|
4114
|
+
);
|
|
4115
|
+
} else {
|
|
4116
|
+
await patchComment(
|
|
4117
|
+
input.repo,
|
|
4118
|
+
stickyRef.id,
|
|
4119
|
+
withShedNote(patchSized.body, patchSized.shedCount, findingsMarker),
|
|
4120
|
+
ghApi
|
|
4121
|
+
);
|
|
4122
|
+
process.stderr.write(
|
|
4123
|
+
`Updated sticky comment #${String(stickyRef.id)} to reflect the review
|
|
4124
|
+
`
|
|
4125
|
+
);
|
|
4126
|
+
}
|
|
4127
|
+
} catch (err) {
|
|
4128
|
+
process.stderr.write(
|
|
4129
|
+
`Warning: failed to update the sticky summary after the review: ${errMsg(err)}
|
|
4130
|
+
`
|
|
4131
|
+
);
|
|
4132
|
+
}
|
|
3818
4133
|
}
|
|
3819
4134
|
}
|
|
3820
4135
|
appendRunSummary(
|
|
@@ -4583,7 +4898,7 @@ var gateCandidate = (kind, rawCandidate) => {
|
|
|
4583
4898
|
const schemaPath = safeSchemaPathFor(kind, candidateVersion(kind, candidate));
|
|
4584
4899
|
if (schemaPath === null) return null;
|
|
4585
4900
|
if (!validateAgainstSchema(candidate, schemaPath).valid) return null;
|
|
4586
|
-
const resolution =
|
|
4901
|
+
const resolution = resolve2(kind, candidate);
|
|
4587
4902
|
return resolution.kind === "ok" ? { version: resolution.version, candidate } : null;
|
|
4588
4903
|
};
|
|
4589
4904
|
var candidateFromJsonText = (kind, text) => {
|
|
@@ -4706,13 +5021,6 @@ var ClaudeCodeEnvelopeCodec = t.intersection([
|
|
|
4706
5021
|
structured_output: t.unknown
|
|
4707
5022
|
})
|
|
4708
5023
|
]);
|
|
4709
|
-
var sumUsage = (into, entry) => ({
|
|
4710
|
-
...into,
|
|
4711
|
-
input_tokens: into.input_tokens + entry.input_tokens,
|
|
4712
|
-
output_tokens: into.output_tokens + entry.output_tokens,
|
|
4713
|
-
...into.cache_read_tokens !== void 0 || entry.cache_read_tokens !== void 0 ? { cache_read_tokens: (into.cache_read_tokens ?? 0) + (entry.cache_read_tokens ?? 0) } : {},
|
|
4714
|
-
...into.cache_write_tokens !== void 0 || entry.cache_write_tokens !== void 0 ? { cache_write_tokens: (into.cache_write_tokens ?? 0) + (entry.cache_write_tokens ?? 0) } : {}
|
|
4715
|
-
});
|
|
4716
5024
|
var mapModelUsage = (modelUsage) => [
|
|
4717
5025
|
...Object.entries(modelUsage).reduce((byModel, [configuredModelId, entry]) => {
|
|
4718
5026
|
const model = modelIdentity(configuredModelId);
|
|
@@ -4724,7 +5032,7 @@ var mapModelUsage = (modelUsage) => [
|
|
|
4724
5032
|
...entry.cacheCreationInputTokens !== void 0 ? { cache_write_tokens: entry.cacheCreationInputTokens } : {}
|
|
4725
5033
|
};
|
|
4726
5034
|
const prior = byModel.get(model);
|
|
4727
|
-
return byModel.set(model, prior === void 0 ? mapped :
|
|
5035
|
+
return byModel.set(model, prior === void 0 ? mapped : addModelUsage(prior, mapped));
|
|
4728
5036
|
}, /* @__PURE__ */ new Map()).values()
|
|
4729
5037
|
];
|
|
4730
5038
|
var findingsOutcome = (native, agentFilePath, agentFileFallbackPath) => {
|
|
@@ -4736,7 +5044,7 @@ var findingsOutcome = (native, agentFilePath, agentFileFallbackPath) => {
|
|
|
4736
5044
|
});
|
|
4737
5045
|
if (ladder.kind !== "ok")
|
|
4738
5046
|
return { kind: "telemetry-only", reason: describeLadderFailure(ladder) };
|
|
4739
|
-
const resolution =
|
|
5047
|
+
const resolution = resolve2("findings", ladder.candidate);
|
|
4740
5048
|
return resolution.kind === "ok" ? { kind: "ok", version: resolution.version, findings: resolution.value } : {
|
|
4741
5049
|
kind: "telemetry-only",
|
|
4742
5050
|
reason: "internal error: the extraction ladder validated a candidate the registry then rejected"
|
|
@@ -4895,7 +5203,7 @@ var parseScope = (raw) => {
|
|
|
4895
5203
|
var resolvedPriorValue = resolveTolerantFindings;
|
|
4896
5204
|
var readJSON = (path) => {
|
|
4897
5205
|
try {
|
|
4898
|
-
return JSON.parse(readFileSync(resolve
|
|
5206
|
+
return JSON.parse(readFileSync(resolve(path), "utf-8"));
|
|
4899
5207
|
} catch (err) {
|
|
4900
5208
|
return fail(`Cannot read ${path}: ${errMsg(err)}`);
|
|
4901
5209
|
}
|
|
@@ -4908,7 +5216,7 @@ var fail = (msg) => {
|
|
|
4908
5216
|
var readJSONOrAbsent = (path) => {
|
|
4909
5217
|
const read = (() => {
|
|
4910
5218
|
try {
|
|
4911
|
-
return { text: readFileSync(resolve
|
|
5219
|
+
return { text: readFileSync(resolve(path), "utf-8") };
|
|
4912
5220
|
} catch (err) {
|
|
4913
5221
|
return { error: errMsg(err) };
|
|
4914
5222
|
}
|
|
@@ -4938,7 +5246,7 @@ var readJSONOrAbsent = (path) => {
|
|
|
4938
5246
|
}
|
|
4939
5247
|
};
|
|
4940
5248
|
var readSandboxConfigForNotice = (path) => {
|
|
4941
|
-
const text = readFileOrNull(resolve
|
|
5249
|
+
const text = readFileOrNull(resolve(path));
|
|
4942
5250
|
if (text === null) return void 0;
|
|
4943
5251
|
const parsed = tryParseJson(text);
|
|
4944
5252
|
if (!parsed.ok) {
|
|
@@ -4979,7 +5287,7 @@ var unwrapAdapt = (either) => {
|
|
|
4979
5287
|
}
|
|
4980
5288
|
};
|
|
4981
5289
|
var transcriptFallbackFrom = (path) => {
|
|
4982
|
-
const tree = readTranscriptTree(resolve
|
|
5290
|
+
const tree = readTranscriptTree(resolve(path));
|
|
4983
5291
|
if (tree.missing)
|
|
4984
5292
|
process.stderr.write(
|
|
4985
5293
|
`code-review adapt: transcript ${path} is unreadable \u2014 no telemetry fallback
|
|
@@ -4988,13 +5296,13 @@ var transcriptFallbackFrom = (path) => {
|
|
|
4988
5296
|
const usage = sumTranscriptUsage(tree.entries);
|
|
4989
5297
|
return { models: usage.models, turns: usage.turns, durationMs: usage.durationMs };
|
|
4990
5298
|
};
|
|
4991
|
-
var bundledPath = (...segments) => resolve
|
|
5299
|
+
var bundledPath = (...segments) => resolve(import.meta.dirname, "..", ...segments);
|
|
4992
5300
|
var readPackageVersion = () => JSON.parse(readFileSync(bundledPath("package.json"), "utf-8")).version;
|
|
4993
5301
|
var packageVersion = readPackageVersion();
|
|
4994
|
-
var resolveTemplatePath = (templateArg) => templateArg ? resolve
|
|
4995
|
-
var resolveInlineTemplatePath = (templateArg) => templateArg ? resolve
|
|
5302
|
+
var resolveTemplatePath = (templateArg) => templateArg ? resolve(templateArg) : bundledPath("templates", "comment.eta");
|
|
5303
|
+
var resolveInlineTemplatePath = (templateArg) => templateArg ? resolve(templateArg) : bundledPath("templates", "inline.eta");
|
|
4996
5304
|
var resolvePrices = (pricesArg) => {
|
|
4997
|
-
if (pricesArg) return { kind: "provided", path: resolve
|
|
5305
|
+
if (pricesArg) return { kind: "provided", path: resolve(pricesArg) };
|
|
4998
5306
|
process.stderr.write(
|
|
4999
5307
|
"code-review: no --prices given \u2014 cost will be reported as N/A (no price map to recompute from)\n"
|
|
5000
5308
|
);
|
|
@@ -5063,6 +5371,7 @@ var renderCmd = defineCommand({
|
|
|
5063
5371
|
const templatePath = resolveTemplatePath(args.template);
|
|
5064
5372
|
const priceResolution = resolvePrices(args.prices);
|
|
5065
5373
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
5374
|
+
warnStalePrices(priceResolution.kind === "provided" ? prices : null);
|
|
5066
5375
|
const template = readFileSync(templatePath, "utf-8");
|
|
5067
5376
|
const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
|
|
5068
5377
|
const clocDiff = args["cloc-diff"] ? loadClocDiff(args["cloc-diff"]) : void 0;
|
|
@@ -5123,7 +5432,7 @@ var inlineCmd = defineCommand({
|
|
|
5123
5432
|
},
|
|
5124
5433
|
run: async ({ args }) => {
|
|
5125
5434
|
const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
|
|
5126
|
-
const diff = readFileSync(resolve
|
|
5435
|
+
const diff = readFileSync(resolve(args.diff), "utf-8");
|
|
5127
5436
|
const inlineTemplate = readFileSync(resolveInlineTemplatePath(args.template), "utf-8");
|
|
5128
5437
|
const floor = parseNitVisibilityFloor(args["nit-visibility-floor"]);
|
|
5129
5438
|
const visibleFindings = findings.findings.filter((f) => !isBelowVisibilityFloor(f, floor));
|
|
@@ -5156,9 +5465,12 @@ var costCmd = defineCommand({
|
|
|
5156
5465
|
run: async ({ args }) => {
|
|
5157
5466
|
const envelope = decode(ResultEnvelopeCodec.decode(readJSON(args.envelope)), "envelope");
|
|
5158
5467
|
const prices = decode(PriceMapCodec.decode(readJSON(args.prices)), "prices");
|
|
5468
|
+
warnStalePrices(prices);
|
|
5159
5469
|
const report = computeCost(
|
|
5160
5470
|
envelope.models,
|
|
5161
5471
|
prices,
|
|
5472
|
+
// Price a saved envelope at the RUN's own instant (issue #170), so re-running `cost` later prices
|
|
5473
|
+
// the same envelope to the same slot deterministically — not at whatever wall clock it is re-run at.
|
|
5162
5474
|
parseInstant(envelope.generated_at) ?? /* @__PURE__ */ new Date()
|
|
5163
5475
|
);
|
|
5164
5476
|
process.stdout.write(JSON.stringify(report, null, 2));
|
|
@@ -5177,11 +5489,11 @@ var checkCostCmd = defineCommand({
|
|
|
5177
5489
|
},
|
|
5178
5490
|
prices: {
|
|
5179
5491
|
type: "string",
|
|
5180
|
-
description: "Path to price map JSON (default: bundled schema/prices.example.json \u2014 token totals stay real, cost reads as
|
|
5492
|
+
description: "Path to price map JSON (default: bundled schema/prices.example.json \u2014 token totals stay real, cost reads as N/A)"
|
|
5181
5493
|
}
|
|
5182
5494
|
},
|
|
5183
5495
|
run: async ({ args }) => {
|
|
5184
|
-
const tree = readTranscriptTree(resolve
|
|
5496
|
+
const tree = readTranscriptTree(resolve(args.transcript));
|
|
5185
5497
|
if (tree.missing) {
|
|
5186
5498
|
process.stderr.write(
|
|
5187
5499
|
`code-review check-cost: transcript ${args.transcript} is unreadable \u2014 reporting zero spend
|
|
@@ -5191,6 +5503,7 @@ var checkCostCmd = defineCommand({
|
|
|
5191
5503
|
const usage = sumTranscriptUsage(tree.entries);
|
|
5192
5504
|
const priceResolution = resolvePrices(args.prices);
|
|
5193
5505
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
5506
|
+
warnStalePrices(priceResolution.kind === "provided" ? prices : null);
|
|
5194
5507
|
const report = computeCost(
|
|
5195
5508
|
usage.models,
|
|
5196
5509
|
prices,
|
|
@@ -5214,7 +5527,7 @@ var checkCostCmd = defineCommand({
|
|
|
5214
5527
|
});
|
|
5215
5528
|
var tryReadPrices = (path) => {
|
|
5216
5529
|
try {
|
|
5217
|
-
const decoded = PriceMapCodec.decode(JSON.parse(readFileSync(resolve
|
|
5530
|
+
const decoded = PriceMapCodec.decode(JSON.parse(readFileSync(resolve(path), "utf-8")));
|
|
5218
5531
|
return decoded._tag === "Right" ? decoded.right : null;
|
|
5219
5532
|
} catch {
|
|
5220
5533
|
return null;
|
|
@@ -5333,13 +5646,13 @@ var budgetHookCmd = defineCommand({
|
|
|
5333
5646
|
},
|
|
5334
5647
|
run: async ({ args }) => {
|
|
5335
5648
|
try {
|
|
5336
|
-
const draftPath = resolve
|
|
5649
|
+
const draftPath = resolve(args.draft);
|
|
5337
5650
|
const input = readStdinJSON();
|
|
5338
5651
|
const transcriptPath = transcriptPathOf(input);
|
|
5339
|
-
const tree = transcriptPath ? readTranscriptTree(resolve
|
|
5652
|
+
const tree = transcriptPath ? readTranscriptTree(resolve(transcriptPath)) : void 0;
|
|
5340
5653
|
const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
|
|
5341
5654
|
const prices = args.prices ? tryReadPrices(args.prices) : null;
|
|
5342
|
-
const
|
|
5655
|
+
const costReport = prices !== null && usage ? (
|
|
5343
5656
|
// Price at the transcript's last activity instant, not the wall clock (issue #170 review
|
|
5344
5657
|
// r2). Silent warn: this budget-steering cost is recomputed on EVERY tool event, so a
|
|
5345
5658
|
// misconfigured slot map would otherwise flood stderr; the final post's cost render warns.
|
|
@@ -5348,8 +5661,27 @@ var budgetHookCmd = defineCommand({
|
|
|
5348
5661
|
prices,
|
|
5349
5662
|
usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date(),
|
|
5350
5663
|
() => void 0
|
|
5351
|
-
)
|
|
5664
|
+
)
|
|
5352
5665
|
) : null;
|
|
5666
|
+
if (!isSubagentHookInput(input)) {
|
|
5667
|
+
const axisPath = costAxisDisengagedPath(draftPath);
|
|
5668
|
+
const action = costAxisAnnouncement(costReport, readFileOrNull(axisPath) !== null);
|
|
5669
|
+
if (action === "announce") {
|
|
5670
|
+
process.stderr.write(
|
|
5671
|
+
"code-review budget-hook: the price map does not cover every model in the transcript, or its slots leave the run instant uncovered \u2014 the cost axis is disengaged for this run; add the missing models (or fix the slot partition) and re-verify\n"
|
|
5672
|
+
);
|
|
5673
|
+
try {
|
|
5674
|
+
writeFileSync(axisPath, "");
|
|
5675
|
+
} catch {
|
|
5676
|
+
}
|
|
5677
|
+
} else if (action === "clear") {
|
|
5678
|
+
try {
|
|
5679
|
+
unlinkSync(axisPath);
|
|
5680
|
+
} catch {
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
const spentUsd = costReport !== null && costReport.allKnown ? costReport.totalCostUSD : null;
|
|
5353
5685
|
const wallMs = args.wall ? parseWallMs(args.wall) : null;
|
|
5354
5686
|
const output2 = evaluateBudgetHook(input, {
|
|
5355
5687
|
spentUsd,
|
|
@@ -5446,7 +5778,7 @@ var printSettingsCmd = defineCommand({
|
|
|
5446
5778
|
if (args.kind && !["findings", "triage", "prices"].includes(args.kind))
|
|
5447
5779
|
fail(`--kind must be one of findings|triage|prices (got '${args.kind}')`);
|
|
5448
5780
|
const settings = composeReviewSettings({
|
|
5449
|
-
draftPath: resolve
|
|
5781
|
+
draftPath: resolve(args.draft),
|
|
5450
5782
|
stop: {
|
|
5451
5783
|
kind: args.kind,
|
|
5452
5784
|
schema: args.schema,
|
|
@@ -5538,7 +5870,7 @@ var validateCmd = defineCommand({
|
|
|
5538
5870
|
run: async ({ args }) => {
|
|
5539
5871
|
const kind = requireSchemaKind(args.kind || "findings");
|
|
5540
5872
|
const documentRaw = readJSON(args.document);
|
|
5541
|
-
const schemaPath = args.schema ? resolve
|
|
5873
|
+
const schemaPath = args.schema ? resolve(args.schema) : requireSchemaPath(kind, args["schema-version"] || derivedSchemaVersion(kind, documentRaw));
|
|
5542
5874
|
const { valid, errors } = validateAgainstSchema(documentRaw, schemaPath);
|
|
5543
5875
|
if (valid) {
|
|
5544
5876
|
process.stdout.write("\u2705 valid\n");
|
|
@@ -5605,7 +5937,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5605
5937
|
}
|
|
5606
5938
|
},
|
|
5607
5939
|
run: async ({ args }) => {
|
|
5608
|
-
const outPath = resolve
|
|
5940
|
+
const outPath = resolve(args.out);
|
|
5609
5941
|
const kindArg = args.kind || "findings";
|
|
5610
5942
|
const kind = isSchemaKind(kindArg) ? kindArg : "findings";
|
|
5611
5943
|
if (kind !== kindArg) {
|
|
@@ -5634,7 +5966,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5634
5966
|
if (!args.prior) return null;
|
|
5635
5967
|
const raw = (() => {
|
|
5636
5968
|
try {
|
|
5637
|
-
return JSON.parse(readFileSync(resolve
|
|
5969
|
+
return JSON.parse(readFileSync(resolve(args.prior), "utf-8"));
|
|
5638
5970
|
} catch {
|
|
5639
5971
|
return null;
|
|
5640
5972
|
}
|
|
@@ -5644,7 +5976,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5644
5976
|
const stagedPrior = (() => {
|
|
5645
5977
|
if (!args["prior-findings"]) return null;
|
|
5646
5978
|
try {
|
|
5647
|
-
return JSON.parse(readFileSync(resolve
|
|
5979
|
+
return JSON.parse(readFileSync(resolve(args["prior-findings"]), "utf-8"));
|
|
5648
5980
|
} catch {
|
|
5649
5981
|
return null;
|
|
5650
5982
|
}
|
|
@@ -5656,7 +5988,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5656
5988
|
const answeredRegistry = (() => {
|
|
5657
5989
|
if (!args["prior-answers"]) return null;
|
|
5658
5990
|
try {
|
|
5659
|
-
const raw = JSON.parse(readFileSync(resolve
|
|
5991
|
+
const raw = JSON.parse(readFileSync(resolve(args["prior-answers"]), "utf-8"));
|
|
5660
5992
|
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
5661
5993
|
const decoded = raw.flatMap((row) => {
|
|
5662
5994
|
const entry = decodeAnsweredEntry(row);
|
|
@@ -5725,7 +6057,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5725
6057
|
}
|
|
5726
6058
|
const seededFromPrior = priorFindings === null ? false : (() => {
|
|
5727
6059
|
try {
|
|
5728
|
-
const schemaPath = args.schema ? resolve
|
|
6060
|
+
const schemaPath = args.schema ? resolve(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
5729
6061
|
const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
|
|
5730
6062
|
Object.entries(priorFindings).filter(
|
|
5731
6063
|
([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
|
|
@@ -5942,7 +6274,7 @@ var readFileLines = (path) => {
|
|
|
5942
6274
|
};
|
|
5943
6275
|
var validateFinding = (finding, repoRoot) => {
|
|
5944
6276
|
if (finding.patch === void 0) return finding;
|
|
5945
|
-
const lines = readFileLines(resolve
|
|
6277
|
+
const lines = readFileLines(resolve(repoRoot, finding.path));
|
|
5946
6278
|
if (lines === null) {
|
|
5947
6279
|
process.stderr.write(
|
|
5948
6280
|
`validate-patches: ${finding.path}: could not read file at "${repoRoot}" \u2014 dropping patch
|
|
@@ -5982,7 +6314,7 @@ var validatePatchesCmd = defineCommand({
|
|
|
5982
6314
|
},
|
|
5983
6315
|
run: async ({ args }) => {
|
|
5984
6316
|
const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
|
|
5985
|
-
const repoRoot = args["repo-root"] ? resolve
|
|
6317
|
+
const repoRoot = args["repo-root"] ? resolve(args["repo-root"]) : process.cwd();
|
|
5986
6318
|
const validated = {
|
|
5987
6319
|
...findings,
|
|
5988
6320
|
findings: findings.findings.map((f) => validateFinding(f, repoRoot))
|
|
@@ -6096,7 +6428,7 @@ var stopGateCmd = defineCommand({
|
|
|
6096
6428
|
}
|
|
6097
6429
|
},
|
|
6098
6430
|
run: async ({ args }) => {
|
|
6099
|
-
const draftPath = resolve
|
|
6431
|
+
const draftPath = resolve(args.draft);
|
|
6100
6432
|
if (args["print-settings"]) {
|
|
6101
6433
|
const command = defaultHookCommand(draftPath, {
|
|
6102
6434
|
kind: args.kind,
|
|
@@ -6112,10 +6444,10 @@ var stopGateCmd = defineCommand({
|
|
|
6112
6444
|
drainStdin();
|
|
6113
6445
|
const kind = requireSchemaKind(args.kind || "findings");
|
|
6114
6446
|
const maxNudges = requireMaxNudges(args["max-nudges"]);
|
|
6115
|
-
const counterPath = args.counter ? resolve
|
|
6447
|
+
const counterPath = args.counter ? resolve(args.counter) : `${draftPath}.nudges`;
|
|
6116
6448
|
const state = draftState(
|
|
6117
6449
|
draftPath,
|
|
6118
|
-
(parsed) => args.schema ? resolve
|
|
6450
|
+
(parsed) => args.schema ? resolve(args.schema) : schemaPathFor(kind, args["schema-version"] || derivedSchemaVersion(kind, parsed))
|
|
6119
6451
|
);
|
|
6120
6452
|
const nudges = readNudges(counterPath);
|
|
6121
6453
|
const decision = decideGate(state, nudges, maxNudges, draftPath, kind);
|
|
@@ -6183,7 +6515,7 @@ var gatherCmd = defineCommand({
|
|
|
6183
6515
|
runId: args["run-id"],
|
|
6184
6516
|
conclusion: args.conclusion,
|
|
6185
6517
|
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
6186
|
-
outDir: args["out-dir"] ? resolve
|
|
6518
|
+
outDir: args["out-dir"] ? resolve(args["out-dir"]) : process.cwd()
|
|
6187
6519
|
});
|
|
6188
6520
|
process.stdout.write(renderOutputs(result));
|
|
6189
6521
|
}
|
|
@@ -6277,6 +6609,11 @@ var postCmd = defineCommand({
|
|
|
6277
6609
|
},
|
|
6278
6610
|
run: async ({ args }) => {
|
|
6279
6611
|
const priceResolution = resolvePrices(args.prices);
|
|
6612
|
+
if (/--|\s/.test(args["head-sha"])) {
|
|
6613
|
+
throw new Error(
|
|
6614
|
+
'--head-sha must be a commit SHA (no "--" or whitespace): ' + args["head-sha"]
|
|
6615
|
+
);
|
|
6616
|
+
}
|
|
6280
6617
|
await post({
|
|
6281
6618
|
repo: args.repo,
|
|
6282
6619
|
// The workflow's post step threads HEAD_REPO env (the fork's owner/name) — a finding
|
|
@@ -6677,7 +7014,7 @@ var sandboxConfigCmd = defineCommand({
|
|
|
6677
7014
|
const json = `${JSON.stringify(config, null, 2)}
|
|
6678
7015
|
`;
|
|
6679
7016
|
if (args.out) {
|
|
6680
|
-
writeFileSync(resolve
|
|
7017
|
+
writeFileSync(resolve(args.out), json);
|
|
6681
7018
|
} else {
|
|
6682
7019
|
process.stdout.write(json);
|
|
6683
7020
|
}
|