@vzn/vx 0.0.18 → 0.0.22

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 (149) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -70
  3. package/package.json +10 -20
  4. package/src/bin.ts +64 -5
  5. package/src/cache/archive.ts +279 -28
  6. package/src/cache/cache.ts +952 -1584
  7. package/src/cache/chained-cache.ts +14 -10
  8. package/src/cache/config-evals.ts +112 -0
  9. package/src/cache/file-hashes.ts +290 -0
  10. package/src/cache/git-inputs.ts +966 -0
  11. package/src/cache/index.ts +23 -7
  12. package/src/cache/inputs.ts +553 -739
  13. package/src/cache/key-fold.ts +232 -0
  14. package/src/cache/layer.ts +674 -0
  15. package/src/cache/layered-cache.ts +211 -49
  16. package/src/cache/output-index.ts +323 -0
  17. package/src/cache/policy.ts +84 -0
  18. package/src/cache/run-history.ts +152 -0
  19. package/src/cache/zstd.ts +183 -0
  20. package/src/cli/cache.ts +88 -30
  21. package/src/cli/completions.ts +104 -0
  22. package/src/cli/core-alias.ts +25 -0
  23. package/src/cli/format.ts +4 -15
  24. package/src/cli/help.ts +76 -38
  25. package/src/cli/index.ts +82 -17
  26. package/src/cli/info.ts +162 -91
  27. package/src/cli/init.ts +87 -0
  28. package/src/cli/last.ts +130 -23
  29. package/src/cli/lock.ts +36 -18
  30. package/src/cli/plan-format.ts +18 -7
  31. package/src/cli/plugin-commands.ts +34 -12
  32. package/src/cli/run.ts +138 -313
  33. package/src/cli/select.ts +342 -0
  34. package/src/cli/show.ts +151 -71
  35. package/src/cli/upgrade.ts +136 -15
  36. package/src/cli/watch.ts +1175 -116
  37. package/src/cli/why.ts +51 -29
  38. package/src/cli/workspace-config.ts +138 -0
  39. package/src/config.ts +183 -126
  40. package/src/exec/env.ts +11 -0
  41. package/src/exec/executor.ts +62 -31
  42. package/src/exec/index.ts +16 -2
  43. package/src/exec/kill-tree.ts +281 -0
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +361 -77
  46. package/src/exec/sandbox-binds.ts +211 -0
  47. package/src/exec/sandbox-paths.ts +70 -0
  48. package/src/exec/sandbox-runtime.ts +1152 -415
  49. package/src/exec/sandbox-violations.ts +293 -0
  50. package/src/graph/dependency-spec.ts +14 -14
  51. package/src/graph/index.ts +6 -4
  52. package/src/graph/priorities.ts +130 -0
  53. package/src/graph/scheduler.ts +332 -329
  54. package/src/graph/task-graph.ts +552 -188
  55. package/src/index.ts +111 -102
  56. package/src/orchestrator/admission.ts +161 -0
  57. package/src/orchestrator/colors.ts +8 -3
  58. package/src/orchestrator/deferred-outputs.ts +25 -7
  59. package/src/orchestrator/doctor.ts +363 -0
  60. package/src/orchestrator/events.ts +111 -8
  61. package/src/orchestrator/excluded-keys.ts +155 -0
  62. package/src/orchestrator/execute-task.ts +450 -565
  63. package/src/orchestrator/failure-mode.ts +169 -28
  64. package/src/orchestrator/failure-recap.ts +121 -0
  65. package/src/orchestrator/fingerprint-watch.ts +89 -0
  66. package/src/orchestrator/framed-output.ts +121 -49
  67. package/src/orchestrator/history.ts +123 -104
  68. package/src/orchestrator/hit-restore.ts +285 -0
  69. package/src/orchestrator/index.ts +42 -5
  70. package/src/orchestrator/keyed-projects.ts +89 -0
  71. package/src/orchestrator/local-shortcircuit.ts +83 -23
  72. package/src/orchestrator/lockfile-claim.ts +401 -0
  73. package/src/orchestrator/logger.ts +128 -41
  74. package/src/orchestrator/metrics.ts +201 -72
  75. package/src/orchestrator/miss-save.ts +232 -0
  76. package/src/orchestrator/options.ts +63 -34
  77. package/src/orchestrator/persistent.ts +99 -0
  78. package/src/orchestrator/placement.ts +212 -0
  79. package/src/orchestrator/plan.ts +24 -11
  80. package/src/orchestrator/plugin-host.ts +200 -49
  81. package/src/orchestrator/plugin.ts +190 -14
  82. package/src/orchestrator/prepare.ts +168 -137
  83. package/src/orchestrator/projects.ts +270 -0
  84. package/src/orchestrator/remote-prefetch.ts +22 -2
  85. package/src/orchestrator/run-artifacts.ts +34 -3
  86. package/src/orchestrator/run-context.ts +20 -5
  87. package/src/orchestrator/run-lock.ts +349 -0
  88. package/src/orchestrator/run-records.ts +132 -0
  89. package/src/orchestrator/run-report.ts +6 -4
  90. package/src/orchestrator/run.ts +467 -611
  91. package/src/orchestrator/sandbox-request.ts +569 -0
  92. package/src/orchestrator/save-lane.ts +44 -0
  93. package/src/orchestrator/shell-verdict.ts +117 -0
  94. package/src/orchestrator/signals.ts +167 -0
  95. package/src/orchestrator/stable-keys.ts +259 -10
  96. package/src/orchestrator/status-line.ts +35 -2
  97. package/src/orchestrator/summary.ts +115 -25
  98. package/src/orchestrator/task-hash.ts +103 -21
  99. package/src/orchestrator/task-log-buffer.ts +15 -18
  100. package/src/orchestrator/telemetry-host.ts +27 -8
  101. package/src/orchestrator/telemetry.ts +71 -44
  102. package/src/orchestrator/upstream.ts +91 -45
  103. package/src/util/bun-version.ts +43 -0
  104. package/src/util/cgroup.ts +160 -0
  105. package/src/util/edit-distance.ts +49 -0
  106. package/src/util/errors.ts +86 -0
  107. package/src/util/hash.ts +10 -1
  108. package/src/util/index.ts +42 -5
  109. package/src/util/paths.ts +168 -5
  110. package/src/util/procfs.ts +20 -0
  111. package/src/util/settle.ts +23 -2
  112. package/src/util/size.ts +33 -0
  113. package/src/util/task-id.ts +20 -0
  114. package/src/util/timing.ts +11 -1
  115. package/src/util/verbs.ts +39 -0
  116. package/src/util/which.ts +48 -0
  117. package/src/workspace/affected.ts +368 -60
  118. package/src/workspace/config-cache.ts +271 -32
  119. package/src/workspace/config-eval.ts +60 -16
  120. package/src/workspace/config-imports.ts +131 -9
  121. package/src/workspace/config-schema.ts +1033 -0
  122. package/src/workspace/filter.ts +69 -17
  123. package/src/workspace/fingerprint.ts +59 -7
  124. package/src/workspace/index.ts +37 -3
  125. package/src/workspace/json-data.ts +90 -0
  126. package/src/workspace/load-reads.ts +32 -0
  127. package/src/workspace/lockfile.ts +41 -14
  128. package/src/workspace/migrate-scripts.ts +187 -0
  129. package/src/workspace/migration.ts +353 -0
  130. package/src/workspace/package-graph.ts +239 -31
  131. package/src/workspace/project-loader.ts +295 -815
  132. package/src/workspace/workspace.ts +266 -31
  133. package/plugins/local-cache/index.ts +0 -2
  134. package/plugins/local-executor/index.ts +0 -2
  135. package/plugins/schedule-history/index.ts +0 -2
  136. package/src/cache/cas-backend.ts +0 -115
  137. package/src/cache/digest.ts +0 -32
  138. package/src/cli/migrate-emit.ts +0 -21
  139. package/src/cli/migrate-nx.ts +0 -473
  140. package/src/cli/migrate-scripts.ts +0 -127
  141. package/src/cli/migrate-turbo.ts +0 -401
  142. package/src/cli/migrate.ts +0 -305
  143. package/src/cli/prune.ts +0 -291
  144. package/src/orchestrator/missing-plugin.ts +0 -9
  145. package/src/orchestrator/resources.ts +0 -45
  146. package/src/orchestrator/verify.ts +0 -174
  147. package/src/plugins/local-cache/index.ts +0 -11
  148. package/src/plugins/local-executor/index.ts +0 -45
  149. package/src/plugins/schedule-history/index.ts +0 -109
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 nxt contributors
3
+ Copyright (c) 2026 vx contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -3,15 +3,33 @@
3
3
  **The fastest way to build a monorepo.**
