@vzn/vx 0.0.650 → 0.0.651

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 (115) hide show
  1. package/README.md +5 -20
  2. package/package.json +5 -5
  3. package/schemas/last.json +15 -0
  4. package/src/bin.ts +9 -31
  5. package/src/cache/cache.ts +202 -25
  6. package/src/cache/file-hashes.ts +3 -0
  7. package/src/cache/git-inputs.ts +188 -39
  8. package/src/cache/index.ts +1 -0
  9. package/src/cache/inputs.ts +3 -0
  10. package/src/cache/layer.ts +11 -0
  11. package/src/cache/layered-cache.ts +4 -0
  12. package/src/cache/linked-inputs.ts +3 -0
  13. package/src/cache/lockfile-memo.ts +3 -2
  14. package/src/cache/output-index.ts +5 -2
  15. package/src/cache/remote-lane.ts +2 -1
  16. package/src/cache/run-history.ts +11 -1
  17. package/src/cache/schema.ts +11 -1
  18. package/src/cli/cache.ts +26 -16
  19. package/src/cli/completions.ts +4 -2
  20. package/src/cli/docs.ts +10 -11
  21. package/src/cli/help.ts +12 -23
  22. package/src/cli/index.ts +4 -9
  23. package/src/cli/info.ts +15 -7
  24. package/src/cli/init.ts +23 -15
  25. package/src/cli/last.ts +213 -203
  26. package/src/cli/lock.ts +55 -5
  27. package/src/cli/plan-format.ts +100 -118
  28. package/src/cli/plugin-commands.ts +8 -2
  29. package/src/cli/plugin-templates.ts +2 -2
  30. package/src/cli/run.ts +46 -27
  31. package/src/cli/select.ts +43 -5
  32. package/src/cli/show.ts +38 -42
  33. package/src/cli/task-verb.ts +22 -0
  34. package/src/cli/upgrade.ts +3 -1
  35. package/src/cli/watch-fs.ts +6 -1
  36. package/src/cli/watch-judge.ts +2 -1
  37. package/src/cli/watch.ts +83 -44
  38. package/src/cli/why.ts +123 -89
  39. package/src/cli/workspace-config.ts +2 -1
  40. package/src/exec/executor.ts +2 -1
  41. package/src/exec/kill-tree.ts +58 -5
  42. package/src/exec/local-executor.ts +1 -1
  43. package/src/exec/pipes.ts +152 -0
  44. package/src/exec/runner.ts +199 -182
  45. package/src/exec/sandbox-binds.ts +1 -1
  46. package/src/exec/sandbox-runtime.ts +11 -2
  47. package/src/exec/spawn-off.ts +154 -0
  48. package/src/exec/warm-shell.ts +146 -0
  49. package/src/graph/critical-path.ts +40 -28
  50. package/src/graph/priorities.ts +98 -44
  51. package/src/graph/scheduler.ts +14 -10
  52. package/src/graph/task-graph.ts +27 -1
  53. package/src/handoff.ts +10 -7
  54. package/src/history/history.ts +5 -0
  55. package/src/history/metrics.ts +9 -1
  56. package/src/index.ts +3 -1
  57. package/src/orchestrator/doctor.ts +1 -1
  58. package/src/orchestrator/execute-task.ts +16 -2
  59. package/src/orchestrator/graph-memo.ts +50 -1
  60. package/src/orchestrator/graph-stage.ts +6 -0
  61. package/src/orchestrator/index.ts +3 -0
  62. package/src/orchestrator/live-usage.ts +16 -11
  63. package/src/orchestrator/locked-digests.ts +63 -0
  64. package/src/orchestrator/lockfile-claim.ts +108 -29
  65. package/src/orchestrator/miss-save.ts +31 -3
  66. package/src/orchestrator/persistent-task.ts +3 -1
  67. package/src/orchestrator/plan.ts +5 -1
  68. package/src/orchestrator/plugin-host.ts +61 -0
  69. package/src/orchestrator/plugin.ts +47 -2
  70. package/src/orchestrator/prepare.ts +16 -1
  71. package/src/orchestrator/projects.ts +4 -1
  72. package/src/orchestrator/run-context.ts +29 -10
  73. package/src/orchestrator/run-failures.ts +1 -1
  74. package/src/orchestrator/run-records.ts +10 -0
  75. package/src/orchestrator/run.ts +53 -22
  76. package/src/orchestrator/sandbox-request.ts +2 -2
  77. package/src/orchestrator/signals.ts +8 -5
  78. package/src/orchestrator/task-hash.ts +17 -3
  79. package/src/report/events.ts +3 -11
  80. package/src/report/forecast.ts +10 -32
  81. package/src/report/framed-output.ts +272 -173
  82. package/src/report/index.ts +8 -3
  83. package/src/report/logger.ts +73 -38
  84. package/src/report/plain-output.ts +1 -1
  85. package/src/report/status-line.ts +49 -357
  86. package/src/report/summary.ts +100 -131
  87. package/src/ui/ansi.ts +48 -0
  88. package/src/ui/components.ts +506 -0
  89. package/src/ui/index.ts +79 -0
  90. package/src/ui/kit.ts +37 -0
  91. package/src/ui/live-region.ts +363 -0
  92. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  93. package/src/ui/terminal.ts +51 -0
  94. package/src/ui/theme.ts +224 -0
  95. package/src/ui/version-line.ts +28 -0
  96. package/src/util/config-frame.ts +65 -10
  97. package/src/util/edit-distance.ts +6 -1
  98. package/src/util/index.ts +2 -15
  99. package/src/util/paths.ts +20 -2
  100. package/src/util/secret-mask.ts +39 -3
  101. package/src/util/settle.ts +31 -0
  102. package/src/util/timing.ts +4 -16
  103. package/src/workspace/affected.ts +89 -30
  104. package/src/workspace/config-schema.ts +4 -2
  105. package/src/workspace/dependencies-lock.ts +191 -0
  106. package/src/workspace/filter.ts +28 -0
  107. package/src/workspace/index.ts +9 -0
  108. package/src/workspace/lockfile.ts +159 -6
  109. package/src/workspace/migration.ts +4 -3
  110. package/src/workspace/workspace.ts +3 -0
  111. package/src/util/colors.ts +0 -133
  112. package/src/util/report.ts +0 -44
  113. package/src/util/ui.ts +0 -258
  114. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  115. /package/src/{report → ui}/terminal-signals.ts +0 -0
