@vzn/vx 0.0.18 → 0.0.22

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 (149) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -70
  3. package/package.json +10 -20
  4. package/src/bin.ts +64 -5
  5. package/src/cache/archive.ts +279 -28
  6. package/src/cache/cache.ts +952 -1584
  7. package/src/cache/chained-cache.ts +14 -10
  8. package/src/cache/config-evals.ts +112 -0
  9. package/src/cache/file-hashes.ts +290 -0
  10. package/src/cache/git-inputs.ts +966 -0
  11. package/src/cache/index.ts +23 -7
  12. package/src/cache/inputs.ts +553 -739
  13. package/src/cache/key-fold.ts +232 -0
  14. package/src/cache/layer.ts +674 -0
  15. package/src/cache/layered-cache.ts +211 -49
  16. package/src/cache/output-index.ts +323 -0
  17. package/src/cache/policy.ts +84 -0
  18. package/src/cache/run-history.ts +152 -0
  19. package/src/cache/zstd.ts +183 -0
  20. package/src/cli/cache.ts +88 -30
  21. package/src/cli/completions.ts +104 -0
  22. package/src/cli/core-alias.ts +25 -0
  23. package/src/cli/format.ts +4 -15
  24. package/src/cli/help.ts +76 -38
  25. package/src/cli/index.ts +82 -17
  26. package/src/cli/info.ts +162 -91
  27. package/src/cli/init.ts +87 -0
  28. package/src/cli/last.ts +130 -23
  29. package/src/cli/lock.ts +36 -18
  30. package/src/cli/plan-format.ts +18 -7
  31. package/src/cli/plugin-commands.ts +34 -12
  32. package/src/cli/run.ts +138 -313
  33. package/src/cli/select.ts +342 -0
  34. package/src/cli/show.ts +151 -71
  35. package/src/cli/upgrade.ts +136 -15
  36. package/src/cli/watch.ts +1175 -116
  37. package/src/cli/why.ts +51 -29
  38. package/src/cli/workspace-config.ts +138 -0
  39. package/src/config.ts +183 -126
  40. package/src/exec/env.ts +11 -0
  41. package/src/exec/executor.ts +62 -31
  42. package/src/exec/index.ts +16 -2
  43. package/src/exec/kill-tree.ts +281 -0
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +361 -77
  46. package/src/exec/sandbox-binds.ts +211 -0
  47. package/src/exec/sandbox-paths.ts +70 -0
  48. package/src/exec/sandbox-runtime.ts +1152 -415
  49. package/src/exec/sandbox-violations.ts +293 -0
  50. package/src/graph/dependency-spec.ts +14 -14
  51. package/src/graph/index.ts +6 -4
  52. package/src/graph/priorities.ts +130 -0
  53. package/src/graph/scheduler.ts +332 -329
  54. package/src/graph/task-graph.ts +552 -188
  55. package/src/index.ts +111 -102
  56. package/src/orchestrator/admission.ts +161 -0
  57. package/src/orchestrator/colors.ts +8 -3
  58. package/src/orchestrator/deferred-outputs.ts +25 -7
  59. package/src/orchestrator/doctor.ts +363 -0
  60. package/src/orchestrator/events.ts +111 -8
  61. package/src/orchestrator/excluded-keys.ts +155 -0
  62. package/src/orchestrator/execute-task.ts +450 -565
  63. package/src/orchestrator/failure-mode.ts +169 -28
  64. package/src/orchestrator/failure-recap.ts +121 -0
  65. package/src/orchestrator/fingerprint-watch.ts +89 -0
  66. package/src/orchestrator/framed-output.ts +121 -49
  67. package/src/orchestrator/history.ts +123 -104
  68. package/src/orchestrator/hit-restore.ts +285 -0
  69. package/src/orchestrator/index.ts +42 -5
  70. package/src/orchestrator/keyed-projects.ts +89 -0
  71. package/src/orchestrator/local-shortcircuit.ts +83 -23
  72. package/src/orchestrator/lockfile-claim.ts +401 -0
  73. package/src/orchestrator/logger.ts +128 -41
  74. package/src/orchestrator/metrics.ts +201 -72
  75. package/src/orchestrator/miss-save.ts +232 -0
  76. package/src/orchestrator/options.ts +63 -34
  77. package/src/orchestrator/persistent.ts +99 -0
  78. package/src/orchestrator/placement.ts +212 -0
  79. package/src/orchestrator/plan.ts +24 -11
  80. package/src/orchestrator/plugin-host.ts +200 -49
  81. package/src/orchestrator/plugin.ts +190 -14
  82. package/src/orchestrator/prepare.ts +168 -137
  83. package/src/orchestrator/projects.ts +270 -0
  84. package/src/orchestrator/remote-prefetch.ts +22 -2
  85. package/src/orchestrator/run-artifacts.ts +34 -3
  86. package/src/orchestrator/run-context.ts +20 -5
  87. package/src/orchestrator/run-lock.ts +349 -0
  88. package/src/orchestrator/run-records.ts +132 -0
  89. package/src/orchestrator/run-report.ts +6 -4
  90. package/src/orchestrator/run.ts +467 -611
  91. package/src/orchestrator/sandbox-request.ts +569 -0
  92. package/src/orchestrator/save-lane.ts +44 -0
  93. package/src/orchestrator/shell-verdict.ts +117 -0
  94. package/src/orchestrator/signals.ts +167 -0
  95. package/src/orchestrator/stable-keys.ts +259 -10
  96. package/src/orchestrator/status-line.ts +35 -2
  97. package/src/orchestrator/summary.ts +115 -25
  98. package/src/orchestrator/task-hash.ts +103 -21
  99. package/src/orchestrator/task-log-buffer.ts +15 -18
  100. package/src/orchestrator/telemetry-host.ts +27 -8
  101. package/src/orchestrator/telemetry.ts +71 -44
  102. package/src/orchestrator/upstream.ts +91 -45
  103. package/src/util/bun-version.ts +43 -0
  104. package/src/util/cgroup.ts +160 -0
  105. package/src/util/edit-distance.ts +49 -0
  106. package/src/util/errors.ts +86 -0
  107. package/src/util/hash.ts +10 -1
  108. package/src/util/index.ts +42 -5
  109. package/src/util/paths.ts +168 -5
  110. package/src/util/procfs.ts +20 -0
  111. package/src/util/settle.ts +23 -2
  112. package/src/util/size.ts +33 -0
  113. package/src/util/task-id.ts +20 -0
  114. package/src/util/timing.ts +11 -1
  115. package/src/util/verbs.ts +39 -0
  116. package/src/util/which.ts +48 -0
  117. package/src/workspace/affected.ts +368 -60
  118. package/src/workspace/config-cache.ts +271 -32
  119. package/src/workspace/config-eval.ts +60 -16
  120. package/src/workspace/config-imports.ts +131 -9
  121. package/src/workspace/config-schema.ts +1033 -0
  122. package/src/workspace/filter.ts +69 -17
  123. package/src/workspace/fingerprint.ts +59 -7
  124. package/src/workspace/index.ts +37 -3
  125. package/src/workspace/json-data.ts +90 -0
  126. package/src/workspace/load-reads.ts +32 -0
  127. package/src/workspace/lockfile.ts +41 -14
  128. package/src/workspace/migrate-scripts.ts +187 -0
  129. package/src/workspace/migration.ts +353 -0
  130. package/src/workspace/package-graph.ts +239 -31
  131. package/src/workspace/project-loader.ts +295 -815
  132. package/src/workspace/workspace.ts +266 -31
  133. package/plugins/local-cache/index.ts +0 -2
  134. package/plugins/local-executor/index.ts +0 -2
  135. package/plugins/schedule-history/index.ts +0 -2
  136. package/src/cache/cas-backend.ts +0 -115
  137. package/src/cache/digest.ts +0 -32
  138. package/src/cli/migrate-emit.ts +0 -21
  139. package/src/cli/migrate-nx.ts +0 -473
  140. package/src/cli/migrate-scripts.ts +0 -127
  141. package/src/cli/migrate-turbo.ts +0 -401
  142. package/src/cli/migrate.ts +0 -305
  143. package/src/cli/prune.ts +0 -291
  144. package/src/orchestrator/missing-plugin.ts +0 -9
  145. package/src/orchestrator/resources.ts +0 -45
  146. package/src/orchestrator/verify.ts +0 -174
  147. package/src/plugins/local-cache/index.ts +0 -11
  148. package/src/plugins/local-executor/index.ts +0 -45
  149. package/src/plugins/schedule-history/index.ts +0 -109
