@vzn/vx 0.0.635 → 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 +47 -19
  2. package/docs/caching.md +222 -210
  3. package/docs/cli.md +699 -664
  4. package/docs/execution.md +67 -75
  5. package/docs/features.md +44 -40
  6. package/docs/patterns.md +31 -32
  7. package/docs/schema.md +418 -202
  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 +13 -4
  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 -61
  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 +123 -144
  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 +191 -71
  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 +25 -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 +192 -70
  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 +43 -28
  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 +114 -73
  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 +127 -88
  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 +251 -241
  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 +22 -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/security.md CHANGED
@@ -8,12 +8,31 @@ vulnerability), never in a public issue.
8
8
 
9
9
  ## What vx trusts
10
10
 
11
- - **Your configs.** `vx.config.ts` and `vx.workspace.ts` are programs vx
11
+ - **Your configs.** `vx.config.ts` and `.vx/workspace.ts` are programs vx
12
12
  runs in its own process. A config can do anything you can: vx
13
- validates what it returns, not what it does. What a config imports
13
+ validates what it returns, not what it does. The workspace root is
14
+ the nearest `.vx/workspace.*`, and vx never looks for it above the git
15
+ top-level, so a file planted in a shared directory such as `/tmp`
16
+ cannot claim your checkout and run as you. Outside git, one another
17
+ user owns is refused, root aside, judged by the file itself (a link,
18
+ not what it names) and its `.vx` (`tests/workspace-root-file.test.ts`).
19
+ No task output reaches `.vx` or a `vx.lock.json`: a clean never deletes
20
+ them and a hit never restores them (`tests/output-never-dotvx.test.ts`).
21
+ What a config imports
14
22
  must already be installed: vx runs Bun with `--no-install`, so an
15
23
  import no `node_modules` provides is refused, never fetched from the
16
24
  registry (L-22).
25
+ - **Your projects lock.** A run executes `.vx/projects.lock.json` as
26
+ written and never re-evaluates or re-validates it, so the lock is code
27
+ too. It is minified and marked generated, which collapses it in a
28
+ pull request's diff: a change to it alone changes what runs while every
29
+ config and `vx.lock.json` reads the same. Run `vx lock --check` in CI
30
+ before anything else; it fails on any entry that differs from what its
31
+ config evaluates to and on one with no config (`tests/lock.test.ts`).
32
+ - **Your project's own vx.** A global vx hands every call to the
33
+ workspace root's `node_modules/.bin/vx` (cli.md § Which vx runs), so even
34
+ `vx --version` in a repo you do not trust runs that repo's code. One
35
+ another user owns is never run.
17
36
  - **Your plugins.** A plugin is code you imported. It runs in vx's
18
37
  process with a config's reach; vx checks the shape of what it hands
19
38
  back and isolates a telemetry plugin's failures, nothing more.
@@ -26,7 +45,7 @@ vulnerability), never in a public issue.
26
45
  choosing is trusted as a writer, as it is in every action cache. Use a
27
46
  store only your builds write to, or `turboCache()`'s signature key,
28
47
  which refuses an artifact a keyholder did not sign. A job you do not
