@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/src/cli/help.ts CHANGED
@@ -102,19 +102,19 @@ export function verbHelpText(verb: string): string {
102
102
 
103
103
  export function helpText(pluginCommands: readonly string[] = []): string {
104
104
  return [
105
- 'vx — open, extensible monorepo task runner',
105
+ 'vx — task orchestration you own',
106
106
  '',
107
107
  'Usage:',
108
108
  ' vx run [OPTIONS] [TASK | PKG#TASK ...] [-- forwarded-args...]',
109
- ' vx watch [OPTIONS] TASK [-- forwarded-args...]',
110
- ' vx cache prune [--older-than <duration>] [--max-size <size>] [--dry-run] [--format pretty|json] [--cache-dir <path>]',
109
+ ' vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]',
110
+ ' vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry-run] [--format pretty|json] [--cache-dir <path>]',
111
111
  ' vx lock [--check] [--format pretty|json]',
112
112
  ' vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <seam>]',
113
113
  ' vx upgrade [TAG]',
114
114
  ' vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]',
115
115
  ' vx info [--format pretty|json] [--cache-dir <path>]',
116
116
  ' vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]',
117
- ' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--format pretty|json] [--cache-dir <path>]',
117
+ ' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--format pretty|json] [--cache-dir <path>]',
118
118
  ' vx docs <query> [--limit N] [--format pretty|json]',
119
119
  ' vx completions bash|zsh|fish',
120
120
  ' vx help [VERB]',
@@ -126,54 +126,80 @@ export function helpText(pluginCommands: readonly string[] = []): string {
126
126
  ' --filter <pat> pnpm-style filter (repeatable). Examples:',
127
127
  ' foo, @scope/*, ./packages/foo, foo..., ...foo,',
128
128
  ' foo^..., !foo, [<since>]',
129
- ' --affected[=<base>] Sugar for --filter "...[<base>]". Default base =',
130
- ' affectedBase, else origin/HEAD, else main or',
131
- ' master, else HEAD~1.',
129
+ ' --affected[=<base>] Projects changed since <base>, and dependents',
130
+ ' (--filter "...[<base>]"); default run.affectedBase,',
131
+ ' else origin/HEAD, main, master, HEAD~1.',
132
132
  " pkg#task Run a specific project's task directly.",
133
133
  '',
134
134
  'Execution (for run):',
135
- ' --concurrency <n> Max parallel tasks (default: the cores this process may use); `50%` = half of them.',
136
- ' --exclude-dependencies[=names] Skip dependsOn edges. No value = all; comma list = specific names.',
137
- ' --no-cache Disable caching entirely (no reads, no writes, outputs left alone).',
138
- ' --force Re-execute everything (skip reads) but still refresh the cache (writes on).',
139
- ' --cache <spec> Per-layer read/write control. Comma list of <layer>:<flags>',
140
- ' (layer = local|remote, flags ⊆ rw). A named layer is set EXACTLY',
141
- ' to its flags; unnamed layers keep their value. Examples:',
142
- ' local:rw,remote:r (remote read-only), remote: (remote off),',
143
- ' local:r (local read-only).',
144
- ' --cache-dir <path> Cache directory override (cwd-relative). Beats VX_CACHE_DIR,',
145
- ' the workspace cacheDir field and the ~/.vx/<id>/cache default.',
146
- ' --retry <n> Re-run a failed task up to <n> more times (default for tasks',
147
- " without their own exec.retries; explicit config wins). Doesn't",
148
- ' affect cache keys.',
149
- ' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
150
- ' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
151
- ' a runaway task. Per-task exec.timeout always wins.',
152
- ' --continue[=<mode>] What a failed task takes down: never | deps-ok (default, skips',
153
- ' its dependents) | always (bare form; dependents run, never save).',
154
- ' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
155
- ' --output-logs <mode> full | errors-only | hash-only | none — overrides the flow default',
156
- ' (focused without --all/--filter/--affected, broad with, full in CI).',
157
- ' --download <mode> all (default) | toplevel | none — where remote outputs land.',
158
- ' --verbosity <n> 0 (default) or 1+: print a per-task summary table after the run.',
135
+ ' Each setting flag beats its VX_* variable, which beats .vx/workspace',
136
+ ' (docs/schema.md § Settings).',
137
+ ' --concurrency <n> Max parallel tasks (default: usable cores);',
138
+ ' 50% means half of them.',
139
+ ' --schedule <rule> most-work (default) | critical-path |',
140
+ ' direct-dependents | ready-order.',
141
+ ' --branch <name> The branch history records and forecasts read',
142
+ " (default: git's, else the CI provider's).",
143
+ ' --exclude-dependencies[=names] Skip dependsOn edges: all with no value,',
144
+ ' or a comma list of names.',
145
+ ' --no-cache No cache reads or writes; outputs are left',
146
+ ' alone.',
147
+ ' --force Skip cache reads but still write: every task',
148
+ ' runs and refreshes its entry.',
149
+ ' --cache-policy <spec> Per-layer read/write: a comma list of',
150
+ ' <layer>:<flags> (layer local or remote, flags',
151
+ ' from rw). A named layer is set to exactly its',
152
+ ' flags; unnamed layers keep theirs. Examples:',
153
+ ' local:rw,remote:r (remote read-only), remote:',
154
+ ' (remote off), local:r (local read-only).',
155
+ " --cache-scope <name> This run's cache.scope (pr-123): write there,",
156
+ ' read the default keys first.',
157
+ ' --cache-dir <path> Cache directory (cwd-relative); default',
158
+ ' .vx/cache, entries in ~/.vx/<id>/cache.',
159
+ ' --cache-download <mode> all (default) | toplevel | none:',
160
+ ' where remote outputs land.',
161
+ ' --retries <n> Up to <n> more runs of a failed task, for',
162
+ ' tasks without their own exec.retries. Not',
163
+ ' folded into cache keys.',
164
+ " --timeout <duration> A task's time limit (30s, 10m), for tasks",
165
+ ' without their own exec.timeout. A task past it',
166
+ ' is killed and fails.',
167
+ ' --kill-grace <duration> How long a task that ignores SIGTERM gets',
168
+ ' before SIGKILL (default 2s).',
169
+ " --teardown-timeout <duration> The bound on each plugin's end-of-run flush",
170
+ ' (default 3s).',
171
+ ' --ready-notice <duration> When vx says a persistent task still waits for',
172
+ ' readyWhen (default 10s).',
173
+ ' --continue[=<mode>] What a failed task takes down: never | deps-ok',
174
+ ' (default, skips its dependents) | always (bare',
175
+ ' form; dependents run, never save).',
176
+ ' --output-logs <mode> full | errors-only | hash-only | none, over',
177
+ ' the flow default (focused without',
178
+ ' --all/--filter/--affected, broad with, full in',
179
+ ' CI).',
180
+ ' --verbosity <n> 0 (default), or 1 and up for a per-task',
181
+ ' summary table after the run.',
159
182
  '',
160
183
  'Planning (for run — skips execution):',
161
- ' --dry[=text|json] Print the task graph + cache hit/miss prediction.',
184
+ " --dry[=text|json] Print the task graph and each task's predicted hit",
185
+ ' or miss.',
162
186
  ' --graph[=<path>] Emit Graphviz DOT (stdout if no path).',
163
187
  '',
164
188
  'Artifacts (for run):',
165
- ' --format <fmt> pretty (default) | json: print the --summarize document',
166
- ' on stdout when the run ends; every other line goes to stderr.',
189
+ ' --format <fmt> pretty (default) | json: print the --summarize',
190
+ ' document on stdout when the run ends; every other',
191
+ ' line goes to stderr.',
167
192
  ' --summarize[=<path>] Write per-run JSON to <cacheDir>/runs/<run_id>.json.',
168
193
  ' --profile[=<path>] Write Chrome-trace JSON (default profile.json).',
169
194
  ' --report[=markdown] Print a markdown run report to stdout after the run.',
170
- ' --report-file <path> Append that report to <path> (for $GITHUB_STEP_SUMMARY).',
171
- ' --tag <k=v> Label this invocation (repeatable); recorded on its history row.',
195
+ ' --report-file <path> Append that report to <path> ($GITHUB_STEP_SUMMARY).',
196
+ ' --tag <k=v> Label this invocation (repeatable); recorded on its',
197
+ ' history row.',
172
198
  '',
173
199
  'Extensions (plugins):',
174
200
  ' vx core runs tasks in-process and nothing else. A dashboard, remote',
175
201
  ' cache, distributed execution, and telemetry export are provided by',
176
- ' PLUGINS declared in vx.workspace.ts — first-party or your own. Core',
202
+ ' PLUGINS declared in .vx/workspace.ts — first-party or your own. Core',
177
203
  ' depends on none of them; a plain `vx run` never requires a plugin.',
178
204
  ' See the plugin guide: https://vznjs.github.io/vx/guides/plugins/',
179
205
  '',
@@ -192,24 +218,27 @@ export function helpText(pluginCommands: readonly string[] = []): string {
192
218
  ' Uses the same flags as `vx run` except --dry /',
193
219
  ' --graph / --summarize / --profile / --report /',
194
220
  ' --report-file / --verbosity (they plan or report',
195
- ' one run). Press Ctrl+C to stop. Cache means most',
221
+ ' one run), plus --poll (watch.poll: poll instead of',
222
+ ' the OS watcher). Press Ctrl+C to stop. Cache means most',
196
223
  ' re-runs are near-zero cost (cache hits restore',
197
224
  ' outputs + replay logs).',
198
225
  '',
199
226
  'Cache management:',
200
- ' vx cache prune --older-than 30d Evict entries last accessed > 30 days ago.',
201
- ' vx cache prune --max-size 1G Keep total cache under 1 GB (LRU eviction).',
227
+ ' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
228
+ ' vx cache clean --max-size 1G Keep total cache under 1 GB (LRU eviction).',
229
+ " vx cache clean Clear .vx/cache and this repo's store ~/.vx/<id>/cache.",
230
+ " --global Clear every repo's store under ~/.vx.",
202
231
  ' --dry-run Say what the policy would reap; delete nothing.',
203
232
  ' --format <fmt> pretty (default) | json.',
204
233
  ' --cache-dir <path> The cache a run with the same flag uses (why, last, info too).',
205
234
  '',
206
- ' Duration units: s, m, h, d. Size units: K, M, G, T (powers of 1024).',
235
+ ' Duration units: ms, s, m, h, d. Size units: K, M, G, T (powers of 1024).',
207
236
  ' Cache statistics — directory, entry count, size — are part of `vx info`.',
208
237
  '',
209
238
  'Introspection:',
210
239
  ' vx show List every project: name, dir, declared task count.',
211
- " vx show <project> Print the project's LIVE resolved config (fresh",
212
- ' evaluation, not the lock), one block per task.',
240
+ " vx show <project> Print the project's resolved config as a run reads",
241
+ ' it from the lock, one block per task.',
213
242
  " vx show <pkg>#<task> Print a single task's resolved config.",
214
243
  ' vx show <task> That task in every project declaring it.',
215
244
  " --filter <pattern> The list (or a task's projects) as `vx run --filter` selects.",
@@ -232,25 +261,27 @@ export function helpText(pluginCommands: readonly string[] = []): string {
232
261
  ' --failed Only failed runs: the latest one, or with --list the list.',
233
262
  " --log <task> That task's output in the run (latest that ran it): a failure's",
234
263
  ' or the cached log; none for an uncached pass.',
264
+ ' --stats [<task>] Per-task p50/p99, pass and hit rate, failure mode, peak',
265
+ ' RSS and CPU over the last 50 runs; a task id or name narrows.',
266
+ " --branch <name> The branch --stats' p50 forecasts from (default: the run's).",
235
267
  ' --format <fmt> pretty (default) | json.',
236
268
  ' info, why and last:',
237
269
  ' --cache-dir <path> Read the cache a run with the same flag uses.',
238
270
  '',
239
271
  'Migration:',
240
- ' vx init Generate vx.workspace.ts + one vx.config.ts per package',
241
- ' from package.json scripts. Unmappable settings become',
242
- ' TODO(vx-migrate) comments. Beside turbo.json or nx.json:',
243
- ' runs @vzn/vx-migrate, which asks native or keep.',
272
+ ' vx init With no package.json: write .vx/workspace.ts and an empty',
273
+ ' root vx.config.ts. With one: runs @vzn/vx-migrate, which maps',
274
+ ' turbo.json, an Nx graph or package.json scripts to one',
275
+ ' vx.config.ts per package (asking native or keep in Turbo/Nx).',
244
276
  ' --dry Print the generated files instead of writing them.',
245
277
  ' --format json With --dry: the plan as one JSON document (schemas/init.json).',
246
278
  ' --force Overwrite existing vx.config.* files.',
247
279
  ' --mjs Write .mjs files (no type import) instead of .ts.',
248
280
  ' --native Turbo or Nx: write a vx.config.ts per package (no question).',
249
- ' --keep Turbo or Nx: only vx.workspace.ts, declaring turbo() or nx().',
281
+ ' --keep Turbo or Nx: only .vx/workspace.ts, declaring turbo() or nx().',
250
282
  ' --plugin <seam> Instead: write plugins/<seam>.ts, a runnable plugin, and its',
251
283
  ' test (executor, cache, telemetry, schedule, admit, commands,',
252
284
  ' project, graph, key).',
253
- ' From turbo.json or an Nx project graph: `vx init` runs `@vzn/vx-migrate` (its own package).',
254
285
  '',
255
286
  'Shell completions:',
256
287
  ' vx completions <shell> Print a completion script for bash, zsh or fish: the',
@@ -267,14 +298,15 @@ export function helpText(pluginCommands: readonly string[] = []): string {
267
298
  ' --format json {query, hits: [{page, heading, url, text}]}.',
268
299
  '',
269
300
  'Config lock:',
270
- ' vx lock Evaluate every vx.config in the current environment and',
271
- ' freeze the resolved objects into vx-lock.json. Plain runs',
272
- ' always evaluate live; only `vx run --frozen` consumes the',
273
- ' lock (CI reproducibility, frozen-env semantics).',
301
+ ' vx lock Evaluate every vx.config in the current environment, check',
302
+ ' the graph rules (run.rules) and freeze the resolved objects',
303
+ ' into .vx/projects.lock.json, the only place a run reads configs',
304
+ ' from. A run never evaluates one, never checks the rules',
305
+ ' and never writes the lock: with none it fails.',
274
306
  ' vx lock --check Audit the lock: file-changed hash report PLUS a full',
275
- ' re-evaluation compared against the frozen objects. Exits 1',
276
- ' on drift (e.g. configs that read env vars). Run it before',
277
- ' `vx run --frozen` in CI. With --format json it prints',
307
+ ' re-evaluation compared against the frozen objects, and the',
308
+ ' graph rules. Exits 1 on drift (e.g. configs that read env',
309
+ ' vars). Run it before the run in CI. With --format json it prints',
278
310
  ' {upToDate, audited, notAudited, problems}.',
279
311
  '',
280
312
  ...(pluginCommands.length > 0 ? ['Plugin commands:', ...pluginCommands, ''] : []),
@@ -313,7 +345,7 @@ export function refusedWord(arg: string): string {
313
345
  * `(did you mean --concurrency?)` for a flag within two edits of one `verb`
314
346
  * accepts; empty for anything else (a positional included). A third edit
315
347
  * is allowed only between flags that share their first five characters,
316
- * so `--retries` reaches `--retry` while `--zzz` does not reach `--all`.
348
+ * so `--timeouts` reaches `--timeout` while `--zzz` does not reach `--all`.
317
349
  */
318
350
  export function flagHint(verb: string, arg: string): string {
319
351
  if (!arg.startsWith('-')) return ''
@@ -341,8 +373,8 @@ export function flagHint(verb: string, arg: string): string {
341
373
  * The documented `--flags` of one verb, read from the help text's
342
374
  * sections headed `… (for <verb>):` so there is no second list to drift —
343
375
  * and no hint that names another verb's flag. Only the flag an option line
344
- * OPENS with is one: `--frozen`'s line says "pair with vx lock --check", and
345
- * reading every flag on it put `--check` in the list, so `vx run --chek`
376
+ * OPENS with is one: an option line that named `vx lock --check` once put
377
+ * `--check` in the list, so `vx run --chek`
346
378
  * was told "did you mean --check?" and `--check` was then refused.
347
379
  */
348
380
  export function documentedFlags(verb: string, text = helpText()): string[] {
@@ -352,8 +384,9 @@ export function documentedFlags(verb: string, text = helpText()): string[] {
352
384
  if (/^[A-Z][A-Za-z ]*(?: \(for [a-z]+\))?:$/.test(line))
353
385
  inVerb = line.endsWith(`(for ${verb}):`)
354
386
  if (!inVerb) continue
355
- const own = /^\s+(--[a-zA-Z][a-zA-Z-]*)/.exec(line)
356
- if (own !== null) flags.add(own[1]!)
387
+ // `--x, --no-x`: a flag and its negation.
388
+ const own = /^\s+(--[a-zA-Z][a-zA-Z-]*)(?:, (--[a-zA-Z][a-zA-Z-]*))?/.exec(line)
389
+ if (own !== null) for (const f of own.slice(1)) if (f !== undefined) flags.add(f)
357
390
  }
358
391
  return [...flags]
359
392
  }
package/src/cli/index.ts CHANGED
@@ -2,13 +2,16 @@
2
2
  // handler lives in a sibling `<name>.ts`; tests import its parsers there.
3
3
 
4
4
  import { VERSION } from '../version.js'
5
+ import { checkSettingsEnv } from '../settings.js'
5
6
  import { CORE_VERBS, flagHint, printHelp, refusedWord, seeHelp } from './help.js'
6
7
  import { FOREIGN_VERBS } from './foreign-flags.js'
7
8
  import {
8
9
  errorDocument,
10
+ hideEnvFromTasks,
11
+ isFsRefusal,
12
+ isOutOfFds,
9
13
  isUserError,
10
14
  maskedLine,
11
- MOVED_VERBS,
12
15
  nearest,
13
16
  UserError,
14
17
  wantsJson,
@@ -24,6 +27,7 @@ const plugins = () => import('./plugin-commands.js')
24
27
 
25
28
  export async function run(argv: readonly string[]): Promise<number> {
26
29
  const [command, ...rest] = argv
30
+ await hideEnvFromTasks()
27
31
 
28
32
  // `vx <verb> --help` is the universal reflex, and every verb answered
29
33
  // `unknown flag: --help` and exited 1 (walkthrough, 2026-09-04). Only args
@@ -34,7 +38,33 @@ export async function run(argv: readonly string[]): Promise<number> {
34
38
  await printHelp(await (await plugins()).pluginCommandHelp(), command)
35
39
  return 0
36
40
  }
41
+ // Each setting's variable once, before any verb: a renamed one is named,
42
+ // and a deep reader trusts what it reads.
43
+ checkSettingsEnv()
37
44
 
45
+ try {
46
+ return await dispatch(command, rest)
47
+ } catch (err) {
48
+ throw await brokenLockNamed(err)
49
+ }
50
+ }
51
+
52
+ /**
53
+ * A crash from a hand-broken lock entry is that entry's fault: runs validate
54
+ * no entry, so it surfaced as a raw TypeError (M, 2026-10-14). The lock is
55
+ * read only on the crash path, so a normal run pays nothing.
56
+ */
57
+ async function brokenLockNamed(err: unknown): Promise<unknown> {
58
+ if (isUserError(err) || isFsRefusal(err) || isOutOfFds(err)) return err
59
+ try {
60
+ const { findWorkspaceRoot, LOCK_INVALID, lockEntryBroken } =
61
+ await import('../workspace/index.js')
62
+ if (lockEntryBroken(await findWorkspaceRoot(process.cwd()))) return new UserError(LOCK_INVALID)
63
+ } catch {}
64
+ return err
65
+ }
66
+
67
+ async function dispatch(command: string | undefined, rest: string[]): Promise<number> {
38
68
  switch (command) {
39
69
  case undefined: {
40
70
  // A bare `vx` where no workspace is printed 151 lines of help, none
@@ -65,7 +95,7 @@ export async function run(argv: readonly string[]): Promise<number> {
65
95
  // for the whole reference: it printed 145 lines and no hint.
66
96
  const resolved = await (await plugins()).resolvePluginCommand(verb)
67
97
  const unknown = resolved === null || 'declaredVerbs' in resolved
68
- const pointer = MOVED_VERBS[verb] ?? FOREIGN_VERBS[verb]
98
+ const pointer = FOREIGN_VERBS[verb]
69
99
  if (unknown && pointer !== undefined) {
70
100
  process.stderr.write(`${pointer}\n`)
71
101
  return 1
@@ -171,12 +201,11 @@ export async function run(argv: readonly string[]): Promise<number> {
171
201
  }
172
202
  const loadNote =
173
203
  resolved !== null && 'loadError' in resolved
174
- ? `\n (plugin verbs could not be looked up: vx.workspace failed to load: ${resolved.loadError})`
204
+ ? `\n (plugin verbs could not be looked up: .vx/workspace failed to load: ${resolved.loadError})`
175
205
  : ''
176
- // A verb core owned once (`migrate`, `prune`) and a package owns now:
177
- // the pointer, but only after the plugins had their chance — a
178
- // workspace that declares a plugin verb of that name keeps it.
179
- const moved = MOVED_VERBS[command] ?? FOREIGN_VERBS[command]
206
+ // Another tool's verb: the pointer, but only after the plugins had
207
+ // their chance — a workspace that declares a plugin verb of that name keeps it.
208
+ const moved = FOREIGN_VERBS[command]
180
209
  if (moved !== undefined) {
181
210
  return unknown(`${moved}${loadNote}\n`)
182
211
  }
@@ -198,7 +227,7 @@ export async function run(argv: readonly string[]): Promise<number> {
198
227
  if (command === 'serve' || command === 'dev') {
199
228
  // vx core is only a task runner — it has no service layer of its
200
229
  // own. A dashboard, remote cache, distributed execution, etc. are
201
- // provided by PLUGINS (declared in vx.workspace.ts), never by core.
230
+ // provided by PLUGINS (declared in .vx/workspace.ts), never by core.
202
231
  // We keep this neutral hint for the common muscle-memory verbs, but
203
232
  // core names no specific plugin package: any package can provide
204
233
  // these.
@@ -213,10 +242,10 @@ export async function run(argv: readonly string[]): Promise<number> {
213
242
  // help after a typo was a hundred lines past the hint that mattered.
214
243
  // The verbs this workspace's plugins declare are verbs HERE, so they
215
244
  // join the "did you mean" set, and when nothing is close the second
216
- // line says where a verb can come from: `vx mpc` in a workspace
217
- // declaring `mcp` read as a plain unknown command, and `vx mcp`
218
- // before the plugin was declared said nothing about the file that
219
- // would declare it (2026-09-20).
245
+ // line says where a verb can come from: a typo of a plugin verb read
246
+ // as a plain unknown command, and a plugin verb before the plugin was
247
+ // declared said nothing about the file that would declare it
248
+ // (2026-09-20).
220
249
  // A word that opens with a dash is a flag, and vx itself takes only
221
250
  // --help and --version: "unknown command: --bogus" named the wrong
222
251
  // thing (M-60).
@@ -268,5 +297,5 @@ function verbSourceNote(resolved: unknown, declaredVerbs: readonly string[]): st
268
297
  return ` This workspace's plugins declare: ${[...declaredVerbs].sort().join(', ')}.\n`
269
298
  }
270
299
  const here = resolved === null ? 'there is no workspace here' : 'this workspace declares none'
271
- return ` A plugin declared in vx.workspace.ts can add verbs; ${here}.\n`
300
+ return ` A plugin declared in .vx/workspace.ts can add verbs; ${here}.\n`
272
301
  }
package/src/cli/info.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  // `vx info` — workspace doctor printout. One screen of environment +
2
2
  // workspace + cache facts for bug reports and quick sanity checks.
3
3
  // The facts come
4
- // from the orchestrator's `collectInfo` (doctor.ts), which `vx mcp` reads
5
- // too; this file parses the flags and renders the rows.
4
+ // from the orchestrator's `collectInfo` (doctor.ts); this file parses the flags and renders the rows.
6
5
 
7
6
  import os from 'node:os'
8
7
  import { collectInfo, type FlakyTask, type InfoFacts } from '../orchestrator/index.js'
@@ -10,6 +9,7 @@ import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
10
9
  import { namedCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
11
10
  import { formatBytes } from './format.js'
12
11
  import { MIN_BUN } from '../util/index.js'
12
+ import { envName } from '../settings.js'
13
13
 
14
14
  interface InfoArgs {
15
15
  format: 'pretty' | 'json'
@@ -46,7 +46,7 @@ export function parseInfoArgs(args: readonly string[]): InfoArgs {
46
46
  export async function infoCmd(args: readonly string[]): Promise<number> {
47
47
  const parsed = parseInfoArgs(args)
48
48
  if (parsed.error) return refuse('info', args, parsed.error, 'VX_E_USAGE')
49
- // `collectInfo` takes a resolved directory from `vx mcp` too, where one
49
+ // `collectInfo` takes a resolved directory, where one
50
50
  // not made yet is a workspace that never ran; the flag is the user's,
51
51
  // and a directory it names that is not there is a typo (item 900).
52
52
  if (parsed.cacheDir !== undefined) namedCacheDir(parsed.cacheDir)
@@ -55,14 +55,17 @@ export async function infoCmd(args: readonly string[]): Promise<number> {
55
55
  warn: warnToStderr,
56
56
  })
57
57
  process.stdout.write(
58
- parsed.format === 'json' ? `${JSON.stringify(facts, null, 2)}\n` : `${renderInfo(facts)}\n`,
58
+ parsed.format === 'json' ? `${JSON.stringify(facts)}\n` : `${renderInfo(facts)}\n`,
59
59
  )
60
60
  return 0
61
61
  }
62
62
 
63
63
  export function renderInfo(f: InfoFacts): string {
64
64
  const rows: [string, string][] = [
65
- ['vx', f.vx],
65
+ [
66
+ 'vx',
67
+ `${f.vx} (${tildePath(f.vxPath)}${f.handedOffFrom === null ? '' : `, handed off from ${tildePath(f.handedOffFrom)}`})`,
68
+ ],
66
69
  // The prose stays in the rendered row; `f.bun` itself is the bare
67
70
  // version a script reads (doctor.ts says why).
68
71
  [
@@ -75,24 +78,25 @@ export function renderInfo(f: InfoFacts): string {
75
78
  ['git status cache', renderGitStatusCache(f.gitStatusCache)],
76
79
  ['workspace root', tildePath(f.workspaceRoot)],
77
80
  ['projects', describeProjects(f)],
78
- // Only when a config did not load: the count above says "zero" for
79
- // it, and this row says which and why.
80
- ...(f.configErrors.length > 0
81
- ? ([['config errors', f.configErrors.map((e) => `${e.path}: ${e.message}`).join('; ')]] as [
82
- string,
83
- string,
84
- ][])
85
- : []),
86
81
  ['plugins', describePlugins(f.plugins)],
87
82
  ['workers', describeWorkers(f.workers)],
88
83
  ['memory', describeMemory(f.memory)],
89
- ['cache dir', tildePath(f.cacheDir)],
84
+ [
85
+ 'cache dir',
86
+ `${tildePath(f.cacheDir)} (${formatBytes(f.cacheDirBytes)}, ${f.cacheStore === null ? 'the entries and ' : ''}this workspace's run history)`,
87
+ ],
90
88
  [
91
89
  'cache store',
92
- f.cacheStore === null ? 'none: the cache dir holds the entries' : tildePath(f.cacheStore),
90
+ f.cacheStore === null
91
+ ? 'none: the cache dir holds the entries'
92
+ : `${tildePath(f.cacheStore)} (${formatBytes(f.cacheStoreBytes ?? 0)}, shared by this repo's checkouts)`,
93
93
  ],
94
94
  ['cache versions', `keys ${f.cacheVersion} · index schema ${f.schemaVersion}`],
95
95
  ['cache entries', describeEntries(f)],
96
+ // Only the settings a variable or the workspace changed.
97
+ ...(Object.values(f.settings).some((s) => s.source !== 'default')
98
+ ? ([['settings', describeSettings(f.settings)]] as [string, string][])
99
+ : []),
96
100
  ...(f.cacheRetention !== null
97
101
  ? ([['cache retention', describeRetention(f.cacheRetention)]] as [string, string][])
98
102
  : []),
@@ -101,7 +105,7 @@ export function renderInfo(f: InfoFacts): string {
101
105
  ? ([
102
106
  [
103
107
  'orphans',
104
- `${f.orphans.artifacts} artifact${f.orphans.artifacts === 1 ? '' : 's'} (${formatBytes(f.orphans.bytes)}) the index does not know — \`vx cache prune\` reaps them`,
108
+ `${f.orphans.artifacts} artifact${f.orphans.artifacts === 1 ? '' : 's'} (${formatBytes(f.orphans.bytes)}) the index does not know — \`vx cache clean\` reaps them`,
105
109
  ],
106
110
  ] as [string, string][])
107
111
  : []),
@@ -116,7 +120,7 @@ export function renderInfo(f: InfoFacts): string {
116
120
  ],
117
121
  ['flaky tasks', describeFlakyTasks(f.flakyTasks)],
118
122
  ['sandbox', describeSandbox(f.sandbox)],
119
- ['vx-lock.json', f.lockfile ? 'yes' : 'no'],
123
+ ['projects lock', f.lockfile ? 'yes' : 'no'],
120
124
  ]
121
125
  const labelW = Math.max(...rows.map(([label]) => label.length))
122
126
  return rows.map(([label, value]) => `${`${label}:`.padEnd(labelW + 1)} ${value}`).join('\n')
@@ -132,6 +136,20 @@ function describeEntries(f: InfoFacts): string {
132
136
  return `${f.cacheEntries} (${size})`
133
137
  }
134
138
 
139
+ /**
140
+ * The settings not at their default, with where each came from:
141
+ * `run.timeout 30m (VX_RUN_TIMEOUT), cache.policy local:rw (.vx/workspace)`.
142
+ */
143
+ function describeSettings(settings: InfoFacts['settings']): string {
144
+ return Object.entries(settings)
145
+ .filter(([, s]) => s.source !== 'default')
146
+ .map(
147
+ ([path, s]) =>
148
+ `${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'})`,
149
+ )
150
+ .join(', ')
151
+ }
152
+
135
153
  /** What a run's end evicts: `entries unused for 30d, and the oldest entries past 10G`. */
136
154
  function describeRetention(r: NonNullable<InfoFacts['cacheRetention']>): string {
137
155
  const parts = [
@@ -163,10 +181,10 @@ function describeSandbox(s: InfoFacts['sandbox']): string {
163
181
  return `unavailable — ${s.reason}; ${declared}${s.declared > 0 ? ' and will fail' : ''}`
164
182
  }
165
183
 
166
- /** `4 — the CPU count`, `2 — cgroup CPU quota 2 of 8 cores`, `8 — vx.workspace.ts (4 cores)`. */
184
+ /** `4 — the CPU count`, `2 — cgroup CPU quota 2 of 8 cores`, `8 — .vx/workspace.ts (4 cores)`. */
167
185
  export function describeWorkers(w: InfoFacts['workers']): string {
168
186
  const quota = w.cpuQuota === null ? '' : `, cgroup CPU quota ${trimCores(w.cpuQuota)}`
169
- if (w.source === 'workspace') return `${w.count} — vx.workspace.ts (${w.cores} cores${quota})`
187
+ if (w.source === 'workspace') return `${w.count} — .vx/workspace.ts (${w.cores} cores${quota})`
170
188
  if (w.source === 'cgroup')
171
189
  return `${w.count} — cgroup CPU quota ${trimCores(w.cpuQuota ?? w.count)} of ${w.cores} cores`
172
190
  return `${w.count} — the CPU count${quota}`
@@ -186,10 +204,7 @@ function trimCores(n: number): string {
186
204
 
187
205
  /** `2 — app#test (3 of 7 runs failed on unchanged inputs); api#e2e (1 of 4)`, or `none`. */
188
206
  function describeProjects(f: InfoFacts): string {
189
- const tasks = `${f.tasks} task${f.tasks === 1 ? '' : 's'}`
190
- const n = f.configErrors.length
191
- if (n === 0) return `${f.projects} (${tasks})`
192
- return `${f.projects} (${tasks} · ${n} config${n === 1 ? '' : 's'} did not load)`
207
+ return `${f.projects} (${f.tasks} task${f.tasks === 1 ? '' : 's'})`
193
208
  }
194
209
 
195
210
  function describeFlakyTasks(tasks: readonly FlakyTask[]): string {