nightralph 0.0.54 → 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 +100 -11
- 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 +740 -137
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +4 -0
- package/dist/learnings.js.map +2 -2
- package/dist/meta.json +69 -16
- package/dist/orchestrator.js +325 -93
- package/dist/orchestrator.js.map +2 -2
- package/dist/resolve.js +98 -9
- package/dist/resolve.js.map +2 -2
- package/dist/review.js +9 -2
- package/dist/review.js.map +2 -2
- package/dist/rules.js +13 -0
- package/dist/rules.js.map +2 -2
- 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 +2 -0
- package/dist/src/learnings.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +71 -17
- package/dist/src/orchestrator.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/review.d.ts +4 -0
- package/dist/src/review.d.ts.map +1 -1
- package/dist/src/rules.d.ts +1 -0
- package/dist/src/rules.d.ts.map +1 -1
- package/dist/src/stats.d.ts +43 -0
- package/dist/src/stats.d.ts.map +1 -0
- package/dist/stats.js +111 -0
- package/dist/stats.js.map +7 -0
- package/package.json +3 -3
package/dist/orchestrator.js
CHANGED
|
@@ -50,6 +50,7 @@ import { runTestCmd } from "./testcmd.js";
|
|
|
50
50
|
import {
|
|
51
51
|
readDocsRule,
|
|
52
52
|
installRule,
|
|
53
|
+
subagentRule,
|
|
53
54
|
testGateRule,
|
|
54
55
|
LINT_RULE,
|
|
55
56
|
integrityRule,
|
|
@@ -59,6 +60,15 @@ import {
|
|
|
59
60
|
agentsMdRule
|
|
60
61
|
} from "./rules.js";
|
|
61
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";
|
|
62
72
|
import {
|
|
63
73
|
EFFORT_LADDERS,
|
|
64
74
|
escalateEffort,
|
|
@@ -84,6 +94,10 @@ import {
|
|
|
84
94
|
selectHistory,
|
|
85
95
|
tailLines
|
|
86
96
|
} from "./learnings.js";
|
|
97
|
+
import {
|
|
98
|
+
parseBlockerField,
|
|
99
|
+
provisionalRefs
|
|
100
|
+
} from "./blockers.js";
|
|
87
101
|
function formatErrorMessage(error) {
|
|
88
102
|
return error instanceof Error ? error.message : String(error);
|
|
89
103
|
}
|
|
@@ -152,21 +166,7 @@ function parseStatus(body) {
|
|
|
152
166
|
return m ? m[1].trim().toLowerCase() : "";
|
|
153
167
|
}
|
|
154
168
|
__name(parseStatus, "parseStatus");
|
|
155
|
-
function
|
|
156
|
-
const m = body.match(/\*\*Blocked by:\*\*\s*(.+)/i);
|
|
157
|
-
if (!m) return [];
|
|
158
|
-
const text = m[1];
|
|
159
|
-
if (/none/i.test(text)) return [];
|
|
160
|
-
const nums = [];
|
|
161
|
-
const re = /(\d+)/g;
|
|
162
|
-
let match;
|
|
163
|
-
while (match = re.exec(text)) {
|
|
164
|
-
nums.push(Number(match[1]));
|
|
165
|
-
}
|
|
166
|
-
return nums;
|
|
167
|
-
}
|
|
168
|
-
__name(parseBlockers, "parseBlockers");
|
|
169
|
-
function scanTickets(dir, feature = "") {
|
|
169
|
+
function scanTickets(dir, feature = "", knownFeatures = /* @__PURE__ */ new Set()) {
|
|
170
170
|
const files = readdirSync(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
171
171
|
b,
|
|
172
172
|
void 0,
|
|
@@ -183,7 +183,10 @@ function scanTickets(dir, feature = "") {
|
|
|
183
183
|
filepath,
|
|
184
184
|
body,
|
|
185
185
|
status: parseStatus(body),
|
|
186
|
-
blockers:
|
|
186
|
+
blockers: provisionalRefs(
|
|
187
|
+
parseBlockerField(body, knownFeatures).refs,
|
|
188
|
+
feature
|
|
189
|
+
),
|
|
187
190
|
feature
|
|
188
191
|
};
|
|
189
192
|
});
|
|
@@ -193,15 +196,35 @@ function ticketKey(feature, num) {
|
|
|
193
196
|
return `${feature}:${num}`;
|
|
194
197
|
}
|
|
195
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");
|
|
196
221
|
function findReadyTickets(tickets) {
|
|
197
222
|
const doneKeys = new Set(
|
|
198
223
|
tickets.filter((t) => t.status === "done").map((t) => ticketKey(t.feature, t.num))
|
|
199
224
|
);
|
|
200
225
|
return tickets.filter((t) => {
|
|
201
226
|
if (t.status !== "ready-for-agent") return false;
|
|
202
|
-
return t.blockers.every(
|
|
203
|
-
(b) => doneKeys.has(ticketKey(t.feature, b))
|
|
204
|
-
);
|
|
227
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
205
228
|
});
|
|
206
229
|
}
|
|
207
230
|
__name(findReadyTickets, "findReadyTickets");
|
|
@@ -210,14 +233,19 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
210
233
|
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
211
234
|
);
|
|
212
235
|
function collectDead(ticket, visited, found) {
|
|
213
|
-
for (const
|
|
214
|
-
const key = ticketKey(
|
|
236
|
+
for (const ref of ticket.blockers) {
|
|
237
|
+
const key = ticketKey(ref.feature, ref.num);
|
|
215
238
|
if (visited.has(key)) continue;
|
|
216
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
|
+
}
|
|
217
245
|
const blocker = byKey.get(key);
|
|
218
246
|
if (!blocker || blocker.status === "done") continue;
|
|
219
247
|
if (failedKeys.has(key) || blocker.status !== "ready-for-agent") {
|
|
220
|
-
found.
|
|
248
|
+
found.set(key, ref);
|
|
221
249
|
continue;
|
|
222
250
|
}
|
|
223
251
|
collectDead(blocker, visited, found);
|
|
@@ -229,9 +257,9 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
229
257
|
const key = ticketKey(t.feature, t.num);
|
|
230
258
|
if (t.status !== "ready-for-agent") continue;
|
|
231
259
|
if (failedKeys.has(key)) continue;
|
|
232
|
-
const found = /* @__PURE__ */ new
|
|
260
|
+
const found = /* @__PURE__ */ new Map();
|
|
233
261
|
collectDead(t, /* @__PURE__ */ new Set([key]), found);
|
|
234
|
-
if (found.size > 0) blocked.set(key, [...found]);
|
|
262
|
+
if (found.size > 0) blocked.set(key, [...found.values()]);
|
|
235
263
|
}
|
|
236
264
|
return blocked;
|
|
237
265
|
}
|
|
@@ -411,6 +439,7 @@ function renderPrompt(specBody, ticket, opts) {
|
|
|
411
439
|
"- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
|
|
412
440
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
413
441
|
...installRule(opts.installCmd),
|
|
442
|
+
...subagentRule(opts.preferSubagent),
|
|
414
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.",
|
|
415
444
|
testGateRule(opts.testCmd, "ticket"),
|
|
416
445
|
LINT_RULE,
|
|
@@ -446,6 +475,7 @@ function renderMergePrompt(specBody, ticket, opts = {}) {
|
|
|
446
475
|
"",
|
|
447
476
|
"Do NOT re-implement the ticket from scratch.",
|
|
448
477
|
...installRule(opts.installCmd),
|
|
478
|
+
...subagentRule(opts.preferSubagent),
|
|
449
479
|
"Do NOT read or modify files outside your current directory (the worktree).",
|
|
450
480
|
"Do NOT run git push."
|
|
451
481
|
].join("\n");
|
|
@@ -454,7 +484,7 @@ __name(renderMergePrompt, "renderMergePrompt");
|
|
|
454
484
|
function listTickets(tickets) {
|
|
455
485
|
console.log("\nTickets:");
|
|
456
486
|
for (const t of tickets) {
|
|
457
|
-
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";
|
|
458
488
|
console.log(
|
|
459
489
|
` ${t.filename} (${t.status}) ${blockerStr}`
|
|
460
490
|
);
|
|
@@ -478,11 +508,7 @@ function* simulateWaves(tickets) {
|
|
|
478
508
|
if (sim.get(key) !== "ready-for-agent") {
|
|
479
509
|
return false;
|
|
480
510
|
}
|
|
481
|
-
return t.blockers.every(
|
|
482
|
-
(b) => doneKeys.has(
|
|
483
|
-
ticketKey(t.feature, b)
|
|
484
|
-
)
|
|
485
|
-
);
|
|
511
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
486
512
|
});
|
|
487
513
|
if (ready.length === 0) break;
|
|
488
514
|
yield { wave, ready };
|
|
@@ -515,9 +541,7 @@ function detectDeadlocks(tickets) {
|
|
|
515
541
|
(t) => ticketKey(t.feature, t.num) === key
|
|
516
542
|
);
|
|
517
543
|
if (ticket.blockers.every(
|
|
518
|
-
(
|
|
519
|
-
ticketKey(ticket.feature, b)
|
|
520
|
-
)
|
|
544
|
+
(r) => blockerSatisfied(r, doneKeys)
|
|
521
545
|
)) {
|
|
522
546
|
doneKeys.add(key);
|
|
523
547
|
readyKeys.delete(key);
|
|
@@ -560,39 +584,46 @@ function getProviderArgs(cmd) {
|
|
|
560
584
|
}
|
|
561
585
|
__name(getProviderArgs, "getProviderArgs");
|
|
562
586
|
function formatStreamLine(line) {
|
|
563
|
-
let
|
|
587
|
+
let value;
|
|
564
588
|
try {
|
|
565
|
-
|
|
589
|
+
value = JSON.parse(line);
|
|
566
590
|
} catch {
|
|
567
591
|
return line;
|
|
568
592
|
}
|
|
593
|
+
return formatStreamObject(value);
|
|
594
|
+
}
|
|
595
|
+
__name(formatStreamLine, "formatStreamLine");
|
|
596
|
+
function formatStreamObject(value) {
|
|
597
|
+
if (!isRecord(value)) return null;
|
|
598
|
+
const obj = value;
|
|
569
599
|
if (obj.type === "assistant") {
|
|
570
|
-
|
|
571
|
-
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;
|
|
572
603
|
if (!content) return null;
|
|
573
604
|
for (const block of content) {
|
|
574
|
-
if (block
|
|
575
|
-
|
|
576
|
-
|
|
605
|
+
if (!isRecord(block)) continue;
|
|
606
|
+
if (block.type !== "text") continue;
|
|
607
|
+
if (typeof block.text === "string") return block.text;
|
|
577
608
|
}
|
|
578
609
|
return null;
|
|
579
610
|
}
|
|
580
611
|
if (obj.type === "result") {
|
|
581
|
-
const sub = obj.subtype
|
|
582
|
-
const cost = obj.total_cost_usd;
|
|
583
|
-
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)})` : "";
|
|
584
615
|
return `${sub}${costStr}`;
|
|
585
616
|
}
|
|
586
617
|
if (obj.type === "system") {
|
|
587
|
-
const sub = obj.subtype;
|
|
618
|
+
const sub = typeof obj.subtype === "string" ? obj.subtype : void 0;
|
|
588
619
|
if (sub === "api_retry") {
|
|
589
|
-
const error = obj.error;
|
|
590
|
-
return `retry: ${error
|
|
620
|
+
const error = typeof obj.error === "string" ? obj.error : "unknown";
|
|
621
|
+
return `retry: ${error}`;
|
|
591
622
|
}
|
|
592
623
|
}
|
|
593
624
|
return null;
|
|
594
625
|
}
|
|
595
|
-
__name(
|
|
626
|
+
__name(formatStreamObject, "formatStreamObject");
|
|
596
627
|
function createPiStreamFormatter() {
|
|
597
628
|
const calls = /* @__PURE__ */ new Map();
|
|
598
629
|
let textBuf = "";
|
|
@@ -745,10 +776,15 @@ function spawnAgent(opts) {
|
|
|
745
776
|
opts.logPath,
|
|
746
777
|
{ flags: "w" }
|
|
747
778
|
);
|
|
779
|
+
const env = isClaude && opts.preferSubagent ? {
|
|
780
|
+
...process.env,
|
|
781
|
+
CLAUDE_CODE_SUBAGENT_MODEL: opts.preferSubagent
|
|
782
|
+
} : void 0;
|
|
748
783
|
const proc = spawn(opts.agentCmd, args, {
|
|
749
784
|
stdio: ["pipe", "pipe", "pipe"],
|
|
750
785
|
cwd: opts.cwd,
|
|
751
|
-
detached: true
|
|
786
|
+
detached: true,
|
|
787
|
+
...env ? { env } : {}
|
|
752
788
|
});
|
|
753
789
|
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
754
790
|
proc.stdout.pipe(logStream, { end: false });
|
|
@@ -766,10 +802,26 @@ function spawnAgent(opts) {
|
|
|
766
802
|
}
|
|
767
803
|
}
|
|
768
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");
|
|
769
821
|
const rl = createInterface({ input: proc.stdout });
|
|
770
822
|
rl.on("line", (line) => {
|
|
771
823
|
if (isClaude) {
|
|
772
|
-
const formatted =
|
|
824
|
+
const formatted = formatClaudeLine(line);
|
|
773
825
|
if (formatted !== null) {
|
|
774
826
|
collectCompleted(formatted);
|
|
775
827
|
const output = `${prefix}${formatted}`;
|
|
@@ -861,7 +913,12 @@ function spawnAgent(opts) {
|
|
|
861
913
|
rlErr.close();
|
|
862
914
|
logStream.end();
|
|
863
915
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
864
|
-
resolve({
|
|
916
|
+
resolve({
|
|
917
|
+
exitCode,
|
|
918
|
+
completedItems,
|
|
919
|
+
timedOut,
|
|
920
|
+
...isClaude ? streamStats.stats : {}
|
|
921
|
+
});
|
|
865
922
|
}
|
|
866
923
|
__name(finish, "finish");
|
|
867
924
|
proc.on("exit", (code) => {
|
|
@@ -905,7 +962,7 @@ function spawnAgent(opts) {
|
|
|
905
962
|
});
|
|
906
963
|
}
|
|
907
964
|
__name(spawnAgent, "spawnAgent");
|
|
908
|
-
function dryRun(tickets, specBody, testCmd) {
|
|
965
|
+
function dryRun(tickets, specBody, testCmd, preferSubagent) {
|
|
909
966
|
listTickets(tickets);
|
|
910
967
|
console.log("\nSimulated wave order:");
|
|
911
968
|
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
@@ -921,6 +978,7 @@ function dryRun(tickets, specBody, testCmd) {
|
|
|
921
978
|
}
|
|
922
979
|
console.log(`
|
|
923
980
|
Test command: ${testCmd || "(none)"}`);
|
|
981
|
+
console.log(`Prefer subagent: ${preferSubagent || "(none)"}`);
|
|
924
982
|
const firstReady = findReadyTickets(tickets);
|
|
925
983
|
if (firstReady.length > 0) {
|
|
926
984
|
console.log(
|
|
@@ -932,7 +990,7 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
932
990
|
renderPrompt(
|
|
933
991
|
specBody,
|
|
934
992
|
firstReady[0],
|
|
935
|
-
{ testCmd }
|
|
993
|
+
{ testCmd, preferSubagent }
|
|
936
994
|
)
|
|
937
995
|
);
|
|
938
996
|
}
|
|
@@ -978,7 +1036,8 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
978
1036
|
firstReady[0],
|
|
979
1037
|
{
|
|
980
1038
|
testCmd: opts.testCmd,
|
|
981
|
-
installCmd: opts.installCmd
|
|
1039
|
+
installCmd: opts.installCmd,
|
|
1040
|
+
preferSubagent: opts.preferSubagent
|
|
982
1041
|
}
|
|
983
1042
|
)
|
|
984
1043
|
);
|
|
@@ -994,6 +1053,9 @@ Test command: ${opts.testCmd || "(none)"}`
|
|
|
994
1053
|
console.log(
|
|
995
1054
|
`Effort: ${opts.effort ?? "(provider default)"}`
|
|
996
1055
|
);
|
|
1056
|
+
console.log(
|
|
1057
|
+
`Prefer subagent: ${opts.preferSubagent || "(none)"}`
|
|
1058
|
+
);
|
|
997
1059
|
console.log(
|
|
998
1060
|
`Retries: ${opts.retries ?? DEFAULT_RETRIES}`
|
|
999
1061
|
);
|
|
@@ -1094,7 +1156,10 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1094
1156
|
const prompt = renderMergePrompt(
|
|
1095
1157
|
opts.specBody,
|
|
1096
1158
|
opts.ticket,
|
|
1097
|
-
{
|
|
1159
|
+
{
|
|
1160
|
+
installCmd: opts.installCmd,
|
|
1161
|
+
preferSubagent: opts.preferSubagent
|
|
1162
|
+
}
|
|
1098
1163
|
);
|
|
1099
1164
|
const logPath = join(
|
|
1100
1165
|
opts.logsDir,
|
|
@@ -1113,8 +1178,10 @@ async function respawnAndRetryMerge(opts) {
|
|
|
1113
1178
|
cwd: opts.worktreePath,
|
|
1114
1179
|
label,
|
|
1115
1180
|
maxTurns: opts.maxTurns,
|
|
1181
|
+
preferSubagent: opts.preferSubagent,
|
|
1116
1182
|
onLine: opts.onLine
|
|
1117
1183
|
});
|
|
1184
|
+
opts.onAgentRun?.(result);
|
|
1118
1185
|
if (result.exitCode !== 0) {
|
|
1119
1186
|
const msg2 = ` ${opts.ticket.filename}: respawned agent exited ${result.exitCode}`;
|
|
1120
1187
|
if (opts.onLine) {
|
|
@@ -1199,17 +1266,29 @@ function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
|
1199
1266
|
return join(logsDir, `${stem}${suffix}${ext}`);
|
|
1200
1267
|
}
|
|
1201
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");
|
|
1202
1289
|
function buildHistory(learningsPath, ticket, d) {
|
|
1203
1290
|
try {
|
|
1204
|
-
return
|
|
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
|
-
));
|
|
1291
|
+
return historyFor(learningsPath, ticket);
|
|
1213
1292
|
} catch (err) {
|
|
1214
1293
|
d.log(
|
|
1215
1294
|
` ${ticket.filename}: could not read learnings: ${formatErrorMessage(err)}`
|
|
@@ -1286,7 +1365,8 @@ async function runTicketAttempt(opts) {
|
|
|
1286
1365
|
opts.learningsPath,
|
|
1287
1366
|
ticket,
|
|
1288
1367
|
d
|
|
1289
|
-
)
|
|
1368
|
+
),
|
|
1369
|
+
preferSubagent: opts.preferSubagent
|
|
1290
1370
|
}
|
|
1291
1371
|
),
|
|
1292
1372
|
agentCmd: opts.agentCmd,
|
|
@@ -1302,8 +1382,11 @@ async function runTicketAttempt(opts) {
|
|
|
1302
1382
|
label: ticket.filename.replace(/\.md$/, ""),
|
|
1303
1383
|
maxTurns: opts.maxTurns,
|
|
1304
1384
|
effort: opts.effort,
|
|
1385
|
+
preferSubagent: opts.preferSubagent,
|
|
1386
|
+
onStats: opts.onStats,
|
|
1305
1387
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1306
1388
|
});
|
|
1389
|
+
opts.onAgentRun?.(agentResult);
|
|
1307
1390
|
let notes = null;
|
|
1308
1391
|
try {
|
|
1309
1392
|
notes = collectNotes(worktree.worktreePath);
|
|
@@ -1316,7 +1399,9 @@ async function runTicketAttempt(opts) {
|
|
|
1316
1399
|
exitCode: agentResult.exitCode,
|
|
1317
1400
|
completedItems: agentResult.completedItems,
|
|
1318
1401
|
timedOut: agentResult.timedOut,
|
|
1319
|
-
notes
|
|
1402
|
+
notes,
|
|
1403
|
+
...agentResult.subagents === void 0 ? {} : { subagents: agentResult.subagents },
|
|
1404
|
+
...agentResult.costUsd === void 0 ? {} : { costUsd: agentResult.costUsd }
|
|
1320
1405
|
};
|
|
1321
1406
|
if (agentResult.timedOut) {
|
|
1322
1407
|
d.log(
|
|
@@ -1484,6 +1569,10 @@ function recordAttempt(opts) {
|
|
|
1484
1569
|
__name(recordAttempt, "recordAttempt");
|
|
1485
1570
|
async function runTicketAttempts(opts) {
|
|
1486
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");
|
|
1487
1576
|
return runWithRetries({
|
|
1488
1577
|
maxAttempts: opts.retries + 1,
|
|
1489
1578
|
retryDelay: opts.retryDelay,
|
|
@@ -1508,7 +1597,12 @@ async function runTicketAttempts(opts) {
|
|
|
1508
1597
|
outcome = await runTicketAttempt({
|
|
1509
1598
|
...opts,
|
|
1510
1599
|
attempt,
|
|
1511
|
-
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")
|
|
1512
1606
|
});
|
|
1513
1607
|
} catch (error) {
|
|
1514
1608
|
d.log(
|
|
@@ -1566,15 +1660,77 @@ function indentFilenames(tickets) {
|
|
|
1566
1660
|
return tickets.map((t) => ` ${t.filename}`).join("\n");
|
|
1567
1661
|
}
|
|
1568
1662
|
__name(indentFilenames, "indentFilenames");
|
|
1569
|
-
function
|
|
1663
|
+
function explainOpen(tickets, failedKeys, warnings) {
|
|
1570
1664
|
const remaining = tickets.filter(
|
|
1571
1665
|
(t) => t.status === "ready-for-agent"
|
|
1572
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 = [];
|
|
1573
1673
|
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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")
|
|
1577
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"
|
|
1732
|
+
);
|
|
1733
|
+
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
1578
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` : "");
|
|
1579
1735
|
const lines = [counts];
|
|
1580
1736
|
const features = new Set(
|
|
@@ -1603,26 +1759,54 @@ function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
|
1603
1759
|
lines.push(detail);
|
|
1604
1760
|
}
|
|
1605
1761
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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])
|
|
1612
1778
|
);
|
|
1613
|
-
|
|
1614
|
-
|
|
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) {
|
|
1615
1793
|
lines.push(
|
|
1616
|
-
|
|
1617
|
-
|
|
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."
|
|
1618
1805
|
);
|
|
1619
1806
|
}
|
|
1620
|
-
lines.push(
|
|
1621
|
-
"Do that work by hand, then set each ticket's **Status:** to done" + (blockedByHuman.length > 0 ? " and run nightralph again." : ".")
|
|
1622
|
-
);
|
|
1623
1807
|
return lines.join("\n");
|
|
1624
1808
|
}
|
|
1625
|
-
__name(
|
|
1809
|
+
__name(renderNothingReady, "renderNothingReady");
|
|
1626
1810
|
function sessionTickets(tickets) {
|
|
1627
1811
|
return [...tickets].filter((t) => t.status === "ready-for-agent").sort((a, b) => a.feature.localeCompare(b.feature) || a.num - b.num);
|
|
1628
1812
|
}
|
|
@@ -1641,7 +1825,10 @@ function reportBlocked(d, tickets, failedKeys) {
|
|
|
1641
1825
|
)) {
|
|
1642
1826
|
const ticket = byKey.get(key);
|
|
1643
1827
|
if (!ticket) continue;
|
|
1644
|
-
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
|
+
});
|
|
1645
1832
|
d.setBlocked(ticket.num, ticket.feature, labels);
|
|
1646
1833
|
}
|
|
1647
1834
|
}
|
|
@@ -1653,7 +1840,7 @@ function runProgress(tickets) {
|
|
|
1653
1840
|
};
|
|
1654
1841
|
}
|
|
1655
1842
|
__name(runProgress, "runProgress");
|
|
1656
|
-
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) {
|
|
1657
1844
|
mkdirSync(logsDir, { recursive: true });
|
|
1658
1845
|
const d = display ?? createDisplay();
|
|
1659
1846
|
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
@@ -1667,6 +1854,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1667
1854
|
let waveNum = 0;
|
|
1668
1855
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1669
1856
|
const session = sessionTickets(tickets);
|
|
1857
|
+
let runStats = emptyRunStats();
|
|
1670
1858
|
while (true) {
|
|
1671
1859
|
if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
|
|
1672
1860
|
const ready = findReadyTickets(tickets).filter(
|
|
@@ -1697,7 +1885,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1697
1885
|
const prompt = renderPrompt(
|
|
1698
1886
|
specBody,
|
|
1699
1887
|
t,
|
|
1700
|
-
{ testCmd }
|
|
1888
|
+
{ testCmd, preferSubagent }
|
|
1701
1889
|
);
|
|
1702
1890
|
const label = t.filename.replace(
|
|
1703
1891
|
/\.md$/,
|
|
@@ -1709,6 +1897,18 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1709
1897
|
t.feature
|
|
1710
1898
|
);
|
|
1711
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");
|
|
1712
1912
|
let level = effort;
|
|
1713
1913
|
let result = await spawnAgent({
|
|
1714
1914
|
prompt,
|
|
@@ -1719,8 +1919,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1719
1919
|
label,
|
|
1720
1920
|
maxTurns,
|
|
1721
1921
|
effort: level,
|
|
1922
|
+
preferSubagent,
|
|
1923
|
+
onStats,
|
|
1722
1924
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1723
1925
|
});
|
|
1926
|
+
tally(result);
|
|
1724
1927
|
for (let attempt = 2; result.exitCode !== 0 && !result.timedOut && attempt <= maxAttempts; attempt++) {
|
|
1725
1928
|
if (isKnown) {
|
|
1726
1929
|
level = escalateEffort(
|
|
@@ -1747,8 +1950,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1747
1950
|
label,
|
|
1748
1951
|
maxTurns,
|
|
1749
1952
|
effort: level,
|
|
1953
|
+
preferSubagent,
|
|
1954
|
+
onStats,
|
|
1750
1955
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
1751
1956
|
});
|
|
1957
|
+
tally(result);
|
|
1752
1958
|
}
|
|
1753
1959
|
return { ticket: t, result };
|
|
1754
1960
|
}
|
|
@@ -1807,8 +2013,11 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
1807
2013
|
d.log("\n" + renderFinishSummary(
|
|
1808
2014
|
tickets,
|
|
1809
2015
|
totalCompleted,
|
|
1810
|
-
failedKeys
|
|
2016
|
+
failedKeys,
|
|
2017
|
+
warnings
|
|
1811
2018
|
));
|
|
2019
|
+
const statsLine = renderRunStats(runStats);
|
|
2020
|
+
if (statsLine) d.log(statsLine);
|
|
1812
2021
|
d.cleanup();
|
|
1813
2022
|
return { allDone: remaining.length === 0 };
|
|
1814
2023
|
}
|
|
@@ -1895,6 +2104,10 @@ async function runWavesIsolated(opts) {
|
|
|
1895
2104
|
let waveNum = 0;
|
|
1896
2105
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1897
2106
|
const session = sessionTickets(opts.tickets);
|
|
2107
|
+
let runStats = emptyRunStats();
|
|
2108
|
+
const onAgentRun = /* @__PURE__ */ __name((result) => {
|
|
2109
|
+
runStats = addRun(runStats, result);
|
|
2110
|
+
}, "onAgentRun");
|
|
1898
2111
|
while (true) {
|
|
1899
2112
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
1900
2113
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
@@ -1936,12 +2149,14 @@ async function runWavesIsolated(opts) {
|
|
|
1936
2149
|
installCmd: opts.installCmd,
|
|
1937
2150
|
maxTurns: opts.maxTurns,
|
|
1938
2151
|
effort: opts.effort,
|
|
2152
|
+
preferSubagent: opts.preferSubagent,
|
|
1939
2153
|
learningsPath: learningsPathFor(
|
|
1940
2154
|
tracker.pathFor(t.feature)
|
|
1941
2155
|
),
|
|
1942
2156
|
retries,
|
|
1943
2157
|
retryDelay,
|
|
1944
|
-
d
|
|
2158
|
+
d,
|
|
2159
|
+
onAgentRun
|
|
1945
2160
|
})
|
|
1946
2161
|
);
|
|
1947
2162
|
const touched = new Set(ready.map((t) => {
|
|
@@ -2161,7 +2376,9 @@ async function runWavesIsolated(opts) {
|
|
|
2161
2376
|
testCmd: opts.testCmd,
|
|
2162
2377
|
installCmd: opts.installCmd,
|
|
2163
2378
|
maxTurns: opts.maxTurns,
|
|
2164
|
-
|
|
2379
|
+
preferSubagent: opts.preferSubagent,
|
|
2380
|
+
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine"),
|
|
2381
|
+
onAgentRun
|
|
2165
2382
|
}) : false;
|
|
2166
2383
|
if (recovered) {
|
|
2167
2384
|
markDone(ticket);
|
|
@@ -2318,10 +2535,18 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
2318
2535
|
installCmd: opts.installCmd,
|
|
2319
2536
|
maxTurns: opts.maxTurns,
|
|
2320
2537
|
effort: opts.effort,
|
|
2538
|
+
preferSubagent: opts.preferSubagent,
|
|
2321
2539
|
retries,
|
|
2322
2540
|
retryDelay,
|
|
2323
2541
|
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
2324
|
-
}, {
|
|
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
|
+
});
|
|
2325
2550
|
reviewLine = renderReviewSummary(
|
|
2326
2551
|
outcome,
|
|
2327
2552
|
relative(repoRoot, opts.review.reviewPath)
|
|
@@ -2337,8 +2562,11 @@ Review failed: ${formatErrorMessage(err)}`);
|
|
|
2337
2562
|
d.log("\n" + renderFinishSummary(
|
|
2338
2563
|
opts.tickets,
|
|
2339
2564
|
totalCompleted,
|
|
2340
|
-
failedKeys
|
|
2565
|
+
failedKeys,
|
|
2566
|
+
opts.warnings ?? []
|
|
2341
2567
|
));
|
|
2568
|
+
const statsLine = renderRunStats(runStats);
|
|
2569
|
+
if (statsLine) d.log(statsLine);
|
|
2342
2570
|
if (reviewLine) d.log(reviewLine);
|
|
2343
2571
|
d.log(`Work is on ${baseBranch}`);
|
|
2344
2572
|
d.cleanup();
|
|
@@ -2355,6 +2583,8 @@ export {
|
|
|
2355
2583
|
HUMAN_STATUS,
|
|
2356
2584
|
TICKET_RE,
|
|
2357
2585
|
TOOL_USE_RE,
|
|
2586
|
+
blockerLabel,
|
|
2587
|
+
blockerSatisfied,
|
|
2358
2588
|
checkItems,
|
|
2359
2589
|
createPiStreamFormatter,
|
|
2360
2590
|
dryRun,
|
|
@@ -2365,15 +2595,17 @@ export {
|
|
|
2365
2595
|
formatErrorMessage,
|
|
2366
2596
|
formatStreamLine,
|
|
2367
2597
|
getProviderArgs,
|
|
2598
|
+
historyFor,
|
|
2368
2599
|
killProcessGroup,
|
|
2369
2600
|
killRunningAgents,
|
|
2370
2601
|
lookupSpec,
|
|
2371
2602
|
markDone,
|
|
2372
|
-
|
|
2603
|
+
nothingReady,
|
|
2373
2604
|
parseStatus,
|
|
2374
2605
|
parseTicketFilename,
|
|
2375
2606
|
renderFinishSummary,
|
|
2376
2607
|
renderMergePrompt,
|
|
2608
|
+
renderNothingReady,
|
|
2377
2609
|
renderPrompt,
|
|
2378
2610
|
retryDelayFor2 as retryDelayFor,
|
|
2379
2611
|
runPool,
|