@vzn/vx 0.0.645 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -32,21 +32,35 @@ import {
|
|
|
32
32
|
identityColor,
|
|
33
33
|
maskedCommand,
|
|
34
34
|
paint,
|
|
35
|
+
printable,
|
|
36
|
+
ruleWidth,
|
|
37
|
+
ui,
|
|
35
38
|
type ColorSupport,
|
|
36
39
|
} from '../util/index.js'
|
|
37
40
|
import { neverStarted } from './summary.js'
|
|
38
|
-
import { outcomeLabel, ranNoCache, skippedReason } from './events.js'
|
|
41
|
+
import { outcomeLabel, outcomeWord, ranNoCache, skippedReason } from './events.js'
|
|
39
42
|
import { fileMemo, linkPaths } from './path-links.js'
|
|
40
43
|
|
|
41
44
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
45
|
+
// Ends any colour a task's output left on, so vx's own lines below never
|
|
46
|
+
// take it (a task killed mid-write leaves one open).
|
|
47
|
+
export const RESET = '\x1b[0m'
|
|
48
|
+
|
|
49
|
+
/** Whether `text`'s last colour code sets a style rather than resetting it. */
|
|
50
|
+
export function leavesStyle(text: string): boolean {
|
|
51
|
+
const at = text.lastIndexOf('\x1b[')
|
|
52
|
+
if (at < 0) return false
|
|
53
|
+
const sgr = /^\[([\d;]*)m/.exec(text.slice(at + 1, at + 64))
|
|
54
|
+
// Not a colour code (a cursor move): look no further, as a reset costs nothing.
|
|
55
|
+
if (sgr === null) return true
|
|
56
|
+
return sgr[1] !== '' && sgr[1] !== '0'
|
|
57
|
+
}
|
|
42
58
|
|
|
43
59
|
const ACCENT = '#06b6d4' // cyan-500 — bullets, remote-hit hint
|
|
44
60
|
// Identity hues (`identityColor`) are outside the status palette (green /
|
|
45
61
|
// red / yellow / cyan) so a task id can never read as an outcome. Each
|
|
46
62
|
// half of an id hashes its own name: same project, same hue; same target,
|
|
47
63
|
// same hue, in every run, list and region row.
|
|
48
|
-
// Section rules + the summary rule share one frame width.
|
|
49
|
-
const FRAME_WIDTH = 60
|
|
50
64
|
|
|
51
65
|
const SUCCESS = '#22c55e' // green-500 — success / fresh
|
|
52
66
|
const WARN = '#eab308' // yellow-500 — skipped
|
|
@@ -98,8 +112,11 @@ export function formatTaskBlock(
|
|
|
98
112
|
|
|
99
113
|
const idPainted = paintTaskId(node, colors, { bold: true })
|
|
100
114
|
const corner = (s: string) => paint('', s, colors, { dim: true })
|
|
101
|
-
|
|
102
|
-
const
|
|
115
|
+
// A ready server's frame reads as the live one does: marked, and running.
|
|
116
|
+
const mark = persistentMark(node, colors)
|
|
117
|
+
const running = mark !== '' && outcome.status === 'success' && outcome.serverExit === undefined
|
|
118
|
+
const header = running ? paint(ACCENT, 'running', colors) : formatBlockHeader(outcome, colors)
|
|
119
|
+
const lines: string[] = [`${corner('┌─')} ${mark}${idPainted} ${corner('>')} ${header}`]
|
|
103
120
|
|
|
104
121
|
// The command section shows what actually ran — executed tasks
|
|
105
122
|
// (success and failed). Cache hits normally skip it (the stored
|
|
@@ -124,10 +141,19 @@ export function formatTaskBlock(
|
|
|
124
141
|
|
|
125
142
|
lines.push(...violationSection(outcome, colors))
|
|
126
143
|
|
|
127
|
-
lines.push(
|
|
144
|
+
lines.push(
|
|
145
|
+
`${corner('└─')} ${mark}${idPainted}${whereNote(outcome, colors)} ${corner('──')}${running ? runningFooter(outcome, colors) : formatBlockFooter(outcome, colors)}`,
|
|
146
|
+
)
|
|
128
147
|
return lines.join('\n') + '\n'
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
/** What a bounded capture dropped from its head, so a capped log never reads as whole. */
|
|
151
|
+
function droppedNote(lost: number, colors: ColorSupport): string {
|
|
152
|
+
return paint('', `… ${lost} earlier character${lost === 1 ? '' : 's'} dropped`, colors, {
|
|
153
|
+
dim: true,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
/**
|
|
132
158
|
* One `├─ <LABEL>` section of captured output, with the head-dropped notice
|
|
133
159
|
* when the capture was bounded. Shared by the task frame and the persistent
|
|
@@ -144,9 +170,10 @@ function pushStreamSection(
|
|
|
144
170
|
if (text.trim().length === 0) return
|
|
145
171
|
lines.push(sectionLine(title, color, colors), '')
|
|
146
172
|
if (lost > 0) {
|
|
147
|
-
lines.push(
|
|
173
|
+
lines.push(droppedNote(lost, colors))
|
|
148
174
|
}
|
|
149
175
|
pushBodyLines(lines, text)
|
|
176
|
+
if (leavesStyle(text)) lines.push(`${lines.pop()}${RESET}`)
|
|
150
177
|
lines.push('')
|
|
151
178
|
}
|
|
152
179
|
|
|
@@ -168,7 +195,7 @@ function pushOutputSection(
|
|
|
168
195
|
if (output.every((c) => c.text.trim().length === 0)) return
|
|
169
196
|
lines.push(sectionLine('OUTPUT', '', colors), '')
|
|
170
197
|
if (lost > 0) {
|
|
171
|
-
lines.push(
|
|
198
|
+
lines.push(droppedNote(lost, colors))
|
|
172
199
|
}
|
|
173
200
|
const body: Array<{ text: string; err: boolean }> = []
|
|
174
201
|
let open: { parts: string[]; err: boolean } | null = null
|
|
@@ -191,6 +218,8 @@ function pushOutputSection(
|
|
|
191
218
|
if (!l.err || !colors.enabled) lines.push(text)
|
|
192
219
|
else lines.push(l.text.includes('\x1b') ? text : paint(ERROR, text, colors))
|
|
193
220
|
}
|
|
221
|
+
const styled = output.findLast((c) => c.text.includes('\x1b['))
|
|
222
|
+
if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${RESET}`)
|
|
194
223
|
lines.push('')
|
|
195
224
|
}
|
|
196
225
|
|
|
@@ -204,8 +233,8 @@ function pushOutputSection(
|
|
|
204
233
|
function sectionLine(title: string, color: string, colors: ColorSupport): string {
|
|
205
234
|
const corner = (s: string) => paint('', s, colors, { dim: true })
|
|
206
235
|
const label = paint(color, title, colors, { bold: true })
|
|
207
|
-
|
|
208
|
-
const rule = corner('─'.repeat(Math.max(1,
|
|
236
|
+
// As wide as the footer's rule, so the frames and the footer line up.
|
|
237
|
+
const rule = corner('─'.repeat(Math.max(1, ruleWidth(colors) - 4 - title.length)))
|
|
209
238
|
return `${corner('├─')} ${label} ${rule}`
|
|
210
239
|
}
|
|
211
240
|
|
|
@@ -260,13 +289,18 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
|
|
|
260
289
|
// the two axes out, each in its own color. Time is right-aligned in a
|
|
261
290
|
// fixed cell so durations line up and a ticking elapsed never shifts
|
|
262
291
|
// the row. All detail (exit code, output) lives in the framed block.
|
|
263
|
-
export const TIME_COL = 7 // "0ms" … "999.99s"
|
|
292
|
+
export const TIME_COL = 7 // "0ms" … "999.99s", then "16m 40s" … "99m 59s"
|
|
264
293
|
const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
|
|
265
294
|
const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
|
|
295
|
+
const BLOCKED_COL = 7 // the live list's count of tasks a running one holds back
|
|
296
|
+
/** The list header's colour: violet-400, the summary rule's first stop. */
|
|
297
|
+
const HEADER = '#a78bfa'
|
|
266
298
|
|
|
267
299
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
268
300
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
269
|
-
|
|
301
|
+
let raw = ms === null ? '' : formatElapsed(ms)
|
|
302
|
+
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
303
|
+
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
270
304
|
return ' '.repeat(Math.max(0, TIME_COL - raw.length)) + paint('', raw, colors, { dim: true })
|
|
271
305
|
}
|
|
272
306
|
|
|
@@ -355,11 +389,32 @@ export function formatTaskRow(
|
|
|
355
389
|
cacheColor: string,
|
|
356
390
|
paintedId: string,
|
|
357
391
|
colors: ColorSupport = NO_COLOR,
|
|
392
|
+
/** The live list's `blocked` cell, right-aligned before the id; absent off that list. */
|
|
393
|
+
blocked?: string,
|
|
358
394
|
): string {
|
|
359
395
|
const st = cell(status, statusColor, STATUS_COL, colors)
|
|
360
396
|
// `aborting` runs one into the cache cell, which is blank beside it.
|
|
361
397
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
362
|
-
|
|
398
|
+
const bl =
|
|
399
|
+
blocked === undefined
|
|
400
|
+
? ''
|
|
401
|
+
: `${' '.repeat(Math.max(0, BLOCKED_COL - blocked.length))}${paint(WARN, blocked, colors)} `
|
|
402
|
+
return ` ${glyph} ${timeCell(ms, colors)} ${st} ${ca} ${bl}${paintedId}`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The header over a task list, on the row grid: bold and coloured, so it
|
|
407
|
+
* reads apart from the rows (dim, it was lost among them). `worker` when an
|
|
408
|
+
* executor places tasks: the name after the id's dot. `blocked` on the live
|
|
409
|
+
* list, whose rows count the tasks each running one holds back.
|
|
410
|
+
*/
|
|
411
|
+
export function formatListHeader(
|
|
412
|
+
worker: boolean,
|
|
413
|
+
colors: ColorSupport = NO_COLOR,
|
|
414
|
+
blocked = false,
|
|
415
|
+
): string {
|
|
416
|
+
const head = ` ${'time'.padStart(TIME_COL)} ${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${blocked ? `${'blocked'.padStart(BLOCKED_COL)} ` : ''}task${worker ? ' \u00b7 worker' : ''}`
|
|
417
|
+
return paint(HEADER, head, colors, { bold: true })
|
|
363
418
|
}
|
|
364
419
|
|
|
365
420
|
/** Compact one-liner for a cache hit with nothing to replay. */
|
|
@@ -367,8 +422,24 @@ export function formatTaskHitLine(
|
|
|
367
422
|
node: TaskNode,
|
|
368
423
|
o: TaskOutcome,
|
|
369
424
|
colors: ColorSupport = NO_COLOR,
|
|
425
|
+
onPath = false,
|
|
370
426
|
): string {
|
|
371
|
-
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors)
|
|
427
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The `--output-logs hash-only` line: outcome word, task id, cache key —
|
|
432
|
+
* absent for a task that never computed one (skipped, uncacheable). The
|
|
433
|
+
* word in its outcome's colour, the key dim.
|
|
434
|
+
*/
|
|
435
|
+
export function formatTaskHashLine(
|
|
436
|
+
node: TaskNode,
|
|
437
|
+
o: TaskOutcome,
|
|
438
|
+
colors: ColorSupport = NO_COLOR,
|
|
439
|
+
): string {
|
|
440
|
+
const color = o.status === 'aborted' ? ABORTED : statusOf(o).color
|
|
441
|
+
const key = o.hash === undefined ? '' : ` ${paint('', o.hash, colors, { dim: true })}`
|
|
442
|
+
return `${paint(color, outcomeWord(o), colors)} ${paintTaskId(node, colors)}${key}`
|
|
372
443
|
}
|
|
373
444
|
|
|
374
445
|
/** Compact one-liner for an executed task in broad mode. */
|
|
@@ -376,8 +447,9 @@ export function formatTaskExecutedLine(
|
|
|
376
447
|
node: TaskNode,
|
|
377
448
|
o: TaskOutcome,
|
|
378
449
|
colors: ColorSupport = NO_COLOR,
|
|
450
|
+
onPath = false,
|
|
379
451
|
): string {
|
|
380
|
-
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors)
|
|
452
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath)
|
|
381
453
|
}
|
|
382
454
|
|
|
383
455
|
/** Shared: render any outcome on the grid. */
|
|
@@ -386,6 +458,7 @@ function formatOutcomeRow(
|
|
|
386
458
|
paintedId: string,
|
|
387
459
|
ms: number | null,
|
|
388
460
|
colors: ColorSupport,
|
|
461
|
+
onPath: boolean,
|
|
389
462
|
): string {
|
|
390
463
|
const st = statusOf(o)
|
|
391
464
|
const ca = cacheOf(o)
|
|
@@ -399,10 +472,31 @@ function formatOutcomeRow(
|
|
|
399
472
|
ca.color,
|
|
400
473
|
paintedId,
|
|
401
474
|
colors,
|
|
402
|
-
) +
|
|
475
|
+
) +
|
|
476
|
+
(onPath ? pathNote(colors) : '') +
|
|
477
|
+
whereNote(o, colors) +
|
|
478
|
+
flakyNote(o, colors)
|
|
403
479
|
)
|
|
404
480
|
}
|
|
405
481
|
|
|
482
|
+
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
483
|
+
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
484
|
+
return ` ${paint(ERROR, 'critical path', colors)}`
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
488
|
+
export function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
489
|
+
return o.where === undefined ? '' : placeNote(o.where, colors)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* ` · <where>` after the id: the dot dim, the place in its own hue. Last
|
|
494
|
+
* on the row, as a worker's name has no width to pad to (2026-10-21).
|
|
495
|
+
*/
|
|
496
|
+
export function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
497
|
+
return `${paint('', ' \u00b7', colors, { dim: true })} ${ui.inline(ui.place(where), colors)}`
|
|
498
|
+
}
|
|
499
|
+
|
|
406
500
|
/**
|
|
407
501
|
* The dim note a flaky task's row and frame carry (`flaky - failed 1×
|
|
408
502
|
* before`): nothing prints below the footer, so the task's own line is
|
|
@@ -429,9 +523,9 @@ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
429
523
|
export function formatTaskSkippedLine(
|
|
430
524
|
node: TaskNode,
|
|
431
525
|
colors: ColorSupport = NO_COLOR,
|
|
432
|
-
|
|
526
|
+
skip?: Parameters<typeof skippedReason>[0],
|
|
433
527
|
): string {
|
|
434
|
-
const why = skippedReason(
|
|
528
|
+
const why = skippedReason(skip)
|
|
435
529
|
const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors, { dim: true })}`
|
|
436
530
|
return (
|
|
437
531
|
formatTaskRow(
|
|
@@ -496,7 +590,7 @@ export function formatFrameOpen(
|
|
|
496
590
|
): string {
|
|
497
591
|
const corner = (t: string) => paint('', t, colors, { dim: true })
|
|
498
592
|
const cmd = shownCommand(node, forwardArgs)
|
|
499
|
-
const mark =
|
|
593
|
+
const mark = persistentMark(node, colors)
|
|
500
594
|
return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
|
|
501
595
|
}
|
|
502
596
|
|
|
@@ -505,8 +599,8 @@ export function formatFrameOpen(
|
|
|
505
599
|
* sees them in context with the failing task, not as loose status output
|
|
506
600
|
* above the box. Shared by BOTH renderers: the focused/live frame used to
|
|
507
601
|
* omit it, so a single-task run — exactly how someone debugging one task
|
|
508
|
-
* invokes vx — showed a failure with no violations at all
|
|
509
|
-
* 2026-09-05).
|
|
602
|
+
* invokes vx — showed a failure with no violations at all
|
|
603
|
+
* (2026-09-05).
|
|
510
604
|
*/
|
|
511
605
|
function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[] {
|
|
512
606
|
const vlines = outcome.sandboxViolationLines
|
|
@@ -516,11 +610,13 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
516
610
|
// VERBATIM: a violation is evidence, and evidence that has been rewritten
|
|
517
611
|
// for display is not the thing the sandbox reported. The outcome keeps
|
|
518
612
|
// every record for telemetry.
|
|
519
|
-
|
|
613
|
+
// Verbatim but for control bytes: a denied path is anyone's bytes, and
|
|
614
|
+
// one named `ESC ]0;…` retitled the terminal (the `\xNN` is still exact).
|
|
615
|
+
const body = [...new Set(vlines)].map(printable)
|
|
520
616
|
// Red, not amber: a violation fails the task, so it reads as a failure.
|
|
521
617
|
// vx's note, after the evidence: the grants that would have allowed it.
|
|
522
618
|
// A read the output depends on is a key input too; the sandbox never
|
|
523
|
-
// adds one (
|
|
619
|
+
// adds one (2026-10-09), so the note names the reads no input
|
|
524
620
|
// glob matches and the user decides.
|
|
525
621
|
const fix = outcome.sandboxFix
|
|
526
622
|
const line = fix === undefined ? undefined : sandboxFixLine(fix)
|
|
@@ -533,12 +629,16 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
533
629
|
...(fix.inputs !== undefined
|
|
534
630
|
? [
|
|
535
631
|
dim(
|
|
536
|
-
`and, if the output depends on them, inputs.files: [${fix.inputs.map((p) => `'${p}'`).join(', ')}]`,
|
|
632
|
+
`and, if the output depends on them, inputs.files: [${fix.inputs.map((p) => `'${printable(p)}'`).join(', ')}]`,
|
|
537
633
|
),
|
|
538
634
|
]
|
|
539
635
|
: []),
|
|
540
636
|
...(fix.withheld !== undefined
|
|
541
|
-
? [
|
|
637
|
+
? [
|
|
638
|
+
dim(
|
|
639
|
+
`credential path, grant it only on purpose: ${fix.withheld.map(printable).join(', ')}`,
|
|
640
|
+
),
|
|
641
|
+
]
|
|
542
642
|
: []),
|
|
543
643
|
'',
|
|
544
644
|
]
|
|
@@ -559,16 +659,15 @@ export function formatFrameClose(
|
|
|
559
659
|
colors: ColorSupport = NO_COLOR,
|
|
560
660
|
): string {
|
|
561
661
|
const corner = (t: string) => paint('', t, colors, { dim: true })
|
|
562
|
-
const
|
|
563
|
-
const mark = persistent ? `${paint(ACCENT, '▸', colors)} ` : ''
|
|
662
|
+
const mark = persistentMark(node, colors)
|
|
564
663
|
// A persistent task's child keeps running past this "ready" outcome,
|
|
565
664
|
// so the close reads `running` (accent), not `success` — the ▸ marks
|
|
566
665
|
// both ends of the frame as a long-lived task.
|
|
567
666
|
const tail =
|
|
568
|
-
|
|
569
|
-
?
|
|
667
|
+
mark !== '' && outcome.status === 'success' && outcome.serverExit === undefined
|
|
668
|
+
? runningFooter(outcome, colors)
|
|
570
669
|
: formatBlockFooter(outcome, colors)
|
|
571
|
-
const close = `${corner('└─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('──')}${tail}`
|
|
670
|
+
const close = `${corner('└─')} ${mark}${paintTaskId(node, colors, { bold: true })}${whereNote(outcome, colors)} ${corner('──')}${tail}`
|
|
572
671
|
const violations = violationSection(outcome, colors)
|
|
573
672
|
return violations.length === 0 ? close : `${violations.join('\n')}\n${close}`
|
|
574
673
|
}
|
|
@@ -582,8 +681,12 @@ function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
|
|
|
582
681
|
)
|
|
583
682
|
}
|
|
584
683
|
|
|
585
|
-
function
|
|
586
|
-
return node.config.exec?.persistent !== undefined
|
|
684
|
+
function persistentMark(node: TaskNode, colors: ColorSupport): string {
|
|
685
|
+
return node.config.exec?.persistent !== undefined ? `${paint(ACCENT, '▸', colors)} ` : ''
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function runningFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
689
|
+
return ` ${paint('', `(${formatElapsed(o.durationMs)})`, colors, { dim: true })} ${paint(ACCENT, 'running', colors)}`
|
|
587
690
|
}
|
|
588
691
|
|
|
589
692
|
/**
|
|
@@ -625,10 +728,37 @@ export function formatKeptLines(node: TaskNode, text: string, colors: ColorSuppo
|
|
|
625
728
|
const prefix = `${paintTaskId(node, colors)} ${paint('', '│', colors, { dim: true })} `
|
|
626
729
|
return text
|
|
627
730
|
.split('\n')
|
|
628
|
-
.map((line) =>
|
|
731
|
+
.map((line) => {
|
|
732
|
+
const shown = lastRedraw(line)
|
|
733
|
+
return `${prefix}${shown}${leavesStyle(shown) ? RESET : ''}\n`
|
|
734
|
+
})
|
|
629
735
|
.join('')
|
|
630
736
|
}
|
|
631
737
|
|
|
738
|
+
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
739
|
+
const LINE_RESET = /\r|\x1b\[[12]K|\x1b\[[01]?G/g
|
|
740
|
+
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
741
|
+
const TRAILING_RESETS = /(?:\r|\x1b\[[12]K|\x1b\[[01]?G)+$/
|
|
742
|
+
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
743
|
+
const SGR = /\x1b\[[0-9;]*m/g
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* What a line shows once its redraws have played: the text after its last
|
|
747
|
+
* `\r`, line erase (`CSI 1K`/`2K`) or move to column 1 (`CSI G`), with the
|
|
748
|
+
* colours set before it. Written after an id prefix, a dev server's redraw
|
|
749
|
+
* (`\x1b[2K\x1b[1Gbuilt in 30ms`, a `10%\r100%` progress line) erased or
|
|
750
|
+
* overwrote the prefix on a terminal. A reset that ends the line, as in
|
|
751
|
+
* CRLF, redraws nothing.
|
|
752
|
+
*/
|
|
753
|
+
function lastRedraw(line: string): string {
|
|
754
|
+
if (!line.includes('\r') && !line.includes('\x1b')) return line
|
|
755
|
+
const text = line.replace(TRAILING_RESETS, '')
|
|
756
|
+
let cut = -1
|
|
757
|
+
for (const m of text.matchAll(LINE_RESET)) cut = m.index + m[0].length
|
|
758
|
+
if (cut === -1) return text
|
|
759
|
+
return (text.slice(0, cut).match(SGR) ?? []).join('') + text.slice(cut)
|
|
760
|
+
}
|
|
761
|
+
|
|
632
762
|
/**
|
|
633
763
|
* Between the requested task's frame and the summary footer, list the
|
|
634
764
|
* persistent tasks (dev servers / watchers) the run is keeping alive in
|
|
@@ -662,6 +792,8 @@ function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
662
792
|
return paint(ERROR, word, colors, { bold: true })
|
|
663
793
|
case 'skipped':
|
|
664
794
|
return paint(WARN, word, colors)
|
|
795
|
+
case 'aborted':
|
|
796
|
+
return paint(ABORTED, word, colors)
|
|
665
797
|
default:
|
|
666
798
|
return dim(word)
|
|
667
799
|
}
|
|
@@ -687,12 +819,10 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
687
819
|
return paint(ERROR, word, colors, { bold: true })
|
|
688
820
|
case 'skipped':
|
|
689
821
|
return paint(WARN, word, colors)
|
|
690
|
-
case '
|
|
691
|
-
|
|
692
|
-
case 'success':
|
|
693
|
-
return paint('', word, colors, { dim: true })
|
|
822
|
+
case 'aborted':
|
|
823
|
+
return paint(ABORTED, word, colors)
|
|
694
824
|
default:
|
|
695
|
-
return
|
|
825
|
+
return paint('', word, colors, { dim: true })
|
|
696
826
|
}
|
|
697
827
|
}
|
|
698
828
|
|
|
@@ -700,14 +830,11 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
700
830
|
* The fix as the config line it is: `exec.sandbox.allow: { read: ['a'] }`,
|
|
701
831
|
* or undefined when every denial was a withheld credential path.
|
|
702
832
|
*/
|
|
703
|
-
function sandboxFixLine({
|
|
704
|
-
withheld
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (Object.keys(fix).length === 0) return undefined
|
|
709
|
-
const parts = Object.entries(fix).map(
|
|
710
|
-
([k, list]: [string, string[]]) => `${k}: [${list.map((p) => `'${p}'`).join(', ')}]`,
|
|
833
|
+
function sandboxFixLine(f: NonNullable<TaskOutcome['sandboxFix']>): string | undefined {
|
|
834
|
+
const fix = Object.entries(f).filter(([k]) => k !== 'withheld' && k !== 'inputs')
|
|
835
|
+
if (fix.length === 0) return undefined
|
|
836
|
+
const parts = fix.map(
|
|
837
|
+
([k, list]: [string, string[]]) => `${k}: [${list.map((p) => `'${printable(p)}'`).join(', ')}]`,
|
|
711
838
|
)
|
|
712
839
|
return `exec.sandbox.allow: { ${parts.join(', ')} }`
|
|
713
840
|
}
|