@vzn/vx 0.0.638 → 0.0.639

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/docs/caching.md +44 -15
  2. package/docs/cli.md +168 -345
  3. package/docs/execution.md +18 -43
  4. package/docs/features.md +7 -10
  5. package/docs/patterns.md +5 -7
  6. package/docs/schema.md +47 -49
  7. package/package.json +5 -5
  8. package/schemas/last.json +7 -4
  9. package/skills/vx/SKILL.md +6 -4
  10. package/src/cache/cache.ts +40 -10
  11. package/src/cache/git-inputs.ts +6 -0
  12. package/src/cache/index.ts +2 -2
  13. package/src/cache/layer.ts +25 -11
  14. package/src/cache/layered-cache.ts +108 -315
  15. package/src/cache/plugin-cache.ts +2 -10
  16. package/src/cache/remote-lane.ts +383 -0
  17. package/src/cache/run-history.ts +24 -8
  18. package/src/cache/run-store.ts +4 -4
  19. package/src/cache/schema.ts +6 -4
  20. package/src/cache/scoped-cache.ts +7 -0
  21. package/src/cli/completions.ts +2 -2
  22. package/src/cli/foreign-flags.ts +10 -22
  23. package/src/cli/help.ts +6 -31
  24. package/src/cli/info.ts +2 -1
  25. package/src/cli/last.ts +6 -3
  26. package/src/cli/plan-format.ts +5 -58
  27. package/src/cli/plugin-commands.ts +2 -2
  28. package/src/cli/run.ts +23 -185
  29. package/src/cli/show.ts +6 -0
  30. package/src/cli/upgrade.ts +123 -39
  31. package/src/cli/watch-cycle.ts +2 -1
  32. package/src/cli/watch.ts +29 -14
  33. package/src/cli/why.ts +4 -4
  34. package/src/config.ts +0 -6
  35. package/src/exec/executor.ts +1 -1
  36. package/src/exec/sandbox-runtime.ts +1 -1
  37. package/src/exec/sandbox-violations.ts +3 -1
  38. package/src/graph/index.ts +9 -1
  39. package/src/graph/priorities.ts +1 -1
  40. package/src/graph/scheduler.ts +4 -16
  41. package/src/graph/task-graph.ts +87 -17
  42. package/src/graph/task-status.ts +95 -0
  43. package/src/{orchestrator → history}/failure-mode.ts +1 -1
  44. package/src/{orchestrator → history}/history.ts +1 -1
  45. package/src/history/index.ts +48 -0
  46. package/src/{orchestrator → history}/metrics.ts +7 -20
  47. package/src/{orchestrator → history}/miss-reason.ts +1 -1
  48. package/src/index.ts +10 -28
  49. package/src/orchestrator/doctor.ts +1 -1
  50. package/src/orchestrator/execute-args.ts +80 -0
  51. package/src/orchestrator/execute-task.ts +21 -386
  52. package/src/orchestrator/fingerprint-watch.ts +2 -2
  53. package/src/orchestrator/index.ts +1 -73
  54. package/src/orchestrator/miss-save.ts +33 -3
  55. package/src/orchestrator/options.ts +1 -36
  56. package/src/orchestrator/output-host.ts +4 -3
  57. package/src/orchestrator/persistent-task.ts +321 -0
  58. package/src/orchestrator/persistent.ts +130 -3
  59. package/src/orchestrator/placement.ts +3 -3
  60. package/src/orchestrator/plan.ts +18 -5
  61. package/src/orchestrator/plugin-host.ts +67 -47
  62. package/src/orchestrator/plugin.ts +6 -6
  63. package/src/orchestrator/prepare.ts +28 -29
  64. package/src/orchestrator/remote-prefetch.ts +14 -1
  65. package/src/orchestrator/run-artifacts.ts +5 -177
  66. package/src/orchestrator/run-context.ts +55 -0
  67. package/src/orchestrator/run-failures.ts +15 -3
  68. package/src/orchestrator/run-records.ts +35 -3
  69. package/src/orchestrator/run-report.ts +81 -179
  70. package/src/orchestrator/run-settings.ts +1 -16
  71. package/src/orchestrator/run-stop.ts +153 -0
  72. package/src/orchestrator/run.ts +260 -601
  73. package/src/orchestrator/sandbox-fix.ts +0 -16
  74. package/src/orchestrator/signals.ts +14 -6
  75. package/src/orchestrator/stable-keys.ts +107 -3
  76. package/src/orchestrator/steps.ts +50 -0
  77. package/src/orchestrator/task-hash.ts +3 -16
  78. package/src/orchestrator/task-log-buffer.ts +1 -1
  79. package/src/orchestrator/task-process.ts +94 -0
  80. package/src/orchestrator/telemetry-host.ts +1 -1
  81. package/src/orchestrator/telemetry.ts +5 -82
  82. package/src/{orchestrator → report}/events.ts +41 -8
  83. package/src/{orchestrator → report}/forecast.ts +35 -2
  84. package/src/{orchestrator → report}/framed-output.ts +39 -33
  85. package/src/report/index.ts +36 -0
  86. package/src/{orchestrator → report}/logger.ts +158 -20
  87. package/src/{orchestrator → report}/status-line.ts +84 -7
  88. package/src/{orchestrator → report}/summary.ts +126 -35
  89. package/src/report/tally.ts +83 -0
  90. package/src/settings.ts +1 -22
  91. package/src/util/colors.ts +44 -4
  92. package/src/util/git-ignored.ts +143 -0
  93. package/src/util/index.ts +19 -4
  94. package/src/util/report.ts +13 -5
  95. package/src/util/settle.ts +14 -39
  96. package/src/util/tail.ts +1 -1
  97. package/src/util/ui.ts +17 -6
  98. package/schemas/summary.json +0 -133
  99. package/src/cache/chained-cache.ts +0 -280
  100. package/src/orchestrator/tally.ts +0 -112
  101. /package/src/{orchestrator → report}/path-links.ts +0 -0
  102. /package/src/{orchestrator → report}/plain-output.ts +0 -0
  103. /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
package/docs/cli.md CHANGED
@@ -64,7 +64,7 @@ included.
64
64
 
65
65
  **Every requested name must resolve.** If any positional matches no
66
66
  project in scope, the run refuses to start — `vx run: no projects
67
- declare task(s): <name>.` on stderr, a run and `--dry` / `--graph` alike
67
+ declare task(s): <name>.` on stderr, a run and `--dry` alike
68
68
  (a run printed it to stdout until 2026-10-03), exit 1, with `Did you mean <task>?` when a
69
69
  declared task (or, for `pkg#task`, a runnable spec) is within two edits,
