@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.
@@ -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
- buildFixSystemPrompt()
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: Array.isArray(parsed.comments) ? parsed.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
- return `- @${author}: ${body}`;
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
@@ -26,7 +26,7 @@ import {
26
26
  worktreesDir,
27
27
  writeMeta,
28
28
  writeState
29
- } from "./chunk-753PEKBT.js";
29
+ } from "./chunk-GKT5VC55.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { spawn as spawn2 } from "child_process";
package/dist/daemon.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  stageAll,
29
29
  waitForPullRequestChecks,
30
30
  writeState
31
- } from "./chunk-753PEKBT.js";
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
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@pepps233/mendr",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Autonomous pull request review agent CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "mendr": "./dist/cli.js"
7
+ "mendr": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"