nightralph 0.0.50 → 0.0.53

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/index.js CHANGED
@@ -837,6 +837,14 @@ var DEFAULT_COLUMNS = 80;
837
837
  var MIN_LOG_ROWS = 1;
838
838
  var MAX_MESSAGE_BUFFER = 200;
839
839
  var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
840
+ function rowText(ticket) {
841
+ const label = ticketLabel(ticket);
842
+ if (ticket.status !== "blocked" || !ticket.blockedBy?.length) {
843
+ return label;
844
+ }
845
+ return `${label} (blocked by ${ticket.blockedBy.join(", ")})`;
846
+ }
847
+ __name(rowText, "rowText");
840
848
  function fitLine(line, width) {
841
849
  const visible = line.replace(ANSI_RE, "");
842
850
  if (visible.length <= width) return line;
@@ -860,13 +868,16 @@ function renderDot(status) {
860
868
  case "in-progress":
861
869
  return color.yellow(FILLED_DOT);
862
870
  // The agent and its test gate are finished; the branch
863
- // waits for the rest of the wave before it is merged.
871
+ // waits for the rest of the wave before it is merged. It
872
+ // stays cyan through the merge and any conflict respawn.
864
873
  case "awaiting-merge":
865
874
  return color.cyan(FILLED_DOT);
866
875
  case "done":
867
876
  return color.green(FILLED_DOT);
868
877
  case "failed":
869
878
  return color.red(FILLED_DOT);
879
+ case "blocked":
880
+ return color.dim(FILLED_DOT);
870
881
  }
871
882
  }
872
883
  __name(renderDot, "renderDot");
@@ -875,8 +886,8 @@ var StatusDisplay = class {
875
886
  __name(this, "StatusDisplay");
876
887
  }
877
888
  wave = null;
878
- progress = null;
879
- remaining = [];
889
+ tickets = [];
890
+ hasRun = false;
880
891
  messages = [];
881
892
  renderedLines = 0;
882
893
  config;
@@ -893,20 +904,28 @@ var StatusDisplay = class {
893
904
  const available = termHeight - overhead;
894
905
  return Math.max(1, available);
895
906
  }