4
4
 
5
5
  vx runs your task graph, remembers every result, and never does the
6
- same work twice. Fully cached runs finish in milliseconds — 76 ms
7
- across 46 packages, 559 ms across a 1,090-package graph of 3,270 tasks,
8
- where Turborepo takes 760 ms and Nx 3.59 s on the identical workspace —
9
- and a cold build of that graph burns 34 s of CPU in vx, 73 s in Turborepo
10
- and 114 minutes in Nx.
11
- Measured, reproducible, on hardware you own
12
- ([benchmarks](docs/benchmarks.md)).
13
-
14
- One binary. No daemon. No Node. Nothing to babysit.
6
+ same work twice.
7
+
8
+ <!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
9
+
10
+ The runner adds seconds to a cold build where others add minutes. On a
11
+ 1,090-package graph of 3,270 tasks whose ideal schedule is 3m 38s,
12
+ vx finishes in 3m 46s (+0:08), Turborepo in 5m 13s (+1:35) and Nx in
13
+ 34m 44s (+31:06) — 8 ms of overhead per package against 88 ms and
14
+ 1,712 ms, so the graph can grow and the runner stays in seconds. The cold build
15
+ burns 35 s of CPU in vx, 73 s in Turborepo and 114 minutes in Nx; a
16
+ fully cached run replays the graph in 510 ms.
17
+ <!-- bench:end -->
18
+
19
+ On a real Turbo monorepo (`solidjs/solid`, its own `turbo.json`, no
20
+ config rewritten) vx replays a cached build in 51 ms where Turbo takes
21
+ 95, restores every output in 66 ms against 127, and builds cold in
22
+ 40.6 s against 45.5 s. Measured, reproducible, on hardware you own
23
+ ([benchmarks](packages/vx/docs/benchmarks.md)).
24
+
25
+ One binary. No daemon, no cloud, no account, no paywall. Sandboxed
26
+ tasks, a plugin at every stage, seconds of overhead at a thousand
27
+ packages.
28
+
29
+ vx does one thing — run and cache a task graph, correctly — and stops
30
+ there. Remote caches, remote execution, telemetry, AI agents, learned
31
+ scheduling and your own verbs are plugins on documented seams, not
32
+ features inside: what Nx would be if it weren't a product.
15
33
 
