@vzn/vx 0.0.639 → 0.0.641
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 +1 -1
- package/docs/caching.md +158 -106
- package/docs/cli.md +80 -45
- package/docs/execution.md +11 -10
- package/docs/features.md +9 -8
- package/docs/patterns.md +1 -1
- package/docs/schema.md +252 -190
- package/docs/security.md +5 -4
- package/launcher.sh +9 -2
- package/package.json +5 -5
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +4 -3
- package/src/bin.ts +8 -6
- package/src/cache/archive.ts +248 -41
- package/src/cache/cache.ts +46 -19
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +2 -2
- package/src/cache/index.ts +6 -0
- package/src/cache/inputs.ts +228 -111
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +15 -8
- package/src/cache/layered-cache.ts +8 -7
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cache/scoped-cache.ts +8 -2
- package/src/cli/foreign-flags.ts +1 -1
- package/src/cli/help.ts +7 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +24 -1
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +9 -6
- package/src/cli/upgrade.ts +132 -21
- package/src/cli/watch-filter.ts +23 -12
- package/src/cli/watch-fs.ts +2 -1
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +74 -2
- package/src/cli/why.ts +5 -3
- package/src/config.ts +19 -15
- package/src/exec/executor.ts +4 -4
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +8 -1
- package/src/graph/scheduler.ts +41 -15
- package/src/graph/task-graph.ts +135 -73
- package/src/handoff.ts +5 -0
- package/src/index.ts +2 -2
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-task.ts +29 -25
- package/src/orchestrator/hit-restore.ts +101 -27
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +9 -1
- package/src/orchestrator/persistent-task.ts +3 -3
- package/src/orchestrator/placement.ts +2 -2
- package/src/orchestrator/plan.ts +1 -1
- package/src/orchestrator/plugin-host.ts +66 -27
- package/src/orchestrator/plugin.ts +12 -2
- package/src/orchestrator/prepare.ts +12 -5
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/run-report.ts +145 -46
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run.ts +76 -44
- package/src/orchestrator/sandbox-fix.ts +1 -1
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/stable-keys.ts +59 -26
- package/src/orchestrator/steps.ts +26 -6
- package/src/orchestrator/task-hash.ts +31 -9
- package/src/orchestrator/telemetry-host.ts +3 -3
- package/src/orchestrator/telemetry.ts +9 -4
- package/src/orchestrator/upstream.ts +4 -181
- package/src/report/framed-output.ts +1 -1
- package/src/report/logger.ts +5 -2
- package/src/report/status-line.ts +7 -3
- package/src/report/summary.ts +6 -4
- package/src/settings.ts +11 -17
- package/src/util/index.ts +3 -0
- package/src/util/paths.ts +39 -5
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-eval.ts +2 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
- package/src/workspace/project-loader.ts +5 -4
package/README.md
CHANGED
|
@@ -65,8 +65,8 @@ export default defineProject({
|
|
|
65
65
|
build: {
|
|
66
66
|
exec: { command: 'tsc -b' },
|
|
67
67
|
dependsOn: ['^build'], // dependencies build first
|
|
68
|
+
inputs: { files: ['src/**', 'tsconfig.json'] },
|
|
68
69
|
cache: {
|
|
69
|
-
inputs: { files: ['src/**', 'tsconfig.json'] },
|
|
70
70
|
outputs: { files: ['dist/**'] },
|
|
71
71
|
},
|
|
72
72
|
},
|
package/docs/caching.md
CHANGED
|
@@ -8,8 +8,10 @@ invalidation_, _what's actually stored_, and _why_.
|
|
|
8
8
|
## Why caching is opt-in
|
|
9
9
|
|
|
10
10
|
A task is cached iff its `TaskConfig` provides a `cache` block, with
|
|
11
|
-
|
|
12
|
-
task always runs; no read, no write.
|
|
11
|
+
`cache.outputs.files` and the task's `inputs.files`. Omit `cache` and
|
|
12
|
+
the task always runs; no read, no write. `inputs` stands alone: a task
|
|
13
|
+
is keyed on it whether it caches or not, and its dependents fold that
|
|
14
|
+
key.
|
|
13
15
|
|
|
14
16
|
The reasoning:
|
|
15
17
|
|
|
@@ -55,9 +57,9 @@ over (in order):
|
|
|
55
57
|
`exec.remote` is dropped, because it says WHERE a task runs and not
|
|
56
58
|
what it does (§ What's NOT in the key). Captures:
|
|
57
59
|
- `exec` block (command, env declarations, timeout, persistent).
|
|
58
|
-
- `dependsOn` and `
|
|
59
|
-
- `cache.outputs.files`, `
|
|
60
|
-
`
|
|
60
|
+
- `dependsOn` and `inputs.tasks` declarations.
|
|
61
|
+
- `cache.outputs.files`, `inputs.files`, `inputs.env`,
|
|
62
|
+
`inputs.runtime` / `workspaceRuntime` declarations (the
|
|
61
63
|
strings themselves; their resolved file content / env values /
|
|
62
64
|
command output contribute separately).
|
|
63
65
|
- `description` (because it's part of the resolved object — even
|
|
@@ -73,14 +75,14 @@ over (in order):
|
|
|
73
75
|
pulled deps don't see them (their cache identity stays clean) — and
|
|
74
76
|
to a command: a requested default `build` runs none and folds none
|
|
75
77
|
(X-119).
|
|
76
|
-
5. **`
|
|
78
|
+
5. **`inputs.env` resolved values** — `[name, value]` pairs
|
|
77
79
|
read from host `process.env` at hash time (delimited `name\0value`
|
|
78
80
|
so boundaries are unambiguous). Listed names get their current
|
|
79
81
|
values; an unset name folds its bare name, with no `\0`, so unset
|
|
80
82
|
and set-to-empty are two keys (the child tells them apart:
|
|
81
83
|
`passThrough` leaves an unset name out). A name holding a NUL is
|
|
82
84
|
refused at load.
|
|
83
|
-
6. **`
|
|
85
|
+
6. **`inputs.runtime` resolved output** — `[command, output]`
|
|
84
86
|
pairs, where `output` is the trimmed stdout of each command run via
|
|
85
87
|
`sh -c` in the **project dir** at hash time; a command with stderr
|
|
86
88
|
(or a NUL in stdout) folds `\0<stdout length>\0<stdout><stderr>`,
|
|
@@ -88,12 +90,12 @@ over (in order):
|
|
|
88
90
|
The runtime-output analog of step 5: the command _strings_ are in
|
|
89
91
|
the resolved config (step 3), their _output_ is resolved live every
|
|
90
92
|
run. Folded with the command count + each `command\0output` pair.
|
|
91
|
-
7. **`
|
|
93
|
+
7. **`inputs.workspaceRuntime` resolved output** — same as step
|
|
92
94
|
8 but commands run at the **workspace root**, and the pairs fold
|
|
93
95
|
into a **distinct namespace** (`ws-runtime-values:`) so an identical
|
|
94
96
|
`(command, output)` never aliases the project-cwd `runtime` values.
|
|
95
97
|
8. **Filtered upstream task cache hashes** — every upstream task's
|
|
96
|
-
own cache key, filtered by `
|
|
98
|
+
own cache key, filtered by `inputs.tasks` (default: all of
|
|
97
99
|
them). Sorted by hash before folding so the ordering of `dependsOn`
|
|
98
100
|
doesn't change the key. This is the cascade mechanism: if anything
|
|
99
101
|
beneath you changes, your hash changes too. The set is `dependsOn`'s,
|
|
@@ -103,7 +105,7 @@ over (in order):
|
|
|
103
105
|
whatever is on disk, which nothing proves current, so a task whose
|
|
104
106
|
key folds one, and everything built on it, may hit but does not
|
|
105
107
|
save (`orchestrator/excluded-keys.ts`); a task with
|
|
106
|
-
`
|
|
108
|
+
`inputs.tasks: []` folds none and saves as usual.
|
|
107
109
|
9. **Plugin key material** — the `{ name: value }` pairs a plugin's
|
|
108
110
|
`key(task, ctx)` stage returned for this task, stored on the node as
|
|
109
111
|
sorted `plugin/name` pairs and folded after the upstream keys and BEFORE the input files,
|
|
@@ -147,7 +149,7 @@ over (in order):
|
|
|
147
149
|
[`modules/fingerprint-watch.md`](./modules/fingerprint-watch.md)).
|
|
148
150
|
With nothing claimed, no root file is read or watched.
|
|
149
151
|
|
|
150
|
-
10. **Input files' content hashes** — `
|
|
152
|
+
10. **Input files' content hashes** — `inputs.files` resolved to
|
|
151
153
|
a concrete list of project-relative paths (gitignore-aware,
|
|
152
154
|
declared-outputs-excluded, nested-projects-excluded), each file
|
|
153
155
|
contributing its **git blob OID** (v20). On a clean tree the OID
|
|
@@ -216,19 +218,23 @@ add` under a clean filter (`core.autocrlf=true`, a `text` rule)
|
|
|
216
218
|
same shape — git reports the nested repository as one changed path,
|
|
217
219
|
and every project under it is selected.
|
|
218
220
|
|
|
219
|
-
A task with **no `cache` block** derives a key too
|
|
220
|
-
fold it (step 8)
|
|
221
|
-
its
|
|
222
|
-
and
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
221
|
+
A task with **no `cache` block** derives a key too, from its
|
|
222
|
+
`inputs` — its dependents fold it (step 8), as Turbo and Nx hash
|
|
223
|
+
every task. That key is for its dependents only: the task always
|
|
224
|
+
runs, and no cache, local or remote, is asked about or handed
|
|
225
|
+
anything under it; its log goes to the run's own store
|
|
226
|
+
(`run/<id>/<task>/log`). A lookup follows `cache`, never a key
|
|
227
|
+
(`tests/uncached-no-lookup.test.ts`). One that also declares no
|
|
228
|
+
`inputs` is **keyless**: no
|
|
229
|
+
key can stand for what it read, so a key that folds it is this
|
|
230
|
+
run's alone, and every task behind it misses and saves nothing.
|
|
231
|
+
A task reading `**` with no `cache` folds its own outputs too,
|
|
232
|
+
since it declared none: its key moves whenever it writes a file
|
|
233
|
+
git does not ignore, so a cached dependent misses once more after
|
|
234
|
+
the upstream's first run. Ignore the outputs in git, narrow its
|
|
235
|
+
`inputs`, or declare the block (`tests/uncached-upstream-key.test.ts`).
|
|
236
|
+
|
|
237
|
+
Declaring no outputs, such a task may also have **written** anywhere
|
|
232
238
|
in its project, so what the run learned about that project at its
|
|
233
239
|
start — the git listing and the index OIDs — is dropped once its command exits, pass or fail, and a later key
|
|
234
240
|
re-lists and hashes by content. For the same reason a task after it
|
|
@@ -260,20 +266,19 @@ add` under a clean filter (`core.autocrlf=true`, a `text` rule)
|
|
|
260
266
|
dropping out of the key or the artifact without a word
|
|
261
267
|
(turborepo#9345). Rename it, or exclude it with a negated glob.
|
|
262
268
|
|
|
263
|
-
Your globs are a **filter over the set git reports**, so a
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
a
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
under it, folds nothing, and is refused (item 576).
|
|
269
|
+
Your globs are a **filter over the set git reports**, so a broad
|
|
270
|
+
glob never reaches a gitignored file. An entry that names one
|
|
271
|
+
outright does: a literal (`.env.local`, `gen`), or a glob whose fixed
|
|
272
|
+
prefix git ignores (`dist/**`), walked on disk under that prefix
|
|
273
|
+
only (owner, 2026-10-16). Such a file is hashed from disk with the
|
|
274
|
+
blob formula an untracked file gets, and is never stored or
|
|
275
|
+
restored. `--affected` reflects only what git sees, so an edit to
|
|
276
|
+
one never selects a task on its own (as in Nx); `vx watch` re-runs
|
|
277
|
+
it on an edit there.
|
|
278
|
+
A **glob** matching nothing stays silent, and so does a literal
|
|
279
|
+
naming a path that does not exist, until it does. An ignored
|
|
280
|
+
directory named outright folds every file under it; an empty one
|
|
281
|
+
folds nothing.
|
|
277
282
|
|
|
278
283
|
A **bracket is literal** in a task glob, and there are no character
|
|
279
284
|
classes (item 667): `app/[id]/**` folds the route directory
|
|
@@ -315,7 +320,15 @@ add` under a clean filter (`core.autocrlf=true`, a `text` rule)
|
|
|
315
320
|
second by two, since FAT32 keeps even seconds; a rewrite later in
|
|
316
321
|
the same stamp keeps it (`racyWindowMs`; until 2026-09-27, A-2 and
|
|
317
322
|
A-38, such a rewrite of the same size was a hit on the first bytes'
|
|
318
|
-
output).
|
|
323
|
+
output). Every such window compares a file's stamp, on its file
|
|
324
|
+
system's clock, with a moment vx read from its own, so a run first
|
|
325
|
+
measures how far the file system runs behind (one file written and
|
|
326
|
+
removed in the cache dir) and widens every window by a lag past the
|
|
327
|
+
kernel's tick (20 ms): a WSL2 VM drifted from the Windows host that
|
|
328
|
+
stamps `/mnt/c`, a Docker Desktop bind mount or an NFS server dated an
|
|
329
|
+
edit made while a command ran before the command, and the entry
|
|
330
|
+
filed the edit's output under the old bytes' key (until 2026-10-16).
|
|
331
|
+
That is the same value the index holds whenever no filter applies,
|
|
319
332
|
so a file's contribution doesn't flip across dirty↔clean
|
|
320
333
|
transitions. Folded as `(relPath, identity)` pairs, sorted for
|
|
321
334
|
stability across OSes and walk orders. The identity is the OID
|
|
@@ -330,19 +343,21 @@ add` under a clean filter (`core.autocrlf=true`, a `text` rule)
|
|
|
330
343
|
there a clean file's mode comes from an lstat too, and its OID
|
|
331
344
|
from the index (item 1076).
|
|
332
345
|
|
|
333
|
-
A **symlink** folds as git folds it
|
|
334
|
-
_string_ (its mode-120000 index OID),
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
A link
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
+
A **symlink** folds as git folds it, the blob of its target
|
|
347
|
+
_string_ (its mode-120000 index OID), and then the content behind
|
|
348
|
+
it, as Nx reads a linked file where it points: a file's identity, or
|
|
349
|
+
for a directory every file beneath it at any depth, tracked or not
|
|
350
|
+
(`.git`, `node_modules`, `.vx/cache` and `.yarn/cache` aside, as Nx's walker skips them), a link
|
|
351
|
+
among them followed in turn. The target may be anywhere: in the
|
|
352
|
+
project, elsewhere in the workspace, outside it. Retargeting a link
|
|
353
|
+
changes the key, to equal bytes too; an edit behind it changes the
|
|
354
|
+
key. A dangling link, or a loop of links, keys its target string
|
|
355
|
+
alone. `--affected` selects a link whose target changed (a file
|
|
356
|
+
the diff names, or one under a linked directory) as a changed path of
|
|
357
|
+
its project, and a target git cannot diff (outside the workspace,
|
|
358
|
+
git-ignored) on every run; `vx watch` arms each link's target and
|
|
359
|
+
names the link when it changes (`linked-inputs.ts`). Nx does not
|
|
360
|
+
enter a linked directory; vx does, since the task reads it.
|
|
346
361
|
|
|
347
362
|
The composition is seed-chained (`xxh3(part, prevDigest)`) with a
|
|
348
363
|
label prefix per field, so two different field layouts can't collide.
|
|
@@ -384,12 +399,26 @@ On a hit:
|
|
|
384
399
|
clock (4 ms at HZ=250), so a same-size rewrite inside the tick of
|
|
385
400
|
vx's own write, in place or as a new file that took the same inode,
|
|
386
401
|
with its mtime forged back, still matches.
|
|
387
|
-
3. Otherwise the
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
`
|
|
391
|
-
|
|
392
|
-
|
|
402
|
+
3. Otherwise the `outputs/<rel>` (+ `workspace-outputs/<rel>`) entries
|
|
403
|
+
are extracted from the artifact (its bytes inline in the index, read
|
|
404
|
+
in one read transaction with the entry's rows, or
|
|
405
|
+
`<cacheDir>/<hash>.tar.zst`) **in place**: every entry is staged in
|
|
406
|
+
the workspace's `.vx/tmp/` (never an output folder), and only once the
|
|
407
|
+
whole archive has staged is each renamed over the file it replaces. A
|
|
408
|
+
file that already holds the entry's bytes (compared byte for byte, up
|
|
409
|
+
to 4 MiB) is left in place, only its mode and mtime set. A
|
|
410
|
+
target on another filesystem goes through a temp beside it; a file
|
|
411
|
+
takes the group its directory gives new files (setgid on Linux, any
|
|
412
|
+
directory on macOS), ACLs are not copied. The next run removes
|
|
413
|
+
the staging a killed run left. A process reading the output tree during the
|
|
414
|
+
restore never finds a declared output missing or half-written, and a
|
|
415
|
+
restore that fails first (a full disk, a damaged entry) leaves the
|
|
416
|
+
outputs as they were. Then the declared outputs the entry does not
|
|
417
|
+
hold are removed — see
|
|
418
|
+
[§ Strict output ownership](#strict-output-ownership). Only a found
|
|
419
|
+
output that blocks a rename (a file where the entry needs a directory,
|
|
420
|
+
or the reverse) goes before. Turbo also restores in place; Nx deletes
|
|
421
|
+
and copies each file, and a reader meets a missing or short file.
|
|
393
422
|
4. Captured stdout is replayed to the live terminal via the logger —
|
|
394
423
|
the framed block looks like a fresh run. (stderr is not cached —
|
|
395
424
|
only successful runs are cached and their stderr is near-always
|
|
@@ -452,10 +481,12 @@ root-anchored output can land in any project's directory — and every
|
|
|
452
481
|
transitive dependant of one with it (their up-front keys fold a
|
|
453
482
|
preliminary key); a glob with no literal prefix reaches every project.
|
|
454
483
|
Before item 584 one such declaration emptied the tier graph-wide.
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
484
|
+
`vx lock` refuses a task whose input globs can match another task's
|
|
485
|
+
declared outputs (X-54, no opt-out), unless git ignores those outputs
|
|
486
|
+
whole (and the entry does not start inside them) or the producer is a
|
|
487
|
+
dependency its key does not fold, whose bytes it then hashes after the
|
|
488
|
+
producer ran, so the clauses on declared outputs fire only for such a
|
|
489
|
+
dependency read or a pair the check cannot decide. An output git ignores whole is no reach either:
|
|
459
490
|
a key reads only what git lists, so a `**/*` reader beside a gitignored
|
|
460
491
|
`**/dist/**` producer keys up front, the same key before and after the
|
|
461
492
|
build. The run asks git once, beside its walk and only when a reader
|
|
@@ -466,10 +497,11 @@ the glob matches (a tracked output, a directory a `!` rule takes back);
|
|
|
466
497
|
anything git cannot answer keeps the key late. Undeclared writes, an
|
|
467
498
|
`outputs.workspaceFiles` producer upstream, a rewriter the key does not
|
|
468
499
|
fold, and runtime probes after a writer still leave a key preliminary.
|
|
469
|
-
|
|
470
|
-
short-circuit
|
|
471
|
-
|
|
472
|
-
|
|
500
|
+
With a
|
|
501
|
+
remote declared the short-circuit probes the local store alone, so a
|
|
502
|
+
local hit still restores ahead of its dependencies and no remote GET
|
|
503
|
+
sits on the critical path (remote prefetch asks the remote for the
|
|
504
|
+
rest). It never fires with local reads off or on a local cache
|
|
473
505
|
that holds no artifact (`Cache.holdsNothing`: every probe would miss,
|
|
474
506
|
and a cold 1,090-package run started its first task ~90 ms sooner
|
|
475
507
|
without it), and never throws —
|
|
@@ -550,9 +582,8 @@ Hard invariants of the remote prefetch:
|
|
|
550
582
|
stat pass.
|
|
551
583
|
- **Stable keys only.** A task with a _preliminary_ key (§ Local
|
|
552
584
|
restore tier's stability gate: undeclared writes, an
|
|
553
|
-
`outputs.workspaceFiles` producer upstream, or
|
|
554
|
-
|
|
555
|
-
declared outputs) is not prefetched: its key would name the wrong
|
|
585
|
+
`outputs.workspaceFiles` producer upstream, or input globs that match
|
|
586
|
+
the outputs of a dependency the key does not fold) is not prefetched: its key would name the wrong
|
|
556
587
|
artifact, and it resolves via the lazy read-through in `execute-task`. Instability propagates: a task that folds an
|
|
557
588
|
unstable upstream is itself unstable. When in doubt, skip.
|
|
558
589
|
- **At most once.** The `LayeredCache` keeps an in-flight map keyed by
|
|
@@ -628,12 +659,14 @@ get/save and the file-hash memo's writes (never `recordRun` / `stats` / `prune`
|
|
|
628
659
|
read-through (`remoteRead`), upload (`remoteWrite`), and prefetch
|
|
629
660
|
(`remoteRead`). The orchestrator derives two booleans per task:
|
|
630
661
|
|
|
631
|
-
- `willRead = task has a cache block AND it is not remote-only AND (localRead || remoteRead)`
|
|
662
|
+
- `willRead = task has a cache block AND it is not remote-only AND it folds no keyless task AND (localRead || remoteRead)`
|
|
632
663
|
- `willWrite = task has a cache block AND it is not remote-only AND (localWrite || remoteWrite)`
|
|
633
664
|
|
|
634
665
|
A task reads the cache only when `willRead`, saves only when
|
|
635
|
-
`willWrite
|
|
636
|
-
|
|
666
|
+
`willWrite` and it folds no keyless task, and cleans its declared outputs
|
|
667
|
+
before exec only when `willWrite`. A keyless task runs a command and
|
|
668
|
+
declares no `inputs`: its key is unknown, so a task that folds it
|
|
669
|
+
(through `dependsOn`, directly or through a group) always misses. Remote-only is an `exec.remote: 'only'` task placed on a
|
|
637
670
|
remote executor: it never touches this machine's disk (no probe, no
|
|
638
671
|
restore, no output clean, no local save), and its result lives in the
|
|
639
672
|
remote executor's own store.
|
|
@@ -784,7 +817,7 @@ from the snapshot's index OID for an output (or for an input the task
|
|
|
784
817
|
rewrote) restored the bytes from before the command.
|
|
785
818
|
|
|
786
819
|
A declared set that resolves to **nothing** is said on the run's
|
|
787
|
-
status line. `
|
|
820
|
+
status line. `inputs matched no files (lib/**)` — the key would
|
|
788
821
|
not change when the source does — is said on every miss of a cacheable
|
|
789
822
|
task, right after `describeTaskInputs` and before the command, whether
|
|
790
823
|
or not it saves. `cache.outputs matched no files (build/**)` — an empty
|
|
@@ -804,7 +837,7 @@ the run (`tests/runner.test.ts`) — and the save resolves the output
|
|
|
804
837
|
globs at once. A descendant the command detached (`setsid … &`, a
|
|
805
838
|
daemon) that writes after that is not in the artifact: the entry saves
|
|
806
839
|
what was there, often nothing (with the `cache.outputs matched no
|
|
807
|
-
files` line), and the next hit's
|
|
840
|
+
files` line), and the next hit's restore removes what the
|
|
808
841
|
descendant wrote and restores the saved entry in its place (upstream
|
|
809
842
|
survey, turborepo#12786). vx does not wait for it and cannot see it: a
|
|
810
843
|
process that left the task's session is outside the process group vx
|
|
@@ -831,14 +864,15 @@ Declared `cache.outputs.files` are wiped in two distinct places:
|
|
|
831
864
|
- **Before exec on a cache miss.** A leftover `dist/old.js` from a
|
|
832
865
|
prior build can't survive into a fresh build that doesn't rewrite
|
|
833
866
|
it.
|
|
834
|
-
- **
|
|
835
|
-
cached snapshot byte-for-byte
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
867
|
+
- **After restore on a cache hit.** The post-restore tree is the
|
|
868
|
+
cached snapshot byte-for-byte: the restore renames the entry's files
|
|
869
|
+
over the ones it finds, then removes what the entry does not hold.
|
|
870
|
+
Hand-edits to output files don't persist through a cache replay. When
|
|
871
|
+
the hit's own check walked the output globs to find the tree stale,
|
|
872
|
+
the removal uses that walk's list instead of walking again (X-161).
|
|
873
|
+
|
|
874
|
+
Both use the same helpers (`cleanOutputs`, `cleanOutputPaths` in
|
|
875
|
+
`src/cache/inputs.ts`) with the same boundary rules, and vx's own paths stay off the wipe
|
|
842
876
|
whatever the glob: `.git`, `.vx` and each `vx.lock.json` (`OUTPUT_NEVER`;
|
|
843
877
|
`.vx` holds the workspace config and the lock, which a clean deleted and a
|
|
844
878
|
hit restored stale while `.vx/cache` alone was listed). `node_modules` is
|
|
@@ -871,7 +905,7 @@ Skipped when:
|
|
|
871
905
|
`.tsbuildinfo`). You opt out of the clean-tree guarantee: the save
|
|
872
906
|
stores every file the outputs match after the run, so a leftover from
|
|
873
907
|
an older build is cached and restored everywhere. A retry cleans as
|
|
874
|
-
usual; a hit still
|
|
908
|
+
usual; a hit's restore still removes what its entry does not hold.
|
|
875
909
|
|
|
876
910
|
A declared output the process cannot remove (a `dist/` another user
|
|
877
911
|
wrote, a read-only checkout) fails the task with `cannot remove declared
|
|
@@ -948,13 +982,13 @@ A task's cache becomes invalid when any of these change:
|
|
|
948
982
|
| Any package manager updates a lockfile (`pnpm`, `npm`, `yarn`, `bun`) | step 9 — the `key` material of the plugin that claims it (`@vzn/vx-lockfile`: only the projects whose dependency closure moved); **NOT a trigger** with no such plugin, since core keys no package file |
|
|
949
983
|
| Edit an install setting (`.npmrc`, `bunfig.toml`, `pnpm-workspace.yaml`, `.yarnrc.yml`) | step 9 — the claiming lockfile plugin folds it whole into every task; **NOT a trigger** without one |
|
|
950
984
|
| Edit `package.json`'s `workspaces` field | membership: a project that joins or leaves changes which projects exist and which nested dirs its parent's globs exclude (step 10) |
|
|
951
|
-
| Edit the project's `package.json` (dep / version / scripts change) | step 10 — an input file of every cached task when `@vzn/vx-lockfile` is declared (it adds `package.json` to `
|
|
985
|
+
| Edit the project's `package.json` (dep / version / scripts change) | step 10 — an input file of every cached task when `@vzn/vx-lockfile` is declared (it adds `package.json` to `inputs.files`), or when a task lists it; **NOT a trigger** otherwise |
|
|
952
986
|
| Edit the task's `vx.config.ts` | step 3 (task config hash) |
|
|
953
987
|
| Edit a config file that the task config imports | step 3 (configHash sees the resolved object after Bun evaluates imports) |
|
|
954
988
|
| Change CLI `forwardArgs` (after `--`) | step 4 |
|
|
955
|
-
| Change a `
|
|
956
|
-
| Change the combined stdout+stderr of a `
|
|
957
|
-
| Change the combined stdout+stderr of a `
|
|
989
|
+
| Change a `inputs.env` host value | step 5 |
|
|
990
|
+
| Change the combined stdout+stderr of a `inputs.runtime` command (resolved at hash time) | step 6 |
|
|
991
|
+
| Change the combined stdout+stderr of a `inputs.workspaceRuntime` command (resolved at hash time) | step 7 |
|
|
958
992
|
| Upstream task's cache key changes (because its inputs changed) | step 8 |
|
|
959
993
|
| Bump `CACHE_VERSION` | step 1 — orphans every entry |
|
|
960
994
|
| Change `exec.env.passThrough` _values_ alone | **NOT a trigger** by design — passThrough values are host-specific |
|
|
@@ -968,7 +1002,7 @@ re-runs automatically.
|
|
|
968
1002
|
|
|
969
1003
|
## Cross-project boundaries
|
|
970
1004
|
|
|
971
|
-
A project's `
|
|
1005
|
+
A project's `inputs.files` globs **never** reach into another
|
|
972
1006
|
project's directory, even if a `**/*` pattern would otherwise match.
|
|
973
1007
|
|
|
974
1008
|
`workspace/nested-dirs.ts` computes the set of nested project
|
|
@@ -985,21 +1019,22 @@ globs is `dependsOn` + upstream-hash propagation (step 8).
|
|
|
985
1019
|
For a task with `exec.sandbox` and `cache`, the sandbox enforces that
|
|
986
1020
|
"only". A workspace package reached through a `node_modules` link is
|
|
987
1021
|
granted only when the task's key folds a task of that package, walking
|
|
988
|
-
the same selection `
|
|
989
|
-
(`orchestrator/keyed-projects.ts`, over `
|
|
1022
|
+
the same selection `inputs.tasks` applies at hash time
|
|
1023
|
+
(`orchestrator/keyed-projects.ts`, over `graph/folds.ts`'s
|
|
990
1024
|
`selectFoldedDeps`); every other link target inside the root is
|
|
991
1025
|
withheld, so an import of a sibling the key never sees fails and is
|
|
992
1026
|
reported instead of caching a result an edit there would not re-run.
|
|
993
|
-
Any exec task counts, cached or not:
|
|
994
|
-
`inputs
|
|
995
|
-
|
|
996
|
-
e2e behind a dev server re-runs when
|
|
1027
|
+
Any exec task counts, cached or not: its key folds its project's files
|
|
1028
|
+
through its `inputs`, and one with no `inputs` is keyless, so a key that
|
|
1029
|
+
folds it is never stored or read. A persistent task counts the same way,
|
|
1030
|
+
so a cached e2e behind a dev server keyed on its sources re-runs when
|
|
1031
|
+
they change.
|
|
997
1032
|
The coverage is per package, not per file: an edge to `ui#source`
|
|
998
1033
|
(inputs `src/**`) also admits a read of `ui/README.md`, whose edit moves
|
|
999
1034
|
no key — the same limit a grant wider than a task's own inputs already
|
|
1000
1035
|
has. A task with no `cache` keeps every link, having no key to be stale.
|
|
1001
1036
|
|
|
1002
|
-
**Exception:** `
|
|
1037
|
+
**Exception:** `inputs.workspaceFiles` /
|
|
1003
1038
|
`cache.outputs.workspaceFiles` are workspace-root-anchored and apply
|
|
1004
1039
|
NO boundary rule — a deliberate escape hatch (owner call: "they don't
|
|
1005
1040
|
care about boundaries; it is bad practice but is there"). Prefer
|
|
@@ -1061,8 +1096,8 @@ the gate exists to avoid paying needlessly.
|
|
|
1061
1096
|
|
|
1062
1097
|
## Runtime inputs and the lock (the env parallel)
|
|
1063
1098
|
|
|
1064
|
-
`
|
|
1065
|
-
exactly on `
|
|
1099
|
+
`inputs.runtime` / `inputs.workspaceRuntime` are modeled
|
|
1100
|
+
exactly on `inputs.env`, and they share its asymmetry with the
|
|
1066
1101
|
lockfile:
|
|
1067
1102
|
|
|
1068
1103
|
- **The command _strings_ live in the resolved config**, so `vx lock`
|
|
@@ -1214,7 +1249,7 @@ by this user, or point cacheDir / --cache-dir at one that is.` (A-40).
|
|
|
1214
1249
|
An input the key must hash that this user cannot read fails its task
|
|
1215
1250
|
naming the read: `<path> is not readable by this user (EACCES), and vx
|
|
1216
1251
|
reads it to derive a cache key. Make it readable, or, for a task input,
|
|
1217
|
-
take it out of
|
|
1252
|
+
take it out of inputs.files.` (A-50).
|
|
1218
1253
|
|
|
1219
1254
|
By default the entries and their artifacts live in a **shared store**
|
|
1220
1255
|
in `~/.vx/<id>/cache`, and each workspace keeps only its own index in
|
|
@@ -1314,8 +1349,9 @@ pax `path`/`size`, GNU long names, header checksums, truncation; an
|
|
|
1314
1349
|
extended header (read whole) past 1 MiB or a pax `size` that is not a
|
|
1315
1350
|
whole number is refused as corrupt, since a 1 GiB pax header costs 2 GiB
|
|
1316
1351
|
of memory from a ~32 KB artifact — and
|
|
1317
|
-
every regular entry is written
|
|
1318
|
-
|
|
1352
|
+
every regular entry is written to the workspace's `.vx/tmp/<pid>-0/`
|
|
1353
|
+
(vx's own, ignored by git, never an output folder) and renamed over its
|
|
1354
|
+
target only after the whole archive has ended cleanly and
|
|
1319
1355
|
the index's recorded outputs are all present. vx itself holds one
|
|
1320
1356
|
chunk of the tar at a time (measured 2026-09-03, incompressible
|
|
1321
1357
|
artifacts, fresh process: 150 MiB peaked at +644 MiB through
|
|
@@ -1394,7 +1430,14 @@ them, `CacheGetContext.outputs`), and never when it holds one path as a
|
|
|
1394
1430
|
file and a directory at once. Either used to reach the tree, an input
|
|
1395
1431
|
overwritten or a `.git/hooks` file planted under a green
|
|
1396
1432
|
`cache-hit-remote`, or a restore that failed every later run from the
|
|
1397
|
-
local copy (item 942); now the read is a miss and the task runs.
|
|
1433
|
+
local copy (item 942); now the read is a miss and the task runs. Every
|
|
1434
|
+
restore asks the same of the local copy, whatever route it reached the
|
|
1435
|
+
store by: an entry outside the declared outputs, one a `!` entry takes
|
|
1436
|
+
back, one under `.git`, `.vx` or a `vx.lock.json`, or one under a
|
|
1437
|
+
`node_modules` no output glob names fails the whole restore before
|
|
1438
|
+
anything is written; the entry is dropped and the task runs. Remote
|
|
1439
|
+
execution (`@vzn/vx-reapi`) writes a remote action's outputs through the
|
|
1440
|
+
same gate (`restorableOutput`). On the save side a
|
|
1398
1441
|
**symlinked output** is captured as its target's bytes and comes back
|
|
1399
1442
|
as a regular file (a hit that finds the task's own link still current
|
|
1400
1443
|
leaves it); a link to a directory, or a dangling one, has no bytes to
|
|
@@ -1850,10 +1893,19 @@ moat.
|
|
|
1850
1893
|
passthrough still bumps the key for affected tasks.
|
|
1851
1894
|
- **Files outside the project directory that aren't declared.**
|
|
1852
1895
|
Workspace-root configs (`tsconfig.base.json`, etc.) are not
|
|
1853
|
-
auto-included — declare them via `
|
|
1896
|
+
auto-included — declare them via `inputs.workspaceFiles`
|
|
1854
1897
|
(root-anchored globs).
|
|
1898
|
+
- **Gitignored files no entry names.** Vite and Next load `.env.local`
|
|
1899
|
+
on their own; a broad glob skips it, so editing it replays the old
|
|
1900
|
+
build. Name it: `files: ['src/**', '.env.local']`. Its bytes are
|
|
1901
|
+
hashed, never stored, and a missing file is simply not an input.
|
|
1902
|
+
Machines with different files compute different keys, so they miss
|
|
1903
|
+
each other's shared entries. `--affected` ignores it (only what git
|
|
1904
|
+
sees selects a task), and `vx watch` re-runs on its edit. A probe
|
|
1905
|
+
(`runtime: ['cat .env.local 2>/dev/null | cksum']`) still works where
|
|
1906
|
+
a command, not a path, decides what to read.
|
|
1855
1907
|
- **Node / Bun / OS / build-tool versions — unless you declare them.**
|
|
1856
|
-
The canonical mechanism is `
|
|
1908
|
+
The canonical mechanism is `inputs.runtime` /
|
|
1857
1909
|
`workspaceRuntime` (e.g. `workspaceRuntime: ['node -v']`): the
|
|
1858
1910
|
command output is resolved live at hash time, so it stays correct
|
|
1859
1911
|
under the lock. Avoid baking versions via
|
|
@@ -2187,11 +2239,11 @@ breaking footer).
|
|
|
2187
2239
|
committed, so git enumerates it, but it's vx's own frozen-config
|
|
2188
2240
|
metadata — never a task input. Without this, a `vx lock` re-write
|
|
2189
2241
|
busts every key on a project that globs the root lockfile (a broad
|
|
2190
|
-
`**/*` on the root project). Tasks whose `
|
|
2242
|
+
`**/*` on the root project). Tasks whose `inputs.files` never
|
|
2191
2243
|
matched it derive byte-identical keys. No SCHEMA bump — only the
|
|
2192
2244
|
hashed file set changed, not the key layout or on-disk format.
|
|
2193
2245
|
|
|
2194
|
-
- **v22 → v23**: fold `
|
|
2246
|
+
- **v22 → v23**: fold `inputs.runtime` / `workspaceRuntime`
|
|
2195
2247
|
command output into the key (two namespaced sections after
|
|
2196
2248
|
env-values). The command _strings_ stay in the config hash (step 3);
|
|
2197
2249
|
their combined trimmed stdout+stderr is resolved live at hash time
|
|
@@ -2236,7 +2288,7 @@ breaking footer).
|
|
|
2236
2288
|
- **v11 → v12** (PR #42): project's `package.json` bytes folded into
|
|
2237
2289
|
every task's cache key implicitly. Matches Turbo / Nx "implicit
|
|
2238
2290
|
dependencies" behavior — a `package.json` dep change invalidates
|
|
2239
|
-
the project's tasks even when `
|
|
2291
|
+
the project's tasks even when `inputs.files` is narrow and
|
|
2240
2292
|
doesn't cover the file.
|
|
2241
2293
|
- **v12 → v13** (PR #65): per-entry on-disk layout unified. Outputs
|
|
2242
2294
|
moved from `<hash>/<rel>` (mixed with metadata) to
|