70
70
  and with no name that near, what exists: `Tasks: build, test.`, `No
@@ -124,20 +124,21 @@ If no task name is given:
124
124
  `missing task name (stdin is not a TTY, so no picker; tasks here: build, test)`,
125
125
  naming the cwd project's tasks, else every project's (twelve, then
126
126
  `and N more`); outside a workspace it reads `vx run <task>, e.g. vx run build`.
127
- - **Under `--format json` or `--dry=json`**, on a terminal too — the
127
+ - **Under `--dry=json`**, on a terminal too — the
128
128
  same refusal (`a JSON answer opens no picker; …`), with the
129
129
  `VX_E_USAGE` error document on stdout: a program cannot pick.
130
130
 
131
131
  Exit codes:
132
132
 
133
- | Code | When |
134
- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
135
- | `0` | Every task finished `success` or `cache-hit` (local or remote); or `--affected` left no project that declares the task. |
136
- | `1` | At least one task ended `failed` or `skipped`; a persistent task exited non-zero after it was ready; a task name no project declares; or parse/setup error. |
137
- | `130` / `143` / `129` | Interrupted (SIGINT / SIGTERM / SIGHUP): each task's process group (the task and what it forked) gets vx's signal once (a SIGHUP as a SIGTERM), `run.killGrace` (2 s) to go, then SIGKILL; a second signal skips the grace. vx then dies of the signal, so a shell script running it stops too (a run that handed a task the terminal exits with the code, so the terminal is restored). |
133
+ | Code | When |
134
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
135
+ | `0` | Every task finished `success` or `cache-hit` (local or remote); or `--affected` left no project that declares the task. |
136
+ | `1` | At least one task ended `failed` or `skipped`; a persistent task exited non-zero after it was ready; a task name no project declares; or parse/setup error. |
137
+ | `130` / `143` / `129` | Interrupted (SIGINT / SIGTERM / SIGHUP): each task's process group (the task and what it forked) gets vx's signal once (a SIGHUP as a SIGTERM), a 2 s grace to go, then SIGKILL; a second signal skips the grace. vx then dies of the signal, so a shell script running it stops too (a run that handed a task the terminal exits with the code, so the terminal is restored). |
138
138
 
139
139
  A task runs in its own session, so a terminal's Ctrl-C reaches vx alone,
140
- and each task hears it once: from vx, as SIGINT.
140
+ and each task hears it once: from vx, as SIGINT. A stop starts no more remote
141
+ cache uploads and does not wait on one in flight.
141
142
 
142
143
  Installed from npm, `vx` is a sh launcher that execs the platform
143
144
  binary, so every signal reaches vx directly and no Node starts first
@@ -480,34 +481,23 @@ than run with the args unheard.
480
481
  | `--schedule <rule>` | value | `run.schedule`, else `most-work` | Which ready task starts first when more are ready than workers: `most-work`, `critical-path`, `direct-dependents`, `ready-order` (`docs/schema.md` § `run.schedule`). |
481
482
  | `--branch <name>` | value | `VX_RUN_BRANCH`, else git's branch, then the CI provider's | The branch the run records in its history and forecasts from. No config key: a committed branch would mislabel every other branch's runs. |
482
483
  | `--no-cache` | boolean | off | Disable caching entirely (no reads, no writes); output globs are NOT cleaned. |
483
- | `--force` | boolean | off | Re-execute everything (skip cache reads) but still REFRESH the cache (writes stay on). Output globs are cleaned (so the saved snapshot is clean). |
484
+ | `--force` | boolean | off | Re-execute everything (skip cache reads); writes stay on. A key already stored is kept, and a remote that holds it is not sent it again. Output globs are cleaned (so the saved snapshot is clean). |
484
485
  | `--cache-policy <spec>` (`--cache-policy`) | value | `VX_CACHE_POLICY` / `cache.policy` | Per-layer read/write control. See below. An EMPTY spec (`--cache-policy=`) is a parse error — it applied nothing and left every axis on; pass `--no-cache` to disable them all. |
485
486
  | `--cache-scope <name>` | value | `VX_CACHE_SCOPE` / workspace `cache.scope` | This run's `cache.scope` (`pr-123`), above `VX_CACHE_SCOPE`, `.vx/workspace`'s and `github()`'s. A name `cache.scope` would refuse is refused. `--cache-scope=<name>` form too. Never folded into a cache key. |
486
487
  | `--cache-dir <path>` | value | `VX_CACHE_DIR` / `cache.dir` / `~/.vx/<id>/cache` | Cache directory override, resolved relative to cwd (absolute paths used as-is). Beats `VX_CACHE_DIR`, the workspace's `cache.dir` and the default; like them, the directory then holds the whole cache, shared through no store, for every cache the run opens, so the workspace's default dir is not created beside it. A per-run knob — never folded into a cache key. `--cache-dir=<path>` form too; the space form rejects a value starting with `-`. A directory this user cannot write into fails the run before any task with `cache directory <path> is not writable (EACCES: …)` — every run records its history there. |
487
488
  | `--retries <n>` | value | `run.retries`, else `0` | Re-run a failed task up to `n` more times; never a persistent one. A task's own `exec.retries` wins (even an explicit `0`). Never affects cache keys. `--retries=<n>` form too. |
488
489
  | `--continue[=<mode>]` | value | `deps-ok` | What a failed task takes down with it. `never` stops dispatch on the first failure; `deps-ok` (default) skips only its dependents; `always` (bare `--continue`) runs dependents anyway. See § Failure propagation. |
489
490
  | `--timeout <duration>` | duration | `run.timeout`, else none | A task's time limit (`30s`, `10m`), for tasks without their own `exec.timeout`, which always wins. A runaway task is killed + `failed`. A bare number is refused naming the forms with units. Never affects cache keys. `--timeout=<duration>` form too. |
490
- | `--kill-grace <duration>` | duration | `run.killGrace`, else `2s` | How long a task that ignores SIGTERM gets before SIGKILL: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. |
491
- | `--teardown-timeout <duration>` | duration | `run.teardownTimeout`, else `3s` | The bound on each plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. |
492
- | `--ready-notice <duration>` | duration | `run.readyNotice`, else `10s` | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. |
493
491
  | `--output-logs <mode>` | value | `VX_RUN_OUTPUT_LOGS`, else flow-derived | `full` \| `errors-only` \| `hash-only` \| `none` — explicit output override. See § `--output-logs`. `--output-logs=<mode>` form too. |
494
492
  | `--cache-download <mode>` | value | `cache.download`, else `all` | `all` \| `toplevel` \| `none` — where a REMOTELY-executed task's outputs land. `none` leaves them in the remote CAS and fetches lazily, only when a locally-placed task needs them. Never affects cache keys. See § `--cache-download`. `--cache-download=<mode>` form too. |
495
493
  | `--verbosity <n>` | int (0+) | `0` | `1` or more prints a per-task table (groups left out) after the framed blocks, above the footer. `--verbosity=<n>` form too. |
496
494
  | `--dry[=text\|json]` | optional value | off | Print the task graph + predicted cache hit/miss; skip execution. `VX_DEBUG_TIMING=1` prints the stage table here as it does for a run. |
497
- | `--graph[=<path>]` | optional value | off | Emit Graphviz DOT (stdout if no path, its directory made if missing); skip execution. A path it cannot write is one line and exit 1. |
498
- | `--format <fmt>` | value | `pretty` | `json` prints the `--summarize` document on stdout when the run ends and moves every other line to stderr ([Machine-readable output](#machine-readable-output)). `--format=<fmt>` form too. |
499
- | `--summarize[=<path>]` | optional value | off | Write per-run JSON to `<cacheDir>/runs/<run_id>.json` (or the explicit path). |
500
495
  | `--profile[=<path>]` | optional value | off (`profile.json` when set) | Write Chrome-trace JSON of the run's wallclock spans. |
501
- | `--tag <k=v>` | repeatable | (none) | Label this invocation. Recorded on the run's `invocations` row so dashboards can filter runs. `--tag=k=v` form too. |
502
- | `--report[=markdown]` | optional value | off | At the end of the run, print a markdown run report to stdout, above the footer. Only `markdown` is supported (`json` is reserved). |
503
- | `--report-file <path>` | value | off | After the run, APPEND the same markdown report to `<path>`, making its directory as the other output paths do. Use this for `$GITHUB_STEP_SUMMARY` — redirecting stdout captures the whole run log too. `--report-file=<path>` form too. |
504
496
 
505
497
  Mutual exclusion:
506
498
 
507
- - `--dry` and `--graph` — both skip execution; pick one.
508
- - `--dry` or `--graph` with `--summarize`, `--profile`, `--report` or
509
- `--report-file` — each needs a real run to write about. `--report` and
510
- `--report-file` were accepted and silently wrote nothing until item 992.
499
+ - `--dry` with `--profile` — the profile needs a real run to write
500
+ about.
511
501
 
512
502
  Unknown flags are a parse error (`unknown flag: --foo`), naming the
513
503
  nearest flag the verb accepts when one is within two edits
@@ -531,13 +521,10 @@ name is the verb, and would change what `vx build` means the day one
531
521
  was declared.
532
522
 
533
523
  **Optional-value flags take their value with `=` only.** `--affected`,
534
- `--exclude-dependencies`, `--dry`, `--graph`, `--summarize`, `--profile`,
535
- and `--report` are all valid bare, so a following word is
536
- read as a task name (a `--graph` word ending `.svg`, `.png`, `.json`
537
- and the like is refused, see § Turbo and Nx flags) — `vx run --affected build` means "run
538
- `build`, affected scope", and there is no way to tell that apart from
539
- "`build` is the git base". Write `--graph=out.dot` or
540
- `--affected=origin/main`. Getting it wrong is loud, not silent: the value
524
+ `--exclude-dependencies`, `--dry` and `--profile` are all valid bare,
525
+ so a following word is read as a task name — `vx run --affected build`
526
+ means "run `build`, affected scope", and there is no way to tell that
527
+ apart from "`build` is the git base". Write `--affected=origin/main`. Getting it wrong is loud, not silent: the value
541
528
  becomes a positional that matches no project, so the run refuses to
542
529
  start (see "Every requested name must resolve" above).
543
530
 
@@ -547,9 +534,9 @@ Pass bare `--exclude-dependencies` for the first, omit the flag for the
547
534
  second.
548
535
 
549
536
  Value flags (`--filter`, `--concurrency`, `--output-logs`,
550
- `--verbosity`, `--cache-dir`, `--report-file`, …)
537
+ `--verbosity`, `--cache-dir`, …)
551
538
  accept both `--flag value` and `--flag=value`. In the space form,
552
- `--cache-dir` and `--report-file` reject a value
539
+ `--cache-dir` rejects a value
553
540
  starting with `-`: that is always a swallowed flag (an unquoted empty
554
541
  shell variable), never a path or task id. Use the `=` form for a literal
555
542
  leading dash.
@@ -559,15 +546,13 @@ leading dash.
559
546
  fractional (`2.7`), signed (`+4`) and space-padded forms, plus anything
560
547
  past `2^53` (it would parse to a number you did not type). These all
561
548
  used to be silently reinterpreted — `--concurrency 0x10` ran 16
562
- workers. A duration flag (`--timeout`, `--kill-grace`,
563
- `--teardown-timeout`, `--ready-notice`) takes its unit (`500ms`, `30s`,
549
+ workers. A duration flag (`--timeout`) takes its unit (`500ms`, `30s`,
564
550
  `10m`): a bare `500` is refused, naming both readings.
565
551
 
566
- An empty `=` value means "no value" on four flags, which then act like
567
- their bare forms: `--profile=` writes `profile.json`, `--summarize=`
568
- writes `<cacheDir>/runs/<run_id>.json`, `--graph=` prints to stdout and
552
+ An empty `=` value means "no value" on two flags, which then act like
553
+ their bare forms: `--profile=` writes `profile.json` and
569
554
  `--affected=` uses the default base. Every other flag refuses an empty
570
- value: `--dry=`, `--report=`, `--continue=` and
555
+ value: `--dry=`, `--continue=` and
571
556
  `--exclude-dependencies=` as well as the ones with no bare form
572
557
  (`--retries=`, `--timeout=`, `--cache-dir=`, `--filter=`, `--cache-policy=`).
573
558
 
@@ -602,10 +587,13 @@ So `--no-cache` always wins over `--force`. The common cases:
602
587
  - `--no-cache` → nothing reads, nothing writes, and declared output
603
588
  globs are left untouched (you're debugging; vx won't manage your
604
589
  tree).
605
- - `--force` → re-execute every task (reads off) but still write fresh
606
- artifacts to both layers (writes on). Output globs ARE cleaned
607
- before each task so the saved snapshot is clean. This is the "rebuild
608
- and refresh the cache" flag.
590
+ - `--force` → re-execute every task (reads off) with writes on. An
591
+ entry is immutable: a key the local cache already holds is kept, and
592
+ each remote that can batch (`hasMany`) is asked once, in the
593
+ background, which keys it holds; one that cannot is asked `has` per
594
+ key before its upload. Held keys are not uploaded again (a probe that
595
+ fails uploads, as before). Output globs ARE cleaned before each task,
596
+ so the disk holds this run's outputs.
609
597
 
610
598
  `--cache-policy=<spec>` is a comma-separated list of `<layer>:<flags>`
611
599
  segments. `layer` is `local` or `remote`; `flags` is any subset of `r`
@@ -625,8 +613,9 @@ layer keeps its current value. Both `--cache-policy=<spec>` and the space form
625
613
  a remote hit is still delivered (the artifact has to land on disk to be
626
614
  extracted), it just never short-circuits the remote read.
627
615
 
628
- Combine with `--force` for "re-execute and refresh only the remote":
629
- `--cache-policy=local: --force` (local off, reads off, remote write-only).
616
+ Combine with `--force` for "re-execute and fill only the remote":
617
+ `--cache-policy=local: --force` (local off, reads off, remote write-only;
618
+ keys the remote already holds are not sent).
630
619
 
631
620
  Invalid layers/flags are a parse error
632
621
  (`--cache-policy: invalid layer 'disk'`, `--cache-policy: invalid flag 'x'`, …);
@@ -691,8 +680,7 @@ What the shapes mean in each column:
691
680
  everything, the blocker included (`⊘ skipped app#deploy • blocked by
