@vzn/vx 0.0.649 → 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 (122) 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 +225 -51
  6. package/src/cache/file-hashes.ts +3 -0
  7. package/src/cache/fold-memo.ts +8 -1
  8. package/src/cache/git-inputs.ts +188 -39
  9. package/src/cache/index.ts +12 -0
  10. package/src/cache/inputs.ts +3 -0
  11. package/src/cache/layer.ts +11 -0
  12. package/src/cache/layered-cache.ts +4 -0
  13. package/src/cache/linked-inputs.ts +3 -0
  14. package/src/cache/lockfile-memo.ts +120 -0
  15. package/src/cache/output-index.ts +5 -2
  16. package/src/cache/remote-hits.ts +9 -1
  17. package/src/cache/remote-lane.ts +2 -1
  18. package/src/cache/run-history.ts +11 -1
  19. package/src/cache/run-notes.ts +123 -0
  20. package/src/cache/schema.ts +54 -1
  21. package/src/cli/cache.ts +26 -16
  22. package/src/cli/completions.ts +4 -2
  23. package/src/cli/docs.ts +10 -11
  24. package/src/cli/help.ts +12 -23
  25. package/src/cli/index.ts +4 -9
  26. package/src/cli/info.ts +15 -7
  27. package/src/cli/init.ts +23 -15
  28. package/src/cli/last.ts +224 -223
  29. package/src/cli/lock.ts +55 -5
  30. package/src/cli/plan-format.ts +100 -118
  31. package/src/cli/plugin-commands.ts +10 -3
  32. package/src/cli/plugin-templates.ts +2 -2
  33. package/src/cli/run.ts +46 -27
  34. package/src/cli/select.ts +43 -5
  35. package/src/cli/show.ts +38 -42
  36. package/src/cli/task-verb.ts +22 -0
  37. package/src/cli/upgrade.ts +3 -1
  38. package/src/cli/watch-fs.ts +6 -1
  39. package/src/cli/watch-judge.ts +2 -1
  40. package/src/cli/watch.ts +83 -44
  41. package/src/cli/why.ts +126 -92
  42. package/src/cli/workspace-config.ts +2 -1
  43. package/src/exec/executor.ts +2 -1
  44. package/src/exec/kill-tree.ts +58 -5
  45. package/src/exec/local-executor.ts +1 -1
  46. package/src/exec/pipes.ts +152 -0
  47. package/src/exec/runner.ts +199 -182
  48. package/src/exec/sandbox-binds.ts +1 -1
  49. package/src/exec/sandbox-runtime.ts +11 -2
  50. package/src/exec/spawn-off.ts +154 -0
  51. package/src/exec/warm-shell.ts +146 -0
  52. package/src/graph/critical-path.ts +40 -28
  53. package/src/graph/priorities.ts +98 -44
  54. package/src/graph/scheduler.ts +12 -2
  55. package/src/graph/task-graph.ts +27 -1
  56. package/src/handoff.ts +10 -7
  57. package/src/history/history.ts +39 -2
  58. package/src/history/index.ts +3 -0
  59. package/src/history/metrics.ts +9 -1
  60. package/src/index.ts +12 -3
  61. package/src/orchestrator/doctor.ts +3 -3
  62. package/src/orchestrator/execute-task.ts +16 -2
  63. package/src/orchestrator/graph-memo.ts +50 -1
  64. package/src/orchestrator/graph-stage.ts +6 -0
  65. package/src/orchestrator/hit-restore.ts +3 -0
  66. package/src/orchestrator/index.ts +3 -1
  67. package/src/orchestrator/live-usage.ts +98 -0
  68. package/src/orchestrator/locked-digests.ts +63 -0
  69. package/src/orchestrator/lockfile-claim.ts +176 -102
  70. package/src/orchestrator/miss-save.ts +31 -3
  71. package/src/orchestrator/persistent-task.ts +3 -1
  72. package/src/orchestrator/plan.ts +5 -1
  73. package/src/orchestrator/plugin-host.ts +61 -0
  74. package/src/orchestrator/plugin.ts +49 -4
  75. package/src/orchestrator/prepare.ts +18 -2
  76. package/src/orchestrator/projects.ts +4 -1
  77. package/src/orchestrator/run-context.ts +29 -10
  78. package/src/orchestrator/run-failures.ts +35 -114
  79. package/src/orchestrator/run-records.ts +10 -0
  80. package/src/orchestrator/run.ts +89 -26
  81. package/src/orchestrator/sandbox-request.ts +2 -2
  82. package/src/orchestrator/signals.ts +8 -5
  83. package/src/orchestrator/steps.ts +2 -2
  84. package/src/orchestrator/task-hash.ts +17 -3
  85. package/src/report/forecast.ts +12 -33
  86. package/src/report/framed-output.ts +388 -161
  87. package/src/report/index.ts +8 -2
  88. package/src/report/logger.ts +100 -36
  89. package/src/report/status-line.ts +81 -365
  90. package/src/report/summary.ts +104 -131
  91. package/src/ui/ansi.ts +48 -0
  92. package/src/ui/components.ts +506 -0
  93. package/src/ui/index.ts +79 -0
  94. package/src/ui/kit.ts +37 -0
  95. package/src/ui/live-region.ts +363 -0
  96. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  97. package/src/ui/terminal.ts +51 -0
  98. package/src/ui/theme.ts +224 -0
  99. package/src/ui/version-line.ts +28 -0
  100. package/src/util/config-frame.ts +65 -10
  101. package/src/util/edit-distance.ts +6 -1
  102. package/src/util/hidden-prompt.ts +8 -2
  103. package/src/util/index.ts +2 -15
  104. package/src/util/paths.ts +20 -2
  105. package/src/util/secret-mask.ts +39 -3
  106. package/src/util/secrets.ts +3 -0
  107. package/src/util/settle.ts +31 -0
  108. package/src/util/timing.ts +4 -16
  109. package/src/workspace/affected.ts +89 -30
  110. package/src/workspace/config-schema.ts +4 -2
  111. package/src/workspace/dependencies-lock.ts +191 -0
  112. package/src/workspace/filter.ts +28 -0
  113. package/src/workspace/index.ts +9 -0
  114. package/src/workspace/lockfile.ts +159 -6
  115. package/src/workspace/migration.ts +4 -3
  116. package/src/workspace/workspace.ts +3 -0
  117. package/src/orchestrator/run-steps.ts +0 -93
  118. package/src/util/colors.ts +0 -130
  119. package/src/util/report.ts +0 -44
  120. package/src/util/ui.ts +0 -258
  121. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  122. /package/src/{report → ui}/terminal-signals.ts +0 -0