16
34
  📖 **[Documentation site →](https://vznjs.github.io/vx/)** — guides,
17
35
  architecture, caching, and the full CLI / config reference.
@@ -49,7 +67,7 @@ export default defineProject({
49
67
  vx init # scaffold vx.workspace.ts + a vx.config.ts per package from scripts
50
68
  vx run build # cwd project + its workspace deps
51
69
  vx run test --all # every project that declares `test`
52
- vx run ci --affected # only what changed since origin/HEAD
70
+ vx run ci --affected # changed since origin/HEAD, and what depends on it
53
71
  vx watch lint # re-run on file changes
54
72
  vx run build --dry # show the plan, don't execute
55
73
  ```
@@ -78,16 +96,11 @@ skip extraction entirely when the tree already matches. In-process
78
96
  tar (no subprocess on the hot path). Atomic artifact publishes.
79
97
  Single-transaction metadata writes. Every optimization is recorded
80
98
  with the invariant that keeps it valid —
81
- [`docs/optimizations.md`](docs/optimizations.md) is the ledger, and
82
- [`bench/`](bench/) reproduces the numbers.
99
+ [`packages/vx/docs/optimizations.md`](packages/vx/docs/optimizations.md) is the ledger, and
100
+ [`packages/vx-bench/`](packages/vx-bench/) reproduces the numbers.
83
101
 
84
102
  ## Built for trust
85
103
 
86
- - **Provable cache correctness.** `vx run --verify` re-runs each
87
- cacheable task and byte-compares outputs (determinism);
88
- `--verify=inputs` sandboxes it against the declared inputs and fails
89
- loud naming any undeclared read (completeness). No other runner can
90
- prove a cache entry safe.
91
104
  - **Corruption can't go live.** A remote artifact is verified against
92
105
  its content digest and validated before it enters the store
93
106
  (zstd-bomb and oversize downloads refused); bad bytes degrade to a
@@ -97,7 +110,11 @@ with the invariant that keeps it valid —
97
110
  - **Readiness you can bound.** Persistent tasks gate downstream work
98
111
  on a `readyWhen` signal; `exec.timeout` bounds any task (with
99
112
  `--timeout` / workspace defaults), `exec.retries` + `--retry` absorb
100
- flakes — and retried-then-passed tasks are flagged flaky.
113
+ flakes.
114
+ - **Flaky tasks are found, not guessed.** The same inputs both passing
115
+ and failing is the definition, and vx holds every hash and outcome
116
+ locally: a run names them under its footer, `--summarize` types them
117
+ per task, `vx info` lists them. No service.
101
118
  - **Kernel-level sandboxing**, opt-in per task, that fails the build
102
119
  on violation instead of hiding it.
103
120
 
@@ -119,9 +136,11 @@ vx lock --check && vx run ci --frozen # CI: audit, then run EXACTLY that gra
119
136
  | `vx lock --check` | full graph | compares — catches env and import drift that byte hashes cannot |
120
137
 
121
138
  Env values read at lock time are frozen by design — cache keys become
122
- reproducible across machines. Bonus: `--frozen` runs skip config
123
- evaluation entirely (~120 ms back per 1,000 packages). No other
124
- runner has an equivalent.
139
+ reproducible across machines. Take it for that, not for speed: measured
140
+ on the 1,000-project bench, a plain warm run and a frozen one are a tie
141
+ (177 ms against 165, 2026-09-12), because the config-evaluation cache
142
+ already serves a pure config without evaluating it. No other runner has
143
+ an equivalent.
125
144
 
126
145
  ## Everything you need, nothing to configure twice
127
146
 
@@ -132,9 +151,9 @@ watch mode · `--dry` / `--graph` plans · persistent dev servers ·
132
151
  retries, timeouts, `--continue` modes · per-layer cache control
133
152
  (`--cache=local:r,remote:`) · `vx why` explains a re-run from the
134
153
  persisted input fingerprints · `vx last` replays a recorded run ·
135
- `vx prune` cuts a Docker-ready workspace subset · `vx info`,
154
+ `vx info`,
136
155
  `--summarize`, `--profile` Chrome traces, `--report` · `vx cache prune`
137
- with TTL and size caps · `vx migrate` from turbo.json or an Nx graph.
156
+ with TTL and size caps · `bunx @vzn/vx-migrate` from turbo.json or an Nx graph.
138
157
 
139
158
  ## A pipeline with seams — plugins decide what happens
140
159
 
@@ -155,37 +174,50 @@ declared in `vx.workspace.ts` hook each stage, Vite-style, on one
155
174
  | observe | `telemetry(ctx)` / `setup` | receive every run record, or the raw event bus |
156
175
  | cli | `commands` | add verbs to `vx` |
157
176
 
158
- Core applies **no plugin by default** — even its own local executor and
159
- cache are plugins your workspace declares, which is what makes "replace
160
- any part" real rather than promised. First-party plugins:
177
+ Core applies **no plugin by default** and names none. Running here and
178
+ caching here are its floor — the tail of every executor list and cache
179
+ chain — so a workspace with no `vx.workspace.ts` runs and caches, and a
180
+ plugin that declines a task hands it back to this machine. First-party
181
+ plugins:
161
182
  [`@vzn/vx-reapi`](packages/vx-reapi) (Bazel Remote Execution API —
162
183
  remote cache and remote execution against NativeLink, BuildBuddy,
163
184
  Buildbarn or bazel-remote), [`@vzn/vx-otel`](packages/vx-otel)
164
185
  (OpenTelemetry traces + metrics + logs, zero SDK deps),
165
186
  [`@vzn/vx-github`](packages/vx-github) (Actions job summary + Checks
166
187
  API), [`@vzn/vx-mcp`](packages/vx-mcp) (`vx mcp` — a read-only Model
167
- Context Protocol server for AI coding agents, no SDK), and
168
- `@vzn/vx/plugins/schedule-history` (order by learned critical path). Nothing distributed ships in this repo; the seams are how you
169
- build it.
188
+ Context Protocol server for AI coding agents, no SDK),
189
+ [`@vzn/vx-migrate`](packages/vx-migrate) (adoption in one package:
190
+ `turbo()` runs a Turbo repo under vx with nothing written, `bunx
191
+ @vzn/vx-migrate` writes configs from `turbo.json` or an Nx graph, and
192
+ `turboCache()` / `nxCache()` keep a remote cache speaking Turbo's
193
+ `/v8/artifacts` API or Nx's self-hosted spec — the wire is theirs, the
194
+ artifacts are vx's),
195
+ [`@vzn/vx-lockfile`](packages/vx-lockfile) (`pnpm()`, `bun()`, `npm()`,
196
+ `yarn()`: the lockfile keyed per project, so one install re-keys only the
197
+ projects it reaches, and `--affected` follows), and [`@vzn/vx-schedule-history`](packages/vx-schedule-history)
198
+ (order by learned critical path) and [`@vzn/vx-migrate`](packages/vx-migrate)
199
+ (`bunx @vzn/vx-migrate`: turbo.json or an Nx graph → vx.config.ts, through
200
+ core's migration seam). Core ships no plugin and reads no other runner's
201
+ format; nothing
202
+ distributed ships in this repo; the seams are how you build it.
170
203
 
171
204
  ## How it compares
172
205
 
173
- | | vx | Turborepo | Nx |
174
- | ------------------------- | --------------------------------------------------------------------- | ------------------------------ | ---------------- |
175
- | Fully cached, 100 pkgs¹ | **144 ms** | 279 ms | 583+ ms |
176
- | Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
177
- | Output ownership | **Strict** — wiped before exec AND restore | Additive (stale files survive) | Additive |
178
- | Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
179
- | Daemon required for speed | **No** | Optional | Yes |
180
- | Per-task sandbox | **Yes** — kernel-level, opt-in | No | No |
181
- | Provable cache safety | **Yes** — `--verify` (determinism) + `--verify=inputs` (completeness) | No | No |
182
- | Plugin API | **Yes** — executor / cache / telemetry seams | No | Yes (TS-tied) |
183
- | OTel CI/CD spans | **Yes** — `otel()` plugin, zero OTel-SDK deps | No | Paid |
184
- | Install | **Single binary** — npm or 1 curl line, no Node/Bun needed | npm + Node | npm + Node |
206
+ | | vx | Turborepo | Nx |
207
+ | ------------------------- | ---------------------------------------------------------------- | ------------------------------ | ---------------- |
208
+ | Fully cached, 100 pkgs¹ | **144 ms** | 279 ms | 583+ ms |
209
+ | Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
210
+ | Output ownership | **Strict** — wiped before exec AND restore | Additive (stale files survive) | Additive |
211
+ | Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
212
+ | Daemon required for speed | **No** | Optional | Yes |
213
+ | Per-task sandbox | **Yes** — kernel-level, opt-in | No | No |
214
+ | Plugin API | **Yes** — executor / cache / telemetry seams | No | Yes (TS-tied) |
215
+ | OTel CI/CD spans | **Yes** — `otel()` plugin, zero OTel-SDK deps | No | Paid |
216
+ | Install | **Single binary** — npm, or one release file; no Node/Bun needed | npm + Node | npm + Node |
185
217
 
186
218
  ¹ Wall-clock, direct binaries, same machine and workspace — full
187
219
  methodology and more scenarios in
188
- [`docs/benchmarks.md`](docs/benchmarks.md).
220
+ [`packages/vx/docs/benchmarks.md`](packages/vx/docs/benchmarks.md).
189
221
 
190
222
  ## Switching from another runner
191
223
 
@@ -233,7 +265,9 @@ Differences to know:
233
265
  - Persistent tasks: `persistent: { readyWhen: 'regex' }` (Turbo uses just `persistent: true`).
234
266
  - Remote caching is a plugin, not a built-in — connect one and every `vx run` reads through it.
235
267
 
236
- Side-by-side feature matrix + every known gap: [`docs/comparison.md`](./docs/comparison.md).
268
+ Side-by-side feature matrix + every known gap: [`packages/vx/docs/comparison.md`](packages/vx/docs/comparison.md).
269
+ What a Turbo or Nx user relies on, spelled in vx and pinned by a test:
270
+ [`packages/vx/docs/parity.md`](packages/vx/docs/parity.md).
237
271
 
238
272
  ## Architecture (one paragraph)
239
273
 
@@ -252,47 +286,50 @@ versioned telemetry contract (`TelemetryRecord` / `RunSummaryRecord`)
252
286
  them. Core never imports a plugin; the arrow only points plugin → core.
253
287
  Every module has a docs page; every interface is a swappable seam.
254
288
 
255
- Read [`docs/architecture.md`](./docs/architecture.md) for the module
256
- map; the design record lives under [`docs/design/`](./docs/design/).
289
+ Read [`packages/vx/docs/architecture.md`](packages/vx/docs/architecture.md) for the module
290
+ map; the design record lives under [`packages/vx/docs/design/`](packages/vx/docs/design/).
257
291
 
258
292
  ## Documentation
259
293
 
260
- Full technical docs live under [`docs/`](./docs/) and on the
294
+ Full technical docs live under [`packages/vx/docs/`](packages/vx/docs/) and on the
261
295
  [documentation site](https://vznjs.github.io/vx/):
262
296
 
263
- - [`docs/architecture.md`](./docs/architecture.md) — module map + data flow
264
- - [`docs/schema.md`](./docs/schema.md) — every config field
265
- - [`docs/caching.md`](./docs/caching.md) — cache-key derivation + invalidation table
266
- - [`docs/execution.md`](./docs/execution.md) — `vx run` lifecycle
267
- - [`docs/cli.md`](./docs/cli.md) — every flag
268
- - [`docs/comparison.md`](./docs/comparison.md) — Turbo / Nx / vite-task feature matrix
269
- - [`docs/modules/`](./docs/modules/) — one reference page per source module
297
+ - [`packages/vx/docs/architecture.md`](packages/vx/docs/architecture.md) — module map + data flow
298
+ - [`packages/vx/docs/schema.md`](packages/vx/docs/schema.md) — every config field
299
+ - [`packages/vx/docs/caching.md`](packages/vx/docs/caching.md) — cache-key derivation + invalidation table
300
+ - [`packages/vx/docs/execution.md`](packages/vx/docs/execution.md) — `vx run` lifecycle
301
+ - [`packages/vx/docs/cli.md`](packages/vx/docs/cli.md) — every flag
302
+ - [`packages/vx/docs/comparison.md`](packages/vx/docs/comparison.md) — Turbo / Nx / vite-task feature matrix
303
+ - [`packages/vx/docs/modules/`](packages/vx/docs/modules/) — one reference page per source module
270
304
 
271
- The design record lives under [`docs/design/`](./docs/design/); the
272
- maintainers' handoff is [`docs/STATUS.md`](./docs/STATUS.md).
305
+ The design record lives under [`packages/vx/docs/design/`](packages/vx/docs/design/); the
306
+ maintainers' handoff is [`packages/vx/docs/STATUS.md`](packages/vx/docs/STATUS.md).
273
307
 
274
308
  ## Status
275
309
 
276
310
  **Pre-alpha.** The schema is settling; we bump `CACHE_VERSION` rather
277
- than maintain back-compat. **~2,500 core tests plus the package suites; CI green on every commit**;
311
+ than maintain back-compat. What 1.0 will promise is in
312
+ [`versioning-1.0.md`](packages/vx/docs/design/versioning-1.0.md). **~3,000 core tests plus the package suites; CI green on every commit**;
278
313
  the project dogfoods itself (`vx run ci`). Published on npm:
279
314
  [`@vzn/vx`](https://www.npmjs.com/package/@vzn/vx) (a prebuilt standalone
280
315
  binary).
281
316
 
282
317
  Production readiness for the **core task runner**: the semantics are
283
- solid; it is dogfooded continuously. The main operational rough edge
284
- is Windows (unsupported).
285
-
286
- | Surface | Maturity | Notes |
287
- | ------------------------------------------------ | -------------------- | ---------------------------------------------------------------------------- |
288
- | Core task runner + caching | **production-ready** | dogfooded continuously; ~2,500 core tests + the package suites, green |
289
- | `vx run --verify` (provable cache correctness) | **shippable** | determinism + input-completeness proofs; CI-gate recipe in docs |
290
- | Plugin pipeline (9 hooks, `commands` included) | **shippable** | crash-isolated, re-validated; core's own executor + cache are plugins |
291
- | `vx init` / `vx migrate` (scripts, Turbo, Nx) | **shippable** | one config per package, TODOs where a source cannot say |
292
- | REAPI remote cache + execution (`@vzn/vx-reapi`) | **shippable** | Bazel AC + CAS + Execute; NativeLink / BuildBuddy / Buildbarn / bazel-remote |
293
- | OTel export (`@vzn/vx-otel`) | **shippable** | OTLP traces + metrics + logs, zero SDK deps |
294
- | GitHub Actions (`@vzn/vx-github`) | **shippable** | job summary + Checks API run |
295
- | MCP server (`@vzn/vx-mcp`) | **shippable** | `vx mcp` — read-only tools for AI agents, no SDK |
318
+ solid; it is dogfooded continuously. Linux and macOS, x64 and arm64;
319
+ Windows runs vx under WSL (POSIX shell is the API), with no native
320
+ build.
321
+
322
+ | Surface | Maturity | Notes |
323
+ | -------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
324
+ | Core task runner + caching | **production-ready** | dogfooded continuously; ~3,000 core tests + the package suites, green |
325
+ | Plugin pipeline (13 hooks, `commands` included) | **shippable** | crash-isolated, re-validated; the local executor + cache are the floor under every plugin |
326
+ | `vx init` / `@vzn/vx-migrate` (scripts; Turbo, Nx) | **shippable** | one config per package, TODOs where a source cannot say |
327
+ | REAPI remote cache + execution (`@vzn/vx-reapi`) | **shippable** | Bazel AC + CAS + Execute; NativeLink / BuildBuddy / Buildbarn / bazel-remote |
328
+ | OTel export (`@vzn/vx-otel`) | **shippable** | OTLP traces + metrics + logs, zero SDK deps |
329
+ | GitHub Actions (`@vzn/vx-github`) | **shippable** | job summary + Checks API run |
330
+ | MCP server (`@vzn/vx-mcp`) | **shippable** | `vx mcp` — read-only tools for AI agents, no SDK |
331
+ | Adoption (`@vzn/vx-migrate`) | **shippable** | `turbo()`: a `turbo.json` workspace runs with no `vx.config` written; `turboCache()` / `nxCache()`: any `/v8/artifacts` or Nx `/v1/cache` server; the migrate CLI |
332
+ | Lockfile keys (`@vzn/vx-lockfile`) | **shippable** | `pnpm()` `bun()` `npm()` `yarn()`: per-project dependency-closure keys; `--affected` follows |
296
333
 
297
334
  ## Development
298
335
 
@@ -303,7 +340,7 @@ bun packages/vx/src/bin.ts run ci --all # lint + test + docs build, every pa
303
340
  bun packages/vx/src/bin.ts run build --filter @vzn/vx # cross-target binaries → packages/vx/dist/
304
341
  ```
305
342
 
306
- vx is self-hosted: every dev task routes through `bun packages/vx/src/bin.ts run <task>` per each package's own `vx.config.ts`. No `package.json` scripts; CI invokes vx directly. Start with [`docs/STATUS.md`](docs/STATUS.md) — the living handoff — and `CLAUDE.md`.
343
+ vx is self-hosted: every dev task routes through `bun packages/vx/src/bin.ts run <task>` per each package's own `vx.config.ts`. No `package.json` scripts; CI invokes vx directly. Start with [`packages/vx/docs/STATUS.md`](packages/vx/docs/STATUS.md) — the living handoff — and `CLAUDE.md`.
307
344
 
308
345
  ## License
309
346
 
package/package.json CHANGED
@@ -1,24 +1,12 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.18",
3
+ "version": "0.0.22",
4
4
  "description": "An open, extensible monorepo task runner.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./src/index.ts",
9
9
  "import": "./src/index.ts"
10
- },
11
- "./plugins/local-executor": {
12
- "types": "./src/plugins/local-executor/index.ts",
13
- "import": "./src/plugins/local-executor/index.ts"
14
- },
15
- "./plugins/local-cache": {
16
- "types": "./src/plugins/local-cache/index.ts",
17
- "import": "./src/plugins/local-cache/index.ts"
18
- },
19
- "./plugins/schedule-history": {
20
- "types": "./src/plugins/schedule-history/index.ts",
21
- "import": "./src/plugins/schedule-history/index.ts"
22
10
  }
23
11
  },
24
12
  "types": "./src/index.ts",
@@ -29,23 +17,25 @@
29
17
  "node": ">=18"
30
18
  },
