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.
@@ -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 doneNums = new Set(
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((b) => doneNums.has(b));
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) => [t.num, t.status])
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 doneNums = new Set(
371
- [...sim.entries()].filter(([, s]) => s === "done").map(([n]) => n)
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
- if (sim.get(t.num) !== "ready-for-agent") {
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) => doneNums.has(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(t.num, "done");
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 doneNums = /* @__PURE__ */ new Set();
392
- const ready = /* @__PURE__ */ new Set();
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
- ready.add(t.num);
419
+ readyKeys.add(key);
396
420
  } else if (t.status === "done") {
397
- doneNums.add(t.num);
421
+ doneKeys.add(key);
398
422
  }
399
423
  }
400
424
  let changed = true;
401
425
  while (changed) {
402
426
  changed = false;
403
- for (const num of ready) {
427
+ for (const key of readyKeys) {
404
428
  const ticket = tickets.find(
405
- (t) => t.num === num
429
+ (t) => ticketKey(t.feature, t.num) === key
406
430
  );
407
431
  if (ticket.blockers.every(
408
- (b) => doneNums.has(b)
432
+ (b) => doneKeys.has(
433
+ ticketKey(ticket.feature, b)
434
+ )
409
435
  )) {
410
- doneNums.add(num);
411
- ready.delete(num);
436
+ doneKeys.add(key);
437
+ readyKeys.delete(key);
412
438
  changed = true;
413
439
  }
414
440
  }
415
441
  }
416
- return [...ready].map(
417
- (num) => tickets.find((t) => t.num === num)
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 worktreePath = `../${opts.repoName}.${num}-${t.slug}`;
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(ticket.num, "in-progress");
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
- opts.specBody,
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(ticket.num, "awaiting-merge");
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, failedNums) {
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) => !failedNums.has(t.num) && t.blockers.some((b) => humanNums.has(b))
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
- const counts = `Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedNums.size > 0 ? `, ${failedNums.size} failed` : "") + (human.length > 0 ? `, ${human.length} need a human` : "");
1258
- if (human.length === 0) return counts;
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
- const lines = [
1261
- counts,
1329
+ lines.push(
1262
1330
  "",
1263
- (remaining.length === 0 && failedNums.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:",
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 failedNums = /* @__PURE__ */ new Set();
1366
+ const failedKeys = /* @__PURE__ */ new Set();
1299
1367
  while (true) {
1300
1368
  const ready = findReadyTickets(tickets).filter(
1301
- (t) => !failedNums.has(t.num)
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 readyNums = new Set(ready.map((t) => t.num));
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" && !readyNums.has(t.num)
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(t.num, "in-progress");
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
- failedNums.add(ticket.num);
1387
- d.setTicketStatus(ticket.num, "failed");
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(ticket.num, "done");
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) => !failedNums.has(t.num));
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
- failedNums
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 progressPath = join(
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
- const progress = mergeProgress(
1449
- generateProgress(opts.featureName, opts.tickets),
1450
- readProgress(progressPath)
1451
- );
1452
- await saveProgress(
1453
- repoRoot,
1454
- progressPath,
1455
- progress,
1456
- `nightralph: start ${opts.featureName}`,
1457
- d
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 failedNums = /* @__PURE__ */ new Set();
1597
+ const failedKeys = /* @__PURE__ */ new Set();
1462
1598
  while (true) {
1463
1599
  const ready = findReadyTickets(opts.tickets).filter(
1464
- (t) => !failedNums.has(t.num)
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 readyNums = new Set(ready.map((t) => t.num));
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" && !readyNums.has(t.num)
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 ticketsByNum = /* @__PURE__ */ new Map();
1512
- const worktreePathsByNum = /* @__PURE__ */ new Map();
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
- failedNums.add(ticket.num);
1518
- d.setTicketStatus(ticket.num, "failed");
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(progress, ticket.num, {
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
- progressPath,
1530
- progress,
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
- failedNums.add(ticket.num);
1538
- d.setTicketStatus(ticket.num, "failed");
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(progress, ticket.num, {
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
- progressPath,
1550
- progress,
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
- failedNums.add(ticket.num);
1558
- d.setTicketStatus(ticket.num, "failed");
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(progress, ticket.num, {
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
- progressPath,
1570
- progress,
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
- failedNums.add(ticket.num);
1578
- d.setTicketStatus(ticket.num, "failed");
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(progress, ticket.num, {
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
- progressPath,
1590
- progress,
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
- ticketsByNum.set(ticket.num, ticket);
1601
- worktreePathsByNum.set(
1602
- ticket.num,
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
- (sb) => ticketsByNum.get(sb.ticketNum)
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 ticket = ticketsByNum.get(
1627
- branchResult.ticketNum
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(ticket.num, "done");
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(progress, ticket.num, {
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
- progressPath,
1645
- progress,
1812
+ tracker.pathFor(ticket.feature),
1813
+ tracker.dataFor(ticket.feature),
1646
1814
  `nightralph: ${ticket.filename} done`,
1647
1815
  d
1648
1816
  );
1649
- const worktreePath = worktreePathsByNum.get(
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 worktreePath = worktreePathsByNum.get(
1660
- ticket.num
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: opts.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(ticket.num, "done");
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(progress, ticket.num, {
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
- progressPath,
1692
- progress,
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
- failedNums.add(ticket.num);
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(progress, ticket.num, {
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
- progressPath,
1726
- progress,
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 ticket = ticketsByNum.get(sb.ticketNum);
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) => !failedNums.has(t.num));
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
- failedNums
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