@@ -30,8 +30,14 @@ export {
30
30
  resolveOutputView,
31
31
  warnOf,
32
32
  } from './logger.js'
33
- export { formatPersistentList } from './framed-output.js'
33
+ export {
34
+ formatPersistentList,
35
+ formatRecordedRow,
36
+ planList,
37
+ type PlannedRow,
38
+ taskList,
39
+ } from './framed-output.js'
34
40
  export { createForecast } from './forecast.js'
35
- export { fileLocations, fileMemo, type OutputLocation } from './path-links.js'
36
41
  export { plainOutput } from './plain-output.js'
42
+ export type { LiveUsage } from './status-line.js'
37
43
  export { formatOutcomeTable, formatRunSummary } from './summary.js'
@@ -6,25 +6,22 @@ import {
6
6
  formatKeptLines,
7
7
  formatPersistentTailBlock,
8
8
  formatListHeader,
9
+ usualCell,
9
10
  formatTaskAbortedLine,
10
11
  formatTaskBlock,
12
+ executedCells,
11
13
  formatTaskExecutedLine,
12
14
  flakyNote,
13
15
  pathNote,
14
- whereNote,
15
16
  formatTaskHashLine,
16
17
  formatTaskHitLine,
17
18
  formatTaskSkippedLine,
18
- leavesStyle,
19
- RESET,
20
19
  type OutputChunk,
21
20
  } from './framed-output.js'
22
21
  import {
23
- createOutputWriter,
24
22
  formatFailureLine,
25
23
  formatStatusRegion,
26
24
  formatStepLine,
27
- type StatusStream,
28
25
  type WorkerSlot,
29
26
  } from './status-line.js'
