@vzn/vx 0.0.652 → 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 +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- 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 +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- 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 +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- 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 +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- 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 +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- 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 +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- 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-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- 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 +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- 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 +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- 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 +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -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/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- 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 +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- 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 +17 -3
- 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 +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- 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 +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- 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 +46 -25
- 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
|
|
@@ -279,7 +301,8 @@ function pad(n: number): string {
|
|
|
279
301
|
|
|
280
302
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
281
303
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
282
|
-
|
|
304
|
+
if (ms === null) return pad(TIME_COL - 1) + paint('muted', UNKNOWN, colors)
|
|
305
|
+
let raw = formatElapsed(ms)
|
|
283
306
|
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
284
307
|
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
285
308
|
// The time is the row's fact (value level); the cells around it are meta.
|
|
@@ -296,24 +319,6 @@ function cell(text: string, color: Style | '', width: number, colors: ColorSuppo
|
|
|
296
319
|
return painted + pad(width - text.length)
|
|
297
320
|
}
|
|
298
321
|
|
|
299
|
-
/** The cache-shape glyph (task-shape for non-success). */
|
|
300
|
-
function glyphShape(o: TaskOutcome): string {
|
|
301
|
-
switch (o.status) {
|
|
302
|
-
case 'success':
|
|
303
|
-
return '⏺\uFE0E' // ⏺ miss (ran), text-presentation (narrow)
|
|
304
|
-
case 'cache-hit':
|
|
305
|
-
return o.restored === false ? '\u25ba' : '\u21e2' // ► fresh : ⇢ local
|
|
306
|
-
case 'cache-hit-remote':
|
|
307
|
-
return o.restored === false ? '\u25ba' : '\u21e3' // ► fresh : ⇣ remote
|
|
308
|
-
case 'failed':
|
|
309
|
-
return '\u25fc\uFE0E' // ◼ failed, text-presentation (narrow)
|
|
310
|
-
case 'skipped':
|
|
311
|
-
return '\u2298' // ⊘ skipped
|
|
312
|
-
default:
|
|
313
|
-
return '⏺\uFE0E'
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
322
|
/** Task-axis word + color (a cache hit is a success). */
|
|
318
323
|
function statusOf(o: TaskOutcome): { word: string; color: Style } {
|
|
319
324
|
switch (o.status) {
|
|
@@ -326,8 +331,19 @@ function statusOf(o: TaskOutcome): { word: string; color: Style } {
|
|
|
326
331
|
}
|
|
327
332
|
}
|
|
328
333
|
|
|
329
|
-
/**
|
|
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
|
+
*/
|
|
330
345
|
function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
346
|
+
if (o.restoreFailed === true || isKeyFailed(o)) return { word: '', color: 'muted' }
|
|
331
347
|
switch (o.status) {
|
|
332
348
|
case 'success':
|
|
333
349
|
case 'failed':
|
|
@@ -346,20 +362,14 @@ function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
|
346
362
|
}
|
|
347
363
|
}
|
|
348
364
|
|
|
349
|
-
/** The painted status glyph (shape = cache axis, color = task axis). */
|
|
350
|
-
function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
|
|
351
|
-
return paint(statusOf(o).color, glyphShape(o), colors)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
365
|
/**
|
|
355
|
-
* One reported task row on the list grid: `<
|
|
356
|
-
* <
|
|
357
|
-
*
|
|
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
|
|
358
369
|
* columns; `paintedId` carries identity coloring. `ms = null` blanks the
|
|
359
370
|
* time (a skip never ran); `cache = ''` and an absent cell blank theirs.
|
|
360
371
|
*/
|
|
361
372
|
export function formatTaskRow(
|
|
362
|
-
glyph: string,
|
|
363
373
|
ms: number | null,
|
|
364
374
|
status: string,
|
|
365
375
|
statusColor: Style | '',
|
|
@@ -369,35 +379,43 @@ export function formatTaskRow(
|
|
|
369
379
|
colors: ColorSupport = NO_COLOR,
|
|
370
380
|
cells: RowCells = {},
|
|
371
381
|
): string {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
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
|
|
376
393
|
const us = `${pad(USUAL_COL - usual.text.length)}${usual.painted}`
|
|
377
|
-
const cpu =
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
: cells.memBytes === null
|
|
383
|
-
? UNKNOWN
|
|
384
|
-
: formatSize(cells.memBytes)
|
|
385
|
-
const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
|
|
386
|
-
const blocks = cells.blocks ?? ''
|
|
387
|
-
const worker = workerName(cells.worker ?? '')
|
|
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)
|
|
388
399
|
return (
|
|
389
|
-
|
|
400
|
+
`${ROW_LEAD}${st} ${ca} ${us} ${timeCell(ms, colors)} ` +
|
|
390
401
|
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
391
|
-
`${pad(BLOCKED_COL - blocks.length)}${paint(blocks === '0' ? 'muted' : 'value', blocks, colors)} ` +
|
|
392
|
-
`${placeCell(worker, colors)}${pad(WORKER_COL - worker.length)}
|
|
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}`
|
|
393
405
|
)
|
|
394
406
|
}
|
|
395
407
|
|
|
408
|
+
/** A cell with nothing in it: unknown, or not applying to the row (a hit ran nothing). */
|
|
409
|
+
const UNKNOWN = '\u2014'
|
|
410
|
+
|
|
396
411
|
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
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).
|
|
399
414
|
*/
|
|
400
|
-
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)
|
|
401
419
|
|
|
402
420
|
/** A row's cells past time, status and cache; each blank when absent, `—` when null. */
|
|
403
421
|
export interface RowCells {
|
|
@@ -408,11 +426,13 @@ export interface RowCells {
|
|
|
408
426
|
/** Resident memory now (live) or at its peak (after). */
|
|
409
427
|
memBytes?: number | null
|
|
410
428
|
/** The bytes of the task's outputs: what its save stored, or its hit restored. */
|
|
411
|
-
outputBytes?: number
|
|
429
|
+
outputBytes?: number | null
|
|
412
430
|
/** How many tasks are downstream. */
|
|
413
431
|
blocks?: string
|
|
414
432
|
/** The machine it runs or ran on: `local`, or the executor's name for it. */
|
|
415
433
|
worker?: string
|
|
434
|
+
/** On the critical path: `◆` before the task's name. */
|
|
435
|
+
critical?: boolean
|
|
416
436
|
}
|
|
417
437
|
|
|
418
438
|
function right(text: string, width: number, colors: ColorSupport): string {
|
|
@@ -435,7 +455,7 @@ export function formatSize(bytes: number): string {
|
|
|
435
455
|
const placeCells = new WeakMap<ColorSupport, Map<string, string>>()
|
|
436
456
|
|
|
437
457
|
function placeCell(worker: string, colors: ColorSupport): string {
|
|
438
|
-
if (worker ===
|
|
458
|
+
if (worker === UNKNOWN) return paint('muted', UNKNOWN, colors)
|
|
439
459
|
let byName = placeCells.get(colors)
|
|
440
460
|
if (byName === undefined) placeCells.set(colors, (byName = new Map()))
|
|
441
461
|
let painted = byName.get(worker)
|
|
@@ -497,10 +517,10 @@ export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
|
|
|
497
517
|
|
|
498
518
|
function header(usual: string, colors: ColorSupport): string {
|
|
499
519
|
const head =
|
|
500
|
-
|
|
501
|
-
`${'
|
|
502
|
-
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)}
|
|
503
|
-
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)
|
|
504
524
|
}
|
|
505
525
|
|
|
506
526
|
/**
|
|
@@ -508,7 +528,7 @@ function header(usual: string, colors: ColorSupport): string {
|
|
|
508
528
|
* body, and `vx last`'s, so a replay reads as the run did.
|
|
509
529
|
*/
|
|
510
530
|
export function taskList(rows: readonly ((colors: ColorSupport) => string)[]): ui.Block {
|
|
511
|
-
return { lines: (c) =>
|
|
531
|
+
return { head: formatListHeader, lines: (c) => rows.map((row) => row(c)) }
|
|
512
532
|
}
|
|
513
533
|
|
|
514
534
|
/** What history keeps of a task's row: `vx last` draws it again from this. */
|
|
@@ -517,6 +537,8 @@ export interface RecordedRow {
|
|
|
517
537
|
task: string
|
|
518
538
|
status: string
|
|
519
539
|
durationMs: number
|
|
540
|
+
/** Null on a cached task's failure: its hit's restore failed. */
|
|
541
|
+
cacheHit: boolean | null
|
|
520
542
|
restored: boolean | null
|
|
521
543
|
cached: boolean | null
|
|
522
544
|
cpuMs: number | null
|
|
@@ -547,6 +569,9 @@ export function formatRecordedRow(
|
|
|
547
569
|
node,
|
|
548
570
|
status: r.status,
|
|
549
571
|
durationMs: r.durationMs,
|
|
572
|
+
...(r.status === 'failed' && r.cached === true && r.cacheHit === null
|
|
573
|
+
? { restoreFailed: true }
|
|
574
|
+
: {}),
|
|
550
575
|
...(r.restored !== null ? { restored: r.restored } : {}),
|
|
551
576
|
...(r.cpuMs !== null ? { cpuMs: r.cpuMs } : {}),
|
|
552
577
|
...(r.peakRssBytes !== null ? { peakRssBytes: r.peakRssBytes } : {}),
|
|
@@ -582,14 +607,13 @@ export function formatExpected(ms: number): string {
|
|
|
582
607
|
/** A task `--dry` lists: what the run would do with it, on the running list's grid. */
|
|
583
608
|
export interface PlannedRow {
|
|
584
609
|
node: TaskNode
|
|
585
|
-
glyph: string
|
|
586
610
|
status: string
|
|
587
|
-
/** The
|
|
611
|
+
/** The status word's hue. */
|
|
588
612
|
tone: Style
|
|
589
613
|
cache: string
|
|
590
614
|
cacheTone: Style | ''
|
|
591
|
-
/** Its usual time, from history. */
|
|
592
|
-
usualMs?: number
|
|
615
|
+
/** Its usual time, from history; null for a task that runs and history never timed. */
|
|
616
|
+
usualMs?: number | null
|
|
593
617
|
/** What a remote hit would download. */
|
|
594
618
|
outputBytes?: number
|
|
595
619
|
/** How many tasks are downstream of it. */
|
|
@@ -602,12 +626,16 @@ export interface PlannedRow {
|
|
|
602
626
|
/** `--dry`'s list: the running list's header and grid, so a plan reads as the run would. */
|
|
603
627
|
export function planList(rows: readonly PlannedRow[]): ui.Block {
|
|
604
628
|
return {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
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)
|
|
609
638
|
const row = formatTaskRow(
|
|
610
|
-
paint(r.tone, r.glyph, c),
|
|
611
639
|
null,
|
|
612
640
|
r.status,
|
|
613
641
|
r.tone,
|
|
@@ -626,7 +654,6 @@ export function planList(rows: readonly PlannedRow[]): ui.Block {
|
|
|
626
654
|
)
|
|
627
655
|
return r.note === '' ? row : `${row} ${paint('muted', r.note, c)}`
|
|
628
656
|
}),
|
|
629
|
-
],
|
|
630
657
|
}
|
|
631
658
|
}
|
|
632
659
|
|
|
@@ -638,9 +665,14 @@ export function formatTaskHitLine(
|
|
|
638
665
|
onPath = false,
|
|
639
666
|
blocks?: number,
|
|
640
667
|
): string {
|
|
641
|
-
return formatOutcomeRow(
|
|
642
|
-
|
|
643
|
-
|
|
668
|
+
return formatOutcomeRow(
|
|
669
|
+
o,
|
|
670
|
+
paintTaskId(node, colors),
|
|
671
|
+
o.durationMs,
|
|
672
|
+
colors,
|
|
673
|
+
onPath,
|
|
674
|
+
ranCells(o, blocks),
|
|
675
|
+
)
|
|
644
676
|
}
|
|
645
677
|
|
|
646
678
|
/**
|
|
@@ -667,13 +699,10 @@ export function formatTaskExecutedLine(
|
|
|
667
699
|
blocks?: number,
|
|
668
700
|
usualMs?: number | null,
|
|
669
701
|
): string {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
? { usual: usualCell(o.durationMs, usualMs, colors) }
|
|
675
|
-
: {}),
|
|
676
|
-
})
|
|
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)
|
|
677
706
|
}
|
|
678
707
|
|
|
679
708
|
/**
|
|
@@ -682,26 +711,28 @@ export function formatTaskExecutedLine(
|
|
|
682
711
|
*/
|
|
683
712
|
export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
684
713
|
const cores = coresOf(o)
|
|
685
|
-
const ran = o
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
? { memBytes: null }
|
|
693
|
-
: {}),
|
|
694
|
-
}
|
|
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
|
|
695
721
|
}
|
|
696
722
|
|
|
697
|
-
/**
|
|
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
|
+
*/
|
|
698
728
|
function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
705
736
|
}
|
|
706
737
|
|
|
707
738
|
/** Shared: render any outcome on the grid. */
|
|
@@ -717,7 +748,6 @@ function formatOutcomeRow(
|
|
|
717
748
|
const ca = cacheOf(o)
|
|
718
749
|
return (
|
|
719
750
|
formatTaskRow(
|
|
720
|
-
taskGlyph(o, colors),
|
|
721
751
|
ms,
|
|
722
752
|
st.word,
|
|
723
753
|
st.color,
|
|
@@ -725,19 +755,11 @@ function formatOutcomeRow(
|
|
|
725
755
|
ca.color,
|
|
726
756
|
paintedId,
|
|
727
757
|
colors,
|
|
728
|
-
cells,
|
|
729
|
-
) +
|
|
730
|
-
(onPath ? pathNote(colors) : '') +
|
|
731
|
-
flakyNote(o, colors)
|
|
758
|
+
onPath ? { ...cells, critical: true } : cells,
|
|
759
|
+
) + flakyNote(o, colors)
|
|
732
760
|
)
|
|
733
761
|
}
|
|
734
762
|
|
|
735
|
-
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
736
|
-
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
737
|
-
// A fact worth knowing, not a failure: never red.
|
|
738
|
-
return ` ${paint('info', 'critical path', colors)}`
|
|
739
|
-
}
|
|
740
|
-
|
|
741
763
|
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
742
764
|
function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
743
765
|
return o.where === undefined ? '' : placeNote(o.where, colors)
|
|
@@ -754,10 +776,14 @@ function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
|
754
776
|
/**
|
|
755
777
|
* The dim note a flaky task's row and frame carry (`flaky - failed 1×
|
|
756
778
|
* before`): nothing prints below the footer, so the task's own line is
|
|
757
|
-
* where the reader learns it.
|
|
779
|
+
* where the reader learns it. Not proved flaky: its attempts, when more
|
|
780
|
+
* than one; else ''.
|
|
758
781
|
*/
|
|
759
782
|
export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
760
|
-
|
|
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)}` : ''
|
|
761
787
|
const said: string[] = []
|
|
762
788
|
if (o.status === 'failed' && o.flaky.passes > 0)
|
|
763
789
|
said.push(`passed ${o.flaky.passes}\u00d7 before`)
|
|
@@ -784,7 +810,6 @@ export function formatTaskSkippedLine(
|
|
|
784
810
|
const suffix = why === '' ? '' : ` ${paint('muted', `• ${why}`, colors)}`
|
|
785
811
|
return (
|
|
786
812
|
formatTaskRow(
|
|
787
|
-
paint('warn', '\u2298', colors),
|
|
788
813
|
null,
|
|
789
814
|
'skipped',
|
|
790
815
|
'warn',
|
|
@@ -811,29 +836,9 @@ export function formatTaskAbortedLine(
|
|
|
811
836
|
const id = paintTaskId(node, colors)
|
|
812
837
|
const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
|
|
813
838
|
if (neverStarted(o)) {
|
|
814
|
-
return formatTaskRow(
|
|
815
|
-
paint('muted', '\u25cc', colors),
|
|
816
|
-
null,
|
|
817
|
-
'not run',
|
|
818
|
-
'',
|
|
819
|
-
'',
|
|
820
|
-
'',
|
|
821
|
-
id,
|
|
822
|
-
colors,
|
|
823
|
-
bl,
|
|
824
|
-
)
|
|
839
|
+
return formatTaskRow(null, 'not run', '', '', '', id, colors, bl)
|
|
825
840
|
}
|
|
826
|
-
return formatTaskRow(
|
|
827
|
-
paint('aborted', '\u2717', colors),
|
|
828
|
-
o.durationMs,
|
|
829
|
-
'aborted',
|
|
830
|
-
'aborted',
|
|
831
|
-
'',
|
|
832
|
-
'',
|
|
833
|
-
id,
|
|
834
|
-
colors,
|
|
835
|
-
ranCells(o, blocks),
|
|
836
|
-
)
|
|
841
|
+
return formatTaskRow(o.durationMs, 'aborted', 'aborted', '', '', id, colors, ranCells(o, blocks))
|
|
837
842
|
}
|
|
838
843
|
|
|
839
844
|
/**
|
|
@@ -889,7 +894,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
889
894
|
...(fix.inputs !== undefined
|
|
890
895
|
? [
|
|
891
896
|
dim(
|
|
892
|
-
`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(', ')}]`,
|
|
893
898
|
),
|
|
894
899
|
]
|
|
895
900
|
: []),
|
|
@@ -1062,6 +1067,9 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
1062
1067
|
}
|
|
1063
1068
|
}
|
|
1064
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
|
+
|
|
1065
1073
|
/**
|
|
1066
1074
|
* The fix as the config line it is: `exec.sandbox.allow: { read: ['a'] }`,
|
|
1067
1075
|
* or undefined when every denial was a withheld credential path.
|
|
@@ -1070,7 +1078,7 @@ function sandboxFixLine(f: NonNullable<TaskOutcome['sandboxFix']>): string | und
|
|
|
1070
1078
|
const fix = Object.entries(f).filter(([k]) => k !== 'withheld' && k !== 'inputs')
|
|
1071
1079
|
if (fix.length === 0) return undefined
|
|
1072
1080
|
const parts = fix.map(
|
|
1073
|
-
([k, list]: [string, string[]]) => `${k}: [${list.map(
|
|
1081
|
+
([k, list]: [string, string[]]) => `${k}: [${list.map(jsString).join(', ')}]`,
|
|
1074
1082
|
)
|
|
1075
1083
|
return `exec.sandbox.allow: { ${parts.join(', ')} }`
|
|
1076
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'
|