31
19
  "optionalDependencies": {
32
- "@vzn/vx-linux-x64": "0.0.18",
33
- "@vzn/vx-linux-arm64": "0.0.18",
34
- "@vzn/vx-darwin-x64": "0.0.18",
35
- "@vzn/vx-darwin-arm64": "0.0.18"
20
+ "@vzn/vx-linux-x64": "0.0.22",
21
+ "@vzn/vx-linux-arm64": "0.0.22",
22
+ "@vzn/vx-darwin-x64": "0.0.22",
23
+ "@vzn/vx-darwin-arm64": "0.0.22"
36
24
  },
37
25
  "dependencies": {
38
- "@anthropic-ai/sandbox-runtime": "^0.0.56"
26
+ "@anthropic-ai/sandbox-runtime": "^0.0.76"
39
27
  },
40
28
  "files": [
41
29
  "index.ts",
42
- "plugins",
43
30
  "src",
44
31
  "launcher.mjs",
45
32
  "README.md",
46
33
  "LICENSE"
47
34
  ],
48
- "repository": "https://github.com/vznjs/vx",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/vznjs/vx.git"
38
+ },
49
39
  "homepage": "https://github.com/vznjs/vx#readme",
50
40
  "bugs": "https://github.com/vznjs/vx/issues",
