@vzn/vx 0.0.639 → 0.0.640

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 (74) hide show
  1. package/README.md +1 -1
  2. package/docs/caching.md +111 -83
  3. package/docs/cli.md +70 -40
  4. package/docs/execution.md +4 -4
  5. package/docs/features.md +9 -8
  6. package/docs/patterns.md +1 -1
  7. package/docs/schema.md +235 -180
  8. package/docs/security.md +1 -1
  9. package/package.json +5 -5
  10. package/schemas/show.json +14 -14
  11. package/skills/vx/SKILL.md +4 -3
  12. package/src/cache/cache.ts +4 -1
  13. package/src/cache/file-hashes.ts +7 -12
  14. package/src/cache/fs-clock.ts +62 -0
  15. package/src/cache/git-inputs.ts +2 -2
  16. package/src/cache/index.ts +3 -0
  17. package/src/cache/inputs.ts +154 -83
  18. package/src/cache/key-fold.ts +23 -3
  19. package/src/cache/layer.ts +8 -7
  20. package/src/cache/linked-inputs.ts +87 -0
  21. package/src/cli/foreign-flags.ts +1 -1
  22. package/src/cli/init.ts +23 -11
  23. package/src/cli/last.ts +24 -1
  24. package/src/cli/plugin-templates.ts +2 -2
  25. package/src/cli/select.ts +16 -6
  26. package/src/cli/show.ts +9 -6
  27. package/src/cli/upgrade.ts +95 -19
  28. package/src/cli/watch-filter.ts +22 -6
  29. package/src/cli/watch-judge.ts +49 -5
  30. package/src/cli/watch-set.ts +18 -8
  31. package/src/cli/watch.ts +74 -2
  32. package/src/cli/why.ts +5 -3
  33. package/src/config.ts +19 -15
  34. package/src/exec/executor.ts +4 -4
  35. package/src/graph/dependency-spec.ts +2 -2
  36. package/src/graph/folds.ts +236 -0
  37. package/src/graph/index.ts +8 -1
  38. package/src/graph/scheduler.ts +41 -15
  39. package/src/graph/task-graph.ts +135 -73
  40. package/src/index.ts +1 -1
  41. package/src/orchestrator/affected-tasks.ts +20 -19
  42. package/src/orchestrator/download-policy.ts +16 -22
  43. package/src/orchestrator/excluded-keys.ts +4 -5
  44. package/src/orchestrator/execute-task.ts +29 -25
  45. package/src/orchestrator/index.ts +1 -0
  46. package/src/orchestrator/keyed-projects.ts +12 -51
  47. package/src/orchestrator/local-shortcircuit.ts +1 -1
  48. package/src/orchestrator/persistent-task.ts +3 -3
  49. package/src/orchestrator/placement.ts +2 -2
  50. package/src/orchestrator/plan.ts +1 -1
  51. package/src/orchestrator/plugin-host.ts +62 -22
  52. package/src/orchestrator/plugin.ts +12 -2
  53. package/src/orchestrator/prepare.ts +12 -5
  54. package/src/orchestrator/projects.ts +7 -10
  55. package/src/orchestrator/run-report.ts +124 -46
  56. package/src/orchestrator/run-steps.ts +57 -0
  57. package/src/orchestrator/run.ts +51 -21
  58. package/src/orchestrator/sandbox-fix.ts +1 -1
  59. package/src/orchestrator/sandbox-request.ts +2 -2
  60. package/src/orchestrator/stable-keys.ts +59 -26
  61. package/src/orchestrator/steps.ts +26 -6
  62. package/src/orchestrator/task-hash.ts +31 -9
  63. package/src/orchestrator/telemetry-host.ts +3 -3
  64. package/src/orchestrator/telemetry.ts +9 -4
  65. package/src/orchestrator/upstream.ts +4 -181
  66. package/src/report/framed-output.ts +1 -1
  67. package/src/report/status-line.ts +7 -3
  68. package/src/settings.ts +11 -17
  69. package/src/util/paths.ts +1 -1
  70. package/src/workspace/affected.ts +81 -2
  71. package/src/workspace/config-imports.ts +1 -1
  72. package/src/workspace/config-schema.ts +142 -143
  73. package/src/workspace/lockfile.ts +1 -1
  74. package/src/workspace/migration.ts +19 -3
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
- **both** `inputs.files` and `outputs.files`. Omit `cache` and the
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 `cache.inputs.tasks` declarations.
59
- - `cache.outputs.files`, `cache.inputs.files`, `cache.inputs.env`,
60
- `cache.inputs.runtime` / `workspaceRuntime` declarations (the
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. **`cache.inputs.env` resolved values** — `[name, value]` pairs
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. **`cache.inputs.runtime` resolved output** — `[command, output]`
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. **`cache.inputs.workspaceRuntime` resolved output** — same as step
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 `cache.inputs.tasks` (default: all of
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
- `cache.inputs.tasks: []` folds none and saves as usual.
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** — `cache.inputs.files` resolved to
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 — its dependents
220
- fold it (step 8) — and, declaring nothing, folds **every file in
221
- its project**: `**/*`, gitignore-aware, untracked files included,
222
- and its own outputs not excluded, since it declared none. So its
223
- key moves whenever it writes a file git does not ignore: a cached
224
- dependent misses once more after the upstream's first run and hits
225
- from the third. `vx why` on the dependent names the upstream as
226
- the moved component; on the upstream it says the file set moved
227
- and that no fingerprints say which. Ignore the outputs in git, or
228
- declare the block (`tests/uncached-upstream-key.test.ts` pins both
229
- arms).
230
-
231
- Declaring nothing, such a task may also have **written** anywhere
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 filter
264
- can only ever remove — a gitignored file can never be filtered back
265
- in, however explicitly you name it. Naming one by hand is therefore
266
- a **hard error** rather than a silent nothing: it would leave the
267
- task ignoring a file its own config claims as an input, reporting
268
- `up-to-date` while that file changed. Turbo lets an explicit entry
269
- override gitignore; vx cannot, because the key would then depend on
270
- a change `git diff` cannot see and `--affected` would stop selecting
271
- the task. If the file is generated, depend on the task that produces
272
- it via `cache.inputs.tasks`. A **glob** matching nothing stays
273
- silent — that is legitimate — and so does a literal naming a file
274
- that does not exist. A literal naming a **directory** is judged the
275
- same way: an ignored one, or an empty one, has no file git lists
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). That is the same value the index holds whenever no filter applies,
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: the blob of its target
334
- _string_ (its mode-120000 index OID), whether it points at a file,
335
- a directory, or nothing. Retargeting a link changes the key; the
336
- bytes behind a link to a directory, or to a file outside the
337
- project, do not — `git diff` and `--affected` cannot see them
338
- either, so declare them with `workspaceFiles`. A link to a file
339
- inside the project tracks that file's content only when the file
340
- matches the task's input globs too: it is then an input of its own.
341
- A link under `src/**` to `other/data.txt` folds the link, not
342
- `other/data.txt`; name the target in the globs to track it.
343
- (Before 2026-09-09 a file link folded the
344
- bytes behind it and a directory or dangling link fell out of the
345
- input set entirely, so retargeting one was a stale hit.)
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.
@@ -452,10 +467,12 @@ root-anchored output can land in any project's directory — and every
452
467
  transitive dependant of one with it (their up-front keys fold a
453
468
  preliminary key); a glob with no literal prefix reaches every project.
454
469
  Before item 584 one such declaration emptied the tier graph-wide.
455
- `rules.upfrontKeys` (on by default, X-54) refuses at load a task whose
456
- input globs can match another task's declared outputs, unless git
457
- ignores those outputs whole, so the clauses on declared outputs fire
458
- only with the rule off. An output git ignores whole is no reach either:
470
+ `vx lock` refuses a task whose input globs can match another task's
471
+ declared outputs (X-54, no opt-out), unless git ignores those outputs
472
+ whole (and the entry does not start inside them) or the producer is a
473
+ dependency its key does not fold, whose bytes it then hashes after the
474
+ producer ran, so the clauses on declared outputs fire only for such a
475
+ dependency read or a pair the check cannot decide. An output git ignores whole is no reach either:
459
476
  a key reads only what git lists, so a `**/*` reader beside a gitignored
460
477
  `**/dist/**` producer keys up front, the same key before and after the
461
478
  build. The run asks git once, beside its walk and only when a reader
@@ -550,9 +567,8 @@ Hard invariants of the remote prefetch:
550
567
  stat pass.
551
568
  - **Stable keys only.** A task with a _preliminary_ key (§ Local
552
569
  restore tier's stability gate: undeclared writes, an
553
- `outputs.workspaceFiles` producer upstream, or, with
554
- `rules.upfrontKeys: false`, input globs that match an upstream's
555
- declared outputs) is not prefetched: its key would name the wrong
570
+ `outputs.workspaceFiles` producer upstream, or input globs that match
571
+ the outputs of a dependency the key does not fold) is not prefetched: its key would name the wrong
556
572
  artifact, and it resolves via the lazy read-through in `execute-task`. Instability propagates: a task that folds an
