agent-review-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2596 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.tsx
4
+ import { useEffect as useEffect2, useState as useState2 } from "react";
5
+ import { Command } from "commander";
6
+ import { render } from "ink";
7
+ import readline from "readline/promises";
8
+ import { stdin as input, stdout as output } from "process";
9
+
10
+ // src/dashboard.tsx
11
+ import React, { useState, useEffect } from "react";
12
+ import { Box, Static, Text, useStdout } from "ink";
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ var T = {
15
+ primary: "cyan",
16
+ success: "green",
17
+ warning: "yellow",
18
+ error: "red",
19
+ muted: "gray",
20
+ border: "gray"
21
+ };
22
+ function fmt(n) {
23
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
24
+ if (n >= 1e4) return `${(n / 1e3).toFixed(1)}K`;
25
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
26
+ return String(n);
27
+ }
28
+ function elapsed(iso) {
29
+ const ms = Math.max(0, Date.now() - new Date(iso).getTime());
30
+ const s = Math.floor(ms / 1e3);
31
+ const m = Math.floor(s / 60);
32
+ const h = Math.floor(m / 60);
33
+ if (h > 0) return `${h}h ${m % 60}m ${s % 60}s`;
34
+ if (m > 0) return `${m}m ${s % 60}s`;
35
+ return `${s}s`;
36
+ }
37
+ var BACKEND_LABEL = {
38
+ codex: "Codex",
39
+ "claude-code": "Claude Code",
40
+ opencode: "OpenCode",
41
+ mixed: "Mixed"
42
+ };
43
+ var PHASE_ORDER = [
44
+ "initializing",
45
+ "planning",
46
+ "discovery",
47
+ "fixing",
48
+ "integrating",
49
+ "verifying",
50
+ "publishing",
51
+ "complete"
52
+ ];
53
+ var PHASE_LABEL = {
54
+ initializing: "INIT",
55
+ planning: "PLAN",
56
+ discovery: "DISCOVER",
57
+ fixing: "FIX",
58
+ integrating: "INTEGRATE",
59
+ verifying: "VERIFY",
60
+ publishing: "PUBLISH",
61
+ complete: "DONE",
62
+ failed: "FAILED"
63
+ };
64
+ var SPIN = ["\u2807", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
65
+ function useTimer(intervalMs) {
66
+ const [tick, setTick] = useState(0);
67
+ useEffect(() => {
68
+ const id = setInterval(() => setTick((t) => t + 1), intervalMs);
69
+ return () => clearInterval(id);
70
+ }, [intervalMs]);
71
+ return tick;
72
+ }
73
+ function humanizeMessage(raw, maxLen) {
74
+ if (!raw) return "";
75
+ const trimmed = raw.trim();
76
+ if (trimmed.startsWith("{")) {
77
+ try {
78
+ const parsed = JSON.parse(trimmed);
79
+ if (typeof parsed.summary === "string" && parsed.summary.trim()) {
80
+ return parsed.summary.trim().slice(0, maxLen);
81
+ }
82
+ for (const key of ["message", "text", "description", "status"]) {
83
+ if (typeof parsed[key] === "string" && parsed[key].trim()) {
84
+ return parsed[key].trim().slice(0, maxLen);
85
+ }
86
+ }
87
+ } catch {
88
+ const match = trimmed.match(/"summary"\s*:\s*"([^"]+)/);
89
+ if (match?.[1]) {
90
+ return match[1].slice(0, maxLen);
91
+ }
92
+ }
93
+ }
94
+ return trimmed.slice(0, maxLen);
95
+ }
96
+ function useTerminalWidth() {
97
+ const { stdout } = useStdout();
98
+ const [width, setWidth] = useState(stdout?.columns ?? 120);
99
+ useEffect(() => {
100
+ if (!stdout) return;
101
+ const onResize = () => setWidth(stdout.columns);
102
+ stdout.on("resize", onResize);
103
+ return () => {
104
+ stdout.off("resize", onResize);
105
+ };
106
+ }, [stdout]);
107
+ return width;
108
+ }
109
+ function Header({ snapshot, termWidth }) {
110
+ useTimer(1e3);
111
+ const el = elapsed(snapshot.startedAt);
112
+ const phaseColor = snapshot.phase === "complete" ? T.success : snapshot.phase === "failed" ? T.error : T.primary;
113
+ const backendLabel = BACKEND_LABEL[snapshot.backend] ?? snapshot.backend;
114
+ return /* @__PURE__ */ jsxs(
115
+ Box,
116
+ {
117
+ borderStyle: "round",
118
+ borderColor: T.primary,
119
+ paddingX: 2,
120
+ flexDirection: "column",
121
+ width: termWidth,
122
+ children: [
123
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
124
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: T.primary, children: [
125
+ " ",
126
+ "AGENT REVIEW",
127
+ " ",
128
+ /* @__PURE__ */ jsxs(Text, { color: T.muted, bold: false, children: [
129
+ "via ",
130
+ backendLabel
131
+ ] })
132
+ ] }),
133
+ /* @__PURE__ */ jsxs(Text, { color: T.muted, children: [
134
+ "elapsed ",
135
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: el })
136
+ ] })
137
+ ] }),
138
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
139
+ /* @__PURE__ */ jsxs(Text, { children: [
140
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "run " }),
141
+ /* @__PURE__ */ jsx(Text, { bold: true, children: snapshot.runId })
142
+ ] }),
143
+ /* @__PURE__ */ jsxs(Text, { children: [
144
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "phase " }),
145
+ /* @__PURE__ */ jsx(Text, { bold: true, color: phaseColor, children: snapshot.phase.toUpperCase() })
146
+ ] })
147
+ ] }),
148
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: T.muted, wrap: "truncate", children: [
149
+ snapshot.currentBranch ?? "",
150
+ snapshot.targetPaths.length > 0 ? ` \u2192 ${snapshot.targetPaths.join(", ")}` : ""
151
+ ] }) })
152
+ ]
153
+ }
154
+ );
155
+ }
156
+ function PhaseBar({ phase }) {
157
+ const currentIdx = PHASE_ORDER.indexOf(phase);
158
+ const failed = phase === "failed";
159
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 2, marginY: 0, children: [
160
+ PHASE_ORDER.map((p, i) => {
161
+ const done = !failed && i < currentIdx;
162
+ const active = !failed && i === currentIdx;
163
+ let color = T.muted;
164
+ let sym = "\u25CB";
165
+ if (done) {
166
+ color = T.success;
167
+ sym = "\u25CF";
168
+ } else if (active) {
169
+ color = T.primary;
170
+ sym = "\u25C9";
171
+ }
172
+ const sepColor = !failed && i <= currentIdx ? T.success : T.muted;
173
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
174
+ i > 0 && /* @__PURE__ */ jsx(Text, { color: sepColor, children: " \u2501\u2501 " }),
175
+ /* @__PURE__ */ jsxs(Text, { color, bold: active, children: [
176
+ sym,
177
+ " ",
178
+ PHASE_LABEL[p]
179
+ ] })
180
+ ] }, p);
181
+ }),
182
+ failed && /* @__PURE__ */ jsxs(Fragment, { children: [
183
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: " \u2501\u2501 " }),
184
+ /* @__PURE__ */ jsx(Text, { color: T.error, bold: true, children: "\\u2717 FAILED" })
185
+ ] })
186
+ ] });
187
+ }
188
+ function StatCard({
189
+ title,
190
+ items,
191
+ borderColor
192
+ }) {
193
+ return /* @__PURE__ */ jsxs(
194
+ Box,
195
+ {
196
+ borderStyle: "round",
197
+ borderColor: borderColor ?? T.border,
198
+ flexGrow: 1,
199
+ flexBasis: 0,
200
+ paddingX: 1,
201
+ flexDirection: "column",
202
+ marginRight: 1,
203
+ children: [
204
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: title }),
205
+ items.map((item) => /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
206
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: item.label }),
207
+ /* @__PURE__ */ jsx(Text, { bold: true, color: item.color ?? "white", children: typeof item.value === "number" ? fmt(item.value) : item.value })
208
+ ] }, item.label))
209
+ ]
210
+ }
211
+ );
212
+ }
213
+ function StatsRow({ snapshot }) {
214
+ const { issueMetrics: im, tokenUsage: tu, agents } = snapshot;
215
+ const running = agents.filter((a) => a.status === "running").length;
216
+ const done = agents.filter((a) => a.status === "completed").length;
217
+ const failed = agents.filter((a) => a.status === "failed").length;
218
+ const pending = agents.filter((a) => a.status === "pending").length;
219
+ return /* @__PURE__ */ jsxs(Box, { children: [
220
+ /* @__PURE__ */ jsx(
221
+ StatCard,
222
+ {
223
+ title: "Issues",
224
+ borderColor: im.found > 0 ? T.warning : T.border,
225
+ items: [
226
+ { label: "Found", value: im.found },
227
+ { label: "Fixed", value: im.fixed, color: im.fixed > 0 ? T.success : void 0 },
228
+ { label: "Open", value: im.open, color: im.open > 0 ? T.warning : void 0 },
229
+ {
230
+ label: "Failed",
231
+ value: im.failed,
232
+ color: im.failed > 0 ? T.error : void 0
233
+ }
234
+ ]
235
+ }
236
+ ),
237
+ /* @__PURE__ */ jsx(
238
+ StatCard,
239
+ {
240
+ title: "Agents",
241
+ borderColor: running > 0 ? T.warning : T.border,
242
+ items: [
243
+ { label: "Running", value: running, color: running > 0 ? T.warning : void 0 },
244
+ { label: "Done", value: done, color: done > 0 ? T.success : void 0 },
245
+ { label: "Failed", value: failed, color: failed > 0 ? T.error : void 0 },
246
+ { label: "Pending", value: pending }
247
+ ]
248
+ }
249
+ ),
250
+ /* @__PURE__ */ jsx(
251
+ StatCard,
252
+ {
253
+ title: "Tokens",
254
+ items: [
255
+ { label: "Input", value: tu.input },
256
+ { label: "Cached", value: tu.cachedInput, color: T.primary },
257
+ { label: "Output", value: tu.output }
258
+ ]
259
+ }
260
+ )
261
+ ] });
262
+ }
263
+ function statusIndicator(status) {
264
+ switch (status) {
265
+ case "running":
266
+ return { sym: "\u25CF", color: T.warning };
267
+ case "completed":
268
+ return { sym: "\u2713", color: T.success };
269
+ case "failed":
270
+ return { sym: "\u2717", color: T.error };
271
+ case "skipped":
272
+ return { sym: "\u2212", color: T.muted };
273
+ default:
274
+ return { sym: "\u25CB", color: T.muted };
275
+ }
276
+ }
277
+ function AgentRow({
278
+ agent,
279
+ spinner,
280
+ infoWidth
281
+ }) {
282
+ const { sym, color } = statusIndicator(agent.status);
283
+ const icon = agent.status === "running" ? spinner : sym;
284
+ const assignedLabel = agent.kind === "discovery" ? `${agent.issuesAssigned} files` : `${agent.issuesAssigned} issues`;
285
+ const infoMaxLen = Math.max(20, infoWidth - 2);
286
+ return /* @__PURE__ */ jsxs(Box, { children: [
287
+ /* @__PURE__ */ jsx(Box, { width: 16, children: /* @__PURE__ */ jsxs(Text, { color, children: [
288
+ icon,
289
+ " ",
290
+ agent.id
291
+ ] }) }),
292
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color, children: agent.status }) }),
293
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { color: T.muted, children: assignedLabel }) }),
294
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsxs(Text, { color: agent.issuesFound > 0 ? T.warning : T.muted, children: [
295
+ agent.issuesFound,
296
+ " found"
297
+ ] }) }),
298
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsxs(Text, { color: agent.issuesFixed > 0 ? T.success : T.muted, children: [
299
+ agent.issuesFixed,
300
+ " fixed"
301
+ ] }) }),
302
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsxs(Text, { color: T.muted, children: [
303
+ fmt(agent.tokenUsage.input + agent.tokenUsage.output),
304
+ " tok"
305
+ ] }) }),
306
+ agent.error ? /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { color: T.error, wrap: "truncate", children: humanizeMessage(agent.error, infoMaxLen) }) }) : agent.lastMessage ? /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { color: T.muted, wrap: "truncate", children: humanizeMessage(agent.lastMessage, infoMaxLen) }) }) : null
307
+ ] });
308
+ }
309
+ function AgentTable({ agents, termWidth }) {
310
+ const hasRunning = agents.some((a) => a.status === "running");
311
+ const tick = useTimer(hasRunning ? 250 : 6e4);
312
+ const spinner = SPIN[tick % SPIN.length];
313
+ const fixedColumnsWidth = 16 + 12 + 14 + 12 + 12 + 12 + 4;
314
+ const infoWidth = Math.max(20, termWidth - fixedColumnsWidth);
315
+ if (agents.length === 0) {
316
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: T.border, paddingX: 1, flexDirection: "column", width: termWidth, children: [
317
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: "Agents" }),
318
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Waiting for agents to start..." })
319
+ ] });
320
+ }
321
+ const planner = agents.filter((a) => a.kind === "planner");
322
+ const discovery = agents.filter((a) => a.kind === "discovery");
323
+ const fix = agents.filter((a) => a.kind === "fix");
324
+ const final = agents.filter((a) => a.kind === "final");
325
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: T.border, paddingX: 1, flexDirection: "column", width: termWidth, children: [
326
+ /* @__PURE__ */ jsxs(Box, { children: [
327
+ /* @__PURE__ */ jsx(Box, { width: 16, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "NAME" }) }),
328
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "STATUS" }) }),
329
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "ASSIGNED" }) }),
330
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "FOUND" }) }),
331
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "FIXED" }) }),
332
+ /* @__PURE__ */ jsx(Box, { width: 12, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "TOKENS" }) }),
333
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { color: T.muted, dimColor: true, children: "INFO" }) })
334
+ ] }),
335
+ planner.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
336
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: "Planning" }),
337
+ planner.map((a) => /* @__PURE__ */ jsx(AgentRow, { agent: a, spinner, infoWidth }, a.id))
338
+ ] }),
339
+ discovery.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
340
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: "Discovery" }),
341
+ discovery.map((a) => /* @__PURE__ */ jsx(AgentRow, { agent: a, spinner, infoWidth }, a.id))
342
+ ] }),
343
+ fix.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
344
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: "Fix" }),
345
+ fix.map((a) => /* @__PURE__ */ jsx(AgentRow, { agent: a, spinner, infoWidth }, a.id))
346
+ ] }),
347
+ final.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
348
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: "Verification" }),
349
+ final.map((a) => /* @__PURE__ */ jsx(AgentRow, { agent: a, spinner, infoWidth }, a.id))
350
+ ] })
351
+ ] });
352
+ }
353
+ function CompletionPanel({ snapshot }) {
354
+ useTimer(snapshot.done ? 1e3 : 6e4);
355
+ const el = elapsed(snapshot.startedAt);
356
+ if (!snapshot.done) return null;
357
+ const ok = !snapshot.failed;
358
+ const borderColor = ok ? T.success : T.error;
359
+ const title = ok ? "RUN COMPLETE" : "RUN FAILED";
360
+ const { issueMetrics: im, tokenUsage: tu } = snapshot;
361
+ return /* @__PURE__ */ jsxs(
362
+ Box,
363
+ {
364
+ borderStyle: "double",
365
+ borderColor,
366
+ paddingX: 2,
367
+ paddingY: 0,
368
+ flexDirection: "column",
369
+ children: [
370
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: borderColor, children: [
371
+ ok ? "\u25CF" : "\u2717",
372
+ " ",
373
+ title
374
+ ] }),
375
+ /* @__PURE__ */ jsx(Text, {}),
376
+ snapshot.error && /* @__PURE__ */ jsx(Text, { color: T.error, children: snapshot.error }),
377
+ /* @__PURE__ */ jsxs(Text, { children: [
378
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Issues: " }),
379
+ /* @__PURE__ */ jsxs(Text, { children: [
380
+ im.found,
381
+ " found"
382
+ ] }),
383
+ /* @__PURE__ */ jsxs(Text, { color: T.success, children: [
384
+ ", ",
385
+ im.fixed,
386
+ " fixed"
387
+ ] }),
388
+ /* @__PURE__ */ jsxs(Text, { color: T.warning, children: [
389
+ ", ",
390
+ im.open,
391
+ " open"
392
+ ] }),
393
+ /* @__PURE__ */ jsxs(Text, { color: T.error, children: [
394
+ ", ",
395
+ im.failed,
396
+ " failed"
397
+ ] })
398
+ ] }),
399
+ /* @__PURE__ */ jsxs(Text, { children: [
400
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Tokens: " }),
401
+ /* @__PURE__ */ jsxs(Text, { children: [
402
+ fmt(tu.input),
403
+ " in, ",
404
+ fmt(tu.cachedInput),
405
+ " cached, ",
406
+ fmt(tu.output),
407
+ " out"
408
+ ] })
409
+ ] }),
410
+ /* @__PURE__ */ jsxs(Text, { children: [
411
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Duration: " }),
412
+ /* @__PURE__ */ jsx(Text, { bold: true, children: el })
413
+ ] }),
414
+ /* @__PURE__ */ jsx(Text, {}),
415
+ snapshot.prUrl && /* @__PURE__ */ jsxs(Text, { children: [
416
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "PR: " }),
417
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.primary, children: snapshot.prUrl })
418
+ ] }),
419
+ snapshot.integrationBranch && /* @__PURE__ */ jsxs(Text, { children: [
420
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Branch: " }),
421
+ /* @__PURE__ */ jsx(Text, { bold: true, color: T.success, children: snapshot.integrationBranch })
422
+ ] }),
423
+ snapshot.reportFile && /* @__PURE__ */ jsxs(Text, { children: [
424
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Report: " }),
425
+ /* @__PURE__ */ jsx(Text, { children: snapshot.reportFile })
426
+ ] }),
427
+ snapshot.issueFile && /* @__PURE__ */ jsxs(Text, { children: [
428
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Issues: " }),
429
+ /* @__PURE__ */ jsx(Text, { children: snapshot.issueFile })
430
+ ] }),
431
+ snapshot.integrationWorktree && /* @__PURE__ */ jsxs(Text, { children: [
432
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: "Worktree: " }),
433
+ /* @__PURE__ */ jsx(Text, { color: T.muted, children: snapshot.integrationWorktree })
434
+ ] })
435
+ ]
436
+ }
437
+ );
438
+ }
439
+ function LogLine({ log }) {
440
+ const m = log.match(/^\[(\d{2}:\d{2}:\d{2})]\s(.*)$/);
441
+ if (m) {
442
+ const isError = m[2].includes("failed") || m[2].includes("errored") || m[2].includes("timed out");
443
+ const isDone = m[2].includes("completed") || m[2].includes("complete");
444
+ let textColor;
445
+ if (isError) textColor = T.error;
446
+ else if (isDone) textColor = T.success;
447
+ return /* @__PURE__ */ jsxs(Text, { children: [
448
+ /* @__PURE__ */ jsxs(Text, { color: T.muted, children: [
449
+ m[1],
450
+ " "
451
+ ] }),
452
+ /* @__PURE__ */ jsx(Text, { color: textColor, children: m[2] })
453
+ ] });
454
+ }
455
+ return /* @__PURE__ */ jsx(Text, { children: log });
456
+ }
457
+ function Dashboard({ snapshot }) {
458
+ const termWidth = useTerminalWidth();
459
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: termWidth, children: [
460
+ /* @__PURE__ */ jsx(Static, { items: snapshot.logs, children: (log, i) => /* @__PURE__ */ jsx(LogLine, { log }, i) }),
461
+ /* @__PURE__ */ jsx(Header, { snapshot, termWidth }),
462
+ /* @__PURE__ */ jsx(PhaseBar, { phase: snapshot.phase }),
463
+ /* @__PURE__ */ jsx(StatsRow, { snapshot }),
464
+ /* @__PURE__ */ jsx(AgentTable, { agents: snapshot.agents, termWidth }),
465
+ /* @__PURE__ */ jsx(CompletionPanel, { snapshot })
466
+ ] });
467
+ }
468
+
469
+ // src/orchestrator.ts
470
+ import { randomUUID } from "crypto";
471
+ import { spawn } from "child_process";
472
+ import path from "path";
473
+ import os from "os";
474
+ import fs from "fs/promises";
475
+ import { constants as fsConstants } from "fs";
476
+ var REVIEWABLE_EXTENSIONS = /* @__PURE__ */ new Set([
477
+ ".js",
478
+ ".jsx",
479
+ ".ts",
480
+ ".tsx",
481
+ ".mjs",
482
+ ".cjs",
483
+ ".json",
484
+ ".md",
485
+ ".mdx",
486
+ ".py",
487
+ ".rb",
488
+ ".go",
489
+ ".rs",
490
+ ".java",
491
+ ".kt",
492
+ ".swift",
493
+ ".dart",
494
+ ".php",
495
+ ".cs",
496
+ ".scala",
497
+ ".c",
498
+ ".h",
499
+ ".cpp",
500
+ ".cc",
501
+ ".hpp",
502
+ ".sql",
503
+ ".yml",
504
+ ".yaml",
505
+ ".toml",
506
+ ".sh",
507
+ ".zsh"
508
+ ]);
509
+ var BINARY_FILE_PATTERN = /\.(png|jpe?g|gif|webp|ico|bmp|svgz?|pdf|woff2?|ttf|eot|zip|gz|tgz|7z|mp3|mp4|mov|avi|wasm|jar|class|dll|so|dylib|exe)$/i;
510
+ var IGNORED_PATH_SEGMENTS = [
511
+ "/node_modules/",
512
+ "/dist/",
513
+ "/build/",
514
+ "/coverage/",
515
+ "/.next/",
516
+ "/.turbo/",
517
+ "/.git/",
518
+ "/vendor/"
519
+ ];
520
+ var EMPTY_USAGE = { input: 0, cachedInput: 0, output: 0 };
521
+ var RETRYABLE_CODEX_FAILURES = [
522
+ "failed to install system skills",
523
+ "no last agent message",
524
+ "wrote empty content",
525
+ "stream disconnected before completion"
526
+ ];
527
+ var VerificationFailure = class extends Error {
528
+ };
529
+ function nowIso() {
530
+ return (/* @__PURE__ */ new Date()).toISOString();
531
+ }
532
+ function normalizeSlashes(input2) {
533
+ return input2.replaceAll(path.sep, "/");
534
+ }
535
+ function isInsidePath(child, parent) {
536
+ const rel = path.relative(parent, child);
537
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
538
+ }
539
+ function parseJson(raw) {
540
+ const trimmed = raw.trim();
541
+ if (!trimmed) throw new Error("Expected JSON content but got empty string");
542
+ try {
543
+ return JSON.parse(trimmed);
544
+ } catch {
545
+ const start = trimmed.indexOf("{");
546
+ const end = trimmed.lastIndexOf("}");
547
+ if (start === -1 || end === -1 || end <= start) {
548
+ throw new Error("Unable to parse JSON payload");
549
+ }
550
+ return JSON.parse(trimmed.slice(start, end + 1));
551
+ }
552
+ }
553
+ async function pathExists(targetPath) {
554
+ try {
555
+ await fs.access(targetPath, fsConstants.F_OK);
556
+ return true;
557
+ } catch {
558
+ return false;
559
+ }
560
+ }
561
+ async function runCommand(command, args, options = {}) {
562
+ const child = spawn(command, args, {
563
+ cwd: options.cwd,
564
+ env: process.env,
565
+ stdio: "pipe"
566
+ });
567
+ let stdout = "";
568
+ let stderr = "";
569
+ child.stdout.setEncoding("utf8");
570
+ child.stderr.setEncoding("utf8");
571
+ child.stdout.on("data", (chunk) => {
572
+ stdout += chunk;
573
+ });
574
+ child.stderr.on("data", (chunk) => {
575
+ stderr += chunk;
576
+ });
577
+ if (options.stdin !== void 0) {
578
+ child.stdin.end(options.stdin);
579
+ } else {
580
+ child.stdin.end();
581
+ }
582
+ const code = await new Promise((resolve, reject) => {
583
+ child.on("error", reject);
584
+ child.on("close", (exitCode) => resolve(exitCode ?? 1));
585
+ });
586
+ if (code !== 0 && !options.allowFailure) {
587
+ throw new Error(`Command failed (${command} ${args.join(" ")}): ${stderr.trim() || stdout.trim()}`);
588
+ }
589
+ return { code, stdout, stderr };
590
+ }
591
+ function createCodexDriver() {
592
+ return {
593
+ async checkVersion() {
594
+ await runCommand("codex", ["--version"]);
595
+ },
596
+ buildArgs(input2) {
597
+ const args = [];
598
+ if (input2.yolo) {
599
+ args.push("--dangerously-bypass-approvals-and-sandbox");
600
+ } else {
601
+ args.push("-s", "read-only");
602
+ }
603
+ if (input2.model) args.push("-m", input2.model);
604
+ args.push("exec", "--json", "--output-schema", input2.schemaPath, "-o", input2.outputPath, input2.prompt);
605
+ return { command: "codex", args };
606
+ },
607
+ parseStdoutLine(parsed) {
608
+ const result = {};
609
+ const type = parsed.type;
610
+ if (type === "turn.completed") {
611
+ const usage = parsed.usage;
612
+ if (usage) {
613
+ result.tokenIncrement = {
614
+ input: usage.input_tokens ?? 0,
615
+ cachedInput: usage.cached_input_tokens ?? 0,
616
+ output: usage.output_tokens ?? 0
617
+ };
618
+ }
619
+ } else if (type === "item.completed") {
620
+ const item = parsed.item;
621
+ if (item?.type === "agent_message" && typeof item.text === "string") {
622
+ result.message = item.text.slice(0, 600);
623
+ }
624
+ } else if (type === "error" || type === "turn.failed") {
625
+ const errMsg = parsed.message ?? parsed.error?.message;
626
+ if (typeof errMsg === "string") {
627
+ result.message = `Error: ${errMsg.slice(0, 500)}`;
628
+ }
629
+ }
630
+ return result;
631
+ }
632
+ };
633
+ }
634
+ function createClaudeCodeDriver() {
635
+ return {
636
+ async checkVersion() {
637
+ await runCommand("claude", ["--version"]);
638
+ },
639
+ buildArgs(input2) {
640
+ const args = ["-p", input2.prompt, "--output-format", "stream-json", "--verbose"];
641
+ if (input2.model) args.push("--model", input2.model);
642
+ if (!input2.yolo) {
643
+ args.push("--allowedTools", "Read,Glob,Grep,Bash(git status),Bash(git diff),Bash(git log)");
644
+ }
645
+ return { command: "claude", args };
646
+ },
647
+ parseStdoutLine(parsed) {
648
+ const result = {};
649
+ const type = parsed.type;
650
+ if (type === "result" || type === "turn_result") {
651
+ const usage = parsed.usage ?? parsed.token_usage;
652
+ if (usage) {
653
+ result.tokenIncrement = {
654
+ input: usage.input_tokens ?? 0,
655
+ cachedInput: usage.cache_read_input_tokens ?? usage.cached_input_tokens ?? 0,
656
+ output: usage.output_tokens ?? 0
657
+ };
658
+ }
659
+ }
660
+ if (type === "assistant" || type === "message") {
661
+ const content = parsed.content ?? parsed.message ?? parsed.text;
662
+ if (typeof content === "string") {
663
+ result.message = content.slice(0, 600);
664
+ } else if (Array.isArray(content)) {
665
+ const textBlock = content.find(
666
+ (b) => b.type === "text" && typeof b.text === "string"
667
+ );
668
+ if (textBlock) {
669
+ result.message = textBlock.text.slice(0, 600);
670
+ }
671
+ }
672
+ }
673
+ if (type === "error") {
674
+ const errMsg = parsed.message ?? parsed.error;
675
+ if (typeof errMsg === "string") {
676
+ result.message = `Error: ${errMsg.slice(0, 500)}`;
677
+ }
678
+ }
679
+ return result;
680
+ }
681
+ };
682
+ }
683
+ function createOpenCodeDriver() {
684
+ return {
685
+ async checkVersion() {
686
+ await runCommand("opencode", ["--version"]);
687
+ },
688
+ buildArgs(input2) {
689
+ const args = [];
690
+ if (input2.model) args.push("--model", input2.model);
691
+ if (input2.yolo) {
692
+ args.push("--dangerously-skip-permissions");
693
+ }
694
+ args.push("run", "--json", "-o", input2.outputPath, input2.prompt);
695
+ return { command: "opencode", args };
696
+ },
697
+ parseStdoutLine(parsed) {
698
+ const result = {};
699
+ const type = parsed.type;
700
+ if (type === "usage" || type === "turn.completed") {
701
+ const usage = parsed.usage ?? parsed;
702
+ if (usage.input_tokens !== void 0 || usage.output_tokens !== void 0) {
703
+ result.tokenIncrement = {
704
+ input: usage.input_tokens ?? 0,
705
+ cachedInput: usage.cached_input_tokens ?? 0,
706
+ output: usage.output_tokens ?? 0
707
+ };
708
+ }
709
+ }
710
+ if (type === "message" || type === "assistant") {
711
+ const text = parsed.text ?? parsed.content ?? parsed.message;
712
+ if (typeof text === "string") {
713
+ result.message = text.slice(0, 600);
714
+ }
715
+ }
716
+ return result;
717
+ }
718
+ };
719
+ }
720
+ function getDriver(backend) {
721
+ switch (backend) {
722
+ case "codex":
723
+ return createCodexDriver();
724
+ case "claude-code":
725
+ return createClaudeCodeDriver();
726
+ case "opencode":
727
+ return createOpenCodeDriver();
728
+ }
729
+ }
730
+ var ALL_SINGLE_BACKENDS = ["codex", "claude-code", "opencode"];
731
+ async function detectAvailableBackends() {
732
+ const available = [];
733
+ for (const backend of ALL_SINGLE_BACKENDS) {
734
+ try {
735
+ await getDriver(backend).checkVersion();
736
+ available.push(backend);
737
+ } catch {
738
+ }
739
+ }
740
+ return available;
741
+ }
742
+ var AgentReviewOrchestrator = class _AgentReviewOrchestrator {
743
+ constructor(config) {
744
+ this.config = config;
745
+ this.codexTimeoutMs = config.codexTimeoutMs ?? 6e5;
746
+ this.codexInactivityTimeoutMs = config.codexInactivityTimeoutMs ?? 18e4;
747
+ this.driver = config.backend === "mixed" ? getDriver("claude-code") : getDriver(config.backend);
748
+ const startedAt = nowIso();
749
+ this.snapshot = {
750
+ runId: "pending",
751
+ phase: "initializing",
752
+ statusMessage: "Bootstrapping run",
753
+ startedAt,
754
+ lastUpdatedAt: startedAt,
755
+ done: false,
756
+ failed: false,
757
+ backend: config.backend,
758
+ targetPaths: [],
759
+ agents: [],
760
+ issueMetrics: { found: 0, open: 0, assigned: 0, fixed: 0, failed: 0 },
761
+ tokenUsage: { ...EMPTY_USAGE },
762
+ logs: []
763
+ };
764
+ }
765
+ listeners = /* @__PURE__ */ new Set();
766
+ snapshot;
767
+ createdWorktrees = /* @__PURE__ */ new Set();
768
+ codexTimeoutMs;
769
+ codexInactivityTimeoutMs;
770
+ driver;
771
+ drivers = /* @__PURE__ */ new Map();
772
+ agentBackends = /* @__PURE__ */ new Map();
773
+ preparedCodexHomes = /* @__PURE__ */ new Map();
774
+ availableBackends = [];
775
+ lastEmitTime = 0;
776
+ emitTimer = null;
777
+ persistQueue = Promise.resolve();
778
+ issueStore;
779
+ issueCounter = 1;
780
+ runId = "";
781
+ repoRoot = "";
782
+ currentBranch = "";
783
+ baseCommit = "";
784
+ runDir = "";
785
+ worktreesDir = "";
786
+ codexHomesDir = "";
787
+ issueFile = "";
788
+ reportFile = "";
789
+ targetPathsAbs = [];
790
+ targetFiles = [];
791
+ sharedWorktree;
792
+ subscribe(listener) {
793
+ this.listeners.add(listener);
794
+ listener(this.cloneSnapshot());
795
+ return () => {
796
+ this.listeners.delete(listener);
797
+ };
798
+ }
799
+ getSnapshot() {
800
+ return this.cloneSnapshot();
801
+ }
802
+ async run() {
803
+ try {
804
+ await this.initialize();
805
+ await this.runPlanningPhase();
806
+ await this.runDiscoveryPhase();
807
+ await this.runFixPhase();
808
+ await this.runVerificationPhase();
809
+ try {
810
+ await this.publishIntegrationBranch();
811
+ } catch (error) {
812
+ this.addLog(`Publishing failed: ${error instanceof Error ? error.message : String(error)}`);
813
+ }
814
+ this.setPhase("complete", "Run complete");
815
+ this.snapshot.done = true;
816
+ this.emitSnapshotNow();
817
+ return {
818
+ success: true,
819
+ runDir: this.runDir,
820
+ issueFile: this.issueFile,
821
+ reportFile: this.reportFile,
822
+ integrationWorktree: this.snapshot.integrationWorktree,
823
+ integrationBranch: this.snapshot.integrationBranch,
824
+ prUrl: this.snapshot.prUrl
825
+ };
826
+ } catch (error) {
827
+ const message = error instanceof Error ? error.message : String(error);
828
+ this.snapshot.failed = true;
829
+ this.snapshot.error = message;
830
+ this.snapshot.done = true;
831
+ this.setPhase("failed", "Run failed");
832
+ this.addLog(`Run failed: ${message}`);
833
+ this.emitSnapshotNow();
834
+ return {
835
+ success: false,
836
+ runDir: this.runDir || void 0,
837
+ issueFile: this.issueFile || void 0,
838
+ reportFile: this.reportFile || void 0,
839
+ integrationWorktree: this.snapshot.integrationWorktree,
840
+ integrationBranch: this.snapshot.integrationBranch,
841
+ prUrl: this.snapshot.prUrl,
842
+ error: message
843
+ };
844
+ } finally {
845
+ if (this.config.cleanup) {
846
+ await this.cleanupWorktrees();
847
+ } else {
848
+ await this.cleanupCodexHomes();
849
+ }
850
+ }
851
+ }
852
+ /* -- State management ------------------------------------------- */
853
+ cloneSnapshot() {
854
+ return {
855
+ ...this.snapshot,
856
+ targetPaths: [...this.snapshot.targetPaths],
857
+ agents: this.snapshot.agents.map((agent) => ({
858
+ ...agent,
859
+ tokenUsage: { ...agent.tokenUsage }
860
+ })),
861
+ issueMetrics: { ...this.snapshot.issueMetrics },
862
+ tokenUsage: { ...this.snapshot.tokenUsage },
863
+ logs: [...this.snapshot.logs]
864
+ };
865
+ }
866
+ /** Throttled emit — batches rapid updates to ~4 renders/sec */
867
+ emitSnapshot() {
868
+ this.snapshot.lastUpdatedAt = nowIso();
869
+ this.recomputeDerivedMetrics();
870
+ const now = Date.now();
871
+ const sinceLast = now - this.lastEmitTime;
872
+ if (sinceLast >= 250) {
873
+ this.lastEmitTime = now;
874
+ if (this.emitTimer) {
875
+ clearTimeout(this.emitTimer);
876
+ this.emitTimer = null;
877
+ }
878
+ this.flushSnapshot();
879
+ return;
880
+ }
881
+ if (!this.emitTimer) {
882
+ this.emitTimer = setTimeout(() => {
883
+ this.emitTimer = null;
884
+ this.lastEmitTime = Date.now();
885
+ this.snapshot.lastUpdatedAt = nowIso();
886
+ this.recomputeDerivedMetrics();
887
+ this.flushSnapshot();
888
+ }, 250 - sinceLast);
889
+ }
890
+ }
891
+ /** Immediate emit — used for phase transitions and run completion */
892
+ emitSnapshotNow() {
893
+ if (this.emitTimer) {
894
+ clearTimeout(this.emitTimer);
895
+ this.emitTimer = null;
896
+ }
897
+ this.snapshot.lastUpdatedAt = nowIso();
898
+ this.recomputeDerivedMetrics();
899
+ this.lastEmitTime = Date.now();
900
+ this.flushSnapshot();
901
+ }
902
+ flushSnapshot() {
903
+ const clone = this.cloneSnapshot();
904
+ for (const listener of this.listeners) {
905
+ listener(clone);
906
+ }
907
+ }
908
+ addLog(message) {
909
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
910
+ this.snapshot.logs.push(`[${stamp}] ${message}`);
911
+ this.emitSnapshot();
912
+ }
913
+ recomputeDerivedMetrics() {
914
+ let input2 = 0;
915
+ let cachedInput = 0;
916
+ let output2 = 0;
917
+ for (const agent of this.snapshot.agents) {
918
+ input2 += agent.tokenUsage.input;
919
+ cachedInput += agent.tokenUsage.cachedInput;
920
+ output2 += agent.tokenUsage.output;
921
+ }
922
+ this.snapshot.tokenUsage = { input: input2, cachedInput, output: output2 };
923
+ if (!this.issueStore) {
924
+ this.snapshot.issueMetrics = { found: 0, open: 0, assigned: 0, fixed: 0, failed: 0 };
925
+ return;
926
+ }
927
+ let open = 0;
928
+ let assigned = 0;
929
+ let fixed = 0;
930
+ let failed = 0;
931
+ for (const issue of this.issueStore.issues) {
932
+ if (issue.status === "open") open += 1;
933
+ if (issue.status === "assigned") assigned += 1;
934
+ if (issue.status === "fixed") fixed += 1;
935
+ if (issue.status === "failed") failed += 1;
936
+ }
937
+ this.snapshot.issueMetrics = { found: this.issueStore.issues.length, open, assigned, fixed, failed };
938
+ }
939
+ setPhase(phase, statusMessage) {
940
+ this.snapshot.phase = phase;
941
+ this.snapshot.statusMessage = statusMessage;
942
+ this.emitSnapshotNow();
943
+ }
944
+ getAgent(agentId) {
945
+ const agent = this.snapshot.agents.find((entry) => entry.id === agentId);
946
+ if (!agent) throw new Error(`Agent not found: ${agentId}`);
947
+ return agent;
948
+ }
949
+ createAgent(input2) {
950
+ const agent = {
951
+ id: input2.id,
952
+ index: input2.index,
953
+ kind: input2.kind,
954
+ status: "pending",
955
+ branch: input2.branch,
956
+ worktreePath: input2.worktreePath,
957
+ issuesAssigned: input2.issuesAssigned,
958
+ issuesFound: 0,
959
+ issuesFixed: 0,
960
+ tokenUsage: { ...EMPTY_USAGE },
961
+ stepsCompleted: 0,
962
+ stepsTotal: 0
963
+ };
964
+ this.snapshot.agents.push(agent);
965
+ this.emitSnapshot();
966
+ return agent;
967
+ }
968
+ updateAgent(agentId, patch) {
969
+ const agent = this.getAgent(agentId);
970
+ Object.assign(agent, patch);
971
+ this.emitSnapshot();
972
+ }
973
+ addAgentUsage(agentId, usage) {
974
+ const agent = this.getAgent(agentId);
975
+ agent.tokenUsage = {
976
+ input: agent.tokenUsage.input + usage.input,
977
+ cachedInput: agent.tokenUsage.cachedInput + usage.cachedInput,
978
+ output: agent.tokenUsage.output + usage.output
979
+ };
980
+ this.emitSnapshot();
981
+ }
982
+ /* -- Initialize ------------------------------------------------- */
983
+ async initialize() {
984
+ this.setPhase("initializing", "Resolving repository context");
985
+ const gitRoot = (await runCommand("git", ["rev-parse", "--show-toplevel"], { cwd: this.config.startCwd })).stdout.trim();
986
+ if (!gitRoot) throw new Error("Unable to resolve git repository root");
987
+ this.repoRoot = gitRoot;
988
+ const branch = (await runCommand("git", ["-C", this.repoRoot, "branch", "--show-current"])).stdout.trim();
989
+ this.currentBranch = branch || (await runCommand("git", ["-C", this.repoRoot, "rev-parse", "--short", "HEAD"])).stdout.trim();
990
+ this.baseCommit = (await runCommand("git", ["-C", this.repoRoot, "rev-parse", "HEAD"])).stdout.trim();
991
+ if (this.config.backend === "mixed") {
992
+ this.availableBackends = await detectAvailableBackends();
993
+ if (this.availableBackends.length === 0) {
994
+ throw new Error("No backends available. Install at least one of: codex, claude, opencode");
995
+ }
996
+ for (const b of this.availableBackends) {
997
+ this.drivers.set(b, getDriver(b));
998
+ }
999
+ this.addLog(`Mixed mode: available backends: ${this.availableBackends.join(", ")}`);
1000
+ } else {
1001
+ await this.driver.checkVersion();
1002
+ const singleBackend = this.config.backend;
1003
+ this.availableBackends = [singleBackend];
1004
+ this.drivers.set(singleBackend, this.driver);
1005
+ }
1006
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[-:TZ.]/g, "").slice(0, 14);
1007
+ this.runId = `${stamp}-${randomUUID().slice(0, 8)}`;
1008
+ this.runDir = path.join(this.repoRoot, ".agent-review", "runs", this.runId);
1009
+ this.worktreesDir = path.join(this.repoRoot, ".agent-review", "worktrees", this.runId);
1010
+ this.codexHomesDir = path.join(os.tmpdir(), "agent-review-codex-homes", this.runId);
1011
+ this.issueFile = path.join(this.runDir, "issues.json");
1012
+ this.reportFile = path.join(this.runDir, "final-report.md");
1013
+ await fs.mkdir(this.runDir, { recursive: true });
1014
+ await fs.mkdir(this.worktreesDir, { recursive: true });
1015
+ await fs.mkdir(this.codexHomesDir, { recursive: true });
1016
+ const sharedBranch = `agent-review/${this.runId}`;
1017
+ this.sharedWorktree = await this.createWorktree({ label: "shared", branch: sharedBranch });
1018
+ const codexDir = path.join(this.sharedWorktree.path, ".agent-review");
1019
+ const resultDir = path.join(codexDir, "results");
1020
+ await fs.mkdir(resultDir, { recursive: true });
1021
+ this.snapshot.runId = this.runId;
1022
+ this.snapshot.repoRoot = this.repoRoot;
1023
+ this.snapshot.currentBranch = this.currentBranch;
1024
+ this.snapshot.targetPrompt = this.config.targetPrompt;
1025
+ this.snapshot.runDir = this.runDir;
1026
+ this.snapshot.issueFile = this.issueFile;
1027
+ this.snapshot.integrationBranch = sharedBranch;
1028
+ this.snapshot.integrationWorktree = this.sharedWorktree.path;
1029
+ this.addLog(`Run ID: ${this.runId}`);
1030
+ this.addLog(`Repository: ${this.repoRoot}`);
1031
+ this.addLog(`Branch: ${this.currentBranch}`);
1032
+ this.addLog(`Backend: ${this.config.backend}`);
1033
+ this.addLog(`Worktree: ${this.sharedWorktree.path}`);
1034
+ }
1035
+ /* -- Planning phase: use agent to identify the right codebase --- */
1036
+ async runPlanningPhase() {
1037
+ this.setPhase("planning", "Identifying target codebase");
1038
+ const directPaths = await this.resolveDirectPaths();
1039
+ if (directPaths.length > 0) {
1040
+ this.targetPathsAbs = directPaths;
1041
+ this.targetFiles = await this.collectTargetFiles(this.targetPathsAbs);
1042
+ await this.finalizePlanningResults();
1043
+ this.addLog("Target resolved via direct path match");
1044
+ return;
1045
+ }
1046
+ const dirTree = await this.buildDirectoryOverview();
1047
+ const plannerAgentId = "planner-1";
1048
+ this.assignBackend(plannerAgentId, 1);
1049
+ this.createAgent({
1050
+ id: plannerAgentId,
1051
+ index: 1,
1052
+ kind: "planner",
1053
+ issuesAssigned: 0
1054
+ });
1055
+ this.updateAgent(plannerAgentId, { status: "running", startedAt: nowIso(), lastMessage: "Analyzing repository structure" });
1056
+ const plannerPrompt = [
1057
+ "You are a codebase navigation agent. Your ONLY job is to identify which directories/paths in this repository match the user's request.",
1058
+ "",
1059
+ `User request: "${this.config.targetPrompt}"`,
1060
+ "",
1061
+ "Repository structure (top-level directories and their immediate children):",
1062
+ dirTree,
1063
+ "",
1064
+ "Instructions:",
1065
+ "1. Based on the user's request, identify the most relevant directory paths to focus a code review on.",
1066
+ '2. Return ONLY a JSON object with this exact structure: {"paths": ["path/to/dir1", "path/to/dir2"], "reasoning": "brief explanation"}',
1067
+ "3. Paths should be relative to the repository root.",
1068
+ `4. Be specific - prefer deeper paths (e.g. "papertrail/web/app") over broad ones (e.g. "papertrail") when the user's request is specific.`,
1069
+ "5. If the request mentions a specific project, app, or feature, narrow to that area.",
1070
+ '6. If the request is general (e.g. "everything", "the whole repo"), return ["."].',
1071
+ "7. Return ONLY the JSON, nothing else."
1072
+ ].join("\n");
1073
+ try {
1074
+ const planWorktree = this.sharedWorktree;
1075
+ const codexDir = path.join(planWorktree.path, ".agent-review");
1076
+ const resultDir = path.join(codexDir, "results");
1077
+ const schemaPath = path.join(codexDir, "planner-schema.json");
1078
+ const outputPath = path.join(resultDir, "planner-output.json");
1079
+ await fs.writeFile(schemaPath, JSON.stringify({
1080
+ type: "object",
1081
+ required: ["paths", "reasoning"],
1082
+ properties: {
1083
+ paths: { type: "array", items: { type: "string" } },
1084
+ reasoning: { type: "string" }
1085
+ }
1086
+ }, null, 2), "utf8");
1087
+ const result = await this.runBackendExec({
1088
+ agentId: plannerAgentId,
1089
+ cwd: planWorktree.path,
1090
+ prompt: plannerPrompt,
1091
+ schemaPath,
1092
+ outputPath,
1093
+ yolo: false
1094
+ });
1095
+ if (result.exitCode === 0) {
1096
+ try {
1097
+ const planOutput = await this.readJsonFile(outputPath);
1098
+ if (planOutput.paths && planOutput.paths.length > 0) {
1099
+ const resolvedPaths = [];
1100
+ for (const p of planOutput.paths) {
1101
+ const abs = path.resolve(this.repoRoot, p);
1102
+ if (await pathExists(abs) && isInsidePath(abs, this.repoRoot)) {
1103
+ resolvedPaths.push(abs);
1104
+ }
1105
+ }
1106
+ if (resolvedPaths.length > 0) {
1107
+ this.targetPathsAbs = resolvedPaths;
1108
+ this.updateAgent(plannerAgentId, {
1109
+ status: "completed",
1110
+ endedAt: nowIso(),
1111
+ lastMessage: planOutput.reasoning
1112
+ });
1113
+ this.addLog(`Planner identified ${resolvedPaths.length} target paths: ${planOutput.reasoning}`);
1114
+ }
1115
+ }
1116
+ } catch {
1117
+ }
1118
+ }
1119
+ } catch (error) {
1120
+ this.updateAgent(plannerAgentId, {
1121
+ status: "failed",
1122
+ endedAt: nowIso(),
1123
+ error: error instanceof Error ? error.message : String(error)
1124
+ });
1125
+ this.addLog("Planner failed, falling back to heuristic matching");
1126
+ }
1127
+ if (this.targetPathsAbs.length === 0) {
1128
+ this.targetPathsAbs = await this.resolveHeuristicTargets();
1129
+ if (this.targetPathsAbs.length === 0) {
1130
+ const fallback = isInsidePath(this.config.startCwd, this.repoRoot) ? this.config.startCwd : this.repoRoot;
1131
+ this.targetPathsAbs = [fallback];
1132
+ }
1133
+ const plannerAgent = this.snapshot.agents.find((a) => a.id === plannerAgentId);
1134
+ if (plannerAgent && plannerAgent.status !== "completed") {
1135
+ this.updateAgent(plannerAgentId, {
1136
+ status: "completed",
1137
+ endedAt: nowIso(),
1138
+ lastMessage: "Resolved via heuristic fallback"
1139
+ });
1140
+ }
1141
+ }
1142
+ this.targetFiles = await this.collectTargetFiles(this.targetPathsAbs);
1143
+ await this.finalizePlanningResults();
1144
+ }
1145
+ async finalizePlanningResults() {
1146
+ this.issueStore = {
1147
+ metadata: {
1148
+ runId: this.runId,
1149
+ createdAt: nowIso(),
1150
+ updatedAt: nowIso(),
1151
+ repoRoot: this.repoRoot,
1152
+ currentBranch: this.currentBranch,
1153
+ targetPrompt: this.config.targetPrompt,
1154
+ targetPaths: this.targetPathsAbs.map((p) => normalizeSlashes(path.relative(this.repoRoot, p) || ".")),
1155
+ instances: this.config.instances,
1156
+ backend: this.config.backend
1157
+ },
1158
+ issues: []
1159
+ };
1160
+ this.snapshot.targetPaths = [...this.issueStore.metadata.targetPaths];
1161
+ await this.persistIssueStore();
1162
+ this.addLog(`Targets: ${this.snapshot.targetPaths.join(", ")}`);
1163
+ this.addLog(`Review file count: ${this.targetFiles.length}`);
1164
+ }
1165
+ async buildDirectoryOverview() {
1166
+ const lines = [];
1167
+ try {
1168
+ const entries = await fs.readdir(this.repoRoot, { withFileTypes: true });
1169
+ for (const entry of entries) {
1170
+ if (!entry.isDirectory()) continue;
1171
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1172
+ lines.push(`${entry.name}/`);
1173
+ try {
1174
+ const subEntries = await fs.readdir(path.join(this.repoRoot, entry.name), { withFileTypes: true });
1175
+ const dirs = subEntries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules");
1176
+ const files = subEntries.filter((e) => e.isFile());
1177
+ for (const d of dirs.slice(0, 15)) {
1178
+ lines.push(` ${entry.name}/${d.name}/`);
1179
+ }
1180
+ if (dirs.length > 15) lines.push(` ... ${dirs.length - 15} more directories`);
1181
+ const keyFiles = files.filter(
1182
+ (f) => ["package.json", "Cargo.toml", "go.mod", "pyproject.toml", "README.md", "Makefile"].includes(f.name)
1183
+ );
1184
+ for (const f of keyFiles) {
1185
+ lines.push(` ${entry.name}/${f.name}`);
1186
+ }
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ } catch {
1191
+ }
1192
+ return lines.join("\n");
1193
+ }
1194
+ async resolveDirectPaths() {
1195
+ const prompt = this.config.targetPrompt.trim();
1196
+ if (!prompt || /^(current(\s+directory)?|here|\.|cwd)$/i.test(prompt)) {
1197
+ const fallback = isInsidePath(this.config.startCwd, this.repoRoot) ? this.config.startCwd : this.repoRoot;
1198
+ return [fallback];
1199
+ }
1200
+ const candidates = prompt.split(/,| and /gi).map((p) => p.trim()).filter(Boolean);
1201
+ const found = /* @__PURE__ */ new Set();
1202
+ for (const candidate of candidates) {
1203
+ const abs = path.isAbsolute(candidate) ? candidate : path.resolve(this.config.startCwd, candidate);
1204
+ if (await pathExists(abs) && isInsidePath(path.resolve(abs), this.repoRoot)) {
1205
+ found.add(path.resolve(abs));
1206
+ }
1207
+ const repoRel = path.resolve(this.repoRoot, candidate);
1208
+ if (await pathExists(repoRel) && isInsidePath(path.resolve(repoRel), this.repoRoot)) {
1209
+ found.add(path.resolve(repoRel));
1210
+ }
1211
+ }
1212
+ return [...found];
1213
+ }
1214
+ async resolveHeuristicTargets() {
1215
+ const prompt = this.config.targetPrompt.trim().toLowerCase();
1216
+ const entries = await fs.readdir(this.repoRoot, { withFileTypes: true });
1217
+ const scored = [];
1218
+ for (const entry of entries) {
1219
+ if (!entry.isDirectory()) continue;
1220
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1221
+ const name = entry.name.toLowerCase();
1222
+ let score = 0;
1223
+ if (name === prompt) score = 100;
1224
+ else if (name.startsWith(prompt)) score = 80;
1225
+ else if (name.includes(prompt)) score = 65;
1226
+ else {
1227
+ const promptTokens = prompt.split(/\s+/g).filter(Boolean);
1228
+ const nameTokens = name.split(/[-_/.\s]+/g);
1229
+ const overlap = promptTokens.filter((t) => nameTokens.includes(t)).length;
1230
+ if (overlap > 0) score = 40 + overlap * 10;
1231
+ }
1232
+ if (score > 0) scored.push({ score, fullPath: path.join(this.repoRoot, entry.name) });
1233
+ }
1234
+ scored.sort((a, b) => b.score - a.score);
1235
+ if (scored.length > 0) {
1236
+ const best = scored[0].score;
1237
+ return scored.filter((s) => s.score >= Math.max(60, best - 10)).slice(0, 3).map((s) => s.fullPath);
1238
+ }
1239
+ return [];
1240
+ }
1241
+ /* -- File collection -------------------------------------------- */
1242
+ async collectTargetFiles(targetPaths) {
1243
+ const relTargets = targetPaths.map((p) => normalizeSlashes(path.relative(this.repoRoot, p) || "."));
1244
+ let files = [];
1245
+ try {
1246
+ const { stdout } = await runCommand("git", ["-C", this.repoRoot, "ls-files", "-z", "--", ...relTargets]);
1247
+ files = stdout.split("\0").map((e) => e.trim()).filter(Boolean).filter((e) => this.shouldReviewFile(e));
1248
+ } catch {
1249
+ files = [];
1250
+ }
1251
+ if (files.length > 0) return files;
1252
+ const discovered = /* @__PURE__ */ new Set();
1253
+ for (const targetPath of targetPaths) {
1254
+ const stats = await fs.stat(targetPath);
1255
+ if (stats.isFile()) {
1256
+ const rel = normalizeSlashes(path.relative(this.repoRoot, targetPath));
1257
+ if (this.shouldReviewFile(rel)) discovered.add(rel);
1258
+ continue;
1259
+ }
1260
+ const stack = [targetPath];
1261
+ while (stack.length > 0) {
1262
+ const current = stack.pop();
1263
+ const dirEntries = await fs.readdir(current, { withFileTypes: true });
1264
+ for (const dirEntry of dirEntries) {
1265
+ const absolute = path.join(current, dirEntry.name);
1266
+ if (dirEntry.isDirectory()) {
1267
+ const relDir = normalizeSlashes(path.relative(this.repoRoot, absolute));
1268
+ if (IGNORED_PATH_SEGMENTS.some((seg) => `/${relDir}/`.includes(seg) || relDir.startsWith(seg.slice(1)))) continue;
1269
+ stack.push(absolute);
1270
+ continue;
1271
+ }
1272
+ if (!dirEntry.isFile()) continue;
1273
+ const relFile = normalizeSlashes(path.relative(this.repoRoot, absolute));
1274
+ if (this.shouldReviewFile(relFile)) discovered.add(relFile);
1275
+ }
1276
+ }
1277
+ }
1278
+ return [...discovered].sort();
1279
+ }
1280
+ shouldReviewFile(relPath) {
1281
+ const normalized = `/${normalizeSlashes(relPath)}`;
1282
+ if (IGNORED_PATH_SEGMENTS.some((seg) => normalized.includes(seg))) return false;
1283
+ const fileName = path.basename(relPath).toLowerCase();
1284
+ if (fileName === "pnpm-lock.yaml" || fileName.endsWith(".lock")) return false;
1285
+ if (BINARY_FILE_PATTERN.test(fileName)) return false;
1286
+ if (/\.min\.(js|css)$/.test(fileName)) return false;
1287
+ const ext = path.extname(fileName);
1288
+ if (!ext) return true;
1289
+ return REVIEWABLE_EXTENSIONS.has(ext);
1290
+ }
1291
+ splitIntoSegments(items) {
1292
+ const count = Math.max(1, this.config.instances);
1293
+ const segments = Array.from({ length: count }, () => []);
1294
+ for (let i = 0; i < items.length; i += 1) {
1295
+ segments[i % count].push(items[i]);
1296
+ }
1297
+ return segments;
1298
+ }
1299
+ /**
1300
+ * Split files into segments grouped by directory so each agent gets
1301
+ * coherent directory-level ownership rather than scattered files.
1302
+ * Large directory groups are sub-divided at deeper path levels to
1303
+ * ensure even distribution across agents.
1304
+ */
1305
+ splitFilesIntoDirectorySegments(files) {
1306
+ const count = Math.max(1, this.config.instances);
1307
+ if (files.length === 0) return Array.from({ length: count }, () => []);
1308
+ const maxPerSegment = Math.ceil(files.length / count);
1309
+ const buildGroups = (items, depth) => {
1310
+ const groups = /* @__PURE__ */ new Map();
1311
+ for (const file of items) {
1312
+ const parts = file.split("/");
1313
+ const key = parts.slice(0, Math.min(depth, parts.length - 1)).join("/") || ".";
1314
+ const group = groups.get(key) ?? [];
1315
+ group.push(file);
1316
+ groups.set(key, group);
1317
+ }
1318
+ return groups;
1319
+ };
1320
+ const chunks = [];
1321
+ const pending = /* @__PURE__ */ new Map();
1322
+ const initial = buildGroups(files, 2);
1323
+ for (const [k, gf] of initial) {
1324
+ pending.set(k, gf);
1325
+ }
1326
+ for (let depth = 3; depth <= 5; depth += 1) {
1327
+ const next = /* @__PURE__ */ new Map();
1328
+ for (const [, groupFiles] of pending) {
1329
+ if (groupFiles.length <= maxPerSegment) {
1330
+ chunks.push(groupFiles);
1331
+ } else {
1332
+ const sub = buildGroups(groupFiles, depth);
1333
+ for (const [sk, sf] of sub) {
1334
+ next.set(sk, sf);
1335
+ }
1336
+ }
1337
+ }
1338
+ pending.clear();
1339
+ for (const [k, gf] of next) {
1340
+ pending.set(k, gf);
1341
+ }
1342
+ }
1343
+ for (const [, groupFiles] of pending) {
1344
+ chunks.push(groupFiles);
1345
+ }
1346
+ chunks.sort((a, b) => b.length - a.length);
1347
+ const segments = Array.from({ length: count }, () => []);
1348
+ for (const chunk of chunks) {
1349
+ let minIdx = 0;
1350
+ for (let i = 1; i < count; i += 1) {
1351
+ if (segments[i].length < segments[minIdx].length) minIdx = i;
1352
+ }
1353
+ segments[minIdx].push(...chunk);
1354
+ }
1355
+ return segments;
1356
+ }
1357
+ /* -- Discovery phase -------------------------------------------- */
1358
+ async runDiscoveryPhase() {
1359
+ this.setPhase("discovery", "Running discovery agents");
1360
+ const segments = this.splitFilesIntoDirectorySegments(this.targetFiles);
1361
+ if (this.targetFiles.length === 0) {
1362
+ this.addLog("No files matched the selected target. Skipping discovery.");
1363
+ return;
1364
+ }
1365
+ const worktree = this.sharedWorktree;
1366
+ for (let i = 1; i <= this.config.instances; i += 1) {
1367
+ const agentId = `discovery-${i}`;
1368
+ const backend = this.assignBackend(agentId, i);
1369
+ this.createAgent({
1370
+ id: agentId,
1371
+ index: i,
1372
+ kind: "discovery",
1373
+ branch: worktree.branch,
1374
+ worktreePath: worktree.path,
1375
+ issuesAssigned: segments[i - 1]?.length ?? 0
1376
+ });
1377
+ this.addLog(`${agentId} assigned to ${backend}`);
1378
+ }
1379
+ await Promise.all(
1380
+ Array.from({ length: this.config.instances }, (_, index) => {
1381
+ const agentId = `discovery-${index + 1}`;
1382
+ const segment = segments[index] ?? [];
1383
+ return this.runDiscoveryAgent(agentId, worktree, segment);
1384
+ })
1385
+ );
1386
+ const actionableAgents = segments.filter((segment) => segment.length > 0).length;
1387
+ const failedAgents = this.snapshot.agents.filter((agent) => agent.kind === "discovery" && agent.status === "failed").length;
1388
+ if (actionableAgents > 0 && failedAgents >= actionableAgents) {
1389
+ throw new Error(`All discovery agents failed (${failedAgents}/${actionableAgents}).`);
1390
+ }
1391
+ this.addLog(`Discovery complete. Findings: ${this.issueStore?.issues.length ?? 0}`);
1392
+ }
1393
+ async runDiscoveryAgent(agentId, worktree, segment) {
1394
+ if (segment.length === 0) {
1395
+ this.updateAgent(agentId, { status: "skipped", startedAt: nowIso(), endedAt: nowIso(), lastMessage: "No files assigned" });
1396
+ return;
1397
+ }
1398
+ this.updateAgent(agentId, { status: "running", startedAt: nowIso(), lastMessage: `Reviewing ${segment.length} files` });
1399
+ this.addLog(`${agentId} started (${segment.length} files)`);
1400
+ const codexDir = path.join(worktree.path, ".agent-review");
1401
+ const resultDir = path.join(codexDir, "results");
1402
+ const manifestPath = path.join(codexDir, `${agentId}-manifest.txt`);
1403
+ const schemaPath = path.join(codexDir, `${agentId}-schema.json`);
1404
+ const outputPath = path.join(resultDir, `${agentId}-output.json`);
1405
+ const schema = this.discoverySchema();
1406
+ await fs.writeFile(manifestPath, `${segment.join("\n")}
1407
+ `, "utf8");
1408
+ await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf8");
1409
+ let prompt = this.discoveryPrompt(agentId, segment, manifestPath);
1410
+ if (this.isClaudeCode(agentId)) {
1411
+ prompt += `
1412
+
1413
+ CRITICAL OUTPUT REQUIREMENT:`;
1414
+ prompt += `
1415
+ You MUST write your results as valid JSON to this exact file path: ${outputPath}`;
1416
+ prompt += `
1417
+ Use the Write tool to create this file. The JSON must match this schema:
1418
+ ${JSON.stringify(schema, null, 2)}`;
1419
+ prompt += `
1420
+ If you find no issues, still write the file with an empty issues array: {"summary": "No issues found.", "issues": []}`;
1421
+ prompt += `
1422
+ Write the JSON file BEFORE your final response. Your final text response should just be the summary.`;
1423
+ prompt += `
1424
+ Failing to write this file will cause the entire review run to fail.`;
1425
+ }
1426
+ try {
1427
+ const result = await this.runBackendExec({ agentId, cwd: worktree.path, prompt, schemaPath, outputPath, yolo: false });
1428
+ if (result.exitCode !== 0) {
1429
+ const errMsg = result.stderr || result.lastMessage || `exited with code ${result.exitCode}`;
1430
+ this.updateAgent(agentId, { status: "failed", endedAt: nowIso(), error: errMsg, lastMessage: errMsg.slice(0, 600) });
1431
+ this.addLog(`${agentId} failed (exit ${result.exitCode}): ${errMsg.slice(0, 300)}`);
1432
+ return;
1433
+ }
1434
+ let payload;
1435
+ try {
1436
+ payload = await this.readJsonFile(outputPath);
1437
+ } catch (readError) {
1438
+ const msg = readError instanceof Error ? readError.message : String(readError);
1439
+ this.updateAgent(agentId, { status: "failed", endedAt: nowIso(), error: `Failed to read output: ${msg}` });
1440
+ this.addLog(`${agentId} failed: could not read output file`);
1441
+ return;
1442
+ }
1443
+ const createdIssues = payload.issues.map((item) => {
1444
+ const issueId = `ISS-${String(this.issueCounter++).padStart(4, "0")}`;
1445
+ const normalizedFile = this.normalizeIssueFile(item.location.file, worktree.path);
1446
+ const line = typeof item.location.line === "number" ? item.location.line : void 0;
1447
+ return {
1448
+ id: issueId,
1449
+ title: item.title.trim(),
1450
+ description: item.description.trim(),
1451
+ suggestedFix: item.suggested_fix.trim(),
1452
+ severity: this.normalizeSeverity(item.severity),
1453
+ location: { file: normalizedFile, line },
1454
+ status: "open",
1455
+ discoveredBy: agentId,
1456
+ confidence: typeof item.confidence === "number" ? item.confidence : void 0
1457
+ };
1458
+ });
1459
+ if (this.issueStore) {
1460
+ this.issueStore.issues.push(...createdIssues);
1461
+ }
1462
+ await this.persistIssueStore();
1463
+ this.updateAgent(agentId, {
1464
+ status: "completed",
1465
+ endedAt: nowIso(),
1466
+ issuesFound: createdIssues.length,
1467
+ lastMessage: payload.summary
1468
+ });
1469
+ this.addLog(`${agentId} completed with ${createdIssues.length} findings`);
1470
+ } catch (error) {
1471
+ this.updateAgent(agentId, { status: "failed", endedAt: nowIso(), error: error instanceof Error ? error.message : String(error) });
1472
+ this.addLog(`${agentId} errored`);
1473
+ }
1474
+ }
1475
+ /* -- Fix phase -------------------------------------------------- */
1476
+ async runFixPhase() {
1477
+ this.setPhase("fixing", "Running fix agents");
1478
+ if (!this.issueStore) return;
1479
+ const openIssues = this.issueStore.issues.filter((issue) => issue.status === "open");
1480
+ if (openIssues.length === 0) {
1481
+ this.addLog("No open issues discovered. Skipping fix phase.");
1482
+ return;
1483
+ }
1484
+ const integrationWorktree = this.sharedWorktree;
1485
+ const chunks = this.splitIntoSegments(openIssues);
1486
+ const agentWork = [];
1487
+ for (let i = 1; i <= this.config.instances; i += 1) {
1488
+ const agentId = `fix-${i}`;
1489
+ const chunk = chunks[i - 1] ?? [];
1490
+ this.assignBackend(agentId, i);
1491
+ if (chunk.length === 0) {
1492
+ this.createAgent({
1493
+ id: agentId,
1494
+ index: i,
1495
+ kind: "fix",
1496
+ branch: integrationWorktree.branch,
1497
+ worktreePath: integrationWorktree.path,
1498
+ issuesAssigned: 0
1499
+ });
1500
+ this.updateAgent(agentId, { status: "skipped", startedAt: nowIso(), endedAt: nowIso(), lastMessage: "No issues assigned" });
1501
+ continue;
1502
+ }
1503
+ const fixBranch = `${integrationWorktree.branch}-fix-${i}`;
1504
+ const worktree = await this.createWorktree({ label: `fix-${i}`, branch: fixBranch, startPoint: integrationWorktree.branch });
1505
+ const resultDir = path.join(worktree.path, ".agent-review", "results");
1506
+ await fs.mkdir(resultDir, { recursive: true });
1507
+ this.createAgent({
1508
+ id: agentId,
1509
+ index: i,
1510
+ kind: "fix",
1511
+ branch: fixBranch,
1512
+ worktreePath: worktree.path,
1513
+ issuesAssigned: chunk.length
1514
+ });
1515
+ for (const issue of chunk) {
1516
+ issue.status = "assigned";
1517
+ issue.assignedTo = agentId;
1518
+ }
1519
+ agentWork.push({ agentId, worktree, issues: chunk });
1520
+ }
1521
+ await this.persistIssueStore();
1522
+ await Promise.all(
1523
+ agentWork.map(
1524
+ ({ agentId, worktree, issues }) => this.runFixAgent(agentId, worktree, issues)
1525
+ )
1526
+ );
1527
+ if (agentWork.some(({ agentId }) => {
1528
+ const a = this.getAgent(agentId);
1529
+ return a.status === "completed" && a.issuesFixed > 0;
1530
+ })) {
1531
+ this.setPhase("integrating", "Merging fix branches");
1532
+ for (const { agentId, worktree } of agentWork) {
1533
+ const agent = this.getAgent(agentId);
1534
+ if (agent.status !== "completed" || agent.issuesFixed === 0) continue;
1535
+ const merge = await runCommand("git", [
1536
+ "-C",
1537
+ integrationWorktree.path,
1538
+ "merge",
1539
+ worktree.branch,
1540
+ "-m",
1541
+ `merge: integrate fixes from ${agentId}`
1542
+ ], { allowFailure: true });
1543
+ if (merge.code === 0) {
1544
+ this.addLog(`Merged ${agentId} fixes into integration branch`);
1545
+ } else {
1546
+ await runCommand("git", ["-C", integrationWorktree.path, "merge", "--abort"], { allowFailure: true });
1547
+ const retry = await runCommand("git", [
1548
+ "-C",
1549
+ integrationWorktree.path,
1550
+ "merge",
1551
+ "-X",
1552
+ "theirs",
1553
+ worktree.branch,
1554
+ "-m",
1555
+ `merge: integrate fixes from ${agentId} (auto-resolved)`
1556
+ ], { allowFailure: true });
1557
+ if (retry.code === 0) {
1558
+ this.addLog(`Merged ${agentId} fixes (auto-resolved conflicts)`);
1559
+ } else {
1560
+ await runCommand("git", ["-C", integrationWorktree.path, "merge", "--abort"], { allowFailure: true });
1561
+ this.addLog(`Could not merge ${agentId} fixes \u2014 skipped`);
1562
+ }
1563
+ }
1564
+ }
1565
+ }
1566
+ this.addLog(`Fix phase complete. Fixed: ${this.snapshot.issueMetrics.fixed}`);
1567
+ }
1568
+ async runFixAgent(agentId, worktree, assignedIssues) {
1569
+ this.updateAgent(agentId, { status: "running", startedAt: nowIso(), lastMessage: `Fixing ${assignedIssues.length} issues` });
1570
+ this.addLog(`${agentId} started (${assignedIssues.length} issues)`);
1571
+ const codexDir = path.join(worktree.path, ".agent-review");
1572
+ const resultDir = path.join(codexDir, "results");
1573
+ const assignmentPath = path.join(codexDir, `${agentId}-issues.json`);
1574
+ const schemaPath = path.join(codexDir, `${agentId}-schema.json`);
1575
+ const outputPath = path.join(resultDir, `${agentId}-output.json`);
1576
+ const schema = this.fixSchema();
1577
+ await fs.writeFile(assignmentPath, JSON.stringify({ assigned_issue_ids: assignedIssues.map((i) => i.id), issues: assignedIssues }, null, 2), "utf8");
1578
+ await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf8");
1579
+ let prompt = this.fixPrompt(agentId, assignedIssues, assignmentPath);
1580
+ if (this.isClaudeCode(agentId)) {
1581
+ prompt += `
1582
+
1583
+ CRITICAL OUTPUT REQUIREMENT:`;
1584
+ prompt += `
1585
+ After making fixes, you MUST write your results as valid JSON to this exact file path: ${outputPath}`;
1586
+ prompt += `
1587
+ Use the Write tool to create this file. The JSON must match this schema:
1588
+ ${JSON.stringify(schema, null, 2)}`;
1589
+ prompt += `
1590
+ Each assigned issue ID must appear exactly once in the results array.`;
1591
+ prompt += `
1592
+ Write the JSON file BEFORE your final response. Your final text response should just be the summary.`;
1593
+ prompt += `
1594
+ Failing to write this file will cause the entire fix run to fail.`;
1595
+ }
1596
+ try {
1597
+ const result = await this.runBackendExec({ agentId, cwd: worktree.path, prompt, schemaPath, outputPath, yolo: true });
1598
+ if (result.exitCode !== 0) {
1599
+ const errMsg = result.stderr || `exited with ${result.exitCode}`;
1600
+ for (const issue of assignedIssues) {
1601
+ issue.status = "failed";
1602
+ issue.failureReason = `fix agent failed: ${errMsg}`;
1603
+ }
1604
+ await this.persistIssueStore();
1605
+ this.updateAgent(agentId, { status: "failed", endedAt: nowIso(), error: errMsg, lastMessage: errMsg.slice(0, 600) });
1606
+ this.addLog(`${agentId} failed: ${errMsg.slice(0, 200)}`);
1607
+ return;
1608
+ }
1609
+ const commitCreated = await this.commitIfDirty(worktree.path, `fix(agent-review): resolve assigned issues (${agentId})`);
1610
+ let payload;
1611
+ try {
1612
+ payload = await this.readJsonFile(outputPath);
1613
+ } catch {
1614
+ if (commitCreated) {
1615
+ this.addLog(`${agentId}: no output file but commit created \u2014 assuming fixes applied`);
1616
+ payload = {
1617
+ summary: result.lastMessage ?? "Fixes applied (output file not written)",
1618
+ results: assignedIssues.map((i) => ({
1619
+ issue_id: i.id,
1620
+ status: "fixed",
1621
+ fix_summary: result.lastMessage ?? "Fixed (inferred from commit)",
1622
+ files_touched: []
1623
+ }))
1624
+ };
1625
+ } else {
1626
+ for (const issue of assignedIssues) {
1627
+ issue.status = "failed";
1628
+ issue.failureReason = "Agent completed but wrote no output and made no code changes";
1629
+ }
1630
+ await this.persistIssueStore();
1631
+ this.updateAgent(agentId, { status: "failed", endedAt: nowIso(), error: "No output file and no code changes" });
1632
+ this.addLog(`${agentId} failed: no output file and no code changes`);
1633
+ return;
1634
+ }
1635
+ }
1636
+ const byIssueId = new Map(payload.results.map((item) => [item.issue_id, item]));
1637
+ const fixedIssueIds = [];
1638
+ for (const issue of assignedIssues) {
1639
+ const entry = byIssueId.get(issue.id);
1640
+ if (entry?.status === "fixed" && commitCreated) {
1641
+ issue.status = "fixed";
1642
+ issue.fixedBy = agentId;
1643
+ issue.fixSummary = entry.fix_summary;
1644
+ issue.failureReason = void 0;
1645
+ fixedIssueIds.push(issue.id);
1646
+ continue;
1647
+ }
1648
+ issue.status = "failed";
1649
+ issue.failureReason = entry?.fix_summary || "Issue not fixed or no code changes were committed";
1650
+ }
1651
+ await this.persistIssueStore();
1652
+ this.updateAgent(agentId, { status: "completed", endedAt: nowIso(), issuesFixed: fixedIssueIds.length, lastMessage: payload.summary });
1653
+ this.addLog(`${agentId} completed with ${fixedIssueIds.length} fixes`);
1654
+ } catch (error) {
1655
+ for (const issue of assignedIssues) {
1656
+ issue.status = "failed";
1657
+ issue.failureReason = `fix agent exception: ${error instanceof Error ? error.message : String(error)}`;
1658
+ }
1659
+ await this.persistIssueStore();
1660
+ this.updateAgent(agentId, { status: "failed", endedAt: nowIso(), error: error instanceof Error ? error.message : String(error) });
1661
+ this.addLog(`${agentId} errored`);
1662
+ }
1663
+ }
1664
+ /* -- Verification ------------------------------------------------ */
1665
+ async runVerificationPhase() {
1666
+ this.setPhase("verifying", "Running final verification agent");
1667
+ const worktree = this.sharedWorktree;
1668
+ const finalAgentId = "final-1";
1669
+ this.assignBackend(finalAgentId, 1);
1670
+ this.createAgent({ id: finalAgentId, index: 1, kind: "final", branch: worktree.branch, worktreePath: worktree.path, issuesAssigned: 0 });
1671
+ this.updateAgent(finalAgentId, { status: "running", startedAt: nowIso(), lastMessage: "Running validation checks" });
1672
+ const codexDir = path.join(worktree.path, ".agent-review");
1673
+ const resultDir = path.join(codexDir, "results");
1674
+ const schema = this.verificationSchema();
1675
+ const schemaPath = path.join(codexDir, "verify-schema.json");
1676
+ const outputPath = path.join(resultDir, "verify-output.json");
1677
+ await fs.writeFile(schemaPath, JSON.stringify(schema, null, 2), "utf8");
1678
+ let prompt = this.verificationPrompt();
1679
+ if (this.isClaudeCode(finalAgentId)) {
1680
+ prompt += `
1681
+
1682
+ CRITICAL OUTPUT REQUIREMENT:`;
1683
+ prompt += `
1684
+ After verification, you MUST write your results as valid JSON to this exact file path: ${outputPath}`;
1685
+ prompt += `
1686
+ Use the Write tool to create this file. The JSON must match this schema:
1687
+ ${JSON.stringify(schema, null, 2)}`;
1688
+ prompt += `
1689
+ Write the JSON file BEFORE your final response. Your final text response should just be the summary.`;
1690
+ prompt += `
1691
+ Failing to write this file will cause the entire verification to fail.`;
1692
+ }
1693
+ try {
1694
+ const result = await this.runBackendExec({ agentId: finalAgentId, cwd: worktree.path, prompt, schemaPath, outputPath, yolo: true });
1695
+ if (result.exitCode !== 0) {
1696
+ const errMsg = result.stderr || `exited with ${result.exitCode}`;
1697
+ this.updateAgent(finalAgentId, { status: "failed", endedAt: nowIso(), error: errMsg, lastMessage: errMsg.slice(0, 600) });
1698
+ this.addLog(`Final verification agent failed: ${errMsg.slice(0, 200)}`);
1699
+ await this.writeFinalReport(void 0, `Final verification agent failed: ${errMsg}`);
1700
+ throw new VerificationFailure(`Final verification agent failed: ${errMsg}`);
1701
+ }
1702
+ let payload;
1703
+ try {
1704
+ payload = await this.readJsonFile(outputPath);
1705
+ } catch (readError) {
1706
+ const msg = readError instanceof Error ? readError.message : String(readError);
1707
+ this.updateAgent(finalAgentId, { status: "failed", endedAt: nowIso(), error: `Failed to read output: ${msg}` });
1708
+ this.addLog("Final verification failed: could not read output file");
1709
+ await this.writeFinalReport(void 0, `Could not read verification output: ${msg}`);
1710
+ throw new VerificationFailure(`Could not read verification output: ${msg}`);
1711
+ }
1712
+ await this.commitIfDirty(worktree.path, "chore(agent-review): final verification follow-ups");
1713
+ this.updateAgent(finalAgentId, { status: "completed", endedAt: nowIso(), lastMessage: payload.summary });
1714
+ this.addLog("Final verification completed");
1715
+ await this.writeFinalReport(payload);
1716
+ } catch (error) {
1717
+ if (error instanceof VerificationFailure) {
1718
+ throw error;
1719
+ }
1720
+ const message = error instanceof Error ? error.message : String(error);
1721
+ this.updateAgent(finalAgentId, { status: "failed", endedAt: nowIso(), error: message });
1722
+ this.addLog("Final verification errored");
1723
+ await this.writeFinalReport(void 0, message);
1724
+ throw new VerificationFailure(message);
1725
+ }
1726
+ }
1727
+ /* -- Publish: rebase, push, and create PR ------------------------ */
1728
+ async getDefaultBranch() {
1729
+ try {
1730
+ const result = await runCommand("gh", ["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], { cwd: this.repoRoot });
1731
+ const branch = result.stdout.trim();
1732
+ if (branch) return branch;
1733
+ } catch {
1734
+ }
1735
+ try {
1736
+ const result = await runCommand("git", ["-C", this.repoRoot, "symbolic-ref", "refs/remotes/origin/HEAD"], { allowFailure: true });
1737
+ const match = result.stdout.trim().match(/refs\/remotes\/origin\/(.+)/);
1738
+ if (match?.[1]) return match[1];
1739
+ } catch {
1740
+ }
1741
+ return "main";
1742
+ }
1743
+ async publishIntegrationBranch() {
1744
+ const worktree = this.sharedWorktree;
1745
+ const branch = this.snapshot.integrationBranch;
1746
+ if (!worktree || !branch) {
1747
+ this.addLog("Skipping publish: no integration branch");
1748
+ return;
1749
+ }
1750
+ const currentHead = (await runCommand("git", ["-C", worktree.path, "rev-parse", "HEAD"])).stdout.trim();
1751
+ if (currentHead === this.baseCommit) {
1752
+ this.addLog("No changes to publish");
1753
+ return;
1754
+ }
1755
+ this.setPhase("publishing", "Rebasing and publishing PR");
1756
+ try {
1757
+ const defaultBranch = await this.getDefaultBranch();
1758
+ this.addLog(`Default branch: ${defaultBranch}`);
1759
+ await runCommand("git", ["-C", worktree.path, "fetch", "origin", defaultBranch]);
1760
+ const rebase = await runCommand("git", [
1761
+ "-C",
1762
+ worktree.path,
1763
+ "rebase",
1764
+ "-X",
1765
+ "theirs",
1766
+ `origin/${defaultBranch}`
1767
+ ], { allowFailure: true });
1768
+ if (rebase.code !== 0) {
1769
+ this.addLog("Rebase had conflicts, attempting merge fallback");
1770
+ await runCommand("git", ["-C", worktree.path, "rebase", "--abort"], { allowFailure: true });
1771
+ const merge = await runCommand("git", [
1772
+ "-C",
1773
+ worktree.path,
1774
+ "merge",
1775
+ "-X",
1776
+ "ours",
1777
+ `origin/${defaultBranch}`,
1778
+ "-m",
1779
+ `merge: integrate with latest ${defaultBranch}`
1780
+ ], { allowFailure: true });
1781
+ if (merge.code !== 0) {
1782
+ await runCommand("git", ["-C", worktree.path, "merge", "--abort"], { allowFailure: true });
1783
+ this.addLog("Could not resolve merge conflicts automatically, pushing as-is");
1784
+ }
1785
+ } else {
1786
+ this.addLog("Rebased onto latest " + defaultBranch);
1787
+ }
1788
+ const push = await runCommand("git", [
1789
+ "-C",
1790
+ worktree.path,
1791
+ "push",
1792
+ "-u",
1793
+ "origin",
1794
+ `HEAD:refs/heads/${branch}`
1795
+ ], { allowFailure: true });
1796
+ if (push.code !== 0) {
1797
+ this.addLog(`Push failed: ${push.stderr.trim()}`);
1798
+ return;
1799
+ }
1800
+ this.addLog(`Pushed branch: ${branch}`);
1801
+ const im = this.snapshot.issueMetrics;
1802
+ const prTitle = `[agent-review] ${this.config.targetPrompt.slice(0, 70)}`;
1803
+ const prBody = [
1804
+ "## Agent Review",
1805
+ "",
1806
+ `Automated code review and fix by \`agent-review\` using **${this.config.backend}**.`,
1807
+ "",
1808
+ `**Target:** ${this.config.targetPrompt}`,
1809
+ "",
1810
+ "### Results",
1811
+ "",
1812
+ `| Metric | Count |`,
1813
+ `|--------|-------|`,
1814
+ `| Issues found | ${im.found} |`,
1815
+ `| Issues fixed | ${im.fixed} |`,
1816
+ `| Issues failed | ${im.failed} |`,
1817
+ `| Issues open | ${im.open} |`,
1818
+ "",
1819
+ `Run ID: \`${this.runId}\``
1820
+ ].join("\n");
1821
+ const pr = await runCommand("gh", [
1822
+ "pr",
1823
+ "create",
1824
+ "--base",
1825
+ defaultBranch,
1826
+ "--head",
1827
+ branch,
1828
+ "--title",
1829
+ prTitle,
1830
+ "--body",
1831
+ prBody
1832
+ ], { cwd: this.repoRoot, allowFailure: true });
1833
+ if (pr.code === 0) {
1834
+ const prUrl = pr.stdout.trim();
1835
+ this.snapshot.prUrl = prUrl;
1836
+ this.addLog(`PR created: ${prUrl}`);
1837
+ } else {
1838
+ this.addLog(`PR creation failed: ${pr.stderr.trim()}`);
1839
+ }
1840
+ } catch (error) {
1841
+ const message = error instanceof Error ? error.message : String(error);
1842
+ this.addLog(`Publish failed: ${message}`);
1843
+ }
1844
+ }
1845
+ /* -- Report ----------------------------------------------------- */
1846
+ async writeFinalReport(verificationOutput, verificationError) {
1847
+ const im = this.snapshot.issueMetrics;
1848
+ const tu = this.snapshot.tokenUsage;
1849
+ const unresolved = this.issueStore?.issues.filter((issue) => issue.status !== "fixed") ?? [];
1850
+ const preview = unresolved.slice(0, 20);
1851
+ const lines = [];
1852
+ lines.push("# Agent Review Report");
1853
+ lines.push("");
1854
+ lines.push(`- Run ID: ${this.runId}`);
1855
+ lines.push(`- Backend: ${this.config.backend}`);
1856
+ lines.push(`- Repo: ${this.repoRoot}`);
1857
+ lines.push(`- Base Branch: ${this.currentBranch}`);
1858
+ lines.push(`- Base Commit: ${this.baseCommit}`);
1859
+ lines.push(`- Target Prompt: ${this.config.targetPrompt}`);
1860
+ lines.push(`- Target Paths: ${this.snapshot.targetPaths.join(", ")}`);
1861
+ lines.push(`- Instances: ${this.config.instances}`);
1862
+ lines.push("");
1863
+ lines.push("## Issue Summary");
1864
+ lines.push("");
1865
+ lines.push(`- Found: ${im.found}`);
1866
+ lines.push(`- Fixed: ${im.fixed}`);
1867
+ lines.push(`- Open: ${im.open}`);
1868
+ lines.push(`- Failed: ${im.failed}`);
1869
+ lines.push("");
1870
+ lines.push("## Token Usage");
1871
+ lines.push("");
1872
+ lines.push(`- Input: ${tu.input}`);
1873
+ lines.push(`- Cached Input: ${tu.cachedInput}`);
1874
+ lines.push(`- Output: ${tu.output}`);
1875
+ lines.push("");
1876
+ lines.push("## Verification");
1877
+ lines.push("");
1878
+ if (verificationError) {
1879
+ lines.push(`- Status: failed`);
1880
+ lines.push(`- Error: ${verificationError}`);
1881
+ } else if (verificationOutput) {
1882
+ lines.push(`- Status: completed`);
1883
+ lines.push(`- Summary: ${verificationOutput.summary}`);
1884
+ lines.push("");
1885
+ lines.push("### Checks");
1886
+ lines.push("");
1887
+ for (const check of verificationOutput.checks_run) {
1888
+ lines.push(`- ${check.command} -> ${check.result}: ${check.details}`);
1889
+ }
1890
+ if (verificationOutput.unresolved_risks.length > 0) {
1891
+ lines.push("");
1892
+ lines.push("### Unresolved Risks");
1893
+ lines.push("");
1894
+ for (const risk of verificationOutput.unresolved_risks) {
1895
+ lines.push(`- ${risk}`);
1896
+ }
1897
+ }
1898
+ } else {
1899
+ lines.push("- Status: skipped");
1900
+ }
1901
+ lines.push("");
1902
+ if (preview.length > 0) {
1903
+ lines.push("## Remaining Issues");
1904
+ lines.push("");
1905
+ for (const issue of preview) {
1906
+ const loc = issue.location.line !== void 0 ? `${issue.location.file}:${issue.location.line}` : issue.location.file;
1907
+ lines.push(`- ${issue.id} [${issue.status}] ${issue.title} (${loc})`);
1908
+ }
1909
+ if (unresolved.length > preview.length) {
1910
+ lines.push(`- ... ${unresolved.length - preview.length} additional issues`);
1911
+ }
1912
+ lines.push("");
1913
+ }
1914
+ await fs.writeFile(this.reportFile, `${lines.join("\n")}
1915
+ `, "utf8");
1916
+ this.snapshot.reportFile = this.reportFile;
1917
+ this.emitSnapshot();
1918
+ }
1919
+ /* -- Helpers ---------------------------------------------------- */
1920
+ normalizeIssueFile(rawFile, worktreePath) {
1921
+ const trimmed = rawFile.trim();
1922
+ if (!trimmed) return ".";
1923
+ if (path.isAbsolute(trimmed)) {
1924
+ if (isInsidePath(trimmed, worktreePath)) return normalizeSlashes(path.relative(worktreePath, trimmed));
1925
+ if (isInsidePath(trimmed, this.repoRoot)) return normalizeSlashes(path.relative(this.repoRoot, trimmed));
1926
+ return normalizeSlashes(trimmed);
1927
+ }
1928
+ return normalizeSlashes(trimmed);
1929
+ }
1930
+ normalizeSeverity(raw) {
1931
+ if (["critical", "high", "medium", "low", "info"].includes(raw)) return raw;
1932
+ return "medium";
1933
+ }
1934
+ async persistIssueStore() {
1935
+ if (!this.issueStore || !this.issueFile) return;
1936
+ this.persistQueue = this.persistQueue.then(async () => {
1937
+ this.issueStore.metadata.updatedAt = nowIso();
1938
+ const tmpPath = `${this.issueFile}.tmp`;
1939
+ await fs.writeFile(tmpPath, JSON.stringify(this.issueStore, null, 2), "utf8");
1940
+ await fs.rename(tmpPath, this.issueFile);
1941
+ });
1942
+ await this.persistQueue;
1943
+ this.emitSnapshot();
1944
+ }
1945
+ async readJsonFile(filePath) {
1946
+ const raw = await fs.readFile(filePath, "utf8");
1947
+ return parseJson(raw);
1948
+ }
1949
+ /* -- Schemas ---------------------------------------------------- */
1950
+ discoverySchema() {
1951
+ return {
1952
+ type: "object",
1953
+ additionalProperties: false,
1954
+ required: ["summary", "issues"],
1955
+ properties: {
1956
+ summary: { type: "string" },
1957
+ issues: {
1958
+ type: "array",
1959
+ items: {
1960
+ type: "object",
1961
+ additionalProperties: false,
1962
+ required: ["title", "description", "suggested_fix", "severity", "confidence", "location"],
1963
+ properties: {
1964
+ title: { type: "string" },
1965
+ description: { type: "string" },
1966
+ suggested_fix: { type: "string" },
1967
+ severity: { type: "string", enum: ["critical", "high", "medium", "low", "info"] },
1968
+ confidence: { type: ["number", "null"] },
1969
+ location: {
1970
+ type: "object",
1971
+ additionalProperties: false,
1972
+ required: ["file", "line"],
1973
+ properties: { file: { type: "string" }, line: { type: ["number", "null"] } }
1974
+ }
1975
+ }
1976
+ }
1977
+ }
1978
+ }
1979
+ };
1980
+ }
1981
+ fixSchema() {
1982
+ return {
1983
+ type: "object",
1984
+ additionalProperties: false,
1985
+ required: ["summary", "results"],
1986
+ properties: {
1987
+ summary: { type: "string" },
1988
+ results: {
1989
+ type: "array",
1990
+ items: {
1991
+ type: "object",
1992
+ additionalProperties: false,
1993
+ required: ["issue_id", "status", "fix_summary", "files_touched"],
1994
+ properties: {
1995
+ issue_id: { type: "string" },
1996
+ status: { type: "string", enum: ["fixed", "not_fixed"] },
1997
+ fix_summary: { type: "string" },
1998
+ files_touched: { type: "array", items: { type: "string" } }
1999
+ }
2000
+ }
2001
+ }
2002
+ }
2003
+ };
2004
+ }
2005
+ verificationSchema() {
2006
+ return {
2007
+ type: "object",
2008
+ additionalProperties: false,
2009
+ required: ["summary", "checks_run", "changes_made", "unresolved_risks"],
2010
+ properties: {
2011
+ summary: { type: "string" },
2012
+ checks_run: {
2013
+ type: "array",
2014
+ items: {
2015
+ type: "object",
2016
+ additionalProperties: false,
2017
+ required: ["command", "result", "details"],
2018
+ properties: {
2019
+ command: { type: "string" },
2020
+ result: { type: "string", enum: ["passed", "failed", "skipped"] },
2021
+ details: { type: "string" }
2022
+ }
2023
+ }
2024
+ },
2025
+ changes_made: { type: "boolean" },
2026
+ unresolved_risks: { type: "array", items: { type: "string" } }
2027
+ }
2028
+ };
2029
+ }
2030
+ /* -- Prompts ---------------------------------------------------- */
2031
+ /**
2032
+ * Try to extract valid JSON from text that may contain markdown code blocks or extra prose.
2033
+ */
2034
+ static extractJson(text) {
2035
+ try {
2036
+ JSON.parse(text.trim());
2037
+ return text.trim();
2038
+ } catch {
2039
+ }
2040
+ const codeBlockMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
2041
+ if (codeBlockMatch) {
2042
+ try {
2043
+ JSON.parse(codeBlockMatch[1].trim());
2044
+ return codeBlockMatch[1].trim();
2045
+ } catch {
2046
+ }
2047
+ }
2048
+ try {
2049
+ const result = parseJson(text);
2050
+ return JSON.stringify(result);
2051
+ } catch {
2052
+ }
2053
+ return void 0;
2054
+ }
2055
+ /**
2056
+ * Extract unique directory paths from a file list, sorted, for prompt context.
2057
+ */
2058
+ static summarizeDirectories(files) {
2059
+ const dirs = /* @__PURE__ */ new Set();
2060
+ for (const f of files) {
2061
+ const dir = f.includes("/") ? f.slice(0, f.lastIndexOf("/")) : ".";
2062
+ dirs.add(dir);
2063
+ }
2064
+ return [...dirs].sort();
2065
+ }
2066
+ /**
2067
+ * Build a compact tree-like overview of a file segment, grouped by top-level directory,
2068
+ * with file counts and extension breakdown per directory.
2069
+ */
2070
+ static buildSegmentOverview(files) {
2071
+ const groups = /* @__PURE__ */ new Map();
2072
+ for (const f of files) {
2073
+ const parts = f.split("/");
2074
+ const key = parts.length > 2 ? `${parts[0]}/${parts[1]}` : parts[0];
2075
+ const group = groups.get(key) ?? [];
2076
+ group.push(f);
2077
+ groups.set(key, group);
2078
+ }
2079
+ const lines = [];
2080
+ for (const [dir, dirFiles] of [...groups.entries()].sort()) {
2081
+ const extCounts = /* @__PURE__ */ new Map();
2082
+ for (const f of dirFiles) {
2083
+ const ext = path.extname(f) || "(no ext)";
2084
+ extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
2085
+ }
2086
+ const extSummary = [...extCounts.entries()].sort((a, b) => b[1] - a[1]).map(([ext, count]) => `${count}${ext}`).join(", ");
2087
+ lines.push(` ${dir}/ (${dirFiles.length} files: ${extSummary})`);
2088
+ const subDirs = /* @__PURE__ */ new Set();
2089
+ for (const f of dirFiles) {
2090
+ const rel = f.slice(dir.length + 1);
2091
+ const subDir = rel.includes("/") ? rel.slice(0, rel.indexOf("/")) : null;
2092
+ if (subDir) subDirs.add(subDir);
2093
+ }
2094
+ if (subDirs.size > 0 && subDirs.size <= 15) {
2095
+ lines.push(` subdirs: ${[...subDirs].sort().join(", ")}`);
2096
+ }
2097
+ }
2098
+ return lines.join("\n");
2099
+ }
2100
+ discoveryPrompt(agentId, segment, manifestPath) {
2101
+ const overview = _AgentReviewOrchestrator.buildSegmentOverview(segment);
2102
+ const topDirs = [...new Set(segment.map((f) => {
2103
+ const parts = f.split("/");
2104
+ return parts.length > 2 ? `${parts[0]}/${parts[1]}` : parts[0];
2105
+ }))].sort();
2106
+ return [
2107
+ `You are ${agentId}, a discovery-only code review agent.`,
2108
+ `Task: perform a deep review focused on logic bugs, type/runtime errors, state handling bugs, edge case failures, and security defects.`,
2109
+ "",
2110
+ "== CONTEXT ==",
2111
+ `Repo root: ${this.repoRoot}`,
2112
+ `Current branch: ${this.currentBranch}`,
2113
+ `User review target: "${this.config.targetPrompt}"`,
2114
+ `Total agents: ${this.config.instances} (running in parallel, each reviewing different sections)`,
2115
+ "",
2116
+ "== YOUR ASSIGNED SECTION ==",
2117
+ `You are responsible for reviewing ${segment.length} files in these areas:`,
2118
+ "",
2119
+ overview,
2120
+ "",
2121
+ `Full file list is in the manifest: ${manifestPath}`,
2122
+ "",
2123
+ "== SCOPE RULES ==",
2124
+ `- ONLY review files within YOUR assigned directories: ${topDirs.join(", ")}`,
2125
+ "- Do NOT review files outside your section. Other agents handle those.",
2126
+ "- You MAY read files outside your section for context (e.g. shared types, imports, config).",
2127
+ "",
2128
+ "== REVIEW GUIDELINES ==",
2129
+ "1. Do NOT modify any files. This is discovery only.",
2130
+ "2. Read files from the manifest and review them in depth.",
2131
+ "3. Report only concrete, actionable defects. Skip style, formatting, or naming nits.",
2132
+ "4. Include precise file paths and line numbers for each issue.",
2133
+ "5. For each issue explain WHY it is a bug and provide a specific fix suggestion.",
2134
+ "6. If unsure whether something is a real bug, do not report it.",
2135
+ "",
2136
+ "== WORKFLOW ==",
2137
+ "1. Read the manifest file to see your complete file list.",
2138
+ "2. Work through each directory in your assigned section systematically.",
2139
+ "3. For each file, look for logic bugs, type errors, race conditions, missing error handling, data loss risks, and security issues.",
2140
+ "4. Return your findings as JSON matching the output schema exactly."
2141
+ ].join("\n");
2142
+ }
2143
+ fixPrompt(agentId, assignedIssues, assignedIssuesPath) {
2144
+ const issueFiles = [...new Set(assignedIssues.map((i) => i.location.file))].sort();
2145
+ const issueDirs = _AgentReviewOrchestrator.summarizeDirectories(issueFiles);
2146
+ const fileListing = issueFiles.map((f) => ` - ${f}`).join("\n");
2147
+ const dirListing = issueDirs.map((d) => ` - ${d}/`).join("\n");
2148
+ return [
2149
+ `You are ${agentId}, a fix agent.`,
2150
+ "Task: resolve ONLY the assigned issues and nothing else.",
2151
+ "",
2152
+ "Context:",
2153
+ `- Repo root: ${this.repoRoot}`,
2154
+ `- Branch: ${this.currentBranch}`,
2155
+ `- Number of assigned issues: ${assignedIssues.length}`,
2156
+ `- Assigned issues file (full details): ${assignedIssuesPath}`,
2157
+ "",
2158
+ "YOUR ASSIGNED SCOPE (only work within these areas):",
2159
+ "Directories:",
2160
+ dirListing,
2161
+ "Files containing issues:",
2162
+ fileListing,
2163
+ "",
2164
+ `There are ${this.config.instances} fix agents, each responsible for different issues. Only fix YOUR assigned issues.`,
2165
+ "",
2166
+ "Hard rules:",
2167
+ "1. Fix only issue IDs listed in the assigned issues file.",
2168
+ "2. No broad refactors or unrelated cleanup.",
2169
+ "3. Keep behavior stable except for the necessary bug fixes.",
2170
+ "4. Make edits directly in this worktree.",
2171
+ "5. If an issue cannot be fixed safely, mark it as not_fixed with reason.",
2172
+ "",
2173
+ "Execution guidance:",
2174
+ "- Read the assigned issues file first to understand all issues you need to fix.",
2175
+ "- Validate each fix with minimal relevant checks where practical.",
2176
+ "- Prefer targeted checks over expensive full-suite runs.",
2177
+ "",
2178
+ "Return JSON matching the output schema exactly. Each assigned issue ID must appear once in results."
2179
+ ].join("\n");
2180
+ }
2181
+ verificationPrompt() {
2182
+ return [
2183
+ "You are the final verification agent for this agent-review run.",
2184
+ "Goal: validate integrated fixes and produce a short reliability report.",
2185
+ "",
2186
+ "Steps:",
2187
+ "1. Inspect package scripts and identify practical quality gates.",
2188
+ "2. Run relevant typecheck/build/test commands using judgment.",
2189
+ "3. If tests appear stale or irrelevant, skip with a concise justification.",
2190
+ "4. If checks fail because of real regressions, apply minimal corrective fixes.",
2191
+ "5. Keep changes narrowly scoped to verification failures.",
2192
+ "",
2193
+ "Return JSON matching the output schema exactly."
2194
+ ].join("\n");
2195
+ }
2196
+ /* -- Git worktree management ------------------------------------ */
2197
+ async createWorktree(input2) {
2198
+ const worktreePath = path.join(this.worktreesDir, input2.label);
2199
+ await runCommand("git", [
2200
+ "-C",
2201
+ this.repoRoot,
2202
+ "worktree",
2203
+ "add",
2204
+ "--force",
2205
+ "-b",
2206
+ input2.branch,
2207
+ worktreePath,
2208
+ input2.startPoint ?? this.currentBranch
2209
+ ]);
2210
+ this.createdWorktrees.add(worktreePath);
2211
+ return { label: input2.label, branch: input2.branch, path: worktreePath };
2212
+ }
2213
+ async commitIfDirty(worktreePath, message) {
2214
+ const status = (await runCommand("git", ["-C", worktreePath, "status", "--porcelain"])).stdout.trim();
2215
+ if (!status) return false;
2216
+ await runCommand("git", ["-C", worktreePath, "add", "-A"]);
2217
+ const commit = await runCommand("git", ["-C", worktreePath, "commit", "-m", message], { allowFailure: true });
2218
+ return commit.code === 0;
2219
+ }
2220
+ /** Assign a backend to an agent (round-robin across available backends) */
2221
+ assignBackend(agentId, index) {
2222
+ const backend = this.availableBackends[(index - 1) % this.availableBackends.length];
2223
+ this.agentBackends.set(agentId, backend);
2224
+ return backend;
2225
+ }
2226
+ /** Get the backend assigned to a specific agent */
2227
+ getAgentBackend(agentId) {
2228
+ return this.agentBackends.get(agentId) ?? this.availableBackends[0];
2229
+ }
2230
+ /** Get the driver for a specific agent */
2231
+ getAgentDriver(agentId) {
2232
+ const backend = this.getAgentBackend(agentId);
2233
+ return this.drivers.get(backend) ?? this.driver;
2234
+ }
2235
+ /** Check if a specific agent uses the claude-code backend */
2236
+ isClaudeCode(agentId) {
2237
+ return this.getAgentBackend(agentId) === "claude-code";
2238
+ }
2239
+ async prepareCodexHome(agentId) {
2240
+ if (this.getAgentBackend(agentId) !== "codex") return void 0;
2241
+ const existing = this.preparedCodexHomes.get(agentId);
2242
+ if (existing) return existing;
2243
+ const setup = this.createCodexHome(agentId).catch((error) => {
2244
+ const message = error instanceof Error ? error.message : String(error);
2245
+ this.addLog(`${agentId} codex home setup warning: ${message}`);
2246
+ return void 0;
2247
+ });
2248
+ this.preparedCodexHomes.set(agentId, setup);
2249
+ return setup;
2250
+ }
2251
+ async createCodexHome(agentId) {
2252
+ const codexHome = path.join(this.codexHomesDir, agentId);
2253
+ await fs.mkdir(codexHome, { recursive: true });
2254
+ const sourceHome = process.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");
2255
+ if (await pathExists(sourceHome)) {
2256
+ await this.copyFileIfPresent(path.join(sourceHome, "auth.json"), path.join(codexHome, "auth.json"));
2257
+ await this.copyFileIfPresent(path.join(sourceHome, "config.toml"), path.join(codexHome, "config.toml"));
2258
+ await this.copyFileIfPresent(path.join(sourceHome, ".codex-global-state.json"), path.join(codexHome, ".codex-global-state.json"));
2259
+ await this.copyFileIfPresent(path.join(sourceHome, "version.json"), path.join(codexHome, "version.json"));
2260
+ await this.copyFileIfPresent(path.join(sourceHome, "models_cache.json"), path.join(codexHome, "models_cache.json"));
2261
+ }
2262
+ return codexHome;
2263
+ }
2264
+ async copyFileIfPresent(sourcePath, targetPath) {
2265
+ try {
2266
+ if (!await pathExists(sourcePath)) return;
2267
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
2268
+ await fs.copyFile(sourcePath, targetPath);
2269
+ } catch {
2270
+ }
2271
+ }
2272
+ shouldRetryCodexExec(result, attempt, maxAttempts) {
2273
+ if (attempt >= maxAttempts) return false;
2274
+ if (result.timedOut) return false;
2275
+ if (result.exitCode === 0) return false;
2276
+ const combined = `${result.stderr}
2277
+ ${result.lastMessage ?? ""}`.toLowerCase();
2278
+ if (!combined.trim()) return true;
2279
+ return RETRYABLE_CODEX_FAILURES.some((pattern) => combined.includes(pattern));
2280
+ }
2281
+ async sleep(ms) {
2282
+ await new Promise((resolve) => setTimeout(resolve, ms));
2283
+ }
2284
+ /* -- Backend exec ----------------------------------------------- */
2285
+ async runBackendExec(input2) {
2286
+ const backend = this.getAgentBackend(input2.agentId);
2287
+ const maxAttempts = backend === "codex" ? 2 : 1;
2288
+ let lastResult;
2289
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2290
+ if (attempt > 1) {
2291
+ this.addLog(`${input2.agentId} retrying ${backend} execution (attempt ${attempt}/${maxAttempts})`);
2292
+ }
2293
+ const result = await this.runBackendExecOnce(input2, attempt);
2294
+ lastResult = result;
2295
+ if (backend !== "codex" || !this.shouldRetryCodexExec(result, attempt, maxAttempts)) {
2296
+ return result;
2297
+ }
2298
+ await this.sleep(750 * attempt);
2299
+ }
2300
+ return lastResult;
2301
+ }
2302
+ async runBackendExecOnce(input2, attempt) {
2303
+ const agentDriver = this.getAgentDriver(input2.agentId);
2304
+ const agentBackend = this.getAgentBackend(input2.agentId);
2305
+ const { command, args } = agentDriver.buildArgs({
2306
+ yolo: input2.yolo,
2307
+ model: this.config.model,
2308
+ schemaPath: input2.schemaPath,
2309
+ outputPath: input2.outputPath,
2310
+ prompt: input2.prompt
2311
+ });
2312
+ const childEnv = { ...process.env };
2313
+ const codexHome = await this.prepareCodexHome(input2.agentId);
2314
+ if (agentBackend === "codex" && codexHome) {
2315
+ childEnv.CODEX_HOME = codexHome;
2316
+ }
2317
+ this.addLog(`${input2.agentId} spawning ${agentBackend}${attempt > 1 ? ` (attempt ${attempt})` : ""}`);
2318
+ const child = spawn(command, args, { cwd: input2.cwd, env: childEnv, stdio: "pipe" });
2319
+ let stderr = "";
2320
+ let buffered = "";
2321
+ let stdoutBytes = 0;
2322
+ let lastMessage;
2323
+ let fullResultText;
2324
+ let lastAssistantFullText;
2325
+ const usage = { ...EMPTY_USAGE };
2326
+ let timedOut = false;
2327
+ const startTime = Date.now();
2328
+ child.stdout.setEncoding("utf8");
2329
+ child.stderr.setEncoding("utf8");
2330
+ child.stdin.end();
2331
+ const killChild = () => {
2332
+ if (timedOut) return;
2333
+ timedOut = true;
2334
+ const el = Math.round((Date.now() - startTime) / 1e3);
2335
+ this.addLog(`${input2.agentId} timed out after ${el}s`);
2336
+ try {
2337
+ child.kill("SIGTERM");
2338
+ } catch {
2339
+ }
2340
+ setTimeout(() => {
2341
+ try {
2342
+ child.kill("SIGKILL");
2343
+ } catch {
2344
+ }
2345
+ }, 5e3);
2346
+ };
2347
+ const overallTimer = setTimeout(killChild, this.codexTimeoutMs);
2348
+ let inactivityTimer = setTimeout(killChild, this.codexInactivityTimeoutMs);
2349
+ const resetInactivity = () => {
2350
+ clearTimeout(inactivityTimer);
2351
+ inactivityTimer = setTimeout(killChild, this.codexInactivityTimeoutMs);
2352
+ };
2353
+ const processJsonLine = (line, emitAgentUpdate) => {
2354
+ if (!line) return;
2355
+ try {
2356
+ const parsed = JSON.parse(line);
2357
+ const result = agentDriver.parseStdoutLine(parsed);
2358
+ if (result.tokenIncrement) {
2359
+ usage.input += result.tokenIncrement.input;
2360
+ usage.cachedInput += result.tokenIncrement.cachedInput;
2361
+ usage.output += result.tokenIncrement.output;
2362
+ this.addAgentUsage(input2.agentId, result.tokenIncrement);
2363
+ }
2364
+ if (result.message) {
2365
+ lastMessage = result.message;
2366
+ if (emitAgentUpdate) this.updateAgent(input2.agentId, { lastMessage });
2367
+ }
2368
+ const pType = parsed.type;
2369
+ if (pType === "result" || pType === "turn_result") {
2370
+ const rc = parsed.result ?? parsed.content ?? parsed.text ?? parsed.message;
2371
+ if (typeof rc === "string") {
2372
+ fullResultText = rc;
2373
+ } else if (Array.isArray(rc)) {
2374
+ const texts = rc.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
2375
+ if (texts.length > 0) fullResultText = texts.join("\n");
2376
+ }
2377
+ }
2378
+ if (pType === "assistant" || pType === "message") {
2379
+ const ac = parsed.content ?? parsed.message ?? parsed.text;
2380
+ if (typeof ac === "string") {
2381
+ lastAssistantFullText = ac;
2382
+ } else if (Array.isArray(ac)) {
2383
+ const texts = ac.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
2384
+ const combined = texts.join("\n");
2385
+ if (combined) lastAssistantFullText = combined;
2386
+ }
2387
+ }
2388
+ } catch {
2389
+ }
2390
+ };
2391
+ child.stdout.on("data", (chunk) => {
2392
+ resetInactivity();
2393
+ stdoutBytes += Buffer.byteLength(chunk, "utf8");
2394
+ buffered += chunk;
2395
+ while (buffered.includes("\n")) {
2396
+ const idx = buffered.indexOf("\n");
2397
+ const line = buffered.slice(0, idx).trim();
2398
+ buffered = buffered.slice(idx + 1);
2399
+ processJsonLine(line, true);
2400
+ }
2401
+ });
2402
+ child.stderr.on("data", (chunk) => {
2403
+ resetInactivity();
2404
+ stderr += chunk;
2405
+ });
2406
+ const exitCode = await new Promise((resolve, reject) => {
2407
+ child.on("error", reject);
2408
+ child.on("close", (code) => resolve(code ?? 1));
2409
+ });
2410
+ clearTimeout(overallTimer);
2411
+ clearTimeout(inactivityTimer);
2412
+ if (buffered.trim()) {
2413
+ const remainingLines = buffered.split("\n").map((l) => l.trim()).filter(Boolean);
2414
+ for (const line of remainingLines) {
2415
+ processJsonLine(line, false);
2416
+ }
2417
+ }
2418
+ const capturedText = fullResultText ?? lastAssistantFullText;
2419
+ if (capturedText) {
2420
+ const outputExists = await pathExists(input2.outputPath);
2421
+ if (!outputExists) {
2422
+ const jsonContent = _AgentReviewOrchestrator.extractJson(capturedText);
2423
+ if (jsonContent) {
2424
+ try {
2425
+ await fs.mkdir(path.dirname(input2.outputPath), { recursive: true });
2426
+ await fs.writeFile(input2.outputPath, jsonContent, "utf8");
2427
+ } catch {
2428
+ }
2429
+ }
2430
+ }
2431
+ }
2432
+ const hasOutput = await pathExists(input2.outputPath);
2433
+ if (!lastMessage || !hasOutput) {
2434
+ const diag = [
2435
+ `exit=${exitCode}`,
2436
+ `stdout=${stdoutBytes}b`,
2437
+ `stderr=${stderr.length}b`,
2438
+ `output_file=${hasOutput ? "yes" : "no"}`
2439
+ ].join(", ");
2440
+ this.addLog(`${input2.agentId} diagnostic: ${diag}`);
2441
+ if (stderr.trim()) {
2442
+ this.addLog(`${input2.agentId} stderr: ${stderr.trim().slice(0, 500)}`);
2443
+ }
2444
+ }
2445
+ const stderrResult = timedOut ? `Process timed out after ${Math.round((Date.now() - startTime) / 1e3)}s${stderr.trim() ? ": " + stderr.trim() : ""}` : stderr.trim();
2446
+ return {
2447
+ exitCode,
2448
+ usage,
2449
+ timedOut,
2450
+ durationMs: Date.now() - startTime,
2451
+ lastMessage,
2452
+ resultText: capturedText,
2453
+ stderr: stderrResult
2454
+ };
2455
+ }
2456
+ async cleanupWorktrees() {
2457
+ if (!this.repoRoot) return;
2458
+ this.addLog("Cleaning up worktrees");
2459
+ for (const worktreePath of this.createdWorktrees) {
2460
+ const result = await runCommand(
2461
+ "git",
2462
+ ["-C", this.repoRoot, "worktree", "remove", "--force", worktreePath],
2463
+ { allowFailure: true }
2464
+ );
2465
+ if (result.code !== 0 && await pathExists(worktreePath)) {
2466
+ try {
2467
+ await fs.rm(worktreePath, { recursive: true, force: true });
2468
+ } catch {
2469
+ }
2470
+ }
2471
+ }
2472
+ await runCommand("git", ["-C", this.repoRoot, "worktree", "prune"], { allowFailure: true });
2473
+ if (this.sharedWorktree?.branch) {
2474
+ await runCommand(
2475
+ "git",
2476
+ ["-C", this.repoRoot, "branch", "-D", this.sharedWorktree.branch],
2477
+ { allowFailure: true }
2478
+ );
2479
+ }
2480
+ if (this.worktreesDir && await pathExists(this.worktreesDir)) {
2481
+ try {
2482
+ await fs.rm(this.worktreesDir, { recursive: true, force: true });
2483
+ } catch {
2484
+ }
2485
+ }
2486
+ if (this.codexHomesDir && await pathExists(this.codexHomesDir)) {
2487
+ try {
2488
+ await fs.rm(this.codexHomesDir, { recursive: true, force: true });
2489
+ } catch {
2490
+ }
2491
+ }
2492
+ this.addLog("Worktree cleanup complete");
2493
+ }
2494
+ async cleanupCodexHomes() {
2495
+ if (!this.codexHomesDir) return;
2496
+ if (!await pathExists(this.codexHomesDir)) return;
2497
+ try {
2498
+ await fs.rm(this.codexHomesDir, { recursive: true, force: true });
2499
+ } catch {
2500
+ }
2501
+ }
2502
+ };
2503
+
2504
+ // src/index.tsx
2505
+ import { jsx as jsx2 } from "react/jsx-runtime";
2506
+ function parsePositiveInt(raw, label) {
2507
+ const value = Number.parseInt(raw, 10);
2508
+ if (!Number.isFinite(value) || value < 1) {
2509
+ throw new Error(`${label} must be a positive integer`);
2510
+ }
2511
+ return value;
2512
+ }
2513
+ function parseBackend(raw) {
2514
+ const normalized = raw.trim().toLowerCase();
2515
+ if (normalized === "codex") return "codex";
2516
+ if (normalized === "claude-code" || normalized === "claude" || normalized === "cc") return "claude-code";
2517
+ if (normalized === "opencode" || normalized === "oc") return "opencode";
2518
+ if (normalized === "mixed" || normalized === "all") return "mixed";
2519
+ throw new Error(`Unknown backend "${raw}". Supported: codex, claude-code, opencode, mixed`);
2520
+ }
2521
+ async function askForTargetPrompt() {
2522
+ const rl = readline.createInterface({ input, output });
2523
+ try {
2524
+ const answer = await rl.question("What should be reviewed? (describe the codebase area in natural language) ");
2525
+ return answer.trim() || "current directory";
2526
+ } finally {
2527
+ rl.close();
2528
+ }
2529
+ }
2530
+ function DashboardApp({ orchestrator }) {
2531
+ const [snapshot, setSnapshot] = useState2(orchestrator.getSnapshot());
2532
+ useEffect2(() => {
2533
+ return orchestrator.subscribe(setSnapshot);
2534
+ }, [orchestrator]);
2535
+ return /* @__PURE__ */ jsx2(Dashboard, { snapshot });
2536
+ }
2537
+ async function main() {
2538
+ const program = new Command();
2539
+ program.name("agent-review").description("Parallel agent-powered code review and fix orchestrator (supports Codex, Claude Code, OpenCode)").argument("[instances]", "Number of parallel agents").option("-i, --instances <number>", "Number of parallel agents").option("-t, --target <prompt>", "What to review (natural language or path)").option("-b, --backend <name>", "Agent backend: codex, claude-code, opencode, mixed (default: codex)").option("-m, --model <name>", "Model override for the backend").option("--no-cleanup", "Keep generated worktrees after run finishes").option("--timeout <ms>", "Overall timeout per agent in ms (default: 300000)").option("--inactivity-timeout <ms>", "Inactivity timeout per agent in ms (default: 120000)").parse(process.argv);
2540
+ const opts = program.opts();
2541
+ const argInstances = program.args[0];
2542
+ const rawInstances = opts.instances ?? argInstances ?? "2";
2543
+ const instances = parsePositiveInt(rawInstances, "instances");
2544
+ const targetPrompt = opts.target?.trim() || await askForTargetPrompt();
2545
+ const backend = parseBackend(opts.backend ?? "codex");
2546
+ const config = {
2547
+ instances,
2548
+ targetPrompt,
2549
+ startCwd: process.cwd(),
2550
+ backend,
2551
+ model: opts.model?.trim() || void 0,
2552
+ cleanup: opts.cleanup,
2553
+ codexTimeoutMs: opts.timeout ? parsePositiveInt(opts.timeout, "timeout") : void 0,
2554
+ codexInactivityTimeoutMs: opts.inactivityTimeout ? parsePositiveInt(opts.inactivityTimeout, "inactivity-timeout") : void 0
2555
+ };
2556
+ const orchestrator = new AgentReviewOrchestrator(config);
2557
+ const app = render(/* @__PURE__ */ jsx2(DashboardApp, { orchestrator }));
2558
+ const result = await orchestrator.run();
2559
+ await new Promise((resolve) => setTimeout(resolve, 250));
2560
+ app.unmount();
2561
+ if (result.success) {
2562
+ if (result.prUrl) {
2563
+ process.stdout.write(`
2564
+ Pull request: ${result.prUrl}
2565
+ `);
2566
+ }
2567
+ if (result.integrationBranch) {
2568
+ process.stdout.write(`Branch: ${result.integrationBranch}
2569
+ `);
2570
+ }
2571
+ if (result.issueFile) {
2572
+ process.stdout.write(`Issue tracker: ${result.issueFile}
2573
+ `);
2574
+ }
2575
+ if (result.reportFile) {
2576
+ process.stdout.write(`Final report: ${result.reportFile}
2577
+ `);
2578
+ }
2579
+ if (result.integrationWorktree) {
2580
+ process.stdout.write(`Worktree: ${result.integrationWorktree}
2581
+ `);
2582
+ }
2583
+ return;
2584
+ }
2585
+ if (result.error) {
2586
+ process.stderr.write(`agent-review failed: ${result.error}
2587
+ `);
2588
+ }
2589
+ process.exitCode = 1;
2590
+ }
2591
+ main().catch((error) => {
2592
+ const message = error instanceof Error ? error.message : String(error);
2593
+ process.stderr.write(`agent-review failed: ${message}
2594
+ `);
2595
+ process.exitCode = 1;
2596
+ });