@pepps233/mendr 0.1.0 → 0.3.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/README.md CHANGED
@@ -38,6 +38,7 @@ mendr ls
38
38
  mendr view <id>
39
39
  mendr stop <id>
40
40
  mendr kill <id>
41
+ mendr version
41
42
  ```
42
43
 
43
44
  ![mendr](https://raw.githubusercontent.com/Pepps233/mendr/main/assets/mendr.gif)
@@ -50,6 +51,8 @@ mendr kill <id>
50
51
  Codex accepts `low`, `medium`, `high`, or `xhigh`.
51
52
  Claude Code accepts `low`, `medium`, `high`, `xhigh`, or `max`.
52
53
  Set `MENDR_CODEX_MODEL`, `MENDR_CODEX_EFFORT`, `MENDR_CLAUDE_MODEL`, or `MENDR_CLAUDE_EFFORT` to change unattended defaults.
54
+ Run `mendr version` to check the installed package version against the latest npm release.
55
+ When an interactive npm-installed `mendr` is behind the latest release, the first command for that installed/latest version pair prompts before continuing so you can upgrade with yes or no.
53
56
 
54
57
  ## Example
55
58
 
@@ -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),
@@ -153,19 +162,6 @@ function issueFingerprint(issue) {
153
162
  normalizeFingerprintPart(issue.description)
154
163
  ].join("|");
155
164
  }
156
- function dedupeIssues(issues) {
157
- const seen = /* @__PURE__ */ new Set();
158
- const deduped = [];
159
- for (const issue of issues) {
160
- const fingerprint = issueFingerprint(issue);
161
- if (seen.has(fingerprint)) {
162
- continue;
163
- }
164
- seen.add(fingerprint);
165
- deduped.push(issue);
166
- }
167
- return deduped;
168
- }
169
165
  function parseIssue(value) {
170
166
  if (!isRecord(value)) {
171
167
  throw new AgentParseError("Agent issue must be an object.");
@@ -201,6 +197,29 @@ function parseFixIssueResult(value) {
201
197
  summary
202
198
  };
203
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
+ }
204
223
  function readOptionalString(value, key) {
205
224
  const raw = value[key];
206
225
  return typeof raw === "string" && raw.trim().length > 0 ? raw : void 0;
@@ -268,6 +287,15 @@ function isRecord(value) {
268
287
  // src/agents/prompts.ts
269
288
  var issueSchema = '[{"title":"specific standalone title","file":"path","line":1,"severity":"low|medium|high|critical","description":"two concise sentences describing the finding"}]';
270
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
+ }
271
299
  function buildReviewSystemPrompt() {
272
300
  return [
273
301
  "You are the REVIEW agent in the pull request review loop.",
@@ -283,6 +311,33 @@ function buildFixSystemPrompt() {
283
311
  "Respond only with JSON matching the requested fix-result schema."
284
312
  ].join("\n");
285
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
+ }
286
341
  function buildReviewPrompt(ctx) {
287
342
  return [
288
343
  "You are a code review agent for a GitHub pull request.",
@@ -362,6 +417,16 @@ function parseClaudeIssues(output) {
362
417
  }
363
418
  throw new AgentParseError("Claude output did not include a result payload.");
364
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
+ }
365
430
  function parseClaudeFixResults(output) {
366
431
  const envelope = extractJsonValue(output);
367
432
  if (isClaudeResultEnvelope(envelope)) {
@@ -372,30 +437,19 @@ function parseClaudeFixResults(output) {
372
437
  }
373
438
  throw new AgentParseError("Claude output did not include a result payload.");
374
439
  }
440
+ function buildClaudeExistingCommentReviewInvocation(ctx) {
441
+ const prompt = buildExistingCommentReviewPrompt(ctx);
442
+ return buildClaudeInvocation(ctx, prompt, buildExistingCommentReviewSystemPrompt());
443
+ }
375
444
  function buildClaudeReviewInvocation(ctx) {
376
445
  const prompt = buildReviewPrompt(ctx);
377
- return {
378
- command: "claude",
379
- args: [
380
- "-p",
381
- prompt,
382
- "--output-format",
383
- "json",
384
- "--model",
385
- ctx.model,
386
- "--effort",
387
- ctx.effort,
388
- "--permission-mode",
389
- "acceptEdits",
390
- "--add-dir",
391
- ctx.repo,
392
- "--append-system-prompt",
393
- buildReviewSystemPrompt()
394
- ]
395
- };
446
+ return buildClaudeInvocation(ctx, prompt, buildReviewSystemPrompt());
396
447
  }
397
448
  function buildClaudeFixInvocation(issues, ctx) {
398
449
  const prompt = buildFixPrompt(issues, ctx);
450
+ return buildClaudeInvocation(ctx, prompt, buildFixSystemPrompt());
451
+ }
452
+ function buildClaudeInvocation(ctx, prompt, systemPrompt) {
399
453
  return {
400
454
  command: "claude",
401
455
  args: [
@@ -412,7 +466,7 @@ function buildClaudeFixInvocation(issues, ctx) {
412
466
  "--add-dir",
413
467
  ctx.repo,
414
468
  "--append-system-prompt",
415
- buildFixSystemPrompt()
469
+ systemPrompt
416
470
  ]
417
471
  };
418
472
  }
@@ -424,37 +478,31 @@ function isClaudeResultEnvelope(value) {
424
478
  function parseCodexIssues(output) {
425
479
  return parseIssueArrayFromText(output);
426
480
  }
481
+ function parseCodexExistingCommentReviewResults(output) {
482
+ return parseExistingCommentReviewResultArrayFromText(output);
483
+ }
427
484
  function parseCodexFixResults(output) {
428
485
  return parseFixIssueResultArrayFromText(output);
429
486
  }
487
+ function buildCodexExistingCommentReviewInvocation(ctx, options) {
488
+ const prompt = buildExistingCommentReviewPrompt(ctx);
489
+ return buildCodexInvocation(ctx, options, prompt);
490
+ }
430
491
  function buildCodexReviewInvocation(ctx, options) {
431
492
  const prompt = buildReviewPrompt(ctx);
432
- return {
433
- command: "codex",
434
- args: [
435
- "exec",
436
- prompt,
437
- "-m",
438
- ctx.model,
439
- "-c",
440
- `model_reasoning_effort=${JSON.stringify(ctx.effort)}`,
441
- "--sandbox",
442
- "workspace-write",
443
- "--json",
444
- "-C",
445
- ctx.repo,
446
- "--output-last-message",
447
- options.outputFile
448
- ]
449
- };
493
+ return buildCodexInvocation(ctx, options, prompt);
450
494
  }
451
495
  function buildCodexFixInvocation(issues, ctx, options) {
452
496
  const prompt = buildFixPrompt(issues, ctx);
497
+ return buildCodexInvocation(ctx, options, prompt);
498
+ }
499
+ function buildCodexInvocation(ctx, options, prompt) {
453
500
  return {
454
501
  command: "codex",
502
+ input: prompt,
455
503
  args: [
456
504
  "exec",
457
- prompt,
505
+ "-",
458
506
  "-m",
459
507
  ctx.model,
460
508
  "-c",
@@ -523,6 +571,16 @@ var ClaudeAgentDriver = class {
523
571
  exec;
524
572
  outputDir;
525
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
+ }
526
584
  async review(ctx) {
527
585
  const label = this.nextLabel("claude", "review");
528
586
  const invocation = buildClaudeReviewInvocation(ctx);
@@ -556,6 +614,21 @@ var CodexAgentDriver = class {
556
614
  exec;
557
615
  outputDir;
558
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
+ }
559
632
  async review(ctx) {
560
633
  const label = this.nextLabel("codex", "review");
561
634
  const outputFile = await this.outputFile(label);
@@ -600,6 +673,7 @@ async function runAgentInvocation(exec, invocation, options) {
600
673
  const stderrFile = join(options.outputDir, `${options.label}.stderr.log`);
601
674
  const result = await exec(invocation.command, invocation.args, {
602
675
  cwd: options.cwd,
676
+ input: invocation.input,
603
677
  timeoutMs: agentTimeoutMs(),
604
678
  stdoutFile,
605
679
  stderrFile
@@ -1004,7 +1078,6 @@ async function readJson(home, id, fileName) {
1004
1078
 
1005
1079
  export {
1006
1080
  issueFingerprint,
1007
- dedupeIssues,
1008
1081
  defaultExec,
1009
1082
  execOk,
1010
1083
  defaultModelForAgent,
package/dist/cli.d.ts CHANGED
@@ -41,6 +41,9 @@ type CliParseResult = {
41
41
  } | {
42
42
  ok: true;
43
43
  command: "ls";
44
+ } | {
45
+ ok: true;
46
+ command: "version";
44
47
  } | {
45
48
  ok: true;
46
49
  command: "view" | "kill" | "stop";
package/dist/cli.js CHANGED
@@ -26,17 +26,298 @@ import {
26
26
  worktreesDir,
27
27
  writeMeta,
28
28
  writeState
29
- } from "./chunk-EGSZLVR6.js";
29
+ } from "./chunk-3EH6IS7Z.js";
30
30
 
31
31
  // src/cli.ts
32
- import { spawn } from "child_process";
32
+ import { spawn as spawn2 } from "child_process";
33
33
  import { realpathSync } from "fs";
34
- import { mkdir, readdir } from "fs/promises";
34
+ import { mkdir as mkdir2, readdir } from "fs/promises";
35
35
  import { fileURLToPath } from "url";
36
36
  import { Command } from "commander";
37
37
  import { Box, Text, render, useApp, useInput, useStdin } from "ink";
38
38
  import Spinner from "ink-spinner";
39
39
  import React, { useEffect, useState } from "react";
40
+
41
+ // src/version.ts
42
+ import { spawn } from "child_process";
43
+ import { mkdir, readFile, writeFile } from "fs/promises";
44
+ import { createRequire } from "module";
45
+ import { join } from "path";
46
+ import { createInterface } from "readline/promises";
47
+ var require2 = createRequire(import.meta.url);
48
+ var manifest = require2("../package.json");
49
+ var promptStateFile = "version-check.json";
50
+ var mendrPackageName = manifest.name;
51
+ var mendrVersion = manifest.version;
52
+ async function getMendrVersionStatus(options = {}) {
53
+ const packageName = options.packageName ?? mendrPackageName;
54
+ const currentVersion = options.currentVersion ?? mendrVersion;
55
+ try {
56
+ const latestVersion = await (options.fetchLatestVersion ?? fetchLatestPackageVersion)(
57
+ packageName
58
+ );
59
+ return {
60
+ packageName,
61
+ currentVersion,
62
+ latestVersion,
63
+ isOutdated: compareNpmVersions(currentVersion, latestVersion) < 0
64
+ };
65
+ } catch (error) {
66
+ return {
67
+ packageName,
68
+ currentVersion,
69
+ isOutdated: false,
70
+ error: error instanceof Error ? error.message : String(error)
71
+ };
72
+ }
73
+ }
74
+ function formatMendrVersionStatus(status) {
75
+ const lines = [`Installed: ${status.packageName}@${status.currentVersion}`];
76
+ if (status.latestVersion) {
77
+ lines.push(`Latest: ${status.latestVersion}`);
78
+ lines.push(status.isOutdated ? "Status: update available" : "Status: up to date");
79
+ return lines.join("\n");
80
+ }
81
+ lines.push("Latest: unavailable");
82
+ if (status.error) {
83
+ lines.push(`Status: unable to check npm registry (${status.error})`);
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+ async function maybePromptForMendrUpgrade(options = {}) {
88
+ const env = options.env ?? process.env;
89
+ if (isUpdateCheckDisabled(env)) {
90
+ return;
91
+ }
92
+ const input = options.input ?? process.stdin;
93
+ const output = options.output ?? process.stderr;
94
+ if (!isInteractive(input, output)) {
95
+ return;
96
+ }
97
+ const packageName = options.packageName ?? mendrPackageName;
98
+ const currentVersion = options.currentVersion ?? mendrVersion;
99
+ const status = await getMendrVersionStatus({
100
+ packageName,
101
+ currentVersion,
102
+ fetchLatestVersion: options.fetchLatestVersion
103
+ });
104
+ if (!status.latestVersion || !status.isOutdated) {
105
+ return;
106
+ }
107
+ const mendrHome = options.mendrHome ?? defaultMendrHome(env);
108
+ const existingState = await readPromptState(mendrHome);
109
+ if (existingState?.currentVersion === currentVersion && existingState.latestVersion === status.latestVersion) {
110
+ return;
111
+ }
112
+ const askForUpgrade = options.askForUpgrade ?? ((context) => askYesNoUpgrade({
113
+ ...context,
114
+ input,
115
+ output
116
+ }));
117
+ const shouldUpgrade = await askForUpgrade({
118
+ packageName,
119
+ currentVersion,
120
+ latestVersion: status.latestVersion
121
+ });
122
+ if (!shouldUpgrade) {
123
+ await writePromptState(mendrHome, {
124
+ currentVersion,
125
+ latestVersion: status.latestVersion,
126
+ response: "declined",
127
+ promptedAt: (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
128
+ });
129
+ return;
130
+ }
131
+ try {
132
+ await (options.installLatestPackage ?? installLatestPackage)(packageName);
133
+ await writePromptState(mendrHome, {
134
+ currentVersion,
135
+ latestVersion: status.latestVersion,
136
+ response: "accepted",
137
+ promptedAt: (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
138
+ });
139
+ } catch (error) {
140
+ output.write(
141
+ `Upgrade failed: ${error instanceof Error ? error.message : String(error)}
142
+ `
143
+ );
144
+ output.write(`Continuing with ${packageName}@${currentVersion}.
145
+ `);
146
+ }
147
+ }
148
+ async function fetchLatestPackageVersion(packageName, options = {}) {
149
+ const fetchFn = options.fetch ?? fetch;
150
+ const controller = new AbortController();
151
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 2e3);
152
+ try {
153
+ const response = await fetchFn(
154
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
155
+ {
156
+ headers: {
157
+ accept: "application/vnd.npm.install-v1+json, application/json"
158
+ },
159
+ signal: controller.signal
160
+ }
161
+ );
162
+ if (!response.ok) {
163
+ throw new Error(`npm registry returned HTTP ${response.status}`);
164
+ }
165
+ const metadata = await response.json();
166
+ const latest = metadata["dist-tags"]?.latest;
167
+ if (typeof latest !== "string" || latest.length === 0) {
168
+ throw new Error("npm registry response did not include dist-tags.latest");
169
+ }
170
+ return latest;
171
+ } finally {
172
+ clearTimeout(timeout);
173
+ }
174
+ }
175
+ async function installLatestPackage(packageName) {
176
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
177
+ await new Promise((resolve, reject) => {
178
+ const child = spawn(npmCommand, ["install", "-g", `${packageName}@latest`], {
179
+ stdio: "inherit"
180
+ });
181
+ child.once("error", reject);
182
+ child.once("close", (code, signal) => {
183
+ if (code === 0) {
184
+ resolve();
185
+ return;
186
+ }
187
+ reject(
188
+ new Error(
189
+ signal ? `npm install -g ${packageName}@latest stopped with ${signal}` : `npm install -g ${packageName}@latest exited with code ${code ?? 1}`
190
+ )
191
+ );
192
+ });
193
+ });
194
+ }
195
+ function compareNpmVersions(left, right) {
196
+ const leftParts = parseSemver(left);
197
+ const rightParts = parseSemver(right);
198
+ if (!leftParts || !rightParts) {
199
+ return left.localeCompare(right);
200
+ }
201
+ const length = Math.max(leftParts.main.length, rightParts.main.length);
202
+ for (let index = 0; index < length; index += 1) {
203
+ const diff = (leftParts.main[index] ?? 0) - (rightParts.main[index] ?? 0);
204
+ if (diff !== 0) {
205
+ return diff;
206
+ }
207
+ }
208
+ return comparePrerelease(leftParts.prerelease, rightParts.prerelease);
209
+ }
210
+ async function askYesNoUpgrade(input) {
211
+ const prompt = `${input.packageName} ${input.latestVersion} is available (installed ${input.currentVersion}). Upgrade now? [y/n] `;
212
+ const readline = createInterface({
213
+ input: input.input,
214
+ output: input.output,
215
+ terminal: true
216
+ });
217
+ try {
218
+ for (; ; ) {
219
+ const answer = (await readline.question(prompt)).trim().toLowerCase();
220
+ if (answer === "y" || answer === "yes") {
221
+ return true;
222
+ }
223
+ if (answer === "n" || answer === "no") {
224
+ return false;
225
+ }
226
+ input.output.write("Please answer yes or no.\n");
227
+ }
228
+ } finally {
229
+ readline.close();
230
+ }
231
+ }
232
+ async function readPromptState(mendrHome) {
233
+ try {
234
+ const raw = await readFile(promptStatePath(mendrHome), "utf8");
235
+ const state = JSON.parse(raw);
236
+ if (typeof state.currentVersion !== "string" || typeof state.latestVersion !== "string" || state.response !== "accepted" && state.response !== "declined" || typeof state.promptedAt !== "string") {
237
+ return void 0;
238
+ }
239
+ return state;
240
+ } catch (error) {
241
+ if (error.code === "ENOENT") {
242
+ return void 0;
243
+ }
244
+ return void 0;
245
+ }
246
+ }
247
+ async function writePromptState(mendrHome, state) {
248
+ await mkdir(mendrHome, { recursive: true });
249
+ await writeFile(promptStatePath(mendrHome), `${JSON.stringify(state, null, 2)}
250
+ `, "utf8");
251
+ }
252
+ function promptStatePath(mendrHome) {
253
+ return join(mendrHome, promptStateFile);
254
+ }
255
+ function isUpdateCheckDisabled(env) {
256
+ return env.CI === "true" || env.MENDR_SKIP_UPDATE_CHECK === "1";
257
+ }
258
+ function isInteractive(input, output) {
259
+ return input.isTTY === true && output.isTTY === true;
260
+ }
261
+ function parseSemver(version) {
262
+ const withoutBuildMetadata = version.replace(/^v/, "").split("+", 1)[0];
263
+ const [mainVersion, prereleaseVersion = ""] = withoutBuildMetadata.split("-", 2);
264
+ const main2 = mainVersion.split(".").map((part) => {
265
+ if (!/^\d+$/.test(part)) {
266
+ return Number.NaN;
267
+ }
268
+ return Number(part);
269
+ });
270
+ if (main2.length === 0 || main2.some((part) => !Number.isSafeInteger(part))) {
271
+ return void 0;
272
+ }
273
+ return {
274
+ main: main2,
275
+ prerelease: prereleaseVersion.length > 0 ? prereleaseVersion.split(".") : []
276
+ };
277
+ }
278
+ function comparePrerelease(left, right) {
279
+ if (left.length === 0 && right.length === 0) {
280
+ return 0;
281
+ }
282
+ if (left.length === 0) {
283
+ return 1;
284
+ }
285
+ if (right.length === 0) {
286
+ return -1;
287
+ }
288
+ const length = Math.max(left.length, right.length);
289
+ for (let index = 0; index < length; index += 1) {
290
+ const leftPart = left[index];
291
+ const rightPart = right[index];
292
+ if (leftPart === void 0) {
293
+ return -1;
294
+ }
295
+ if (rightPart === void 0) {
296
+ return 1;
297
+ }
298
+ const comparison = comparePrereleasePart(leftPart, rightPart);
299
+ if (comparison !== 0) {
300
+ return comparison;
301
+ }
302
+ }
303
+ return 0;
304
+ }
305
+ function comparePrereleasePart(left, right) {
306
+ const leftNumeric = /^\d+$/.test(left);
307
+ const rightNumeric = /^\d+$/.test(right);
308
+ if (leftNumeric && rightNumeric) {
309
+ return Number(left) - Number(right);
310
+ }
311
+ if (leftNumeric) {
312
+ return -1;
313
+ }
314
+ if (rightNumeric) {
315
+ return 1;
316
+ }
317
+ return left.localeCompare(right);
318
+ }
319
+
320
+ // src/cli.ts
40
321
  var agents = /* @__PURE__ */ new Set(["claude", "codex"]);
41
322
  function parseCliArgs(argv) {
42
323
  const args = argv.slice(2);
@@ -46,6 +327,12 @@ function parseCliArgs(argv) {
46
327
  command: "ls"
47
328
  };
48
329
  }
330
+ if (args[0] === "version" || args[0] === "--version" || args[0] === "-V") {
331
+ return {
332
+ ok: true,
333
+ command: "version"
334
+ };
335
+ }
49
336
  if (args[0] === "view" || args[0] === "kill" || args[0] === "stop") {
50
337
  const reviewId = args[1];
51
338
  if (!reviewId) {
@@ -134,7 +421,7 @@ async function startReview(options) {
134
421
  };
135
422
  let worktreeCreated = false;
136
423
  try {
137
- await mkdir(worktreesDir(mendrHome), { recursive: true });
424
+ await mkdir2(worktreesDir(mendrHome), { recursive: true });
138
425
  await fetchPullRequestHeadRef(exec, repo, options.pr, worktreeRef);
139
426
  await createDetachedWorktree(exec, repo, worktreePath, worktreeRef);
140
427
  worktreeCreated = true;
@@ -477,7 +764,7 @@ async function assertBinary(exec, command, args, cwd) {
477
764
  }
478
765
  function defaultSpawnDaemon(args) {
479
766
  const daemonPath = fileURLToPath(new URL("./daemon.js", import.meta.url));
480
- const child = spawn(
767
+ const child = spawn2(
481
768
  process.execPath,
482
769
  [daemonPath, "--home", args.mendrHome, "--id", args.reviewId],
483
770
  {
@@ -496,7 +783,7 @@ async function createReviewId(mendrHome) {
496
783
  for (; ; ) {
497
784
  const id = String(candidate);
498
785
  try {
499
- await mkdir(reviewDir(mendrHome, id));
786
+ await mkdir2(reviewDir(mendrHome, id));
500
787
  return id;
501
788
  } catch (error) {
502
789
  if (error.code === "EEXIST") {
@@ -633,7 +920,7 @@ function parsePositiveInteger(value) {
633
920
  }
634
921
  async function main(argv) {
635
922
  const program = new Command();
636
- program.name("mendr").description("Run an autonomous agentic review loop on a GitHub pull request.").argument("[agent]", "agent CLI to use: claude or codex").argument("[pr]", "pull request number or GitHub pull request URL").option("-r, --rounds <n>", "maximum review and fix iterations", "3").option("-m, --model <model>", "agent model override").option("-e, --effort <effort>", "agent effort override").action(
923
+ program.name("mendr").version(mendrVersion).description("Run an autonomous agentic review loop on a GitHub pull request.").argument("[agent]", "agent CLI to use: claude or codex").argument("[pr]", "pull request number or GitHub pull request URL").option("-r, --rounds <n>", "maximum review and fix iterations", "3").option("-m, --model <model>", "agent model override").option("-e, --effort <effort>", "agent effort override").action(
637
924
  async (agent, prArg, options) => {
638
925
  if (!agent && !prArg) {
639
926
  program.help();
@@ -673,6 +960,9 @@ async function main(argv) {
673
960
  program.command("ls").description("List review sessions.").action(async () => {
674
961
  console.log(await renderReviewList());
675
962
  });
963
+ program.command("version").description("Check the installed mendr version.").action(async () => {
964
+ console.log(formatMendrVersionStatus(await getMendrVersionStatus()));
965
+ });
676
966
  program.command("view").description("Watch a live review status view.").argument("<id>", "review id").action(async (reviewId) => {
677
967
  await startLiveReviewView({ reviewId });
678
968
  });
@@ -684,6 +974,9 @@ async function main(argv) {
684
974
  await closeReview({ reviewId });
685
975
  console.log(`Killed ${reviewId}`);
686
976
  });
977
+ program.hook("preAction", async () => {
978
+ await maybePromptForMendrUpgrade();
979
+ });
687
980
  await program.parseAsync(argv);
688
981
  }
689
982
  function isCliEntrypoint(invokedPath, modulePath = fileURLToPath(import.meta.url)) {
package/dist/daemon.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  appendIssueRecord,
6
6
  commitStaged,
7
7
  createAgentDriver,
8
- dedupeIssues,
9
8
  defaultEffortForAgent,
10
9
  defaultExec,
11
10
  defaultMendrHome,
@@ -29,7 +28,7 @@ import {
29
28
  stageAll,
30
29
  waitForPullRequestChecks,
31
30
  writeState
32
- } from "./chunk-EGSZLVR6.js";
31
+ } from "./chunk-3EH6IS7Z.js";
33
32
 
34
33
  // src/orchestrator.ts
35
34
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -40,6 +39,7 @@ import { setTimeout as delay } from "timers/promises";
40
39
  import { Buffer } from "buffer";
41
40
  var REPORT_HEADING = "## Summary by Mendr";
42
41
  var LEGACY_REPORT_HEADING = "## Summary";
42
+ var EXISTING_COMMENT_FOLLOW_UP_HEADING = "### Existing Comment Follow-up";
43
43
  var RESOLVED_ISSUES_HEADING = "### Resolved Issues";
44
44
  var UNRESOLVED_ISSUES_HEADING = "### Unresolved Issues";
45
45
  var ROUND_CAP_HEADING = "### Round Cap";
@@ -66,6 +66,16 @@ ${legacyShaLine}`)) {
66
66
  function appendIssueResult(report, entry) {
67
67
  return appendResolvedIssue(report, entry);
68
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
+ }
69
79
  function appendUnresolvedIssue(report, entry) {
70
80
  let normalized = ensureSummary(report);
71
81
  const issueLine = `#### ${entry.issue.title}`;
@@ -105,6 +115,29 @@ function appendRoundCapNote(report, note) {
105
115
  normalized = ensureSection(normalized, ROUND_CAP_HEADING);
106
116
  return appendBlock(normalized, [capLine, ...openIssueLines]);
107
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
+ }
108
141
  function ensureSummary(report) {
109
142
  const trimmed = report.trim();
110
143
  if (trimmed.length === 0) {
@@ -266,6 +299,7 @@ var PullRequestHeadChangedError = class extends Error {
266
299
  };
267
300
  var PR_HEAD_PROPAGATION_TIMEOUT_MS = 3e4;
268
301
  var PR_HEAD_PROPAGATION_POLL_MS = 2e3;
302
+ var MAX_REVIEW_DIFF_LINES = 4e3;
269
303
  async function runOrchestrator(options) {
270
304
  const exec = options.exec ?? defaultExec;
271
305
  const dir = reviewDir(options.mendrHome, options.reviewId);
@@ -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",
@@ -317,13 +374,14 @@ async function runOrchestrator(options) {
317
374
  reviewMarkdown,
318
375
  reportMarkdown: report
319
376
  });
320
- let issues = [];
377
+ let reviewedIssues = [];
321
378
  try {
322
- issues = dedupeIssues(await agentDriver.review(ctx));
379
+ reviewedIssues = await reviewDiffChunks(agentDriver, ctx);
323
380
  } catch (error) {
324
381
  await fail(options, state, "Review failed", error);
325
382
  return;
326
383
  }
384
+ const issues = reviewedIssues.map((entry) => entry.issue);
327
385
  await persistIssueRecords(options, round, issues);
328
386
  state = await updateStatus(options, state, {
329
387
  issuesFound: state.issuesFound + issues.length
@@ -337,8 +395,9 @@ async function runOrchestrator(options) {
337
395
  break;
338
396
  }
339
397
  openIssues = issues;
340
- const issueAttempts = issues.map((issue, index) => ({
341
- issue,
398
+ const issueAttempts = reviewedIssues.map((entry, index) => ({
399
+ issue: entry.issue,
400
+ diff: entry.diff,
342
401
  round,
343
402
  issueIndex: index + 1
344
403
  }));
@@ -440,6 +499,375 @@ async function runOrchestrator(options) {
440
499
  await fail(options, state, "Orchestrator failed", error);
441
500
  }
442
501
  }
502
+ async function reviewDiffChunks(agentDriver, ctx) {
503
+ const reviewedIssues = [];
504
+ for (const diff of splitDiffForReview(ctx.diff)) {
505
+ const issues = await agentDriver.review({
506
+ ...ctx,
507
+ diff
508
+ });
509
+ for (const issue of issues) {
510
+ reviewedIssues.push({
511
+ issue,
512
+ diff
513
+ });
514
+ }
515
+ }
516
+ return dedupeReviewedIssues(reviewedIssues);
517
+ }
518
+ function dedupeReviewedIssues(reviewedIssues) {
519
+ const seen = /* @__PURE__ */ new Set();
520
+ const deduped = [];
521
+ for (const entry of reviewedIssues) {
522
+ const fingerprint = issueFingerprint(entry.issue);
523
+ if (seen.has(fingerprint)) {
524
+ continue;
525
+ }
526
+ seen.add(fingerprint);
527
+ deduped.push(entry);
528
+ }
529
+ return deduped;
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
+ }
687
+ function splitDiffForReview(diff, maxLines = MAX_REVIEW_DIFF_LINES) {
688
+ const normalized = diff.replace(/\r\n?/g, "\n");
689
+ if (lineCount(normalized) <= maxLines) {
690
+ return [normalized];
691
+ }
692
+ const chunks = [];
693
+ let currentChunk = [];
694
+ for (const fileBlock of splitDiffFileBlocks(normalized.split("\n"))) {
695
+ const boundedBlocks = fileBlock.length > maxLines ? splitOversizedFileBlock(fileBlock, maxLines) : [fileBlock];
696
+ for (const block of boundedBlocks) {
697
+ for (const boundedBlock of splitRawLines(block, maxLines)) {
698
+ if (currentChunk.length > 0 && currentChunk.length + boundedBlock.length > maxLines) {
699
+ chunks.push(currentChunk);
700
+ currentChunk = [];
701
+ }
702
+ currentChunk.push(...boundedBlock);
703
+ }
704
+ }
705
+ }
706
+ if (currentChunk.length > 0) {
707
+ chunks.push(currentChunk);
708
+ }
709
+ return chunks.map((chunk) => chunk.join("\n"));
710
+ }
711
+ function splitDiffFileBlocks(lines) {
712
+ const blocks = [];
713
+ let currentBlock = [];
714
+ for (const line of lines) {
715
+ if (line.startsWith("diff --git ") && currentBlock.length > 0) {
716
+ blocks.push(currentBlock);
717
+ currentBlock = [];
718
+ }
719
+ currentBlock.push(line);
720
+ }
721
+ if (currentBlock.length > 0) {
722
+ blocks.push(currentBlock);
723
+ }
724
+ return blocks;
725
+ }
726
+ function splitOversizedFileBlock(block, maxLines) {
727
+ const firstHunkIndex = block.findIndex((line) => line.startsWith("@@"));
728
+ if (firstHunkIndex === -1) {
729
+ return splitRawLines(block, maxLines);
730
+ }
731
+ const header = block.slice(0, firstHunkIndex);
732
+ const hunks = splitHunks(block.slice(firstHunkIndex));
733
+ const chunks = [];
734
+ let currentChunk = [...header];
735
+ for (const hunk of hunks) {
736
+ if (header.length + hunk.length > maxLines) {
737
+ if (currentChunk.length > header.length) {
738
+ chunks.push(currentChunk);
739
+ currentChunk = [...header];
740
+ }
741
+ chunks.push(...splitOversizedHunk(header, hunk, maxLines));
742
+ continue;
743
+ }
744
+ if (currentChunk.length + hunk.length > maxLines) {
745
+ chunks.push(currentChunk);
746
+ currentChunk = [...header];
747
+ }
748
+ currentChunk.push(...hunk);
749
+ }
750
+ if (currentChunk.length > header.length) {
751
+ chunks.push(currentChunk);
752
+ }
753
+ return chunks;
754
+ }
755
+ function splitHunks(lines) {
756
+ const hunks = [];
757
+ let currentHunk = [];
758
+ for (const line of lines) {
759
+ if (line.startsWith("@@") && currentHunk.length > 0) {
760
+ hunks.push(currentHunk);
761
+ currentHunk = [];
762
+ }
763
+ currentHunk.push(line);
764
+ }
765
+ if (currentHunk.length > 0) {
766
+ hunks.push(currentHunk);
767
+ }
768
+ return hunks;
769
+ }
770
+ function splitOversizedHunk(header, hunk, maxLines) {
771
+ const parsedHeader = parseHunkHeader(hunk[0]);
772
+ if (!parsedHeader) {
773
+ const hunkHeader = hunk[0]?.startsWith("@@") ? [hunk[0]] : [];
774
+ const body2 = hunkHeader.length > 0 ? hunk.slice(1) : hunk;
775
+ const prefix = [...header, ...hunkHeader];
776
+ const bodyCapacity2 = maxLines - prefix.length;
777
+ if (bodyCapacity2 <= 0) {
778
+ return splitRawLines([...prefix, ...body2], maxLines);
779
+ }
780
+ if (body2.length === 0) {
781
+ return [prefix];
782
+ }
783
+ const chunks2 = [];
784
+ for (let index = 0; index < body2.length; index += bodyCapacity2) {
785
+ chunks2.push([...prefix, ...body2.slice(index, index + bodyCapacity2)]);
786
+ }
787
+ return chunks2;
788
+ }
789
+ const body = hunk.slice(1);
790
+ const bodyCapacity = maxLines - header.length - 1;
791
+ if (bodyCapacity <= 0) {
792
+ return splitRawLines([...header, hunk[0], ...body], maxLines);
793
+ }
794
+ if (body.length === 0) {
795
+ return [[...header, hunk[0]]];
796
+ }
797
+ const chunks = [];
798
+ let oldCursor = parsedHeader.oldStart;
799
+ let newCursor = parsedHeader.newStart;
800
+ let oldAnchor = initialHunkAnchor(parsedHeader.oldStart, parsedHeader.oldCount);
801
+ let newAnchor = initialHunkAnchor(parsedHeader.newStart, parsedHeader.newCount);
802
+ for (let index = 0; index < body.length; index += bodyCapacity) {
803
+ const bodySlice = body.slice(index, index + bodyCapacity);
804
+ const oldCount = countHunkLines(bodySlice, "old");
805
+ const newCount = countHunkLines(bodySlice, "new");
806
+ const hunkHeader = formatHunkHeader(parsedHeader, {
807
+ oldStart: hunkRangeStart(oldCursor, oldAnchor, oldCount),
808
+ oldCount,
809
+ newStart: hunkRangeStart(newCursor, newAnchor, newCount),
810
+ newCount
811
+ });
812
+ chunks.push([...header, hunkHeader, ...bodySlice]);
813
+ if (oldCount > 0) {
814
+ oldAnchor = oldCursor + oldCount - 1;
815
+ oldCursor += oldCount;
816
+ }
817
+ if (newCount > 0) {
818
+ newAnchor = newCursor + newCount - 1;
819
+ newCursor += newCount;
820
+ }
821
+ }
822
+ return chunks;
823
+ }
824
+ function parseHunkHeader(line) {
825
+ const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/.exec(
826
+ line ?? ""
827
+ );
828
+ if (!match) {
829
+ return void 0;
830
+ }
831
+ return {
832
+ oldStart: Number(match[1]),
833
+ oldCount: match[2] === void 0 ? 1 : Number(match[2]),
834
+ newStart: Number(match[3]),
835
+ newCount: match[4] === void 0 ? 1 : Number(match[4]),
836
+ section: match[5]
837
+ };
838
+ }
839
+ function formatHunkHeader(parsedHeader, range) {
840
+ return [
841
+ `@@ -${range.oldStart},${range.oldCount}`,
842
+ `+${range.newStart},${range.newCount}`,
843
+ `@@${parsedHeader.section}`
844
+ ].join(" ");
845
+ }
846
+ function initialHunkAnchor(start, count) {
847
+ return count === 0 ? start : Math.max(0, start - 1);
848
+ }
849
+ function hunkRangeStart(cursor, anchor, count) {
850
+ return count === 0 ? anchor : cursor;
851
+ }
852
+ function countHunkLines(lines, side) {
853
+ return lines.filter((line) => {
854
+ const prefix = line[0];
855
+ if (prefix === " ") {
856
+ return true;
857
+ }
858
+ return side === "old" ? prefix === "-" : prefix === "+";
859
+ }).length;
860
+ }
861
+ function splitRawLines(lines, maxLines) {
862
+ const chunks = [];
863
+ for (let index = 0; index < lines.length; index += maxLines) {
864
+ chunks.push(lines.slice(index, index + maxLines));
865
+ }
866
+ return chunks;
867
+ }
868
+ function lineCount(text) {
869
+ return text.length === 0 ? 0 : text.split("\n").length;
870
+ }
443
871
  async function runFixRound(input) {
444
872
  let report = input.report;
445
873
  let fixedCount = 0;
@@ -450,6 +878,7 @@ async function runFixRound(input) {
450
878
  ...input,
451
879
  ctx: {
452
880
  ...input.ctx,
881
+ diff: attempt.diff,
453
882
  reportMarkdown: report
454
883
  },
455
884
  attempt,
@@ -610,129 +1039,23 @@ function validateCommitMessage(message) {
610
1039
  reason: "commit messages must not contain NUL bytes"
611
1040
  };
612
1041
  }
613
- const forbiddenReason = forbiddenCommitMessageReason(normalized);
614
- if (forbiddenReason) {
615
- return {
616
- valid: false,
617
- reason: forbiddenReason
618
- };
619
- }
620
- const lines = normalized.split("\n");
621
- if (lines.length !== 4) {
622
- return {
623
- valid: false,
624
- reason: "commit messages must contain a subject, a blank line, and exactly two bullet lines"
625
- };
626
- }
627
- if (lines[1] !== "") {
628
- return {
629
- valid: false,
630
- reason: "commit messages must separate the subject from the body with a blank line"
631
- };
632
- }
633
- const subject = parseCommitSubject(lines[0]);
634
- if (!subject) {
635
- return {
636
- valid: false,
637
- reason: "commit message subjects must match <type>(<scope>): <short imperative summary>"
638
- };
639
- }
640
- if (subject.summary.endsWith(".")) {
1042
+ const sanitized = removeCoAuthorLines(normalized).trim();
1043
+ if (!sanitized) {
641
1044
  return {
642
1045
  valid: false,
643
- reason: "commit message summaries must not end with a period"
644
- };
645
- }
646
- if (looksNonImperative(subject.summary)) {
647
- return {
648
- valid: false,
649
- reason: "commit message summaries must be imperative"
650
- };
651
- }
652
- if (!lines[2].startsWith("- ") || lines[2].trim() === "-") {
653
- return {
654
- valid: false,
655
- reason: "commit message bodies must use a non-empty first bullet"
656
- };
657
- }
658
- if (!lines[3].startsWith("- ") || lines[3].trim() === "-") {
659
- return {
660
- valid: false,
661
- reason: "commit message bodies must use a non-empty second bullet"
1046
+ reason: "the fixer did not provide a commit message after unsupported trailers were removed"
662
1047
  };
663
1048
  }
664
1049
  return {
665
1050
  valid: true,
666
- message: normalized
1051
+ message: sanitized
667
1052
  };
668
1053
  }
669
1054
  function invalidCommitMessageSummary(reason) {
670
1055
  return `The fixer reported this issue as fixed, but its commit message is invalid: ${reason}. Manual follow-up is required before Mendr can safely record and push the fix.`;
671
1056
  }
672
- function forbiddenCommitMessageReason(message) {
673
- const lines = message.split("\n");
674
- if (lines.some((line) => /^co-authored-by\s*:/i.test(line.trim()))) {
675
- return "commit messages must not include co-author lines";
676
- }
677
- if (/\b(?:ai|a\.i\.|openai|chatgpt|claude|anthropic|codex|providers?)\b/i.test(message)) {
678
- return "commit messages must not include AI or provider references";
679
- }
680
- return void 0;
681
- }
682
- function parseCommitSubject(line) {
683
- const match = /^([a-z][a-z0-9-]*)\(([a-z0-9._/-]+)\): ([A-Za-z][^\n]*)$/.exec(line);
684
- if (!match) {
685
- return void 0;
686
- }
687
- const [, type, scope, summary] = match;
688
- if (!summary.trim()) {
689
- return void 0;
690
- }
691
- return {
692
- type,
693
- scope,
694
- summary
695
- };
696
- }
697
- function looksNonImperative(summary) {
698
- const firstWord = summary.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
699
- return [
700
- "added",
701
- "adding",
702
- "adds",
703
- "changed",
704
- "changing",
705
- "changes",
706
- "created",
707
- "creating",
708
- "creates",
709
- "fixed",
710
- "fixing",
711
- "fixes",
712
- "handled",
713
- "handling",
714
- "handles",
715
- "made",
716
- "makes",
717
- "prevented",
718
- "preventing",
719
- "prevents",
720
- "recorded",
721
- "recording",
722
- "records",
723
- "rejected",
724
- "rejecting",
725
- "rejects",
726
- "updated",
727
- "updating",
728
- "updates",
729
- "used",
730
- "using",
731
- "uses",
732
- "validated",
733
- "validating",
734
- "validates"
735
- ].includes(firstWord);
1057
+ function removeCoAuthorLines(message) {
1058
+ return message.split("\n").filter((line) => !/^co-authored-by\s*:/i.test(line.trim())).join("\n");
736
1059
  }
737
1060
  async function pushWithRetry(exec, repo, remote, branch) {
738
1061
  try {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@pepps233/mendr",
3
- "version": "0.1.0",
3
+ "version": "0.3.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"