557
573
  unstable upstream is itself unstable. When in doubt, skip.
558
574
  - **At most once.** The `LayeredCache` keeps an in-flight map keyed by
@@ -628,12 +644,14 @@ get/save and the file-hash memo's writes (never `recordRun` / `stats` / `prune`
628
644
  read-through (`remoteRead`), upload (`remoteWrite`), and prefetch
629
645
  (`remoteRead`). The orchestrator derives two booleans per task:
630
646
 
631
- - `willRead = task has a cache block AND it is not remote-only AND (localRead || remoteRead)`
647
+ - `willRead = task has a cache block AND it is not remote-only AND it folds no keyless task AND (localRead || remoteRead)`
632
648
  - `willWrite = task has a cache block AND it is not remote-only AND (localWrite || remoteWrite)`
633
649
 
634
650
  A task reads the cache only when `willRead`, saves only when
635
- `willWrite`, and cleans its declared outputs before exec only when
636
- `willWrite`. Remote-only is an `exec.remote: 'only'` task placed on a
651
+ `willWrite` and it folds no keyless task, and cleans its declared outputs
652
+ before exec only when `willWrite`. A keyless task runs a command and
653
+ declares no `inputs`: its key is unknown, so a task that folds it
654
+ (through `dependsOn`, directly or through a group) always misses. Remote-only is an `exec.remote: 'only'` task placed on a
637
655
  remote executor: it never touches this machine's disk (no probe, no
