nightralph 0.0.53 → 0.0.56

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.
Files changed (60) hide show
  1. package/README.md +181 -19
  2. package/dist/blockers.js +149 -0
  3. package/dist/blockers.js.map +7 -0
  4. package/dist/display.js +12 -4
  5. package/dist/display.js.map +2 -2
  6. package/dist/docs-templates/issue-tracker.md +9 -1
  7. package/dist/index.js +2735 -415
  8. package/dist/index.js.map +4 -4
  9. package/dist/learnings.js +360 -0
  10. package/dist/learnings.js.map +7 -0
  11. package/dist/meta.json +289 -16
  12. package/dist/orchestrator.js +753 -188
  13. package/dist/orchestrator.js.map +3 -3
  14. package/dist/preflight.js +34 -0
  15. package/dist/preflight.js.map +7 -0
  16. package/dist/progress.js +5 -4
  17. package/dist/progress.js.map +2 -2
  18. package/dist/resolve.js +98 -9
  19. package/dist/resolve.js.map +2 -2
  20. package/dist/retry.js +83 -0
  21. package/dist/retry.js.map +7 -0
  22. package/dist/review.js +669 -0
  23. package/dist/review.js.map +7 -0
  24. package/dist/rules.js +67 -0
  25. package/dist/rules.js.map +7 -0
  26. package/dist/skills/to-tickets/SKILL.md +5 -5
  27. package/dist/src/blockers.d.ts +45 -0
  28. package/dist/src/blockers.d.ts.map +1 -0
  29. package/dist/src/display.d.ts +9 -0
  30. package/dist/src/display.d.ts.map +1 -1
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/learnings.d.ts +70 -0
  33. package/dist/src/learnings.d.ts.map +1 -0
  34. package/dist/src/orchestrator.d.ts +85 -28
  35. package/dist/src/orchestrator.d.ts.map +1 -1
  36. package/dist/src/preflight.d.ts +6 -0
  37. package/dist/src/preflight.d.ts.map +1 -0
  38. package/dist/src/progress.d.ts +1 -1
  39. package/dist/src/progress.d.ts.map +1 -1
  40. package/dist/src/resolve.d.ts +24 -0
  41. package/dist/src/resolve.d.ts.map +1 -1
  42. package/dist/src/retry.d.ts +26 -0
  43. package/dist/src/retry.d.ts.map +1 -0
  44. package/dist/src/review.d.ts +131 -0
  45. package/dist/src/review.d.ts.map +1 -0
  46. package/dist/src/rules.d.ts +11 -0
  47. package/dist/src/rules.d.ts.map +1 -0
  48. package/dist/src/stats.d.ts +43 -0
  49. package/dist/src/stats.d.ts.map +1 -0
  50. package/dist/src/testcmd.d.ts +1 -0
  51. package/dist/src/testcmd.d.ts.map +1 -1
  52. package/dist/src/worktree.d.ts +30 -1
  53. package/dist/src/worktree.d.ts.map +1 -1
  54. package/dist/stats.js +111 -0
  55. package/dist/stats.js.map +7 -0
  56. package/dist/testcmd.js +13 -0
  57. package/dist/testcmd.js.map +2 -2
  58. package/dist/worktree.js +236 -32
  59. package/dist/worktree.js.map +2 -2
  60. package/package.json +3 -2
