@vzn/vx 0.0.636 → 0.0.637

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 (141) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +213 -207
  3. package/docs/cli.md +699 -655
  4. package/docs/execution.md +67 -75
  5. package/docs/features.md +44 -40
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +376 -198
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +2 -2
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +16 -12
  19. package/src/bin.ts +17 -2
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +105 -53
  22. package/src/cache/git-inputs.ts +277 -63
  23. package/src/cache/index.ts +7 -1
  24. package/src/cache/inputs.ts +125 -35
  25. package/src/cache/key-fold.ts +8 -7
  26. package/src/cache/layer.ts +8 -17
  27. package/src/cache/layered-cache.ts +16 -102
  28. package/src/cache/policy.ts +25 -29
  29. package/src/cache/run-history.ts +36 -14
  30. package/src/cache/schema.ts +21 -23
  31. package/src/cache/scoped-cache.ts +224 -0
  32. package/src/cli/cache.ts +150 -28
  33. package/src/cli/completions.ts +2 -2
  34. package/src/cli/core-alias.ts +1 -1
  35. package/src/cli/foreign-flags.ts +13 -12
  36. package/src/cli/help.ts +94 -63
  37. package/src/cli/index.ts +42 -13
  38. package/src/cli/info.ts +38 -23
  39. package/src/cli/init.ts +115 -406
  40. package/src/cli/last.ts +145 -4
  41. package/src/cli/lock.ts +333 -111
  42. package/src/cli/plan-format.ts +2 -2
  43. package/src/cli/plugin-commands.ts +5 -9
  44. package/src/cli/plugin-templates.ts +10 -10
  45. package/src/cli/run.ts +103 -149
  46. package/src/cli/select.ts +33 -39
  47. package/src/cli/show.ts +17 -19
  48. package/src/cli/task-verb.ts +2 -9
  49. package/src/cli/upgrade.ts +402 -145
  50. package/src/cli/watch-cycle.ts +9 -2
  51. package/src/cli/watch-filter.ts +33 -60
  52. package/src/cli/watch-fs.ts +29 -16
  53. package/src/cli/watch-judge.ts +12 -3
  54. package/src/cli/watch-set.ts +16 -42
  55. package/src/cli/watch.ts +188 -215
  56. package/src/cli/why.ts +28 -17
  57. package/src/cli/workspace-config.ts +31 -68
  58. package/src/config.ts +169 -70
  59. package/src/exec/env.ts +1 -31
  60. package/src/exec/executor.ts +1 -5
  61. package/src/exec/kill-tree.ts +2 -1
  62. package/src/exec/sandbox-runtime.ts +12 -1
  63. package/src/exec/sandbox-violations.ts +9 -1
  64. package/src/graph/dependency-spec.ts +1 -1
  65. package/src/graph/index.ts +1 -0
  66. package/src/graph/priorities.ts +1 -1
  67. package/src/graph/scheduler.ts +19 -7
  68. package/src/graph/task-graph.ts +44 -20
  69. package/src/handoff.ts +169 -0
  70. package/src/index.ts +24 -57
  71. package/src/orchestrator/affected-tasks.ts +7 -5
  72. package/src/orchestrator/command-windows.ts +44 -0
  73. package/src/orchestrator/deferred-outputs.ts +12 -10
  74. package/src/orchestrator/doctor.ts +98 -110
  75. package/src/orchestrator/download-policy.ts +3 -3
  76. package/src/orchestrator/events.ts +19 -3
  77. package/src/orchestrator/excluded-keys.ts +0 -2
  78. package/src/orchestrator/execute-task.ts +158 -63
  79. package/src/orchestrator/fingerprint-watch.ts +18 -15
  80. package/src/orchestrator/forecast.ts +10 -2
  81. package/src/orchestrator/framed-output.ts +8 -3
  82. package/src/orchestrator/graph-stage.ts +5 -5
  83. package/src/orchestrator/history.ts +70 -30
  84. package/src/orchestrator/hit-restore.ts +15 -3
  85. package/src/orchestrator/index.ts +18 -4
  86. package/src/orchestrator/local-shortcircuit.ts +2 -1
  87. package/src/orchestrator/lockfile-claim.ts +79 -20
  88. package/src/orchestrator/logger.ts +71 -39
  89. package/src/orchestrator/metrics.ts +71 -6
  90. package/src/orchestrator/miss-save.ts +3 -0
  91. package/src/orchestrator/options.ts +42 -29
  92. package/src/orchestrator/output-host.ts +72 -0
  93. package/src/orchestrator/plan.ts +6 -8
  94. package/src/orchestrator/plugin-host.ts +87 -63
  95. package/src/orchestrator/plugin.ts +94 -60
  96. package/src/orchestrator/prepare.ts +113 -72
  97. package/src/orchestrator/projects.ts +102 -99
  98. package/src/orchestrator/remote-prefetch.ts +2 -1
  99. package/src/orchestrator/run-context.ts +25 -6
  100. package/src/orchestrator/run-failures.ts +17 -8
  101. package/src/orchestrator/run-lock.ts +1 -1
  102. package/src/orchestrator/run-report.ts +9 -9
  103. package/src/orchestrator/run-settings.ts +48 -0
  104. package/src/orchestrator/run.ts +121 -95
  105. package/src/orchestrator/sandbox-fix.ts +39 -20
  106. package/src/orchestrator/sandbox-request.ts +84 -13
  107. package/src/orchestrator/signals.ts +1 -1
  108. package/src/orchestrator/stable-keys.ts +13 -11
  109. package/src/orchestrator/status-line.ts +48 -6
  110. package/src/orchestrator/summary.ts +46 -62
  111. package/src/orchestrator/task-hash.ts +12 -83
  112. package/src/orchestrator/telemetry.ts +3 -3
  113. package/src/settings.ts +417 -0
  114. package/src/util/errors.ts +1 -1
  115. package/src/util/essential-env.ts +31 -0
  116. package/src/util/hide-env.ts +107 -0
  117. package/src/util/index.ts +8 -2
  118. package/src/util/paths.ts +10 -0
  119. package/src/util/settle.ts +49 -30
  120. package/src/util/size.ts +23 -4
  121. package/src/util/timing.ts +3 -2
  122. package/src/util/ulid.ts +1 -1
  123. package/src/util/verbs.ts +0 -15
  124. package/src/workspace/affected.ts +77 -222
  125. package/src/workspace/config-eval.ts +15 -20
  126. package/src/workspace/config-imports.ts +78 -3
  127. package/src/workspace/config-schema.ts +209 -268
  128. package/src/workspace/dependency-rules.ts +57 -0
  129. package/src/workspace/filter.ts +2 -2
  130. package/src/workspace/fingerprint.ts +11 -121
  131. package/src/workspace/index.ts +20 -27
  132. package/src/workspace/load-reads.ts +4 -4
  133. package/src/workspace/lockfile.ts +96 -140
  134. package/src/workspace/migration.ts +242 -146
  135. package/src/workspace/package-graph.ts +30 -216
  136. package/src/workspace/project-loader.ts +80 -285
  137. package/src/workspace/repo-id.ts +2 -1
  138. package/src/workspace/workspace.ts +362 -831
  139. package/src/cache/config-evals.ts +0 -130
  140. package/src/workspace/config-cache.ts +0 -661
  141. package/src/workspace/migrate-scripts.ts +0 -1121
package/docs/execution.md CHANGED
@@ -13,9 +13,9 @@ terminal and a task succeeding or failing. Read it alongside
13
13
  │ 2. cli/index.ts dispatches by subcommand (run / watch / cache /
14
14
  │ lock / init / upgrade / show / info / why / last / docs /
15
15
  │ completions / help / version); any other verb is asked of the
16
- │ workspace's plugins (`commands` seam — `vx mcp` is one).
16
+ │ workspace's plugins (`commands` seam).
17
17
  │ 3. cli/run.ts:parseRunArgs(argv) → RunArgs (validated; resolves
18
- │ the 4-axis cache policy from --cache / --no-cache / --force).
18
+ │ the 4-axis cache policy from --cache-policy / --no-cache / --force).
19
19
  │ 4. cli/run.ts:runCmd resolves the project scope:
20
20
  │ - bare positionals respect --all / --filter / --affected / cwd
