@vzn/vx 0.0.651 → 0.0.653
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.
- package/README.md +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -26,11 +26,18 @@
|
|
|
26
26
|
// `--ui=stream` mode.
|
|
27
27
|
|
|
28
28
|
import { withForwardArgs } from '../exec/index.js'
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
isCacheHit,
|
|
31
|
+
isGroupTask,
|
|
32
|
+
isKeyFailed,
|
|
33
|
+
type TaskNode,
|
|
34
|
+
type TaskOutcome,
|
|
35
|
+
} from '../graph/index.js'
|
|
30
36
|
import { formatElapsed, maskedCommand, printable } from '../util/index.js'
|
|
31
37
|
import * as ui from '../ui/index.js'
|
|
32
38
|
import {
|
|
33
39
|
fileMemo,
|
|
40
|
+
GUTTER,
|
|
34
41
|
hasEscape,
|
|
35
42
|
idParts,
|
|
36
43
|
lastRedraw,
|
|
@@ -99,7 +106,7 @@ export function formatTaskBlock(
|
|
|
99
106
|
// (success and failed). Cache hits normally skip it (the stored
|
|
100
107
|
// output is the interesting part), but `forceCommand` keeps it for
|
|
101
108
|
// focused requested tasks so their frame is hit/miss-identical.
|
|
102
|
-
if (forceCommand || outcome
|
|
109
|
+
if (forceCommand || ranCommand(outcome)) {
|
|
103
110
|
// No section label for the command (owner cut it) — the dim `$ `
|
|
104
111
|
// line under the header reads as the command on its own.
|
|
105
112
|
lines.push('', corner(`$ ${shownCommand(node, forwardArgs)}`), '')
|
|
@@ -120,8 +127,10 @@ export function formatTaskBlock(
|
|
|
120
127
|
|
|
121
128
|
lines.push(
|
|
122
129
|
`${corner('└─')} ${mark}${idPainted}${whereNote(outcome, colors)} ${corner('──')}${running ? runningFooter(outcome, colors) : formatBlockFooter(outcome, colors)}`,
|
|
130
|
+
'',
|
|
123
131
|
)
|
|
124
|
-
|
|
132
|
+
// Not `+ '\n'`: a rope over a 100 MB frame was flattened into a copy.
|
|
133
|
+
return lines.join('\n')
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
/** What a bounded capture dropped from its head, so a capped log never reads as whole. */
|
|
@@ -172,27 +181,41 @@ function pushOutputSection(
|
|
|
172
181
|
if (lost > 0) {
|
|
173
182
|
lines.push(droppedNote(lost, colors))
|
|
174
183
|
}
|
|
175
|
-
const
|
|
184
|
+
const emit = (line: string, err: boolean): void => {
|
|
185
|
+
const text = link === undefined ? line : link(line)
|
|
186
|
+
if (!err || !colors.enabled) lines.push(text)
|
|
187
|
+
else lines.push(hasEscape(line) ? text : paint('error', text, colors))
|
|
188
|
+
}
|
|
189
|
+
// A run of whole lines nothing restyles is pushed as one slice, its
|
|
190
|
+
// newlines inside: an object and a string per line peaked a 50 MB log's
|
|
191
|
+
// frame at ~11x its size.
|
|
176
192
|
let open: { parts: string[]; err: boolean } | null = null
|
|
177
193
|
for (const c of output) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
body.push({ text: open.parts.join(''), err: open.err })
|
|
194
|
+
const text = c.text
|
|
195
|
+
let at = 0
|
|
196
|
+
if (open !== null) {
|
|
197
|
+
const nl = text.indexOf('\n')
|
|
198
|
+
open.parts.push(nl < 0 ? text : text.slice(0, nl))
|
|
199
|
+
if (nl < 0) continue
|
|
200
|
+
emit(open.parts.join(''), open.err)
|
|
186
201
|
open = null
|
|
187
|
-
|
|
202
|
+
at = nl + 1
|
|
188
203
|
}
|
|
204
|
+
if (link === undefined && (!c.err || !colors.enabled)) {
|
|
205
|
+
const last = text.lastIndexOf('\n')
|
|
206
|
+
if (last >= at) {
|
|
207
|
+
lines.push(text.slice(at, last))
|
|
208
|
+
at = last + 1
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
for (let nl = text.indexOf('\n', at); nl >= 0; nl = text.indexOf('\n', at)) {
|
|
212
|
+
emit(text.slice(at, nl), c.err)
|
|
213
|
+
at = nl + 1
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (at < text.length) open = { parts: [text.slice(at)], err: c.err }
|
|
189
217
|
}
|
|
190
|
-
if (open !== null)
|
|
191
|
-
for (const l of body) {
|
|
192
|
-
const text = link === undefined ? l.text : link(l.text)
|
|
193
|
-
if (!l.err || !colors.enabled) lines.push(text)
|
|
194
|
-
else lines.push(hasEscape(l.text) ? text : paint('error', text, colors))
|
|
195
|
-
}
|
|
218
|
+
if (open !== null) emit(open.parts.join(''), open.err)
|
|
196
219
|
const styled = output.findLast((c) => hasEscape(c.text))
|
|
197
220
|
if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${STYLE_END}`)
|
|
198
221
|
lines.push('')
|
|
@@ -251,14 +274,13 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
|
|
|
251
274
|
return paintId(node.projectName, node.taskName, colors, opts)
|
|
252
275
|
}
|
|
253
276
|
|
|
254
|
-
// ── Reported-line grid:
|
|
255
|
-
// glyph
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// the two axes out, each in its own color. Time is right-aligned in a
|
|
277
|
+
// ── Reported-line grid: status · cache · usual · time · … · name ────────
|
|
278
|
+
// No glyph (2026-11-06): the status word, first at the Summary's value
|
|
279
|
+
// column, is the task axis and the cache word the cache axis, each in
|
|
280
|
+
// its own color. Time is right-aligned in a
|
|
259
281
|
// fixed cell so durations line up and a ticking elapsed never shifts
|
|
260
282
|
// the row. All detail (exit code, output) lives in the framed block.
|
|
261
|
-
|
|
283
|
+
const TIME_COL = 7 // "0ms" … "999.99s", then "16m 40s" … "99m 59s"
|
|
262
284
|
const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
|
|
263
285
|
const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
|
|
264
286
|
const BLOCKED_COL = 7 // `blocks`: how many tasks are downstream of the row's
|
|
@@ -268,13 +290,23 @@ const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
|
|
|
268
290
|
const OUT_COL = 7 // `outputs`: "12MB", "900B"
|
|
269
291
|
const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
|
|
270
292
|
|
|
293
|
+
// A row's padding, built once: `' '.repeat` per cell was ~7 ms of an
|
|
294
|
+
// 8,002-hit warm run's rows, the largest single cost of writing them.
|
|
295
|
+
const SPACES = Array.from({ length: 33 }, (_, n) => ' '.repeat(n))
|
|
296
|
+
|
|
297
|
+
/** `n` spaces (none below 1). */
|
|
298
|
+
function pad(n: number): string {
|
|
299
|
+
return n <= 0 ? '' : (SPACES[n] ?? ' '.repeat(n))
|
|
300
|
+
}
|
|
301
|
+
|
|
271
302
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
272
303
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
273
|
-
|
|
304
|
+
if (ms === null) return pad(TIME_COL - 1) + paint('muted', UNKNOWN, colors)
|
|
305
|
+
let raw = formatElapsed(ms)
|
|
274
306
|
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
275
307
|
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
276
308
|
// The time is the row's fact (value level); the cells around it are meta.
|
|
277
|
-
return
|
|
309
|
+
return pad(TIME_COL - raw.length) + paint('value', raw, colors)
|
|
278
310
|
}
|
|
279
311
|
|
|
280
312
|
/**
|
|
@@ -284,25 +316,7 @@ function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
|
284
316
|
*/
|
|
285
317
|
function cell(text: string, color: Style | '', width: number, colors: ColorSupport): string {
|
|
286
318
|
const painted = text === '' ? '' : paint(color === '' ? 'muted' : color, text, colors)
|
|
287
|
-
return painted +
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** The cache-shape glyph (task-shape for non-success). */
|
|
291
|
-
function glyphShape(o: TaskOutcome): string {
|
|
292
|
-
switch (o.status) {
|
|
293
|
-
case 'success':
|
|
294
|
-
return '⏺\uFE0E' // ⏺ miss (ran), text-presentation (narrow)
|
|
295
|
-
case 'cache-hit':
|
|
296
|
-
return o.restored === false ? '\u25ba' : '\u21e2' // ► fresh : ⇢ local
|
|
297
|
-
case 'cache-hit-remote':
|
|
298
|
-
return o.restored === false ? '\u25ba' : '\u21e3' // ► fresh : ⇣ remote
|
|
299
|
-
case 'failed':
|
|
300
|
-
return '\u25fc\uFE0E' // ◼ failed, text-presentation (narrow)
|
|
301
|
-
case 'skipped':
|
|
302
|
-
return '\u2298' // ⊘ skipped
|
|
303
|
-
default:
|
|
304
|
-
return '⏺\uFE0E'
|
|
305
|
-
}
|
|
319
|
+
return painted + pad(width - text.length)
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
/** Task-axis word + color (a cache hit is a success). */
|
|
@@ -317,8 +331,19 @@ function statusOf(o: TaskOutcome): { word: string; color: Style } {
|
|
|
317
331
|
}
|
|
318
332
|
}
|
|
319
333
|
|
|
320
|
-
/**
|
|
334
|
+
/** Whether the task ran its command: a hit whose restore failed did not, nor a task with no key. */
|
|
335
|
+
function ranCommand(o: TaskOutcome): boolean {
|
|
336
|
+
return (
|
|
337
|
+
o.status === 'success' || (o.status === 'failed' && o.restoreFailed !== true && !isKeyFailed(o))
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Cache-axis word + color. Empty for states that never reached the cache,
|
|
343
|
+
* a hit whose restore failed (neither a miss nor a hit) and a task with no key.
|
|
344
|
+
*/
|
|
321
345
|
function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
346
|
+
if (o.restoreFailed === true || isKeyFailed(o)) return { word: '', color: 'muted' }
|
|
322
347
|
switch (o.status) {
|
|
323
348
|
case 'success':
|
|
324
349
|
case 'failed':
|
|
@@ -337,20 +362,14 @@ function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
|
337
362
|
}
|
|
338
363
|
}
|
|
339
364
|
|
|
340
|
-
/** The painted status glyph (shape = cache axis, color = task axis). */
|
|
341
|
-
function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
|
|
342
|
-
return paint(statusOf(o).color, glyphShape(o), colors)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
365
|
/**
|
|
346
|
-
* One reported task row on the list grid: `<
|
|
347
|
-
* <
|
|
348
|
-
*
|
|
366
|
+
* One reported task row on the list grid: `<status> <cache> <usual>
|
|
367
|
+
* <time> <cpu> <mem> <outputs> <blocks> <worker> <mark> <name>`, the status at
|
|
368
|
+
* the Summary's value column, no glyph (2026-11-06). Status/cache are raw text + color, padded to their
|
|
349
369
|
* columns; `paintedId` carries identity coloring. `ms = null` blanks the
|
|
350
370
|
* time (a skip never ran); `cache = ''` and an absent cell blank theirs.
|
|
351
371
|
*/
|
|
352
372
|
export function formatTaskRow(
|
|
353
|
-
glyph: string,
|
|
354
373
|
ms: number | null,
|
|
355
374
|
status: string,
|
|
356
375
|
statusColor: Style | '',
|
|
@@ -360,35 +379,43 @@ export function formatTaskRow(
|
|
|
360
379
|
colors: ColorSupport = NO_COLOR,
|
|
361
380
|
cells: RowCells = {},
|
|
362
381
|
): string {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
const
|
|
382
|
+
// Every cell says something: one with nothing in it reads `—` (2026-11-06).
|
|
383
|
+
const st = cell(status || UNKNOWN, statusColor, STATUS_COL, colors)
|
|
384
|
+
// `aborting` runs one into the cache cell.
|
|
385
|
+
const ca = cell(
|
|
386
|
+
cache || UNKNOWN,
|
|
387
|
+
cache === '' ? '' : cacheColor,
|
|
388
|
+
CACHE_COL - Math.max(0, status.length - STATUS_COL),
|
|
389
|
+
colors,
|
|
390
|
+
)
|
|
391
|
+
const usual =
|
|
392
|
+
cells.usual === undefined || cells.usual.text === '' ? unknownUsual(colors) : cells.usual
|
|
393
|
+
const us = `${pad(USUAL_COL - usual.text.length)}${usual.painted}`
|
|
394
|
+
const cpu = cells.cpu == null ? UNKNOWN : `${Math.round(cells.cpu * 100)}%`
|
|
395
|
+
const mem = cells.memBytes == null ? UNKNOWN : formatSize(cells.memBytes)
|
|
396
|
+
const out = cells.outputBytes == null ? UNKNOWN : formatSize(cells.outputBytes)
|
|
397
|
+
const blocks = cells.blocks || UNKNOWN
|
|
398
|
+
const worker = workerName(cells.worker || UNKNOWN)
|
|
379
399
|
return (
|
|
380
|
-
|
|
400
|
+
`${ROW_LEAD}${st} ${ca} ${us} ${timeCell(ms, colors)} ` +
|
|
381
401
|
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
382
|
-
`${
|
|
383
|
-
`${placeCell(worker, colors)}${
|
|
402
|
+
`${pad(BLOCKED_COL - blocks.length)}${paint(blocks === '0' || blocks === UNKNOWN ? 'muted' : 'value', blocks, colors)} ` +
|
|
403
|
+
`${placeCell(worker, colors)}${pad(WORKER_COL - worker.length)} ` +
|
|
404
|
+
`${cells.critical === true ? paint('warn', CRITICAL_MARK, colors) : ' '} ${paintedId}`
|
|
384
405
|
)
|
|
385
406
|
}
|
|
386
407
|
|
|
408
|
+
/** A cell with nothing in it: unknown, or not applying to the row (a hit ran nothing). */
|
|
409
|
+
const UNKNOWN = '\u2014'
|
|
410
|
+
|
|
387
411
|
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
412
|
+
* The critical path's mark, before the task's name: the row's task held the
|
|
413
|
+
* run's end while others ran beside it. One glyph, yellow (2026-11-06).
|
|
390
414
|
*/
|
|
391
|
-
const
|
|
415
|
+
const CRITICAL_MARK = '\u25c6'
|
|
416
|
+
|
|
417
|
+
/** A grid row's lead: the gutter, so a row starts where a field's value does. */
|
|
418
|
+
const ROW_LEAD = ' '.repeat(GUTTER)
|
|
392
419
|
|
|
393
420
|
/** A row's cells past time, status and cache; each blank when absent, `—` when null. */
|
|
394
421
|
export interface RowCells {
|
|
@@ -399,15 +426,17 @@ export interface RowCells {
|
|
|
399
426
|
/** Resident memory now (live) or at its peak (after). */
|
|
400
427
|
memBytes?: number | null
|
|
401
428
|
/** The bytes of the task's outputs: what its save stored, or its hit restored. */
|
|
402
|
-
outputBytes?: number
|
|
429
|
+
outputBytes?: number | null
|
|
403
430
|
/** How many tasks are downstream. */
|
|
404
431
|
blocks?: string
|
|
405
432
|
/** The machine it runs or ran on: `local`, or the executor's name for it. */
|
|
406
433
|
worker?: string
|
|
434
|
+
/** On the critical path: `◆` before the task's name. */
|
|
435
|
+
critical?: boolean
|
|
407
436
|
}
|
|
408
437
|
|
|
409
438
|
function right(text: string, width: number, colors: ColorSupport): string {
|
|
410
|
-
return
|
|
439
|
+
return pad(width - text.length) + paint('muted', text, colors)
|
|
411
440
|
}
|
|
412
441
|
|
|
413
442
|
/** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
|
|
@@ -426,7 +455,7 @@ export function formatSize(bytes: number): string {
|
|
|
426
455
|
const placeCells = new WeakMap<ColorSupport, Map<string, string>>()
|
|
427
456
|
|
|
428
457
|
function placeCell(worker: string, colors: ColorSupport): string {
|
|
429
|
-
if (worker ===
|
|
458
|
+
if (worker === UNKNOWN) return paint('muted', UNKNOWN, colors)
|
|
430
459
|
let byName = placeCells.get(colors)
|
|
431
460
|
if (byName === undefined) placeCells.set(colors, (byName = new Map()))
|
|
432
461
|
let painted = byName.get(worker)
|
|
@@ -488,10 +517,10 @@ export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
|
|
|
488
517
|
|
|
489
518
|
function header(usual: string, colors: ColorSupport): string {
|
|
490
519
|
const head =
|
|
491
|
-
|
|
492
|
-
`${'
|
|
493
|
-
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)}
|
|
494
|
-
return paint('
|
|
520
|
+
`${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${usual.padStart(USUAL_COL)} ` +
|
|
521
|
+
`${'time'.padStart(TIME_COL)} ${'cpu'.padStart(CPU_COL)} ` +
|
|
522
|
+
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
|
|
523
|
+
return paint('muted', head, colors)
|
|
495
524
|
}
|
|
496
525
|
|
|
497
526
|
/**
|
|
@@ -499,7 +528,7 @@ function header(usual: string, colors: ColorSupport): string {
|
|
|
499
528
|
* body, and `vx last`'s, so a replay reads as the run did.
|
|
500
529
|
*/
|
|
501
530
|
export function taskList(rows: readonly ((colors: ColorSupport) => string)[]): ui.Block {
|
|
502
|
-
return { lines: (c) =>
|
|
531
|
+
return { head: formatListHeader, lines: (c) => rows.map((row) => row(c)) }
|
|
503
532
|
}
|
|
504
533
|
|
|
505
534
|
/** What history keeps of a task's row: `vx last` draws it again from this. */
|
|
@@ -508,6 +537,8 @@ export interface RecordedRow {
|
|
|
508
537
|
task: string
|
|
509
538
|
status: string
|
|
510
539
|
durationMs: number
|
|
540
|
+
/** Null on a cached task's failure: its hit's restore failed. */
|
|
541
|
+
cacheHit: boolean | null
|
|
511
542
|
restored: boolean | null
|
|
512
543
|
cached: boolean | null
|
|
513
544
|
cpuMs: number | null
|
|
@@ -532,12 +563,15 @@ export function formatRecordedRow(
|
|
|
532
563
|
id: `${r.project}#${r.task}`,
|
|
533
564
|
projectName: r.project,
|
|
534
565
|
taskName: r.task,
|
|
535
|
-
config: r.cached === false ? {} : { cache:
|
|
566
|
+
config: r.cached === false ? {} : { cache: true },
|
|
536
567
|
} as unknown as TaskNode
|
|
537
568
|
const o = {
|
|
538
569
|
node,
|
|
539
570
|
status: r.status,
|
|
540
571
|
durationMs: r.durationMs,
|
|
572
|
+
...(r.status === 'failed' && r.cached === true && r.cacheHit === null
|
|
573
|
+
? { restoreFailed: true }
|
|
574
|
+
: {}),
|
|
541
575
|
...(r.restored !== null ? { restored: r.restored } : {}),
|
|
542
576
|
...(r.cpuMs !== null ? { cpuMs: r.cpuMs } : {}),
|
|
543
577
|
...(r.peakRssBytes !== null ? { peakRssBytes: r.peakRssBytes } : {}),
|
|
@@ -573,14 +607,13 @@ export function formatExpected(ms: number): string {
|
|
|
573
607
|
/** A task `--dry` lists: what the run would do with it, on the running list's grid. */
|
|
574
608
|
export interface PlannedRow {
|
|
575
609
|
node: TaskNode
|
|
576
|
-
glyph: string
|
|
577
610
|
status: string
|
|
578
|
-
/** The
|
|
611
|
+
/** The status word's hue. */
|
|
579
612
|
tone: Style
|
|
580
613
|
cache: string
|
|
581
614
|
cacheTone: Style | ''
|
|
582
|
-
/** Its usual time, from history. */
|
|
583
|
-
usualMs?: number
|
|
615
|
+
/** Its usual time, from history; null for a task that runs and history never timed. */
|
|
616
|
+
usualMs?: number | null
|
|
584
617
|
/** What a remote hit would download. */
|
|
585
618
|
outputBytes?: number
|
|
586
619
|
/** How many tasks are downstream of it. */
|
|
@@ -593,12 +626,16 @@ export interface PlannedRow {
|
|
|
593
626
|
/** `--dry`'s list: the running list's header and grid, so a plan reads as the run would. */
|
|
594
627
|
export function planList(rows: readonly PlannedRow[]): ui.Block {
|
|
595
628
|
return {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const usual =
|
|
629
|
+
head: formatRunningHeader,
|
|
630
|
+
lines: (c) =>
|
|
631
|
+
rows.map((r) => {
|
|
632
|
+
const usual =
|
|
633
|
+
r.usualMs === undefined
|
|
634
|
+
? undefined
|
|
635
|
+
: r.usualMs === null
|
|
636
|
+
? UNKNOWN
|
|
637
|
+
: formatExpected(r.usualMs)
|
|
600
638
|
const row = formatTaskRow(
|
|
601
|
-
paint(r.tone, r.glyph, c),
|
|
602
639
|
null,
|
|
603
640
|
r.status,
|
|
604
641
|
r.tone,
|
|
@@ -617,7 +654,6 @@ export function planList(rows: readonly PlannedRow[]): ui.Block {
|
|
|
617
654
|
)
|
|
618
655
|
return r.note === '' ? row : `${row} ${paint('muted', r.note, c)}`
|
|
619
656
|
}),
|
|
620
|
-
],
|
|
621
657
|
}
|
|
622
658
|
}
|
|
623
659
|
|
|
@@ -629,9 +665,14 @@ export function formatTaskHitLine(
|
|
|
629
665
|
onPath = false,
|
|
630
666
|
blocks?: number,
|
|
631
667
|
): string {
|
|
632
|
-
return formatOutcomeRow(
|
|
633
|
-
|
|
634
|
-
|
|
668
|
+
return formatOutcomeRow(
|
|
669
|
+
o,
|
|
670
|
+
paintTaskId(node, colors),
|
|
671
|
+
o.durationMs,
|
|
672
|
+
colors,
|
|
673
|
+
onPath,
|
|
674
|
+
ranCells(o, blocks),
|
|
675
|
+
)
|
|
635
676
|
}
|
|
636
677
|
|
|
637
678
|
/**
|
|
@@ -658,13 +699,10 @@ export function formatTaskExecutedLine(
|
|
|
658
699
|
blocks?: number,
|
|
659
700
|
usualMs?: number | null,
|
|
660
701
|
): string {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
? { usual: usualCell(o.durationMs, usualMs, colors) }
|
|
666
|
-
: {}),
|
|
667
|
-
})
|
|
702
|
+
const cells = executedCells(o, blocks)
|
|
703
|
+
// A hit's time is a restore's, not the task's: nothing to compare.
|
|
704
|
+
if (ranCommand(o)) cells.usual = usualCell(o.durationMs, usualMs, colors)
|
|
705
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, cells)
|
|
668
706
|
}
|
|
669
707
|
|
|
670
708
|
/**
|
|
@@ -673,26 +711,28 @@ export function formatTaskExecutedLine(
|
|
|
673
711
|
*/
|
|
674
712
|
export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
675
713
|
const cores = coresOf(o)
|
|
676
|
-
const ran = o
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
? { memBytes: null }
|
|
684
|
-
: {}),
|
|
685
|
-
}
|
|
714
|
+
const ran = ranCommand(o)
|
|
715
|
+
const cells = ranCells(o, blocks)
|
|
716
|
+
if (cores !== undefined) cells.cpu = cores
|
|
717
|
+
else if (ran) cells.cpu = null
|
|
718
|
+
if (o.peakRssBytes !== undefined) cells.memBytes = o.peakRssBytes
|
|
719
|
+
else if (ran) cells.memBytes = null
|
|
720
|
+
return cells
|
|
686
721
|
}
|
|
687
722
|
|
|
688
|
-
/**
|
|
723
|
+
/**
|
|
724
|
+
* The cells every row of a task that reached a machine carries. Assigned,
|
|
725
|
+
* not spread: a warm run builds one per hit, and the spreads, copied again
|
|
726
|
+
* by the caller, were ~4 ms of 8,002 hits' rows (2026-11-07).
|
|
727
|
+
*/
|
|
689
728
|
function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
729
|
+
const cells: RowCells = {}
|
|
730
|
+
// A skip reached no machine.
|
|
731
|
+
if (o.status !== 'skipped') cells.worker = o.where ?? 'local'
|
|
732
|
+
if (blocks !== undefined) cells.blocks = String(blocks)
|
|
733
|
+
// A row with no outputs size says so (`—`); blank read as a column left out.
|
|
734
|
+
if (o.status !== 'skipped') cells.outputBytes = o.outputBytes ?? null
|
|
735
|
+
return cells
|
|
696
736
|
}
|
|
697
737
|
|
|
698
738
|
/** Shared: render any outcome on the grid. */
|
|
@@ -708,7 +748,6 @@ function formatOutcomeRow(
|
|
|
708
748
|
const ca = cacheOf(o)
|
|
709
749
|
return (
|
|
710
750
|
formatTaskRow(
|
|
711
|
-
taskGlyph(o, colors),
|
|
712
751
|
ms,
|
|
713
752
|
st.word,
|
|
714
753
|
st.color,
|
|
@@ -716,19 +755,11 @@ function formatOutcomeRow(
|
|
|
716
755
|
ca.color,
|
|
717
756
|
paintedId,
|
|
718
757
|
colors,
|
|
719
|
-
cells,
|
|
720
|
-
) +
|
|
721
|
-
(onPath ? pathNote(colors) : '') +
|
|
722
|
-
flakyNote(o, colors)
|
|
758
|
+
onPath ? { ...cells, critical: true } : cells,
|
|
759
|
+
) + flakyNote(o, colors)
|
|
723
760
|
)
|
|
724
761
|
}
|
|
725
762
|
|
|
726
|
-
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
727
|
-
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
728
|
-
// A fact worth knowing, not a failure: never red.
|
|
729
|
-
return ` ${paint('info', 'critical path', colors)}`
|
|
730
|
-
}
|
|
731
|
-
|
|
732
763
|
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
733
764
|
function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
734
765
|
return o.where === undefined ? '' : placeNote(o.where, colors)
|
|
@@ -745,10 +776,14 @@ function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
|
745
776
|
/**
|
|
746
777
|
* The dim note a flaky task's row and frame carry (`flaky - failed 1×
|
|
747
778
|
* before`): nothing prints below the footer, so the task's own line is
|
|
748
|
-
* where the reader learns it.
|
|
779
|
+
* where the reader learns it. Not proved flaky: its attempts, when more
|
|
780
|
+
* than one; else ''.
|
|
749
781
|
*/
|
|
750
782
|
export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
751
|
-
|
|
783
|
+
// A retry with no key to prove a flake (no `cache: true`) still ran:
|
|
784
|
+
// the row is the only place a live run says so (`vx last` did).
|
|
785
|
+
if (o.flaky === undefined)
|
|
786
|
+
return (o.attempts ?? 1) > 1 ? ` ${paint('muted', `${o.attempts} attempts`, colors)}` : ''
|
|
752
787
|
const said: string[] = []
|
|
753
788
|
if (o.status === 'failed' && o.flaky.passes > 0)
|
|
754
789
|
said.push(`passed ${o.flaky.passes}\u00d7 before`)
|
|
@@ -775,7 +810,6 @@ export function formatTaskSkippedLine(
|
|
|
775
810
|
const suffix = why === '' ? '' : ` ${paint('muted', `• ${why}`, colors)}`
|
|
776
811
|
return (
|
|
777
812
|
formatTaskRow(
|
|
778
|
-
paint('warn', '\u2298', colors),
|
|
779
813
|
null,
|
|
780
814
|
'skipped',
|
|
781
815
|
'warn',
|
|
@@ -802,29 +836,9 @@ export function formatTaskAbortedLine(
|
|
|
802
836
|
const id = paintTaskId(node, colors)
|
|
803
837
|
const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
|
|
804
838
|
if (neverStarted(o)) {
|
|
805
|
-
return formatTaskRow(
|
|
806
|
-
paint('muted', '\u25cc', colors),
|
|
807
|
-
null,
|
|
808
|
-
'not run',
|
|
809
|
-
'',
|
|
810
|
-
'',
|
|
811
|
-
'',
|
|
812
|
-
id,
|
|
813
|
-
colors,
|
|
814
|
-
bl,
|
|
815
|
-
)
|
|
839
|
+
return formatTaskRow(null, 'not run', '', '', '', id, colors, bl)
|
|
816
840
|
}
|
|
817
|
-
return formatTaskRow(
|
|
818
|
-
paint('aborted', '\u2717', colors),
|
|
819
|
-
o.durationMs,
|
|
820
|
-
'aborted',
|
|
821
|
-
'aborted',
|
|
822
|
-
'',
|
|
823
|
-
'',
|
|
824
|
-
id,
|
|
825
|
-
colors,
|
|
826
|
-
ranCells(o, blocks),
|
|
827
|
-
)
|
|
841
|
+
return formatTaskRow(o.durationMs, 'aborted', 'aborted', '', '', id, colors, ranCells(o, blocks))
|
|
828
842
|
}
|
|
829
843
|
|
|
830
844
|
/**
|
|
@@ -880,7 +894,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
880
894
|
...(fix.inputs !== undefined
|
|
881
895
|
? [
|
|
882
896
|
dim(
|
|
883
|
-
`and, if the output depends on them, inputs.files: [${fix.inputs.map(
|
|
897
|
+
`and, if the output depends on them, inputs.files: [${fix.inputs.map(jsString).join(', ')}]`,
|
|
884
898
|
),
|
|
885
899
|
]
|
|
886
900
|
: []),
|
|
@@ -1053,6 +1067,9 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
1053
1067
|
}
|
|
1054
1068
|
}
|
|
1055
1069
|
|
|
1070
|
+
/** A fix path as the single-quoted JS literal the config line pastes. */
|
|
1071
|
+
const jsString = (p: string): string => `'${printable(p.replace(/[\\']/g, '\\$&'))}'`
|
|
1072
|
+
|
|
1056
1073
|
/**
|
|
1057
1074
|
* The fix as the config line it is: `exec.sandbox.allow: { read: ['a'] }`,
|
|
1058
1075
|
* or undefined when every denial was a withheld credential path.
|
|
@@ -1061,7 +1078,7 @@ function sandboxFixLine(f: NonNullable<TaskOutcome['sandboxFix']>): string | und
|
|
|
1061
1078
|
const fix = Object.entries(f).filter(([k]) => k !== 'withheld' && k !== 'inputs')
|
|
1062
1079
|
if (fix.length === 0) return undefined
|
|
1063
1080
|
const parts = fix.map(
|
|
1064
|
-
([k, list]: [string, string[]]) => `${k}: [${list.map(
|
|
1081
|
+
([k, list]: [string, string[]]) => `${k}: [${list.map(jsString).join(', ')}]`,
|
|
1065
1082
|
)
|
|
1066
1083
|
return `exec.sandbox.allow: { ${parts.join(', ')} }`
|
|
1067
1084
|
}
|
package/src/report/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export {
|
|
|
33
33
|
export {
|
|
34
34
|
formatPersistentList,
|
|
35
35
|
formatRecordedRow,
|
|
36
|
+
formatSize,
|
|
36
37
|
planList,
|
|
37
38
|
type PlannedRow,
|
|
38
39
|
taskList,
|
|
@@ -40,4 +41,4 @@ export {
|
|
|
40
41
|
export { createForecast } from './forecast.js'
|
|
41
42
|
export { plainOutput } from './plain-output.js'
|
|
42
43
|
export type { LiveUsage } from './status-line.js'
|
|
43
|
-
export { formatOutcomeTable, formatRunSummary } from './summary.js'
|
|
44
|
+
export { formatOutcomeTable, formatRunSummary, neverStarted } from './summary.js'
|