29
- trust (a fork's pull request) reads at most: `--cache=local:rw,remote:r`,
48
+ trust (a fork's pull request) reads at most: `--cache-policy=local:rw,remote:r`,
30
49
  or the tools' own `TURBO_REMOTE_CACHE_READ_ONLY`, `TURBO_CACHE` and
31
50
  `NX_SKIP_REMOTE_CACHE`, which the cache plugins honour (L-34).
32
51
 
@@ -35,14 +54,20 @@ vulnerability), never in a public issue.
35
54
  A pull request that can write the cache the default branch reads can
36
55
  plant bytes main later replays (CREEP, CVE-2025-36852). The boundary is
37
56
  the credential, enforced by the cache server: the default branch's job
38
- holds a token that writes the trusted keys, and a pull request's job
57
+ holds a token that writes the default keys, and a pull request's job
39
58
  holds one that cannot (read-only, or confined by the server to its own
40
59
  namespace), or none.
41
60
 
42
- `cacheScope` is the client half. `'pr-123'` reads its own keys,
43
- then the trusted ones, and writes only its own, which `github()` sets on
44
- Actions (`pr-<n>`, `ref-<name>`; the default branch stays trusted)
45
- and `VX_CACHE_SCOPE` sets on any CI.
61
+ `cache.scope` is the client half. `'pr-123'` reads the default keys,
62
+ then its own, and writes only its own, which `github()` sets on
63
+ Actions (`pr-<n>`, `ref-<name>`; the default branch sets none)
64
+ and `VX_CACHE_SCOPE` or `--cache-scope` sets on any CI. Whether a run
65
+ writes is `cache.policy`'s call: off CI the default reads the remote and
66
+ writes nothing, since a key holds only declared inputs and a laptop's
67
+ undeclared tool or file would replay on CI. A laptop that should never
68
+ write gets a read-only token too. The local cache follows the same
69
+ key rule, so on one disk a PR's entries sit under its scope's keys and
70
+ a persistent runner never serves them to main.
46
71
  It keeps honest runs apart and stops a non-hermetic PR build from
47
72
  landing where main reads; it is not a boundary, since the PR's own
48
73
  config decides it and any run holding a write credential can write any
@@ -58,7 +83,7 @@ bazel-remote --dir /data --max_size 50 --grpc_address 0.0.0.0:9092 \
58
83
  ```
59
84
 
60
85
  ```ts
61
- // vx.workspace.ts: the header only where the secret is set
86
+ // .vx/workspace.ts: the header only where the secret is set
62
87
  reapi({
63
88
  endpoint: 'grpcs://cache.example.com:9092',
64
89
  headers: process.env.CACHE_AUTH ? { authorization: `Basic ${process.env.CACHE_AUTH}` } : {},
@@ -114,7 +139,8 @@ the local store:
114
139
 
115
140
  Remote execution (`@vzn/vx-reapi`) holds each blob to its digest, and
116
141
  writes only the server's outputs a declared glob names, inside the
117
- workspace, through no link that leads out of it.
142
+ workspace, through no link that leads out of it, and through no link
143
+ into a place no declared glob names.
118
144
 
119
145
  ## What the sandbox stops
120
146
 
@@ -130,22 +156,45 @@ A task with `exec.sandbox` runs where only what it declares exists:
130
156
  - its temp directory, port-bridge socket and trace log are its own
131
157
  (mode 0700), unreachable from another task and another local user;
132
158
  - the host's credential stores (`~/.ssh`, `~/.gnupg`, `~/.aws`,
133
- `~/.npmrc`, `~/.netrc` and the like; the list is in the schema's
134
- sandbox section) are unreadable unless `allow.read` names one (L-41);
135
- - vx's own cache directory, wherever `cacheDir` puts it in the
136
- workspace, is a wall like `.vx`: a broad grant stops at it, and a write
159
+ `~/.npmrc`, `~/.netrc`, the token files of deploy and agent CLIs such
160
+ as Vercel, Wrangler, Fly or Claude, and the like; the list is in the
161
+ schema's sandbox section) are unreadable unless `allow.read` names one
162
+ (L-41), cannot be hard-linked into an output for vx to pack, and a denied read's `fix:` hint never suggests granting one
163
+ (`tests/sandbox-credential-stores.unsafe.test.ts`, `tests/sandbox-fix.test.ts`);
164
+ - vx's own cache directory, wherever `cache.dir` puts it in the
165
+ workspace, is a wall like `.vx/cache`: a broad grant stops at it, and a write
137
166
  grant that would bind it is refused (L-24);
138
167
  - a symlinked output must resolve inside its project: vx packs outputs
139
168
  outside the sandbox, and a link to another project's file would have
140
169
  carried that file into the cache (L-23). An output directory linked out
141
- of the project refuses the task by name (X-88).
170
+ of the project refuses the task by name (X-88). Inside the project, a
171
+ link must name a file the task could read (a read or write grant, its
172
+ `node_modules`) or one of its own outputs, so `dist/x -> ../.env` under
173
+ `read: ['src/']` is refused, and Linux judges making the link by the
174
+ link, never its target (`tests/sandbox-output-link.unsafe.test.ts`).
175
+ - vx's own environment, which holds the variables a task is not given
176
+ (a token, a key), is unreadable: the task cannot see vx's
177
+ `/proc/<pid>/environ` on Linux or read it through `ps eww` on macOS
178
+ (`tests/sandbox-parent-env.unsafe.test.ts`).
142
179
 
143
180
  An undeclared touch of the task's own files fails the task, and a failed
144
181
  task is never cached. See [Sandboxing tasks](https://vznjs.github.io/vx/guides/sandboxing/).
145
182
 
183
+ The sandbox costs each task it runs; a cache hit runs nothing and costs
184
+ nothing. `tests/sandbox-parent-env.unsafe.test.ts` prints the cost of one
185
+ `true` task, min of 20, on every CI run. On GitHub-hosted runners on
186
+ 2026-10-13 it printed +269 ms on Linux and +97 ms on macOS.
187
+
146
188
  ## What it does not stop
147
189
 
148
- - **A task with no `exec.sandbox`.** It runs with your permissions.
190
+ - **A task with no `exec.sandbox`.** It runs with your permissions:
191
+ it reads anything you can. vx hides its own environment from it on
192
+ every command: on Linux vx makes itself non-dumpable (about 0.8 ms), on
193
+ macOS it zeroes the values `ps eww` reads, every variable but the
194
+ essentials every task gets (about 0.7 ms). It starts its helpers with `PATH`
195
+ alone and each `git` with only what git reads
196
+ (`tests/hide-env.unsafe.test.ts`, `tests/git-env.test.ts`). That hides
197
+ the environment only, not files.
149
198
  - **Reads outside the workspace root.** `~/.cache`, `/etc` and the like
150
199
  stay readable (tools need them) and fold into no key. The credential
151
200
  stores under home (`~/.ssh`, `~/.aws`, `~/.npmrc` and the like) are
@@ -156,24 +205,6 @@ task is never cached. See [Sandboxing tasks](https://vznjs.github.io/vx/guides/s
156
205
  `weakerWhenNested` in a container, a task that itself sandboxes on
157
206
  macOS. `vx info` says what the host supports.
158
207
 
159
- ## Agents (`vx mcp`)
160
-
161
- An agent connected to `vx mcp` can do what `vx run` can do, nothing more.
162
- Every tool but `runTasks` only reads. `runTasks` runs `vx run --format json`
163
- as a child process: the agent names tasks and sets `all`, `force`,
164
- `filter` and `affected`, and nothing else. A task name that starts with
165
- `-` is refused, a filter goes as one `--filter=` value, and a ref reaches
166
- git after `--end-of-options`, so no argument becomes a flag. The commands
167
- that run are the ones your configs declare, from the workspace root, with
168
- the environment `vx mcp` started with, filtered by each task's `exec.env`
169
- and held by its `exec.sandbox`. `force` costs a rerun, not a wrong entry:
170
- the key stays the same. What reaches the agent (summaries, refusals,
171
- `getFailures` output) is masked as described under Secrets.
172
- `mcp({ run: false })` takes `runTasks` away, and `mcp({ run: ['test'] })`
173
- limits it to those task names. An agent that
174
- can edit `vx.config.ts` can run anything, as you can, because configs are
175
- code vx trusts.
176
-
177
208
  ## Secrets
178
209
 
179
210
  The value, of 6 characters or more, of a variable whose name holds
@@ -183,7 +214,7 @@ task lists in `exec.env.secret`, and the password of a URL
183
214
  (`scheme://user:<password>@`) in any variable's value, is printed as `***` wherever vx shows, stores or
184
215
  exports it: task output, the stdout a hit replays, commands, an
185
216
  executor's error, telemetry,
186
- `vx show` and `vx mcp`'s `listTasks` (L-26). Values in the run history are digests under a per-store
217
+ `vx show` (L-26). Values in the run history are digests under a per-store
187
218
  salt, and what follows `--` on the command line reaches telemetry as a
188
219
  count, not a quote, a secret before it masked (L-44). A remote executor (`@vzn/vx-reapi`) receives a
189
220
  task's `exec.env.define` values and the `cache.inputs.env` values its
@@ -204,10 +235,16 @@ secret values: a name-based match would refuse public `*_KEY` values too.
204
235
 
205
236
  Release binaries carry a build-provenance attestation
206
237
  (`gh attestation verify vx-<target> --repo vznjs/vx`); `vx upgrade`
207
- checks each download's SHA-256 before it replaces anything and stages it in a fresh directory, never a name another user could plant a link at, which proves
208
- the bytes arrived intact, not who built them: the digest comes from the
209
- release that serves the asset, and no signature is checked (owner,
210
- 2026-10-03: no release signing key for now); npm packages
238
+ installs a binary only after npm's registry signature over the platform
239
+ package's `name@version:integrity` verifies against npm's key, pinned
240
+ in vx, and the tarball matches that integrity: the host that serves the
241
+ bytes cannot make the signature, so a swapped tarball is refused before
242
+ anything is written, and a key id vx does not pin is refused with a hint
243
+ that the installer needs updating (owner, 2026-10-13; pnpm's installer
244
+ checks the same). The new binary is staged in a fresh directory, never a
245
+ name another user could plant a link at. A vx a package manager
246
+ installed is upgraded by that manager, which checks its own integrity.
247
+ npm packages
211
248
  publish with provenance; every third-party action in CI runs from a
212
249
  full commit SHA; and a workflow hands an event's or a dispatcher's value
213
250
  to a script through `env`, never pasted in (a release tag is checked as a
package/launcher.sh CHANGED
@@ -29,6 +29,49 @@ base=${here##*/}
29
29
  scope=${here%/*}
30
30
  scope=${scope##*/}
31
31
 
32
+ # A global vx hands off to the project's own (owner, 2026-10-13): the
33
+ # workspace root's node_modules/<scope>/<name>, through its .bin entry. The
34
+ # root is the first directory holding a .vx/workspace.* from the working
35
+ # directory up to the git top-level (handoff.ts workspaceFile, the same
36
+ # rule). One hop (VX_HANDED_OFF, which also names this launcher), never to
37
+ # this package itself (`-ef`: same inode, so a pnpm symlink counts as
38
+ # itself). Shell tests and builtins only, no fork per level.
39
+ if [ -z "${VX_HANDED_OFF-}" ]; then
40
+ d=$PWD
41
+ while :; do
42
+ root=
43
+ for ext in ts mts js mjs cts cjs; do
44
+ if [ -f "$d/.vx/workspace.$ext" ]; then
45
+ root=${d:-/}
46
+ break
47
+ fi
48
+ done
49
+ if [ -n "$root" ]; then
50
+ bin=$d/node_modules/.bin/$base
51
+ # -O: a vx another user planted would run as you.
52
+ if [ -d "$d/node_modules/$scope/$base" ] && [ ! "$d/node_modules/$scope/$base" -ef "$here" ] && [ -x "$bin" ] && [ -O "$bin" ]; then
53
+ VX_HANDED_OFF=$self
54
+ export VX_HANDED_OFF
55
+ exec "$bin" "$@"
56
+ fi
57
+ break
58
+ fi
59
+ [ -z "$d" ] && break
60
+ # A submodule's .git file points into its superproject's .git/modules:
61
+ # the walk goes on to the superproject.
62
+ if [ -d "$d/.git" ]; then
63
+ break
64
+ elif [ -f "$d/.git" ]; then
65
+ read -r gitdir <"$d/.git" || :
66
+ case $gitdir in
67
+ *.git/modules/*) ;;
68
+ *) break ;;
69
+ esac
70
+ fi
71
+ d=${d%/*}
72
+ done
73
+ fi
74
+
32
75
  key=
33
76
  loader=
34
77
  case $(uname -sm) in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.635",
3
+ "version": "0.0.637",
4
4
  "description": "Task orchestration you own: correct, sandboxed, extensible, with a content-addressed build cache.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.635",
25
- "@vzn/vx-linux-arm64": "0.0.635",
26
- "@vzn/vx-darwin-x64": "0.0.635",
27
- "@vzn/vx-darwin-arm64": "0.0.635"
24
+ "@vzn/vx-linux-x64": "0.0.637",
25
+ "@vzn/vx-linux-arm64": "0.0.637",
26
+ "@vzn/vx-darwin-x64": "0.0.637",
27
+ "@vzn/vx-darwin-arm64": "0.0.637"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -1,24 +1,46 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "title": "vx cache prune --format json",
4
- "description": "What the policy evicted and the orphan sweep reaped; with dryRun, what they would.",
5
- "type": "object",
6
- "additionalProperties": false,
7
- "required": ["dryRun", "evicted", "bytesFreed", "orphans", "orphanBytes"],
8
- "properties": {
9
- "dryRun": { "type": "boolean" },
10
- "evicted": {
11
- "type": "integer",
12
- "description": "index entries evicted by the age / size policy"
3
+ "title": "vx cache clean --format json",
4
+ "description": "With a bound (prune): what the policy evicted and the orphan sweep reaped. With no bound (clear): the directories it deleted. With dryRun, what they would.",
5
+ "oneOf": [{ "$ref": "#/$defs/prune" }, { "$ref": "#/$defs/clear" }],
6
+ "$defs": {
7
+ "prune": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "required": ["dryRun", "evicted", "bytesFreed", "orphans", "orphanBytes"],
11
+ "properties": {
12
+ "dryRun": { "type": "boolean" },
13
+ "evicted": {
14
+ "type": "integer",
15
+ "description": "index entries evicted by the age / size policy"
16
+ },
17
+ "bytesFreed": {
18
+ "type": "integer",
19
+ "description": "bytes those entries occupied, per the index"
20
+ },
21
+ "orphans": {
22
+ "type": "integer",
23
+ "description": "artifacts and temp files with no index row, reaped"
24
+ },
25
+ "orphanBytes": { "type": "integer" }
26
+ }
13
27
  },
14
- "bytesFreed": {
15
- "type": "integer",
16
- "description": "bytes those entries occupied, per the index"
17
- },
18
- "orphans": {
19
- "type": "integer",
20
- "description": "artifacts and temp files with no index row, reaped"
21
- },
22
- "orphanBytes": { "type": "integer" }
28
+ "clear": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["dryRun", "cleared", "bytesFreed"],
32
+ "properties": {
33
+ "dryRun": { "type": "boolean" },
34
+ "cleared": {
35
+ "type": "array",
36
+ "items": { "type": "string" },
37
+ "description": "the cache directory and the store deleted: workspace-relative inside it, else absolute"
38
+ },
39
+ "bytesFreed": {
40
+ "type": "integer",
41
+ "description": "bytes under those directories"
42
+ }
43
+ }
44
+ }
23
45
  }
24
46
  }
package/schemas/info.json CHANGED
@@ -5,6 +5,8 @@
5
5
  "additionalProperties": false,
6
6
  "required": [
7
7
  "vx",
8
+ "vxPath",
9
+ "handedOffFrom",
8
10
  "bun",
9
11
  "bunSupported",
10
12
  "git",
@@ -12,16 +14,18 @@
12
14
  "workspaceRoot",
13
15
  "projects",
14
16
  "tasks",
15
- "configErrors",
16
17
  "plugins",
17
18
  "workers",
18
19
  "memory",
19
20
  "cacheDir",
21
+ "cacheDirBytes",
20
22
  "cacheStore",
23
+ "cacheStoreBytes",
21
24
  "cacheVersion",
22
25
  "schemaVersion",
23
26
  "cacheEntries",
24
27
  "cacheBytes",
28
+ "settings",
25
29
  "cacheRetention",
26
30
  "orphans",
27
31
  "runs24h",
@@ -33,6 +37,8 @@
33
37
  ],
34
38
  "properties": {
35
39
  "vx": { "type": "string" },
40
+ "vxPath": { "type": "string" },
41
+ "handedOffFrom": { "type": ["string", "null"] },
36
42
  "bun": { "type": "string" },
37
43
  "bunSupported": { "type": "boolean" },
38
44
  "git": { "type": ["string", "null"] },
@@ -45,15 +51,6 @@
45
51
  "workspaceRoot": { "type": "string" },
46
52
  "projects": { "type": "integer" },
47
53
  "tasks": { "type": "integer" },
48
- "configErrors": {
49
- "type": "array",
50
- "items": {
51
- "type": "object",
52
- "additionalProperties": false,
53
- "required": ["path", "message"],
54
- "properties": { "path": { "type": "string" }, "message": { "type": "string" } }
55
- }
56
- },
57
54
  "plugins": {
58
55
  "type": "array",
59
56
  "items": {
@@ -88,14 +85,29 @@
88
85
  }
89
86
  },
90
87
  "cacheDir": { "type": "string" },
88
+ "cacheDirBytes": { "type": "integer" },
91
89
  "cacheStore": { "type": ["string", "null"] },
90
+ "cacheStoreBytes": { "type": ["integer", "null"] },
92
91
  "cacheVersion": { "type": "string" },
93
92
  "schemaVersion": { "type": "string" },
94
93
  "cacheEntries": { "type": "integer" },
95
94
  "cacheBytes": { "type": "integer" },
95
+ "settings": {
96
+ "type": "object",
97
+ "description": "every setting (docs/schema.md § Settings) by path, as a run with no flags takes it: its variable as written, else .vx/workspace's value, else null with source default",
98
+ "additionalProperties": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "required": ["value", "source"],
102
+ "properties": {
103
+ "value": { "type": ["string", "number", "boolean", "null"] },
104
+ "source": { "enum": ["env", "workspace", "default"] }
105
+ }
106
+ }
107
+ },
96
108
  "cacheRetention": {
97
109
  "type": ["object", "null"],
98
- "description": "the workspace's cacheRetention, maxSize also in bytes; null when none is declared",
110
+ "description": "cache.retention from its variables, else the workspace, maxSize also in bytes; null when none is declared",
99
111
  "additionalProperties": false,
100
112
  "properties": {
101
113
  "olderThan": { "type": "string" },
package/schemas/last.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "vx last --format json",
4
- "description": "`vx last [RUNID] [--failed]` prints a run; `--list` prints an array of invocations, newest first; `--log <task>` prints that task's output.",
4
+ "description": "`vx last [RUNID] [--failed]` prints a run; `--list` prints an array of invocations, newest first; `--log <task>` prints that task's output; `--stats` prints per-task history numbers.",
5
5
  "oneOf": [
6
6
  {
7
7
  "type": "object",
@@ -13,9 +13,55 @@
13
13
  }
14
14
  },
15
15
  { "type": "array", "items": { "$ref": "#/$defs/invocation" } },
16
- { "$ref": "#/$defs/log" }
16
+ { "$ref": "#/$defs/log" },
17
+ { "$ref": "#/$defs/stats" }
17
18
  ],
18
19
  "$defs": {
20
+ "stats": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["window", "branch", "stats"],
24
+ "properties": {
25
+ "window": { "type": "integer", "description": "the newest runs the numbers cover" },
26
+ "branch": {
27
+ "type": ["string", "null"],
28
+ "description": "the branch p50 forecasts from: --branch, VX_RUN_BRANCH, git's, then CI's"
29
+ },
30
+ "stats": { "type": "array", "items": { "$ref": "#/$defs/taskStats" } }
31
+ }
32
+ },
33
+ "taskStats": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "required": [
37
+ "taskId",
38
+ "runs",
39
+ "p50DurationMs",
40
+ "p99DurationMs",
41
+ "successRate",
42
+ "hitRate",
43
+ "failureMode",
44
+ "maxPeakRssBytes",
45
+ "maxCpuParallelism"
46
+ ],
47
+ "properties": {
48
+ "taskId": { "type": "string" },
49
+ "runs": { "type": "integer", "description": "executed runs in the window, skips aside" },
50
+ "p50DurationMs": {
51
+ "type": ["number", "null"],
52
+ "description": "the forecast a run shows: median of the last 5 timings on branch, else on any branch; null when none"
53
+ },
54
+ "p99DurationMs": { "type": ["number", "null"] },
55
+ "successRate": { "type": "number", "description": "0 to 1" },
56
+ "hitRate": { "type": "number", "description": "0 to 1" },
57
+ "failureMode": { "enum": ["stable", "flaky-recoverable", "flaky-fatal"] },
58
+ "maxPeakRssBytes": { "type": ["integer", "null"] },
59
+ "maxCpuParallelism": {
60
+ "type": ["number", "null"],
61
+ "description": "cpu time over wall time; 2.0 is two cores busy"
62
+ }
63
+ }
64
+ },
19
65
  "log": {
20
66
  "type": "object",
21
67
  "additionalProperties": false,
package/schemas/lock.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "audited": { "type": "integer", "description": "projects with a vx.config, each audited" },
11
11
  "notAudited": {
12
12
  "type": "integer",
13
- "description": "projects without a vx.config: nothing to freeze, never audited"
13
+ "description": "projects with tasks and no vx.config: nothing to freeze, never audited"
14
14
  },
15
15
  "problems": {
16
16
  "type": "array",
package/schemas/plan.json CHANGED
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "download": {
37
37
  "const": "deferred",
38
- "description": "outputs would stay remote (--download)"
38
+ "description": "outputs would stay remote (--cache-download)"
39
39
  },
40
40
  "description": { "type": "string" },
41
41
  "affected": {
@@ -80,7 +80,7 @@
80
80
  },
81
81
  "downloadDowngrades": {
82
82
  "type": "array",
83
- "description": "producers --download wanted to defer but could not, and why",
83
+ "description": "producers --cache-download wanted to defer but could not, and why",
84
84
  "items": {
85
85
  "type": "object",
86
86
  "additionalProperties": false,
package/schemas/show.json CHANGED
@@ -32,9 +32,10 @@
32
32
  "dir": { "type": "string" },
33
33
  "config": {
34
34
  "type": ["object", "null"],
35
- "description": "null when the package has no vx config and no plugin gave it one",
35
+ "description": "null when the project has no vx config and no plugin gave it one",
36
36
  "additionalProperties": false,
37
37
  "properties": {
38
+ "name": { "type": "string" },
38
39
  "tags": { "$ref": "#/$defs/stringList" },
39
40
  "tasks": { "type": "object", "additionalProperties": { "$ref": "#/$defs/taskConfig" } }
40
41
  }
@@ -66,7 +67,7 @@
66
67
  "properties": {
67
68
  "command": { "type": "string" },
68
69
  "remote": { "enum": [true, false, "only"] },
69
- "timeout": { "type": "number", "description": "ms" },
70
+ "timeout": { "type": "string", "description": "a duration as written, like '30s'" },
70
71
  "retries": { "type": "integer" },
71
72
  "env": {
72
73
  "type": "object",
@@ -113,10 +114,18 @@
113
114
  "required": ["files"],
114
115
  "properties": {
115
116
  "files": { "$ref": "#/$defs/stringList" },
116
- "workspaceFiles": { "$ref": "#/$defs/stringList" }
117
+ "workspaceFiles": { "$ref": "#/$defs/stringList" },
118
+ "clean": { "type": "boolean" }
117
119
  }
118
120
  },
119
- "rewrittenInputs": { "enum": ["withhold", "compare"] }
121
+ "inputStability": {
122
+ "type": "object",
123
+ "additionalProperties": false,
124
+ "properties": {
125
+ "strategy": { "enum": ["immutable", "mutable", "unchecked"] },
126
+ "severity": { "enum": ["warn", "error"] }
127
+ }
128
+ }
120
129
  }
121
130
  }
122
131
  }
package/schemas/why.json CHANGED
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
9
- "required": ["taskId", "runId", "why", "diff", "roots"],
9
+ "required": ["taskId", "runId", "why", "diff", "roots", "writers"],
10
10
  "properties": {
11
11
  "taskId": { "type": "string" },
12
12
  "runId": { "type": "string" },
@@ -38,6 +38,20 @@
38
38
  }
39
39
  }
40
40
  }
41
+ },
42
+ "writers": {
43
+ "type": "array",
44
+ "description": "For each moved input file of the task or a root cause, the recorded tasks running when the file last changed: a task writing a file another key reads re-keys it every run.",
45
+ "items": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["taskId", "file", "writers"],
49
+ "properties": {
50
+ "taskId": { "type": "string" },
51
+ "file": { "type": "string" },
52
+ "writers": { "type": "array", "items": { "type": "string" } }
53
+ }
54
+ }
41
55
  }
42
56
  }
43
57
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vx
3
- description: Run, inspect and debug tasks in a vx monorepo (vx.config.ts, vx.workspace.ts). Use to build or test projects, find what a change affects, read why a task failed or re-ran, and query the cache as JSON.
3
+ description: Run, inspect and debug tasks in a vx monorepo (vx.config.ts, .vx/workspace.ts). Use to build or test projects, find what a change affects, read why a task failed or re-ran, and query the cache as JSON.
4
4
  ---
5
5
 
6
6
  # vx
@@ -59,22 +59,20 @@ files, fix, and re-run the same command: cached tasks that passed replay from th
59
59
  vx why app#build --format json # which input changed, down to the root cause
60
60
  ```
61
61
 
62
- ## Workspace health
62
+ ## Run history
63
63
 
64
64
  ```sh
65
- vx info --format json # vx and Bun versions, projects, config errors, plugins, cache
65
+ vx last --stats --format json # per task: p50 (the live estimate's), p99, pass and hit rate, failure mode, peak RSS
66
+ vx last --stats --branch main --format json # p50 from main's timings
67
+ vx last --stats app#build --format json # one task; a task name covers every project
68
+ vx last --list --format json # recent runs, newest first
66
69
  ```
67
70
 
68
- ## MCP
71
+ ## Workspace health
69
72
 
70
- When `@vzn/vx-mcp` is a plugin in `vx.workspace.ts`, `vx mcp` serves the
71
- same facts and runs tasks. Adoption does not add it: install
72
- `@vzn/vx-mcp` and put `mcp()` (from `'@vzn/vx-mcp'`) in `plugins`. Its tools: `listTasks`, `getCacheStats`,
73
- `getRunHistory`, `explainCacheKey`, `whyDidThisRerun`, `getFailures`, `getTaskLog`, `getConfig`, `checkLock`, `pruneCache`, `planInit`, `searchDocs`,
74
- `getWorkspaceInfo`, `runTasks`, `planTasks`. `runTasks` answers what
75
- `vx run <tasks> --format json` prints, `planTasks` what
76
- `vx run <tasks> --dry=json` prints, `pruneCache` evicts only with
77
- `dryRun: false`; the others only read.
73
+ ```sh
74
+ vx info --format json # vx and Bun versions, projects, config errors, plugins, cache
75
+ ```
78
76
 
79
77
  ## Rules
80
78
 
@@ -93,3 +91,9 @@ same facts and runs tasks. Adoption does not add it: install
93
91
  `https://raw.githubusercontent.com/vznjs/vx/main/packages/vx-docs/src/content/docs/<page>.md`:
94
92
  `quickstart`, `guides/migrate`, `guides/configure`, `guides/agents`,
95
93
  `guides/troubleshooting`, `guides/ci`, `guides/plugins`.
94
+
95
+ ## Install
96
+
97
+ `npx skills add vznjs/vx/packages/vx/skills/vx` installs this skill. To match the vx a workspace
98
+ has installed, copy it from there instead:
99
+ `cp node_modules/@vzn/vx/skills/vx/SKILL.md .claude/skills/vx/`.