692
681
  lib#build`; a fail-fast skip, which nothing blocked, carries no
693
682
  suffix). Every view that lists tasks prints it, a dependency's
694
- included: nothing prints below the footer. `--report` reads the same fact into the status cell,
695
- `skipped (blocked by lib#build)`.
683
+ included: nothing prints below the footer.
696
684
  - **`frame, or one-liner if quiet`** — a cache hit with stored stdout is
697
685
  worth a frame (the output is the point); a hit with nothing to replay
698
686
  compresses to one line, which is what keeps a 2000-task warm run
@@ -746,7 +734,7 @@ tracks the run live. Top to bottom:
746
734
  persistent tasks it depends on, until it — or, with several, the
747
735
  first of them — exits (one kept only as a dependency that exits 0,
748
736
  a daemon that forked and returned, does not end it); the rest still up are then torn
749
- down (SIGTERM, `run.killGrace`, SIGKILL), one status line names
737
+ down (SIGTERM, a 2 s grace, SIGKILL), one status line names
750
738
  the task and its code (`vx: app#dev exited with code 1; stopping 1
751
739
  other persistent task`), and a non-zero exit makes the run exit 1
752
740
  and is what `vx last` records.
@@ -762,16 +750,39 @@ other persistent task`), and a non-zero exit makes the run exit 1
762
750
  3. **Worker rows** — one per worker slot that has been busy (at most
763
751
  `min(concurrency, 10)`), no glyph and no spinner: the live ticking
764
752
  elapsed time leads (` 568ms running <id>`). A task stays in
765
- its row for its whole life; a row whose task ended holds its place
766
- dimmed (`idle`), so nothing ever jumps; overflow shows as `+k more`. A second in, a row
767
- says what waits on it: `critical path` (yellow) on the running task
753
+ its row for its whole life, so nothing jumps; overflow shows as
754
+ `+k more`. A second in, an idle worker row holds a task an
755
+ unfinished dependency blocks, one per row, in the order the
756
+ scheduler starts them (most dependents first):
757
+ `blocked solid-js#build by solid-js#types` (dim `blocked`, the id,
758
+ then the running task that holds it back). The task starts in the
759
+ row that held it. A row with no blocked task left to hold leaves the
760
+ region and the rows below close up; blocked tasks only dwindle, so
761
+ it never comes back. A running row says what waits on it:
762
+ `critical path` (yellow) on the running task
768
763
  the longest unfinished chain waits on, by the same p50s as the
769
764
  forecast below and only while two or more run, and a dim
770
765
  `blocks N`, its unfinished dependents direct and transitive. Where
771
766
  local history knows the task, its row also shows its usual duration
772
767
  (its executed p50) in the cache column, fixed while the elapsed time
773
768
  counts up toward it (`3.33s running ~5.2s solid-js#types`).
774
- 4. **The live summary section** — the SAME meters the final footer
769
+ 4. **vx's own steps** — what vx does around the tasks, as rows of
770
+ their own below the worker rows: `vx:load` (configs and graph from
771
+ the lock), `vx:run-lock`, `<plugin>:setup`, `vx:keys` (keys and local
772
+ probes), `vx:prefetch`, `vx:schedule`, then after the graph
773
+ `vx:uploads`, `vx:history`, `vx:keep-run`, `vx:telemetry`,
774
+ `<plugin>:teardown`, `vx:snapshots`, `vx:retention`, `vx:close` and
775
+ `vx:sandbox`. A step shows once it has run 150 ms, ticking like a
776
+ task (` 1.2s running vx:uploads`); no worker slot or
777
+ `--concurrency` holds it back. When it ends it leaves a list row
778
+ like a task's, with no cache column (`⏺︎ 1.2s success vx:uploads`);
779
+ a quicker one shows nowhere. `:` is no task id's separator, so a
780
+ step never reads as a task: a project named `vx` keeps `vx#build`.
781
+ No step is a task: no count, bar, history row, forecast, flaky
782
+ verdict, `vx last` row or telemetry record holds one; `--profile`
783
+ and `VX_DEBUG_TIMING` time vx's stages their own way. Steps show only
784
+ beside a live region; a log (CI, a pipe) prints none.
785
+ 5. **The live summary section** — the SAME meters the final footer
775
786
  prints (`tasks` + `cache` bars with legends, `time`), filling in as
776
787
  the run progresses, under a bare `vx` wordmark rule. A second in,
777
788
  the `time` row adds what is left (`time 1.41s · ~3s left`): each
