nightralph 0.0.48 → 0.0.50
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/dist/display.js +22 -6
- package/dist/display.js.map +2 -2
- package/dist/index.js +478 -196
- package/dist/index.js.map +3 -3
- package/dist/meta.json +24 -14
- package/dist/orchestrator.js +298 -116
- package/dist/orchestrator.js.map +2 -2
- package/dist/progress.js +36 -0
- package/dist/progress.js.map +2 -2
- package/dist/resolve.js +52 -23
- package/dist/resolve.js.map +2 -2
- package/dist/src/display.d.ts +10 -3
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +7 -2
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +12 -0
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/resolve.d.ts +6 -4
- package/dist/src/resolve.d.ts.map +1 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/worktree.js +5 -4
- package/dist/worktree.js.map +2 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:f
|
|
|
7
7
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
8
8
|
import yargs from "yargs";
|
|
9
9
|
import { hideBin } from "yargs/helpers";
|
|
10
|
-
import { select
|
|
10
|
+
import { select } from "@inquirer/prompts";
|
|
11
11
|
|
|
12
12
|
// src/setup.ts
|
|
13
13
|
import {
|
|
@@ -257,18 +257,19 @@ async function branchExists(repoRoot, branchName) {
|
|
|
257
257
|
__name(branchExists, "branchExists");
|
|
258
258
|
function buildBranchName(repoName, ticket) {
|
|
259
259
|
const num = String(ticket.num).padStart(2, "0");
|
|
260
|
-
|
|
260
|
+
const prefix = ticket.feature ? `${ticket.feature}-` : "";
|
|
261
|
+
return `${repoName}--${prefix}${num}-${ticket.slug}`;
|
|
261
262
|
}
|
|
262
263
|
__name(buildBranchName, "buildBranchName");
|
|
263
264
|
async function createWorktreeNow(opts) {
|
|
264
265
|
const { repoRoot, repoName, baseBranch, ticket } = opts;
|
|
265
266
|
const branchName = buildBranchName(repoName, ticket);
|
|
267
|
+
const num = String(ticket.num).padStart(2, "0");
|
|
268
|
+
const prefix = ticket.feature ? `${ticket.feature}-` : "";
|
|
266
269
|
const worktreePath = join2(
|
|
267
270
|
repoRoot,
|
|
268
271
|
"..",
|
|
269
|
-
`${repoName}.${
|
|
270
|
-
ticket.num
|
|
271
|
-
).padStart(2, "0")}-${ticket.slug}`
|
|
272
|
+
`${repoName}.${prefix}${num}-${ticket.slug}`
|
|
272
273
|
);
|
|
273
274
|
if (existsSync2(worktreePath)) {
|
|
274
275
|
try {
|
|
@@ -403,6 +404,39 @@ function generateProgress(feature, tickets) {
|
|
|
403
404
|
};
|
|
404
405
|
}
|
|
405
406
|
__name(generateProgress, "generateProgress");
|
|
407
|
+
function generateMultiFeatureProgress(tickets) {
|
|
408
|
+
const byFeature = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const t of tickets) {
|
|
410
|
+
const key = t.feature || "";
|
|
411
|
+
const arr = byFeature.get(key) ?? [];
|
|
412
|
+
arr.push(t);
|
|
413
|
+
byFeature.set(key, arr);
|
|
414
|
+
}
|
|
415
|
+
const result = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const [feat, ft] of byFeature) {
|
|
417
|
+
result.set(feat, generateProgress(feat, ft));
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
__name(generateMultiFeatureProgress, "generateMultiFeatureProgress");
|
|
422
|
+
function createProgressTracker(opts) {
|
|
423
|
+
return {
|
|
424
|
+
dataFor(feature) {
|
|
425
|
+
return opts.dataMap.get(feature) ?? opts.fallbackData;
|
|
426
|
+
},
|
|
427
|
+
pathFor(feature) {
|
|
428
|
+
return opts.progressPaths.get(feature) ?? opts.fallbackPath;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
__name(createProgressTracker, "createProgressTracker");
|
|
433
|
+
function isSpecComplete(progressPath) {
|
|
434
|
+
const data = readProgress(progressPath);
|
|
435
|
+
if (!data) return false;
|
|
436
|
+
if (data.tickets.length === 0) return false;
|
|
437
|
+
return data.tickets.every((t) => t.done);
|
|
438
|
+
}
|
|
439
|
+
__name(isSpecComplete, "isSpecComplete");
|
|
406
440
|
function readProgress(progressPath) {
|
|
407
441
|
let content;
|
|
408
442
|
try {
|
|
@@ -478,8 +512,8 @@ function renderProgress(data) {
|
|
|
478
512
|
""
|
|
479
513
|
];
|
|
480
514
|
for (const ticket of data.tickets) {
|
|
481
|
-
const
|
|
482
|
-
lines.push(`- ${
|
|
515
|
+
const checkbox2 = ticket.done ? "[x]" : "[ ]";
|
|
516
|
+
lines.push(`- ${checkbox2} ${ticket.filename}`);
|
|
483
517
|
if (ticket.done || ticket.exitCode !== null) {
|
|
484
518
|
if (ticket.branch) {
|
|
485
519
|
lines.push(
|
|
@@ -788,6 +822,14 @@ var color = {
|
|
|
788
822
|
return `${ESC}2m${text}${ESC}0m`;
|
|
789
823
|
}
|
|
790
824
|
};
|
|
825
|
+
function ticketLabel(ticket) {
|
|
826
|
+
if (ticket.feature) {
|
|
827
|
+
const padded = String(ticket.num).padStart(2, "0");
|
|
828
|
+
return `${ticket.feature}/${padded}`;
|
|
829
|
+
}
|
|
830
|
+
return ticket.filename;
|
|
831
|
+
}
|
|
832
|
+
__name(ticketLabel, "ticketLabel");
|
|
791
833
|
var FILLED_DOT = "\u25CF";
|
|
792
834
|
var EMPTY_DOT = "\u25CB";
|
|
793
835
|
var SEPARATOR_WIDTH = 50;
|
|
@@ -883,9 +925,11 @@ var StatusDisplay = class {
|
|
|
883
925
|
}
|
|
884
926
|
this.render();
|
|
885
927
|
}
|
|
886
|
-
setTicketStatus(num, status) {
|
|
928
|
+
setTicketStatus(num, status, feature) {
|
|
887
929
|
if (!this.wave) return;
|
|
888
|
-
const ticket = this.wave.tickets.find(
|
|
930
|
+
const ticket = feature ? this.wave.tickets.find(
|
|
931
|
+
(t) => t.num === num && t.feature === feature
|
|
932
|
+
) : this.wave.tickets.find((t) => t.num === num);
|
|
889
933
|
if (ticket) {
|
|
890
934
|
ticket.status = status;
|
|
891
935
|
if (status === "in-progress") {
|
|
@@ -951,7 +995,10 @@ var StatusDisplay = class {
|
|
|
951
995
|
if (tickets.length <= availableRows) {
|
|
952
996
|
for (const ticket of tickets) {
|
|
953
997
|
const dot = renderDot(ticket.status);
|
|
954
|
-
const name = fitLine(
|
|
998
|
+
const name = fitLine(
|
|
999
|
+
ticketLabel(ticket),
|
|
1000
|
+
width - 2
|
|
1001
|
+
);
|
|
955
1002
|
output += `${dot} ${name}
|
|
956
1003
|
`;
|
|
957
1004
|
}
|
|
@@ -996,7 +1043,10 @@ var StatusDisplay = class {
|
|
|
996
1043
|
);
|
|
997
1044
|
for (const ticket of visibleTickets) {
|
|
998
1045
|
const dot = renderDot(ticket.status);
|
|
999
|
-
const name = fitLine(
|
|
1046
|
+
const name = fitLine(
|
|
1047
|
+
ticketLabel(ticket),
|
|
1048
|
+
width - 2
|
|
1049
|
+
);
|
|
1000
1050
|
output += `${dot} ${name}
|
|
1001
1051
|
`;
|
|
1002
1052
|
}
|
|
@@ -1037,10 +1087,10 @@ var PlainDisplay = class {
|
|
|
1037
1087
|
const pct = progress && progress.total > 0 ? ` (${progress.completed}/${progress.total} done)` : "";
|
|
1038
1088
|
console.log(
|
|
1039
1089
|
`
|
|
1040
|
-
Wave ${waveNum}${pct}: ` + tickets.map((t) => t
|
|
1090
|
+
Wave ${waveNum}${pct}: ` + tickets.map((t) => ticketLabel(t)).join(", ")
|
|
1041
1091
|
);
|
|
1042
1092
|
}
|
|
1043
|
-
setTicketStatus(_num, _status) {
|
|
1093
|
+
setTicketStatus(_num, _status, _feature) {
|
|
1044
1094
|
}
|
|
1045
1095
|
log(message) {
|
|
1046
1096
|
console.log(message);
|
|
@@ -1212,7 +1262,7 @@ function parseBlockers(body) {
|
|
|
1212
1262
|
return nums;
|
|
1213
1263
|
}
|
|
1214
1264
|
__name(parseBlockers, "parseBlockers");
|
|
1215
|
-
function scanTickets(dir) {
|
|
1265
|
+
function scanTickets(dir, feature = "") {
|
|
1216
1266
|
const files = readdirSync2(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
1217
1267
|
b,
|
|
1218
1268
|
void 0,
|
|
@@ -1229,18 +1279,25 @@ function scanTickets(dir) {
|
|
|
1229
1279
|
filepath,
|
|
1230
1280
|
body,
|
|
1231
1281
|
status: parseStatus(body),
|
|
1232
|
-
blockers: parseBlockers(body)
|
|
1282
|
+
blockers: parseBlockers(body),
|
|
1283
|
+
feature
|
|
1233
1284
|
};
|
|
1234
1285
|
});
|
|
1235
1286
|
}
|
|
1236
1287
|
__name(scanTickets, "scanTickets");
|
|
1288
|
+
function ticketKey(feature, num) {
|
|
1289
|
+
return `${feature}:${num}`;
|
|
1290
|
+
}
|
|
1291
|
+
__name(ticketKey, "ticketKey");
|
|
1237
1292
|
function findReadyTickets(tickets) {
|
|
1238
|
-
const
|
|
1239
|
-
tickets.filter((t) => t.status === "done").map((t) => t.num)
|
|
1293
|
+
const doneKeys = new Set(
|
|
1294
|
+
tickets.filter((t) => t.status === "done").map((t) => ticketKey(t.feature, t.num))
|
|
1240
1295
|
);
|
|
1241
1296
|
return tickets.filter((t) => {
|
|
1242
1297
|
if (t.status !== "ready-for-agent") return false;
|
|
1243
|
-
return t.blockers.every(
|
|
1298
|
+
return t.blockers.every(
|
|
1299
|
+
(b) => doneKeys.has(ticketKey(t.feature, b))
|
|
1300
|
+
);
|
|
1244
1301
|
});
|
|
1245
1302
|
}
|
|
1246
1303
|
__name(findReadyTickets, "findReadyTickets");
|
|
@@ -1384,6 +1441,11 @@ function stripSpecSection(specBody, heading) {
|
|
|
1384
1441
|
return out.join("\n");
|
|
1385
1442
|
}
|
|
1386
1443
|
__name(stripSpecSection, "stripSpecSection");
|
|
1444
|
+
function lookupSpec(specBodies, fallback, feature) {
|
|
1445
|
+
if (!feature || !specBodies) return fallback;
|
|
1446
|
+
return specBodies.get(feature) ?? fallback;
|
|
1447
|
+
}
|
|
1448
|
+
__name(lookupSpec, "lookupSpec");
|
|
1387
1449
|
function renderPrompt(specBody, ticket, opts) {
|
|
1388
1450
|
return [
|
|
1389
1451
|
"---- SPEC CONTEXT ----",
|
|
@@ -1451,58 +1513,72 @@ function listTickets(tickets) {
|
|
|
1451
1513
|
__name(listTickets, "listTickets");
|
|
1452
1514
|
function* simulateWaves(tickets) {
|
|
1453
1515
|
const sim = new Map(
|
|
1454
|
-
tickets.map((t) => [
|
|
1516
|
+
tickets.map((t) => [
|
|
1517
|
+
ticketKey(t.feature, t.num),
|
|
1518
|
+
t.status
|
|
1519
|
+
])
|
|
1455
1520
|
);
|
|
1456
1521
|
let wave = 1;
|
|
1457
1522
|
while (true) {
|
|
1458
|
-
const
|
|
1459
|
-
[...sim.entries()].filter(([, s]) => s === "done").map(([
|
|
1523
|
+
const doneKeys = new Set(
|
|
1524
|
+
[...sim.entries()].filter(([, s]) => s === "done").map(([k]) => k)
|
|
1460
1525
|
);
|
|
1461
1526
|
const ready = [...tickets].filter((t) => {
|
|
1462
|
-
|
|
1527
|
+
const key = ticketKey(t.feature, t.num);
|
|
1528
|
+
if (sim.get(key) !== "ready-for-agent") {
|
|
1463
1529
|
return false;
|
|
1464
1530
|
}
|
|
1465
1531
|
return t.blockers.every(
|
|
1466
|
-
(b) =>
|
|
1532
|
+
(b) => doneKeys.has(
|
|
1533
|
+
ticketKey(t.feature, b)
|
|
1534
|
+
)
|
|
1467
1535
|
);
|
|
1468
1536
|
});
|
|
1469
1537
|
if (ready.length === 0) break;
|
|
1470
1538
|
yield { wave, ready };
|
|
1471
1539
|
for (const t of ready) {
|
|
1472
|
-
sim.set(
|
|
1540
|
+
sim.set(
|
|
1541
|
+
ticketKey(t.feature, t.num),
|
|
1542
|
+
"done"
|
|
1543
|
+
);
|
|
1473
1544
|
}
|
|
1474
1545
|
wave++;
|
|
1475
1546
|
}
|
|
1476
1547
|
}
|
|
1477
1548
|
__name(simulateWaves, "simulateWaves");
|
|
1478
1549
|
function detectDeadlocks(tickets) {
|
|
1479
|
-
const
|
|
1480
|
-
const
|
|
1550
|
+
const doneKeys = /* @__PURE__ */ new Set();
|
|
1551
|
+
const readyKeys = /* @__PURE__ */ new Set();
|
|
1481
1552
|
for (const t of tickets) {
|
|
1553
|
+
const key = ticketKey(t.feature, t.num);
|
|
1482
1554
|
if (t.status === "ready-for-agent") {
|
|
1483
|
-
|
|
1555
|
+
readyKeys.add(key);
|
|
1484
1556
|
} else if (t.status === "done") {
|
|
1485
|
-
|
|
1557
|
+
doneKeys.add(key);
|
|
1486
1558
|
}
|
|
1487
1559
|
}
|
|
1488
1560
|
let changed = true;
|
|
1489
1561
|
while (changed) {
|
|
1490
1562
|
changed = false;
|
|
1491
|
-
for (const
|
|
1563
|
+
for (const key of readyKeys) {
|
|
1492
1564
|
const ticket = tickets.find(
|
|
1493
|
-
(t) => t.num ===
|
|
1565
|
+
(t) => ticketKey(t.feature, t.num) === key
|
|
1494
1566
|
);
|
|
1495
1567
|
if (ticket.blockers.every(
|
|
1496
|
-
(b) =>
|
|
1568
|
+
(b) => doneKeys.has(
|
|
1569
|
+
ticketKey(ticket.feature, b)
|
|
1570
|
+
)
|
|
1497
1571
|
)) {
|
|
1498
|
-
|
|
1499
|
-
|
|
1572
|
+
doneKeys.add(key);
|
|
1573
|
+
readyKeys.delete(key);
|
|
1500
1574
|
changed = true;
|
|
1501
1575
|
}
|
|
1502
1576
|
}
|
|
1503
1577
|
}
|
|
1504
|
-
return [...
|
|
1505
|
-
(
|
|
1578
|
+
return [...readyKeys].map(
|
|
1579
|
+
(key) => tickets.find(
|
|
1580
|
+
(t) => ticketKey(t.feature, t.num) === key
|
|
1581
|
+
)
|
|
1506
1582
|
);
|
|
1507
1583
|
}
|
|
1508
1584
|
__name(detectDeadlocks, "detectDeadlocks");
|
|
@@ -1956,7 +2032,8 @@ function dryRunIsolated(opts) {
|
|
|
1956
2032
|
opts.repoName,
|
|
1957
2033
|
t
|
|
1958
2034
|
);
|
|
1959
|
-
const
|
|
2035
|
+
const prefix = t.feature ? `${t.feature}.` : "";
|
|
2036
|
+
const worktreePath = `../${opts.repoName}.${prefix}${num}-${t.slug}`;
|
|
1960
2037
|
console.log(` worktree: ${worktreePath}`);
|
|
1961
2038
|
console.log(` branch: ${branchName}`);
|
|
1962
2039
|
}
|
|
@@ -2176,10 +2253,19 @@ async function runTicketAttempt(opts) {
|
|
|
2176
2253
|
} catch (error) {
|
|
2177
2254
|
return { kind: "worktree-error", error };
|
|
2178
2255
|
}
|
|
2179
|
-
d.setTicketStatus(
|
|
2256
|
+
d.setTicketStatus(
|
|
2257
|
+
ticket.num,
|
|
2258
|
+
"in-progress",
|
|
2259
|
+
ticket.feature
|
|
2260
|
+
);
|
|
2261
|
+
const ticketSpec = lookupSpec(
|
|
2262
|
+
opts.specBodies,
|
|
2263
|
+
opts.specBody,
|
|
2264
|
+
ticket.feature
|
|
2265
|
+
);
|
|
2180
2266
|
const agentResult = await spawnAgent({
|
|
2181
2267
|
prompt: renderPrompt(
|
|
2182
|
-
|
|
2268
|
+
ticketSpec,
|
|
2183
2269
|
ticket,
|
|
2184
2270
|
{ testCmd: opts.testCmd }
|
|
2185
2271
|
),
|
|
@@ -2309,7 +2395,11 @@ async function runTicketAttempts(opts) {
|
|
|
2309
2395
|
return { kind: "error", error };
|
|
2310
2396
|
}
|
|
2311
2397
|
if (outcome.kind === "ready") {
|
|
2312
|
-
d.setTicketStatus(
|
|
2398
|
+
d.setTicketStatus(
|
|
2399
|
+
ticket.num,
|
|
2400
|
+
"awaiting-merge",
|
|
2401
|
+
ticket.feature
|
|
2402
|
+
);
|
|
2313
2403
|
return outcome;
|
|
2314
2404
|
}
|
|
2315
2405
|
if (outcome.kind === "worktree-error") return outcome;
|
|
@@ -2333,24 +2423,50 @@ function indentFilenames(tickets) {
|
|
|
2333
2423
|
return tickets.map((t) => ` ${t.filename}`).join("\n");
|
|
2334
2424
|
}
|
|
2335
2425
|
__name(indentFilenames, "indentFilenames");
|
|
2336
|
-
function renderFinishSummary(tickets, totalCompleted,
|
|
2426
|
+
function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
2337
2427
|
const remaining = tickets.filter(
|
|
2338
2428
|
(t) => t.status === "ready-for-agent"
|
|
2339
2429
|
);
|
|
2340
2430
|
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
2341
2431
|
const humanNums = new Set(human.map((t) => t.num));
|
|
2342
2432
|
const blockedByHuman = remaining.filter(
|
|
2343
|
-
(t) => !
|
|
2433
|
+
(t) => !failedKeys.has(ticketKey(t.feature, t.num)) && t.blockers.some((b) => humanNums.has(b))
|
|
2344
2434
|
);
|
|
2345
|
-
const counts = `Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (
|
|
2346
|
-
|
|
2435
|
+
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` : "");
|
|
2436
|
+
const lines = [counts];
|
|
2437
|
+
const features = new Set(
|
|
2438
|
+
tickets.map((t) => t.feature).filter(Boolean)
|
|
2439
|
+
);
|
|
2440
|
+
if (features.size > 1) {
|
|
2441
|
+
lines.push("");
|
|
2442
|
+
for (const feat of [...features].sort()) {
|
|
2443
|
+
const ft = tickets.filter(
|
|
2444
|
+
(t) => t.feature === feat
|
|
2445
|
+
);
|
|
2446
|
+
const done = ft.filter(
|
|
2447
|
+
(t) => t.status === "done"
|
|
2448
|
+
).length;
|
|
2449
|
+
const failed = ft.filter(
|
|
2450
|
+
(t) => failedKeys.has(
|
|
2451
|
+
ticketKey(t.feature, t.num)
|
|
2452
|
+
)
|
|
2453
|
+
).length;
|
|
2454
|
+
const rem = ft.filter(
|
|
2455
|
+
(t) => t.status === "ready-for-agent"
|
|
2456
|
+
).length;
|
|
2457
|
+
let detail = ` ${feat}: ${done} completed`;
|
|
2458
|
+
if (rem > 0) detail += `, ${rem} remaining`;
|
|
2459
|
+
if (failed > 0) detail += `, ${failed} failed`;
|
|
2460
|
+
lines.push(detail);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (human.length === 0) return lines.join("\n");
|
|
2347
2464
|
const noun = human.length === 1 ? "ticket is" : "tickets are";
|
|
2348
|
-
|
|
2349
|
-
counts,
|
|
2465
|
+
lines.push(
|
|
2350
2466
|
"",
|
|
2351
|
-
(remaining.length === 0 &&
|
|
2467
|
+
(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:",
|
|
2352
2468
|
indentFilenames(human)
|
|
2353
|
-
|
|
2469
|
+
);
|
|
2354
2470
|
if (blockedByHuman.length > 0) {
|
|
2355
2471
|
const n = blockedByHuman.length;
|
|
2356
2472
|
lines.push(
|
|
@@ -2383,28 +2499,34 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2383
2499
|
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
2384
2500
|
let totalCompleted = 0;
|
|
2385
2501
|
let waveNum = 0;
|
|
2386
|
-
const
|
|
2502
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
2387
2503
|
while (true) {
|
|
2388
2504
|
const ready = findReadyTickets(tickets).filter(
|
|
2389
|
-
(t) => !
|
|
2505
|
+
(t) => !failedKeys.has(
|
|
2506
|
+
ticketKey(t.feature, t.num)
|
|
2507
|
+
)
|
|
2390
2508
|
);
|
|
2391
2509
|
if (ready.length === 0) break;
|
|
2392
2510
|
waveNum++;
|
|
2393
|
-
const
|
|
2511
|
+
const readyKeys = new Set(
|
|
2512
|
+
ready.map((t) => ticketKey(t.feature, t.num))
|
|
2513
|
+
);
|
|
2394
2514
|
const remaining2 = tickets.filter(
|
|
2395
|
-
(t) => t.status === "ready-for-agent" && !
|
|
2515
|
+
(t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
|
|
2396
2516
|
);
|
|
2397
2517
|
d.startWave(
|
|
2398
2518
|
waveNum,
|
|
2399
2519
|
totalWaves,
|
|
2400
2520
|
ready.map((t) => ({
|
|
2401
2521
|
num: t.num,
|
|
2402
|
-
filename: t.filename
|
|
2522
|
+
filename: t.filename,
|
|
2523
|
+
feature: t.feature
|
|
2403
2524
|
})),
|
|
2404
2525
|
runProgress(tickets),
|
|
2405
2526
|
remaining2.map((t) => ({
|
|
2406
2527
|
num: t.num,
|
|
2407
|
-
filename: t.filename
|
|
2528
|
+
filename: t.filename,
|
|
2529
|
+
feature: t.feature
|
|
2408
2530
|
}))
|
|
2409
2531
|
);
|
|
2410
2532
|
const results = await runPool(
|
|
@@ -2420,7 +2542,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2420
2542
|
/\.md$/,
|
|
2421
2543
|
""
|
|
2422
2544
|
);
|
|
2423
|
-
d.setTicketStatus(
|
|
2545
|
+
d.setTicketStatus(
|
|
2546
|
+
t.num,
|
|
2547
|
+
"in-progress",
|
|
2548
|
+
t.feature
|
|
2549
|
+
);
|
|
2424
2550
|
d.log(` Starting ${t.filename}`);
|
|
2425
2551
|
let level = effort;
|
|
2426
2552
|
let result = await spawnAgent({
|
|
@@ -2471,8 +2597,14 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2471
2597
|
if (r.status === "rejected") continue;
|
|
2472
2598
|
const { ticket, result } = r.value;
|
|
2473
2599
|
if (result.exitCode !== 0) {
|
|
2474
|
-
|
|
2475
|
-
|
|
2600
|
+
failedKeys.add(
|
|
2601
|
+
ticketKey(ticket.feature, ticket.num)
|
|
2602
|
+
);
|
|
2603
|
+
d.setTicketStatus(
|
|
2604
|
+
ticket.num,
|
|
2605
|
+
"failed",
|
|
2606
|
+
ticket.feature
|
|
2607
|
+
);
|
|
2476
2608
|
d.log(
|
|
2477
2609
|
` ${ticket.filename}: agent exited ${result.exitCode}`
|
|
2478
2610
|
);
|
|
@@ -2482,13 +2614,19 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2482
2614
|
markDone(ticket);
|
|
2483
2615
|
waveCompleted++;
|
|
2484
2616
|
totalCompleted++;
|
|
2485
|
-
d.setTicketStatus(
|
|
2617
|
+
d.setTicketStatus(
|
|
2618
|
+
ticket.num,
|
|
2619
|
+
"done",
|
|
2620
|
+
ticket.feature
|
|
2621
|
+
);
|
|
2486
2622
|
d.log(
|
|
2487
2623
|
` ${ticket.filename}: done`
|
|
2488
2624
|
);
|
|
2489
2625
|
}
|
|
2490
2626
|
if (waveCompleted === 0) {
|
|
2491
|
-
const remaining3 = findReadyTickets(tickets).filter((t) => !
|
|
2627
|
+
const remaining3 = findReadyTickets(tickets).filter((t) => !failedKeys.has(
|
|
2628
|
+
ticketKey(t.feature, t.num)
|
|
2629
|
+
));
|
|
2492
2630
|
if (remaining3.length === 0) {
|
|
2493
2631
|
d.log(
|
|
2494
2632
|
"\nNo tickets completed this wave and no ready tickets remain. Stopping."
|
|
@@ -2507,7 +2645,7 @@ No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.le
|
|
|
2507
2645
|
d.log("\n" + renderFinishSummary(
|
|
2508
2646
|
tickets,
|
|
2509
2647
|
totalCompleted,
|
|
2510
|
-
|
|
2648
|
+
failedKeys
|
|
2511
2649
|
));
|
|
2512
2650
|
d.cleanup();
|
|
2513
2651
|
return { allDone: remaining.length === 0 };
|
|
@@ -2529,45 +2667,97 @@ async function runWavesIsolated(opts) {
|
|
|
2529
2667
|
"Working tree is dirty; commit or stash changes before running"
|
|
2530
2668
|
);
|
|
2531
2669
|
}
|
|
2532
|
-
const
|
|
2670
|
+
const features = new Set(
|
|
2671
|
+
opts.tickets.map((t) => t.feature).filter(Boolean)
|
|
2672
|
+
);
|
|
2673
|
+
const multiFeature = features.size > 1 && opts.progressPaths !== void 0;
|
|
2674
|
+
const fallbackProgressPath = join4(
|
|
2533
2675
|
dirname2(opts.logsDir),
|
|
2534
2676
|
"progress.md"
|
|
2535
2677
|
);
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2678
|
+
let tracker;
|
|
2679
|
+
if (multiFeature) {
|
|
2680
|
+
const multiData = generateMultiFeatureProgress(
|
|
2681
|
+
opts.tickets
|
|
2682
|
+
);
|
|
2683
|
+
const dataMap = /* @__PURE__ */ new Map();
|
|
2684
|
+
for (const [feat, fresh] of multiData) {
|
|
2685
|
+
const path = opts.progressPaths.get(feat) ?? fallbackProgressPath;
|
|
2686
|
+
const merged = mergeProgress(
|
|
2687
|
+
fresh,
|
|
2688
|
+
readProgress(path)
|
|
2689
|
+
);
|
|
2690
|
+
dataMap.set(feat, merged);
|
|
2691
|
+
await saveProgress(
|
|
2692
|
+
repoRoot,
|
|
2693
|
+
path,
|
|
2694
|
+
merged,
|
|
2695
|
+
`nightralph: start ${feat}`,
|
|
2696
|
+
d
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
tracker = createProgressTracker({
|
|
2700
|
+
progressPaths: opts.progressPaths,
|
|
2701
|
+
dataMap,
|
|
2702
|
+
fallbackPath: fallbackProgressPath,
|
|
2703
|
+
fallbackData: generateProgress(
|
|
2704
|
+
opts.featureName,
|
|
2705
|
+
[]
|
|
2706
|
+
)
|
|
2707
|
+
});
|
|
2708
|
+
} else {
|
|
2709
|
+
const progressPath = fallbackProgressPath;
|
|
2710
|
+
const progress = mergeProgress(
|
|
2711
|
+
generateProgress(
|
|
2712
|
+
opts.featureName,
|
|
2713
|
+
opts.tickets
|
|
2714
|
+
),
|
|
2715
|
+
readProgress(progressPath)
|
|
2716
|
+
);
|
|
2717
|
+
await saveProgress(
|
|
2718
|
+
repoRoot,
|
|
2719
|
+
progressPath,
|
|
2720
|
+
progress,
|
|
2721
|
+
`nightralph: start ${opts.featureName}`,
|
|
2722
|
+
d
|
|
2723
|
+
);
|
|
2724
|
+
tracker = createProgressTracker({
|
|
2725
|
+
progressPaths: /* @__PURE__ */ new Map(),
|
|
2726
|
+
dataMap: /* @__PURE__ */ new Map(),
|
|
2727
|
+
fallbackPath: progressPath,
|
|
2728
|
+
fallbackData: progress
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2547
2731
|
let totalCompleted = 0;
|
|
2548
2732
|
let waveNum = 0;
|
|
2549
|
-
const
|
|
2733
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
2550
2734
|
while (true) {
|
|
2551
2735
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
2552
|
-
(t) => !
|
|
2736
|
+
(t) => !failedKeys.has(
|
|
2737
|
+
ticketKey(t.feature, t.num)
|
|
2738
|
+
)
|
|
2553
2739
|
);
|
|
2554
2740
|
if (ready.length === 0) break;
|
|
2555
2741
|
waveNum++;
|
|
2556
|
-
const
|
|
2742
|
+
const readyKeys = new Set(
|
|
2743
|
+
ready.map((t) => ticketKey(t.feature, t.num))
|
|
2744
|
+
);
|
|
2557
2745
|
const remaining2 = opts.tickets.filter(
|
|
2558
|
-
(t) => t.status === "ready-for-agent" && !
|
|
2746
|
+
(t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
|
|
2559
2747
|
);
|
|
2560
2748
|
d.startWave(
|
|
2561
2749
|
waveNum,
|
|
2562
2750
|
totalWaves,
|
|
2563
2751
|
ready.map((t) => ({
|
|
2564
2752
|
num: t.num,
|
|
2565
|
-
filename: t.filename
|
|
2753
|
+
filename: t.filename,
|
|
2754
|
+
feature: t.feature
|
|
2566
2755
|
})),
|
|
2567
2756
|
runProgress(opts.tickets),
|
|
2568
2757
|
remaining2.map((t) => ({
|
|
2569
2758
|
num: t.num,
|
|
2570
|
-
filename: t.filename
|
|
2759
|
+
filename: t.filename,
|
|
2760
|
+
feature: t.feature
|
|
2571
2761
|
}))
|
|
2572
2762
|
);
|
|
2573
2763
|
const settled = await runPool(
|
|
@@ -2579,6 +2769,7 @@ async function runWavesIsolated(opts) {
|
|
|
2579
2769
|
repoName,
|
|
2580
2770
|
baseBranch,
|
|
2581
2771
|
specBody: opts.specBody,
|
|
2772
|
+
specBodies: opts.specBodies,
|
|
2582
2773
|
agentCmd: opts.agentCmd,
|
|
2583
2774
|
model: opts.model,
|
|
2584
2775
|
timeout: opts.timeout,
|
|
@@ -2596,86 +2787,104 @@ async function runWavesIsolated(opts) {
|
|
|
2596
2787
|
);
|
|
2597
2788
|
let waveCompleted = 0;
|
|
2598
2789
|
const successfulBranches = [];
|
|
2599
|
-
const
|
|
2600
|
-
const
|
|
2790
|
+
const ticketsByKey = /* @__PURE__ */ new Map();
|
|
2791
|
+
const worktreePathsByKey = /* @__PURE__ */ new Map();
|
|
2792
|
+
const branchToKey = /* @__PURE__ */ new Map();
|
|
2601
2793
|
for (let i = 0; i < outcomes.length; i++) {
|
|
2602
2794
|
const outcome = outcomes[i];
|
|
2603
2795
|
const ticket = ready[i];
|
|
2796
|
+
const key = ticketKey(ticket.feature, ticket.num);
|
|
2604
2797
|
if (outcome.kind === "worktree-error") {
|
|
2605
|
-
|
|
2606
|
-
d.setTicketStatus(
|
|
2798
|
+
failedKeys.add(key);
|
|
2799
|
+
d.setTicketStatus(
|
|
2800
|
+
ticket.num,
|
|
2801
|
+
"failed",
|
|
2802
|
+
ticket.feature
|
|
2803
|
+
);
|
|
2607
2804
|
d.log(
|
|
2608
2805
|
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(outcome.error)}`
|
|
2609
2806
|
);
|
|
2610
|
-
checkTicket(
|
|
2807
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2611
2808
|
branch: "",
|
|
2612
2809
|
exitCode: -1,
|
|
2613
2810
|
done: false
|
|
2614
2811
|
});
|
|
2615
2812
|
await saveProgress(
|
|
2616
2813
|
repoRoot,
|
|
2617
|
-
|
|
2618
|
-
|
|
2814
|
+
tracker.pathFor(ticket.feature),
|
|
2815
|
+
tracker.dataFor(ticket.feature),
|
|
2619
2816
|
`nightralph: ${ticket.filename} worktree error`,
|
|
2620
2817
|
d
|
|
2621
2818
|
);
|
|
2622
2819
|
continue;
|
|
2623
2820
|
}
|
|
2624
2821
|
if (outcome.kind === "error") {
|
|
2625
|
-
|
|
2626
|
-
d.setTicketStatus(
|
|
2822
|
+
failedKeys.add(key);
|
|
2823
|
+
d.setTicketStatus(
|
|
2824
|
+
ticket.num,
|
|
2825
|
+
"failed",
|
|
2826
|
+
ticket.feature
|
|
2827
|
+
);
|
|
2627
2828
|
d.log(
|
|
2628
2829
|
` ${ticket.filename}: attempt error: ` + formatErrorMessage(outcome.error)
|
|
2629
2830
|
);
|
|
2630
|
-
checkTicket(
|
|
2831
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2631
2832
|
branch: outcome.branchName ?? "",
|
|
2632
2833
|
exitCode: -1,
|
|
2633
2834
|
done: false
|
|
2634
2835
|
});
|
|
2635
2836
|
await saveProgress(
|
|
2636
2837
|
repoRoot,
|
|
2637
|
-
|
|
2638
|
-
|
|
2838
|
+
tracker.pathFor(ticket.feature),
|
|
2839
|
+
tracker.dataFor(ticket.feature),
|
|
2639
2840
|
`nightralph: ${ticket.filename} error`,
|
|
2640
2841
|
d
|
|
2641
2842
|
);
|
|
2642
2843
|
continue;
|
|
2643
2844
|
}
|
|
2644
2845
|
if (outcome.kind === "no-commits") {
|
|
2645
|
-
|
|
2646
|
-
d.setTicketStatus(
|
|
2846
|
+
failedKeys.add(key);
|
|
2847
|
+
d.setTicketStatus(
|
|
2848
|
+
ticket.num,
|
|
2849
|
+
"failed",
|
|
2850
|
+
ticket.feature
|
|
2851
|
+
);
|
|
2647
2852
|
d.log(
|
|
2648
2853
|
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
2649
2854
|
);
|
|
2650
|
-
checkTicket(
|
|
2855
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2651
2856
|
branch: outcome.branchName,
|
|
2652
2857
|
exitCode: 0,
|
|
2653
2858
|
done: false
|
|
2654
2859
|
});
|
|
2655
2860
|
await saveProgress(
|
|
2656
2861
|
repoRoot,
|
|
2657
|
-
|
|
2658
|
-
|
|
2862
|
+
tracker.pathFor(ticket.feature),
|
|
2863
|
+
tracker.dataFor(ticket.feature),
|
|
2659
2864
|
`nightralph: ${ticket.filename} no commits`,
|
|
2660
2865
|
d
|
|
2661
2866
|
);
|
|
2662
2867
|
continue;
|
|
2663
2868
|
}
|
|
2664
2869
|
if (outcome.kind === "failed") {
|
|
2665
|
-
|
|
2666
|
-
d.setTicketStatus(
|
|
2870
|
+
failedKeys.add(key);
|
|
2871
|
+
d.setTicketStatus(
|
|
2872
|
+
ticket.num,
|
|
2873
|
+
"failed",
|
|
2874
|
+
ticket.feature
|
|
2875
|
+
);
|
|
2667
2876
|
if (outcome.reason === "exit") {
|
|
2668
2877
|
checkItems(ticket, outcome.completedItems);
|
|
2669
2878
|
}
|
|
2670
|
-
checkTicket(
|
|
2879
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2671
2880
|
branch: outcome.branchName,
|
|
2672
2881
|
exitCode: outcome.exitCode,
|
|
2673
2882
|
done: false
|
|
2674
2883
|
});
|
|
2675
2884
|
await saveProgress(
|
|
2676
2885
|
repoRoot,
|
|
2677
|
-
|
|
2678
|
-
|
|
2886
|
+
tracker.pathFor(ticket.feature),
|
|
2887
|
+
tracker.dataFor(ticket.feature),
|
|
2679
2888
|
`nightralph: ${ticket.filename} ` + (outcome.reason === "tests" ? "tests failed" : "failed"),
|
|
2680
2889
|
d
|
|
2681
2890
|
);
|
|
@@ -2685,11 +2894,12 @@ async function runWavesIsolated(opts) {
|
|
|
2685
2894
|
d.log(
|
|
2686
2895
|
` ${ticket.filename}: agent finished, awaiting merge`
|
|
2687
2896
|
);
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2897
|
+
ticketsByKey.set(key, ticket);
|
|
2898
|
+
worktreePathsByKey.set(
|
|
2899
|
+
key,
|
|
2691
2900
|
outcome.worktreePath
|
|
2692
2901
|
);
|
|
2902
|
+
branchToKey.set(outcome.branchName, key);
|
|
2693
2903
|
successfulBranches.push({
|
|
2694
2904
|
branch: outcome.branchName,
|
|
2695
2905
|
ticketNum: ticket.num,
|
|
@@ -2699,9 +2909,10 @@ async function runWavesIsolated(opts) {
|
|
|
2699
2909
|
if (successfulBranches.length > 0) {
|
|
2700
2910
|
await commitTicketFiles(
|
|
2701
2911
|
repoRoot,
|
|
2702
|
-
successfulBranches.map(
|
|
2703
|
-
|
|
2704
|
-
|
|
2912
|
+
successfulBranches.map((sb) => {
|
|
2913
|
+
const k = branchToKey.get(sb.branch);
|
|
2914
|
+
return ticketsByKey.get(k);
|
|
2915
|
+
}),
|
|
2705
2916
|
d
|
|
2706
2917
|
);
|
|
2707
2918
|
const mergeResult = await mergeWave({
|
|
@@ -2711,32 +2922,35 @@ async function runWavesIsolated(opts) {
|
|
|
2711
2922
|
strategy: opts.conflictStrategy
|
|
2712
2923
|
});
|
|
2713
2924
|
for (const branchResult of mergeResult.merged) {
|
|
2714
|
-
const
|
|
2715
|
-
branchResult.
|
|
2925
|
+
const bk = branchToKey.get(
|
|
2926
|
+
branchResult.branch
|
|
2716
2927
|
);
|
|
2928
|
+
const ticket = bk ? ticketsByKey.get(bk) : void 0;
|
|
2717
2929
|
if (!ticket) continue;
|
|
2718
2930
|
if (branchResult.merged) {
|
|
2719
2931
|
markDone(ticket);
|
|
2720
2932
|
waveCompleted++;
|
|
2721
2933
|
totalCompleted++;
|
|
2722
|
-
d.setTicketStatus(
|
|
2934
|
+
d.setTicketStatus(
|
|
2935
|
+
ticket.num,
|
|
2936
|
+
"done",
|
|
2937
|
+
ticket.feature
|
|
2938
|
+
);
|
|
2723
2939
|
d.log(
|
|
2724
2940
|
` ${ticket.filename}: done`
|
|
2725
2941
|
);
|
|
2726
|
-
checkTicket(
|
|
2942
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2727
2943
|
branch: branchResult.branch,
|
|
2728
2944
|
exitCode: 0
|
|
2729
2945
|
});
|
|
2730
2946
|
await saveProgress(
|
|
2731
2947
|
repoRoot,
|
|
2732
|
-
|
|
2733
|
-
|
|
2948
|
+
tracker.pathFor(ticket.feature),
|
|
2949
|
+
tracker.dataFor(ticket.feature),
|
|
2734
2950
|
`nightralph: ${ticket.filename} done`,
|
|
2735
2951
|
d
|
|
2736
2952
|
);
|
|
2737
|
-
const worktreePath =
|
|
2738
|
-
ticket.num
|
|
2739
|
-
);
|
|
2953
|
+
const worktreePath = bk ? worktreePathsByKey.get(bk) : void 0;
|
|
2740
2954
|
if (worktreePath) {
|
|
2741
2955
|
await removeWorktreeQuietly(
|
|
2742
2956
|
worktreePath,
|
|
@@ -2744,16 +2958,21 @@ async function runWavesIsolated(opts) {
|
|
|
2744
2958
|
);
|
|
2745
2959
|
}
|
|
2746
2960
|
} else {
|
|
2747
|
-
const
|
|
2748
|
-
|
|
2961
|
+
const wtKey = branchToKey.get(
|
|
2962
|
+
branchResult.branch
|
|
2749
2963
|
);
|
|
2964
|
+
const worktreePath = wtKey ? worktreePathsByKey.get(wtKey) : void 0;
|
|
2750
2965
|
const recovered = opts.conflictStrategy === "respawn" && worktreePath ? await respawnAndRetryMerge({
|
|
2751
2966
|
repoRoot,
|
|
2752
2967
|
baseBranch,
|
|
2753
2968
|
branch: branchResult.branch,
|
|
2754
2969
|
ticket,
|
|
2755
2970
|
worktreePath,
|
|
2756
|
-
specBody:
|
|
2971
|
+
specBody: lookupSpec(
|
|
2972
|
+
opts.specBodies,
|
|
2973
|
+
opts.specBody,
|
|
2974
|
+
ticket.feature
|
|
2975
|
+
),
|
|
2757
2976
|
agentCmd: opts.agentCmd,
|
|
2758
2977
|
model: opts.model,
|
|
2759
2978
|
timeout: opts.timeout,
|
|
@@ -2766,18 +2985,22 @@ async function runWavesIsolated(opts) {
|
|
|
2766
2985
|
markDone(ticket);
|
|
2767
2986
|
waveCompleted++;
|
|
2768
2987
|
totalCompleted++;
|
|
2769
|
-
d.setTicketStatus(
|
|
2988
|
+
d.setTicketStatus(
|
|
2989
|
+
ticket.num,
|
|
2990
|
+
"done",
|
|
2991
|
+
ticket.feature
|
|
2992
|
+
);
|
|
2770
2993
|
d.log(
|
|
2771
2994
|
` ${ticket.filename}: done (resolved via respawn)`
|
|
2772
2995
|
);
|
|
2773
|
-
checkTicket(
|
|
2996
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2774
2997
|
branch: branchResult.branch,
|
|
2775
2998
|
exitCode: 0
|
|
2776
2999
|
});
|
|
2777
3000
|
await saveProgress(
|
|
2778
3001
|
repoRoot,
|
|
2779
|
-
|
|
2780
|
-
|
|
3002
|
+
tracker.pathFor(ticket.feature),
|
|
3003
|
+
tracker.dataFor(ticket.feature),
|
|
2781
3004
|
`nightralph: ${ticket.filename} done (respawn)`,
|
|
2782
3005
|
d
|
|
2783
3006
|
);
|
|
@@ -2794,7 +3017,9 @@ async function runWavesIsolated(opts) {
|
|
|
2794
3017
|
}
|
|
2795
3018
|
}
|
|
2796
3019
|
} else {
|
|
2797
|
-
|
|
3020
|
+
failedKeys.add(
|
|
3021
|
+
ticketKey(ticket.feature, ticket.num)
|
|
3022
|
+
);
|
|
2798
3023
|
d.log(
|
|
2799
3024
|
` ${ticket.filename}: merge failed, staying ready-for-agent`
|
|
2800
3025
|
);
|
|
@@ -2803,15 +3028,15 @@ async function runWavesIsolated(opts) {
|
|
|
2803
3028
|
" git: " + branchResult.stderr.trim()
|
|
2804
3029
|
);
|
|
2805
3030
|
}
|
|
2806
|
-
checkTicket(
|
|
3031
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
2807
3032
|
branch: branchResult.branch,
|
|
2808
3033
|
exitCode: 0,
|
|
2809
3034
|
done: false
|
|
2810
3035
|
});
|
|
2811
3036
|
await saveProgress(
|
|
2812
3037
|
repoRoot,
|
|
2813
|
-
|
|
2814
|
-
|
|
3038
|
+
tracker.pathFor(ticket.feature),
|
|
3039
|
+
tracker.dataFor(ticket.feature),
|
|
2815
3040
|
`nightralph: ${ticket.filename} merge failed`,
|
|
2816
3041
|
d
|
|
2817
3042
|
);
|
|
@@ -2824,7 +3049,8 @@ async function runWavesIsolated(opts) {
|
|
|
2824
3049
|
Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
2825
3050
|
);
|
|
2826
3051
|
for (const sb of successfulBranches) {
|
|
2827
|
-
const
|
|
3052
|
+
const sbk = branchToKey.get(sb.branch);
|
|
3053
|
+
const ticket = sbk ? ticketsByKey.get(sbk) : void 0;
|
|
2828
3054
|
if (ticket && ticket.status === "done") continue;
|
|
2829
3055
|
try {
|
|
2830
3056
|
await removeWorktree(
|
|
@@ -2843,7 +3069,9 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
2843
3069
|
if (waveCompleted === 0) {
|
|
2844
3070
|
const remaining3 = findReadyTickets(
|
|
2845
3071
|
opts.tickets
|
|
2846
|
-
).filter((t) => !
|
|
3072
|
+
).filter((t) => !failedKeys.has(
|
|
3073
|
+
ticketKey(t.feature, t.num)
|
|
3074
|
+
));
|
|
2847
3075
|
if (remaining3.length === 0) {
|
|
2848
3076
|
d.log(
|
|
2849
3077
|
"\nNo tickets completed this wave and no ready tickets remain. Stopping."
|
|
@@ -2862,7 +3090,7 @@ No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.le
|
|
|
2862
3090
|
d.log("\n" + renderFinishSummary(
|
|
2863
3091
|
opts.tickets,
|
|
2864
3092
|
totalCompleted,
|
|
2865
|
-
|
|
3093
|
+
failedKeys
|
|
2866
3094
|
));
|
|
2867
3095
|
d.cleanup();
|
|
2868
3096
|
return { allDone: remaining.length === 0 };
|
|
@@ -2873,7 +3101,7 @@ __name(runWavesIsolated, "runWavesIsolated");
|
|
|
2873
3101
|
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
2874
3102
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2875
3103
|
import { fileURLToPath } from "node:url";
|
|
2876
|
-
import {
|
|
3104
|
+
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2877
3105
|
function getScriptDir(metaUrl) {
|
|
2878
3106
|
return dirname3(fileURLToPath(metaUrl));
|
|
2879
3107
|
}
|
|
@@ -2898,24 +3126,32 @@ function findFeatureDirs(cwd) {
|
|
|
2898
3126
|
entry.name,
|
|
2899
3127
|
"spec.md"
|
|
2900
3128
|
);
|
|
2901
|
-
if (existsSync4(issuesDir))
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3129
|
+
if (!existsSync4(issuesDir)) continue;
|
|
3130
|
+
const progressFile = join5(
|
|
3131
|
+
scratchDir,
|
|
3132
|
+
entry.name,
|
|
3133
|
+
"progress.md"
|
|
3134
|
+
);
|
|
3135
|
+
if (isSpecComplete(progressFile)) continue;
|
|
3136
|
+
results.push({
|
|
3137
|
+
name: entry.name,
|
|
3138
|
+
dir: issuesDir,
|
|
3139
|
+
spec: specFile
|
|
3140
|
+
});
|
|
2908
3141
|
}
|
|
3142
|
+
results.sort((a, b) => a.name.localeCompare(b.name));
|
|
2909
3143
|
return results;
|
|
2910
3144
|
}
|
|
2911
3145
|
__name(findFeatureDirs, "findFeatureDirs");
|
|
3146
|
+
var ALL_VALUE = "__all__";
|
|
2912
3147
|
async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
2913
3148
|
if (specName) {
|
|
2914
3149
|
const featureDir = join5(cwd, ".scratch", specName);
|
|
2915
|
-
return {
|
|
3150
|
+
return [{
|
|
2916
3151
|
dir: join5(featureDir, "issues"),
|
|
2917
|
-
spec: join5(featureDir, "spec.md")
|
|
2918
|
-
|
|
3152
|
+
spec: join5(featureDir, "spec.md"),
|
|
3153
|
+
name: specName
|
|
3154
|
+
}];
|
|
2919
3155
|
}
|
|
2920
3156
|
const features = findFeatureDirs(cwd);
|
|
2921
3157
|
if (features.length === 0) {
|
|
@@ -2924,21 +3160,40 @@ async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
|
2924
3160
|
);
|
|
2925
3161
|
process.exit(2);
|
|
2926
3162
|
}
|
|
2927
|
-
let chosen;
|
|
2928
3163
|
if (features.length === 1) {
|
|
2929
|
-
|
|
2930
|
-
console.log(`Using feature: ${
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
})
|
|
3164
|
+
const f = features[0];
|
|
3165
|
+
console.log(`Using feature: ${f.name}`);
|
|
3166
|
+
return [{ dir: f.dir, spec: f.spec, name: f.name }];
|
|
3167
|
+
}
|
|
3168
|
+
const featureChoices = features.map((f) => {
|
|
3169
|
+
const tickets = scanTickets(f.dir);
|
|
3170
|
+
const ready = findReadyTickets(tickets).length;
|
|
3171
|
+
return {
|
|
3172
|
+
name: `${f.name} (${ready}/${tickets.length} ready)`,
|
|
3173
|
+
value: f.name
|
|
3174
|
+
};
|
|
3175
|
+
});
|
|
3176
|
+
while (true) {
|
|
3177
|
+
const selected = await checkbox({
|
|
3178
|
+
message: "Which features?",
|
|
3179
|
+
choices: [
|
|
3180
|
+
{ name: "All", value: ALL_VALUE },
|
|
3181
|
+
new Separator(),
|
|
3182
|
+
...featureChoices
|
|
3183
|
+
]
|
|
3184
|
+
});
|
|
3185
|
+
if (selected.length === 0) {
|
|
3186
|
+
console.log("Select at least one feature");
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
const names = selected.includes(ALL_VALUE) ? features.map((f) => f.name) : selected;
|
|
3190
|
+
return names.map((name) => {
|
|
3191
|
+
const f = features.find(
|
|
3192
|
+
(ff) => ff.name === name
|
|
3193
|
+
);
|
|
3194
|
+
return { dir: f.dir, spec: f.spec, name: f.name };
|
|
2938
3195
|
});
|
|
2939
|
-
chosen = features.find((f) => f.name === name);
|
|
2940
3196
|
}
|
|
2941
|
-
return { dir: chosen.dir, spec: chosen.spec };
|
|
2942
3197
|
}
|
|
2943
3198
|
__name(resolveIssuesDir, "resolveIssuesDir");
|
|
2944
3199
|
|
|
@@ -2949,7 +3204,7 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
2949
3204
|
"Install bundled skills and doc templates",
|
|
2950
3205
|
(y) => y,
|
|
2951
3206
|
async () => {
|
|
2952
|
-
const tracker = await
|
|
3207
|
+
const tracker = await select({
|
|
2953
3208
|
message: "Issue tracker format",
|
|
2954
3209
|
choices: [
|
|
2955
3210
|
{
|
|
@@ -3072,61 +3327,86 @@ function extractFeatureName(spec) {
|
|
|
3072
3327
|
}
|
|
3073
3328
|
__name(extractFeatureName, "extractFeatureName");
|
|
3074
3329
|
async function runExecute(argv) {
|
|
3075
|
-
const
|
|
3330
|
+
const selections = await resolveIssuesDir(
|
|
3076
3331
|
argv.spec
|
|
3077
3332
|
);
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3333
|
+
const allTickets = [];
|
|
3334
|
+
const specBodies = /* @__PURE__ */ new Map();
|
|
3335
|
+
const progressPaths = /* @__PURE__ */ new Map();
|
|
3336
|
+
let firstFeatureName = "";
|
|
3337
|
+
let firstLogsDir = "";
|
|
3338
|
+
for (const selection of selections) {
|
|
3339
|
+
const { dir, spec, name } = selection;
|
|
3340
|
+
if (!existsSync5(dir)) {
|
|
3341
|
+
console.error(
|
|
3342
|
+
`Directory not found: ${dir}`
|
|
3343
|
+
);
|
|
3344
|
+
process.exit(2);
|
|
3345
|
+
}
|
|
3346
|
+
if (!existsSync5(spec)) {
|
|
3347
|
+
console.error(
|
|
3348
|
+
`Spec file not found: ${spec}`
|
|
3349
|
+
);
|
|
3350
|
+
process.exit(2);
|
|
3351
|
+
}
|
|
3352
|
+
const specBody = stripSpecSection(
|
|
3353
|
+
readFileSync5(spec, "utf8"),
|
|
3354
|
+
"User Stories"
|
|
3355
|
+
);
|
|
3356
|
+
const tickets = scanTickets(dir, name);
|
|
3357
|
+
if (tickets.length === 0) {
|
|
3358
|
+
console.log(
|
|
3359
|
+
`No ticket files found for ${name}. Skipping.`
|
|
3360
|
+
);
|
|
3361
|
+
continue;
|
|
3362
|
+
}
|
|
3363
|
+
console.log(
|
|
3364
|
+
`[${name}] Found ${tickets.length} tickets`
|
|
3365
|
+
);
|
|
3366
|
+
const knownNums = new Set(
|
|
3367
|
+
tickets.map((t) => t.num)
|
|
3100
3368
|
);
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3369
|
+
for (const t of tickets) {
|
|
3370
|
+
const unknown = t.blockers.filter(
|
|
3371
|
+
(b) => !knownNums.has(b)
|
|
3372
|
+
);
|
|
3373
|
+
if (unknown.length > 0) {
|
|
3374
|
+
console.warn(
|
|
3375
|
+
`${t.filename}: blockers [${unknown.join(", ")}] not found in directory`
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
t.blockers = t.blockers.filter(
|
|
3379
|
+
(b) => knownNums.has(b)
|
|
3104
3380
|
);
|
|
3105
3381
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3382
|
+
specBodies.set(name, specBody);
|
|
3383
|
+
progressPaths.set(
|
|
3384
|
+
name,
|
|
3385
|
+
join6(dirname4(dir), "progress.md")
|
|
3108
3386
|
);
|
|
3387
|
+
allTickets.push(...tickets);
|
|
3388
|
+
if (!firstFeatureName) {
|
|
3389
|
+
firstFeatureName = extractFeatureName(spec);
|
|
3390
|
+
firstLogsDir = join6(dirname4(dir), "logs");
|
|
3391
|
+
}
|
|
3109
3392
|
}
|
|
3110
|
-
|
|
3393
|
+
if (allTickets.length === 0) return;
|
|
3111
3394
|
const timeout = argv.timeout ?? 900;
|
|
3112
3395
|
const testCmd = argv.testCmd || detectTestCmd(
|
|
3113
3396
|
isGitRepo() ? getRepoRoot() : process.cwd()
|
|
3114
3397
|
) || "npm test";
|
|
3115
|
-
const featureName = extractFeatureName(spec);
|
|
3116
3398
|
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
"progress.md"
|
|
3120
|
-
);
|
|
3399
|
+
const logsDir = firstLogsDir;
|
|
3400
|
+
const fallbackSpec = specBodies.values().next().value;
|
|
3121
3401
|
if (argv.dryRun) {
|
|
3122
3402
|
if (isGitRepo()) {
|
|
3123
3403
|
const repoName = getRepoName();
|
|
3124
3404
|
dryRunIsolated({
|
|
3125
|
-
tickets,
|
|
3126
|
-
specBody,
|
|
3405
|
+
tickets: allTickets,
|
|
3406
|
+
specBody: fallbackSpec,
|
|
3127
3407
|
repoName,
|
|
3128
3408
|
conflictStrategy,
|
|
3129
|
-
progressPath,
|
|
3409
|
+
progressPath: progressPaths.values().next().value,
|
|
3130
3410
|
testCmd,
|
|
3131
3411
|
effort: argv.effort,
|
|
3132
3412
|
retries: argv.retries,
|
|
@@ -3134,7 +3414,7 @@ async function runExecute(argv) {
|
|
|
3134
3414
|
concurrency: argv.concurrency
|
|
3135
3415
|
});
|
|
3136
3416
|
} else {
|
|
3137
|
-
dryRun(
|
|
3417
|
+
dryRun(allTickets, fallbackSpec, testCmd);
|
|
3138
3418
|
}
|
|
3139
3419
|
return;
|
|
3140
3420
|
}
|
|
@@ -3150,13 +3430,14 @@ async function runExecute(argv) {
|
|
|
3150
3430
|
let result;
|
|
3151
3431
|
if (isGitRepo()) {
|
|
3152
3432
|
result = await runWavesIsolated({
|
|
3153
|
-
tickets,
|
|
3154
|
-
specBody,
|
|
3433
|
+
tickets: allTickets,
|
|
3434
|
+
specBody: fallbackSpec,
|
|
3435
|
+
specBodies,
|
|
3155
3436
|
agentCmd: argv.provider,
|
|
3156
3437
|
model: argv.model ?? null,
|
|
3157
3438
|
timeout,
|
|
3158
3439
|
logsDir,
|
|
3159
|
-
featureName,
|
|
3440
|
+
featureName: firstFeatureName,
|
|
3160
3441
|
conflictStrategy,
|
|
3161
3442
|
display,
|
|
3162
3443
|
testCmd,
|
|
@@ -3164,12 +3445,13 @@ async function runExecute(argv) {
|
|
|
3164
3445
|
effort: argv.effort,
|
|
3165
3446
|
retries: argv.retries,
|
|
3166
3447
|
retryDelay: argv.retryDelay,
|
|
3167
|
-
concurrency: argv.concurrency
|
|
3448
|
+
concurrency: argv.concurrency,
|
|
3449
|
+
progressPaths
|
|
3168
3450
|
});
|
|
3169
3451
|
} else {
|
|
3170
3452
|
result = await runWaves(
|
|
3171
|
-
|
|
3172
|
-
|
|
3453
|
+
allTickets,
|
|
3454
|
+
fallbackSpec,
|
|
3173
3455
|
argv.provider,
|
|
3174
3456
|
argv.model ?? null,
|
|
3175
3457
|
timeout,
|