@pepps233/mendr 0.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/chunk-EGSZLVR6.js +1051 -0
- package/dist/cli.d.ts +113 -0
- package/dist/cli.js +718 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +1060 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
// src/exec.ts
|
|
2
|
+
import { createWriteStream } from "fs";
|
|
3
|
+
import { mkdir } from "fs/promises";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
var CommandFailedError = class extends Error {
|
|
7
|
+
constructor(command, args, result) {
|
|
8
|
+
super(formatCommandFailure(command, args, result));
|
|
9
|
+
this.command = command;
|
|
10
|
+
this.args = args;
|
|
11
|
+
this.result = result;
|
|
12
|
+
this.name = "CommandFailedError";
|
|
13
|
+
}
|
|
14
|
+
command;
|
|
15
|
+
args;
|
|
16
|
+
result;
|
|
17
|
+
};
|
|
18
|
+
var defaultExec = async (command, args, options = {}) => {
|
|
19
|
+
const logStreams = await createLogStreams(options);
|
|
20
|
+
const subprocess = execa(command, args, {
|
|
21
|
+
cwd: options.cwd,
|
|
22
|
+
env: options.env,
|
|
23
|
+
input: options.input,
|
|
24
|
+
stdin: options.input === void 0 ? "ignore" : "pipe",
|
|
25
|
+
timeout: options.timeoutMs,
|
|
26
|
+
reject: false
|
|
27
|
+
});
|
|
28
|
+
teeStream(subprocess.stdout, logStreams.stdout);
|
|
29
|
+
teeStream(subprocess.stderr, logStreams.stderr);
|
|
30
|
+
try {
|
|
31
|
+
const result = await subprocess;
|
|
32
|
+
return {
|
|
33
|
+
stdout: result.stdout,
|
|
34
|
+
stderr: result.stderr,
|
|
35
|
+
exitCode: result.exitCode ?? (result.timedOut ? 124 : 0),
|
|
36
|
+
timedOut: result.timedOut
|
|
37
|
+
};
|
|
38
|
+
} finally {
|
|
39
|
+
await closeLogStreams(logStreams);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
async function execOk(exec, command, args, options) {
|
|
43
|
+
const result = await exec(command, args, options);
|
|
44
|
+
if (result.exitCode !== 0) {
|
|
45
|
+
throw new CommandFailedError(command, args, result);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function formatCommandFailure(command, args, result) {
|
|
50
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
|
|
51
|
+
const formattedCommand = [command, ...args.map(formatCommandArg)].join(" ");
|
|
52
|
+
if (result.timedOut) {
|
|
53
|
+
return `${formattedCommand} timed out: ${detail}`;
|
|
54
|
+
}
|
|
55
|
+
return `${formattedCommand} failed: ${detail}`;
|
|
56
|
+
}
|
|
57
|
+
function formatCommandArg(arg) {
|
|
58
|
+
const normalized = arg.replace(/\s+/g, " ");
|
|
59
|
+
return normalized.length > 160 ? `${normalized.slice(0, 157)}...` : normalized;
|
|
60
|
+
}
|
|
61
|
+
async function createLogStreams(options) {
|
|
62
|
+
await Promise.all(
|
|
63
|
+
[options.stdoutFile, options.stderrFile].filter((path) => path !== void 0).map((path) => mkdir(dirname(path), { recursive: true }))
|
|
64
|
+
);
|
|
65
|
+
return {
|
|
66
|
+
stdout: options.stdoutFile ? createWriteStream(options.stdoutFile, { flags: "w" }) : void 0,
|
|
67
|
+
stderr: options.stderrFile ? createWriteStream(options.stderrFile, { flags: "w" }) : void 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function teeStream(source, destination) {
|
|
71
|
+
if (!source || !destination) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
source.on("data", (chunk) => {
|
|
75
|
+
destination.write(chunk);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function closeLogStreams(streams) {
|
|
79
|
+
await Promise.all([closeLogStream(streams.stdout), closeLogStream(streams.stderr)]);
|
|
80
|
+
}
|
|
81
|
+
async function closeLogStream(stream) {
|
|
82
|
+
if (!stream) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
stream.once("error", reject);
|
|
87
|
+
stream.end(() => {
|
|
88
|
+
stream.off("error", reject);
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/agents/driver.ts
|
|
95
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
96
|
+
import { join } from "path";
|
|
97
|
+
|
|
98
|
+
// src/agents/types.ts
|
|
99
|
+
var AgentParseError = class extends Error {
|
|
100
|
+
constructor(message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "AgentParseError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
function extractJsonValue(text) {
|
|
106
|
+
const trimmed = text.trim();
|
|
107
|
+
if (trimmed.length === 0) {
|
|
108
|
+
throw new AgentParseError("Agent output was empty.");
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(trimmed);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
for (const match of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
|
|
115
|
+
const candidate2 = match[1]?.trim();
|
|
116
|
+
if (!candidate2) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(candidate2);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const candidate = findFirstJsonCandidate(trimmed);
|
|
125
|
+
if (candidate) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(candidate);
|
|
128
|
+
} catch {
|
|
129
|
+
throw new AgentParseError("Agent output contained malformed JSON.");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw new AgentParseError("Agent output did not contain JSON.");
|
|
133
|
+
}
|
|
134
|
+
function parseIssueArrayFromText(text) {
|
|
135
|
+
const value = extractJsonValue(text);
|
|
136
|
+
if (!Array.isArray(value)) {
|
|
137
|
+
throw new AgentParseError("Agent JSON payload must be an issue array.");
|
|
138
|
+
}
|
|
139
|
+
return value.map(parseIssue);
|
|
140
|
+
}
|
|
141
|
+
function parseFixIssueResultArrayFromText(text) {
|
|
142
|
+
const value = extractJsonValue(text);
|
|
143
|
+
if (!Array.isArray(value)) {
|
|
144
|
+
throw new AgentParseError("Agent JSON payload must be a fix result array.");
|
|
145
|
+
}
|
|
146
|
+
return value.map(parseFixIssueResult);
|
|
147
|
+
}
|
|
148
|
+
function issueFingerprint(issue) {
|
|
149
|
+
return [
|
|
150
|
+
normalizeFingerprintPart(issue.title),
|
|
151
|
+
normalizeFingerprintPart(issue.file),
|
|
152
|
+
String(issue.line),
|
|
153
|
+
normalizeFingerprintPart(issue.description)
|
|
154
|
+
].join("|");
|
|
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
|
+
function parseIssue(value) {
|
|
170
|
+
if (!isRecord(value)) {
|
|
171
|
+
throw new AgentParseError("Agent issue must be an object.");
|
|
172
|
+
}
|
|
173
|
+
const { title, file, line, severity, description } = value;
|
|
174
|
+
if (typeof title !== "string" || typeof file !== "string" || typeof line !== "number" || !Number.isInteger(line) || typeof severity !== "string" || typeof description !== "string") {
|
|
175
|
+
throw new AgentParseError("Agent issue has an invalid schema.");
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
title,
|
|
179
|
+
file,
|
|
180
|
+
line,
|
|
181
|
+
severity,
|
|
182
|
+
description
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function parseFixIssueResult(value) {
|
|
186
|
+
if (!isRecord(value)) {
|
|
187
|
+
throw new AgentParseError("Agent fix result must be an object.");
|
|
188
|
+
}
|
|
189
|
+
const { title, fingerprint, status, summary } = value;
|
|
190
|
+
const sha = readOptionalString(value, "sha") ?? readOptionalString(value, "commitSha");
|
|
191
|
+
const commitMessage = readOptionalString(value, "commitMessage");
|
|
192
|
+
if (typeof title !== "string" || typeof fingerprint !== "string" || status !== "fixed" && status !== "failed" || typeof summary !== "string") {
|
|
193
|
+
throw new AgentParseError("Agent fix result has an invalid schema.");
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
title,
|
|
197
|
+
fingerprint,
|
|
198
|
+
status,
|
|
199
|
+
sha,
|
|
200
|
+
commitMessage,
|
|
201
|
+
summary
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function readOptionalString(value, key) {
|
|
205
|
+
const raw = value[key];
|
|
206
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw : void 0;
|
|
207
|
+
}
|
|
208
|
+
function normalizeFingerprintPart(value) {
|
|
209
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
function findFirstJsonCandidate(text) {
|
|
212
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
213
|
+
const char = text[index];
|
|
214
|
+
if (char !== "[" && char !== "{") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const end = findJsonEnd(text, index, char === "[" ? "]" : "}");
|
|
218
|
+
if (end !== void 0) {
|
|
219
|
+
return text.slice(index, end + 1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
function findJsonEnd(text, start, expectedClose) {
|
|
225
|
+
const stack = [expectedClose];
|
|
226
|
+
let inString = false;
|
|
227
|
+
let escaped = false;
|
|
228
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
229
|
+
const char = text[index];
|
|
230
|
+
if (inString) {
|
|
231
|
+
if (escaped) {
|
|
232
|
+
escaped = false;
|
|
233
|
+
} else if (char === "\\") {
|
|
234
|
+
escaped = true;
|
|
235
|
+
} else if (char === '"') {
|
|
236
|
+
inString = false;
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (char === '"') {
|
|
241
|
+
inString = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (char === "[") {
|
|
245
|
+
stack.push("]");
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (char === "{") {
|
|
249
|
+
stack.push("}");
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (char === "]" || char === "}") {
|
|
253
|
+
if (stack.at(-1) !== char) {
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
stack.pop();
|
|
257
|
+
if (stack.length === 0) {
|
|
258
|
+
return index;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
function isRecord(value) {
|
|
265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/agents/prompts.ts
|
|
269
|
+
var issueSchema = '[{"title":"specific standalone title","file":"path","line":1,"severity":"low|medium|high|critical","description":"two concise sentences describing the finding"}]';
|
|
270
|
+
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"}]';
|
|
271
|
+
function buildReviewSystemPrompt() {
|
|
272
|
+
return [
|
|
273
|
+
"You are the REVIEW agent in the pull request review loop.",
|
|
274
|
+
"Report only issues that are strictly inside the provided PR diff scope.",
|
|
275
|
+
"Respond only with JSON matching the requested issue schema."
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
function buildFixSystemPrompt() {
|
|
279
|
+
return [
|
|
280
|
+
"You are the FIX agent in the pull request review loop.",
|
|
281
|
+
"Fix only the supplied issue and stay inside the changed PR scope.",
|
|
282
|
+
"Do not create commits or push changes.",
|
|
283
|
+
"Respond only with JSON matching the requested fix-result schema."
|
|
284
|
+
].join("\n");
|
|
285
|
+
}
|
|
286
|
+
function buildReviewPrompt(ctx) {
|
|
287
|
+
return [
|
|
288
|
+
"You are a code review agent for a GitHub pull request.",
|
|
289
|
+
"Review only changes in the provided PR diff.",
|
|
290
|
+
"Do not report issues outside the changed scope.",
|
|
291
|
+
"Look for security issues, correctness issues, maintainability issues, and unnecessary redundancies.",
|
|
292
|
+
"Return an empty JSON array when there are no changed-scope issues.",
|
|
293
|
+
"Use issue titles that are specific enough to stand alone in the final summary.",
|
|
294
|
+
"Use exactly two concise sentences for each issue description.",
|
|
295
|
+
"respond ONLY with JSON matching this schema:",
|
|
296
|
+
issueSchema,
|
|
297
|
+
"",
|
|
298
|
+
`Review PR ${ctx.pr}.`,
|
|
299
|
+
"",
|
|
300
|
+
"PR review.md:",
|
|
301
|
+
ctx.reviewMarkdown,
|
|
302
|
+
"",
|
|
303
|
+
"Current report.md:",
|
|
304
|
+
ctx.reportMarkdown,
|
|
305
|
+
"",
|
|
306
|
+
"PR diff:",
|
|
307
|
+
ctx.diff
|
|
308
|
+
].join("\n");
|
|
309
|
+
}
|
|
310
|
+
function buildFixPrompt(issues, ctx) {
|
|
311
|
+
const issuePayload = issues.map((issue) => ({
|
|
312
|
+
...issue,
|
|
313
|
+
fingerprint: issueFingerprint(issue)
|
|
314
|
+
}));
|
|
315
|
+
return [
|
|
316
|
+
"You are a code fixer agent for a GitHub pull request.",
|
|
317
|
+
"Fix only the single issue listed below and stay inside the changed PR scope.",
|
|
318
|
+
"Use one fresh session to fix this issue.",
|
|
319
|
+
"Do not create commits, push changes, or include commit SHAs in the result.",
|
|
320
|
+
"For fixed issues, write the exact commitMessage mendr should use after your process exits.",
|
|
321
|
+
"The commitMessage must describe what the commit changed, not restate the reviewed issue.",
|
|
322
|
+
"Commit messages must use exactly this format:",
|
|
323
|
+
"<type>(<scope>): <short imperative summary>",
|
|
324
|
+
"",
|
|
325
|
+
"- <why this change was needed>",
|
|
326
|
+
"- <why this approach or impact matters>",
|
|
327
|
+
"",
|
|
328
|
+
"Commit-message summaries must be imperative and must not end with a period.",
|
|
329
|
+
"Do not include co-author lines, AI references, provider references, or non-imperative summaries.",
|
|
330
|
+
"mendr will stage, commit with your commitMessage, record, and push successful fixes after your process exits.",
|
|
331
|
+
"After fixing, respond ONLY with JSON matching this schema:",
|
|
332
|
+
fixSchema,
|
|
333
|
+
"",
|
|
334
|
+
"The result must include the issue title and fingerprint shown in the issue payload.",
|
|
335
|
+
"For fixed issues, summarize the concrete code changes you made, not the issue title.",
|
|
336
|
+
"For failed issues, set status to failed and explain why in two sentences.",
|
|
337
|
+
"",
|
|
338
|
+
`Fix PR ${ctx.pr}.`,
|
|
339
|
+
"",
|
|
340
|
+
"Issue payload:",
|
|
341
|
+
JSON.stringify(issuePayload, null, 2),
|
|
342
|
+
"",
|
|
343
|
+
"PR review.md:",
|
|
344
|
+
ctx.reviewMarkdown,
|
|
345
|
+
"",
|
|
346
|
+
"Current report.md:",
|
|
347
|
+
ctx.reportMarkdown,
|
|
348
|
+
"",
|
|
349
|
+
"PR diff:",
|
|
350
|
+
ctx.diff
|
|
351
|
+
].join("\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/agents/claude.ts
|
|
355
|
+
function parseClaudeIssues(output) {
|
|
356
|
+
const envelope = extractJsonValue(output);
|
|
357
|
+
if (isClaudeResultEnvelope(envelope)) {
|
|
358
|
+
return parseIssueArrayFromText(envelope.result);
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(envelope)) {
|
|
361
|
+
return parseIssueArrayFromText(JSON.stringify(envelope));
|
|
362
|
+
}
|
|
363
|
+
throw new AgentParseError("Claude output did not include a result payload.");
|
|
364
|
+
}
|
|
365
|
+
function parseClaudeFixResults(output) {
|
|
366
|
+
const envelope = extractJsonValue(output);
|
|
367
|
+
if (isClaudeResultEnvelope(envelope)) {
|
|
368
|
+
return parseFixIssueResultArrayFromText(envelope.result);
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(envelope)) {
|
|
371
|
+
return parseFixIssueResultArrayFromText(JSON.stringify(envelope));
|
|
372
|
+
}
|
|
373
|
+
throw new AgentParseError("Claude output did not include a result payload.");
|
|
374
|
+
}
|
|
375
|
+
function buildClaudeReviewInvocation(ctx) {
|
|
376
|
+
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
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function buildClaudeFixInvocation(issues, ctx) {
|
|
398
|
+
const prompt = buildFixPrompt(issues, ctx);
|
|
399
|
+
return {
|
|
400
|
+
command: "claude",
|
|
401
|
+
args: [
|
|
402
|
+
"-p",
|
|
403
|
+
prompt,
|
|
404
|
+
"--output-format",
|
|
405
|
+
"json",
|
|
406
|
+
"--model",
|
|
407
|
+
ctx.model,
|
|
408
|
+
"--effort",
|
|
409
|
+
ctx.effort,
|
|
410
|
+
"--permission-mode",
|
|
411
|
+
"acceptEdits",
|
|
412
|
+
"--add-dir",
|
|
413
|
+
ctx.repo,
|
|
414
|
+
"--append-system-prompt",
|
|
415
|
+
buildFixSystemPrompt()
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function isClaudeResultEnvelope(value) {
|
|
420
|
+
return typeof value === "object" && value !== null && "result" in value && typeof value.result === "string";
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/agents/codex.ts
|
|
424
|
+
function parseCodexIssues(output) {
|
|
425
|
+
return parseIssueArrayFromText(output);
|
|
426
|
+
}
|
|
427
|
+
function parseCodexFixResults(output) {
|
|
428
|
+
return parseFixIssueResultArrayFromText(output);
|
|
429
|
+
}
|
|
430
|
+
function buildCodexReviewInvocation(ctx, options) {
|
|
431
|
+
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
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function buildCodexFixInvocation(issues, ctx, options) {
|
|
452
|
+
const prompt = buildFixPrompt(issues, ctx);
|
|
453
|
+
return {
|
|
454
|
+
command: "codex",
|
|
455
|
+
args: [
|
|
456
|
+
"exec",
|
|
457
|
+
prompt,
|
|
458
|
+
"-m",
|
|
459
|
+
ctx.model,
|
|
460
|
+
"-c",
|
|
461
|
+
`model_reasoning_effort=${JSON.stringify(ctx.effort)}`,
|
|
462
|
+
"--sandbox",
|
|
463
|
+
"workspace-write",
|
|
464
|
+
"--json",
|
|
465
|
+
"-C",
|
|
466
|
+
ctx.repo,
|
|
467
|
+
"--output-last-message",
|
|
468
|
+
options.outputFile
|
|
469
|
+
]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/agents/driver.ts
|
|
474
|
+
var codexEfforts = ["low", "medium", "high", "xhigh"];
|
|
475
|
+
var claudeEfforts = ["low", "medium", "high", "xhigh", "max"];
|
|
476
|
+
var defaultAgentTimeoutMs = 10 * 60 * 1e3;
|
|
477
|
+
function defaultModelForAgent(agent) {
|
|
478
|
+
if (agent === "codex") {
|
|
479
|
+
return process.env.MENDR_CODEX_MODEL ?? "gpt-5.5";
|
|
480
|
+
}
|
|
481
|
+
return process.env.MENDR_CLAUDE_MODEL ?? "claude-opus-4-8";
|
|
482
|
+
}
|
|
483
|
+
function defaultEffortForAgent(agent) {
|
|
484
|
+
const effort = agent === "codex" ? process.env.MENDR_CODEX_EFFORT : process.env.MENDR_CLAUDE_EFFORT;
|
|
485
|
+
if (effort) {
|
|
486
|
+
if (isEffortForAgent(agent, effort)) {
|
|
487
|
+
return effort;
|
|
488
|
+
}
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Invalid ${agent} effort "${effort}". Expected one of: ${allowedEffortsForAgent(agent).join(", ")}.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return agent === "codex" ? "xhigh" : "high";
|
|
494
|
+
}
|
|
495
|
+
function allowedEffortsForAgent(agent) {
|
|
496
|
+
return agent === "codex" ? codexEfforts : claudeEfforts;
|
|
497
|
+
}
|
|
498
|
+
function isEffortForAgent(agent, effort) {
|
|
499
|
+
return allowedEffortsForAgent(agent).includes(effort);
|
|
500
|
+
}
|
|
501
|
+
function agentTimeoutMs(env = process.env) {
|
|
502
|
+
const rawTimeout = env.MENDR_AGENT_TIMEOUT_MS;
|
|
503
|
+
if (rawTimeout === void 0) {
|
|
504
|
+
return defaultAgentTimeoutMs;
|
|
505
|
+
}
|
|
506
|
+
const timeout = Number(rawTimeout);
|
|
507
|
+
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
508
|
+
throw new Error("Invalid MENDR_AGENT_TIMEOUT_MS. Expected a non-negative integer.");
|
|
509
|
+
}
|
|
510
|
+
return timeout === 0 ? void 0 : timeout;
|
|
511
|
+
}
|
|
512
|
+
function createAgentDriver(options) {
|
|
513
|
+
if (options.agent === "codex") {
|
|
514
|
+
return new CodexAgentDriver(options.exec, options.outputDir);
|
|
515
|
+
}
|
|
516
|
+
return new ClaudeAgentDriver(options.exec, options.outputDir);
|
|
517
|
+
}
|
|
518
|
+
var ClaudeAgentDriver = class {
|
|
519
|
+
constructor(exec, outputDir) {
|
|
520
|
+
this.exec = exec;
|
|
521
|
+
this.outputDir = outputDir;
|
|
522
|
+
}
|
|
523
|
+
exec;
|
|
524
|
+
outputDir;
|
|
525
|
+
outputIndex = 0;
|
|
526
|
+
async review(ctx) {
|
|
527
|
+
const label = this.nextLabel("claude", "review");
|
|
528
|
+
const invocation = buildClaudeReviewInvocation(ctx);
|
|
529
|
+
const result = await runAgentInvocation(this.exec, invocation, {
|
|
530
|
+
cwd: ctx.repo,
|
|
531
|
+
outputDir: this.outputDir,
|
|
532
|
+
label
|
|
533
|
+
});
|
|
534
|
+
return parseClaudeIssues(result.stdout);
|
|
535
|
+
}
|
|
536
|
+
async fix(issues, ctx) {
|
|
537
|
+
const label = this.nextLabel("claude", "fix");
|
|
538
|
+
const invocation = buildClaudeFixInvocation(issues, ctx);
|
|
539
|
+
const result = await runAgentInvocation(this.exec, invocation, {
|
|
540
|
+
cwd: ctx.repo,
|
|
541
|
+
outputDir: this.outputDir,
|
|
542
|
+
label
|
|
543
|
+
});
|
|
544
|
+
return parseClaudeFixResults(result.stdout);
|
|
545
|
+
}
|
|
546
|
+
nextLabel(agent, kind) {
|
|
547
|
+
this.outputIndex += 1;
|
|
548
|
+
return `${agent}-${kind}-${this.outputIndex}`;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
var CodexAgentDriver = class {
|
|
552
|
+
constructor(exec, outputDir) {
|
|
553
|
+
this.exec = exec;
|
|
554
|
+
this.outputDir = outputDir;
|
|
555
|
+
}
|
|
556
|
+
exec;
|
|
557
|
+
outputDir;
|
|
558
|
+
outputIndex = 0;
|
|
559
|
+
async review(ctx) {
|
|
560
|
+
const label = this.nextLabel("codex", "review");
|
|
561
|
+
const outputFile = await this.outputFile(label);
|
|
562
|
+
const invocation = buildCodexReviewInvocation(ctx, { outputFile });
|
|
563
|
+
const result = await runAgentInvocation(this.exec, invocation, {
|
|
564
|
+
cwd: ctx.repo,
|
|
565
|
+
outputDir: this.outputDir,
|
|
566
|
+
label
|
|
567
|
+
});
|
|
568
|
+
const finalMessage = await readFile(outputFile, "utf8");
|
|
569
|
+
await writeAgentIo(this.outputDir, label, result, {
|
|
570
|
+
"final-message.md": finalMessage
|
|
571
|
+
});
|
|
572
|
+
return parseCodexIssues(finalMessage);
|
|
573
|
+
}
|
|
574
|
+
async fix(issues, ctx) {
|
|
575
|
+
const label = this.nextLabel("codex", "fix");
|
|
576
|
+
const outputFile = await this.outputFile(label);
|
|
577
|
+
const invocation = buildCodexFixInvocation(issues, ctx, { outputFile });
|
|
578
|
+
const result = await runAgentInvocation(this.exec, invocation, {
|
|
579
|
+
cwd: ctx.repo,
|
|
580
|
+
outputDir: this.outputDir,
|
|
581
|
+
label
|
|
582
|
+
});
|
|
583
|
+
const finalMessage = await readFile(outputFile, "utf8");
|
|
584
|
+
await writeAgentIo(this.outputDir, label, result, {
|
|
585
|
+
"final-message.md": finalMessage
|
|
586
|
+
});
|
|
587
|
+
return parseCodexFixResults(finalMessage);
|
|
588
|
+
}
|
|
589
|
+
nextLabel(agent, kind) {
|
|
590
|
+
this.outputIndex += 1;
|
|
591
|
+
return `${agent}-${kind}-${this.outputIndex}`;
|
|
592
|
+
}
|
|
593
|
+
async outputFile(label) {
|
|
594
|
+
await mkdir2(this.outputDir, { recursive: true });
|
|
595
|
+
return join(this.outputDir, `${label}.final-message.md`);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
async function runAgentInvocation(exec, invocation, options) {
|
|
599
|
+
const stdoutFile = join(options.outputDir, `${options.label}.stdout.log`);
|
|
600
|
+
const stderrFile = join(options.outputDir, `${options.label}.stderr.log`);
|
|
601
|
+
const result = await exec(invocation.command, invocation.args, {
|
|
602
|
+
cwd: options.cwd,
|
|
603
|
+
timeoutMs: agentTimeoutMs(),
|
|
604
|
+
stdoutFile,
|
|
605
|
+
stderrFile
|
|
606
|
+
});
|
|
607
|
+
await writeAgentIo(options.outputDir, options.label, result);
|
|
608
|
+
if (result.exitCode !== 0) {
|
|
609
|
+
throw new CommandFailedError(invocation.command, invocation.args, result);
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
async function writeAgentIo(outputDir, label, result, extraFiles = {}) {
|
|
614
|
+
await mkdir2(outputDir, { recursive: true });
|
|
615
|
+
await Promise.all([
|
|
616
|
+
writeFile(join(outputDir, `${label}.stdout.log`), result.stdout, "utf8"),
|
|
617
|
+
writeFile(join(outputDir, `${label}.stderr.log`), result.stderr, "utf8"),
|
|
618
|
+
...Object.entries(extraFiles).map(
|
|
619
|
+
([suffix, content]) => writeFile(join(outputDir, `${label}.${suffix}`), content, "utf8")
|
|
620
|
+
)
|
|
621
|
+
]);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/git.ts
|
|
625
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
626
|
+
import { tmpdir } from "os";
|
|
627
|
+
import { join as join2 } from "path";
|
|
628
|
+
async function getRepoRoot(exec, cwd) {
|
|
629
|
+
const result = await execOk(exec, "git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
630
|
+
return result.stdout.trim();
|
|
631
|
+
}
|
|
632
|
+
async function getHeadCommitSha(exec, repo) {
|
|
633
|
+
const result = await execOk(exec, "git", ["rev-parse", "HEAD"], { cwd: repo });
|
|
634
|
+
return result.stdout.trim();
|
|
635
|
+
}
|
|
636
|
+
async function fetchPullRequestHeadRef(exec, repo, pr, ref) {
|
|
637
|
+
await execOk(exec, "git", ["fetch", "origin", `+refs/pull/${pr}/head:${ref}`], {
|
|
638
|
+
cwd: repo
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
async function createDetachedWorktree(exec, repo, worktreePath, ref) {
|
|
642
|
+
await execOk(exec, "git", ["worktree", "add", "--detach", worktreePath, ref], {
|
|
643
|
+
cwd: repo
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async function removeWorktree(exec, repo, worktreePath) {
|
|
647
|
+
await execOk(exec, "git", ["worktree", "remove", "--force", worktreePath], {
|
|
648
|
+
cwd: repo
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async function getPorcelainStatus(exec, repo) {
|
|
652
|
+
const result = await execOk(exec, "git", ["status", "--porcelain"], { cwd: repo });
|
|
653
|
+
return result.stdout.trim();
|
|
654
|
+
}
|
|
655
|
+
async function fetchRemoteBranch(exec, repo, remote, branch) {
|
|
656
|
+
const remoteRef = `refs/remotes/${remote}/${branch}`;
|
|
657
|
+
await execOk(exec, "git", ["fetch", remote, `+refs/heads/${branch}:${remoteRef}`], {
|
|
658
|
+
cwd: repo
|
|
659
|
+
});
|
|
660
|
+
return remoteRef;
|
|
661
|
+
}
|
|
662
|
+
async function ensureMergeableWithRef(exec, repo, baseRef) {
|
|
663
|
+
try {
|
|
664
|
+
await execOk(exec, "git", ["merge-tree", "--write-tree", "--quiet", baseRef, "HEAD"], {
|
|
665
|
+
cwd: repo
|
|
666
|
+
});
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (!isModernMergeTreeUnsupported(error)) {
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
await ensureMergeableWithTemporaryWorktree(exec, repo, baseRef);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
async function ensureMergeableWithTemporaryWorktree(exec, repo, baseRef) {
|
|
675
|
+
const worktreePath = await mkdtemp(join2(tmpdir(), "mendr-merge-check-"));
|
|
676
|
+
let mergeError;
|
|
677
|
+
let worktreeCreated = false;
|
|
678
|
+
try {
|
|
679
|
+
await createDetachedWorktree(exec, repo, worktreePath, "HEAD");
|
|
680
|
+
worktreeCreated = true;
|
|
681
|
+
await execOk(exec, "git", ["merge", "--no-commit", "--no-ff", baseRef], {
|
|
682
|
+
cwd: worktreePath
|
|
683
|
+
});
|
|
684
|
+
} catch (error) {
|
|
685
|
+
mergeError = error;
|
|
686
|
+
} finally {
|
|
687
|
+
try {
|
|
688
|
+
if (worktreeCreated) {
|
|
689
|
+
await removeWorktree(exec, repo, worktreePath);
|
|
690
|
+
} else {
|
|
691
|
+
await rm(worktreePath, { recursive: true, force: true });
|
|
692
|
+
}
|
|
693
|
+
} catch (cleanupError) {
|
|
694
|
+
if (mergeError === void 0) {
|
|
695
|
+
throw cleanupError;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (mergeError !== void 0) {
|
|
700
|
+
throw mergeError;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function isModernMergeTreeUnsupported(error) {
|
|
704
|
+
if (!(error instanceof CommandFailedError)) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
if (error.command !== "git" || error.args[0] !== "merge-tree") {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
const output = `${error.result.stderr}
|
|
711
|
+
${error.result.stdout}`.toLowerCase();
|
|
712
|
+
return output.includes("not a git command") || output.includes("usage: git merge-tree") || output.includes("unknown option") || output.includes("unrecognized option") || output.includes("unknown switch");
|
|
713
|
+
}
|
|
714
|
+
async function stageAll(exec, repo) {
|
|
715
|
+
await execOk(exec, "git", ["add", "-A"], { cwd: repo });
|
|
716
|
+
}
|
|
717
|
+
async function commitStaged(exec, repo, message) {
|
|
718
|
+
await execOk(exec, "git", ["commit", "-F", "-"], {
|
|
719
|
+
cwd: repo,
|
|
720
|
+
input: `${message.trim()}
|
|
721
|
+
`
|
|
722
|
+
});
|
|
723
|
+
return getHeadCommitSha(exec, repo);
|
|
724
|
+
}
|
|
725
|
+
async function resetWorktreeToCommit(exec, repo, sha) {
|
|
726
|
+
await execOk(exec, "git", ["reset", "--hard", sha], { cwd: repo });
|
|
727
|
+
await execOk(exec, "git", ["clean", "-fdx"], { cwd: repo });
|
|
728
|
+
}
|
|
729
|
+
async function pushHeadToBranch(exec, repo, remote, branch) {
|
|
730
|
+
await execOk(exec, "git", ["push", remote, `HEAD:${branch}`], { cwd: repo });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/github.ts
|
|
734
|
+
async function fetchPullRequestReadinessRefs(exec, repo, pr) {
|
|
735
|
+
const result = await execOk(
|
|
736
|
+
exec,
|
|
737
|
+
"gh",
|
|
738
|
+
["pr", "view", pr, "--json", "baseRefName,headRefOid"],
|
|
739
|
+
{ cwd: repo }
|
|
740
|
+
);
|
|
741
|
+
const parsed = JSON.parse(result.stdout);
|
|
742
|
+
const baseBranch = typeof parsed.baseRefName === "string" ? parsed.baseRefName.trim() : "";
|
|
743
|
+
const headSha = typeof parsed.headRefOid === "string" ? parsed.headRefOid.trim() : "";
|
|
744
|
+
if (baseBranch.length === 0) {
|
|
745
|
+
throw new Error("Could not resolve the pull request base branch from GitHub.");
|
|
746
|
+
}
|
|
747
|
+
if (headSha.length === 0) {
|
|
748
|
+
throw new Error("Could not resolve the pull request head SHA from GitHub.");
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
baseBranch,
|
|
752
|
+
headSha
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async function fetchPullRequestDetails(exec, repo, pr) {
|
|
756
|
+
const result = await execOk(
|
|
757
|
+
exec,
|
|
758
|
+
"gh",
|
|
759
|
+
["pr", "view", pr, "--json", "title,body,comments"],
|
|
760
|
+
{ cwd: repo }
|
|
761
|
+
);
|
|
762
|
+
const parsed = JSON.parse(result.stdout);
|
|
763
|
+
return {
|
|
764
|
+
title: typeof parsed.title === "string" ? parsed.title : "",
|
|
765
|
+
body: typeof parsed.body === "string" ? parsed.body : "",
|
|
766
|
+
comments: Array.isArray(parsed.comments) ? parsed.comments : []
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async function fetchPullRequestDiff(exec, repo, pr) {
|
|
770
|
+
const result = await execOk(exec, "gh", ["pr", "diff", pr], { cwd: repo });
|
|
771
|
+
return result.stdout;
|
|
772
|
+
}
|
|
773
|
+
async function fetchPullRequestHeadBranch(exec, repo, pr) {
|
|
774
|
+
const result = await execOk(
|
|
775
|
+
exec,
|
|
776
|
+
"gh",
|
|
777
|
+
[
|
|
778
|
+
"pr",
|
|
779
|
+
"view",
|
|
780
|
+
pr,
|
|
781
|
+
"--json",
|
|
782
|
+
"headRefName,headRepository,headRepositoryOwner,isCrossRepository"
|
|
783
|
+
],
|
|
784
|
+
{
|
|
785
|
+
cwd: repo
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
const parsed = JSON.parse(result.stdout);
|
|
789
|
+
const branch = typeof parsed.headRefName === "string" ? parsed.headRefName.trim() : "";
|
|
790
|
+
if (branch.length === 0) {
|
|
791
|
+
throw new Error("Could not resolve the pull request head branch from GitHub.");
|
|
792
|
+
}
|
|
793
|
+
const branchPushRemote = resolveBranchPushRemote(parsed);
|
|
794
|
+
return {
|
|
795
|
+
branch,
|
|
796
|
+
branchPushRemote
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
async function validatePullRequest(exec, repo, pr) {
|
|
800
|
+
await execOk(exec, "gh", ["pr", "view", pr, "--json", "number,url"], { cwd: repo });
|
|
801
|
+
}
|
|
802
|
+
async function waitForPullRequestChecks(exec, repo, pr) {
|
|
803
|
+
await execOk(
|
|
804
|
+
exec,
|
|
805
|
+
"gh",
|
|
806
|
+
["pr", "checks", pr, "--watch", "--fail-fast", "--interval", "10"],
|
|
807
|
+
{
|
|
808
|
+
cwd: repo,
|
|
809
|
+
timeoutMs: 30 * 60 * 1e3
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
async function postPullRequestComment(exec, repo, pr, bodyFile) {
|
|
814
|
+
await execOk(exec, "gh", ["pr", "comment", pr, "--body-file", bodyFile], { cwd: repo });
|
|
815
|
+
}
|
|
816
|
+
function renderReviewMarkdown(pr, details) {
|
|
817
|
+
const comments = details.comments.length ? details.comments.map(renderComment).join("\n\n") : "No comments.";
|
|
818
|
+
return [
|
|
819
|
+
`# PR ${pr}: ${details.title || "(untitled)"}`,
|
|
820
|
+
"",
|
|
821
|
+
"## Body",
|
|
822
|
+
details.body.trim() || "No body.",
|
|
823
|
+
"",
|
|
824
|
+
"## Comments",
|
|
825
|
+
comments,
|
|
826
|
+
""
|
|
827
|
+
].join("\n");
|
|
828
|
+
}
|
|
829
|
+
function renderComment(comment) {
|
|
830
|
+
const author = comment.author?.login ?? "unknown";
|
|
831
|
+
const body = comment.body?.trim() || "(empty comment)";
|
|
832
|
+
return `- @${author}: ${body}`;
|
|
833
|
+
}
|
|
834
|
+
function resolveBranchPushRemote(input) {
|
|
835
|
+
const headRepository = readRepositoryInfo(input.headRepository, input.headRepositoryOwner);
|
|
836
|
+
const baseRepository = readRepositoryInfo(input.baseRepository);
|
|
837
|
+
const isCrossRepository = typeof input.isCrossRepository === "boolean" ? input.isCrossRepository : void 0;
|
|
838
|
+
if (!headRepository) {
|
|
839
|
+
throw new Error("Could not resolve the pull request head repository from GitHub.");
|
|
840
|
+
}
|
|
841
|
+
if (isCrossRepository === false) {
|
|
842
|
+
return "origin";
|
|
843
|
+
}
|
|
844
|
+
if (isCrossRepository === void 0 && headRepository.nameWithOwner && baseRepository?.nameWithOwner && headRepository.nameWithOwner === baseRepository.nameWithOwner) {
|
|
845
|
+
return "origin";
|
|
846
|
+
}
|
|
847
|
+
const remote = headRepository.sshUrl ?? normalizedGitUrl(headRepository.url);
|
|
848
|
+
if (remote) {
|
|
849
|
+
return remote;
|
|
850
|
+
}
|
|
851
|
+
if (headRepository.nameWithOwner) {
|
|
852
|
+
return `https://github.com/${headRepository.nameWithOwner}.git`;
|
|
853
|
+
}
|
|
854
|
+
throw new Error("Could not resolve the pull request head repository push remote from GitHub.");
|
|
855
|
+
}
|
|
856
|
+
function readRepositoryInfo(value, ownerFallback) {
|
|
857
|
+
if (!isRecord2(value)) {
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
const nameWithOwner = readString(value, "nameWithOwner") ?? readNameWithOwner(
|
|
861
|
+
readOwnerLogin(value.owner) ?? readOwnerLogin(ownerFallback),
|
|
862
|
+
readString(value, "name")
|
|
863
|
+
);
|
|
864
|
+
const url = readString(value, "url");
|
|
865
|
+
const sshUrl = readString(value, "sshUrl") ?? readString(value, "sshURL");
|
|
866
|
+
if (!nameWithOwner && !url && !sshUrl) {
|
|
867
|
+
return void 0;
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
...nameWithOwner ? { nameWithOwner } : {},
|
|
871
|
+
...url ? { url } : {},
|
|
872
|
+
...sshUrl ? { sshUrl } : {}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function readNameWithOwner(owner, name) {
|
|
876
|
+
if (!owner || !name) {
|
|
877
|
+
return void 0;
|
|
878
|
+
}
|
|
879
|
+
return `${owner}/${name}`;
|
|
880
|
+
}
|
|
881
|
+
function readOwnerLogin(value) {
|
|
882
|
+
if (!isRecord2(value)) {
|
|
883
|
+
return void 0;
|
|
884
|
+
}
|
|
885
|
+
return readString(value, "login");
|
|
886
|
+
}
|
|
887
|
+
function normalizedGitUrl(url) {
|
|
888
|
+
if (!url) {
|
|
889
|
+
return void 0;
|
|
890
|
+
}
|
|
891
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
892
|
+
return url;
|
|
893
|
+
}
|
|
894
|
+
return url.endsWith(".git") ? url : `${url}.git`;
|
|
895
|
+
}
|
|
896
|
+
function readString(value, key) {
|
|
897
|
+
const raw = value[key];
|
|
898
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
|
|
899
|
+
}
|
|
900
|
+
function isRecord2(value) {
|
|
901
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/paths.ts
|
|
905
|
+
import { homedir } from "os";
|
|
906
|
+
import { join as join3 } from "path";
|
|
907
|
+
var homeEnvVar = "MENDR_HOME";
|
|
908
|
+
function defaultMendrHome(env = process.env) {
|
|
909
|
+
return env[homeEnvVar] ?? join3(homedir(), ".mendr");
|
|
910
|
+
}
|
|
911
|
+
function reviewsDir(mendrHome) {
|
|
912
|
+
return join3(mendrHome, "reviews");
|
|
913
|
+
}
|
|
914
|
+
function reviewDir(mendrHome, id) {
|
|
915
|
+
return join3(reviewsDir(mendrHome), id);
|
|
916
|
+
}
|
|
917
|
+
function worktreesDir(mendrHome) {
|
|
918
|
+
return join3(mendrHome, "worktrees");
|
|
919
|
+
}
|
|
920
|
+
function sessionWorktreePath(mendrHome, id, pr) {
|
|
921
|
+
return join3(worktreesDir(mendrHome), `session-${id}-pr-${pr}`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/state.ts
|
|
925
|
+
import { appendFile, mkdir as mkdir3, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
926
|
+
import { join as join4 } from "path";
|
|
927
|
+
var terminalReviewPhases = /* @__PURE__ */ new Set(["complete", "stopped", "failed"]);
|
|
928
|
+
function isTerminalReviewState(state) {
|
|
929
|
+
return state.done || terminalReviewPhases.has(state.phase);
|
|
930
|
+
}
|
|
931
|
+
async function writeMeta(home, id, meta) {
|
|
932
|
+
await ensureMendrHome(home);
|
|
933
|
+
await writeJson(home, id, "meta.json", meta);
|
|
934
|
+
}
|
|
935
|
+
async function readMeta(home, id) {
|
|
936
|
+
const meta = await readJson(home, id, "meta.json");
|
|
937
|
+
return {
|
|
938
|
+
...meta,
|
|
939
|
+
maxRounds: meta.maxRounds ?? 3
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
async function writeState(home, id, state) {
|
|
943
|
+
await ensureMendrHome(home);
|
|
944
|
+
await writeJson(home, id, "state.json", state);
|
|
945
|
+
}
|
|
946
|
+
async function readState(home, id) {
|
|
947
|
+
return readJson(home, id, "state.json");
|
|
948
|
+
}
|
|
949
|
+
async function appendEvent(home, id, event) {
|
|
950
|
+
const dir = reviewDir(home, id);
|
|
951
|
+
const line = JSON.stringify({
|
|
952
|
+
...event,
|
|
953
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
954
|
+
});
|
|
955
|
+
await mkdir3(dir, { recursive: true });
|
|
956
|
+
await appendFile(join4(dir, "events.log"), `${line}
|
|
957
|
+
`, "utf8");
|
|
958
|
+
}
|
|
959
|
+
async function appendIssueRecord(home, id, issue) {
|
|
960
|
+
await appendJsonl(home, id, "issues.jsonl", issue);
|
|
961
|
+
}
|
|
962
|
+
async function appendFixAttempt(home, id, attempt) {
|
|
963
|
+
await appendJsonl(home, id, "fixes.jsonl", attempt);
|
|
964
|
+
}
|
|
965
|
+
async function ensureMendrHome(home) {
|
|
966
|
+
await mkdir3(reviewsDir(home), { recursive: true });
|
|
967
|
+
}
|
|
968
|
+
async function closeReviewSession(home, id) {
|
|
969
|
+
await rm2(reviewDir(home, id), { recursive: true, force: true });
|
|
970
|
+
}
|
|
971
|
+
async function readEvents(home, id) {
|
|
972
|
+
let raw;
|
|
973
|
+
try {
|
|
974
|
+
raw = await readFile2(join4(reviewDir(home, id), "events.log"), "utf8");
|
|
975
|
+
} catch (error) {
|
|
976
|
+
if (error.code === "ENOENT") {
|
|
977
|
+
return [];
|
|
978
|
+
}
|
|
979
|
+
throw error;
|
|
980
|
+
}
|
|
981
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
982
|
+
try {
|
|
983
|
+
return [JSON.parse(line)];
|
|
984
|
+
} catch {
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
async function writeJson(home, id, fileName, value) {
|
|
990
|
+
const dir = reviewDir(home, id);
|
|
991
|
+
await mkdir3(dir, { recursive: true });
|
|
992
|
+
await writeFile2(join4(dir, fileName), `${JSON.stringify(value, null, 2)}
|
|
993
|
+
`, "utf8");
|
|
994
|
+
}
|
|
995
|
+
async function appendJsonl(home, id, fileName, value) {
|
|
996
|
+
const dir = reviewDir(home, id);
|
|
997
|
+
await mkdir3(dir, { recursive: true });
|
|
998
|
+
await appendFile(join4(dir, fileName), `${JSON.stringify(value)}
|
|
999
|
+
`, "utf8");
|
|
1000
|
+
}
|
|
1001
|
+
async function readJson(home, id, fileName) {
|
|
1002
|
+
return JSON.parse(await readFile2(join4(reviewDir(home, id), fileName), "utf8"));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export {
|
|
1006
|
+
issueFingerprint,
|
|
1007
|
+
dedupeIssues,
|
|
1008
|
+
defaultExec,
|
|
1009
|
+
execOk,
|
|
1010
|
+
defaultModelForAgent,
|
|
1011
|
+
defaultEffortForAgent,
|
|
1012
|
+
allowedEffortsForAgent,
|
|
1013
|
+
isEffortForAgent,
|
|
1014
|
+
createAgentDriver,
|
|
1015
|
+
getRepoRoot,
|
|
1016
|
+
getHeadCommitSha,
|
|
1017
|
+
fetchPullRequestHeadRef,
|
|
1018
|
+
createDetachedWorktree,
|
|
1019
|
+
removeWorktree,
|
|
1020
|
+
getPorcelainStatus,
|
|
1021
|
+
fetchRemoteBranch,
|
|
1022
|
+
ensureMergeableWithRef,
|
|
1023
|
+
stageAll,
|
|
1024
|
+
commitStaged,
|
|
1025
|
+
resetWorktreeToCommit,
|
|
1026
|
+
pushHeadToBranch,
|
|
1027
|
+
fetchPullRequestReadinessRefs,
|
|
1028
|
+
fetchPullRequestDetails,
|
|
1029
|
+
fetchPullRequestDiff,
|
|
1030
|
+
fetchPullRequestHeadBranch,
|
|
1031
|
+
validatePullRequest,
|
|
1032
|
+
waitForPullRequestChecks,
|
|
1033
|
+
postPullRequestComment,
|
|
1034
|
+
renderReviewMarkdown,
|
|
1035
|
+
defaultMendrHome,
|
|
1036
|
+
reviewsDir,
|
|
1037
|
+
reviewDir,
|
|
1038
|
+
worktreesDir,
|
|
1039
|
+
sessionWorktreePath,
|
|
1040
|
+
isTerminalReviewState,
|
|
1041
|
+
writeMeta,
|
|
1042
|
+
readMeta,
|
|
1043
|
+
writeState,
|
|
1044
|
+
readState,
|
|
1045
|
+
appendEvent,
|
|
1046
|
+
appendIssueRecord,
|
|
1047
|
+
appendFixAttempt,
|
|
1048
|
+
ensureMendrHome,
|
|
1049
|
+
closeReviewSession,
|
|
1050
|
+
readEvents
|
|
1051
|
+
};
|