@@ -814,11 +825,11 @@ redraws within 30 ms of the last draw coalesce into a single
814
825
  trailing draw when the floor expires (the final state always lands).
815
826
  On a 3,270-task warm run this cuts ~6.7 MB of redraw ANSI to ~20 KB.
816
827
 
817
- Identity coloring: every `project#task` renders its project half in
818
- a stable hue hashed from the project name (same project = same color
819
- in every run and every surface) and its task half in a fixed pink —
820
- both deliberately outside the status palette, so an id can never
821
- read as an outcome.
828
+ Identity coloring: each half of a `project#task` renders in a stable
829
+ hue hashed from its own name (same project = same color, same target
830
+ name = same color, in every run and every surface: every `#build`
831
+ matches), with a dim `#` between, all outside the status palette, so
832
+ an id can never read as an outcome.
822
833
 
823
834
  On GitHub Actions (`GITHUB_ACTIONS` truthy, full output mode), each
824
835
  task's block is wrapped in `::group::<id> (<outcome> <duration>)` /
@@ -903,9 +914,9 @@ not read a deferred output (the same reason vx refuses to infer inputs
903
914
  by tracing). When bytes are fetched later, vx saves an ordinary
904
915
  cache entry for them, so the next run is a plain local hit.
905
916
 
906
- ## Planning mode (`--dry`, `--graph`)
917
+ ## Planning mode (`--dry`)
907
918
 
908
- Both flags short-circuit execution. They build the full task graph,
919
+ `--dry` short-circuits execution. It builds the full task graph,
909
920
  compute every task's cache key, and probe the cache to predict the
910
921
  hit/miss outcome. Against a remote cache the probe is a lightweight
911
922
  existence check — planning never downloads or ingests artifacts.
