nightralph 0.0.19 → 0.0.21
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 +74 -10
- package/dist/display.js +0 -12
- package/dist/display.js.map +2 -2
- package/dist/docs-templates/issue-tracker-github.md +15 -85
- package/dist/docs-templates/issue-tracker.md +16 -10
- package/dist/index.js +679 -190
- package/dist/index.js.map +4 -4
- package/dist/meta.json +66 -12
- package/dist/orchestrator.js +517 -142
- package/dist/orchestrator.js.map +3 -3
- package/dist/progress.js +10 -0
- package/dist/progress.js.map +2 -2
- package/dist/skills/to-spec/SKILL.md +1 -1
- package/dist/skills/to-tickets/SKILL.md +2 -2
- package/dist/src/display.d.ts +0 -1
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +25 -2
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/testcmd.d.ts +11 -0
- package/dist/src/testcmd.d.ts.map +1 -0
- package/dist/src/worktree.d.ts +3 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/testcmd.js +67 -0
- package/dist/testcmd.js.map +7 -0
- package/dist/worktree.js +19 -6
- package/dist/worktree.js.map +2 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
import { existsSync as
|
|
7
|
-
import { dirname as dirname3, join as
|
|
6
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
7
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
8
8
|
import yargs from "yargs";
|
|
9
9
|
import { hideBin } from "yargs/helpers";
|
|
10
10
|
import { select as select2 } from "@inquirer/prompts";
|
|
@@ -181,17 +181,17 @@ Source: ${upstream.repo} @ ${upstream.commit}`
|
|
|
181
181
|
__name(setup, "setup");
|
|
182
182
|
|
|
183
183
|
// src/orchestrator.ts
|
|
184
|
-
import { spawn } from "node:child_process";
|
|
184
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
185
185
|
import {
|
|
186
186
|
createWriteStream,
|
|
187
187
|
mkdirSync as mkdirSync2,
|
|
188
|
-
readFileSync as
|
|
188
|
+
readFileSync as readFileSync4,
|
|
189
189
|
writeFileSync as writeFileSync3,
|
|
190
190
|
unlinkSync,
|
|
191
191
|
readdirSync as readdirSync2
|
|
192
192
|
} from "node:fs";
|
|
193
193
|
import { tmpdir } from "node:os";
|
|
194
|
-
import { join as
|
|
194
|
+
import { join as join4, dirname, basename as basename2 } from "node:path";
|
|
195
195
|
import { createInterface } from "node:readline";
|
|
196
196
|
|
|
197
197
|
// src/worktree.ts
|
|
@@ -235,7 +235,7 @@ async function branchExists(repoRoot, branchName) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
__name(branchExists, "branchExists");
|
|
238
|
-
async function
|
|
238
|
+
async function createWorktreeNow(opts) {
|
|
239
239
|
const { repoRoot, repoName, baseBranch, ticket } = opts;
|
|
240
240
|
const branchName = `${repoName}/${String(
|
|
241
241
|
ticket.num
|
|
@@ -286,12 +286,25 @@ async function createWorktree(opts) {
|
|
|
286
286
|
);
|
|
287
287
|
return { worktreePath, branchName };
|
|
288
288
|
}
|
|
289
|
+
__name(createWorktreeNow, "createWorktreeNow");
|
|
290
|
+
var worktreeQueue = Promise.resolve();
|
|
291
|
+
function createWorktree(opts) {
|
|
292
|
+
const result = worktreeQueue.then(() => createWorktreeNow(opts));
|
|
293
|
+
worktreeQueue = result.catch(() => {
|
|
294
|
+
});
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
289
297
|
__name(createWorktree, "createWorktree");
|
|
290
|
-
async function removeWorktree(worktreePath) {
|
|
291
|
-
|
|
292
|
-
"
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
async function removeWorktree(worktreePath, opts) {
|
|
299
|
+
const args = [
|
|
300
|
+
"-C",
|
|
301
|
+
worktreePath,
|
|
302
|
+
"worktree",
|
|
303
|
+
"remove"
|
|
304
|
+
];
|
|
305
|
+
if (opts?.force) args.push("--force");
|
|
306
|
+
args.push(worktreePath);
|
|
307
|
+
await execFileAsync("git", args);
|
|
295
308
|
}
|
|
296
309
|
__name(removeWorktree, "removeWorktree");
|
|
297
310
|
function hasNewCommits(repoRoot, base, branch) {
|
|
@@ -482,11 +495,21 @@ async function commitProgress(repoRoot, progressPath, message) {
|
|
|
482
495
|
["add", "--force", progressPath],
|
|
483
496
|
{ cwd: repoRoot }
|
|
484
497
|
);
|
|
498
|
+
try {
|
|
499
|
+
await execFileAsync2(
|
|
500
|
+
"git",
|
|
501
|
+
["diff", "--cached", "--quiet", "--", progressPath],
|
|
502
|
+
{ cwd: repoRoot }
|
|
503
|
+
);
|
|
504
|
+
return false;
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
485
507
|
await execFileAsync2(
|
|
486
508
|
"git",
|
|
487
509
|
["commit", "-m", message, "--", progressPath],
|
|
488
510
|
{ cwd: repoRoot }
|
|
489
511
|
);
|
|
512
|
+
return true;
|
|
490
513
|
}
|
|
491
514
|
__name(commitProgress, "commitProgress");
|
|
492
515
|
|
|
@@ -768,7 +791,6 @@ var StatusDisplay = class {
|
|
|
768
791
|
config;
|
|
769
792
|
viewportStart = 0;
|
|
770
793
|
resizeListener = null;
|
|
771
|
-
signalHandler = null;
|
|
772
794
|
constructor(config) {
|
|
773
795
|
this.config = {
|
|
774
796
|
stream: config?.stream ?? process.stdout
|
|
@@ -810,13 +832,6 @@ var StatusDisplay = class {
|
|
|
810
832
|
this.resizeListener
|
|
811
833
|
);
|
|
812
834
|
}
|
|
813
|
-
if (this.signalHandler) {
|
|
814
|
-
process.removeListener("SIGINT", this.signalHandler);
|
|
815
|
-
process.removeListener("SIGTERM", this.signalHandler);
|
|
816
|
-
}
|
|
817
|
-
this.signalHandler = () => this.cleanup();
|
|
818
|
-
process.on("SIGINT", this.signalHandler);
|
|
819
|
-
process.on("SIGTERM", this.signalHandler);
|
|
820
835
|
this.render();
|
|
821
836
|
}
|
|
822
837
|
setTicketStatus(num, status) {
|
|
@@ -852,10 +867,6 @@ var StatusDisplay = class {
|
|
|
852
867
|
if (this.resizeListener && this.config.stream.isTTY) {
|
|
853
868
|
this.config.stream.removeListener("resize", this.resizeListener);
|
|
854
869
|
}
|
|
855
|
-
if (this.signalHandler) {
|
|
856
|
-
process.removeListener("SIGINT", this.signalHandler);
|
|
857
|
-
process.removeListener("SIGTERM", this.signalHandler);
|
|
858
|
-
}
|
|
859
870
|
this.config.stream.write(ansi.cursorShow());
|
|
860
871
|
}
|
|
861
872
|
render() {
|
|
@@ -988,11 +999,83 @@ function createDisplay(stream) {
|
|
|
988
999
|
}
|
|
989
1000
|
__name(createDisplay, "createDisplay");
|
|
990
1001
|
|
|
1002
|
+
// src/testcmd.ts
|
|
1003
|
+
import { spawn } from "node:child_process";
|
|
1004
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1005
|
+
import { join as join3 } from "node:path";
|
|
1006
|
+
function detectTestCmd(projectDir) {
|
|
1007
|
+
const pkgPath = join3(projectDir, "package.json");
|
|
1008
|
+
if (!existsSync3(pkgPath)) return null;
|
|
1009
|
+
try {
|
|
1010
|
+
const pkg = JSON.parse(
|
|
1011
|
+
readFileSync3(pkgPath, "utf8")
|
|
1012
|
+
);
|
|
1013
|
+
return pkg.scripts?.test ? "npm test" : null;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
__name(detectTestCmd, "detectTestCmd");
|
|
1019
|
+
function runTestCmd(opts) {
|
|
1020
|
+
return new Promise((resolve) => {
|
|
1021
|
+
let settled = false;
|
|
1022
|
+
const child = spawn("sh", ["-c", opts.cmd], {
|
|
1023
|
+
cwd: opts.cwd,
|
|
1024
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1025
|
+
detached: true
|
|
1026
|
+
});
|
|
1027
|
+
const chunks = [];
|
|
1028
|
+
child.stdout.on("data", (d) => chunks.push(String(d)));
|
|
1029
|
+
child.stderr.on("data", (d) => chunks.push(String(d)));
|
|
1030
|
+
const timer = setTimeout(() => {
|
|
1031
|
+
if (child.pid) {
|
|
1032
|
+
try {
|
|
1033
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1034
|
+
} catch {
|
|
1035
|
+
child.kill("SIGKILL");
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
child.kill("SIGKILL");
|
|
1039
|
+
}
|
|
1040
|
+
}, opts.timeout * 1e3);
|
|
1041
|
+
child.on("error", (err) => {
|
|
1042
|
+
if (settled) return;
|
|
1043
|
+
settled = true;
|
|
1044
|
+
clearTimeout(timer);
|
|
1045
|
+
chunks.push(String(err));
|
|
1046
|
+
resolve({
|
|
1047
|
+
exitCode: 1,
|
|
1048
|
+
output: chunks.join("")
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
child.on("close", (code) => {
|
|
1052
|
+
if (settled) return;
|
|
1053
|
+
settled = true;
|
|
1054
|
+
clearTimeout(timer);
|
|
1055
|
+
resolve({
|
|
1056
|
+
exitCode: code ?? 1,
|
|
1057
|
+
output: chunks.join("")
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
__name(runTestCmd, "runTestCmd");
|
|
1063
|
+
|
|
991
1064
|
// src/orchestrator.ts
|
|
992
1065
|
function formatErrorMessage(error) {
|
|
993
1066
|
return error instanceof Error ? error.message : String(error);
|
|
994
1067
|
}
|
|
995
1068
|
__name(formatErrorMessage, "formatErrorMessage");
|
|
1069
|
+
async function removeWorktreeQuietly(worktreePath, d) {
|
|
1070
|
+
try {
|
|
1071
|
+
await removeWorktree(worktreePath, { force: true });
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
d.log(
|
|
1074
|
+
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
__name(removeWorktreeQuietly, "removeWorktreeQuietly");
|
|
996
1079
|
var TICKET_RE = /^(\d+)-(.+)\.md$/;
|
|
997
1080
|
function parseTicketFilename(filename) {
|
|
998
1081
|
const m = filename.match(TICKET_RE);
|
|
@@ -1027,8 +1110,8 @@ function scanTickets(dir) {
|
|
|
1027
1110
|
));
|
|
1028
1111
|
return files.map((filename) => {
|
|
1029
1112
|
const parsed = parseTicketFilename(filename);
|
|
1030
|
-
const filepath =
|
|
1031
|
-
const body =
|
|
1113
|
+
const filepath = join4(dir, filename);
|
|
1114
|
+
const body = readFileSync4(filepath, "utf8");
|
|
1032
1115
|
return {
|
|
1033
1116
|
num: parsed.num,
|
|
1034
1117
|
slug: parsed.slug,
|
|
@@ -1083,7 +1166,39 @@ function markDone(ticket) {
|
|
|
1083
1166
|
ticket.body = updated;
|
|
1084
1167
|
}
|
|
1085
1168
|
__name(markDone, "markDone");
|
|
1086
|
-
function
|
|
1169
|
+
function stripSpecSection(specBody, heading) {
|
|
1170
|
+
const out = [];
|
|
1171
|
+
let skipping = false;
|
|
1172
|
+
let skipLevel = 0;
|
|
1173
|
+
let inFence = false;
|
|
1174
|
+
for (const line of specBody.split("\n")) {
|
|
1175
|
+
if (/^\s*```/.test(line)) {
|
|
1176
|
+
inFence = !inFence;
|
|
1177
|
+
}
|
|
1178
|
+
if (!inFence) {
|
|
1179
|
+
const m = /^(#{1,6})\s+(.*?)\s*$/.exec(line);
|
|
1180
|
+
if (m) {
|
|
1181
|
+
const level = m[1].length;
|
|
1182
|
+
if (skipping && level <= skipLevel) {
|
|
1183
|
+
skipping = false;
|
|
1184
|
+
}
|
|
1185
|
+
if (!skipping && m[2] === heading) {
|
|
1186
|
+
skipping = true;
|
|
1187
|
+
skipLevel = level;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (!skipping) out.push(line);
|
|
1193
|
+
}
|
|
1194
|
+
return out.join("\n");
|
|
1195
|
+
}
|
|
1196
|
+
__name(stripSpecSection, "stripSpecSection");
|
|
1197
|
+
function renderPrompt(specBody, ticket, opts = {}) {
|
|
1198
|
+
const testRules = opts.testCmd ? [
|
|
1199
|
+
"- Work test-first. For each checklist item, write a failing test at the seams named in the spec's Testing Decisions section, then write only enough code to make it pass.",
|
|
1200
|
+
`- Run \`${opts.testCmd}\` before committing. Do not commit with failing tests. The orchestrator runs the same command after you exit and rejects the ticket if it fails.`
|
|
1201
|
+
] : [];
|
|
1087
1202
|
return [
|
|
1088
1203
|
"---- SPEC CONTEXT ----",
|
|
1089
1204
|
specBody,
|
|
@@ -1095,10 +1210,12 @@ function renderPrompt(specBody, ticket) {
|
|
|
1095
1210
|
"---- AGENT INSTRUCTIONS ----",
|
|
1096
1211
|
"You are an autonomous coding agent. Implement the ticket.",
|
|
1097
1212
|
"",
|
|
1098
|
-
"
|
|
1099
|
-
"-
|
|
1213
|
+
"Follow these rules:",
|
|
1214
|
+
"- Read the files directly relevant to the ticket before writing code.",
|
|
1100
1215
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
1101
|
-
|
|
1216
|
+
...testRules,
|
|
1217
|
+
"- Do NOT read or modify files outside your current directory (the worktree).",
|
|
1218
|
+
"- Do NOT run git push.",
|
|
1102
1219
|
"- Commit your work when finished.",
|
|
1103
1220
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
1104
1221
|
""
|
|
@@ -1124,7 +1241,8 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
1124
1241
|
"2. Make sure the code compiles and tests pass.",
|
|
1125
1242
|
"3. Commit the resolved files.",
|
|
1126
1243
|
"",
|
|
1127
|
-
"Do NOT re-implement the ticket from scratch."
|
|
1244
|
+
"Do NOT re-implement the ticket from scratch.",
|
|
1245
|
+
"Do NOT run git push."
|
|
1128
1246
|
].join("\n");
|
|
1129
1247
|
}
|
|
1130
1248
|
__name(renderMergePrompt, "renderMergePrompt");
|
|
@@ -1203,7 +1321,11 @@ var PROVIDER_ARGS = {
|
|
|
1203
1321
|
"stream-json",
|
|
1204
1322
|
"--dangerously-skip-permissions"
|
|
1205
1323
|
],
|
|
1206
|
-
codex
|
|
1324
|
+
// codex removed --full-auto; this is the counterpart
|
|
1325
|
+
// of claude's --dangerously-skip-permissions, and the
|
|
1326
|
+
// workspace-write sandbox would block commits because
|
|
1327
|
+
// a worktree's .git link points outside the tree
|
|
1328
|
+
codex: ["exec", "--dangerously-bypass-approvals-and-sandbox"],
|
|
1207
1329
|
pi: [
|
|
1208
1330
|
"-p",
|
|
1209
1331
|
"--verbose",
|
|
@@ -1218,6 +1340,21 @@ function getProviderArgs(cmd) {
|
|
|
1218
1340
|
return PROVIDER_ARGS[name] ?? [];
|
|
1219
1341
|
}
|
|
1220
1342
|
__name(getProviderArgs, "getProviderArgs");
|
|
1343
|
+
var THINKING_LEVELS = [
|
|
1344
|
+
"off",
|
|
1345
|
+
"minimal",
|
|
1346
|
+
"low",
|
|
1347
|
+
"medium",
|
|
1348
|
+
"high",
|
|
1349
|
+
"xhigh",
|
|
1350
|
+
"max"
|
|
1351
|
+
];
|
|
1352
|
+
function escalateThinking(level) {
|
|
1353
|
+
if (!level) return "high";
|
|
1354
|
+
const i = THINKING_LEVELS.indexOf(level);
|
|
1355
|
+
return THINKING_LEVELS[Math.min(i + 1, THINKING_LEVELS.length - 1)];
|
|
1356
|
+
}
|
|
1357
|
+
__name(escalateThinking, "escalateThinking");
|
|
1221
1358
|
function formatStreamLine(line) {
|
|
1222
1359
|
let obj;
|
|
1223
1360
|
try {
|
|
@@ -1318,7 +1455,7 @@ function createPiStreamFormatter() {
|
|
|
1318
1455
|
__name(createPiStreamFormatter, "createPiStreamFormatter");
|
|
1319
1456
|
function writePromptFile(prompt) {
|
|
1320
1457
|
const name = `nightralph-${Date.now()}-${Math.random().toString(36).slice(2)}.md`;
|
|
1321
|
-
const filePath =
|
|
1458
|
+
const filePath = join4(tmpdir(), name);
|
|
1322
1459
|
writeFileSync3(filePath, prompt);
|
|
1323
1460
|
return filePath;
|
|
1324
1461
|
}
|
|
@@ -1330,6 +1467,22 @@ function cleanupPromptFile(filePath) {
|
|
|
1330
1467
|
}
|
|
1331
1468
|
}
|
|
1332
1469
|
__name(cleanupPromptFile, "cleanupPromptFile");
|
|
1470
|
+
var CLOSE_GRACE_MS = 2e3;
|
|
1471
|
+
function killProcessGroup(pid) {
|
|
1472
|
+
if (pid === void 0) return;
|
|
1473
|
+
try {
|
|
1474
|
+
process.kill(-pid, "SIGKILL");
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
__name(killProcessGroup, "killProcessGroup");
|
|
1479
|
+
var runningAgents = /* @__PURE__ */ new Set();
|
|
1480
|
+
function killRunningAgents() {
|
|
1481
|
+
for (const pid of runningAgents) {
|
|
1482
|
+
killProcessGroup(pid);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
__name(killRunningAgents, "killRunningAgents");
|
|
1333
1486
|
function spawnAgent(opts) {
|
|
1334
1487
|
const args = [
|
|
1335
1488
|
...getProviderArgs(opts.agentCmd)
|
|
@@ -1337,9 +1490,15 @@ function spawnAgent(opts) {
|
|
|
1337
1490
|
if (opts.model) {
|
|
1338
1491
|
args.push("--model", opts.model);
|
|
1339
1492
|
}
|
|
1493
|
+
if (opts.maxTurns) {
|
|
1494
|
+
args.push("--max-turns", String(opts.maxTurns));
|
|
1495
|
+
}
|
|
1340
1496
|
const providerName = basename2(opts.agentCmd);
|
|
1341
1497
|
const isClaude = providerName === "claude";
|
|
1342
1498
|
const isPi = providerName === "pi";
|
|
1499
|
+
if (isPi && opts.thinking) {
|
|
1500
|
+
args.push("--thinking", opts.thinking);
|
|
1501
|
+
}
|
|
1343
1502
|
const prefix = opts.label ? ` [${opts.label}] ` : " ";
|
|
1344
1503
|
let promptFile = null;
|
|
1345
1504
|
if (isPi) {
|
|
@@ -1350,10 +1509,12 @@ function spawnAgent(opts) {
|
|
|
1350
1509
|
opts.logPath,
|
|
1351
1510
|
{ flags: "w" }
|
|
1352
1511
|
);
|
|
1353
|
-
const proc =
|
|
1512
|
+
const proc = spawn2(opts.agentCmd, args, {
|
|
1354
1513
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1355
|
-
cwd: opts.cwd
|
|
1514
|
+
cwd: opts.cwd,
|
|
1515
|
+
detached: true
|
|
1356
1516
|
});
|
|
1517
|
+
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
1357
1518
|
proc.stdout.pipe(logStream);
|
|
1358
1519
|
const piFormatter = isPi ? createPiStreamFormatter() : null;
|
|
1359
1520
|
const completedItems = [];
|
|
@@ -1407,21 +1568,41 @@ function spawnAgent(opts) {
|
|
|
1407
1568
|
console.error(output);
|
|
1408
1569
|
}
|
|
1409
1570
|
});
|
|
1571
|
+
let timedOut = false;
|
|
1410
1572
|
const timeout = setTimeout(() => {
|
|
1411
|
-
if (
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1573
|
+
if (proc.killed) return;
|
|
1574
|
+
timedOut = true;
|
|
1575
|
+
const msg = `agent timed out after ${opts.timeout}s; killing it`;
|
|
1576
|
+
logStream.write(`nightralph: ${msg}
|
|
1577
|
+
`);
|
|
1578
|
+
if (opts.onLine) {
|
|
1579
|
+
opts.onLine(`${prefix}${msg}`);
|
|
1580
|
+
} else {
|
|
1581
|
+
console.warn(`${prefix}${msg}`);
|
|
1416
1582
|
}
|
|
1583
|
+
killProcessGroup(proc.pid);
|
|
1417
1584
|
}, opts.timeout * 1e3);
|
|
1585
|
+
proc.stdin.on("error", (err) => {
|
|
1586
|
+
logStream.write(
|
|
1587
|
+
`nightralph: stdin write failed: ${formatErrorMessage(err)}
|
|
1588
|
+
`
|
|
1589
|
+
);
|
|
1590
|
+
});
|
|
1418
1591
|
if (!promptFile) {
|
|
1419
1592
|
proc.stdin.write(opts.prompt);
|
|
1420
1593
|
}
|
|
1421
1594
|
proc.stdin.end();
|
|
1422
1595
|
return new Promise((resolve) => {
|
|
1423
|
-
|
|
1596
|
+
let settled = false;
|
|
1597
|
+
let graceTimer;
|
|
1598
|
+
function finish(exitCode) {
|
|
1599
|
+
if (settled) return;
|
|
1600
|
+
settled = true;
|
|
1424
1601
|
clearTimeout(timeout);
|
|
1602
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
1603
|
+
if (proc.pid !== void 0) {
|
|
1604
|
+
runningAgents.delete(proc.pid);
|
|
1605
|
+
}
|
|
1425
1606
|
if (piFormatter) {
|
|
1426
1607
|
for (const line of piFormatter.flush()) {
|
|
1427
1608
|
collectCompleted(line);
|
|
@@ -1437,13 +1618,27 @@ function spawnAgent(opts) {
|
|
|
1437
1618
|
rlErr.close();
|
|
1438
1619
|
logStream.end();
|
|
1439
1620
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
1440
|
-
resolve({
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1621
|
+
resolve({ exitCode, completedItems, timedOut });
|
|
1622
|
+
}
|
|
1623
|
+
__name(finish, "finish");
|
|
1624
|
+
proc.on("exit", (code) => {
|
|
1625
|
+
graceTimer = setTimeout(() => {
|
|
1626
|
+
proc.stdout.destroy();
|
|
1627
|
+
proc.stderr.destroy();
|
|
1628
|
+
finish(code ?? 1);
|
|
1629
|
+
}, CLOSE_GRACE_MS);
|
|
1630
|
+
});
|
|
1631
|
+
proc.on("close", (code) => {
|
|
1632
|
+
finish(code ?? 1);
|
|
1444
1633
|
});
|
|
1445
1634
|
proc.on("error", (err) => {
|
|
1635
|
+
if (settled) return;
|
|
1636
|
+
settled = true;
|
|
1637
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
1446
1638
|
clearTimeout(timeout);
|
|
1639
|
+
if (proc.pid !== void 0) {
|
|
1640
|
+
runningAgents.delete(proc.pid);
|
|
1641
|
+
}
|
|
1447
1642
|
rl.close();
|
|
1448
1643
|
rlErr.close();
|
|
1449
1644
|
logStream.end();
|
|
@@ -1452,7 +1647,11 @@ function spawnAgent(opts) {
|
|
|
1452
1647
|
"Agent process error:",
|
|
1453
1648
|
formatErrorMessage(err)
|
|
1454
1649
|
);
|
|
1455
|
-
resolve({
|
|
1650
|
+
resolve({
|
|
1651
|
+
exitCode: 1,
|
|
1652
|
+
completedItems,
|
|
1653
|
+
timedOut
|
|
1654
|
+
});
|
|
1456
1655
|
});
|
|
1457
1656
|
});
|
|
1458
1657
|
}
|
|
@@ -1515,18 +1714,65 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
1515
1714
|
`
|
|
1516
1715
|
);
|
|
1517
1716
|
console.log(
|
|
1518
|
-
renderPrompt(
|
|
1717
|
+
renderPrompt(
|
|
1718
|
+
opts.specBody,
|
|
1719
|
+
firstReady[0],
|
|
1720
|
+
{ testCmd: opts.testCmd }
|
|
1721
|
+
)
|
|
1519
1722
|
);
|
|
1520
1723
|
}
|
|
1521
1724
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
1522
1725
|
console.log(
|
|
1523
1726
|
`
|
|
1727
|
+
Test command: ${opts.testCmd ?? "(none)"}`
|
|
1728
|
+
);
|
|
1729
|
+
console.log(
|
|
1730
|
+
`Thinking: ${opts.thinking ?? "(provider default)"}`
|
|
1731
|
+
);
|
|
1732
|
+
console.log(
|
|
1733
|
+
`Retries: ${opts.retries ?? DEFAULT_RETRIES}`
|
|
1734
|
+
);
|
|
1735
|
+
console.log(
|
|
1736
|
+
`Retry delay: ${opts.retryDelay ?? DEFAULT_RETRY_DELAY}s`
|
|
1737
|
+
);
|
|
1738
|
+
console.log(
|
|
1739
|
+
`
|
|
1524
1740
|
Merge strategy: ${strategyDesc}`
|
|
1525
1741
|
);
|
|
1526
1742
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
1527
1743
|
}
|
|
1528
1744
|
__name(dryRunIsolated, "dryRunIsolated");
|
|
1745
|
+
async function runTestGate(opts) {
|
|
1746
|
+
const result = await runTestCmd({
|
|
1747
|
+
cmd: opts.testCmd,
|
|
1748
|
+
cwd: opts.worktreePath,
|
|
1749
|
+
timeout: opts.timeout
|
|
1750
|
+
});
|
|
1751
|
+
const testLogPath = attemptLogPath(
|
|
1752
|
+
opts.logsDir,
|
|
1753
|
+
opts.ticket,
|
|
1754
|
+
opts.attempt ?? 1,
|
|
1755
|
+
".test.log"
|
|
1756
|
+
);
|
|
1757
|
+
writeFileSync3(testLogPath, result.output);
|
|
1758
|
+
if (result.exitCode !== 0) {
|
|
1759
|
+
opts.log(
|
|
1760
|
+
` ${opts.ticket.filename}: tests failed (exit ${result.exitCode}); see ${testLogPath}`
|
|
1761
|
+
);
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
opts.log(` ${opts.ticket.filename}: tests passed`);
|
|
1765
|
+
return true;
|
|
1766
|
+
}
|
|
1767
|
+
__name(runTestGate, "runTestGate");
|
|
1529
1768
|
async function respawnAndRetryMerge(opts) {
|
|
1769
|
+
const log = /* @__PURE__ */ __name((msg) => {
|
|
1770
|
+
if (opts.onLine) {
|
|
1771
|
+
opts.onLine(msg);
|
|
1772
|
+
} else {
|
|
1773
|
+
console.log(msg);
|
|
1774
|
+
}
|
|
1775
|
+
}, "log");
|
|
1530
1776
|
await updateWorktreeFromBase(
|
|
1531
1777
|
opts.worktreePath,
|
|
1532
1778
|
opts.baseBranch
|
|
@@ -1541,7 +1787,7 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1541
1787
|
opts.specBody,
|
|
1542
1788
|
opts.ticket
|
|
1543
1789
|
);
|
|
1544
|
-
const logPath =
|
|
1790
|
+
const logPath = join4(
|
|
1545
1791
|
opts.logsDir,
|
|
1546
1792
|
opts.ticket.filename.replace(/\.md$/, ".respawn.log")
|
|
1547
1793
|
);
|
|
@@ -1557,6 +1803,7 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1557
1803
|
logPath,
|
|
1558
1804
|
cwd: opts.worktreePath,
|
|
1559
1805
|
label,
|
|
1806
|
+
maxTurns: opts.maxTurns,
|
|
1560
1807
|
onLine: opts.onLine
|
|
1561
1808
|
});
|
|
1562
1809
|
if (result.exitCode !== 0) {
|
|
@@ -1568,6 +1815,22 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1568
1815
|
}
|
|
1569
1816
|
return false;
|
|
1570
1817
|
}
|
|
1818
|
+
if (opts.testCmd) {
|
|
1819
|
+
const passed = await runTestGate({
|
|
1820
|
+
testCmd: opts.testCmd,
|
|
1821
|
+
ticket: opts.ticket,
|
|
1822
|
+
worktreePath: opts.worktreePath,
|
|
1823
|
+
timeout: opts.timeout,
|
|
1824
|
+
logsDir: opts.logsDir,
|
|
1825
|
+
log
|
|
1826
|
+
});
|
|
1827
|
+
if (!passed) {
|
|
1828
|
+
log(
|
|
1829
|
+
` ${opts.ticket.filename}: tests failed after conflict respawn; not merging`
|
|
1830
|
+
);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1571
1834
|
const mergeResult = await mergeBranch(
|
|
1572
1835
|
opts.repoRoot,
|
|
1573
1836
|
opts.branch
|
|
@@ -1591,6 +1854,171 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1591
1854
|
return mergeResult.success;
|
|
1592
1855
|
}
|
|
1593
1856
|
__name(respawnAndRetryMerge, "respawnAndRetryMerge");
|
|
1857
|
+
var DEFAULT_RETRIES = 2;
|
|
1858
|
+
var DEFAULT_RETRY_DELAY = 30;
|
|
1859
|
+
function retryDelayFor(base, attempt) {
|
|
1860
|
+
return base * 2 ** (attempt - 2);
|
|
1861
|
+
}
|
|
1862
|
+
__name(retryDelayFor, "retryDelayFor");
|
|
1863
|
+
function sleep(ms) {
|
|
1864
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1865
|
+
}
|
|
1866
|
+
__name(sleep, "sleep");
|
|
1867
|
+
function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
1868
|
+
const stem = ticket.filename.replace(/\.md$/, "");
|
|
1869
|
+
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
1870
|
+
return join4(logsDir, `${stem}${suffix}${ext}`);
|
|
1871
|
+
}
|
|
1872
|
+
__name(attemptLogPath, "attemptLogPath");
|
|
1873
|
+
async function runTicketAttempt(opts) {
|
|
1874
|
+
const { ticket, d } = opts;
|
|
1875
|
+
let worktree;
|
|
1876
|
+
try {
|
|
1877
|
+
worktree = await createWorktree({
|
|
1878
|
+
repoRoot: opts.repoRoot,
|
|
1879
|
+
repoName: opts.repoName,
|
|
1880
|
+
baseBranch: opts.baseBranch,
|
|
1881
|
+
ticket
|
|
1882
|
+
});
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
return { kind: "worktree-error", error };
|
|
1885
|
+
}
|
|
1886
|
+
d.setTicketStatus(ticket.num, "in-progress");
|
|
1887
|
+
const agentResult = await spawnAgent({
|
|
1888
|
+
prompt: renderPrompt(
|
|
1889
|
+
opts.specBody,
|
|
1890
|
+
ticket,
|
|
1891
|
+
{ testCmd: opts.testCmd }
|
|
1892
|
+
),
|
|
1893
|
+
agentCmd: opts.agentCmd,
|
|
1894
|
+
model: opts.model,
|
|
1895
|
+
timeout: opts.timeout,
|
|
1896
|
+
logPath: attemptLogPath(
|
|
1897
|
+
opts.logsDir,
|
|
1898
|
+
ticket,
|
|
1899
|
+
opts.attempt,
|
|
1900
|
+
".log"
|
|
1901
|
+
),
|
|
1902
|
+
cwd: worktree.worktreePath,
|
|
1903
|
+
label: ticket.filename.replace(/\.md$/, ""),
|
|
1904
|
+
maxTurns: opts.maxTurns,
|
|
1905
|
+
thinking: opts.thinking,
|
|
1906
|
+
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1907
|
+
});
|
|
1908
|
+
if (agentResult.timedOut) {
|
|
1909
|
+
d.log(
|
|
1910
|
+
` ${ticket.filename}: timed out; keeping the work it committed`
|
|
1911
|
+
);
|
|
1912
|
+
} else if (agentResult.exitCode !== 0) {
|
|
1913
|
+
d.log(
|
|
1914
|
+
` ${ticket.filename}: agent exited ${agentResult.exitCode}`
|
|
1915
|
+
);
|
|
1916
|
+
await removeWorktreeQuietly(worktree.worktreePath, d);
|
|
1917
|
+
return {
|
|
1918
|
+
kind: "failed",
|
|
1919
|
+
reason: "exit",
|
|
1920
|
+
branchName: worktree.branchName,
|
|
1921
|
+
exitCode: agentResult.exitCode,
|
|
1922
|
+
completedItems: agentResult.completedItems
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
if (isDirty(worktree.worktreePath)) {
|
|
1926
|
+
const committed = await commitDirty(
|
|
1927
|
+
worktree.worktreePath,
|
|
1928
|
+
`nightralph: auto-commit ${ticket.filename} remaining changes`
|
|
1929
|
+
);
|
|
1930
|
+
if (committed) {
|
|
1931
|
+
d.log(
|
|
1932
|
+
` ${ticket.filename}: auto-committed remaining changes`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (!hasNewCommits(
|
|
1937
|
+
opts.repoRoot,
|
|
1938
|
+
opts.baseBranch,
|
|
1939
|
+
worktree.branchName
|
|
1940
|
+
)) {
|
|
1941
|
+
return { kind: "no-commits" };
|
|
1942
|
+
}
|
|
1943
|
+
if (opts.testCmd) {
|
|
1944
|
+
const passed = await runTestGate({
|
|
1945
|
+
testCmd: opts.testCmd,
|
|
1946
|
+
ticket,
|
|
1947
|
+
worktreePath: worktree.worktreePath,
|
|
1948
|
+
timeout: opts.timeout,
|
|
1949
|
+
logsDir: opts.logsDir,
|
|
1950
|
+
attempt: opts.attempt,
|
|
1951
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
1952
|
+
});
|
|
1953
|
+
if (!passed) {
|
|
1954
|
+
await removeWorktreeQuietly(
|
|
1955
|
+
worktree.worktreePath,
|
|
1956
|
+
d
|
|
1957
|
+
);
|
|
1958
|
+
return {
|
|
1959
|
+
kind: "failed",
|
|
1960
|
+
reason: "tests",
|
|
1961
|
+
branchName: worktree.branchName,
|
|
1962
|
+
exitCode: 1,
|
|
1963
|
+
completedItems: agentResult.completedItems
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return {
|
|
1968
|
+
kind: "ready",
|
|
1969
|
+
worktreePath: worktree.worktreePath,
|
|
1970
|
+
branchName: worktree.branchName,
|
|
1971
|
+
completedItems: agentResult.completedItems
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
__name(runTicketAttempt, "runTicketAttempt");
|
|
1975
|
+
async function runTicketAttempts(opts) {
|
|
1976
|
+
const { ticket, d } = opts;
|
|
1977
|
+
const maxAttempts = opts.retries + 1;
|
|
1978
|
+
const isPi = basename2(opts.agentCmd) === "pi";
|
|
1979
|
+
let thinking = opts.thinking;
|
|
1980
|
+
let outcome = { kind: "no-commits" };
|
|
1981
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1982
|
+
if (attempt === 1) {
|
|
1983
|
+
d.log(` Starting ${ticket.filename}`);
|
|
1984
|
+
} else {
|
|
1985
|
+
thinking = escalateThinking(thinking);
|
|
1986
|
+
const delay = retryDelayFor(opts.retryDelay, attempt);
|
|
1987
|
+
d.log(
|
|
1988
|
+
` ${ticket.filename}: retrying (attempt ${attempt}/${maxAttempts}` + (isPi ? `, thinking ${thinking}` : "") + (delay > 0 ? `, waiting ${delay}s` : "") + ")"
|
|
1989
|
+
);
|
|
1990
|
+
if (delay > 0) await sleep(delay * 1e3);
|
|
1991
|
+
}
|
|
1992
|
+
try {
|
|
1993
|
+
outcome = await runTicketAttempt({
|
|
1994
|
+
...opts,
|
|
1995
|
+
attempt,
|
|
1996
|
+
thinking
|
|
1997
|
+
});
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
d.log(
|
|
2000
|
+
` ${ticket.filename}: attempt ${attempt} threw: ${formatErrorMessage(error)}`
|
|
2001
|
+
);
|
|
2002
|
+
return { kind: "error", error };
|
|
2003
|
+
}
|
|
2004
|
+
if (outcome.kind === "ready" || outcome.kind === "worktree-error") {
|
|
2005
|
+
return outcome;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return outcome;
|
|
2009
|
+
}
|
|
2010
|
+
__name(runTicketAttempts, "runTicketAttempts");
|
|
2011
|
+
async function saveProgress(repoRoot, progressPath, progress, message, d) {
|
|
2012
|
+
writeProgress(progressPath, progress);
|
|
2013
|
+
try {
|
|
2014
|
+
await commitProgress(repoRoot, progressPath, message);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
d.log(
|
|
2017
|
+
" Failed to commit progress: " + formatErrorMessage(err)
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
__name(saveProgress, "saveProgress");
|
|
1594
2022
|
function runProgress(tickets) {
|
|
1595
2023
|
return {
|
|
1596
2024
|
completed: tickets.filter((t) => t.status === "done").length,
|
|
@@ -1598,15 +2026,21 @@ function runProgress(tickets) {
|
|
|
1598
2026
|
};
|
|
1599
2027
|
}
|
|
1600
2028
|
__name(runProgress, "runProgress");
|
|
1601
|
-
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, display) {
|
|
2029
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, display, maxTurns, thinking, retries, retryDelay) {
|
|
1602
2030
|
mkdirSync2(logsDir, { recursive: true });
|
|
1603
2031
|
const d = display ?? createDisplay();
|
|
2032
|
+
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
2033
|
+
const delayBase = retryDelay ?? 0;
|
|
2034
|
+
const isPi = basename2(agentCmd) === "pi";
|
|
1604
2035
|
let totalWaves = 0;
|
|
1605
2036
|
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
1606
2037
|
let totalCompleted = 0;
|
|
1607
2038
|
let waveNum = 0;
|
|
2039
|
+
const failedNums = /* @__PURE__ */ new Set();
|
|
1608
2040
|
while (true) {
|
|
1609
|
-
const ready = findReadyTickets(tickets)
|
|
2041
|
+
const ready = findReadyTickets(tickets).filter(
|
|
2042
|
+
(t) => !failedNums.has(t.num)
|
|
2043
|
+
);
|
|
1610
2044
|
if (ready.length === 0) break;
|
|
1611
2045
|
waveNum++;
|
|
1612
2046
|
const readyNums = new Set(ready.map((t) => t.num));
|
|
@@ -1629,25 +2063,48 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
1629
2063
|
const results = await Promise.allSettled(
|
|
1630
2064
|
ready.map(async (t) => {
|
|
1631
2065
|
const prompt = renderPrompt(specBody, t);
|
|
1632
|
-
const logPath = join3(
|
|
1633
|
-
logsDir,
|
|
1634
|
-
t.filename.replace(/\.md$/, ".log")
|
|
1635
|
-
);
|
|
1636
2066
|
const label = t.filename.replace(
|
|
1637
2067
|
/\.md$/,
|
|
1638
2068
|
""
|
|
1639
2069
|
);
|
|
1640
2070
|
d.setTicketStatus(t.num, "in-progress");
|
|
1641
2071
|
d.log(` Starting ${t.filename}`);
|
|
1642
|
-
|
|
2072
|
+
let level = thinking;
|
|
2073
|
+
let result = await spawnAgent({
|
|
1643
2074
|
prompt,
|
|
1644
2075
|
agentCmd,
|
|
1645
2076
|
model,
|
|
1646
2077
|
timeout,
|
|
1647
|
-
logPath,
|
|
2078
|
+
logPath: attemptLogPath(logsDir, t, 1, ".log"),
|
|
1648
2079
|
label,
|
|
2080
|
+
maxTurns,
|
|
2081
|
+
thinking: level,
|
|
1649
2082
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1650
2083
|
});
|
|
2084
|
+
for (let attempt = 2; result.exitCode !== 0 && !result.timedOut && attempt <= maxAttempts; attempt++) {
|
|
2085
|
+
level = escalateThinking(level);
|
|
2086
|
+
const delay = retryDelayFor(delayBase, attempt);
|
|
2087
|
+
d.log(
|
|
2088
|
+
` ${t.filename}: agent exited ${result.exitCode}; retrying (attempt ${attempt}/${maxAttempts}` + (isPi ? `, thinking ${level}` : "") + (delay > 0 ? `, waiting ${delay}s` : "") + ")"
|
|
2089
|
+
);
|
|
2090
|
+
if (delay > 0) await sleep(delay * 1e3);
|
|
2091
|
+
result = await spawnAgent({
|
|
2092
|
+
prompt,
|
|
2093
|
+
agentCmd,
|
|
2094
|
+
model,
|
|
2095
|
+
timeout,
|
|
2096
|
+
logPath: attemptLogPath(
|
|
2097
|
+
logsDir,
|
|
2098
|
+
t,
|
|
2099
|
+
attempt,
|
|
2100
|
+
".log"
|
|
2101
|
+
),
|
|
2102
|
+
label,
|
|
2103
|
+
maxTurns,
|
|
2104
|
+
thinking: level,
|
|
2105
|
+
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
1651
2108
|
return { ticket: t, result };
|
|
1652
2109
|
})
|
|
1653
2110
|
);
|
|
@@ -1656,6 +2113,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
1656
2113
|
if (r.status === "rejected") continue;
|
|
1657
2114
|
const { ticket, result } = r.value;
|
|
1658
2115
|
if (result.exitCode !== 0) {
|
|
2116
|
+
failedNums.add(ticket.num);
|
|
1659
2117
|
d.setTicketStatus(ticket.num, "failed");
|
|
1660
2118
|
d.log(
|
|
1661
2119
|
` ${ticket.filename}: agent exited ${result.exitCode}`
|
|
@@ -1683,7 +2141,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
1683
2141
|
);
|
|
1684
2142
|
d.log(
|
|
1685
2143
|
`
|
|
1686
|
-
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
2144
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedNums.size > 0 ? `, ${failedNums.size} failed` : "")
|
|
1687
2145
|
);
|
|
1688
2146
|
d.cleanup();
|
|
1689
2147
|
return { allDone: remaining.length === 0 };
|
|
@@ -1692,6 +2150,8 @@ __name(runWaves, "runWaves");
|
|
|
1692
2150
|
async function runWavesIsolated(opts) {
|
|
1693
2151
|
mkdirSync2(opts.logsDir, { recursive: true });
|
|
1694
2152
|
const d = opts.display ?? createDisplay();
|
|
2153
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
2154
|
+
const retryDelay = opts.retryDelay ?? 0;
|
|
1695
2155
|
let totalWaves = 0;
|
|
1696
2156
|
for (const _ of simulateWaves(opts.tickets)) totalWaves++;
|
|
1697
2157
|
const repoRoot = getRepoRoot();
|
|
@@ -1702,7 +2162,7 @@ async function runWavesIsolated(opts) {
|
|
|
1702
2162
|
"Working tree is dirty; commit or stash changes before running"
|
|
1703
2163
|
);
|
|
1704
2164
|
}
|
|
1705
|
-
const progressPath =
|
|
2165
|
+
const progressPath = join4(
|
|
1706
2166
|
dirname(opts.logsDir),
|
|
1707
2167
|
"progress.md"
|
|
1708
2168
|
);
|
|
@@ -1710,16 +2170,23 @@ async function runWavesIsolated(opts) {
|
|
|
1710
2170
|
generateProgress(opts.featureName, opts.tickets),
|
|
1711
2171
|
readProgress(progressPath)
|
|
1712
2172
|
);
|
|
1713
|
-
|
|
1714
|
-
await commitProgress(
|
|
2173
|
+
await saveProgress(
|
|
1715
2174
|
repoRoot,
|
|
1716
2175
|
progressPath,
|
|
1717
|
-
|
|
2176
|
+
progress,
|
|
2177
|
+
`nightralph: start ${opts.featureName}`,
|
|
2178
|
+
d
|
|
1718
2179
|
);
|
|
2180
|
+
if (!opts.testCmd) {
|
|
2181
|
+
d.log(" Test gate disabled: no test command");
|
|
2182
|
+
}
|
|
1719
2183
|
let totalCompleted = 0;
|
|
1720
2184
|
let waveNum = 0;
|
|
2185
|
+
const failedNums = /* @__PURE__ */ new Set();
|
|
1721
2186
|
while (true) {
|
|
1722
|
-
const ready = findReadyTickets(opts.tickets)
|
|
2187
|
+
const ready = findReadyTickets(opts.tickets).filter(
|
|
2188
|
+
(t) => !failedNums.has(t.num)
|
|
2189
|
+
);
|
|
1723
2190
|
if (ready.length === 0) break;
|
|
1724
2191
|
waveNum++;
|
|
1725
2192
|
const readyNums = new Set(ready.map((t) => t.num));
|
|
@@ -1739,125 +2206,89 @@ async function runWavesIsolated(opts) {
|
|
|
1739
2206
|
filename: t.filename
|
|
1740
2207
|
}))
|
|
1741
2208
|
);
|
|
1742
|
-
const
|
|
1743
|
-
ready.map((t) =>
|
|
2209
|
+
const outcomes = await Promise.all(
|
|
2210
|
+
ready.map((t) => runTicketAttempts({
|
|
2211
|
+
ticket: t,
|
|
1744
2212
|
repoRoot,
|
|
1745
2213
|
repoName,
|
|
1746
2214
|
baseBranch,
|
|
1747
|
-
|
|
2215
|
+
specBody: opts.specBody,
|
|
2216
|
+
agentCmd: opts.agentCmd,
|
|
2217
|
+
model: opts.model,
|
|
2218
|
+
timeout: opts.timeout,
|
|
2219
|
+
logsDir: opts.logsDir,
|
|
2220
|
+
testCmd: opts.testCmd,
|
|
2221
|
+
maxTurns: opts.maxTurns,
|
|
2222
|
+
thinking: opts.thinking,
|
|
2223
|
+
retries,
|
|
2224
|
+
retryDelay,
|
|
2225
|
+
d
|
|
1748
2226
|
}))
|
|
1749
2227
|
);
|
|
1750
|
-
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1753
|
-
|
|
2228
|
+
let waveCompleted = 0;
|
|
2229
|
+
const successfulBranches = [];
|
|
2230
|
+
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
2231
|
+
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
2232
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
2233
|
+
const outcome = outcomes[i];
|
|
1754
2234
|
const ticket = ready[i];
|
|
1755
|
-
if (
|
|
2235
|
+
if (outcome.kind === "worktree-error") {
|
|
2236
|
+
failedNums.add(ticket.num);
|
|
1756
2237
|
d.setTicketStatus(ticket.num, "failed");
|
|
1757
2238
|
d.log(
|
|
1758
|
-
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(
|
|
2239
|
+
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(outcome.error)}`
|
|
1759
2240
|
);
|
|
1760
2241
|
continue;
|
|
1761
2242
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const prompt = renderPrompt(opts.specBody, t);
|
|
1768
|
-
const logPath = join3(
|
|
1769
|
-
opts.logsDir,
|
|
1770
|
-
t.filename.replace(/\.md$/, ".log")
|
|
1771
|
-
);
|
|
1772
|
-
const label = t.filename.replace(
|
|
1773
|
-
/\.md$/,
|
|
1774
|
-
""
|
|
2243
|
+
if (outcome.kind === "error") {
|
|
2244
|
+
failedNums.add(ticket.num);
|
|
2245
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
2246
|
+
d.log(
|
|
2247
|
+
` ${ticket.filename}: attempt error: ` + formatErrorMessage(outcome.error)
|
|
1775
2248
|
);
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
agentCmd: opts.agentCmd,
|
|
1781
|
-
model: opts.model,
|
|
1782
|
-
timeout: opts.timeout,
|
|
1783
|
-
logPath,
|
|
1784
|
-
cwd: worktrees[i].worktreePath,
|
|
1785
|
-
label,
|
|
1786
|
-
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1787
|
-
});
|
|
1788
|
-
})
|
|
1789
|
-
);
|
|
1790
|
-
let waveCompleted = 0;
|
|
1791
|
-
const successfulBranches = [];
|
|
1792
|
-
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
1793
|
-
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
1794
|
-
for (let i = 0; i < results.length; i++) {
|
|
1795
|
-
const r = results[i];
|
|
1796
|
-
if (r.status === "rejected") continue;
|
|
1797
|
-
const agentResult = r.value;
|
|
1798
|
-
const ticket = activeTickets[i];
|
|
1799
|
-
const worktree = worktrees[i];
|
|
1800
|
-
if (agentResult.exitCode !== 0) {
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
if (outcome.kind === "no-commits") {
|
|
2252
|
+
failedNums.add(ticket.num);
|
|
1801
2253
|
d.setTicketStatus(ticket.num, "failed");
|
|
1802
2254
|
d.log(
|
|
1803
|
-
` ${ticket.filename}: agent exited
|
|
2255
|
+
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
1804
2256
|
);
|
|
1805
|
-
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
if (outcome.kind === "failed") {
|
|
2260
|
+
failedNums.add(ticket.num);
|
|
2261
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
2262
|
+
if (outcome.reason === "exit") {
|
|
2263
|
+
checkItems(ticket, outcome.completedItems);
|
|
2264
|
+
}
|
|
1806
2265
|
checkTicket(progress, ticket.num, {
|
|
1807
|
-
branch:
|
|
1808
|
-
exitCode:
|
|
2266
|
+
branch: outcome.branchName,
|
|
2267
|
+
exitCode: outcome.exitCode,
|
|
1809
2268
|
done: false
|
|
1810
2269
|
});
|
|
1811
|
-
|
|
1812
|
-
await commitProgress(
|
|
2270
|
+
await saveProgress(
|
|
1813
2271
|
repoRoot,
|
|
1814
2272
|
progressPath,
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
await removeWorktree(worktree.worktreePath);
|
|
1819
|
-
} catch (err) {
|
|
1820
|
-
d.log(
|
|
1821
|
-
` Failed to remove worktree ${worktree.worktreePath}: ` + formatErrorMessage(err)
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1826
|
-
if (isDirty(worktree.worktreePath)) {
|
|
1827
|
-
const committed = await commitDirty(
|
|
1828
|
-
worktree.worktreePath,
|
|
1829
|
-
`nightralph: auto-commit ${ticket.filename} remaining changes`
|
|
1830
|
-
);
|
|
1831
|
-
if (committed) {
|
|
1832
|
-
d.log(
|
|
1833
|
-
` ${ticket.filename}: auto-committed remaining changes`
|
|
1834
|
-
);
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
if (!hasNewCommits(
|
|
1838
|
-
repoRoot,
|
|
1839
|
-
baseBranch,
|
|
1840
|
-
worktree.branchName
|
|
1841
|
-
)) {
|
|
1842
|
-
d.setTicketStatus(ticket.num, "failed");
|
|
1843
|
-
d.log(
|
|
1844
|
-
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
2273
|
+
progress,
|
|
2274
|
+
`nightralph: ${ticket.filename} ` + (outcome.reason === "tests" ? "tests failed" : "failed"),
|
|
2275
|
+
d
|
|
1845
2276
|
);
|
|
1846
2277
|
continue;
|
|
1847
2278
|
}
|
|
1848
|
-
checkItems(ticket,
|
|
2279
|
+
checkItems(ticket, outcome.completedItems);
|
|
1849
2280
|
d.log(
|
|
1850
2281
|
` ${ticket.filename}: agent finished, awaiting merge`
|
|
1851
2282
|
);
|
|
1852
2283
|
ticketsByNum.set(ticket.num, ticket);
|
|
1853
2284
|
worktreePathsByNum.set(
|
|
1854
2285
|
ticket.num,
|
|
1855
|
-
|
|
2286
|
+
outcome.worktreePath
|
|
1856
2287
|
);
|
|
1857
2288
|
successfulBranches.push({
|
|
1858
|
-
branch:
|
|
2289
|
+
branch: outcome.branchName,
|
|
1859
2290
|
ticketNum: ticket.num,
|
|
1860
|
-
worktreePath:
|
|
2291
|
+
worktreePath: outcome.worktreePath
|
|
1861
2292
|
});
|
|
1862
2293
|
}
|
|
1863
2294
|
if (successfulBranches.length > 0) {
|
|
@@ -1884,23 +2315,21 @@ async function runWavesIsolated(opts) {
|
|
|
1884
2315
|
branch: branchResult.branch,
|
|
1885
2316
|
exitCode: 0
|
|
1886
2317
|
});
|
|
1887
|
-
|
|
1888
|
-
await commitProgress(
|
|
2318
|
+
await saveProgress(
|
|
1889
2319
|
repoRoot,
|
|
1890
2320
|
progressPath,
|
|
1891
|
-
|
|
2321
|
+
progress,
|
|
2322
|
+
`nightralph: ${ticket.filename} done`,
|
|
2323
|
+
d
|
|
1892
2324
|
);
|
|
1893
2325
|
const worktreePath = worktreePathsByNum.get(
|
|
1894
2326
|
ticket.num
|
|
1895
2327
|
);
|
|
1896
2328
|
if (worktreePath) {
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
2329
|
+
await removeWorktreeQuietly(
|
|
2330
|
+
worktreePath,
|
|
2331
|
+
d
|
|
2332
|
+
);
|
|
1904
2333
|
}
|
|
1905
2334
|
} else {
|
|
1906
2335
|
const worktreePath = worktreePathsByNum.get(
|
|
@@ -1917,6 +2346,8 @@ async function runWavesIsolated(opts) {
|
|
|
1917
2346
|
model: opts.model,
|
|
1918
2347
|
timeout: opts.timeout,
|
|
1919
2348
|
logsDir: opts.logsDir,
|
|
2349
|
+
testCmd: opts.testCmd,
|
|
2350
|
+
maxTurns: opts.maxTurns,
|
|
1920
2351
|
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine")
|
|
1921
2352
|
}) : false;
|
|
1922
2353
|
if (recovered) {
|
|
@@ -1931,15 +2362,19 @@ async function runWavesIsolated(opts) {
|
|
|
1931
2362
|
branch: branchResult.branch,
|
|
1932
2363
|
exitCode: 0
|
|
1933
2364
|
});
|
|
1934
|
-
|
|
1935
|
-
await commitProgress(
|
|
2365
|
+
await saveProgress(
|
|
1936
2366
|
repoRoot,
|
|
1937
2367
|
progressPath,
|
|
1938
|
-
|
|
2368
|
+
progress,
|
|
2369
|
+
`nightralph: ${ticket.filename} done (respawn)`,
|
|
2370
|
+
d
|
|
1939
2371
|
);
|
|
1940
2372
|
if (worktreePath) {
|
|
1941
2373
|
try {
|
|
1942
|
-
await removeWorktree(
|
|
2374
|
+
await removeWorktree(
|
|
2375
|
+
worktreePath,
|
|
2376
|
+
{ force: true }
|
|
2377
|
+
);
|
|
1943
2378
|
} catch (err) {
|
|
1944
2379
|
d.log(
|
|
1945
2380
|
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
@@ -1960,11 +2395,12 @@ async function runWavesIsolated(opts) {
|
|
|
1960
2395
|
exitCode: 0,
|
|
1961
2396
|
done: false
|
|
1962
2397
|
});
|
|
1963
|
-
|
|
1964
|
-
await commitProgress(
|
|
2398
|
+
await saveProgress(
|
|
1965
2399
|
repoRoot,
|
|
1966
2400
|
progressPath,
|
|
1967
|
-
|
|
2401
|
+
progress,
|
|
2402
|
+
`nightralph: ${ticket.filename} merge failed`,
|
|
2403
|
+
d
|
|
1968
2404
|
);
|
|
1969
2405
|
}
|
|
1970
2406
|
}
|
|
@@ -1978,7 +2414,10 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1978
2414
|
const ticket = ticketsByNum.get(sb.ticketNum);
|
|
1979
2415
|
if (ticket && ticket.status === "done") continue;
|
|
1980
2416
|
try {
|
|
1981
|
-
await removeWorktree(
|
|
2417
|
+
await removeWorktree(
|
|
2418
|
+
sb.worktreePath,
|
|
2419
|
+
{ force: true }
|
|
2420
|
+
);
|
|
1982
2421
|
} catch (err) {
|
|
1983
2422
|
d.log(
|
|
1984
2423
|
` Failed to remove worktree ${sb.worktreePath}: ` + formatErrorMessage(err)
|
|
@@ -2000,7 +2439,7 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
2000
2439
|
);
|
|
2001
2440
|
d.log(
|
|
2002
2441
|
`
|
|
2003
|
-
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
2442
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedNums.size > 0 ? `, ${failedNums.size} failed` : "")
|
|
2004
2443
|
);
|
|
2005
2444
|
d.cleanup();
|
|
2006
2445
|
return { allDone: remaining.length === 0 };
|
|
@@ -2008,8 +2447,8 @@ Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.
|
|
|
2008
2447
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
2009
2448
|
|
|
2010
2449
|
// src/resolve.ts
|
|
2011
|
-
import { existsSync as
|
|
2012
|
-
import { dirname as dirname2, join as
|
|
2450
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
2451
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
2013
2452
|
import { fileURLToPath } from "node:url";
|
|
2014
2453
|
import { select } from "@inquirer/prompts";
|
|
2015
2454
|
function getScriptDir(metaUrl) {
|
|
@@ -2017,8 +2456,8 @@ function getScriptDir(metaUrl) {
|
|
|
2017
2456
|
}
|
|
2018
2457
|
__name(getScriptDir, "getScriptDir");
|
|
2019
2458
|
function findFeatureDirs(cwd) {
|
|
2020
|
-
const scratchDir =
|
|
2021
|
-
if (!
|
|
2459
|
+
const scratchDir = join5(cwd, ".scratch");
|
|
2460
|
+
if (!existsSync4(scratchDir)) return [];
|
|
2022
2461
|
const entries = readdirSync3(
|
|
2023
2462
|
scratchDir,
|
|
2024
2463
|
{ withFileTypes: true }
|
|
@@ -2026,17 +2465,17 @@ function findFeatureDirs(cwd) {
|
|
|
2026
2465
|
const results = [];
|
|
2027
2466
|
for (const entry of entries) {
|
|
2028
2467
|
if (!entry.isDirectory()) continue;
|
|
2029
|
-
const issuesDir =
|
|
2468
|
+
const issuesDir = join5(
|
|
2030
2469
|
scratchDir,
|
|
2031
2470
|
entry.name,
|
|
2032
2471
|
"issues"
|
|
2033
2472
|
);
|
|
2034
|
-
const specFile =
|
|
2473
|
+
const specFile = join5(
|
|
2035
2474
|
scratchDir,
|
|
2036
2475
|
entry.name,
|
|
2037
2476
|
"spec.md"
|
|
2038
2477
|
);
|
|
2039
|
-
if (
|
|
2478
|
+
if (existsSync4(issuesDir)) {
|
|
2040
2479
|
results.push({
|
|
2041
2480
|
name: entry.name,
|
|
2042
2481
|
dir: issuesDir,
|
|
@@ -2049,10 +2488,10 @@ function findFeatureDirs(cwd) {
|
|
|
2049
2488
|
__name(findFeatureDirs, "findFeatureDirs");
|
|
2050
2489
|
async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
2051
2490
|
if (specName) {
|
|
2052
|
-
const featureDir =
|
|
2491
|
+
const featureDir = join5(cwd, ".scratch", specName);
|
|
2053
2492
|
return {
|
|
2054
|
-
dir:
|
|
2055
|
-
spec:
|
|
2493
|
+
dir: join5(featureDir, "issues"),
|
|
2494
|
+
spec: join5(featureDir, "spec.md")
|
|
2056
2495
|
};
|
|
2057
2496
|
}
|
|
2058
2497
|
const features = findFeatureDirs(cwd);
|
|
@@ -2115,8 +2554,8 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2115
2554
|
}
|
|
2116
2555
|
}
|
|
2117
2556
|
setup({
|
|
2118
|
-
bundledSkillsDir:
|
|
2119
|
-
docsTemplatesDir:
|
|
2557
|
+
bundledSkillsDir: join6(scriptDir, "skills"),
|
|
2558
|
+
docsTemplatesDir: join6(
|
|
2120
2559
|
scriptDir,
|
|
2121
2560
|
"docs-templates"
|
|
2122
2561
|
),
|
|
@@ -2146,11 +2585,29 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2146
2585
|
}).option("timeout", {
|
|
2147
2586
|
type: "number",
|
|
2148
2587
|
describe: "Kill agent after N seconds",
|
|
2149
|
-
default:
|
|
2588
|
+
default: 3600
|
|
2589
|
+
}).option("test-cmd", {
|
|
2590
|
+
type: "string",
|
|
2591
|
+
describe: 'Command run in each worktree after the agent exits; non-zero exit rejects the ticket. Defaults to "npm test" when package.json has a test script. Pass "" to disable.'
|
|
2150
2592
|
}).option("X", {
|
|
2151
2593
|
type: "boolean",
|
|
2152
2594
|
describe: "Stop on merge conflict instead of re-spawning",
|
|
2153
2595
|
default: false
|
|
2596
|
+
}).option("max-turns", {
|
|
2597
|
+
type: "number",
|
|
2598
|
+
describe: "Max agentic turns per ticket (forwarded to the agent CLI)"
|
|
2599
|
+
}).option("thinking", {
|
|
2600
|
+
type: "string",
|
|
2601
|
+
choices: THINKING_LEVELS,
|
|
2602
|
+
describe: "Starting thinking level for the pi provider. Omitted = pi's default. Retries step up from here."
|
|
2603
|
+
}).option("retries", {
|
|
2604
|
+
type: "number",
|
|
2605
|
+
default: DEFAULT_RETRIES,
|
|
2606
|
+
describe: "Extra attempts per ticket after a failed run, each with a higher thinking level (pi). 0 disables retries."
|
|
2607
|
+
}).option("retry-delay", {
|
|
2608
|
+
type: "number",
|
|
2609
|
+
default: DEFAULT_RETRY_DELAY,
|
|
2610
|
+
describe: "Seconds to wait before the first retry; doubles each further retry. 0 disables the wait."
|
|
2154
2611
|
}),
|
|
2155
2612
|
async (argv) => {
|
|
2156
2613
|
await runExecute({
|
|
@@ -2159,7 +2616,12 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2159
2616
|
spec: argv.spec,
|
|
2160
2617
|
dryRun: argv.dryRun,
|
|
2161
2618
|
timeout: argv.timeout,
|
|
2162
|
-
|
|
2619
|
+
testCmd: argv.testCmd,
|
|
2620
|
+
stopOnConflict: argv.X,
|
|
2621
|
+
maxTurns: argv.maxTurns,
|
|
2622
|
+
thinking: argv.thinking,
|
|
2623
|
+
retries: argv.retries,
|
|
2624
|
+
retryDelay: argv.retryDelay
|
|
2163
2625
|
});
|
|
2164
2626
|
}
|
|
2165
2627
|
).strict().demandCommand(1, "Specify a command or provider").help().parse();
|
|
@@ -2187,15 +2649,18 @@ async function runExecute(argv) {
|
|
|
2187
2649
|
const { dir, spec } = await resolveIssuesDir(
|
|
2188
2650
|
argv.spec
|
|
2189
2651
|
);
|
|
2190
|
-
if (!
|
|
2652
|
+
if (!existsSync5(dir)) {
|
|
2191
2653
|
console.error(`Directory not found: ${dir}`);
|
|
2192
2654
|
process.exit(2);
|
|
2193
2655
|
}
|
|
2194
|
-
if (!
|
|
2656
|
+
if (!existsSync5(spec)) {
|
|
2195
2657
|
console.error(`Spec file not found: ${spec}`);
|
|
2196
2658
|
process.exit(2);
|
|
2197
2659
|
}
|
|
2198
|
-
const specBody =
|
|
2660
|
+
const specBody = stripSpecSection(
|
|
2661
|
+
readFileSync5(spec, "utf8"),
|
|
2662
|
+
"User Stories"
|
|
2663
|
+
);
|
|
2199
2664
|
const tickets = scanTickets(dir);
|
|
2200
2665
|
if (tickets.length === 0) {
|
|
2201
2666
|
console.log("No ticket files found. Exiting.");
|
|
@@ -2216,11 +2681,14 @@ async function runExecute(argv) {
|
|
|
2216
2681
|
(b) => knownNums.has(b)
|
|
2217
2682
|
);
|
|
2218
2683
|
}
|
|
2219
|
-
const logsDir =
|
|
2220
|
-
const timeout = argv.timeout ??
|
|
2684
|
+
const logsDir = join6(dirname3(dir), "logs");
|
|
2685
|
+
const timeout = argv.timeout ?? 900;
|
|
2686
|
+
const testCmd = argv.testCmd === void 0 ? detectTestCmd(
|
|
2687
|
+
isGitRepo() ? getRepoRoot() : process.cwd()
|
|
2688
|
+
) : argv.testCmd || null;
|
|
2221
2689
|
const featureName = extractFeatureName(spec);
|
|
2222
2690
|
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
2223
|
-
const progressPath =
|
|
2691
|
+
const progressPath = join6(
|
|
2224
2692
|
dirname3(dir),
|
|
2225
2693
|
"progress.md"
|
|
2226
2694
|
);
|
|
@@ -2232,7 +2700,11 @@ async function runExecute(argv) {
|
|
|
2232
2700
|
specBody,
|
|
2233
2701
|
repoName,
|
|
2234
2702
|
conflictStrategy,
|
|
2235
|
-
progressPath
|
|
2703
|
+
progressPath,
|
|
2704
|
+
testCmd,
|
|
2705
|
+
thinking: argv.thinking,
|
|
2706
|
+
retries: argv.retries,
|
|
2707
|
+
retryDelay: argv.retryDelay
|
|
2236
2708
|
});
|
|
2237
2709
|
} else {
|
|
2238
2710
|
dryRun(tickets, specBody);
|
|
@@ -2240,6 +2712,14 @@ async function runExecute(argv) {
|
|
|
2240
2712
|
return;
|
|
2241
2713
|
}
|
|
2242
2714
|
const display = createDisplay();
|
|
2715
|
+
function onSignal(exitCode) {
|
|
2716
|
+
killRunningAgents();
|
|
2717
|
+
display.cleanup();
|
|
2718
|
+
process.exit(exitCode);
|
|
2719
|
+
}
|
|
2720
|
+
__name(onSignal, "onSignal");
|
|
2721
|
+
process.on("SIGINT", () => onSignal(130));
|
|
2722
|
+
process.on("SIGTERM", () => onSignal(143));
|
|
2243
2723
|
let result;
|
|
2244
2724
|
if (isGitRepo()) {
|
|
2245
2725
|
result = await runWavesIsolated({
|
|
@@ -2251,7 +2731,12 @@ async function runExecute(argv) {
|
|
|
2251
2731
|
logsDir,
|
|
2252
2732
|
featureName,
|
|
2253
2733
|
conflictStrategy,
|
|
2254
|
-
display
|
|
2734
|
+
display,
|
|
2735
|
+
testCmd,
|
|
2736
|
+
maxTurns: argv.maxTurns,
|
|
2737
|
+
thinking: argv.thinking,
|
|
2738
|
+
retries: argv.retries,
|
|
2739
|
+
retryDelay: argv.retryDelay
|
|
2255
2740
|
});
|
|
2256
2741
|
} else {
|
|
2257
2742
|
result = await runWaves(
|
|
@@ -2261,7 +2746,11 @@ async function runExecute(argv) {
|
|
|
2261
2746
|
argv.model ?? null,
|
|
2262
2747
|
timeout,
|
|
2263
2748
|
logsDir,
|
|
2264
|
-
display
|
|
2749
|
+
display,
|
|
2750
|
+
argv.maxTurns,
|
|
2751
|
+
argv.thinking,
|
|
2752
|
+
argv.retries,
|
|
2753
|
+
argv.retryDelay
|
|
2265
2754
|
);
|
|
2266
2755
|
}
|
|
2267
2756
|
if (!result.allDone) {
|