@vzn/vx 0.0.633 → 0.0.635

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 (53) hide show
  1. package/README.md +51 -52
  2. package/docs/caching.md +11 -2
  3. package/docs/cli.md +192 -56
  4. package/docs/execution.md +1 -1
  5. package/docs/features.md +6 -3
  6. package/docs/patterns.md +8 -5
  7. package/docs/schema.md +48 -2
  8. package/docs/security.md +6 -0
  9. package/launcher.cjs +0 -0
  10. package/launcher.sh +50 -0
  11. package/package.json +17 -8
  12. package/schemas/error.json +5 -1
  13. package/schemas/last.json +5 -1
  14. package/schemas/plan.json +4 -0
  15. package/schemas/show.json +2 -1
  16. package/schemas/why.json +4 -1
  17. package/skills/vx/SKILL.md +7 -1
  18. package/src/cache/cache.ts +60 -5
  19. package/src/cache/inputs.ts +66 -30
  20. package/src/cache/layered-cache.ts +3 -1
  21. package/src/cli/docs.ts +9 -3
  22. package/src/cli/help.ts +2 -1
  23. package/src/cli/init.ts +9 -2
  24. package/src/cli/last.ts +9 -1
  25. package/src/cli/plan-format.ts +3 -1
  26. package/src/cli/plugin-templates.ts +1 -1
  27. package/src/cli/run.ts +17 -8
  28. package/src/cli/select.ts +17 -11
  29. package/src/cli/show.ts +2 -2
  30. package/src/config.ts +23 -2
  31. package/src/graph/priorities.ts +72 -6
  32. package/src/graph/scheduler.ts +31 -7
  33. package/src/graph/task-graph.ts +75 -20
  34. package/src/index.ts +6 -1
  35. package/src/orchestrator/execute-task.ts +30 -1
  36. package/src/orchestrator/framed-output.ts +27 -1
  37. package/src/orchestrator/history.ts +75 -1
  38. package/src/orchestrator/index.ts +1 -0
  39. package/src/orchestrator/path-links.ts +5 -3
  40. package/src/orchestrator/plugin.ts +3 -1
  41. package/src/orchestrator/prepare.ts +15 -12
  42. package/src/orchestrator/remote-prefetch.ts +8 -2
  43. package/src/orchestrator/run-failures.ts +20 -3
  44. package/src/orchestrator/run.ts +24 -2
  45. package/src/orchestrator/sandbox-fix.ts +123 -0
  46. package/src/orchestrator/task-hash.ts +7 -1
  47. package/src/util/errors.ts +9 -2
  48. package/src/util/paths.ts +32 -0
  49. package/src/workspace/affected.ts +16 -4
  50. package/src/workspace/config-schema.ts +30 -2
  51. package/src/workspace/lockfile.ts +29 -2
  52. package/src/workspace/migration.ts +51 -4
  53. package/src/workspace/project-loader.ts +3 -1
package/README.md CHANGED
@@ -5,8 +5,9 @@
5
5
  <h1 align="center">vx</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>The fastest task runner for JS monorepos.</strong><br>
9
- Measured against Turborepo and Nx, each in its own native config.
8
+ <strong>Task orchestration you own.</strong><br>
9
+ Correct, sandboxed, extensible. No cloud, no account, no paywall.<br>
10
+ <a href="https://vznjs.github.io/vx/"><strong>vznjs.github.io/vx</strong></a>
10
11
  </p>
11
12
 
12
13
  <p align="center">
@@ -16,29 +17,9 @@
16
17
  <a href="https://vznjs.github.io/vx/"><img src="https://img.shields.io/badge/docs-vznjs.github.io%2Fvx-c6f84e" alt="Documentation"></a>
17
18
  </p>
18
19
 
19
- <!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
20
-
21
- | 1,090 packages, 3,270 tasks | vx | Turborepo | Nx | Vite Task |
22
- | ----------------------------------------------- | -------------- | -------------------------- | ------------------------- | -------------------------- |
23
- | Cold build: total time | **3 min 40 s** | 4 min 59 s (vx 36% faster) | 3 min 49 s (vx 4% faster) | 4 min 49 s (vx 31% faster) |
24
- | Cold build: CPU burned | **17 s** | 21 s (vx 22% faster) | 52 s (vx 3× faster) | 12 s (vx 39% slower) |
25
- | Fully cached run (restored) | **650 ms** | 997 ms (vx 53% faster) | 6.25 s (vx 9.6× faster) | 2.64 s (vx 4.1× faster) |
26
- | Fully cached run (up-to-date) | **393 ms** | 463 ms (vx 18% faster) | 6.45 s (vx 16× faster) | 2.49 s (vx 6.3× faster) |
27
- | Secondary: time the runner adds to a cold build | **2 s** | 1 min 21 s | 11 s | 1 min 11 s |
28
-
29
- vx N% or N× faster: that tool takes N% longer or N times as long as vx.
30
-
31
- Benchmark workload: a synthetic monorepo of 1,090 packages and 3,270 tasks in 100 dependency layers, every build and test taking 1 s; real repos with uneven task times will differ.
32
- Run 2026-10-04 on linux x64, 4 cores: vx from source, Turborepo 2.11.7, Nx 23.2.1, Vite Task (vite-plus) 1.0.0.
33
-
34
- The secondary row is the wall time over the tasks' own ideal schedule (3 min 38 s).
35
- Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
36
-
37
- <!-- bench:end -->
38
-
39
20
  ## What vx is
40
21
 
41
- A task runner and build cache for JavaScript monorepos.
22
+ Task orchestration with a cache you can trust, on a core you can extend.
42
23
  Each task is one shell command with declared inputs and outputs; vx runs
43
24
  them in dependency order and replays results from a content-addressed cache.
44
25
 