@@ -913,8 +924,8 @@ existence check — planning never downloads or ingests artifacts.
913
924
  ```
914
925
  $ vx run ci --dry
915
926
  would run:
916
- ◉ @vzn/vx#format-check cache hit (local) 02bfe8a9
917
- ◉ @vzn/vx#lint cache hit (local) d66cfed2
927
+ ✓ @vzn/vx#format-check cache hit (local) 02bfe8a9
928
+ ✓ @vzn/vx#lint cache hit (local) d66cfed2
918
929
  ▶ @vzn/vx#test cache miss — would exec 68595e49 ~72.64s
919
930
 
920
931
  3 task(s) planned, 2 cache hits (2 local), 1 would run.
@@ -947,11 +958,11 @@ Status legend:
947
958
 
948
959
  | Symbol | Meaning |
949
960
  | ------ | ------------------------------------------------------------------------------------------ |
950
- | `◉` | cache hit (local) — entry already in `<cacheDir>/` |
961
+ | `✓` | cache hit (local) — entry already in `<cacheDir>/` |
951
962
  | `↓` | cache hit (remote) — entry would be fetched from the layer |
952
963
  | `▶` | cache miss — task would execute (under `--force` too: nothing is read, what runs is saved) |
953
- | `·` | no-cache — task opts out (no `cache` block, or `--no-cache`) |
954
- | `∅` | `@noop` — task would not run anywhere |
964
+ | `∅` | no-cache — task opts out (no `cache` block, or `--no-cache`) |
965
+ | `·` | `@noop` — task would not run anywhere |
955
966
  | `−` | not started — a server you did not request whose every dependant restores a local hit |
956
967
  | `○` | group task (suppressed in human view; in DOT + JSON) |
957
968
 
@@ -1013,150 +1024,12 @@ against: a bare `--affected` guesses it (`origin/HEAD`, a trunk branch,
1013
1024
  else `HEAD~1`), so a reason that names no file may come from a commit
1014
1025
  the base reaches, a root `package.json` edit among them.
1015
1026
 
1016
- ### Task graph (`--graph`)
1027
+ ## Run artifacts (`--profile`)
1017
1028
 
1018
- `--graph` prints Graphviz DOT (stdout by default; `--graph=path`
1019
- writes a file):
1020
-
1021
- ```
1022
- vx run ci --graph | dot -Tsvg > graph.svg
1023
- vx run ci --graph=graph.dot
1024
- ```
1025
-
1026
- Node `fillcolor` varies by predicted status (green = local hit,
1027
- sky-blue = remote hit, orange = miss, gray = no-cache, fuchsia =
1028
- group, white = not started). Edges are unstyled.
1029
-
1030
- ## Run artifacts (`--summarize`, `--profile`)
1031
-
1032
- Both flags add a side-effect after a real run completes. Errors
1029
+ `--profile` adds a side-effect after a real run completes. Errors
1033
1030
  writing the artifact are surfaced via `vx: failed to write …` but
1034
1031
  don't change the run's exit code — the run already happened.
1035
1032
 
1036
- ### `--summarize[=<path>]`
1037
-
1038
- Writes a per-run JSON file; `schemas/summary.json` is its JSON Schema:
1039
-
1040
- ```json
1041
- {
1042
- "runId": "01a0e3ef-206c-708d-95bf-c2b07211adf6",
1043
- "ok": true,
1044
- "exitCode": 0,
1045
- "startedAt": "2026-09-27T17:34:54.572Z",
1046
- "endedAt": "2026-09-27T17:34:54.586Z",
1047
- "totalMs": 14.123643,
1048
- "savedMs": 9,
1049
- "tasks": [
1050
- {
1051
- "id": "a#build",
1052
- "project": "a",
1053
- "task": "build",
1054
- "status": "cache-hit",
1055
- "exitCode": 0,
1056
- "durationMs": 3,
1057
- "hash": "48007ccadd42ed7d",
1058
- "storedDurationMs": 9,
1059
- "storedCpuMs": 2.059,
1060
- "wallclockStartNs": "9456491",
1061
- "wallclockEndNs": "12902888"
1062
- }
1063
- ],
1064
- "aborted": [],
1065
- "summary": {
1066
- "successful": 1,
1067
- "failed": 0,
1068
- "skipped": 0,
1069
- "cachedLocal": 1,
1070
- "restoredLocal": 0,
1071
- "restoredRemote": 0,
1072
- "upToDate": 1,
1073
- "cachedRemote": 0,
1074
- "aborted": 0,
1075
- "total": 1
1076
- }
1077
- }
1078
- ```
1079
-
1080
- `runId` is a UUIDv7. Default path: `<cacheDir>/runs/<run_id>.json`. hrtime fields are
1081
- strings (bigints serialized as strings) to preserve ns precision
1082
- through JSON. `cpuMs` / `peakRssBytes` are what the task's own
1083
- execution used and appear on executed rows only (`peakRssBytes` only
1084
- when the task's peak rose above vx's own footprint — a lighter task's
1085
- figure would be vx's, handed back by the kernel; a Linux sandboxed task
1086
- reports neither, since what bwrap's pid namespace used never reaches vx);
1087
- `restored` appears on a hit only: `true` when its outputs were restored,
1088
- `false` when they were already up to date. A hit's `durationMs`
1089
- is the restore it cost, and what the PRODUCING execution used rides the
1090
- artifact and appears under its own keys, `storedDurationMs` /
1091
- `storedCpuMs` / `storedPeakRssBytes` (the work the hit skipped) — a
1092
- remote worker's peak is never presented as this run's. `savedMs` is
1093
- their sum over the hits, the footer's `saved` as a number. `admissionHeldMs` appears on a row
1094
- only when an `admit` policy (a plugin's — `@vzn/vx-schedule-history`
1095
- packs learned reservations) refused the task while a worker was free:
1096
- the wait from that first refusal to its dispatch, the plugin's hand on
1097
- the run. The footer's `info` row sums the waits, task-seconds:
1098
- `admit held 3 tasks, 4.2s in all` (85 held tasks can read 4783s beside
1099
- a 160 s run).
1100
-
1101
- **`ok` / `exitCode`** are the run's verdict — the same value the CLI
1102
- exits with: a stopping signal's code (130 for Ctrl-C), and with a kept
1103
- server, its exit (the file is written again when the server ends). Gate on these rather than re-deriving a pass from the
1104
- buckets: a run can be red without a single failed task (see `aborted`).
1105
-
1106
- **`tasks[]` and `summary` describe the same population**, so
1107
- `tasks.length === summary.total` always holds. Group tasks (no `exec`)
1108
- are in neither — they do no work.
1109
-
1110
- **`aborted[]`** lists tasks whose attempt ended while the run was
1111
- stopping (Ctrl-C, a signal to vx), with their exit code, and tasks the stop reached before they started, with
1112
- exit 1, `durationMs: 0` and no `wallclockStartNs`. Neither joins an
1113
- outcome bucket or `total`, but they make the run red, so they are
1114
- listed separately and counted as `summary.aborted`.
1115
-
1116
- **`noCache: true`** marks a task no cache answered for: it declares no
1117
- `cache` block (it executes every run by design), or the run's `--cache-policy`
1118
- read and wrote nothing (`--no-cache`), so a hit rate should leave it out
1119
- of the denominator. The key is present only when true; every other row is
1120
- unchanged. Its `hash` is still set: dependents fold it.
1121
-
1122
- **`notReady`** is present only on a failed persistent task: why it never
1123
- became ready — `timeout` (the readiness deadline fired; `exitCode` is
1124
- the kill's, 143 or 137 after the grace, and 143 for a server that
1125
- traps the TERM and exits 0, as an ordinary timeout's),
1126
- `exited` (the child exited first; `exitCode` is then its own) or `spawn`
1127
- (the spawn itself failed). Every label reads it, `failed (never ready:
1128
- timed out, exit 143)`. A server the run's stop (a Ctrl-C) killed while it started is
1129
- `aborted`, not failed, as any task the stop kills; so is one that
1130
- became ready only after the stop.
1131
-
1132
- **`sandboxViolations`** is present only on a sandboxed task with a
1133
- SANDBOX VIOLATIONS section — the count of its denials (vx's own notes
1134
- there count none), and the reason the task failed (its exit is forced to 1
1135
- when it was 0); every label counts it, `failed (exit 1, 2 sandbox
1136
- violations)`.
1137
-
1138
- **`timedOut: true`** is present only on a `failed` row vx's own
1139
- `timeout` killed: its exit is the shell's 143, and every label reads
1140
- `failed (timed out, exit 143)` rather than a signal.
1141
-
1142
- **`blockedBy`** is present only on a `skipped` row: the id of the failed
1143
- (or aborted) task at the root of what blocked it, through any chain of
1144
- skips between — what the skipped row prints, for a script.
1145
- A fail-fast skip has none.
1146
-
1147
- **`flaky: { passes, failures, attempts }`** is present only on a task
1148
- this run proved flaky (its row's `flaky` note, typed): `passes` and
1149
- `failures` count the outcomes on record for this exact `hash`, this run
1150
- included, and `attempts` is what this run took. A consumer gating on
1151
- `status: "failed"` can tell a break (no `flaky` key) from a flake
1152
- without reading the history. Every other row is byte-identical.
1153
-
1154
- **`durationMs` is always what THIS run spent on the task.** For a cache
1155
- hit that is the probe + restore, not the exec time the entry was stored
1156
- with — so it is small even for an expensive task. The work a hit
1157
- _skipped_ is a different number; `--report`'s "N saved" is the surface
1158
- that reports it.
1159
-
1160
1033
  ### `--profile[=<path>]`
1161
1034
 
1162
1035
  Writes a Chrome-trace JSON of every task's wallclock span. Open in
@@ -1192,91 +1065,6 @@ from the per-task `hrtime.bigint()` spans the runner captures.
1192
1065
 
1193
1066
  Default path: `profile.json` (cwd-relative).
1194
1067
 
1195
- ### `--report[=markdown]`
1196
-
1197
- At the end of a real run, prints a markdown run report to **stdout**,
1198
- just above the footer: nothing prints below the footer. The report
1199
- itself is machine-clean. One header line of
1200
- totals plus a table, one row per task:
1201
-
1202
- ```markdown
1203
- ## vx run — passed
1204
-
1205
- **3 tasks** · 3 success · 0 failed · 2 cached (1 up-to-date, 1 restored) · 1.23s total · 2.65s saved
1206
-
1207
- | Task | Status | Cache | Duration |
1208
- | --------- | ------- | ---------- | -------- |
1209
- | web#build | success | miss | 1.23s |
1210
- | web#test | success | local | 5ms |
1211
- | api#test | success | up-to-date | 3ms |
1212
- ```
1213
-
1214
- `Status` is the task outcome (`success` / `failed (exit N)`, with the
1215
- signal an exit above 128 stands for, `failed (exit 137, 128 + SIGKILL)`,
1216
- or a timeout's reason, `failed (timed out, exit 143)`, a persistent
1217
- task's `never ready: …`, or a sandboxed task's violation count, or
1218
- `skipped`, naming what blocked it: `skipped (blocked by lib#build)`);
1219
- `Cache` is its provenance (`miss` / `no-cache` for a task no cache
1220
- answered for — no `cache` block, or a `--no-cache` / `--cache-policy=local:` run —
1221
- which never consulted one / `local` / `remote` / `up-to-date` /
1222
- `—`). A stopped run (a Ctrl-C teardown) reads `## vx run — interrupted`,
1223
- and its tasks keep the terminal's words: one the signal killed is
1224
- `aborted` with its time so far, one the stop reached before it ran is
1225
- `not run` with no time. Both count in the header's task total, listed
1226
- as `1 aborted · 1 not run`, so a red report with no failing row still
1227
- says why. Group tasks (no `exec`) get neither — they are not
1228
- work, and the header's counts match the terminal summary and
1229
- `--summarize` exactly.
1230
-
1231
- The two durations in the header mean different things, and the
1232
- distinction is the point:
1233
-
1234
- - **`N total`** sums `Duration` over the tasks that actually EXECUTED,
1235
- an aborted task's time so far included — the time this run spent.
1236
- - **`N saved`** sums the exec times the cache hits SKIPPED, read from
1237
- each entry as it was stored (above, 2.01s and 640ms). It is
1238
- deliberately not the hits' `Duration` column, which is the restore
1239
- they cost this run: summing that reported a task taking 2.01s cold
1240
- as "6ms saved".
1241
-
1242
- Only `markdown` is supported today (`json` is reserved; a bad value is a
1243
- parse error). Built purely from the run's outcomes after it returns — it
1244
- adds zero cost when both flags are absent.
1245
-
1246
- ### `--report-file <path>`
1247
-
1248
- Writes the same markdown report to a file instead of (or as well as)
1249
- stdout, and is the form to use for a CI step summary:
1250
-
1251
- ```sh
1252
- vx run ci --report-file="$GITHUB_STEP_SUMMARY"
1253
- ```
1254
-
1255
- **Do not redirect stdout for this.** The report itself is machine-clean,
1256
- but stdout is not vx's alone — the status logger writes frames, meter
1257
- bars and `::group::` workflow commands to the same stream, so
1258
- `--report=markdown >> "$GITHUB_STEP_SUMMARY"` puts the entire run log
1259
- into the step summary above the table.
1260
-
1261
- The report is **appended**, never truncated: `$GITHUB_STEP_SUMMARY` is a
1262
- shared, append-only file that other steps in the same job also write to,
1263
- so overwriting it would silently discard their content. Passing both
1264
- flags writes the report to stdout AND appends it to the file. A run
1265
- stopped by Ctrl-C or SIGTERM still writes both, its stopped tasks
1266
- `aborted`. A write
1267
- failure is reported (`vx: failed to write report to …`) but does not
1268
- change the exit code — the run already happened, the same contract
1269
- `--summarize` and `--profile` follow.
1270
-
1271
- ### `--tag <k=v>`
1272
-
1273
- Labels the invocation. Repeatable; `--tag=k=v` form too. The pair is
1274
- split on the **first** `=`, so values may contain `=` (e.g. a URL). An
1275
- empty key is a parse error. Tags are recorded on the run's
1276
- `invocations` row so dashboards can filter runs by label, and reach
1277
- telemetry as `vx.tag.<key>`; a secret value in one is masked there as
1278
- in the task's output (see [Masking](./schema.md)).
1279
-
1280
1068
  ## Sandbox
1281
1069
 
1282
1070
  Sandbox isolation is opt-in **per task** via an `exec.sandbox` block in
@@ -1553,10 +1341,10 @@ re-reads the watched set (item 971).
1553
1341
  The following flags are rejected (parser exits 1 before the initial
1554
1342
  run):
1555
1343
 
1556
- - `--dry` / `--graph` — those skip execution; nothing to watch.
1557
- - `--summarize` / `--profile` — would overwrite their target per cycle.
1558
- - `--report` / `--report-file` / `--verbosity <n>` (n > 0) / `--format json` — all
1559
- format ONE run's result; a watch loop has no single run to report. (`--verbosity 0`
1344
+ - `--dry` — it skips execution; nothing to watch.
1345
+ - `--profile` — would overwrite its target per cycle.
1346
+ - `--verbosity <n>` (n > 0) — formats
1347
+ ONE run's result; a watch loop has no single run to report. (`--verbosity 0`
1560
1348
  is accepted: it asks for what watch already prints.)