@@ -26,49 +26,26 @@
26
26
  // `--ui=stream` mode.
27
27
 
28
28
  import { withForwardArgs } from '../exec/index.js'
29
- import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
29
+ import { isCacheHit, isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
30
+ import { formatElapsed, maskedCommand, printable } from '../util/index.js'
31
+ import * as ui from '../ui/index.js'
30
32
  import {
31
- formatElapsed,
32
- identityColor,
33
- maskedCommand,
33
+ fileMemo,
34
+ hasEscape,
35
+ idParts,
36
+ lastRedraw,
37
+ leavesStyle,
38
+ linkPaths,
34
39
  paint,
35
- printable,
36
40
  ruleWidth,
37
- ui,
41
+ STYLE_END,
38
42
  type ColorSupport,
39
- } from '../util/index.js'
43
+ type Style,
44
+ } from '../ui/index.js'
40
45
  import { neverStarted } from './summary.js'
41
46
  import { outcomeLabel, outcomeWord, ranNoCache, skippedReason } from './events.js'
42
- import { fileMemo, linkPaths } from './path-links.js'
43
47
 
44
48
  const NO_COLOR: ColorSupport = { enabled: false }
45
- // Ends any colour a task's output left on, so vx's own lines below never
46
- // take it (a task killed mid-write leaves one open).
47
- export const RESET = '\x1b[0m'
48
-
49
- /** Whether `text`'s last colour code sets a style rather than resetting it. */
50
- export function leavesStyle(text: string): boolean {
51
- const at = text.lastIndexOf('\x1b[')
52
- if (at < 0) return false
53
- const sgr = /^\[([\d;]*)m/.exec(text.slice(at + 1, at + 64))
54
- // Not a colour code (a cursor move): look no further, as a reset costs nothing.
55
- if (sgr === null) return true
56
- return sgr[1] !== '' && sgr[1] !== '0'
57
- }
58
-
59
- const ACCENT = '#06b6d4' // cyan-500 — bullets, remote-hit hint
60
- // Identity hues (`identityColor`) are outside the status palette (green /
61
- // red / yellow / cyan) so a task id can never read as an outcome. Each
62
- // half of an id hashes its own name: same project, same hue; same target,
63
- // same hue, in every run, list and region row.
64
-
65
- const SUCCESS = '#22c55e' // green-500 — success / fresh
66
- const WARN = '#eab308' // yellow-500 — skipped
67
- const ERROR = '#ef4444' // red-500 — failed
68
- const ABORTED = '#f97316' // orange-500 — killed by a shutdown signal
69
- const LOCAL = '#38bdf8' // sky-400 — local cache hit
70
- const REMOTE = '#2563eb' // blue-600 — remote cache hit
71
-
72
49
  /** A piece of a task's output, as written: `err` for stderr. */
73
50
  export interface OutputChunk {
74
51
  text: string
@@ -111,11 +88,11 @@ export function formatTaskBlock(
111
88
  const output = body.output ?? []
112
89
 
113
90
  const idPainted = paintTaskId(node, colors, { bold: true })
114
- const corner = (s: string) => paint('', s, colors, { dim: true })
91
+ const corner = (s: string) => paint('muted', s, colors)
115
92
  // A ready server's frame reads as the live one does: marked, and running.
116
93
  const mark = persistentMark(node, colors)
117
94
  const running = mark !== '' && outcome.status === 'success' && outcome.serverExit === undefined
118
- const header = running ? paint(ACCENT, 'running', colors) : formatBlockHeader(outcome, colors)
95
+ const header = running ? paint('info', 'running', colors) : formatBlockHeader(outcome, colors)
119
96
  const lines: string[] = [`${corner('┌─')} ${mark}${idPainted} ${corner('>')} ${header}`]
120
97
 
121
98
  // The command section shows what actually ran — executed tasks
@@ -149,9 +126,7 @@ export function formatTaskBlock(
149
126
 
150
127
  /** What a bounded capture dropped from its head, so a capped log never reads as whole. */
151
128
  function droppedNote(lost: number, colors: ColorSupport): string {
152
- return paint('', `… ${lost} earlier character${lost === 1 ? '' : 's'} dropped`, colors, {
153
- dim: true,
154
- })
129
+ return paint('muted', `… ${lost} earlier character${lost === 1 ? '' : 's'} dropped`, colors)
155
130
  }
156
131
 
157
132
  /**
@@ -163,7 +138,7 @@ function pushStreamSection(
163
138
  lines: string[],
164
139
  text: string,
165
140
  title: string,
166
- color: string,
141
+ color: Style,
167
142
  lost: number,
168
143
  colors: ColorSupport,
169
144
  ): void {
@@ -173,7 +148,7 @@ function pushStreamSection(
173
148
  lines.push(droppedNote(lost, colors))
174
149
  }
175
150
  pushBodyLines(lines, text)
176
- if (leavesStyle(text)) lines.push(`${lines.pop()}${RESET}`)
151
+ if (leavesStyle(text)) lines.push(`${lines.pop()}${STYLE_END}`)
177
152
  lines.push('')
178
153
  }
179
154
 
@@ -193,7 +168,7 @@ function pushOutputSection(
193
168
  link?: (line: string) => string,
194
169
  ): void {
195
170
  if (output.every((c) => c.text.trim().length === 0)) return
196
- lines.push(sectionLine('OUTPUT', '', colors), '')
171
+ lines.push(sectionLine('OUTPUT', 'strong', colors), '')
197
172
  if (lost > 0) {
198
173
  lines.push(droppedNote(lost, colors))
199
174
  }
@@ -216,10 +191,10 @@ function pushOutputSection(
216
191
  for (const l of body) {
217
192
  const text = link === undefined ? l.text : link(l.text)
218
193
  if (!l.err || !colors.enabled) lines.push(text)
219
- else lines.push(l.text.includes('\x1b') ? text : paint(ERROR, text, colors))
194
+ else lines.push(hasEscape(l.text) ? text : paint('error', text, colors))
220
195
  }
221
- const styled = output.findLast((c) => c.text.includes('\x1b['))
222
- if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${RESET}`)
196
+ const styled = output.findLast((c) => hasEscape(c.text))
197
+ if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${STYLE_END}`)
223
198
  lines.push('')
224
199
  }
225
200
 
@@ -230,9 +205,9 @@ function pushOutputSection(
230
205
  * the frame width, and sections get vertical margins so content stands
231
206
  * clear of the furniture.
232
207
  */
233
- function sectionLine(title: string, color: string, colors: ColorSupport): string {
234
- const corner = (s: string) => paint('', s, colors, { dim: true })
235
- const label = paint(color, title, colors, { bold: true })
208
+ function sectionLine(title: string, color: Style, colors: ColorSupport): string {
209
+ const corner = (s: string) => paint('muted', s, colors)
210
+ const label = paint(color, title, colors, true)
236
211
  // As wide as the footer's rule, so the frames and the footer line up.
237
212
  const rule = corner('─'.repeat(Math.max(1, ruleWidth(colors) - 4 - title.length)))
238
213
  return `${corner('├─')} ${label} ${rule}`
@@ -260,13 +235,7 @@ export function paintIdParts(
260
235
  colors: ColorSupport,
261
236
  opts: { bold?: boolean; sep?: '#' | ':' } = {},
262
237
  ): string {
263
- const dim = (t: string) => paint('', t, colors, { dim: true })
264
- const { sep = '#', ...style } = opts
265
- return (
266
- paint(identityColor(hueSource), projectText, colors, style) +
267
- dim(sep) +
268
- paint(identityColor(taskText), taskText, colors, style)
269
- )
238
+ return idParts(hueSource, projectText, taskText, colors, opts)
270
239
  }
271
240
 
272
241
  function paintId(
@@ -294,19 +263,18 @@ const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run
294
263
  const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
295
264
  const BLOCKED_COL = 7 // `blocks`: how many tasks are downstream of the row's
296
265
  const USUAL_COL = 7 // "~18s" live, "+6.0s" / "−1m 05s" after
297
- const WAITED_COL = 7 // the time cell's width: "1.20s"
298
266
  const CPU_COL = 5 // "180%", "1200%"
299
267
  const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
300
268
  const OUT_COL = 7 // `outputs`: "12MB", "900B"
301
269
  const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
302
- /** The list header's colour: violet-400, the summary rule's first stop. */
303
270
 
304
271
  /** Right-align the duration in a TIME_COL cell (pad on the left). */
305
272
  function timeCell(ms: number | null, colors: ColorSupport): string {
306
273
  let raw = ms === null ? '' : formatElapsed(ms)
307
274
  // `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
308
275
  if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
309
- return ' '.repeat(Math.max(0, TIME_COL - raw.length)) + paint('', raw, colors, { dim: true })
276
+ // The time is the row's fact (value level); the cells around it are meta.
277
+ return ' '.repeat(Math.max(0, TIME_COL - raw.length)) + paint('value', raw, colors)
310
278
  }
311
279
 
312
280
  /**
@@ -314,13 +282,8 @@ function timeCell(ms: number | null, colors: ColorSupport): string {
314
282
  * An empty color string renders the text dim (used for the `miss` cache
315
283
  * word); empty text is a blank cell.
316
284
  */
317
- function cell(text: string, color: string, width: number, colors: ColorSupport): string {
318
- const painted =
319
- text === ''
320
- ? ''
321
- : color === ''
322
- ? paint('', text, colors, { dim: true })
323
- : paint(color, text, colors)
285
+ function cell(text: string, color: Style | '', width: number, colors: ColorSupport): string {
286
+ const painted = text === '' ? '' : paint(color === '' ? 'muted' : color, text, colors)
324
287
  return painted + ' '.repeat(Math.max(0, width - text.length))
325
288
  }
326
289
 
@@ -343,34 +306,34 @@ function glyphShape(o: TaskOutcome): string {
343
306
  }
344
307
 
345
308
  /** Task-axis word + color (a cache hit is a success). */
346
- function statusOf(o: TaskOutcome): { word: string; color: string } {
309
+ function statusOf(o: TaskOutcome): { word: string; color: Style } {
347
310
  switch (o.status) {
348
311
  case 'failed':
349
- return { word: 'failed', color: ERROR }
312
+ return { word: 'failed', color: 'error' }
350
313
  case 'skipped':
351
- return { word: 'skipped', color: WARN }
314
+ return { word: 'skipped', color: 'warn' }
352
315
  default:
353
- return { word: 'success', color: SUCCESS }
316
+ return { word: 'success', color: 'ok' }
354
317
  }
355
318
  }
356
319
 
357
320
  /** Cache-axis word + color. Empty for states that never reached the cache. */
358
- function cacheOf(o: TaskOutcome): { word: string; color: string } {
321
+ function cacheOf(o: TaskOutcome): { word: string; color: Style } {
359
322
  switch (o.status) {
360
323
  case 'success':
361
324
  case 'failed':
362
325
  // A task no cache answered for never consulted one: not a miss.
363
- return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: '' } // dim
326
+ return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: 'muted' } // dim
364
327
  case 'cache-hit':
365
328
  return o.restored === false
366
- ? { word: 'fresh', color: SUCCESS }
367
- : { word: 'local', color: LOCAL }
329
+ ? { word: 'fresh', color: 'ok' }
330
+ : { word: 'local', color: 'local' }
368
331
  case 'cache-hit-remote':
369
332
  return o.restored === false
370
- ? { word: 'fresh', color: SUCCESS }
371
- : { word: 'remote', color: REMOTE }
333
+ ? { word: 'fresh', color: 'ok' }
334
+ : { word: 'remote', color: 'remote' }
372
335
  default:
373
- return { word: '', color: '' }
336
+ return { word: '', color: 'muted' }
374
337
  }
375
338
  }
376
339
 
@@ -380,7 +343,7 @@ function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
380
343
  }
381
344
 
382
345
  /**
383
- * One reported task row on the list grid: `<glyph> <time> <usual> <waited>
346
+ * One reported task row on the list grid: `<glyph> <time> <usual>
384
347
  * <status> <cache> <cpu> <mem> <blocks> <worker> <name>`. `glyph` is
385
348
  * pre-painted; status/cache are raw text + color, padded to their
386
349
  * columns; `paintedId` carries identity coloring. `ms = null` blanks the
@@ -390,9 +353,9 @@ export function formatTaskRow(
390
353
  glyph: string,
391
354
  ms: number | null,
392
355
  status: string,
393
- statusColor: string,
356
+ statusColor: Style | '',
394
357
  cache: string,
395
- cacheColor: string,
358
+ cacheColor: Style | '',
396
359
  paintedId: string,
397
360
  colors: ColorSupport = NO_COLOR,
398
361
  cells: RowCells = {},
@@ -402,30 +365,39 @@ export function formatTaskRow(
402
365
  const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
403
366
  const usual = cells.usual ?? { text: '', painted: '' }
404
367
  const us = `${' '.repeat(Math.max(0, USUAL_COL - usual.text.length))}${usual.painted}`
405
- const waited = cells.waitedMs === undefined ? '' : formatElapsed(cells.waitedMs)
406
- const cpu = cells.cpu === undefined ? '' : `${Math.round(cells.cpu * 100)}%`
407
- const mem = cells.memBytes === undefined ? '' : formatSize(cells.memBytes)
368
+ const cpu =
369
+ cells.cpu === undefined ? '' : cells.cpu === null ? UNKNOWN : `${Math.round(cells.cpu * 100)}%`
370
+ const mem =
371
+ cells.memBytes === undefined
372
+ ? ''
373
+ : cells.memBytes === null
374
+ ? UNKNOWN
375
+ : formatSize(cells.memBytes)
408
376
  const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
409
377
  const blocks = cells.blocks ?? ''
410
378
  const worker = workerName(cells.worker ?? '')
411
379
  return (
412
- ` ${glyph} ${timeCell(ms, colors)} ${us} ${right(waited, WAITED_COL, colors)} ${st} ${ca} ` +
380
+ ` ${glyph} ${timeCell(ms, colors)} ${us} ${st} ${ca} ` +
413
381
  `${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
414
- `${' '.repeat(Math.max(0, BLOCKED_COL - blocks.length))}${paint(WARN, blocks, colors)} ` +
415
- `${worker === '' ? '' : ui.inline(ui.place(worker), colors)}${' '.repeat(WORKER_COL - worker.length)} ${paintedId}`
382
+ `${' '.repeat(Math.max(0, BLOCKED_COL - blocks.length))}${paint(blocks === '0' ? 'muted' : 'value', blocks, colors)} ` +
383
+ `${placeCell(worker, colors)}${' '.repeat(WORKER_COL - worker.length)} ${paintedId}`
416
384
  )
417
385
  }
418
386
 
419
- /** A row's cells past time, status and cache; each blank when absent. */
387
+ /**
388
+ * A cell vx cannot know: a peak no look saw, a task history never timed.
389
+ * Blank is a cell that does not apply (a hit ran nothing).
390
+ */
391
+ const UNKNOWN = '\u2014'
392
+
393
+ /** A row's cells past time, status and cache; each blank when absent, `—` when null. */
420
394
  export interface RowCells {
421
395
  /** After the time, pre-painted: `~18s` live, `+6.0s` / `−1.4s` after. */
422
396
  usual?: { text: string; painted: string }
423
- /** How long the task waited ready for a slot. */
424
- waitedMs?: number
425
397
  /** Cores in use (live) or on average (after): 1.8 prints `180%`. */
426
- cpu?: number
398
+ cpu?: number | null
427
399
  /** Resident memory now (live) or at its peak (after). */
428
- memBytes?: number
400
+ memBytes?: number | null
429
401
  /** The bytes of the task's outputs: what its save stored, or its hit restored. */
430
402
  outputBytes?: number
431
403
  /** How many tasks are downstream. */
@@ -435,7 +407,7 @@ export interface RowCells {
435
407
  }
436
408
 
437
409
  function right(text: string, width: number, colors: ColorSupport): string {
438
- return ' '.repeat(Math.max(0, width - text.length)) + paint('', text, colors, { dim: true })
410
+ return ' '.repeat(Math.max(0, width - text.length)) + paint('muted', text, colors)
439
411
  }
440
412
 
441
413
  /** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
@@ -449,19 +421,24 @@ export function formatSize(bytes: number): string {
449
421
  return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
450
422
  }
451
423
 
424
+ // A run names a handful of workers over thousands of rows: each painted
425
+ // once (escaping and painting per row was ~12 ms of a 9,603-task warm run).
426
+ const placeCells = new WeakMap<ColorSupport, Map<string, string>>()
427
+
428
+ function placeCell(worker: string, colors: ColorSupport): string {
429
+ if (worker === '') return ''
430
+ let byName = placeCells.get(colors)
431
+ if (byName === undefined) placeCells.set(colors, (byName = new Map()))
432
+ let painted = byName.get(worker)
433
+ if (painted === undefined) byName.set(worker, (painted = ui.inline(ui.place(worker), colors)))
434
+ return painted
435
+ }
436
+
452
437
  /** A worker's name in its 12 columns: a longer one ends in `…`. */
453
438
  function workerName(name: string): string {
454
439
  return name.length > WORKER_COL ? `${name.slice(0, WORKER_COL - 1)}\u2026` : name
455
440
  }
456
441
 
457
- /**
458
- * A wait worth a cell: under 50 ms the task started at once, the
459
- * scheduler's own tick and a sibling's spawn, not a full pool.
460
- */
461
- export function waited(ms: number | undefined): boolean {
462
- return ms !== undefined && ms >= 50
463
- }
464
-
465
442
  /** A task's average cores over its run: CPU time over wall time. */
466
443
  function coresOf(o: TaskOutcome): number | undefined {
467
444
  return o.cpuMs === undefined || o.durationMs <= 0 ? undefined : o.cpuMs / o.durationMs
@@ -469,19 +446,27 @@ function coresOf(o: TaskOutcome): number | undefined {
469
446
 
470
447
  /**
471
448
  * The results list's `±usual` cell: how far an executed task ran from its
472
- * usual time, `+6.0s` red or `−1.4s` green, only past a fifth of the usual
473
- * either way; blank with no usual time to compare.
449
+ * usual time, `+6.0s` red or `−1.4s` green past a fifth of the usual either
450
+ * way, dim within it; `—` (null) with no history for the task, blank
451
+ * (undefined) with no history to ask. A blank within a fifth read as no
452
+ * usual at all (2026-10-29).
474
453
  */
475
- function usualDelta(
454
+ export function usualCell(
476
455
  ms: number,
477
- usualMs: number | undefined,
456
+ usualMs: number | null | undefined,
478
457
  colors: ColorSupport = NO_COLOR,
479
458
  ): { text: string; painted: string } {
480
- if (usualMs === undefined || usualMs <= 0 || Math.abs(ms - usualMs) <= usualMs * 0.2)
481
- return { text: '', painted: '' }
459
+ if (usualMs === undefined) return { text: '', painted: '' }
460
+ if (usualMs === null || usualMs <= 0) return unknownUsual(colors)
482
461
  const d = ms - usualMs
483
- const text = `${d > 0 ? '+' : '\u2212'}${formatElapsed(Math.abs(d), true)}`
484
- return { text, painted: paint(d > 0 ? ERROR : SUCCESS, text, colors) }
462
+ const text = `${d >= 0 ? '+' : '\u2212'}${formatElapsed(Math.abs(d), true)}`
463
+ if (Math.abs(d) <= usualMs * 0.2) return { text, painted: paint('muted', text, colors) }
464
+ return { text, painted: paint(d > 0 ? 'error' : 'ok', text, colors) }
465
+ }
466
+
467
+ /** The `usual` cell of a task history has never timed. */
468
+ export function unknownUsual(colors: ColorSupport = NO_COLOR): { text: string; painted: string } {
469
+ return { text: UNKNOWN, painted: paint('muted', UNKNOWN, colors) }
485
470
  }
486
471
 
487
472
  /**
@@ -503,10 +488,137 @@ export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
503
488
 
504
489
  function header(usual: string, colors: ColorSupport): string {
505
490
  const head =
506
- ` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ${'waited'.padStart(WAITED_COL)} ` +
491
+ ` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ` +
507
492
  `${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${'cpu'.padStart(CPU_COL)} ` +
508
493
  `${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
509
- return paint('', head, colors, { dim: true, underline: true })
494
+ return paint('label', head, colors)
495
+ }
496
+
497
+ /**
498
+ * The task list on its grid, under its header: the run's results panel's
499
+ * body, and `vx last`'s, so a replay reads as the run did.
500
+ */
501
+ export function taskList(rows: readonly ((colors: ColorSupport) => string)[]): ui.Block {
502
+ return { lines: (c) => [formatListHeader(c), ...rows.map((row) => row(c))] }
503
+ }
504
+
505
+ /** What history keeps of a task's row: `vx last` draws it again from this. */
506
+ export interface RecordedRow {
507
+ project: string
508
+ task: string
509
+ status: string
510
+ durationMs: number
511
+ restored: boolean | null
512
+ cached: boolean | null
513
+ cpuMs: number | null
514
+ peakRssBytes: number | null
515
+ placement: string | null
516
+ outputBytes: number | null
517
+ blocks: number | null
518
+ }
519
+
520
+ /**
521
+ * A recorded task's row, as the run printed it; `note` (dim) after its id.
522
+ * `usualMs` is the task's usual time now, from history (`null`: never timed).
523
+ */
524
+ export function formatRecordedRow(
525
+ r: RecordedRow,
526
+ colors: ColorSupport = NO_COLOR,
527
+ note = '',
528
+ usualMs?: number | null,
529
+ ): string {
530
+ // The row functions read a node's names and whether it declared a cache.
531
+ const node = {
532
+ id: `${r.project}#${r.task}`,
533
+ projectName: r.project,
534
+ taskName: r.task,
535
+ config: r.cached === false ? {} : { cache: {} },
536
+ } as unknown as TaskNode
537
+ const o = {
538
+ node,
539
+ status: r.status,
540
+ durationMs: r.durationMs,
541
+ ...(r.restored !== null ? { restored: r.restored } : {}),
542
+ ...(r.cpuMs !== null ? { cpuMs: r.cpuMs } : {}),
543
+ ...(r.peakRssBytes !== null ? { peakRssBytes: r.peakRssBytes } : {}),
544
+ ...(r.placement !== null ? { where: r.placement } : {}),
545
+ ...(r.outputBytes !== null ? { outputBytes: r.outputBytes } : {}),
546
+ } as TaskOutcome
547
+ const blocks = r.blocks ?? undefined
548
+ const row =
549
+ o.status === 'skipped'
550
+ ? formatTaskSkippedLine(node, colors, undefined, blocks)
551
+ : o.status === 'aborted'
552
+ ? formatTaskAbortedLine(node, o, colors, blocks)
553
+ : isCacheHit(o.status)
554
+ ? formatTaskHitLine(node, o, colors, false, blocks)
555
+ : formatTaskExecutedLine(node, o, colors, false, blocks, usualMs)
556
+ return note === '' ? row : `${row} ${paint('muted', note, colors)}`
557
+ }
558
+
559
+ /**
560
+ * A running task's usual duration, fixed while the elapsed time counts up
561
+ * toward it (2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
562
+ * column below 100 minutes.
563
+ */
564
+ export function formatExpected(ms: number): string {
565
+ // The tier by the rounded value: 999.6 ms printed `~1000ms` (item 1034's class).
566
+ if (Math.round(ms) < 1000) return `~${Math.round(ms)}ms`
567
+ if (ms < 9950) return `~${(ms / 1000).toFixed(1)}s`
568
+ const s = Math.round(ms / 1000)
569
+ if (s < 60) return `~${s}s`
570
+ return `~${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
571
+ }
572
+
573
+ /** A task `--dry` lists: what the run would do with it, on the running list's grid. */
574
+ export interface PlannedRow {
575
+ node: TaskNode
576
+ glyph: string
577
+ status: string
578
+ /** The glyph's and the status word's hue. */
579
+ tone: Style
580
+ cache: string
581
+ cacheTone: Style | ''
582
+ /** Its usual time, from history. */
583
+ usualMs?: number
584
+ /** What a remote hit would download. */
585
+ outputBytes?: number
586
+ /** How many tasks are downstream of it. */
587
+ blocks?: number
588
+ worker: string
589
+ /** Dim, after the id: its description, why it is affected. */
590
+ note: string
591
+ }
592
+
593
+ /** `--dry`'s list: the running list's header and grid, so a plan reads as the run would. */
594
+ export function planList(rows: readonly PlannedRow[]): ui.Block {
595
+ return {
596
+ lines: (c) => [
597
+ formatRunningHeader(c),
598
+ ...rows.map((r) => {
599
+ const usual = r.usualMs === undefined ? undefined : formatExpected(r.usualMs)
600
+ const row = formatTaskRow(
601
+ paint(r.tone, r.glyph, c),
602
+ null,
603
+ r.status,
604
+ r.tone,
605
+ r.cache,
606
+ r.cacheTone,
607
+ paintTaskId(r.node, c),
608
+ c,
609
+ {
610
+ ...(usual !== undefined
611
+ ? { usual: { text: usual, painted: paint('muted', usual, c) } }
612
+ : {}),
613
+ ...(r.outputBytes !== undefined ? { outputBytes: r.outputBytes } : {}),
614
+ ...(r.blocks !== undefined ? { blocks: String(r.blocks) } : {}),
615
+ worker: r.worker,
616
+ },
617
+ )
618
+ return r.note === '' ? row : `${row} ${paint('muted', r.note, c)}`
619
+ }),
620
+ ],
621
+ }
510
622
  }
511
623
 
512
624
  /** Compact one-liner for a cache hit with nothing to replay. */
@@ -532,8 +644,8 @@ export function formatTaskHashLine(
532
644
  o: TaskOutcome,
533
645
  colors: ColorSupport = NO_COLOR,
534
646
  ): string {
535
- const color = o.status === 'aborted' ? ABORTED : statusOf(o).color
536
- const key = o.hash === undefined ? '' : ` ${paint('', o.hash, colors, { dim: true })}`
647
+ const color = o.status === 'aborted' ? 'aborted' : statusOf(o).color
648
+ const key = o.hash === undefined ? '' : ` ${paint('muted', o.hash, colors)}`
537
649
  return `${paint(color, outcomeWord(o), colors)} ${paintTaskId(node, colors)}${key}`
538
650
  }
539
651
 
@@ -544,21 +656,32 @@ export function formatTaskExecutedLine(
544
656
  colors: ColorSupport = NO_COLOR,
545
657
  onPath = false,
546
658
  blocks?: number,
547
- usualMs?: number,
659
+ usualMs?: number | null,
548
660
  ): string {
549
661
  return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
550
662
  ...executedCells(o, blocks),
551
- usual: usualDelta(o.durationMs, usualMs, colors),
663
+ // A hit's time is a restore's, not the task's: nothing to compare.
664
+ ...(o.status === 'success' || o.status === 'failed'
665
+ ? { usual: usualCell(o.durationMs, usualMs, colors) }
666
+ : {}),
552
667
  })
553
668
  }
554
669
 
555
- /** An executed task's cells: where and how long it waited, its average cores and peak memory. */
670
+ /**
671
+ * An executed task's cells: where it ran, its average cores and peak
672
+ * memory; a task that ran with either unread shows `—`, a hit nothing.
673
+ */
556
674
  export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
557
675
  const cores = coresOf(o)
676
+ const ran = o.status === 'success' || o.status === 'failed'
558
677
  return {
559
678
  ...ranCells(o, blocks),
560
- ...(cores !== undefined ? { cpu: cores } : {}),
561
- ...(o.peakRssBytes !== undefined ? { memBytes: o.peakRssBytes } : {}),
679
+ ...(cores !== undefined ? { cpu: cores } : ran ? { cpu: null } : {}),
680
+ ...(o.peakRssBytes !== undefined
681
+ ? { memBytes: o.peakRssBytes }
682
+ : ran
683
+ ? { memBytes: null }
684
+ : {}),
562
685
  }
563
686
  }
564
687
 
@@ -568,7 +691,6 @@ function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
568
691
  // A skip reached no machine.
569
692
  ...(o.status === 'skipped' ? {} : { worker: o.where ?? 'local' }),
570
693
  ...(blocks !== undefined ? { blocks: String(blocks) } : {}),
571
- ...(waited(o.queuedMs) ? { waitedMs: o.queuedMs! } : {}),
572
694
  ...(o.outputBytes !== undefined ? { outputBytes: o.outputBytes } : {}),
573
695
  }
574
696
  }
@@ -603,7 +725,8 @@ function formatOutcomeRow(
603
725
 
604
726
  /** `critical path`: the row's task held the run's end while others ran beside it. */
605
727
  export function pathNote(colors: ColorSupport = NO_COLOR): string {
606
- return ` ${paint(ERROR, 'critical path', colors)}`
728
+ // A fact worth knowing, not a failure: never red.
729
+ return ` ${paint('info', 'critical path', colors)}`
607
730
  }
608
731
 
609
732
  /** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
@@ -616,7 +739,7 @@ function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
616
739
  * on the row, as a worker's name has no width to pad to (2026-10-21).
617
740
  */
618
741
  function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
619
- return `${paint('', ' \u00b7', colors, { dim: true })} ${ui.inline(ui.place(where), colors)}`
742
+ return `${paint('muted', ' \u00b7', colors)} ${ui.inline(ui.place(where), colors)}`
620
743
  }
621
744
 
622
745
  /**
@@ -634,7 +757,7 @@ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
634
757
  }
635
758
  if ((o.attempts ?? 1) > 1) said.push(`${o.attempts} attempts`)
636
759
  const text = said.length === 0 ? 'flaky' : `flaky - ${said.join(' \u00b7 ')}`
637
- return ` ${paint('', text, colors, { dim: true })}`
760
+ return ` ${paint('muted', text, colors)}`
638
761
  }
639
762
 
640
763
  /**
@@ -649,13 +772,13 @@ export function formatTaskSkippedLine(
649
772
  blocks?: number,
650
773
  ): string {
651
774
  const why = skippedReason(skip)
652
- const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors, { dim: true })}`
775
+ const suffix = why === '' ? '' : ` ${paint('muted', `• ${why}`, colors)}`
653
776
  return (
654
777
  formatTaskRow(
655
- paint(WARN, '\u2298', colors),
778
+ paint('warn', '\u2298', colors),
656
779
  null,
657
780
  'skipped',
658
- WARN,
781
+ 'warn',
659
782
  '',
660
783
  '',
661
784
  paintTaskId(node, colors),
@@ -680,7 +803,7 @@ export function formatTaskAbortedLine(
680
803
  const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
681
804
  if (neverStarted(o)) {
682
805
  return formatTaskRow(
683
- paint('', '\u25cc', colors, { dim: true }),
806
+ paint('muted', '\u25cc', colors),
684
807
  null,
685
808
  'not run',
686
809
  '',
@@ -692,10 +815,10 @@ export function formatTaskAbortedLine(
692
815
  )
693
816
  }
694
817
  return formatTaskRow(
695
- paint(ABORTED, '\u2717', colors),
818
+ paint('aborted', '\u2717', colors),
696
819
  o.durationMs,
697
820
  'aborted',
698
- ABORTED,
821
+ 'aborted',
699
822
  '',
700
823
  '',
701
824
  id,
@@ -716,7 +839,7 @@ export function formatFrameOpen(
716
839
  colors: ColorSupport = NO_COLOR,
717
840
  forwardArgs: readonly string[] = [],
718
841
  ): string {
719
- const corner = (t: string) => paint('', t, colors, { dim: true })
842
+ const corner = (t: string) => paint('muted', t, colors)
720
843
  const cmd = shownCommand(node, forwardArgs)
721
844
  const mark = persistentMark(node, colors)
722
845
  return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
@@ -748,7 +871,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
748
871
  // glob matches and the user decides.
749
872
  const fix = outcome.sandboxFix
750
873
  const line = fix === undefined ? undefined : sandboxFixLine(fix)
751
- const dim = (text: string): string => paint('', text, colors, { dim: true })
874
+ const dim = (text: string): string => paint('muted', text, colors)
752
875
  const note =
753
876
  fix === undefined
754
877
  ? []
@@ -773,7 +896,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
773
896
  // vx's own notes (`vx: …`) explain a failure; they are not denials.
774
897
  const denials = body.filter((l) => !l.startsWith('vx: ')).length
775
898
  return [
776
- sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX', ERROR, colors),
899
+ sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX', 'error', colors),
777
900
  '',
778
901
  ...body,
779
902
  '',
@@ -786,7 +909,7 @@ export function formatFrameClose(
786
909
  outcome: TaskOutcome,
787
910
  colors: ColorSupport = NO_COLOR,
788
911
  ): string {
789
- const corner = (t: string) => paint('', t, colors, { dim: true })
912
+ const corner = (t: string) => paint('muted', t, colors)
790
913
  const mark = persistentMark(node, colors)
791
914
  // A persistent task's child keeps running past this "ready" outcome,
792
915
  // so the close reads `running` (accent), not `success` — the ▸ marks
@@ -810,11 +933,11 @@ function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
810
933
  }
811
934
 
812
935
  function persistentMark(node: TaskNode, colors: ColorSupport): string {
813
- return node.config.exec?.persistent !== undefined ? `${paint(ACCENT, '▸', colors)} ` : ''
936
+ return node.config.exec?.persistent !== undefined ? `${paint('info', '▸', colors)} ` : ''
814
937
  }
815
938
 
816
939
  function runningFooter(o: TaskOutcome, colors: ColorSupport): string {
817
- return ` ${paint('', `(${formatElapsed(o.durationMs)})`, colors, { dim: true })} ${paint(ACCENT, 'running', colors)}`
940
+ return ` ${paint('muted', `(${formatElapsed(o.durationMs)})`, colors)} ${paint('info', 'running', colors)}`
818
941
  }
819
942
 
820
943
  /**
@@ -841,8 +964,8 @@ export function formatPersistentTailBlock(
841
964
  const stderr = body.stderr ?? ''
842
965
  if (stdout.trim().length === 0 && stderr.trim().length === 0) return ''
843
966
  const lines: string[] = [formatFrameOpen(node, colors, forwardArgs)]
844
- pushStreamSection(lines, stdout, 'STDOUT (since ready)', SUCCESS, dropped.stdout ?? 0, colors)
845
- pushStreamSection(lines, stderr, 'STDERR (since ready)', ERROR, dropped.stderr ?? 0, colors)
967
+ pushStreamSection(lines, stdout, 'STDOUT (since ready)', 'ok', dropped.stdout ?? 0, colors)
968
+ pushStreamSection(lines, stderr, 'STDERR (since ready)', 'error', dropped.stderr ?? 0, colors)
846
969
  lines.push(formatFrameClose(node, outcome, colors))
847
970
  return lines.join('\n') + '\n'
848
971
  }
@@ -853,40 +976,16 @@ export function formatPersistentTailBlock(
853
976
  * logs read apart. `text` holds whole lines, its last newline cut off.
854
977
  */
855
978
  export function formatKeptLines(node: TaskNode, text: string, colors: ColorSupport): string {
856
- const prefix = `${paintTaskId(node, colors)} ${paint('', '│', colors, { dim: true })} `
979
+ const prefix = `${paintTaskId(node, colors)} ${paint('muted', '│', colors)} `
857
980
  return text
858
981
  .split('\n')
859
982
  .map((line) => {
860
983
  const shown = lastRedraw(line)
861
- return `${prefix}${shown}${leavesStyle(shown) ? RESET : ''}\n`
984
+ return `${prefix}${shown}${leavesStyle(shown) ? STYLE_END : ''}\n`
862
985
  })
863
986
  .join('')
864
987
  }
865
988
 
866
- // eslint-disable-next-line no-control-regex -- ESC is the point
867
- const LINE_RESET = /\r|\x1b\[[12]K|\x1b\[[01]?G/g
868
- // eslint-disable-next-line no-control-regex -- ESC is the point
869
- const TRAILING_RESETS = /(?:\r|\x1b\[[12]K|\x1b\[[01]?G)+$/
870
- // eslint-disable-next-line no-control-regex -- ESC is the point
871
- const SGR = /\x1b\[[0-9;]*m/g
872
-
873
- /**
874
- * What a line shows once its redraws have played: the text after its last
875
- * `\r`, line erase (`CSI 1K`/`2K`) or move to column 1 (`CSI G`), with the
876
- * colours set before it. Written after an id prefix, a dev server's redraw
877
- * (`\x1b[2K\x1b[1Gbuilt in 30ms`, a `10%\r100%` progress line) erased or
878
- * overwrote the prefix on a terminal. A reset that ends the line, as in
879
- * CRLF, redraws nothing.
880
- */
881
- function lastRedraw(line: string): string {
882
- if (!line.includes('\r') && !line.includes('\x1b')) return line
883
- const text = line.replace(TRAILING_RESETS, '')
884
- let cut = -1
885
- for (const m of text.matchAll(LINE_RESET)) cut = m.index + m[0].length
886
- if (cut === -1) return text
887
- return (text.slice(0, cut).match(SGR) ?? []).join('') + text.slice(cut)
888
- }
889
-
890
989
  /**
891
990
  * Between the requested task's frame and the summary footer, list the
892
991
  * persistent tasks (dev servers / watchers) the run is keeping alive in
@@ -900,28 +999,28 @@ export function formatPersistentList(
900
999
  ): string[] {
901
1000
  return nodes.map(
902
1001
  (n) =>
903
- ` ${paint(ACCENT, '▸', colors)} ${paintTaskId(n, colors)} ${paint('', 'running', colors, { dim: true })}`,
1002
+ ` ${paint('info', '▸', colors)} ${paintTaskId(n, colors)} ${paint('muted', 'running', colors)}`,
904
1003
  )
905
1004
  }
906
1005
 
907
1006
  function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
908
1007
  const shortHash = o.hash ? o.hash.slice(0, 8) : ''
909
- const dim = (s: string) => paint('', s, colors, { dim: true })
1008
+ const dim = (s: string) => paint('muted', s, colors)
910
1009
  const word = outcomeLabel(o)
911
1010
  switch (o.status) {
912
1011
  case 'cache-hit':
913
- return `${paint(SUCCESS, word, colors)} ${dim(`• ${shortHash}`)}`
1012
+ return `${paint('ok', word, colors)} ${dim(`• ${shortHash}`)}`
914
1013
  case 'cache-hit-remote':
915
1014
  // A remote restore is the one hit painted accent: bytes came off the wire.
916
- return `${paint(o.restored === false ? SUCCESS : ACCENT, word, colors)} ${dim(`• ${shortHash}`)}`
1015
+ return `${paint(o.restored === false ? 'ok' : 'info', word, colors)} ${dim(`• ${shortHash}`)}`
917
1016
  case 'failed':
918
1017
  // The command lives in its own `├─ command` section; the header
919
1018
  // carries the outcome like every other status.
920
- return paint(ERROR, word, colors, { bold: true })
1019
+ return paint('error', word, colors, true)
921
1020
  case 'skipped':
922
- return paint(WARN, word, colors)
1021
+ return paint('warn', word, colors)
923
1022
  case 'aborted':
924
- return paint(ABORTED, word, colors)
1023
+ return paint('aborted', word, colors)
925
1024
  default:
926
1025
  return dim(word)
927
1026
  }
@@ -935,7 +1034,7 @@ function formatBlockFooter(o: TaskOutcome, colors: ColorSupport): string {
935
1034
  // saved" on the strength of it. The stored time lives on
936
1035
  // `TaskOutcome.storedDurationMs`.) Status differs by outcome — see
937
1036
  // formatStatusTag.
938
- const dur = paint('', `(${formatElapsed(o.durationMs)})`, colors, { dim: true })
1037
+ const dur = paint('muted', `(${formatElapsed(o.durationMs)})`, colors)
939
1038
  const tag = formatStatusTag(o, colors)
940
1039
  return ` ${dur} ${tag}${flakyNote(o, colors)}`
941
1040
  }
@@ -944,13 +1043,13 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
944
1043
  const word = outcomeLabel(o)
945
1044
  switch (o.status) {
946
1045
  case 'failed':
947
- return paint(ERROR, word, colors, { bold: true })
1046
+ return paint('error', word, colors, true)
948
1047
  case 'skipped':
949
- return paint(WARN, word, colors)
1048
+ return paint('warn', word, colors)
950
1049
  case 'aborted':
951
- return paint(ABORTED, word, colors)
1050
+ return paint('aborted', word, colors)
952
1051
  default:
953
- return paint('', word, colors, { dim: true })
1052
+ return paint('muted', word, colors)
954
1053
  }
955
1054
  }
956
1055