A live run now shows what vx itself is doing and what each idle worker waits on, the footer counts remote failures and uploads in flight, --force no longer re-uploads what the cache holds, vx lock refuses fewer configs that were fine, and vx run sheds nine flags.
A live run now shows what vx itself is doing and what each idle worker
waits on. The footer counts remote failures and uploads still in flight,
and --force no longer re-uploads what the cache holds. vx lock refuses fewer
configs that were fine, and vx run sheds nine flags.
Read the breaking changes before you update: most
are flags that are gone; plugin authors also get a new put rule and API changes.
A run spends time on its own work too: reading the lock, keying tasks,
setting up plugins, uploading. On a terminal that time showed as a
frozen screen before the first task and after the last. And once a
worker finished its task, its row sat there as a dim idle, so you saw
what was running but not what was queued behind it.
Now each of vx’s own steps is a row beside your tasks, named
<owner>:<step>: vx:keys, vx:uploads, @vzn/vx-otel:setup. It
shows its elapsed time while it runs, and a step under 150 ms is not
shown at all. A step is not a task: it has no count, history row or
forecast. An idle worker row now holds a task that is blocked, and names
the running task holding it back:
Terminal window
2.42srunning~18ssolid-js#typesblocks2
blockedsolid-js#buildbysolid-js#types
blockedsolid-js#testbysolid-js#types
When the blocked task starts, it runs in the row that held it. Each half
of a task id also has a colour of its own now, so every #build looks
the same across projects and runs, and no id uses red, green, yellow or
cyan, the colours of an outcome.
These rows show only on a terminal. CI logs and piped output are
unchanged. The output also behaves better around the edges:
On a terminal under 62 columns, such as a side panel, the footer’s
bars and the frames’ rules shrink to fit instead of wrapping.
With TERM=dumb (Emacs M-x shell, some CI log viewers) vx prints
plain rows, as it does to a pipe.
Warnings go to stderr, so vx run build > build.log keeps only the
rows and the footer.
After a Ctrl-C, the aborted rows list above the failure frames, which
close the log.
A failed remote read became a miss without a word in the footer, and
uploads still running at the end of a run were invisible. The two cache
dots, ● for a fresh hit and ○ for no cache, were also easy to mix
up.
Now a fresh hit is ✓ and a task with no cache is ∅. The remote part
of the cache line adds ✗ for tasks whose remote read or write failed,
and ⇡ for uploads still running. A run that ends with uploads in
flight keeps its footer on screen until they finish. A task saved here
and uploaded counts once, as a remote ↑:
vx last names a remote failure on the task’s row, such as
remote ✗ download: connection refused; upload: 503. --dry uses the
same marks, so a plan and its run read alike.
vx run --force re-ran every task and then uploaded every result, even
when the remote already held that key. On CI that was one upload per
task for bytes the cache already had.
Now --force re-runs every task and keeps what is stored under the same
key, locally and remotely. Each
remote is asked which keys it holds: once for the whole run when its
plugin has hasMany, else has per key just before that upload. A held
key is not sent again. No task waits for those answers, and a failed
probe means vx uploads, as before.
The same rule now binds cache plugins. vx no longer asks has before a
put, so a remote’s put of a key it already holds must keep the stored
bytes. turboCache() and reapi() do; Nx servers refuse the overwrite
themselves. A bad entry is removed at the server, or locally with
vx cache clean.
A stopped run also stops waiting on uploads. After a Ctrl-C or a CI
cancellation it starts no more uploads, does not wait on the one in
flight, prints its footer and exits with the signal’s code.
vx lock checks that no task’s key reads another task’s outputs, and
that no two cached tasks write the same path. Both checks refused
configs that were safe.
A key reads only what git lists, and git never lists an ignored file. So
an output git ignores whole is no longer an overlap. A lint reading
**/* beside a build writing a gitignored **/dist/** now locks:
vx asks git rather than reading a .gitignore, so a rule in any
.gitignore or in .git/info/exclude counts. Without the rule, vx lock
still refuses and names the !**/dist/** entry to add.
A ! entry in a task’s outputs now counts too. A build writing **/dist
with !coverage/** no longer clashes with a task writing coverage/**,
because the two never touch the same file. bunx @vzn/vx-migrate uses
this to keep such a task cached where it used to leave it uncached.
Nine vx run flags are gone. Most repeated what another command already
does; the rest set a value nobody needed to change. A removed flag
refuses and says what to use instead:
Terminal window
$vxrunbuild--all--summarize
✗vxrun--summarize (turbo): use `vx last --format json`for the run's JSON record
see vx run --help
--summarize and --format json → vx last --format json after the
run.
--graph → vx run <task> --dry=json, which lists every planned task
with its deps.
--report and --report-file → github() from @vzn/vx-ci, which
writes the GitHub job summary.
--tag → nothing; no command ever read it back.
The ready notice, the kill grace and the teardown bound → fixed at
10 s, 2 s and 3 s.
The first run resets vx’s local index once: run history starts over,
and cached outputs still hit. Then:
--summarize, --format json, --graph, --report,
--report-file, --tag, --ready-notice, --kill-grace and
--teardown-timeout → removed, with run.readyNotice,
run.killGrace, run.teardownTimeout and their VX_RUN_*
variables. A leftover key in .vx/workspace.ts is refused.
--force → keeps an entry already stored under the same key. A cache
plugin’s put of a key it holds must keep the stored bytes.
reapi() with an IP address over TLS → refused at startup. Name the
server by host name, or use grpc:// for plaintext.
Footer ● / ○ → ✓ / ∅; the local ↑ no longer counts a task
that was also uploaded.
Plugin API: RunContext.cache.uploads → traffic;
CacheLayer.save loses skipLocalWrite; RunOptions.tags,
summarize and json are removed; telemetry schema → 4.
Run warnings → stderr; their text is unchanged.
The flag and plugin API changes are also on Upgrading.
Inputs move onto the task, so every task is keyed whether it caches or not. A gitignored file or a symlink you name is an input. A test can read what its build wrote, vx upgrade moves your plugins with it, and a run waits on less.
Every vx verb now speaks in one shape and says how to fix what it refuses, a CI run keeps the logs and outputs you need to debug it, a new branch forecasts from main's timings, and a run your local cache answers stays off the network.
A cache hit now rewrites only the outputs that changed, in place, and writes nothing a task did not declare. A local hit restores while its dependencies still run. A generated client in node_modules can be an input, vx upgrade -g moves only the global vx, and only vx decides what reaches a remote cache.
Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.