1561
1349
 
1562
1350
  A requested persistent task (`exec.persistent`, a dev server) starts
@@ -1581,7 +1369,7 @@ re-runs itself and what it depends on.
1581
1369
  - `0` — clean Ctrl+C / SIGTERM exit. A cycle in flight is torn down
1582
1370
  first — its children, and the dev server held between cycles, get the
1583
1371
  signal watch received (a Ctrl-C as SIGINT; a SIGTERM or SIGHUP as SIGTERM),
1584
- `run.killGrace` (2 s), then SIGKILL — and the process leaves only
1372
+ a 2 s grace, then SIGKILL — and the process leaves only
1585
1373
  once it has returned, so a CI cancellation never orphans a task.
1586
1374
  Also `0`, with no watch, when `--affected` selects nothing: watch
1587
1375
  prints `vx watch: nothing affected since <ref>` and exits as `vx run`
@@ -2075,9 +1863,6 @@ manager is its lockfile (`pnpm-lock.yaml`, `bun.lock`, `yarn.lock`,
2075
1863
  `package-lock.json`), else `packageManager`, else npm; the field it is
2076
1864
  listed in is kept (`--save-dev`, `--dev`, …), and pnpm at a workspace
2077
1865
  root adds `--workspace-root`. The lockfile is updated by that manager.
2078
- Run inside a project that installs `@vzn/vx`, a global vx hands
2079
- `vx upgrade` to the project's own (§ Which vx runs), so the project's
2080
- dependency is what moves.
2081
1866
  vx prints what it runs, runs it there, and exits with an error if it
2082
1867
  fails:
2083
1868
 
@@ -2087,6 +1872,28 @@ fails:
2087
1872
  bun add @vzn/vx@0.0.2 in /repo
2088
1873
  ```
2089
1874
 
1875
+ **Inside a project with its own vx**, a global vx hands `vx upgrade` to
1876
+ the project's (§ Which vx runs), and one run moves both to the same
1877
+ version (`TAG`, or latest): first the project's dependency, then the
1878
+ global vx that started the run, each the way it was installed. Each
1879
+ line names its install, and a refusal names the one that failed
1880
+ (`vx upgrade: global vx /usr/local/bin/vx: cannot write to …`); a
1881
+ project that fails leaves the global as it was. The project's vx run
1882
+ directly (a package script, `node_modules/.bin/vx`) has no global behind it, so only
1883
+ the project moves.
1884
+
1885
+ ```
1886
+ › vx upgrade project 0.0.1 → 0.0.2
1887
+
1888
+ bun add @vzn/vx@0.0.2 in /repo
1889
+
1890
+ › vx upgrade global 0.0.1 → 0.0.2
1891
+
1892
+ /usr/local/bin/vx
1893
+
1894
+ ✓ vx upgrade global installed 0.0.2
1895
+ ```
1896
+
2090
1897
  vx never installs Node or a package manager: one not on `PATH` is refused with the command to run. A vx run from an npx or
2091
1898
  bunx cache has nothing installed to upgrade, and says to ask for
2092
1899
  `@vzn/vx@latest` instead.
@@ -2201,8 +2008,8 @@ none is dropped in silence. An Nx flag's camelCase spelling (`--nxBail`,
2201
2008
  `--skipNxCache`), which Nx's parser takes and its docs print, is its
2202
2009
  kebab-case row. A `[=<bool>]` row takes `=true` (the flag bare) and `=false` (left out), as Turbo's and Nx's parsers do. `vx run-many` and `vx affected` name the
2203
2010
  `vx run` that does the same, and the other verbs a hand types from
2204
- either tool name what does it here: `graph` (`--graph`), `ls`
2205
- (`vx show`), `query` (`--dry=json`), `reset` (`vx cache clean`), and
2011
+ either tool name what does it here: `graph` and `query` (`--dry=json`), `ls`
2012
+ (`vx show`), `reset` (`vx cache clean`), and
2206
2013
  `daemon`, `login`, `logout`, `link` and `unlink`, which vx has no use
2207
2014
  for. Nx's `project:target` (`vx run web:build`,
2208
2015
  when `web` declares `build`) is answered with `vx run web#build`: from
@@ -2221,11 +2028,10 @@ copy to the source.
2221
2028
  | turbo | `--continue=dependencies-successful` | alias | `--continue=deps-ok` |
2222
2029
  | turbo | `--continue` | same | `--continue[=never\|deps-ok\|always]` |
2223
2030
  | turbo | `--dry-run` | alias | `--dry[=text\|json]` |
2224
- | turbo | `--graph=<file>.svg\|png\|json\|html\|…` | refuse | vx writes Graphviz DOT only: `--graph=<file>.dot`, then `dot -Tsvg` |
2225
- | turbo | `--graph` | same | `--graph[=<file>.dot]` |
2031
+ | turbo | `--graph` | refuse | `--dry=json` prints the planned task graph, each task with its deps |
2226
2032
  | turbo | `--force[=<bool>]` | same | `--force`: skip cache reads, keep writes |
2227
2033
  | turbo | `--affected` | same | `--affected[=<base>]` |
2228
- | turbo | `--summarize[=<bool>]` | same | `--summarize[=<path>]` |
2034
+ | turbo | `--summarize` | refuse | use `vx last --format json` for the run's JSON record |
2229
2035
  | turbo | `--output-logs=new-only` | refuse | use `--output-logs=full` (a hit replays its log) or `errors-only` |
2230
2036
  | turbo | `--output-logs <v>` | same | `--output-logs full\|errors-only\|hash-only\|none` |
2231
2037
  | turbo | `--no-cache` | same | `--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache-policy local:r,remote:r` |
@@ -2254,8 +2060,8 @@ copy to the source.
2254
2060
  | turbo | `--env-mode <v>` | refuse | vx passes only the variables a task declares (strict): list the rest in `exec.env.passThrough` |
2255
2061
  | turbo | `--framework-inference <v>` | refuse | under `turbo()` inference is Turbo's; take a name back with a `!` entry in the task's `env` |
