@vzn/vx 0.0.646 → 0.0.648

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 (184) hide show
  1. package/README.md +29 -22
  2. package/launcher.cjs +9 -2
  3. package/launcher.sh +2 -2
  4. package/package.json +5 -5
  5. package/schemas/cache.json +5 -5
  6. package/schemas/error.json +1 -1
  7. package/schemas/info.json +16 -2
  8. package/schemas/last.json +112 -5
  9. package/schemas/plan.json +28 -2
  10. package/schemas/show.json +10 -1
  11. package/skills/vx/SKILL.md +18 -2
  12. package/src/bin.ts +53 -12
  13. package/src/cache/archive.ts +324 -23
  14. package/src/cache/cache.ts +268 -53
  15. package/src/cache/git-inputs.ts +219 -87
  16. package/src/cache/index.ts +3 -2
  17. package/src/cache/inputs.ts +460 -78
  18. package/src/cache/key-fold.ts +15 -6
  19. package/src/cache/layer.ts +49 -9
  20. package/src/cache/layered-cache.ts +123 -58
  21. package/src/cache/linked-inputs.ts +1 -1
  22. package/src/cache/output-index.ts +19 -3
  23. package/src/cache/plugin-cache.ts +49 -23
  24. package/src/cache/remote-hits.ts +72 -0
  25. package/src/cache/remote-lane.ts +90 -14
  26. package/src/cache/run-history.ts +70 -23
  27. package/src/cache/run-store.ts +11 -10
  28. package/src/cache/schema.ts +25 -6
  29. package/src/cache/tar-stream.ts +5 -0
  30. package/src/cache/umask.ts +1 -2
  31. package/src/cli/cache.ts +58 -27
  32. package/src/cli/completions.ts +12 -5
  33. package/src/cli/docs.ts +42 -16
  34. package/src/cli/help.ts +58 -19
  35. package/src/cli/index.ts +79 -33
  36. package/src/cli/info.ts +12 -6
  37. package/src/cli/init.ts +186 -31
  38. package/src/cli/last.ts +439 -45
  39. package/src/cli/lock.ts +197 -52
  40. package/src/cli/plan-format.ts +91 -49
  41. package/src/cli/plugin-commands.ts +70 -14
  42. package/src/cli/plugin-templates.ts +13 -13
  43. package/src/cli/registry.ts +209 -0
  44. package/src/cli/run.ts +109 -34
  45. package/src/cli/select.ts +143 -46
  46. package/src/cli/show.ts +59 -16
  47. package/src/cli/task-verb.ts +16 -6
  48. package/src/cli/upgrade.ts +151 -49
  49. package/src/cli/watch-cycle.ts +10 -5
  50. package/src/cli/watch-filter.ts +71 -3
  51. package/src/cli/watch-fs.ts +22 -6
  52. package/src/cli/watch-judge.ts +48 -6
  53. package/src/cli/watch.ts +163 -42
  54. package/src/cli/why.ts +211 -73
  55. package/src/cli/workspace-config.ts +54 -15
  56. package/src/config.ts +13 -1
  57. package/src/exec/env.ts +7 -2
  58. package/src/exec/executor.ts +154 -10
  59. package/src/exec/index.ts +6 -2
  60. package/src/exec/kill-tree.ts +15 -0
  61. package/src/exec/local-executor.ts +1 -1
  62. package/src/exec/proc-sample.ts +3 -1
  63. package/src/exec/runner.ts +253 -54
  64. package/src/exec/sandbox-binds.ts +7 -3
  65. package/src/exec/sandbox-paths.ts +1 -1
  66. package/src/exec/sandbox-runtime.ts +211 -24
  67. package/src/exec/sandbox-violations.ts +52 -20
  68. package/src/graph/critical-path.ts +79 -0
  69. package/src/graph/default-build.ts +19 -0
  70. package/src/graph/definition.ts +38 -0
  71. package/src/graph/folds.ts +38 -5
  72. package/src/graph/index.ts +4 -1
  73. package/src/graph/priorities.ts +74 -8
  74. package/src/graph/scheduler.ts +270 -60
  75. package/src/graph/task-graph.ts +228 -35
  76. package/src/handoff.ts +97 -9
  77. package/src/history/failure-mode.ts +6 -3
  78. package/src/history/history.ts +167 -61
  79. package/src/history/index.ts +2 -0
  80. package/src/history/metrics.ts +90 -30
  81. package/src/index.ts +30 -13
  82. package/src/orchestrator/admission.ts +10 -6
  83. package/src/orchestrator/affected-tasks.ts +11 -6
  84. package/src/orchestrator/deferred-outputs.ts +2 -0
  85. package/src/orchestrator/doctor.ts +134 -42
  86. package/src/orchestrator/execute-args.ts +4 -1
  87. package/src/orchestrator/execute-task.ts +130 -38
  88. package/src/orchestrator/fingerprint-watch.ts +1 -1
  89. package/src/orchestrator/graph-memo.ts +140 -0
  90. package/src/orchestrator/graph-stage.ts +18 -1
  91. package/src/orchestrator/held-servers.ts +1 -1
  92. package/src/orchestrator/hit-restore.ts +217 -119
  93. package/src/orchestrator/index.ts +4 -3
  94. package/src/orchestrator/local-floor.ts +119 -25
  95. package/src/orchestrator/local-shortcircuit.ts +28 -4
  96. package/src/orchestrator/lockfile-claim.ts +96 -16
  97. package/src/orchestrator/miss-save.ts +42 -8
  98. package/src/orchestrator/options.ts +8 -6
  99. package/src/orchestrator/output-host.ts +43 -14
  100. package/src/orchestrator/output-log.ts +90 -10
  101. package/src/orchestrator/persistent-task.ts +84 -29
  102. package/src/orchestrator/persistent.ts +16 -4
  103. package/src/orchestrator/placement.ts +185 -17
  104. package/src/orchestrator/plan.ts +64 -7
  105. package/src/orchestrator/plugin-host.ts +214 -42
  106. package/src/orchestrator/plugin.ts +113 -37
  107. package/src/orchestrator/prepare.ts +168 -30
  108. package/src/orchestrator/projects.ts +65 -40
  109. package/src/orchestrator/remote-prefetch.ts +119 -25
  110. package/src/orchestrator/run-artifacts.ts +95 -32
  111. package/src/orchestrator/run-context.ts +39 -41
  112. package/src/orchestrator/run-failures.ts +65 -23
  113. package/src/orchestrator/run-id.ts +6 -3
  114. package/src/orchestrator/run-keys.ts +149 -13
  115. package/src/orchestrator/run-live.ts +294 -0
  116. package/src/orchestrator/run-lock.ts +13 -7
  117. package/src/orchestrator/run-records.ts +35 -29
  118. package/src/orchestrator/run-report.ts +3 -3
  119. package/src/orchestrator/run-steps.ts +58 -22
  120. package/src/orchestrator/run-stop.ts +22 -5
  121. package/src/orchestrator/run.ts +331 -88
  122. package/src/orchestrator/sandbox-fix.ts +7 -5
  123. package/src/orchestrator/sandbox-request.ts +70 -20
  124. package/src/orchestrator/shell-verdict.ts +5 -5
  125. package/src/orchestrator/signals.ts +26 -1
  126. package/src/orchestrator/stable-keys.ts +92 -8
  127. package/src/orchestrator/steps.ts +2 -2
  128. package/src/orchestrator/task-artifacts.ts +90 -26
  129. package/src/orchestrator/task-hash.ts +78 -13
  130. package/src/orchestrator/task-log-buffer.ts +72 -36
  131. package/src/orchestrator/task-process.ts +7 -1
  132. package/src/orchestrator/telemetry-host.ts +22 -2
  133. package/src/orchestrator/telemetry.ts +53 -21
  134. package/src/orchestrator/upstream.ts +15 -0
  135. package/src/report/events.ts +32 -13
  136. package/src/report/forecast.ts +31 -9
  137. package/src/report/framed-output.ts +141 -50
  138. package/src/report/index.ts +1 -0
  139. package/src/report/logger.ts +170 -179
  140. package/src/report/path-links.ts +12 -4
  141. package/src/report/plain-output.ts +13 -5
  142. package/src/report/status-line.ts +81 -53
  143. package/src/report/summary.ts +114 -32
  144. package/src/report/terminal-signals.ts +13 -2
  145. package/src/settings.ts +7 -6
  146. package/src/util/bun-version.ts +4 -1
  147. package/src/util/colors.ts +4 -2
  148. package/src/util/config-frame.ts +41 -4
  149. package/src/{graph → util}/dependency-spec.ts +22 -13
  150. package/src/util/edit-distance.ts +13 -1
  151. package/src/util/env-flag.ts +9 -0
  152. package/src/util/errors.ts +59 -3
  153. package/src/util/essential-env.ts +57 -0
  154. package/src/util/git-ignored.ts +1 -1
  155. package/src/util/hash.ts +35 -0
  156. package/src/util/hide-env.ts +45 -5
  157. package/src/util/index.ts +21 -5
  158. package/src/util/num.ts +8 -0
  159. package/src/util/paths.ts +91 -21
  160. package/src/util/report.ts +6 -1
  161. package/src/util/secret-mask.ts +62 -1
  162. package/src/util/secrets.ts +7 -2
  163. package/src/util/size.ts +11 -3
  164. package/src/util/tail.ts +6 -1
  165. package/src/util/timing.ts +13 -1
  166. package/src/util/ui.ts +61 -3
  167. package/src/util/which.ts +1 -1
  168. package/src/util/write-file.ts +67 -0
  169. package/src/workspace/affected.ts +710 -107
  170. package/src/workspace/bracket-literal.ts +84 -0
  171. package/src/workspace/canonical-dir.ts +37 -0
  172. package/src/workspace/config-bytecode.ts +17 -11
  173. package/src/workspace/config-eval.ts +29 -8
  174. package/src/workspace/config-imports.ts +199 -58
  175. package/src/workspace/config-schema.ts +242 -39
  176. package/src/workspace/filter.ts +37 -8
  177. package/src/workspace/index.ts +11 -1
  178. package/src/workspace/json-data.ts +3 -4
  179. package/src/workspace/lockfile.ts +57 -8
  180. package/src/workspace/migration.ts +69 -18
  181. package/src/workspace/package-graph.ts +5 -4
  182. package/src/workspace/project-loader.ts +226 -37
  183. package/src/workspace/repo-id.ts +1 -1
  184. package/src/workspace/workspace.ts +176 -33
