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