nightralph 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -10
- package/dist/display.js +0 -12
- package/dist/display.js.map +2 -2
- package/dist/docs-templates/issue-tracker-github.md +15 -85
- package/dist/docs-templates/issue-tracker.md +16 -10
- package/dist/index.js +679 -190
- package/dist/index.js.map +4 -4
- package/dist/meta.json +66 -12
- package/dist/orchestrator.js +517 -142
- package/dist/orchestrator.js.map +3 -3
- package/dist/progress.js +10 -0
- package/dist/progress.js.map +2 -2
- package/dist/skills/to-spec/SKILL.md +1 -1
- package/dist/skills/to-tickets/SKILL.md +2 -2
- package/dist/src/display.d.ts +0 -1
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +25 -2
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/testcmd.d.ts +11 -0
- package/dist/src/testcmd.d.ts.map +1 -0
- package/dist/src/worktree.d.ts +3 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/testcmd.js +67 -0
- package/dist/testcmd.js.map +7 -0
- package/dist/worktree.js +19 -6
- package/dist/worktree.js.map +2 -2
- package/package.json +3 -3
package/dist/orchestrator.js
CHANGED
|
@@ -38,10 +38,21 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
createDisplay
|
|
40
40
|
} from "./display.js";
|
|
41
|
+
import { runTestCmd } from "./testcmd.js";
|
|
41
42
|
function formatErrorMessage(error) {
|
|
42
43
|
return error instanceof Error ? error.message : String(error);
|
|
43
44
|
}
|
|
44
45
|
__name(formatErrorMessage, "formatErrorMessage");
|
|
46
|
+
async function removeWorktreeQuietly(worktreePath, d) {
|
|
47
|
+
try {
|
|
48
|
+
await removeWorktree(worktreePath, { force: true });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
d.log(
|
|
51
|
+
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
__name(removeWorktreeQuietly, "removeWorktreeQuietly");
|
|
45
56
|
const TICKET_RE = /^(\d+)-(.+)\.md$/;
|
|
46
57
|
function parseTicketFilename(filename) {
|
|
47
58
|
const m = filename.match(TICKET_RE);
|
|
@@ -132,7 +143,39 @@ function markDone(ticket) {
|
|
|
132
143
|
ticket.body = updated;
|
|
133
144
|
}
|
|
134
145
|
__name(markDone, "markDone");
|
|
135
|
-
function
|
|
146
|
+
function stripSpecSection(specBody, heading) {
|
|
147
|
+
const out = [];
|
|
148
|
+
let skipping = false;
|
|
149
|
+
let skipLevel = 0;
|
|
150
|
+
let inFence = false;
|
|
151
|
+
for (const line of specBody.split("\n")) {
|
|
152
|
+
if (/^\s*```/.test(line)) {
|
|
153
|
+
inFence = !inFence;
|
|
154
|
+
}
|
|
155
|
+
if (!inFence) {
|
|
156
|
+
const m = /^(#{1,6})\s+(.*?)\s*$/.exec(line);
|
|
157
|
+
if (m) {
|
|
158
|
+
const level = m[1].length;
|
|
159
|
+
if (skipping && level <= skipLevel) {
|
|
160
|
+
skipping = false;
|
|
161
|
+
}
|
|
162
|
+
if (!skipping && m[2] === heading) {
|
|
163
|
+
skipping = true;
|
|
164
|
+
skipLevel = level;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!skipping) out.push(line);
|
|
170
|
+
}
|
|
171
|
+
return out.join("\n");
|
|
172
|
+
}
|
|
173
|
+
__name(stripSpecSection, "stripSpecSection");
|
|
174
|
+
function renderPrompt(specBody, ticket, opts = {}) {
|
|
175
|
+
const testRules = opts.testCmd ? [
|
|
176
|
+
"- 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.",
|
|
177
|
+
`- 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.`
|
|
178
|
+
] : [];
|
|
136
179
|
return [
|
|
137
180
|
"---- SPEC CONTEXT ----",
|
|
138
181
|
specBody,
|
|
@@ -144,10 +187,12 @@ function renderPrompt(specBody, ticket) {
|
|
|
144
187
|
"---- AGENT INSTRUCTIONS ----",
|
|
145
188
|
"You are an autonomous coding agent. Implement the ticket.",
|
|
146
189
|
"",
|
|
147
|
-
"
|
|
148
|
-
"-
|
|
190
|
+
"Follow these rules:",
|
|
191
|
+
"- Read the files directly relevant to the ticket before writing code.",
|
|
149
192
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
150
|
-
|
|
193
|
+
...testRules,
|
|
194
|
+
"- Do NOT read or modify files outside your current directory (the worktree).",
|
|
195
|
+
"- Do NOT run git push.",
|
|
151
196
|
"- Commit your work when finished.",
|
|
152
197
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
153
198
|
""
|
|
@@ -173,7 +218,8 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
173
218
|
"2. Make sure the code compiles and tests pass.",
|
|
174
219
|
"3. Commit the resolved files.",
|
|
175
220
|
"",
|
|
176
|
-
"Do NOT re-implement the ticket from scratch."
|
|
221
|
+
"Do NOT re-implement the ticket from scratch.",
|
|
222
|
+
"Do NOT run git push."
|
|
177
223
|
].join("\n");
|
|
178
224
|
}
|
|
179
225
|
__name(renderMergePrompt, "renderMergePrompt");
|
|
@@ -252,7 +298,11 @@ const PROVIDER_ARGS = {
|
|
|
252
298
|
"stream-json",
|
|
253
299
|
"--dangerously-skip-permissions"
|
|
254
300
|
],
|
|
255
|
-
codex
|
|
301
|
+
// codex removed --full-auto; this is the counterpart
|
|
302
|
+
// of claude's --dangerously-skip-permissions, and the
|
|
303
|
+
// workspace-write sandbox would block commits because
|
|
304
|
+
// a worktree's .git link points outside the tree
|
|
305
|
+
codex: ["exec", "--dangerously-bypass-approvals-and-sandbox"],
|
|
256
306
|
pi: [
|
|
257
307
|
"-p",
|
|
258
308
|
"--verbose",
|
|
@@ -267,6 +317,21 @@ function getProviderArgs(cmd) {
|
|
|
267
317
|
return PROVIDER_ARGS[name] ?? [];
|
|
268
318
|
}
|
|
269
319
|
__name(getProviderArgs, "getProviderArgs");
|
|
320
|
+
const THINKING_LEVELS = [
|
|
321
|
+
"off",
|
|
322
|
+
"minimal",
|
|
323
|
+
"low",
|
|
324
|
+
"medium",
|
|
325
|
+
"high",
|
|
326
|
+
"xhigh",
|
|
327
|
+
"max"
|
|
328
|
+
];
|
|
329
|
+
function escalateThinking(level) {
|
|
330
|
+
if (!level) return "high";
|
|
331
|
+
const i = THINKING_LEVELS.indexOf(level);
|
|
332
|
+
return THINKING_LEVELS[Math.min(i + 1, THINKING_LEVELS.length - 1)];
|
|
333
|
+
}
|
|
334
|
+
__name(escalateThinking, "escalateThinking");
|
|
270
335
|
function formatStreamLine(line) {
|
|
271
336
|
let obj;
|
|
272
337
|
try {
|
|
@@ -318,7 +383,7 @@ function createPiStreamFormatter() {
|
|
|
318
383
|
return [out];
|
|
319
384
|
}
|
|
320
385
|
__name(flushBuf, "flushBuf");
|
|
321
|
-
function
|
|
386
|
+
function process2(line) {
|
|
322
387
|
let obj;
|
|
323
388
|
try {
|
|
324
389
|
obj = JSON.parse(line);
|
|
@@ -361,8 +426,8 @@ function createPiStreamFormatter() {
|
|
|
361
426
|
}
|
|
362
427
|
return [];
|
|
363
428
|
}
|
|
364
|
-
__name(
|
|
365
|
-
return { process, flush: flushBuf };
|
|
429
|
+
__name(process2, "process");
|
|
430
|
+
return { process: process2, flush: flushBuf };
|
|
366
431
|
}
|
|
367
432
|
__name(createPiStreamFormatter, "createPiStreamFormatter");
|
|
368
433
|
function writePromptFile(prompt) {
|
|
@@ -379,6 +444,22 @@ function cleanupPromptFile(filePath) {
|
|
|
379
444
|
}
|
|
380
445
|
}
|
|
381
446
|
__name(cleanupPromptFile, "cleanupPromptFile");
|
|
447
|
+
const CLOSE_GRACE_MS = 2e3;
|
|
448
|
+
function killProcessGroup(pid) {
|
|
449
|
+
if (pid === void 0) return;
|
|
450
|
+
try {
|
|
451
|
+
process.kill(-pid, "SIGKILL");
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
__name(killProcessGroup, "killProcessGroup");
|
|
456
|
+
const runningAgents = /* @__PURE__ */ new Set();
|
|
457
|
+
function killRunningAgents() {
|
|
458
|
+
for (const pid of runningAgents) {
|
|
459
|
+
killProcessGroup(pid);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
__name(killRunningAgents, "killRunningAgents");
|
|
382
463
|
function spawnAgent(opts) {
|
|
383
464
|
const args = [
|
|
384
465
|
...getProviderArgs(opts.agentCmd)
|
|
@@ -386,9 +467,15 @@ function spawnAgent(opts) {
|
|
|
386
467
|
if (opts.model) {
|
|
387
468
|
args.push("--model", opts.model);
|
|
388
469
|
}
|
|
470
|
+
if (opts.maxTurns) {
|
|
471
|
+
args.push("--max-turns", String(opts.maxTurns));
|
|
472
|
+
}
|
|
389
473
|
const providerName = basename(opts.agentCmd);
|
|
390
474
|
const isClaude = providerName === "claude";
|
|
391
475
|
const isPi = providerName === "pi";
|
|
476
|
+
if (isPi && opts.thinking) {
|
|
477
|
+
args.push("--thinking", opts.thinking);
|
|
478
|
+
}
|
|
392
479
|
const prefix = opts.label ? ` [${opts.label}] ` : " ";
|
|
393
480
|
let promptFile = null;
|
|
394
481
|
if (isPi) {
|
|
@@ -401,8 +488,10 @@ function spawnAgent(opts) {
|
|
|
401
488
|
);
|
|
402
489
|
const proc = spawn(opts.agentCmd, args, {
|
|
403
490
|
stdio: ["pipe", "pipe", "pipe"],
|
|
404
|
-
cwd: opts.cwd
|
|
491
|
+
cwd: opts.cwd,
|
|
492
|
+
detached: true
|
|
405
493
|
});
|
|
494
|
+
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
406
495
|
proc.stdout.pipe(logStream);
|
|
407
496
|
const piFormatter = isPi ? createPiStreamFormatter() : null;
|
|
408
497
|
const completedItems = [];
|
|
@@ -456,21 +545,41 @@ function spawnAgent(opts) {
|
|
|
456
545
|
console.error(output);
|
|
457
546
|
}
|
|
458
547
|
});
|
|
548
|
+
let timedOut = false;
|
|
459
549
|
const timeout = setTimeout(() => {
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
550
|
+
if (proc.killed) return;
|
|
551
|
+
timedOut = true;
|
|
552
|
+
const msg = `agent timed out after ${opts.timeout}s; killing it`;
|
|
553
|
+
logStream.write(`nightralph: ${msg}
|
|
554
|
+
`);
|
|
555
|
+
if (opts.onLine) {
|
|
556
|
+
opts.onLine(`${prefix}${msg}`);
|
|
557
|
+
} else {
|
|
558
|
+
console.warn(`${prefix}${msg}`);
|
|
465
559
|
}
|
|
560
|
+
killProcessGroup(proc.pid);
|
|
466
561
|
}, opts.timeout * 1e3);
|
|
562
|
+
proc.stdin.on("error", (err) => {
|
|
563
|
+
logStream.write(
|
|
564
|
+
`nightralph: stdin write failed: ${formatErrorMessage(err)}
|
|
565
|
+
`
|
|
566
|
+
);
|
|
567
|
+
});
|
|
467
568
|
if (!promptFile) {
|
|
468
569
|
proc.stdin.write(opts.prompt);
|
|
469
570
|
}
|
|
470
571
|
proc.stdin.end();
|
|
471
572
|
return new Promise((resolve) => {
|
|
472
|
-
|
|
573
|
+
let settled = false;
|
|
574
|
+
let graceTimer;
|
|
575
|
+
function finish(exitCode) {
|
|
576
|
+
if (settled) return;
|
|
577
|
+
settled = true;
|
|
473
578
|
clearTimeout(timeout);
|
|
579
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
580
|
+
if (proc.pid !== void 0) {
|
|
581
|
+
runningAgents.delete(proc.pid);
|
|
582
|
+
}
|
|
474
583
|
if (piFormatter) {
|
|
475
584
|
for (const line of piFormatter.flush()) {
|
|
476
585
|
collectCompleted(line);
|
|
@@ -486,13 +595,27 @@ function spawnAgent(opts) {
|
|
|
486
595
|
rlErr.close();
|
|
487
596
|
logStream.end();
|
|
488
597
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
489
|
-
resolve({
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
598
|
+
resolve({ exitCode, completedItems, timedOut });
|
|
599
|
+
}
|
|
600
|
+
__name(finish, "finish");
|
|
601
|
+
proc.on("exit", (code) => {
|
|
602
|
+
graceTimer = setTimeout(() => {
|
|
603
|
+
proc.stdout.destroy();
|
|
604
|
+
proc.stderr.destroy();
|
|
605
|
+
finish(code ?? 1);
|
|
606
|
+
}, CLOSE_GRACE_MS);
|
|
607
|
+
});
|
|
608
|
+
proc.on("close", (code) => {
|
|
609
|
+
finish(code ?? 1);
|
|
493
610
|
});
|
|
494
611
|
proc.on("error", (err) => {
|
|
612
|
+
if (settled) return;
|
|
613
|
+
settled = true;
|
|
614
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
495
615
|
clearTimeout(timeout);
|
|
616
|
+
if (proc.pid !== void 0) {
|
|
617
|
+
runningAgents.delete(proc.pid);
|
|
618
|
+
}
|
|
496
619
|
rl.close();
|
|
497
620
|
rlErr.close();
|
|
498
621
|
logStream.end();
|
|
@@ -501,7 +624,11 @@ function spawnAgent(opts) {
|
|
|
501
624
|
"Agent process error:",
|
|
502
625
|
formatErrorMessage(err)
|
|
503
626
|
);
|
|
504
|
-
resolve({
|
|
627
|
+
resolve({
|
|
628
|
+
exitCode: 1,
|
|
629
|
+
completedItems,
|
|
630
|
+
timedOut
|
|
631
|
+
});
|
|
505
632
|
});
|
|
506
633
|
});
|
|
507
634
|
}
|
|
@@ -564,18 +691,65 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
564
691
|
`
|
|
565
692
|
);
|
|
566
693
|
console.log(
|
|
567
|
-
renderPrompt(
|
|
694
|
+
renderPrompt(
|
|
695
|
+
opts.specBody,
|
|
696
|
+
firstReady[0],
|
|
697
|
+
{ testCmd: opts.testCmd }
|
|
698
|
+
)
|
|
568
699
|
);
|
|
569
700
|
}
|
|
570
701
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
571
702
|
console.log(
|
|
572
703
|
`
|
|
704
|
+
Test command: ${opts.testCmd ?? "(none)"}`
|
|
705
|
+
);
|
|
706
|
+
console.log(
|
|
707
|
+
`Thinking: ${opts.thinking ?? "(provider default)"}`
|
|
708
|
+
);
|
|
709
|
+
console.log(
|
|
710
|
+
`Retries: ${opts.retries ?? DEFAULT_RETRIES}`
|
|
711
|
+
);
|
|
712
|
+
console.log(
|
|
713
|
+
`Retry delay: ${opts.retryDelay ?? DEFAULT_RETRY_DELAY}s`
|
|
714
|
+
);
|
|
715
|
+
console.log(
|
|
716
|
+
`
|
|
573
717
|
Merge strategy: ${strategyDesc}`
|
|
574
718
|
);
|
|
575
719
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
576
720
|
}
|
|
577
721
|
__name(dryRunIsolated, "dryRunIsolated");
|
|
722
|
+
async function runTestGate(opts) {
|
|
723
|
+
const result = await runTestCmd({
|
|
724
|
+
cmd: opts.testCmd,
|
|
725
|
+
cwd: opts.worktreePath,
|
|
726
|
+
timeout: opts.timeout
|
|
727
|
+
});
|
|
728
|
+
const testLogPath = attemptLogPath(
|
|
729
|
+
opts.logsDir,
|
|
730
|
+
opts.ticket,
|
|
731
|
+
opts.attempt ?? 1,
|
|
732
|
+
".test.log"
|
|
733
|
+
);
|
|
734
|
+
writeFileSync(testLogPath, result.output);
|
|
735
|
+
if (result.exitCode !== 0) {
|
|
736
|
+
opts.log(
|
|
737
|
+
` ${opts.ticket.filename}: tests failed (exit ${result.exitCode}); see ${testLogPath}`
|
|
738
|
+
);
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
opts.log(` ${opts.ticket.filename}: tests passed`);
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
__name(runTestGate, "runTestGate");
|
|
578
745
|
async function respawnAndRetryMerge(opts) {
|
|
746
|
+
const log = /* @__PURE__ */ __name((msg) => {
|
|
747
|
+
if (opts.onLine) {
|
|
748
|
+
opts.onLine(msg);
|
|
749
|
+
} else {
|
|
750
|
+
console.log(msg);
|
|
751
|
+
}
|
|
752
|
+
}, "log");
|
|
579
753
|
await updateWorktreeFromBase(
|
|
580
754
|
opts.worktreePath,
|
|
581
755
|
opts.baseBranch
|
|
@@ -606,6 +780,7 @@ async function respawnAndRetryMerge(opts) {
|
|
|
606
780
|
logPath,
|
|
607
781
|
cwd: opts.worktreePath,
|
|
608
782
|
label,
|
|
783
|
+
maxTurns: opts.maxTurns,
|
|
609
784
|
onLine: opts.onLine
|
|
610
785
|
});
|
|
611
786
|
if (result.exitCode !== 0) {
|
|
@@ -617,6 +792,22 @@ async function respawnAndRetryMerge(opts) {
|
|
|
617
792
|
}
|
|
618
793
|
return false;
|
|
619
794
|
}
|
|
795
|
+
if (opts.testCmd) {
|
|
796
|
+
const passed = await runTestGate({
|
|
797
|
+
testCmd: opts.testCmd,
|
|
798
|
+
ticket: opts.ticket,
|
|
799
|
+
worktreePath: opts.worktreePath,
|
|
800
|
+
timeout: opts.timeout,
|
|
801
|
+
logsDir: opts.logsDir,
|
|
802
|
+
log
|
|
803
|
+
});
|
|
804
|
+
if (!passed) {
|
|
805
|
+
log(
|
|
806
|
+
` ${opts.ticket.filename}: tests failed after conflict respawn; not merging`
|
|
807
|
+
);
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
620
811
|
const mergeResult = await mergeBranch(
|
|
621
812
|
opts.repoRoot,
|
|
622
813
|
opts.branch
|
|
@@ -640,6 +831,171 @@ async function respawnAndRetryMerge(opts) {
|
|
|
640
831
|
return mergeResult.success;
|
|
641
832
|
}
|
|
642
833
|
__name(respawnAndRetryMerge, "respawnAndRetryMerge");
|
|
834
|
+
const DEFAULT_RETRIES = 2;
|
|
835
|
+
const DEFAULT_RETRY_DELAY = 30;
|
|
836
|
+
function retryDelayFor(base, attempt) {
|
|
837
|
+
return base * 2 ** (attempt - 2);
|
|
838
|
+
}
|
|
839
|
+
__name(retryDelayFor, "retryDelayFor");
|
|
840
|
+
function sleep(ms) {
|
|
841
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
842
|
+
}
|
|
843
|
+
__name(sleep, "sleep");
|
|
844
|
+
function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
845
|
+
const stem = ticket.filename.replace(/\.md$/, "");
|
|
846
|
+
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
847
|
+
return join(logsDir, `${stem}${suffix}${ext}`);
|
|
848
|
+
}
|
|
849
|
+
__name(attemptLogPath, "attemptLogPath");
|
|
850
|
+
async function runTicketAttempt(opts) {
|
|
851
|
+
const { ticket, d } = opts;
|
|
852
|
+
let worktree;
|
|
853
|
+
try {
|
|
854
|
+
worktree = await createWorktree({
|
|
855
|
+
repoRoot: opts.repoRoot,
|
|
856
|
+
repoName: opts.repoName,
|
|
857
|
+
baseBranch: opts.baseBranch,
|
|
858
|
+
ticket
|
|
859
|
+
});
|
|
860
|
+
} catch (error) {
|
|
861
|
+
return { kind: "worktree-error", error };
|
|
862
|
+
}
|
|
863
|
+
d.setTicketStatus(ticket.num, "in-progress");
|
|
864
|
+
const agentResult = await spawnAgent({
|
|
865
|
+
prompt: renderPrompt(
|
|
866
|
+
opts.specBody,
|
|
867
|
+
ticket,
|
|
868
|
+
{ testCmd: opts.testCmd }
|
|
869
|
+
),
|
|
870
|
+
agentCmd: opts.agentCmd,
|
|
871
|
+
model: opts.model,
|
|
872
|
+
timeout: opts.timeout,
|
|
873
|
+
logPath: attemptLogPath(
|
|
874
|
+
opts.logsDir,
|
|
875
|
+
ticket,
|
|
876
|
+
opts.attempt,
|
|
877
|
+
".log"
|
|
878
|
+
),
|
|
879
|
+
cwd: worktree.worktreePath,
|
|
880
|
+
label: ticket.filename.replace(/\.md$/, ""),
|
|
881
|
+
maxTurns: opts.maxTurns,
|
|
882
|
+
thinking: opts.thinking,
|
|
883
|
+
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
884
|
+
});
|
|
885
|
+
if (agentResult.timedOut) {
|
|
886
|
+
d.log(
|
|
887
|
+
` ${ticket.filename}: timed out; keeping the work it committed`
|
|
888
|
+
);
|
|
889
|
+
} else if (agentResult.exitCode !== 0) {
|
|
890
|
+
d.log(
|
|
891
|
+
` ${ticket.filename}: agent exited ${agentResult.exitCode}`
|
|
892
|
+
);
|
|
893
|
+
await removeWorktreeQuietly(worktree.worktreePath, d);
|
|
894
|
+
return {
|
|
895
|
+
kind: "failed",
|
|
896
|
+
reason: "exit",
|
|
897
|
+
branchName: worktree.branchName,
|
|
898
|
+
exitCode: agentResult.exitCode,
|
|
899
|
+
completedItems: agentResult.completedItems
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (isDirty(worktree.worktreePath)) {
|
|
903
|
+
const committed = await commitDirty(
|
|
904
|
+
worktree.worktreePath,
|
|
905
|
+
`nightralph: auto-commit ${ticket.filename} remaining changes`
|
|
906
|
+
);
|
|
907
|
+
if (committed) {
|
|
908
|
+
d.log(
|
|
909
|
+
` ${ticket.filename}: auto-committed remaining changes`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (!hasNewCommits(
|
|
914
|
+
opts.repoRoot,
|
|
915
|
+
opts.baseBranch,
|
|
916
|
+
worktree.branchName
|
|
917
|
+
)) {
|
|
918
|
+
return { kind: "no-commits" };
|
|
919
|
+
}
|
|
920
|
+
if (opts.testCmd) {
|
|
921
|
+
const passed = await runTestGate({
|
|
922
|
+
testCmd: opts.testCmd,
|
|
923
|
+
ticket,
|
|
924
|
+
worktreePath: worktree.worktreePath,
|
|
925
|
+
timeout: opts.timeout,
|
|
926
|
+
logsDir: opts.logsDir,
|
|
927
|
+
attempt: opts.attempt,
|
|
928
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
929
|
+
});
|
|
930
|
+
if (!passed) {
|
|
931
|
+
await removeWorktreeQuietly(
|
|
932
|
+
worktree.worktreePath,
|
|
933
|
+
d
|
|
934
|
+
);
|
|
935
|
+
return {
|
|
936
|
+
kind: "failed",
|
|
937
|
+
reason: "tests",
|
|
938
|
+
branchName: worktree.branchName,
|
|
939
|
+
exitCode: 1,
|
|
940
|
+
completedItems: agentResult.completedItems
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
kind: "ready",
|
|
946
|
+
worktreePath: worktree.worktreePath,
|
|
947
|
+
branchName: worktree.branchName,
|
|
948
|
+
completedItems: agentResult.completedItems
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
__name(runTicketAttempt, "runTicketAttempt");
|
|
952
|
+
async function runTicketAttempts(opts) {
|
|
953
|
+
const { ticket, d } = opts;
|
|
954
|
+
const maxAttempts = opts.retries + 1;
|
|
955
|
+
const isPi = basename(opts.agentCmd) === "pi";
|
|
956
|
+
let thinking = opts.thinking;
|
|
957
|
+
let outcome = { kind: "no-commits" };
|
|
958
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
959
|
+
if (attempt === 1) {
|
|
960
|
+
d.log(` Starting ${ticket.filename}`);
|
|
961
|
+
} else {
|
|
962
|
+
thinking = escalateThinking(thinking);
|
|
963
|
+
const delay = retryDelayFor(opts.retryDelay, attempt);
|
|
964
|
+
d.log(
|
|
965
|
+
` ${ticket.filename}: retrying (attempt ${attempt}/${maxAttempts}` + (isPi ? `, thinking ${thinking}` : "") + (delay > 0 ? `, waiting ${delay}s` : "") + ")"
|
|
966
|
+
);
|
|
967
|
+
if (delay > 0) await sleep(delay * 1e3);
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
outcome = await runTicketAttempt({
|
|
971
|
+
...opts,
|
|
972
|
+
attempt,
|
|
973
|
+
thinking
|
|
974
|
+
});
|
|
975
|
+
} catch (error) {
|
|
976
|
+
d.log(
|
|
977
|
+
` ${ticket.filename}: attempt ${attempt} threw: ${formatErrorMessage(error)}`
|
|
978
|
+
);
|
|
979
|
+
return { kind: "error", error };
|
|
980
|
+
}
|
|
981
|
+
if (outcome.kind === "ready" || outcome.kind === "worktree-error") {
|
|
982
|
+
return outcome;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return outcome;
|
|
986
|
+
}
|
|
987
|
+
__name(runTicketAttempts, "runTicketAttempts");
|
|
988
|
+
async function saveProgress(repoRoot, progressPath, progress, message, d) {
|
|
989
|
+
writeProgress(progressPath, progress);
|
|
990
|
+
try {
|
|
991
|
+
await commitProgress(repoRoot, progressPath, message);
|
|
992
|
+
} catch (err) {
|
|
993
|
+
d.log(
|
|
994
|
+
" Failed to commit progress: " + formatErrorMessage(err)
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
__name(saveProgress, "saveProgress");
|
|
643
999
|
function runProgress(tickets) {
|
|
644
1000
|
return {
|
|
645
1001
|
completed: tickets.filter((t) => t.status === "done").length,
|
|
@@ -647,15 +1003,21 @@ function runProgress(tickets) {
|
|
|
647
1003
|
};
|
|
648
1004
|
}
|
|
649
1005
|
__name(runProgress, "runProgress");
|
|
650
|
-
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, display) {
|
|
1006
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, display, maxTurns, thinking, retries, retryDelay) {
|
|
651
1007
|
mkdirSync(logsDir, { recursive: true });
|
|
652
1008
|
const d = display ?? createDisplay();
|
|
1009
|
+
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
1010
|
+
const delayBase = retryDelay ?? 0;
|
|
1011
|
+
const isPi = basename(agentCmd) === "pi";
|
|
653
1012
|
let totalWaves = 0;
|
|
654
1013
|
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
655
1014
|
let totalCompleted = 0;
|
|
656
1015
|
let waveNum = 0;
|
|
1016
|
+
const failedNums = /* @__PURE__ */ new Set();
|
|
657
1017
|
while (true) {
|
|
658
|
-
const ready = findReadyTickets(tickets)
|
|
1018
|
+
const ready = findReadyTickets(tickets).filter(
|
|
1019
|
+
(t) => !failedNums.has(t.num)
|
|
1020
|
+
);
|
|
659
1021
|
if (ready.length === 0) break;
|
|
660
1022
|
waveNum++;
|
|
661
1023
|
const readyNums = new Set(ready.map((t) => t.num));
|
|
@@ -678,25 +1040,48 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
678
1040
|
const results = await Promise.allSettled(
|
|
679
1041
|
ready.map(async (t) => {
|
|
680
1042
|
const prompt = renderPrompt(specBody, t);
|
|
681
|
-
const logPath = join(
|
|
682
|
-
logsDir,
|
|
683
|
-
t.filename.replace(/\.md$/, ".log")
|
|
684
|
-
);
|
|
685
1043
|
const label = t.filename.replace(
|
|
686
1044
|
/\.md$/,
|
|
687
1045
|
""
|
|
688
1046
|
);
|
|
689
1047
|
d.setTicketStatus(t.num, "in-progress");
|
|
690
1048
|
d.log(` Starting ${t.filename}`);
|
|
691
|
-
|
|
1049
|
+
let level = thinking;
|
|
1050
|
+
let result = await spawnAgent({
|
|
692
1051
|
prompt,
|
|
693
1052
|
agentCmd,
|
|
694
1053
|
model,
|
|
695
1054
|
timeout,
|
|
696
|
-
logPath,
|
|
1055
|
+
logPath: attemptLogPath(logsDir, t, 1, ".log"),
|
|
697
1056
|
label,
|
|
1057
|
+
maxTurns,
|
|
1058
|
+
thinking: level,
|
|
698
1059
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
699
1060
|
});
|
|
1061
|
+
for (let attempt = 2; result.exitCode !== 0 && !result.timedOut && attempt <= maxAttempts; attempt++) {
|
|
1062
|
+
level = escalateThinking(level);
|
|
1063
|
+
const delay = retryDelayFor(delayBase, attempt);
|
|
1064
|
+
d.log(
|
|
1065
|
+
` ${t.filename}: agent exited ${result.exitCode}; retrying (attempt ${attempt}/${maxAttempts}` + (isPi ? `, thinking ${level}` : "") + (delay > 0 ? `, waiting ${delay}s` : "") + ")"
|
|
1066
|
+
);
|
|
1067
|
+
if (delay > 0) await sleep(delay * 1e3);
|
|
1068
|
+
result = await spawnAgent({
|
|
1069
|
+
prompt,
|
|
1070
|
+
agentCmd,
|
|
1071
|
+
model,
|
|
1072
|
+
timeout,
|
|
1073
|
+
logPath: attemptLogPath(
|
|
1074
|
+
logsDir,
|
|
1075
|
+
t,
|
|
1076
|
+
attempt,
|
|
1077
|
+
".log"
|
|
1078
|
+
),
|
|
1079
|
+
label,
|
|
1080
|
+
maxTurns,
|
|
1081
|
+
thinking: level,
|
|
1082
|
+
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
700
1085
|
return { ticket: t, result };
|
|
701
1086
|
})
|
|
702
1087
|
);
|
|
@@ -705,6 +1090,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
705
1090
|
if (r.status === "rejected") continue;
|
|
706
1091
|
const { ticket, result } = r.value;
|
|
707
1092
|
if (result.exitCode !== 0) {
|
|
1093
|
+
failedNums.add(ticket.num);
|
|
708
1094
|
d.setTicketStatus(ticket.num, "failed");
|
|
709
1095
|
d.log(
|
|
710
1096
|
` ${ticket.filename}: agent exited ${result.exitCode}`
|
|
@@ -732,7 +1118,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
|
|
|
732
1118
|
);
|
|
733
1119
|
d.log(
|
|
734
1120
|
`
|
|
735
|
-
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
1121
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedNums.size > 0 ? `, ${failedNums.size} failed` : "")
|
|
736
1122
|
);
|
|
737
1123
|
d.cleanup();
|
|
738
1124
|
return { allDone: remaining.length === 0 };
|
|
@@ -741,6 +1127,8 @@ __name(runWaves, "runWaves");
|
|
|
741
1127
|
async function runWavesIsolated(opts) {
|
|
742
1128
|
mkdirSync(opts.logsDir, { recursive: true });
|
|
743
1129
|
const d = opts.display ?? createDisplay();
|
|
1130
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
1131
|
+
const retryDelay = opts.retryDelay ?? 0;
|
|
744
1132
|
let totalWaves = 0;
|
|
745
1133
|
for (const _ of simulateWaves(opts.tickets)) totalWaves++;
|
|
746
1134
|
const repoRoot = getRepoRoot();
|
|
@@ -759,16 +1147,23 @@ async function runWavesIsolated(opts) {
|
|
|
759
1147
|
generateProgress(opts.featureName, opts.tickets),
|
|
760
1148
|
readProgress(progressPath)
|
|
761
1149
|
);
|
|
762
|
-
|
|
763
|
-
await commitProgress(
|
|
1150
|
+
await saveProgress(
|
|
764
1151
|
repoRoot,
|
|
765
1152
|
progressPath,
|
|
766
|
-
|
|
1153
|
+
progress,
|
|
1154
|
+
`nightralph: start ${opts.featureName}`,
|
|
1155
|
+
d
|
|
767
1156
|
);
|
|
1157
|
+
if (!opts.testCmd) {
|
|
1158
|
+
d.log(" Test gate disabled: no test command");
|
|
1159
|
+
}
|
|
768
1160
|
let totalCompleted = 0;
|
|
769
1161
|
let waveNum = 0;
|
|
1162
|
+
const failedNums = /* @__PURE__ */ new Set();
|
|
770
1163
|
while (true) {
|
|
771
|
-
const ready = findReadyTickets(opts.tickets)
|
|
1164
|
+
const ready = findReadyTickets(opts.tickets).filter(
|
|
1165
|
+
(t) => !failedNums.has(t.num)
|
|
1166
|
+
);
|
|
772
1167
|
if (ready.length === 0) break;
|
|
773
1168
|
waveNum++;
|
|
774
1169
|
const readyNums = new Set(ready.map((t) => t.num));
|
|
@@ -788,125 +1183,89 @@ async function runWavesIsolated(opts) {
|
|
|
788
1183
|
filename: t.filename
|
|
789
1184
|
}))
|
|
790
1185
|
);
|
|
791
|
-
const
|
|
792
|
-
ready.map((t) =>
|
|
1186
|
+
const outcomes = await Promise.all(
|
|
1187
|
+
ready.map((t) => runTicketAttempts({
|
|
1188
|
+
ticket: t,
|
|
793
1189
|
repoRoot,
|
|
794
1190
|
repoName,
|
|
795
1191
|
baseBranch,
|
|
796
|
-
|
|
1192
|
+
specBody: opts.specBody,
|
|
1193
|
+
agentCmd: opts.agentCmd,
|
|
1194
|
+
model: opts.model,
|
|
1195
|
+
timeout: opts.timeout,
|
|
1196
|
+
logsDir: opts.logsDir,
|
|
1197
|
+
testCmd: opts.testCmd,
|
|
1198
|
+
maxTurns: opts.maxTurns,
|
|
1199
|
+
thinking: opts.thinking,
|
|
1200
|
+
retries,
|
|
1201
|
+
retryDelay,
|
|
1202
|
+
d
|
|
797
1203
|
}))
|
|
798
1204
|
);
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
1205
|
+
let waveCompleted = 0;
|
|
1206
|
+
const successfulBranches = [];
|
|
1207
|
+
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
1208
|
+
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
1210
|
+
const outcome = outcomes[i];
|
|
803
1211
|
const ticket = ready[i];
|
|
804
|
-
if (
|
|
1212
|
+
if (outcome.kind === "worktree-error") {
|
|
1213
|
+
failedNums.add(ticket.num);
|
|
805
1214
|
d.setTicketStatus(ticket.num, "failed");
|
|
806
1215
|
d.log(
|
|
807
|
-
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(
|
|
1216
|
+
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(outcome.error)}`
|
|
808
1217
|
);
|
|
809
1218
|
continue;
|
|
810
1219
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const prompt = renderPrompt(opts.specBody, t);
|
|
817
|
-
const logPath = join(
|
|
818
|
-
opts.logsDir,
|
|
819
|
-
t.filename.replace(/\.md$/, ".log")
|
|
820
|
-
);
|
|
821
|
-
const label = t.filename.replace(
|
|
822
|
-
/\.md$/,
|
|
823
|
-
""
|
|
1220
|
+
if (outcome.kind === "error") {
|
|
1221
|
+
failedNums.add(ticket.num);
|
|
1222
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
1223
|
+
d.log(
|
|
1224
|
+
` ${ticket.filename}: attempt error: ` + formatErrorMessage(outcome.error)
|
|
824
1225
|
);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
agentCmd: opts.agentCmd,
|
|
830
|
-
model: opts.model,
|
|
831
|
-
timeout: opts.timeout,
|
|
832
|
-
logPath,
|
|
833
|
-
cwd: worktrees[i].worktreePath,
|
|
834
|
-
label,
|
|
835
|
-
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
836
|
-
});
|
|
837
|
-
})
|
|
838
|
-
);
|
|
839
|
-
let waveCompleted = 0;
|
|
840
|
-
const successfulBranches = [];
|
|
841
|
-
const ticketsByNum = /* @__PURE__ */ new Map();
|
|
842
|
-
const worktreePathsByNum = /* @__PURE__ */ new Map();
|
|
843
|
-
for (let i = 0; i < results.length; i++) {
|
|
844
|
-
const r = results[i];
|
|
845
|
-
if (r.status === "rejected") continue;
|
|
846
|
-
const agentResult = r.value;
|
|
847
|
-
const ticket = activeTickets[i];
|
|
848
|
-
const worktree = worktrees[i];
|
|
849
|
-
if (agentResult.exitCode !== 0) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (outcome.kind === "no-commits") {
|
|
1229
|
+
failedNums.add(ticket.num);
|
|
850
1230
|
d.setTicketStatus(ticket.num, "failed");
|
|
851
1231
|
d.log(
|
|
852
|
-
` ${ticket.filename}: agent exited
|
|
1232
|
+
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
853
1233
|
);
|
|
854
|
-
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (outcome.kind === "failed") {
|
|
1237
|
+
failedNums.add(ticket.num);
|
|
1238
|
+
d.setTicketStatus(ticket.num, "failed");
|
|
1239
|
+
if (outcome.reason === "exit") {
|
|
1240
|
+
checkItems(ticket, outcome.completedItems);
|
|
1241
|
+
}
|
|
855
1242
|
checkTicket(progress, ticket.num, {
|
|
856
|
-
branch:
|
|
857
|
-
exitCode:
|
|
1243
|
+
branch: outcome.branchName,
|
|
1244
|
+
exitCode: outcome.exitCode,
|
|
858
1245
|
done: false
|
|
859
1246
|
});
|
|
860
|
-
|
|
861
|
-
await commitProgress(
|
|
1247
|
+
await saveProgress(
|
|
862
1248
|
repoRoot,
|
|
863
1249
|
progressPath,
|
|
864
|
-
|
|
1250
|
+
progress,
|
|
1251
|
+
`nightralph: ${ticket.filename} ` + (outcome.reason === "tests" ? "tests failed" : "failed"),
|
|
1252
|
+
d
|
|
865
1253
|
);
|
|
866
|
-
try {
|
|
867
|
-
await removeWorktree(worktree.worktreePath);
|
|
868
|
-
} catch (err) {
|
|
869
|
-
d.log(
|
|
870
|
-
` Failed to remove worktree ${worktree.worktreePath}: ` + formatErrorMessage(err)
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
1254
|
continue;
|
|
874
1255
|
}
|
|
875
|
-
|
|
876
|
-
const committed = await commitDirty(
|
|
877
|
-
worktree.worktreePath,
|
|
878
|
-
`nightralph: auto-commit ${ticket.filename} remaining changes`
|
|
879
|
-
);
|
|
880
|
-
if (committed) {
|
|
881
|
-
d.log(
|
|
882
|
-
` ${ticket.filename}: auto-committed remaining changes`
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (!hasNewCommits(
|
|
887
|
-
repoRoot,
|
|
888
|
-
baseBranch,
|
|
889
|
-
worktree.branchName
|
|
890
|
-
)) {
|
|
891
|
-
d.setTicketStatus(ticket.num, "failed");
|
|
892
|
-
d.log(
|
|
893
|
-
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
894
|
-
);
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
checkItems(ticket, agentResult.completedItems);
|
|
1256
|
+
checkItems(ticket, outcome.completedItems);
|
|
898
1257
|
d.log(
|
|
899
1258
|
` ${ticket.filename}: agent finished, awaiting merge`
|
|
900
1259
|
);
|
|
901
1260
|
ticketsByNum.set(ticket.num, ticket);
|
|
902
1261
|
worktreePathsByNum.set(
|
|
903
1262
|
ticket.num,
|
|
904
|
-
|
|
1263
|
+
outcome.worktreePath
|
|
905
1264
|
);
|
|
906
1265
|
successfulBranches.push({
|
|
907
|
-
branch:
|
|
1266
|
+
branch: outcome.branchName,
|
|
908
1267
|
ticketNum: ticket.num,
|
|
909
|
-
worktreePath:
|
|
1268
|
+
worktreePath: outcome.worktreePath
|
|
910
1269
|
});
|
|
911
1270
|
}
|
|
912
1271
|
if (successfulBranches.length > 0) {
|
|
@@ -933,23 +1292,21 @@ async function runWavesIsolated(opts) {
|
|
|
933
1292
|
branch: branchResult.branch,
|
|
934
1293
|
exitCode: 0
|
|
935
1294
|
});
|
|
936
|
-
|
|
937
|
-
await commitProgress(
|
|
1295
|
+
await saveProgress(
|
|
938
1296
|
repoRoot,
|
|
939
1297
|
progressPath,
|
|
940
|
-
|
|
1298
|
+
progress,
|
|
1299
|
+
`nightralph: ${ticket.filename} done`,
|
|
1300
|
+
d
|
|
941
1301
|
);
|
|
942
1302
|
const worktreePath = worktreePathsByNum.get(
|
|
943
1303
|
ticket.num
|
|
944
1304
|
);
|
|
945
1305
|
if (worktreePath) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
951
|
-
);
|
|
952
|
-
}
|
|
1306
|
+
await removeWorktreeQuietly(
|
|
1307
|
+
worktreePath,
|
|
1308
|
+
d
|
|
1309
|
+
);
|
|
953
1310
|
}
|
|
954
1311
|
} else {
|
|
955
1312
|
const worktreePath = worktreePathsByNum.get(
|
|
@@ -966,6 +1323,8 @@ async function runWavesIsolated(opts) {
|
|
|
966
1323
|
model: opts.model,
|
|
967
1324
|
timeout: opts.timeout,
|
|
968
1325
|
logsDir: opts.logsDir,
|
|
1326
|
+
testCmd: opts.testCmd,
|
|
1327
|
+
maxTurns: opts.maxTurns,
|
|
969
1328
|
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine")
|
|
970
1329
|
}) : false;
|
|
971
1330
|
if (recovered) {
|
|
@@ -980,15 +1339,19 @@ async function runWavesIsolated(opts) {
|
|
|
980
1339
|
branch: branchResult.branch,
|
|
981
1340
|
exitCode: 0
|
|
982
1341
|
});
|
|
983
|
-
|
|
984
|
-
await commitProgress(
|
|
1342
|
+
await saveProgress(
|
|
985
1343
|
repoRoot,
|
|
986
1344
|
progressPath,
|
|
987
|
-
|
|
1345
|
+
progress,
|
|
1346
|
+
`nightralph: ${ticket.filename} done (respawn)`,
|
|
1347
|
+
d
|
|
988
1348
|
);
|
|
989
1349
|
if (worktreePath) {
|
|
990
1350
|
try {
|
|
991
|
-
await removeWorktree(
|
|
1351
|
+
await removeWorktree(
|
|
1352
|
+
worktreePath,
|
|
1353
|
+
{ force: true }
|
|
1354
|
+
);
|
|
992
1355
|
} catch (err) {
|
|
993
1356
|
d.log(
|
|
994
1357
|
` Failed to remove worktree ${worktreePath}: ` + formatErrorMessage(err)
|
|
@@ -1009,11 +1372,12 @@ async function runWavesIsolated(opts) {
|
|
|
1009
1372
|
exitCode: 0,
|
|
1010
1373
|
done: false
|
|
1011
1374
|
});
|
|
1012
|
-
|
|
1013
|
-
await commitProgress(
|
|
1375
|
+
await saveProgress(
|
|
1014
1376
|
repoRoot,
|
|
1015
1377
|
progressPath,
|
|
1016
|
-
|
|
1378
|
+
progress,
|
|
1379
|
+
`nightralph: ${ticket.filename} merge failed`,
|
|
1380
|
+
d
|
|
1017
1381
|
);
|
|
1018
1382
|
}
|
|
1019
1383
|
}
|
|
@@ -1027,7 +1391,10 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1027
1391
|
const ticket = ticketsByNum.get(sb.ticketNum);
|
|
1028
1392
|
if (ticket && ticket.status === "done") continue;
|
|
1029
1393
|
try {
|
|
1030
|
-
await removeWorktree(
|
|
1394
|
+
await removeWorktree(
|
|
1395
|
+
sb.worktreePath,
|
|
1396
|
+
{ force: true }
|
|
1397
|
+
);
|
|
1031
1398
|
} catch (err) {
|
|
1032
1399
|
d.log(
|
|
1033
1400
|
` Failed to remove worktree ${sb.worktreePath}: ` + formatErrorMessage(err)
|
|
@@ -1049,7 +1416,7 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1049
1416
|
);
|
|
1050
1417
|
d.log(
|
|
1051
1418
|
`
|
|
1052
|
-
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "")
|
|
1419
|
+
Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedNums.size > 0 ? `, ${failedNums.size} failed` : "")
|
|
1053
1420
|
);
|
|
1054
1421
|
d.cleanup();
|
|
1055
1422
|
return { allDone: remaining.length === 0 };
|
|
@@ -1057,25 +1424,33 @@ Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.
|
|
|
1057
1424
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
1058
1425
|
export {
|
|
1059
1426
|
COMPLETED_RE,
|
|
1427
|
+
DEFAULT_RETRIES,
|
|
1428
|
+
DEFAULT_RETRY_DELAY,
|
|
1429
|
+
THINKING_LEVELS,
|
|
1060
1430
|
TICKET_RE,
|
|
1061
1431
|
TOOL_USE_RE,
|
|
1062
1432
|
checkItems,
|
|
1063
1433
|
createPiStreamFormatter,
|
|
1064
1434
|
dryRun,
|
|
1065
1435
|
dryRunIsolated,
|
|
1436
|
+
escalateThinking,
|
|
1066
1437
|
findReadyTickets,
|
|
1067
1438
|
formatErrorMessage,
|
|
1068
1439
|
formatStreamLine,
|
|
1069
1440
|
getProviderArgs,
|
|
1441
|
+
killProcessGroup,
|
|
1442
|
+
killRunningAgents,
|
|
1070
1443
|
markDone,
|
|
1071
1444
|
parseBlockers,
|
|
1072
1445
|
parseStatus,
|
|
1073
1446
|
parseTicketFilename,
|
|
1074
1447
|
renderMergePrompt,
|
|
1075
1448
|
renderPrompt,
|
|
1449
|
+
retryDelayFor,
|
|
1076
1450
|
runWaves,
|
|
1077
1451
|
runWavesIsolated,
|
|
1078
1452
|
scanTickets,
|
|
1079
|
-
spawnAgent
|
|
1453
|
+
spawnAgent,
|
|
1454
|
+
stripSpecSection
|
|
1080
1455
|
};
|
|
1081
1456
|
//# sourceMappingURL=orchestrator.js.map
|