@vzn/vx 0.0.645 → 0.0.648

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 (186) hide show
  1. package/README.md +39 -50
  2. package/launcher.cjs +9 -2
  3. package/launcher.sh +2 -2
  4. package/package.json +5 -5
  5. package/schemas/cache.json +5 -5
  6. package/schemas/error.json +1 -1
  7. package/schemas/info.json +16 -2
  8. package/schemas/last.json +127 -5
  9. package/schemas/plan.json +28 -2
  10. package/schemas/show.json +10 -1
  11. package/skills/vx/SKILL.md +19 -3
  12. package/src/bin.ts +54 -13
  13. package/src/cache/archive.ts +324 -23
  14. package/src/cache/cache.ts +330 -65
  15. package/src/cache/git-inputs.ts +274 -97
  16. package/src/cache/index.ts +7 -2
  17. package/src/cache/inputs.ts +515 -83
  18. package/src/cache/key-fold.ts +15 -6
  19. package/src/cache/layer.ts +74 -11
  20. package/src/cache/layered-cache.ts +154 -59
  21. package/src/cache/linked-inputs.ts +1 -1
  22. package/src/cache/output-index.ts +44 -14
  23. package/src/cache/plugin-cache.ts +49 -23
  24. package/src/cache/remote-hits.ts +72 -0
  25. package/src/cache/remote-lane.ts +100 -15
  26. package/src/cache/run-history.ts +77 -23
  27. package/src/cache/run-store.ts +11 -10
  28. package/src/cache/schema.ts +42 -6
  29. package/src/cache/tar-stream.ts +5 -0
  30. package/src/cache/umask.ts +1 -2
  31. package/src/cli/cache.ts +58 -27
  32. package/src/cli/completions.ts +12 -5
  33. package/src/cli/core-alias.ts +5 -2
  34. package/src/cli/docs.ts +42 -16
  35. package/src/cli/help.ts +62 -25
  36. package/src/cli/index.ts +79 -45
  37. package/src/cli/info.ts +12 -6
  38. package/src/cli/init.ts +222 -34
  39. package/src/cli/last.ts +443 -47
  40. package/src/cli/lock.ts +197 -52
  41. package/src/cli/plan-format.ts +91 -49
  42. package/src/cli/plugin-commands.ts +160 -11
  43. package/src/cli/plugin-templates.ts +13 -13
  44. package/src/cli/registry.ts +209 -0
  45. package/src/cli/run.ts +111 -41
  46. package/src/cli/select.ts +148 -49
  47. package/src/cli/show.ts +59 -16
  48. package/src/cli/task-verb.ts +16 -6
  49. package/src/cli/upgrade.ts +155 -54
  50. package/src/cli/watch-cycle.ts +10 -5
  51. package/src/cli/watch-filter.ts +71 -3
  52. package/src/cli/watch-fs.ts +22 -6
  53. package/src/cli/watch-judge.ts +48 -6
  54. package/src/cli/watch.ts +163 -42
  55. package/src/cli/why.ts +211 -73
  56. package/src/cli/workspace-config.ts +54 -15
  57. package/src/config.ts +13 -1
  58. package/src/exec/env.ts +7 -2
  59. package/src/exec/executor.ts +262 -15
  60. package/src/exec/index.ts +8 -2
  61. package/src/exec/kill-tree.ts +15 -0
  62. package/src/exec/local-executor.ts +1 -1
  63. package/src/exec/proc-sample.ts +70 -6
  64. package/src/exec/runner.ts +256 -53
  65. package/src/exec/sandbox-binds.ts +7 -3
  66. package/src/exec/sandbox-paths.ts +1 -1
  67. package/src/exec/sandbox-runtime.ts +222 -28
  68. package/src/exec/sandbox-violations.ts +52 -20
  69. package/src/graph/critical-path.ts +79 -0
  70. package/src/graph/default-build.ts +19 -0
  71. package/src/graph/definition.ts +38 -0
  72. package/src/graph/folds.ts +38 -5
  73. package/src/graph/index.ts +4 -1
  74. package/src/graph/priorities.ts +74 -8
  75. package/src/graph/scheduler.ts +326 -65
  76. package/src/graph/task-graph.ts +228 -35
  77. package/src/handoff.ts +97 -9
  78. package/src/history/failure-mode.ts +6 -3
  79. package/src/history/history.ts +268 -50
  80. package/src/history/index.ts +2 -0
  81. package/src/history/metrics.ts +142 -45
  82. package/src/index.ts +42 -10
  83. package/src/orchestrator/admission.ts +10 -6
  84. package/src/orchestrator/affected-tasks.ts +11 -6
  85. package/src/orchestrator/deferred-outputs.ts +2 -0
  86. package/src/orchestrator/doctor.ts +134 -42
  87. package/src/orchestrator/execute-args.ts +6 -0
  88. package/src/orchestrator/execute-task.ts +155 -42
  89. package/src/orchestrator/fingerprint-watch.ts +1 -1
  90. package/src/orchestrator/graph-memo.ts +140 -0
  91. package/src/orchestrator/graph-stage.ts +18 -1
  92. package/src/orchestrator/held-servers.ts +86 -0
  93. package/src/orchestrator/hit-restore.ts +227 -122
  94. package/src/orchestrator/index.ts +20 -3
  95. package/src/orchestrator/local-floor.ts +198 -0
  96. package/src/orchestrator/local-shortcircuit.ts +51 -8
  97. package/src/orchestrator/lockfile-claim.ts +96 -16
  98. package/src/orchestrator/miss-save.ts +52 -12
  99. package/src/orchestrator/options.ts +8 -6
  100. package/src/orchestrator/output-host.ts +43 -14
  101. package/src/orchestrator/output-log.ts +90 -10
  102. package/src/orchestrator/persistent-task.ts +97 -28
  103. package/src/orchestrator/persistent.ts +16 -4
  104. package/src/orchestrator/placement.ts +257 -14
  105. package/src/orchestrator/plan.ts +64 -7
  106. package/src/orchestrator/plugin-host.ts +230 -40
  107. package/src/orchestrator/plugin.ts +150 -37
  108. package/src/orchestrator/prepare.ts +178 -32
  109. package/src/orchestrator/projects.ts +65 -40
  110. package/src/orchestrator/remote-prefetch.ts +119 -25
  111. package/src/orchestrator/run-artifacts.ts +95 -32
  112. package/src/orchestrator/run-context.ts +39 -41
  113. package/src/orchestrator/run-failures.ts +65 -23
  114. package/src/orchestrator/run-id.ts +6 -3
  115. package/src/orchestrator/run-keys.ts +300 -0
  116. package/src/orchestrator/run-live.ts +294 -0
  117. package/src/orchestrator/run-lock.ts +13 -7
  118. package/src/orchestrator/run-records.ts +35 -23
  119. package/src/orchestrator/run-report.ts +3 -3
  120. package/src/orchestrator/run-steps.ts +58 -22
  121. package/src/orchestrator/run-stop.ts +22 -5
  122. package/src/orchestrator/run.ts +409 -79
  123. package/src/orchestrator/sandbox-fix.ts +7 -5
  124. package/src/orchestrator/sandbox-request.ts +78 -22
  125. package/src/orchestrator/shell-verdict.ts +5 -5
  126. package/src/orchestrator/signals.ts +26 -1
  127. package/src/orchestrator/stable-keys.ts +92 -8
  128. package/src/orchestrator/steps.ts +2 -2
  129. package/src/orchestrator/task-artifacts.ts +206 -0
  130. package/src/orchestrator/task-hash.ts +89 -14
  131. package/src/orchestrator/task-log-buffer.ts +72 -36
  132. package/src/orchestrator/task-process.ts +22 -0
  133. package/src/orchestrator/telemetry-host.ts +24 -3
  134. package/src/orchestrator/telemetry.ts +113 -38
  135. package/src/orchestrator/upstream.ts +15 -0
  136. package/src/report/events.ts +32 -13
  137. package/src/report/forecast.ts +36 -35
  138. package/src/report/framed-output.ts +173 -46
  139. package/src/report/index.ts +1 -0
  140. package/src/report/logger.ts +217 -186
  141. package/src/report/path-links.ts +12 -4
  142. package/src/report/plain-output.ts +13 -5
  143. package/src/report/status-line.ts +94 -46
  144. package/src/report/summary.ts +174 -38
  145. package/src/report/terminal-signals.ts +13 -2
  146. package/src/settings.ts +14 -6
  147. package/src/util/bun-version.ts +4 -1
  148. package/src/util/colors.ts +20 -3
  149. package/src/util/config-frame.ts +41 -4
  150. package/src/{graph → util}/dependency-spec.ts +22 -13
  151. package/src/util/edit-distance.ts +13 -1
  152. package/src/util/env-flag.ts +9 -0
  153. package/src/util/errors.ts +59 -3
  154. package/src/util/essential-env.ts +57 -0
  155. package/src/util/git-ignored.ts +1 -1
  156. package/src/util/hash.ts +35 -0
  157. package/src/util/hide-env.ts +45 -5
  158. package/src/util/index.ts +22 -5
  159. package/src/util/num.ts +8 -0
  160. package/src/util/paths.ts +121 -21
  161. package/src/util/report.ts +6 -1
  162. package/src/util/secret-mask.ts +62 -1
  163. package/src/util/secrets.ts +7 -2
  164. package/src/util/size.ts +11 -3
  165. package/src/util/tail.ts +6 -1
  166. package/src/util/timing.ts +13 -1
  167. package/src/util/ui.ts +65 -4
  168. package/src/util/which.ts +1 -1
  169. package/src/util/write-file.ts +67 -0
  170. package/src/workspace/affected.ts +710 -107
  171. package/src/workspace/bracket-literal.ts +84 -0
  172. package/src/workspace/canonical-dir.ts +37 -0
  173. package/src/workspace/config-bytecode.ts +282 -0
  174. package/src/workspace/config-eval.ts +29 -8
  175. package/src/workspace/config-imports.ts +199 -58
  176. package/src/workspace/config-schema.ts +242 -39
  177. package/src/workspace/filter.ts +37 -8
  178. package/src/workspace/index.ts +11 -2
  179. package/src/workspace/json-data.ts +3 -4
  180. package/src/workspace/lockfile.ts +57 -8
  181. package/src/workspace/migration.ts +72 -49
  182. package/src/workspace/package-graph.ts +5 -4
  183. package/src/workspace/project-loader.ts +314 -47
  184. package/src/workspace/repo-id.ts +1 -1
  185. package/src/workspace/workspace.ts +176 -33
  186. package/src/cli/foreign-flags.ts +0 -727
