nightralph 0.0.48 → 0.0.52
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 +6 -0
- 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/orchestrator.js
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
} from "./worktree.js";
|
|
28
28
|
import {
|
|
29
29
|
generateProgress,
|
|
30
|
+
generateMultiFeatureProgress,
|
|
31
|
+
createProgressTracker,
|
|
30
32
|
readProgress,
|
|
31
33
|
mergeProgress,
|
|
32
34
|
writeProgress,
|
|
@@ -124,7 +126,7 @@ function parseBlockers(body) {
|
|
|
124
126
|
return nums;
|
|
125
127
|
}
|
|
126
128
|
__name(parseBlockers, "parseBlockers");
|
|
127
|
-
function scanTickets(dir) {
|
|
129
|
+
function scanTickets(dir, feature = "") {
|
|
128
130
|
const files = readdirSync(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
129
131
|
b,
|
|
130
132
|
void 0,
|
|
@@ -141,18 +143,25 @@ function scanTickets(dir) {
|
|
|
141
143
|
filepath,
|
|
142
144
|
body,
|
|
143
145
|
status: parseStatus(body),
|
|
144
|
-
blockers: parseBlockers(body)
|
|
146
|
+
blockers: parseBlockers(body),
|
|
147
|
+
feature
|
|
145
148
|
};
|
|
146
149
|
});
|
|
147
150
|
}
|
|
148
151
|
__name(scanTickets, "scanTickets");
|
|
152
|
+
function ticketKey(feature, num) {
|
|
153
|
+
return `${feature}:${num}`;
|
|
154
|
+
}
|
|
155
|
+
__name(ticketKey, "ticketKey");
|
|
149
156
|
function findReadyTickets(tickets) {
|
|
150
|
-
const
|
|
151
|
-
tickets.filter((t) => t.status === "done").map((t) => t.num)
|
|
157
|
+
const doneKeys = new Set(
|
|
158
|
+
tickets.filter((t) => t.status === "done").map((t) => ticketKey(t.feature, t.num))
|
|
152
159
|
);
|
|
153
160
|
return tickets.filter((t) => {
|
|
154
161
|
if (t.status !== "ready-for-agent") return false;
|
|
155
|
-
return t.blockers.every(
|
|
162
|
+
return t.blockers.every(
|
|
163
|
+
(b) => doneKeys.has(ticketKey(t.feature, b))
|
|
164
|
+
);
|
|
156
165
|
});
|
|
157
166
|
}
|
|
158
167
|
__name(findReadyTickets, "findReadyTickets");
|
|
@@ -296,6 +305,11 @@ function stripSpecSection(specBody, heading) {
|
|
|
296
305
|
return out.join("\n");
|
|
297
306
|
}
|
|
298
307
|
__name(stripSpecSection, "stripSpecSection");
|
|
308
|
+
function lookupSpec(specBodies, fallback, feature) {
|
|
309
|
+
if (!feature || !specBodies) return fallback;
|
|
310
|
+
return specBodies.get(feature) ?? fallback;
|
|
311
|
+
}
|
|
312
|
+
__name(lookupSpec, "lookupSpec");
|
|
299
313
|
function renderPrompt(specBody, ticket, opts) {
|
|
300
314
|
return [
|
|
301
315
|
"---- SPEC CONTEXT ----",
|
|
@@ -363,58 +377,72 @@ function listTickets(tickets) {
|
|
|
363
377
|
__name(listTickets, "listTickets");
|
|
364
378
|
function* simulateWaves(tickets) {
|
|
365
379
|
const sim = new Map(
|
|
366
|
-
tickets.map((t) => [
|
|
380
|
+
tickets.map((t) => [
|
|
381
|
+
ticketKey(t.feature, t.num),
|
|
382
|
+
t.status
|
|
383
|
+
])
|
|
367
384
|
);
|
|
368
385
|
let wave = 1;
|
|
369
386
|
while (true) {
|
|
370
|
-
const
|
|
371
|
-
[...sim.entries()].filter(([, s]) => s === "done").map(([
|
|
387
|
+
const doneKeys = new Set(
|
|
388
|
+
[...sim.entries()].filter(([, s]) => s === "done").map(([k]) => k)
|
|
372
389
|
);
|
|
373
390
|
const ready = [...tickets].filter((t) => {
|
|
374
|
-
|
|
391
|
+
const key = ticketKey(t.feature, t.num);
|
|
392
|
+
if (sim.get(key) !== "ready-for-agent") {
|
|
375
393
|
return false;
|
|
376
394
|
}
|
|
377
395
|
return t.blockers.every(
|
|
378
|
-
(b) =>
|
|
396
|
+
(b) => doneKeys.has(
|
|
397
|
+
ticketKey(t.feature, b)
|
|
398
|
+
)
|
|
379
399
|
);
|
|
380
400
|
});
|
|
381
401
|
if (ready.length === 0) break;
|
|
382
402
|
yield { wave, ready };
|
|
383
403
|
for (const t of ready) {
|
|
384
|
-
sim.set(
|
|
404
|
+
sim.set(
|
|
405
|
+
ticketKey(t.feature, t.num),
|
|
406
|
+
"done"
|
|
407
|
+
);
|
|
385
408
|
}
|
|
386
409
|
wave++;
|
|
387
410
|
}
|
|
388
411
|
}
|
|
389
412
|
__name(simulateWaves, "simulateWaves");
|
|
390
413
|
function detectDeadlocks(tickets) {
|
|
391
|
-
const
|
|
392
|
-
const
|
|
414
|
+
const doneKeys = /* @__PURE__ */ new Set();
|
|
415
|
+
const readyKeys = /* @__PURE__ */ new Set();
|
|
393
416
|
for (const t of tickets) {
|
|
417
|
+
const key = ticketKey(t.feature, t.num);
|
|
394
418
|
if (t.status === "ready-for-agent") {
|
|
395
|
-
|
|
419
|
+
readyKeys.add(key);
|
|
396
420
|
} else if (t.status === "done") {
|
|
397
|
-
|
|
421
|
+
doneKeys.add(key);
|
|
398
422
|
}
|
|
399
423
|
}
|
|
400
424
|
let changed = true;
|
|
401
425
|
while (changed) {
|
|
402
426
|
changed = false;
|
|
403
|
-
for (const
|
|
427
|
+
for (const key of readyKeys) {
|
|
404
428
|
const ticket = tickets.find(
|
|
405
|
-
(t) => t.num ===
|
|
429
|
+
(t) => ticketKey(t.feature, t.num) === key
|
|
406
430
|
);
|
|
407
431
|
if (ticket.blockers.every(
|
|
408
|
-
(b) =>
|
|
432
|
+
(b) => doneKeys.has(
|
|
433
|
+
ticketKey(ticket.feature, b)
|
|
434
|
+
)
|
|
409
435
|
)) {
|
|
410
|
-
|
|
411
|
-
|
|
436
|
+
doneKeys.add(key);
|
|
437
|
+
readyKeys.delete(key);
|
|
412
438
|
changed = true;
|
|
413
439
|
}
|
|
414
440
|
}
|
|
415
441
|
}
|
|
416
|
-
return [...
|
|
417
|
-
(
|
|
442
|
+
return [...readyKeys].map(
|
|
443
|
+
(key) => tickets.find(
|
|
444
|
+
(t) => ticketKey(t.feature, t.num) === key
|
|
445
|
+
)
|
|
418
446
|
);
|
|
419
447
|
}
|
|
420
448
|
__name(detectDeadlocks, "detectDeadlocks");
|
|
@@ -868,7 +896,8 @@ function dryRunIsolated(opts) {
|
|
|
868
896
|
opts.repoName,
|
|
869
897
|
t
|
|
870
898
|
);
|
|
871
|
-
const
|
|
899
|
+
const prefix = t.feature ? `${t.feature}.` : "";
|
|
900
|
+
const worktreePath = `../${opts.repoName}.${prefix}${num}-${t.slug}`;
|
|
872
901
|
console.log(` worktree: ${worktreePath}`);
|
|
873
902
|
console.log(` branch: ${branchName}`);
|
|
874
903
|
}
|
|
@@ -1088,10 +1117,19 @@ async function runTicketAttempt(opts) {
|
|
|
1088
1117
|
} catch (error) {
|
|
1089
1118
|
return { kind: "worktree-error", error };
|
|
1090
1119
|
}
|
|
1091
|
-
d.setTicketStatus(
|
|
1120
|
+
d.setTicketStatus(
|
|
1121
|
+
ticket.num,
|
|
1122
|
+
"in-progress",
|
|
1123
|
+
ticket.feature
|
|
1124
|
+
);
|
|
1125
|
+
const ticketSpec = lookupSpec(
|
|
1126
|
+
opts.specBodies,
|
|
1127
|
+
opts.specBody,
|
|
1128
|
+
ticket.feature
|
|
1129
|
+
);
|
|
1092
1130
|
const agentResult = await spawnAgent({
|
|
1093
1131
|
prompt: renderPrompt(
|
|
1094
|
-
|
|
1132
|
+
ticketSpec,
|
|
1095
1133
|
ticket,
|
|
1096
1134
|
{ testCmd: opts.testCmd }
|
|
1097
1135
|
),
|
|
@@ -1221,7 +1259,11 @@ async function runTicketAttempts(opts) {
|
|
|
1221
1259
|
return { kind: "error", error };
|
|
1222
1260
|
}
|
|
1223
1261
|
if (outcome.kind === "ready") {
|
|
1224
|
-
d.setTicketStatus(
|
|
1262
|
+
d.setTicketStatus(
|
|
1263
|
+
ticket.num,
|
|
1264
|
+
"awaiting-merge",
|
|
1265
|
+
ticket.feature
|
|
1266
|
+
);
|
|
1225
1267
|
return outcome;
|
|
1226
1268
|
}
|
|
1227
1269
|
if (outcome.kind === "worktree-error") return outcome;
|
|
@@ -1245,24 +1287,50 @@ function indentFilenames(tickets) {
|
|
|
1245
1287
|
return tickets.map((t) => ` ${t.filename}`).join("\n");
|
|
1246
1288
|
}
|
|
1247
1289
|
__name(indentFilenames, "indentFilenames");
|
|
1248
|
-
function renderFinishSummary(tickets, totalCompleted,
|
|
1290
|
+
function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
1249
1291
|
const remaining = tickets.filter(
|
|
1250
1292
|
(t) => t.status === "ready-for-agent"
|
|
1251
1293
|
);
|
|
1252
1294
|
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
1253
1295
|
const humanNums = new Set(human.map((t) => t.num));
|
|
1254
1296
|
const blockedByHuman = remaining.filter(
|
|
1255
|
-
(t) => !
|
|
1297
|
+
(t) => !failedKeys.has(ticketKey(t.feature, t.num)) && t.blockers.some((b) => humanNums.has(b))
|
|
1298
|
+
);
|
|
1299
|
+
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` : "");
|
|
1300
|
+
const lines = [counts];
|
|
1301
|
+
const features = new Set(
|
|
1302
|
+
tickets.map((t) => t.feature).filter(Boolean)
|
|
1256
1303
|
);
|
|
1257
|
-
|
|
1258
|
-
|
|
1304
|
+
if (features.size > 1) {
|
|
1305
|
+
lines.push("");
|
|
1306
|
+
for (const feat of [...features].sort()) {
|
|
1307
|
+
const ft = tickets.filter(
|
|
1308
|
+
(t) => t.feature === feat
|
|
1309
|
+
);
|
|
1310
|
+
const done = ft.filter(
|
|
1311
|
+
(t) => t.status === "done"
|
|
1312
|
+
).length;
|
|
1313
|
+
const failed = ft.filter(
|
|
1314
|
+
(t) => failedKeys.has(
|
|
1315
|
+
ticketKey(t.feature, t.num)
|
|
1316
|
+
)
|
|
1317
|
+
).length;
|
|
1318
|
+
const rem = ft.filter(
|
|
1319
|
+
(t) => t.status === "ready-for-agent"
|
|
1320
|
+
).length;
|
|
1321
|
+
let detail = ` ${feat}: ${done} completed`;
|
|
1322
|
+
if (rem > 0) detail += `, ${rem} remaining`;
|
|
1323
|
+
if (failed > 0) detail += `, ${failed} failed`;
|
|
1324
|
+
lines.push(detail);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (human.length === 0) return lines.join("\n");
|
|
1259
1328
|
const noun = human.length === 1 ? "ticket is" : "tickets are";
|
|
1260
|
-
|
|
1261
|
-
counts,
|
|
1329
|
+
lines.push(
|
|
1262
1330
|
"",
|
|
1263
|
-
(remaining.length === 0 &&
|
|
1331
|
+
(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:",
|
|
1264
1332
|
indentFilenames(human)
|
|
1265
|
-
|
|
1333
|
+
);
|
|
1266
1334
|
if (blockedByHuman.length > 0) {
|
|
1267
1335
|
const n = blockedByHuman.length;
|
|
1268
1336
|
lines.push(
|
|
@@ -1295,28 +1363,34 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1295
1363
|
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
1296
1364
|
let totalCompleted = 0;
|
|
1297
1365
|
let waveNum = 0;
|
|
1298
|
-
const
|
|
1366
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
1299
1367
|
while (true) {
|
|
1300
1368
|
const ready = findReadyTickets(tickets).filter(
|
|
1301
|
-
(t) => !
|
|
1369
|
+
(t) => !failedKeys.has(
|
|
1370
|
+
ticketKey(t.feature, t.num)
|
|
1371
|
+
)
|
|
1302
1372
|
);
|
|
1303
1373
|
if (ready.length === 0) break;
|
|
1304
1374
|
waveNum++;
|
|
1305
|
-
const
|
|
1375
|
+
const readyKeys = new Set(
|
|
1376
|
+
ready.map((t) => ticketKey(t.feature, t.num))
|
|
1377
|
+
);
|
|
1306
1378
|
const remaining2 = tickets.filter(
|
|
1307
|
-
(t) => t.status === "ready-for-agent" && !
|
|
1379
|
+
(t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
|
|
1308
1380
|
);
|
|
1309
1381
|
d.startWave(
|
|
1310
1382
|
waveNum,
|
|
1311
1383
|
totalWaves,
|
|
1312
1384
|
ready.map((t) => ({
|
|
1313
1385
|
num: t.num,
|
|
1314
|
-
filename: t.filename
|
|
1386
|
+
filename: t.filename,
|
|
1387
|
+
feature: t.feature
|
|
1315
1388
|
})),
|
|
1316
1389
|
runProgress(tickets),
|
|
1317
1390
|
remaining2.map((t) => ({
|
|
1318
1391
|
num: t.num,
|
|
1319
|
-
filename: t.filename
|
|
1392
|
+
filename: t.filename,
|
|
1393
|
+
feature: t.feature
|
|
1320
1394
|
}))
|
|
1321
1395
|
);
|
|
1322
1396
|
const results = await runPool(
|
|
@@ -1332,7 +1406,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1332
1406
|
/\.md$/,
|
|
1333
1407
|
""
|
|
1334
1408
|
);
|
|
1335
|
-
d.setTicketStatus(
|
|
1409
|
+
d.setTicketStatus(
|
|
1410
|
+
t.num,
|
|
1411
|
+
"in-progress",
|
|
1412
|
+
t.feature
|
|
1413
|
+
);
|
|
1336
1414
|
d.log(` Starting ${t.filename}`);
|
|
1337
1415
|
let level = effort;
|
|
1338
1416
|
let result = await spawnAgent({
|
|
@@ -1383,8 +1461,14 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1383
1461
|
if (r.status === "rejected") continue;
|
|
1384
1462
|
const { ticket, result } = r.value;
|
|
1385
1463
|
if (result.exitCode !== 0) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1464
|
+
failedKeys.add(
|
|
1465
|
+
ticketKey(ticket.feature, ticket.num)
|
|
1466
|
+
);
|
|
1467
|
+
d.setTicketStatus(
|
|
1468
|
+
ticket.num,
|
|
1469
|
+
"failed",
|
|
1470
|
+
ticket.feature
|
|
1471
|
+
);
|
|
1388
1472
|
d.log(
|
|
1389
1473
|
` ${ticket.filename}: agent exited ${result.exitCode}`
|
|
1390
1474
|
);
|
|
@@ -1394,13 +1478,19 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
1394
1478
|
markDone(ticket);
|
|
1395
1479
|
waveCompleted++;
|
|
1396
1480
|
totalCompleted++;
|
|
1397
|
-
d.setTicketStatus(
|
|
1481
|
+
d.setTicketStatus(
|
|
1482
|
+
ticket.num,
|
|
1483
|
+
"done",
|
|
1484
|
+
ticket.feature
|
|
1485
|
+
);
|
|
1398
1486
|
d.log(
|
|
1399
1487
|
` ${ticket.filename}: done`
|
|
1400
1488
|
);
|
|
1401
1489
|
}
|
|
1402
1490
|
if (waveCompleted === 0) {
|
|
1403
|
-
const remaining3 = findReadyTickets(tickets).filter((t) => !
|
|
1491
|
+
const remaining3 = findReadyTickets(tickets).filter((t) => !failedKeys.has(
|
|
1492
|
+
ticketKey(t.feature, t.num)
|
|
1493
|
+
));
|
|
1404
1494
|
if (remaining3.length === 0) {
|
|
1405
1495
|
d.log(
|
|
1406
1496
|
"\nNo tickets completed this wave and no ready tickets remain. Stopping."
|
|
@@ -1419,7 +1509,7 @@ No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.le
|
|
|
1419
1509
|
d.log("\n" + renderFinishSummary(
|
|
1420
1510
|
tickets,
|
|
1421
1511
|
totalCompleted,
|
|
1422
|
-
|
|
1512
|
+
failedKeys
|
|
1423
1513
|
));
|
|
1424
1514
|
d.cleanup();
|
|
1425
1515
|
return { allDone: remaining.length === 0 };
|
|
@@ -1441,45 +1531,97 @@ async function runWavesIsolated(opts) {
|
|
|
1441
1531
|
"Working tree is dirty; commit or stash changes before running"
|
|
1442
1532
|
);
|
|
1443
1533
|
}
|
|
1444
|
-
const
|
|
1534
|
+
const features = new Set(
|
|
1535
|
+
opts.tickets.map((t) => t.feature).filter(Boolean)
|
|
1536
|
+
);
|
|
1537
|
+
const multiFeature = features.size > 1 && opts.progressPaths !== void 0;
|
|
1538
|
+
const fallbackProgressPath = join(
|
|
1445
1539
|
dirname(opts.logsDir),
|
|
1446
1540
|
"progress.md"
|
|
1447
1541
|
);
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1542
|
+
let tracker;
|
|
1543
|
+
if (multiFeature) {
|
|
1544
|
+
const multiData = generateMultiFeatureProgress(
|
|
1545
|
+
opts.tickets
|
|
1546
|
+
);
|
|
1547
|
+
const dataMap = /* @__PURE__ */ new Map();
|
|
1548
|
+
for (const [feat, fresh] of multiData) {
|
|
1549
|
+
const path = opts.progressPaths.get(feat) ?? fallbackProgressPath;
|
|
1550
|
+
const merged = mergeProgress(
|
|
1551
|
+
fresh,
|
|
1552
|
+
readProgress(path)
|
|
1553
|
+
);
|
|
1554
|
+
dataMap.set(feat, merged);
|
|
1555
|
+
await saveProgress(
|
|
1556
|
+
repoRoot,
|
|
1557
|
+
path,
|
|
1558
|
+
merged,
|
|
1559
|
+
`nightralph: start ${feat}`,
|
|
1560
|
+
d
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
tracker = createProgressTracker({
|
|
1564
|
+
progressPaths: opts.progressPaths,
|
|
1565
|
+
dataMap,
|
|
1566
|
+
fallbackPath: fallbackProgressPath,
|
|
1567
|
+
fallbackData: generateProgress(
|
|
1568
|
+
opts.featureName,
|
|
1569
|
+
[]
|
|
1570
|
+
)
|
|
1571
|
+
});
|
|
1572
|
+
} else {
|
|
1573
|
+
const progressPath = fallbackProgressPath;
|
|
1574
|
+
const progress = mergeProgress(
|
|
1575
|
+
generateProgress(
|
|
1576
|
+
opts.featureName,
|
|
1577
|
+
opts.tickets
|
|
1578
|
+
),
|
|
1579
|
+
readProgress(progressPath)
|
|
1580
|
+
);
|
|
1581
|
+
await saveProgress(
|
|
1582
|
+
repoRoot,
|
|
1583
|
+
progressPath,
|
|
1584
|
+
progress,
|
|
1585
|
+
`nightralph: start ${opts.featureName}`,
|
|
1586
|
+
d
|
|
1587
|
+
);
|
|
1588
|
+
tracker = createProgressTracker({
|
|
1589
|
+
progressPaths: /* @__PURE__ */ new Map(),
|
|
1590
|
+
dataMap: /* @__PURE__ */ new Map(),
|
|
1591
|
+
fallbackPath: progressPath,
|
|
1592
|
+
fallbackData: progress
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1459
1595
|
let totalCompleted = 0;
|
|
1460
1596
|
let waveNum = 0;
|
|
1461
|
-
const
|
|
1597
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
1462
1598
|
while (true) {
|
|
1463
1599
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
1464
|
-
(t) => !
|
|
1600
|
+
(t) => !failedKeys.has(
|
|
1601
|
+
ticketKey(t.feature, t.num)
|
|
1602
|
+
)
|
|
1465
1603
|
);
|
|
1466
1604
|
if (ready.length === 0) break;
|
|
1467
1605
|
waveNum++;
|
|
1468
|
-
const
|
|
1606
|
+
const readyKeys = new Set(
|
|
1607
|
+
ready.map((t) => ticketKey(t.feature, t.num))
|
|
1608
|
+
);
|
|
1469
1609
|
const remaining2 = opts.tickets.filter(
|
|
1470
|
-
(t) => t.status === "ready-for-agent" && !
|
|
1610
|
+
(t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
|
|
1471
1611
|
);
|
|
1472
1612
|
d.startWave(
|
|
1473
1613
|
waveNum,
|
|
1474
1614
|
totalWaves,
|
|
1475
1615
|
ready.map((t) => ({
|
|
1476
1616
|
num: t.num,
|
|
1477
|
-
filename: t.filename
|
|
1617
|
+
filename: t.filename,
|
|
1618
|
+
feature: t.feature
|
|
1478
1619
|
})),
|
|
1479
1620
|
runProgress(opts.tickets),
|
|
1480
1621
|
remaining2.map((t) => ({
|
|
1481
1622
|
num: t.num,
|
|
1482
|
-
filename: t.filename
|
|
1623
|
+
filename: t.filename,
|
|
1624
|
+
feature: t.feature
|
|
1483
1625
|
}))
|
|
1484
1626
|
);
|
|
1485
1627
|
const settled = await runPool(
|
|
@@ -1491,6 +1633,7 @@ async function runWavesIsolated(opts) {
|
|
|
1491
1633
|
repoName,
|
|
1492
1634
|
baseBranch,
|
|
1493
1635
|
specBody: opts.specBody,
|
|
1636
|
+
specBodies: opts.specBodies,
|
|
1494
1637
|
agentCmd: opts.agentCmd,
|
|
1495
1638
|
model: opts.model,
|
|
1496
1639
|
timeout: opts.timeout,
|
|
@@ -1508,86 +1651,104 @@ async function runWavesIsolated(opts) {
|
|
|
1508
1651
|
);
|
|
1509
1652
|
let waveCompleted = 0;
|
|
1510
1653
|
const successfulBranches = [];
|
|
1511
|
-
const
|
|
1512
|
-
const
|
|
1654
|
+
const ticketsByKey = /* @__PURE__ */ new Map();
|
|
1655
|
+
const worktreePathsByKey = /* @__PURE__ */ new Map();
|
|
1656
|
+
const branchToKey = /* @__PURE__ */ new Map();
|
|
1513
1657
|
for (let i = 0; i < outcomes.length; i++) {
|
|
1514
1658
|
const outcome = outcomes[i];
|
|
1515
1659
|
const ticket = ready[i];
|
|
1660
|
+
const key = ticketKey(ticket.feature, ticket.num);
|
|
1516
1661
|
if (outcome.kind === "worktree-error") {
|
|
1517
|
-
|
|
1518
|
-
d.setTicketStatus(
|
|
1662
|
+
failedKeys.add(key);
|
|
1663
|
+
d.setTicketStatus(
|
|
1664
|
+
ticket.num,
|
|
1665
|
+
"failed",
|
|
1666
|
+
ticket.feature
|
|
1667
|
+
);
|
|
1519
1668
|
d.log(
|
|
1520
1669
|
` ${ticket.filename}: failed to create worktree: ${formatErrorMessage(outcome.error)}`
|
|
1521
1670
|
);
|
|
1522
|
-
checkTicket(
|
|
1671
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1523
1672
|
branch: "",
|
|
1524
1673
|
exitCode: -1,
|
|
1525
1674
|
done: false
|
|
1526
1675
|
});
|
|
1527
1676
|
await saveProgress(
|
|
1528
1677
|
repoRoot,
|
|
1529
|
-
|
|
1530
|
-
|
|
1678
|
+
tracker.pathFor(ticket.feature),
|
|
1679
|
+
tracker.dataFor(ticket.feature),
|
|
1531
1680
|
`nightralph: ${ticket.filename} worktree error`,
|
|
1532
1681
|
d
|
|
1533
1682
|
);
|
|
1534
1683
|
continue;
|
|
1535
1684
|
}
|
|
1536
1685
|
if (outcome.kind === "error") {
|
|
1537
|
-
|
|
1538
|
-
d.setTicketStatus(
|
|
1686
|
+
failedKeys.add(key);
|
|
1687
|
+
d.setTicketStatus(
|
|
1688
|
+
ticket.num,
|
|
1689
|
+
"failed",
|
|
1690
|
+
ticket.feature
|
|
1691
|
+
);
|
|
1539
1692
|
d.log(
|
|
1540
1693
|
` ${ticket.filename}: attempt error: ` + formatErrorMessage(outcome.error)
|
|
1541
1694
|
);
|
|
1542
|
-
checkTicket(
|
|
1695
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1543
1696
|
branch: outcome.branchName ?? "",
|
|
1544
1697
|
exitCode: -1,
|
|
1545
1698
|
done: false
|
|
1546
1699
|
});
|
|
1547
1700
|
await saveProgress(
|
|
1548
1701
|
repoRoot,
|
|
1549
|
-
|
|
1550
|
-
|
|
1702
|
+
tracker.pathFor(ticket.feature),
|
|
1703
|
+
tracker.dataFor(ticket.feature),
|
|
1551
1704
|
`nightralph: ${ticket.filename} error`,
|
|
1552
1705
|
d
|
|
1553
1706
|
);
|
|
1554
1707
|
continue;
|
|
1555
1708
|
}
|
|
1556
1709
|
if (outcome.kind === "no-commits") {
|
|
1557
|
-
|
|
1558
|
-
d.setTicketStatus(
|
|
1710
|
+
failedKeys.add(key);
|
|
1711
|
+
d.setTicketStatus(
|
|
1712
|
+
ticket.num,
|
|
1713
|
+
"failed",
|
|
1714
|
+
ticket.feature
|
|
1715
|
+
);
|
|
1559
1716
|
d.log(
|
|
1560
1717
|
` ${ticket.filename}: agent exited 0 but made no changes; leaving ticket ready-for-agent`
|
|
1561
1718
|
);
|
|
1562
|
-
checkTicket(
|
|
1719
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1563
1720
|
branch: outcome.branchName,
|
|
1564
1721
|
exitCode: 0,
|
|
1565
1722
|
done: false
|
|
1566
1723
|
});
|
|
1567
1724
|
await saveProgress(
|
|
1568
1725
|
repoRoot,
|
|
1569
|
-
|
|
1570
|
-
|
|
1726
|
+
tracker.pathFor(ticket.feature),
|
|
1727
|
+
tracker.dataFor(ticket.feature),
|
|
1571
1728
|
`nightralph: ${ticket.filename} no commits`,
|
|
1572
1729
|
d
|
|
1573
1730
|
);
|
|
1574
1731
|
continue;
|
|
1575
1732
|
}
|
|
1576
1733
|
if (outcome.kind === "failed") {
|
|
1577
|
-
|
|
1578
|
-
d.setTicketStatus(
|
|
1734
|
+
failedKeys.add(key);
|
|
1735
|
+
d.setTicketStatus(
|
|
1736
|
+
ticket.num,
|
|
1737
|
+
"failed",
|
|
1738
|
+
ticket.feature
|
|
1739
|
+
);
|
|
1579
1740
|
if (outcome.reason === "exit") {
|
|
1580
1741
|
checkItems(ticket, outcome.completedItems);
|
|
1581
1742
|
}
|
|
1582
|
-
checkTicket(
|
|
1743
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1583
1744
|
branch: outcome.branchName,
|
|
1584
1745
|
exitCode: outcome.exitCode,
|
|
1585
1746
|
done: false
|
|
1586
1747
|
});
|
|
1587
1748
|
await saveProgress(
|
|
1588
1749
|
repoRoot,
|
|
1589
|
-
|
|
1590
|
-
|
|
1750
|
+
tracker.pathFor(ticket.feature),
|
|
1751
|
+
tracker.dataFor(ticket.feature),
|
|
1591
1752
|
`nightralph: ${ticket.filename} ` + (outcome.reason === "tests" ? "tests failed" : "failed"),
|
|
1592
1753
|
d
|
|
1593
1754
|
);
|
|
@@ -1597,11 +1758,12 @@ async function runWavesIsolated(opts) {
|
|
|
1597
1758
|
d.log(
|
|
1598
1759
|
` ${ticket.filename}: agent finished, awaiting merge`
|
|
1599
1760
|
);
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1761
|
+
ticketsByKey.set(key, ticket);
|
|
1762
|
+
worktreePathsByKey.set(
|
|
1763
|
+
key,
|
|
1603
1764
|
outcome.worktreePath
|
|
1604
1765
|
);
|
|
1766
|
+
branchToKey.set(outcome.branchName, key);
|
|
1605
1767
|
successfulBranches.push({
|
|
1606
1768
|
branch: outcome.branchName,
|
|
1607
1769
|
ticketNum: ticket.num,
|
|
@@ -1611,9 +1773,10 @@ async function runWavesIsolated(opts) {
|
|
|
1611
1773
|
if (successfulBranches.length > 0) {
|
|
1612
1774
|
await commitTicketFiles(
|
|
1613
1775
|
repoRoot,
|
|
1614
|
-
successfulBranches.map(
|
|
1615
|
-
|
|
1616
|
-
|
|
1776
|
+
successfulBranches.map((sb) => {
|
|
1777
|
+
const k = branchToKey.get(sb.branch);
|
|
1778
|
+
return ticketsByKey.get(k);
|
|
1779
|
+
}),
|
|
1617
1780
|
d
|
|
1618
1781
|
);
|
|
1619
1782
|
const mergeResult = await mergeWave({
|
|
@@ -1623,32 +1786,35 @@ async function runWavesIsolated(opts) {
|
|
|
1623
1786
|
strategy: opts.conflictStrategy
|
|
1624
1787
|
});
|
|
1625
1788
|
for (const branchResult of mergeResult.merged) {
|
|
1626
|
-
const
|
|
1627
|
-
branchResult.
|
|
1789
|
+
const bk = branchToKey.get(
|
|
1790
|
+
branchResult.branch
|
|
1628
1791
|
);
|
|
1792
|
+
const ticket = bk ? ticketsByKey.get(bk) : void 0;
|
|
1629
1793
|
if (!ticket) continue;
|
|
1630
1794
|
if (branchResult.merged) {
|
|
1631
1795
|
markDone(ticket);
|
|
1632
1796
|
waveCompleted++;
|
|
1633
1797
|
totalCompleted++;
|
|
1634
|
-
d.setTicketStatus(
|
|
1798
|
+
d.setTicketStatus(
|
|
1799
|
+
ticket.num,
|
|
1800
|
+
"done",
|
|
1801
|
+
ticket.feature
|
|
1802
|
+
);
|
|
1635
1803
|
d.log(
|
|
1636
1804
|
` ${ticket.filename}: done`
|
|
1637
1805
|
);
|
|
1638
|
-
checkTicket(
|
|
1806
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1639
1807
|
branch: branchResult.branch,
|
|
1640
1808
|
exitCode: 0
|
|
1641
1809
|
});
|
|
1642
1810
|
await saveProgress(
|
|
1643
1811
|
repoRoot,
|
|
1644
|
-
|
|
1645
|
-
|
|
1812
|
+
tracker.pathFor(ticket.feature),
|
|
1813
|
+
tracker.dataFor(ticket.feature),
|
|
1646
1814
|
`nightralph: ${ticket.filename} done`,
|
|
1647
1815
|
d
|
|
1648
1816
|
);
|
|
1649
|
-
const worktreePath =
|
|
1650
|
-
ticket.num
|
|
1651
|
-
);
|
|
1817
|
+
const worktreePath = bk ? worktreePathsByKey.get(bk) : void 0;
|
|
1652
1818
|
if (worktreePath) {
|
|
1653
1819
|
await removeWorktreeQuietly(
|
|
1654
1820
|
worktreePath,
|
|
@@ -1656,16 +1822,21 @@ async function runWavesIsolated(opts) {
|
|
|
1656
1822
|
);
|
|
1657
1823
|
}
|
|
1658
1824
|
} else {
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1825
|
+
const wtKey = branchToKey.get(
|
|
1826
|
+
branchResult.branch
|
|
1661
1827
|
);
|
|
1828
|
+
const worktreePath = wtKey ? worktreePathsByKey.get(wtKey) : void 0;
|
|
1662
1829
|
const recovered = opts.conflictStrategy === "respawn" && worktreePath ? await respawnAndRetryMerge({
|
|
1663
1830
|
repoRoot,
|
|
1664
1831
|
baseBranch,
|
|
1665
1832
|
branch: branchResult.branch,
|
|
1666
1833
|
ticket,
|
|
1667
1834
|
worktreePath,
|
|
1668
|
-
specBody:
|
|
1835
|
+
specBody: lookupSpec(
|
|
1836
|
+
opts.specBodies,
|
|
1837
|
+
opts.specBody,
|
|
1838
|
+
ticket.feature
|
|
1839
|
+
),
|
|
1669
1840
|
agentCmd: opts.agentCmd,
|
|
1670
1841
|
model: opts.model,
|
|
1671
1842
|
timeout: opts.timeout,
|
|
@@ -1678,18 +1849,22 @@ async function runWavesIsolated(opts) {
|
|
|
1678
1849
|
markDone(ticket);
|
|
1679
1850
|
waveCompleted++;
|
|
1680
1851
|
totalCompleted++;
|
|
1681
|
-
d.setTicketStatus(
|
|
1852
|
+
d.setTicketStatus(
|
|
1853
|
+
ticket.num,
|
|
1854
|
+
"done",
|
|
1855
|
+
ticket.feature
|
|
1856
|
+
);
|
|
1682
1857
|
d.log(
|
|
1683
1858
|
` ${ticket.filename}: done (resolved via respawn)`
|
|
1684
1859
|
);
|
|
1685
|
-
checkTicket(
|
|
1860
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1686
1861
|
branch: branchResult.branch,
|
|
1687
1862
|
exitCode: 0
|
|
1688
1863
|
});
|
|
1689
1864
|
await saveProgress(
|
|
1690
1865
|
repoRoot,
|
|
1691
|
-
|
|
1692
|
-
|
|
1866
|
+
tracker.pathFor(ticket.feature),
|
|
1867
|
+
tracker.dataFor(ticket.feature),
|
|
1693
1868
|
`nightralph: ${ticket.filename} done (respawn)`,
|
|
1694
1869
|
d
|
|
1695
1870
|
);
|
|
@@ -1706,7 +1881,9 @@ async function runWavesIsolated(opts) {
|
|
|
1706
1881
|
}
|
|
1707
1882
|
}
|
|
1708
1883
|
} else {
|
|
1709
|
-
|
|
1884
|
+
failedKeys.add(
|
|
1885
|
+
ticketKey(ticket.feature, ticket.num)
|
|
1886
|
+
);
|
|
1710
1887
|
d.log(
|
|
1711
1888
|
` ${ticket.filename}: merge failed, staying ready-for-agent`
|
|
1712
1889
|
);
|
|
@@ -1715,15 +1892,15 @@ async function runWavesIsolated(opts) {
|
|
|
1715
1892
|
" git: " + branchResult.stderr.trim()
|
|
1716
1893
|
);
|
|
1717
1894
|
}
|
|
1718
|
-
checkTicket(
|
|
1895
|
+
checkTicket(tracker.dataFor(ticket.feature), ticket.num, {
|
|
1719
1896
|
branch: branchResult.branch,
|
|
1720
1897
|
exitCode: 0,
|
|
1721
1898
|
done: false
|
|
1722
1899
|
});
|
|
1723
1900
|
await saveProgress(
|
|
1724
1901
|
repoRoot,
|
|
1725
|
-
|
|
1726
|
-
|
|
1902
|
+
tracker.pathFor(ticket.feature),
|
|
1903
|
+
tracker.dataFor(ticket.feature),
|
|
1727
1904
|
`nightralph: ${ticket.filename} merge failed`,
|
|
1728
1905
|
d
|
|
1729
1906
|
);
|
|
@@ -1736,7 +1913,8 @@ async function runWavesIsolated(opts) {
|
|
|
1736
1913
|
Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
1737
1914
|
);
|
|
1738
1915
|
for (const sb of successfulBranches) {
|
|
1739
|
-
const
|
|
1916
|
+
const sbk = branchToKey.get(sb.branch);
|
|
1917
|
+
const ticket = sbk ? ticketsByKey.get(sbk) : void 0;
|
|
1740
1918
|
if (ticket && ticket.status === "done") continue;
|
|
1741
1919
|
try {
|
|
1742
1920
|
await removeWorktree(
|
|
@@ -1755,7 +1933,9 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
|
|
|
1755
1933
|
if (waveCompleted === 0) {
|
|
1756
1934
|
const remaining3 = findReadyTickets(
|
|
1757
1935
|
opts.tickets
|
|
1758
|
-
).filter((t) => !
|
|
1936
|
+
).filter((t) => !failedKeys.has(
|
|
1937
|
+
ticketKey(t.feature, t.num)
|
|
1938
|
+
));
|
|
1759
1939
|
if (remaining3.length === 0) {
|
|
1760
1940
|
d.log(
|
|
1761
1941
|
"\nNo tickets completed this wave and no ready tickets remain. Stopping."
|
|
@@ -1774,7 +1954,7 @@ No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.le
|
|
|
1774
1954
|
d.log("\n" + renderFinishSummary(
|
|
1775
1955
|
opts.tickets,
|
|
1776
1956
|
totalCompleted,
|
|
1777
|
-
|
|
1957
|
+
failedKeys
|
|
1778
1958
|
));
|
|
1779
1959
|
d.cleanup();
|
|
1780
1960
|
return { allDone: remaining.length === 0 };
|
|
@@ -1801,6 +1981,7 @@ export {
|
|
|
1801
1981
|
getProviderArgs,
|
|
1802
1982
|
killProcessGroup,
|
|
1803
1983
|
killRunningAgents,
|
|
1984
|
+
lookupSpec,
|
|
1804
1985
|
markDone,
|
|
1805
1986
|
parseBlockers,
|
|
1806
1987
|
parseStatus,
|
|
@@ -1814,6 +1995,7 @@ export {
|
|
|
1814
1995
|
runWavesIsolated,
|
|
1815
1996
|
scanTickets,
|
|
1816
1997
|
spawnAgent,
|
|
1817
|
-
stripSpecSection
|
|
1998
|
+
stripSpecSection,
|
|
1999
|
+
ticketKey
|
|
1818
2000
|
};
|
|
1819
2001
|
//# sourceMappingURL=orchestrator.js.map
|