nightralph 0.0.15 → 0.0.17

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 CHANGED
@@ -214,17 +214,29 @@ This is expected to run for a long time, like overnight.
214
214
 
215
215
  ### Mac OS
216
216
 
217
- #### Claude
217
+ Use `caffeinate` to prevent your machine from sleeping. These examples will
218
+ keep the machine awake, but the display can still go dark.
219
+
220
+ #### Claude Opus 4.6
218
221
 
219
222
  ```sh
220
223
  caffeinate -i npx nightralph claude -m claude-opus-4-6
221
224
  ```
222
225
 
223
- ##### Run with `pi` and an OpenRouter model
226
+ #### Claude Sonnet 5
227
+
228
+ ```sh
229
+ caffeinate -i npx nightralph claude -m claude-sonnet-5
230
+ ```
231
+
232
+ #### `pi` + OpenRouter model
224
233
 
225
234
  ```sh
226
235
  caffeinate -i npx nightralph pi -m openrouter/z-ai/glm-5.3-flash
227
236
  ```
228
237
 
229
- That command will prevent your machine from sleeping, but the display can still
230
- go dark.
238
+ #### `codex` + Luna
239
+
240
+ ```sh
241
+ caffeinate -i npx nightralph codex -m gpt-5.6-luna
242
+ ```
package/dist/display.js CHANGED
@@ -70,6 +70,7 @@ class StatusDisplay {
70
70
  }
71
71
  wave = null;
72
72
  progress = null;
73
+ remaining = [];
73
74
  messages = [];
74
75
  renderedLines = 0;
75
76
  config;
@@ -88,7 +89,7 @@ class StatusDisplay {
88
89
  const available = termHeight - overhead;
89
90
  return Math.max(1, available);
90
91
  }
91
- startWave(waveNum, totalWaves, tickets, progress) {
92
+ startWave(waveNum, totalWaves, tickets, progress, remaining) {
92
93
  this.wave = {
93
94
  waveNum,
94
95
  totalWaves,
@@ -97,6 +98,10 @@ class StatusDisplay {
97
98
  status: "pending"
98
99
  }))
99
100
  };
101
+ this.remaining = (remaining ?? []).map((t) => ({
102
+ ...t,
103
+ status: "pending"
104
+ }));
100
105
  this.progress = progress ?? null;
101
106
  this.messages = [];
102
107
  this.viewportStart = 0;