51
41
  "license": "MIT",
package/src/bin.ts CHANGED
@@ -1,25 +1,84 @@
1
1
  #!/usr/bin/env bun
2
- import { run } from './cli/index.js'
3
- import { isUserError } from './util/index.js'
2
+ import { registerCoreAlias, run } from './cli/index.js'
3
+ import { fsRefusalHint, isFsRefusal, isUserError } from './util/index.js'
4
+
5
+ // Every `import … from '@vzn/vx'` this process evaluates — a plugin
6
+ // package, a workspace or project config — resolves to THIS core, not to
7
+ // a second copy from node_modules (core-alias.ts says why and what it
8
+ // costs). Registered before any verb runs; the façade loads on first use.
9
+ registerCoreAlias(() => import('./index.js') as Promise<Record<string, unknown>>)
4
10
 
5
11
  // Wrapped in an explicit async main so `bun build --compile` accepts
6
12
  // the file. The compile target doesn't allow top-level await.
13
+ let settled = false
14
+
7
15
  async function main(): Promise<void> {
8
16
  try {
9
- process.exit(await run(process.argv.slice(2)))
17
+ const code = await run(process.argv.slice(2))
18
+ // NOTHING calls `process.exit` here, and that is the fix rather than a
19
+ // simplification. Bun drops what a pipe has not yet taken when
20
+ // `process.exit` follows a large write: 300 KB written then exit
21
+ // delivered 64 KiB (128 KiB after a tick), and `vx history --format
22
+ // json` on a 300-project workspace was cut mid-string (2026-09-15).
23
+ // `stdout.end(cb)` fixed that on 1.4.2 — and on 1.3.11 the callback
24
+ // still fires early: 2 MiB written, 214 KB delivered (2026-09-20).
25
+ // Setting the code and letting the loop drain is the one form that
26
+ // does not depend on when a runtime decides a pipe is flushed; it
27
+ // costs a hang if a verb leaves a handle open, which the suite's
28
+ // several hundred spawns of this binary would show at once.
29
+ process.exitCode = code
10
30
  } catch (err) {
11
31
  // UserError (workspace not found, cycle, config invalid, ...) —
12
32
  // print the message only; the stack is noise the user can't act
13
33
  // on. Everything else gets the full stack so internal bugs are
14
34
  // debuggable.
15
35
  if (isUserError(err)) {
16
- process.stderr.write(`vx: ${err.message}\n`)
36
+ // A message that already names the tool (`vx why: …`, thrown by a verb
37
+ // that wants its own name in the line) is printed as it is; prefixing
38
+ // it produced `vx: vx why: …` (walkthrough, 2026-09-04).
39
+ const m = err.message
40
+ process.stderr.write(m.startsWith('vx ') ? `${m}\n` : `vx: ${m}\n`)
41
+ } else if (isFsRefusal(err)) {
42
+ // The file system's refusal names the path; the stack would name
43
+ // the verb's write, which the reader cannot act on either.
44
+ process.stderr.write(`vx: ${err.message} — ${fsRefusalHint(err)}\n`)
17
45
  } else {
18
46
  const message = err instanceof Error ? (err.stack ?? err.message) : String(err)
19
47
  process.stderr.write(`vx: ${message}\n`)
20
48
  }
21
- process.exit(1)
49
+ // Same reason as the success path above: a large stderr is truncated by
50
+ // `process.exit` too, and an error message cut in half is the one a
51
+ // reader most needs whole.
52
+ process.exitCode = 1
22
53
  }
54
+ settled = true
55
+ }
56
+
57
+ // The loop drained while the verb was still pending: something it awaits
58
+ // can never settle (a plugin hook whose promise is never resolved), and
59
+ // with no exit code set Bun exits 0 — a run whose task failed reported
60
+ // green, and its task had not even run (item 921). An exit that no
61
+ // verdict reached is a failure, and says so.
62
+ process.on('beforeExit', () => {
63
+ if (settled) return
64
+ settled = true
65
+ process.stderr.write(
66
+ 'vx: the run stopped before it finished: something it awaited can never settle (a plugin hook?)\n',
67
+ )
68
+ process.exitCode = 1
69
+ })
70
+
71
+ // A reader that leaves is not the run's failure. `vx run build | head -1`
72
+ // closes the pipe after one line; every later write gets EPIPE, which Bun
73
+ // raises as an `error` event on the stream, and an `error` nobody listens
74
+ // for is an uncaught exception: the run died with a stack after its task
75
+ // had succeeded, exit 1, its lock directory left behind (2026-09-16). With
76
+ // a listener the write returns false, the run finishes, saves, releases,
77
+ // and exits with its own verdict; what was going to the reader goes
78
+ // nowhere. Same for stderr (`2>&1 | head`). Here and not in the logger:
79
+ // an embedder's streams are the embedder's.
80
+ for (const stream of [process.stdout, process.stderr]) {
81
+ stream.on('error', () => {})
23
82
  }
24
83
 
25
84
  void main()