@@ -1,6 +1,7 @@
1
1
  import type { TaskNode, TaskOutcome } from '../graph/index.js'
2
2
  import { detectColors, type ColorSupport } from './colors.js'
3
3
  import {
4
+ formatFailureRecap,
4
5
  formatFrameClose,
5
6
  formatFrameOpen,
6
7
  formatPersistentTailBlock,
@@ -8,7 +9,15 @@ import {
8
9
  formatTaskExecutedLine,
9
10
  formatTaskHitLine,
10
11
  formatTaskSkippedLine,
12
+ type RecapEntry,
11
13
  } from './framed-output.js'
14
+ import {
15
+ appendRecapRing,
16
+ createRecapRing,
17
+ RECAP_TASKS,
18
+ recapTail,
19
+ type RecapRing,
20
+ } from './failure-recap.js'
12
21
  import {
13
22
  createOutputWriter,
14
23
  formatFailureLine,
@@ -20,6 +29,7 @@ import { formatDuration, formatSummarySection, type RunContext } from './summary
20
29
  import { isGroupTask } from '../graph/index.js'
21
30
  import { appendTail, createTail, resetTail, tailText, type Tail } from '../util/index.js'
22
31
  import { isCacheHit } from './telemetry.js'
32
+ import { failedLabel, outcomeWord } from './events.js'
23
33
 
24
34
  export interface Logger {
25
35
  /** Header / footer / status text. Written verbatim, one trailing \n added. */
@@ -52,6 +62,21 @@ export interface Logger {
52
62
  runEnd?(): void
53
63
  }
54
64
 
65
+ /** The logger `run()` builds when none is passed, and the one that owns the recap. */
66
+ export interface DefaultLogger extends Logger {
67
+ /**
68
+ * The run's last block: each failed task's last lines (item 706). Empty
69
+ * when nothing failed, and in the modes that promise no task output.
70
+ */
71
+ failureRecap(): string[]
72
+ /**
73
+ * Hand over any output still coalesced, and write straight through from
74
+ * now on. `runEnd` does it; the run's own `finally` does it again for a
75
+ * throw that never reached `runEnd`.
76
+ */
77
+ settle(): void
78
+ }
79
+
55
80
  /**
56
81
  * The default logger's per-task output policy. Resolved once per run
57
82
  * from (in priority order) the explicit `--output-logs` override, a
@@ -124,19 +149,8 @@ function ghaFence(body: string, token: string): string {
124
149
  return `::stop-commands::${token}\n${body}::${token}::\n`
125
150
  }
126
151
 
127
- /** The unified outcome vocabulary word for a non-failed outcome. */
128
- function outcomeWord(o: TaskOutcome): string {
129
- switch (o.status) {
130
- case 'success':
131
- return 'success'
132
- case 'cache-hit':
133
- return o.restored === false ? 'up-to-date' : 'restored-local'
134
- case 'cache-hit-remote':
135
- return o.restored === false ? 'up-to-date' : 'restored-remote'
136
- default:
137
- return o.status
138
- }
139
- }
152
+ const cacheWordOf = (n: TaskNode): 'miss' | 'no-cache' =>
153
+ n.config.cache === undefined ? 'no-cache' : 'miss'
140
154
 
141
155
  export function resolveOutputView(
142
156
  options: {
@@ -162,8 +176,8 @@ export function defaultLogger(
162
176
  colors: ColorSupport = detectColors(),
163
177
  view: OutputView = { mode: 'full' },
164
178
  out: StatusStream = process.stdout,
165
- opts: { forceFloorMs?: number } = {},
166
- ): Logger {
179
+ opts: { forceFloorMs?: number; coalesce?: boolean } = {},
180
+ ): DefaultLogger {
167
181
  // Per-task buffers, split by stream. Splitting lets the framed-output
168
182
  // renderer put stdout under `├─ stdout` and stderr under `├─ stderr`.
169
183
  // The price: chunks that interleaved at runtime get re-ordered (all
@@ -182,8 +196,6 @@ export function defaultLogger(
182
196
  // a one-liner or streamed output since the last block does.
183
197
  let lineEmitted = false
184
198
  let streamedSinceBlock = false
185
- // Ids whose output went straight to the terminal (focused mode).
186
- const streamed = new Set<string>()
187
199
  // True while the live stream sits mid-line (chunk without trailing
188
200
  // newline) — the frame close must not glue onto partial output.
189
201
  let streamMidLine = false
@@ -227,6 +239,7 @@ export function defaultLogger(
227
239
  const writer = createOutputWriter(out, {
228
240
  enabled: view.ci !== true,
229
241
  ...(opts.forceFloorMs !== undefined ? { forceFloorMs: opts.forceFloorMs } : {}),
242
+ ...(opts.coalesce === true ? { coalesce: true } : {}),
230
243
  })
231
244
 
232
245
  // Status-display state, driven by the optional lifecycle hooks.
@@ -257,6 +270,8 @@ export function defaultLogger(
257
270
  let slots: (WorkerSlot | null)[] = []
258
271
  const slotQueue: WorkerSlot[] = []
259
272
  let succeeded = 0
273
+ // Executed with no `cache` block: never a miss, counted apart.
274
+ let noCache = 0
260
275
  let upToDate = 0
261
276
  let restoredLocal = 0
262
277
  let restoredRemote = 0
@@ -267,11 +282,12 @@ export function defaultLogger(
267
282
  let spreadMin = Infinity
268
283
  let spreadSum = 0
269
284
  let spreadCount = 0
270
- // Pinned zones above the worker rows: failures accumulate as they
271
- // happen; persistent tasks pin at ready (their outcome lands while
272
- // the child keeps running). Both live until runEnd kills the region.
273
- // Full failure frames, deferred to runEnd (owner: '✗ line, continue,
274
- // all full frames at the end'). 'full'/CI keeps frames inline.
285
+ // The pinned zone above the worker rows: persistent tasks pin at
286
+ // ready (their outcome lands while the child keeps running) and live
287
+ // until runEnd kills the region. A failure is not pinned: it is a
288
+ // permanent ◼ row the moment it happens, and its full frame is
289
+ // deferred to runEnd (owner: '✗ line, continue, all full frames at
290
+ // the end'). 'full'/CI keeps frames inline.
275
291
  const deferredFailures: string[] = []
276
292
  const pinnedPersistent: string[] = []
277
293
  let flushedFailures = false
@@ -297,6 +313,13 @@ export function defaultLogger(
297
313
  { node: TaskNode; outcome?: TaskOutcome; out: Tail; err: Tail }
298
314
  >()
299
315
  let flushedPersistent = false
316
+ // The recap's material, decided at each failure so nothing is held for a
317
+ // task that passed. A buffered task's tail is read off the buffers the
318
+ // frame drains; a live-streamed one keeps no buffer, so a bounded ring
319
+ // follows it from its frame-open until its outcome.
320
+ const recapEntries: RecapEntry[] = []
321
+ const recapMore: string[] = []
322
+ const liveRings = new Map<string, RecapRing>()
300
323
 
301
324
  const refresh = (force: boolean): void => {
302
325
  if (statusDead) return
@@ -311,7 +334,8 @@ export function defaultLogger(
311
334
  upToDate,
312
335
  restoredLocal,
313
336
  restoredRemote,
314
- miss: succeeded + failed,
337
+ miss: succeeded + failed - noCache,
338
+ noCache,
315
339
  left: total - done,
316
340
  spread:
317
341
  spreadCount > 0
@@ -381,6 +405,23 @@ export function defaultLogger(
381
405
  view.mode === 'focused' && isPrimary(node) && !isGroupTask(node) && requestedCount <= 1
382
406
 
383
407
  return {
408
+ settle() {
409
+ writer.settle()
410
+ },
411
+ failureRecap() {
412
+ if (recapEntries.length === 0) return []
413
+ // Fenced like a frame: a tail is the task's own text, and a line of it
414
+ // must not become a workflow command. Never inside a group, so it reads
415
+ // with every group collapsed.
416
+ const fence =
417
+ view.gha === true
418
+ ? (lines: string[]) =>
419
+ ghaFence(`${lines.join('\n')}\n`, ghaToken)
420
+ .slice(0, -1)
421
+ .split('\n')
422
+ : undefined
423
+ return formatFailureRecap(recapEntries, recapMore, colors, fence)
424
+ },
384
425
  status(line) {
385
426
  writer.write(`${line}\n`)
386
427
  },
@@ -413,6 +454,7 @@ export function defaultLogger(
413
454
  // Open the live frame: full task info even when the command
414
455
  // streams nothing (or the hit replays nothing).
415
456
  emitLine(formatFrameOpen(node, colors))
457
+ liveRings.set(node.id, createRecapRing())
416
458
  return
417
459
  }
418
460
  // Bound a persistent task's output from its FIRST chunk. Waiting for
@@ -461,21 +503,25 @@ export function defaultLogger(
461
503
  }
462
504
  }
463
505
  // Failures end the log: every deferred frame replays here, right
464
- // above the summary — the ✗ one-liners marked them in the
465
- // stream, the full diagnostics read last where eyes land.
506
+ // above the summary — the ◼ rows marked them in the stream,
507
+ // the full diagnostics read last where eyes land.
466
508
  // Guarded for repeat runEnd calls.
467
509
  if (!flushedFailures && deferredFailures.length > 0) {
468
510
  flushedFailures = true
469
511
  for (const block of deferredFailures) emitBlock(block)
470
512
  }
513
+ // The summary and the CLI's own lines follow on the stream directly:
514
+ // nothing may still be held, and nothing after this is.
515
+ writer.settle()
471
516
  },
472
517
  taskStdout(node, chunk) {
473
518
  if (discardsOutput) return
474
519
  if (streamsLive(node)) {
475
- streamed.add(node.id)
476
520
  streamedSinceBlock = true
477
521
  if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
478
522
  writer.write(chunk)
523
+ const ring = liveRings.get(node.id)
524
+ if (ring !== undefined) appendRecapRing(ring, chunk)
479
525
  return
480
526
  }
481
527
  const tail = persistentTails.get(node.id)
@@ -488,10 +534,11 @@ export function defaultLogger(
488
534
  taskStderr(node, chunk) {
489
535
  if (discardsOutput) return
490
536
  if (streamsLive(node)) {
491
- streamed.add(node.id)
492
537
  streamedSinceBlock = true
493
538
  if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
494
539
  writer.write(chunk)
540
+ const ring = liveRings.get(node.id)
541
+ if (ring !== undefined) appendRecapRing(ring, chunk)
495
542
  return
496
543
  }
497
544
  const tail = persistentTails.get(node.id)
@@ -516,6 +563,28 @@ export function defaultLogger(
516
563
  // The pre-ready window closed either way: a failed task is over, and
517
564
  // a ready one starts a fresh post-ready tail below.
518
565
  persistentTails.delete(node.id)
566
+ const liveRing = liveRings.get(node.id)
567
+ liveRings.delete(node.id)
568
+ if (outcome.status === 'failed' && !discardsOutput) {
569
+ if (recapEntries.length === RECAP_TASKS) recapMore.push(node.id)
570
+ else {
571
+ let ring = liveRing
572
+ if (ring === undefined) {
573
+ ring = createRecapRing()
574
+ appendRecapRing(ring, stdout)
575
+ if (stdout.length > 0 && stderr.length > 0 && !stdout.endsWith('\n')) {
576
+ appendRecapRing(ring, '\n')
577
+ }
578
+ appendRecapRing(ring, stderr)
579
+ }
580
+ recapEntries.push({
581
+ node,
582
+ outcome,
583
+ tail: recapTail(ring),
584
+ droppedChars: (preReady?.out.dropped ?? 0) + (preReady?.err.dropped ?? 0),
585
+ })
586
+ }
587
+ }
519
588
  // An aborted task (child killed by a shutdown signal) reverts
520
589
  // to pending: free its worker slot, but never count or render it
521
590
  // — the run is tearing down and it has no honest outcome.
@@ -527,6 +596,22 @@ export function defaultLogger(
527
596
  if (qi >= 0) slotQueue.splice(qi, 1)
528
597
  }
529
598
  refresh(true)
599
+ // What it printed while it stopped (a trap's cleanup) still reaches
600
+ // the terminal where a frame would have: a task that traps Ctrl-C
601
+ // and exits 0 is aborted since item 962, and its frame was dropped
602
+ // with the count.
603
+ const framed = view.mode === 'full' || (view.mode === 'focused' && isPrimary(node))
604
+ if (framed && !isGroupTask(node)) {
605
+ if (streamsLive(node)) {
606
+ if (streamMidLine) {
607
+ writer.write('\n')
608
+ streamMidLine = false
609
+ }
610
+ emitFrameClose(formatFrameClose(node, outcome, colors))
611
+ } else if (stdout.length > 0 || stderr.length > 0) {
612
+ emitBlock(formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors, true))
613
+ }
614
+ }
530
615
  return
531
616
  }
532
617
  // Group tasks (no exec) do no work — no surface prints them.
@@ -534,6 +619,7 @@ export function defaultLogger(
534
619
  done++
535
620
  if (outcome.status === 'failed') {
536
621
  failed++
622
+ if (node.config.cache === undefined) noCache++
537
623
  } else if (node.config.exec?.persistent !== undefined && outcome.status === 'success') {
538
624
  // A persistent task's outcome arrives at READY; the child
539
625
  // keeps running until the orchestrator SIGTERMs it at run
@@ -557,6 +643,7 @@ export function defaultLogger(
557
643
  switch (outcome.status) {
558
644
  case 'success':
559
645
  succeeded++
646
+ if (node.config.cache === undefined) noCache++
560
647
  break
561
648
  case 'cache-hit':
562
649
  if (outcome.restored === false) upToDate++
@@ -593,7 +680,7 @@ export function defaultLogger(
593
680
  }
594
681
  case 'errors-only':
595
682
  if (outcome.status !== 'failed') return
596
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
683
+ emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
597
684
  deferredFailures.push(
598
685
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
599
686
  )
@@ -604,8 +691,8 @@ export function defaultLogger(
604
691
  // their replay buffers are deliberately dropped; the counts
605
692
  // surface in the end-of-run summary.
606
693
  if (outcome.status === 'failed') {
607
- // ✗ marker now; the full frame replays at runEnd.
608
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
694
+ // ◼ row now; the full frame replays at runEnd.
695
+ emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
609
696
  deferredFailures.push(
610
697
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
611
698
  )
@@ -615,11 +702,11 @@ export function defaultLogger(
615
702
  return
616
703
  case 'focused':
617
704
  if (isPrimary(node)) {
618
- // Skipped tasks never started (upstream failed), so no
619
- // frame-open fired — and a skip has no output, so a
620
- // one-liner carries everything a frame would.
705
+ // Skipped tasks never started (blocked upstream, or the run
706
+ // stopped), so no frame-open fired — and a skip has no output,
707
+ // so a one-liner carries everything a frame would.
621
708
  if (outcome.status === 'skipped') {
622
- emitLine(formatTaskSkippedLine(node, colors))
709
+ emitLine(formatTaskSkippedLine(node, colors, outcome.blockedBy))
623
710
  return
624
711
  }
625
712
  if (streamsLive(node)) {
@@ -640,7 +727,7 @@ export function defaultLogger(
640
727
  // to runEnd like everywhere else; everything else emits
641
728
  // ONE atomic block from the buffered output.
642
729
  if (outcome.status === 'failed') {
643
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
730
+ emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
644
731
  deferredFailures.push(
645
732
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
646
733
  )
@@ -652,9 +739,9 @@ export function defaultLogger(
652
739
  return
653
740
  }
654
741
  // Dependency-pulled nodes: silent on success; failures get
655
- // the ✗ marker now and their frame replayed at runEnd.
742
+ // the ◼ row now and their frame replayed at runEnd.
656
743
  if (outcome.status === 'failed') {
657
- emitLine(formatFailureLine(node.id, outcome.durationMs, colors))
744
+ emitLine(formatFailureLine(node.id, outcome.durationMs, colors, cacheWordOf(node)))
658
745
  deferredFailures.push(
659
746
  formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors),
660
747
  )
@@ -672,7 +759,7 @@ export function defaultLogger(
672
759
  return
673
760
  }
674
761
  if (outcome.status === 'skipped') {
675
- emitLine(formatTaskSkippedLine(node, colors))
762
+ emitLine(formatTaskSkippedLine(node, colors, outcome.blockedBy))
676
763
  return
677
764
  }
678
765
  const block = formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors)
@@ -685,10 +772,10 @@ export function defaultLogger(
685
772
  // Failed tasks stay pre-expanded in the Actions viewer:
686
773
  // an ::error annotation instead of a collapsed group.
687
774
  if (outcome.status === 'failed') {
688
- // The message is ours (a literal + an integer); only the title
689
- // carries user input.
775
+ // The message is ours (a literal, an integer and a signal
776
+ // name); only the title carries user input.
690
777
  emitBlock(
691
- `::error title=${ghaProperty(node.id)}::failed (exit ${outcome.exitCode})\n${body}`,
778
+ `::error title=${ghaProperty(node.id)}::${failedLabel(outcome.exitCode, outcome.timedOut, outcome.sandboxViolations, outcome.notReady)}\n${body}`,
692
779
  )
693
780
  } else {
694
781
  emitBlock(