package/README.md CHANGED
@@ -79,12 +79,11 @@ stale lock before any task runs.
79
79
 
80
80
  Starters: [`examples/basic`](examples/basic), and
81
81
  [`examples/ci`](examples/ci) with a GitHub Actions workflow; CI runs both
82
- on every commit. The [CI guide](https://vznjs.github.io/vx/guides/ci/)
82
+ on every commit. The [CI guide](https://vznjs.github.io/vx/docs/ci/)
83
83
  explains the workflow.
84
84
  More: [Quickstart](https://vznjs.github.io/vx/quickstart/) ·
85
- [Configure](https://vznjs.github.io/vx/guides/configure/) ·
86
- [CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md) ·
87
- [playground](https://vznjs.github.io/vx/playground/) (the planner, in your browser).
85
+ [Configure](https://vznjs.github.io/vx/docs/workspace/) ·
86
+ [CLI](packages/vx/docs/cli.md) · [every config field](packages/vx/docs/schema.md).
88
87
 
89
88
  ## A foundation you own
90
89
 
@@ -102,16 +101,24 @@ More: [Quickstart](https://vznjs.github.io/vx/quickstart/) ·
102
101
 
103
102
  What vx can do, not what it is limited to. Declared in `.vx/workspace.ts`; without any, vx runs and caches locally.
104
103
 
105
- | Package | What it adds |
106
- | ---------------------------------------------------------- | ----------------------------------------------------------------------------- |
107
- | [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
108
- | [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
109
- | [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
110
- | [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
111
- | [`@vzn/vx-ci`](packages/vx-ci) | GitHub Actions job summary and Checks API |
112
- | [`@vzn/vx-schedule-history`](packages/vx-schedule-history) | Schedule by the critical path learned from past runs |
113
-
114
- Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
104
+ | Package | What it adds |
105
+ | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
106
+ | [`@vzn/vx-migrate`](packages/vx-migrate) | Migrate from Turbo or Nx: write native configs, keep its remote cache |
107
+ | [`@vzn/vx-reapi`](packages/vx-reapi) | Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
108
+ | [`@vzn/vx-lockfile`](packages/vx-lockfile) | Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
109
+ | [`@vzn/vx-otel`](packages/vx-otel) | OpenTelemetry traces, metrics and logs, no SDK |
110
+ | [`@vzn/vx-ci`](packages/vx-ci) | GitHub Actions job summary |
111
+ | [`@vzn/vx-history`](packages/vx-history) | Schedule by the critical path learned from past runs |
112
+ | [`@vzn/vx-cache`](packages/vx-cache) | Remote cache for CI jobs, on a vx cache server |
113
+ | [`@vzn/vx-cache-server`](packages/vx-cache-server) | The cache server you host: Bun or Docker with S3, or Workers with R2 |
114
+ | [`@vzn/vx-history-server`](packages/vx-history-server) | The task timings server `vxHistory()` reads: Bun, Docker or Workers |
115
+ | [`@vzn/vx-devtools`](packages/vx-devtools) | Code the servers and plugins share: the CI OIDC token check |
116
+ | [`@vzn/vx-dte`](packages/vx-dte) | Spread one run over workers your own CI starts |
117
+ | [`@vzn/vx-dte-server`](packages/vx-dte-server) | The relay `dte()` meets its workers at: Workers, Bun or Docker |
118
+
119
+ Hosting the three servers: [servers docs](https://vznjs.github.io/vx/docs/servers/).
120
+
121
+ Write your own: [plugin guide](https://vznjs.github.io/vx/docs/writing-plugins/) ·
115
122
  [architecture](packages/vx/docs/architecture.md).
116
123
 
117
124
  ## Coming from Turbo or Nx
@@ -145,19 +152,19 @@ Every gap, and when another tool is the better pick:
145
152
  - **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
146
153
  - **One pass:** one git walk per run; each stat and lookup happens once.
147
154
 
148
- <!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
155
+ <!-- bench:start — generated by private/vx-bench/update-site.ts from results.json; do not hand-edit -->
149
156
 
150
- | Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
151
- | ---------------------------------------------------------- | ---------- | --------- | ---------- | --------- |
152
- | Nothing changed | **757 ms** | 761 ms | 22.63 s | 12.26 s |
153
- | Nothing changed, outputs restored | **1.27 s** | 1.76 s | 22.16 s | 11.66 s |
154
- | Cold build | **6.13 s** | 8.87 s | 3 min 20 s | 7.94 s |
155
- | Cold build: CPU the runner burns | 30.06 s | 1 min 8 s | 5 min 45 s | 23.91 s |
157
+ | Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
158
+ | ---------------------------------------------------------- | ---------- | ---------- | ---------- | --------- |
159
+ | Nothing changed | **799 ms** | 927 ms | 24.15 s | 12.07 s |
160
+ | Nothing changed, outputs restored | **1.28 s** | 2.01 s | 24.45 s | 11.86 s |
161
+ | Cold build | **7.57 s** | 9.63 s | 3 min 6 s | 9.01 s |
162
+ | Cold build: CPU the runner burns | 36.64 s | 1 min 12 s | 5 min 34 s | 25.32 s |
156
163
 
157
164
  Time each tool adds over the ideal run; lower is better.
158
165
 
159
166
  Benchmark workload: a synthetic monorepo of 1,601 projects and 9,603 tasks in 30 dependency levels, five core libraries a quarter of the projects use; build 300 ms, test and typecheck 150 ms, lint 75 ms, publish 30 ms; real repos with uneven task times will differ.
160
- Run 2026-10-20 on linux x64, 4 cores: vx at commit a88652b0b, Turborepo 2.11.9, Nx 23.3.0, Vite Task (vite-plus) 1.3.0.
167
+ Run 2026-10-25 on linux x64, 4 cores: vx at commit 9abdfb52c, Turborepo 2.11.10, Nx 24.0.1, Vite Task (vite-plus) 1.4.0.
161
168
 
162
169
  Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
163
170
 
package/launcher.cjs CHANGED
@@ -102,8 +102,15 @@ function run(cmd, cmdArgs) {
102
102
  process.stderr.write(`${base}: failed to launch (${err.message})\n`)
103
103
  process.exit(1)
104
104
  })
105
- // Mirror the child's exit; a signal death maps to the POSIX 128+signo code.
105
+ // Mirror the child's exit. A stop signal vx died of is mirrored by dying
106
+ // of it: a shell running vx in a script stops on Ctrl-C only when its
107
+ // child died of SIGINT, and an exit 130 ran the script's next line. The
108
+ // POSIX 128+signo code is the rest, and the fallback should it not land.
106
109
  child.on('exit', (code, signal) => {
110
+ if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP') {
111
+ process.removeAllListeners(signal)
112
+ process.kill(process.pid, signal)
113
+ }
107
114
  process.exit(signal ? 128 + (osConstants.signals[signal] ?? 1) : (code ?? 0))
108
115
  })
109
116
  }
@@ -133,7 +140,7 @@ if (bin !== undefined) {
133
140
  `${base}: no prebuilt binary for ${key}${noGlibc ? ' without glibc (musl)' : ''}.\n` +
134
141
  ` Supported platforms: ${supported}.\n` +
135
142
  ` If your platform should be supported, reinstall so npm fetches the\n` +
136
- ` matching ${name}-${key} optionalDependency, or install Bun (>=1.4) to\n` +
143
+ ` matching ${name}-${key} optionalDependency, or install Bun (>=1.4.4) to\n` +
137
144
  ` run ${base} from source.\n`,
138
145
  )
139
146
  process.exit(1)
package/launcher.sh CHANGED
@@ -29,14 +29,14 @@ 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
32
+ # A global vx hands off to the project's own (2026-10-13): the
33
33
  # workspace root's node_modules/<scope>/<name>, through its .bin entry. The
34
34
  # root is the first directory holding a .vx/workspace.* from the working
35
35
  # directory up to the git top-level (handoff.ts workspaceFile, the same
36
36
  # rule). One hop (VX_HANDED_OFF, which also names this launcher), never to
37
37
  # this package itself (`-ef`: same inode, so a pnpm symlink counts as
38
38
  # itself). Shell tests and builtins only, no fork per level. `vx upgrade -g`
39
- # moves only this global vx, so it never hands off (owner, 2026-10-16).
39
+ # moves only this global vx, so it never hands off (2026-10-16).
40
40
  stay=
41
41
  if [ "${1-}" = upgrade ]; then
42
42
  for a in "$@"; do
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.646",
3
+ "version": "0.0.648",
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.646",
25
- "@vzn/vx-linux-arm64": "0.0.646",
26
- "@vzn/vx-darwin-x64": "0.0.646",
27
- "@vzn/vx-darwin-arm64": "0.0.646"
24
+ "@vzn/vx-linux-x64": "0.0.648",
25
+ "@vzn/vx-linux-arm64": "0.0.648",
26
+ "@vzn/vx-darwin-x64": "0.0.648",
27
+ "@vzn/vx-darwin-arm64": "0.0.648"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
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.",
4
+ "description": "With a bound (prune): what the policy evicted and the orphan sweep reaped. With no bound (clear): the directories it deleted. With dry, what they would.",
5
5
  "oneOf": [{ "$ref": "#/$defs/prune" }, { "$ref": "#/$defs/clear" }],
6
6
  "$defs": {
7
7
  "prune": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
10
- "required": ["dryRun", "evicted", "bytesFreed", "orphans", "orphanBytes"],
10
+ "required": ["dry", "evicted", "bytesFreed", "orphans", "orphanBytes"],
11
11
  "properties": {
12
- "dryRun": { "type": "boolean" },
12
+ "dry": { "type": "boolean" },
13
13
  "evicted": {
14
14
  "type": "integer",
15
15
  "description": "index entries evicted by the age / size policy"
@@ -28,9 +28,9 @@
28
28
  "clear": {
29
29
  "type": "object",
30
30
  "additionalProperties": false,
31
- "required": ["dryRun", "cleared", "bytesFreed"],
31
+ "required": ["dry", "cleared", "bytesFreed"],
32
32
  "properties": {
33
- "dryRun": { "type": "boolean" },
33
+ "dry": { "type": "boolean" },
34
34
  "cleared": {
35
35
  "type": "array",
36
36
  "items": { "type": "string" },
@@ -14,7 +14,7 @@
14
14
  "properties": {
15
15
  "code": {
16
16
  "type": "string",
17
- "description": "Stable; branch on it. VX_E_USAGE, VX_E_UNKNOWN_COMMAND, VX_E_UNKNOWN_TASK, VX_E_UNKNOWN_PROJECT, VX_E_NO_HISTORY, VX_E_AMBIGUOUS, VX_E_CONFIG, VX_E_CYCLE, VX_E_NO_WORKSPACE, VX_E_CACHE_VERSION, VX_E_REFUSED, VX_E_FS, VX_E_FDS, VX_E_INTERNAL."
17
+ "description": "Stable; branch on it. VX_E_USAGE, VX_E_UNKNOWN_COMMAND, VX_E_UNKNOWN_TASK, VX_E_UNKNOWN_PROJECT, VX_E_NO_HISTORY, VX_E_AMBIGUOUS, VX_E_CONFIG, VX_E_CYCLE, VX_E_LOCK, VX_E_NO_WORKSPACE, VX_E_CACHE_VERSION, VX_E_REFUSED, VX_E_FS, VX_E_FDS, VX_E_AFFECTED_BASE, VX_E_NO_GIT, VX_E_GIT, VX_E_NETWORK, VX_E_DEPENDS_ON, VX_E_OUTPUT_OVERLAP, VX_E_INPUT_IS_OUTPUT, VX_E_DEPENDENCY_RULE, VX_E_NESTED_INPUT, VX_E_INTERNAL. A plugin may name its own."
18
18
  },
19
19
  "message": { "type": "string", "description": "The refusal in prose; may change." },
20
20
  "docs": {
package/schemas/info.json CHANGED
@@ -25,6 +25,7 @@
25
25
  "schemaVersion",
26
26
  "cacheEntries",
27
27
  "cacheBytes",
28
+ "cacheIndexError",
28
29
  "settings",
29
30
  "orphans",
30
31
  "runs24h",
@@ -32,6 +33,7 @@
32
33
  "restored24h",
33
34
  "flakyTasks",
34
35
  "lockfile",
36
+ "lockError",
35
37
  "sandbox"
36
38
  ],
37
39
  "properties": {
@@ -68,7 +70,7 @@
68
70
  "required": ["count", "source", "cores", "cpuQuota"],
69
71
  "properties": {
70
72
  "count": { "type": "integer" },
71
- "source": { "enum": ["workspace", "cgroup", "cores"] },
73
+ "source": { "enum": ["env", "workspace", "cgroup", "cores"] },
72
74
  "cores": { "type": "integer" },
73
75
  "cpuQuota": { "type": ["number", "null"] }
74
76
  }
@@ -91,6 +93,10 @@
91
93
  "schemaVersion": { "type": "string" },
92
94
  "cacheEntries": { "type": "integer" },
93
95
  "cacheBytes": { "type": "integer" },
96
+ "cacheIndexError": {
97
+ "type": ["string", "null"],
98
+ "description": "Why the cache index cannot be read, with the fix; null when it reads. While set, the counts it holds read 0."
99
+ },
94
100
  "settings": {
95
101
  "type": "object",
96
102
  "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",
@@ -100,7 +106,11 @@
100
106
  "required": ["value", "source"],
101
107
  "properties": {
102
108
  "value": { "type": ["string", "number", "boolean", "null"] },
103
- "source": { "enum": ["env", "workspace", "default"] }
109
+ "source": { "enum": ["env", "workspace", "default"] },
110
+ "refused": {
111
+ "type": "string",
112
+ "description": "why a run refuses the variable's value; absent when a run takes it"
113
+ }
104
114
  }
105
115
  }
106
116
  },
@@ -133,6 +143,10 @@
133
143
  }
134
144
  },
135
145
  "lockfile": { "type": "boolean" },
146
+ "lockError": {
147
+ "type": ["string", "null"],
148
+ "description": "Why a run refuses the lock that is there (run `vx lock`); null when a run reads it or there is none."
149
+ },
136
150
  "sandbox": {
137
151
  "type": "object",
138
152
  "additionalProperties": false,
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; `--stats` prints per-task history numbers.",
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; `--sql` prints the rows a read-only query over the history selects; `--live` prints the run in progress (with `--follow`, its events after, one per line).",
5
5
  "oneOf": [
6
6
  {
7
7
  "type": "object",
@@ -9,14 +9,88 @@
9
9
  "required": ["invocation", "tasks"],
10
10
  "properties": {
11
11
  "invocation": { "$ref": "#/$defs/invocation" },
12
- "tasks": { "type": "array", "items": { "$ref": "#/$defs/task" } }
12
+ "tasks": { "type": "array", "items": { "$ref": "#/$defs/task" } },
13
+ "warnings": {
14
+ "type": "array",
15
+ "description": "what the run warned of, plain text; absent when it warned of nothing",
16
+ "items": { "type": "string" }
17
+ },
18
+ "steps": {
19
+ "type": "array",
20
+ "description": "vx's own steps the run did not wait on (vx:prefetch); absent when it had none or they are no longer kept",
21
+ "items": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["id", "durationMs", "ok"],
25
+ "properties": {
26
+ "id": { "type": "string" },
27
+ "durationMs": { "type": "number" },
28
+ "ok": { "type": "boolean" }
29
+ }
30
+ }
31
+ }
13
32
  }
14
33
  },
15
34
  { "type": "array", "items": { "$ref": "#/$defs/invocation" } },
16
35
  { "$ref": "#/$defs/log" },
17
- { "$ref": "#/$defs/stats" }
36
+ { "$ref": "#/$defs/stats" },
37
+ { "$ref": "#/$defs/rows" },
38
+ { "$ref": "#/$defs/live" }
18
39
  ],
19
40
  "$defs": {
41
+ "live": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["live", "tasks", "warnings"],
45
+ "properties": {
46
+ "live": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["runId", "command", "pid", "startedAt", "elapsedMs"],
50
+ "properties": {
51
+ "runId": { "type": "string" },
52
+ "command": { "type": "string" },
53
+ "pid": { "type": "integer" },
54
+ "startedAt": { "type": "integer", "description": "epoch ms" },
55
+ "elapsedMs": { "type": "integer" }
56
+ }
57
+ },
58
+ "tasks": {
59
+ "type": "array",
60
+ "description": "`--live`: running first, then ended, then waiting; groups left out",
61
+ "items": {
62
+ "type": "object",
63
+ "additionalProperties": false,
64
+ "required": ["taskId", "status", "startedAt", "durationMs"],
65
+ "properties": {
66
+ "taskId": { "type": "string" },
67
+ "status": {
68
+ "type": "string",
69
+ "description": "running, waiting, or the status it ended with"
70
+ },
71
+ "startedAt": { "type": ["integer", "null"] },
72
+ "durationMs": { "type": ["number", "null"] }
73
+ }
74
+ }
75
+ },
76
+ "warnings": { "type": "array", "items": { "type": "string" } }
77
+ }
78
+ },
79
+ "rows": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "required": ["rows"],
83
+ "properties": {
84
+ "rows": {
85
+ "type": "array",
86
+ "description": "`--sql`: one object per row, keyed by the query's column names",
87
+ "items": {
88
+ "type": "object",
89
+ "additionalProperties": { "type": ["string", "number", "null"] }
90
+ }
91
+ }
92
+ }
93
+ },
20
94
  "stats": {
21
95
  "type": "object",
22
96
  "additionalProperties": false,
@@ -176,14 +250,18 @@
176
250
  "sandboxViolations",
177
251
  "notReady",
178
252
  "remoteError",
179
- "forwarded"
253
+ "forwarded",
254
+ "attempts"
180
255
  ],
181
256
  "properties": {
182
257
  "runId": { "type": ["string", "null"] },
183
258
  "project": { "type": "string" },
184
259
  "task": { "type": "string" },
185
260
  "status": { "type": "string" },
186
- "exitCode": { "type": "integer" },
261
+ "exitCode": {
262
+ "type": "integer",
263
+ "description": "a skipped task records 0 when an exec.platforms skip is its root, 1 behind a failure or fail-fast"
264
+ },
187
265
  "durationMs": { "type": "number" },
188
266
  "startedAt": { "type": "integer", "description": "epoch ms" },
189
267
  "endedAt": { "type": "integer", "description": "epoch ms" },
@@ -228,6 +306,14 @@
228
306
  "description": "each failed remote read or write of the key, `; `-joined"
229
307
  },
230
308
  "forwarded": { "type": "boolean" },
309
+ "inputMoved": {
310
+ "type": "string",
311
+ "description": "on a row failed by inputStability.severity 'error': the input that changed during the run"
312
+ },
313
+ "attempts": {
314
+ "type": ["integer", "null"],
315
+ "description": "the attempts a retried task took (more than one); null when it ran once"
316
+ },
231
317
  "output": {
232
318
  "type": "string",
233
319
  "description": "on a failed row: its output, plain text, bounded (first 8 KiB, last 56 KiB), secrets masked; '' when none was recorded"
@@ -246,6 +332,27 @@
246
332
  }
247
333
  }
248
334
  },
335
+ "sandboxFix": {
336
+ "type": "object",
337
+ "description": "on a sandboxed failed row: the exec.sandbox.allow entries that would have granted its denials",
338
+ "additionalProperties": false,
339
+ "properties": {
340
+ "read": { "type": "array", "items": { "type": "string" } },
341
+ "write": { "type": "array", "items": { "type": "string" } },
342
+ "network": { "type": "array", "items": { "type": "string" } },
343
+ "systemInfo": { "type": "array", "items": { "type": "string" } },
344
+ "withheld": {
345
+ "type": "array",
346
+ "description": "credential paths it denied and left out of the grants",
347
+ "items": { "type": "string" }
348
+ },
349
+ "inputs": {
350
+ "type": "array",
351
+ "description": "a cached task's reads of its project no inputs.files glob matches",
352
+ "items": { "type": "string" }
353
+ }
354
+ }
355
+ },
249
356
  "fixedIn": {
250
357
  "type": "string",
251
358
  "description": "on a failed row: the first later run where this task passed (ran or hit); absent while it has not"
package/schemas/plan.json CHANGED
@@ -20,9 +20,21 @@
20
20
  "id": { "type": "string", "description": "project#task" },
21
21
  "project": { "type": "string" },
22
22
  "task": { "type": "string" },
23
- "hash": { "type": "string", "description": "the cache key" },
23
+ "hash": {
24
+ "type": "string",
25
+ "description": "the cache key; empty when the run derives none"
26
+ },
24
27
  "cacheStatus": {
25
- "enum": ["hit-local", "hit-remote", "miss", "no-cache", "group", "not-started"]
28
+ "enum": [
29
+ "hit-local",
30
+ "hit-remote",
31
+ "miss",
32
+ "no-cache",
33
+ "group",
34
+ "not-started",
35
+ "skipped"
36
+ ],
37
+ "description": "skipped: exec.platforms leave this machine out, or a task it depends on is skipped; the run skips it too"
26
38
  },
27
39
  "deps": { "type": "array", "items": { "type": "string" } },
28
40
  "p50Ms": {
@@ -38,6 +50,20 @@
38
50
  "description": "outputs would stay remote (--cache-download)"
39
51
  },
40
52
  "description": { "type": "string" },
53
+ "otherPlatform": {
54
+ "type": "object",
55
+ "description": "a skipped task's own exec.platforms, and this machine's",
56
+ "additionalProperties": false,
57
+ "required": ["needs", "host"],
58
+ "properties": {
59
+ "needs": { "type": "array", "items": { "type": "string" } },
60
+ "host": { "type": "string" }
61
+ }
62
+ },
63
+ "blockedBy": {
64
+ "type": "string",
65
+ "description": "a skipped task's root: the task off this machine's platforms it depends on"
66
+ },
41
67
  "affected": {
42
68
  "type": "object",
43
69
  "description": "why --affected kept this requested task; absent on a dependency it pulled in",
package/schemas/show.json CHANGED
@@ -6,7 +6,12 @@
6
6
  { "type": "array", "items": { "$ref": "#/$defs/listRow" } },
7
7
  { "$ref": "#/$defs/project" },
8
8
  { "$ref": "#/$defs/task" },
9
- { "type": "array", "items": { "$ref": "#/$defs/task" } }
9
+ {
10
+ "type": "array",
11
+ "minItems": 1,
12
+ "description": "`vx show <task>`; when no project in scope declares it, `[]`, which the list branch holds",
13
+ "items": { "$ref": "#/$defs/task" }
14
+ }
10
15
  ],
11
16
  "$defs": {
12
17
  "listRow": {
@@ -84,6 +89,10 @@
84
89
  "properties": { "readyWhen": { "type": "string" } }
85
90
  },
86
91
  "interactive": { "type": "boolean" },
92
+ "platforms": {
93
+ "type": "array",
94
+ "items": { "enum": ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64"] }
95
+ },
87
96
  "sandbox": {
88
97
  "type": "object",
89
98
  "description": "as declared; its fields are the `exec.sandbox` section of the config schema"
@@ -53,6 +53,13 @@ does not):
53
53
  is one of its inputs), `project`, `package`, `named` or `selected`, and
54
54
  `via` is the `dependsOn` chain that carried the change.
55
55
 
56
+ ## While a run is in progress
57
+
58
+ ```sh
59
+ vx last --live --format json # what runs, what ended, what waits, warnings so far
60
+ vx last --live --follow --format json # then each event (output included), one per line, until the run ends
61
+ ```
62
+
56
63
  ## When a task fails
57
64
 
58
65
  ```sh
@@ -60,6 +67,7 @@ vx last --failed --format json # the last failed run; a failed row has output
60
67
  vx last --log app#build # one task's output: a failure's, or its cached log
61
68
  ```
62
69
 
70
+ `warnings` holds what the run warned of (absent when nothing).
63
71
  A failed row's `output` is the task's output as plain text (secrets masked)
64
72
  and `locations` the files it names as `{ file, line?, col? }`. Read those
65
73
  files, fix, and re-run the same command: cached tasks that passed replay from the cache.
@@ -77,12 +85,20 @@ vx last --stats --format json # per task: p50 (the live estimate's),
77
85
  vx last --stats --branch main --format json # p50 from main's timings
78
86
  vx last --stats app#build --format json # one task; a task name covers every project
79
87
  vx last --list --format json # recent runs, newest first
88
+ vx last --sql "SELECT name, sql FROM sqlite_schema" --format json # the history's tables
80
89
  ```
81
90
 
91
+ Any other question (one task's memory trend, failures per project, which
92
+ inputs a key folded) is one read-only query: `vx last --sql "<SELECT>"
93
+ --format json` prints `{ rows }`. `runs` holds one row per task per run
94
+ (duration, cache hit, cpu, memory, attempts, failure reasons),
95
+ `invocations` one per run; join on `run_id`. Times are epoch ms.
96
+
82
97
  ## Workspace health
83
98
 
84
99
  ```sh
85
- vx info --format json # vx and Bun versions, projects, config errors, plugins, cache
100
+ vx info --format json # vx and Bun versions, projects, plugins, cache
101
+ vx lock --check --format json # a vx.config that changed since the lock, or no longer loads
86
102
  ```
87
103
 
88
104
  ## Rules
@@ -100,7 +116,7 @@ vx info --format json # vx and Bun versions, projects, config errors, plugins,
100
116
  print whole.
101
117
  - Docs as markdown: https://vznjs.github.io/vx/llms.txt (its index).
102
118
  When that host is unreachable, the same pages are
103
- `https://raw.githubusercontent.com/vznjs/vx/main/packages/vx-docs/src/content/docs/<page>.md`:
119
+ `https://raw.githubusercontent.com/vznjs/vx/main/private/vx-docs/src/content/docs/<page>.md`:
104
120
  `quickstart`, `guides/migrate`, `guides/configure`, `guides/agents`,
105
121
  `guides/troubleshooting`, `guides/ci`, `guides/plugins`.
106
122
 
package/src/bin.ts CHANGED
@@ -25,11 +25,28 @@ async function main(): Promise<void> {
25
25
  // ~7 ms, half of this verb (2026-10-02). A reader gone (EPIPE) is no
26
26
  // failure of the line.
27
27
  // Handed off, it says which vx ran and which one handed it the call.
28
- const from = process.env['VX_HANDED_OFF']
28
+ const from = process.env['VX_HANDED_OFF'] || undefined
29
29
  const where = from === undefined ? '' : ` (${vxPath()}, handed off from ${from})`
30
- await Bun.write(Bun.stdout, `vx ${(await import('./version.js')).VERSION}${where}\n`).catch(
31
- () => {},
32
- )
30
+ // Painted as every other line; piped, the same words a script reads.
31
+ // Decided here, not by util's `detectColors`: the util barrel was the
32
+ // 20 ms above, and `node:tty` 17 ms; fd 1 as a character device is the
33
+ // TTY answer. A CI's log is not painted for this one line.
34
+ const [{ VERSION }, { fstatSync }] = await Promise.all([
35
+ import('./version.js'),
36
+ import('node:fs'),
37
+ ])
38
+ const env = process.env
39
+ const on = env['NO_COLOR']
40
+ ? false
41
+ : env['FORCE_COLOR'] !== undefined
42
+ ? env['FORCE_COLOR'] !== '0' && env['FORCE_COLOR'] !== 'false'
43
+ : env['TERM'] !== 'dumb' && fstatSync(1).isCharacterDevice()
44
+ const sgr = (code: string, text: string): string =>
45
+ on ? `\x1b[${code}m${text}\x1b[0m` : text
46
+ await Bun.write(
47
+ Bun.stdout,
48
+ `${sgr('1', 'vx')} ${sgr('38;2;6;182;212', VERSION)}${where === '' ? '' : sgr('2', where)}\n`,
49
+ ).catch(() => {})
33
50
  settled = true
34
51
  return
35
52
  }
@@ -64,15 +81,19 @@ async function main(): Promise<void> {
64
81
  // A plugin's failure quotes what it was told (a remote's reply, a
65
82
  // header), and a config's own throw is its author's text: a secret in
66
83
  // either reached stderr whole (L-11).
67
- const say = (line: string): void => void process.stderr.write(maskedLine(line))
84
+ const say = (line: string): void =>
85
+ void process.stderr.write(
86
+ `${ui.tagged('error', maskedLine(line), detectColors(process.stderr))}\n`,
87
+ )
68
88
  // UserError (workspace not found, cycle, config invalid, ...) —
69
89
  // print the message only; the stack is noise the user can't act
70
90
  // on. Everything else gets the full stack so internal bugs are
71
91
  // debuggable.
72
92
  if (isUserError(err)) {
73
- // A message that names its verb (`vx why: …`) is that verb's; any
74
- // other is the verb typed. A config refusal shows its line.
75
- const named = /^(vx [a-z][\w-]*): /.exec(err.message)
93
+ // A message that names its verb (`vx why: …`, a plugin's `vx dte
94
+ // worker: …`) is that verb's; any other is the verb typed. A config
95
+ // refusal shows its line.
96
+ const named = /^(vx [a-z][\w-]*(?: [a-z][\w-]*)?): /.exec(err.message)
76
97
  const typed = /^[a-z][\w-]*$/.test(argv[0] ?? '') ? `vx ${argv[0]}` : 'vx'
77
98
  const block = refusalNotice(
78
99
  named?.[1] ?? typed,
@@ -87,8 +108,7 @@ async function main(): Promise<void> {
87
108
  // the verb's write, which the reader cannot act on either.
88
109
  say(`vx: ${err.message} — ${fsRefusalHint(err)}\n`)
89
110
  } else {
90
- const message = err instanceof Error ? (err.stack ?? err.message) : String(err)
91
- say(`vx: ${message}\n`)
111
+ say(`vx: ${withCauses(err)}\n`)
92
112
  }
93
113
  // A verb asked for JSON answers JSON on a refusal too: an agent reading
94
114
  // stdout got nothing to parse, and only prose on stderr to branch on.
@@ -126,6 +146,26 @@ async function exitOnceFlushed(): Promise<void> {
126
146
  process.stderr.end(ended)
127
147
  }
128
148
 
149
+ /**
150
+ * An error's stack, then each `cause` under it: a config or plugin that
151
+ * wraps a failure (`new Error(msg, { cause })`) put the reason there, and
152
+ * the stack alone printed only the wrapper. Bounded, and a cause seen
153
+ * before ends the chain.
154
+ */
155
+ function withCauses(err: unknown): string {
156
+ const text = (e: unknown): string =>
157
+ e instanceof Error ? (e.stack ?? `${e.name}: ${e.message}`) : String(e)
158
+ let out = text(err)
159
+ const seen = new Set<unknown>([err])
160
+ let at = err instanceof Error ? err.cause : undefined
161
+ for (let depth = 0; at !== undefined && at !== null && !seen.has(at) && depth < 8; depth++) {
162
+ seen.add(at)
163
+ out += `\nCaused by: ${at instanceof Error ? text(at) : Bun.inspect(at)}`
164
+ at = at instanceof Error ? at.cause : undefined
165
+ }
166
+ return out
167
+ }
168
+
129
169
  // The loop drained while the verb was still pending: something it awaits
130
170
  // can never settle (a plugin hook whose promise is never resolved), and
131
171
  // with no exit code set Bun exits 0 — a run whose task failed reported
@@ -134,8 +174,9 @@ async function exitOnceFlushed(): Promise<void> {
134
174
  process.on('beforeExit', () => {
135
175
  if (settled) return
136
176
  settled = true
137
- process.stderr.write(
138
- 'vx: the run stopped before it finished: something it awaited can never settle (a plugin hook?)\n',
177
+ ;(require('./util/index.js') as typeof import('./util/index.js')).ui.say(
178
+ 'error',
179
+ 'vx: the run stopped before it finished: something it awaited can never settle (a plugin hook?)',
139
180
  )
140
181
  process.exitCode = 1
141
182
  })