nightralph 0.0.18 → 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 +12 -20
- 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 +691 -198
- 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 -2
- 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
|
|
|
@@ -734,6 +757,8 @@ var FILLED_DOT = "\u25CF";
|
|
|
734
757
|
var EMPTY_DOT = "\u25CB";
|
|
735
758
|
var SEPARATOR_WIDTH = 50;
|
|
736
759
|
var DEFAULT_COLUMNS = 80;
|
|
760
|
+
var MIN_LOG_ROWS = 1;
|
|
761
|
+
var MAX_MESSAGE_BUFFER = 200;
|
|
737
762
|
var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
738
763
|
function fitLine(line, width) {
|
|
739
764
|
const visible = line.replace(ANSI_RE, "");
|
|
@@ -766,16 +791,14 @@ var StatusDisplay = class {
|
|
|
766
791
|
config;
|
|
767
792
|
viewportStart = 0;
|
|
768
793
|
resizeListener = null;
|
|
769
|
-
signalHandler = null;
|
|
770
794
|
constructor(config) {
|
|
771
795
|
this.config = {
|
|
772
|
-
messageLines: config?.messageLines ?? 10,
|
|
773
796
|
stream: config?.stream ?? process.stdout
|
|
774
797
|
};
|
|
775
798
|
}
|
|
776
799
|
getAvailableRows() {
|
|
777
800
|
const termHeight = this.config.stream.rows ?? 24;
|
|
778
|
-
const overhead = 2 +
|
|
801
|
+
const overhead = 2 + MIN_LOG_ROWS;
|
|
779
802
|
const available = termHeight - overhead;
|
|
780
803
|
return Math.max(1, available);
|
|
781
804
|
}
|
|
@@ -809,13 +832,6 @@ var StatusDisplay = class {
|
|
|
809
832
|
this.resizeListener
|
|
810
833
|
);
|
|
811
834
|
}
|
|
812
|
-
if (this.signalHandler) {
|
|
813
|
-
process.removeListener("SIGINT", this.signalHandler);
|
|
814
|
-
process.removeListener("SIGTERM", this.signalHandler);
|
|
815
|
-
}
|
|
816
|
-
this.signalHandler = () => this.cleanup();
|
|
817
|
-
process.on("SIGINT", this.signalHandler);
|
|
818
|
-
process.on("SIGTERM", this.signalHandler);
|
|
819
835
|
this.render();
|
|
820
836
|
}
|
|
821
837
|
setTicketStatus(num, status) {
|
|
@@ -842,7 +858,7 @@ var StatusDisplay = class {
|
|
|
842
858
|
}
|
|
843
859
|
log(message) {
|
|
844
860
|
this.messages.push(message);
|
|
845
|
-
if (this.messages.length >
|
|
861
|
+
if (this.messages.length > MAX_MESSAGE_BUFFER) {
|
|
846
862
|
this.messages.shift();
|
|
847
863
|
}
|
|
848
864
|
this.render();
|
|
@@ -851,10 +867,6 @@ var StatusDisplay = class {
|
|
|
851
867
|
if (this.resizeListener && this.config.stream.isTTY) {
|
|
852
868
|
this.config.stream.removeListener("resize", this.resizeListener);
|
|
853
869
|
}
|
|
854
|
-
if (this.signalHandler) {
|
|
855
|
-
process.removeListener("SIGINT", this.signalHandler);
|
|
856
|
-
process.removeListener("SIGTERM", this.signalHandler);
|
|
857
|
-
}
|
|
858
870
|
this.config.stream.write(ansi.cursorShow());
|
|
859
871
|
}
|
|
860
872
|
render() {
|
|
@@ -941,18 +953,21 @@ var StatusDisplay = class {
|
|
|
941
953
|
renderedTicketCount = (hasMoreAbove ? 1 : 0) + visibleTickets.length + (hasMoreBelow ? 1 : 0);
|
|
942
954
|
}
|
|
943
955
|
output += "-".repeat(Math.min(SEPARATOR_WIDTH, width)) + "\n";
|
|
944
|
-
const
|
|
956
|
+
const termHeight = this.config.stream.rows ?? 24;
|
|
957
|
+
const logRows = Math.max(
|
|
945
958
|
0,
|
|
946
|
-
|
|
959
|
+
termHeight - 2 - renderedTicketCount
|
|
947
960
|
);
|
|
948
|
-
|
|
961
|
+
const visibleMessages = this.messages.slice(-logRows);
|
|
962
|
+
for (const msg of visibleMessages) {
|
|
949
963
|
output += `${fitLine(msg, width)}
|
|
950
964
|
`;
|
|
951
965
|
}
|
|
952
|
-
|
|
966
|
+
const emptyRows = logRows - visibleMessages.length;
|
|
967
|
+
for (let i = 0; i < emptyRows; i++) {
|
|
953
968
|
output += "\n";
|
|
954
969
|
}
|
|
955
|
-
this.renderedLines = 2 + renderedTicketCount +
|
|
970
|
+
this.renderedLines = 2 + renderedTicketCount + logRows;
|
|
956
971
|
this.config.stream.write(output);
|
|
957
972
|
}
|
|
958
973
|
};
|
|
@@ -984,11 +999,83 @@ function createDisplay(stream) {
|
|
|
984
999
|
}
|
|
985
1000
|
__name(createDisplay, "createDisplay");
|
|
986
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
|
+
|
|
987
1064
|
// src/orchestrator.ts
|
|
988
1065
|
function formatErrorMessage(error) {
|
|
989
1066
|
return error instanceof Error ? error.message : String(error);
|
|
990
1067
|
}
|
|
991
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");
|
|
992
1079
|
var TICKET_RE = /^(\d+)-(.+)\.md$/;
|
|
993
1080
|
function parseTicketFilename(filename) {
|
|
994
1081
|
const m = filename.match(TICKET_RE);
|
|
@@ -1023,8 +1110,8 @@ function scanTickets(dir) {
|
|
|
1023
1110
|
));
|
|
1024
1111
|
return files.map((filename) => {
|
|
1025
1112
|
const parsed = parseTicketFilename(filename);
|
|
1026
|
-
const filepath =
|
|
1027
|
-
const body =
|
|
1113
|
+
const filepath = join4(dir, filename);
|
|
1114
|
+
const body = readFileSync4(filepath, "utf8");
|
|
1028
1115
|
return {
|
|
1029
1116
|
num: parsed.num,
|
|
1030
1117
|
slug: parsed.slug,
|
|
@@ -1079,7 +1166,39 @@ function markDone(ticket) {
|
|
|
1079
1166
|
ticket.body = updated;
|
|
1080
1167
|
}
|
|
1081
1168
|
__name(markDone, "markDone");
|
|
1082
|
-
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
|
+
] : [];
|
|
1083
1202
|
return [
|
|
1084
1203
|
"---- SPEC CONTEXT ----",
|
|
1085
1204
|
specBody,
|
|
@@ -1091,10 +1210,12 @@ function renderPrompt(specBody, ticket) {
|
|
|
1091
1210
|
"---- AGENT INSTRUCTIONS ----",
|
|
1092
1211
|
"You are an autonomous coding agent. Implement the ticket.",
|
|
1093
1212
|
"",
|
|
1094
|
-
"
|
|
1095
|
-
"-
|
|
1213
|
+
"Follow these rules:",
|
|
1214
|
+
"- Read the files directly relevant to the ticket before writing code.",
|
|
1096
1215
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
1097
|
-
|
|
1216
|
+
...testRules,
|
|
1217
|
+
"- Do NOT read or modify files outside your current directory (the worktree).",
|
|
1218
|
+
"- Do NOT run git push.",
|
|
1098
1219
|
"- Commit your work when finished.",
|
|
1099
1220
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
1100
1221
|
""
|
|
@@ -1120,7 +1241,8 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
1120
1241
|
"2. Make sure the code compiles and tests pass.",
|
|
1121
1242
|
"3. Commit the resolved files.",
|
|
1122
1243
|
"",
|
|
1123
|
-
"Do NOT re-implement the ticket from scratch."
|
|
1244
|
+
"Do NOT re-implement the ticket from scratch.",
|
|
1245
|
+
"Do NOT run git push."
|
|
1124
1246
|
].join("\n");
|
|
1125
1247
|
}
|
|
1126
1248
|
__name(renderMergePrompt, "renderMergePrompt");
|
|
@@ -1199,7 +1321,11 @@ var PROVIDER_ARGS = {
|
|
|
1199
1321
|
"stream-json",
|
|
1200
1322
|
"--dangerously-skip-permissions"
|
|
1201
1323
|
],
|
|
1202
|
-
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"],
|
|
1203
1329
|
pi: [
|
|
1204
1330
|
"-p",
|
|
1205
1331
|
"--verbose",
|
|
@@ -1214,6 +1340,21 @@ function getProviderArgs(cmd) {
|
|
|
1214
1340
|
return PROVIDER_ARGS[name] ?? [];
|
|
1215
1341
|
}
|
|
1216
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");
|
|
1217
1358
|
function formatStreamLine(line) {
|
|
1218
1359
|
let obj;
|
|
1219
1360
|
try {
|
|
@@ -1314,7 +1455,7 @@ function createPiStreamFormatter() {
|
|
|
1314
1455
|
__name(createPiStreamFormatter, "createPiStreamFormatter");
|
|
1315
1456
|
function writePromptFile(prompt) {
|
|
1316
1457
|
const name = `nightralph-${Date.now()}-${Math.random().toString(36).slice(2)}.md`;
|
|
1317
|
-
const filePath =
|
|
1458
|
+
const filePath = join4(tmpdir(), name);
|
|
1318
1459
|
writeFileSync3(filePath, prompt);
|
|
1319
1460
|
return filePath;
|
|
1320
1461
|
}
|
|
@@ -1326,6 +1467,22 @@ function cleanupPromptFile(filePath) {
|
|
|
1326
1467
|
}
|
|
1327
1468
|
}
|
|
1328
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");
|
|
1329
1486
|
function spawnAgent(opts) {
|
|
1330
1487
|
const args = [
|
|
1331
1488
|
...getProviderArgs(opts.agentCmd)
|
|
@@ -1333,9 +1490,15 @@ function spawnAgent(opts) {
|
|
|
1333
1490
|
if (opts.model) {
|
|
1334
1491
|
args.push("--model", opts.model);
|
|
1335
1492
|
}
|
|
1493
|
+
if (opts.maxTurns) {
|
|
1494
|
+
args.push("--max-turns", String(opts.maxTurns));
|
|
1495
|
+
}
|
|
1336
1496
|
const providerName = basename2(opts.agentCmd);
|
|
1337
1497
|
const isClaude = providerName === "claude";
|
|
1338
1498
|
const isPi = providerName === "pi";
|
|
1499
|
+
if (isPi && opts.thinking) {
|
|
1500
|
+
args.push("--thinking", opts.thinking);
|
|
1501
|
+
}
|
|
1339
1502
|
const prefix = opts.label ? ` [${opts.label}] ` : " ";
|
|
1340
1503
|
let promptFile = null;
|
|
1341
1504
|
if (isPi) {
|
|
@@ -1346,10 +1509,12 @@ function spawnAgent(opts) {
|
|
|
1346
1509
|
opts.logPath,
|
|
1347
1510
|
{ flags: "w" }
|
|
1348
1511
|
);
|
|
1349
|
-
const proc =
|
|
1512
|
+
const proc = spawn2(opts.agentCmd, args, {
|
|
1350
1513
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1351
|
-
cwd: opts.cwd
|
|
1514
|
+
cwd: opts.cwd,
|
|
1515
|
+
detached: true
|
|
1352
1516
|
});
|
|
1517
|
+
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
1353
1518
|
proc.stdout.pipe(logStream);
|
|
1354
1519
|
const piFormatter = isPi ? createPiStreamFormatter() : null;
|
|
1355
1520
|
const completedItems = [];
|
|
@@ -1403,21 +1568,41 @@ function spawnAgent(opts) {
|
|
|
1403
1568
|
console.error(output);
|
|
1404
1569
|
}
|
|
1405
1570
|
});
|
|
1571
|
+
let timedOut = false;
|
|
1406
1572
|
const timeout = setTimeout(() => {
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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}`);
|
|
1412
1582
|
}
|
|
1583
|
+
killProcessGroup(proc.pid);
|
|
1413
1584
|
}, opts.timeout * 1e3);
|
|
1585
|
+
proc.stdin.on("error", (err) => {
|
|
1586
|
+
logStream.write(
|
|
1587
|
+
`nightralph: stdin write failed: ${formatErrorMessage(err)}
|
|
1588
|
+
`
|
|
1589
|
+
);
|
|
1590
|
+
});
|
|
1414
1591
|
if (!promptFile) {
|
|
1415
1592
|
proc.stdin.write(opts.prompt);
|
|
1416
1593
|
}
|
|
1417
1594
|
proc.stdin.end();
|
|
1418
1595
|
return new Promise((resolve) => {
|
|
1419
|
-
|
|
1596
|
+
let settled = false;
|
|
1597
|
+
let graceTimer;
|
|
1598
|
+
function finish(exitCode) {
|
|
1599
|
+
if (settled) return;
|
|
1600
|
+
settled = true;
|
|
1420
1601
|
clearTimeout(timeout);
|
|
1602
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
1603
|
+
if (proc.pid !== void 0) {
|
|
1604
|
+
runningAgents.delete(proc.pid);
|
|
1605
|
+
}
|
|
1421
1606
|
if (piFormatter) {
|
|
1422
1607
|
for (const line of piFormatter.flush()) {
|
|
1423
1608
|
collectCompleted(line);
|
|
@@ -1433,13 +1618,27 @@ function spawnAgent(opts) {
|
|
|
1433
1618
|
rlErr.close();
|
|
1434
1619
|
logStream.end();
|
|
1435
1620
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
1436
|
-
resolve({
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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);
|
|
1440
1633
|
});
|
|
1441
1634
|
proc.on("error", (err) => {
|
|
1635
|
+
if (settled) return;
|
|
1636
|
+
settled = true;
|
|
1637
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
1442
1638
|
clearTimeout(timeout);
|
|
1639
|
+
if (proc.pid !== void 0) {
|
|
1640
|
+
runningAgents.delete(proc.pid);
|
|
1641
|
+
}
|
|
1443
1642
|
rl.close();
|
|
1444
1643
|
rlErr.close();
|
|
1445
1644
|
logStream.end();
|
|
@@ -1448,7 +1647,11 @@ function spawnAgent(opts) {
|
|
|
1448
1647
|
"Agent process error:",
|
|
1449
1648
|
formatErrorMessage(err)
|
|
1450
1649
|
);
|
|
1451
|
-
resolve({
|
|
1650
|
+
resolve({
|
|
1651
|
+
exitCode: 1,
|
|
1652
|
+
completedItems,
|
|
1653
|
+
timedOut
|
|
1654
|
+
});
|
|
1452
1655
|
});
|
|
1453
1656
|
});
|
|
1454
1657
|
}
|
|
@@ -1511,18 +1714,65 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
1511
1714
|
`
|
|
1512
1715
|
);
|
|
1513
1716
|
console.log(
|
|
1514
|
-
renderPrompt(
|
|
1717
|
+
renderPrompt(
|
|
1718
|
+
opts.specBody,
|
|
1719
|
+
firstReady[0],
|
|
1720
|
+
{ testCmd: opts.testCmd }
|
|
1721
|
+
)
|
|
1515
1722
|
);
|
|
1516
1723
|
}
|
|
1517
1724
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
1518
1725
|
console.log(
|
|
1519
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
|
+
`
|
|
1520
1740
|
Merge strategy: ${strategyDesc}`
|
|
1521
1741
|
);
|
|
1522
1742
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
1523
1743
|
}
|
|
1524
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");
|
|
1525
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");
|
|
1526
1776
|
await updateWorktreeFromBase(
|
|
1527
1777
|
opts.worktreePath,
|
|
1528
1778
|
opts.baseBranch
|
|
@@ -1537,7 +1787,7 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1537
1787
|
opts.specBody,
|
|
1538
1788
|
opts.ticket
|
|
1539
1789
|
);
|
|
1540
|
-
const logPath =
|
|
1790
|
+
const logPath = join4(
|
|
1541
1791
|
opts.logsDir,
|
|
1542
1792
|
opts.ticket.filename.replace(/\.md$/, ".respawn.log")
|
|
1543
1793
|
);
|
|
@@ -1553,6 +1803,7 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1553
1803
|
logPath,
|
|
1554
1804
|
cwd: opts.worktreePath,
|
|
1555
1805
|
label,
|
|
1806
|
+
maxTurns: opts.maxTurns,
|
|
1556
1807
|
onLine: opts.onLine
|
|
1557
1808
|
});
|
|
1558
1809
|
if (result.exitCode !== 0) {
|
|
@@ -1564,6 +1815,22 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1564
1815
|
}
|
|
1565
1816
|
return false;
|
|
1566
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
|
+
}
|
|
1567
1834
|
const mergeResult = await mergeBranch(
|
|
1568
1835
|
opts.repoRoot,
|
|
1569
1836
|
opts.branch
|
|
@@ -1587,6 +1854,171 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1587
1854
|
return mergeResult.success;
|
|
1588
1855
|
}
|
|
1589
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");
|
|
1590
2022
|
function runProgress(tickets) {
|
|
1591
2023
|
return {
|
|
1592
2024
|
completed: tickets.filter((t) => t.status === "done").length,
|
|
@@ -1594,15 +2026,21 @@ function runProgress(tickets) {
|
|
|
1594
2026
|
};
|
|
1595
2027
|
}
|
|
1596
2028
|
__name(runProgress, "runProgress");
|
|
1597
|
-
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) {
|
|
1598
2030
|
mkdirSync2(logsDir, { recursive: true });
|
|
1599
2031
|
const d = display ?? createDisplay();
|
|
2032
|
+
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
2033
|
+
const delayBase = retryDelay ?? 0;
|
|
2034
|
+
const isPi = basename2(agentCmd) === "pi";
|
|
1600
2035
|
let totalWaves = 0;
|
|
1601
2036
|
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
1602
2037
|
let totalCompleted = 0;
|
|
1603
2038
|
let waveNum = 0;
|
|
2039
|
+
const failedNums = /* @__PURE__ */ new Set();
|
|
1604
2040
|
while (true) {
|
|
1605
|
-
const ready = findReadyTickets(tickets)
|
|
2041
|
+
const ready = findReadyTickets(tickets).filter(
|
|
2042
|
+
(t) => !failedNums.has(t.num)
|
|
2043
|
+
);
|
|
1606
2044
|
if (ready.length === 0) break;
|
|
1607
2045
|
waveNum++;
|
|
1608
2046
|
const readyNums = new Set(ready.map((t) => t.num));
|
|
@@ -1625,25 +2063,48 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
1625
2063
|
const results = await Promise.allSettled(
|
|
1626
2064
|
ready.map(async (t) => {
|
|
1627
2065
|
const prompt = renderPrompt(specBody, t);
|
|
1628
|
-
const logPath = join3(
|
|
1629
|
-
logsDir,
|
|
1630
|
-
t.filename.replace(/\.md$/, ".log")
|
|
1631
|
-
);
|
|
1632
2066
|
const label = t.filename.replace(
|
|
1633
2067
|
/\.md$/,
|
|
1634
2068
|
""
|
|
1635
2069
|
);
|
|
1636
2070
|
d.setTicketStatus(t.num, "in-progress");
|
|
1637
2071
|
d.log(` Starting ${t.filename}`);
|
|
1638
|
-
|
|
2072
|
+
let level = thinking;
|
|
2073
|
+
let result = await spawnAgent({
|
|
1639
2074
|
prompt,
|
|
1640
2075
|
agentCmd,
|
|
1641
2076
|
model,
|
|
1642
2077
|
timeout,
|
|
1643
|
-
logPath,
|
|
2078
|
+
logPath: attemptLogPath(logsDir, t, 1, ".log"),
|
|
1644
2079
|
label,
|
|
2080
|
+
maxTurns,
|
|
2081
|
+
thinking: level,
|
|
1645
2082
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1646
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
|
+
}
|
|
1647
2108
|
return { ticket: t, result };
|
|
1648
2109
|
})
|
|
1649
2110
|
);
|
|
@@ -1652,6 +2113,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
1652
2113
|
if (r.status === "rejected") continue;
|
|
1653
2114
|
const { ticket, result } = r.value;
|
|
1654
2115
|
if (result.exitCode !== 0) {
|
|
2116
|
+
failedNums.add(ticket.num);
|
|
1655
2117
|
d.setTicketStatus(ticket.num, "failed");
|
|
1656
2118
|
d.log(
|
|
1657
2119
|
` ${ticket.filename}: agent exited ${result.exitCode}`
|
|
@@ -1679,7 +2141,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
1679
2141
|
);
|
|
1680
2142
|
d.log(
|
|
1681
2143
|
`
|
|
1682
|
-
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` : "")
|
|
1683
2145
|
);
|
|
1684
2146
|
d.cleanup();
|
|
1685
2147
|
return { allDone: remaining.length === 0 };
|
|
@@ -1688,6 +2150,8 @@ __name(runWaves, "runWaves");
|
|
|
1688
2150
|
async function runWavesIsolated(opts) {
|
|
1689
2151
|
mkdirSync2(opts.logsDir, { recursive: true });
|
|
1690
2152
|
const d = opts.display ?? createDisplay();
|
|
2153
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
2154
|
+
const retryDelay = opts.retryDelay ?? 0;
|
|
1691
2155
|
let totalWaves = 0;
|
|
1692
2156
|
for (const _ of simulateWaves(opts.tickets)) totalWaves++;
|
|
1693
2157
|
const repoRoot = getRepoRoot();
|
|
@@ -1698,7 +2162,7 @@ async function runWavesIsolated(opts) {
|
|
|
1698
2162
|
"Working tree is dirty; commit or stash changes before running"
|
|
1699
2163
|
);
|
|
1700
2164
|
}
|
|
1701
|
-
const progressPath =
|
|
2165
|
+
const progressPath = join4(
|
|
1702
2166
|
dirname(opts.logsDir),
|
|
1703
2167
|
"progress.md"
|
|
1704
2168
|
);
|
|
@@ -1706,16 +2170,23 @@ async function runWavesIsolated(opts) {
|
|
|
1706
2170
|
generateProgress(opts.featureName, opts.tickets),
|
|
1707
2171
|
readProgress(progressPath)
|
|
1708
2172
|
);
|
|
1709
|
-
|
|
1710
|
-
await commitProgress(
|
|
2173
|
+
await saveProgress(
|
|
1711
2174
|
repoRoot,
|
|
1712
2175
|
progressPath,
|
|
1713
|
-
|
|
2176
|
+
progress,
|
|
2177
|
+
`nightralph: start ${opts.featureName}`,
|
|
2178
|
+
d
|
|
1714
2179
|
);
|
|
2180
|
+
if (!opts.testCmd) {
|
|
2181
|
+
d.log(" Test gate disabled: no test command");
|
|
2182
|
+
}
|
|
1715
2183
|
let totalCompleted = 0;
|
|
1716
2184
|
let waveNum = 0;
|
|
2185
|
+
const failedNums = /* @__PURE__ */ new Set();
|
|
1717
2186
|
while (true) {
|
|
1718
|
-
const ready = findReadyTickets(opts.tickets)
|
|
2187
|
+
const ready = findReadyTickets(opts.tickets).filter(
|
|
2188
|
+
(t) => !failedNums.has(t.num)
|
|
2189
|
+
);
|
|
1719
2190
|
if (ready.length === 0) break;
|
|
1720
2191
|
waveNum++;
|
|
1721
2192
|
const readyNums = new Set(ready.map((t) => t.num));
|
|
@@ -1735,125 +2206,89 @@ async function runWavesIsolated(opts) {
|
|
|
1735
2206
|
filename: t.filename
|
|
1736
2207
|
}))
|
|
1737
2208
|
);
|
|
1738
|
-
const
|
|
1739
|
-
ready.map((t) =>
|
|
2209
|
+
const outcomes = await Promise.all(
|
|
2210
|
+
ready.map((t) => runTicketAttempts({
|
|
2211
|
+
ticket: t,
|
|
1740
2212
|
repoRoot,
|
|
1741
2213
|
repoName,
|
|
1742
2214
|
baseBranch,
|
|
1743
|
-
|
|
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
|
|
1744
2226
|
}))
|
|
1745
2227
|
);
|
|
1746
|
-
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
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];
|
|
1750
2234
|
const ticket = ready[i];
|
|
1751
|
-
if (
|
|
2235
|
+
if (outcome.kind === "worktree-error") {
|
|
2236
|
+
failedNums.add(ticket.num);
|
|
1752
2237
|
d.setTicketStatus(ticket.num, "failed");
|
|
1753
2238
|
d.log(
|
|
1754
|
-
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(
|
|
2239
|
+
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(outcome.error)}`
|
|
1755
2240
|
);
|
|
1756
2241
|
continue;
|
|
1757
2242
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
const prompt = renderPrompt(opts.specBody, t);
|
|
1764
|
-
const logPath = join3(
|
|
1765
|
-
opts.logsDir,
|
|
1766
|
-
t.filename.replace(/\.md$/, ".log")
|
|
1767
|
-
);
|
|
1768
|
-
const label = t.filename.replace(
|
|
1769
|
-
/\.md$/,
|
|
1770
|
-
""
|
|
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)
|
|
1771
2248
|
);
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
agentCmd: opts.agentCmd,
|
|
1777
|
-
model: opts.model,
|
|
1778
|
-
timeout: opts.timeout,
|
|
1779
|
-
logPath,
|
|
1780
|
-
cwd: worktrees[i].worktreePath,
|
|
1781
|
-
label,
|
|
1782
|
-
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1783
|
-
});
|
|
1784
|
-
})
|
|
1785
|
-
);
|
|
1786
|
-
let waveCompleted = 0;
|
|
1787
|
-
const successfulBranches = [];
|
|
1788
|
-
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
1789
|
-
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
1790
|
-
for (let i = 0; i < results.length; i++) {
|
|
1791
|
-
const r = results[i];
|
|
1792
|
-
if (r.status === "rejected") continue;
|
|
1793
|
-
const agentResult = r.value;
|
|
1794
|
-
const ticket = activeTickets[i];
|
|
1795
|
-
const worktree = worktrees[i];
|
|
1796
|
-
if (agentResult.exitCode !== 0) {
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
if (outcome.kind === "no-commits") {
|
|
2252
|
+
failedNums.add(ticket.num);
|
|
1797
2253
|
d.setTicketStatus(ticket.num, "failed");
|
|
1798
2254
|
d.log(
|
|
1799
|
-
` ${ticket.filename}: agent exited
|
|
2255
|
+
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
1800
2256
|
);
|
|
1801
|
-
|
|
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
|
+
}
|
|
1802
2265
|
checkTicket(progress, ticket.num, {
|
|
1803
|
-
branch:
|
|
1804
|
-
exitCode:
|
|
2266
|
+
branch: outcome.branchName,
|
|
2267
|
+
exitCode: outcome.exitCode,
|
|
1805
2268
|
done: false
|
|
1806
2269
|
});
|
|
1807
|
-
|
|
1808
|
-
await commitProgress(
|
|
2270
|
+
await saveProgress(
|
|
1809
2271
|
repoRoot,
|
|
1810
2272
|
progressPath,
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
await removeWorktree(worktree.worktreePath);
|
|
1815
|
-
} catch (err) {
|
|
1816
|
-
d.log(
|
|
1817
|
-
` Failed to remove worktree ${worktree.worktreePath}: ` + formatErrorMessage(err)
|
|
1818
|
-
);
|
|
1819
|
-
}
|
|
1820
|
-
continue;
|
|
1821
|
-
}
|
|
1822
|
-
if (isDirty(worktree.worktreePath)) {
|
|
1823
|
-
const committed = await commitDirty(
|
|
1824
|
-
worktree.worktreePath,
|
|
1825
|
-
`nightralph: auto-commit ${ticket.filename} remaining changes`
|
|
1826
|
-
);
|
|
1827
|
-
if (committed) {
|
|
1828
|
-
d.log(
|
|
1829
|
-
` ${ticket.filename}: auto-committed remaining changes`
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
if (!hasNewCommits(
|
|
1834
|
-
repoRoot,
|
|
1835
|
-
baseBranch,
|
|
1836
|
-
worktree.branchName
|
|
1837
|
-
)) {
|
|
1838
|
-
d.setTicketStatus(ticket.num, "failed");
|
|
1839
|
-
d.log(
|
|
1840
|
-
` ${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
|
|
1841
2276
|
);
|
|
1842
2277
|
continue;
|
|
1843
2278
|
}
|
|
1844
|
-
checkItems(ticket,
|
|
2279
|
+
checkItems(ticket, outcome.completedItems);
|
|
1845
2280
|
d.log(
|
|
1846
2281
|
` ${ticket.filename}: agent finished, awaiting merge`
|
|
1847
2282
|
);
|
|
1848
2283
|
ticketsByNum.set(ticket.num, ticket);
|
|
1849
2284
|
worktreePathsByNum.set(
|
|
1850
2285
|
ticket.num,
|
|
1851
|
-
|
|
2286
|
+
outcome.worktreePath
|
|
1852
2287
|
);
|
|
1853
2288
|
successfulBranches.push({
|
|
1854
|
-
branch:
|
|
2289
|
+
branch: outcome.branchName,
|
|
1855
2290
|
ticketNum: ticket.num,
|
|
1856
|
-
worktreePath:
|
|
2291
|
+
worktreePath: outcome.worktreePath
|
|
1857
2292
|
});
|
|
1858
2293
|
}
|
|
1859
2294
|
if (successfulBranches.length > 0) {
|
|
@@ -1880,23 +2315,21 @@ async function runWavesIsolated(opts) {
|
|
|
1880
2315
|
branch: branchResult.branch,
|
|
1881
2316
|
exitCode: 0
|
|
1882
2317
|
});
|
|
1883
|
-
|
|
1884
|
-
await commitProgress(
|
|
2318
|
+
await saveProgress(
|
|
1885
2319
|
repoRoot,
|
|
1886
2320
|
progressPath,
|
|
1887
|
-
|
|
2321
|
+
progress,
|
|
2322
|
+
`nightralph: ${ticket.filename} done`,
|
|
2323
|
+
d
|
|
1888
2324
|
);
|
|
1889
2325
|
const worktreePath = worktreePathsByNum.get(
|
|
1890
2326
|
ticket.num
|
|
1891
2327
|
);
|
|
1892
2328
|
if (worktreePath) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
2329
|
+
await removeWorktreeQuietly(
|
|
2330
|
+
worktreePath,
|
|
2331
|
+
d
|
|
2332
|
+
);
|
|
1900
2333
|
}
|
|
1901
2334
|
} else {
|
|
1902
2335
|
const worktreePath = worktreePathsByNum.get(
|
|
@@ -1913,6 +2346,8 @@ async function runWavesIsolated(opts) {
|
|
|
1913
2346
|
model: opts.model,
|
|
1914
2347
|
timeout: opts.timeout,
|
|
1915
2348
|
logsDir: opts.logsDir,
|
|
2349
|
+
testCmd: opts.testCmd,
|
|
2350
|
+
maxTurns: opts.maxTurns,
|
|
1916
2351
|
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine")
|
|
1917
2352
|
}) : false;
|
|
1918
2353
|
if (recovered) {
|
|
@@ -1927,15 +2362,19 @@ async function runWavesIsolated(opts) {
|
|
|
1927
2362
|
branch: branchResult.branch,
|
|
1928
2363
|
exitCode: 0
|
|
1929
2364
|
});
|
|
1930
|
-
|
|
1931
|
-
await commitProgress(
|
|
2365
|
+
await saveProgress(
|
|
1932
2366
|
repoRoot,
|
|
1933
2367
|
progressPath,
|
|
1934
|
-
|
|
2368
|
+
progress,
|
|
2369
|
+
`nightralph: ${ticket.filename} done (respawn)`,
|
|
2370
|
+
d
|
|
1935
2371
|
);
|
|
1936
2372
|
if (worktreePath) {
|
|
1937
2373
|
try {
|
|
1938
|
-
await removeWorktree(
|
|
2374
|
+
await removeWorktree(
|
|
2375
|
+
worktreePath,
|
|
2376
|
+
{ force: true }
|
|
2377
|
+
);
|
|
1939
2378
|
} catch (err) {
|
|
1940
2379
|
d.log(
|
|
1941
2380
|
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
@@ -1956,11 +2395,12 @@ async function runWavesIsolated(opts) {
|
|
|
1956
2395
|
exitCode: 0,
|
|
1957
2396
|
done: false
|
|
1958
2397
|
});
|
|
1959
|
-
|
|
1960
|
-
await commitProgress(
|
|
2398
|
+
await saveProgress(
|
|
1961
2399
|
repoRoot,
|
|
1962
2400
|
progressPath,
|
|
1963
|
-
|
|
2401
|
+
progress,
|
|
2402
|
+
`nightralph: ${ticket.filename} merge failed`,
|
|
2403
|
+
d
|
|
1964
2404
|
);
|
|
1965
2405
|
}
|
|
1966
2406
|
}
|
|
@@ -1974,7 +2414,10 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1974
2414
|
const ticket = ticketsByNum.get(sb.ticketNum);
|
|
1975
2415
|
if (ticket && ticket.status === "done") continue;
|
|
1976
2416
|
try {
|
|
1977
|
-
await removeWorktree(
|
|
2417
|
+
await removeWorktree(
|
|
2418
|
+
sb.worktreePath,
|
|
2419
|
+
{ force: true }
|
|
2420
|
+
);
|
|
1978
2421
|
} catch (err) {
|
|
1979
2422
|
d.log(
|
|
1980
2423
|
` Failed to remove worktree ${sb.worktreePath}: ` + formatErrorMessage(err)
|
|
@@ -1996,7 +2439,7 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1996
2439
|
);
|
|
1997
2440
|
d.log(
|
|
1998
2441
|
`
|
|
1999
|
-
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` : "")
|
|
2000
2443
|
);
|
|
2001
2444
|
d.cleanup();
|
|
2002
2445
|
return { allDone: remaining.length === 0 };
|
|
@@ -2004,8 +2447,8 @@ Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.
|
|
|
2004
2447
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
2005
2448
|
|
|
2006
2449
|
// src/resolve.ts
|
|
2007
|
-
import { existsSync as
|
|
2008
|
-
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";
|
|
2009
2452
|
import { fileURLToPath } from "node:url";
|
|
2010
2453
|
import { select } from "@inquirer/prompts";
|
|
2011
2454
|
function getScriptDir(metaUrl) {
|
|
@@ -2013,8 +2456,8 @@ function getScriptDir(metaUrl) {
|
|
|
2013
2456
|
}
|
|
2014
2457
|
__name(getScriptDir, "getScriptDir");
|
|
2015
2458
|
function findFeatureDirs(cwd) {
|
|
2016
|
-
const scratchDir =
|
|
2017
|
-
if (!
|
|
2459
|
+
const scratchDir = join5(cwd, ".scratch");
|
|
2460
|
+
if (!existsSync4(scratchDir)) return [];
|
|
2018
2461
|
const entries = readdirSync3(
|
|
2019
2462
|
scratchDir,
|
|
2020
2463
|
{ withFileTypes: true }
|
|
@@ -2022,17 +2465,17 @@ function findFeatureDirs(cwd) {
|
|
|
2022
2465
|
const results = [];
|
|
2023
2466
|
for (const entry of entries) {
|
|
2024
2467
|
if (!entry.isDirectory()) continue;
|
|
2025
|
-
const issuesDir =
|
|
2468
|
+
const issuesDir = join5(
|
|
2026
2469
|
scratchDir,
|
|
2027
2470
|
entry.name,
|
|
2028
2471
|
"issues"
|
|
2029
2472
|
);
|
|
2030
|
-
const specFile =
|
|
2473
|
+
const specFile = join5(
|
|
2031
2474
|
scratchDir,
|
|
2032
2475
|
entry.name,
|
|
2033
2476
|
"spec.md"
|
|
2034
2477
|
);
|
|
2035
|
-
if (
|
|
2478
|
+
if (existsSync4(issuesDir)) {
|
|
2036
2479
|
results.push({
|
|
2037
2480
|
name: entry.name,
|
|
2038
2481
|
dir: issuesDir,
|
|
@@ -2045,10 +2488,10 @@ function findFeatureDirs(cwd) {
|
|
|
2045
2488
|
__name(findFeatureDirs, "findFeatureDirs");
|
|
2046
2489
|
async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
2047
2490
|
if (specName) {
|
|
2048
|
-
const featureDir =
|
|
2491
|
+
const featureDir = join5(cwd, ".scratch", specName);
|
|
2049
2492
|
return {
|
|
2050
|
-
dir:
|
|
2051
|
-
spec:
|
|
2493
|
+
dir: join5(featureDir, "issues"),
|
|
2494
|
+
spec: join5(featureDir, "spec.md")
|
|
2052
2495
|
};
|
|
2053
2496
|
}
|
|
2054
2497
|
const features = findFeatureDirs(cwd);
|
|
@@ -2111,8 +2554,8 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2111
2554
|
}
|
|
2112
2555
|
}
|
|
2113
2556
|
setup({
|
|
2114
|
-
bundledSkillsDir:
|
|
2115
|
-
docsTemplatesDir:
|
|
2557
|
+
bundledSkillsDir: join6(scriptDir, "skills"),
|
|
2558
|
+
docsTemplatesDir: join6(
|
|
2116
2559
|
scriptDir,
|
|
2117
2560
|
"docs-templates"
|
|
2118
2561
|
),
|
|
@@ -2142,11 +2585,29 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2142
2585
|
}).option("timeout", {
|
|
2143
2586
|
type: "number",
|
|
2144
2587
|
describe: "Kill agent after N seconds",
|
|
2145
|
-
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.'
|
|
2146
2592
|
}).option("X", {
|
|
2147
2593
|
type: "boolean",
|
|
2148
2594
|
describe: "Stop on merge conflict instead of re-spawning",
|
|
2149
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."
|
|
2150
2611
|
}),
|
|
2151
2612
|
async (argv) => {
|
|
2152
2613
|
await runExecute({
|
|
@@ -2155,7 +2616,12 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2155
2616
|
spec: argv.spec,
|
|
2156
2617
|
dryRun: argv.dryRun,
|
|
2157
2618
|
timeout: argv.timeout,
|
|
2158
|
-
|
|
2619
|
+
testCmd: argv.testCmd,
|
|
2620
|
+
stopOnConflict: argv.X,
|
|
2621
|
+
maxTurns: argv.maxTurns,
|
|
2622
|
+
thinking: argv.thinking,
|
|
2623
|
+
retries: argv.retries,
|
|
2624
|
+
retryDelay: argv.retryDelay
|
|
2159
2625
|
});
|
|
2160
2626
|
}
|
|
2161
2627
|
).strict().demandCommand(1, "Specify a command or provider").help().parse();
|
|
@@ -2183,15 +2649,18 @@ async function runExecute(argv) {
|
|
|
2183
2649
|
const { dir, spec } = await resolveIssuesDir(
|
|
2184
2650
|
argv.spec
|
|
2185
2651
|
);
|
|
2186
|
-
if (!
|
|
2652
|
+
if (!existsSync5(dir)) {
|
|
2187
2653
|
console.error(`Directory not found: ${dir}`);
|
|
2188
2654
|
process.exit(2);
|
|
2189
2655
|
}
|
|
2190
|
-
if (!
|
|
2656
|
+
if (!existsSync5(spec)) {
|
|
2191
2657
|
console.error(`Spec file not found: ${spec}`);
|
|
2192
2658
|
process.exit(2);
|
|
2193
2659
|
}
|
|
2194
|
-
const specBody =
|
|
2660
|
+
const specBody = stripSpecSection(
|
|
2661
|
+
readFileSync5(spec, "utf8"),
|
|
2662
|
+
"User Stories"
|
|
2663
|
+
);
|
|
2195
2664
|
const tickets = scanTickets(dir);
|
|
2196
2665
|
if (tickets.length === 0) {
|
|
2197
2666
|
console.log("No ticket files found. Exiting.");
|
|
@@ -2212,11 +2681,14 @@ async function runExecute(argv) {
|
|
|
2212
2681
|
(b) => knownNums.has(b)
|
|
2213
2682
|
);
|
|
2214
2683
|
}
|
|
2215
|
-
const logsDir =
|
|
2216
|
-
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;
|
|
2217
2689
|
const featureName = extractFeatureName(spec);
|
|
2218
2690
|
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
2219
|
-
const progressPath =
|
|
2691
|
+
const progressPath = join6(
|
|
2220
2692
|
dirname3(dir),
|
|
2221
2693
|
"progress.md"
|
|
2222
2694
|
);
|
|
@@ -2228,7 +2700,11 @@ async function runExecute(argv) {
|
|
|
2228
2700
|
specBody,
|
|
2229
2701
|
repoName,
|
|
2230
2702
|
conflictStrategy,
|
|
2231
|
-
progressPath
|
|
2703
|
+
progressPath,
|
|
2704
|
+
testCmd,
|
|
2705
|
+
thinking: argv.thinking,
|
|
2706
|
+
retries: argv.retries,
|
|
2707
|
+
retryDelay: argv.retryDelay
|
|
2232
2708
|
});
|
|
2233
2709
|
} else {
|
|
2234
2710
|
dryRun(tickets, specBody);
|
|
@@ -2236,6 +2712,14 @@ async function runExecute(argv) {
|
|
|
2236
2712
|
return;
|
|
2237
2713
|
}
|
|
2238
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));
|
|
2239
2723
|
let result;
|
|
2240
2724
|
if (isGitRepo()) {
|
|
2241
2725
|
result = await runWavesIsolated({
|
|
@@ -2247,7 +2731,12 @@ async function runExecute(argv) {
|
|
|
2247
2731
|
logsDir,
|
|
2248
2732
|
featureName,
|
|
2249
2733
|
conflictStrategy,
|
|
2250
|
-
display
|
|
2734
|
+
display,
|
|
2735
|
+
testCmd,
|
|
2736
|
+
maxTurns: argv.maxTurns,
|
|
2737
|
+
thinking: argv.thinking,
|
|
2738
|
+
retries: argv.retries,
|
|
2739
|
+
retryDelay: argv.retryDelay
|
|
2251
2740
|
});
|
|
2252
2741
|
} else {
|
|
2253
2742
|
result = await runWaves(
|
|
@@ -2257,7 +2746,11 @@ async function runExecute(argv) {
|
|
|
2257
2746
|
argv.model ?? null,
|
|
2258
2747
|
timeout,
|
|
2259
2748
|
logsDir,
|
|
2260
|
-
display
|
|
2749
|
+
display,
|
|
2750
|
+
argv.maxTurns,
|
|
2751
|
+
argv.thinking,
|
|
2752
|
+
argv.retries,
|
|
2753
|
+
argv.retryDelay
|
|
2261
2754
|
);
|
|
2262
2755
|
}
|
|
2263
2756
|
if (!result.allDone) {
|