nightralph 0.0.53 → 0.0.56
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 +181 -19
- package/dist/blockers.js +149 -0
- package/dist/blockers.js.map +7 -0
- package/dist/display.js +12 -4
- package/dist/display.js.map +2 -2
- package/dist/docs-templates/issue-tracker.md +9 -1
- package/dist/index.js +2735 -415
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +360 -0
- package/dist/learnings.js.map +7 -0
- package/dist/meta.json +289 -16
- package/dist/orchestrator.js +753 -188
- 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/resolve.js +98 -9
- package/dist/resolve.js.map +2 -2
- package/dist/retry.js +83 -0
- package/dist/retry.js.map +7 -0
- package/dist/review.js +669 -0
- package/dist/review.js.map +7 -0
- package/dist/rules.js +67 -0
- package/dist/rules.js.map +7 -0
- package/dist/skills/to-tickets/SKILL.md +5 -5
- package/dist/src/blockers.d.ts +45 -0
- package/dist/src/blockers.d.ts.map +1 -0
- package/dist/src/display.d.ts +9 -0
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/learnings.d.ts +70 -0
- package/dist/src/learnings.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +85 -28
- 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/resolve.d.ts +24 -0
- package/dist/src/resolve.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 +131 -0
- package/dist/src/review.d.ts.map +1 -0
- package/dist/src/rules.d.ts +11 -0
- package/dist/src/rules.d.ts.map +1 -0
- package/dist/src/stats.d.ts +43 -0
- package/dist/src/stats.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/stats.js +111 -0
- package/dist/stats.js.map +7 -0
- 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 +3 -2
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,
|
|
@@ -45,6 +47,57 @@ import {
|
|
|
45
47
|
ticketLabel
|
|
46
48
|
} from "./display.js";
|
|
47
49
|
import { runTestCmd } from "./testcmd.js";
|
|
50
|
+
import {
|
|
51
|
+
readDocsRule,
|
|
52
|
+
installRule,
|
|
53
|
+
subagentRule,
|
|
54
|
+
testGateRule,
|
|
55
|
+
LINT_RULE,
|
|
56
|
+
integrityRule,
|
|
57
|
+
CONFINEMENT_RULES,
|
|
58
|
+
commitRule,
|
|
59
|
+
notesRule,
|
|
60
|
+
agentsMdRule
|
|
61
|
+
} from "./rules.js";
|
|
62
|
+
import { runReviewPass, renderReviewSummary } from "./review.js";
|
|
63
|
+
import {
|
|
64
|
+
addRun,
|
|
65
|
+
addStats,
|
|
66
|
+
emptyRunStats,
|
|
67
|
+
emptyStreamStats,
|
|
68
|
+
isRecord,
|
|
69
|
+
nextStreamStats,
|
|
70
|
+
renderRunStats
|
|
71
|
+
} from "./stats.js";
|
|
72
|
+
import {
|
|
73
|
+
EFFORT_LADDERS,
|
|
74
|
+
escalateEffort,
|
|
75
|
+
retryDelayFor,
|
|
76
|
+
runWithRetries,
|
|
77
|
+
sleep
|
|
78
|
+
} from "./retry.js";
|
|
79
|
+
import {
|
|
80
|
+
EFFORT_LADDERS as EFFORT_LADDERS2,
|
|
81
|
+
ALL_EFFORT_LEVELS,
|
|
82
|
+
escalateEffort as escalateEffort2,
|
|
83
|
+
retryDelayFor as retryDelayFor2
|
|
84
|
+
} from "./retry.js";
|
|
85
|
+
import {
|
|
86
|
+
LOG_TAIL_LINES,
|
|
87
|
+
appendEntry,
|
|
88
|
+
collectNotes,
|
|
89
|
+
ensureNotesIgnored,
|
|
90
|
+
learningsPathFor,
|
|
91
|
+
readEntries,
|
|
92
|
+
renderEntry,
|
|
93
|
+
renderHistory,
|
|
94
|
+
selectHistory,
|
|
95
|
+
tailLines
|
|
96
|
+
} from "./learnings.js";
|
|
97
|
+
import {
|
|
98
|
+
parseBlockerField,
|
|
99
|
+
provisionalRefs
|
|
100
|
+
} from "./blockers.js";
|
|
48
101
|
function formatErrorMessage(error) {
|
|
49
102
|
return error instanceof Error ? error.message : String(error);
|
|
50
103
|
}
|
|
@@ -113,21 +166,7 @@ function parseStatus(body) {
|
|
|
113
166
|
return m ? m[1].trim().toLowerCase() : "";
|
|
114
167
|
}
|
|
115
168
|
__name(parseStatus, "parseStatus");
|
|
116
|
-
function
|
|
117
|
-
const m = body.match(/\*\*Blocked by:\*\*\s*(.+)/i);
|
|
118
|
-
if (!m) return [];
|
|
119
|
-
const text = m[1];
|
|
120
|
-
if (/none/i.test(text)) return [];
|
|
121
|
-
const nums = [];
|
|
122
|
-
const re = /(\d+)/g;
|
|
123
|
-
let match;
|
|
124
|
-
while (match = re.exec(text)) {
|
|
125
|
-
nums.push(Number(match[1]));
|
|
126
|
-
}
|
|
127
|
-
return nums;
|
|
128
|
-
}
|
|
129
|
-
__name(parseBlockers, "parseBlockers");
|
|
130
|
-
function scanTickets(dir, feature = "") {
|
|
169
|
+
function scanTickets(dir, feature = "", knownFeatures = /* @__PURE__ */ new Set()) {
|
|
131
170
|
const files = readdirSync(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
132
171
|
b,
|
|
133
172
|
void 0,
|
|
@@ -144,7 +183,10 @@ function scanTickets(dir, feature = "") {
|
|
|
144
183
|
filepath,
|
|
145
184
|
body,
|
|
146
185
|
status: parseStatus(body),
|
|
147
|
-
blockers:
|
|
186
|
+
blockers: provisionalRefs(
|
|
187
|
+
parseBlockerField(body, knownFeatures).refs,
|
|
188
|
+
feature
|
|
189
|
+
),
|
|
148
190
|
feature
|
|
149
191
|
};
|
|
150
192
|
});
|
|
@@ -154,15 +196,35 @@ function ticketKey(feature, num) {
|
|
|
154
196
|
return `${feature}:${num}`;
|
|
155
197
|
}
|
|
156
198
|
__name(ticketKey, "ticketKey");
|
|
199
|
+
function blockerSatisfied(ref, doneKeys) {
|
|
200
|
+
switch (ref.where) {
|
|
201
|
+
case "run":
|
|
202
|
+
return doneKeys.has(ticketKey(ref.feature, ref.num));
|
|
203
|
+
case "done":
|
|
204
|
+
return true;
|
|
205
|
+
case "pending":
|
|
206
|
+
case "missing":
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
__name(blockerSatisfied, "blockerSatisfied");
|
|
211
|
+
function blockerLabel(ref) {
|
|
212
|
+
const nn = String(ref.num).padStart(2, "0");
|
|
213
|
+
const base = ref.feature ? `${ref.feature}/${nn}` : nn;
|
|
214
|
+
if (ref.where === "pending") {
|
|
215
|
+
return `${base} (${ref.status ?? "not done"}, not in run)`;
|
|
216
|
+
}
|
|
217
|
+
if (ref.where === "missing") return `${base} (not found)`;
|
|
218
|
+
return base;
|
|
219
|
+
}
|
|
220
|
+
__name(blockerLabel, "blockerLabel");
|
|
157
221
|
function findReadyTickets(tickets) {
|
|
158
222
|
const doneKeys = new Set(
|
|
159
223
|
tickets.filter((t) => t.status === "done").map((t) => ticketKey(t.feature, t.num))
|
|
160
224
|
);
|
|
161
225
|
return tickets.filter((t) => {
|
|
162
226
|
if (t.status !== "ready-for-agent") return false;
|
|
163
|
-
return t.blockers.every(
|
|
164
|
-
(b) => doneKeys.has(ticketKey(t.feature, b))
|
|
165
|
-
);
|
|
227
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
166
228
|
});
|
|
167
229
|
}
|
|
168
230
|
__name(findReadyTickets, "findReadyTickets");
|
|
@@ -171,14 +233,19 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
171
233
|
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
172
234
|
);
|
|
173
235
|
function collectDead(ticket, visited, found) {
|
|
174
|
-
for (const
|
|
175
|
-
const key = ticketKey(
|
|
236
|
+
for (const ref of ticket.blockers) {
|
|
237
|
+
const key = ticketKey(ref.feature, ref.num);
|
|
176
238
|
if (visited.has(key)) continue;
|
|
177
239
|
visited.add(key);
|
|
240
|
+
if (ref.where === "done") continue;
|
|
241
|
+
if (ref.where === "pending" || ref.where === "missing") {
|
|
242
|
+
found.set(key, ref);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
178
245
|
const blocker = byKey.get(key);
|
|
179
246
|
if (!blocker || blocker.status === "done") continue;
|
|
180
247
|
if (failedKeys.has(key) || blocker.status !== "ready-for-agent") {
|
|
181
|
-
found.
|
|
248
|
+
found.set(key, ref);
|
|
182
249
|
continue;
|
|
183
250
|
}
|
|
184
251
|
collectDead(blocker, visited, found);
|
|
@@ -190,9 +257,9 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
190
257
|
const key = ticketKey(t.feature, t.num);
|
|
191
258
|
if (t.status !== "ready-for-agent") continue;
|
|
192
259
|
if (failedKeys.has(key)) continue;
|
|
193
|
-
const found = /* @__PURE__ */ new
|
|
260
|
+
const found = /* @__PURE__ */ new Map();
|
|
194
261
|
collectDead(t, /* @__PURE__ */ new Set([key]), found);
|
|
195
|
-
if (found.size > 0) blocked.set(key, [...found]);
|
|
262
|
+
if (found.size > 0) blocked.set(key, [...found.values()]);
|
|
196
263
|
}
|
|
197
264
|
return blocked;
|
|
198
265
|
}
|
|
@@ -337,11 +404,23 @@ function stripSpecSection(specBody, heading) {
|
|
|
337
404
|
return out.join("\n");
|
|
338
405
|
}
|
|
339
406
|
__name(stripSpecSection, "stripSpecSection");
|
|
407
|
+
function reviewSpecBody(specBody, specBodies) {
|
|
408
|
+
if (!specBodies || specBodies.size <= 1) return specBody;
|
|
409
|
+
return [...specBodies].map(([name, body]) => `# Feature: ${name}
|
|
410
|
+
|
|
411
|
+
${body}`).join("\n\n");
|
|
412
|
+
}
|
|
413
|
+
__name(reviewSpecBody, "reviewSpecBody");
|
|
340
414
|
function lookupSpec(specBodies, fallback, feature) {
|
|
341
415
|
if (!feature || !specBodies) return fallback;
|
|
342
416
|
return specBodies.get(feature) ?? fallback;
|
|
343
417
|
}
|
|
344
418
|
__name(lookupSpec, "lookupSpec");
|
|
419
|
+
function historyBlock(history) {
|
|
420
|
+
if (!history) return [];
|
|
421
|
+
return [history, ""];
|
|
422
|
+
}
|
|
423
|
+
__name(historyBlock, "historyBlock");
|
|
345
424
|
function renderPrompt(specBody, ticket, opts) {
|
|
346
425
|
return [
|
|
347
426
|
"---- SPEC CONTEXT ----",
|
|
@@ -351,27 +430,30 @@ function renderPrompt(specBody, ticket, opts) {
|
|
|
351
430
|
ticket.filename,
|
|
352
431
|
ticket.body,
|
|
353
432
|
"",
|
|
433
|
+
...historyBlock(opts.history),
|
|
354
434
|
"---- AGENT INSTRUCTIONS ----",
|
|
355
435
|
"You are an autonomous coding agent. Implement the ticket using /tdd.",
|
|
356
436
|
"",
|
|
357
437
|
"Follow these rules:",
|
|
358
|
-
"
|
|
438
|
+
readDocsRule("ticket"),
|
|
359
439
|
"- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
|
|
360
440
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"
|
|
367
|
-
|
|
441
|
+
...installRule(opts.installCmd),
|
|
442
|
+
...subagentRule(opts.preferSubagent),
|
|
443
|
+
"- 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.",
|
|
444
|
+
testGateRule(opts.testCmd, "ticket"),
|
|
445
|
+
LINT_RULE,
|
|
446
|
+
integrityRule("checklist item"),
|
|
447
|
+
...CONFINEMENT_RULES,
|
|
448
|
+
commitRule("checklist item"),
|
|
449
|
+
notesRule("checklist item", "ticket"),
|
|
368
450
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
369
|
-
"
|
|
451
|
+
agentsMdRule("ticket"),
|
|
370
452
|
""
|
|
371
453
|
].join("\n");
|
|
372
454
|
}
|
|
373
455
|
__name(renderPrompt, "renderPrompt");
|
|
374
|
-
function renderMergePrompt(specBody, ticket) {
|
|
456
|
+
function renderMergePrompt(specBody, ticket, opts = {}) {
|
|
375
457
|
return [
|
|
376
458
|
"---- SPEC CONTEXT ----",
|
|
377
459
|
specBody,
|
|
@@ -392,6 +474,8 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
392
474
|
"4. Commit the resolved files.",
|
|
393
475
|
"",
|
|
394
476
|
"Do NOT re-implement the ticket from scratch.",
|
|
477
|
+
...installRule(opts.installCmd),
|
|
478
|
+
...subagentRule(opts.preferSubagent),
|
|
395
479
|
"Do NOT read or modify files outside your current directory (the worktree).",
|
|
396
480
|
"Do NOT run git push."
|
|
397
481
|
].join("\n");
|
|
@@ -400,7 +484,7 @@ __name(renderMergePrompt, "renderMergePrompt");
|
|
|
400
484
|
function listTickets(tickets) {
|
|
401
485
|
console.log("\nTickets:");
|
|
402
486
|
for (const t of tickets) {
|
|
403
|
-
const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.join(", ")}]` : "no blockers";
|
|
487
|
+
const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.map(blockerLabel).join(", ")}]` : "no blockers";
|
|
404
488
|
console.log(
|
|
405
489
|
` ${t.filename} (${t.status}) ${blockerStr}`
|
|
406
490
|
);
|
|
@@ -424,11 +508,7 @@ function* simulateWaves(tickets) {
|
|
|
424
508
|
if (sim.get(key) !== "ready-for-agent") {
|
|
425
509
|
return false;
|
|
426
510
|
}
|
|
427
|
-
return t.blockers.every(
|
|
428
|
-
(b) => doneKeys.has(
|
|
429
|
-
ticketKey(t.feature, b)
|
|
430
|
-
)
|
|
431
|
-
);
|
|
511
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
432
512
|
});
|
|
433
513
|
if (ready.length === 0) break;
|
|
434
514
|
yield { wave, ready };
|
|
@@ -461,9 +541,7 @@ function detectDeadlocks(tickets) {
|
|
|
461
541
|
(t) => ticketKey(t.feature, t.num) === key
|
|
462
542
|
);
|
|
463
543
|
if (ticket.blockers.every(
|
|
464
|
-
(
|
|
465
|
-
ticketKey(ticket.feature, b)
|
|
466
|
-
)
|
|
544
|
+
(r) => blockerSatisfied(r, doneKeys)
|
|
467
545
|
)) {
|
|
468
546
|
doneKeys.add(key);
|
|
469
547
|
readyKeys.delete(key);
|
|
@@ -505,76 +583,47 @@ function getProviderArgs(cmd) {
|
|
|
505
583
|
return PROVIDER_ARGS[name] ?? [];
|
|
506
584
|
}
|
|
507
585
|
__name(getProviderArgs, "getProviderArgs");
|
|
508
|
-
const EFFORT_LADDERS = {
|
|
509
|
-
pi: [
|
|
510
|
-
"off",
|
|
511
|
-
"minimal",
|
|
512
|
-
"low",
|
|
513
|
-
"medium",
|
|
514
|
-
"high",
|
|
515
|
-
"xhigh",
|
|
516
|
-
"max"
|
|
517
|
-
],
|
|
518
|
-
claude: [
|
|
519
|
-
"low",
|
|
520
|
-
"medium",
|
|
521
|
-
"high",
|
|
522
|
-
"xhigh",
|
|
523
|
-
"max"
|
|
524
|
-
],
|
|
525
|
-
codex: ["low", "medium", "high"]
|
|
526
|
-
};
|
|
527
|
-
const ALL_EFFORT_LEVELS = [
|
|
528
|
-
"off",
|
|
529
|
-
"minimal",
|
|
530
|
-
"low",
|
|
531
|
-
"medium",
|
|
532
|
-
"high",
|
|
533
|
-
"xhigh",
|
|
534
|
-
"max"
|
|
535
|
-
];
|
|
536
|
-
function escalateEffort(provider, level) {
|
|
537
|
-
if (!level) return "high";
|
|
538
|
-
const ladder = EFFORT_LADDERS[provider];
|
|
539
|
-
const i = ladder.indexOf(level);
|
|
540
|
-
if (i === -1) return level;
|
|
541
|
-
return ladder[Math.min(i + 1, ladder.length - 1)];
|
|
542
|
-
}
|
|
543
|
-
__name(escalateEffort, "escalateEffort");
|
|
544
586
|
function formatStreamLine(line) {
|
|
545
|
-
let
|
|
587
|
+
let value;
|
|
546
588
|
try {
|
|
547
|
-
|
|
589
|
+
value = JSON.parse(line);
|
|
548
590
|
} catch {
|
|
549
591
|
return line;
|
|
550
592
|
}
|
|
593
|
+
return formatStreamObject(value);
|
|
594
|
+
}
|
|
595
|
+
__name(formatStreamLine, "formatStreamLine");
|
|
596
|
+
function formatStreamObject(value) {
|
|
597
|
+
if (!isRecord(value)) return null;
|
|
598
|
+
const obj = value;
|
|
551
599
|
if (obj.type === "assistant") {
|
|
552
|
-
|
|
553
|
-
const
|
|
600
|
+
if (obj.parent_tool_use_id != null) return null;
|
|
601
|
+
const msg = isRecord(obj.message) ? obj.message : void 0;
|
|
602
|
+
const content = Array.isArray(msg?.content) ? msg.content : void 0;
|
|
554
603
|
if (!content) return null;
|
|
555
604
|
for (const block of content) {
|
|
556
|
-
if (block
|
|
557
|
-
|
|
558
|
-
|
|
605
|
+
if (!isRecord(block)) continue;
|
|
606
|
+
if (block.type !== "text") continue;
|
|
607
|
+
if (typeof block.text === "string") return block.text;
|
|
559
608
|
}
|
|
560
609
|
return null;
|
|
561
610
|
}
|
|
562
611
|
if (obj.type === "result") {
|
|
563
|
-
const sub = obj.subtype
|
|
564
|
-
const cost = obj.total_cost_usd;
|
|
565
|
-
const costStr = cost
|
|
612
|
+
const sub = typeof obj.subtype === "string" ? obj.subtype : "done";
|
|
613
|
+
const cost = typeof obj.total_cost_usd === "number" ? obj.total_cost_usd : void 0;
|
|
614
|
+
const costStr = cost !== void 0 ? ` ($${cost.toFixed(2)})` : "";
|
|
566
615
|
return `${sub}${costStr}`;
|
|
567
616
|
}
|
|
568
617
|
if (obj.type === "system") {
|
|
569
|
-
const sub = obj.subtype;
|
|
618
|
+
const sub = typeof obj.subtype === "string" ? obj.subtype : void 0;
|
|
570
619
|
if (sub === "api_retry") {
|
|
571
|
-
const error = obj.error;
|
|
572
|
-
return `retry: ${error
|
|
620
|
+
const error = typeof obj.error === "string" ? obj.error : "unknown";
|
|
621
|
+
return `retry: ${error}`;
|
|
573
622
|
}
|
|
574
623
|
}
|
|
575
624
|
return null;
|
|
576
625
|
}
|
|
577
|
-
__name(
|
|
626
|
+
__name(formatStreamObject, "formatStreamObject");
|
|
578
627
|
function createPiStreamFormatter() {
|
|
579
628
|
const calls = /* @__PURE__ */ new Map();
|
|
580
629
|
let textBuf = "";
|
|
@@ -727,10 +776,15 @@ function spawnAgent(opts) {
|
|
|
727
776
|
opts.logPath,
|
|
728
777
|
{ flags: "w" }
|
|
729
778
|
);
|
|
779
|
+
const env = isClaude && opts.preferSubagent ? {
|
|
780
|
+
...process.env,
|
|
781
|
+
CLAUDE_CODE_SUBAGENT_MODEL: opts.preferSubagent
|
|
782
|
+
} : void 0;
|
|
730
783
|
const proc = spawn(opts.agentCmd, args, {
|
|
731
784
|
stdio: ["pipe", "pipe", "pipe"],
|
|
732
785
|
cwd: opts.cwd,
|
|
733
|
-
detached: true
|
|
786
|
+
detached: true,
|
|
787
|
+
...env ? { env } : {}
|
|
734
788
|
});
|
|
735
789
|
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
736
790
|
proc.stdout.pipe(logStream, { end: false });
|
|
@@ -748,10 +802,26 @@ function spawnAgent(opts) {
|
|
|
748
802
|
}
|
|
749
803
|
}
|
|
750
804
|
__name(collectCompleted, "collectCompleted");
|
|
805
|
+
let streamStats = emptyStreamStats();
|
|
806
|
+
function formatClaudeLine(line) {
|
|
807
|
+
let obj;
|
|
808
|
+
try {
|
|
809
|
+
obj = JSON.parse(line);
|
|
810
|
+
} catch {
|
|
811
|
+
return line;
|
|
812
|
+
}
|
|
813
|
+
const next = nextStreamStats(streamStats, obj);
|
|
814
|
+
if (next !== streamStats) {
|
|
815
|
+
streamStats = next;
|
|
816
|
+
opts.onStats?.(next.stats);
|
|
817
|
+
}
|
|
818
|
+
return formatStreamObject(obj);
|
|
819
|
+
}
|
|
820
|
+
__name(formatClaudeLine, "formatClaudeLine");
|
|
751
821
|
const rl = createInterface({ input: proc.stdout });
|
|
752
822
|
rl.on("line", (line) => {
|
|
753
823
|
if (isClaude) {
|
|
754
|
-
const formatted =
|
|
824
|
+
const formatted = formatClaudeLine(line);
|
|
755
825
|
if (formatted !== null) {
|
|
756
826
|
collectCompleted(formatted);
|
|
757
827
|
const output = `${prefix}${formatted}`;
|
|
@@ -843,7 +913,12 @@ function spawnAgent(opts) {
|
|
|
843
913
|
rlErr.close();
|
|
844
914
|
logStream.end();
|
|
845
915
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
846
|
-
resolve({
|
|
916
|
+
resolve({
|
|
917
|
+
exitCode,
|
|
918
|
+
completedItems,
|
|
919
|
+
timedOut,
|
|
920
|
+
...isClaude ? streamStats.stats : {}
|
|
921
|
+
});
|
|
847
922
|
}
|
|
848
923
|
__name(finish, "finish");
|
|
849
924
|
proc.on("exit", (code) => {
|
|
@@ -867,22 +942,27 @@ function spawnAgent(opts) {
|
|
|
867
942
|
}
|
|
868
943
|
rl.close();
|
|
869
944
|
rlErr.close();
|
|
870
|
-
|
|
945
|
+
const msg = "agent process error: " + formatErrorMessage(err);
|
|
946
|
+
logStream.write(`nightralph: ${msg}
|
|
947
|
+
`);
|
|
871
948
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
949
|
+
if (opts.onLine) {
|
|
950
|
+
opts.onLine(`${prefix}${msg}`);
|
|
951
|
+
} else {
|
|
952
|
+
console.error(`${prefix}${msg}`);
|
|
953
|
+
}
|
|
954
|
+
logStream.end(() => {
|
|
955
|
+
resolve({
|
|
956
|
+
exitCode: 1,
|
|
957
|
+
completedItems,
|
|
958
|
+
timedOut
|
|
959
|
+
});
|
|
880
960
|
});
|
|
881
961
|
});
|
|
882
962
|
});
|
|
883
963
|
}
|
|
884
964
|
__name(spawnAgent, "spawnAgent");
|
|
885
|
-
function dryRun(tickets, specBody, testCmd) {
|
|
965
|
+
function dryRun(tickets, specBody, testCmd, preferSubagent) {
|
|
886
966
|
listTickets(tickets);
|
|
887
967
|
console.log("\nSimulated wave order:");
|
|
888
968
|
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
@@ -896,6 +976,9 @@ function dryRun(tickets, specBody, testCmd) {
|
|
|
896
976
|
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
897
977
|
);
|
|
898
978
|
}
|
|
979
|
+
console.log(`
|
|
980
|
+
Test command: ${testCmd || "(none)"}`);
|
|
981
|
+
console.log(`Prefer subagent: ${preferSubagent || "(none)"}`);
|
|
899
982
|
const firstReady = findReadyTickets(tickets);
|
|
900
983
|
if (firstReady.length > 0) {
|
|
901
984
|
console.log(
|
|
@@ -907,7 +990,7 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
907
990
|
renderPrompt(
|
|
908
991
|
specBody,
|
|
909
992
|
firstReady[0],
|
|
910
|
-
{ testCmd }
|
|
993
|
+
{ testCmd, preferSubagent }
|
|
911
994
|
)
|
|
912
995
|
);
|
|
913
996
|
}
|
|
@@ -951,18 +1034,28 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
951
1034
|
renderPrompt(
|
|
952
1035
|
opts.specBody,
|
|
953
1036
|
firstReady[0],
|
|
954
|
-
{
|
|
1037
|
+
{
|
|
1038
|
+
testCmd: opts.testCmd,
|
|
1039
|
+
installCmd: opts.installCmd,
|
|
1040
|
+
preferSubagent: opts.preferSubagent
|
|
1041
|
+
}
|
|
955
1042
|
)
|
|
956
1043
|
);
|
|
957
1044
|
}
|
|
958
1045
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
959
1046
|
console.log(
|
|
960
1047
|
`
|
|
961
|
-
Test command: ${opts.testCmd}`
|
|
1048
|
+
Test command: ${opts.testCmd || "(none)"}`
|
|
1049
|
+
);
|
|
1050
|
+
console.log(
|
|
1051
|
+
`Install: ${opts.installCmd || "(none)"}`
|
|
962
1052
|
);
|
|
963
1053
|
console.log(
|
|
964
1054
|
`Effort: ${opts.effort ?? "(provider default)"}`
|
|
965
1055
|
);
|
|
1056
|
+
console.log(
|
|
1057
|
+
`Prefer subagent: ${opts.preferSubagent || "(none)"}`
|
|
1058
|
+
);
|
|
966
1059
|
console.log(
|
|
967
1060
|
`Retries: ${opts.retries ?? DEFAULT_RETRIES}`
|
|
968
1061
|
);
|
|
@@ -979,7 +1072,34 @@ Merge strategy: ${strategyDesc}`
|
|
|
979
1072
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
980
1073
|
}
|
|
981
1074
|
__name(dryRunIsolated, "dryRunIsolated");
|
|
1075
|
+
async function runInstall(opts) {
|
|
1076
|
+
const result = await runTestCmd({
|
|
1077
|
+
cmd: opts.installCmd,
|
|
1078
|
+
cwd: opts.worktreePath,
|
|
1079
|
+
timeout: opts.timeout,
|
|
1080
|
+
registry: opts.registry
|
|
1081
|
+
});
|
|
1082
|
+
const installLogPath = attemptLogPath(
|
|
1083
|
+
opts.logsDir,
|
|
1084
|
+
opts.ticket,
|
|
1085
|
+
opts.attempt ?? 1,
|
|
1086
|
+
".install.log"
|
|
1087
|
+
);
|
|
1088
|
+
writeFileSync(installLogPath, result.output);
|
|
1089
|
+
if (result.exitCode !== 0) {
|
|
1090
|
+
opts.log(
|
|
1091
|
+
` ${opts.ticket.filename}: install failed (exit ${result.exitCode}); see ${installLogPath}`
|
|
1092
|
+
);
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
__name(runInstall, "runInstall");
|
|
982
1098
|
async function runTestGate(opts) {
|
|
1099
|
+
if (opts.testCmd === "") {
|
|
1100
|
+
opts.log(` ${opts.ticket.filename}: no test gate`);
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
983
1103
|
const result = await runTestCmd({
|
|
984
1104
|
cmd: opts.testCmd,
|
|
985
1105
|
cwd: opts.worktreePath,
|
|
@@ -1021,9 +1141,25 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1021
1141
|
} else {
|
|
1022
1142
|
console.log(msg1);
|
|
1023
1143
|
}
|
|
1144
|
+
if (opts.installCmd) {
|
|
1145
|
+
const installed = await runInstall({
|
|
1146
|
+
installCmd: opts.installCmd,
|
|
1147
|
+
ticket: opts.ticket,
|
|
1148
|
+
worktreePath: opts.worktreePath,
|
|
1149
|
+
timeout: opts.timeout,
|
|
1150
|
+
logsDir: opts.logsDir,
|
|
1151
|
+
log,
|
|
1152
|
+
registry: runningAgents
|
|
1153
|
+
});
|
|
1154
|
+
if (!installed) return false;
|
|
1155
|
+
}
|
|
1024
1156
|
const prompt = renderMergePrompt(
|
|
1025
1157
|
opts.specBody,
|
|
1026
|
-
opts.ticket
|
|
1158
|
+
opts.ticket,
|
|
1159
|
+
{
|
|
1160
|
+
installCmd: opts.installCmd,
|
|
1161
|
+
preferSubagent: opts.preferSubagent
|
|
1162
|
+
}
|
|
1027
1163
|
);
|
|
1028
1164
|
const logPath = join(
|
|
1029
1165
|
opts.logsDir,
|
|
@@ -1042,8 +1178,10 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1042
1178
|
cwd: opts.worktreePath,
|
|
1043
1179
|
label,
|
|
1044
1180
|
maxTurns: opts.maxTurns,
|
|
1181
|
+
preferSubagent: opts.preferSubagent,
|
|
1045
1182
|
onLine: opts.onLine
|
|
1046
1183
|
});
|
|
1184
|
+
opts.onAgentRun?.(result);
|
|
1047
1185
|
if (result.exitCode !== 0) {
|
|
1048
1186
|
const msg2 = ` ${opts.ticket.filename}: respawned agent exited ${result.exitCode}`;
|
|
1049
1187
|
if (opts.onLine) {
|
|
@@ -1122,20 +1260,43 @@ async function runPool(items, limit, fn) {
|
|
|
1122
1260
|
return results;
|
|
1123
1261
|
}
|
|
1124
1262
|
__name(runPool, "runPool");
|
|
1125
|
-
function retryDelayFor(base, attempt) {
|
|
1126
|
-
return base * 2 ** (attempt - 2);
|
|
1127
|
-
}
|
|
1128
|
-
__name(retryDelayFor, "retryDelayFor");
|
|
1129
|
-
function sleep(ms) {
|
|
1130
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1131
|
-
}
|
|
1132
|
-
__name(sleep, "sleep");
|
|
1133
1263
|
function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
1134
1264
|
const stem = ticket.filename.replace(/\.md$/, "");
|
|
1135
1265
|
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
1136
1266
|
return join(logsDir, `${stem}${suffix}${ext}`);
|
|
1137
1267
|
}
|
|
1138
1268
|
__name(attemptLogPath, "attemptLogPath");
|
|
1269
|
+
function historyFor(learningsPath, ticket) {
|
|
1270
|
+
const featureDir = dirname(dirname(ticket.filepath));
|
|
1271
|
+
const scratchDir = ticket.feature ? join(featureDir, ...ticket.feature.split("/").map(() => "..")) : dirname(featureDir);
|
|
1272
|
+
const others = new Set(
|
|
1273
|
+
ticket.blockers.map((r) => r.feature).filter((f) => f !== ticket.feature)
|
|
1274
|
+
);
|
|
1275
|
+
const entries = [
|
|
1276
|
+
...readEntries(learningsPath),
|
|
1277
|
+
...[...others].flatMap((f) => readEntries(
|
|
1278
|
+
join(scratchDir, f, "learnings.md")
|
|
1279
|
+
))
|
|
1280
|
+
];
|
|
1281
|
+
return renderHistory(selectHistory(entries, {
|
|
1282
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
1283
|
+
blockerKeys: ticket.blockers.map((r) => {
|
|
1284
|
+
return ticketKey(r.feature, r.num);
|
|
1285
|
+
})
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1288
|
+
__name(historyFor, "historyFor");
|
|
1289
|
+
function buildHistory(learningsPath, ticket, d) {
|
|
1290
|
+
try {
|
|
1291
|
+
return historyFor(learningsPath, ticket);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
d.log(
|
|
1294
|
+
` ${ticket.filename}: could not read learnings: ${formatErrorMessage(err)}`
|
|
1295
|
+
);
|
|
1296
|
+
return "";
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
__name(buildHistory, "buildHistory");
|
|
1139
1300
|
async function runTicketAttempt(opts) {
|
|
1140
1301
|
const { ticket, d } = opts;
|
|
1141
1302
|
let worktree;
|
|
@@ -1154,6 +1315,38 @@ async function runTicketAttempt(opts) {
|
|
|
1154
1315
|
"in-progress",
|
|
1155
1316
|
ticket.feature
|
|
1156
1317
|
);
|
|
1318
|
+
if (opts.installCmd) {
|
|
1319
|
+
const installed = await runInstall({
|
|
1320
|
+
installCmd: opts.installCmd,
|
|
1321
|
+
ticket,
|
|
1322
|
+
worktreePath: worktree.worktreePath,
|
|
1323
|
+
timeout: opts.timeout,
|
|
1324
|
+
logsDir: opts.logsDir,
|
|
1325
|
+
attempt: opts.attempt,
|
|
1326
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log"),
|
|
1327
|
+
registry: runningAgents
|
|
1328
|
+
});
|
|
1329
|
+
if (!installed) {
|
|
1330
|
+
await removeWorktreeQuietly(
|
|
1331
|
+
worktree.worktreePath,
|
|
1332
|
+
d
|
|
1333
|
+
);
|
|
1334
|
+
return {
|
|
1335
|
+
kind: "failed",
|
|
1336
|
+
reason: "install",
|
|
1337
|
+
branchName: worktree.branchName,
|
|
1338
|
+
exitCode: 1,
|
|
1339
|
+
completedItems: []
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
ensureNotesIgnored(worktree.worktreePath);
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
d.log(
|
|
1347
|
+
` ${ticket.filename}: could not set up the notes directory: ${formatErrorMessage(err)}`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1157
1350
|
const ticketSpec = lookupSpec(
|
|
1158
1351
|
opts.specBodies,
|
|
1159
1352
|
opts.specBody,
|
|
@@ -1163,7 +1356,18 @@ async function runTicketAttempt(opts) {
|
|
|
1163
1356
|
prompt: renderPrompt(
|
|
1164
1357
|
ticketSpec,
|
|
1165
1358
|
ticket,
|
|
1166
|
-
{
|
|
1359
|
+
{
|
|
1360
|
+
testCmd: opts.testCmd,
|
|
1361
|
+
installCmd: opts.installCmd,
|
|
1362
|
+
// Rebuilt every attempt, so a retry sees the
|
|
1363
|
+
// entry its previous attempt just wrote
|
|
1364
|
+
history: buildHistory(
|
|
1365
|
+
opts.learningsPath,
|
|
1366
|
+
ticket,
|
|
1367
|
+
d
|
|
1368
|
+
),
|
|
1369
|
+
preferSubagent: opts.preferSubagent
|
|
1370
|
+
}
|
|
1167
1371
|
),
|
|
1168
1372
|
agentCmd: opts.agentCmd,
|
|
1169
1373
|
model: opts.model,
|
|
@@ -1178,8 +1382,27 @@ async function runTicketAttempt(opts) {
|
|
|
1178
1382
|
label: ticket.filename.replace(/\.md$/, ""),
|
|
1179
1383
|
maxTurns: opts.maxTurns,
|
|
1180
1384
|
effort: opts.effort,
|
|
1385
|
+
preferSubagent: opts.preferSubagent,
|
|
1386
|
+
onStats: opts.onStats,
|
|
1181
1387
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1182
1388
|
});
|
|
1389
|
+
opts.onAgentRun?.(agentResult);
|
|
1390
|
+
let notes = null;
|
|
1391
|
+
try {
|
|
1392
|
+
notes = collectNotes(worktree.worktreePath);
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
d.log(
|
|
1395
|
+
` ${ticket.filename}: could not collect notes: ${formatErrorMessage(err)}`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const agentRun = {
|
|
1399
|
+
exitCode: agentResult.exitCode,
|
|
1400
|
+
completedItems: agentResult.completedItems,
|
|
1401
|
+
timedOut: agentResult.timedOut,
|
|
1402
|
+
notes,
|
|
1403
|
+
...agentResult.subagents === void 0 ? {} : { subagents: agentResult.subagents },
|
|
1404
|
+
...agentResult.costUsd === void 0 ? {} : { costUsd: agentResult.costUsd }
|
|
1405
|
+
};
|
|
1183
1406
|
if (agentResult.timedOut) {
|
|
1184
1407
|
d.log(
|
|
1185
1408
|
` ${ticket.filename}: timed out; keeping the work it committed`
|
|
@@ -1194,7 +1417,8 @@ async function runTicketAttempt(opts) {
|
|
|
1194
1417
|
reason: "exit",
|
|
1195
1418
|
branchName: worktree.branchName,
|
|
1196
1419
|
exitCode: agentResult.exitCode,
|
|
1197
|
-
completedItems: agentResult.completedItems
|
|
1420
|
+
completedItems: agentResult.completedItems,
|
|
1421
|
+
agentRun
|
|
1198
1422
|
};
|
|
1199
1423
|
}
|
|
1200
1424
|
if (isDirty(worktree.worktreePath)) {
|
|
@@ -1217,7 +1441,8 @@ async function runTicketAttempt(opts) {
|
|
|
1217
1441
|
return {
|
|
1218
1442
|
kind: "no-commits",
|
|
1219
1443
|
worktreePath: worktree.worktreePath,
|
|
1220
|
-
branchName: worktree.branchName
|
|
1444
|
+
branchName: worktree.branchName,
|
|
1445
|
+
agentRun
|
|
1221
1446
|
};
|
|
1222
1447
|
}
|
|
1223
1448
|
const passed = await runTestGate({
|
|
@@ -1240,73 +1465,189 @@ async function runTicketAttempt(opts) {
|
|
|
1240
1465
|
reason: "tests",
|
|
1241
1466
|
branchName: worktree.branchName,
|
|
1242
1467
|
exitCode: 1,
|
|
1243
|
-
completedItems: agentResult.completedItems
|
|
1468
|
+
completedItems: agentResult.completedItems,
|
|
1469
|
+
agentRun
|
|
1244
1470
|
};
|
|
1245
1471
|
}
|
|
1246
1472
|
return {
|
|
1247
1473
|
kind: "ready",
|
|
1248
1474
|
worktreePath: worktree.worktreePath,
|
|
1249
1475
|
branchName: worktree.branchName,
|
|
1250
|
-
completedItems: agentResult.completedItems
|
|
1476
|
+
completedItems: agentResult.completedItems,
|
|
1477
|
+
agentRun
|
|
1251
1478
|
};
|
|
1252
1479
|
}
|
|
1253
1480
|
__name(runTicketAttempt, "runTicketAttempt");
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1481
|
+
function readLogTail(path) {
|
|
1482
|
+
try {
|
|
1483
|
+
return tailLines(readFileSync(path, "utf8"), LOG_TAIL_LINES);
|
|
1484
|
+
} catch {
|
|
1485
|
+
return "";
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
__name(readLogTail, "readLogTail");
|
|
1489
|
+
function attemptEntry(opts) {
|
|
1490
|
+
const { logsDir, ticket, attempt, effort, outcome } = opts;
|
|
1491
|
+
const base = {
|
|
1492
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
1493
|
+
label: ticketLabel(ticket),
|
|
1494
|
+
attempt,
|
|
1495
|
+
now: /* @__PURE__ */ new Date()
|
|
1264
1496
|
};
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1497
|
+
const logFor = /* @__PURE__ */ __name((ext) => {
|
|
1498
|
+
const path = attemptLogPath(logsDir, ticket, attempt, ext);
|
|
1499
|
+
return { logName: basename(path), logTail: readLogTail(path) };
|
|
1500
|
+
}, "logFor");
|
|
1501
|
+
switch (outcome.kind) {
|
|
1502
|
+
case "ready":
|
|
1503
|
+
return {
|
|
1504
|
+
...base,
|
|
1505
|
+
outcome: "done",
|
|
1506
|
+
run: outcome.agentRun,
|
|
1507
|
+
effort
|
|
1508
|
+
};
|
|
1509
|
+
case "failed":
|
|
1510
|
+
switch (outcome.reason) {
|
|
1511
|
+
case "exit":
|
|
1512
|
+
return {
|
|
1513
|
+
...base,
|
|
1514
|
+
outcome: "exit",
|
|
1515
|
+
run: outcome.agentRun,
|
|
1516
|
+
effort
|
|
1517
|
+
};
|
|
1518
|
+
case "tests":
|
|
1519
|
+
return {
|
|
1520
|
+
...base,
|
|
1521
|
+
outcome: "tests",
|
|
1522
|
+
run: outcome.agentRun,
|
|
1523
|
+
effort,
|
|
1524
|
+
...logFor(".test.log")
|
|
1525
|
+
};
|
|
1526
|
+
case "install":
|
|
1527
|
+
return {
|
|
1528
|
+
...base,
|
|
1529
|
+
outcome: "install",
|
|
1530
|
+
...logFor(".install.log")
|
|
1531
|
+
};
|
|
1532
|
+
default: {
|
|
1533
|
+
const _exhaustive = outcome;
|
|
1534
|
+
return _exhaustive;
|
|
1535
|
+
}
|
|
1274
1536
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1537
|
+
case "no-commits":
|
|
1538
|
+
return {
|
|
1539
|
+
...base,
|
|
1540
|
+
outcome: "no-commits",
|
|
1541
|
+
run: outcome.agentRun,
|
|
1542
|
+
effort
|
|
1543
|
+
};
|
|
1544
|
+
case "error":
|
|
1545
|
+
case "worktree-error":
|
|
1546
|
+
return {
|
|
1547
|
+
...base,
|
|
1548
|
+
outcome: "error",
|
|
1549
|
+
error: formatErrorMessage(outcome.error)
|
|
1550
|
+
};
|
|
1551
|
+
default: {
|
|
1552
|
+
const _exhaustive = outcome;
|
|
1553
|
+
return _exhaustive;
|
|
1280
1554
|
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
__name(attemptEntry, "attemptEntry");
|
|
1558
|
+
function recordAttempt(opts) {
|
|
1559
|
+
try {
|
|
1560
|
+
const entry = attemptEntry(opts);
|
|
1561
|
+
if (!entry) return;
|
|
1562
|
+
appendEntry(opts.learningsPath, renderEntry(entry));
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
opts.d.log(
|
|
1565
|
+
` ${opts.ticket.filename}: could not record learnings: ${formatErrorMessage(err)}`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
__name(recordAttempt, "recordAttempt");
|
|
1570
|
+
async function runTicketAttempts(opts) {
|
|
1571
|
+
const { ticket, d } = opts;
|
|
1572
|
+
let prior = { subagents: 0 };
|
|
1573
|
+
const showStats = /* @__PURE__ */ __name((stats) => {
|
|
1574
|
+
d.setTicketStats(ticket.num, ticket.feature, stats);
|
|
1575
|
+
}, "showStats");
|
|
1576
|
+
return runWithRetries({
|
|
1577
|
+
maxAttempts: opts.retries + 1,
|
|
1578
|
+
retryDelay: opts.retryDelay,
|
|
1579
|
+
agentCmd: opts.agentCmd,
|
|
1580
|
+
effort: opts.effort,
|
|
1581
|
+
label: ticket.filename,
|
|
1582
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
1583
|
+
}, async (attempt, effort) => {
|
|
1584
|
+
const record = /* @__PURE__ */ __name((o) => {
|
|
1585
|
+
recordAttempt({
|
|
1586
|
+
learningsPath: opts.learningsPath,
|
|
1587
|
+
logsDir: opts.logsDir,
|
|
1588
|
+
ticket,
|
|
1589
|
+
attempt,
|
|
1590
|
+
effort,
|
|
1591
|
+
outcome: o,
|
|
1592
|
+
d
|
|
1593
|
+
});
|
|
1594
|
+
}, "record");
|
|
1595
|
+
let outcome;
|
|
1281
1596
|
try {
|
|
1282
1597
|
outcome = await runTicketAttempt({
|
|
1283
1598
|
...opts,
|
|
1284
1599
|
attempt,
|
|
1285
|
-
effort
|
|
1600
|
+
effort,
|
|
1601
|
+
onStats: /* @__PURE__ */ __name((stats) => showStats(addStats(prior, stats)), "onStats"),
|
|
1602
|
+
onAgentRun: /* @__PURE__ */ __name((result) => {
|
|
1603
|
+
prior = addStats(prior, result);
|
|
1604
|
+
opts.onAgentRun?.(result);
|
|
1605
|
+
}, "onAgentRun")
|
|
1286
1606
|
});
|
|
1287
1607
|
} catch (error) {
|
|
1288
1608
|
d.log(
|
|
1289
1609
|
` ${ticket.filename}: attempt ${attempt} threw: ${formatErrorMessage(error)}`
|
|
1290
1610
|
);
|
|
1291
|
-
|
|
1611
|
+
const errorOutcome = { kind: "error", error };
|
|
1612
|
+
record(errorOutcome);
|
|
1613
|
+
return { result: errorOutcome, stop: true };
|
|
1292
1614
|
}
|
|
1615
|
+
record(outcome);
|
|
1293
1616
|
if (outcome.kind === "ready") {
|
|
1294
1617
|
d.setTicketStatus(
|
|
1295
1618
|
ticket.num,
|
|
1296
1619
|
"awaiting-merge",
|
|
1297
1620
|
ticket.feature
|
|
1298
1621
|
);
|
|
1299
|
-
return outcome;
|
|
1622
|
+
return { result: outcome, stop: true };
|
|
1300
1623
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1624
|
+
return {
|
|
1625
|
+
result: outcome,
|
|
1626
|
+
stop: outcome.kind === "worktree-error"
|
|
1627
|
+
};
|
|
1628
|
+
});
|
|
1304
1629
|
}
|
|
1305
1630
|
__name(runTicketAttempts, "runTicketAttempts");
|
|
1631
|
+
function failureNote(reason) {
|
|
1632
|
+
if (reason === "tests") return "tests failed";
|
|
1633
|
+
if (reason === "install") return "install failed";
|
|
1634
|
+
return "failed";
|
|
1635
|
+
}
|
|
1636
|
+
__name(failureNote, "failureNote");
|
|
1637
|
+
function isRegularFile(path) {
|
|
1638
|
+
try {
|
|
1639
|
+
return statSync(path).isFile();
|
|
1640
|
+
} catch {
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
__name(isRegularFile, "isRegularFile");
|
|
1306
1645
|
async function saveProgress(repoRoot, progressPath, progress, message, d) {
|
|
1307
1646
|
writeProgress(progressPath, progress);
|
|
1647
|
+
const learningsPath = learningsPathFor(progressPath);
|
|
1648
|
+
const paths = isRegularFile(learningsPath) ? [progressPath, learningsPath] : progressPath;
|
|
1308
1649
|
try {
|
|
1309
|
-
await commitProgress(repoRoot,
|
|
1650
|
+
await commitProgress(repoRoot, paths, message);
|
|
1310
1651
|
} catch (err) {
|
|
1311
1652
|
d.log(
|
|
1312
1653
|
" Failed to commit progress: " + formatErrorMessage(err)
|
|
@@ -1319,15 +1660,77 @@ function indentFilenames(tickets) {
|
|
|
1319
1660
|
return tickets.map((t) => ` ${t.filename}`).join("\n");
|
|
1320
1661
|
}
|
|
1321
1662
|
__name(indentFilenames, "indentFilenames");
|
|
1322
|
-
function
|
|
1663
|
+
function explainOpen(tickets, failedKeys, warnings) {
|
|
1323
1664
|
const remaining = tickets.filter(
|
|
1324
1665
|
(t) => t.status === "ready-for-agent"
|
|
1325
1666
|
);
|
|
1667
|
+
const open = remaining.filter(
|
|
1668
|
+
(t) => !failedKeys.has(ticketKey(t.feature, t.num))
|
|
1669
|
+
);
|
|
1670
|
+
const dead = findBlockedTickets(tickets, failedKeys);
|
|
1671
|
+
const deadRefs = /* @__PURE__ */ __name((t) => dead.get(ticketKey(t.feature, t.num)) ?? [], "deadRefs");
|
|
1672
|
+
const lines = [];
|
|
1326
1673
|
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1674
|
+
if (human.length > 0) {
|
|
1675
|
+
const humanKeys = new Set(
|
|
1676
|
+
human.map((t) => ticketKey(t.feature, t.num))
|
|
1677
|
+
);
|
|
1678
|
+
const blockedByHuman = open.filter((t) => deadRefs(t).some(
|
|
1679
|
+
(r) => humanKeys.has(ticketKey(r.feature, r.num))
|
|
1680
|
+
));
|
|
1681
|
+
const noun = human.length === 1 ? "ticket is" : "tickets are";
|
|
1682
|
+
lines.push(
|
|
1683
|
+
"",
|
|
1684
|
+
(remaining.length === 0 && failedKeys.size === 0 ? `All remaining ${noun} marked ${HUMAN_STATUS}. ` : `${human.length} ${noun} marked ${HUMAN_STATUS}. `) + "nightralph only runs ready-for-agent tickets, so an agent cannot finish these:",
|
|
1685
|
+
indentFilenames(human)
|
|
1686
|
+
);
|
|
1687
|
+
if (blockedByHuman.length > 0) {
|
|
1688
|
+
const n = blockedByHuman.length;
|
|
1689
|
+
lines.push(
|
|
1690
|
+
`${n} ready-for-agent ticket${n === 1 ? " is" : "s are"} blocked until they are done:`,
|
|
1691
|
+
indentFilenames(blockedByHuman)
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
lines.push(
|
|
1695
|
+
"Do that work by hand, then set each ticket's **Status:** to done" + (blockedByHuman.length > 0 ? " and run nightralph again." : ".")
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
const blockers = [];
|
|
1699
|
+
const outside = open.filter(
|
|
1700
|
+
(t) => deadRefs(t).some((r) => r.where === "pending")
|
|
1701
|
+
);
|
|
1702
|
+
if (outside.length > 0) {
|
|
1703
|
+
const n = outside.length;
|
|
1704
|
+
blockers.push(
|
|
1705
|
+
`${n} ready-for-agent ticket${n === 1 ? " waits" : "s wait"} on tickets outside this run. Select those features too, or finish them first:`,
|
|
1706
|
+
...outside.map((t) => ` ${ticketLabel(toRef(t))} waits on ` + deadRefs(t).filter((r) => r.where === "pending").map(blockerLabel).join(", "))
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
const missing = open.flatMap((t) => t.blockers.filter((r) => r.where === "missing").map((r) => ` ${ticketLabel(toRef(t))}: ${blockerLabel(r)}`));
|
|
1710
|
+
if (missing.length > 0) {
|
|
1711
|
+
const n = missing.length;
|
|
1712
|
+
blockers.push(
|
|
1713
|
+
`${n} blocker reference${n === 1 ? "" : "s"} match no ticket. Fix the **Blocked by:** line:`,
|
|
1714
|
+
...missing
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
const typos = warnings.filter((w) => w.kind === "typo");
|
|
1718
|
+
if (typos.length > 0) {
|
|
1719
|
+
const n = typos.length;
|
|
1720
|
+
blockers.push(
|
|
1721
|
+
`${n} word${n === 1 ? "" : "s"} in a **Blocked by:** line look${n === 1 ? "s" : ""} like a feature name but match no .scratch/<name>/issues directory:`,
|
|
1722
|
+
...typos.map((w) => ` ${w.ticket}: ${w.text}`)
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
if (blockers.length > 0) lines.push("", ...blockers);
|
|
1726
|
+
return lines;
|
|
1727
|
+
}
|
|
1728
|
+
__name(explainOpen, "explainOpen");
|
|
1729
|
+
function renderFinishSummary(tickets, totalCompleted, failedKeys, warnings = []) {
|
|
1730
|
+
const remaining = tickets.filter(
|
|
1731
|
+
(t) => t.status === "ready-for-agent"
|
|
1330
1732
|
);
|
|
1733
|
+
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
1331
1734
|
const counts = `Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedKeys.size > 0 ? `, ${failedKeys.size} failed` : "") + (human.length > 0 ? `, ${human.length} need a human` : "");
|
|
1332
1735
|
const lines = [counts];
|
|
1333
1736
|
const features = new Set(
|
|
@@ -1356,26 +1759,54 @@ function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
|
1356
1759
|
lines.push(detail);
|
|
1357
1760
|
}
|
|
1358
1761
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1762
|
+
lines.push(...explainOpen(tickets, failedKeys, warnings));
|
|
1763
|
+
return lines.join("\n");
|
|
1764
|
+
}
|
|
1765
|
+
__name(renderFinishSummary, "renderFinishSummary");
|
|
1766
|
+
function nothingReady(tickets) {
|
|
1767
|
+
return findReadyTickets(tickets).length === 0 && tickets.some((t) => t.status === "ready-for-agent");
|
|
1768
|
+
}
|
|
1769
|
+
__name(nothingReady, "nothingReady");
|
|
1770
|
+
function renderNothingReady(tickets, warnings) {
|
|
1771
|
+
const lines = [
|
|
1772
|
+
"Nothing to run: every ready-for-agent ticket is waiting on a blocker.",
|
|
1773
|
+
...explainOpen(tickets, /* @__PURE__ */ new Set(), warnings)
|
|
1774
|
+
];
|
|
1775
|
+
const dead = findBlockedTickets(tickets, /* @__PURE__ */ new Set());
|
|
1776
|
+
const byKey = new Map(
|
|
1777
|
+
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
1365
1778
|
);
|
|
1366
|
-
|
|
1367
|
-
|
|
1779
|
+
const other = [];
|
|
1780
|
+
for (const [key, refs] of dead) {
|
|
1781
|
+
const stuck = refs.flatMap((r) => {
|
|
1782
|
+
const b = byKey.get(ticketKey(r.feature, r.num));
|
|
1783
|
+
return b && b.status !== HUMAN_STATUS ? [`${ticketLabel(toRef(b))} (${b.status})`] : [];
|
|
1784
|
+
});
|
|
1785
|
+
const t = byKey.get(key);
|
|
1786
|
+
if (t && stuck.length > 0) {
|
|
1787
|
+
other.push(
|
|
1788
|
+
` ${ticketLabel(toRef(t))} waits on ` + stuck.join(", ")
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (other.length > 0) {
|
|
1368
1793
|
lines.push(
|
|
1369
|
-
|
|
1370
|
-
|
|
1794
|
+
"",
|
|
1795
|
+
"These tickets wait on tickets that are not ready-for-agent:",
|
|
1796
|
+
...other,
|
|
1797
|
+
"Set those tickets to ready-for-agent or done, or fix the **Blocked by:** lines."
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
const cycle = detectDeadlocks(tickets).filter((t) => !dead.has(ticketKey(t.feature, t.num)));
|
|
1801
|
+
if (cycle.length > 0) {
|
|
1802
|
+
lines.push(
|
|
1803
|
+
"",
|
|
1804
|
+
"Deadlocked tickets (waiting on each other): " + cycle.map((t) => ticketLabel(toRef(t))).join(", ") + ". Fix their **Blocked by:** lines."
|
|
1371
1805
|
);
|
|
1372
1806
|
}
|
|
1373
|
-
lines.push(
|
|
1374
|
-
"Do that work by hand, then set each ticket's **Status:** to done" + (blockedByHuman.length > 0 ? " and run nightralph again." : ".")
|
|
1375
|
-
);
|
|
1376
1807
|
return lines.join("\n");
|
|
1377
1808
|
}
|
|
1378
|
-
__name(
|
|
1809
|
+
__name(renderNothingReady, "renderNothingReady");
|
|
1379
1810
|
function sessionTickets(tickets) {
|
|
1380
1811
|
return [...tickets].filter((t) => t.status === "ready-for-agent").sort((a, b) => a.feature.localeCompare(b.feature) || a.num - b.num);
|
|
1381
1812
|
}
|
|
@@ -1394,7 +1825,10 @@ function reportBlocked(d, tickets, failedKeys) {
|
|
|
1394
1825
|
)) {
|
|
1395
1826
|
const ticket = byKey.get(key);
|
|
1396
1827
|
if (!ticket) continue;
|
|
1397
|
-
const labels = dead.map((
|
|
1828
|
+
const labels = dead.map((ref) => {
|
|
1829
|
+
const t = byKey.get(ticketKey(ref.feature, ref.num));
|
|
1830
|
+
return t ? ticketLabel(toRef(t)) : blockerLabel(ref);
|
|
1831
|
+
});
|
|
1398
1832
|
d.setBlocked(ticket.num, ticket.feature, labels);
|
|
1399
1833
|
}
|
|
1400
1834
|
}
|
|
@@ -1406,7 +1840,7 @@ function runProgress(tickets) {
|
|
|
1406
1840
|
};
|
|
1407
1841
|
}
|
|
1408
1842
|
__name(runProgress, "runProgress");
|
|
1409
|
-
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay, concurrency) {
|
|
1843
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay, concurrency, warnings = [], preferSubagent) {
|
|
1410
1844
|
mkdirSync(logsDir, { recursive: true });
|
|
1411
1845
|
const d = display ?? createDisplay();
|
|
1412
1846
|
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
@@ -1420,6 +1854,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1420
1854
|
let waveNum = 0;
|
|
1421
1855
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1422
1856
|
const session = sessionTickets(tickets);
|
|
1857
|
+
let runStats = emptyRunStats();
|
|
1423
1858
|
while (true) {
|
|
1424
1859
|
if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
|
|
1425
1860
|
const ready = findReadyTickets(tickets).filter(
|
|
@@ -1450,7 +1885,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1450
1885
|
const prompt = renderPrompt(
|
|
1451
1886
|
specBody,
|
|
1452
1887
|
t,
|
|
1453
|
-
{ testCmd }
|
|
1888
|
+
{ testCmd, preferSubagent }
|
|
1454
1889
|
);
|
|
1455
1890
|
const label = t.filename.replace(
|
|
1456
1891
|
/\.md$/,
|
|
@@ -1462,6 +1897,18 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1462
1897
|
t.feature
|
|
1463
1898
|
);
|
|
1464
1899
|
d.log(` Starting ${t.filename}`);
|
|
1900
|
+
let prior = { subagents: 0 };
|
|
1901
|
+
const onStats = /* @__PURE__ */ __name((stats) => {
|
|
1902
|
+
d.setTicketStats(
|
|
1903
|
+
t.num,
|
|
1904
|
+
t.feature,
|
|
1905
|
+
addStats(prior, stats)
|
|
1906
|
+
);
|
|
1907
|
+
}, "onStats");
|
|
1908
|
+
const tally = /* @__PURE__ */ __name((r) => {
|
|
1909
|
+
prior = addStats(prior, r);
|
|
1910
|
+
runStats = addRun(runStats, r);
|
|
1911
|
+
}, "tally");
|
|
1465
1912
|
let level = effort;
|
|
1466
1913
|
let result = await spawnAgent({
|
|
1467
1914
|
prompt,
|
|
@@ -1472,8 +1919,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1472
1919
|
label,
|
|
1473
1920
|
maxTurns,
|
|
1474
1921
|
effort: level,
|
|
1922
|
+
preferSubagent,
|
|
1923
|
+
onStats,
|
|
1475
1924
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1476
1925
|
});
|
|
1926
|
+
tally(result);
|
|
1477
1927
|
for (let attempt = 2; result.exitCode !== 0 && !result.timedOut && attempt <= maxAttempts; attempt++) {
|
|
1478
1928
|
if (isKnown) {
|
|
1479
1929
|
level = escalateEffort(
|
|
@@ -1500,8 +1950,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1500
1950
|
label,
|
|
1501
1951
|
maxTurns,
|
|
1502
1952
|
effort: level,
|
|
1953
|
+
preferSubagent,
|
|
1954
|
+
onStats,
|
|
1503
1955
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1504
1956
|
});
|
|
1957
|
+
tally(result);
|
|
1505
1958
|
}
|
|
1506
1959
|
return { ticket: t, result };
|
|
1507
1960
|
}
|
|
@@ -1560,8 +2013,11 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
1560
2013
|
d.log("\n" + renderFinishSummary(
|
|
1561
2014
|
tickets,
|
|
1562
2015
|
totalCompleted,
|
|
1563
|
-
failedKeys
|
|
2016
|
+
failedKeys,
|
|
2017
|
+
warnings
|
|
1564
2018
|
));
|
|
2019
|
+
const statsLine = renderRunStats(runStats);
|
|
2020
|
+
if (statsLine) d.log(statsLine);
|
|
1565
2021
|
d.cleanup();
|
|
1566
2022
|
return { allDone: remaining.length === 0 };
|
|
1567
2023
|
}
|
|
@@ -1582,6 +2038,7 @@ async function runWavesIsolated(opts) {
|
|
|
1582
2038
|
"Working tree is dirty; commit or stash changes before running"
|
|
1583
2039
|
);
|
|
1584
2040
|
}
|
|
2041
|
+
const launchCommit = headCommit(repoRoot);
|
|
1585
2042
|
const features = new Set(
|
|
1586
2043
|
opts.tickets.map((t) => t.feature).filter(Boolean)
|
|
1587
2044
|
);
|
|
@@ -1647,6 +2104,10 @@ async function runWavesIsolated(opts) {
|
|
|
1647
2104
|
let waveNum = 0;
|
|
1648
2105
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1649
2106
|
const session = sessionTickets(opts.tickets);
|
|
2107
|
+
let runStats = emptyRunStats();
|
|
2108
|
+
const onAgentRun = /* @__PURE__ */ __name((result) => {
|
|
2109
|
+
runStats = addRun(runStats, result);
|
|
2110
|
+
}, "onAgentRun");
|
|
1650
2111
|
while (true) {
|
|
1651
2112
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
1652
2113
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
@@ -1685,13 +2146,36 @@ async function runWavesIsolated(opts) {
|
|
|
1685
2146
|
timeout: opts.timeout,
|
|
1686
2147
|
logsDir: opts.logsDir,
|
|
1687
2148
|
testCmd: opts.testCmd,
|
|
2149
|
+
installCmd: opts.installCmd,
|
|
1688
2150
|
maxTurns: opts.maxTurns,
|
|
1689
2151
|
effort: opts.effort,
|
|
2152
|
+
preferSubagent: opts.preferSubagent,
|
|
2153
|
+
learningsPath: learningsPathFor(
|
|
2154
|
+
tracker.pathFor(t.feature)
|
|
2155
|
+
),
|
|
1690
2156
|
retries,
|
|
1691
2157
|
retryDelay,
|
|
1692
|
-
d
|
|
2158
|
+
d,
|
|
2159
|
+
onAgentRun
|
|
1693
2160
|
})
|
|
1694
2161
|
);
|
|
2162
|
+
const touched = new Set(ready.map((t) => {
|
|
2163
|
+
return learningsPathFor(tracker.pathFor(t.feature));
|
|
2164
|
+
}));
|
|
2165
|
+
for (const path of touched) {
|
|
2166
|
+
if (!isRegularFile(path)) continue;
|
|
2167
|
+
try {
|
|
2168
|
+
await commitProgress(
|
|
2169
|
+
repoRoot,
|
|
2170
|
+
path,
|
|
2171
|
+
"nightralph: record attempt learnings"
|
|
2172
|
+
);
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
d.log(
|
|
2175
|
+
" Failed to commit learnings: " + formatErrorMessage(err)
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
1695
2179
|
const outcomes = settled.map(
|
|
1696
2180
|
(r) => r.status === "fulfilled" ? r.value : { kind: "error", error: r.reason }
|
|
1697
2181
|
);
|
|
@@ -1795,7 +2279,7 @@ async function runWavesIsolated(opts) {
|
|
|
1795
2279
|
repoRoot,
|
|
1796
2280
|
tracker.pathFor(ticket.feature),
|
|
1797
2281
|
tracker.dataFor(ticket.feature),
|
|
1798
|
-
`nightralph: ${ticket.filename} ` + (outcome.reason
|
|
2282
|
+
`nightralph: ${ticket.filename} ` + failureNote(outcome.reason),
|
|
1799
2283
|
d
|
|
1800
2284
|
);
|
|
1801
2285
|
continue;
|
|
@@ -1831,6 +2315,7 @@ async function runWavesIsolated(opts) {
|
|
|
1831
2315
|
branches: successfulBranches,
|
|
1832
2316
|
strategy: opts.conflictStrategy
|
|
1833
2317
|
});
|
|
2318
|
+
const doneThisWave = [];
|
|
1834
2319
|
for (const branchResult of mergeResult.merged) {
|
|
1835
2320
|
const bk = branchToKey.get(
|
|
1836
2321
|
branchResult.branch
|
|
@@ -1839,6 +2324,7 @@ async function runWavesIsolated(opts) {
|
|
|
1839
2324
|
if (!ticket) continue;
|
|
1840
2325
|
if (branchResult.merged) {
|
|
1841
2326
|
markDone(ticket);
|
|
2327
|
+
doneThisWave.push(ticket);
|
|
1842
2328
|
waveCompleted++;
|
|
1843
2329
|
totalCompleted++;
|
|
1844
2330
|
d.setTicketStatus(
|
|
@@ -1888,11 +2374,15 @@ async function runWavesIsolated(opts) {
|
|
|
1888
2374
|
timeout: opts.timeout,
|
|
1889
2375
|
logsDir: opts.logsDir,
|
|
1890
2376
|
testCmd: opts.testCmd,
|
|
2377
|
+
installCmd: opts.installCmd,
|
|
1891
2378
|
maxTurns: opts.maxTurns,
|
|
1892
|
-
|
|
2379
|
+
preferSubagent: opts.preferSubagent,
|
|
2380
|
+
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine"),
|
|
2381
|
+
onAgentRun
|
|
1893
2382
|
}) : false;
|
|
1894
2383
|
if (recovered) {
|
|
1895
2384
|
markDone(ticket);
|
|
2385
|
+
doneThisWave.push(ticket);
|
|
1896
2386
|
waveCompleted++;
|
|
1897
2387
|
totalCompleted++;
|
|
1898
2388
|
d.setTicketStatus(
|
|
@@ -1948,6 +2438,28 @@ async function runWavesIsolated(opts) {
|
|
|
1948
2438
|
exitCode: 0,
|
|
1949
2439
|
done: false
|
|
1950
2440
|
});
|
|
2441
|
+
try {
|
|
2442
|
+
appendEntry(
|
|
2443
|
+
learningsPathFor(
|
|
2444
|
+
tracker.pathFor(ticket.feature)
|
|
2445
|
+
),
|
|
2446
|
+
renderEntry({
|
|
2447
|
+
outcome: "merge",
|
|
2448
|
+
key: ticketKey(
|
|
2449
|
+
ticket.feature,
|
|
2450
|
+
ticket.num
|
|
2451
|
+
),
|
|
2452
|
+
label: ticketLabel(ticket),
|
|
2453
|
+
attempt: 0,
|
|
2454
|
+
now: /* @__PURE__ */ new Date(),
|
|
2455
|
+
stderr: branchResult.stderr ?? ""
|
|
2456
|
+
})
|
|
2457
|
+
);
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
d.log(
|
|
2460
|
+
` ${ticket.filename}: could not record learnings: ` + formatErrorMessage(err)
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
1951
2463
|
await saveProgress(
|
|
1952
2464
|
repoRoot,
|
|
1953
2465
|
tracker.pathFor(ticket.feature),
|
|
@@ -1958,6 +2470,7 @@ async function runWavesIsolated(opts) {
|
|
|
1958
2470
|
}
|
|
1959
2471
|
}
|
|
1960
2472
|
}
|
|
2473
|
+
await commitTicketFiles(repoRoot, doneThisWave, d);
|
|
1961
2474
|
if (mergeResult.stoppedAt) {
|
|
1962
2475
|
d.log(
|
|
1963
2476
|
`
|
|
@@ -2000,16 +2513,64 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
2000
2513
|
}
|
|
2001
2514
|
}
|
|
2002
2515
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
2516
|
+
let reviewLine = null;
|
|
2517
|
+
if (opts.review) {
|
|
2518
|
+
try {
|
|
2519
|
+
const outcome = await runReviewPass({
|
|
2520
|
+
repoRoot,
|
|
2521
|
+
repoName,
|
|
2522
|
+
baseBranch,
|
|
2523
|
+
reviewPath: opts.review.reviewPath,
|
|
2524
|
+
forkedFrom: opts.review.forkedFrom,
|
|
2525
|
+
launchCommit,
|
|
2526
|
+
specBody: reviewSpecBody(
|
|
2527
|
+
opts.specBody,
|
|
2528
|
+
opts.specBodies
|
|
2529
|
+
),
|
|
2530
|
+
agentCmd: opts.agentCmd,
|
|
2531
|
+
model: opts.model,
|
|
2532
|
+
timeout: opts.timeout,
|
|
2533
|
+
logsDir: opts.logsDir,
|
|
2534
|
+
testCmd: opts.testCmd,
|
|
2535
|
+
installCmd: opts.installCmd,
|
|
2536
|
+
maxTurns: opts.maxTurns,
|
|
2537
|
+
effort: opts.effort,
|
|
2538
|
+
preferSubagent: opts.preferSubagent,
|
|
2539
|
+
retries,
|
|
2540
|
+
retryDelay,
|
|
2541
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
2542
|
+
}, {
|
|
2543
|
+
spawnAgent: /* @__PURE__ */ __name(async (spawnOpts) => {
|
|
2544
|
+
const result = await spawnAgent(spawnOpts);
|
|
2545
|
+
onAgentRun(result);
|
|
2546
|
+
return result;
|
|
2547
|
+
}, "spawnAgent"),
|
|
2548
|
+
registry: runningAgents
|
|
2549
|
+
});
|
|
2550
|
+
reviewLine = renderReviewSummary(
|
|
2551
|
+
outcome,
|
|
2552
|
+
relative(repoRoot, opts.review.reviewPath)
|
|
2553
|
+
);
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
d.log(`
|
|
2556
|
+
Review failed: ${formatErrorMessage(err)}`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2003
2559
|
const remaining = opts.tickets.filter(
|
|
2004
2560
|
(t) => t.status === "ready-for-agent"
|
|
2005
2561
|
);
|
|
2006
2562
|
d.log("\n" + renderFinishSummary(
|
|
2007
2563
|
opts.tickets,
|
|
2008
2564
|
totalCompleted,
|
|
2009
|
-
failedKeys
|
|
2565
|
+
failedKeys,
|
|
2566
|
+
opts.warnings ?? []
|
|
2010
2567
|
));
|
|
2568
|
+
const statsLine = renderRunStats(runStats);
|
|
2569
|
+
if (statsLine) d.log(statsLine);
|
|
2570
|
+
if (reviewLine) d.log(reviewLine);
|
|
2571
|
+
d.log(`Work is on ${baseBranch}`);
|
|
2011
2572
|
d.cleanup();
|
|
2012
|
-
return { allDone: remaining.length === 0 };
|
|
2573
|
+
return { allDone: remaining.length === 0, branch: baseBranch };
|
|
2013
2574
|
}
|
|
2014
2575
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
2015
2576
|
export {
|
|
@@ -2018,31 +2579,35 @@ export {
|
|
|
2018
2579
|
DEFAULT_CONCURRENCY,
|
|
2019
2580
|
DEFAULT_RETRIES,
|
|
2020
2581
|
DEFAULT_RETRY_DELAY,
|
|
2021
|
-
EFFORT_LADDERS,
|
|
2582
|
+
EFFORT_LADDERS2 as EFFORT_LADDERS,
|
|
2022
2583
|
HUMAN_STATUS,
|
|
2023
2584
|
TICKET_RE,
|
|
2024
2585
|
TOOL_USE_RE,
|
|
2586
|
+
blockerLabel,
|
|
2587
|
+
blockerSatisfied,
|
|
2025
2588
|
checkItems,
|
|
2026
2589
|
createPiStreamFormatter,
|
|
2027
2590
|
dryRun,
|
|
2028
2591
|
dryRunIsolated,
|
|
2029
|
-
escalateEffort,
|
|
2592
|
+
escalateEffort2 as escalateEffort,
|
|
2030
2593
|
findBlockedTickets,
|
|
2031
2594
|
findReadyTickets,
|
|
2032
2595
|
formatErrorMessage,
|
|
2033
2596
|
formatStreamLine,
|
|
2034
2597
|
getProviderArgs,
|
|
2598
|
+
historyFor,
|
|
2035
2599
|
killProcessGroup,
|
|
2036
2600
|
killRunningAgents,
|
|
2037
2601
|
lookupSpec,
|
|
2038
2602
|
markDone,
|
|
2039
|
-
|
|
2603
|
+
nothingReady,
|
|
2040
2604
|
parseStatus,
|
|
2041
2605
|
parseTicketFilename,
|
|
2042
2606
|
renderFinishSummary,
|
|
2043
2607
|
renderMergePrompt,
|
|
2608
|
+
renderNothingReady,
|
|
2044
2609
|
renderPrompt,
|
|
2045
|
-
retryDelayFor,
|
|
2610
|
+
retryDelayFor2 as retryDelayFor,
|
|
2046
2611
|
runPool,
|
|
2047
2612
|
runWaves,
|
|
2048
2613
|
runWavesIsolated,
|