@pepps233/mendr 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-753PEKBT.js → chunk-GKT5VC55.js} +246 -41
- package/dist/cli.js +1 -1
- package/dist/daemon.js +214 -1
- package/package.json +2 -2
|
@@ -145,6 +145,15 @@ function parseFixIssueResultArrayFromText(text) {
|
|
|
145
145
|
}
|
|
146
146
|
return value.map(parseFixIssueResult);
|
|
147
147
|
}
|
|
148
|
+
function parseExistingCommentReviewResultArrayFromText(text) {
|
|
149
|
+
const value = extractJsonValue(text);
|
|
150
|
+
if (!Array.isArray(value)) {
|
|
151
|
+
throw new AgentParseError(
|
|
152
|
+
"Agent JSON payload must be an existing-comment review result array."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return value.map(parseExistingCommentReviewResult);
|
|
156
|
+
}
|
|
148
157
|
function issueFingerprint(issue) {
|
|
149
158
|
return [
|
|
150
159
|
normalizeFingerprintPart(issue.title),
|
|
@@ -188,6 +197,29 @@ function parseFixIssueResult(value) {
|
|
|
188
197
|
summary
|
|
189
198
|
};
|
|
190
199
|
}
|
|
200
|
+
function parseExistingCommentReviewResult(value) {
|
|
201
|
+
if (!isRecord(value)) {
|
|
202
|
+
throw new AgentParseError("Agent existing-comment review result must be an object.");
|
|
203
|
+
}
|
|
204
|
+
const { status, summary } = value;
|
|
205
|
+
if (status !== "needs_fix" && status !== "already_addressed" && status !== "does_not_exist") {
|
|
206
|
+
throw new AgentParseError("Agent existing-comment review result has an invalid status.");
|
|
207
|
+
}
|
|
208
|
+
if (typeof summary !== "string") {
|
|
209
|
+
throw new AgentParseError("Agent existing-comment review result has an invalid summary.");
|
|
210
|
+
}
|
|
211
|
+
if (status !== "needs_fix") {
|
|
212
|
+
return {
|
|
213
|
+
status,
|
|
214
|
+
summary
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
summary,
|
|
220
|
+
issue: parseIssue(value)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
191
223
|
function readOptionalString(value, key) {
|
|
192
224
|
const raw = value[key];
|
|
193
225
|
return typeof raw === "string" && raw.trim().length > 0 ? raw : void 0;
|
|
@@ -255,6 +287,15 @@ function isRecord(value) {
|
|
|
255
287
|
// src/agents/prompts.ts
|
|
256
288
|
var issueSchema = '[{"title":"specific standalone title","file":"path","line":1,"severity":"low|medium|high|critical","description":"two concise sentences describing the finding"}]';
|
|
257
289
|
var fixSchema = '[{"title":"issue title","fingerprint":"issue fingerprint","status":"fixed","commitMessage":"<type>(<scope>): <short imperative summary>\\n\\n- <why this change was needed>\\n- <why this approach or impact matters>","summary":"exactly two sentences"},{"title":"issue title","fingerprint":"issue fingerprint","status":"failed","summary":"exactly two sentences explaining the failure"}]';
|
|
290
|
+
var existingCommentReviewSchema = '[{"status":"needs_fix","title":"specific standalone title","file":"path","line":1,"severity":"low|medium|high|critical","description":"two concise sentences describing the finding","summary":"two concise sentences for final reporting"},{"status":"already_addressed","summary":"two concise sentences for final reporting"},{"status":"does_not_exist","summary":"two concise sentences for final reporting"}]';
|
|
291
|
+
function buildExistingCommentReviewSystemPrompt() {
|
|
292
|
+
return [
|
|
293
|
+
"You are the EXISTING COMMENT REVIEW agent in the pull request review loop.",
|
|
294
|
+
"Read existing PR comments only from the provided review.md.",
|
|
295
|
+
"Classify only concrete unresolved code correction requests.",
|
|
296
|
+
"Respond only with JSON matching the requested existing-comment review schema."
|
|
297
|
+
].join("\n");
|
|
298
|
+
}
|
|
258
299
|
function buildReviewSystemPrompt() {
|
|
259
300
|
return [
|
|
260
301
|
"You are the REVIEW agent in the pull request review loop.",
|
|
@@ -270,6 +311,33 @@ function buildFixSystemPrompt() {
|
|
|
270
311
|
"Respond only with JSON matching the requested fix-result schema."
|
|
271
312
|
].join("\n");
|
|
272
313
|
}
|
|
314
|
+
function buildExistingCommentReviewPrompt(ctx) {
|
|
315
|
+
return [
|
|
316
|
+
"You are reviewing existing GitHub pull request comments before normal review rounds begin.",
|
|
317
|
+
"Read only the provided PR review.md as the source for existing comments.",
|
|
318
|
+
"Ignore comments that do not describe a concrete bug, issue, or requested code correction.",
|
|
319
|
+
"If a newer comment clearly says an earlier issue was handled, return already_addressed.",
|
|
320
|
+
"If no newer resolution comment exists, inspect the current code in the repository and the PR diff.",
|
|
321
|
+
"If the code already handles the issue, return already_addressed.",
|
|
322
|
+
"If the alleged issue is not reproducible or not true in the codebase, return does_not_exist.",
|
|
323
|
+
"Otherwise return needs_fix with issue fields matching the standard issue schema.",
|
|
324
|
+
"Use exactly two concise sentences for every summary and needs_fix description.",
|
|
325
|
+
"Return an empty JSON array when no existing comments need follow-up.",
|
|
326
|
+
"respond ONLY with JSON matching this schema:",
|
|
327
|
+
existingCommentReviewSchema,
|
|
328
|
+
"",
|
|
329
|
+
`Review existing comments for PR ${ctx.pr}.`,
|
|
330
|
+
"",
|
|
331
|
+
"PR review.md:",
|
|
332
|
+
ctx.reviewMarkdown,
|
|
333
|
+
"",
|
|
334
|
+
"Current report.md:",
|
|
335
|
+
ctx.reportMarkdown,
|
|
336
|
+
"",
|
|
337
|
+
"PR diff:",
|
|
338
|
+
ctx.diff
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
273
341
|
function buildReviewPrompt(ctx) {
|
|
274
342
|
return [
|
|
275
343
|
"You are a code review agent for a GitHub pull request.",
|
|
@@ -349,6 +417,16 @@ function parseClaudeIssues(output) {
|
|
|
349
417
|
}
|
|
350
418
|
throw new AgentParseError("Claude output did not include a result payload.");
|
|
351
419
|
}
|
|
420
|
+
function parseClaudeExistingCommentReviewResults(output) {
|
|
421
|
+
const envelope = extractJsonValue(output);
|
|
422
|
+
if (isClaudeResultEnvelope(envelope)) {
|
|
423
|
+
return parseExistingCommentReviewResultArrayFromText(envelope.result);
|
|
424
|
+
}
|
|
425
|
+
if (Array.isArray(envelope)) {
|
|
426
|
+
return parseExistingCommentReviewResultArrayFromText(JSON.stringify(envelope));
|
|
427
|
+
}
|
|
428
|
+
throw new AgentParseError("Claude output did not include a result payload.");
|
|
429
|
+
}
|
|
352
430
|
function parseClaudeFixResults(output) {
|
|
353
431
|
const envelope = extractJsonValue(output);
|
|
354
432
|
if (isClaudeResultEnvelope(envelope)) {
|
|
@@ -359,30 +437,19 @@ function parseClaudeFixResults(output) {
|
|
|
359
437
|
}
|
|
360
438
|
throw new AgentParseError("Claude output did not include a result payload.");
|
|
361
439
|
}
|
|
440
|
+
function buildClaudeExistingCommentReviewInvocation(ctx) {
|
|
441
|
+
const prompt = buildExistingCommentReviewPrompt(ctx);
|
|
442
|
+
return buildClaudeInvocation(ctx, prompt, buildExistingCommentReviewSystemPrompt());
|
|
443
|
+
}
|
|
362
444
|
function buildClaudeReviewInvocation(ctx) {
|
|
363
445
|
const prompt = buildReviewPrompt(ctx);
|
|
364
|
-
return
|
|
365
|
-
command: "claude",
|
|
366
|
-
args: [
|
|
367
|
-
"-p",
|
|
368
|
-
prompt,
|
|
369
|
-
"--output-format",
|
|
370
|
-
"json",
|
|
371
|
-
"--model",
|
|
372
|
-
ctx.model,
|
|
373
|
-
"--effort",
|
|
374
|
-
ctx.effort,
|
|
375
|
-
"--permission-mode",
|
|
376
|
-
"acceptEdits",
|
|
377
|
-
"--add-dir",
|
|
378
|
-
ctx.repo,
|
|
379
|
-
"--append-system-prompt",
|
|
380
|
-
buildReviewSystemPrompt()
|
|
381
|
-
]
|
|
382
|
-
};
|
|
446
|
+
return buildClaudeInvocation(ctx, prompt, buildReviewSystemPrompt());
|
|
383
447
|
}
|
|
384
448
|
function buildClaudeFixInvocation(issues, ctx) {
|
|
385
449
|
const prompt = buildFixPrompt(issues, ctx);
|
|
450
|
+
return buildClaudeInvocation(ctx, prompt, buildFixSystemPrompt());
|
|
451
|
+
}
|
|
452
|
+
function buildClaudeInvocation(ctx, prompt, systemPrompt) {
|
|
386
453
|
return {
|
|
387
454
|
command: "claude",
|
|
388
455
|
args: [
|
|
@@ -399,7 +466,7 @@ function buildClaudeFixInvocation(issues, ctx) {
|
|
|
399
466
|
"--add-dir",
|
|
400
467
|
ctx.repo,
|
|
401
468
|
"--append-system-prompt",
|
|
402
|
-
|
|
469
|
+
systemPrompt
|
|
403
470
|
]
|
|
404
471
|
};
|
|
405
472
|
}
|
|
@@ -411,33 +478,25 @@ function isClaudeResultEnvelope(value) {
|
|
|
411
478
|
function parseCodexIssues(output) {
|
|
412
479
|
return parseIssueArrayFromText(output);
|
|
413
480
|
}
|
|
481
|
+
function parseCodexExistingCommentReviewResults(output) {
|
|
482
|
+
return parseExistingCommentReviewResultArrayFromText(output);
|
|
483
|
+
}
|
|
414
484
|
function parseCodexFixResults(output) {
|
|
415
485
|
return parseFixIssueResultArrayFromText(output);
|
|
416
486
|
}
|
|
487
|
+
function buildCodexExistingCommentReviewInvocation(ctx, options) {
|
|
488
|
+
const prompt = buildExistingCommentReviewPrompt(ctx);
|
|
489
|
+
return buildCodexInvocation(ctx, options, prompt);
|
|
490
|
+
}
|
|
417
491
|
function buildCodexReviewInvocation(ctx, options) {
|
|
418
492
|
const prompt = buildReviewPrompt(ctx);
|
|
419
|
-
return
|
|
420
|
-
command: "codex",
|
|
421
|
-
input: prompt,
|
|
422
|
-
args: [
|
|
423
|
-
"exec",
|
|
424
|
-
"-",
|
|
425
|
-
"-m",
|
|
426
|
-
ctx.model,
|
|
427
|
-
"-c",
|
|
428
|
-
`model_reasoning_effort=${JSON.stringify(ctx.effort)}`,
|
|
429
|
-
"--sandbox",
|
|
430
|
-
"workspace-write",
|
|
431
|
-
"--json",
|
|
432
|
-
"-C",
|
|
433
|
-
ctx.repo,
|
|
434
|
-
"--output-last-message",
|
|
435
|
-
options.outputFile
|
|
436
|
-
]
|
|
437
|
-
};
|
|
493
|
+
return buildCodexInvocation(ctx, options, prompt);
|
|
438
494
|
}
|
|
439
495
|
function buildCodexFixInvocation(issues, ctx, options) {
|
|
440
496
|
const prompt = buildFixPrompt(issues, ctx);
|
|
497
|
+
return buildCodexInvocation(ctx, options, prompt);
|
|
498
|
+
}
|
|
499
|
+
function buildCodexInvocation(ctx, options, prompt) {
|
|
441
500
|
return {
|
|
442
501
|
command: "codex",
|
|
443
502
|
input: prompt,
|
|
@@ -512,6 +571,16 @@ var ClaudeAgentDriver = class {
|
|
|
512
571
|
exec;
|
|
513
572
|
outputDir;
|
|
514
573
|
outputIndex = 0;
|
|
574
|
+
async reviewExistingComments(ctx) {
|
|
575
|
+
const label = this.nextLabel("claude", "comment-review");
|
|
576
|
+
const invocation = buildClaudeExistingCommentReviewInvocation(ctx);
|
|
577
|
+
const result = await runAgentInvocation(this.exec, invocation, {
|
|
578
|
+
cwd: ctx.repo,
|
|
579
|
+
outputDir: this.outputDir,
|
|
580
|
+
label
|
|
581
|
+
});
|
|
582
|
+
return parseClaudeExistingCommentReviewResults(result.stdout);
|
|
583
|
+
}
|
|
515
584
|
async review(ctx) {
|
|
516
585
|
const label = this.nextLabel("claude", "review");
|
|
517
586
|
const invocation = buildClaudeReviewInvocation(ctx);
|
|
@@ -545,6 +614,21 @@ var CodexAgentDriver = class {
|
|
|
545
614
|
exec;
|
|
546
615
|
outputDir;
|
|
547
616
|
outputIndex = 0;
|
|
617
|
+
async reviewExistingComments(ctx) {
|
|
618
|
+
const label = this.nextLabel("codex", "comment-review");
|
|
619
|
+
const outputFile = await this.outputFile(label);
|
|
620
|
+
const invocation = buildCodexExistingCommentReviewInvocation(ctx, { outputFile });
|
|
621
|
+
const result = await runAgentInvocation(this.exec, invocation, {
|
|
622
|
+
cwd: ctx.repo,
|
|
623
|
+
outputDir: this.outputDir,
|
|
624
|
+
label
|
|
625
|
+
});
|
|
626
|
+
const finalMessage = await readFile(outputFile, "utf8");
|
|
627
|
+
await writeAgentIo(this.outputDir, label, result, {
|
|
628
|
+
"final-message.md": finalMessage
|
|
629
|
+
});
|
|
630
|
+
return parseCodexExistingCommentReviewResults(finalMessage);
|
|
631
|
+
}
|
|
548
632
|
async review(ctx) {
|
|
549
633
|
const label = this.nextLabel("codex", "review");
|
|
550
634
|
const outputFile = await this.outputFile(label);
|
|
@@ -750,10 +834,13 @@ async function fetchPullRequestDetails(exec, repo, pr) {
|
|
|
750
834
|
{ cwd: repo }
|
|
751
835
|
);
|
|
752
836
|
const parsed = JSON.parse(result.stdout);
|
|
837
|
+
const comments = normalizeIssueComments(parsed.comments);
|
|
838
|
+
const reviewBodies = await fetchPullRequestReviewBodies(exec, repo, pr);
|
|
839
|
+
const reviewComments = await fetchPullRequestReviewComments(exec, repo, pr);
|
|
753
840
|
return {
|
|
754
841
|
title: typeof parsed.title === "string" ? parsed.title : "",
|
|
755
842
|
body: typeof parsed.body === "string" ? parsed.body : "",
|
|
756
|
-
comments:
|
|
843
|
+
comments: sortCommentsByCreatedAt([...comments, ...reviewBodies, ...reviewComments])
|
|
757
844
|
};
|
|
758
845
|
}
|
|
759
846
|
async function fetchPullRequestDiff(exec, repo, pr) {
|
|
@@ -819,7 +906,114 @@ function renderReviewMarkdown(pr, details) {
|
|
|
819
906
|
function renderComment(comment) {
|
|
820
907
|
const author = comment.author?.login ?? "unknown";
|
|
821
908
|
const body = comment.body?.trim() || "(empty comment)";
|
|
822
|
-
|
|
909
|
+
const context = renderCommentContext(comment);
|
|
910
|
+
return `- @${author}${context}: ${body}`;
|
|
911
|
+
}
|
|
912
|
+
async function fetchPullRequestReviewBodies(exec, repo, pr) {
|
|
913
|
+
const result = await execOk(
|
|
914
|
+
exec,
|
|
915
|
+
"gh",
|
|
916
|
+
["api", "--paginate", "--slurp", `repos/{owner}/{repo}/pulls/${pr}/reviews`],
|
|
917
|
+
{ cwd: repo }
|
|
918
|
+
);
|
|
919
|
+
return readGhApiArray(result.stdout).map(normalizeReviewBody).filter((comment) => comment !== void 0);
|
|
920
|
+
}
|
|
921
|
+
async function fetchPullRequestReviewComments(exec, repo, pr) {
|
|
922
|
+
const result = await execOk(
|
|
923
|
+
exec,
|
|
924
|
+
"gh",
|
|
925
|
+
["api", "--paginate", "--slurp", `repos/{owner}/{repo}/pulls/${pr}/comments`],
|
|
926
|
+
{ cwd: repo }
|
|
927
|
+
);
|
|
928
|
+
return readGhApiArray(result.stdout).filter(isRecord2).map(normalizeReviewComment);
|
|
929
|
+
}
|
|
930
|
+
function normalizeIssueComments(value) {
|
|
931
|
+
if (!Array.isArray(value)) {
|
|
932
|
+
return [];
|
|
933
|
+
}
|
|
934
|
+
return value.filter(isRecord2).map((comment) => ({
|
|
935
|
+
author: readAuthor(comment.author),
|
|
936
|
+
body: readString(comment, "body"),
|
|
937
|
+
createdAt: readString(comment, "createdAt") ?? readString(comment, "created_at"),
|
|
938
|
+
kind: "comment",
|
|
939
|
+
url: readString(comment, "url") ?? readString(comment, "html_url")
|
|
940
|
+
}));
|
|
941
|
+
}
|
|
942
|
+
function normalizeReviewBody(value) {
|
|
943
|
+
if (!isRecord2(value)) {
|
|
944
|
+
return void 0;
|
|
945
|
+
}
|
|
946
|
+
const body = readString(value, "body");
|
|
947
|
+
const url = readString(value, "html_url") ?? readString(value, "url");
|
|
948
|
+
if (!body && !url) {
|
|
949
|
+
return void 0;
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
author: readAuthor(value.user) ?? readAuthor(value.author),
|
|
953
|
+
body,
|
|
954
|
+
createdAt: readString(value, "submitted_at") ?? readString(value, "submittedAt"),
|
|
955
|
+
kind: "review",
|
|
956
|
+
state: readString(value, "state"),
|
|
957
|
+
url
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function normalizeReviewComment(value) {
|
|
961
|
+
return {
|
|
962
|
+
author: readAuthor(value.user) ?? readAuthor(value.author),
|
|
963
|
+
body: readString(value, "body"),
|
|
964
|
+
createdAt: readString(value, "created_at") ?? readString(value, "createdAt"),
|
|
965
|
+
kind: "review_comment",
|
|
966
|
+
line: readNumber(value, "line") ?? readNumber(value, "original_line"),
|
|
967
|
+
path: readString(value, "path"),
|
|
968
|
+
url: readString(value, "html_url") ?? readString(value, "url")
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function readGhApiArray(stdout) {
|
|
972
|
+
const parsed = JSON.parse(stdout);
|
|
973
|
+
if (!Array.isArray(parsed)) {
|
|
974
|
+
return [];
|
|
975
|
+
}
|
|
976
|
+
if (parsed.every(Array.isArray)) {
|
|
977
|
+
return parsed.flat();
|
|
978
|
+
}
|
|
979
|
+
return parsed;
|
|
980
|
+
}
|
|
981
|
+
function sortCommentsByCreatedAt(comments) {
|
|
982
|
+
return comments.map((comment, index) => ({ comment, index })).sort((left, right) => {
|
|
983
|
+
const leftTime = readCommentTime(left.comment);
|
|
984
|
+
const rightTime = readCommentTime(right.comment);
|
|
985
|
+
if (leftTime === rightTime) {
|
|
986
|
+
return left.index - right.index;
|
|
987
|
+
}
|
|
988
|
+
return leftTime - rightTime;
|
|
989
|
+
}).map(({ comment }) => comment);
|
|
990
|
+
}
|
|
991
|
+
function readCommentTime(comment) {
|
|
992
|
+
const time = comment.createdAt ? Date.parse(comment.createdAt) : Number.NaN;
|
|
993
|
+
return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
|
|
994
|
+
}
|
|
995
|
+
function renderCommentContext(comment) {
|
|
996
|
+
const parts = [];
|
|
997
|
+
if (comment.kind === "review") {
|
|
998
|
+
parts.push(comment.state ? `review ${comment.state.toLowerCase()}` : "review");
|
|
999
|
+
} else if (comment.kind === "review_comment") {
|
|
1000
|
+
parts.push(renderReviewCommentLocation(comment));
|
|
1001
|
+
} else {
|
|
1002
|
+
parts.push("comment");
|
|
1003
|
+
}
|
|
1004
|
+
if (comment.url) {
|
|
1005
|
+
parts.push(comment.url);
|
|
1006
|
+
}
|
|
1007
|
+
return ` (${parts.join(", ")})`;
|
|
1008
|
+
}
|
|
1009
|
+
function renderReviewCommentLocation(comment) {
|
|
1010
|
+
if (comment.path && comment.line !== void 0) {
|
|
1011
|
+
return `inline comment on ${comment.path}:${comment.line}`;
|
|
1012
|
+
}
|
|
1013
|
+
if (comment.path) {
|
|
1014
|
+
return `inline comment on ${comment.path}`;
|
|
1015
|
+
}
|
|
1016
|
+
return "inline comment";
|
|
823
1017
|
}
|
|
824
1018
|
function resolveBranchPushRemote(input) {
|
|
825
1019
|
const headRepository = readRepositoryInfo(input.headRepository, input.headRepositoryOwner);
|
|
@@ -874,6 +1068,13 @@ function readOwnerLogin(value) {
|
|
|
874
1068
|
}
|
|
875
1069
|
return readString(value, "login");
|
|
876
1070
|
}
|
|
1071
|
+
function readAuthor(value) {
|
|
1072
|
+
if (!isRecord2(value)) {
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
const login = readString(value, "login");
|
|
1076
|
+
return login ? { login } : void 0;
|
|
1077
|
+
}
|
|
877
1078
|
function normalizedGitUrl(url) {
|
|
878
1079
|
if (!url) {
|
|
879
1080
|
return void 0;
|
|
@@ -887,6 +1088,10 @@ function readString(value, key) {
|
|
|
887
1088
|
const raw = value[key];
|
|
888
1089
|
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
|
|
889
1090
|
}
|
|
1091
|
+
function readNumber(value, key) {
|
|
1092
|
+
const raw = value[key];
|
|
1093
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
|
|
1094
|
+
}
|
|
890
1095
|
function isRecord2(value) {
|
|
891
1096
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
892
1097
|
}
|
package/dist/cli.js
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
stageAll,
|
|
29
29
|
waitForPullRequestChecks,
|
|
30
30
|
writeState
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-GKT5VC55.js";
|
|
32
32
|
|
|
33
33
|
// src/orchestrator.ts
|
|
34
34
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -39,6 +39,7 @@ import { setTimeout as delay } from "timers/promises";
|
|
|
39
39
|
import { Buffer } from "buffer";
|
|
40
40
|
var REPORT_HEADING = "## Summary by Mendr";
|
|
41
41
|
var LEGACY_REPORT_HEADING = "## Summary";
|
|
42
|
+
var EXISTING_COMMENT_FOLLOW_UP_HEADING = "### Existing Comment Follow-up";
|
|
42
43
|
var RESOLVED_ISSUES_HEADING = "### Resolved Issues";
|
|
43
44
|
var UNRESOLVED_ISSUES_HEADING = "### Unresolved Issues";
|
|
44
45
|
var ROUND_CAP_HEADING = "### Round Cap";
|
|
@@ -65,6 +66,16 @@ ${legacyShaLine}`)) {
|
|
|
65
66
|
function appendIssueResult(report, entry) {
|
|
66
67
|
return appendResolvedIssue(report, entry);
|
|
67
68
|
}
|
|
69
|
+
function appendExistingCommentFollowUp(report, entry) {
|
|
70
|
+
let normalized = ensureSummary(report);
|
|
71
|
+
const lines = existingCommentFollowUpLines(entry);
|
|
72
|
+
const block = lines.join("\n");
|
|
73
|
+
if (normalized.includes(block)) {
|
|
74
|
+
return normalized === report ? report : normalized;
|
|
75
|
+
}
|
|
76
|
+
normalized = ensureSection(normalized, EXISTING_COMMENT_FOLLOW_UP_HEADING);
|
|
77
|
+
return appendBlock(normalized, lines);
|
|
78
|
+
}
|
|
68
79
|
function appendUnresolvedIssue(report, entry) {
|
|
69
80
|
let normalized = ensureSummary(report);
|
|
70
81
|
const issueLine = `#### ${entry.issue.title}`;
|
|
@@ -104,6 +115,29 @@ function appendRoundCapNote(report, note) {
|
|
|
104
115
|
normalized = ensureSection(normalized, ROUND_CAP_HEADING);
|
|
105
116
|
return appendBlock(normalized, [capLine, ...openIssueLines]);
|
|
106
117
|
}
|
|
118
|
+
function existingCommentFollowUpLines(entry) {
|
|
119
|
+
if (entry.status === "fixed") {
|
|
120
|
+
return [
|
|
121
|
+
`#### ${entry.issue.title}`,
|
|
122
|
+
issueFingerprintLine(entry.issue),
|
|
123
|
+
"**Status:** Fixed",
|
|
124
|
+
`**Commit:** ${entry.sha}`,
|
|
125
|
+
entry.summary
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
if (entry.status === "fixer_failed") {
|
|
129
|
+
return [
|
|
130
|
+
`#### ${entry.issue.title}`,
|
|
131
|
+
issueFingerprintLine(entry.issue),
|
|
132
|
+
"**Status:** Fixer failed",
|
|
133
|
+
entry.summary
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
if (entry.status === "already_addressed") {
|
|
137
|
+
return ["#### Already addressed", "**Status:** Already addressed", entry.summary];
|
|
138
|
+
}
|
|
139
|
+
return ["#### Issue does not exist", "**Status:** Issue does not exist", entry.summary];
|
|
140
|
+
}
|
|
107
141
|
function ensureSummary(report) {
|
|
108
142
|
const trimmed = report.trim();
|
|
109
143
|
if (trimmed.length === 0) {
|
|
@@ -297,6 +331,29 @@ async function runOrchestrator(options) {
|
|
|
297
331
|
let report = await readReport(reportPath);
|
|
298
332
|
let openIssues = [];
|
|
299
333
|
const attemptedIssueFingerprints = /* @__PURE__ */ new Set();
|
|
334
|
+
const commentPrepassOutcome = await runExistingCommentPrepass({
|
|
335
|
+
options,
|
|
336
|
+
exec,
|
|
337
|
+
agentDriver,
|
|
338
|
+
repo: sessionRepo,
|
|
339
|
+
pr: meta.pr,
|
|
340
|
+
model,
|
|
341
|
+
effort,
|
|
342
|
+
reviewPath,
|
|
343
|
+
report,
|
|
344
|
+
reportPath,
|
|
345
|
+
branch: meta.branch,
|
|
346
|
+
branchPushRemote: normalizeBranchPushRemote(meta.branchPushRemote),
|
|
347
|
+
state,
|
|
348
|
+
setState: (nextState) => {
|
|
349
|
+
state = nextState;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
report = commentPrepassOutcome.report;
|
|
353
|
+
state = commentPrepassOutcome.state;
|
|
354
|
+
for (const issue of commentPrepassOutcome.attemptedIssues) {
|
|
355
|
+
attemptedIssueFingerprints.add(issueFingerprint(issue));
|
|
356
|
+
}
|
|
300
357
|
for (let round = 1; round <= meta.maxRounds; round += 1) {
|
|
301
358
|
state = await updateStatus(options, state, {
|
|
302
359
|
phase: "reviewing",
|
|
@@ -471,6 +528,162 @@ function dedupeReviewedIssues(reviewedIssues) {
|
|
|
471
528
|
}
|
|
472
529
|
return deduped;
|
|
473
530
|
}
|
|
531
|
+
async function runExistingCommentPrepass(input) {
|
|
532
|
+
if (!input.agentDriver.reviewExistingComments) {
|
|
533
|
+
return {
|
|
534
|
+
report: input.report,
|
|
535
|
+
state: input.state,
|
|
536
|
+
attemptedIssues: []
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
let state = await updateStatus(input.options, input.state, {
|
|
540
|
+
phase: "reviewing",
|
|
541
|
+
currentStatus: "Reviewing existing comments"
|
|
542
|
+
});
|
|
543
|
+
input.setState(state);
|
|
544
|
+
await appendEvent(input.options.mendrHome, input.options.reviewId, {
|
|
545
|
+
status: "Reviewing existing comments",
|
|
546
|
+
detail: "existing comment follow-up"
|
|
547
|
+
});
|
|
548
|
+
const diff = await fetchPullRequestDiff(input.exec, input.repo, input.pr);
|
|
549
|
+
const reviewMarkdown = await readFile(input.reviewPath, "utf8");
|
|
550
|
+
const ctx = buildContext({
|
|
551
|
+
repo: input.repo,
|
|
552
|
+
pr: input.pr,
|
|
553
|
+
model: input.model,
|
|
554
|
+
effort: input.effort,
|
|
555
|
+
diff,
|
|
556
|
+
reviewMarkdown,
|
|
557
|
+
reportMarkdown: input.report
|
|
558
|
+
});
|
|
559
|
+
let results = [];
|
|
560
|
+
try {
|
|
561
|
+
results = await input.agentDriver.reviewExistingComments(ctx);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
await fail(input.options, state, "Existing comment review failed", error);
|
|
564
|
+
}
|
|
565
|
+
let report = input.report;
|
|
566
|
+
const attempts = [];
|
|
567
|
+
for (const result of results) {
|
|
568
|
+
if (result.status === "needs_fix" && result.issue) {
|
|
569
|
+
const attempt = {
|
|
570
|
+
issue: result.issue,
|
|
571
|
+
diff,
|
|
572
|
+
round: 0,
|
|
573
|
+
issueIndex: attempts.length + 1
|
|
574
|
+
};
|
|
575
|
+
attempts.push({ result, attempt });
|
|
576
|
+
await appendIssueRecord(input.options.mendrHome, input.options.reviewId, {
|
|
577
|
+
sessionId: input.options.reviewId,
|
|
578
|
+
round: 0,
|
|
579
|
+
issueIndex: attempt.issueIndex,
|
|
580
|
+
fingerprint: issueFingerprint(result.issue),
|
|
581
|
+
title: result.issue.title,
|
|
582
|
+
file: result.issue.file,
|
|
583
|
+
line: result.issue.line,
|
|
584
|
+
severity: result.issue.severity,
|
|
585
|
+
description: result.issue.description
|
|
586
|
+
});
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (result.status === "already_addressed" || result.status === "does_not_exist") {
|
|
590
|
+
report = appendExistingCommentFollowUp(report, {
|
|
591
|
+
status: result.status,
|
|
592
|
+
summary: result.summary
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (results.length > 0) {
|
|
597
|
+
await writeFile(input.reportPath, report, "utf8");
|
|
598
|
+
}
|
|
599
|
+
if (attempts.length === 0) {
|
|
600
|
+
return {
|
|
601
|
+
report,
|
|
602
|
+
state,
|
|
603
|
+
attemptedIssues: []
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
state = await updateStatus(input.options, state, {
|
|
607
|
+
phase: "fixing",
|
|
608
|
+
currentStatus: "Resolving existing comments",
|
|
609
|
+
issuesFound: state.issuesFound + attempts.length
|
|
610
|
+
});
|
|
611
|
+
input.setState(state);
|
|
612
|
+
await appendEvent(input.options.mendrHome, input.options.reviewId, {
|
|
613
|
+
status: "Resolving existing comments",
|
|
614
|
+
detail: `existing comment follow-up with ${attempts.length} issue${attempts.length === 1 ? "" : "s"}`
|
|
615
|
+
});
|
|
616
|
+
let fixedCount = 0;
|
|
617
|
+
let lastSuccessfulSha = await getHeadCommitSha(input.exec, input.repo);
|
|
618
|
+
for (const { result, attempt } of attempts) {
|
|
619
|
+
const outcome = await runSingleIssueFix({
|
|
620
|
+
options: input.options,
|
|
621
|
+
exec: input.exec,
|
|
622
|
+
agentDriver: input.agentDriver,
|
|
623
|
+
ctx: {
|
|
624
|
+
...ctx,
|
|
625
|
+
diff: attempt.diff,
|
|
626
|
+
reportMarkdown: report
|
|
627
|
+
},
|
|
628
|
+
attempt,
|
|
629
|
+
lastSuccessfulSha,
|
|
630
|
+
state
|
|
631
|
+
});
|
|
632
|
+
if (outcome.status === "fixed" && outcome.sha) {
|
|
633
|
+
report = appendExistingCommentFollowUp(report, {
|
|
634
|
+
status: "fixed",
|
|
635
|
+
issue: attempt.issue,
|
|
636
|
+
sha: outcome.sha,
|
|
637
|
+
summary: outcome.summary
|
|
638
|
+
});
|
|
639
|
+
} else {
|
|
640
|
+
report = appendExistingCommentFollowUp(report, {
|
|
641
|
+
status: "fixer_failed",
|
|
642
|
+
issue: attempt.issue,
|
|
643
|
+
summary: outcome.summary
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
await appendFixAttempt(input.options.mendrHome, input.options.reviewId, {
|
|
647
|
+
sessionId: input.options.reviewId,
|
|
648
|
+
round: 0,
|
|
649
|
+
issueIndex: attempt.issueIndex,
|
|
650
|
+
fingerprint: issueFingerprint(attempt.issue),
|
|
651
|
+
title: attempt.issue.title,
|
|
652
|
+
status: outcome.status,
|
|
653
|
+
summary: outcome.summary,
|
|
654
|
+
...outcome.sha ? { commitSha: outcome.sha } : {}
|
|
655
|
+
});
|
|
656
|
+
await writeFile(input.reportPath, report, "utf8");
|
|
657
|
+
if (outcome.status === "fixed" && outcome.sha) {
|
|
658
|
+
fixedCount += 1;
|
|
659
|
+
lastSuccessfulSha = outcome.sha;
|
|
660
|
+
state = await updateStatus(input.options, state, {
|
|
661
|
+
issuesFixed: state.issuesFixed + 1
|
|
662
|
+
});
|
|
663
|
+
input.setState(state);
|
|
664
|
+
} else {
|
|
665
|
+
await appendEvent(input.options.mendrHome, input.options.reviewId, {
|
|
666
|
+
status: "Fix failed",
|
|
667
|
+
detail: `${attempt.issue.title}: ${result.summary} ${outcome.summary}`
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (fixedCount > 0) {
|
|
672
|
+
try {
|
|
673
|
+
await pushWithRetry(input.exec, input.repo, input.branchPushRemote, input.branch);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const message = errorToMessage(error);
|
|
676
|
+
const failedReport = appendFailureNote(report, `push failed: ${message}`);
|
|
677
|
+
await writeFile(input.reportPath, failedReport, "utf8");
|
|
678
|
+
await fail(input.options, state, "Push failed", error);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
report,
|
|
683
|
+
state,
|
|
684
|
+
attemptedIssues: attempts.map(({ attempt }) => attempt.issue)
|
|
685
|
+
};
|
|
686
|
+
}
|
|
474
687
|
function splitDiffForReview(diff, maxLines = MAX_REVIEW_DIFF_LINES) {
|
|
475
688
|
const normalized = diff.replace(/\r\n?/g, "\n");
|
|
476
689
|
if (lineCount(normalized) <= maxLines) {
|
package/package.json
CHANGED