@@ -95,22 +76,7 @@ More: [Quickstart](https://vznjs.github.io/vx/quickstart/) ·
95
76
  [CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md) ·
96
77
  [playground](https://vznjs.github.io/vx/playground/) (the planner, in your browser).
97
78
 
98
- ## Coming from Turbo or Nx
99
-
100
- Beside `turbo.json` or `nx.json`, `npx vx init` runs `@vzn/vx-migrate`:
101
- it writes a `vx.config.ts` per package from `turbo.json` or the Nx graph,
102
- and you keep those and delete the old config. `vx init --keep` writes only
103
- a `vx.workspace.ts` that maps the old config instead: a temporary start,
104
- not a way to run the repo. The numbers above are native vx config.
105
- [Migration guide](https://vznjs.github.io/vx/guides/migrate/).
106
-
107
- ## Why it is faster
108
-
109
- - **Nothing boots per task:** no daemon, no Node process; vx spawns your command.
110
- - **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
111
- - **One pass:** one git walk per run; each stat and lookup happens once.
112
-
113
- ## More than speed
79
+ ## A foundation you own
114
80
 
115
81
  - **A cache you can trust:** TypeScript configs are evaluated before
116
82
  hashing, so imports and computed values are in the key. Outputs are
@@ -122,6 +88,32 @@ not a way to run the repo. The numbers above are native vx config.
122
88
  CLI verbs. Core ships none and needs none.
123
89
  - **Free:** MIT. No paywall, no cloud, no account.
124
90
 
91
+ ## Plugins
92
+
93
+ What vx can do, not what it is limited to. Declared in `vx.workspace.ts`; without any, vx runs and caches locally.
94
+
95
+ | Package | What it adds |
96
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
97
+ | [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
98
+ | [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
99
+ | [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
100
+ | [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
101
+ | [`@vzn/vx-ci`](packages/vx-ci) | GitHub Actions job summary and Checks API |
102
+ | [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: an MCP server for AI coding agents that also runs tasks |
103
+ | [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
104
+
105
+ Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
106
+ [architecture](packages/vx/docs/architecture.md).
107
+
108
+ ## Coming from Turbo or Nx
109
+
110
+ Beside `turbo.json` or `nx.json`, `npx vx init` runs `@vzn/vx-migrate`:
111
+ it writes a `vx.config.ts` per package from `turbo.json` or the Nx graph,
112
+ and you keep those and delete the old config. `vx init --keep` writes only
113
+ a `vx.workspace.ts` that maps the old config instead: a temporary start,
114
+ not a way to run the repo. The benchmark below uses native vx config.
115
+ [Migration guide](https://vznjs.github.io/vx/guides/migrate/).
116
+
125
117
  ## How it compares
126
118
 
127
119
  | | vx | Turborepo | Nx |
@@ -138,22 +130,29 @@ Every gap, and when another tool is the better pick:
138
130
  [comparison](packages/vx/docs/comparison.md) ·
139
131
  [vx, Turborepo, Nx, Bazel](https://vznjs.github.io/vx/compare/).
140
132
 
141
- ## Plugins
133
+ ## Fast, without trading correctness
142
134
 
143
- Declared in `vx.workspace.ts`; without any, vx runs and caches locally.
135
+ - **Nothing boots per task:** no daemon, no Node process; vx spawns your command.
136
+ - **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
137
+ - **One pass:** one git walk per run; each stat and lookup happens once.
144
138
 
145
- | Package | What it adds |
146
- | ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
147
- | [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
148
- | [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
149
- | [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
150
- | [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
151
- | [`@vzn/vx-ci`](packages/vx-ci) | GitHub Actions job summary and Checks API |
152
- | [`@vzn/vx-mcp`](packages/vx-mcp) | `vx mcp`: an MCP server for AI coding agents that also runs tasks |
153
- | [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
139
+ <!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
154
140
 
155
- Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
156
- [architecture](packages/vx/docs/architecture.md).
141
+ | Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
142
+ | ---------------------------------------------------------- | ----------- | ------------------------- | -------------------------- | ------------------------ |
143
+ | Nothing changed | **1.07 s** | 898 ms (vx 19% slower) | 25.45 s (vx 24× faster) | 11.75 s (vx 11× faster) |
144
+ | Nothing changed, outputs restored | **1.27 s** | 844 ms (vx 51% slower) | 23.82 s (vx 19× faster) | 11.83 s (vx 9.3× faster) |
145
+ | Cold build | **7.07 s** | 9.15 s (vx 29% faster) | 3 min 48 s (vx 32× faster) | 8.16 s (vx 15% faster) |
146
+ | Cold build: CPU the runner burns | **32.28 s** | 1 min 3 s (vx 94% faster) | 6 min 6 s (vx 11× faster) | 24.82 s (vx 30% slower) |
147
+
148
+ Time each tool adds over the ideal run; vx N% or N× faster means that tool adds N% more or N times as much as vx.
149
+
150
+ Benchmark workload: a synthetic monorepo of 1,601 projects and 9,603 tasks in 30 dependency levels, five core libraries a quarter of the projects use; build 300 ms, test and typecheck 150 ms, lint 75 ms, publish 30 ms; real repos with uneven task times will differ.
151
+ Run 2026-10-09 on linux x64, 4 cores: vx at commit 3d48a981d, Turborepo 2.11.7, Nx 23.3.0, Vite Task (vite-plus) 1.1.0.
152
+
153
+ Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
154
+
155
+ <!-- bench:end -->
157
156
 
158
157
  ## Status
159
158
 
package/docs/caching.md CHANGED
@@ -465,7 +465,10 @@ fold, and runtime probes after a writer still leave a key preliminary.
465
465
  The
466
466
  short-circuit never runs under a `LayeredCache` (remote prefetch owns
467
467
  those runs — an up-front `get` there would put remote GETs on the
468
- critical path), never fires with local reads off, and never throws —
468
+ critical path), never fires with local reads off or on a local cache
469
+ that holds no artifact (`Cache.holdsNothing`: every probe would miss,
470
+ and a cold 1,090-package run started its first task ~90 ms sooner
471
+ without it), and never throws —
469
472
  any error degrades to the normal lazy schedule. Measured: −6.6% on a
470
473
  mixed slow-upstream/warm-downstream workload; parity on all-hit warm
471
474
  runs.
@@ -701,7 +704,13 @@ is not saved either: its write cannot be told from an edit reverted
701
704
  mid-run, so it pays a re-run each time rather than risk a stale entry;
702
705
  declare what it writes as an output, which the status line says when it
703
706
  names a file other than `package.json` (TanStack Router's committed
704
- `routeTree.gen.ts`, rewritten by every build). A file ADDED under an
707
+ `routeTree.gen.ts`, rewritten by every build). That holds on CI too: a
708
+ task of the same project running alongside can change an input and put
709
+ it back. A task that rewrites its own input to identical bytes (tsdown's
710
+ `package.json`) sets `cache.rewrittenInputs: 'compare'`, and a write
711
+ during its command is then judged by its bytes, as an earlier write is;
712
+ different bytes still withhold the entry. `VX_REWRITTEN_INPUTS=compare`
713
+ makes that the default (owner, 2026-10-12). A file ADDED under an
705
714
  input glob since the listing the key filtered (`addedInput`) withholds
706
715
  the save the same way, named on the same line: the listing's
707
716
  directories a glob reaches are `lstat`ed, one whose ctime moved since
package/docs/cli.md CHANGED
@@ -137,9 +137,12 @@ Exit codes:
137
137
  A task runs in its own session, so a terminal's Ctrl-C reaches vx alone,
138
138
  and each task hears it once: from vx, as SIGINT.
139
139
 
140
- Installed from npm, `vx` is a Node launcher. On Node 22.15 or later it
141
- replaces itself with the binary (`process.execve`), so every signal
142
- reaches vx directly. On older Node it runs the binary and waits for it:
140
+ Installed from npm, `vx` is a sh launcher that execs the platform
141
+ binary, so every signal reaches vx directly and no Node starts first
142
+ (a `vx --version` through npm: 14 ms, was 56 ms with the Node launcher, 2026-10-12). Where it
143
+ finds no binary beside it (musl, Yarn PnP, a platform with none) it
144
+ hands off to a Node launcher. On Node 22.15 or later that one
145
+ replaces itself with the binary (`process.execve`) too. On older Node it runs the binary and waits for it:
143
146
  a signal sent to the launcher alone (`kill`, a process manager) is
144
147
  passed to the binary, and a terminal's Ctrl-C already reaches both, so
145
148
  the launcher does not send it twice.
@@ -957,8 +960,10 @@ readable. Its `criticalPath` names the would-run tasks with history on
957
960
  the chain `wallMs` measures, dependencies first: the tasks to speed up
958
961
  to shorten the run.
959
962
 
960
- **Why a task is affected.** Under `--affected`, each requested task the
961
- diff kept gets a line, and its JSON object an `affected` reason:
963
+ ### Why a task is affected (`affected.kind`)
964
+
965
+ Under `--affected`, each requested task the diff kept gets a line, and
966
+ its JSON object an `affected` reason:
962
967
 
963
968
  ```
964
969
  $ vx run build lint --affected=main --dry
@@ -976,6 +981,12 @@ single `file` for a lockfile claim, a manifest edge or a config import),
976
981
  selected its project). `via` is the `dependsOn` chain, nearest first, to
977
982
  the task the change reached. A dependency the kept tasks pull in carries
978
983
  none. The reasons are gathered only for a plan; a run pays nothing.
984
+ `--dry=json` also states `affectedBase`, the ref the diff was taken
985
+ against: a bare `--affected` guesses it (`origin/HEAD`, a trunk branch,
986
+ else `HEAD~1`), so a reason that names no file may come from a commit
987
+ the base reaches, a root `package.json` edit among them.
988
+
989
+ ### Task graph (`--graph`)
979
990
 
980
991
  `--graph` prints Graphviz DOT (stdout by default; `--graph=path`
981
992
  writes a file):
@@ -1279,7 +1290,19 @@ package, are reported.
1279
1290
  Policy: **fail on violation.** Any task that touches a path it didn't
1280
1291
  declare either fails naturally (Linux: `ENOENT` from bwrap's
1281
1292
  mount-namespace hide) or is flagged via the macOS violation store
1282
- and forced to exit non-zero. No cache is written for a failed task.
1293
+ and forced to exit non-zero. No cache is written for a failed task. The
1294
+ task's `SANDBOX VIOLATIONS` section ends with a dimmed `fix:` line,
1295
+ the grants that would have allowed each denial, project-relative:
1296
+ `fix: exec.sandbox.allow: { read: ['tsconfig.json'], write: ['dist/'] }`.
1297
+ A write grants the top directory it landed in, so the next file there
1298
+ passes too, unless `cache.inputs.files` reads that directory: then it
1299
+ grants the file alone, so the task cannot write its own inputs; a read the output depends on is a key input as well, and
1300
+ the line says so, since a grant never adds one. A credential path
1301
+ (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube`, `~/.config/gh`,
1302
+ `~/.config/gcloud`, `~/.netrc`, `~/.npmrc`, `~/.pypirc`,
1303
+ `~/.git-credentials`, `~/.docker/config.json`, a `.env` file) is never put in the
1304
+ line: an agent applies it as given, so it is named apart as one to
1305
+ grant only on purpose.
1283
1306
  "Didn't declare" means the grants, which on Linux are wider than they
1284
1307
  read: a file-shaped write grant is bound as its whole directory, so
1285
1308
  every file beside it is readable without a violation — `schema.md`
@@ -1957,13 +1980,17 @@ install its dependencies. `--mjs` writes the same objects as `vx.config.mjs` and
1957
1980
  own `tsconfig` includes every `.ts` under it compiles a `vx.config.ts`
1958
1981
  into its dist (TanStack/query's `tsc --build`, 2026-09-11), and an
1959
1982
  `.mjs` is outside that include. `@vzn/vx-migrate` takes the same flag.
1983
+ A file it writes that `.gitignore` drops (vueuse ignores
1984
+ `packages/**/*.mjs`) gets a `note:` naming it and the `!vx.config.mjs`
1985
+ line that keeps it, since a commit would leave it behind unnoticed.
1960
1986
 
1961
1987
  Each script becomes a task with its command verbatim. `build` gets
1962
1988
  `dependsOn: ['^build']` (a `build` that only delegates, `pnpm run
1963
1989
  compile`, is a group, and the edge goes on the script that does the
1964
1990
  work) and **no cache block** — under a
1965
1991
  `TODO(vx-migrate)` showing the block to add with the package's real
1966
- inputs and outputs. A task without a cache block always runs; a block
1992
+ inputs and outputs, and the same block commented as the task's first
1993
+ line: set the real globs and delete the `// `. A task without a cache block always runs; a block
1967
1994
  with EMPTY outputs is not "uncached" but a no-output task that hits on
1968
1995
  unchanged inputs and skips the build with nothing to restore (a deleted
1969
1996
  `dist` stays deleted under a green `up-to-date` run — what `init`
@@ -2109,8 +2136,8 @@ order may matter, so it is not made a group.
2109
2136
 
2110
2137
  The report lists each TODO once per reason: tasks that share one are
2111
2138
  named together (the first five, then a count; the files carry each),
2112
- and when no task caches it says a cache block from a TODO makes the
2113
- second run a hit. Its `next:` line is a command the user can type: the
2139
+ and when no task caches it says uncommenting a TODO task's cache line
2140
+ makes the second run a hit. Its `next:` line is a command the user can type: the
2114
2141
  runner that started vx (`npx`, `pnpm`, `yarn`, `bunx`, read from
2115
2142
  `npm_config_user_agent`) with the installed `vx` bin, else the
2116
2143
  `@vzn/vx` package (`npx` under Yarn 1, which has no `dlx`); with no
@@ -2287,26 +2314,125 @@ refuses `--dry` (use `--dry=json`), a bare `--graph` and
2287
2314
 
2288
2315
  A verb asked for JSON (`--format json`, `--format=json`, `--dry=json`,
2289
2316
  before any `--`) answers a refusal on stdout too, as one line that
2290
- `schemas/error.json` describes: `{"ok":false,"error":{"code","message"}}`.
2291
- The exit code (1) and the prose on stderr do not change. Branch on the
2292
- code; the message may change.
2293
-
2294
- | Code | When |
2295
- | ---------------------- | --------------------------------------------------------------------- |
2296
- | `VX_E_USAGE` | A flag, argument or selection the verb does not take. |
2297
- | `VX_E_UNKNOWN_COMMAND` | No verb of that name here, core's or a declared plugin's. |
2298
- | `VX_E_UNKNOWN_TASK` | No project in scope declares a requested task. |
2299
- | `VX_E_UNKNOWN_PROJECT` | `vx show` names a project the workspace does not have. |
2300
- | `VX_E_NO_HISTORY` | `vx why` / `vx last` find no recorded run for what they were asked. |
2301
- | `VX_E_AMBIGUOUS` | `vx why <task>` matches the task in several projects; name one. |
2302
- | `VX_E_CONFIG` | A `vx.config.*` or `vx.workspace.*` that does not load or is refused. |
2303
- | `VX_E_CYCLE` | The task graph has a cycle. |
2304
- | `VX_E_NO_WORKSPACE` | No workspace root above the working directory. |
2305
- | `VX_E_CACHE_VERSION` | A reading verb met a cache index from another vx version. |
2306
- | `VX_E_REFUSED` | Any other refusal (a plugin's, a check's) with no code of its own. |
2307
- | `VX_E_FS` | The file system refused a write: permissions, a full disk. |
2308
- | `VX_E_FDS` | Out of file descriptors. |
2309
- | `VX_E_INTERNAL` | A defect in vx; stderr carries the stack. |
2317
+ `schemas/error.json` describes: `{"ok":false,"error":{"code","message","docs"}}`.
2318
+ `docs` links the code's section below, which says what to do;
2319
+ `vx docs <code>` prints that section offline. The exit code (1) and the
2320
+ prose on stderr do not change. Branch on the code; the message may change.
2321
+
2322
+ #### `VX_E_USAGE`
2323
+
2324
+ A flag, argument or selection the verb does not take. The message names
2325
+ it, and a close spelling the verb does take when there is one;
2326
+ `vx <verb> --help` lists every flag.
2327
+
2328
+ #### `VX_E_UNKNOWN_COMMAND`
2329
+
2330
+ No verb of that name here, core's or a declared plugin's. `vx --help`
2331
+ lists core's verbs. A plugin's verb exists once its plugin is in
2332
+ `vx.workspace.ts` `plugins`; a task runs as `vx run <task>`.
2333
+
2334
+ #### `VX_E_UNKNOWN_TASK`
2335
+
2336
+ No project in scope declares a requested task. The message lists the
2337
+ tasks it found; `vx show --format json` lists each project's. Check the
2338
+ spelling, and the scope: `--filter`, `--all`, or the project the working
2339
+ directory is in.
2340
+
2341
+ #### `VX_E_UNKNOWN_PROJECT`
2342
+
2343
+ `vx show` names a project the workspace does not have. A project is named
2344
+ by its `package.json` `name`; `vx show --format json` lists them.
2345
+
2346
+ #### `VX_E_NO_HISTORY`
2347
+
2348
+ `vx why` / `vx last` find no recorded run for what they were asked. Run
2349
+ the task once (`vx run <task>`); history is this checkout's own.
2350
+
2351
+ #### `VX_E_AMBIGUOUS`
2352
+
2353
+ `vx why <task>` matches the task in several projects. Name one:
2354
+ `vx why <project>#<task>`.
2355
+
2356
+ #### `VX_E_CONFIG`
2357
+
2358
+ A `vx.config.*` or `vx.workspace.*` that does not load or is refused. The
2359
+ message names the file and the field; fix that field
2360
+ (`vx docs <field>` prints its reference). `vx info --format json` lists
2361
+ every config error at once.
2362
+
2363
+ #### `VX_E_CYCLE`
2364
+
2365
+ The task graph has a cycle. The message names the loop; drop one of its
2366
+ `dependsOn` edges.
2367
+
2368
+ #### `VX_E_NO_WORKSPACE`
2369
+
2370
+ No workspace root above the working directory. Run vx inside the repo,
2371
+ or create a `package.json` and run `vx init`.
2372
+
2373
+ #### `VX_E_CACHE_VERSION`
2374
+
2375
+ A reading verb met a cache index from another vx version. It reads
2376
+ nothing from it and leaves it be; the next `vx run` resets it. Or run the
2377
+ vx version that wrote it.
2378
+
2379
+ #### `VX_E_REFUSED`
2380
+
2381
+ Any other refusal (a plugin's, a check's) with no code of its own. The
2382
+ message is that plugin's or check's: do what it says.
2383
+ `vx info --format json` lists the plugins.
2384
+
2385
+ #### `VX_E_FS`
2386
+
2387
+ The file system refused a write: permissions, a full disk. The message
2388
+ names the path. Free space (`vx cache prune --max-size <size>` trims the
2389
+ cache) or grant the write, then re-run.
2390
+
2391
+ #### `VX_E_FDS`
2392
+
2393
+ Out of file descriptors. Raise the limit (`ulimit -n 4096`), or run fewer
2394
+ tasks at once (`--concurrency`), then re-run.
2395
+
2396
+ #### `VX_E_AFFECTED_BASE`
2397
+
2398
+ `--affected` (or `--affected=<ref>`) has no base to diff against: the
2399
+ ref does not resolve, is a range (`A..B`) or an exclusion (`^A`), names
2400
+ a path inside a commit, or the repository has no history to compare
2401
+ with. Pass a branch or commit you have locally, alone:
2402
+ `--affected=origin/main`. On CI, fetch the history the base needs
2403
+ (`fetch-depth: 0` on actions/checkout, or `git fetch --unshallow`).
2404
+
2405
+ #### `VX_E_GIT`
2406
+
2407
+ A git command vx runs to read the diff failed; the message carries
2408
+ git's exit and its own words. Run that command in the workspace to see
2409
+ why (a broken index, a missing object in a partial clone).
2410
+
2411
+ #### `VX_E_DEPENDS_ON`
2412
+
2413
+ A task's `dependsOn` names a task or project that is not declared, or uses
2414
+ a form vx does not take (a bare wildcard, a negation, a pattern in
2415
+ `pkg#task`). The message names the task and the entry: declare the
2416
+ target, fix its spelling, or drop the entry.
2417
+
2418
+ #### `VX_E_OUTPUT_OVERLAP`
2419
+
2420
+ Two tasks declare overlapping outputs with no `dependsOn` edge between
2421
+ them, so whichever runs second deletes the other's files. Give each task
2422
+ its own output path, or make one depend on the other.
2423
+
2424
+ #### `VX_E_INPUT_IS_OUTPUT`
2425
+
2426
+ A task's `cache.inputs` match outputs: its own (vx cleans outputs before
2427
+ a run, so it would delete its sources) or another task's (its key must
2428
+ not read them; `dependsOn` already cascades). Exclude the outputs from
2429
+ the inputs with a `!` glob, as the message shows, or declare no outputs
2430
+ for a task that rewrites files in place.
2431
+
2432
+ #### `VX_E_INTERNAL`
2433
+
2434
+ A defect in vx; stderr carries the stack. Report it with that stack at
2435
+ https://github.com/vznjs/vx/issues.
2310
2436
 
2311
2437
  A plugin can give its refusal a code: `new UserError(message, 'MY_CODE')`.
2312
2438
 
@@ -2740,8 +2866,14 @@ failed runs. `--format json`
2740
2866
  emits `{ invocation, tasks }` for scripting; a failed task's row adds
2741
2867
  `output` (its output as plain text, the first 8 KiB and last 56 KiB,
2742
2868
  secrets masked) and `locations` (the existing files it names,
2743
- `{ file, line?, col? }`, file absolute), so an agent reads why without
2744
- the terminal. They live in `<cacheDir>/failures/<runId>.json`, the
2869
+ `{ file, line?, col? }`, file absolute; a file also named with a line
2870
+ drops its bare mention, such as bun test's `app.test.js:` header), so an
2871
+ agent reads why without the terminal. `fixedIn` names the first later run where that task
2872
+ passed, so a replayed failure that is already fixed says so. A
2873
+ sandboxed task's row adds `sandboxFix`, the `exec.sandbox.allow`
2874
+ entries (`read`, `write`, `network`, `systemInfo`) that would have
2875
+ granted its denials, the same grants the terminal prints as `fix:`;
2876
+ `withheld` lists the credential paths it denied and left out. They live in `<cacheDir>/failures/<runId>.json`, the
2745
2877
  newest 50 failed runs; past those, or when the write was refused, a
2746
2878
  failed row reads `output: ''` and `locations: []`. `--log <task>`
2747
2879
  prints one task's output instead of the summary: in the latest run that
@@ -2786,7 +2918,9 @@ parse error.
2786
2918
  `schema`, `caching`, `execution`, `patterns`, `security` and `features`,
2787
2919
  which ship inside the package and the compiled binary. Each page is cut
2788
2920
  at its headings; a section that holds every word of the query matches,
2789
- and a word in its heading ranks it above any count in its text. The best
2921
+ and a word in its heading ranks it above any count in its text; a
2922
+ one-word query that titles a section, an error code
2923
+ (`vx docs VX_E_CYCLE`), prints that section alone. The best
2790
2924
  `--limit` sections (default 3) print whole, each under its page,
2791
2925
  heading and URL on the site. `--format json` prints
2792
2926
  `{query, hits: [{page, heading, url, text}]}` (`schemas/docs.json`).
@@ -2889,30 +3023,32 @@ REAPI endpoints, the GitHub token) are in its README; what a TASK sees
2889
3023
  is `exec.env` in `docs/schema.md`, and the two markers vx sets on every
2890
3024
  task are the last row.
2891
3025
 
2892
- | Var | Value | Default | Effect |
2893
- | --------------------------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2894
- | `VX_TIMING` | any non-empty | off | Print the stage table to stderr after a run, a `--dry`, or each `vx watch` cycle (`docs/modules/timing.md`). |
2895
- | `VX_TASK_TIMEOUT` | positive integer (ms) | none | The default timeout for tasks without their own `exec.timeout`, one rung below `--timeout` / `RunOptions.timeout` and one above the workspace `timeout`. Empty, non-integer or non-positive is ignored; a value past the largest timer (~24.8 days) is clamped to it, never refused. |
2896
- | `VX_CACHE_DIR` | a path | none | The workspace's cache dir, relative to the workspace root, below `--cache-dir` and `vx.workspace`'s `cacheDir`. A named cache dir holds the whole cache and shares nothing; unset, entries live in the user's shared store (`docs/caching.md`). Empty is ignored; whitespace alone is refused, as `cacheDir` is. |
2897
- | `VX_CACHE_SCOPE` | a `cacheScope` value | none | The run's `cacheScope` (`trusted`, `read-only`, `pr-123`), above `vx.workspace`'s and `github()`'s. Empty is ignored; a value `cacheScope` would refuse is refused before any task. |
2898
- | `VX_KILL_GRACE_MS` | positive integer (ms) | 2000 | The SIGTERM → SIGKILL grace a child that ignores SIGTERM gets: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. Out of range falls back to the default. |
2899
- | `VX_READY_NOTICE_MS` | positive integer (ms) | 10000 | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. Out of range falls back to the default. |
2900
- | `VX_TEARDOWN_TIMEOUT_MS` | integer (ms) | 3000 | The bound on one plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. Out of range falls back to the default, never clamps: a bound of 24.8 days is no bound. |
2901
- | `VX_CONFIG_WORKER_TIMEOUT_MS` | integer (ms) | 30000 | How long one `vx.config.ts` evaluation may take, in process or in its worker, before the load fails naming the config (a real evaluation is ~10 ms). A synchronous loop on a first load holds the thread the deadline needs, so it hangs until killed. Out of range falls back to the default. |
2902
- | `VX_WATCH_POLL` | any non-empty | off | `vx watch` polls every 250 ms from the start instead of probing the OS watcher (§ `vx watch` › How changes are seen). |
2903
- | `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | set by vx | — | Set on every task's environment (the workspace root; `project#task`). Read back by a `vx run` a task starts: one in the same workspace is refused, since a nested run is invisible to the outer graph and a loop back to its own task forks without bound (`docs/schema.md` § `env`). |
2904
- | Var | Value | Default | Effect |
2905
- | --------------------------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2906
- | `VX_TIMING` | any non-empty | off | Print the stage table to stderr after a run, a `--dry`, or each `vx watch` cycle (`docs/modules/timing.md`). |
2907
- | `VX_TASK_TIMEOUT` | positive integer (ms) | none | The default timeout for tasks without their own `exec.timeout`, one rung below `--timeout` / `RunOptions.timeout` and one above the workspace `timeout`. Empty, non-integer or non-positive is ignored; a value past the largest timer (~24.8 days) is clamped to it, never refused. |
2908
- | `VX_CACHE_DIR` | a path | none | The workspace's cache dir, relative to the workspace root, below `--cache-dir` and `vx.workspace`'s `cacheDir`. A named cache dir holds the whole cache and shares nothing; unset, entries live in the user's shared store (`docs/caching.md`). Empty is ignored. |
2909
- | `VX_CACHE_SCOPE` | a `cacheScope` value | none | The run's `cacheScope` (`trusted`, `read-only`, `pr-123`), above `vx.workspace`'s and `github()`'s. Empty is ignored; a value `cacheScope` would refuse is refused before any task. |
2910
- | `VX_KILL_GRACE_MS` | positive integer (ms) | 2000 | The SIGTERM → SIGKILL grace a child that ignores SIGTERM gets: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. Out of range falls back to the default. |
2911
- | `VX_READY_NOTICE_MS` | positive integer (ms) | 10000 | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. Out of range falls back to the default. |
2912
- | `VX_TEARDOWN_TIMEOUT_MS` | integer (ms) | 3000 | The bound on one plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. Out of range falls back to the default, never clamps: a bound of 24.8 days is no bound. |
2913
- | `VX_CONFIG_WORKER_TIMEOUT_MS` | integer (ms) | 30000 | How long one `vx.config.ts` evaluation may take, in process or in its worker, before the load fails naming the config (a real evaluation is ~10 ms). A synchronous loop on a first load holds the thread the deadline needs, so it hangs until killed. Out of range (0, or past 2³¹−1) falls back to the default. |
2914
- | `VX_WATCH_POLL` | any non-empty | off | `vx watch` polls every 250 ms from the start instead of probing the OS watcher (§ `vx watch` › How changes are seen). |
2915
- | `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | set by vx | — | Set on every task's environment (the workspace root; `project#task`). Read back by a `vx run` a task starts: one in the same workspace is refused, since a nested run is invisible to the outer graph and a loop back to its own task forks without bound (`docs/schema.md` § `env`). |
3026
+ | Var | Value | Default | Effect |
3027
+ | --------------------------------- | --------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3028
+ | `VX_TIMING` | any non-empty | off | Print the stage table to stderr after a run, a `--dry`, or each `vx watch` cycle (`docs/modules/timing.md`). |
3029
+ | `VX_TASK_TIMEOUT` | positive integer (ms) | none | The default timeout for tasks without their own `exec.timeout`, one rung below `--timeout` / `RunOptions.timeout` and one above the workspace `timeout`. Empty, non-integer or non-positive is ignored; a value past the largest timer (~24.8 days) is clamped to it, never refused. |
3030
+ | `VX_CACHE_DIR` | a path | none | The workspace's cache dir, relative to the workspace root, below `--cache-dir` and `vx.workspace`'s `cacheDir`. A named cache dir holds the whole cache and shares nothing; unset, entries live in the user's shared store (`docs/caching.md`). Empty is ignored; whitespace alone is refused, as `cacheDir` is. |
3031
+ | `VX_CACHE_SCOPE` | a `cacheScope` value | none | The run's `cacheScope` (`trusted`, `read-only`, `pr-123`), above `vx.workspace`'s and `github()`'s. Empty is ignored; a value `cacheScope` would refuse is refused before any task. |
3032
+ | `VX_REWRITTEN_INPUTS` | `withhold`, `compare` | `withhold` | What a write to an input during the command means for a task that does not set `cache.rewrittenInputs`: `compare` saves the entry when the file holds the bytes the key folded, anything else withholds it (`docs/schema.md`). |
3033
+ | `VX_KILL_GRACE_MS` | positive integer (ms) | 2000 | The SIGTERM → SIGKILL grace a child that ignores SIGTERM gets: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. Out of range falls back to the default. |
3034
+ | `VX_READY_NOTICE_MS` | positive integer (ms) | 10000 | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. Out of range falls back to the default. |
3035
+ | `VX_TEARDOWN_TIMEOUT_MS` | integer (ms) | 3000 | The bound on one plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. Out of range falls back to the default, never clamps: a bound of 24.8 days is no bound. |
3036
+ | `VX_CONFIG_WORKER_TIMEOUT_MS` | integer (ms) | 30000 | How long one `vx.config.ts` evaluation may take, in process or in its worker, before the load fails naming the config (a real evaluation is ~10 ms). A synchronous loop on a first load holds the thread the deadline needs, so it hangs until killed. Out of range falls back to the default. |
3037
+ | `VX_WATCH_POLL` | any non-empty | off | `vx watch` polls every 250 ms from the start instead of probing the OS watcher (§ `vx watch` › How changes are seen). |
3038
+ | `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | set by vx | — | Set on every task's environment (the workspace root; `project#task`). Read back by a `vx run` a task starts: one in the same workspace is refused, since a nested run is invisible to the outer graph and a loop back to its own task forks without bound (`docs/schema.md` § `env`). |
3039
+ | Var | Value | Default | Effect |
3040
+ | --------------------------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3041
+ | `VX_TIMING` | any non-empty | off | Print the stage table to stderr after a run, a `--dry`, or each `vx watch` cycle (`docs/modules/timing.md`). |
3042
+ | `VX_TASK_TIMEOUT` | positive integer (ms) | none | The default timeout for tasks without their own `exec.timeout`, one rung below `--timeout` / `RunOptions.timeout` and one above the workspace `timeout`. Empty, non-integer or non-positive is ignored; a value past the largest timer (~24.8 days) is clamped to it, never refused. |
3043
+ | `VX_CACHE_DIR` | a path | none | The workspace's cache dir, relative to the workspace root, below `--cache-dir` and `vx.workspace`'s `cacheDir`. A named cache dir holds the whole cache and shares nothing; unset, entries live in the user's shared store (`docs/caching.md`). Empty is ignored. |
3044
+ | `VX_CACHE_SCOPE` | a `cacheScope` value | none | The run's `cacheScope` (`trusted`, `read-only`, `pr-123`), above `vx.workspace`'s and `github()`'s. Empty is ignored; a value `cacheScope` would refuse is refused before any task. |
3045
+ | `VX_REWRITTEN_INPUTS` | `withhold`, `compare` | `withhold` | What a write to an input during the command means for a task that does not set `cache.rewrittenInputs`: `compare` saves the entry when the file holds the bytes the key folded, anything else withholds it (`docs/schema.md`). |
3046
+ | `VX_KILL_GRACE_MS` | positive integer (ms) | 2000 | The SIGTERM → SIGKILL grace a child that ignores SIGTERM gets: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. Out of range falls back to the default. |
3047
+ | `VX_READY_NOTICE_MS` | positive integer (ms) | 10000 | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. Out of range falls back to the default. |
3048
+ | `VX_TEARDOWN_TIMEOUT_MS` | integer (ms) | 3000 | The bound on one plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. Out of range falls back to the default, never clamps: a bound of 24.8 days is no bound. |
3049
+ | `VX_CONFIG_WORKER_TIMEOUT_MS` | integer (ms) | 30000 | How long one `vx.config.ts` evaluation may take, in process or in its worker, before the load fails naming the config (a real evaluation is ~10 ms). A synchronous loop on a first load holds the thread the deadline needs, so it hangs until killed. Out of range (0, or past 2³¹−1) falls back to the default. |
3050
+ | `VX_WATCH_POLL` | any non-empty | off | `vx watch` polls every 250 ms from the start instead of probing the OS watcher (§ `vx watch` › How changes are seen). |
3051
+ | `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | set by vx | — | Set on every task's environment (the workspace root; `project#task`). Read back by a `vx run` a task starts: one in the same workspace is refused, since a nested run is invisible to the outer graph and a loop back to its own task forks without bound (`docs/schema.md` § `env`). |
2916
3052
 
2917
3053
  Colors are the two conventions in § Output format › Colors (`NO_COLOR`,
2918
3054
  `FORCE_COLOR`). A truthy `CI` picks the CI output flow and
package/docs/execution.md CHANGED
@@ -45,7 +45,7 @@ terminal and a task succeeding or failing. Read it alongside
45
45
  │ 3. loadWorkspacePlugins — loadWorkspaceConfig reads the optional
46
46
  │ vx.workspace.{ts,mts,js,mjs,cts,cjs} at the root (concurrency /
47
47
  │ cacheDir / timeout / cacheRetention / affectedBase / cacheScope /
48
- │ rules / plugins),
48
+ │ rules / schedule / plugins),
49
49
  │ then the plugin `config` stage runs on it.
50
50
  │ 4. listProjects — globs every workspace member's package.json,
51
51
  │ finds sibling vx.config.* files, detects duplicate package
package/docs/features.md CHANGED
@@ -28,6 +28,7 @@ config key or an environment variable is missing here.
28
28
  - **Turbo and Nx spellings** (`-t`, `-p`, `--exclude`, `--parallel`, `--base`, `--dry-run`, `--skip-nx-cache`) — accepted, or refused naming the vx spelling. [page](https://vznjs.github.io/vx/features/turbo-nx-flags/) · post: [Flags you already know](https://vznjs.github.io/vx/blog/flags-you-already-know/)
29
29
  - **Ctrl-C leaves nothing running** (`VX_KILL_GRACE_MS`, `VX_TEARDOWN_TIMEOUT_MS`) — the whole process tree stops, then teardown runs. [page](https://vznjs.github.io/vx/features/ctrl-c/) · post: [Ctrl-C leaves nothing running](https://vznjs.github.io/vx/blog/ctrl-c/)
30
30
  - **Longest chain first** — the scheduler starts the critical path first; `@vzn/vx-schedule-history` learns it from past runs. [page](https://vznjs.github.io/vx/features/critical-path/) · post: [Bitsets, popcount, and a scheduler tick](https://vznjs.github.io/vx/blog/bitsets-and-the-scheduler/)
31
+ - **Schedule strategy** (`schedule`) — pick the order with no timings: most work waiting (default), longest chain, most direct dependents, or ready order; plugins still lead. [page](https://vznjs.github.io/vx/features/critical-path/) · post: [Pick the order your graph wants](https://vznjs.github.io/vx/blog/schedule-strategy/)
31
32
  - **No daemon** — every run starts cold and still answers in milliseconds. [page](https://vznjs.github.io/vx/features/no-daemon/) · post: [No daemon, on purpose](https://vznjs.github.io/vx/blog/no-daemon/)
32
33
  - **Group tasks** (`dependsOn` with no `exec`) — a task that only runs its dependencies; `dependsOn: []` is a named no-op. [page](https://vznjs.github.io/vx/features/group-tasks/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
33
34
  - **Keyed groups** (`cache` with no `exec`) — a key a dependant folds: it re-runs on those inputs, and nothing spawns or counts for the group. [page](https://vznjs.github.io/vx/features/group-tasks/) · post: [A key with nothing to run](https://vznjs.github.io/vx/blog/keyed-groups/)
@@ -68,10 +69,10 @@ config key or an environment variable is missing here.
68
69
  - **vx info** (`vx info`) — workspace doctor: versions, projects, cache size against `cacheRetention.maxSize`. [page](https://vznjs.github.io/vx/features/vx-info/) · post: [One command to know your workspace](https://vznjs.github.io/vx/blog/know-your-workspace/)
69
70
  - **JSON everywhere** (`--format`) — `show`, `info`, `why`, `last` and `cache` print JSON for agents. [page](https://vznjs.github.io/vx/features/json-everywhere/) · post: [Built for the agent at the keyboard](https://vznjs.github.io/vx/blog/built-for-agents/)
70
71
  - **Run analytics in SQLite** — every task's time, CPU, peak memory and cache result, queryable with `sqlite3`. [page](https://vznjs.github.io/vx/features/run-analytics/) · post: [Built for the agent at the keyboard](https://vznjs.github.io/vx/blog/built-for-agents/)
71
- - **Stable error codes** (`VX_E_…`) — under `--format json` a refusal is a JSON line on stdout with a code an agent branches on. [page](https://vznjs.github.io/vx/features/error-codes/) · post: [A refusal an agent can read](https://vznjs.github.io/vx/blog/error-codes/)
72
+ - **Stable error codes** (`VX_E_…`) — under `--format json` a refusal is a JSON line on stdout with a code an agent branches on and a `docs` link to its fix (`vx docs <code>` offline). [page](https://vznjs.github.io/vx/features/error-codes/) · post: [A refusal an agent can read](https://vznjs.github.io/vx/blog/error-codes/)
72
73
  - **Shipped JSON Schemas** (`@vzn/vx/schemas/`) — every `--format json` shape has a schema, and stdout always parses. [page](https://vznjs.github.io/vx/features/json-schemas/) · post: [Built for the agent at the keyboard](https://vznjs.github.io/vx/blog/built-for-agents/)
73
74
  - **Docs for agents** (`llms.txt`, `llms-full.txt`) — the whole site as markdown for coding agents. [page](https://vznjs.github.io/vx/features/llms-txt/) · post: [Built for the agent at the keyboard](https://vznjs.github.io/vx/blog/built-for-agents/)
74
- - **vx docs** (`vx docs`, `--limit`) — search the CLI, config and cache reference offline; it ships inside vx. [page](https://vznjs.github.io/vx/cli/#vx-docs) · post: [The reference, offline](https://vznjs.github.io/vx/blog/vx-docs-offline/)
75
+ - **vx docs** (`vx docs`, `--limit`) — search the CLI, config and cache reference offline; it ships inside vx. [page](https://vznjs.github.io/vx/features/vx-docs/) · post: [The reference, offline](https://vznjs.github.io/vx/blog/vx-docs-offline/)
75
76
  - **Affected reasons** (`--affected --dry`) — each kept task says the changed file or the dependency chain that reached it, text or JSON. [page](https://vznjs.github.io/vx/features/affected-reasons/) · post: [See inside a run](https://vznjs.github.io/vx/blog/see-inside-a-run/)
76
77
  - **Agent skill** (`skills/vx/SKILL.md`) — an installable skill that teaches a coding agent to run, debug and query vx. [page](https://vznjs.github.io/vx/features/agent-skill/) · post: [Built for the agent at the keyboard](https://vznjs.github.io/vx/blog/built-for-agents/)
77
78
 
@@ -80,6 +81,7 @@ config key or an environment variable is missing here.
80
81
  - **Opt-in, explicit inputs** (`cache.inputs.files`) — a task caches only when it names its inputs. [page](https://vznjs.github.io/vx/features/explicit-inputs/) · post: [Explicit over magical](https://vznjs.github.io/vx/blog/explicit-over-magical/)
81
82
  - **Inputs** (`cache.inputs.env`, `cache.inputs.runtime`, `cache.inputs.tasks`, `cache.inputs.workspaceFiles`, `cache.inputs.workspaceRuntime`) — env vars, tool versions, upstream tasks and workspace files in the key. [page](https://vznjs.github.io/vx/features/key-inputs/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
82
83
  - **Outputs** (`cache.outputs.files`, `cache.outputs.workspaceFiles`) — what a hit restores. [page](https://vznjs.github.io/vx/features/cache-outputs/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
84
+ - **Rewritten inputs** (`cache.rewrittenInputs`, `VX_REWRITTEN_INPUTS`) — an input written while the command runs withholds the save, on CI too; `compare` accepts a task's identical rewrite of its own input. no post
83
85
  - **Outputs are exactly the snapshot** (`exclusiveOutputs`) — a hit leaves the tree as the run did; two tasks may not own one output without an edge. [page](https://vznjs.github.io/vx/features/strict-outputs/) · post: [The tree is exactly the snapshot](https://vznjs.github.io/vx/blog/strict-output-ownership/)
84
86
  - **Keys from git's index** — tracked clean files hash by their blob id, no read. [page](https://vznjs.github.io/vx/features/keys-from-git/) · post: [Your cache key is already in git's index](https://vznjs.github.io/vx/blog/keys-from-git/)
85
87
  - **Config keyed as evaluated** — the key sees the resolved config object. [page](https://vznjs.github.io/vx/features/typescript-config/) · post: [Configs are programs](https://vznjs.github.io/vx/blog/resolved-config-hashing/)
@@ -155,7 +157,7 @@ config key or an environment variable is missing here.
155
157
 
156
158
  ## Plugins
157
159
 
158
- - **A pipeline with seams** (`plugins`, `definePlugin`; stages `config` `discover` `project` `graph` `key` `fingerprint` `schedule` `admit` `executor` `cache` `telemetry` `commands`) — every stage is a plugin seam. [page](https://vznjs.github.io/vx/features/plugins/) · post: [A pipeline with seams](https://vznjs.github.io/vx/blog/pipeline-with-seams/)
160
+ - **A pipeline with seams** (`plugins`, `definePlugin`; stages `config` `discover` `project` `graph` `key` `fingerprint` `schedule` `admit` `executor` `cache` `telemetry` `commands`) — every stage is a plugin seam; an `undefined`, `null` or `false` entry is skipped. [page](https://vznjs.github.io/vx/features/plugins/) · post: [A pipeline with seams](https://vznjs.github.io/vx/blog/pipeline-with-seams/)
159
161
  - **Write a plugin** (`vx init --plugin`) — scaffold a runnable plugin for a seam. [page](https://vznjs.github.io/vx/features/plugins/) · post: [Extend vx in an afternoon](https://vznjs.github.io/vx/blog/extend-vx/)
160
162
  - **The local floor** — running and caching here are core's, not plugins. [page](https://vznjs.github.io/vx/features/local-floor/) · post: [The local floor](https://vznjs.github.io/vx/blog/the-local-floor/)
161
163
  - **Remote cache and execution** (`@vzn/vx-reapi`, `exec.remote`) — Bazel REAPI cache and workers; the scheduler stays here. [page](https://vznjs.github.io/vx/features/remote-execution/) · post: [Remote execution without moving the scheduler](https://vznjs.github.io/vx/blog/remote-execution/)
@@ -170,3 +172,4 @@ config key or an environment variable is missing here.
170
172
  - **Install as a remote action** (`exec.remote: 'only'`) — `node_modules` is built by an action, so stateless workers have it. [page](https://vznjs.github.io/vx/features/remote-install/) · post: [Remote execution without moving the scheduler](https://vznjs.github.io/vx/blog/remote-execution/)
171
173
  - **OTel live export** (`otel({ live })`) — spans and metrics stream as tasks end, so a dashboard follows a CI run live. [page](https://vznjs.github.io/vx/features/otel-live/) · post: [Watch a CI run while it runs](https://vznjs.github.io/vx/blog/otel-live/)
172
174
  - **Memory-aware admission** (`@vzn/vx-schedule-history`) — tasks are packed by the peak memory learned from past runs. [page](https://vznjs.github.io/vx/features/memory-admission/) · post: [A scheduler that learns from your runs](https://vznjs.github.io/vx/blog/a-scheduler-that-learns/)
175
+ - **CI timings file** (`@vzn/vx-schedule-history`, `file`) — one JSON file of learned task times a CI cache carries between runners, so a fresh runner orders by real times. [page](https://vznjs.github.io/vx/features/critical-path/) · post: [Carry the scheduler's memory between CI runners](https://vznjs.github.io/vx/blog/ci-timings-file/)