@jphutchins/code-review 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +263 -82
- 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 "";
|
|
@@ -751,10 +762,14 @@ var money = (n) => `$${n.toFixed(2)}`;
|
|
|
751
762
|
var mins = (ms) => `${(ms / 6e4).toFixed(1)}m`;
|
|
752
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)}`;
|
|
753
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`;
|
|
754
|
-
var directive = (phase, draftPath) =>
|
|
755
|
-
|
|
765
|
+
var directive = (phase, draftPath, isSubagent) => {
|
|
766
|
+
if (isSubagent)
|
|
767
|
+
return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now and report the findings you have back to the main agent in your reply. Do not write ${draftPath} yourself; only the main agent writes it.` : `Wind down investigation and report the findings you have back to the main agent in your reply \u2014 do not write ${draftPath} yourself; the main agent writes it, and you may run out of budget otherwise.`;
|
|
768
|
+
return 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.`;
|
|
769
|
+
};
|
|
770
|
+
var budgetMessage = (i, phase, draftPath, isSubagent) => {
|
|
756
771
|
const status = [spendClause(i), timeClause(i)].filter((c) => c !== null).join(" \xB7 ");
|
|
757
|
-
return `Budget check \u2014 ${status}. ${directive(phase, draftPath)}`;
|
|
772
|
+
return `Budget check \u2014 ${status}. ${directive(phase, draftPath, isSubagent)}`;
|
|
758
773
|
};
|
|
759
774
|
var invokesCodeReviewValidate = (toolInput) => {
|
|
760
775
|
const cmd = asRecord(toolInput)?.["command"];
|
|
@@ -767,6 +782,35 @@ var blockedDuringConvergence = (toolName, toolInput) => {
|
|
|
767
782
|
if (toolName === "Bash") return !invokesCodeReviewValidate(toolInput);
|
|
768
783
|
return false;
|
|
769
784
|
};
|
|
785
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
786
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
787
|
+
var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
788
|
+
const rec = asRecord(toolInput);
|
|
789
|
+
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
790
|
+
if (WRITE_TOOLS.has(toolName)) {
|
|
791
|
+
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
792
|
+
if (typeof fp === "string" && targets.some((t4) => fp === t4 || basename(fp) === basename(t4)))
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
if (toolName === "Bash") {
|
|
796
|
+
const cmd = rec?.["command"];
|
|
797
|
+
if (typeof cmd !== "string") return false;
|
|
798
|
+
const alt = targets.map(escapeRegExp).join("|");
|
|
799
|
+
const end = "(?=$|[\\s|&;)])";
|
|
800
|
+
const redirect = new RegExp(`>>?\\|?\\s*(['"]?)(?:${alt})\\1${end}`);
|
|
801
|
+
const teeArg = new RegExp(`\\btee\\b(?:\\s+-{1,2}\\S+)*\\s+(['"]?)(?:${alt})\\1${end}`);
|
|
802
|
+
return redirect.test(cmd) || teeArg.test(cmd);
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
};
|
|
806
|
+
var singleWriterMessage = (draftPath) => `Only the main agent may write ${draftPath}. When a subagent writes it too, the concurrent writers clobber each other and the review comes out empty. Do NOT write, edit, or redirect into ${draftPath} \u2014 instead, return the findings you discovered in your reply (the field names are in the schema); the main agent collects every subagent's reported findings and writes the draft itself.`;
|
|
807
|
+
var denyPreTool = (reason) => ({
|
|
808
|
+
hookSpecificOutput: {
|
|
809
|
+
hookEventName: "PreToolUse",
|
|
810
|
+
permissionDecision: "deny",
|
|
811
|
+
permissionDecisionReason: reason
|
|
812
|
+
}
|
|
813
|
+
});
|
|
770
814
|
var evaluateBudgetHook = (input, params) => {
|
|
771
815
|
const rec = asRecord(input);
|
|
772
816
|
const inputs = {
|
|
@@ -777,25 +821,23 @@ var evaluateBudgetHook = (input, params) => {
|
|
|
777
821
|
reserve: params.reserve
|
|
778
822
|
};
|
|
779
823
|
const phase = decideBudget(inputs);
|
|
824
|
+
const agentId = rec?.["agent_id"];
|
|
825
|
+
const isSubagent = typeof agentId === "string" && agentId.length > 0;
|
|
780
826
|
switch (rec?.["hook_event_name"]) {
|
|
781
827
|
case "PostToolBatch":
|
|
782
828
|
return phase.kind === "ok" ? {} : {
|
|
783
829
|
hookSpecificOutput: {
|
|
784
830
|
hookEventName: "PostToolBatch",
|
|
785
|
-
additionalContext: budgetMessage(inputs, phase, params.draftPath)
|
|
831
|
+
additionalContext: budgetMessage(inputs, phase, params.draftPath, isSubagent)
|
|
786
832
|
}
|
|
787
833
|
};
|
|
788
834
|
case "PreToolUse": {
|
|
789
|
-
if (phase.kind !== "hard") return {};
|
|
790
835
|
const toolName = rec["tool_name"];
|
|
836
|
+
if (isSubagent && typeof toolName === "string" && writesToDraft(toolName, rec["tool_input"], params.draftPath))
|
|
837
|
+
return denyPreTool(singleWriterMessage(params.draftPath));
|
|
838
|
+
if (phase.kind !== "hard") return {};
|
|
791
839
|
if (typeof toolName === "string" && blockedDuringConvergence(toolName, rec["tool_input"]))
|
|
792
|
-
return
|
|
793
|
-
hookSpecificOutput: {
|
|
794
|
-
hookEventName: "PreToolUse",
|
|
795
|
-
permissionDecision: "deny",
|
|
796
|
-
permissionDecisionReason: budgetMessage(inputs, phase, params.draftPath)
|
|
797
|
-
}
|
|
798
|
-
};
|
|
840
|
+
return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
|
|
799
841
|
return {};
|
|
800
842
|
}
|
|
801
843
|
default:
|
|
@@ -1089,25 +1131,50 @@ var parseHtmlUrl = (raw) => {
|
|
|
1089
1131
|
return void 0;
|
|
1090
1132
|
}
|
|
1091
1133
|
};
|
|
1092
|
-
var
|
|
1134
|
+
var commentPayload = (c) => ({
|
|
1135
|
+
path: c.path,
|
|
1136
|
+
line: c.line,
|
|
1137
|
+
side: c.side,
|
|
1138
|
+
...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
|
|
1139
|
+
body: formatMarkdown(c.body)
|
|
1140
|
+
});
|
|
1141
|
+
var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, marker, ghApi) => {
|
|
1093
1142
|
const pointer = reviewBodyPointer(headSha, stickyUrl, marker);
|
|
1094
|
-
const
|
|
1143
|
+
const reviewBody = (withComments) => JSON.stringify({
|
|
1095
1144
|
body: pointer,
|
|
1096
1145
|
commit_id: headSha,
|
|
1097
1146
|
event: "COMMENT",
|
|
1098
|
-
comments: comments.map(
|
|
1099
|
-
path: c.path,
|
|
1100
|
-
line: c.line,
|
|
1101
|
-
side: c.side,
|
|
1102
|
-
...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
|
|
1103
|
-
body: formatMarkdown(c.body)
|
|
1104
|
-
}))
|
|
1147
|
+
comments: withComments ? comments.map(commentPayload) : []
|
|
1105
1148
|
});
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1149
|
+
const reviewsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/reviews`, "--input", "-"];
|
|
1150
|
+
try {
|
|
1151
|
+
const stdout = await ghApi(reviewsEndpoint, reviewBody(true));
|
|
1152
|
+
return { url: parseHtmlUrl(stdout), inlinePosted: comments.length, unposted: [] };
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
if (comments.length === 0) throw err;
|
|
1155
|
+
process.stderr.write(
|
|
1156
|
+
`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)
|
|
1157
|
+
`
|
|
1158
|
+
);
|
|
1159
|
+
const url = parseHtmlUrl(await ghApi(reviewsEndpoint, reviewBody(false)));
|
|
1160
|
+
const commentsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/comments`, "--input", "-"];
|
|
1161
|
+
const unposted = [];
|
|
1162
|
+
let inlinePosted = 0;
|
|
1163
|
+
for (const [i, c] of comments.entries()) {
|
|
1164
|
+
try {
|
|
1165
|
+
await ghApi(commentsEndpoint, JSON.stringify({ commit_id: headSha, ...commentPayload(c) }));
|
|
1166
|
+
inlinePosted += 1;
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
const finding = inDiff[i];
|
|
1169
|
+
if (finding) unposted.push(finding);
|
|
1170
|
+
process.stderr.write(
|
|
1171
|
+
`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)
|
|
1172
|
+
`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return { url, inlinePosted, unposted };
|
|
1177
|
+
}
|
|
1111
1178
|
};
|
|
1112
1179
|
var findBotComment = async (repo, prNumber, botLogin, marker, ghApi) => {
|
|
1113
1180
|
const stdout = await ghApi(
|
|
@@ -1174,10 +1241,7 @@ var fetchBotReviews = async (repo, prNumber, botLogin, ghApi) => {
|
|
|
1174
1241
|
return [];
|
|
1175
1242
|
}
|
|
1176
1243
|
if (!Array.isArray(reviews)) return [];
|
|
1177
|
-
return reviews.filter(isBotReview).filter((r) => r.user.login === botLogin && r.state !== "DISMISSED").map((r) => ({
|
|
1178
|
-
id: r.id,
|
|
1179
|
-
commitId: typeof r.commit_id === "string" ? r.commit_id : ""
|
|
1180
|
-
}));
|
|
1244
|
+
return reviews.filter(isBotReview).filter((r) => r.user.login === botLogin && r.state !== "DISMISSED").map((r) => ({ id: r.id }));
|
|
1181
1245
|
};
|
|
1182
1246
|
var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
1183
1247
|
for (const id of ids) {
|
|
@@ -1200,16 +1264,15 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
|
1200
1264
|
}
|
|
1201
1265
|
}
|
|
1202
1266
|
};
|
|
1203
|
-
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}
|
|
1267
|
+
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}}}}}}}}";
|
|
1204
1268
|
var MINIMIZE_COMMENT_MUTATION = "mutation($id:ID!){minimizeComment(input:{subjectId:$id,classifier:OUTDATED}){minimizedComment{isMinimized}}}";
|
|
1205
|
-
var
|
|
1269
|
+
var priorBotCommentId = (c, logins) => {
|
|
1206
1270
|
if (typeof c !== "object" || c === null) return null;
|
|
1207
1271
|
const o = c;
|
|
1208
1272
|
const login = o.author?.login;
|
|
1209
|
-
|
|
1210
|
-
return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) && typeof oid === "string" && oid !== headSha ? o.id : null;
|
|
1273
|
+
return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) ? o.id : null;
|
|
1211
1274
|
};
|
|
1212
|
-
var
|
|
1275
|
+
var priorBotCommentIds = (raw, botLogin) => {
|
|
1213
1276
|
let parsed;
|
|
1214
1277
|
try {
|
|
1215
1278
|
parsed = JSON.parse(raw);
|
|
@@ -1223,13 +1286,13 @@ var supersededBotCommentIds = (raw, headSha, botLogin) => {
|
|
|
1223
1286
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
1224
1287
|
const ids = nodes.flatMap((t4) => {
|
|
1225
1288
|
const cnodes = t4.comments?.nodes;
|
|
1226
|
-
return Array.isArray(cnodes) ? cnodes.map((c) =>
|
|
1289
|
+
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
1227
1290
|
});
|
|
1228
1291
|
return { ids, truncated };
|
|
1229
1292
|
};
|
|
1230
|
-
var
|
|
1293
|
+
var listPriorBotCommentIds = async (repo, prNumber, botLogin, ghApi) => {
|
|
1231
1294
|
const slash = repo.indexOf("/");
|
|
1232
|
-
if (slash <= 0) return;
|
|
1295
|
+
if (slash <= 0) return [];
|
|
1233
1296
|
const owner = repo.slice(0, slash);
|
|
1234
1297
|
const name = repo.slice(slash + 1);
|
|
1235
1298
|
let raw;
|
|
@@ -1250,15 +1313,18 @@ var minimizeSupersededComments = async (repo, prNumber, headSha, botLogin, ghApi
|
|
|
1250
1313
|
`Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
|
|
1251
1314
|
`
|
|
1252
1315
|
);
|
|
1253
|
-
return;
|
|
1316
|
+
return [];
|
|
1254
1317
|
}
|
|
1255
|
-
const { ids, truncated } =
|
|
1318
|
+
const { ids, truncated } = priorBotCommentIds(raw, botLogin);
|
|
1256
1319
|
if (truncated) {
|
|
1257
1320
|
process.stderr.write(
|
|
1258
1321
|
`Note: PR #${String(prNumber)} has more than 100 review threads \u2014 only the first 100 were scanned for stale bot comments
|
|
1259
1322
|
`
|
|
1260
1323
|
);
|
|
1261
1324
|
}
|
|
1325
|
+
return ids;
|
|
1326
|
+
};
|
|
1327
|
+
var minimizeComments = async (prNumber, ids, ghApi) => {
|
|
1262
1328
|
let minimized = 0;
|
|
1263
1329
|
for (const id of ids) {
|
|
1264
1330
|
try {
|
|
@@ -1373,7 +1439,11 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1373
1439
|
process.exit(0);
|
|
1374
1440
|
}
|
|
1375
1441
|
const findingsMarker = findingsPointer(findings, input.jsonUrl);
|
|
1376
|
-
const {
|
|
1442
|
+
const {
|
|
1443
|
+
comments: rawComments,
|
|
1444
|
+
strays,
|
|
1445
|
+
inDiff
|
|
1446
|
+
} = buildInlineComments(findings.findings, diff, {
|
|
1377
1447
|
inlineTemplate,
|
|
1378
1448
|
models: envelope.models.map((m) => m.model),
|
|
1379
1449
|
findings,
|
|
@@ -1387,8 +1457,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1387
1457
|
);
|
|
1388
1458
|
}
|
|
1389
1459
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
1390
|
-
const
|
|
1391
|
-
const initialDisposition = comments.length > 0 ? alreadyReviewedThisSha ? { kind: "suppressed-existing-review", sha: input.headSha } : void 0 : strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1460
|
+
const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1392
1461
|
const commonRenderInput = {
|
|
1393
1462
|
findings,
|
|
1394
1463
|
envelope,
|
|
@@ -1412,7 +1481,15 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1412
1481
|
|
|
1413
1482
|
> **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.
|
|
1414
1483
|
` : "";
|
|
1415
|
-
const renderBody = (inlineDisposition, reviewUrl2
|
|
1484
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2) => formatMarkdown(
|
|
1485
|
+
render({
|
|
1486
|
+
...commonRenderInput,
|
|
1487
|
+
...straysOverride ? { strays: straysOverride } : {},
|
|
1488
|
+
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
1489
|
+
inlineDisposition,
|
|
1490
|
+
reviewUrl: reviewUrl2
|
|
1491
|
+
}) + longFilesNote
|
|
1492
|
+
);
|
|
1416
1493
|
const stickyRef = await upsertSticky(
|
|
1417
1494
|
input.repo,
|
|
1418
1495
|
prNumber,
|
|
@@ -1420,49 +1497,53 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1420
1497
|
renderBody(initialDisposition),
|
|
1421
1498
|
ghApi
|
|
1422
1499
|
);
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
}
|
|
1434
|
-
const reviewUrl = await postInlineReview(
|
|
1500
|
+
const priorInlineComments = await listPriorBotCommentIds(
|
|
1501
|
+
input.repo,
|
|
1502
|
+
prNumber,
|
|
1503
|
+
input.botLogin,
|
|
1504
|
+
ghApi
|
|
1505
|
+
);
|
|
1506
|
+
const {
|
|
1507
|
+
url: reviewUrl,
|
|
1508
|
+
inlinePosted,
|
|
1509
|
+
unposted
|
|
1510
|
+
} = await postInlineReview(
|
|
1435
1511
|
input.repo,
|
|
1436
1512
|
prNumber,
|
|
1437
1513
|
input.headSha,
|
|
1438
1514
|
comments,
|
|
1515
|
+
inDiff,
|
|
1439
1516
|
stickyRef?.url,
|
|
1440
1517
|
findingsMarker,
|
|
1441
1518
|
ghApi
|
|
1442
1519
|
);
|
|
1443
1520
|
process.stderr.write(
|
|
1444
|
-
`Posted a review with ${String(
|
|
1521
|
+
`Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
|
|
1445
1522
|
`
|
|
1446
1523
|
);
|
|
1447
|
-
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1524
|
+
const priorReviewIds = botReviews.map((r) => r.id);
|
|
1525
|
+
if (priorReviewIds.length > 0) {
|
|
1526
|
+
await dismissReviews(input.repo, prNumber, priorReviewIds, ghApi);
|
|
1527
|
+
}
|
|
1528
|
+
await minimizeComments(prNumber, priorInlineComments, ghApi);
|
|
1529
|
+
const unanchoredCount = unposted.length;
|
|
1530
|
+
const finalStrays = unanchoredCount > 0 ? [...unposted, ...strays] : strays;
|
|
1531
|
+
if (stickyRef !== null && (inlinePosted > 0 || unanchoredCount > 0)) {
|
|
1532
|
+
const finalDisposition = inlinePosted > 0 ? { kind: "posted", count: inlinePosted, sha: input.headSha } : { kind: "inline-unavailable" };
|
|
1454
1533
|
try {
|
|
1455
1534
|
await patchComment(
|
|
1456
1535
|
input.repo,
|
|
1457
1536
|
stickyRef.id,
|
|
1458
|
-
renderBody(
|
|
1537
|
+
renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount),
|
|
1459
1538
|
ghApi
|
|
1460
1539
|
);
|
|
1461
|
-
process.stderr.write(
|
|
1462
|
-
`)
|
|
1540
|
+
process.stderr.write(
|
|
1541
|
+
`Updated sticky comment #${String(stickyRef.id)} to reflect the review
|
|
1542
|
+
`
|
|
1543
|
+
);
|
|
1463
1544
|
} catch (err) {
|
|
1464
1545
|
process.stderr.write(
|
|
1465
|
-
`Warning: failed to
|
|
1546
|
+
`Warning: failed to update the sticky summary after the review: ${err instanceof Error ? err.message : String(err)}
|
|
1466
1547
|
`
|
|
1467
1548
|
);
|
|
1468
1549
|
}
|
|
@@ -1800,18 +1881,18 @@ var withMeta = (base, meta) => ({
|
|
|
1800
1881
|
...meta.effort ? { effort: meta.effort } : {}
|
|
1801
1882
|
});
|
|
1802
1883
|
var resolveTelemetry = (native, meta) => {
|
|
1803
|
-
const fb =
|
|
1804
|
-
|
|
1884
|
+
const fb = (() => {
|
|
1885
|
+
try {
|
|
1886
|
+
return meta.transcriptFallback?.();
|
|
1887
|
+
} catch {
|
|
1888
|
+
return void 0;
|
|
1889
|
+
}
|
|
1890
|
+
})();
|
|
1891
|
+
const wallTurns = fb !== void 0 && fb.durationMs > 0 ? { turns: fb.turns, duration_ms: fb.durationMs } : { turns: native.turns, duration_ms: native.durationMs };
|
|
1805
1892
|
return withMeta(
|
|
1806
|
-
|
|
1807
|
-
models: [...fb.models],
|
|
1808
|
-
|
|
1809
|
-
duration_ms: fb.durationMs,
|
|
1810
|
-
vendor_cost_usd: native.vendorCostUsd
|
|
1811
|
-
} : {
|
|
1812
|
-
models: native.models,
|
|
1813
|
-
turns: native.turns,
|
|
1814
|
-
duration_ms: native.durationMs,
|
|
1893
|
+
{
|
|
1894
|
+
models: native.models.length > 0 ? native.models : fb ? [...fb.models] : native.models,
|
|
1895
|
+
...wallTurns,
|
|
1815
1896
|
vendor_cost_usd: native.vendorCostUsd
|
|
1816
1897
|
},
|
|
1817
1898
|
meta
|
|
@@ -2400,6 +2481,105 @@ ${printableSchema(schemaPath)}
|
|
|
2400
2481
|
}
|
|
2401
2482
|
}
|
|
2402
2483
|
});
|
|
2484
|
+
var seedDraftCmd = defineCommand({
|
|
2485
|
+
meta: {
|
|
2486
|
+
name: "seed-draft",
|
|
2487
|
+
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"
|
|
2488
|
+
},
|
|
2489
|
+
args: {
|
|
2490
|
+
prior: {
|
|
2491
|
+
type: "string",
|
|
2492
|
+
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"
|
|
2493
|
+
},
|
|
2494
|
+
out: {
|
|
2495
|
+
type: "string",
|
|
2496
|
+
description: "Path to write the seed $DRAFT to (an absolute path outside the worktree)",
|
|
2497
|
+
required: true
|
|
2498
|
+
},
|
|
2499
|
+
kind: {
|
|
2500
|
+
type: "string",
|
|
2501
|
+
description: "Schema kind to validate the prior findings against (default: findings)"
|
|
2502
|
+
},
|
|
2503
|
+
schema: {
|
|
2504
|
+
type: "string",
|
|
2505
|
+
description: "Path to a schema file (wins over --kind/--schema-version)"
|
|
2506
|
+
},
|
|
2507
|
+
"schema-version": {
|
|
2508
|
+
type: "string",
|
|
2509
|
+
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)"
|
|
2510
|
+
}
|
|
2511
|
+
},
|
|
2512
|
+
run: async ({ args }) => {
|
|
2513
|
+
const outPath = resolve$1(args.out);
|
|
2514
|
+
const kindArg = args.kind || "findings";
|
|
2515
|
+
const kind = isSchemaKind(kindArg) ? kindArg : "findings";
|
|
2516
|
+
if (kind !== kindArg) {
|
|
2517
|
+
process.stderr.write(
|
|
2518
|
+
`Warning: unknown --kind "${kindArg}" \u2014 validating against "findings"
|
|
2519
|
+
`
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
const writeEmptyScaffold = () => {
|
|
2523
|
+
try {
|
|
2524
|
+
writeFileSync(outPath, `${JSON.stringify(noticeFindings(""), null, 2)}
|
|
2525
|
+
`);
|
|
2526
|
+
process.stderr.write(
|
|
2527
|
+
`Seeded ${outPath} with an empty valid scaffold \u2014 no decodable prior findings to build on
|
|
2528
|
+
`
|
|
2529
|
+
);
|
|
2530
|
+
process.stdout.write("empty\n");
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
process.stderr.write(
|
|
2533
|
+
`Warning: could not write the seed scaffold to ${outPath} (${err instanceof Error ? err.message : String(err)}) \u2014 the agent will create $DRAFT itself
|
|
2534
|
+
`
|
|
2535
|
+
);
|
|
2536
|
+
process.stdout.write("none\n");
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2539
|
+
const priorFindings = (() => {
|
|
2540
|
+
if (!args.prior) return null;
|
|
2541
|
+
const raw = (() => {
|
|
2542
|
+
try {
|
|
2543
|
+
return JSON.parse(readFileSync(resolve$1(args.prior), "utf-8"));
|
|
2544
|
+
} catch {
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
})();
|
|
2548
|
+
const body = typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
2549
|
+
return body === null ? null : parseFindingsMarker(body);
|
|
2550
|
+
})();
|
|
2551
|
+
if (priorFindings === null) {
|
|
2552
|
+
writeEmptyScaffold();
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
const seededFromPrior = (() => {
|
|
2556
|
+
try {
|
|
2557
|
+
const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
2558
|
+
if (!validateAgainstSchema(priorFindings, schemaPath).valid) return false;
|
|
2559
|
+
writeFileSync(outPath, `${JSON.stringify(priorFindings, null, 2)}
|
|
2560
|
+
`);
|
|
2561
|
+
return true;
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
process.stderr.write(
|
|
2564
|
+
`Warning: could not seed from the prior review (${err instanceof Error ? err.message : String(err)}) \u2014 falling back to the empty scaffold
|
|
2565
|
+
`
|
|
2566
|
+
);
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
})();
|
|
2570
|
+
if (seededFromPrior) {
|
|
2571
|
+
const priorList = priorFindings.findings;
|
|
2572
|
+
const count = Array.isArray(priorList) ? priorList.length : 0;
|
|
2573
|
+
process.stderr.write(
|
|
2574
|
+
`Seeded ${outPath} from the prior review (${String(count)} finding(s)) \u2014 verify each still holds against the current diff and refine in place
|
|
2575
|
+
`
|
|
2576
|
+
);
|
|
2577
|
+
process.stdout.write("prior\n");
|
|
2578
|
+
} else {
|
|
2579
|
+
writeEmptyScaffold();
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
2403
2583
|
var adaptCmd = defineCommand({
|
|
2404
2584
|
meta: {
|
|
2405
2585
|
name: "adapt",
|
|
@@ -2430,7 +2610,7 @@ var adaptCmd = defineCommand({
|
|
|
2430
2610
|
},
|
|
2431
2611
|
transcript: {
|
|
2432
2612
|
type: "string",
|
|
2433
|
-
description: "Path to the session transcript (the main .jsonl)
|
|
2613
|
+
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)"
|
|
2434
2614
|
}
|
|
2435
2615
|
},
|
|
2436
2616
|
run: async ({ args }) => {
|
|
@@ -2844,7 +3024,7 @@ var main = defineCommand({
|
|
|
2844
3024
|
meta: {
|
|
2845
3025
|
name: "code-review",
|
|
2846
3026
|
version: packageVersion,
|
|
2847
|
-
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"
|
|
3027
|
+
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"
|
|
2848
3028
|
},
|
|
2849
3029
|
subCommands: {
|
|
2850
3030
|
gather: gatherCmd,
|
|
@@ -2854,6 +3034,7 @@ var main = defineCommand({
|
|
|
2854
3034
|
cost: costCmd,
|
|
2855
3035
|
"check-cost": checkCostCmd,
|
|
2856
3036
|
validate: validateCmd,
|
|
3037
|
+
"seed-draft": seedDraftCmd,
|
|
2857
3038
|
adapt: adaptCmd,
|
|
2858
3039
|
extract: extractCmd,
|
|
2859
3040
|
"validate-patches": validatePatchesCmd,
|