@@ -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 | 'awaiting-merge'\n | 'done'\n | 'failed'\n | 'blocked'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n status:TicketStatus\n blockedBy?:ReadonlyArray<string>\n}\n\nexport type TicketRef = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n}\n\nexport type DisplayConfig = {\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts this run's tickets done before the wave\n * started; `total` is the size of this run's ticket list. Only\n * PlainDisplay prints it; StatusDisplay counts from its own list.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n /**\n * Called once at the start of the run, fixes the ticket list\n * for the entire session.\n */\n startRun(tickets:ReadonlyArray<TicketRef>):void\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n progress?:RunProgress,\n ):void\n setTicketStatus(\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void\n /**\n * Mark a ticket as blocked; `blockedBy` holds display labels\n * of the upstream tickets that failed or need a human.\n */\n setBlocked(\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):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 cyan (text:string):string {\n return `${ESC}36m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nexport function ticketLabel (\n ticket:{ num:number; filename:string; feature?:string },\n):string {\n if (ticket.feature) {\n const padded = String(ticket.num).padStart(2, '0')\n return `${ticket.feature}/${padded}`\n }\n return ticket.filename\n}\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\nconst MIN_LOG_ROWS = 1\nconst MAX_MESSAGE_BUFFER = 200\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\nfunction rowText (ticket:DisplayTicket):string {\n const label = ticketLabel(ticket)\n if (ticket.status !== 'blocked' || !ticket.blockedBy?.length) {\n return label\n }\n return `${label} (blocked by ${ticket.blockedBy.join(', ')})`\n}\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\n/**\n * Split a line into rows of at most `width` visible columns,\n * the way a terminal would wrap it. Short lines keep their\n * ANSI styling; long lines are wrapped on visible text only,\n * so `render` can count every row it emits.\n */\nexport function wrapLine (line:string, width:number):Array<string> {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return [line]\n const rows:Array<string> = []\n for (let i = 0; i < visible.length; i += width) {\n rows.push(visible.slice(i, i + width))\n }\n return rows\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 // The agent and its test gate are finished; the branch\n // waits for the rest of the wave before it is merged. It\n // stays cyan through the merge and any conflict respawn.\n case 'awaiting-merge':\n return color.cyan(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n case 'blocked':\n // Ticket cannot run this session because an upstream\n // ticket failed or needs a human.\n return color.dim(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private tickets:Array<DisplayTicket> = []\n private hasRun:boolean = false\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\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\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 + MIN_LOG_ROWS\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n private findTicket (\n num:number,\n feature?:string,\n ):DisplayTicket|null {\n const found = feature ?\n this.tickets.find(\n t => t.num === num && t.feature === feature,\n ) :\n this.tickets.find(t => t.num === num)\n return found ?? null\n }\n\n startRun (tickets:ReadonlyArray<TicketRef>):void {\n this.tickets = tickets.map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.hasRun = true\n this.viewportStart = 0\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n _progress?:RunProgress,\n ):void {\n if (!this.hasRun) {\n this.startRun(tickets)\n }\n\n this.wave = {\n waveNum,\n totalWaves,\n }\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 this.render()\n }\n\n setTicketStatus (\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.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 setBlocked (\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = 'blocked'\n ticket.blockedBy = [...blockedBy]\n this.render()\n }\n\n log (message:string):void {\n // Before the first wave there is no grid to repaint,\n // so a message would otherwise be dropped. Write it\n // straight to the stream instead. A later render\n // never erases these rows, since renderedLines is 0.\n if (!this.wave) {\n this.config.stream.write(message + '\\n')\n return\n }\n for (const line of message.split('\\n')) {\n this.messages.push(line)\n }\n while (this.messages.length > MAX_MESSAGE_BUFFER) {\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 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 done = this.tickets.filter(\n t => t.status === 'done',\n ).length\n const total = this.tickets.length\n const pct = total === 0 ? 0 : Math.round((done / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n const header = `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${done}/${total} done - ${pct}%`\n output += fitLine(header, width) + '\\n'\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n let renderedTicketCount = 0\n\n if (this.tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of this.tickets) {\n const dot = renderDot(ticket.status)\n const text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = this.tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n this.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 this.tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n this.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 < this.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 = this.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 text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n this.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 termHeight = this.config.stream.rows ?? 24\n const logRows = Math.max(\n 0,\n termHeight - 3 - renderedTicketCount,\n )\n const wrapped = this.messages.flatMap(\n msg => wrapLine(msg, width),\n )\n const visibleMessages = wrapped.slice(-logRows)\n for (const row of visibleMessages) {\n output += `${row}\\n`\n }\n const emptyRows = logRows - visibleMessages.length\n for (let i = 0; i < emptyRows; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + logRows\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startRun (_tickets:ReadonlyArray<TicketRef>):void {\n // no-op: plain mode has no fixed list\n }\n\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\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 => ticketLabel(t)).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus,\n _feature?:string,\n ):void {\n // no-op: plain mode reports via log()\n }\n\n setBlocked (\n _num:number,\n _feature:string|undefined,\n _blockedBy:ReadonlyArray<string>,\n ):void {\n // no-op: plain mode has no fixed list\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": ["import { formatStats, type SubagentStats } from './stats.js'\n\nexport type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'awaiting-merge'\n | 'done'\n | 'failed'\n | 'blocked'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n status:TicketStatus\n blockedBy?:ReadonlyArray<string>\n stats?:SubagentStats\n}\n\nexport type TicketRef = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n}\n\nexport type DisplayConfig = {\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts this run's tickets done before the wave\n * started; `total` is the size of this run's ticket list. Only\n * PlainDisplay prints it; StatusDisplay counts from its own list.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n /**\n * Called once at the start of the run, fixes the ticket list\n * for the entire session.\n */\n startRun(tickets:ReadonlyArray<TicketRef>):void\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n progress?:RunProgress,\n ):void\n setTicketStatus(\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void\n /**\n * Mark a ticket as blocked; `blockedBy` holds display labels\n * of the upstream tickets that failed or need a human.\n */\n setBlocked(\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void\n /**\n * Subagent stats for a ticket's row: the running total of\n * its attempts so far.\n */\n setTicketStats(\n num:number,\n feature:string|undefined,\n stats:SubagentStats,\n ):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 cyan (text:string):string {\n return `${ESC}36m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nexport function ticketLabel (\n ticket:{ num:number; filename:string; feature?:string },\n):string {\n if (ticket.feature) {\n const padded = String(ticket.num).padStart(2, '0')\n return `${ticket.feature}/${padded}`\n }\n return ticket.filename\n}\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\nconst MIN_LOG_ROWS = 1\nconst MAX_MESSAGE_BUFFER = 200\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\nfunction rowText (ticket:DisplayTicket):string {\n const label = ticketLabel(ticket)\n const base = ticket.status === 'blocked' &&\n ticket.blockedBy?.length ?\n `${label} (blocked by ${ticket.blockedBy.join(', ')})` :\n label\n const suffix = formatStats(ticket.stats)\n return suffix === null ? base : `${base} - ${suffix}`\n}\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\n/**\n * Split a line into rows of at most `width` visible columns,\n * the way a terminal would wrap it. Short lines keep their\n * ANSI styling; long lines are wrapped on visible text only,\n * so `render` can count every row it emits.\n */\nexport function wrapLine (line:string, width:number):Array<string> {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return [line]\n const rows:Array<string> = []\n for (let i = 0; i < visible.length; i += width) {\n rows.push(visible.slice(i, i + width))\n }\n return rows\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 // The agent and its test gate are finished; the branch\n // waits for the rest of the wave before it is merged. It\n // stays cyan through the merge and any conflict respawn.\n case 'awaiting-merge':\n return color.cyan(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n case 'blocked':\n // Ticket cannot run this session because an upstream\n // ticket failed or needs a human.\n return color.dim(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private tickets:Array<DisplayTicket> = []\n private hasRun:boolean = false\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\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\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 + MIN_LOG_ROWS\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n private findTicket (\n num:number,\n feature?:string,\n ):DisplayTicket|null {\n const found = feature ?\n this.tickets.find(\n t => t.num === num && t.feature === feature,\n ) :\n this.tickets.find(t => t.num === num)\n return found ?? null\n }\n\n startRun (tickets:ReadonlyArray<TicketRef>):void {\n this.tickets = tickets.map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.hasRun = true\n this.viewportStart = 0\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n _progress?:RunProgress,\n ):void {\n if (!this.hasRun) {\n this.startRun(tickets)\n }\n\n this.wave = {\n waveNum,\n totalWaves,\n }\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 this.render()\n }\n\n setTicketStatus (\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.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 setBlocked (\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = 'blocked'\n ticket.blockedBy = [...blockedBy]\n this.render()\n }\n\n setTicketStats (\n num:number,\n feature:string|undefined,\n stats:SubagentStats,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.stats = { ...stats }\n this.render()\n }\n\n log (message:string):void {\n // Before the first wave there is no grid to repaint,\n // so a message would otherwise be dropped. Write it\n // straight to the stream instead. A later render\n // never erases these rows, since renderedLines is 0.\n if (!this.wave) {\n this.config.stream.write(message + '\\n')\n return\n }\n for (const line of message.split('\\n')) {\n this.messages.push(line)\n }\n while (this.messages.length > MAX_MESSAGE_BUFFER) {\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 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 done = this.tickets.filter(\n t => t.status === 'done',\n ).length\n const total = this.tickets.length\n const pct = total === 0 ? 0 : Math.round((done / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n const header = `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${done}/${total} done - ${pct}%`\n output += fitLine(header, width) + '\\n'\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n let renderedTicketCount = 0\n\n if (this.tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of this.tickets) {\n const dot = renderDot(ticket.status)\n const text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = this.tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n this.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 this.tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n this.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 < this.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 = this.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 text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n this.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 termHeight = this.config.stream.rows ?? 24\n const logRows = Math.max(\n 0,\n termHeight - 3 - renderedTicketCount,\n )\n const wrapped = this.messages.flatMap(\n msg => wrapLine(msg, width),\n )\n const visibleMessages = wrapped.slice(-logRows)\n for (const row of visibleMessages) {\n output += `${row}\\n`\n }\n const emptyRows = logRows - visibleMessages.length\n for (let i = 0; i < emptyRows; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + logRows\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startRun (_tickets:ReadonlyArray<TicketRef>):void {\n // no-op: plain mode has no fixed list\n }\n\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\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 => ticketLabel(t)).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus,\n _feature?:string,\n ):void {\n // no-op: plain mode reports via log()\n }\n\n setBlocked (\n _num:number,\n _feature:string|undefined,\n _blockedBy:ReadonlyArray<string>,\n ):void {\n // no-op: plain mode has no fixed list\n }\n\n setTicketStats (\n _num:number,\n _feature:string|undefined,\n _stats:SubagentStats,\n ):void {\n // no-op: plain mode has no fixed list\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
  }
@@ -13,7 +13,15 @@ Issues and specs for this repo live as markdown files in `.scratch/`.
13
13
  issue file (see `triage-labels.md` for the role strings)
14
14
  - Dependencies are recorded as a `**Blocked by:**` line near the top of
15
15
  each issue file, listing the numbers of the tickets that must complete
16
- first, or `None`
16
+ first, or `None`. A ticket in another feature is `<feature> <NN>`,
17
+ where `<feature>` is its `.scratch/<feature>/` directory name
18
+ (a path such as `rollout/billing` for a nested feature),
19
+ repeated before each of its numbers. List every number: `03-05`
20
+ means 3 and 5, and a feature name before it applies to both. Notes
21
+ in parentheses are ignored. The list may wrap onto the next lines
22
+ and ends at a blank line, the next `**Field:**` line, a heading or
23
+ a checklist item. Example:
24
+ `**Blocked by:** 01, 02, newsletter-audit 16 (shares the import route)`
17
25
  - Comments and conversation history append to the bottom of the file under
18
26
  a `## Comments` heading
19
27