30
27
  import {
@@ -43,21 +40,26 @@ import {
43
40
  resetTail,
44
41
  tailText,
45
42
  type Tail,
46
- detectColors,
47
43
  envFlagOn,
48
- type ColorSupport,
49
- ui,
50
44
  } from '../util/index.js'
51
- import { isCacheHit } from '../graph/index.js'
52
- import { failedLabel, outcomeWord, ranNoCache } from './events.js'
53
- import type { BlockedTask, Forecast } from './forecast.js'
45
+ import * as ui from '../ui/index.js'
54
46
  import {
47
+ createOutputWriter,
48
+ detectColors,
49
+ hasEscape,
50
+ leavesStyle,
55
51
  notifyEscape,
56
52
  PROGRESS_CLEAR,
57
53
  progressEscape,
54
+ STYLE_END,
58
55
  terminalLinks,
59
56
  terminalSignals,
60
- } from './terminal-signals.js'
57
+ type ColorSupport,
58
+ type StatusStream,
59
+ } from '../ui/index.js'
60
+ import { isCacheHit } from '../graph/index.js'
61
+ import { failedLabel, outcomeWord, ranNoCache } from './events.js'
62
+ import type { BlockedTask, Forecast } from './forecast.js'
61
63
 
62
64
  export interface Logger {
63
65
  /** Header / footer / status text. Written verbatim, one trailing \n added. */
@@ -353,7 +355,7 @@ export function defaultLogger(
353
355
  // Tasks the region showed on the critical path: their finished rows say so too.
354
356
  const onPath = new Set<string>()
355
357
  let blockedCount = 0
356
- let expected: ReadonlyMap<string, number> | undefined
358
+ // Each task's usual time once history is read: the results' `±usual`.
357
359
  let blocked: readonly BlockedTask[] | undefined
358
360
  let blockedBy: ReadonlyMap<string, number> | undefined
359
361
  let etaAtMs = 0
@@ -518,7 +520,6 @@ export function defaultLogger(
518
520
  : Math.min(deadlineMs ?? now, now)
519
521
  critical = f.critical
520
522
  blockedCount = f.waiting
521
- expected = f.expected
522
523
  blocked = f.blocked
523
524
  blockedBy = f.blockedBy
524
525
  }
@@ -604,7 +605,8 @@ export function defaultLogger(
604
605
  nowMs: Date.now(),
605
606
  summaryLines,
606
607
  ...(runContext?.whereRunning !== undefined ? { where: runContext.whereRunning } : {}),
607
- ...(expected !== undefined ? { expected } : {}),
608
+ ...(runContext?.usage !== undefined ? { usage: runContext.usage } : {}),
609
+ ...(runContext?.usual !== undefined ? { usual: runContext.usual } : {}),
608
610
  ...(critical !== undefined ? { critical } : {}),
609
611
  },
610
612
  colors,
@@ -654,7 +656,7 @@ export function defaultLogger(
654
656
  // task`, and took its colour.
655
657
  let streamStyled = false
656
658
  const endStreamLine = (): void => {
657
- const end = (streamStyled ? RESET : '') + (streamMidLine ? '\n' : '')
659
+ const end = (streamStyled ? STYLE_END : '') + (streamMidLine ? '\n' : '')
658
660
  streamStyled = false
659
661
  streamMidLine = false
660
662
  if (end.length > 0) logWrite(end)
@@ -666,10 +668,11 @@ export function defaultLogger(
666
668
  const streamWrite = (chunk: string): void => {
667
669
  streamedSinceBlock = true
668
670
  if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
669
- if (chunk.includes('\x1b[')) streamStyled = leavesStyle(chunk)
671
+ if (hasEscape(chunk)) streamStyled = leavesStyle(chunk)
670
672
  logWrite(chunk)
671
673
  }
672
674
  const logWrite = (chunk: string): void => {
675
+ if (panelOpen !== undefined && !inPanel && chunk.length > 0) closePanel()
673
676
  if (chunk.length > 0) endsBlank = chunk.endsWith('\n\n')
674
677
  if (!logOpened && chunk.length > 0) {
675
678
  logOpened = true
@@ -682,15 +685,33 @@ export function defaultLogger(
682
685
  taskWrite(`${line}\n`)
683
686
  lineEmitted = true
684
687
  }
685
- // The list's header, once over its first row, where the live region
686
- // heads its own rows the same way (2026-10-21).
687
- let headed = !writer.enabled
688
+ // The open panel: rows of one kind share one box, the Tasks list on the
689
+ // log's stream and the Warnings on stderr; anything else said closes it
690
+ // first, and the next row opens it again. Painted, a box; plain on a
691
+ // terminal, the Tasks rule and header over the rows, as the live region
692
+ // heads its own (2026-10-21); piped, the bare rows a script reads.
693
+ const panels = colors.enabled || writer.enabled
694
+ let panelOpen: 'Tasks' | 'Warnings' | undefined
695
+ let inPanel = false
696
+ function closePanel(): void {
697
+ const was = panelOpen
698
+ if (was === undefined) return
699
+ panelOpen = undefined
700
+ }
688
701
  const emitRow = (line: string): void => {
689
- if (!headed) {
690
- headed = true
691
- emitLine(formatListHeader(runContext?.whereRunning !== undefined, colors))
702
+ if (!panels) {
703
+ emitLine(line)
704
+ return
705
+ }
706
+ if (panelOpen === 'Warnings') closePanel()
707
+ inPanel = true
708
+ if (panelOpen !== 'Tasks') {
709
+ emitLine(ui.panelTop('Tasks', colors))
710
+ emitLine(formatListHeader(colors))
711
+ panelOpen = 'Tasks'
692
712
  }
693
713
  emitLine(line)
714
+ inPanel = false
694
715
  }
695
716
  // formatTaskBlock returns '' for group tasks (no exec) — skip the
696
717
  // write so a stray newline doesn't sneak into the output.
@@ -731,10 +752,17 @@ export function defaultLogger(
731
752
  (framesTerminal && holdsTerminal(node)) ||
732
753
  (view.mode === 'focused' && isPrimary(node) && !isGroupTask(node) && requestedCount <= 1)
733
754
 
755
+ // How many tasks are downstream of each: every results row's `blocks`.
756
+ const blocksOf = (id: string): number | undefined => runContext?.blocks?.().get(id)
757
+ // A row's usual time: null for a task history never timed, undefined with no history to ask.
758
+ const usualMsOf = (id: string): number | null | undefined =>
759
+ runContext?.usual === undefined ? undefined : (runContext.usual(id) ?? null)
734
760
  const failureRow = (node: TaskNode, outcome: TaskOutcome): string =>
735
- formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(outcome)) +
761
+ formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(outcome), {
762
+ ...executedCells(outcome, blocksOf(node.id)),
763
+ usual: usualCell(outcome.durationMs, usualMsOf(node.id), colors),
764
+ }) +
736
765
  (onPath.has(node.id) ? pathNote(colors) : '') +
737
- whereNote(outcome, colors) +
738
766
  flakyNote(outcome, colors)
739
767
 
740
768
  return {
@@ -751,6 +779,7 @@ export function defaultLogger(
751
779
  // A live-streamed server keeps writing after its frame closes, so a
752
780
  // summary row or "exited with code" can follow its partial line.
753
781
  endStreamLine()
782
+ closePanel()
754
783
  if (line === '' && endsBlank) return
755
784
  endsBlank = line === ''
756
785
  writer.write(`${line}\n`)
@@ -758,7 +787,20 @@ export function defaultLogger(
758
787
  warn(line) {
759
788
  flushKept()
760
789
  endStreamLine()
761
- writer.writeTo(errOut, `${ui.tagged('warn', line, colors)}\n`)
790
+ if (!colors.enabled) {
791
+ writer.writeTo(errOut, `${ui.tagged('warn', line, colors)}\n`)
792
+ return
793
+ }
794
+ if (panelOpen === 'Tasks') closePanel()
795
+ if (panelOpen !== 'Warnings') {
796
+ writer.writeTo(errOut, `${ui.panelTop('Warnings', colors)}\n`)
797
+ panelOpen = 'Warnings'
798
+ }
799
+ const rows = line
800
+ .replace(/\n+$/, '')
801
+ .split('\n')
802
+ .map((l) => ui.tagged('warn', l, colors))
803
+ writer.writeTo(errOut, `${rows.join('\n')}\n`)
762
804
  },
763
805
  runStart(info) {
764
806
  total = info.total
@@ -766,7 +808,13 @@ export function defaultLogger(
766
808
  runContext = info.context
767
809
  startedAtMs = Date.now()
768
810
  started = true
769
- slots = Array.from({ length: Math.max(1, info.concurrency ?? 10) }, () => null)
811
+ // No more slots than tasks: one per allowed worker, every frame walked
812
+ // them all (`--concurrency=100000000` over six tasks took 80 s, and
813
+ // 2^32 threw here).
814
+ slots = Array.from(
815
+ { length: Math.max(1, Math.min(info.concurrency ?? 10, info.total)) },
816
+ () => null,
817
+ )
770
818
  tick()
771
819
  refresh(true)
772
820
  },
@@ -820,7 +868,11 @@ export function defaultLogger(
820
868
  }
821
869
  // The rows are the status region's; with none, nothing reads them.
822
870
  if (statusDead) return
823
- const slot: WorkerSlot = { id: node.id, startedMs: Date.now() }
871
+ const slot: WorkerSlot = {
872
+ id: node.id,
873
+ startedMs: Date.now(),
874
+ cache: ranNoCache({ node }) ? 'no-cache' : 'miss',
875
+ }
824
876
  const free = slots.indexOf(null)
825
877
  if (free >= 0) slots[free] = slot
826
878
  else slotQueue.push(slot)
@@ -912,6 +964,7 @@ export function defaultLogger(
912
964
  flushedFailures = true
913
965
  for (const block of deferredFailures) emitBlock(fenced(block))
914
966
  }
967
+ closePanel()
915
968
  // The summary and the CLI's own lines follow on the stream directly:
916
969
  // nothing may still be held, and nothing after this is.
917
970
  writer.settle()
@@ -1016,7 +1069,7 @@ export function defaultLogger(
1016
1069
  // Otherwise a row in the task list, in every view that lists a
1017
1070
  // failure: the run exits red, and the row names why.
1018
1071
  if (view.mode !== 'none' && view.mode !== 'hash-only') {
1019
- emitRow(formatTaskAbortedLine(node, outcome, colors))
1072
+ emitRow(formatTaskAbortedLine(node, outcome, colors, blocksOf(node.id)))
1020
1073
  }
1021
1074
  return
1022
1075
  }
@@ -1099,7 +1152,7 @@ export function defaultLogger(
1099
1152
  }
1100
1153
  case 'errors-only':
1101
1154
  if (outcome.status === 'skipped') {
1102
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1155
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1103
1156
  return
1104
1157
  }
1105
1158
  if (outcome.status !== 'failed') return
@@ -1136,9 +1189,18 @@ export function defaultLogger(
1136
1189
  ),
1137
1190
  )
1138
1191
  } else if (outcome.status === 'success') {
1139
- emitRow(formatTaskExecutedLine(node, outcome, colors, onPath.has(node.id)))
1192
+ emitRow(
1193
+ formatTaskExecutedLine(
1194
+ node,
1195
+ outcome,
1196
+ colors,
1197
+ onPath.has(node.id),
1198
+ blocksOf(node.id),
1199
+ usualMsOf(node.id),
1200
+ ),
1201
+ )
1140
1202
  } else if (outcome.status === 'skipped') {
1141
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1203
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1142
1204
  }
1143
1205
  return
1144
1206
  case 'focused':
@@ -1147,7 +1209,7 @@ export function defaultLogger(
1147
1209
  // stopped), so no frame-open fired — and a skip has no output,
1148
1210
  // so a one-liner carries everything a frame would.
1149
1211
  if (outcome.status === 'skipped') {
1150
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1212
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1151
1213
  return
1152
1214
  }
1153
1215
  if (streamsLive(node)) {
@@ -1198,7 +1260,7 @@ export function defaultLogger(
1198
1260
  // Dependency-pulled nodes: silent on success; a skip gets its
1199
1261
  // row, failures the ◼ row now and their frame replayed at runEnd.
1200
1262
  if (outcome.status === 'skipped') {
1201
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1263
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1202
1264
  } else if (outcome.status === 'failed') {
1203
1265
  emitRow(failureRow(node, outcome))
1204
1266
  deferredFailures.push(
@@ -1222,11 +1284,13 @@ export function defaultLogger(
1222
1284
  // point); misses/failures are always framed. One-liners
1223
1285
  // stay outside ::group:: — there's nothing to collapse.
1224
1286
  if (isHit && output.every((c) => c.text.trim().length === 0)) {
1225
- emitRow(formatTaskHitLine(node, outcome, colors, onPath.has(node.id)))
1287
+ emitRow(
1288
+ formatTaskHitLine(node, outcome, colors, onPath.has(node.id), blocksOf(node.id)),
1289
+ )
1226
1290
  return
1227
1291
  }
1228
1292
  if (outcome.status === 'skipped') {
1229
- emitRow(formatTaskSkippedLine(node, colors, outcome))
1293
+ emitRow(formatTaskSkippedLine(node, colors, outcome, blocksOf(node.id)))
1230
1294
  return
1231
1295
  }
1232
1296
  const block = formatTaskBlock(