@pepps233/mendr 0.1.0 → 0.2.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
 
@@ -153,19 +153,6 @@ function issueFingerprint(issue) {
153
153
  normalizeFingerprintPart(issue.description)
154
154
  ].join("|");
155
155
  }
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
156
  function parseIssue(value) {
170
157
  if (!isRecord(value)) {
171
158
  throw new AgentParseError("Agent issue must be an object.");
@@ -431,9 +418,10 @@ function buildCodexReviewInvocation(ctx, options) {
431
418
  const prompt = buildReviewPrompt(ctx);
432
419
  return {
433
420
  command: "codex",
421
+ input: prompt,
434
422
  args: [
435
423
  "exec",
436
- prompt,
424
+ "-",
437
425
  "-m",
438
426
  ctx.model,
439
427
  "-c",
@@ -452,9 +440,10 @@ function buildCodexFixInvocation(issues, ctx, options) {
452
440
  const prompt = buildFixPrompt(issues, ctx);
453
441
  return {
454
442
  command: "codex",
443
+ input: prompt,
455
444
  args: [
456
445
  "exec",
457
- prompt,
446
+ "-",
458
447
  "-m",
459
448
  ctx.model,
460
449
  "-c",
@@ -600,6 +589,7 @@ async function runAgentInvocation(exec, invocation, options) {
600
589
  const stderrFile = join(options.outputDir, `${options.label}.stderr.log`);
601
590
  const result = await exec(invocation.command, invocation.args, {
602
591
  cwd: options.cwd,
592
+ input: invocation.input,
603
593
  timeoutMs: agentTimeoutMs(),
604
594
  stdoutFile,
605
595
  stderrFile
@@ -1004,7 +994,6 @@ async function readJson(home, id, fileName) {
1004
994
 
1005
995
  export {
1006
996
  issueFingerprint,
1007
- dedupeIssues,
1008
997
  defaultExec,
1009
998
  execOk,
1010
999
  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-753PEKBT.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-753PEKBT.js";
33
32
 
34
33
  // src/orchestrator.ts
35
34
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -266,6 +265,7 @@ var PullRequestHeadChangedError = class extends Error {
266
265
  };
267
266
  var PR_HEAD_PROPAGATION_TIMEOUT_MS = 3e4;
268
267
  var PR_HEAD_PROPAGATION_POLL_MS = 2e3;
268
+ var MAX_REVIEW_DIFF_LINES = 4e3;
269
269
  async function runOrchestrator(options) {
270
270
  const exec = options.exec ?? defaultExec;
271
271
  const dir = reviewDir(options.mendrHome, options.reviewId);
@@ -317,13 +317,14 @@ async function runOrchestrator(options) {
317
317
  reviewMarkdown,
318
318
  reportMarkdown: report
319
319
  });
320
- let issues = [];
320
+ let reviewedIssues = [];
321
321
  try {
322
- issues = dedupeIssues(await agentDriver.review(ctx));
322
+ reviewedIssues = await reviewDiffChunks(agentDriver, ctx);
323
323
  } catch (error) {
324
324
  await fail(options, state, "Review failed", error);
325
325
  return;
326
326
  }
327
+ const issues = reviewedIssues.map((entry) => entry.issue);
327
328
  await persistIssueRecords(options, round, issues);
328
329
  state = await updateStatus(options, state, {
329
330
  issuesFound: state.issuesFound + issues.length
@@ -337,8 +338,9 @@ async function runOrchestrator(options) {
337
338
  break;
338
339
  }
339
340
  openIssues = issues;
340
- const issueAttempts = issues.map((issue, index) => ({
341
- issue,
341
+ const issueAttempts = reviewedIssues.map((entry, index) => ({
342
+ issue: entry.issue,
343
+ diff: entry.diff,
342
344
  round,
343
345
  issueIndex: index + 1
344
346
  }));
@@ -440,6 +442,219 @@ async function runOrchestrator(options) {
440
442
  await fail(options, state, "Orchestrator failed", error);
441
443
  }
442
444
  }
445
+ async function reviewDiffChunks(agentDriver, ctx) {
446
+ const reviewedIssues = [];
447
+ for (const diff of splitDiffForReview(ctx.diff)) {
448
+ const issues = await agentDriver.review({
449
+ ...ctx,
450
+ diff
451
+ });
452
+ for (const issue of issues) {
453
+ reviewedIssues.push({
454
+ issue,
455
+ diff
456
+ });
457
+ }
458
+ }
459
+ return dedupeReviewedIssues(reviewedIssues);
460
+ }
461
+ function dedupeReviewedIssues(reviewedIssues) {
462
+ const seen = /* @__PURE__ */ new Set();
463
+ const deduped = [];
464
+ for (const entry of reviewedIssues) {
465
+ const fingerprint = issueFingerprint(entry.issue);
466
+ if (seen.has(fingerprint)) {
467
+ continue;
468
+ }
469
+ seen.add(fingerprint);
470
+ deduped.push(entry);
471
+ }
472
+ return deduped;
473
+ }
474
+ function splitDiffForReview(diff, maxLines = MAX_REVIEW_DIFF_LINES) {
475
+ const normalized = diff.replace(/\r\n?/g, "\n");
476
+ if (lineCount(normalized) <= maxLines) {
477
+ return [normalized];
478
+ }
479
+ const chunks = [];
480
+ let currentChunk = [];
481
+ for (const fileBlock of splitDiffFileBlocks(normalized.split("\n"))) {
482
+ const boundedBlocks = fileBlock.length > maxLines ? splitOversizedFileBlock(fileBlock, maxLines) : [fileBlock];
483
+ for (const block of boundedBlocks) {
484
+ for (const boundedBlock of splitRawLines(block, maxLines)) {
485
+ if (currentChunk.length > 0 && currentChunk.length + boundedBlock.length > maxLines) {
486
+ chunks.push(currentChunk);
487
+ currentChunk = [];
488
+ }
489
+ currentChunk.push(...boundedBlock);
490
+ }
491
+ }
492
+ }
493
+ if (currentChunk.length > 0) {
494
+ chunks.push(currentChunk);
495
+ }
496
+ return chunks.map((chunk) => chunk.join("\n"));
497
+ }
498
+ function splitDiffFileBlocks(lines) {
499
+ const blocks = [];
500
+ let currentBlock = [];
501
+ for (const line of lines) {
502
+ if (line.startsWith("diff --git ") && currentBlock.length > 0) {
503
+ blocks.push(currentBlock);
504
+ currentBlock = [];
505
+ }
506
+ currentBlock.push(line);
507
+ }
508
+ if (currentBlock.length > 0) {
509
+ blocks.push(currentBlock);
510
+ }
511
+ return blocks;
512
+ }
513
+ function splitOversizedFileBlock(block, maxLines) {
514
+ const firstHunkIndex = block.findIndex((line) => line.startsWith("@@"));
515
+ if (firstHunkIndex === -1) {
516
+ return splitRawLines(block, maxLines);
517
+ }
518
+ const header = block.slice(0, firstHunkIndex);
519
+ const hunks = splitHunks(block.slice(firstHunkIndex));
520
+ const chunks = [];
521
+ let currentChunk = [...header];
522
+ for (const hunk of hunks) {
523
+ if (header.length + hunk.length > maxLines) {
524
+ if (currentChunk.length > header.length) {
525
+ chunks.push(currentChunk);
526
+ currentChunk = [...header];
527
+ }
528
+ chunks.push(...splitOversizedHunk(header, hunk, maxLines));
529
+ continue;
530
+ }
531
+ if (currentChunk.length + hunk.length > maxLines) {
532
+ chunks.push(currentChunk);
533
+ currentChunk = [...header];
534
+ }
535
+ currentChunk.push(...hunk);
536
+ }
537
+ if (currentChunk.length > header.length) {
538
+ chunks.push(currentChunk);
539
+ }
540
+ return chunks;
541
+ }
542
+ function splitHunks(lines) {
543
+ const hunks = [];
544
+ let currentHunk = [];
545
+ for (const line of lines) {
546
+ if (line.startsWith("@@") && currentHunk.length > 0) {
547
+ hunks.push(currentHunk);
548
+ currentHunk = [];
549
+ }
550
+ currentHunk.push(line);
551
+ }
552
+ if (currentHunk.length > 0) {
553
+ hunks.push(currentHunk);
554
+ }
555
+ return hunks;
556
+ }
557
+ function splitOversizedHunk(header, hunk, maxLines) {
558
+ const parsedHeader = parseHunkHeader(hunk[0]);
559
+ if (!parsedHeader) {
560
+ const hunkHeader = hunk[0]?.startsWith("@@") ? [hunk[0]] : [];
561
+ const body2 = hunkHeader.length > 0 ? hunk.slice(1) : hunk;
562
+ const prefix = [...header, ...hunkHeader];
563
+ const bodyCapacity2 = maxLines - prefix.length;
564
+ if (bodyCapacity2 <= 0) {
565
+ return splitRawLines([...prefix, ...body2], maxLines);
566
+ }
567
+ if (body2.length === 0) {
568
+ return [prefix];
569
+ }
570
+ const chunks2 = [];
571
+ for (let index = 0; index < body2.length; index += bodyCapacity2) {
572
+ chunks2.push([...prefix, ...body2.slice(index, index + bodyCapacity2)]);
573
+ }
574
+ return chunks2;
575
+ }
576
+ const body = hunk.slice(1);
577
+ const bodyCapacity = maxLines - header.length - 1;
578
+ if (bodyCapacity <= 0) {
579
+ return splitRawLines([...header, hunk[0], ...body], maxLines);
580
+ }
581
+ if (body.length === 0) {
582
+ return [[...header, hunk[0]]];
583
+ }
584
+ const chunks = [];
585
+ let oldCursor = parsedHeader.oldStart;
586
+ let newCursor = parsedHeader.newStart;
587
+ let oldAnchor = initialHunkAnchor(parsedHeader.oldStart, parsedHeader.oldCount);
588
+ let newAnchor = initialHunkAnchor(parsedHeader.newStart, parsedHeader.newCount);
589
+ for (let index = 0; index < body.length; index += bodyCapacity) {
590
+ const bodySlice = body.slice(index, index + bodyCapacity);
591
+ const oldCount = countHunkLines(bodySlice, "old");
592
+ const newCount = countHunkLines(bodySlice, "new");
593
+ const hunkHeader = formatHunkHeader(parsedHeader, {
594
+ oldStart: hunkRangeStart(oldCursor, oldAnchor, oldCount),
595
+ oldCount,
596
+ newStart: hunkRangeStart(newCursor, newAnchor, newCount),
597
+ newCount
598
+ });
599
+ chunks.push([...header, hunkHeader, ...bodySlice]);
600
+ if (oldCount > 0) {
601
+ oldAnchor = oldCursor + oldCount - 1;
602
+ oldCursor += oldCount;
603
+ }
604
+ if (newCount > 0) {
605
+ newAnchor = newCursor + newCount - 1;
606
+ newCursor += newCount;
607
+ }
608
+ }
609
+ return chunks;
610
+ }
611
+ function parseHunkHeader(line) {
612
+ const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/.exec(
613
+ line ?? ""
614
+ );
615
+ if (!match) {
616
+ return void 0;
617
+ }
618
+ return {
619
+ oldStart: Number(match[1]),
620
+ oldCount: match[2] === void 0 ? 1 : Number(match[2]),
621
+ newStart: Number(match[3]),
622
+ newCount: match[4] === void 0 ? 1 : Number(match[4]),
623
+ section: match[5]
624
+ };
625
+ }
626
+ function formatHunkHeader(parsedHeader, range) {
627
+ return [
628
+ `@@ -${range.oldStart},${range.oldCount}`,
629
+ `+${range.newStart},${range.newCount}`,
630
+ `@@${parsedHeader.section}`
631
+ ].join(" ");
632
+ }
633
+ function initialHunkAnchor(start, count) {
634
+ return count === 0 ? start : Math.max(0, start - 1);
635
+ }
636
+ function hunkRangeStart(cursor, anchor, count) {
637
+ return count === 0 ? anchor : cursor;
638
+ }
639
+ function countHunkLines(lines, side) {
640
+ return lines.filter((line) => {
641
+ const prefix = line[0];
642
+ if (prefix === " ") {
643
+ return true;
644
+ }
645
+ return side === "old" ? prefix === "-" : prefix === "+";
646
+ }).length;
647
+ }
648
+ function splitRawLines(lines, maxLines) {
649
+ const chunks = [];
650
+ for (let index = 0; index < lines.length; index += maxLines) {
651
+ chunks.push(lines.slice(index, index + maxLines));
652
+ }
653
+ return chunks;
654
+ }
655
+ function lineCount(text) {
656
+ return text.length === 0 ? 0 : text.split("\n").length;
657
+ }
443
658
  async function runFixRound(input) {
444
659
  let report = input.report;
445
660
  let fixedCount = 0;
@@ -450,6 +665,7 @@ async function runFixRound(input) {
450
665
  ...input,
451
666
  ctx: {
452
667
  ...input.ctx,
668
+ diff: attempt.diff,
453
669
  reportMarkdown: report
454
670
  },
455
671
  attempt,
@@ -610,129 +826,23 @@ function validateCommitMessage(message) {
610
826
  reason: "commit messages must not contain NUL bytes"
611
827
  };
612
828
  }
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) {
829
+ const sanitized = removeCoAuthorLines(normalized).trim();
830
+ if (!sanitized) {
622
831
  return {
623
832
  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(".")) {
641
- return {
642
- 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"
833
+ reason: "the fixer did not provide a commit message after unsupported trailers were removed"
662
834
  };
663
835
  }
664
836
  return {
665
837
  valid: true,
666
- message: normalized
838
+ message: sanitized
667
839
  };
668
840
  }
669
841
  function invalidCommitMessageSummary(reason) {
670
842
  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
843
  }
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);
844
+ function removeCoAuthorLines(message) {
845
+ return message.split("\n").filter((line) => !/^co-authored-by\s*:/i.test(line.trim())).join("\n");
736
846
  }
737
847
  async function pushWithRetry(exec, repo, remote, branch) {
738
848
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pepps233/mendr",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Autonomous pull request review agent CLI.",
5
5
  "type": "module",
6
6
  "bin": {