638
656
  restore, no output clean, no local save), and its result lives in the
639
657
  remote executor's own store.
@@ -784,7 +802,7 @@ from the snapshot's index OID for an output (or for an input the task
784
802
  rewrote) restored the bytes from before the command.
785
803
 
786
804
  A declared set that resolves to **nothing** is said on the run's
787
- status line. `cache.inputs matched no files (lib/**)` — the key would
805
+ status line. `inputs matched no files (lib/**)` — the key would
788
806
  not change when the source does — is said on every miss of a cacheable
789
807
  task, right after `describeTaskInputs` and before the command, whether
790
808
  or not it saves. `cache.outputs matched no files (build/**)` — an empty
@@ -948,13 +966,13 @@ A task's cache becomes invalid when any of these change:
948
966
  | 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
967
  | 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
968
  | 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 `cache.inputs.files`), or when a task lists it; **NOT a trigger** otherwise |
969
+ | 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
970
  | Edit the task's `vx.config.ts` | step 3 (task config hash) |
953
971
  | Edit a config file that the task config imports | step 3 (configHash sees the resolved object after Bun evaluates imports) |
954
972
  | Change CLI `forwardArgs` (after `--`) | step 4 |
955
- | Change a `cache.inputs.env` host value | step 5 |
956
- | Change the combined stdout+stderr of a `cache.inputs.runtime` command (resolved at hash time) | step 6 |
957
- | Change the combined stdout+stderr of a `cache.inputs.workspaceRuntime` command (resolved at hash time) | step 7 |
973
+ | Change a `inputs.env` host value | step 5 |
974
+ | Change the combined stdout+stderr of a `inputs.runtime` command (resolved at hash time) | step 6 |
975
+ | Change the combined stdout+stderr of a `inputs.workspaceRuntime` command (resolved at hash time) | step 7 |
958
976
  | Upstream task's cache key changes (because its inputs changed) | step 8 |