@@ -6,12 +6,19 @@
6
6
 
7
7
  import { statSync } from 'node:fs'
8
8
  import path from 'node:path'
9
+ import { pathToFileURL } from 'node:url'
9
10
 
10
11
  // `src/a.ts`, `./a.ts:12`, `/abs/a.ts:12:5`, tsc's `src/a.ts(12,5)`. Not
11
12
  // after a word, dot, slash or `@`, so a URL's path or a scoped name's tail
12
- // never starts a match. Groups: file, then line and column of either form.
13
- const PATH =
14
- /(?<![\w./@+-])((?:\/|\.{1,2}\/)?(?:[\w@.+-]+\/)*[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,9})(?::(\d+)(?::(\d+))?|\((\d+),(\d+)\))?/g
13
+ // never starts a match. Colour codes (`\x1b[4m`) are invisible: a path one
14
+ // starts is linked (tasks run with FORCE_COLOR), one it splits from its
15
+ // directory is not. Groups: file, then line and column of either form.
16
+ const SGR = String.raw`\x1b\[[\d;]*m`
17
+ const PATH = new RegExp(
18
+ String.raw`(?<!\x1b\[[\d;]*)(?:(?<![\w./@+-])|(?<=${SGR}))(?<![\w./@+-](?<!${SGR})(?:${SGR})+)` +
19
+ String.raw`((?:\/|\.{1,2}\/)?(?:[\w@.+-]+\/)*[\w@+-][\w@.+-]*\.[A-Za-z][A-Za-z0-9]{0,9})(?::(\d+)(?::(\d+))?|\((\d+),(\d+)\))?`,
20
+ 'g',
21
+ )
15
22
 
