@jphutchins/code-review 0.1.0-alpha.11 → 0.1.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +266 -83
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/comment.eta +17 -16
package/dist/index.js
CHANGED
|
@@ -178,6 +178,16 @@ ${marker}` : "";
|
|
|
178
178
|
};
|
|
179
179
|
var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
|
|
180
180
|
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
181
|
+
var parseFindingsMarker = (body) => {
|
|
182
|
+
const match = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body);
|
|
183
|
+
const b64 = match?.[1];
|
|
184
|
+
if (b64 === void 0) return null;
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
181
191
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
182
192
|
var projectPatch = (patch) => {
|
|
183
193
|
if (patch === void 0) return { kind: "none" };
|
|
@@ -234,6 +244,7 @@ var render = (input) => {
|
|
|
234
244
|
postedAt: input.postedAt ?? "",
|
|
235
245
|
severityCounts: input.severityCounts ?? computeSeverityCounts(input.findings.findings),
|
|
236
246
|
strays: (input.strays ?? []).map(sanitizeFinding),
|
|
247
|
+
unanchoredCount: input.unanchoredCount ?? 0,
|
|
237
248
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
238
249
|
runUrl: input.runUrl ?? null,
|
|
239
250
|
jsonUrl: input.jsonUrl ?? null,
|
|
@@ -355,7 +366,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
355
366
|
}
|
|
356
367
|
return comment;
|
|
357
368
|
});
|
|
358
|
-
return { comments, strays };
|
|
369
|
+
return { comments, strays, inDiff };
|
|
359
370
|
};
|
|
360
371
|
var renderStraysSection = (strays) => {
|
|
361
372
|
if (strays.length === 0) return "";
|
|
@@ -660,7 +671,7 @@ var decideGate = (state, nudges, maxNudges, draftPath, kind) => {
|
|
|
660
671
|
reason: [
|
|
661
672
|
`This review is not complete \u2014 ${whatsWrong(state, draftPath, kind)}`,
|
|
662
673
|
`The only deliverable is a ${kind} document that validates against the ${kind} schema \u2014 run "code-review print-schema ${kind}" to see the exact shape.`,
|
|
663
|
-
`Write it to ${draftPath}, then run "code-review validate ${draftPath} --kind ${kind}" until it exits 0 before ending your turn.`
|
|
674
|
+
`Write it to ${draftPath}, then run "code-review validate ${draftPath} --kind ${kind} --explain" until it exits 0 before ending your turn (--explain prints the schema when the shape is wrong).`
|
|
664
675
|
].join("\n")
|
|
665
676
|
};
|
|
666
677
|
};
|
|
@@ -724,19 +735,26 @@ var stopHookSettings = (command) => ({
|
|
|
724
735
|
|
|
725
736
|
// src/budget.ts
|
|
726
737
|
var DEADLINE_ENV = "CODE_REVIEW_DEADLINE_EPOCH";
|
|
727
|
-
var DEFAULT_RESERVE = {
|
|
738
|
+
var DEFAULT_RESERVE = {
|
|
739
|
+
frac: 0.15,
|
|
740
|
+
growth: 0.25,
|
|
741
|
+
flatUsd: 0.02,
|
|
742
|
+
flatMs: 12e4
|
|
743
|
+
};
|
|
728
744
|
var SOFT_MULTIPLE = 2;
|
|
729
745
|
var costAxis = (i) => i.spentUsd !== null && i.budgetUsd !== null && i.budgetUsd > 0 ? { used: i.spentUsd, limit: i.budgetUsd, flat: i.reserve.flatUsd } : null;
|
|
730
746
|
var timeAxis = (i) => i.elapsedMs !== null && i.wallMs !== null && i.wallMs > 0 ? { used: i.elapsedMs, limit: i.wallMs, flat: i.reserve.flatMs } : null;
|
|
731
|
-
var axisSeverity = (a,
|
|
732
|
-
const
|
|
747
|
+
var axisSeverity = (a, reserve) => {
|
|
748
|
+
const usedFrac = Math.min(1, Math.max(0, a.used / a.limit));
|
|
749
|
+
const effFrac = reserve.frac + reserve.growth * usedFrac;
|
|
750
|
+
const hardReserve = Math.max(a.flat, effFrac * a.limit);
|
|
733
751
|
const remaining = a.limit - a.used;
|
|
734
752
|
if (remaining <= hardReserve) return 2;
|
|
735
753
|
if (remaining <= SOFT_MULTIPLE * hardReserve) return 1;
|
|
736
754
|
return 0;
|
|
737
755
|
};
|
|
738
756
|
var decideBudget = (i) => {
|
|
739
|
-
const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a, i.reserve
|
|
757
|
+
const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a, i.reserve)), 0);
|
|
740
758
|
return worst === 2 ? { kind: "hard" } : worst === 1 ? { kind: "soft" } : { kind: "ok" };
|
|
741
759
|
};
|
|
742
760
|
var pct = (n) => `${String(Math.round(n * 100))}%`;
|
|
@@ -744,7 +762,7 @@ var money = (n) => `$${n.toFixed(2)}`;
|
|
|
744
762
|
var mins = (ms) => `${(ms / 6e4).toFixed(1)}m`;
|
|
745
763
|
var spendClause = (i) => i.spentUsd === null ? null : i.budgetUsd !== null && i.budgetUsd > 0 ? `spent ${money(i.spentUsd)}/${money(i.budgetUsd)} (${pct(i.spentUsd / i.budgetUsd)})` : `spent ${money(i.spentUsd)}`;
|
|
746
764
|
var timeClause = (i) => i.elapsedMs === null ? null : i.wallMs !== null && i.wallMs > 0 ? `${mins(i.elapsedMs)}/${mins(i.wallMs)} elapsed (${pct(i.elapsedMs / i.wallMs)})` : `${mins(i.elapsedMs)} elapsed`;
|
|
747
|
-
var directive = (phase, draftPath) => phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Write your COMPLETE findings to ${draftPath} and run \`code-review validate ${draftPath}\` until it passes. Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, then validate \u2014 you may run out of budget before you finish otherwise.`;
|
|
765
|
+
var directive = (phase, draftPath) => phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Write your COMPLETE findings to ${draftPath} and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 you may run out of budget before you finish otherwise.`;
|
|
748
766
|
var budgetMessage = (i, phase, draftPath) => {
|
|
749
767
|
const status = [spendClause(i), timeClause(i)].filter((c) => c !== null).join(" \xB7 ");
|
|
750
768
|
return `Budget check \u2014 ${status}. ${directive(phase, draftPath)}`;
|
|
@@ -838,6 +856,7 @@ var budgetHookCommand = (draftPath, opts) => [
|
|
|
838
856
|
...opts.wall ? ["--wall", shellQuote(opts.wall)] : [],
|
|
839
857
|
...opts.prices ? ["--prices", shellQuote(opts.prices)] : [],
|
|
840
858
|
...opts.reserveFrac ? ["--reserve-frac", shellQuote(opts.reserveFrac)] : [],
|
|
859
|
+
...opts.reserveGrowth ? ["--reserve-growth", shellQuote(opts.reserveGrowth)] : [],
|
|
841
860
|
...opts.reserveUsd ? ["--reserve-usd", shellQuote(opts.reserveUsd)] : [],
|
|
842
861
|
...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : []
|
|
843
862
|
].join(" ");
|
|
@@ -1081,25 +1100,50 @@ var parseHtmlUrl = (raw) => {
|
|
|
1081
1100
|
return void 0;
|
|
1082
1101
|
}
|
|
1083
1102
|
};
|
|
1084
|
-
var
|
|
1103
|
+
var commentPayload = (c) => ({
|
|
1104
|
+
path: c.path,
|
|
1105
|
+
line: c.line,
|
|
1106
|
+
side: c.side,
|
|
1107
|
+
...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
|
|
1108
|
+
body: formatMarkdown(c.body)
|
|
1109
|
+
});
|
|
1110
|
+
var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, marker, ghApi) => {
|
|
1085
1111
|
const pointer = reviewBodyPointer(headSha, stickyUrl, marker);
|
|
1086
|
-
const
|
|
1112
|
+
const reviewBody = (withComments) => JSON.stringify({
|
|
1087
1113
|
body: pointer,
|
|
1088
1114
|
commit_id: headSha,
|
|
1089
1115
|
event: "COMMENT",
|
|
1090
|
-
comments: comments.map(
|
|
1091
|
-
path: c.path,
|
|
1092
|
-
line: c.line,
|
|
1093
|
-
side: c.side,
|
|
1094
|
-
...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
|
|
1095
|
-
body: formatMarkdown(c.body)
|
|
1096
|
-
}))
|
|
1116
|
+
comments: withComments ? comments.map(commentPayload) : []
|
|
1097
1117
|
});
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1118
|
+
const reviewsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/reviews`, "--input", "-"];
|
|
1119
|
+
try {
|
|
1120
|
+
const stdout = await ghApi(reviewsEndpoint, reviewBody(true));
|
|
1121
|
+
return { url: parseHtmlUrl(stdout), inlinePosted: comments.length, unposted: [] };
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
if (comments.length === 0) throw err;
|
|
1124
|
+
process.stderr.write(
|
|
1125
|
+
`Warning: the batched inline review on PR #${String(prNumber)} was rejected (${err instanceof Error ? err.message : String(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
|
|
1126
|
+
`
|
|
1127
|
+
);
|
|
1128
|
+
const url = parseHtmlUrl(await ghApi(reviewsEndpoint, reviewBody(false)));
|
|
1129
|
+
const commentsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/comments`, "--input", "-"];
|
|
1130
|
+
const unposted = [];
|
|
1131
|
+
let inlinePosted = 0;
|
|
1132
|
+
for (const [i, c] of comments.entries()) {
|
|
1133
|
+
try {
|
|
1134
|
+
await ghApi(commentsEndpoint, JSON.stringify({ commit_id: headSha, ...commentPayload(c) }));
|
|
1135
|
+
inlinePosted += 1;
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
const finding = inDiff[i];
|
|
1138
|
+
if (finding) unposted.push(finding);
|
|
1139
|
+
process.stderr.write(
|
|
1140
|
+
`Warning: inline comment on ${c.path}:${String(c.line)} rejected (${e instanceof Error ? e.message : String(e)}) \u2014 surfacing that finding in the sticky instead (issue #57)
|
|
1141
|
+
`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return { url, inlinePosted, unposted };
|
|
1146
|
+
}
|
|
1103
1147
|
};
|
|
1104
1148
|
var findBotComment = async (repo, prNumber, botLogin, marker, ghApi) => {
|
|
1105
1149
|
const stdout = await ghApi(
|
|
@@ -1166,10 +1210,7 @@ var fetchBotReviews = async (repo, prNumber, botLogin, ghApi) => {
|
|
|
1166
1210
|
return [];
|
|
1167
1211
|
}
|
|
1168
1212
|
if (!Array.isArray(reviews)) return [];
|
|
1169
|
-
return reviews.filter(isBotReview).filter((r) => r.user.login === botLogin && r.state !== "DISMISSED").map((r) => ({
|
|
1170
|
-
id: r.id,
|
|
1171
|
-
commitId: typeof r.commit_id === "string" ? r.commit_id : ""
|
|
1172
|
-
}));
|
|
1213
|
+
return reviews.filter(isBotReview).filter((r) => r.user.login === botLogin && r.state !== "DISMISSED").map((r) => ({ id: r.id }));
|
|
1173
1214
|
};
|
|
1174
1215
|
var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
1175
1216
|
for (const id of ids) {
|
|
@@ -1192,16 +1233,15 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
|
1192
1233
|
}
|
|
1193
1234
|
}
|
|
1194
1235
|
};
|
|
1195
|
-
var REVIEW_THREAD_COMMENTS_QUERY = "query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){reviewThreads(first:100){pageInfo{hasNextPage}nodes{comments(first:100){nodes{id isMinimized author{login}
|
|
1236
|
+
var REVIEW_THREAD_COMMENTS_QUERY = "query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){reviewThreads(first:100){pageInfo{hasNextPage}nodes{comments(first:100){nodes{id isMinimized author{login}}}}}}}}";
|
|
1196
1237
|
var MINIMIZE_COMMENT_MUTATION = "mutation($id:ID!){minimizeComment(input:{subjectId:$id,classifier:OUTDATED}){minimizedComment{isMinimized}}}";
|
|
1197
|
-
var
|
|
1238
|
+
var priorBotCommentId = (c, logins) => {
|
|
1198
1239
|
if (typeof c !== "object" || c === null) return null;
|
|
1199
1240
|
const o = c;
|
|
1200
1241
|
const login = o.author?.login;
|
|
1201
|
-
|
|
1202
|
-
return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) && typeof oid === "string" && oid !== headSha ? o.id : null;
|
|
1242
|
+
return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) ? o.id : null;
|
|
1203
1243
|
};
|
|
1204
|
-
var
|
|
1244
|
+
var priorBotCommentIds = (raw, botLogin) => {
|
|
1205
1245
|
let parsed;
|
|
1206
1246
|
try {
|
|
1207
1247
|
parsed = JSON.parse(raw);
|
|
@@ -1215,13 +1255,13 @@ var supersededBotCommentIds = (raw, headSha, botLogin) => {
|
|
|
1215
1255
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
1216
1256
|
const ids = nodes.flatMap((t4) => {
|
|
1217
1257
|
const cnodes = t4.comments?.nodes;
|
|
1218
|
-
return Array.isArray(cnodes) ? cnodes.map((c) =>
|
|
1258
|
+
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
1219
1259
|
});
|
|
1220
1260
|
return { ids, truncated };
|
|
1221
1261
|
};
|
|
1222
|
-
var
|
|
1262
|
+
var listPriorBotCommentIds = async (repo, prNumber, botLogin, ghApi) => {
|
|
1223
1263
|
const slash = repo.indexOf("/");
|
|
1224
|
-
if (slash <= 0) return;
|
|
1264
|
+
if (slash <= 0) return [];
|
|
1225
1265
|
const owner = repo.slice(0, slash);
|
|
1226
1266
|
const name = repo.slice(slash + 1);
|
|
1227
1267
|
let raw;
|
|
@@ -1242,15 +1282,18 @@ var minimizeSupersededComments = async (repo, prNumber, headSha, botLogin, ghApi
|
|
|
1242
1282
|
`Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
|
|
1243
1283
|
`
|
|
1244
1284
|
);
|
|
1245
|
-
return;
|
|
1285
|
+
return [];
|
|
1246
1286
|
}
|
|
1247
|
-
const { ids, truncated } =
|
|
1287
|
+
const { ids, truncated } = priorBotCommentIds(raw, botLogin);
|
|
1248
1288
|
if (truncated) {
|
|
1249
1289
|
process.stderr.write(
|
|
1250
1290
|
`Note: PR #${String(prNumber)} has more than 100 review threads \u2014 only the first 100 were scanned for stale bot comments
|
|
1251
1291
|
`
|
|
1252
1292
|
);
|
|
1253
1293
|
}
|
|
1294
|
+
return ids;
|
|
1295
|
+
};
|
|
1296
|
+
var minimizeComments = async (prNumber, ids, ghApi) => {
|
|
1254
1297
|
let minimized = 0;
|
|
1255
1298
|
for (const id of ids) {
|
|
1256
1299
|
try {
|
|
@@ -1365,7 +1408,11 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1365
1408
|
process.exit(0);
|
|
1366
1409
|
}
|
|
1367
1410
|
const findingsMarker = findingsPointer(findings, input.jsonUrl);
|
|
1368
|
-
const {
|
|
1411
|
+
const {
|
|
1412
|
+
comments: rawComments,
|
|
1413
|
+
strays,
|
|
1414
|
+
inDiff
|
|
1415
|
+
} = buildInlineComments(findings.findings, diff, {
|
|
1369
1416
|
inlineTemplate,
|
|
1370
1417
|
models: envelope.models.map((m) => m.model),
|
|
1371
1418
|
findings,
|
|
@@ -1379,8 +1426,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1379
1426
|
);
|
|
1380
1427
|
}
|
|
1381
1428
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
1382
|
-
const
|
|
1383
|
-
const initialDisposition = comments.length > 0 ? alreadyReviewedThisSha ? { kind: "suppressed-existing-review", sha: input.headSha } : void 0 : strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1429
|
+
const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1384
1430
|
const commonRenderInput = {
|
|
1385
1431
|
findings,
|
|
1386
1432
|
envelope,
|
|
@@ -1404,7 +1450,15 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1404
1450
|
|
|
1405
1451
|
> **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.
|
|
1406
1452
|
` : "";
|
|
1407
|
-
const renderBody = (inlineDisposition, reviewUrl2
|
|
1453
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2) => formatMarkdown(
|
|
1454
|
+
render({
|
|
1455
|
+
...commonRenderInput,
|
|
1456
|
+
...straysOverride ? { strays: straysOverride } : {},
|
|
1457
|
+
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
1458
|
+
inlineDisposition,
|
|
1459
|
+
reviewUrl: reviewUrl2
|
|
1460
|
+
}) + longFilesNote
|
|
1461
|
+
);
|
|
1408
1462
|
const stickyRef = await upsertSticky(
|
|
1409
1463
|
input.repo,
|
|
1410
1464
|
prNumber,
|
|
@@ -1412,49 +1466,53 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1412
1466
|
renderBody(initialDisposition),
|
|
1413
1467
|
ghApi
|
|
1414
1468
|
);
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
}
|
|
1426
|
-
const reviewUrl = await postInlineReview(
|
|
1469
|
+
const priorInlineComments = await listPriorBotCommentIds(
|
|
1470
|
+
input.repo,
|
|
1471
|
+
prNumber,
|
|
1472
|
+
input.botLogin,
|
|
1473
|
+
ghApi
|
|
1474
|
+
);
|
|
1475
|
+
const {
|
|
1476
|
+
url: reviewUrl,
|
|
1477
|
+
inlinePosted,
|
|
1478
|
+
unposted
|
|
1479
|
+
} = await postInlineReview(
|
|
1427
1480
|
input.repo,
|
|
1428
1481
|
prNumber,
|
|
1429
1482
|
input.headSha,
|
|
1430
1483
|
comments,
|
|
1484
|
+
inDiff,
|
|
1431
1485
|
stickyRef?.url,
|
|
1432
1486
|
findingsMarker,
|
|
1433
1487
|
ghApi
|
|
1434
1488
|
);
|
|
1435
1489
|
process.stderr.write(
|
|
1436
|
-
`Posted a review with ${String(
|
|
1490
|
+
`Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
|
|
1437
1491
|
`
|
|
1438
1492
|
);
|
|
1439
|
-
|
|
1440
|
-
if (
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1493
|
+
const priorReviewIds = botReviews.map((r) => r.id);
|
|
1494
|
+
if (priorReviewIds.length > 0) {
|
|
1495
|
+
await dismissReviews(input.repo, prNumber, priorReviewIds, ghApi);
|
|
1496
|
+
}
|
|
1497
|
+
await minimizeComments(prNumber, priorInlineComments, ghApi);
|
|
1498
|
+
const unanchoredCount = unposted.length;
|
|
1499
|
+
const finalStrays = unanchoredCount > 0 ? [...unposted, ...strays] : strays;
|
|
1500
|
+
if (stickyRef !== null && (inlinePosted > 0 || unanchoredCount > 0)) {
|
|
1501
|
+
const finalDisposition = inlinePosted > 0 ? { kind: "posted", count: inlinePosted, sha: input.headSha } : { kind: "inline-unavailable" };
|
|
1446
1502
|
try {
|
|
1447
1503
|
await patchComment(
|
|
1448
1504
|
input.repo,
|
|
1449
1505
|
stickyRef.id,
|
|
1450
|
-
renderBody(
|
|
1506
|
+
renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount),
|
|
1451
1507
|
ghApi
|
|
1452
1508
|
);
|
|
1453
|
-
process.stderr.write(
|
|
1454
|
-
`)
|
|
1509
|
+
process.stderr.write(
|
|
1510
|
+
`Updated sticky comment #${String(stickyRef.id)} to reflect the review
|
|
1511
|
+
`
|
|
1512
|
+
);
|
|
1455
1513
|
} catch (err) {
|
|
1456
1514
|
process.stderr.write(
|
|
1457
|
-
`Warning: failed to
|
|
1515
|
+
`Warning: failed to update the sticky summary after the review: ${err instanceof Error ? err.message : String(err)}
|
|
1458
1516
|
`
|
|
1459
1517
|
);
|
|
1460
1518
|
}
|
|
@@ -1792,18 +1850,18 @@ var withMeta = (base, meta) => ({
|
|
|
1792
1850
|
...meta.effort ? { effort: meta.effort } : {}
|
|
1793
1851
|
});
|
|
1794
1852
|
var resolveTelemetry = (native, meta) => {
|
|
1795
|
-
const fb =
|
|
1796
|
-
|
|
1853
|
+
const fb = (() => {
|
|
1854
|
+
try {
|
|
1855
|
+
return meta.transcriptFallback?.();
|
|
1856
|
+
} catch {
|
|
1857
|
+
return void 0;
|
|
1858
|
+
}
|
|
1859
|
+
})();
|
|
1860
|
+
const wallTurns = fb !== void 0 && fb.durationMs > 0 ? { turns: fb.turns, duration_ms: fb.durationMs } : { turns: native.turns, duration_ms: native.durationMs };
|
|
1797
1861
|
return withMeta(
|
|
1798
|
-
|
|
1799
|
-
models: [...fb.models],
|
|
1800
|
-
|
|
1801
|
-
duration_ms: fb.durationMs,
|
|
1802
|
-
vendor_cost_usd: native.vendorCostUsd
|
|
1803
|
-
} : {
|
|
1804
|
-
models: native.models,
|
|
1805
|
-
turns: native.turns,
|
|
1806
|
-
duration_ms: native.durationMs,
|
|
1862
|
+
{
|
|
1863
|
+
models: native.models.length > 0 ? native.models : fb ? [...fb.models] : native.models,
|
|
1864
|
+
...wallTurns,
|
|
1807
1865
|
vendor_cost_usd: native.vendorCostUsd
|
|
1808
1866
|
},
|
|
1809
1867
|
meta
|
|
@@ -2171,7 +2229,11 @@ var budgetHookCmd = defineCommand({
|
|
|
2171
2229
|
},
|
|
2172
2230
|
"reserve-frac": {
|
|
2173
2231
|
type: "string",
|
|
2174
|
-
description: "
|
|
2232
|
+
description: "Base wind-down headroom as a fraction of each budget: converge once less than this remains (default: 0.15; the soft steer tier reserves 2\xD7 this)"
|
|
2233
|
+
},
|
|
2234
|
+
"reserve-growth": {
|
|
2235
|
+
type: "string",
|
|
2236
|
+
description: "How much the reserve grows as a budget is spent \u2014 added at full usage, so convergence lands earlier the longer the run has gone (default: 0.25; 0 = flat reserve)"
|
|
2175
2237
|
},
|
|
2176
2238
|
"reserve-usd": {
|
|
2177
2239
|
type: "string",
|
|
@@ -2204,6 +2266,7 @@ var budgetHookCmd = defineCommand({
|
|
|
2204
2266
|
wallMs,
|
|
2205
2267
|
reserve: {
|
|
2206
2268
|
frac: parseFraction(args["reserve-frac"], DEFAULT_RESERVE.frac),
|
|
2269
|
+
growth: parseFraction(args["reserve-growth"], DEFAULT_RESERVE.growth),
|
|
2207
2270
|
flatUsd: parseBudgetUsd(args["reserve-usd"]) ?? DEFAULT_RESERVE.flatUsd,
|
|
2208
2271
|
flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs
|
|
2209
2272
|
},
|
|
@@ -2265,7 +2328,11 @@ var printSettingsCmd = defineCommand({
|
|
|
2265
2328
|
},
|
|
2266
2329
|
"reserve-frac": {
|
|
2267
2330
|
type: "string",
|
|
2268
|
-
description: "
|
|
2331
|
+
description: "Base wind-down headroom as a fraction of each budget (default: 0.15; soft tier is 2\xD7)"
|
|
2332
|
+
},
|
|
2333
|
+
"reserve-growth": {
|
|
2334
|
+
type: "string",
|
|
2335
|
+
description: "How much the reserve grows as a budget is spent, converging earlier the longer the run has gone (default: 0.25; 0 = flat)"
|
|
2269
2336
|
},
|
|
2270
2337
|
"reserve-usd": {
|
|
2271
2338
|
type: "string",
|
|
@@ -2293,6 +2360,7 @@ var printSettingsCmd = defineCommand({
|
|
|
2293
2360
|
wall: args.wall,
|
|
2294
2361
|
prices: args.prices,
|
|
2295
2362
|
reserveFrac: args["reserve-frac"],
|
|
2363
|
+
reserveGrowth: args["reserve-growth"],
|
|
2296
2364
|
reserveUsd: args["reserve-usd"],
|
|
2297
2365
|
reserveWall: args["reserve-wall"]
|
|
2298
2366
|
}
|
|
@@ -2324,6 +2392,13 @@ var deadlineCmd = defineCommand({
|
|
|
2324
2392
|
}
|
|
2325
2393
|
});
|
|
2326
2394
|
var derivedSchemaVersion = (kind, raw) => kind === "findings" ? declaredVersion(raw) : void 0;
|
|
2395
|
+
var printableSchema = (schemaPath) => {
|
|
2396
|
+
const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
|
|
2397
|
+
const enforcementSchema = Object.fromEntries(
|
|
2398
|
+
Object.entries(schema).filter(([key2]) => key2 !== "$schema")
|
|
2399
|
+
);
|
|
2400
|
+
return JSON.stringify(enforcementSchema, null, 2);
|
|
2401
|
+
};
|
|
2327
2402
|
var validateCmd = defineCommand({
|
|
2328
2403
|
meta: {
|
|
2329
2404
|
name: "validate",
|
|
@@ -2346,6 +2421,10 @@ var validateCmd = defineCommand({
|
|
|
2346
2421
|
"schema-version": {
|
|
2347
2422
|
type: "string",
|
|
2348
2423
|
description: "Schema major.minor version to validate against (default: the document's declared schema_version for findings, or the kind's latest)"
|
|
2424
|
+
},
|
|
2425
|
+
explain: {
|
|
2426
|
+
type: "boolean",
|
|
2427
|
+
description: "On failure, also print the schema after the errors \u2014 its field descriptions are the authoritative spec, so the document can be fixed in one pass instead of by trial and error"
|
|
2349
2428
|
}
|
|
2350
2429
|
},
|
|
2351
2430
|
run: async ({ args }) => {
|
|
@@ -2359,10 +2438,117 @@ var validateCmd = defineCommand({
|
|
|
2359
2438
|
process.stderr.write("\u274C invalid\n");
|
|
2360
2439
|
for (const e of errors) process.stderr.write(` - ${e}
|
|
2361
2440
|
`);
|
|
2441
|
+
if (args.explain) {
|
|
2442
|
+
process.stderr.write(
|
|
2443
|
+
`
|
|
2444
|
+
The ${kind} document must conform to this schema (the field descriptions are the authoritative spec \u2014 match the property names exactly):
|
|
2445
|
+
${printableSchema(schemaPath)}
|
|
2446
|
+
`
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2362
2449
|
process.exit(1);
|
|
2363
2450
|
}
|
|
2364
2451
|
}
|
|
2365
2452
|
});
|
|
2453
|
+
var seedDraftCmd = defineCommand({
|
|
2454
|
+
meta: {
|
|
2455
|
+
name: "seed-draft",
|
|
2456
|
+
description: "Write a valid findings $DRAFT before the review runs: the decoded findings from a prior review when one exists and still validates (incremental re-review), else an empty-but-valid scaffold \u2014 so a valid draft exists from turn 0 (issues #52, #53). Prints the mode chosen (prior|empty|none \u2014 none when even the scaffold write failed) to stdout; always exits 0"
|
|
2457
|
+
},
|
|
2458
|
+
args: {
|
|
2459
|
+
prior: {
|
|
2460
|
+
type: "string",
|
|
2461
|
+
description: "Path to the prior-review JSON gather staged ({ id, body }, or the literal null); its embedded base64 findings marker is decoded and becomes the seed when it validates against the schema"
|
|
2462
|
+
},
|
|
2463
|
+
out: {
|
|
2464
|
+
type: "string",
|
|
2465
|
+
description: "Path to write the seed $DRAFT to (an absolute path outside the worktree)",
|
|
2466
|
+
required: true
|
|
2467
|
+
},
|
|
2468
|
+
kind: {
|
|
2469
|
+
type: "string",
|
|
2470
|
+
description: "Schema kind to validate the prior findings against (default: findings)"
|
|
2471
|
+
},
|
|
2472
|
+
schema: {
|
|
2473
|
+
type: "string",
|
|
2474
|
+
description: "Path to a schema file (wins over --kind/--schema-version)"
|
|
2475
|
+
},
|
|
2476
|
+
"schema-version": {
|
|
2477
|
+
type: "string",
|
|
2478
|
+
description: "Schema major.minor to validate the prior findings against (default: the kind's latest \u2014 an older-shaped prior review then falls back to the empty scaffold)"
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
run: async ({ args }) => {
|
|
2482
|
+
const outPath = resolve$1(args.out);
|
|
2483
|
+
const kindArg = args.kind || "findings";
|
|
2484
|
+
const kind = isSchemaKind(kindArg) ? kindArg : "findings";
|
|
2485
|
+
if (kind !== kindArg) {
|
|
2486
|
+
process.stderr.write(
|
|
2487
|
+
`Warning: unknown --kind "${kindArg}" \u2014 validating against "findings"
|
|
2488
|
+
`
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
const writeEmptyScaffold = () => {
|
|
2492
|
+
try {
|
|
2493
|
+
writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
|
|
2494
|
+
`);
|
|
2495
|
+
process.stderr.write(
|
|
2496
|
+
`Seeded ${outPath} with an empty valid scaffold \u2014 no decodable prior findings to build on
|
|
2497
|
+
`
|
|
2498
|
+
);
|
|
2499
|
+
process.stdout.write("empty\n");
|
|
2500
|
+
} catch (err) {
|
|
2501
|
+
process.stderr.write(
|
|
2502
|
+
`Warning: could not write the seed scaffold to ${outPath} (${err instanceof Error ? err.message : String(err)}) \u2014 the agent will create $DRAFT itself
|
|
2503
|
+
`
|
|
2504
|
+
);
|
|
2505
|
+
process.stdout.write("none\n");
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
const priorFindings = (() => {
|
|
2509
|
+
if (!args.prior) return null;
|
|
2510
|
+
const raw = (() => {
|
|
2511
|
+
try {
|
|
2512
|
+
return JSON.parse(readFileSync(resolve$1(args.prior), "utf-8"));
|
|
2513
|
+
} catch {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
})();
|
|
2517
|
+
const body = typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
2518
|
+
return body === null ? null : parseFindingsMarker(body);
|
|
2519
|
+
})();
|
|
2520
|
+
if (priorFindings === null) {
|
|
2521
|
+
writeEmptyScaffold();
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const seededFromPrior = (() => {
|
|
2525
|
+
try {
|
|
2526
|
+
const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
2527
|
+
if (!validateAgainstSchema(priorFindings, schemaPath).valid) return false;
|
|
2528
|
+
writeFileSync(outPath, `${JSON.stringify(priorFindings, null, 2)}
|
|
2529
|
+
`);
|
|
2530
|
+
return true;
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
process.stderr.write(
|
|
2533
|
+
`Warning: could not seed from the prior review (${err instanceof Error ? err.message : String(err)}) \u2014 falling back to the empty scaffold
|
|
2534
|
+
`
|
|
2535
|
+
);
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
})();
|
|
2539
|
+
if (seededFromPrior) {
|
|
2540
|
+
const priorList = priorFindings.findings;
|
|
2541
|
+
const count = Array.isArray(priorList) ? priorList.length : 0;
|
|
2542
|
+
process.stderr.write(
|
|
2543
|
+
`Seeded ${outPath} from the prior review (${String(count)} finding(s)) \u2014 verify each still holds against the current diff and refine in place
|
|
2544
|
+
`
|
|
2545
|
+
);
|
|
2546
|
+
process.stdout.write("prior\n");
|
|
2547
|
+
} else {
|
|
2548
|
+
writeEmptyScaffold();
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2366
2552
|
var adaptCmd = defineCommand({
|
|
2367
2553
|
meta: {
|
|
2368
2554
|
name: "adapt",
|
|
@@ -2393,7 +2579,7 @@ var adaptCmd = defineCommand({
|
|
|
2393
2579
|
},
|
|
2394
2580
|
transcript: {
|
|
2395
2581
|
type: "string",
|
|
2396
|
-
description: "Path to the session transcript (the main .jsonl)
|
|
2582
|
+
description: "Path to the session transcript (the main .jsonl). Its tree (main + subagents) is the source of the true wall + turn count \u2014 the native envelope only sees the main agent and under-reports a fan-out (issue #59) \u2014 and refills per-model usage too when the native has none (a wall-clock kill leaves it empty, so cost is real not $0.00 \u2014 issues #39/#36)"
|
|
2397
2583
|
}
|
|
2398
2584
|
},
|
|
2399
2585
|
run: async ({ args }) => {
|
|
@@ -2571,11 +2757,7 @@ var printSchemaCmd = defineCommand({
|
|
|
2571
2757
|
run: async ({ args }) => {
|
|
2572
2758
|
const schemaKind = requireSchemaKind(args.name);
|
|
2573
2759
|
const schemaPath = requireSchemaPath(schemaKind, args["schema-version"]);
|
|
2574
|
-
|
|
2575
|
-
const enforcementSchema = Object.fromEntries(
|
|
2576
|
-
Object.entries(schema).filter(([key2]) => key2 !== "$schema")
|
|
2577
|
-
);
|
|
2578
|
-
process.stdout.write(`${JSON.stringify(enforcementSchema, null, 2)}
|
|
2760
|
+
process.stdout.write(`${printableSchema(schemaPath)}
|
|
2579
2761
|
`);
|
|
2580
2762
|
}
|
|
2581
2763
|
});
|
|
@@ -2811,7 +2993,7 @@ var main = defineCommand({
|
|
|
2811
2993
|
meta: {
|
|
2812
2994
|
name: "code-review",
|
|
2813
2995
|
version: packageVersion,
|
|
2814
|
-
description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, check-cost, validate, stop-gate, budget-hook, print-settings, and deadline"
|
|
2996
|
+
description: "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, validate-patches, cost, check-cost, validate, seed-draft, stop-gate, budget-hook, print-settings, and deadline"
|
|
2815
2997
|
},
|
|
2816
2998
|
subCommands: {
|
|
2817
2999
|
gather: gatherCmd,
|
|
@@ -2821,6 +3003,7 @@ var main = defineCommand({
|
|
|
2821
3003
|
cost: costCmd,
|
|
2822
3004
|
"check-cost": checkCostCmd,
|
|
2823
3005
|
validate: validateCmd,
|
|
3006
|
+
"seed-draft": seedDraftCmd,
|
|
2824
3007
|
adapt: adaptCmd,
|
|
2825
3008
|
extract: extractCmd,
|
|
2826
3009
|
"validate-patches": validatePatchesCmd,
|