@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 +3 -0
- package/dist/{chunk-EGSZLVR6.js → chunk-753PEKBT.js} +5 -16
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +300 -7
- package/dist/daemon.js +228 -118
- package/package.json +1 -1
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
|

|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
package/dist/cli.js
CHANGED
|
@@ -26,17 +26,298 @@ import {
|
|
|
26
26
|
worktreesDir,
|
|
27
27
|
writeMeta,
|
|
28
28
|
writeState
|
|
29
|
-
} from "./chunk-
|
|
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
|
|
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 =
|
|
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
|
|
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-
|
|
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
|
|
320
|
+
let reviewedIssues = [];
|
|
321
321
|
try {
|
|
322
|
-
|
|
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 =
|
|
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
|
|
614
|
-
if (
|
|
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: "
|
|
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:
|
|
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
|
|
673
|
-
|
|
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 {
|