@vexdo/cli 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -2
- package/dist/index.js +1236 -334
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs10 from "fs";
|
|
3
|
+
import path12 from "path";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/abort.ts
|
|
@@ -79,6 +79,15 @@ function parseReview(value) {
|
|
|
79
79
|
auto_submit
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
function parseMaxConcurrent(value) {
|
|
83
|
+
if (value === void 0) {
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
87
|
+
throw new Error("maxConcurrent must be a positive integer");
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
82
91
|
function parseCodex(value) {
|
|
83
92
|
if (value === void 0) {
|
|
84
93
|
return { model: DEFAULT_CODEX_MODEL };
|
|
@@ -113,8 +122,8 @@ function loadConfig(projectRoot) {
|
|
|
113
122
|
let parsed;
|
|
114
123
|
try {
|
|
115
124
|
parsed = parse(configRaw);
|
|
116
|
-
} catch (
|
|
117
|
-
const message =
|
|
125
|
+
} catch (error2) {
|
|
126
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
118
127
|
throw new Error(`Invalid YAML in .vexdo.yml: ${message}`);
|
|
119
128
|
}
|
|
120
129
|
if (!isRecord(parsed)) {
|
|
@@ -127,11 +136,13 @@ function loadConfig(projectRoot) {
|
|
|
127
136
|
const services = parseServices(readObjectField(parsed, "services"));
|
|
128
137
|
const review = parseReview(readObjectField(parsed, "review"));
|
|
129
138
|
const codex = parseCodex(readObjectField(parsed, "codex"));
|
|
139
|
+
const maxConcurrent = parseMaxConcurrent(readObjectField(parsed, "maxConcurrent"));
|
|
130
140
|
return {
|
|
131
141
|
version: 1,
|
|
132
142
|
services,
|
|
133
143
|
review,
|
|
134
|
-
codex
|
|
144
|
+
codex,
|
|
145
|
+
maxConcurrent
|
|
135
146
|
};
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -161,15 +172,15 @@ function success(message) {
|
|
|
161
172
|
function warn(message) {
|
|
162
173
|
safeLog("log", `${pc.yellow("\u26A0")} ${message}`);
|
|
163
174
|
}
|
|
175
|
+
function error(message) {
|
|
176
|
+
safeLog("error", `${pc.red("\u2716")} ${message}`);
|
|
177
|
+
}
|
|
164
178
|
function debug(message) {
|
|
165
179
|
if (!verboseEnabled) {
|
|
166
180
|
return;
|
|
167
181
|
}
|
|
168
182
|
safeLog("log", `${pc.gray("\u2022")} ${message}`);
|
|
169
183
|
}
|
|
170
|
-
function step(n, total, title) {
|
|
171
|
-
safeLog("log", `${pc.bold(`Step ${String(n)}/${String(total)}:`)} ${title}`);
|
|
172
|
-
}
|
|
173
184
|
function iteration(n, max) {
|
|
174
185
|
safeLog("log", pc.gray(`Iteration ${String(n)}/${String(max)}`));
|
|
175
186
|
}
|
|
@@ -228,6 +239,56 @@ function reviewSummary(comments) {
|
|
|
228
239
|
safeLog("log", `- ${comment.severity}${location}: ${comment.comment}`);
|
|
229
240
|
}
|
|
230
241
|
}
|
|
242
|
+
function prefixed(prefix, message) {
|
|
243
|
+
return `${prefix} ${message}`;
|
|
244
|
+
}
|
|
245
|
+
function withPrefix(prefix) {
|
|
246
|
+
return {
|
|
247
|
+
info: (message) => {
|
|
248
|
+
info(prefixed(prefix, message));
|
|
249
|
+
},
|
|
250
|
+
success: (message) => {
|
|
251
|
+
success(prefixed(prefix, message));
|
|
252
|
+
},
|
|
253
|
+
warn: (message) => {
|
|
254
|
+
warn(prefixed(prefix, message));
|
|
255
|
+
},
|
|
256
|
+
error: (message) => {
|
|
257
|
+
error(prefixed(prefix, message));
|
|
258
|
+
},
|
|
259
|
+
debug: (message) => {
|
|
260
|
+
debug(prefixed(prefix, message));
|
|
261
|
+
},
|
|
262
|
+
iteration: (n, max) => {
|
|
263
|
+
safeLog("log", prefixed(prefix, pc.gray(`Iteration ${String(n)}/${String(max)}`)));
|
|
264
|
+
},
|
|
265
|
+
reviewSummary: (comments) => {
|
|
266
|
+
const counts = {
|
|
267
|
+
critical: 0,
|
|
268
|
+
important: 0,
|
|
269
|
+
minor: 0,
|
|
270
|
+
noise: 0
|
|
271
|
+
};
|
|
272
|
+
for (const comment of comments) {
|
|
273
|
+
counts[comment.severity] += 1;
|
|
274
|
+
}
|
|
275
|
+
safeLog(
|
|
276
|
+
"log",
|
|
277
|
+
prefixed(
|
|
278
|
+
prefix,
|
|
279
|
+
`${pc.bold("Review:")} ${String(counts.critical)} critical ${String(counts.important)} important ${String(counts.minor)} minor`
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
for (const comment of comments) {
|
|
283
|
+
if (comment.severity === "noise") {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const location = comment.file ? ` (${comment.file}${comment.line ? `:${String(comment.line)}` : ""})` : "";
|
|
287
|
+
safeLog("log", prefixed(prefix, `- ${comment.severity}${location}: ${comment.comment}`));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
231
292
|
|
|
232
293
|
// src/lib/state.ts
|
|
233
294
|
import fs2 from "fs";
|
|
@@ -257,8 +318,8 @@ function loadState(projectRoot) {
|
|
|
257
318
|
const raw = fs2.readFileSync(statePath, "utf8");
|
|
258
319
|
try {
|
|
259
320
|
return JSON.parse(raw);
|
|
260
|
-
} catch (
|
|
261
|
-
const message =
|
|
321
|
+
} catch (error2) {
|
|
322
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
262
323
|
throw new Error(`Corrupt state file at ${statePath}: ${message}`);
|
|
263
324
|
}
|
|
264
325
|
}
|
|
@@ -271,6 +332,34 @@ function saveState(projectRoot, state) {
|
|
|
271
332
|
};
|
|
272
333
|
fs2.writeFileSync(getStatePath(projectRoot), JSON.stringify(nextState, null, 2) + "\n", "utf8");
|
|
273
334
|
}
|
|
335
|
+
async function updateStep(projectRoot, taskId, service, updates) {
|
|
336
|
+
const currentState = loadState(projectRoot);
|
|
337
|
+
if (!currentState) {
|
|
338
|
+
throw new Error("No active state found to update.");
|
|
339
|
+
}
|
|
340
|
+
if (currentState.taskId !== taskId) {
|
|
341
|
+
throw new Error(`State task mismatch. Expected '${taskId}', found '${currentState.taskId}'.`);
|
|
342
|
+
}
|
|
343
|
+
const nextState = {
|
|
344
|
+
...currentState,
|
|
345
|
+
steps: currentState.steps.map((step) => {
|
|
346
|
+
if (step.service !== service) {
|
|
347
|
+
return step;
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
...step,
|
|
351
|
+
...updates
|
|
352
|
+
};
|
|
353
|
+
}),
|
|
354
|
+
updatedAt: nowIso()
|
|
355
|
+
};
|
|
356
|
+
const stateDir = getStateDir(projectRoot);
|
|
357
|
+
fs2.mkdirSync(stateDir, { recursive: true });
|
|
358
|
+
const statePath = getStatePath(projectRoot);
|
|
359
|
+
const tmpPath = `${statePath}.tmp`;
|
|
360
|
+
await fs2.promises.writeFile(tmpPath, JSON.stringify(nextState, null, 2) + "\n", "utf8");
|
|
361
|
+
await fs2.promises.rename(tmpPath, statePath);
|
|
362
|
+
}
|
|
274
363
|
function clearState(projectRoot) {
|
|
275
364
|
const statePath = getStatePath(projectRoot);
|
|
276
365
|
if (fs2.existsSync(statePath)) {
|
|
@@ -341,13 +430,46 @@ function parseTaskStep(value, index, config) {
|
|
|
341
430
|
depends_on
|
|
342
431
|
};
|
|
343
432
|
}
|
|
433
|
+
function validateDependencies(steps) {
|
|
434
|
+
const byService = new Map(steps.map((step) => [step.service, step]));
|
|
435
|
+
for (const step of steps) {
|
|
436
|
+
for (const dep of step.depends_on ?? []) {
|
|
437
|
+
if (!byService.has(dep)) {
|
|
438
|
+
throw new Error(`step '${step.service}' depends on unknown service '${dep}'`);
|
|
439
|
+
}
|
|
440
|
+
if (dep === step.service) {
|
|
441
|
+
throw new Error(`step '${step.service}' cannot depend on itself`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
446
|
+
const visited = /* @__PURE__ */ new Set();
|
|
447
|
+
const visit = (service) => {
|
|
448
|
+
if (visited.has(service)) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (visiting.has(service)) {
|
|
452
|
+
throw new Error(`Dependency cycle detected involving service '${service}'`);
|
|
453
|
+
}
|
|
454
|
+
visiting.add(service);
|
|
455
|
+
const step = byService.get(service);
|
|
456
|
+
for (const dep of step?.depends_on ?? []) {
|
|
457
|
+
visit(dep);
|
|
458
|
+
}
|
|
459
|
+
visiting.delete(service);
|
|
460
|
+
visited.add(service);
|
|
461
|
+
};
|
|
462
|
+
for (const step of steps) {
|
|
463
|
+
visit(step.service);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
344
466
|
function loadAndValidateTask(taskPath, config) {
|
|
345
467
|
const raw = fs3.readFileSync(taskPath, "utf8");
|
|
346
468
|
let parsed;
|
|
347
469
|
try {
|
|
348
470
|
parsed = parseYaml(raw);
|
|
349
|
-
} catch (
|
|
350
|
-
const message =
|
|
471
|
+
} catch (error2) {
|
|
472
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
351
473
|
throw new Error(`Invalid task YAML: ${message}`);
|
|
352
474
|
}
|
|
353
475
|
if (!isRecord2(parsed)) {
|
|
@@ -358,12 +480,13 @@ function loadAndValidateTask(taskPath, config) {
|
|
|
358
480
|
if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) {
|
|
359
481
|
throw new Error("steps must be a non-empty array");
|
|
360
482
|
}
|
|
361
|
-
const steps = parsed.steps.map((
|
|
483
|
+
const steps = parsed.steps.map((step, index) => parseTaskStep(step, index, config));
|
|
484
|
+
validateDependencies(steps);
|
|
362
485
|
return { id, title, steps };
|
|
363
486
|
}
|
|
364
487
|
function buildInitialStepState(task) {
|
|
365
|
-
return task.steps.map((
|
|
366
|
-
service:
|
|
488
|
+
return task.steps.map((step) => ({
|
|
489
|
+
service: step.service,
|
|
367
490
|
status: "pending",
|
|
368
491
|
iteration: 0
|
|
369
492
|
}));
|
|
@@ -423,8 +546,328 @@ function registerAbortCommand(program2) {
|
|
|
423
546
|
});
|
|
424
547
|
}
|
|
425
548
|
|
|
549
|
+
// src/commands/board.tsx
|
|
550
|
+
import { render } from "ink";
|
|
551
|
+
|
|
552
|
+
// src/components/Board.tsx
|
|
553
|
+
import { spawnSync } from "child_process";
|
|
554
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
555
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
556
|
+
|
|
557
|
+
// src/lib/board.ts
|
|
558
|
+
import fs5 from "fs";
|
|
559
|
+
import path5 from "path";
|
|
560
|
+
import { parse as parse2 } from "yaml";
|
|
561
|
+
function parseTaskYaml(raw) {
|
|
562
|
+
try {
|
|
563
|
+
const parsed = parse2(raw);
|
|
564
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
565
|
+
return {};
|
|
566
|
+
}
|
|
567
|
+
const obj = parsed;
|
|
568
|
+
return {
|
|
569
|
+
id: typeof obj.id === "string" ? obj.id : void 0,
|
|
570
|
+
title: typeof obj.title === "string" ? obj.title : void 0
|
|
571
|
+
};
|
|
572
|
+
} catch {
|
|
573
|
+
return {};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function listTaskFiles(directory) {
|
|
577
|
+
if (!fs5.existsSync(directory)) {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
const entries = await fs5.promises.readdir(directory, { withFileTypes: true });
|
|
581
|
+
return entries.filter((entry) => entry.isFile() && (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml"))).map((entry) => path5.join(directory, entry.name));
|
|
582
|
+
}
|
|
583
|
+
async function readTaskSummary(filePath) {
|
|
584
|
+
const raw = await fs5.promises.readFile(filePath, "utf8");
|
|
585
|
+
const parsed = parseTaskYaml(raw);
|
|
586
|
+
const fallbackId = path5.basename(filePath).replace(/\.ya?ml$/i, "");
|
|
587
|
+
return {
|
|
588
|
+
id: parsed.id ?? fallbackId,
|
|
589
|
+
title: parsed.title ?? fallbackId,
|
|
590
|
+
path: filePath
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async function loadColumn(projectRoot, columnDir) {
|
|
594
|
+
const directory = path5.join(projectRoot, "tasks", columnDir);
|
|
595
|
+
const files = await listTaskFiles(directory);
|
|
596
|
+
const tasks = await Promise.all(files.map((filePath) => readTaskSummary(filePath)));
|
|
597
|
+
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
598
|
+
}
|
|
599
|
+
async function loadBoardState(projectRoot) {
|
|
600
|
+
const [backlog, inProgress, review, blocked, doneFiles] = await Promise.all([
|
|
601
|
+
loadColumn(projectRoot, "backlog"),
|
|
602
|
+
loadColumn(projectRoot, "in_progress"),
|
|
603
|
+
loadColumn(projectRoot, "review"),
|
|
604
|
+
loadColumn(projectRoot, "blocked"),
|
|
605
|
+
listTaskFiles(path5.join(projectRoot, "tasks", "done"))
|
|
606
|
+
]);
|
|
607
|
+
const doneWithStats = await Promise.all(
|
|
608
|
+
doneFiles.map(async (filePath) => ({
|
|
609
|
+
filePath,
|
|
610
|
+
stat: await fs5.promises.stat(filePath)
|
|
611
|
+
}))
|
|
612
|
+
);
|
|
613
|
+
doneWithStats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
614
|
+
const done = await Promise.all(doneWithStats.slice(0, 20).map((item) => readTaskSummary(item.filePath)));
|
|
615
|
+
return {
|
|
616
|
+
backlog,
|
|
617
|
+
in_progress: inProgress,
|
|
618
|
+
review,
|
|
619
|
+
done,
|
|
620
|
+
blocked: blocked.map((task) => ({ ...task, blocked: true }))
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/components/Board.tsx
|
|
625
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
626
|
+
function clamp(value, min, max) {
|
|
627
|
+
return Math.max(min, Math.min(value, max));
|
|
628
|
+
}
|
|
629
|
+
function runExternal(command, args) {
|
|
630
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
631
|
+
if (process.stdin.isTTY && wasRaw) {
|
|
632
|
+
process.stdin.setRawMode(false);
|
|
633
|
+
}
|
|
634
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
635
|
+
if (process.stdin.isTTY && wasRaw) {
|
|
636
|
+
process.stdin.setRawMode(true);
|
|
637
|
+
}
|
|
638
|
+
if (result.error) {
|
|
639
|
+
return {
|
|
640
|
+
ok: false,
|
|
641
|
+
message: result.error.message
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
ok: result.status === 0,
|
|
646
|
+
message: result.status === 0 ? "Command completed." : `Command exited with status ${String(result.status)}.`
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function openInEditor(filePath) {
|
|
650
|
+
const editor = process.env.EDITOR;
|
|
651
|
+
if (!editor) {
|
|
652
|
+
return { ok: false, message: "EDITOR is not set." };
|
|
653
|
+
}
|
|
654
|
+
return runExternal(editor, [filePath]);
|
|
655
|
+
}
|
|
656
|
+
function runPrimaryAction(column, task) {
|
|
657
|
+
if (task.blocked) {
|
|
658
|
+
return runExternal("vexdo", ["logs", task.id]);
|
|
659
|
+
}
|
|
660
|
+
if (column === "backlog") {
|
|
661
|
+
return runExternal("vexdo", ["start", task.path]);
|
|
662
|
+
}
|
|
663
|
+
if (column === "in_progress") {
|
|
664
|
+
return runExternal("vexdo", ["status"]);
|
|
665
|
+
}
|
|
666
|
+
if (column === "review") {
|
|
667
|
+
return runExternal("vexdo", ["submit"]);
|
|
668
|
+
}
|
|
669
|
+
return openInEditor(task.path);
|
|
670
|
+
}
|
|
671
|
+
function TaskCard({ task, selected }) {
|
|
672
|
+
const prefix = task.blocked ? "\u26A0 " : "";
|
|
673
|
+
return /* @__PURE__ */ jsx(Text, { children: `${selected ? "> " : " "}${prefix}${task.id}` });
|
|
674
|
+
}
|
|
675
|
+
function Column({
|
|
676
|
+
title,
|
|
677
|
+
count,
|
|
678
|
+
tasks,
|
|
679
|
+
selectedRow,
|
|
680
|
+
active
|
|
681
|
+
}) {
|
|
682
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", paddingX: 1, children: [
|
|
683
|
+
/* @__PURE__ */ jsx(Text, { color: active ? "cyan" : void 0, children: title }),
|
|
684
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: `(${String(count)})` }),
|
|
685
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
686
|
+
tasks.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014" }) : null,
|
|
687
|
+
tasks.map((task, index) => /* @__PURE__ */ jsx(TaskCard, { task, selected: active && index === selectedRow }, `${task.path}-${task.id}`))
|
|
688
|
+
] })
|
|
689
|
+
] });
|
|
690
|
+
}
|
|
691
|
+
function StatusBar({
|
|
692
|
+
task,
|
|
693
|
+
column,
|
|
694
|
+
message,
|
|
695
|
+
confirmAbort
|
|
696
|
+
}) {
|
|
697
|
+
if (!task) {
|
|
698
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
|
|
699
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "No task selected." }),
|
|
700
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[\u2190/\u2192/\u2191/\u2193] navigate [r] refresh [q] quit" })
|
|
701
|
+
] });
|
|
702
|
+
}
|
|
703
|
+
const primaryLabel = task.blocked ? "[\u21B5] logs" : column === "backlog" ? "[\u21B5] start" : column === "in_progress" ? "[\u21B5] status" : column === "review" ? "[\u21B5] submit" : "[\u21B5] edit";
|
|
704
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
|
|
705
|
+
/* @__PURE__ */ jsx(Text, { children: `${task.id} \xB7 ${task.title}` }),
|
|
706
|
+
/* @__PURE__ */ jsx(Text, { children: `${primaryLabel} [e] edit [l] logs [a] abort [r] refresh [q] quit${confirmAbort ? " Confirm abort: press [a] again" : ""}` }),
|
|
707
|
+
message ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: message }) : null
|
|
708
|
+
] });
|
|
709
|
+
}
|
|
710
|
+
function Board({ projectRoot }) {
|
|
711
|
+
const { exit } = useApp();
|
|
712
|
+
const [state, setState] = useState(null);
|
|
713
|
+
const [loading, setLoading] = useState(true);
|
|
714
|
+
const [cursor, setCursor] = useState({ column: 0, row: 0 });
|
|
715
|
+
const [message, setMessage] = useState(null);
|
|
716
|
+
const [confirmAbort, setConfirmAbort] = useState(false);
|
|
717
|
+
const refresh = useCallback(async () => {
|
|
718
|
+
setLoading(true);
|
|
719
|
+
try {
|
|
720
|
+
const next = await loadBoardState(projectRoot);
|
|
721
|
+
setState(next);
|
|
722
|
+
setMessage(null);
|
|
723
|
+
} catch (error2) {
|
|
724
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
725
|
+
setMessage(`Failed to load board: ${errorMessage}`);
|
|
726
|
+
} finally {
|
|
727
|
+
setLoading(false);
|
|
728
|
+
}
|
|
729
|
+
}, [projectRoot]);
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
void refresh();
|
|
732
|
+
}, [refresh]);
|
|
733
|
+
const columns = useMemo(() => {
|
|
734
|
+
if (!state) {
|
|
735
|
+
return [
|
|
736
|
+
{ key: "backlog", title: "BACKLOG", tasks: [] },
|
|
737
|
+
{ key: "in_progress", title: "IN PROGRESS", tasks: [] },
|
|
738
|
+
{ key: "review", title: "REVIEW", tasks: [] },
|
|
739
|
+
{ key: "done", title: "DONE", tasks: [] }
|
|
740
|
+
];
|
|
741
|
+
}
|
|
742
|
+
return [
|
|
743
|
+
{ key: "backlog", title: "BACKLOG", tasks: [...state.blocked, ...state.backlog] },
|
|
744
|
+
{ key: "in_progress", title: "IN PROGRESS", tasks: state.in_progress },
|
|
745
|
+
{ key: "review", title: "REVIEW", tasks: state.review },
|
|
746
|
+
{ key: "done", title: "DONE", tasks: state.done }
|
|
747
|
+
];
|
|
748
|
+
}, [state]);
|
|
749
|
+
const activeColumn = columns[cursor.column] ?? columns[0];
|
|
750
|
+
const selectedTask = activeColumn.tasks.at(cursor.row);
|
|
751
|
+
useEffect(() => {
|
|
752
|
+
const nextColumn = clamp(cursor.column, 0, columns.length - 1);
|
|
753
|
+
const maxRow = Math.max(0, (columns[nextColumn]?.tasks.length ?? 1) - 1);
|
|
754
|
+
const nextRow = clamp(cursor.row, 0, maxRow);
|
|
755
|
+
if (nextColumn !== cursor.column || nextRow !== cursor.row) {
|
|
756
|
+
setCursor({ column: nextColumn, row: nextRow });
|
|
757
|
+
}
|
|
758
|
+
}, [columns, cursor.column, cursor.row]);
|
|
759
|
+
useInput((input2, key) => {
|
|
760
|
+
if (input2 === "q" || key.ctrl && input2 === "c") {
|
|
761
|
+
exit();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (key.leftArrow) {
|
|
765
|
+
setConfirmAbort(false);
|
|
766
|
+
setCursor((prev) => {
|
|
767
|
+
const nextColumn = clamp(prev.column - 1, 0, columns.length - 1);
|
|
768
|
+
const maxRow = Math.max(0, columns[nextColumn].tasks.length - 1);
|
|
769
|
+
return { column: nextColumn, row: clamp(prev.row, 0, maxRow) };
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (key.rightArrow) {
|
|
774
|
+
setConfirmAbort(false);
|
|
775
|
+
setCursor((prev) => {
|
|
776
|
+
const nextColumn = clamp(prev.column + 1, 0, columns.length - 1);
|
|
777
|
+
const maxRow = Math.max(0, columns[nextColumn].tasks.length - 1);
|
|
778
|
+
return { column: nextColumn, row: clamp(prev.row, 0, maxRow) };
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (key.upArrow) {
|
|
783
|
+
setConfirmAbort(false);
|
|
784
|
+
setCursor((prev) => ({ ...prev, row: clamp(prev.row - 1, 0, Math.max(0, activeColumn.tasks.length - 1)) }));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (key.downArrow) {
|
|
788
|
+
setConfirmAbort(false);
|
|
789
|
+
setCursor((prev) => ({ ...prev, row: clamp(prev.row + 1, 0, Math.max(0, activeColumn.tasks.length - 1)) }));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (input2 === "r") {
|
|
793
|
+
setConfirmAbort(false);
|
|
794
|
+
void refresh();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (key.return && selectedTask) {
|
|
798
|
+
const result = runPrimaryAction(activeColumn.key, selectedTask);
|
|
799
|
+
setMessage(result.message);
|
|
800
|
+
setConfirmAbort(false);
|
|
801
|
+
void refresh();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (input2 === "e" && selectedTask) {
|
|
805
|
+
const result = openInEditor(selectedTask.path);
|
|
806
|
+
setMessage(result.message);
|
|
807
|
+
setConfirmAbort(false);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (input2 === "l" && selectedTask) {
|
|
811
|
+
const result = runExternal("vexdo", ["logs", selectedTask.id]);
|
|
812
|
+
setMessage(result.message);
|
|
813
|
+
setConfirmAbort(false);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (input2 === "a" && selectedTask) {
|
|
817
|
+
if (!confirmAbort) {
|
|
818
|
+
setConfirmAbort(true);
|
|
819
|
+
setMessage(`Confirm abort task '${selectedTask.id}' by pressing 'a' again.`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const result = runExternal("vexdo", ["abort"]);
|
|
823
|
+
setMessage(result.message);
|
|
824
|
+
setConfirmAbort(false);
|
|
825
|
+
void refresh();
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
const terminalRows = typeof process.stdout.rows === "number" ? process.stdout.rows : 24;
|
|
829
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: terminalRows, children: [
|
|
830
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", borderStyle: "single", paddingX: 1, children: [
|
|
831
|
+
/* @__PURE__ */ jsx(Text, { children: "vexdo board" }),
|
|
832
|
+
/* @__PURE__ */ jsx(Text, { children: "[q] quit" })
|
|
833
|
+
] }),
|
|
834
|
+
loading ? /* @__PURE__ */ jsx(Box, { borderStyle: "single", paddingX: 1, children: /* @__PURE__ */ jsx(Text, { children: "Loading tasks\u2026" }) }) : /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: columns.map((column, index) => /* @__PURE__ */ jsx(
|
|
835
|
+
Column,
|
|
836
|
+
{
|
|
837
|
+
title: column.title,
|
|
838
|
+
count: column.tasks.length,
|
|
839
|
+
tasks: column.tasks,
|
|
840
|
+
selectedRow: cursor.row,
|
|
841
|
+
active: index === cursor.column
|
|
842
|
+
},
|
|
843
|
+
column.key
|
|
844
|
+
)) }),
|
|
845
|
+
/* @__PURE__ */ jsx(StatusBar, { task: selectedTask, column: activeColumn.key, message, confirmAbort })
|
|
846
|
+
] });
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/commands/board.tsx
|
|
850
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
851
|
+
function fatalAndExit2(message) {
|
|
852
|
+
fatal(message);
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
async function runBoard() {
|
|
856
|
+
const projectRoot = findProjectRoot();
|
|
857
|
+
if (!projectRoot) {
|
|
858
|
+
fatalAndExit2("Not inside a vexdo project.");
|
|
859
|
+
}
|
|
860
|
+
const instance = render(/* @__PURE__ */ jsx2(Board, { projectRoot }));
|
|
861
|
+
await instance.waitUntilExit();
|
|
862
|
+
}
|
|
863
|
+
function registerBoardCommand(program2) {
|
|
864
|
+
program2.command("board").description("Open interactive task board").action(() => {
|
|
865
|
+
void runBoard();
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
426
869
|
// src/commands/fix.ts
|
|
427
|
-
import
|
|
870
|
+
import path7 from "path";
|
|
428
871
|
|
|
429
872
|
// src/lib/claude.ts
|
|
430
873
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -547,10 +990,10 @@ ${JSON.stringify(opts.reviewComments)}`
|
|
|
547
990
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
548
991
|
try {
|
|
549
992
|
return await fn();
|
|
550
|
-
} catch (
|
|
551
|
-
lastError =
|
|
552
|
-
if (!isRetryableError(
|
|
553
|
-
throw new ClaudeError(attempt,
|
|
993
|
+
} catch (error2) {
|
|
994
|
+
lastError = error2;
|
|
995
|
+
if (!isRetryableError(error2) || attempt === MAX_ATTEMPTS) {
|
|
996
|
+
throw new ClaudeError(attempt, error2);
|
|
554
997
|
}
|
|
555
998
|
const backoffMs = 1e3 * 2 ** (attempt - 1);
|
|
556
999
|
warn(
|
|
@@ -579,7 +1022,7 @@ function extractJson(text) {
|
|
|
579
1022
|
return trimmed;
|
|
580
1023
|
}
|
|
581
1024
|
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
|
|
582
|
-
if (fenced
|
|
1025
|
+
if (fenced?.[1]) {
|
|
583
1026
|
return fenced[1].trim();
|
|
584
1027
|
}
|
|
585
1028
|
return trimmed;
|
|
@@ -588,8 +1031,8 @@ function parseReviewerResult(raw) {
|
|
|
588
1031
|
let parsed;
|
|
589
1032
|
try {
|
|
590
1033
|
parsed = JSON.parse(extractJson(raw));
|
|
591
|
-
} catch (
|
|
592
|
-
throw new Error(`Failed to parse reviewer JSON: ${
|
|
1034
|
+
} catch (error2) {
|
|
1035
|
+
throw new Error(`Failed to parse reviewer JSON: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
593
1036
|
}
|
|
594
1037
|
if (!isReviewResult(parsed)) {
|
|
595
1038
|
throw new Error("Reviewer JSON does not match schema");
|
|
@@ -600,8 +1043,8 @@ function parseArbiterResult(raw) {
|
|
|
600
1043
|
let parsed;
|
|
601
1044
|
try {
|
|
602
1045
|
parsed = JSON.parse(extractJson(raw));
|
|
603
|
-
} catch (
|
|
604
|
-
throw new Error(`Failed to parse arbiter JSON: ${
|
|
1046
|
+
} catch (error2) {
|
|
1047
|
+
throw new Error(`Failed to parse arbiter JSON: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
605
1048
|
}
|
|
606
1049
|
if (!isArbiterResult(parsed)) {
|
|
607
1050
|
throw new Error("Arbiter JSON does not match schema");
|
|
@@ -650,11 +1093,11 @@ function isArbiterResult(value) {
|
|
|
650
1093
|
}
|
|
651
1094
|
return candidate.feedback_for_codex === void 0;
|
|
652
1095
|
}
|
|
653
|
-
function getStatusCode(
|
|
654
|
-
if (typeof
|
|
1096
|
+
function getStatusCode(error2) {
|
|
1097
|
+
if (typeof error2 !== "object" || error2 === null) {
|
|
655
1098
|
return void 0;
|
|
656
1099
|
}
|
|
657
|
-
const candidate =
|
|
1100
|
+
const candidate = error2;
|
|
658
1101
|
if (typeof candidate.status === "number") {
|
|
659
1102
|
return candidate.status;
|
|
660
1103
|
}
|
|
@@ -663,8 +1106,8 @@ function getStatusCode(error) {
|
|
|
663
1106
|
}
|
|
664
1107
|
return void 0;
|
|
665
1108
|
}
|
|
666
|
-
function isRetryableError(
|
|
667
|
-
const status = getStatusCode(
|
|
1109
|
+
function isRetryableError(error2) {
|
|
1110
|
+
const status = getStatusCode(error2);
|
|
668
1111
|
if (status === 400 || status === 401 || status === 403) {
|
|
669
1112
|
return false;
|
|
670
1113
|
}
|
|
@@ -675,19 +1118,31 @@ function isRetryableError(error) {
|
|
|
675
1118
|
}
|
|
676
1119
|
|
|
677
1120
|
// src/lib/codex.ts
|
|
678
|
-
import {
|
|
1121
|
+
import { spawn } from "child_process";
|
|
679
1122
|
var CODEX_TIMEOUT_MS = 6e5;
|
|
680
|
-
var VERBOSE_HEARTBEAT_MS = 15e3;
|
|
681
1123
|
var CodexError = class extends Error {
|
|
1124
|
+
code;
|
|
682
1125
|
stdout;
|
|
683
1126
|
stderr;
|
|
684
1127
|
exitCode;
|
|
685
|
-
constructor(
|
|
686
|
-
super(
|
|
1128
|
+
constructor(code, message, details) {
|
|
1129
|
+
super(message);
|
|
687
1130
|
this.name = "CodexError";
|
|
688
|
-
this.
|
|
689
|
-
this.
|
|
690
|
-
this.
|
|
1131
|
+
this.code = code;
|
|
1132
|
+
this.stdout = details?.stdout ?? "";
|
|
1133
|
+
this.stderr = details?.stderr ?? "";
|
|
1134
|
+
this.exitCode = details?.exitCode ?? 1;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
var CodexTimeoutError = class extends CodexError {
|
|
1138
|
+
sessionId;
|
|
1139
|
+
constructor(sessionId, timeoutMs) {
|
|
1140
|
+
super(
|
|
1141
|
+
"poll_timeout",
|
|
1142
|
+
`Timed out waiting for codex cloud session ${sessionId} after ${String(timeoutMs)}ms. Check status with: codex cloud status ${sessionId}`
|
|
1143
|
+
);
|
|
1144
|
+
this.name = "CodexTimeoutError";
|
|
1145
|
+
this.sessionId = sessionId;
|
|
691
1146
|
}
|
|
692
1147
|
};
|
|
693
1148
|
var CodexNotFoundError = class extends Error {
|
|
@@ -696,121 +1151,191 @@ var CodexNotFoundError = class extends Error {
|
|
|
696
1151
|
this.name = "CodexNotFoundError";
|
|
697
1152
|
}
|
|
698
1153
|
};
|
|
699
|
-
function
|
|
700
|
-
|
|
701
|
-
|
|
1154
|
+
function runCodexCommand(args, opts) {
|
|
1155
|
+
return new Promise((resolve, reject) => {
|
|
1156
|
+
const child = spawn("codex", args, {
|
|
1157
|
+
cwd: opts?.cwd,
|
|
1158
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1159
|
+
});
|
|
1160
|
+
let stdout = "";
|
|
1161
|
+
let stderr = "";
|
|
1162
|
+
child.stdout.on("data", (chunk) => {
|
|
1163
|
+
stdout += chunk.toString();
|
|
1164
|
+
});
|
|
1165
|
+
child.stderr.on("data", (chunk) => {
|
|
1166
|
+
stderr += chunk.toString();
|
|
1167
|
+
});
|
|
1168
|
+
const timeout = setTimeout(() => {
|
|
1169
|
+
child.kill("SIGTERM");
|
|
1170
|
+
reject(new Error(`codex command timed out after ${String(opts?.timeoutMs ?? CODEX_TIMEOUT_MS)}ms`));
|
|
1171
|
+
}, opts?.timeoutMs ?? CODEX_TIMEOUT_MS);
|
|
1172
|
+
child.once("error", (error2) => {
|
|
1173
|
+
clearTimeout(timeout);
|
|
1174
|
+
reject(error2);
|
|
1175
|
+
});
|
|
1176
|
+
child.once("close", (code) => {
|
|
1177
|
+
clearTimeout(timeout);
|
|
1178
|
+
resolve({
|
|
1179
|
+
stdout: stdout.trim(),
|
|
1180
|
+
stderr: stderr.trim(),
|
|
1181
|
+
exitCode: code ?? 1
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
702
1185
|
}
|
|
703
|
-
function
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
flush() {
|
|
718
|
-
if (!partialLine) {
|
|
719
|
-
return;
|
|
1186
|
+
function parseSessionId(output2) {
|
|
1187
|
+
const match = /session[_-]?id\s*[:=]\s*([A-Za-z0-9._-]+)/i.exec(output2);
|
|
1188
|
+
if (match?.[1]) {
|
|
1189
|
+
return match[1];
|
|
1190
|
+
}
|
|
1191
|
+
for (const line of output2.split(/\r?\n/)) {
|
|
1192
|
+
const trimmed = line.trim();
|
|
1193
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
const parsed = JSON.parse(trimmed);
|
|
1198
|
+
if (typeof parsed.session_id === "string" && parsed.session_id.length > 0) {
|
|
1199
|
+
return parsed.session_id;
|
|
720
1200
|
}
|
|
721
|
-
|
|
722
|
-
partialLine = "";
|
|
1201
|
+
} catch {
|
|
723
1202
|
}
|
|
724
|
-
}
|
|
1203
|
+
}
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
function parseStatus(output2) {
|
|
1207
|
+
const match = /\b(completed|failed)\b/i.exec(output2);
|
|
1208
|
+
if (!match?.[1]) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
return match[1].toLowerCase();
|
|
725
1212
|
}
|
|
726
1213
|
async function checkCodexAvailable() {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1214
|
+
try {
|
|
1215
|
+
const result = await runCodexCommand(["--version"]);
|
|
1216
|
+
if (result.exitCode !== 0) {
|
|
1217
|
+
throw new CodexNotFoundError();
|
|
1218
|
+
}
|
|
1219
|
+
} catch {
|
|
1220
|
+
throw new CodexNotFoundError();
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function submitTask(prompt, options) {
|
|
1224
|
+
const args = ["cloud", "exec", prompt];
|
|
1225
|
+
if (options?.cwd) {
|
|
1226
|
+
args.push("-C", options.cwd);
|
|
1227
|
+
}
|
|
1228
|
+
const result = await runCodexCommand(args, { cwd: options?.cwd });
|
|
1229
|
+
const sessionId = parseSessionId(result.stdout);
|
|
1230
|
+
if (result.exitCode !== 0 || !sessionId) {
|
|
1231
|
+
throw new CodexError("submit_failed", "Failed to submit task to codex cloud.", result);
|
|
1232
|
+
}
|
|
1233
|
+
return sessionId;
|
|
1234
|
+
}
|
|
1235
|
+
async function resumeTask(sessionId, feedback) {
|
|
1236
|
+
const result = await runCodexCommand(["cloud", "exec", "resume", sessionId, feedback]);
|
|
1237
|
+
const nextSessionId = parseSessionId(result.stdout);
|
|
1238
|
+
if (result.exitCode !== 0 || !nextSessionId) {
|
|
1239
|
+
throw new CodexError("resume_failed", `Failed to resume codex cloud session ${sessionId}.`, result);
|
|
1240
|
+
}
|
|
1241
|
+
return nextSessionId;
|
|
1242
|
+
}
|
|
1243
|
+
async function pollStatus(sessionId, opts) {
|
|
1244
|
+
const startedAt = Date.now();
|
|
1245
|
+
for (; ; ) {
|
|
1246
|
+
const result = await runCodexCommand(["cloud", "status", sessionId]);
|
|
1247
|
+
if (result.exitCode !== 0) {
|
|
1248
|
+
throw new CodexError("poll_failed", `Failed to poll codex cloud status for session ${sessionId}.`, result);
|
|
1249
|
+
}
|
|
1250
|
+
const status = parseStatus(result.stdout);
|
|
1251
|
+
if (status === "completed" || status === "failed") {
|
|
1252
|
+
return status;
|
|
1253
|
+
}
|
|
1254
|
+
if (Date.now() - startedAt >= opts.timeoutMs) {
|
|
1255
|
+
throw new CodexTimeoutError(sessionId, opts.timeoutMs);
|
|
1256
|
+
}
|
|
1257
|
+
await new Promise((resolve) => {
|
|
1258
|
+
setTimeout(resolve, opts.intervalMs);
|
|
734
1259
|
});
|
|
735
|
-
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function getDiff(sessionId) {
|
|
1263
|
+
const result = await runCodexCommand(["cloud", "diff", sessionId]);
|
|
1264
|
+
if (result.exitCode !== 0) {
|
|
1265
|
+
throw new CodexError("poll_failed", `Failed to get diff for codex cloud session ${sessionId}.`, result);
|
|
1266
|
+
}
|
|
1267
|
+
if (!result.stdout.trim()) {
|
|
1268
|
+
throw new CodexError("diff_empty", `Diff was empty for codex cloud session ${sessionId}.`, result);
|
|
1269
|
+
}
|
|
1270
|
+
return result.stdout;
|
|
1271
|
+
}
|
|
1272
|
+
async function applyDiff(sessionId) {
|
|
1273
|
+
const result = await runCodexCommand(["cloud", "apply", sessionId]);
|
|
1274
|
+
if (result.exitCode !== 0) {
|
|
1275
|
+
throw new CodexError("apply_failed", `Failed to apply diff for codex cloud session ${sessionId}.`, result);
|
|
1276
|
+
}
|
|
736
1277
|
}
|
|
737
1278
|
async function exec(opts) {
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
if (
|
|
741
|
-
|
|
1279
|
+
const sessionId = await submitTask(opts.spec, { cwd: opts.cwd });
|
|
1280
|
+
const status = await pollStatus(sessionId, { intervalMs: 2e3, timeoutMs: CODEX_TIMEOUT_MS });
|
|
1281
|
+
if (status !== "completed") {
|
|
1282
|
+
throw new CodexError("submit_failed", `Codex cloud session ${sessionId} ended with status '${status}'.`);
|
|
742
1283
|
}
|
|
743
|
-
|
|
744
|
-
let liveLogsAttached = false;
|
|
745
|
-
const stdoutHandler = buildVerboseStreamHandler("stdout");
|
|
746
|
-
const stderrHandler = buildVerboseStreamHandler("stderr");
|
|
747
|
-
const heartbeat = opts.verbose ? setInterval(() => {
|
|
748
|
-
debug(`[codex] still running (${formatElapsed(startedAt)})`);
|
|
749
|
-
}, VERBOSE_HEARTBEAT_MS) : null;
|
|
750
|
-
const child = execFileCb(
|
|
751
|
-
"codex",
|
|
752
|
-
args,
|
|
753
|
-
{ cwd: opts.cwd, timeout: CODEX_TIMEOUT_MS, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
|
|
754
|
-
(error, stdout, stderr) => {
|
|
755
|
-
if (heartbeat) {
|
|
756
|
-
clearInterval(heartbeat);
|
|
757
|
-
}
|
|
758
|
-
stdoutHandler.flush();
|
|
759
|
-
stderrHandler.flush();
|
|
760
|
-
const normalizedStdout = stdout.trimEnd();
|
|
761
|
-
const normalizedStderr = stderr.trimEnd();
|
|
762
|
-
if (opts.verbose) {
|
|
763
|
-
debug(`[codex] finished in ${formatElapsed(startedAt)}`);
|
|
764
|
-
if (!liveLogsAttached && normalizedStdout) {
|
|
765
|
-
debug(normalizedStdout);
|
|
766
|
-
}
|
|
767
|
-
if (!liveLogsAttached && normalizedStderr) {
|
|
768
|
-
debug(normalizedStderr);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (error) {
|
|
772
|
-
const exitCode = typeof error.code === "number" ? error.code : 1;
|
|
773
|
-
if (opts.verbose) {
|
|
774
|
-
debug(`[codex] failed in ${formatElapsed(startedAt)} with exit ${String(exitCode)}`);
|
|
775
|
-
}
|
|
776
|
-
reject(new CodexError(normalizedStdout, normalizedStderr || error.message, exitCode));
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
resolve({
|
|
780
|
-
stdout: normalizedStdout,
|
|
781
|
-
stderr: normalizedStderr,
|
|
782
|
-
exitCode: 0
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
);
|
|
786
|
-
if (opts.verbose) {
|
|
787
|
-
const stdout = child.stdout;
|
|
788
|
-
const stderr = child.stderr;
|
|
789
|
-
if (stdout) {
|
|
790
|
-
liveLogsAttached = true;
|
|
791
|
-
stdout.on("data", stdoutHandler.onData);
|
|
792
|
-
}
|
|
793
|
-
if (stderr) {
|
|
794
|
-
liveLogsAttached = true;
|
|
795
|
-
stderr.on("data", stderrHandler.onData);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
});
|
|
1284
|
+
await applyDiff(sessionId);
|
|
799
1285
|
}
|
|
800
1286
|
|
|
801
1287
|
// src/lib/gh.ts
|
|
802
|
-
import { execFile as
|
|
1288
|
+
import { execFile as execFileCb } from "child_process";
|
|
803
1289
|
var GH_TIMEOUT_MS = 3e4;
|
|
1290
|
+
var GIT_TIMEOUT_MS = 3e4;
|
|
804
1291
|
var GhNotFoundError = class extends Error {
|
|
805
1292
|
constructor() {
|
|
806
1293
|
super("gh CLI not found. Install it: https://cli.github.com");
|
|
807
1294
|
this.name = "GhNotFoundError";
|
|
808
1295
|
}
|
|
809
1296
|
};
|
|
1297
|
+
var GitCommandError = class extends Error {
|
|
1298
|
+
exitCode;
|
|
1299
|
+
stderr;
|
|
1300
|
+
constructor(args, exitCode, stderr) {
|
|
1301
|
+
super(`git ${args.join(" ")} failed (exit ${String(exitCode)}): ${stderr}`);
|
|
1302
|
+
this.name = "GitCommandError";
|
|
1303
|
+
this.exitCode = exitCode;
|
|
1304
|
+
this.stderr = stderr;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
async function execGit(args, cwd) {
|
|
1308
|
+
await new Promise((resolve, reject) => {
|
|
1309
|
+
execFileCb("git", args, { cwd, timeout: GIT_TIMEOUT_MS, encoding: "utf8" }, (error2, _stdout, stderr) => {
|
|
1310
|
+
if (error2) {
|
|
1311
|
+
const exitCode = typeof error2.code === "number" ? error2.code : -1;
|
|
1312
|
+
reject(new GitCommandError(args, exitCode, (stderr || error2.message).trim()));
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
resolve();
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function isNoUpstreamError(error2) {
|
|
1320
|
+
const text = error2.stderr.toLowerCase();
|
|
1321
|
+
return text.includes("no upstream configured for branch") || text.includes("has no upstream branch");
|
|
1322
|
+
}
|
|
1323
|
+
async function pushCurrentBranch(cwd) {
|
|
1324
|
+
try {
|
|
1325
|
+
await execGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd);
|
|
1326
|
+
await execGit(["push"], cwd);
|
|
1327
|
+
} catch (error2) {
|
|
1328
|
+
if (error2 instanceof GitCommandError && isNoUpstreamError(error2)) {
|
|
1329
|
+
await execGit(["push", "--set-upstream", "origin", "HEAD"], cwd);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
throw error2;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
810
1335
|
async function checkGhAvailable() {
|
|
811
1336
|
await new Promise((resolve, reject) => {
|
|
812
|
-
|
|
813
|
-
if (
|
|
1337
|
+
execFileCb("gh", ["--version"], { timeout: GH_TIMEOUT_MS, encoding: "utf8" }, (error2) => {
|
|
1338
|
+
if (error2) {
|
|
814
1339
|
reject(new GhNotFoundError());
|
|
815
1340
|
return;
|
|
816
1341
|
}
|
|
@@ -820,14 +1345,19 @@ async function checkGhAvailable() {
|
|
|
820
1345
|
}
|
|
821
1346
|
async function createPr(opts) {
|
|
822
1347
|
const base = opts.base ?? "main";
|
|
1348
|
+
try {
|
|
1349
|
+
await pushCurrentBranch(opts.cwd);
|
|
1350
|
+
} catch (error2) {
|
|
1351
|
+
throw new Error(`Failed to push current branch before creating PR: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
1352
|
+
}
|
|
823
1353
|
return await new Promise((resolve, reject) => {
|
|
824
|
-
|
|
1354
|
+
execFileCb(
|
|
825
1355
|
"gh",
|
|
826
1356
|
["pr", "create", "--title", opts.title, "--body", opts.body, "--base", base],
|
|
827
1357
|
{ cwd: opts.cwd, timeout: GH_TIMEOUT_MS, encoding: "utf8" },
|
|
828
|
-
(
|
|
829
|
-
if (
|
|
830
|
-
reject(new Error((stderr ||
|
|
1358
|
+
(error2, stdout, stderr) => {
|
|
1359
|
+
if (error2) {
|
|
1360
|
+
reject(new Error((stderr || error2.message).trim()));
|
|
831
1361
|
return;
|
|
832
1362
|
}
|
|
833
1363
|
resolve(stdout.trim());
|
|
@@ -849,11 +1379,194 @@ async function requireGhAvailable() {
|
|
|
849
1379
|
}
|
|
850
1380
|
|
|
851
1381
|
// src/lib/review-loop.ts
|
|
852
|
-
import
|
|
1382
|
+
import path6 from "path";
|
|
1383
|
+
|
|
1384
|
+
// src/lib/copilot.ts
|
|
1385
|
+
import { spawn as spawn2 } from "child_process";
|
|
1386
|
+
var COPILOT_TIMEOUT_MS = 12e4;
|
|
1387
|
+
var CopilotReviewError = class extends Error {
|
|
1388
|
+
code;
|
|
1389
|
+
stdout;
|
|
1390
|
+
stderr;
|
|
1391
|
+
exitCode;
|
|
1392
|
+
constructor(code, message, details) {
|
|
1393
|
+
super(message);
|
|
1394
|
+
this.name = "CopilotReviewError";
|
|
1395
|
+
this.code = code;
|
|
1396
|
+
this.stdout = details?.stdout ?? "";
|
|
1397
|
+
this.stderr = details?.stderr ?? "";
|
|
1398
|
+
this.exitCode = details?.exitCode ?? 1;
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var CopilotNotFoundError = class extends CopilotReviewError {
|
|
1402
|
+
constructor() {
|
|
1403
|
+
super("not_found", "copilot CLI not found. Install and authenticate GitHub Copilot CLI, then retry.");
|
|
1404
|
+
this.name = "CopilotNotFoundError";
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
function runCopilotCommand(args, opts) {
|
|
1408
|
+
return new Promise((resolve, reject) => {
|
|
1409
|
+
const child = spawn2("copilot", args, {
|
|
1410
|
+
cwd: opts?.cwd,
|
|
1411
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1412
|
+
});
|
|
1413
|
+
let stdout = "";
|
|
1414
|
+
let stderr = "";
|
|
1415
|
+
child.stdout.on("data", (chunk) => {
|
|
1416
|
+
stdout += chunk.toString();
|
|
1417
|
+
});
|
|
1418
|
+
child.stderr.on("data", (chunk) => {
|
|
1419
|
+
stderr += chunk.toString();
|
|
1420
|
+
});
|
|
1421
|
+
const timeout = setTimeout(() => {
|
|
1422
|
+
child.kill("SIGTERM");
|
|
1423
|
+
reject(new Error(`copilot command timed out after ${String(opts?.timeoutMs ?? COPILOT_TIMEOUT_MS)}ms`));
|
|
1424
|
+
}, opts?.timeoutMs ?? COPILOT_TIMEOUT_MS);
|
|
1425
|
+
child.once("error", (error2) => {
|
|
1426
|
+
clearTimeout(timeout);
|
|
1427
|
+
reject(error2);
|
|
1428
|
+
});
|
|
1429
|
+
child.once("close", (code) => {
|
|
1430
|
+
clearTimeout(timeout);
|
|
1431
|
+
resolve({
|
|
1432
|
+
stdout: stdout.trim(),
|
|
1433
|
+
stderr: stderr.trim(),
|
|
1434
|
+
exitCode: code ?? 1
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
function normalizeSeverity(raw) {
|
|
1440
|
+
const sev = typeof raw === "string" ? raw.toLowerCase() : "";
|
|
1441
|
+
if (sev === "error" || sev === "high" || sev === "critical") {
|
|
1442
|
+
return "critical";
|
|
1443
|
+
}
|
|
1444
|
+
if (sev === "warning" || sev === "medium") {
|
|
1445
|
+
return "important";
|
|
1446
|
+
}
|
|
1447
|
+
if (sev === "info" || sev === "low") {
|
|
1448
|
+
return "minor";
|
|
1449
|
+
}
|
|
1450
|
+
return "minor";
|
|
1451
|
+
}
|
|
1452
|
+
function maybeNumber(raw) {
|
|
1453
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
1454
|
+
return Math.trunc(raw);
|
|
1455
|
+
}
|
|
1456
|
+
if (typeof raw === "string") {
|
|
1457
|
+
const n = Number.parseInt(raw, 10);
|
|
1458
|
+
if (!Number.isNaN(n)) {
|
|
1459
|
+
return n;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return void 0;
|
|
1463
|
+
}
|
|
1464
|
+
function collectParsedComments(value, comments) {
|
|
1465
|
+
if (Array.isArray(value)) {
|
|
1466
|
+
for (const item of value) {
|
|
1467
|
+
collectParsedComments(item, comments);
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (!value || typeof value !== "object") {
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const record = value;
|
|
1475
|
+
const message = [record.message, record.body, record.text].find((field) => typeof field === "string");
|
|
1476
|
+
if (typeof message === "string" && message.trim()) {
|
|
1477
|
+
comments.push({
|
|
1478
|
+
message: message.trim(),
|
|
1479
|
+
severity: record.severity ?? record.priority,
|
|
1480
|
+
file: record.file ?? record.path,
|
|
1481
|
+
line: record.line
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
for (const nested of Object.values(record)) {
|
|
1485
|
+
collectParsedComments(nested, comments);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function parseReviewComments(stdout) {
|
|
1489
|
+
const parsedObjects = [];
|
|
1490
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1491
|
+
const trimmed = line.trim();
|
|
1492
|
+
if (!trimmed) {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
parsedObjects.push(JSON.parse(trimmed));
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const parsed = [];
|
|
1501
|
+
for (const entry of parsedObjects) {
|
|
1502
|
+
collectParsedComments(entry, parsed);
|
|
1503
|
+
}
|
|
1504
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
1505
|
+
const output2 = [];
|
|
1506
|
+
for (const item of parsed) {
|
|
1507
|
+
const file = typeof item.file === "string" && item.file.length > 0 ? item.file : void 0;
|
|
1508
|
+
const line = maybeNumber(item.line);
|
|
1509
|
+
const lineKey = line === void 0 ? "" : String(line);
|
|
1510
|
+
const key = `${item.message}::${file ?? ""}::${lineKey}`;
|
|
1511
|
+
if (deduped.has(key)) {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
deduped.add(key);
|
|
1515
|
+
output2.push({
|
|
1516
|
+
severity: normalizeSeverity(item.severity),
|
|
1517
|
+
file,
|
|
1518
|
+
line,
|
|
1519
|
+
comment: item.message
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return output2;
|
|
1523
|
+
}
|
|
1524
|
+
async function checkCopilotAvailable() {
|
|
1525
|
+
try {
|
|
1526
|
+
const result = await runCopilotCommand(["--version"]);
|
|
1527
|
+
if (result.exitCode !== 0) {
|
|
1528
|
+
throw new CopilotNotFoundError();
|
|
1529
|
+
}
|
|
1530
|
+
} catch {
|
|
1531
|
+
throw new CopilotNotFoundError();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async function runCopilotReview(spec, opts) {
|
|
1535
|
+
const prompt = `Review the staged changes against the following spec.
|
|
1536
|
+
Report bugs, missing requirements, security issues, and logic errors.
|
|
1537
|
+
Ignore style issues.
|
|
1538
|
+
|
|
1539
|
+
Spec:
|
|
1540
|
+
${spec}`;
|
|
1541
|
+
let result;
|
|
1542
|
+
try {
|
|
1543
|
+
result = await runCopilotCommand(["-p", prompt, "--silent", "--output-format=json"], { cwd: opts?.cwd });
|
|
1544
|
+
} catch (error2) {
|
|
1545
|
+
throw new CopilotReviewError(
|
|
1546
|
+
"review_failed",
|
|
1547
|
+
`Failed to execute copilot review: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
if (result.exitCode !== 0) {
|
|
1551
|
+
throw new CopilotReviewError("review_failed", "Copilot review command failed.", result);
|
|
1552
|
+
}
|
|
1553
|
+
if (!result.stdout.trim()) {
|
|
1554
|
+
throw new CopilotReviewError("review_failed", "Copilot review produced no output.", result);
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
return parseReviewComments(result.stdout);
|
|
1558
|
+
} catch (error2) {
|
|
1559
|
+
throw new CopilotReviewError(
|
|
1560
|
+
"parse_failed",
|
|
1561
|
+
`Failed to parse Copilot review output: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
1562
|
+
result
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
853
1566
|
|
|
854
1567
|
// src/lib/git.ts
|
|
855
|
-
import { execFile as
|
|
856
|
-
var
|
|
1568
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
1569
|
+
var GIT_TIMEOUT_MS2 = 3e4;
|
|
857
1570
|
var GitError = class extends Error {
|
|
858
1571
|
command;
|
|
859
1572
|
exitCode;
|
|
@@ -868,10 +1581,10 @@ var GitError = class extends Error {
|
|
|
868
1581
|
};
|
|
869
1582
|
async function exec2(args, cwd) {
|
|
870
1583
|
return new Promise((resolve, reject) => {
|
|
871
|
-
|
|
872
|
-
if (
|
|
873
|
-
const exitCode = typeof
|
|
874
|
-
reject(new GitError(args, exitCode, (stderr ||
|
|
1584
|
+
execFileCb2("git", args, { cwd, timeout: GIT_TIMEOUT_MS2, encoding: "utf8" }, (error2, stdout, stderr) => {
|
|
1585
|
+
if (error2) {
|
|
1586
|
+
const exitCode = typeof error2.code === "number" ? error2.code : -1;
|
|
1587
|
+
reject(new GitError(args, exitCode, (stderr || error2.message).trim()));
|
|
875
1588
|
return;
|
|
876
1589
|
}
|
|
877
1590
|
resolve(stdout.trimEnd());
|
|
@@ -882,11 +1595,11 @@ async function branchExists(name, cwd) {
|
|
|
882
1595
|
try {
|
|
883
1596
|
await exec2(["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], cwd);
|
|
884
1597
|
return true;
|
|
885
|
-
} catch (
|
|
886
|
-
if (
|
|
1598
|
+
} catch (error2) {
|
|
1599
|
+
if (error2 instanceof GitError && error2.exitCode === 1) {
|
|
887
1600
|
return false;
|
|
888
1601
|
}
|
|
889
|
-
throw
|
|
1602
|
+
throw error2;
|
|
890
1603
|
}
|
|
891
1604
|
}
|
|
892
1605
|
async function createBranch(name, cwd) {
|
|
@@ -898,7 +1611,7 @@ async function createBranch(name, cwd) {
|
|
|
898
1611
|
async function checkoutBranch(name, cwd) {
|
|
899
1612
|
await exec2(["checkout", name], cwd);
|
|
900
1613
|
}
|
|
901
|
-
async function
|
|
1614
|
+
async function getDiff2(cwd, base) {
|
|
902
1615
|
if (base) {
|
|
903
1616
|
return exec2(["diff", `${base}..HEAD`], cwd);
|
|
904
1617
|
}
|
|
@@ -909,7 +1622,7 @@ function getBranchName(taskId, service) {
|
|
|
909
1622
|
}
|
|
910
1623
|
|
|
911
1624
|
// src/lib/review-loop.ts
|
|
912
|
-
function
|
|
1625
|
+
function formatElapsed(startedAt) {
|
|
913
1626
|
const seconds = Math.max(0, Math.round((Date.now() - startedAt) / 1e3));
|
|
914
1627
|
return `${String(seconds)}s`;
|
|
915
1628
|
}
|
|
@@ -931,12 +1644,12 @@ async function runReviewLoop(opts) {
|
|
|
931
1644
|
if (!serviceConfig) {
|
|
932
1645
|
throw new Error(`Unknown service in step: ${opts.step.service}`);
|
|
933
1646
|
}
|
|
934
|
-
const serviceRoot =
|
|
1647
|
+
const serviceRoot = path6.resolve(opts.projectRoot, serviceConfig.path);
|
|
935
1648
|
let iteration2 = opts.stepState.iteration;
|
|
936
1649
|
for (; ; ) {
|
|
937
1650
|
iteration(iteration2 + 1, opts.config.review.max_iterations);
|
|
938
1651
|
info(`Collecting git diff for service ${opts.step.service}`);
|
|
939
|
-
const diff = await
|
|
1652
|
+
const diff = await getDiff2(serviceRoot);
|
|
940
1653
|
if (opts.verbose) {
|
|
941
1654
|
info(`Diff collected (${String(diff.length)} chars)`);
|
|
942
1655
|
}
|
|
@@ -955,23 +1668,20 @@ async function runReviewLoop(opts) {
|
|
|
955
1668
|
info(`Requesting reviewer analysis (model: ${opts.config.review.model})`);
|
|
956
1669
|
const reviewerStartedAt = Date.now();
|
|
957
1670
|
const reviewerHeartbeat = opts.verbose ? setInterval(() => {
|
|
958
|
-
info(`Waiting for reviewer response (${
|
|
1671
|
+
info(`Waiting for reviewer response (${formatElapsed(reviewerStartedAt)})`);
|
|
959
1672
|
}, 15e3) : null;
|
|
960
|
-
const
|
|
961
|
-
spec: opts.step.spec,
|
|
962
|
-
diff,
|
|
963
|
-
model: opts.config.review.model
|
|
964
|
-
}).finally(() => {
|
|
1673
|
+
const comments = await runCopilotReview(opts.step.spec, { cwd: serviceRoot }).finally(() => {
|
|
965
1674
|
if (reviewerHeartbeat) {
|
|
966
1675
|
clearInterval(reviewerHeartbeat);
|
|
967
1676
|
}
|
|
968
1677
|
});
|
|
969
|
-
info(`Reviewer response received in ${
|
|
1678
|
+
info(`Reviewer response received in ${formatElapsed(reviewerStartedAt)}`);
|
|
1679
|
+
const review = { comments };
|
|
970
1680
|
reviewSummary(review.comments);
|
|
971
1681
|
info(`Requesting arbiter decision (model: ${opts.config.review.model})`);
|
|
972
1682
|
const arbiterStartedAt = Date.now();
|
|
973
1683
|
const arbiterHeartbeat = opts.verbose ? setInterval(() => {
|
|
974
|
-
info(`Waiting for arbiter response (${
|
|
1684
|
+
info(`Waiting for arbiter response (${formatElapsed(arbiterStartedAt)})`);
|
|
975
1685
|
}, 15e3) : null;
|
|
976
1686
|
const arbiter = await opts.claude.runArbiter({
|
|
977
1687
|
spec: opts.step.spec,
|
|
@@ -983,7 +1693,7 @@ async function runReviewLoop(opts) {
|
|
|
983
1693
|
clearInterval(arbiterHeartbeat);
|
|
984
1694
|
}
|
|
985
1695
|
});
|
|
986
|
-
info(`Arbiter response received in ${
|
|
1696
|
+
info(`Arbiter response received in ${formatElapsed(arbiterStartedAt)}`);
|
|
987
1697
|
info(`Arbiter decision: ${arbiter.decision} (${arbiter.summary})`);
|
|
988
1698
|
saveIterationLog(opts.projectRoot, opts.taskId, opts.step.service, iteration2, {
|
|
989
1699
|
diff,
|
|
@@ -1045,7 +1755,7 @@ async function runReviewLoop(opts) {
|
|
|
1045
1755
|
}
|
|
1046
1756
|
|
|
1047
1757
|
// src/commands/fix.ts
|
|
1048
|
-
function
|
|
1758
|
+
function fatalAndExit3(message) {
|
|
1049
1759
|
fatal(message);
|
|
1050
1760
|
process.exit(1);
|
|
1051
1761
|
}
|
|
@@ -1053,43 +1763,43 @@ async function runFix(feedback, options) {
|
|
|
1053
1763
|
try {
|
|
1054
1764
|
const projectRoot = findProjectRoot();
|
|
1055
1765
|
if (!projectRoot) {
|
|
1056
|
-
|
|
1766
|
+
fatalAndExit3("Not inside a vexdo project.");
|
|
1057
1767
|
}
|
|
1058
1768
|
const config = loadConfig(projectRoot);
|
|
1059
1769
|
const state = loadState(projectRoot);
|
|
1060
1770
|
if (!state) {
|
|
1061
|
-
|
|
1771
|
+
fatalAndExit3("No active task.");
|
|
1062
1772
|
}
|
|
1063
1773
|
if (!options.dryRun) {
|
|
1064
1774
|
requireAnthropicApiKey();
|
|
1065
1775
|
await checkCodexAvailable();
|
|
1066
1776
|
}
|
|
1067
|
-
const currentStep = state.steps.find((
|
|
1777
|
+
const currentStep = state.steps.find((step2) => step2.status === "in_progress" || step2.status === "pending");
|
|
1068
1778
|
if (!currentStep) {
|
|
1069
|
-
|
|
1779
|
+
fatalAndExit3("No in-progress step found in active task.");
|
|
1070
1780
|
}
|
|
1071
1781
|
const task = loadAndValidateTask(state.taskPath, config);
|
|
1072
|
-
const
|
|
1073
|
-
if (!
|
|
1074
|
-
|
|
1782
|
+
const step = task.steps.find((item) => item.service === currentStep.service);
|
|
1783
|
+
if (!step) {
|
|
1784
|
+
fatalAndExit3(`Could not locate task step for service '${currentStep.service}'.`);
|
|
1075
1785
|
}
|
|
1076
1786
|
if (!options.dryRun) {
|
|
1077
1787
|
const serviceConfig = config.services.find((service) => service.name === currentStep.service);
|
|
1078
1788
|
if (!serviceConfig) {
|
|
1079
|
-
|
|
1789
|
+
fatalAndExit3(`Unknown service in step: ${currentStep.service}`);
|
|
1080
1790
|
}
|
|
1081
1791
|
await exec({
|
|
1082
1792
|
spec: feedback,
|
|
1083
1793
|
model: config.codex.model,
|
|
1084
|
-
cwd:
|
|
1794
|
+
cwd: path7.resolve(projectRoot, serviceConfig.path),
|
|
1085
1795
|
verbose: options.verbose
|
|
1086
1796
|
});
|
|
1087
1797
|
}
|
|
1088
|
-
info(`Running review loop for service ${
|
|
1798
|
+
info(`Running review loop for service ${step.service}`);
|
|
1089
1799
|
const result = await runReviewLoop({
|
|
1090
1800
|
taskId: task.id,
|
|
1091
1801
|
task,
|
|
1092
|
-
step
|
|
1802
|
+
step,
|
|
1093
1803
|
stepState: currentStep,
|
|
1094
1804
|
projectRoot,
|
|
1095
1805
|
config,
|
|
@@ -1112,8 +1822,8 @@ async function runFix(feedback, options) {
|
|
|
1112
1822
|
if (!options.dryRun) {
|
|
1113
1823
|
saveState(projectRoot, state);
|
|
1114
1824
|
}
|
|
1115
|
-
} catch (
|
|
1116
|
-
|
|
1825
|
+
} catch (error2) {
|
|
1826
|
+
fatalAndExit3(error2 instanceof Error ? error2.message : String(error2));
|
|
1117
1827
|
}
|
|
1118
1828
|
}
|
|
1119
1829
|
function registerFixCommand(program2) {
|
|
@@ -1124,8 +1834,8 @@ function registerFixCommand(program2) {
|
|
|
1124
1834
|
}
|
|
1125
1835
|
|
|
1126
1836
|
// src/commands/init.ts
|
|
1127
|
-
import
|
|
1128
|
-
import
|
|
1837
|
+
import fs6 from "fs";
|
|
1838
|
+
import path8 from "path";
|
|
1129
1839
|
import { createInterface } from "readline/promises";
|
|
1130
1840
|
import { stdin as input, stdout as output } from "process";
|
|
1131
1841
|
import { stringify } from "yaml";
|
|
@@ -1154,24 +1864,24 @@ function parseMaxIterations(value) {
|
|
|
1154
1864
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_ITERATIONS2;
|
|
1155
1865
|
}
|
|
1156
1866
|
function ensureGitignoreEntry(gitignorePath, entry) {
|
|
1157
|
-
if (!
|
|
1158
|
-
|
|
1867
|
+
if (!fs6.existsSync(gitignorePath)) {
|
|
1868
|
+
fs6.writeFileSync(gitignorePath, `${entry}
|
|
1159
1869
|
`, "utf8");
|
|
1160
1870
|
return true;
|
|
1161
1871
|
}
|
|
1162
|
-
const content =
|
|
1872
|
+
const content = fs6.readFileSync(gitignorePath, "utf8");
|
|
1163
1873
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
1164
1874
|
if (lines.includes(entry)) {
|
|
1165
1875
|
return false;
|
|
1166
1876
|
}
|
|
1167
1877
|
const suffix = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
1168
|
-
|
|
1878
|
+
fs6.appendFileSync(gitignorePath, `${suffix}${entry}
|
|
1169
1879
|
`, "utf8");
|
|
1170
1880
|
return true;
|
|
1171
1881
|
}
|
|
1172
1882
|
async function runInit(projectRoot, prompt = defaultPrompt) {
|
|
1173
|
-
const configPath =
|
|
1174
|
-
if (
|
|
1883
|
+
const configPath = path8.join(projectRoot, ".vexdo.yml");
|
|
1884
|
+
if (fs6.existsSync(configPath)) {
|
|
1175
1885
|
warn("Found existing .vexdo.yml.");
|
|
1176
1886
|
const overwriteAnswer = await prompt("Overwrite existing .vexdo.yml? (y/N): ");
|
|
1177
1887
|
if (!parseBoolean(overwriteAnswer)) {
|
|
@@ -1207,20 +1917,20 @@ async function runInit(projectRoot, prompt = defaultPrompt) {
|
|
|
1207
1917
|
model: codexModelRaw.trim() || DEFAULT_CODEX_MODEL2
|
|
1208
1918
|
}
|
|
1209
1919
|
};
|
|
1210
|
-
|
|
1920
|
+
fs6.writeFileSync(configPath, stringify(config), "utf8");
|
|
1211
1921
|
const createdDirs = [];
|
|
1212
1922
|
for (const taskDir of TASK_DIRS) {
|
|
1213
|
-
const directory =
|
|
1214
|
-
|
|
1215
|
-
createdDirs.push(
|
|
1216
|
-
}
|
|
1217
|
-
const logDir =
|
|
1218
|
-
|
|
1219
|
-
createdDirs.push(
|
|
1220
|
-
const gitignorePath =
|
|
1923
|
+
const directory = path8.join(projectRoot, "tasks", taskDir);
|
|
1924
|
+
fs6.mkdirSync(directory, { recursive: true });
|
|
1925
|
+
createdDirs.push(path8.relative(projectRoot, directory));
|
|
1926
|
+
}
|
|
1927
|
+
const logDir = path8.join(projectRoot, ".vexdo", "logs");
|
|
1928
|
+
fs6.mkdirSync(logDir, { recursive: true });
|
|
1929
|
+
createdDirs.push(path8.relative(projectRoot, logDir));
|
|
1930
|
+
const gitignorePath = path8.join(projectRoot, ".gitignore");
|
|
1221
1931
|
const gitignoreUpdated = ensureGitignoreEntry(gitignorePath, ".vexdo/");
|
|
1222
1932
|
success("Initialized vexdo project.");
|
|
1223
|
-
info(`Created: ${
|
|
1933
|
+
info(`Created: ${path8.relative(projectRoot, configPath)}`);
|
|
1224
1934
|
info(`Created directories: ${createdDirs.join(", ")}`);
|
|
1225
1935
|
if (gitignoreUpdated) {
|
|
1226
1936
|
info("Updated .gitignore with .vexdo/");
|
|
@@ -1234,46 +1944,46 @@ function registerInitCommand(program2) {
|
|
|
1234
1944
|
}
|
|
1235
1945
|
|
|
1236
1946
|
// src/commands/logs.ts
|
|
1237
|
-
import
|
|
1238
|
-
import
|
|
1239
|
-
function
|
|
1947
|
+
import fs7 from "fs";
|
|
1948
|
+
import path9 from "path";
|
|
1949
|
+
function fatalAndExit4(message) {
|
|
1240
1950
|
fatal(message);
|
|
1241
1951
|
process.exit(1);
|
|
1242
1952
|
}
|
|
1243
1953
|
function runLogs(taskIdArg, options) {
|
|
1244
1954
|
const projectRoot = findProjectRoot();
|
|
1245
1955
|
if (!projectRoot) {
|
|
1246
|
-
|
|
1956
|
+
fatalAndExit4("Not inside a vexdo project.");
|
|
1247
1957
|
}
|
|
1248
1958
|
const state = loadState(projectRoot);
|
|
1249
1959
|
const taskId = taskIdArg ?? state?.taskId;
|
|
1250
1960
|
if (!taskId) {
|
|
1251
|
-
const base =
|
|
1252
|
-
if (!
|
|
1961
|
+
const base = path9.join(getStateDir(projectRoot), "logs");
|
|
1962
|
+
if (!fs7.existsSync(base)) {
|
|
1253
1963
|
info("No logs available.");
|
|
1254
1964
|
return;
|
|
1255
1965
|
}
|
|
1256
|
-
const tasks =
|
|
1966
|
+
const tasks = fs7.readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
1257
1967
|
for (const dir of tasks) {
|
|
1258
1968
|
info(dir.name);
|
|
1259
1969
|
}
|
|
1260
1970
|
return;
|
|
1261
1971
|
}
|
|
1262
1972
|
const logsDir = getLogsDir(projectRoot, taskId);
|
|
1263
|
-
if (!
|
|
1264
|
-
|
|
1973
|
+
if (!fs7.existsSync(logsDir)) {
|
|
1974
|
+
fatalAndExit4(`No logs found for task '${taskId}'.`);
|
|
1265
1975
|
}
|
|
1266
|
-
const files =
|
|
1976
|
+
const files = fs7.readdirSync(logsDir).filter((name) => name.endsWith("-arbiter.json"));
|
|
1267
1977
|
for (const arbiterFile of files) {
|
|
1268
1978
|
const base = arbiterFile.replace(/-arbiter\.json$/, "");
|
|
1269
|
-
const arbiterPath =
|
|
1270
|
-
const reviewPath =
|
|
1271
|
-
const diffPath =
|
|
1272
|
-
const arbiter = JSON.parse(
|
|
1273
|
-
const review = JSON.parse(
|
|
1979
|
+
const arbiterPath = path9.join(logsDir, `${base}-arbiter.json`);
|
|
1980
|
+
const reviewPath = path9.join(logsDir, `${base}-review.json`);
|
|
1981
|
+
const diffPath = path9.join(logsDir, `${base}-diff.txt`);
|
|
1982
|
+
const arbiter = JSON.parse(fs7.readFileSync(arbiterPath, "utf8"));
|
|
1983
|
+
const review = JSON.parse(fs7.readFileSync(reviewPath, "utf8"));
|
|
1274
1984
|
info(`${base}: decision=${arbiter.decision}, comments=${String(review.comments?.length ?? 0)}, summary=${arbiter.summary}`);
|
|
1275
1985
|
if (options?.full) {
|
|
1276
|
-
console.log(
|
|
1986
|
+
console.log(fs7.readFileSync(diffPath, "utf8"));
|
|
1277
1987
|
console.log(JSON.stringify(review, null, 2));
|
|
1278
1988
|
console.log(JSON.stringify(arbiter, null, 2));
|
|
1279
1989
|
}
|
|
@@ -1286,8 +1996,8 @@ function registerLogsCommand(program2) {
|
|
|
1286
1996
|
}
|
|
1287
1997
|
|
|
1288
1998
|
// src/commands/review.ts
|
|
1289
|
-
import
|
|
1290
|
-
function
|
|
1999
|
+
import fs8 from "fs";
|
|
2000
|
+
function fatalAndExit5(message) {
|
|
1291
2001
|
fatal(message);
|
|
1292
2002
|
process.exit(1);
|
|
1293
2003
|
}
|
|
@@ -1295,33 +2005,33 @@ async function runReview(options) {
|
|
|
1295
2005
|
try {
|
|
1296
2006
|
const projectRoot = findProjectRoot();
|
|
1297
2007
|
if (!projectRoot) {
|
|
1298
|
-
|
|
2008
|
+
fatalAndExit5("Not inside a vexdo project.");
|
|
1299
2009
|
}
|
|
1300
2010
|
const config = loadConfig(projectRoot);
|
|
1301
2011
|
const state = loadState(projectRoot);
|
|
1302
2012
|
if (!state) {
|
|
1303
|
-
|
|
2013
|
+
fatalAndExit5("No active task.");
|
|
1304
2014
|
}
|
|
1305
2015
|
if (!options.dryRun) {
|
|
1306
2016
|
requireAnthropicApiKey();
|
|
1307
2017
|
}
|
|
1308
|
-
const currentStep = state.steps.find((
|
|
2018
|
+
const currentStep = state.steps.find((step2) => step2.status === "in_progress" || step2.status === "pending");
|
|
1309
2019
|
if (!currentStep) {
|
|
1310
|
-
|
|
2020
|
+
fatalAndExit5("No in-progress step found in active task.");
|
|
1311
2021
|
}
|
|
1312
|
-
if (!
|
|
1313
|
-
|
|
2022
|
+
if (!fs8.existsSync(state.taskPath)) {
|
|
2023
|
+
fatalAndExit5(`Task file not found: ${state.taskPath}`);
|
|
1314
2024
|
}
|
|
1315
2025
|
const task = loadAndValidateTask(state.taskPath, config);
|
|
1316
|
-
const
|
|
1317
|
-
if (!
|
|
1318
|
-
|
|
2026
|
+
const step = task.steps.find((item) => item.service === currentStep.service);
|
|
2027
|
+
if (!step) {
|
|
2028
|
+
fatalAndExit5(`Could not locate task step for service '${currentStep.service}'.`);
|
|
1319
2029
|
}
|
|
1320
|
-
info(`Running review loop for service ${
|
|
2030
|
+
info(`Running review loop for service ${step.service}`);
|
|
1321
2031
|
const result = await runReviewLoop({
|
|
1322
2032
|
taskId: task.id,
|
|
1323
2033
|
task,
|
|
1324
|
-
step
|
|
2034
|
+
step,
|
|
1325
2035
|
stepState: currentStep,
|
|
1326
2036
|
projectRoot,
|
|
1327
2037
|
config,
|
|
@@ -1332,9 +2042,9 @@ async function runReview(options) {
|
|
|
1332
2042
|
if (result.decision === "escalate") {
|
|
1333
2043
|
escalation({
|
|
1334
2044
|
taskId: task.id,
|
|
1335
|
-
service:
|
|
2045
|
+
service: step.service,
|
|
1336
2046
|
iteration: result.finalIteration,
|
|
1337
|
-
spec:
|
|
2047
|
+
spec: step.spec,
|
|
1338
2048
|
diff: "",
|
|
1339
2049
|
reviewComments: result.lastReviewComments,
|
|
1340
2050
|
arbiterReasoning: result.lastArbiterResult.reasoning,
|
|
@@ -1354,9 +2064,9 @@ async function runReview(options) {
|
|
|
1354
2064
|
if (!options.dryRun) {
|
|
1355
2065
|
saveState(projectRoot, state);
|
|
1356
2066
|
}
|
|
1357
|
-
} catch (
|
|
1358
|
-
const message =
|
|
1359
|
-
|
|
2067
|
+
} catch (error2) {
|
|
2068
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2069
|
+
fatalAndExit5(message);
|
|
1360
2070
|
}
|
|
1361
2071
|
}
|
|
1362
2072
|
function registerReviewCommand(program2) {
|
|
@@ -1367,25 +2077,103 @@ function registerReviewCommand(program2) {
|
|
|
1367
2077
|
}
|
|
1368
2078
|
|
|
1369
2079
|
// src/commands/start.ts
|
|
1370
|
-
import
|
|
2080
|
+
import path11 from "path";
|
|
2081
|
+
|
|
2082
|
+
// src/lib/runner.ts
|
|
2083
|
+
async function runStepsConcurrently(steps, config, runStep) {
|
|
2084
|
+
const maxConcurrent = config.maxConcurrent ?? Number.POSITIVE_INFINITY;
|
|
2085
|
+
if (!Number.isFinite(maxConcurrent) && maxConcurrent !== Number.POSITIVE_INFINITY) {
|
|
2086
|
+
throw new Error("maxConcurrent must be a finite number when specified");
|
|
2087
|
+
}
|
|
2088
|
+
if (Number.isFinite(maxConcurrent) && (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0)) {
|
|
2089
|
+
throw new Error("maxConcurrent must be a positive integer when specified");
|
|
2090
|
+
}
|
|
2091
|
+
const stepByService = new Map(steps.map((step) => [step.service, step]));
|
|
2092
|
+
for (const step of steps) {
|
|
2093
|
+
for (const dependency of step.depends_on ?? []) {
|
|
2094
|
+
if (!stepByService.has(dependency)) {
|
|
2095
|
+
throw new Error(`Unknown dependency '${dependency}' for service '${step.service}'`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
const results = /* @__PURE__ */ new Map();
|
|
2100
|
+
const running = /* @__PURE__ */ new Map();
|
|
2101
|
+
while (results.size < steps.length) {
|
|
2102
|
+
let dispatched = false;
|
|
2103
|
+
for (const step of steps) {
|
|
2104
|
+
if (results.has(step.service) || running.has(step.service)) {
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
const dependencies = step.depends_on ?? [];
|
|
2108
|
+
const depResults = dependencies.map((service) => results.get(service));
|
|
2109
|
+
if (depResults.some((result) => result === void 0)) {
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
const blockedByDependencyFailure = depResults.some((result) => result?.status !== "done");
|
|
2113
|
+
if (blockedByDependencyFailure) {
|
|
2114
|
+
results.set(step.service, {
|
|
2115
|
+
service: step.service,
|
|
2116
|
+
status: "failed",
|
|
2117
|
+
error: "dependency_failed"
|
|
2118
|
+
});
|
|
2119
|
+
dispatched = true;
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
if (running.size >= maxConcurrent) {
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
dispatched = true;
|
|
2126
|
+
const runningPromise = runStep(step).then((result) => {
|
|
2127
|
+
results.set(step.service, result);
|
|
2128
|
+
}).catch((error2) => {
|
|
2129
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2130
|
+
results.set(step.service, {
|
|
2131
|
+
service: step.service,
|
|
2132
|
+
status: "failed",
|
|
2133
|
+
error: message
|
|
2134
|
+
});
|
|
2135
|
+
}).finally(() => {
|
|
2136
|
+
running.delete(step.service);
|
|
2137
|
+
});
|
|
2138
|
+
running.set(step.service, runningPromise);
|
|
2139
|
+
}
|
|
2140
|
+
if (!dispatched) {
|
|
2141
|
+
if (running.size === 0) {
|
|
2142
|
+
throw new Error("Unable to schedule all steps; dependency graph may contain a cycle.");
|
|
2143
|
+
}
|
|
2144
|
+
await Promise.race(running.values());
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return steps.map((step) => {
|
|
2148
|
+
const result = results.get(step.service);
|
|
2149
|
+
if (!result) {
|
|
2150
|
+
return {
|
|
2151
|
+
service: step.service,
|
|
2152
|
+
status: "failed",
|
|
2153
|
+
error: "not_executed"
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
return result;
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
1371
2159
|
|
|
1372
2160
|
// src/lib/submit-task.ts
|
|
1373
|
-
import
|
|
1374
|
-
import
|
|
2161
|
+
import fs9 from "fs";
|
|
2162
|
+
import path10 from "path";
|
|
1375
2163
|
async function submitActiveTask(projectRoot, config, state) {
|
|
1376
|
-
for (const
|
|
1377
|
-
if (
|
|
2164
|
+
for (const step of state.steps) {
|
|
2165
|
+
if (step.status !== "done" && step.status !== "in_progress") {
|
|
1378
2166
|
continue;
|
|
1379
2167
|
}
|
|
1380
|
-
const service = config.services.find((item) => item.name ===
|
|
2168
|
+
const service = config.services.find((item) => item.name === step.service);
|
|
1381
2169
|
if (!service) {
|
|
1382
|
-
throw new Error(`Unknown service in state: ${
|
|
2170
|
+
throw new Error(`Unknown service in state: ${step.service}`);
|
|
1383
2171
|
}
|
|
1384
|
-
const servicePath =
|
|
2172
|
+
const servicePath = path10.resolve(projectRoot, service.path);
|
|
1385
2173
|
const body = `Task: ${state.taskId}
|
|
1386
|
-
Service: ${
|
|
2174
|
+
Service: ${step.service}`;
|
|
1387
2175
|
const url = await createPr({
|
|
1388
|
-
title: `${state.taskTitle} [${
|
|
2176
|
+
title: `${state.taskTitle} [${step.service}]`,
|
|
1389
2177
|
body,
|
|
1390
2178
|
base: "main",
|
|
1391
2179
|
cwd: servicePath
|
|
@@ -1395,7 +2183,7 @@ Service: ${step2.service}`;
|
|
|
1395
2183
|
state.status = "done";
|
|
1396
2184
|
saveState(projectRoot, state);
|
|
1397
2185
|
const doneDir = ensureTaskDirectory(projectRoot, "done");
|
|
1398
|
-
if (
|
|
2186
|
+
if (fs9.existsSync(state.taskPath)) {
|
|
1399
2187
|
state.taskPath = moveTaskFileAtomically(state.taskPath, doneDir);
|
|
1400
2188
|
saveState(projectRoot, state);
|
|
1401
2189
|
}
|
|
@@ -1403,7 +2191,9 @@ Service: ${step2.service}`;
|
|
|
1403
2191
|
}
|
|
1404
2192
|
|
|
1405
2193
|
// src/commands/start.ts
|
|
1406
|
-
|
|
2194
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
2195
|
+
var POLL_TIMEOUT_MS = 10 * 6e4;
|
|
2196
|
+
function fatalAndExit6(message, hint) {
|
|
1407
2197
|
fatal(message, hint);
|
|
1408
2198
|
process.exit(1);
|
|
1409
2199
|
}
|
|
@@ -1411,17 +2201,18 @@ async function runStart(taskFile, options) {
|
|
|
1411
2201
|
try {
|
|
1412
2202
|
const projectRoot = findProjectRoot();
|
|
1413
2203
|
if (!projectRoot) {
|
|
1414
|
-
|
|
2204
|
+
fatalAndExit6("Not inside a vexdo project. Could not find .vexdo.yml.");
|
|
1415
2205
|
}
|
|
1416
2206
|
const config = loadConfig(projectRoot);
|
|
1417
|
-
const taskPath =
|
|
2207
|
+
const taskPath = path11.resolve(taskFile);
|
|
1418
2208
|
const task = loadAndValidateTask(taskPath, config);
|
|
1419
2209
|
if (hasActiveTask(projectRoot) && !options.resume) {
|
|
1420
|
-
|
|
2210
|
+
fatalAndExit6("An active task already exists.", "Use --resume to continue or 'vexdo abort' to cancel.");
|
|
1421
2211
|
}
|
|
1422
2212
|
if (!options.dryRun) {
|
|
1423
2213
|
requireAnthropicApiKey();
|
|
1424
2214
|
await checkCodexAvailable();
|
|
2215
|
+
await checkCopilotAvailable();
|
|
1425
2216
|
}
|
|
1426
2217
|
let state = loadState(projectRoot);
|
|
1427
2218
|
if (!options.resume) {
|
|
@@ -1436,94 +2227,119 @@ async function runStart(taskFile, options) {
|
|
|
1436
2227
|
}
|
|
1437
2228
|
}
|
|
1438
2229
|
if (!state) {
|
|
1439
|
-
|
|
2230
|
+
fatalAndExit6("No resumable task state found.");
|
|
1440
2231
|
}
|
|
1441
2232
|
const claude = new ClaudeClient(process.env.ANTHROPIC_API_KEY ?? "");
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
2233
|
+
const stepResults = await runStepsConcurrently(
|
|
2234
|
+
task.steps,
|
|
2235
|
+
{ maxConcurrent: config.maxConcurrent },
|
|
2236
|
+
async (step) => {
|
|
2237
|
+
const scopedLogger = withPrefix(`[${step.service}]`);
|
|
2238
|
+
const latestState = options.dryRun ? state : loadState(projectRoot);
|
|
2239
|
+
const stepState = latestState?.steps.find((item) => item.service === step.service);
|
|
2240
|
+
if (!stepState) {
|
|
2241
|
+
return { service: step.service, status: "failed", error: "step_state_missing" };
|
|
2242
|
+
}
|
|
2243
|
+
if (stepState.status === "done") {
|
|
2244
|
+
return { service: step.service, status: "done", sessionId: stepState.session_id };
|
|
2245
|
+
}
|
|
2246
|
+
const serviceCfg = config.services.find((service) => service.name === step.service);
|
|
2247
|
+
if (!serviceCfg) {
|
|
2248
|
+
return { service: step.service, status: "failed", error: `Unknown service in step: ${step.service}` };
|
|
2249
|
+
}
|
|
2250
|
+
const serviceRoot = path11.resolve(projectRoot, serviceCfg.path);
|
|
2251
|
+
const branch = stepState.branch ?? getBranchName(task.id, step.service);
|
|
2252
|
+
try {
|
|
2253
|
+
if (!options.dryRun) {
|
|
2254
|
+
if (options.resume) {
|
|
2255
|
+
if (stepState.branch) {
|
|
2256
|
+
await checkoutBranch(stepState.branch, serviceRoot);
|
|
2257
|
+
} else {
|
|
2258
|
+
await createBranch(branch, serviceRoot);
|
|
2259
|
+
}
|
|
2260
|
+
} else {
|
|
2261
|
+
await createBranch(branch, serviceRoot);
|
|
2262
|
+
}
|
|
2263
|
+
await updateStep(projectRoot, task.id, step.service, {
|
|
2264
|
+
status: "in_progress",
|
|
2265
|
+
branch
|
|
2266
|
+
});
|
|
1457
2267
|
}
|
|
2268
|
+
if (options.dryRun) {
|
|
2269
|
+
scopedLogger.info(`[dry-run] Would run codex cloud implementation for service ${step.service}`);
|
|
2270
|
+
return { service: step.service, status: "done" };
|
|
2271
|
+
}
|
|
2272
|
+
scopedLogger.info("Submitting to Codex Cloud...");
|
|
2273
|
+
const submissionSession = stepState.session_id ?? await submitTask(step.spec, { cwd: serviceRoot });
|
|
2274
|
+
await updateStep(projectRoot, task.id, step.service, { session_id: submissionSession });
|
|
2275
|
+
const execution = await runCloudReviewLoop({
|
|
2276
|
+
taskId: task.id,
|
|
2277
|
+
service: step.service,
|
|
2278
|
+
spec: step.spec,
|
|
2279
|
+
sessionId: submissionSession,
|
|
2280
|
+
stepState: {
|
|
2281
|
+
iteration: stepState.iteration,
|
|
2282
|
+
session_id: submissionSession
|
|
2283
|
+
},
|
|
2284
|
+
projectRoot,
|
|
2285
|
+
config,
|
|
2286
|
+
claude,
|
|
2287
|
+
verbose: options.verbose,
|
|
2288
|
+
log: scopedLogger,
|
|
2289
|
+
serviceRoot
|
|
2290
|
+
});
|
|
2291
|
+
await updateStep(projectRoot, task.id, step.service, {
|
|
2292
|
+
lastReviewComments: execution.lastReviewComments,
|
|
2293
|
+
lastArbiterResult: execution.lastArbiterResult,
|
|
2294
|
+
iteration: execution.finalIteration,
|
|
2295
|
+
session_id: execution.sessionId
|
|
2296
|
+
});
|
|
2297
|
+
if (execution.lastArbiterResult.decision === "escalate") {
|
|
2298
|
+
escalation({
|
|
2299
|
+
taskId: task.id,
|
|
2300
|
+
service: step.service,
|
|
2301
|
+
iteration: execution.finalIteration,
|
|
2302
|
+
spec: step.spec,
|
|
2303
|
+
diff: "",
|
|
2304
|
+
reviewComments: execution.lastReviewComments,
|
|
2305
|
+
arbiterReasoning: execution.lastArbiterResult.reasoning,
|
|
2306
|
+
summary: execution.lastArbiterResult.summary
|
|
2307
|
+
});
|
|
2308
|
+
await updateStep(projectRoot, task.id, step.service, { status: "escalated" });
|
|
2309
|
+
return { service: step.service, status: "escalated", sessionId: execution.sessionId };
|
|
2310
|
+
}
|
|
2311
|
+
await updateStep(projectRoot, task.id, step.service, { status: "done" });
|
|
2312
|
+
scopedLogger.success("Review passed \u2014 ready for PR");
|
|
2313
|
+
return { service: step.service, status: "done", sessionId: execution.sessionId };
|
|
2314
|
+
} catch (error2) {
|
|
2315
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2316
|
+
if (!options.dryRun) {
|
|
2317
|
+
await updateStep(projectRoot, task.id, step.service, { status: "failed" });
|
|
2318
|
+
}
|
|
2319
|
+
scopedLogger.error(message);
|
|
2320
|
+
return { service: step.service, status: "failed", error: message };
|
|
1458
2321
|
}
|
|
1459
2322
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
const serviceRoot = path10.resolve(projectRoot, serviceCfg.path);
|
|
1466
|
-
const branch = getBranchName(task.id, step2.service);
|
|
1467
|
-
if (!options.dryRun) {
|
|
1468
|
-
if (options.resume) {
|
|
1469
|
-
await checkoutBranch(stepState.branch ?? branch, serviceRoot);
|
|
1470
|
-
} else {
|
|
1471
|
-
await createBranch(branch, serviceRoot);
|
|
2323
|
+
);
|
|
2324
|
+
if (!options.dryRun) {
|
|
2325
|
+
for (const result of stepResults) {
|
|
2326
|
+
if (result.status === "failed" && result.error === "dependency_failed") {
|
|
2327
|
+
await updateStep(projectRoot, task.id, result.service, { status: "failed" });
|
|
1472
2328
|
}
|
|
1473
2329
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
2330
|
+
}
|
|
2331
|
+
const hasEscalation = stepResults.some((result) => result.status === "escalated");
|
|
2332
|
+
const hasFailure = stepResults.some((result) => result.status === "failed");
|
|
2333
|
+
state = loadState(projectRoot) ?? state;
|
|
2334
|
+
if (hasEscalation || hasFailure) {
|
|
2335
|
+
state.status = hasEscalation ? "escalated" : "blocked";
|
|
1476
2336
|
if (!options.dryRun) {
|
|
1477
2337
|
saveState(projectRoot, state);
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
info(`Running codex implementation for service ${step2.service}`);
|
|
1481
|
-
await exec({
|
|
1482
|
-
spec: step2.spec,
|
|
1483
|
-
model: config.codex.model,
|
|
1484
|
-
cwd: serviceRoot,
|
|
1485
|
-
verbose: options.verbose
|
|
1486
|
-
});
|
|
1487
|
-
} else if (options.dryRun) {
|
|
1488
|
-
info(`[dry-run] Would run codex for service ${step2.service}`);
|
|
1489
|
-
}
|
|
1490
|
-
info(`Starting review loop for service ${step2.service}`);
|
|
1491
|
-
const result = await runReviewLoop({
|
|
1492
|
-
taskId: task.id,
|
|
1493
|
-
task,
|
|
1494
|
-
step: step2,
|
|
1495
|
-
stepState,
|
|
1496
|
-
projectRoot,
|
|
1497
|
-
config,
|
|
1498
|
-
claude,
|
|
1499
|
-
dryRun: options.dryRun,
|
|
1500
|
-
verbose: options.verbose
|
|
1501
|
-
});
|
|
1502
|
-
if (result.decision === "escalate") {
|
|
1503
|
-
escalation({
|
|
1504
|
-
taskId: task.id,
|
|
1505
|
-
service: step2.service,
|
|
1506
|
-
iteration: result.finalIteration,
|
|
1507
|
-
spec: step2.spec,
|
|
1508
|
-
diff: "",
|
|
1509
|
-
reviewComments: result.lastReviewComments,
|
|
1510
|
-
arbiterReasoning: result.lastArbiterResult.reasoning,
|
|
1511
|
-
summary: result.lastArbiterResult.summary
|
|
1512
|
-
});
|
|
1513
|
-
stepState.status = "escalated";
|
|
1514
|
-
state.status = "escalated";
|
|
1515
|
-
if (!options.dryRun) {
|
|
1516
|
-
saveState(projectRoot, state);
|
|
1517
|
-
const blockedDir = ensureTaskDirectory(projectRoot, "blocked");
|
|
1518
|
-
state.taskPath = moveTaskFileAtomically(state.taskPath, blockedDir);
|
|
1519
|
-
saveState(projectRoot, state);
|
|
1520
|
-
}
|
|
1521
|
-
process.exit(1);
|
|
1522
|
-
}
|
|
1523
|
-
stepState.status = "done";
|
|
1524
|
-
if (!options.dryRun) {
|
|
2338
|
+
const blockedDir = ensureTaskDirectory(projectRoot, "blocked");
|
|
2339
|
+
state.taskPath = moveTaskFileAtomically(state.taskPath, blockedDir);
|
|
1525
2340
|
saveState(projectRoot, state);
|
|
1526
2341
|
}
|
|
2342
|
+
process.exit(1);
|
|
1527
2343
|
}
|
|
1528
2344
|
state.status = "review";
|
|
1529
2345
|
if (!options.dryRun) {
|
|
@@ -1536,9 +2352,94 @@ async function runStart(taskFile, options) {
|
|
|
1536
2352
|
return;
|
|
1537
2353
|
}
|
|
1538
2354
|
success("Task ready for PR. Run 'vexdo submit' to create PR.");
|
|
1539
|
-
} catch (
|
|
1540
|
-
const message =
|
|
1541
|
-
|
|
2355
|
+
} catch (error2) {
|
|
2356
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2357
|
+
fatalAndExit6(message);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
async function runCloudReviewLoop(opts) {
|
|
2361
|
+
let sessionId = opts.sessionId;
|
|
2362
|
+
let iteration2 = opts.stepState.iteration;
|
|
2363
|
+
for (; ; ) {
|
|
2364
|
+
opts.log.iteration(iteration2 + 1, opts.config.review.max_iterations);
|
|
2365
|
+
opts.log.info(`Polling codex cloud session ${sessionId}`);
|
|
2366
|
+
const status = await pollStatus(sessionId, {
|
|
2367
|
+
intervalMs: POLL_INTERVAL_MS,
|
|
2368
|
+
timeoutMs: POLL_TIMEOUT_MS
|
|
2369
|
+
});
|
|
2370
|
+
if (status !== "completed") {
|
|
2371
|
+
return {
|
|
2372
|
+
sessionId,
|
|
2373
|
+
finalIteration: iteration2,
|
|
2374
|
+
lastReviewComments: [],
|
|
2375
|
+
lastArbiterResult: {
|
|
2376
|
+
decision: "escalate",
|
|
2377
|
+
reasoning: `Codex Cloud session ended with status '${status}'.`,
|
|
2378
|
+
summary: "Escalated due to codex cloud execution failure."
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
opts.log.success("Codex task completed");
|
|
2383
|
+
opts.log.info(`Retrieving diff for session ${sessionId}`);
|
|
2384
|
+
const diff = await getDiff(sessionId);
|
|
2385
|
+
opts.log.info(`Running review (iteration ${String(iteration2 + 1)})...`);
|
|
2386
|
+
await applyDiff(sessionId);
|
|
2387
|
+
const comments = await runCopilotReview(opts.spec, { cwd: opts.serviceRoot }).finally(async () => {
|
|
2388
|
+
await exec2(["checkout", "."], opts.serviceRoot);
|
|
2389
|
+
});
|
|
2390
|
+
const review = { comments };
|
|
2391
|
+
opts.log.reviewSummary(review.comments);
|
|
2392
|
+
opts.log.info(`Requesting arbiter decision (model: ${opts.config.review.model})`);
|
|
2393
|
+
const arbiter = await opts.claude.runArbiter({
|
|
2394
|
+
spec: opts.spec,
|
|
2395
|
+
diff,
|
|
2396
|
+
reviewComments: review.comments,
|
|
2397
|
+
model: opts.config.review.model
|
|
2398
|
+
});
|
|
2399
|
+
saveIterationLog(opts.projectRoot, opts.taskId, opts.service, iteration2, {
|
|
2400
|
+
diff,
|
|
2401
|
+
review,
|
|
2402
|
+
arbiter
|
|
2403
|
+
});
|
|
2404
|
+
opts.stepState.iteration = iteration2;
|
|
2405
|
+
opts.stepState.session_id = sessionId;
|
|
2406
|
+
if (arbiter.decision === "submit" || arbiter.decision === "escalate") {
|
|
2407
|
+
return {
|
|
2408
|
+
sessionId,
|
|
2409
|
+
finalIteration: iteration2,
|
|
2410
|
+
lastReviewComments: review.comments,
|
|
2411
|
+
lastArbiterResult: arbiter
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
if (iteration2 >= opts.config.review.max_iterations) {
|
|
2415
|
+
return {
|
|
2416
|
+
sessionId,
|
|
2417
|
+
finalIteration: iteration2,
|
|
2418
|
+
lastReviewComments: review.comments,
|
|
2419
|
+
lastArbiterResult: {
|
|
2420
|
+
decision: "escalate",
|
|
2421
|
+
reasoning: "Max review iterations reached while arbiter still requested fixes.",
|
|
2422
|
+
summary: "Escalated because maximum iterations were exhausted."
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
if (!arbiter.feedback_for_codex) {
|
|
2427
|
+
return {
|
|
2428
|
+
sessionId,
|
|
2429
|
+
finalIteration: iteration2,
|
|
2430
|
+
lastReviewComments: review.comments,
|
|
2431
|
+
lastArbiterResult: {
|
|
2432
|
+
decision: "escalate",
|
|
2433
|
+
reasoning: "Arbiter returned fix decision without feedback_for_codex.",
|
|
2434
|
+
summary: "Escalated because fix instructions were missing."
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
opts.log.warn(`Review requested fixes (iteration ${String(iteration2 + 1)}/${String(opts.config.review.max_iterations)})`);
|
|
2439
|
+
sessionId = await resumeTask(sessionId, arbiter.feedback_for_codex);
|
|
2440
|
+
opts.stepState.session_id = sessionId;
|
|
2441
|
+
iteration2 += 1;
|
|
2442
|
+
opts.stepState.iteration = iteration2;
|
|
1542
2443
|
}
|
|
1543
2444
|
}
|
|
1544
2445
|
function registerStartCommand(program2) {
|
|
@@ -1549,11 +2450,11 @@ function registerStartCommand(program2) {
|
|
|
1549
2450
|
}
|
|
1550
2451
|
|
|
1551
2452
|
// src/commands/status.ts
|
|
1552
|
-
function
|
|
2453
|
+
function fatalAndExit7(message) {
|
|
1553
2454
|
fatal(message);
|
|
1554
2455
|
process.exit(1);
|
|
1555
2456
|
}
|
|
1556
|
-
function
|
|
2457
|
+
function formatElapsed2(startedAt) {
|
|
1557
2458
|
const elapsedMs = Date.now() - new Date(startedAt).getTime();
|
|
1558
2459
|
const minutes = Math.floor(elapsedMs / 1e3 / 60);
|
|
1559
2460
|
const hours = Math.floor(minutes / 60);
|
|
@@ -1565,23 +2466,23 @@ function formatElapsed3(startedAt) {
|
|
|
1565
2466
|
function runStatus() {
|
|
1566
2467
|
const projectRoot = findProjectRoot();
|
|
1567
2468
|
if (!projectRoot) {
|
|
1568
|
-
|
|
2469
|
+
fatalAndExit7("Not inside a vexdo project.");
|
|
1569
2470
|
}
|
|
1570
2471
|
const state = loadState(projectRoot);
|
|
1571
2472
|
if (!state) {
|
|
1572
|
-
|
|
2473
|
+
fatalAndExit7("No active task.");
|
|
1573
2474
|
}
|
|
1574
2475
|
info(`Task: ${state.taskId} \u2014 ${state.taskTitle}`);
|
|
1575
2476
|
info(`Status: ${state.status}`);
|
|
1576
2477
|
console.log("service | status | iteration | branch");
|
|
1577
|
-
for (const
|
|
1578
|
-
console.log(`${
|
|
2478
|
+
for (const step of state.steps) {
|
|
2479
|
+
console.log(`${step.service} | ${step.status} | ${String(step.iteration)} | ${step.branch ?? "-"}`);
|
|
1579
2480
|
}
|
|
1580
|
-
const inProgress = state.steps.find((
|
|
2481
|
+
const inProgress = state.steps.find((step) => step.status === "in_progress");
|
|
1581
2482
|
if (inProgress?.lastArbiterResult?.summary) {
|
|
1582
2483
|
info(`Last arbiter summary: ${inProgress.lastArbiterResult.summary}`);
|
|
1583
2484
|
}
|
|
1584
|
-
info(`Elapsed: ${
|
|
2485
|
+
info(`Elapsed: ${formatElapsed2(state.startedAt)}`);
|
|
1585
2486
|
}
|
|
1586
2487
|
function registerStatusCommand(program2) {
|
|
1587
2488
|
program2.command("status").description("Print active task status").action(() => {
|
|
@@ -1590,7 +2491,7 @@ function registerStatusCommand(program2) {
|
|
|
1590
2491
|
}
|
|
1591
2492
|
|
|
1592
2493
|
// src/commands/submit.ts
|
|
1593
|
-
function
|
|
2494
|
+
function fatalAndExit8(message) {
|
|
1594
2495
|
fatal(message);
|
|
1595
2496
|
process.exit(1);
|
|
1596
2497
|
}
|
|
@@ -1598,17 +2499,17 @@ async function runSubmit() {
|
|
|
1598
2499
|
try {
|
|
1599
2500
|
const projectRoot = findProjectRoot();
|
|
1600
2501
|
if (!projectRoot) {
|
|
1601
|
-
|
|
2502
|
+
fatalAndExit8("Not inside a vexdo project.");
|
|
1602
2503
|
}
|
|
1603
2504
|
const config = loadConfig(projectRoot);
|
|
1604
2505
|
const state = loadState(projectRoot);
|
|
1605
2506
|
if (!state) {
|
|
1606
|
-
|
|
2507
|
+
fatalAndExit8("No active task.");
|
|
1607
2508
|
}
|
|
1608
2509
|
await requireGhAvailable();
|
|
1609
2510
|
await submitActiveTask(projectRoot, config, state);
|
|
1610
|
-
} catch (
|
|
1611
|
-
|
|
2511
|
+
} catch (error2) {
|
|
2512
|
+
fatalAndExit8(error2 instanceof Error ? error2.message : String(error2));
|
|
1612
2513
|
}
|
|
1613
2514
|
}
|
|
1614
2515
|
function registerSubmitCommand(program2) {
|
|
@@ -1618,8 +2519,8 @@ function registerSubmitCommand(program2) {
|
|
|
1618
2519
|
}
|
|
1619
2520
|
|
|
1620
2521
|
// src/index.ts
|
|
1621
|
-
var packageJsonPath =
|
|
1622
|
-
var packageJson = JSON.parse(
|
|
2522
|
+
var packageJsonPath = path12.resolve(path12.dirname(new URL(import.meta.url).pathname), "..", "package.json");
|
|
2523
|
+
var packageJson = JSON.parse(fs10.readFileSync(packageJsonPath, "utf8"));
|
|
1623
2524
|
var program = new Command();
|
|
1624
2525
|
program.name("vexdo").description("Vexdo CLI").version(packageJson.version).option("--verbose", "Enable verbose logs").option("--dry-run", "Print plan without making changes");
|
|
1625
2526
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
@@ -1634,4 +2535,5 @@ registerSubmitCommand(program);
|
|
|
1634
2535
|
registerStatusCommand(program);
|
|
1635
2536
|
registerAbortCommand(program);
|
|
1636
2537
|
registerLogsCommand(program);
|
|
2538
|
+
registerBoardCommand(program);
|
|
1637
2539
|
program.parse(process.argv);
|