959
977
  | Bump `CACHE_VERSION` | step 1 — orphans every entry |
960
978
  | Change `exec.env.passThrough` _values_ alone | **NOT a trigger** by design — passThrough values are host-specific |
@@ -968,7 +986,7 @@ re-runs automatically.
968
986
 
969
987
  ## Cross-project boundaries
970
988
 
971
- A project's `cache.inputs.files` globs **never** reach into another
989
+ A project's `inputs.files` globs **never** reach into another
972
990
  project's directory, even if a `**/*` pattern would otherwise match.
973
991
 
974
992
  `workspace/nested-dirs.ts` computes the set of nested project
@@ -985,21 +1003,22 @@ globs is `dependsOn` + upstream-hash propagation (step 8).
985
1003
  For a task with `exec.sandbox` and `cache`, the sandbox enforces that
986
1004
  "only". A workspace package reached through a `node_modules` link is
987
1005
  granted only when the task's key folds a task of that package, walking
988
- the same selection `cache.inputs.tasks` applies at hash time
989
- (`orchestrator/keyed-projects.ts`, over `upstream.ts`'s
1006
+ the same selection `inputs.tasks` applies at hash time
1007
+ (`orchestrator/keyed-projects.ts`, over `graph/folds.ts`'s
990
1008
  `selectFoldedDeps`); every other link target inside the root is
991
1009
  withheld, so an import of a sibling the key never sees fails and is
992
1010
  reported instead of caching a result an edit there would not re-run.
993
- Any exec task counts, cached or not: a task with no `cache` declares no
994
- `inputs.files`, so its key folds every file of its project. A persistent
995
- task counts the same way: its key folds its whole project, so a cached
996
- e2e behind a dev server re-runs when the server's sources change.
1011
+ Any exec task counts, cached or not: its key folds its project's files
1012
+ through its `inputs`, and one with no `inputs` is keyless, so a key that
1013
+ folds it is never stored or read. A persistent task counts the same way,
1014
+ so a cached e2e behind a dev server keyed on its sources re-runs when
1015
+ they change.
997
1016
  The coverage is per package, not per file: an edge to `ui#source`
998
1017
  (inputs `src/**`) also admits a read of `ui/README.md`, whose edit moves
999
1018
  no key — the same limit a grant wider than a task's own inputs already
1000
1019
  has. A task with no `cache` keeps every link, having no key to be stale.
1001
1020
 
1002
- **Exception:** `cache.inputs.workspaceFiles` /
1021
+ **Exception:** `inputs.workspaceFiles` /
1003
1022
  `cache.outputs.workspaceFiles` are workspace-root-anchored and apply
1004
1023
  NO boundary rule — a deliberate escape hatch (owner call: "they don't
1005
1024
  care about boundaries; it is bad practice but is there"). Prefer
@@ -1061,8 +1080,8 @@ the gate exists to avoid paying needlessly.
1061
1080
 
1062
1081
  ## Runtime inputs and the lock (the env parallel)
1063
1082
 
1064
- `cache.inputs.runtime` / `cache.inputs.workspaceRuntime` are modeled
1065
- exactly on `cache.inputs.env`, and they share its asymmetry with the
1083
+ `inputs.runtime` / `inputs.workspaceRuntime` are modeled
1084
+ exactly on `inputs.env`, and they share its asymmetry with the
1066
1085
  lockfile:
1067
1086
 
1068
1087
  - **The command _strings_ live in the resolved config**, so `vx lock`
@@ -1214,7 +1233,7 @@ by this user, or point cacheDir / --cache-dir at one that is.` (A-40).
1214
1233
  An input the key must hash that this user cannot read fails its task
1215
1234
  naming the read: `<path> is not readable by this user (EACCES), and vx
1216
1235
  reads it to derive a cache key. Make it readable, or, for a task input,
1217
- take it out of cache.inputs.files.` (A-50).
1236
+ take it out of inputs.files.` (A-50).
1218
1237
 
1219
1238
  By default the entries and their artifacts live in a **shared store**
1220
1239
  in `~/.vx/<id>/cache`, and each workspace keeps only its own index in
@@ -1850,10 +1869,19 @@ moat.
1850
1869
  passthrough still bumps the key for affected tasks.
1851
1870
  - **Files outside the project directory that aren't declared.**
1852
1871
  Workspace-root configs (`tsconfig.base.json`, etc.) are not
1853
- auto-included — declare them via `cache.inputs.workspaceFiles`
1872
+ auto-included — declare them via `inputs.workspaceFiles`
1854
1873
  (root-anchored globs).
1874
+ - **Gitignored files no entry names.** Vite and Next load `.env.local`
1875
+ on their own; a broad glob skips it, so editing it replays the old
1876
+ build. Name it: `files: ['src/**', '.env.local']`. Its bytes are
1877
+ hashed, never stored, and a missing file is simply not an input.
1878
+ Machines with different files compute different keys, so they miss
1879
+ each other's shared entries. `--affected` ignores it (only what git
1880
+ sees selects a task), and `vx watch` re-runs on its edit. A probe
1881
+ (`runtime: ['cat .env.local 2>/dev/null | cksum']`) still works where
1882
+ a command, not a path, decides what to read.
1855
1883
  - **Node / Bun / OS / build-tool versions — unless you declare them.**
1856
- The canonical mechanism is `cache.inputs.runtime` /
1884
+ The canonical mechanism is `inputs.runtime` /
1857
1885
  `workspaceRuntime` (e.g. `workspaceRuntime: ['node -v']`): the
1858
1886
  command output is resolved live at hash time, so it stays correct
1859
1887
  under the lock. Avoid baking versions via
@@ -2187,11 +2215,11 @@ breaking footer).
2187
2215
  committed, so git enumerates it, but it's vx's own frozen-config