2256
2062
  | turbo | `--global-deps <v>` | refuse | declare them in `cache.inputs.workspaceFiles` (under `turbo()`, turbo.json's `globalDependencies`) |
2257
- | turbo | `--json` | refuse | use `--format json` for the run's result on stdout, `--dry=json` for the plan |
2258
- | turbo | `--log-file` | refuse | use `--summarize[=<path>]` for the run's JSON record |
2063
+ | turbo | `--json` | refuse | use `vx last --format json` for the run's result, `--dry=json` for the plan |
2064
+ | turbo | `--log-file` | refuse | use `vx last --format json` for the run's JSON record |
2259
2065
  | turbo | `--preflight` | refuse | set `turboCache({ preflight: true })` or `TURBO_PREFLIGHT=1` |
2260
2066
  | turbo | `--remote-cache-timeout <v>` | refuse | set `turboCache({ timeoutMs })` or `TURBO_REMOTE_CACHE_TIMEOUT` |
2261
2067
  | turbo | `--single-package` | refuse | a repo with no workspaces is one project already: drop it |
@@ -2297,9 +2103,8 @@ copy to the source.
2297
2103
  ## Machine-readable output
2298
2104
 
2299
2105
  `vx show`, `vx info`, `vx why`, `vx last` and `vx cache clean` take
2300
- `--format json`. Each prints one JSON document on one line, as
2301
- `vx run --format json` does, whose shape is a
2302
- checked-in JSON Schema (draft 2020-12) shipped with the package:
2106
+ `--format json`. Each prints one JSON document on one line, whose shape
2107
+ is a checked-in JSON Schema (draft 2020-12) shipped with the package:
2303
2108
  `schemas/show.json`, `schemas/info.json`, `schemas/why.json`,
2304
2109
  `schemas/last.json`, `schemas/cache.json`
2305
2110
  (`node_modules/@vzn/vx/schemas/` in an install). Every object closes its
@@ -2311,13 +2116,9 @@ schemas: each `{ … }` a verb's section shows is an object its schema
2311
2116
  closes with those keys, each object the verb prints whole is shown, and
2312
2117
  `vx info`'s field list is its schema's top level.
2313
2118
 
2314
- `vx run --format json` prints, when the run ends, the `--summarize`
2315
- document (`schemas/summary.json`: `runId`, `ok`, `exitCode`, one row per
2316
- task with its status, exit code and key). Everything else the run writes,
2317
- the frame and the tasks' output with it, goes to stderr, so
2318
- `vx run build --all --format json | jq .ok` reads only the result. It
2319
- refuses `--dry` (use `--dry=json`), a bare `--graph` and
2320
- `--report=markdown`, which print on stdout too.
2119
+ A run's result as JSON is `vx last --format json`, read after the run
2120
+ from the run history: one row per task with its status, key and cache
2121
+ result. `vx run --dry=json` prints the plan.
2321
2122
 
2322
2123
  ### Error codes
2323
2124
 
@@ -2433,8 +2234,9 @@ target, fix its spelling, or drop the entry.
2433
2234
  `vx lock` refuses it, not a run: a run trusts the graph the lock holds.
2434
2235
 
2435
2236
  A `^task` with no edge source is refused too: ``<id>: ^build needs
2436
- project edges; add pnpm()/bun()/npm()/yarn() or a plugin that returns
2437
- `deps` ``. Core reads no edge from a manifest, so with no plugin
2237
+ project edges; add pnpm()/bun()/npm()/yarn() and install so its
2238
+ lockfile exists, or a plugin that returns `deps` ``. A lockfile plugin
2239
+ whose lockfile is absent gives no edges. Core reads no edge from a manifest, so with no plugin
2438
2240
  returning `deps` every `^` would resolve to nothing and fold no
2439
2241
  upstream into the key. Declare the lockfile plugin in
2440
2242
  `.vx/workspace.ts` (or a plugin of your own that returns `deps`); a run
@@ -2470,12 +2272,11 @@ https://github.com/vznjs/vx/issues.
2470
2272
  A plugin can give its refusal a code: `new UserError(message, 'MY_CODE')`.
2471
2273
 
2472
2274
  **Streams.** A verb whose stdout is a product (a `--format json`
2473
- document, `--dry` / `--dry=json`, `--graph`'s DOT, a completion
2275
+ document, `--dry` / `--dry=json`, a completion
2474
2276
  script) writes that product alone there; a notice or warning it meets
2475
2277
  on the way (a cache index from another vx version, a plugin's warning) goes
2476
2278
  to stderr, as every refusal does. `vx run`'s stdout is the run's frame,
2477
- the tasks' output it carries, except under `--format json`, where that
2478
- moves to stderr. `tests/cli-streams.test.ts` holds each
2279
+ the tasks' output it carries. `tests/cli-streams.test.ts` holds each
2479
2280
  product verb to it under a notice.
2480
2281
 
2481
2282
  ## `vx show`
@@ -2571,6 +2372,9 @@ or `; projects and tasks: …` (M-57).
2571
2372
 
2572
2373
  An empty target (`vx show ''`) is refused: omit it to list every project.
2573
2374
 
2375
+ In a workspace with no project, the list is empty and `vx show` says so
2376
+ on stderr (`no projects`) and names `vx init`.
2377
+
2574
2378
  Exit codes: `0` success; `1` parse error or unknown target.
2575
2379
 
2576
2380
  Two runs on one workspace take turns: the second waits for the first's
@@ -2614,7 +2418,7 @@ $ vx info
2614
2418
  memory 13 GB usable — cgroup limit; the machine has 16 GB
2615
2419
  cache dir /work/repo/.vx/cache (8.2 MB, this workspace's run history)
2616
2420
  cache store /home/me/.vx/3f2a9c1e7b4d5a60/cache (1.3 GB, shared by this repo's checkouts)
2617
- cache versions keys vx-cache-v43 · index schema v38
2421
+ cache versions keys vx-cache-v43 · index schema v40
2618
2422
  cache entries 42 (1.3 GB)
2619
2423
  orphans 3 artifacts (12 MB) the index does not know — `vx cache clean` reaps them
2620
2424
  task runs (24h) 7 (5 cache hits)
@@ -2875,7 +2679,9 @@ command that exits 137 on its own reads the same). Where the run's
2875
2679
  footer gave a reason, the row ends with it instead: `timed out`,
2876
2680
  `never ready: exited`, `2 sandbox violations`, and for a skipped task
2877
2681
  `after lib#build failed` (the v27 columns; rows older than them read
2878
- as before). Rows run failures first, then what executed or was
2682
+ as before). A task whose key met a remote failure ends with it, read or
2683
+ write alike: `remote ✗ download: connection refused; upload: 503` (the
2684
+ v40 column, `remoteError` in `--format json`). Rows run failures first, then what executed or was
2879
2685
  skipped, then hits; past sixteen hits the rest fold into one line with
2880
2686
  their count (`… +980 more cache hits`) — a thousand-task warm run is a
2881
2687
  thousand rows otherwise, with the one failure a screen above the
@@ -3066,8 +2872,8 @@ names no package: it lists what the workspace itself declares.
3066
2872
 
3067
2873
  ## Environment variables vx reads
3068
2874
 
3069
- Every setting has a variable derived from its path (`run.killGrace` →
3070
- `VX_RUN_KILL_GRACE`), beating `.vx/workspace` and beaten by the flag; the
2875
+ Every setting has a variable derived from its path (`run.timeout` →
2876
+ `VX_RUN_TIMEOUT`), beating `.vx/workspace` and beaten by the flag; the
3071
2877
  table in `docs/schema.md` § Settings gives each value's form and
3072
2878
  default. Each is checked once before any verb runs: an empty one is
3073
2879
  unset, a value its kind does not take is refused naming the form, and a
@@ -3087,9 +2893,6 @@ what a TASK sees is `exec.env` in `docs/schema.md`.
3087
2893
  | `VX_RUN_RETRIES` | [`run.retries`](schema.md#settings) | Re-runs of a failed task, for tasks without their own `exec.retries`. |
3088
2894
  | `VX_RUN_CONTINUE` | [`run.continue`](schema.md#settings) | What a failed task takes down: `never` stops the run, `deps-ok` skips its dependents, `always` runs them too. |
3089
2895
  | `VX_RUN_OUTPUT_LOGS` | [`run.outputLogs`](schema.md#settings) | Which task output the run prints. Chosen before the workspace loads, so flag and env only. |
3090
- | `VX_RUN_KILL_GRACE` | [`run.killGrace`](schema.md#settings) | How long a task that ignores SIGTERM gets before SIGKILL. |
3091
- | `VX_RUN_TEARDOWN_TIMEOUT` | [`run.teardownTimeout`](schema.md#settings) | The bound on each plugin's end-of-run flush and teardown. |
3092
- | `VX_RUN_READY_NOTICE` | [`run.readyNotice`](schema.md#settings) | How long a persistent task may take to match `readyWhen` before vx says it waits. |
3093
2896
  | `VX_RUN_RULES_EXCLUSIVE_OUTPUTS` | [`run.rules.exclusiveOutputs`](schema.md#settings) | Refuse two tasks whose outputs overlap even when an edge orders them. |
3094
2897
  | `VX_RUN_RULES_UPFRONT_KEYS` | [`run.rules.upfrontKeys`](schema.md#settings) | Refuse a task whose inputs can match another task's outputs. |
3095
2898
  | `VX_CACHE_DIR` | [`cache.dir`](schema.md#settings) | Where the cache lives, relative to the workspace root; named, it holds the whole cache. |
@@ -3121,9 +2924,8 @@ Colors are the two conventions in § Output format › Colors (`NO_COLOR`,
3121
2924
  `FORCE_COLOR`). A truthy `CI` picks the CI output flow and
3122
2925
  `GITHUB_ACTIONS` its `::group::` framing (§ Output); the run's
3123
2926
  `invocations` row records the provider, the first truthy of
3124
- `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, then `CI`. Core never reads `GITHUB_STEP_SUMMARY`: `--report`
3125
- prints to stdout, and `--report-file=<path>` appends to a file, so on
3126
- Actions pass `--report-file="$GITHUB_STEP_SUMMARY"`. A sandboxed
2927
+ `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, then `CI`. Core never reads `GITHUB_STEP_SUMMARY`; the job summary is
2928
+ `@vzn/vx-ci`'s. A sandboxed
3127
2929
  task's temp dir sits under `CLAUDE_CODE_TMPDIR` (else `CLAUDE_TMPDIR`,
3128
2930
  else `/tmp/claude`), the sandbox runtime's own convention
3129
2931
  (`docs/modules/sandbox-runtime.md`).
@@ -3158,8 +2960,8 @@ write ../shared/notes.txt
3158
2960
  Every section is conditional: the `$ <command>` line only for an
3159
2961
  executed task (success or failed — a hit replays its stored output and
3160
2962
  shows none), `OUTPUT` only when the task wrote something,
3161
- `SANDBOX VIOLATIONS (N)` only when the sandbox recorded some or vx
3162
- has a note on a failure. The
2963
+ `SANDBOX VIOLATIONS (N)` only when the sandbox recorded some (`N`
2964
+ counts them) or `SANDBOX` when vx has only a note on a failure. The
3163
2965
  header carries the outcome (`restored-local • abc12345`, `failed (exit
3164
2966
  N)`, …) and the footer repeats it after the duration. A test renders
3165
2967
  this block and checks it against this page, byte for byte.
@@ -3206,17 +3008,27 @@ A broad run looks like:
3206
3008
  ```
3207
3009
 
3208
3010
  The three bars are meters: `projects` is what the run covered against
3209
- the workspace, `tasks` is failed / success / skipped, `cache` is miss /
3210
- no-cache / up-to-date / local / remote (a skipped task rides it too, so
3211
- the two bars fill alike; its count is on the tasks legend only). Its
3212
- legend reads per layer: `local 1● 1○ 2↓ 4↑ · remote 3↓ 4↑`.
3213
- `●` is a fresh hit (outputs already on disk, whichever layer answered,
3214
- so it counts under local), `○` a task with no cache, `↓` is restored
3215
- from that layer and `↑` saved to it (local `↑` counts only tasks that ran here; remote `↑` counts
3216
- uploads that landed). A layer shows only when the run's
3217
- `--cache-policy` uses it. `N not saved` (ran, succeeded, saved nothing),
3218
- `N not downloaded` (outputs left in the remote store) and `N ↑ failed`
3219
- show only when non-zero. The
3011
+ the workspace, `tasks` is failed / success / skipped, `cache` has a
3012
+ segment per legend state (a skipped task rides it too, so the two bars
3013
+ fill alike; its count is on the tasks legend only). Its legend reads
3014
+ per layer: `local 1✓ 1∅ 2↓ 1↑ · remote 3↓ 4↑ 2✗ 1⇡`. `✓` is a fresh
3015
+ hit (outputs already on disk, whichever layer answered, so it counts
3016
+ under local), `∅` a task with no cache, `↓` is restored from that
3017
+ layer and `↑` saved to it, `✗` a task a remote read or write failed
3018
+ for, and `⇡` an upload still running (live only, always last). The
3019
+ region stays through vx's closing steps, worker rows gone, and the
3020
+ final footer replaces it in place. Each task counts once, under its strongest state: a
3021
+ failed remote read or write, then an upload running, then saved to a
3022
+ remote, then saved here, then restored, then fresh, then no cache; so
3023
+ a task saved here and uploaded is a remote `↑`. A key the remote already
3024
+ held is not sent, so it is no remote `↑`. Each count is painted
3025
+ in its segment's colour, number and glyph alike: `✓` green, `∅` dim,
3026
+ local blue, remote dark blue, `✗` and a miss red, `⇡` cyan; the
3027
+ `local` and `remote` words are dim. A layer shows only when the run's
3028
+ `--cache-policy` uses it. `N not saved` (ran, succeeded, saved
3029
+ nothing), `N not downloaded` (outputs left in the remote store) and
3030
+ every count show only when non-zero. `vx last` names each remote
3031
+ failure on its task's row. The
3220
3032
  `time` row is the wall time, then what the cache saved (the hits' stored
3221
3033
  exec times, never the restore), then the spread over executed tasks
3222
3034
  only — a hit's restore time never enters it. The footer never says a
@@ -3234,12 +3046,22 @@ running a group focused surfaces its real member tasks instead.
3234
3046
  **Nothing below the footer.** The footer is the run's last word.
3235
3047
  What a run has to say about one task rides that task's row; what it
3236
3048
  says about the run (a server that died, outputs left remote, a file
3237
- `--summarize` / `--profile` wrote, a history, upload, plugin or sandbox
3049
+ `--profile` wrote, a history, upload, plugin or sandbox
3238
3050
  warning) prints above the footer: it prints once the run's closing work
3239
3051
  is done, the `VX_DEBUG_TIMING=1` table included. Cache housekeeping
3240
3052
  (`cache.retention`) prints nothing. Only a kept server's own output
3241
3053
  follows it.
3242
3054
 
3055
+ **Narrow terminals.** The footer is 62 columns: its rule and 50-cell
3056
+ bars. On a terminal narrower than that the bars shrink and the rule
3057
+ with them, as do a frame's section rules, so none wraps onto a second
3058
+ row. Off a terminal they keep their width.
3059
+
3060
+ **Warnings on stderr.** A run's warnings, such as `[vx] app#build:
3061
+ cache.outputs matched no files …` or a plugin's, go to stderr. Stdout
3062
+ carries only the rows, the frames and the footer, so `vx run build >
3063
+ log.txt` keeps them out of the log.
3064
+
3243
3065
  **Skipped rows.** A red run lists every task that never started as a
3244
3066
  row naming the failure that blocked it, so the footer's `1 skipped`
3245
3067
  always has a name above it:
@@ -3301,6 +3123,7 @@ ANSI truecolor (`ansi-16m`) sequences, gated by env:
3301
3123
  | `FORCE_COLOR=0` / `=false` | Force off, on a TTY too. |
3302
3124
  | `FORCE_COLOR=…` (any other) | Force on — `1`, `true`, `2`, `3`, the empty string, etc. |
3303
3125
  | (neither) | On iff `stdout.isTTY`. |
3126
+ | `TERM=dumb` | Off unless `FORCE_COLOR` forces it, and no live region. |
3304
3127
 
3305
3128
  `0` and `false` are `FORCE_COLOR`'s "off" by the convention
3306
3129
  supports-color, chalk and Node follow; until 2026-09-24 vx read every
@@ -3326,7 +3149,7 @@ Only a run's own output (`vx run`, `vx watch`) is painted, and the
3326
3149
  help on a terminal: headings bold, `vx <verb>` and each flag or example
3327
3150
  in cyan; `NO_COLOR` or `FORCE_COLOR=0` turns it off
3328
3151
  (`tests/help-colors.test.ts`). Every other verb (`show`, `info`, `why`,
3329
- `last`, a `--dry` plan, `--graph`, `cache clean`), and the help off a
3152
+ `last`, a `--dry` plan, `cache clean`), and the help off a
3330
3153
  terminal, prints plain text whatever `FORCE_COLOR` says, so a script
3331
3154
  that forces colour for a run's log still parses them
3332
3155
  (`tests/cli-colors-e2e.test.ts`).
@@ -3343,7 +3166,7 @@ Reads try local first, then remote (hydrating local on remote hit),
3343
3166
  with a background prefetch pass overlapping remote GETs with
3344
3167
  execution. Writes go to local immediately; the remote upload is a
3345
3168
  fire-and-forget background task drained at end of run — failures are
3346
- logged via `onRemoteError` but never fail the build.
3169
+ warned but never fail the build.
3347
3170
 
3348
3171
  For any OTHER cache server (a Turbo-wire deployment, S3-direct, …),
3349
3172
  implement core's `RemoteCacheLayer` interface in a plugin's `cache`
@@ -3358,7 +3181,7 @@ consult).
3358
3181
  size, runs + hits in the last 24 h). For anything deeper, vx records
3359
3182
  every task to a `runs` table in `cache.db` (UUIDv7 `run_id`, hrtime
3360
3183
  wallclock spans, cpu_ms, peak RSS, status, cache_hit flag) plus one
3361
- `invocations` header row per run (command, git/CI context, tags,
3184
+ `invocations` header row per run (command, git/CI context,
3362
3185
  counts). The SQLite file IS the API:
3363
3186
 
3364
3187
  ```sh
@@ -3407,7 +3230,7 @@ Surface:
3407
3230
 
3408
3231
  - `run(options)` — execute. Returns `Promise<RunSummary>`.
3409
3232
  - `planRun(options)` — predict, no execute. Returns
3410
- `Promise<RunPlan>`. Used by `--dry` / `--graph`.
3233
+ `Promise<RunPlan>`. Used by `--dry`.
3411
3234
  - `prepareRun(options, log)` — the shared setup (discovery → configs →
3412
3235
  graph → cache). What an embedder builds on.
3413
3236
  - `defineProject` / `defineWorkspace` — identity helpers for type