16
23
  /** A file a task's output names; `file` absolute. */
17
24
  export interface OutputLocation {
@@ -29,7 +36,8 @@ export function linkPaths(text: string, dir: string, isFile: (abs: string) => bo
29
36
  return text.replace(PATH, (match: string, file: string) => {
30
37
  const abs = path.resolve(dir, file)
31
38
  if (!isFile(abs)) return match
32
- return `\x1b]8;;file://${encodeURI(abs)}\x1b\\${match}\x1b]8;;\x1b\\`
39
+ // Not `encodeURI`: it keeps a `#` or `?`, which the URL reads as its fragment or query.
40
+ return `\x1b]8;;${pathToFileURL(abs).href}\x1b\\${match}\x1b]8;;\x1b\\`
33
41
  })
34
42
  }
35
43
 
@@ -1,11 +1,12 @@
1
1
  import type { TaskNode } from '../graph/index.js'
2
2
  import type { Logger } from './logger.js'
3
3
 
4
- // A prefix that may yet complete as a CSI or OSC sequence. An escape cut
5
- // at a chunk's end is held for the next chunk: stripped alone, its
6
- // remainder (`1m`) reached the log as text.
4
+ // A prefix that may yet complete as a CSI, OSC or charset sequence
5
+ // (`tput sgr0` writes `\x1b(B`). An escape cut at a chunk's end is held for
6
+ // the next chunk: stripped alone, its remainder (`1m`, `B`) reached the log
7
+ // as text.
7
8
  // eslint-disable-next-line no-control-regex -- ESC is the point
8
- const PARTIAL_ESCAPE = /^\x1b(?:\[[0-?]*[ -/]*|\][^\x07\x1b]*\x1b?)?$/
9
+ const PARTIAL_ESCAPE = /^\x1b(?:\[[0-?]*[ -/]*|\][^\x07\x1b]*\x1b?|[ -/]+)?$/
9
10
 
10
11
  /**
11
12
  * `sink` with terminal escapes stripped from task output. Tasks run with
@@ -34,12 +35,19 @@ export function plainOutput(sink: Logger): Logger {
34
35
  if (text !== '') write(node, text)
35
36
  }
36
37
  // Delegated one by one, never spread: an embedder's logger may be a class
37
- // whose methods live on its prototype and read `this`.
38
+ // whose methods live on its prototype and read `this`. Every hook the sink
39
+ // has: a dropped `warn` put warnings on stdout, a dropped `runDone` left
40
+ // the live region under the footer.
38
41
  return {
39
42
  status: (line) => sink.status(line),
43
+ ...(sink.warn && { warn: (line) => sink.warn!(line) }),
40
44
  ...(sink.runStart && { runStart: (info) => sink.runStart!(info) }),
41
45
  ...(sink.taskStart && { taskStart: (node) => sink.taskStart!(node) }),
42
46
  ...(sink.runEnd && { runEnd: () => sink.runEnd!() }),
47
+ ...(sink.stepStart && { stepStart: (id) => sink.stepStart!(id) }),
48
+ ...(sink.stepEnd && { stepEnd: (id, info) => sink.stepEnd!(id, info) }),
49
+ ...(sink.runStop && { runStop: () => sink.runStop!() }),
50
+ ...(sink.runDone && { runDone: () => sink.runDone!() }),
43
51
  taskStdout(node, chunk) {
44
52
  const text = strip(`${node.id}\0out`, chunk)
45
53
  if (text !== '' || chunk === '') sink.taskStdout(node, text)
@@ -6,7 +6,13 @@
6
6
  // then redraws, so the display can never interleave with task output.
7
7
 
8
8
  import { dumbTerminal, paint, type ColorSupport } from '../util/index.js'
9
- import { formatTaskRow, paintIdParts, TIME_COL } from './framed-output.js'
9
+ import {
10
+ formatListHeader,
11
+ formatTaskRow,
12
+ paintIdParts,
13
+ pathNote,
14
+ placeNote,
15
+ } from './framed-output.js'
10
16
  import type { BlockedTask } from './forecast.js'
11
17
 
12
18
  const NO_COLOR: ColorSupport = { enabled: false }
@@ -32,6 +38,9 @@ export interface StatusStream {
32
38
  rows?: number
33
39
  }
34
40
 
41
+ /** A region's lines, or what builds them when it is drawn. */
42
+ export type Region = readonly string[] | (() => readonly string[])
43
+
35
44
  export interface OutputWriter {
36
45
  /** True only when the stream is a TTY and the run isn't CI. */
37
46
  readonly enabled: boolean
@@ -49,9 +58,11 @@ export interface OutputWriter {
49
58
  setStatus(line: string, opts?: { force?: boolean }): void
50
59
  /**
51
60
  * Replace the multi-line status region. Same throttle contract as
52
- * `setStatus`; the two share one display slot.
61
+ * `setStatus`; the two share one display slot. A function is called
62
+ * when the region is next drawn, once: a set the throttle coalesces
63
+ * builds nothing.
53
64
  */
54
- setRegion(lines: readonly string[], opts?: { force?: boolean }): void
65
+ setRegion(lines: Region, opts?: { force?: boolean }): void
55
66
  /** Hand over what `coalesce` holds and write straight through from now on. */
56
67
  settle(): void
57
68
  /** Permanently remove the status display; later set* is a no-op. */
@@ -107,7 +118,7 @@ export function createOutputWriter(
107
118
  stream.write(text)
108
119
  }
109
120
 
110
- let current: readonly string[] | null = null
121
+ let current: Region | null = null
111
122
  let shown = false
112
123
  // PHYSICAL rows the shown region occupies, not logical lines — see
113
124
  // `regionRows`.
@@ -193,13 +204,18 @@ export function createOutputWriter(
193
204
  // only repeat these bytes.
194
205
  cancelTrailing()
195
206
  const erase = shown ? eraseSeq() : CLEAR
207
+ // Built here, not at the set: a warm run's task events set the
208
+ // region hundreds of times inside one floor, and each build was a
209
+ // forecast pass and a summary render no frame showed (200 hits on a
210
+ // terminal: run graph 62 → 9 ms). Kept built: a write redraws it.
211
+ if (typeof current === 'function') current = current()
196
212
  const lines = fitted(current!)
197
213
  stream.write(erase + lines.join('\n'))
198
214
  shown = true
199
215
  shownRows = regionRows(lines)
200
216
  lastDraw = now()
201
217
  }
202
- const set = (lines: readonly string[], o: { force?: boolean }): void => {
218
+ const set = (lines: Region, o: { force?: boolean }): void => {
203
219
  if (!enabled || dead) return
204
220
  current = lines
205
221
  if (!atLineStart || holds > 0) return
@@ -306,18 +322,18 @@ export function createOutputWriter(
306
322
  }
307
323
  }
308
324
 
309
- const IDLE = '#6b7280'
310
-
311
325
  export interface WorkerSlot {
312
326
  id: string
313
327
  startedMs: number
328
+ /** Its executor holds it for a machine to begin it (`TaskExecutor.running` is `null`). */
329
+ waiting?: boolean
314
330
  }
315
331
 
316
332
  export interface StatusRegionState {
317
- /** A running task's unfinished dependents (`blocks 14` on its row); 0 says nothing. */
318
- blocks?: ReadonlyMap<string, number>
319
333
  /** The running task at the head of the longest unfinished chain. */
320
334
  critical?: string
335
+ /** Where a running task runs, when not here (`TaskExecutor.running`); its row shows it. */
336
+ where?: (taskId: string) => string | null | undefined
321
337
  /** A running task's usual duration from history; its row shows it. */
322
338
  expected?: ReadonlyMap<string, number>
323
339
  /**
@@ -327,17 +343,23 @@ export interface StatusRegionState {
327
343
  * the honest end of their life.
328
344
  */
329
345
  pinnedPersistent: readonly string[]
330
- /** Fixed-size slot array; null = idle worker. Index = display row. */
346
+ /** The running tasks by slot, null for a free one; a task past the slots follows them. */
331
347
  slots: readonly (WorkerSlot | null)[]
332
- /** The blocked task an idle slot's row holds instead of `idle`, by the same index. */
333
- blocked?: readonly (BlockedTask | null)[]
334
- /** Running tasks beyond the displayed slots. */
335
- overflow: number
348
+ /** The list's height: one row per worker, whatever runs, so it never jumps. */
349
+ size?: number
350
+ /**
351
+ * The tasks the scheduler has picked for the next free workers, each
352
+ * nested under the running task it waits on, in the rows no running task
353
+ * takes; nothing further ahead.
354
+ */
355
+ next?: readonly BlockedTask[]
356
+ /** Each running task's count of the tasks it holds back: its `blocked` cell. */
357
+ blockedBy?: ReadonlyMap<string, number>
336
358
  /** The run was stopped: running rows say `aborting`, idle rows leave. */
337
359
  stopping?: boolean
338
360
  /**
339
361
  * vx's own steps still running, shown below the worker rows: no worker
340
- * slot or `--concurrency` bounds them (M, 2026-10-15).
362
+ * slot or `--concurrency` bounds them (2026-10-15).
341
363
  */
342
364
  steps?: readonly WorkerSlot[]
343
365
  /** Clock value used for per-slot elapsed (injectable for tests). */
@@ -392,7 +414,7 @@ const INTERNAL = 'internal'
392
414
  /**
393
415
  * The list row of one of vx's own steps (`vx:uploads`), on the task grid
394
416
  * with `internal` in the cache column: a step is no task and caches
395
- * nothing (M, 2026-10-16: `4.60s success internal vx:keep-run`).
417
+ * nothing (2026-10-16: `4.60s success internal vx:keep-run`).
396
418
  */
397
419
  export function formatStepLine(
398
420
  id: string,
@@ -441,31 +463,39 @@ export function formatStatusRegion(
441
463
  '',
442
464
  paintPinnedId(id, colors),
443
465
  colors,
466
+ // A blank `blocked` cell, so the id sits under the header's `task`.
467
+ '',
444
468
  ),
445
469
  )
446
470
  }
447
471
 
448
- // Worker rows: NO glyph — the live elapsed time leads (no spinner;
449
- // the ticking time IS the motion), then `running`, full id (never
450
- // truncated — name is the last column). Idle rows hold their slot's
451
- // place, dim, aligned under the status column: the blocked task the row
452
- // holds and what holds it back, else `idle`.
453
- // One task running has no path to compare against.
454
- const busy = s.slots.filter((x) => x !== null).length
455
- for (const [row, slot] of s.slots.entries()) {
456
- if (slot === null && s.stopping === true) continue
457
- if (slot === null) {
458
- const w = s.blocked?.[row]
459
- lines.push(
460
- w == null
461
- ? `${' '.repeat(3 + TIME_COL + 1)}${paint(IDLE, 'idle', colors, { dim: true })}`
462
- : formatTaskRow(' ', null, 'blocked', '', '', '', paintPinnedId(w.id, colors), colors) +
463
- ` ${dim(`by ${w.by}`)}`,
464
- )
465
- continue
472
+ // Running rows: NO glyph — the live elapsed time leads (no spinner;
473
+ // the ticking time IS the motion), then `running`, the usual duration,
474
+ // how many tasks wait on it, and the full id (never truncated — name is
475
+ // the last column). The rows no task runs in hold the tasks planned
476
+ // next, each nested under what it waits on; the rest stay blank, so the
477
+ // list keeps its height.
478
+ const running = s.slots.filter((x): x is WorkerSlot => x !== null && x.waiting !== true)
479
+ // Placed by an executor (DTE), the list is what runs: blocked tasks are
480
+ // the tasks line's count alone, and its size is no machine's.
481
+ const placed = s.where !== undefined
482
+ const free = Math.max(0, (s.size ?? 0) - running.length)
483
+ const nested = new Map<string, BlockedTask[]>()
484
+ if (!placed && s.stopping !== true) {
485
+ const ids = new Set(running.map((x) => x.id))
486
+ let n = 0
487
+ for (const b of s.next ?? []) {
488
+ if (n === free) break
489
+ if (!ids.has(b.by)) continue
490
+ let list = nested.get(b.by)
491
+ if (list === undefined) nested.set(b.by, (list = []))
492
+ list.push(b)
493
+ n++
466
494
  }
467
- // What waits on it, after the id: the row to watch is the one the
468
- // longest chain waits on, and the one the most tasks wait on.
495
+ }
496
+ let rows = 0
497
+ for (const slot of running) {
498
+ rows++
469
499
  if (s.stopping === true) {
470
500
  lines.push(
471
501
  formatTaskRow(
@@ -477,14 +507,12 @@ export function formatStatusRegion(
477
507
  '',
478
508
  paintPinnedId(slot.id, colors),
479
509
  colors,
510
+ '',
480
511
  ),
481
512
  )
482
513
  continue
483
514
  }
484
- const waiting = s.blocks?.get(slot.id) ?? 0
485
- const notes: string[] = []
486
- if (s.critical === slot.id && busy > 1) notes.push(paint(WARN, 'critical path', colors))
487
- if (waiting > 0) notes.push(dim(`blocks ${waiting}`))
515
+ const at = s.where?.(slot.id)
488
516
  const usual = s.expected?.get(slot.id)
489
517
  lines.push(
490
518
  formatTaskRow(
@@ -496,12 +524,29 @@ export function formatStatusRegion(
496
524
  '',
497
525
  paintPinnedId(slot.id, colors),
498
526
  colors,
499
- ) + (notes.length > 0 ? ` ${notes.join(dim(' \u00b7 '))}` : ''),
527
+ String(s.blockedBy?.get(slot.id) ?? 0),
528
+ ) +
529
+ (s.critical === slot.id ? pathNote(colors) : '') +
530
+ (at != null ? placeNote(at, colors) : ''),
500
531
  )
532
+ for (const b of nested.get(slot.id) ?? []) {
533
+ rows++
534
+ lines.push(
535
+ formatTaskRow(
536
+ ' ',
537
+ null,
538
+ 'blocked',
539
+ '',
540
+ '',
541
+ '',
542
+ `${dim('\u2514')} ${paintPinnedId(b.id, colors)}`,
543
+ colors,
544
+ '',
545
+ ),
546
+ )
547
+ }
501
548
  }
502
- if (s.overflow > 0) {
503
- lines.push(dim(`\u2026 +${s.overflow} more running`))
504
- }
549
+ if (!placed) for (; rows < (s.size ?? 0); rows++) lines.push('')
505
550
  for (const step of s.steps ?? []) {
506
551
  lines.push(
507
552
  formatTaskRow(
@@ -513,9 +558,11 @@ export function formatStatusRegion(
513
558
  '',
514
559
  paintPinnedId(step.id, colors),
515
560
  colors,
561
+ '',
516
562
  ),
517
563
  )
518
564
  }
565
+ if (lines.length > 1) lines.splice(1, 0, formatListHeader(placed, colors, true))
519
566
  // The live summary section — identical shape to the final printout,
520
567
  // so the region visually BECOMES the summary when the run ends.
521
568
  lines.push(...s.summaryLines)
@@ -524,11 +571,12 @@ export function formatStatusRegion(
524
571
 
525
572
  /**
526
573
  * A running task's usual duration, fixed while the elapsed time counts up
527
- * toward it (M, 2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
574
+ * toward it (2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
528
575
  * column below 100 minutes.
529
576
  */
530
577
  function formatExpected(ms: number): string {
531
- if (ms < 1000) return `~${Math.round(ms)}ms`
578
+ // The tier by the rounded value: 999.6 ms printed `~1000ms` (item 1034's class).
579
+ if (Math.round(ms) < 1000) return `~${Math.round(ms)}ms`
532
580
  if (ms < 9950) return `~${(ms / 1000).toFixed(1)}s`
533
581
  const s = Math.round(ms / 1000)
534
582
  if (s < 60) return `~${s}s`