nightralph 0.0.52 → 0.0.54
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 +145 -16
- package/dist/display.js +80 -62
- package/dist/display.js.map +2 -2
- package/dist/index.js +2099 -314
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +356 -0
- package/dist/learnings.js.map +7 -0
- package/dist/meta.json +234 -14
- package/dist/orchestrator.js +528 -142
- package/dist/orchestrator.js.map +3 -3
- package/dist/preflight.js +34 -0
- package/dist/preflight.js.map +7 -0
- package/dist/progress.js +5 -4
- package/dist/progress.js.map +2 -2
- package/dist/retry.js +83 -0
- package/dist/retry.js.map +7 -0
- package/dist/review.js +662 -0
- package/dist/review.js.map +7 -0
- package/dist/rules.js +54 -0
- package/dist/rules.js.map +7 -0
- package/dist/src/display.d.ts +35 -33
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/learnings.d.ts +68 -0
- package/dist/src/learnings.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +24 -11
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/preflight.d.ts +6 -0
- package/dist/src/preflight.d.ts.map +1 -0
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/retry.d.ts +26 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/review.d.ts +127 -0
- package/dist/src/review.d.ts.map +1 -0
- package/dist/src/rules.d.ts +10 -0
- package/dist/src/rules.d.ts.map +1 -0
- package/dist/src/testcmd.d.ts +1 -0
- package/dist/src/testcmd.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +30 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/testcmd.js +13 -0
- package/dist/testcmd.js.map +2 -2
- package/dist/worktree.js +236 -32
- package/dist/worktree.js.map +2 -2
- package/package.json +5 -4
package/dist/orchestrator.js
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
readFileSync,
|
|
8
8
|
writeFileSync,
|
|
9
9
|
unlinkSync,
|
|
10
|
-
readdirSync
|
|
10
|
+
readdirSync,
|
|
11
|
+
statSync
|
|
11
12
|
} from "node:fs";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
|
-
import { join, dirname, basename } from "node:path";
|
|
14
|
+
import { join, dirname, basename, relative } from "node:path";
|
|
14
15
|
import { createInterface } from "node:readline";
|
|
15
16
|
import { promisify } from "node:util";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
@@ -23,7 +24,8 @@ import {
|
|
|
23
24
|
buildBranchName,
|
|
24
25
|
hasNewCommits,
|
|
25
26
|
isDirty,
|
|
26
|
-
commitDirty
|
|
27
|
+
commitDirty,
|
|
28
|
+
headCommit
|
|
27
29
|
} from "./worktree.js";
|
|
28
30
|
import {
|
|
29
31
|
generateProgress,
|
|
@@ -41,9 +43,47 @@ import {
|
|
|
41
43
|
updateWorktreeFromBase
|
|
42
44
|
} from "./merge.js";
|
|
43
45
|
import {
|
|
44
|
-
createDisplay
|
|
46
|
+
createDisplay,
|
|
47
|
+
ticketLabel
|
|
45
48
|
} from "./display.js";
|
|
46
49
|
import { runTestCmd } from "./testcmd.js";
|
|
50
|
+
import {
|
|
51
|
+
readDocsRule,
|
|
52
|
+
installRule,
|
|
53
|
+
testGateRule,
|
|
54
|
+
LINT_RULE,
|
|
55
|
+
integrityRule,
|
|
56
|
+
CONFINEMENT_RULES,
|
|
57
|
+
commitRule,
|
|
58
|
+
notesRule,
|
|
59
|
+
agentsMdRule
|
|
60
|
+
} from "./rules.js";
|
|
61
|
+
import { runReviewPass, renderReviewSummary } from "./review.js";
|
|
62
|
+
import {
|
|
63
|
+
EFFORT_LADDERS,
|
|
64
|
+
escalateEffort,
|
|
65
|
+
retryDelayFor,
|
|
66
|
+
runWithRetries,
|
|
67
|
+
sleep
|
|
68
|
+
} from "./retry.js";
|
|
69
|
+
import {
|
|
70
|
+
EFFORT_LADDERS as EFFORT_LADDERS2,
|
|
71
|
+
ALL_EFFORT_LEVELS,
|
|
72
|
+
escalateEffort as escalateEffort2,
|
|
73
|
+
retryDelayFor as retryDelayFor2
|
|
74
|
+
} from "./retry.js";
|
|
75
|
+
import {
|
|
76
|
+
LOG_TAIL_LINES,
|
|
77
|
+
appendEntry,
|
|
78
|
+
collectNotes,
|
|
79
|
+
ensureNotesIgnored,
|
|
80
|
+
learningsPathFor,
|
|
81
|
+
readEntries,
|
|
82
|
+
renderEntry,
|
|
83
|
+
renderHistory,
|
|
84
|
+
selectHistory,
|
|
85
|
+
tailLines
|
|
86
|
+
} from "./learnings.js";
|
|
47
87
|
function formatErrorMessage(error) {
|
|
48
88
|
return error instanceof Error ? error.message : String(error);
|
|
49
89
|
}
|
|
@@ -165,6 +205,37 @@ function findReadyTickets(tickets) {
|
|
|
165
205
|
});
|
|
166
206
|
}
|
|
167
207
|
__name(findReadyTickets, "findReadyTickets");
|
|
208
|
+
function findBlockedTickets(tickets, failedKeys) {
|
|
209
|
+
const byKey = new Map(
|
|
210
|
+
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
211
|
+
);
|
|
212
|
+
function collectDead(ticket, visited, found) {
|
|
213
|
+
for (const b of ticket.blockers) {
|
|
214
|
+
const key = ticketKey(ticket.feature, b);
|
|
215
|
+
if (visited.has(key)) continue;
|
|
216
|
+
visited.add(key);
|
|
217
|
+
const blocker = byKey.get(key);
|
|
218
|
+
if (!blocker || blocker.status === "done") continue;
|
|
219
|
+
if (failedKeys.has(key) || blocker.status !== "ready-for-agent") {
|
|
220
|
+
found.add(key);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
collectDead(blocker, visited, found);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
__name(collectDead, "collectDead");
|
|
227
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
228
|
+
for (const t of tickets) {
|
|
229
|
+
const key = ticketKey(t.feature, t.num);
|
|
230
|
+
if (t.status !== "ready-for-agent") continue;
|
|
231
|
+
if (failedKeys.has(key)) continue;
|
|
232
|
+
const found = /* @__PURE__ */ new Set();
|
|
233
|
+
collectDead(t, /* @__PURE__ */ new Set([key]), found);
|
|
234
|
+
if (found.size > 0) blocked.set(key, [...found]);
|
|
235
|
+
}
|
|
236
|
+
return blocked;
|
|
237
|
+
}
|
|
238
|
+
__name(findBlockedTickets, "findBlockedTickets");
|
|
168
239
|
const COMPLETED_RE = /^\s*[-*+]?\s*\[x\]\s+(.+)$/i;
|
|
169
240
|
const TOOL_USE_RE = /^\[[\w.]+\]/;
|
|
170
241
|
const CHECKBOX_RE = /^(\s*[-*+] )\[( |x|X)\]( .*)$/;
|
|
@@ -305,11 +376,23 @@ function stripSpecSection(specBody, heading) {
|
|
|
305
376
|
return out.join("\n");
|
|
306
377
|
}
|
|
307
378
|
__name(stripSpecSection, "stripSpecSection");
|
|
379
|
+
function reviewSpecBody(specBody, specBodies) {
|
|
380
|
+
if (!specBodies || specBodies.size <= 1) return specBody;
|
|
381
|
+
return [...specBodies].map(([name, body]) => `# Feature: ${name}
|
|
382
|
+
|
|
383
|
+
${body}`).join("\n\n");
|
|
384
|
+
}
|
|
385
|
+
__name(reviewSpecBody, "reviewSpecBody");
|
|
308
386
|
function lookupSpec(specBodies, fallback, feature) {
|
|
309
387
|
if (!feature || !specBodies) return fallback;
|
|
310
388
|
return specBodies.get(feature) ?? fallback;
|
|
311
389
|
}
|
|
312
390
|
__name(lookupSpec, "lookupSpec");
|
|
391
|
+
function historyBlock(history) {
|
|
392
|
+
if (!history) return [];
|
|
393
|
+
return [history, ""];
|
|
394
|
+
}
|
|
395
|
+
__name(historyBlock, "historyBlock");
|
|
313
396
|
function renderPrompt(specBody, ticket, opts) {
|
|
314
397
|
return [
|
|
315
398
|
"---- SPEC CONTEXT ----",
|
|
@@ -319,27 +402,29 @@ function renderPrompt(specBody, ticket, opts) {
|
|
|
319
402
|
ticket.filename,
|
|
320
403
|
ticket.body,
|
|
321
404
|
"",
|
|
405
|
+
...historyBlock(opts.history),
|
|
322
406
|
"---- AGENT INSTRUCTIONS ----",
|
|
323
407
|
"You are an autonomous coding agent. Implement the ticket using /tdd.",
|
|
324
408
|
"",
|
|
325
409
|
"Follow these rules:",
|
|
326
|
-
"
|
|
410
|
+
readDocsRule("ticket"),
|
|
327
411
|
"- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
|
|
328
412
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
"
|
|
413
|
+
...installRule(opts.installCmd),
|
|
414
|
+
"- Use /tdd for every checklist item. For each 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, then refactor. For a checklist item that is pure scaffolding, config, or docs (where a test adds no value), skip test-first and record that decision in your notes file.",
|
|
415
|
+
testGateRule(opts.testCmd, "ticket"),
|
|
416
|
+
LINT_RULE,
|
|
417
|
+
integrityRule("checklist item"),
|
|
418
|
+
...CONFINEMENT_RULES,
|
|
419
|
+
commitRule("checklist item"),
|
|
420
|
+
notesRule("checklist item", "ticket"),
|
|
336
421
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
337
|
-
"
|
|
422
|
+
agentsMdRule("ticket"),
|
|
338
423
|
""
|
|
339
424
|
].join("\n");
|
|
340
425
|
}
|
|
341
426
|
__name(renderPrompt, "renderPrompt");
|
|
342
|
-
function renderMergePrompt(specBody, ticket) {
|
|
427
|
+
function renderMergePrompt(specBody, ticket, opts = {}) {
|
|
343
428
|
return [
|
|
344
429
|
"---- SPEC CONTEXT ----",
|
|
345
430
|
specBody,
|
|
@@ -360,6 +445,7 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
360
445
|
"4. Commit the resolved files.",
|
|
361
446
|
"",
|
|
362
447
|
"Do NOT re-implement the ticket from scratch.",
|
|
448
|
+
...installRule(opts.installCmd),
|
|
363
449
|
"Do NOT read or modify files outside your current directory (the worktree).",
|
|
364
450
|
"Do NOT run git push."
|
|
365
451
|
].join("\n");
|
|
@@ -473,42 +559,6 @@ function getProviderArgs(cmd) {
|
|
|
473
559
|
return PROVIDER_ARGS[name] ?? [];
|
|
474
560
|
}
|
|
475
561
|
__name(getProviderArgs, "getProviderArgs");
|
|
476
|
-
const EFFORT_LADDERS = {
|
|
477
|
-
pi: [
|
|
478
|
-
"off",
|
|
479
|
-
"minimal",
|
|
480
|
-
"low",
|
|
481
|
-
"medium",
|
|
482
|
-
"high",
|
|
483
|
-
"xhigh",
|
|
484
|
-
"max"
|
|
485
|
-
],
|
|
486
|
-
claude: [
|
|
487
|
-
"low",
|
|
488
|
-
"medium",
|
|
489
|
-
"high",
|
|
490
|
-
"xhigh",
|
|
491
|
-
"max"
|
|
492
|
-
],
|
|
493
|
-
codex: ["low", "medium", "high"]
|
|
494
|
-
};
|
|
495
|
-
const ALL_EFFORT_LEVELS = [
|
|
496
|
-
"off",
|
|
497
|
-
"minimal",
|
|
498
|
-
"low",
|
|
499
|
-
"medium",
|
|
500
|
-
"high",
|
|
501
|
-
"xhigh",
|
|
502
|
-
"max"
|
|
503
|
-
];
|
|
504
|
-
function escalateEffort(provider, level) {
|
|
505
|
-
if (!level) return "high";
|
|
506
|
-
const ladder = EFFORT_LADDERS[provider];
|
|
507
|
-
const i = ladder.indexOf(level);
|
|
508
|
-
if (i === -1) return level;
|
|
509
|
-
return ladder[Math.min(i + 1, ladder.length - 1)];
|
|
510
|
-
}
|
|
511
|
-
__name(escalateEffort, "escalateEffort");
|
|
512
562
|
function formatStreamLine(line) {
|
|
513
563
|
let obj;
|
|
514
564
|
try {
|
|
@@ -701,7 +751,7 @@ function spawnAgent(opts) {
|
|
|
701
751
|
detached: true
|
|
702
752
|
});
|
|
703
753
|
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
704
|
-
proc.stdout.pipe(logStream);
|
|
754
|
+
proc.stdout.pipe(logStream, { end: false });
|
|
705
755
|
const piFormatter = isPi ? createPiStreamFormatter() : null;
|
|
706
756
|
const completedItems = [];
|
|
707
757
|
const seenItems = /* @__PURE__ */ new Set();
|
|
@@ -750,7 +800,7 @@ function spawnAgent(opts) {
|
|
|
750
800
|
}
|
|
751
801
|
}
|
|
752
802
|
});
|
|
753
|
-
proc.stderr.pipe(logStream);
|
|
803
|
+
proc.stderr.pipe(logStream, { end: false });
|
|
754
804
|
const rlErr = createInterface({ input: proc.stderr });
|
|
755
805
|
rlErr.on("line", (line) => {
|
|
756
806
|
if (isClaude && !TOOL_USE_RE.test(line)) return;
|
|
@@ -835,16 +885,21 @@ function spawnAgent(opts) {
|
|
|
835
885
|
}
|
|
836
886
|
rl.close();
|
|
837
887
|
rlErr.close();
|
|
838
|
-
|
|
888
|
+
const msg = "agent process error: " + formatErrorMessage(err);
|
|
889
|
+
logStream.write(`nightralph: ${msg}
|
|
890
|
+
`);
|
|
839
891
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
892
|
+
if (opts.onLine) {
|
|
893
|
+
opts.onLine(`${prefix}${msg}`);
|
|
894
|
+
} else {
|
|
895
|
+
console.error(`${prefix}${msg}`);
|
|
896
|
+
}
|
|
897
|
+
logStream.end(() => {
|
|
898
|
+
resolve({
|
|
899
|
+
exitCode: 1,
|
|
900
|
+
completedItems,
|
|
901
|
+
timedOut
|
|
902
|
+
});
|
|
848
903
|
});
|
|
849
904
|
});
|
|
850
905
|
});
|
|
@@ -864,6 +919,8 @@ function dryRun(tickets, specBody, testCmd) {
|
|
|
864
919
|
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
865
920
|
);
|
|
866
921
|
}
|
|
922
|
+
console.log(`
|
|
923
|
+
Test command: ${testCmd || "(none)"}`);
|
|
867
924
|
const firstReady = findReadyTickets(tickets);
|
|
868
925
|
if (firstReady.length > 0) {
|
|
869
926
|
console.log(
|
|
@@ -919,14 +976,20 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
919
976
|
renderPrompt(
|
|
920
977
|
opts.specBody,
|
|
921
978
|
firstReady[0],
|
|
922
|
-
{
|
|
979
|
+
{
|
|
980
|
+
testCmd: opts.testCmd,
|
|
981
|
+
installCmd: opts.installCmd
|
|
982
|
+
}
|
|
923
983
|
)
|
|
924
984
|
);
|
|
925
985
|
}
|
|
926
986
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
927
987
|
console.log(
|
|
928
988
|
`
|
|
929
|
-
Test command: ${opts.testCmd}`
|
|
989
|
+
Test command: ${opts.testCmd || "(none)"}`
|
|
990
|
+
);
|
|
991
|
+
console.log(
|
|
992
|
+
`Install: ${opts.installCmd || "(none)"}`
|
|
930
993
|
);
|
|
931
994
|
console.log(
|
|
932
995
|
`Effort: ${opts.effort ?? "(provider default)"}`
|
|
@@ -947,7 +1010,34 @@ Merge strategy: ${strategyDesc}`
|
|
|
947
1010
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
948
1011
|
}
|
|
949
1012
|
__name(dryRunIsolated, "dryRunIsolated");
|
|
1013
|
+
async function runInstall(opts) {
|
|
1014
|
+
const result = await runTestCmd({
|
|
1015
|
+
cmd: opts.installCmd,
|
|
1016
|
+
cwd: opts.worktreePath,
|
|
1017
|
+
timeout: opts.timeout,
|
|
1018
|
+
registry: opts.registry
|
|
1019
|
+
});
|
|
1020
|
+
const installLogPath = attemptLogPath(
|
|
1021
|
+
opts.logsDir,
|
|
1022
|
+
opts.ticket,
|
|
1023
|
+
opts.attempt ?? 1,
|
|
1024
|
+
".install.log"
|
|
1025
|
+
);
|
|
1026
|
+
writeFileSync(installLogPath, result.output);
|
|
1027
|
+
if (result.exitCode !== 0) {
|
|
1028
|
+
opts.log(
|
|
1029
|
+
` ${opts.ticket.filename}: install failed (exit ${result.exitCode}); see ${installLogPath}`
|
|
1030
|
+
);
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
__name(runInstall, "runInstall");
|
|
950
1036
|
async function runTestGate(opts) {
|
|
1037
|
+
if (opts.testCmd === "") {
|
|
1038
|
+
opts.log(` ${opts.ticket.filename}: no test gate`);
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
951
1041
|
const result = await runTestCmd({
|
|
952
1042
|
cmd: opts.testCmd,
|
|
953
1043
|
cwd: opts.worktreePath,
|
|
@@ -989,9 +1079,22 @@ async function respawnAndRetryMerge(opts) {
|
|
|
989
1079
|
} else {
|
|
990
1080
|
console.log(msg1);
|
|
991
1081
|
}
|
|
1082
|
+
if (opts.installCmd) {
|
|
1083
|
+
const installed = await runInstall({
|
|
1084
|
+
installCmd: opts.installCmd,
|
|
1085
|
+
ticket: opts.ticket,
|
|
1086
|
+
worktreePath: opts.worktreePath,
|
|
1087
|
+
timeout: opts.timeout,
|
|
1088
|
+
logsDir: opts.logsDir,
|
|
1089
|
+
log,
|
|
1090
|
+
registry: runningAgents
|
|
1091
|
+
});
|
|
1092
|
+
if (!installed) return false;
|
|
1093
|
+
}
|
|
992
1094
|
const prompt = renderMergePrompt(
|
|
993
1095
|
opts.specBody,
|
|
994
|
-
opts.ticket
|
|
1096
|
+
opts.ticket,
|
|
1097
|
+
{ installCmd: opts.installCmd }
|
|
995
1098
|
);
|
|
996
1099
|
const logPath = join(
|
|
997
1100
|
opts.logsDir,
|
|
@@ -1090,20 +1193,31 @@ async function runPool(items, limit, fn) {
|
|
|
1090
1193
|
return results;
|
|
1091
1194
|
}
|
|
1092
1195
|
__name(runPool, "runPool");
|
|
1093
|
-
function retryDelayFor(base, attempt) {
|
|
1094
|
-
return base * 2 ** (attempt - 2);
|
|
1095
|
-
}
|
|
1096
|
-
__name(retryDelayFor, "retryDelayFor");
|
|
1097
|
-
function sleep(ms) {
|
|
1098
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1099
|
-
}
|
|
1100
|
-
__name(sleep, "sleep");
|
|
1101
1196
|
function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
1102
1197
|
const stem = ticket.filename.replace(/\.md$/, "");
|
|
1103
1198
|
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
1104
1199
|
return join(logsDir, `${stem}${suffix}${ext}`);
|
|
1105
1200
|
}
|
|
1106
1201
|
__name(attemptLogPath, "attemptLogPath");
|
|
1202
|
+
function buildHistory(learningsPath, ticket, d) {
|
|
1203
|
+
try {
|
|
1204
|
+
return renderHistory(selectHistory(
|
|
1205
|
+
readEntries(learningsPath),
|
|
1206
|
+
{
|
|
1207
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
1208
|
+
blockerKeys: ticket.blockers.map((b) => {
|
|
1209
|
+
return ticketKey(ticket.feature, b);
|
|
1210
|
+
})
|
|
1211
|
+
}
|
|
1212
|
+
));
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
d.log(
|
|
1215
|
+
` ${ticket.filename}: could not read learnings: ${formatErrorMessage(err)}`
|
|
1216
|
+
);
|
|
1217
|
+
return "";
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
__name(buildHistory, "buildHistory");
|
|
1107
1221
|
async function runTicketAttempt(opts) {
|
|
1108
1222
|
const { ticket, d } = opts;
|
|
1109
1223
|
let worktree;
|
|
@@ -1122,6 +1236,38 @@ async function runTicketAttempt(opts) {
|
|
|
1122
1236
|
"in-progress",
|
|
1123
1237
|
ticket.feature
|
|
1124
1238
|
);
|
|
1239
|
+
if (opts.installCmd) {
|
|
1240
|
+
const installed = await runInstall({
|
|
1241
|
+
installCmd: opts.installCmd,
|
|
1242
|
+
ticket,
|
|
1243
|
+
worktreePath: worktree.worktreePath,
|
|
1244
|
+
timeout: opts.timeout,
|
|
1245
|
+
logsDir: opts.logsDir,
|
|
1246
|
+
attempt: opts.attempt,
|
|
1247
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log"),
|
|
1248
|
+
registry: runningAgents
|
|
1249
|
+
});
|
|
1250
|
+
if (!installed) {
|
|
1251
|
+
await removeWorktreeQuietly(
|
|
1252
|
+
worktree.worktreePath,
|
|
1253
|
+
d
|
|
1254
|
+
);
|
|
1255
|
+
return {
|
|
1256
|
+
kind: "failed",
|
|
1257
|
+
reason: "install",
|
|
1258
|
+
branchName: worktree.branchName,
|
|
1259
|
+
exitCode: 1,
|
|
1260
|
+
completedItems: []
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
ensureNotesIgnored(worktree.worktreePath);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
d.log(
|
|
1268
|
+
` ${ticket.filename}: could not set up the notes directory: ${formatErrorMessage(err)}`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1125
1271
|
const ticketSpec = lookupSpec(
|
|
1126
1272
|
opts.specBodies,
|
|
1127
1273
|
opts.specBody,
|
|
@@ -1131,7 +1277,17 @@ async function runTicketAttempt(opts) {
|
|
|
1131
1277
|
prompt: renderPrompt(
|
|
1132
1278
|
ticketSpec,
|
|
1133
1279
|
ticket,
|
|
1134
|
-
{
|
|
1280
|
+
{
|
|
1281
|
+
testCmd: opts.testCmd,
|
|
1282
|
+
installCmd: opts.installCmd,
|
|
1283
|
+
// Rebuilt every attempt, so a retry sees the
|
|
1284
|
+
// entry its previous attempt just wrote
|
|
1285
|
+
history: buildHistory(
|
|
1286
|
+
opts.learningsPath,
|
|
1287
|
+
ticket,
|
|
1288
|
+
d
|
|
1289
|
+
)
|
|
1290
|
+
}
|
|
1135
1291
|
),
|
|
1136
1292
|
agentCmd: opts.agentCmd,
|
|
1137
1293
|
model: opts.model,
|
|
@@ -1148,6 +1304,20 @@ async function runTicketAttempt(opts) {
|
|
|
1148
1304
|
effort: opts.effort,
|
|
1149
1305
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1150
1306
|
});
|
|
1307
|
+
let notes = null;
|
|
1308
|
+
try {
|
|
1309
|
+
notes = collectNotes(worktree.worktreePath);
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
d.log(
|
|
1312
|
+
` ${ticket.filename}: could not collect notes: ${formatErrorMessage(err)}`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
const agentRun = {
|
|
1316
|
+
exitCode: agentResult.exitCode,
|
|
1317
|
+
completedItems: agentResult.completedItems,
|
|
1318
|
+
timedOut: agentResult.timedOut,
|
|
1319
|
+
notes
|
|
1320
|
+
};
|
|
1151
1321
|
if (agentResult.timedOut) {
|
|
1152
1322
|
d.log(
|
|
1153
1323
|
` ${ticket.filename}: timed out; keeping the work it committed`
|
|
@@ -1162,7 +1332,8 @@ async function runTicketAttempt(opts) {
|
|
|
1162
1332
|
reason: "exit",
|
|
1163
1333
|
branchName: worktree.branchName,
|
|
1164
1334
|
exitCode: agentResult.exitCode,
|
|
1165
|
-
completedItems: agentResult.completedItems
|
|
1335
|
+
completedItems: agentResult.completedItems,
|
|
1336
|
+
agentRun
|
|
1166
1337
|
};
|
|
1167
1338
|
}
|
|
1168
1339
|
if (isDirty(worktree.worktreePath)) {
|
|
@@ -1185,7 +1356,8 @@ async function runTicketAttempt(opts) {
|
|
|
1185
1356
|
return {
|
|
1186
1357
|
kind: "no-commits",
|
|
1187
1358
|
worktreePath: worktree.worktreePath,
|
|
1188
|
-
branchName: worktree.branchName
|
|
1359
|
+
branchName: worktree.branchName,
|
|
1360
|
+
agentRun
|
|
1189
1361
|
};
|
|
1190
1362
|
}
|
|
1191
1363
|
const passed = await runTestGate({
|
|
@@ -1208,44 +1380,130 @@ async function runTicketAttempt(opts) {
|
|
|
1208
1380
|
reason: "tests",
|
|
1209
1381
|
branchName: worktree.branchName,
|
|
1210
1382
|
exitCode: 1,
|
|
1211
|
-
completedItems: agentResult.completedItems
|
|
1383
|
+
completedItems: agentResult.completedItems,
|
|
1384
|
+
agentRun
|
|
1212
1385
|
};
|
|
1213
1386
|
}
|
|
1214
1387
|
return {
|
|
1215
1388
|
kind: "ready",
|
|
1216
1389
|
worktreePath: worktree.worktreePath,
|
|
1217
1390
|
branchName: worktree.branchName,
|
|
1218
|
-
completedItems: agentResult.completedItems
|
|
1391
|
+
completedItems: agentResult.completedItems,
|
|
1392
|
+
agentRun
|
|
1219
1393
|
};
|
|
1220
1394
|
}
|
|
1221
1395
|
__name(runTicketAttempt, "runTicketAttempt");
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1396
|
+
function readLogTail(path) {
|
|
1397
|
+
try {
|
|
1398
|
+
return tailLines(readFileSync(path, "utf8"), LOG_TAIL_LINES);
|
|
1399
|
+
} catch {
|
|
1400
|
+
return "";
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
__name(readLogTail, "readLogTail");
|
|
1404
|
+
function attemptEntry(opts) {
|
|
1405
|
+
const { logsDir, ticket, attempt, effort, outcome } = opts;
|
|
1406
|
+
const base = {
|
|
1407
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
1408
|
+
label: ticketLabel(ticket),
|
|
1409
|
+
attempt,
|
|
1410
|
+
now: /* @__PURE__ */ new Date()
|
|
1232
1411
|
};
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1412
|
+
const logFor = /* @__PURE__ */ __name((ext) => {
|
|
1413
|
+
const path = attemptLogPath(logsDir, ticket, attempt, ext);
|
|
1414
|
+
return { logName: basename(path), logTail: readLogTail(path) };
|
|
1415
|
+
}, "logFor");
|
|
1416
|
+
switch (outcome.kind) {
|
|
1417
|
+
case "ready":
|
|
1418
|
+
return {
|
|
1419
|
+
...base,
|
|
1420
|
+
outcome: "done",
|
|
1421
|
+
run: outcome.agentRun,
|
|
1422
|
+
effort
|
|
1423
|
+
};
|
|
1424
|
+
case "failed":
|
|
1425
|
+
switch (outcome.reason) {
|
|
1426
|
+
case "exit":
|
|
1427
|
+
return {
|
|
1428
|
+
...base,
|
|
1429
|
+
outcome: "exit",
|
|
1430
|
+
run: outcome.agentRun,
|
|
1431
|
+
effort
|
|
1432
|
+
};
|
|
1433
|
+
case "tests":
|
|
1434
|
+
return {
|
|
1435
|
+
...base,
|
|
1436
|
+
outcome: "tests",
|
|
1437
|
+
run: outcome.agentRun,
|
|
1438
|
+
effort,
|
|
1439
|
+
...logFor(".test.log")
|
|
1440
|
+
};
|
|
1441
|
+
case "install":
|
|
1442
|
+
return {
|
|
1443
|
+
...base,
|
|
1444
|
+
outcome: "install",
|
|
1445
|
+
...logFor(".install.log")
|
|
1446
|
+
};
|
|
1447
|
+
default: {
|
|
1448
|
+
const _exhaustive = outcome;
|
|
1449
|
+
return _exhaustive;
|
|
1450
|
+
}
|
|
1242
1451
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1452
|
+
case "no-commits":
|
|
1453
|
+
return {
|
|
1454
|
+
...base,
|
|
1455
|
+
outcome: "no-commits",
|
|
1456
|
+
run: outcome.agentRun,
|
|
1457
|
+
effort
|
|
1458
|
+
};
|
|
1459
|
+
case "error":
|
|
1460
|
+
case "worktree-error":
|
|
1461
|
+
return {
|
|
1462
|
+
...base,
|
|
1463
|
+
outcome: "error",
|
|
1464
|
+
error: formatErrorMessage(outcome.error)
|
|
1465
|
+
};
|
|
1466
|
+
default: {
|
|
1467
|
+
const _exhaustive = outcome;
|
|
1468
|
+
return _exhaustive;
|
|
1248
1469
|
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
__name(attemptEntry, "attemptEntry");
|
|
1473
|
+
function recordAttempt(opts) {
|
|
1474
|
+
try {
|
|
1475
|
+
const entry = attemptEntry(opts);
|
|
1476
|
+
if (!entry) return;
|
|
1477
|
+
appendEntry(opts.learningsPath, renderEntry(entry));
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
opts.d.log(
|
|
1480
|
+
` ${opts.ticket.filename}: could not record learnings: ${formatErrorMessage(err)}`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
__name(recordAttempt, "recordAttempt");
|
|
1485
|
+
async function runTicketAttempts(opts) {
|
|
1486
|
+
const { ticket, d } = opts;
|
|
1487
|
+
return runWithRetries({
|
|
1488
|
+
maxAttempts: opts.retries + 1,
|
|
1489
|
+
retryDelay: opts.retryDelay,
|
|
1490
|
+
agentCmd: opts.agentCmd,
|
|
1491
|
+
effort: opts.effort,
|
|
1492
|
+
label: ticket.filename,
|
|
1493
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
1494
|
+
}, async (attempt, effort) => {
|
|
1495
|
+
const record = /* @__PURE__ */ __name((o) => {
|
|
1496
|
+
recordAttempt({
|
|
1497
|
+
learningsPath: opts.learningsPath,
|
|
1498
|
+
logsDir: opts.logsDir,
|
|
1499
|
+
ticket,
|
|
1500
|
+
attempt,
|
|
1501
|
+
effort,
|
|
1502
|
+
outcome: o,
|
|
1503
|
+
d
|
|
1504
|
+
});
|
|
1505
|
+
}, "record");
|
|
1506
|
+
let outcome;
|
|
1249
1507
|
try {
|
|
1250
1508
|
outcome = await runTicketAttempt({
|
|
1251
1509
|
...opts,
|
|
@@ -1256,25 +1514,46 @@ async function runTicketAttempts(opts) {
|
|
|
1256
1514
|
d.log(
|
|
1257
1515
|
` ${ticket.filename}: attempt ${attempt} threw: ${formatErrorMessage(error)}`
|
|
1258
1516
|
);
|
|
1259
|
-
|
|
1517
|
+
const errorOutcome = { kind: "error", error };
|
|
1518
|
+
record(errorOutcome);
|
|
1519
|
+
return { result: errorOutcome, stop: true };
|
|
1260
1520
|
}
|
|
1521
|
+
record(outcome);
|
|
1261
1522
|
if (outcome.kind === "ready") {
|
|
1262
1523
|
d.setTicketStatus(
|
|
1263
1524
|
ticket.num,
|
|
1264
1525
|
"awaiting-merge",
|
|
1265
1526
|
ticket.feature
|
|
1266
1527
|
);
|
|
1267
|
-
return outcome;
|
|
1528
|
+
return { result: outcome, stop: true };
|
|
1268
1529
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1530
|
+
return {
|
|
1531
|
+
result: outcome,
|
|
1532
|
+
stop: outcome.kind === "worktree-error"
|
|
1533
|
+
};
|
|
1534
|
+
});
|
|
1272
1535
|
}
|
|
1273
1536
|
__name(runTicketAttempts, "runTicketAttempts");
|
|
1537
|
+
function failureNote(reason) {
|
|
1538
|
+
if (reason === "tests") return "tests failed";
|
|
1539
|
+
if (reason === "install") return "install failed";
|
|
1540
|
+
return "failed";
|
|
1541
|
+
}
|
|
1542
|
+
__name(failureNote, "failureNote");
|
|
1543
|
+
function isRegularFile(path) {
|
|
1544
|
+
try {
|
|
1545
|
+
return statSync(path).isFile();
|
|
1546
|
+
} catch {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
__name(isRegularFile, "isRegularFile");
|
|
1274
1551
|
async function saveProgress(repoRoot, progressPath, progress, message, d) {
|
|
1275
1552
|
writeProgress(progressPath, progress);
|
|
1553
|
+
const learningsPath = learningsPathFor(progressPath);
|
|
1554
|
+
const paths = isRegularFile(learningsPath) ? [progressPath, learningsPath] : progressPath;
|
|
1276
1555
|
try {
|
|
1277
|
-
await commitProgress(repoRoot,
|
|
1556
|
+
await commitProgress(repoRoot, paths, message);
|
|
1278
1557
|
} catch (err) {
|
|
1279
1558
|
d.log(
|
|
1280
1559
|
" Failed to commit progress: " + formatErrorMessage(err)
|
|
@@ -1344,6 +1623,29 @@ function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
|
1344
1623
|
return lines.join("\n");
|
|
1345
1624
|
}
|
|
1346
1625
|
__name(renderFinishSummary, "renderFinishSummary");
|
|
1626
|
+
function sessionTickets(tickets) {
|
|
1627
|
+
return [...tickets].filter((t) => t.status === "ready-for-agent").sort((a, b) => a.feature.localeCompare(b.feature) || a.num - b.num);
|
|
1628
|
+
}
|
|
1629
|
+
__name(sessionTickets, "sessionTickets");
|
|
1630
|
+
function toRef(t) {
|
|
1631
|
+
return { num: t.num, filename: t.filename, feature: t.feature };
|
|
1632
|
+
}
|
|
1633
|
+
__name(toRef, "toRef");
|
|
1634
|
+
function reportBlocked(d, tickets, failedKeys) {
|
|
1635
|
+
const byKey = new Map(
|
|
1636
|
+
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
1637
|
+
);
|
|
1638
|
+
for (const [key, dead] of findBlockedTickets(
|
|
1639
|
+
tickets,
|
|
1640
|
+
failedKeys
|
|
1641
|
+
)) {
|
|
1642
|
+
const ticket = byKey.get(key);
|
|
1643
|
+
if (!ticket) continue;
|
|
1644
|
+
const labels = dead.map((k) => byKey.get(k)).filter((t) => t !== void 0).map((t) => ticketLabel(toRef(t)));
|
|
1645
|
+
d.setBlocked(ticket.num, ticket.feature, labels);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
__name(reportBlocked, "reportBlocked");
|
|
1347
1649
|
function runProgress(tickets) {
|
|
1348
1650
|
return {
|
|
1349
1651
|
completed: tickets.filter((t) => t.status === "done").length,
|
|
@@ -1364,7 +1666,9 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1364
1666
|
let totalCompleted = 0;
|
|
1365
1667
|
let waveNum = 0;
|
|
1366
1668
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1669
|
+
const session = sessionTickets(tickets);
|
|
1367
1670
|
while (true) {
|
|
1671
|
+
if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
|
|
1368
1672
|
const ready = findReadyTickets(tickets).filter(
|
|
1369
1673
|
(t) => !failedKeys.has(
|
|
1370
1674
|
ticketKey(t.feature, t.num)
|
|
@@ -1372,12 +1676,10 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1372
1676
|
);
|
|
1373
1677
|
if (ready.length === 0) break;
|
|
1374
1678
|
waveNum++;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
(t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
|
|
1380
|
-
);
|
|
1679
|
+
if (waveNum === 1) {
|
|
1680
|
+
d.startRun(session.map(toRef));
|
|
1681
|
+
reportBlocked(d, tickets, failedKeys);
|
|
1682
|
+
}
|
|
1381
1683
|
d.startWave(
|
|
1382
1684
|
waveNum,
|
|
1383
1685
|
totalWaves,
|
|
@@ -1386,12 +1688,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1386
1688
|
filename: t.filename,
|
|
1387
1689
|
feature: t.feature
|
|
1388
1690
|
})),
|
|
1389
|
-
runProgress(
|
|
1390
|
-
remaining2.map((t) => ({
|
|
1391
|
-
num: t.num,
|
|
1392
|
-
filename: t.filename,
|
|
1393
|
-
feature: t.feature
|
|
1394
|
-
}))
|
|
1691
|
+
runProgress(session)
|
|
1395
1692
|
);
|
|
1396
1693
|
const results = await runPool(
|
|
1397
1694
|
ready,
|
|
@@ -1488,10 +1785,10 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1488
1785
|
);
|
|
1489
1786
|
}
|
|
1490
1787
|
if (waveCompleted === 0) {
|
|
1491
|
-
const
|
|
1788
|
+
const remaining2 = findReadyTickets(tickets).filter((t) => !failedKeys.has(
|
|
1492
1789
|
ticketKey(t.feature, t.num)
|
|
1493
1790
|
));
|
|
1494
|
-
if (
|
|
1791
|
+
if (remaining2.length === 0) {
|
|
1495
1792
|
d.log(
|
|
1496
1793
|
"\nNo tickets completed this wave and no ready tickets remain. Stopping."
|
|
1497
1794
|
);
|
|
@@ -1499,10 +1796,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1499
1796
|
}
|
|
1500
1797
|
d.log(
|
|
1501
1798
|
`
|
|
1502
|
-
No tickets completed this wave. ${
|
|
1799
|
+
No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.length === 1 ? "" : "s"} remain; continuing.`
|
|
1503
1800
|
);
|
|
1504
1801
|
}
|
|
1505
1802
|
}
|
|
1803
|
+
if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
|
|
1506
1804
|
const remaining = tickets.filter(
|
|
1507
1805
|
(t) => t.status === "ready-for-agent"
|
|
1508
1806
|
);
|
|
@@ -1531,6 +1829,7 @@ async function runWavesIsolated(opts) {
|
|
|
1531
1829
|
"Working tree is dirty; commit or stash changes before running"
|
|
1532
1830
|
);
|
|
1533
1831
|
}
|
|
1832
|
+
const launchCommit = headCommit(repoRoot);
|
|
1534
1833
|
const features = new Set(
|
|
1535
1834
|
opts.tickets.map((t) => t.feature).filter(Boolean)
|
|
1536
1835
|
);
|
|
@@ -1595,7 +1894,9 @@ async function runWavesIsolated(opts) {
|
|
|
1595
1894
|
let totalCompleted = 0;
|
|
1596
1895
|
let waveNum = 0;
|
|
1597
1896
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1897
|
+
const session = sessionTickets(opts.tickets);
|
|
1598
1898
|
while (true) {
|
|
1899
|
+
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
1599
1900
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
1600
1901
|
(t) => !failedKeys.has(
|
|
1601
1902
|
ticketKey(t.feature, t.num)
|
|
@@ -1603,12 +1904,10 @@ async function runWavesIsolated(opts) {
|
|
|
1603
1904
|
);
|
|
1604
1905
|
if (ready.length === 0) break;
|
|
1605
1906
|
waveNum++;
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
(t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
|
|
1611
|
-
);
|
|
1907
|
+
if (waveNum === 1) {
|
|
1908
|
+
d.startRun(session.map(toRef));
|
|
1909
|
+
reportBlocked(d, opts.tickets, failedKeys);
|
|
1910
|
+
}
|
|
1612
1911
|
d.startWave(
|
|
1613
1912
|
waveNum,
|
|
1614
1913
|
totalWaves,
|
|
@@ -1617,12 +1916,7 @@ async function runWavesIsolated(opts) {
|
|
|
1617
1916
|
filename: t.filename,
|
|
1618
1917
|
feature: t.feature
|
|
1619
1918
|
})),
|
|
1620
|
-
runProgress(
|
|
1621
|
-
remaining2.map((t) => ({
|
|
1622
|
-
num: t.num,
|
|
1623
|
-
filename: t.filename,
|
|
1624
|
-
feature: t.feature
|
|
1625
|
-
}))
|
|
1919
|
+
runProgress(session)
|
|
1626
1920
|
);
|
|
1627
1921
|
const settled = await runPool(
|
|
1628
1922
|
ready,
|
|
@@ -1639,13 +1933,34 @@ async function runWavesIsolated(opts) {
|
|
|
1639
1933
|
timeout: opts.timeout,
|
|
1640
1934
|
logsDir: opts.logsDir,
|
|
1641
1935
|
testCmd: opts.testCmd,
|
|
1936
|
+
installCmd: opts.installCmd,
|
|
1642
1937
|
maxTurns: opts.maxTurns,
|
|
1643
1938
|
effort: opts.effort,
|
|
1939
|
+
learningsPath: learningsPathFor(
|
|
1940
|
+
tracker.pathFor(t.feature)
|
|
1941
|
+
),
|
|
1644
1942
|
retries,
|
|
1645
1943
|
retryDelay,
|
|
1646
1944
|
d
|
|
1647
1945
|
})
|
|
1648
1946
|
);
|
|
1947
|
+
const touched = new Set(ready.map((t) => {
|
|
1948
|
+
return learningsPathFor(tracker.pathFor(t.feature));
|
|
1949
|
+
}));
|
|
1950
|
+
for (const path of touched) {
|
|
1951
|
+
if (!isRegularFile(path)) continue;
|
|
1952
|
+
try {
|
|
1953
|
+
await commitProgress(
|
|
1954
|
+
repoRoot,
|
|
1955
|
+
path,
|
|
1956
|
+
"nightralph: record attempt learnings"
|
|
1957
|
+
);
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
d.log(
|
|
1960
|
+
" Failed to commit learnings: " + formatErrorMessage(err)
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1649
1964
|
const outcomes = settled.map(
|
|
1650
1965
|
(r) => r.status === "fulfilled" ? r.value : { kind: "error", error: r.reason }
|
|
1651
1966
|
);
|
|
@@ -1749,7 +2064,7 @@ async function runWavesIsolated(opts) {
|
|
|
1749
2064
|
repoRoot,
|
|
1750
2065
|
tracker.pathFor(ticket.feature),
|
|
1751
2066
|
tracker.dataFor(ticket.feature),
|
|
1752
|
-
`nightralph: ${ticket.filename} ` + (outcome.reason
|
|
2067
|
+
`nightralph: ${ticket.filename} ` + failureNote(outcome.reason),
|
|
1753
2068
|
d
|
|
1754
2069
|
);
|
|
1755
2070
|
continue;
|
|
@@ -1785,6 +2100,7 @@ async function runWavesIsolated(opts) {
|
|
|
1785
2100
|
branches: successfulBranches,
|
|
1786
2101
|
strategy: opts.conflictStrategy
|
|
1787
2102
|
});
|
|
2103
|
+
const doneThisWave = [];
|
|
1788
2104
|
for (const branchResult of mergeResult.merged) {
|
|
1789
2105
|
const bk = branchToKey.get(
|
|
1790
2106
|
branchResult.branch
|
|
@@ -1793,6 +2109,7 @@ async function runWavesIsolated(opts) {
|
|
|
1793
2109
|
if (!ticket) continue;
|
|
1794
2110
|
if (branchResult.merged) {
|
|
1795
2111
|
markDone(ticket);
|
|
2112
|
+
doneThisWave.push(ticket);
|
|
1796
2113
|
waveCompleted++;
|
|
1797
2114
|
totalCompleted++;
|
|
1798
2115
|
d.setTicketStatus(
|
|
@@ -1842,11 +2159,13 @@ async function runWavesIsolated(opts) {
|
|
|
1842
2159
|
timeout: opts.timeout,
|
|
1843
2160
|
logsDir: opts.logsDir,
|
|
1844
2161
|
testCmd: opts.testCmd,
|
|
2162
|
+
installCmd: opts.installCmd,
|
|
1845
2163
|
maxTurns: opts.maxTurns,
|
|
1846
2164
|
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine")
|
|
1847
2165
|
}) : false;
|
|
1848
2166
|
if (recovered) {
|
|
1849
2167
|
markDone(ticket);
|
|
2168
|
+
doneThisWave.push(ticket);
|
|
1850
2169
|
waveCompleted++;
|
|
1851
2170
|
totalCompleted++;
|
|
1852
2171
|
d.setTicketStatus(
|
|
@@ -1884,6 +2203,11 @@ async function runWavesIsolated(opts) {
|
|
|
1884
2203
|
failedKeys.add(
|
|
1885
2204
|
ticketKey(ticket.feature, ticket.num)
|
|
1886
2205
|
);
|
|
2206
|
+
d.setTicketStatus(
|
|
2207
|
+
ticket.num,
|
|
2208
|
+
"failed",
|
|
2209
|
+
ticket.feature
|
|
2210
|
+
);
|
|
1887
2211
|
d.log(
|
|
1888
2212
|
` ${ticket.filename}: merge failed, staying ready-for-agent`
|
|
1889
2213
|
);
|
|
@@ -1897,6 +2221,28 @@ async function runWavesIsolated(opts) {
|
|
|
1897
2221
|
exitCode: 0,
|
|
1898
2222
|
done: false
|
|
1899
2223
|
});
|
|
2224
|
+
try {
|
|
2225
|
+
appendEntry(
|
|
2226
|
+
learningsPathFor(
|
|
2227
|
+
tracker.pathFor(ticket.feature)
|
|
2228
|
+
),
|
|
2229
|
+
renderEntry({
|
|
2230
|
+
outcome: "merge",
|
|
2231
|
+
key: ticketKey(
|
|
2232
|
+
ticket.feature,
|
|
2233
|
+
ticket.num
|
|
2234
|
+
),
|
|
2235
|
+
label: ticketLabel(ticket),
|
|
2236
|
+
attempt: 0,
|
|
2237
|
+
now: /* @__PURE__ */ new Date(),
|
|
2238
|
+
stderr: branchResult.stderr ?? ""
|
|
2239
|
+
})
|
|
2240
|
+
);
|
|
2241
|
+
} catch (err) {
|
|
2242
|
+
d.log(
|
|
2243
|
+
` ${ticket.filename}: could not record learnings: ` + formatErrorMessage(err)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
1900
2246
|
await saveProgress(
|
|
1901
2247
|
repoRoot,
|
|
1902
2248
|
tracker.pathFor(ticket.feature),
|
|
@@ -1907,6 +2253,7 @@ async function runWavesIsolated(opts) {
|
|
|
1907
2253
|
}
|
|
1908
2254
|
}
|
|
1909
2255
|
}
|
|
2256
|
+
await commitTicketFiles(repoRoot, doneThisWave, d);
|
|
1910
2257
|
if (mergeResult.stoppedAt) {
|
|
1911
2258
|
d.log(
|
|
1912
2259
|
`
|
|
@@ -1931,12 +2278,12 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1931
2278
|
}
|
|
1932
2279
|
}
|
|
1933
2280
|
if (waveCompleted === 0) {
|
|
1934
|
-
const
|
|
2281
|
+
const remaining2 = findReadyTickets(
|
|
1935
2282
|
opts.tickets
|
|
1936
2283
|
).filter((t) => !failedKeys.has(
|
|
1937
2284
|
ticketKey(t.feature, t.num)
|
|
1938
2285
|
));
|
|
1939
|
-
if (
|
|
2286
|
+
if (remaining2.length === 0) {
|
|
1940
2287
|
d.log(
|
|
1941
2288
|
"\nNo tickets completed this wave and no ready tickets remain. Stopping."
|
|
1942
2289
|
);
|
|
@@ -1944,10 +2291,46 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1944
2291
|
}
|
|
1945
2292
|
d.log(
|
|
1946
2293
|
`
|
|
1947
|
-
No tickets completed this wave. ${
|
|
2294
|
+
No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.length === 1 ? "" : "s"} remain; continuing.`
|
|
1948
2295
|
);
|
|
1949
2296
|
}
|
|
1950
2297
|
}
|
|
2298
|
+
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
2299
|
+
let reviewLine = null;
|
|
2300
|
+
if (opts.review) {
|
|
2301
|
+
try {
|
|
2302
|
+
const outcome = await runReviewPass({
|
|
2303
|
+
repoRoot,
|
|
2304
|
+
repoName,
|
|
2305
|
+
baseBranch,
|
|
2306
|
+
reviewPath: opts.review.reviewPath,
|
|
2307
|
+
forkedFrom: opts.review.forkedFrom,
|
|
2308
|
+
launchCommit,
|
|
2309
|
+
specBody: reviewSpecBody(
|
|
2310
|
+
opts.specBody,
|
|
2311
|
+
opts.specBodies
|
|
2312
|
+
),
|
|
2313
|
+
agentCmd: opts.agentCmd,
|
|
2314
|
+
model: opts.model,
|
|
2315
|
+
timeout: opts.timeout,
|
|
2316
|
+
logsDir: opts.logsDir,
|
|
2317
|
+
testCmd: opts.testCmd,
|
|
2318
|
+
installCmd: opts.installCmd,
|
|
2319
|
+
maxTurns: opts.maxTurns,
|
|
2320
|
+
effort: opts.effort,
|
|
2321
|
+
retries,
|
|
2322
|
+
retryDelay,
|
|
2323
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
2324
|
+
}, { spawnAgent, registry: runningAgents });
|
|
2325
|
+
reviewLine = renderReviewSummary(
|
|
2326
|
+
outcome,
|
|
2327
|
+
relative(repoRoot, opts.review.reviewPath)
|
|
2328
|
+
);
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
d.log(`
|
|
2331
|
+
Review failed: ${formatErrorMessage(err)}`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
1951
2334
|
const remaining = opts.tickets.filter(
|
|
1952
2335
|
(t) => t.status === "ready-for-agent"
|
|
1953
2336
|
);
|
|
@@ -1956,8 +2339,10 @@ No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.le
|
|
|
1956
2339
|
totalCompleted,
|
|
1957
2340
|
failedKeys
|
|
1958
2341
|
));
|
|
2342
|
+
if (reviewLine) d.log(reviewLine);
|
|
2343
|
+
d.log(`Work is on ${baseBranch}`);
|
|
1959
2344
|
d.cleanup();
|
|
1960
|
-
return { allDone: remaining.length === 0 };
|
|
2345
|
+
return { allDone: remaining.length === 0, branch: baseBranch };
|
|
1961
2346
|
}
|
|
1962
2347
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
1963
2348
|
export {
|
|
@@ -1966,7 +2351,7 @@ export {
|
|
|
1966
2351
|
DEFAULT_CONCURRENCY,
|
|
1967
2352
|
DEFAULT_RETRIES,
|
|
1968
2353
|
DEFAULT_RETRY_DELAY,
|
|
1969
|
-
EFFORT_LADDERS,
|
|
2354
|
+
EFFORT_LADDERS2 as EFFORT_LADDERS,
|
|
1970
2355
|
HUMAN_STATUS,
|
|
1971
2356
|
TICKET_RE,
|
|
1972
2357
|
TOOL_USE_RE,
|
|
@@ -1974,7 +2359,8 @@ export {
|
|
|
1974
2359
|
createPiStreamFormatter,
|
|
1975
2360
|
dryRun,
|
|
1976
2361
|
dryRunIsolated,
|
|
1977
|
-
escalateEffort,
|
|
2362
|
+
escalateEffort2 as escalateEffort,
|
|
2363
|
+
findBlockedTickets,
|
|
1978
2364
|
findReadyTickets,
|
|
1979
2365
|
formatErrorMessage,
|
|
1980
2366
|
formatStreamLine,
|
|
@@ -1989,7 +2375,7 @@ export {
|
|
|
1989
2375
|
renderFinishSummary,
|
|
1990
2376
|
renderMergePrompt,
|
|
1991
2377
|
renderPrompt,
|
|
1992
|
-
retryDelayFor,
|
|
2378
|
+
retryDelayFor2 as retryDelayFor,
|
|
1993
2379
|
runPool,
|
|
1994
2380
|
runWaves,
|
|
1995
2381
|
runWavesIsolated,
|