21
21
  │ - anchored positionals (pkg#task) target directly
@@ -27,51 +27,43 @@ terminal and a task succeeding or failing. Read it alongside
27
27
  │ --dry / --graph short-circuit into planRun instead.
28
28
  │
29
29
  ├─ Prepare (src/orchestrator/prepare.ts:prepareRun — shared with planRun)
30
- │ 1. findWorkspaceRoot(cwd) — walks up over pnpm-workspace.yaml,
31
- │ package.json with a `workspaces` field (npm/yarn/bun), or a
32
- │ bare package.json (single-project mode). The nearest one whose
33
- │ package globs CLAIM cwd wins, so running from inside a member
34
- │ resolves the declaring root, not the member; the first
35
- │ directory holding pnpm-workspace.yaml stops the walk whether
36
- │ or not its globs claim cwd (pnpm's rule); when nothing claims
37
- │ cwd the nearest candidate wins. An UNSCOPED run (no explicit
38
- │ project scope, at least one bare task name) then starts
39
- │ step 12's git enumeration over the whole tree, overlapping
40
- │ steps 2–11; a scoped run waits, since its pathspecs depend on
41
- │ which projects the task graph holds.
42
- │ 2. loadWorkspace — parses the appropriate manifest. Bun.YAML
43
- │ for pnpm; the package.json forms read as bytes (readOnce,
44
- │ shared with step 1) and JSON.parse.
45
- │ 3. loadWorkspacePlugins — loadWorkspaceConfig reads the optional
46
- │ vx.workspace.{ts,mts,js,mjs,cts,cjs} at the root (concurrency /
47
- │ cacheDir / timeout / cacheRetention / affectedBase / cacheScope /
48
- │ rules / schedule / inputStability / plugins),
30
+ │ 1. findWorkspaceRoot(cwd) — the directory of the nearest
31
+ │ .vx/workspace.* up to the git top-level. An UNSCOPED run (no
32
+ │ explicit project scope, at least one bare task name) then
33
+ │ starts step 12's git enumeration over the whole tree,
34
+ │ overlapping steps 2–11; a scoped run waits, since its
35
+ │ pathspecs depend on which projects the task graph holds.
36
+ │ 2. Read .vx/projects.lock.json: the project directories `vx lock`
37
+ │ found (every directory holding a vx.config.*, git's ignore
38
+ │ rules applied) and their configs. Each locked config file is
39
+ │ stat'ed; one gone is refused ("run `vx lock`").
40
+ │ 3. loadWorkspacePlugins — loadWorkspaceConfig reads the
41
+ │ .vx/workspace.{ts,mts,js,mjs,cts,cjs} the root lookup found (run / cache /
42
+ │ watch / plugins / lock),
49
43
  │ then the plugin `config` stage runs on it.
50
- │ 4. listProjects — globs every workspace member's package.json,
51
- │ finds sibling vx.config.* files, detects duplicate package
52
- │ names (hard error with both paths).
53
- │ 5. buildPackageGraph — workspace dep edges from package.json.
44
+ │ 4. The plugin `discover` stage: more directories, names and
45
+ │ edges. A project is named by its config's `name`, else a
46
+ │ plugin's, else its path (`//` for the root); two of one name
47
+ │ are refused naming both directories.
48
+ │ 5. buildPackageGraph — project edges the `discover` plugins gave.
54
49
  │ 6. Local cache open: new Cache(cacheDir, { read, write }, root)
55
- │ with the policy's local slice — BEFORE the configs load,
56
- │ because it also holds their cached evaluations. Then
57
- │ computeWorkspaceFingerprints: one read of every supported
58
- │ lockfile + pnpm-workspace.yaml + .yarnrc.yml + .npmrc + bunfig.toml at the root
59
- │ yields `all` (every file; keys the config-evaluation cache),
60
- │ `unclaimed` (minus the files a `fingerprint` plugin claims —
61
- │ `@vzn/vx-lockfile` keys those per project; reused for every
62
- │ task's cache key) and `files` (the bytes, for the mid-run
63
- │ fingerprint check).
50
+ │ with the policy's local slice. Then
51
+ │ readClaimedFiles: one read of each root file a `fingerprint`
52
+ │ plugin claims (`@vzn/vx-lockfile`: its lockfile and install
53
+ │ settings; nothing without one), the bytes for the mid-run
54
+ │ check. Core folds none of them into a key.
64
55
  │ 7. SCOPED config loading — only in-scope projects plus their
65
- │ transitive dependency closure evaluate ('^task' frontier
56
+ │ transitive dependency closure load ('^task' frontier
66
57
  │ expansion never escapes the closure); one staged load,
67
- │ loadProjects, shared with every reading verb. Under --frozen,
68
- │ configs load from vx-lock.json — no evaluation, no staleness
69
- │ check of its own (vx lock --check is the audit), frozen-env
70
- │ semantics; a missing lock or entry is a hard UserError.
71
- │ Otherwise loadProjectConfigs in one batch: native Bun await
72
- │ import() with a content-hash query-string bust, cached
73
- │ evaluations served for pure configs; the loader validates
74
- │ each TaskConfig shape, and the plugin `project` stage runs
58
+ │ loadProjects, shared with every reading verb. A run's configs
59
+ │ load from .vx/projects.lock.json — no evaluation, no staleness check
60
+ │ of its own (vx lock --check is the audit), frozen-env
61
+ │ semantics; a missing lock or entry is a hard UserError. The
62
+ │ verbs that evaluate (vx lock, the readers) use
63
+ │ loadProjectConfigs in one batch: native Bun await import()
64
+ │ with a content-hash query-string bust, every config
65
+ │ evaluated; the loader validates each
66
+ │ TaskConfig shape. Either way the plugin `project` stage runs
75
67
  │ on each config with a re-validation after every plugin.
76
68
  │ 8. computeNestedProjectDirs — set of projects rooted inside each
77
69
  │ project, computed over EVERY config-bearing project (loaded
@@ -79,8 +71,8 @@ terminal and a task succeeding or failing. Read it alongside
79
71
  │ 9. expandRequested (see Task selection below).
80
72
  │ 10. Cache layer: an injected RunOptions.remoteCache is composed
81
73
  │ with the local cache into a LayeredCache (it wins); else
82
- │ resolveCache lets a plugin's `cache` capability wrap or
83
- │ replace it; else bare local.
74
+ │ resolveCache layers each plugin's remote over the local
75
+ │ cache, in declared order; else bare local.
84
76
  │ 11. buildTaskGraph (see below).
85
77
  │ 12. Bulk git populate — the enumeration step 1 started is
86
78
  │ awaited, or a scoped run starts it here over the projects
@@ -163,7 +155,7 @@ terminal and a task succeeding or failing. Read it alongside
163
155
  │ signal, forward it (SIGHUP as SIGTERM) to everything
164
156
  │ in liveChildren + persistentRegistry (and kill any
165
157
  │ running cache.inputs.runtime probe, C-65), wait
166
- │ VX_KILL_GRACE_MS (2 s) for their groups, SIGKILL
158
+ │ VX_RUN_KILL_GRACE (2 s) for their groups, SIGKILL
167
159
  │ what is still there, let the run finish its own
168
160
  │ end (flush, teardown, cache close), then
169
161
  │ die of the signal (SIGINT → 130, SIGTERM → 143,
@@ -250,11 +242,9 @@ terminal and a task succeeding or failing. Read it alongside
250
242
  │ output (deduped per run; a non-zero exit throws a
251
243
  │ UserError that fails this task — its dependents skip,
252
244
  │ independent tasks continue)
253
- │ 2. hashTaskConfig + project package.json hash (both memoized
254
- │ per run via HashCache)
245
+ │ 2. hashTaskConfig (memoized per run via HashCache)
255
246
  │ 3. filterUpstreamHashes (apply cache.inputs.tasks filter)
256
- │ 4. cache.key({ taskId, workspaceFingerprint,
257
- │ projectPackageJsonHash, taskConfigHash,
247
+ │ 4. cache.key({ taskId, taskConfigHash,
258
248
  │ forwardArgs, envValues, runtimeValues,
259
249
  │ workspaceRuntimeValues, upstreamHashes,
260
250
  │ pluginParts, inputFiles }) → 16-hex xxh3
@@ -283,9 +273,9 @@ terminal and a task succeeding or failing. Read it alongside
283
273
  │ exec.timeout SIGTERMs an overrun → real `failed`, never
284
274
  │ cached. An attempt that ends while the run is STOPPING
285
275
  │ (Ctrl-C teardown) classifies as `aborted` instead, whatever
286
- │ its exit — not counted, not recorded, not retried.
276
+ │ its exit — not recorded, not retried.
287
277
  │ 8. On exit 0 + willSave (willWrite, no tainted upstream):
288
- │ keyStillTrue() — if the workspace fingerprint or an
278
+ │ keyStillTrue() — if a claimed root file or an
289
279
  │ input moved since the key was taken, the result stands
290
280
  │ but the save is withheld (said on the status line).
291
281
  │ saveMiss — in the slot: resolveOutputs(outputs) +
@@ -386,7 +376,7 @@ The child process gets, in priority order (lowest first):
386
376
  `TMPDIR`, `TEMP`, `TMP`, `LANG`, `LC_ALL`, `LC_CTYPE`, `TERM`,
387
377
  `COLORTERM`, `FORCE_COLOR`, `NO_COLOR`, `CI`, `NODE_OPTIONS`,
388
378
  `COREPACK_HOME`, `PNPM_HOME` — the list is
389
- `ESSENTIAL_ENV` in `src/exec/env.ts`).
379
+ `ESSENTIAL_ENV` in `src/util/essential-env.ts`).
390
380
  2. **`exec.env.passThrough`** names → values from host `process.env`.
391
381
  3. **`exec.env.define`** literal name/value pairs.
392
382
  4. **PATH augmentation** — `<projectDir>/node_modules/.bin`, then
@@ -410,14 +400,16 @@ The child process gets, in priority order (lowest first):
410
400
  timeout and a shutdown's SIGTERM keep their own lines, and a task the
411
401
  run's stop killed (the SIGKILL past the grace too) gets none.
412
402
 
413
- Anything not in these four layers is invisible to the child, except
403
+ Anything not in these four layers is absent from the child's environment, except
414
404
  the two vx sets itself — `VX_RUN_WORKSPACE` and `VX_RUN_TASK` — so a
415
405
  task that shells out to `vx run` in its own workspace is refused
416
406
  before it forks without bound, and `npm_execpath`, the workspace's
417
407
  package manager (unkeyed; `schema.md` § `env`). A sandboxed task also gets the sandbox's own
418
408
  proxy, CA and `TMPDIR` values
419
409
  ([`modules/sandbox-runtime.md`](./modules/sandbox-runtime.md#the-environment-srt-sets)). This prevents incidental env leakage
420
- between machines and gives reproducible runs.
410
+ between machines and gives reproducible runs. It is hygiene, not a
411
+ boundary: an unsandboxed task runs as you and can still read your home
412
+ (vx hides its own environment, `modules/util-hide-env.md`). `exec.sandbox` is the boundary ([`security.md`](./security.md#what-the-sandbox-stops)).
421
413
 
422
414
  The allowlist + isolation contract lives in
423
415
  [`modules/env.md`](./modules/env.md) and is the only field the
@@ -428,22 +420,22 @@ broader access has cache-stability implications).
428
420
 
429
421
  ## Failure handling
430
422
 
431
- | Failure | Behavior |
432
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
433
- | Exec exits non-zero | Task is `failed`; cache NOT written; output streamed live + the failure frame replays at run end |
434
- | `exec.timeout` overrun | SIGTERM to the task's group, SIGKILL for whoever is left after the grace; task is `failed` (timed out), exit 143 (137 when the SIGKILL took it, and the line says so), never cached |
435
- | Child killed by Ctrl-C teardown (SIGINT/SIGTERM/SIGHUP), or by an embedder's `RunOptions.signal` abort — which also completes every never-started task `aborted` | Task is `aborted` — not counted, not recorded. An attempt that ends while the run is stopping is `aborted` whatever its exit: a trap that exits 0 is not cached, and a failure is not retried (item 962). What it printed still shows in its frame. A child killed by a signal while the run is NOT stopping (a supervisor's SIGTERM, a `kill` from another shell) is `failed (exit 143, 128 + SIGTERM)`: retried, shown, and fail-fast trips (item 1100) |
436
- | `execute()` throws (internal error) | Task marked `failed`; stderr written `[vx] internal error in <id>` (a `UserError` reports plainly); a plugin executor's throw reports plainly as `plugin '<p>' (executor '<e>') failed in execute: <reason>` (C-63, C-85) |
437
- | Persistent task exits before ready | Task marked `failed` with `exited before becoming ready (exit N)`; its output already streamed live; `aborted` when the run's stop killed it (C-62) |
438
- | Upstream task fails | Under the default `--continue=deps-ok`, dependents marked `skipped` (exit 1, durationMs 0); no command runs — EXCEPT a restore-tier task, whose confirmed cache hit still restores (its key is dep-independent). `--continue=never` stops dispatch; `--continue=always` runs dependents, never saved |
439
- | Sandbox violation (macOS monitor / Linux structural) | Task is `failed`; violations render in the frame; nothing cached |
440
- | Remote-cache error (500, timeout, corrupt artifact) | Degrades to a cache miss; never fails the run |
441
- | No pnpm-workspace.yaml or package.json in cwd or any parent | `findWorkspaceRoot` throws (UserError); `vx run` exits 1 |
442
- | Same-project task referenced in `dependsOn` not declared | `buildTaskGraph` throws with the offending edge |
443
- | Duplicate workspace package name | `listProjects` throws with both paths |
444
- | Cycle in task graph | `detectCycle` throws with the cycle path |
445
- | Malformed config | `loadProjectConfigs` throws (UserError) with file + field |
446
- | `cache.inputs.runtime` command exits non-zero | UserError naming the command + exit code; that task is `failed`, dependents skip |
423
+ | Failure | Behavior |
424
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
425
+ | Exec exits non-zero | Task is `failed`; cache NOT written; output streamed live + the failure frame replays at run end |
426
+ | `exec.timeout` overrun | SIGTERM to the task's group, SIGKILL for whoever is left after the grace; task is `failed` (timed out), exit 143 (137 when the SIGKILL took it, and the line says so), never cached |
427
+ | Child killed by Ctrl-C teardown (SIGINT/SIGTERM/SIGHUP), or by an embedder's `RunOptions.signal` abort — which also completes every never-started task `aborted` | Task is `aborted` — not recorded in history. An attempt that ends while the run is stopping is `aborted` whatever its exit: a trap that exits 0 is not cached, and a failure is not retried (item 962). What it printed still shows in its frame. A child killed by a signal while the run is NOT stopping (a supervisor's SIGTERM, a `kill` from another shell) is `failed (exit 143, 128 + SIGTERM)`: retried, shown, and fail-fast trips (item 1100) |
428
+ | `execute()` throws (internal error) | Task marked `failed`; stderr written `[vx] internal error in <id>` (a `UserError` reports plainly); a plugin executor's throw reports plainly as `plugin '<p>' (executor '<e>') failed in execute: <reason>` (C-63, C-85) |
429
+ | Persistent task exits before ready | Task marked `failed` with `exited before becoming ready (exit N)`; its output already streamed live; `aborted` when the run's stop killed it (C-62) |
430
+ | Upstream task fails | Under the default `--continue=deps-ok`, dependents marked `skipped` (exit 1, durationMs 0); no command runs — EXCEPT a restore-tier task, whose confirmed cache hit still restores (its key is dep-independent). `--continue=never` stops dispatch; `--continue=always` runs dependents, never saved |
431
+ | Sandbox violation (macOS monitor / Linux structural) | Task is `failed`; violations render in the frame; nothing cached |
432
+ | Remote-cache error (500, timeout, corrupt artifact) | Degrades to a cache miss; never fails the run |
433
+ | No .vx/workspace.* in cwd or any parent up to the git top-level | `findWorkspaceRoot` throws (UserError); `vx run` exits 1 |
434
+ | Same-project task referenced in `dependsOn` not declared | `buildTaskGraph` throws with the offending edge |
435
+ | Two projects of one name | `checkProjectNames` throws with both paths |
436
+ | Cycle in task graph | `detectCycle` throws with the cycle path |
437
+ | Malformed config | `loadProjectConfigs` throws (UserError) with file + field |
438
+ | `cache.inputs.runtime` command exits non-zero | UserError naming the command + exit code; that task is `failed`, dependents skip |
447
439
 
448
440
  By default (`--continue=deps-ok`) failures don't kill the scheduler —
449
441
  independent tasks already in flight finish, and unrelated tasks not yet
@@ -566,7 +558,7 @@ The colors / framing modules:
566
558
  - `orchestrator/logger.ts` — composes them; resolves the output view
567
559
  and applies the per-flow visibility policy.
568
560
  - `orchestrator/summary.ts` — the closing footer (wordmark rule,
569
- projects/tasks/cache meters, info, time and result rows).
561
+ projects/tasks/cache meters, info and time rows).
570
562
 
571
563
  ## Concurrency
572
564
 
@@ -574,7 +566,7 @@ The colors / framing modules:
574
566
  capped by the cgroup CPU quota a container runs under (`cpu.max` on v2,
575
567
  `cpu.cfs_quota_us` on v1, an ancestor's quota binding too; a 1.5-core
576
568
  quota is two workers, never below one — `util/cgroup.ts`), or
577
- `vx.workspace.ts`'s `concurrency` field when set. Inside a container the
569
+ `.vx/workspace.ts`'s `run.concurrency` field when set. Inside a container the
578
570
  raw count is the HOST's, and eight workers on a two-core quota is
579
571
  oversubscription by four; `--concurrency <n>%` is a percentage of the
580
572
  same capped count.
@@ -621,8 +613,8 @@ mid-debug would be hostile.
621
613
 
622
614
  `--force` turns only the READ axes off: every task re-executes, but
623
615
  fresh artifacts are still written (outputs ARE cleaned so the saved
624
- snapshot is clean). `--cache=<spec>` gives per-layer control. Full
625
- semantics: [`cli.md` § Cache control](./cli.md#cache-control---cache---no-cache---force)
616
+ snapshot is clean). `--cache-policy=<spec>` gives per-layer control. Full
617
+ semantics: [`cli.md` § Cache control](./cli.md#cache-control---cache-policy---no-cache---force)
626
618
  and [`caching.md` § Cache policy](./caching.md#cache-policy-readwrite-axes).
627
619
 
628
620
  ## Planning paths (`--dry`, `--graph`)
package/docs/features.md CHANGED
@@ -15,25 +15,25 @@ config key or an environment variable is missing here.
15
15
  - **Run a task** (`vx run`) — run a task in the cwd's project, `pkg#task` directly, or several at once. [page](https://vznjs.github.io/vx/features/quickstart/) · post: [The basics, done carefully](https://vznjs.github.io/vx/blog/the-basics/)
16
16
  - **Every project** (`--all`) — run the task in every project that declares it. [page](https://vznjs.github.io/vx/features/quickstart/) · post: [The basics, done carefully](https://vznjs.github.io/vx/blog/the-basics/)
17
17
  - **pnpm-style filters** (`--filter`) — select projects by name, glob, path, dependencies (`foo...`), dependents (`...foo`), negation or a git range. [page](https://vznjs.github.io/vx/features/affected/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
18
- - **Affected only** (`--affected`, `affectedBase`) — run what a change reaches, following task edges from a git base. [page](https://vznjs.github.io/vx/features/affected/) · post: [Run only what a change reaches](https://vznjs.github.io/vx/blog/affected/)
19
- - **Concurrency** (`--concurrency`, `concurrency`) — a count or a share of the CPUs this process may use (`50%`). [page](https://vznjs.github.io/vx/features/concurrency/) · post: [One failure, and exactly what it takes down](https://vznjs.github.io/vx/blog/when-a-build-fails/)
18
+ - **Affected only** (`--affected`, `run.affectedBase`, `VX_RUN_AFFECTED_BASE`) — run what a change reaches, following task edges from a git base. [page](https://vznjs.github.io/vx/features/affected/) · post: [Run only what a change reaches](https://vznjs.github.io/vx/blog/affected/)
19
+ - **Concurrency** (`--concurrency`, `run.concurrency`, `VX_RUN_CONCURRENCY`) — a count or a share of the CPUs this process may use (`50%`). [page](https://vznjs.github.io/vx/features/concurrency/) · post: [One failure, and exactly what it takes down](https://vznjs.github.io/vx/blog/when-a-build-fails/)
20
20
  - **Skip dependencies** (`--exclude-dependencies`) — skip all `dependsOn` edges, or named ones. [page](https://vznjs.github.io/vx/features/skip-dependencies/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
21
- - **Failure policy** (`--continue`) — never, deps-ok (default: dependents skip) or always. [page](https://vznjs.github.io/vx/features/skipped-blockers/) · post: [One failure, and exactly what it takes down](https://vznjs.github.io/vx/blog/when-a-build-fails/)
21
+ - **Failure policy** (`--continue`, `run.continue`, `VX_RUN_CONTINUE`) — never, deps-ok (default: dependents skip) or always. [page](https://vznjs.github.io/vx/features/skipped-blockers/) · post: [One failure, and exactly what it takes down](https://vznjs.github.io/vx/blog/when-a-build-fails/)
22
22
  - **Every skip names its blocker** — a skipped task says which failure blocked it. [page](https://vznjs.github.io/vx/features/skipped-blockers/) · post: [One failure, and exactly what it takes down](https://vznjs.github.io/vx/blog/when-a-build-fails/)
23
- - **Retries** (`--retry`, `exec.retries`) — re-run a failed task; a pass after a failure marks it flaky. [page](https://vznjs.github.io/vx/features/flaky-detection/) · post: [Flaky is a claim only declared inputs can back](https://vznjs.github.io/vx/blog/flaky-tasks/)
24
- - **Timeouts** (`--timeout`, `exec.timeout`, `timeout`, `VX_TASK_TIMEOUT`) — kill and fail a runaway task. [page](https://vznjs.github.io/vx/features/timeouts/) · post: [When a task misbehaves](https://vznjs.github.io/vx/blog/tasks-that-misbehave/)
23
+ - **Retries** (`--retries`, `exec.retries`, `run.retries`, `VX_RUN_RETRIES`) — re-run a failed task; a pass after a failure marks it flaky. [page](https://vznjs.github.io/vx/features/flaky-detection/) · post: [Flaky is a claim only declared inputs can back](https://vznjs.github.io/vx/blog/flaky-tasks/)
24
+ - **Timeouts** (`--timeout`, `exec.timeout`, `run.timeout`, `VX_RUN_TIMEOUT`) — kill and fail a runaway task. [page](https://vznjs.github.io/vx/features/timeouts/) · post: [When a task misbehaves](https://vznjs.github.io/vx/blog/tasks-that-misbehave/)
25
25
  - **Argument forwarding** (`--`) — args after `--` reach the task's command and fold into its key. [page](https://vznjs.github.io/vx/features/forward-args/) · post: [When a task misbehaves](https://vznjs.github.io/vx/blog/tasks-that-misbehave/)
26
26
  - **Task picker** — `vx run` with no task in a terminal lists tasks to pick. [page](https://vznjs.github.io/vx/features/task-picker/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
27
27
  - **Typo hints** — an unknown task, project or filter suggests the nearest name. [page](https://vznjs.github.io/vx/features/filter-hints/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
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
- - **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/)
29
+ - **Ctrl-C leaves nothing running** (`--kill-grace`, `--teardown-timeout`, `run.killGrace`, `run.teardownTimeout`, `VX_RUN_KILL_GRACE`, `VX_RUN_TEARDOWN_TIMEOUT`) — 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
+ - **Schedule strategy** (`--schedule`, `run.schedule`, `VX_RUN_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: [Which task runs first?](https://vznjs.github.io/vx/blog/scheduling-strategies/#pick-the-guess)
32
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/)
33
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/)
34
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/)
35
35
  - **Implicit keyed `build`** — a project with no `build` gets a `^build` group keyed on its files, so source-only packages still move their dependants' keys. [page](https://vznjs.github.io/vx/features/implicit-build/) · post: [The basics, done carefully](https://vznjs.github.io/vx/blog/the-basics/)
36
- - **dependsOn syntax** (`^name`, `pkg#name`, `name.*`, `^name.*`) — upstream, cross-project and pattern edges. [page](https://vznjs.github.io/vx/features/depends-on-syntax/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
36
+ - **dependsOn syntax** (`^name`, `pkg#name`, `name.*`, `^name.*`) — upstream, cross-project and pattern edges; `^` follows the project edges a plugin supplies, and with none is refused naming the fix. [page](https://vznjs.github.io/vx/features/depends-on-syntax/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
37
37
  - **Every name must resolve** — `vx run lint test typecheck` refuses to start if one name matches no project. [page](https://vznjs.github.io/vx/features/names-must-resolve/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
38
38
  - **Tag filters** (`--filter tag:<pattern>`) — select projects by their config `tags`. [page](https://vznjs.github.io/vx/features/tag-filters/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
39
39
  - **Directory and root filters** (`./<dir>`, `{<dir>}`, `.`, `//`) — select projects by path, or the root project. [page](https://vznjs.github.io/vx/features/dir-filters/) · post: [Say exactly which tasks to run](https://vznjs.github.io/vx/blog/pick-your-tasks/)
@@ -43,15 +43,16 @@ config key or an environment variable is missing here.
43
43
  ## Output
44
44
 
45
45
  - **Framed output** — each task's log in its own frame, never interleaved. [page](https://vznjs.github.io/vx/features/framed-output/) · post: [A run you can read](https://vznjs.github.io/vx/blog/a-run-you-can-read/)
46
- - **Output modes** (`--output-logs`) — full, errors-only, hash-only or none; the default follows the flow. [page](https://vznjs.github.io/vx/features/output-modes/) · post: [Output that fits the run](https://vznjs.github.io/vx/blog/output-that-fits-the-run/)
46
+ - **Output modes** (`--output-logs`, `VX_RUN_OUTPUT_LOGS`) — full, errors-only, hash-only or none; the default follows the flow. [page](https://vznjs.github.io/vx/features/output-modes/) · post: [Output that fits the run](https://vznjs.github.io/vx/blog/output-that-fits-the-run/)
47
47
  - **Run summary** — one block: projects, tasks, cache, time; nothing prints below it. [page](https://vznjs.github.io/vx/features/run-summary/) · post: [A run you can read](https://vznjs.github.io/vx/blog/a-run-you-can-read/)
48
48
  - **Per-task table** (`--verbosity`) — a per-task summary after the run. [page](https://vznjs.github.io/vx/features/per-task-table/) · post: [One failure, and exactly what it takes down](https://vznjs.github.io/vx/blog/when-a-build-fails/)
49
49
  - **Failed output kept for agents** — a failure's full log is saved and pointed to. [page](https://vznjs.github.io/vx/features/failed-output-kept/) · post: [Built for the agent at the keyboard](https://vznjs.github.io/vx/blog/built-for-agents/)
50
50
  - **Markdown report** (`--report`, `--report-file`) — a run report for a PR or `$GITHUB_STEP_SUMMARY`. [page](https://vznjs.github.io/vx/features/markdown-report/) · post: [Output that fits the run](https://vznjs.github.io/vx/blog/output-that-fits-the-run/)
51
51
  - **Run JSON** (`--summarize`) — per-run JSON for scripts, with the time the cache saved (`savedMs`). [page](https://vznjs.github.io/vx/features/run-json/) · post: [See inside a run](https://vznjs.github.io/vx/blog/see-inside-a-run/)
52
52
  - **Trace profile** (`--profile`) — Chrome-trace JSON of the run. [page](https://vznjs.github.io/vx/features/profile/) · post: [Output that fits the run](https://vznjs.github.io/vx/blog/output-that-fits-the-run/)
53
+ - **Run branch** (`--branch`, `VX_RUN_BRANCH`) — name the branch a run records in its history and forecasts from; unset, git's branch, then the CI provider's. no post
53
54
  - **Run tags** (`--tag`) — label a run; recorded in history. [page](https://vznjs.github.io/vx/features/run-tags/) · post: [See inside a run](https://vznjs.github.io/vx/blog/see-inside-a-run/)
54
- - **Stage timing** (`VX_TIMING`) — vx's own stage table, for performance work. [page](https://vznjs.github.io/vx/features/stage-timing/) · post: [See inside a run](https://vznjs.github.io/vx/blog/see-inside-a-run/)
55
+ - **Stage timing** (`VX_DEBUG_TIMING`) — vx's own stage table, for performance work. [page](https://vznjs.github.io/vx/features/stage-timing/) · post: [See inside a run](https://vznjs.github.io/vx/blog/see-inside-a-run/)
55
56
  - **Output flows** — what is printed follows the run's intent (focused, broad or CI); a truthy `CI` wins. [page](https://vznjs.github.io/vx/features/output-flows/) · post: [Output that fits the run](https://vznjs.github.io/vx/blog/output-that-fits-the-run/)
56
57
  - **Cache-aware glyphs** — each task line's glyph shows ran, fresh, restored locally or remotely, failed, skipped or persistent. [page](https://vznjs.github.io/vx/features/cache-glyphs/) · post: [Output that fits the run](https://vznjs.github.io/vx/blog/output-that-fits-the-run/)
57
58
  - **GitHub Actions log groups** — on Actions, each task's block folds in a `::group::` with its outcome and time. [page](https://vznjs.github.io/vx/features/actions-log-groups/) · post: [The basics, done carefully](https://vznjs.github.io/vx/blog/the-basics/)
@@ -63,10 +64,10 @@ config key or an environment variable is missing here.
63
64
 
64
65
  - **Dry run** (`--dry`) — the task graph and the predicted hits and misses, text or JSON (with the predicted wall time and its critical path), nothing runs. [page](https://vznjs.github.io/vx/features/dry-run/) · post: [See the plan before you run it](https://vznjs.github.io/vx/blog/dry-run/)
65
66
  - **Graph** (`--graph`) — the task graph as Graphviz DOT. [page](https://vznjs.github.io/vx/features/task-graph/) · post: [See inside a run](https://vznjs.github.io/vx/blog/see-inside-a-run/)
66
- - **vx why** (`vx why`, `--run`) — why a task re-ran, down to the file, env var or config that changed. [page](https://vznjs.github.io/vx/features/vx-why/) · post: [Why did this re-run?](https://vznjs.github.io/vx/blog/why-did-this-rerun/)
67
- - **vx show** (`vx show`) — every project, a project's or a task's live resolved config. [page](https://vznjs.github.io/vx/features/vx-show/) · post: [See what a task really is](https://vznjs.github.io/vx/blog/vx-show/)
68
- - **vx last** (`vx last`, `--list`, `--failed`, `--log`) — replay a recorded run's summary, list recent runs, or print one task's output. [page](https://vznjs.github.io/vx/features/vx-last/) · post: [The last run, on request](https://vznjs.github.io/vx/blog/vx-last/)
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/)
67
+ - **vx why** (`vx why`, `--run`) — why a task re-ran, down to the file, env var or config that changed, and the task that wrote a moved input. [page](https://vznjs.github.io/vx/features/vx-why/) · post: [Why did this re-run?](https://vznjs.github.io/vx/blog/why-did-this-rerun/)
68
+ - **vx show** (`vx show`) — every project, a project's or a task's resolved config, read from the lock as a run reads it. [page](https://vznjs.github.io/vx/features/vx-show/) · post: [See what a task really is](https://vznjs.github.io/vx/blog/vx-show/)
69
+ - **vx last** (`vx last`, `--list`, `--failed`, `--log`, `--stats`, `--branch`) — replay a recorded run's summary, list recent runs, print one task's output, or per-task history stats (p50 as the live estimate forecasts it on a branch, p99, pass and hit rate, failure mode, peak RSS). [page](https://vznjs.github.io/vx/features/vx-last/) · post: [The last run, on request](https://vznjs.github.io/vx/blog/vx-last/)
70
+ - **vx info** (`vx info`) — workspace doctor: versions, projects, cache size against `cache.retention.maxSize`, every setting and where it came from. [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/)
70
71
  - **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/)
71
72
  - **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/)
72
73
  - **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/)
@@ -74,30 +75,31 @@ config key or an environment variable is missing here.
74
75
  - **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/)
75
76
  - **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/)
76
77
  - **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/)
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/)
78
+ - **Agent skill** (`skills/vx/SKILL.md`, `npx skills add vznjs/vx/packages/vx/skills/vx`) — an installable skill that teaches a coding agent to run, debug and query vx through the CLI's `--format json`; copy it from `node_modules/@vzn/vx/skills/vx/` to match the installed version. [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/)
78
79
 
79
80
  ## Cache
80
81
 
81
82
  - **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/)
82
83
  - **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/)
83
84
  - **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
- - **Input stability** (`cache.inputStability`, workspace `inputStability`, `VX_INPUT_STABILITY`, `VX_INPUT_STABILITY_SEVERITY`) — an input written while the command runs withholds the save, on CI too, and the saves of every task that ran over its outputs; `strategy: 'mutable'` accepts a task's identical rewrite of its own input, `'unchecked'` skips the check; `severity: 'error'` fails the run instead of warning. no post
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/)
85
+ - **Input stability** (`cache.inputStability`, workspace `cache.inputStability`, `VX_CACHE_INPUT_STABILITY_STRATEGY`, `VX_CACHE_INPUT_STABILITY_SEVERITY`) — an input written while the command runs withholds the save, on CI too, and the saves of every task that ran over its outputs; `strategy: 'mutable'` accepts a task's identical rewrite of its own input, `'unchecked'` skips the check; `severity: 'error'` fails the run instead of warning. no post
86
+ - **Keep outputs on a miss** (`cache.outputs.clean`, workspace `cache.outputs.clean`, `VX_CACHE_OUTPUTS_CLEAN`) — `false` leaves a task's outputs in place for a tool that reads its last output (tsc's `.tsbuildinfo`); if the run leaves any old file unchanged, nothing is cached. no post
87
+ - **Outputs are exactly the snapshot** (`run.rules.exclusiveOutputs`, `VX_RUN_RULES_EXCLUSIVE_OUTPUTS`) — 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/)
86
88
  - **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/)
87
89
  - **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/)
88
90
  - **Cascade through inputs** — a task's key folds its upstream input keys, never outputs. [page](https://vznjs.github.io/vx/features/cascade/) · post: [Cascade through dependencies](https://vznjs.github.io/vx/blog/cascade-through-inputs/)
89
- - **Lockfile-aware keys** (`@vzn/vx-lockfile`: `pnpm()`, `bun()`, `npm()`, `yarn()`) — a bump re-keys only the projects whose closure changed. [page](https://vznjs.github.io/vx/features/lockfile-keys/) · post: [A lockfile bump should re-key two tasks](https://vznjs.github.io/vx/blog/lockfile-aware-keys/)
90
- - **Upfront keys** (`upfrontKeys`) — refuses an input glob a same-project task's outputs could match, so every key is known before anything runs. [page](https://vznjs.github.io/vx/features/upfront-keys/) · post: [Cascade through dependencies by folding input keys, never outputs](https://vznjs.github.io/vx/blog/cascade-through-inputs/)
91
- - **Cache controls** (`--no-cache`, `--force`, `--cache`, `VX_CACHE`) — off, refresh, or per-layer read/write. [page](https://vznjs.github.io/vx/features/cache-controls/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
92
- - **Cache location** (`--cache-dir`, `cacheDir`, `VX_CACHE_DIR`) — where the cache lives. [page](https://vznjs.github.io/vx/features/cache-location/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
93
- - **Cache scope** (`cacheScope`, `VX_CACHE_SCOPE`, `--cache-scope`) — trusted CI writes the remote cache; a laptop reads it. [page](https://vznjs.github.io/vx/features/cache-scope/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
94
- - **Cache pruning** (`vx cache prune`, `--older-than`, `--max-size`, `--dry-run`, `cacheRetention`, `maxSize`, `olderThan`) — evict by age or size, LRU. [page](https://vznjs.github.io/vx/features/cache-prune/) · post: [One command to know your workspace](https://vznjs.github.io/vx/blog/know-your-workspace/)
95
- - **Remote outputs** (`--download`) — all, top-level only, or none. [page](https://vznjs.github.io/vx/features/remote-downloads/) · post: [A remote server you can trust in production](https://vznjs.github.io/vx/blog/reapi-in-production/)
91
+ - **Lockfile-aware keys** (`@vzn/vx-lockfile`: `pnpm()`, `bun()`, `npm()`, `yarn()`) — a bump re-keys only the projects whose closure changed; each also adds every member with a `package.json` as a project named by its `name`, gives vx the project edges the lockfile records (core reads no `package.json`), keys each package's `package.json` as an input and folds its manager's install settings (`.npmrc`, `bunfig.toml`, `pnpm-workspace.yaml`, `.yarnrc.yml`) into every task. Core keys no package file, so without one an install re-keys nothing; `npm()` reads `npm-shrinkwrap.json` first, and `bun()` refuses a lone `bun.lockb`. [page](https://vznjs.github.io/vx/features/lockfile-keys/) · post: [A lockfile bump should re-key two tasks](https://vznjs.github.io/vx/blog/lockfile-aware-keys/)
92
+ - **Upfront keys** (`run.rules.upfrontKeys`, `VX_RUN_RULES_UPFRONT_KEYS`) — refuses an input glob a same-project task's outputs could match, so every key is known before anything runs. [page](https://vznjs.github.io/vx/features/upfront-keys/) · post: [Cascade through dependencies by folding input keys, never outputs](https://vznjs.github.io/vx/blog/cascade-through-inputs/)
93
+ - **Dependency rules** (`run.rules.dependencies`) — `vx lock` refuses a project depending on one a tag or name rule denies or does not allow, every broken edge named (`VX_E_DEPENDENCY_RULE`). [schema](https://vznjs.github.io/vx/schema/) · post: [Say which projects may depend on which](https://vznjs.github.io/vx/blog/dependency-rules/)
94
+ - **Cache controls** (`--no-cache`, `--force`, `--cache-policy`, `--cache-policy`, `VX_CACHE_POLICY`, `cache.policy`) — off, refresh, or per-layer read/write. [page](https://vznjs.github.io/vx/features/cache-controls/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
95
+ - **Cache location** (`--cache-dir`, `cache.dir`, `VX_CACHE_DIR`) — where the cache lives. [page](https://vznjs.github.io/vx/features/cache-location/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
96
+ - **Cache scope** (`cache.scope`, `VX_CACHE_SCOPE`, `--cache-scope`) — a scope reads the default entries, then its own, and writes only its own. [page](https://vznjs.github.io/vx/features/cache-scope/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
97
+ - **Cache pruning** (`vx cache clean`, `--older-than`, `--max-size`, `--global`, `--dry-run`, `cache.retention`, `maxSize`, `olderThan`, `VX_CACHE_RETENTION_MAX_SIZE`, `VX_CACHE_RETENTION_OLDER_THAN`) — evict by age or size, LRU; with no bound, clear this repo's store (shared by its checkouts) and run history; `--global` clears every repo's store. [page](https://vznjs.github.io/vx/features/cache-prune/) · post: [One command to know your workspace](https://vznjs.github.io/vx/blog/know-your-workspace/)
98
+ - **Remote outputs** (`--cache-download`, `cache.download`, `VX_CACHE_DOWNLOAD`) — all, top-level only, or none. [page](https://vznjs.github.io/vx/features/remote-downloads/) · post: [A remote server you can trust in production](https://vznjs.github.io/vx/blog/reapi-in-production/)
96
99
  - **One store for every checkout** (`~/.vx/<id>/cache`) — clones and worktrees of a repo share cache entries. [page](https://vznjs.github.io/vx/features/shared-store/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
97
100
  - **Warm hits restore nothing** — when outputs on disk already match, a hit costs a few stats. [page](https://vznjs.github.io/vx/features/warm-hits/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
98
101
  - **Hits replay both streams** — stdout and stderr come back in the order the run printed them. [page](https://vznjs.github.io/vx/features/both-streams/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
99
102
  - **Restore lane** — cache restores run on their own lane, up to twice `--concurrency`. [page](https://vznjs.github.io/vx/features/restore-lane/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
100
- - **Config evaluation cache** — provably pure `vx.config.ts` files are read back as data, not evaluated again. [page](https://vznjs.github.io/vx/features/config-cache/) · post: [Guard rails that tell you the fix](https://vznjs.github.io/vx/blog/guard-rails/)
101
103
  - **Line-ending-correct keys** — files git filters (`eol`, `core.autocrlf`) key on the bytes the build sees. [page](https://vznjs.github.io/vx/features/line-endings/) · post: [Your cache key is already in git's index](https://vznjs.github.io/vx/blog/keys-from-git/)
102
104
  - **Background remote uploads** — remote writes drain at the end of the run and never fail the build. [page](https://vznjs.github.io/vx/features/background-uploads/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
103
105
  - **Bring your own remote cache** (plugin `cache`) — plug any cache server in through one interface. [page](https://vznjs.github.io/vx/features/custom-remote-cache/) · post: [Extend vx in an afternoon](https://vznjs.github.io/vx/blog/extend-vx/)
@@ -105,32 +107,33 @@ config key or an environment variable is missing here.
105
107
  ## Correctness
106
108
 
107
109
  - **Sandboxed tasks** (`exec.sandbox`: `allow`, `deny`, `ignore`, `weakerNetworkIsolation`, `weakerWhenNested`) — a task reads and writes only what it declares; `allow` names `read`, `write`, `network`, `unixSockets`, `localBinding`, `pty`, `systemInfo`, `gitConfig`, `machLookup`. [page](https://vznjs.github.io/vx/features/sandbox/) · post: [The sandbox](https://vznjs.github.io/vx/blog/the-sandbox/)
108
- - **Env isolation** (`exec.env`: `define`, `passThrough`, `secret`) — a task sees only the env it names; secrets are masked. [page](https://vznjs.github.io/vx/features/env-isolation/) · post: [A task sees only the env it names](https://vznjs.github.io/vx/blog/env-isolation/)
109
- - **vx lock** (`vx lock`, `--check`, `--format json`, `--frozen`) — freeze what configs evaluate to; CI and agents check it. [page](https://vznjs.github.io/vx/features/vx-lock/) · post: [vx lock: freezing what the key sees](https://vznjs.github.io/vx/blog/lock-and-frozen/)
110
+ - **Env isolation** (`exec.env`: `define`, `passThrough`, `secret`, `VX_DEBUG_DUMPABLE`) — a task sees only the env it names; secrets are masked; vx hides its own env from tasks (Linux and macOS). [page](https://vznjs.github.io/vx/features/env-isolation/) · post: [A task sees only the env it names](https://vznjs.github.io/vx/blog/env-isolation/)
111
+ - **vx lock** (`vx lock`, `--check`, `--format json`, `lock.reviewFiles`, `VX_LOCK_REVIEW_FILES`) — find the projects (every directory holding a `vx.config`), freeze what their configs evaluate to and check the graph rules once; only `vx lock` evaluates configs: runs, `vx show` and `vx info` read the lock and never write it; a pretty `vx.lock.json` beside each config shows reviewers what it resolves to. [page](https://vznjs.github.io/vx/features/vx-lock/) · post: [vx lock: freezing what the key sees](https://vznjs.github.io/vx/blog/lock-and-frozen/)
110
112
  - **Flaky detection** — a task that fails then passes is reported flaky. [page](https://vznjs.github.io/vx/features/flaky-detection/) · post: [Flaky is a claim](https://vznjs.github.io/vx/blog/flaky-tasks/)
111
113
  - **Project boundaries** — globs never cross into another project. [page](https://vznjs.github.io/vx/features/project-boundaries/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
112
114
  - **Artifact integrity checks** — a CRC-32, a key match and an outputs-only check on every artifact; damage is a miss. [page](https://vznjs.github.io/vx/features/artifact-integrity/) · post: [What goes into a key, and what comes back](https://vznjs.github.io/vx/blog/inside-a-cache-hit/)
113
115
  - **Sandbox names what to grant** — a refused write is named beside the failed task with the path to allow. [page](https://vznjs.github.io/vx/features/sandbox-grants/) · post: [Guard rails that tell you the fix](https://vznjs.github.io/vx/blog/guard-rails/)
114
116
  - **Strict numeric flags** — `0x10`, `1e3` and `2.7` are refused, never reinterpreted. [page](https://vznjs.github.io/vx/features/strict-numbers/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
115
- - **Verified releases** — binaries carry provenance, `vx upgrade` checks SHA-256, npm publishes with provenance. [page](https://vznjs.github.io/vx/features/verified-releases/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
117
+ - **Verified releases** — binaries carry provenance, `vx upgrade` checks npm's registry signature and the SHA-512, npm publishes with provenance. [page](https://vznjs.github.io/vx/features/verified-releases/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
116
118
 
117
119
  ## Daily work
118
120
 
119
- - **Watch mode** (`vx watch`, `VX_WATCH_POLL`) — re-run what a change affects, on content, not events. [page](https://vznjs.github.io/vx/features/watch/) · post: [Watch: a content gate](https://vznjs.github.io/vx/blog/watch-mode/)
120
- - **Dev servers in the graph** (`exec.persistent`, `readyWhen`, `VX_READY_NOTICE_MS`) — a server is a node; dependents start when it is ready. [page](https://vznjs.github.io/vx/features/dev-servers/) · post: [Dev servers as graph nodes](https://vznjs.github.io/vx/blog/dev-servers-in-the-graph/)
121
+ - **Watch mode** (`vx watch`, `--poll`, `watch.poll`, `VX_WATCH_POLL`) — re-run what a change affects, on content, not events. [page](https://vznjs.github.io/vx/features/watch/) · post: [Watch: a content gate](https://vznjs.github.io/vx/blog/watch-mode/)
122
+ - **Dev servers in the graph** (`exec.persistent`, `readyWhen`, `--ready-notice`, `run.readyNotice`, `VX_RUN_READY_NOTICE`) — a server is a node; dependents start when it is ready. [page](https://vznjs.github.io/vx/features/dev-servers/) · post: [Dev servers as graph nodes](https://vznjs.github.io/vx/blog/dev-servers-in-the-graph/)
121
123
  - **Interactive tasks** (`exec.interactive`) — a task that owns the terminal. [page](https://vznjs.github.io/vx/features/interactive-tasks/) · post: [When a task misbehaves](https://vznjs.github.io/vx/blog/tasks-that-misbehave/)
122
124
  - **Shell completions** (`vx completions`) — bash, zsh, fish. [page](https://vznjs.github.io/vx/features/completions/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
123
- - **vx upgrade** (`vx upgrade`) — replace the binary with a release. [page](https://vznjs.github.io/vx/features/upgrade/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
125
+ - **Project-local vx** (`VX_HANDED_OFF`) — a global vx hands every call to the project's own `@vzn/vx`. [page](https://vznjs.github.io/vx/cli/#which-vx-runs)
126
+ - **vx upgrade** (`vx upgrade`) — upgrade vx the way it was installed: a verified binary, or its package manager. [page](https://vznjs.github.io/vx/features/upgrade/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
124
127
  - **Help and version** (`vx help`, `vx version`) — every verb's reference. [page](https://vznjs.github.io/vx/features/help-and-version/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
125
128
  - **Did-you-mean** — a mistyped flag or verb gets the nearest valid spelling. [page](https://vznjs.github.io/vx/features/did-you-mean/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
126
129
  - **Task typed as a verb** — `vx build app` answers with the exact `vx run` command that does it. [page](https://vznjs.github.io/vx/features/task-as-verb/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
127
130
 
128
131
  ## Config
129
132
 
130
- - **Config in TypeScript** (`vx.config.ts`, `defineProject`, `vx.workspace.ts`, `defineWorkspace`) — typed, composable; no named inputs. [page](https://vznjs.github.io/vx/features/typescript-config/) · post: [Config in TypeScript](https://vznjs.github.io/vx/blog/config-in-typescript/)
133
+ - **Config in TypeScript** (`vx.config.ts`, `defineProject`, `.vx/workspace.ts`, `defineWorkspace`) — typed, composable; no named inputs. `.vx/workspace.ts` is required: the nearest one up from the cwd, never above the git top-level, marks the workspace root. [page](https://vznjs.github.io/vx/features/typescript-config/) · post: [Config in TypeScript](https://vznjs.github.io/vx/blog/config-in-typescript/)
131
134
  - **Tasks** (`tasks`, `exec.command`, `dependsOn`, `description`, `tags`) — one command per task; the shell is the API. [page](https://vznjs.github.io/vx/features/one-command-tasks/) · post: [One command per task](https://vznjs.github.io/vx/blog/one-command-per-task/)
132
135
  - **Workspace rules** (`rules`) — speed-only checks, on by default, configurable. [page](https://vznjs.github.io/vx/features/workspace-rules/) · post: [Guard rails that tell you the fix](https://vznjs.github.io/vx/blog/guard-rails/)
133
- - **Config worker timeout** (`VX_CONFIG_WORKER_TIMEOUT_MS`) — bound a config's evaluation. [page](https://vznjs.github.io/vx/features/config-timeout/) · post: [Guard rails that tell you the fix](https://vznjs.github.io/vx/blog/guard-rails/)
136
+ - **Config worker timeout** (`VX_CONFIG_WORKER_TIMEOUT`) — bound a config's evaluation. [page](https://vznjs.github.io/vx/features/config-timeout/) · post: [Guard rails that tell you the fix](https://vznjs.github.io/vx/blog/guard-rails/)
134
137
  - **No nested runs** (`VX_RUN_TASK`, `VX_RUN_WORKSPACE`) — set on every task; a `vx run` inside a task of the same workspace is refused. [page](https://vznjs.github.io/vx/features/no-nested-runs/) · post: [When a task misbehaves](https://vznjs.github.io/vx/blog/tasks-that-misbehave/)
135
138
  - **Typed config helpers** (`defineProject`) — autocomplete for task names in `dependsOn`, errors while you edit. [page](https://vznjs.github.io/vx/features/typed-helpers/) · post: [Config in TypeScript, and why there are no named inputs](https://vznjs.github.io/vx/blog/config-in-typescript/)
136
139
  - **Presets** — a TypeScript function returning a task config, shared across projects. [page](https://vznjs.github.io/vx/features/presets/) · post: [Config in TypeScript, and why there are no named inputs](https://vznjs.github.io/vx/blog/config-in-typescript/)
@@ -140,30 +143,31 @@ config key or an environment variable is missing here.
140
143
 
141
144
  - **Results on GitHub** (`@vzn/vx-ci`: `github()`) — job summary and Checks API annotations. [page](https://vznjs.github.io/vx/features/github-ci/) · post: [Your run, on the pull request](https://vznjs.github.io/vx/blog/results-on-github/)
142
145
  - **PR check run** (`github({ checks })`) — a check run on the commit with the run summary as its output. [page](https://vznjs.github.io/vx/features/pr-check-run/) · post: [Your run, on the pull request](https://vznjs.github.io/vx/blog/results-on-github/)
143
- - **Cache scope from the ref** (`github({ cacheScope })`) — main writes trusted keys; a PR writes only its own scope. [page](https://vznjs.github.io/vx/features/ref-cache-scope/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
146
+ - **Cache scope from the ref** (`github({ cacheScope })`) — main writes the default keys; a PR writes only its own scope. [page](https://vznjs.github.io/vx/features/ref-cache-scope/) · post: [The cache on your terms](https://vznjs.github.io/vx/blog/the-cache-on-your-terms/)
144
147
 
145
148
  ## Adoption
146
149
 
147
- - **Start in a minute** (`vx init`, `--dry`, `--format json`, `--force`, `--mjs`) — write `vx.workspace.ts` and one `vx.config.ts` per package; `--dry --format json` hands an agent the plan as data. [page](https://vznjs.github.io/vx/features/quickstart/) · post: [Hello, vx](https://vznjs.github.io/vx/blog/hello-vx/)
150
+ - **Start in a minute** (`vx init`, `--dry`, `--format json`, `--force`, `--mjs`) — write `.vx/workspace.ts`, one `vx.config.ts` per package (through `@vzn/vx-migrate`; with no `package.json`, one empty root project) and `.vx/projects.lock.json`; `--dry --format json` hands an agent the plan as data. [page](https://vznjs.github.io/vx/features/quickstart/) · post: [Hello, vx](https://vznjs.github.io/vx/blog/hello-vx/)
148
151
  - **Migrate from Turborepo or Nx** (`vx init`, `--native`, `--keep`, `@vzn/vx-migrate`) — native config, or keep `turbo()` / `nx()` as a start. [page](https://vznjs.github.io/vx/features/migrate/) · posts: [From Turborepo](https://vznjs.github.io/vx/blog/from-turborepo/), [From Nx](https://vznjs.github.io/vx/blog/from-nx/)
149
152
  - **Keep a Turbo or Nx remote cache** (`turboCache()`, `nxCache()`) — reuse the cache server you have. [page](https://vznjs.github.io/vx/features/keep-remote-cache/) · post: [From Nx: keep the graph, drop the platform](https://vznjs.github.io/vx/blog/from-nx/)
150
153
  - **One binary** — one file, nothing to install underneath. [page](https://vznjs.github.io/vx/features/one-binary/) · post: [One binary](https://vznjs.github.io/vx/blog/one-binary/)
151
154
  - **The playground** — vx's planner in the browser. [page](https://vznjs.github.io/vx/features/playground/) · post: [Try the planner in your browser](https://vznjs.github.io/vx/blog/the-playground/)
152
- - **Benchmarks you can re-run** (`@vzn/vx-bench`) — vx against Turborepo and Nx. [page](https://vznjs.github.io/vx/features/fastest/) · posts: [Benchmarks you can re-run](https://vznjs.github.io/vx/blog/honest-benchmarks/), [Why vx is fast](https://vznjs.github.io/vx/blog/why-vx-is-fast/)
155
+ - **Graph explorer** (`--dry=json`) — paste your own plan to see what each task waits on, what waits on it, and why. [page](https://vznjs.github.io/vx/guides/graph-explorer/)
156
+ - **Benchmarks you can re-run** (`@vzn/vx-bench`) — vx against Turborepo and Nx. [page](https://vznjs.github.io/vx/features/benchmarks/) · posts: [Benchmarks you can re-run](https://vznjs.github.io/vx/blog/honest-benchmarks/), [Why vx is fast](https://vznjs.github.io/vx/blog/why-vx-is-fast/)
153
157
  - **npm pre/post scripts** — `pre<x>` and `post<x>` hooks fold into `x`'s command when `vx init` maps scripts. [page](https://vznjs.github.io/vx/features/npm-hooks/) · post: [From npm scripts or Vite Task](https://vznjs.github.io/vx/blog/from-scripts-and-vite-task/)
154
- - **Vite Task adoption** (`bunx @vzn/vx-migrate`) — writes configs from vite-plus `run.tasks` as well as Turbo and Nx. [page](https://vznjs.github.io/vx/features/vite-task-adoption/) · post: [From npm scripts or Vite Task](https://vznjs.github.io/vx/blog/from-scripts-and-vite-task/)
158
+ - **Vite Task adoption** (`bunx @vzn/vx-migrate`) — writes configs from vite-plus `run.tasks` as well as Turbo, Nx and `package.json` scripts (`--from scripts` picks them beside the others). [page](https://vznjs.github.io/vx/features/vite-task-adoption/) · post: [From npm scripts or Vite Task](https://vznjs.github.io/vx/blog/from-scripts-and-vite-task/)
155
159
  - **Nx executors as one process** (`nx-exec`) — any Nx executor runs as one vx task with its Nx env set. [page](https://vznjs.github.io/vx/features/nx-exec/) · post: [From Nx: keep the graph, drop the platform](https://vznjs.github.io/vx/blog/from-nx/)
156
160
  - **Programmatic API** (`run`, `planRun`) — run or plan from your own scripts via `@vzn/vx`. [page](https://vznjs.github.io/vx/features/programmatic-api/) · post: [Extend vx in an afternoon](https://vznjs.github.io/vx/blog/extend-vx/)
157
161
 
158
162
  ## Plugins
159
163
 
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/)
164
+ - **A pipeline with seams** (`plugins`, `definePlugin`; stages `config` `discover` `project` `graph` `key` `fingerprint` `schedule` `admit` `executor` `cache` `telemetry` `output` `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/)
165
+ - **A project is a directory with a vx.config** (`name`) — in any language; named by its config's `name`, else a plugin's, else its path (`//` for the root); two of one name are refused. [page](https://vznjs.github.io/vx/features/vx-lock/) · post: [vx lock: freezing what the key sees](https://vznjs.github.io/vx/blog/lock-and-frozen/)
166
+ - **Plugin projects and edges** (`discover`, `DiscoveredProject`) — a plugin adds and names projects and gives them edges as directories: `deps` orders `^task` and reaches `--filter` and `--affected`; `reach` only reaches. [page](https://vznjs.github.io/vx/features/plugins/)
161
167
  - **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/)
162
168
  - **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/)
163
169
  - **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/)
164
170
  - **OpenTelemetry** (`@vzn/vx-otel`) — each run exported to your OpenTelemetry backend, never breaking it. [page](https://vznjs.github.io/vx/features/opentelemetry/) · post: [Observability that cannot break a run](https://vznjs.github.io/vx/blog/telemetry-never-breaks-a-run/)
165
- - **vx mcp** (`@vzn/vx-mcp`, `vx mcp`) — cache stats, run history, `vx why`'s full answer, a run tool (`mcp({ run })` limits it to named tasks or turns it off), a plan tool, a task's resolved config, the lock audit, cache pruning, the `vx init` plan, an offline docs search and a task list narrowed by `--filter` / `--affected` for coding agents. [page](https://vznjs.github.io/vx/features/mcp/) · post: [Give your coding agent the build's memory](https://vznjs.github.io/vx/blog/agents-and-mcp/)
166
- - **vx prune** (`@vzn/vx-lockfile`, `vx prune`, `--production`) — copy projects and their deps, lockfile pruned, for a Docker build; `--production` leaves out what only dev dependencies need. [page](https://vznjs.github.io/vx/features/vx-prune/) · post: [Ship one app, not the whole monorepo](https://vznjs.github.io/vx/blog/vx-prune/), [A runtime image without the dev tools](https://vznjs.github.io/vx/blog/prune-production/)
167
171
  - **vx history** (`@vzn/vx-schedule-history`, `vx history`) — what the scheduler learned per task. [page](https://vznjs.github.io/vx/features/vx-history/) · post: [A scheduler that learns from your runs](https://vznjs.github.io/vx/blog/a-scheduler-that-learns/)
168
172
  - **Setup and teardown hooks** (`setup`, `teardown`) — plugin code around the run, bounded by a timeout. [page](https://vznjs.github.io/vx/features/setup-teardown/) · post: [Extend vx in an afternoon](https://vznjs.github.io/vx/blog/extend-vx/)
169
173
  - **REAPI TLS, mTLS and headers** — connect to hosted servers such as BuildBuddy the way Bazel does. [page](https://vznjs.github.io/vx/features/reapi-tls/) · post: [A remote server you can trust in production](https://vznjs.github.io/vx/blog/reapi-in-production/)