@@ -181,7 +186,10 @@ class StatusDisplay {
181
186
  output += `Wave ${this.wave.waveNum}/${this.wave.totalWaves} - ${pct}% complete
182
187
  `;
183
188
  const availableRows = this.getAvailableRows();
184
- const tickets = this.wave.tickets;
189
+ const tickets = [
190
+ ...this.wave.tickets,
191
+ ...this.remaining
192
+ ];
185
193
  let renderedTicketCount = 0;
186
194
  if (tickets.length <= availableRows) {
187
195
  for (const ticket of tickets) {
@@ -262,7 +270,7 @@ class PlainDisplay {
262
270
  static {
263
271
  __name(this, "PlainDisplay");
264
272
  }
265
- startWave(waveNum, _totalWaves, tickets, progress) {
273
+ startWave(waveNum, _totalWaves, tickets, progress, _remaining) {
266
274
  const pct = progress && progress.total > 0 ? ` (${progress.completed}/${progress.total} done)` : "";
267
275
  console.log(
268
276
  `
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/display.ts"],
4
- "sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n status:TicketStatus\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n readonly tickets:Array<DisplayTicket>\n}\n\nexport type DisplayConfig = {\n readonly messageLines:number\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts tickets done before the wave started;\n * tickets that finish during the wave are added on top.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n }>,\n progress?:RunProgress\n ):void\n setTicketStatus(num:number, status:TicketStatus):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\n/**\n * Clamp a line to `width` visible columns so it can never wrap.\n * A wrapped line occupies more terminal rows than `render`\n * counts, which leaves stale rows behind on the next erase.\n */\nexport function fitLine (line:string, width:number):string {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return line\n return visible.slice(0, width)\n}\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private progress:RunProgress|null = null\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n private signalHandler:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n messageLines:config?.messageLines ?? 10,\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + this.config.messageLines\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:Array<Readonly<{ num:number; filename:string }>>,\n progress?:RunProgress,\n ):void {\n this.wave = {\n waveNum,\n totalWaves,\n tickets:tickets.map(t => ({\n ...t,\n status:'pending' as const,\n })),\n }\n this.progress = progress ?? null\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n // Register signal handler to restore cursor on interruption\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.signalHandler = () => this.cleanup()\n process.on('SIGINT', this.signalHandler)\n process.on('SIGTERM', this.signalHandler)\n\n this.render()\n }\n\n setTicketStatus (num:number, status:TicketStatus):void {\n if (!this.wave) return\n\n const ticket = this.wave.tickets.find(t => t.num === num)\n if (ticket) {\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.wave.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.wave.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (ticketIndex >= this.viewportStart + availableRows) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart = ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(this.viewportStart, totalTickets - 1),\n )\n }\n\n this.render()\n }\n }\n\n log (message:string):void {\n this.messages.push(message)\n if (this.messages.length > this.config.messageLines) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n // Remove signal handlers\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output += ansi.eraseLines(this.renderedLines + 1)\n }\n\n const doneInWave = this.wave.tickets.filter(\n t => t.status === 'done',\n ).length\n const completed = doneInWave + (this.progress?.completed ?? 0)\n const total = this.progress?.total ?? this.wave.tickets.length\n const pct = total === 0 ? 0 : Math.round((completed / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n output += `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${pct}% complete\\n`\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n const tickets = this.wave.tickets\n let renderedTicketCount = 0\n\n if (tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of tickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(ticket.filename, width - 2)\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount = availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow = (\n this.viewportStart +\n tentativeCount < tickets.length\n )\n ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0)\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(ticket.filename, width - 2)\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n tickets.length -\n (this.viewportStart + ticketsToShow)\n )\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(Math.min(SEPARATOR_WIDTH, width)) + '\\n'\n\n const msgLines = Math.max(\n 0,\n this.config.messageLines - this.messages.length,\n )\n for (const msg of this.messages) {\n output += `${fitLine(msg, width)}\\n`\n }\n for (let i = 0; i < msgLines; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + this.config.messageLines\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<{ num:number; filename:string }>,\n progress?:RunProgress,\n ):void {\n const pct = progress && progress.total > 0 ?\n ` (${progress.completed}/${progress.total} done)` :\n ''\n console.log(\n `\\nWave ${waveNum}${pct}: ` +\n tickets.map(t => t.filename).join(', ')\n )\n }\n\n setTicketStatus (_num:number, _status:TicketStatus):void {\n // no-op: plain mode reports via log()\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (stream?:NodeJS.WriteStream):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n\n return new PlainDisplay()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n status:TicketStatus\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n readonly tickets:Array<DisplayTicket>\n}\n\nexport type DisplayConfig = {\n readonly messageLines:number\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts tickets done before the wave started;\n * tickets that finish during the wave are added on top.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n }>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n }>,\n ):void\n setTicketStatus(num:number, status:TicketStatus):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\n/**\n * Clamp a line to `width` visible columns so it can never wrap.\n * A wrapped line occupies more terminal rows than `render`\n * counts, which leaves stale rows behind on the next erase.\n */\nexport function fitLine (line:string, width:number):string {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return line\n return visible.slice(0, width)\n}\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private progress:RunProgress|null = null\n private remaining:Array<DisplayTicket> = []\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n private signalHandler:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n messageLines:config?.messageLines ?? 10,\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + this.config.messageLines\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:Array<Readonly<{ num:number; filename:string }>>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n }>,\n ):void {\n this.wave = {\n waveNum,\n totalWaves,\n tickets:tickets.map(t => ({\n ...t,\n status:'pending' as const,\n })),\n }\n this.remaining = (remaining ?? []).map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.progress = progress ?? null\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n // Register signal handler to restore cursor on interruption\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.signalHandler = () => this.cleanup()\n process.on('SIGINT', this.signalHandler)\n process.on('SIGTERM', this.signalHandler)\n\n this.render()\n }\n\n setTicketStatus (num:number, status:TicketStatus):void {\n if (!this.wave) return\n\n const ticket = this.wave.tickets.find(t => t.num === num)\n if (ticket) {\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.wave.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.wave.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (ticketIndex >= this.viewportStart + availableRows) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart = ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(this.viewportStart, totalTickets - 1),\n )\n }\n\n this.render()\n }\n }\n\n log (message:string):void {\n this.messages.push(message)\n if (this.messages.length > this.config.messageLines) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n // Remove signal handlers\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output += ansi.eraseLines(this.renderedLines + 1)\n }\n\n const doneInWave = this.wave.tickets.filter(\n t => t.status === 'done',\n ).length\n const completed = doneInWave + (this.progress?.completed ?? 0)\n const total = this.progress?.total ?? this.wave.tickets.length\n const pct = total === 0 ? 0 : Math.round((completed / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n output += `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${pct}% complete\\n`\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n const tickets:ReadonlyArray<DisplayTicket> = [\n ...this.wave.tickets,\n ...this.remaining,\n ]\n let renderedTicketCount = 0\n\n if (tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of tickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(ticket.filename, width - 2)\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount = availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow = (\n this.viewportStart +\n tentativeCount < tickets.length\n )\n ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0)\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(ticket.filename, width - 2)\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n tickets.length -\n (this.viewportStart + ticketsToShow)\n )\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(Math.min(SEPARATOR_WIDTH, width)) + '\\n'\n\n const msgLines = Math.max(\n 0,\n this.config.messageLines - this.messages.length,\n )\n for (const msg of this.messages) {\n output += `${fitLine(msg, width)}\\n`\n }\n for (let i = 0; i < msgLines; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + this.config.messageLines\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<{ num:number; filename:string }>,\n progress?:RunProgress,\n _remaining?:ReadonlyArray<{\n num:number\n filename:string\n }>,\n ):void {\n const pct = progress && progress.total > 0 ?\n ` (${progress.completed}/${progress.total} done)` :\n ''\n console.log(\n `\\nWave ${waveNum}${pct}: ` +\n tickets.map(t => t.filename).join(', ')\n )\n }\n\n setTicketStatus (_num:number, _status:TicketStatus):void {\n // no-op: plain mode reports via log()\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (stream?:NodeJS.WriteStream):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n\n return new PlainDisplay()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/index.js CHANGED
@@ -507,8 +507,10 @@ async function mergeBranch(repoRoot, branch) {
507
507
  ["merge", branch, "--no-edit"],
508
508
  { cwd: repoRoot }
509
509
  );
510
- return true;
511
- } catch {
510
+ return { success: true };
511
+ } catch (err) {
512
+ const execErr = err;
513
+ const stderr = execErr.stderr || execErr.stdout;
512
514
  try {
513
515
  await execFileAsync3(
514
516
  "git",
@@ -517,7 +519,7 @@ async function mergeBranch(repoRoot, branch) {
517
519
  );
518
520
  } catch {
519
521
  }
520
- return false;
522
+ return { success: false, stderr };
521
523
  }
522
524
  }
523
525
  __name(mergeBranch, "mergeBranch");
@@ -538,8 +540,10 @@ async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
538
540
  ["rebase", onto],
539
541
  { cwd: worktreePath }
540
542
  );
541
- return true;
542
- } catch {
543
+ return { success: true };
544
+ } catch (err) {
545
+ const execErr = err;
546
+ const stderr = execErr.stderr || execErr.stdout;
543
547
  try {
544
548
  await execFileAsync3(
545
549
  "git",
@@ -548,7 +552,7 @@ async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
548
552
  );
549
553
  } catch {
550
554
  }
551
- return false;
555
+ return { success: false, stderr };
552
556
  }
553
557
  }
554
558
  try {
@@ -567,8 +571,9 @@ async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
567
571
  ["checkout", onto],
568
572
  { cwd: repoRoot }
569
573
  );
570
- return true;
571
- } catch {
574
+ return { success: true };
575
+ } catch (err) {
576
+ const stderr = err.stderr;
572
577
  try {
573
578
  await execFileAsync3(
574
579
  "git",
@@ -585,7 +590,7 @@ async function rebaseBranch(repoRoot, branch, onto, worktreePath) {
585
590
  );
586
591
  } catch {
587
592
  }
588
- return false;
593
+ return { success: false, stderr };
589
594
  }
590
595
  }
591
596
  __name(rebaseBranch, "rebaseBranch");
@@ -619,13 +624,16 @@ async function mergeWave(opts) {
619
624
  conflicted: false
620
625
  };
621
626
  if (i === 0) {
622
- const success = await mergeBranch(
627
+ const result = await mergeBranch(
623
628
  repoRoot,
624
629
  branch
625
630
  );
626
- mergingResult.merged = success;
627
- mergingResult.conflicted = !success;
628
- if (!success) {
631
+ mergingResult.merged = result.success;
632
+ mergingResult.conflicted = !result.success;
633
+ if (!result.success) {
634
+ mergingResult.stderr = result.stderr;
635
+ }
636
+ if (!result.success) {
629
637
  if (strategy === "stop") {
630
638
  stoppedAt = mergingResult;
631
639
  merged.push(mergingResult);
@@ -635,14 +643,15 @@ async function mergeWave(opts) {
635
643
  continue;
636
644
  }
637
645
  } else {
638
- const rebaseSuccess = await rebaseBranch(
646
+ const rebaseResult = await rebaseBranch(
639
647
  repoRoot,
640
648
  branch,
641
649
  baseBranch,
642
650
  worktreePath
643
651
  );
644
- if (!rebaseSuccess) {
652
+ if (!rebaseResult.success) {
645
653
  mergingResult.conflicted = true;
654
+ mergingResult.stderr = rebaseResult.stderr;
646
655
  if (strategy === "stop") {
647
656
  stoppedAt = mergingResult;
648
657
  merged.push(mergingResult);
@@ -652,13 +661,16 @@ async function mergeWave(opts) {
652
661
  continue;
653
662
  }
654
663
  }
655
- const mergeSuccess = await mergeBranch(
664
+ const mergeResult = await mergeBranch(
656
665
  repoRoot,
657
666
  branch
658
667
  );
659
- mergingResult.merged = mergeSuccess;
660
- mergingResult.conflicted = !mergeSuccess;
661
- if (!mergeSuccess) {
668
+ mergingResult.merged = mergeResult.success;
669
+ mergingResult.conflicted = !mergeResult.success;
670
+ if (!mergeResult.success) {
671
+ mergingResult.stderr = mergeResult.stderr;
672
+ }
673
+ if (!mergeResult.success) {
662
674
  if (strategy === "stop") {
663
675
  stoppedAt = mergingResult;
664
676
  merged.push(mergingResult);
@@ -748,6 +760,7 @@ var StatusDisplay = class {
748
760
  }
749
761
  wave = null;
750
762
  progress = null;
763
+ remaining = [];
751
764
  messages = [];
752
765
  renderedLines = 0;
753
766
  config;
@@ -766,7 +779,7 @@ var StatusDisplay = class {
766
779
  const available = termHeight - overhead;
767
780
  return Math.max(1, available);
768
781
  }
769
- startWave(waveNum, totalWaves, tickets, progress) {
782
+ startWave(waveNum, totalWaves, tickets, progress, remaining) {
770
783
  this.wave = {
771
784
  waveNum,
772
785
  totalWaves,
@@ -775,6 +788,10 @@ var StatusDisplay = class {
775
788
  status: "pending"
776
789
  }))
777
790
  };
791
+ this.remaining = (remaining ?? []).map((t) => ({
792
+ ...t,
793
+ status: "pending"
794
+ }));
778
795
  this.progress = progress ?? null;
779
796
  this.messages = [];
780
797
  this.viewportStart = 0;
@@ -859,7 +876,10 @@ var StatusDisplay = class {
859
876
  output += `Wave ${this.wave.waveNum}/${this.wave.totalWaves} - ${pct}% complete
860
877
  `;
861
878
  const availableRows = this.getAvailableRows();
862
- const tickets = this.wave.tickets;
879
+ const tickets = [
880
+ ...this.wave.tickets,
881
+ ...this.remaining
882
+ ];
863
883
  let renderedTicketCount = 0;
864
884
  if (tickets.length <= availableRows) {
865
885
  for (const ticket of tickets) {
@@ -940,7 +960,7 @@ var PlainDisplay = class {
940
960
  static {
941
961
  __name(this, "PlainDisplay");
942
962
  }
943
- startWave(waveNum, _totalWaves, tickets, progress) {
963
+ startWave(waveNum, _totalWaves, tickets, progress, _remaining) {
944
964
  const pct = progress && progress.total > 0 ? ` (${progress.completed}/${progress.total} done)` : "";
945
965
  console.log(
946
966
  `
@@ -1081,6 +1101,29 @@ function renderPrompt(specBody, ticket) {
1081
1101
  ].join("\n");
1082
1102
  }
1083
1103
  __name(renderPrompt, "renderPrompt");
1104
+ function renderMergePrompt(specBody, ticket) {
1105
+ return [
1106
+ "---- SPEC CONTEXT ----",
1107
+ specBody,
1108
+ "",
1109
+ "---- TICKET ----",
1110
+ ticket.filename,
1111
+ ticket.body,
1112
+ "",
1113
+ "---- AGENT INSTRUCTIONS ----",
1114
+ "You are an autonomous coding agent resolving a merge conflict.",
1115
+ "",
1116
+ "Another agent already implemented this ticket on a feature branch. The branch was then merged with the base branch, but conflicts arose. The working tree may contain conflict markers that need resolution.",
1117
+ "",
1118
+ "Your task:",
1119
+ "1. Find and resolve all conflict markers (<<<<<<< / ======= / >>>>>>>).",
1120
+ "2. Make sure the code compiles and tests pass.",
1121
+ "3. Commit the resolved files.",
1122
+ "",
1123
+ "Do NOT re-implement the ticket from scratch."
1124
+ ].join("\n");
1125
+ }
1126
+ __name(renderMergePrompt, "renderMergePrompt");
1084
1127
  function listTickets(tickets) {
1085
1128
  console.log("\nTickets:");
1086
1129
  for (const t of tickets) {
@@ -1490,7 +1533,10 @@ async function respawnAndRetryMerge(opts) {
1490
1533
  } else {
1491
1534
  console.log(msg1);
1492
1535
  }
1493
- const prompt = renderPrompt(opts.specBody, opts.ticket);
1536
+ const prompt = renderMergePrompt(
1537
+ opts.specBody,
1538
+ opts.ticket
1539
+ );
1494
1540
  const logPath = join3(
1495
1541
  opts.logsDir,
1496
1542
  opts.ticket.filename.replace(/\.md$/, ".respawn.log")
@@ -1518,19 +1564,27 @@ async function respawnAndRetryMerge(opts) {
1518
1564
  }
1519
1565
  return false;
1520
1566
  }
1521
- const merged = await mergeBranch(
1567
+ const mergeResult = await mergeBranch(
1522
1568
  opts.repoRoot,
1523
1569
  opts.branch
1524
1570
  );
1525
- if (!merged) {
1571
+ if (!mergeResult.success) {
1526
1572
  const msg3 = ` ${opts.ticket.filename}: merge still conflicted after respawn`;
1527
1573
  if (opts.onLine) {
1528
1574
  opts.onLine(msg3);
1529
1575
  } else {
1530
1576
  console.log(msg3);
1531
1577
  }
1578
+ if (mergeResult.stderr) {
1579
+ const stderrMsg = ` git: ${mergeResult.stderr.trim()}`;
1580
+ if (opts.onLine) {
1581
+ opts.onLine(stderrMsg);
1582
+ } else {
1583
+ console.log(stderrMsg);
1584
+ }
1585
+ }
1532
1586
  }
1533
- return merged;
1587
+ return mergeResult.success;
1534
1588
  }
1535
1589
  __name(respawnAndRetryMerge, "respawnAndRetryMerge");
1536
1590
  function runProgress(tickets) {
@@ -1551,6 +1605,10 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
1551
1605
  const ready = findReadyTickets(tickets);
1552
1606
  if (ready.length === 0) break;
1553
1607
  waveNum++;
1608
+ const readyNums = new Set(ready.map((t) => t.num));
1609
+ const remaining2 = tickets.filter(
1610
+ (t) => t.status === "ready-for-agent" && !readyNums.has(t.num)
1611
+ );
1554
1612
  d.startWave(
1555
1613
  waveNum,
1556
1614
  totalWaves,
@@ -1558,7 +1616,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, di
1558
1616
  num: t.num,
1559
1617
  filename: t.filename
1560
1618
  })),
1561
- runProgress(tickets)
1619
+ runProgress(tickets),
1620
+ remaining2.map((t) => ({
1621
+ num: t.num,
1622
+ filename: t.filename
1623
+ }))
1562
1624
  );
1563
1625
  const results = await Promise.allSettled(
1564
1626
  ready.map(async (t) => {
@@ -1631,6 +1693,11 @@ async function runWavesIsolated(opts) {
1631
1693
  const repoRoot = getRepoRoot();
1632
1694
  const repoName = getRepoName();
1633
1695
  const baseBranch = getCurrentBranch();
1696
+ if (isDirty(repoRoot)) {
1697
+ throw new Error(
1698
+ "Working tree is dirty; commit or stash changes before running"
1699
+ );
1700
+ }
1634
1701
  const progressPath = join3(
1635
1702
  dirname(opts.logsDir),
1636
1703
  "progress.md"
@@ -1651,6 +1718,10 @@ async function runWavesIsolated(opts) {
1651
1718
  const ready = findReadyTickets(opts.tickets);
1652
1719
  if (ready.length === 0) break;
1653
1720
  waveNum++;
1721
+ const readyNums = new Set(ready.map((t) => t.num));
1722
+ const remaining2 = opts.tickets.filter(
1723
+ (t) => t.status === "ready-for-agent" && !readyNums.has(t.num)
1724
+ );
1654
1725
  d.startWave(
1655
1726
  waveNum,
1656
1727
  totalWaves,
@@ -1658,7 +1729,11 @@ async function runWavesIsolated(opts) {
1658
1729
  num: t.num,
1659
1730
  filename: t.filename
1660
1731
  })),
1661
- runProgress(opts.tickets)
1732
+ runProgress(opts.tickets),
1733
+ remaining2.map((t) => ({
1734
+ num: t.num,
1735
+ filename: t.filename
1736
+ }))
1662
1737
  );
1663
1738
  const worktreeSettled = await Promise.allSettled(
1664
1739
  ready.map((t) => createWorktree({
@@ -1871,6 +1946,22 @@ async function runWavesIsolated(opts) {
1871
1946
  d.log(
1872
1947
  ` ${ticket.filename}: merge failed, staying ready-for-agent`
1873
1948
  );
1949
+ if (branchResult.stderr) {
1950
+ d.log(
1951
+ " git: " + branchResult.stderr.trim()
1952
+ );
1953
+ }
1954
+ checkTicket(progress, ticket.num, {
1955
+ branch: branchResult.branch,
1956
+ exitCode: 0,
1957
+ done: false
1958
+ });
1959
+ writeProgress(progressPath, progress);
1960
+ await commitProgress(
1961
+ repoRoot,
1962
+ progressPath,
1963
+ `nightralph: ${ticket.filename} merge failed`
1964
+ );
1874
1965
  }
1875
1966
  }
1876
1967
  }