@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 +3 -0
- package/dist/{chunk-EGSZLVR6.js → chunk-3EH6IS7Z.js} +126 -53
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +300 -7
- package/dist/daemon.js +441 -118
- package/package.json +2 -2
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
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-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
|
|
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-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
|
|
377
|
+
let reviewedIssues = [];
|
|
321
378
|
try {
|
|
322
|
-
|
|
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 =
|
|
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
|
|
614
|
-
if (
|
|
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
|
|
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:
|
|
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
|
|
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);
|
|
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