896
- startWave(waveNum, totalWaves, tickets, progress, remaining) {
897
- this.wave = {
898
- waveNum,
899
- totalWaves,
900
- tickets: tickets.map((t) => ({
901
- ...t,
902
- status: "pending"
903
- }))
904
- };
905
- this.remaining = (remaining ?? []).map((t) => ({
907
+ findTicket(num, feature) {
908
+ const found = feature ? this.tickets.find(
909
+ (t) => t.num === num && t.feature === feature
910
+ ) : this.tickets.find((t) => t.num === num);
911
+ return found ?? null;
912
+ }
913
+ startRun(tickets) {
914
+ this.tickets = tickets.map((t) => ({
906
915
  ...t,
907
916
  status: "pending"
908
917
  }));
909
- this.progress = progress ?? null;
918
+ this.hasRun = true;
919
+ this.viewportStart = 0;
920
+ }
921
+ startWave(waveNum, totalWaves, tickets, _progress) {
922
+ if (!this.hasRun) {
923
+ this.startRun(tickets);
924
+ }
925
+ this.wave = {
926
+ waveNum,
927
+ totalWaves
928
+ };
910
929
  this.messages = [];
911
930
  this.viewportStart = 0;
912
931
  this.config.stream.write(ansi.cursorHide());
@@ -926,28 +945,31 @@ var StatusDisplay = class {
926
945
  this.render();
927
946
  }
928
947
  setTicketStatus(num, status, feature) {
929
- if (!this.wave) return;
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);
933
- if (ticket) {
934
- ticket.status = status;
935
- if (status === "in-progress") {
936
- const ticketIndex = this.wave.tickets.indexOf(ticket);
937
- const availableRows = this.getAvailableRows();
938
- const totalTickets = this.wave.tickets.length;
939
- if (ticketIndex < this.viewportStart) {
940
- this.viewportStart = ticketIndex;
941
- } else if (ticketIndex >= this.viewportStart + availableRows) {
942
- this.viewportStart = ticketIndex - availableRows + 3;
943
- }
944
- this.viewportStart = Math.max(
945
- 0,
946
- Math.min(this.viewportStart, totalTickets - 1)
947
- );
948
+ const ticket = this.findTicket(num, feature);
949
+ if (!ticket) return;
950
+ ticket.status = status;
951
+ if (status === "in-progress") {
952
+ const ticketIndex = this.tickets.indexOf(ticket);
953
+ const availableRows = this.getAvailableRows();
954
+ const totalTickets = this.tickets.length;
955
+ if (ticketIndex < this.viewportStart) {
956
+ this.viewportStart = ticketIndex;
957
+ } else if (ticketIndex >= this.viewportStart + availableRows) {
958
+ this.viewportStart = ticketIndex - availableRows + 3;
948
959
  }
949
- this.render();
960
+ this.viewportStart = Math.max(
961
+ 0,
962
+ Math.min(this.viewportStart, totalTickets - 1)
963
+ );
950
964
  }
965
+ this.render();
966
+ }
967
+ setBlocked(num, feature, blockedBy) {
968
+ const ticket = this.findTicket(num, feature);
969
+ if (!ticket) return;
970
+ ticket.status = "blocked";
971
+ ticket.blockedBy = [...blockedBy];
972
+ this.render();
951
973
  }
952
974
  log(message) {
953
975
  if (!this.wave) {
@@ -974,59 +996,52 @@ var StatusDisplay = class {
974
996
  if (this.renderedLines > 0) {
975
997
  output += ansi.eraseLines(this.renderedLines + 1);
976
998
  }
977
- const doneInWave = this.wave.tickets.filter(
999
+ const done = this.tickets.filter(
978
1000
  (t) => t.status === "done"
979
1001
  ).length;
980
- const completed = doneInWave + (this.progress?.completed ?? 0);
981
- const total = this.progress?.total ?? this.wave.tickets.length;
982
- const pct = total === 0 ? 0 : Math.round(completed / total * 100);
1002
+ const total = this.tickets.length;
1003
+ const pct = total === 0 ? 0 : Math.round(done / total * 100);
983
1004
  const width = Math.max(
984
1005
  1,
985
1006
  (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1
986
1007
  );
987
- output += `Wave ${this.wave.waveNum}/${this.wave.totalWaves} - ${pct}% complete
988
- `;
1008
+ const header = `Wave ${this.wave.waveNum}/${this.wave.totalWaves} - ${done}/${total} done - ${pct}%`;
1009
+ output += fitLine(header, width) + "\n";
989
1010
  const availableRows = this.getAvailableRows();
990
- const tickets = [
991
- ...this.wave.tickets,
992
- ...this.remaining
993
- ];
994
1011
  let renderedTicketCount = 0;
995
- if (tickets.length <= availableRows) {
996
- for (const ticket of tickets) {
1012
+ if (this.tickets.length <= availableRows) {
1013
+ for (const ticket of this.tickets) {
997
1014
  const dot = renderDot(ticket.status);
998
- const name = fitLine(
999
- ticketLabel(ticket),
1000
- width - 2
1001
- );
1015
+ const text = rowText(ticket);
1016
+ const name = fitLine(text, width - 2);
1002
1017
  output += `${dot} ${name}
1003
1018
  `;
1004
1019
  }
1005
- renderedTicketCount = tickets.length;
1020
+ renderedTicketCount = this.tickets.length;
1006
1021
  } else {
1007
1022
  this.viewportStart = Math.max(
1008
1023
  0,
1009
1024
  Math.min(
1010
1025
  this.viewportStart,
1011
- tickets.length - 1
1026
+ this.tickets.length - 1
1012
1027
  )
1013
1028
  );
1014
1029
  let hasMoreAbove = this.viewportStart > 0;
1015
1030
  let tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
1016
- let hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
1031
+ let hasMoreBelow = this.viewportStart + tentativeCount < this.tickets.length;
1017
1032
  let ticketsToShow = Math.max(
1018
1033
  0,
1019
1034
  tentativeCount - (hasMoreBelow ? 1 : 0)
1020
1035
  );
1021
1036
  const maxStart = Math.max(
1022
1037
  0,
1023
- tickets.length - Math.max(1, ticketsToShow)
1038
+ this.tickets.length - Math.max(1, ticketsToShow)
1024
1039
  );
1025
1040
  if (this.viewportStart > maxStart) {
1026
1041
  this.viewportStart = maxStart;
1027
1042
  hasMoreAbove = this.viewportStart > 0;
1028
1043
  tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
1029
- hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
1044
+ hasMoreBelow = this.viewportStart + tentativeCount < this.tickets.length;
1030
1045
  ticketsToShow = Math.max(
1031
1046
  0,
1032
1047
  tentativeCount - (hasMoreBelow ? 1 : 0)
@@ -1037,21 +1052,19 @@ var StatusDisplay = class {
1037
1052
  output += ` ... ${moreCount} more above
1038
1053
  `;
1039
1054
  }
1040
- const visibleTickets = tickets.slice(
1055
+ const visibleTickets = this.tickets.slice(
1041
1056
  this.viewportStart,
1042
1057
  this.viewportStart + ticketsToShow
1043
1058
  );
1044
1059
  for (const ticket of visibleTickets) {
1045
1060
  const dot = renderDot(ticket.status);
1046
- const name = fitLine(
1047
- ticketLabel(ticket),
1048
- width - 2
1049
- );
1061
+ const text = rowText(ticket);
1062
+ const name = fitLine(text, width - 2);
1050
1063
  output += `${dot} ${name}
1051
1064
  `;
1052
1065
  }
1053
1066
  if (hasMoreBelow) {
1054
- const moreCount = tickets.length - (this.viewportStart + ticketsToShow);
1067
+ const moreCount = this.tickets.length - (this.viewportStart + ticketsToShow);
1055
1068
  output += ` ... ${moreCount} more below
1056
1069
  `;
1057
1070
  }
@@ -1083,7 +1096,9 @@ var PlainDisplay = class {
1083
1096
  static {
1084
1097
  __name(this, "PlainDisplay");
1085
1098
  }
1086
- startWave(waveNum, _totalWaves, tickets, progress, _remaining) {
1099
+ startRun(_tickets) {
1100
+ }
1101
+ startWave(waveNum, _totalWaves, tickets, progress) {
1087
1102
  const pct = progress && progress.total > 0 ? ` (${progress.completed}/${progress.total} done)` : "";
1088
1103
  console.log(
1089
1104
  `
@@ -1092,6 +1107,8 @@ Wave ${waveNum}${pct}: ` + tickets.map((t) => ticketLabel(t)).join(", ")
1092
1107
  }
1093
1108
  setTicketStatus(_num, _status, _feature) {
1094
1109
  }
1110
+ setBlocked(_num, _feature, _blockedBy) {
1111
+ }
1095
1112
  log(message) {
1096
1113
  console.log(message);
1097
1114
  }
@@ -1301,6 +1318,37 @@ function findReadyTickets(tickets) {
1301
1318
  });
1302
1319
  }
1303
1320
  __name(findReadyTickets, "findReadyTickets");
1321
+ function findBlockedTickets(tickets, failedKeys) {
1322
+ const byKey = new Map(
1323
+ tickets.map((t) => [ticketKey(t.feature, t.num), t])
1324
+ );
1325
+ function collectDead(ticket, visited, found) {
1326
+ for (const b of ticket.blockers) {
1327
+ const key = ticketKey(ticket.feature, b);
1328
+ if (visited.has(key)) continue;
1329
+ visited.add(key);
1330
+ const blocker = byKey.get(key);
1331
+ if (!blocker || blocker.status === "done") continue;
1332
+ if (failedKeys.has(key) || blocker.status !== "ready-for-agent") {
1333
+ found.add(key);
1334
+ continue;
1335
+ }
1336
+ collectDead(blocker, visited, found);
1337
+ }
1338
+ }
1339
+ __name(collectDead, "collectDead");
1340
+ const blocked = /* @__PURE__ */ new Map();
1341
+ for (const t of tickets) {
1342
+ const key = ticketKey(t.feature, t.num);
1343
+ if (t.status !== "ready-for-agent") continue;
1344
+ if (failedKeys.has(key)) continue;
1345
+ const found = /* @__PURE__ */ new Set();
1346
+ collectDead(t, /* @__PURE__ */ new Set([key]), found);
1347
+ if (found.size > 0) blocked.set(key, [...found]);
1348
+ }
1349
+ return blocked;
1350
+ }
1351
+ __name(findBlockedTickets, "findBlockedTickets");
1304
1352
  var COMPLETED_RE = /^\s*[-*+]?\s*\[x\]\s+(.+)$/i;
1305
1353
  var TOOL_USE_RE = /^\[[\w.]+\]/;
1306
1354
  var CHECKBOX_RE = /^(\s*[-*+] )\[( |x|X)\]( .*)$/;
@@ -1837,7 +1885,7 @@ function spawnAgent(opts) {
1837
1885
  detached: true
1838
1886
  });
1839
1887
  if (proc.pid !== void 0) runningAgents.add(proc.pid);
1840
- proc.stdout.pipe(logStream);
1888
+ proc.stdout.pipe(logStream, { end: false });
1841
1889
  const piFormatter = isPi ? createPiStreamFormatter() : null;
1842
1890
  const completedItems = [];
1843
1891
  const seenItems = /* @__PURE__ */ new Set();
@@ -1886,7 +1934,7 @@ function spawnAgent(opts) {
1886
1934
  }
1887
1935
  }
1888
1936
  });
1889
- proc.stderr.pipe(logStream);
1937
+ proc.stderr.pipe(logStream, { end: false });
1890
1938
  const rlErr = createInterface({ input: proc.stderr });
1891
1939
  rlErr.on("line", (line) => {
1892
1940
  if (isClaude && !TOOL_USE_RE.test(line)) return;
@@ -2480,6 +2528,29 @@ function renderFinishSummary(tickets, totalCompleted, failedKeys) {
2480
2528
  return lines.join("\n");
2481
2529
  }
2482
2530
  __name(renderFinishSummary, "renderFinishSummary");
2531
+ function sessionTickets(tickets) {
2532
+ return [...tickets].filter((t) => t.status === "ready-for-agent").sort((a, b) => a.feature.localeCompare(b.feature) || a.num - b.num);
2533
+ }
2534
+ __name(sessionTickets, "sessionTickets");
2535
+ function toRef(t) {
2536
+ return { num: t.num, filename: t.filename, feature: t.feature };
2537
+ }
2538
+ __name(toRef, "toRef");
2539
+ function reportBlocked(d, tickets, failedKeys) {
2540
+ const byKey = new Map(
2541
+ tickets.map((t) => [ticketKey(t.feature, t.num), t])
2542
+ );
2543
+ for (const [key, dead] of findBlockedTickets(
2544
+ tickets,
2545
+ failedKeys
2546
+ )) {
2547
+ const ticket = byKey.get(key);
2548
+ if (!ticket) continue;
2549
+ const labels = dead.map((k) => byKey.get(k)).filter((t) => t !== void 0).map((t) => ticketLabel(toRef(t)));
2550
+ d.setBlocked(ticket.num, ticket.feature, labels);
2551
+ }
2552
+ }
2553
+ __name(reportBlocked, "reportBlocked");
2483
2554
  function runProgress(tickets) {
2484
2555
  return {
2485
2556
  completed: tickets.filter((t) => t.status === "done").length,
@@ -2500,7 +2571,9 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
2500
2571
  let totalCompleted = 0;
2501
2572
  let waveNum = 0;
2502
2573
  const failedKeys = /* @__PURE__ */ new Set();
2574
+ const session = sessionTickets(tickets);
2503
2575
  while (true) {
2576
+ if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
2504
2577
  const ready = findReadyTickets(tickets).filter(
2505
2578
  (t) => !failedKeys.has(
2506
2579
  ticketKey(t.feature, t.num)
@@ -2508,12 +2581,10 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
2508
2581
  );
2509
2582
  if (ready.length === 0) break;
2510
2583
  waveNum++;
2511
- const readyKeys = new Set(
2512
- ready.map((t) => ticketKey(t.feature, t.num))
2513
- );
2514
- const remaining2 = tickets.filter(
2515
- (t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
2516
- );
2584
+ if (waveNum === 1) {
2585
+ d.startRun(session.map(toRef));
2586
+ reportBlocked(d, tickets, failedKeys);
2587
+ }
2517
2588
  d.startWave(
2518
2589
  waveNum,
2519
2590
  totalWaves,
@@ -2522,12 +2593,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
2522
2593
  filename: t.filename,
2523
2594
  feature: t.feature
2524
2595
  })),
2525
- runProgress(tickets),
2526
- remaining2.map((t) => ({
2527
- num: t.num,
2528
- filename: t.filename,
2529
- feature: t.feature
2530
- }))
2596
+ runProgress(session)
2531
2597
  );
2532
2598
  const results = await runPool(
2533
2599
  ready,
@@ -2624,10 +2690,10 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
2624
2690
  );
2625
2691
  }
2626
2692
  if (waveCompleted === 0) {
2627
- const remaining3 = findReadyTickets(tickets).filter((t) => !failedKeys.has(
2693
+ const remaining2 = findReadyTickets(tickets).filter((t) => !failedKeys.has(
2628
2694
  ticketKey(t.feature, t.num)
2629
2695
  ));
2630
- if (remaining3.length === 0) {
2696
+ if (remaining2.length === 0) {
2631
2697
  d.log(
2632
2698
  "\nNo tickets completed this wave and no ready tickets remain. Stopping."
2633
2699
  );
@@ -2635,10 +2701,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
2635
2701
  }
2636
2702
  d.log(
2637
2703
  `
2638
- No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.length === 1 ? "" : "s"} remain; continuing.`
2704
+ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.length === 1 ? "" : "s"} remain; continuing.`
2639
2705
  );
2640
2706
  }
2641
2707
  }
2708
+ if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
2642
2709
  const remaining = tickets.filter(
2643
2710
  (t) => t.status === "ready-for-agent"
2644
2711
  );
@@ -2731,7 +2798,9 @@ async function runWavesIsolated(opts) {
2731
2798
  let totalCompleted = 0;
2732
2799
  let waveNum = 0;
2733
2800
  const failedKeys = /* @__PURE__ */ new Set();
2801
+ const session = sessionTickets(opts.tickets);
2734
2802
  while (true) {
2803
+ if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
2735
2804
  const ready = findReadyTickets(opts.tickets).filter(
2736
2805
  (t) => !failedKeys.has(
2737
2806
  ticketKey(t.feature, t.num)
@@ -2739,12 +2808,10 @@ async function runWavesIsolated(opts) {
2739
2808
  );
2740
2809
  if (ready.length === 0) break;
2741
2810
  waveNum++;
2742
- const readyKeys = new Set(
2743
- ready.map((t) => ticketKey(t.feature, t.num))
2744
- );
2745
- const remaining2 = opts.tickets.filter(
2746
- (t) => t.status === "ready-for-agent" && !readyKeys.has(ticketKey(t.feature, t.num))
2747
- );
2811
+ if (waveNum === 1) {
2812
+ d.startRun(session.map(toRef));
2813
+ reportBlocked(d, opts.tickets, failedKeys);
2814
+ }
2748
2815
  d.startWave(
2749
2816
  waveNum,
2750
2817
  totalWaves,
@@ -2753,12 +2820,7 @@ async function runWavesIsolated(opts) {
2753
2820
  filename: t.filename,
2754
2821
  feature: t.feature
2755
2822
  })),
2756
- runProgress(opts.tickets),
2757
- remaining2.map((t) => ({
2758
- num: t.num,
2759
- filename: t.filename,
2760
- feature: t.feature
2761
- }))
2823
+ runProgress(session)
2762
2824
  );
2763
2825
  const settled = await runPool(
2764
2826
  ready,
@@ -3020,6 +3082,11 @@ async function runWavesIsolated(opts) {
3020
3082
  failedKeys.add(
3021
3083
  ticketKey(ticket.feature, ticket.num)
3022
3084
  );
3085
+ d.setTicketStatus(
3086
+ ticket.num,
3087
+ "failed",
3088
+ ticket.feature
3089
+ );
3023
3090
  d.log(
3024
3091
  ` ${ticket.filename}: merge failed, staying ready-for-agent`
3025
3092
  );
@@ -3067,12 +3134,12 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
3067
3134
  }
3068
3135
  }
3069
3136
  if (waveCompleted === 0) {
3070
- const remaining3 = findReadyTickets(
3137
+ const remaining2 = findReadyTickets(
3071
3138
  opts.tickets
3072
3139
  ).filter((t) => !failedKeys.has(
3073
3140
  ticketKey(t.feature, t.num)
3074
3141
  ));
3075
- if (remaining3.length === 0) {
3142
+ if (remaining2.length === 0) {
3076
3143
  d.log(
3077
3144
  "\nNo tickets completed this wave and no ready tickets remain. Stopping."
3078
3145
  );
@@ -3080,10 +3147,11 @@ Merge stopped at ${mergeResult.stoppedAt.branch}`
3080
3147
  }
3081
3148
  d.log(
3082
3149
  `
3083
- No tickets completed this wave. ${remaining3.length} ready ticket${remaining3.length === 1 ? "" : "s"} remain; continuing.`
3150
+ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.length === 1 ? "" : "s"} remain; continuing.`
3084
3151
  );
3085
3152
  }
3086
3153
  }
3154
+ if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
3087
3155
  const remaining = opts.tickets.filter(
3088
3156
  (t) => t.status === "ready-for-agent"
3089
3157
  );