2188
2216
  metadata — never a task input. Without this, a `vx lock` re-write
2189
2217
  busts every key on a project that globs the root lockfile (a broad
2190
- `**/*` on the root project). Tasks whose `cache.inputs.files` never
2218
+ `**/*` on the root project). Tasks whose `inputs.files` never
2191
2219
  matched it derive byte-identical keys. No SCHEMA bump — only the
2192
2220
  hashed file set changed, not the key layout or on-disk format.
2193
2221
 
2194
- - **v22 → v23**: fold `cache.inputs.runtime` / `workspaceRuntime`
2222
+ - **v22 → v23**: fold `inputs.runtime` / `workspaceRuntime`
2195
2223
  command output into the key (two namespaced sections after
2196
2224
  env-values). The command _strings_ stay in the config hash (step 3);
2197
2225
  their combined trimmed stdout+stderr is resolved live at hash time
@@ -2236,7 +2264,7 @@ breaking footer).
2236
2264
  - **v11 → v12** (PR #42): project's `package.json` bytes folded into
2237
2265
  every task's cache key implicitly. Matches Turbo / Nx "implicit
2238
2266
  dependencies" behavior — a `package.json` dep change invalidates
2239
- the project's tasks even when `cache.inputs.files` is narrow and
2267
+ the project's tasks even when `inputs.files` is narrow and
2240
2268
  doesn't cover the file.
2241
2269
  - **v12 → v13** (PR #65): per-entry on-disk layout unified. Outputs
2242
2270
  moved from `<hash>/<rel>` (mixed with metadata) to