@vzn/vx 0.0.636 → 0.0.638

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 (151) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +246 -213
  3. package/docs/cli.md +791 -705
  4. package/docs/execution.md +68 -76
  5. package/docs/features.md +47 -42
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +392 -205
  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 +4 -4
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +28 -13
  19. package/src/bin.ts +30 -9
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +124 -53
  22. package/src/cache/chained-cache.ts +4 -0
  23. package/src/cache/git-inputs.ts +277 -63
  24. package/src/cache/index.ts +15 -1
  25. package/src/cache/inputs.ts +102 -36
  26. package/src/cache/key-fold.ts +8 -7
  27. package/src/cache/layer.ts +15 -18
  28. package/src/cache/layered-cache.ts +20 -102
  29. package/src/cache/plugin-cache.ts +248 -0
  30. package/src/cache/policy.ts +32 -27
  31. package/src/cache/run-history.ts +104 -14
  32. package/src/cache/run-store.ts +77 -0
  33. package/src/cache/schema.ts +58 -31
  34. package/src/cache/scoped-cache.ts +237 -0
  35. package/src/cli/cache.ts +173 -34
  36. package/src/cli/completions.ts +11 -7
  37. package/src/cli/core-alias.ts +1 -1
  38. package/src/cli/foreign-flags.ts +13 -12
  39. package/src/cli/help.ts +110 -70
  40. package/src/cli/index.ts +67 -24
  41. package/src/cli/info.ts +43 -27
  42. package/src/cli/init.ts +115 -407
  43. package/src/cli/last.ts +157 -9
  44. package/src/cli/lock.ts +356 -122
  45. package/src/cli/plan-format.ts +7 -5
  46. package/src/cli/plugin-commands.ts +81 -14
  47. package/src/cli/plugin-templates.ts +11 -11
  48. package/src/cli/run.ts +116 -160
  49. package/src/cli/select.ts +50 -47
  50. package/src/cli/show.ts +83 -59
  51. package/src/cli/task-verb.ts +6 -13
  52. package/src/cli/upgrade.ts +415 -149
  53. package/src/cli/watch-cycle.ts +9 -2
  54. package/src/cli/watch-filter.ts +39 -61
  55. package/src/cli/watch-fs.ts +29 -16
  56. package/src/cli/watch-judge.ts +18 -4
  57. package/src/cli/watch-set.ts +16 -42
  58. package/src/cli/watch.ts +227 -230
  59. package/src/cli/why.ts +38 -18
  60. package/src/cli/workspace-config.ts +33 -70
  61. package/src/config.ts +169 -70
  62. package/src/exec/env.ts +1 -31
  63. package/src/exec/executor.ts +1 -5
  64. package/src/exec/kill-tree.ts +2 -1
  65. package/src/exec/sandbox-runtime.ts +12 -1
  66. package/src/exec/sandbox-violations.ts +9 -1
  67. package/src/graph/dependency-spec.ts +1 -1
  68. package/src/graph/index.ts +1 -0
  69. package/src/graph/priorities.ts +1 -1
  70. package/src/graph/scheduler.ts +27 -7
  71. package/src/graph/task-graph.ts +76 -24
  72. package/src/handoff.ts +169 -0
  73. package/src/index.ts +35 -58
  74. package/src/orchestrator/affected-tasks.ts +15 -8
  75. package/src/orchestrator/command-windows.ts +44 -0
  76. package/src/orchestrator/deferred-outputs.ts +12 -10
  77. package/src/orchestrator/doctor.ts +98 -110
  78. package/src/orchestrator/download-policy.ts +3 -3
  79. package/src/orchestrator/events.ts +19 -3
  80. package/src/orchestrator/excluded-keys.ts +0 -2
  81. package/src/orchestrator/execute-task.ts +147 -69
  82. package/src/orchestrator/failure-mode.ts +19 -10
  83. package/src/orchestrator/fingerprint-watch.ts +18 -15
  84. package/src/orchestrator/forecast.ts +10 -2
  85. package/src/orchestrator/framed-output.ts +9 -5
  86. package/src/orchestrator/graph-stage.ts +5 -5
  87. package/src/orchestrator/history.ts +126 -30
  88. package/src/orchestrator/hit-restore.ts +15 -3
  89. package/src/orchestrator/index.ts +20 -5
  90. package/src/orchestrator/local-shortcircuit.ts +2 -1
  91. package/src/orchestrator/lockfile-claim.ts +79 -20
  92. package/src/orchestrator/logger.ts +80 -41
  93. package/src/orchestrator/metrics.ts +78 -9
  94. package/src/orchestrator/miss-save.ts +3 -0
  95. package/src/orchestrator/options.ts +42 -29
  96. package/src/orchestrator/output-host.ts +72 -0
  97. package/src/orchestrator/plan.ts +6 -8
  98. package/src/orchestrator/plugin-host.ts +100 -66
  99. package/src/orchestrator/plugin.ts +173 -67
  100. package/src/orchestrator/prepare.ts +163 -78
  101. package/src/orchestrator/projects.ts +105 -99
  102. package/src/orchestrator/remote-prefetch.ts +11 -6
  103. package/src/orchestrator/run-context.ts +25 -6
  104. package/src/orchestrator/run-failures.ts +17 -8
  105. package/src/orchestrator/run-lock.ts +1 -1
  106. package/src/orchestrator/run-records.ts +1 -1
  107. package/src/orchestrator/run-report.ts +9 -9
  108. package/src/orchestrator/run-settings.ts +48 -0
  109. package/src/orchestrator/run.ts +244 -119
  110. package/src/orchestrator/sandbox-fix.ts +39 -20
  111. package/src/orchestrator/sandbox-request.ts +84 -13
  112. package/src/orchestrator/signals.ts +1 -1
  113. package/src/orchestrator/stable-keys.ts +13 -11
  114. package/src/orchestrator/status-line.ts +49 -7
  115. package/src/orchestrator/summary.ts +115 -106
  116. package/src/orchestrator/task-hash.ts +12 -83
  117. package/src/orchestrator/telemetry.ts +4 -4
  118. package/src/settings.ts +428 -0
  119. package/src/{orchestrator → util}/colors.ts +5 -0
  120. package/src/util/config-frame.ts +31 -2
  121. package/src/util/errors.ts +33 -1
  122. package/src/util/essential-env.ts +31 -0
  123. package/src/util/hide-env.ts +107 -0
  124. package/src/util/index.ts +16 -3
  125. package/src/util/paths.ts +10 -0
  126. package/src/util/refusal-notice.ts +111 -0
  127. package/src/util/report.ts +31 -0
  128. package/src/util/settle.ts +49 -30
  129. package/src/util/size.ts +23 -4
  130. package/src/util/timing.ts +3 -2
  131. package/src/util/ui.ts +186 -0
  132. package/src/util/ulid.ts +1 -1
  133. package/src/util/verbs.ts +0 -15
  134. package/src/workspace/affected.ts +84 -232
  135. package/src/workspace/config-eval.ts +15 -20
  136. package/src/workspace/config-imports.ts +78 -3
  137. package/src/workspace/config-schema.ts +209 -268
  138. package/src/workspace/dependency-rules.ts +65 -0
  139. package/src/workspace/filter.ts +2 -2
  140. package/src/workspace/fingerprint.ts +11 -121
  141. package/src/workspace/index.ts +20 -27
  142. package/src/workspace/load-reads.ts +4 -4
  143. package/src/workspace/lockfile.ts +96 -140
  144. package/src/workspace/migration.ts +307 -215
  145. package/src/workspace/package-graph.ts +30 -216
  146. package/src/workspace/project-loader.ts +80 -285
  147. package/src/workspace/repo-id.ts +2 -1
  148. package/src/workspace/workspace.ts +363 -831
  149. package/src/cache/config-evals.ts +0 -130
  150. package/src/workspace/config-cache.ts +0 -661
  151. package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/help.ts CHANGED
@@ -1,14 +1,21 @@
1
- import type { ColorSupport, paint as Paint } from '../orchestrator/index.js'
2
- import { CORE_VERBS, errorDocument, maskedLine, nearest, wantsJson } from '../util/index.js'
1
+ import {
2
+ CORE_VERBS,
3
+ detectColors,
4
+ errorDocument,
5
+ maskedLine,
6
+ nearest,
7
+ paint,
8
+ refusalNotice,
9
+ ui,
10
+ wantsJson,
11
+ type ColorSupport,
12
+ } from '../util/index.js'
3
13
 
4
14
  export async function printHelp(
5
15
  pluginCommands: readonly string[] = [],
6
16
  verb?: string,
7
17
  ): Promise<void> {
8
18
  const text = verb === undefined ? helpText(pluginCommands) : verbHelpText(verb)
9
- // The orchestrator is already loaded by the plugin-verb lookup every
10
- // caller awaits first, so this import costs `vx --version` nothing.
11
- const { detectColors, paint } = await import('../orchestrator/index.js')
12
19
  process.stdout.write(paintHelp(text, helpColors(process.stdout, detectColors), paint))
13
20
  }
14
21
 
@@ -33,7 +40,7 @@ const TERM = '#22d3ee' // cyan-400 — a flag, a selector, an example
33
40
  * `(for <verb>)` dim), `vx <verb>` bold cyan, and the term an option or
34
41
  * example line opens with cyan. Prose and continuation lines stay plain.
35
42
  */
36
- export function paintHelp(text: string, colors: ColorSupport, paint: typeof Paint): string {
43
+ export function paintHelp(text: string, colors: ColorSupport, paint: Paint): string {
37
44
  if (!colors.enabled) return text
38
45
  const lines = text.split('\n')
39
46
  return lines
@@ -102,19 +109,19 @@ export function verbHelpText(verb: string): string {
102
109
 
103
110
  export function helpText(pluginCommands: readonly string[] = []): string {
104
111
  return [
105
- 'vx — open, extensible monorepo task runner',
112
+ 'vx — task orchestration you own',
106
113
  '',
107
114
  'Usage:',
108
115
  ' 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>]',
116
+ ' vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]',
117
+ ' vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry-run] [--format pretty|json] [--cache-dir <path>]',
111
118
  ' vx lock [--check] [--format pretty|json]',
112
119
  ' vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <seam>]',
113
120
  ' vx upgrade [TAG]',
114
121
  ' vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]',
115
122
  ' vx info [--format pretty|json] [--cache-dir <path>]',
116
123
  ' 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>]',
124
+ ' vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--format pretty|json] [--cache-dir <path>]',
118
125
  ' vx docs <query> [--limit N] [--format pretty|json]',
119
126
  ' vx completions bash|zsh|fish',
120
127
  ' vx help [VERB]',
@@ -126,56 +133,80 @@ export function helpText(pluginCommands: readonly string[] = []): string {
126
133
  ' --filter <pat> pnpm-style filter (repeatable). Examples:',
127
134
  ' foo, @scope/*, ./packages/foo, foo..., ...foo,',
128
135
  ' 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.',
136
+ ' --affected[=<base>] Projects changed since <base>, and dependents',
137
+ ' (--filter "...[<base>]"); default run.affectedBase,',
138
+ ' else origin/HEAD, main, master, HEAD~1.',
132
139
  " pkg#task Run a specific project's task directly.",
133
140
  '',
134
141
  '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). Without it, VX_CACHE holds the spec.',
144
- " --cache-scope <name> This run's cacheScope: trusted | read-only | a scope (pr-123).",
145
- ' Beats VX_CACHE_SCOPE and the workspace cacheScope.',
146
- ' --cache-dir <path> Cache directory override (cwd-relative). Beats VX_CACHE_DIR,',
147
- ' the workspace cacheDir field and the ~/.vx/<id>/cache default.',
148
- ' --retry <n> Re-run a failed task up to <n> more times (default for tasks',
149
- " without their own exec.retries; explicit config wins). Doesn't",
150
- ' affect cache keys.',
151
- ' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
152
- ' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
153
- ' a runaway task. Per-task exec.timeout always wins.',
154
- ' --continue[=<mode>] What a failed task takes down: never | deps-ok (default, skips',
155
- ' its dependents) | always (bare form; dependents run, never save).',
156
- ' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
157
- ' --output-logs <mode> full | errors-only | hash-only | none — overrides the flow default',
158
- ' (focused without --all/--filter/--affected, broad with, full in CI).',
159
- ' --download <mode> all (default) | toplevel | none — where remote outputs land.',
160
- ' --verbosity <n> 0 (default) or 1+: print a per-task summary table after the run.',
142
+ ' Each setting flag beats its VX_* variable, which beats .vx/workspace',
143
+ ' (docs/schema.md § Settings).',
144
+ ' --concurrency <n> Max parallel tasks (default: usable cores);',
145
+ ' 50% means half of them.',
146
+ ' --schedule <rule> most-work (default) | critical-path |',
147
+ ' direct-dependents | ready-order.',
148
+ ' --branch <name> The branch history records and forecasts read',
149
+ " (default: git's, else the CI provider's).",
150
+ ' --exclude-dependencies[=names] Skip dependsOn edges: all with no value,',
151
+ ' or a comma list of names.',
152
+ ' --no-cache No cache reads or writes; outputs are left',
153
+ ' alone.',
154
+ ' --force Skip cache reads but still write: every task',
155
+ ' runs and refreshes its entry.',
156
+ ' --cache-policy <spec> Per-layer read/write: a comma list of',
157
+ ' <layer>:<flags> (layer local or remote, flags',
158
+ ' from rw). A named layer is set to exactly its',
159
+ ' flags; unnamed layers keep theirs. Examples:',
160
+ ' local:rw,remote:r (remote read-only), remote:',
161
+ ' (remote off), local:r (local read-only).',
162
+ " --cache-scope <name> This run's cache.scope (pr-123): write there,",
163
+ ' read the default keys first.',
164
+ ' --cache-dir <path> Cache directory (cwd-relative); default',
165
+ ' .vx/cache, entries in ~/.vx/<id>/cache.',
166
+ ' --cache-download <mode> all (default) | toplevel | none:',
167
+ ' where remote outputs land.',
168
+ ' --retries <n> Up to <n> more runs of a failed task, for',
169
+ ' tasks without their own exec.retries. Not',
170
+ ' folded into cache keys.',
171
+ " --timeout <duration> A task's time limit (30s, 10m), for tasks",
172
+ ' without their own exec.timeout. A task past it',
173
+ ' is killed and fails.',
174
+ ' --kill-grace <duration> How long a task that ignores SIGTERM gets',
175
+ ' before SIGKILL (default 2s).',
176
+ " --teardown-timeout <duration> The bound on each plugin's end-of-run flush",
177
+ ' (default 3s).',
178
+ ' --ready-notice <duration> When vx says a persistent task still waits for',
179
+ ' readyWhen (default 10s).',
180
+ ' --continue[=<mode>] What a failed task takes down: never | deps-ok',
181
+ ' (default, skips its dependents) | always (bare',
182
+ ' form; dependents run, never save).',
183
+ ' --output-logs <mode> full | errors-only | hash-only | none, over',
184
+ ' the flow default (focused without',
185
+ ' --all/--filter/--affected, broad with, full in',
186
+ ' CI).',
187
+ ' --verbosity <n> 0 (default), or 1 and up for a per-task',
188
+ ' summary table after the run.',
161
189
  '',
162
190
  'Planning (for run — skips execution):',
163
- ' --dry[=text|json] Print the task graph + cache hit/miss prediction.',
191
+ " --dry[=text|json] Print the task graph and each task's predicted hit",
192
+ ' or miss.',
164
193
  ' --graph[=<path>] Emit Graphviz DOT (stdout if no path).',
165
194
  '',
166
195
  'Artifacts (for run):',
167
- ' --format <fmt> pretty (default) | json: print the --summarize document',
168
- ' on stdout when the run ends; every other line goes to stderr.',
196
+ ' --format <fmt> pretty (default) | json: print the --summarize',
197
+ ' document on stdout when the run ends; every other',
198
+ ' line goes to stderr.',
169
199
  ' --summarize[=<path>] Write per-run JSON to <cacheDir>/runs/<run_id>.json.',
170
200
  ' --profile[=<path>] Write Chrome-trace JSON (default profile.json).',
171
201
  ' --report[=markdown] Print a markdown run report to stdout after the run.',
172
- ' --report-file <path> Append that report to <path> (for $GITHUB_STEP_SUMMARY).',
173
- ' --tag <k=v> Label this invocation (repeatable); recorded on its history row.',
202
+ ' --report-file <path> Append that report to <path> ($GITHUB_STEP_SUMMARY).',
203
+ ' --tag <k=v> Label this invocation (repeatable); recorded on its',
204
+ ' history row.',
174
205
  '',
175
206
  'Extensions (plugins):',
176
207
  ' vx core runs tasks in-process and nothing else. A dashboard, remote',
177
208
  ' cache, distributed execution, and telemetry export are provided by',
178
- ' PLUGINS declared in vx.workspace.ts — first-party or your own. Core',
209
+ ' PLUGINS declared in .vx/workspace.ts — first-party or your own. Core',
179
210
  ' depends on none of them; a plain `vx run` never requires a plugin.',
180
211
  ' See the plugin guide: https://vznjs.github.io/vx/guides/plugins/',
181
212
  '',
@@ -194,24 +225,27 @@ export function helpText(pluginCommands: readonly string[] = []): string {
194
225
  ' Uses the same flags as `vx run` except --dry /',
195
226
  ' --graph / --summarize / --profile / --report /',
196
227
  ' --report-file / --verbosity (they plan or report',
197
- ' one run). Press Ctrl+C to stop. Cache means most',
228
+ ' one run), plus --poll (watch.poll: poll instead of',
229
+ ' the OS watcher). Press Ctrl+C to stop. Cache means most',
198
230
  ' re-runs are near-zero cost (cache hits restore',
199
231
  ' outputs + replay logs).',
200
232
  '',
201
233
  'Cache management:',
202
- ' vx cache prune --older-than 30d Evict entries last accessed > 30 days ago.',
203
- ' vx cache prune --max-size 1G Keep total cache under 1 GB (LRU eviction).',
234
+ ' vx cache clean --older-than 30d Evict entries last accessed > 30 days ago.',
235
+ ' vx cache clean --max-size 1G Keep total cache under 1 GB (LRU eviction).',
236
+ " vx cache clean Clear .vx/cache and this repo's store ~/.vx/<id>/cache.",
237
+ " --global Clear every repo's store under ~/.vx.",
204
238
  ' --dry-run Say what the policy would reap; delete nothing.',
205
239
  ' --format <fmt> pretty (default) | json.',
206
240
  ' --cache-dir <path> The cache a run with the same flag uses (why, last, info too).',
207
241
  '',
208
- ' Duration units: s, m, h, d. Size units: K, M, G, T (powers of 1024).',
242
+ ' Duration units: ms, s, m, h, d. Size units: K, M, G, T (powers of 1024).',
209
243
  ' Cache statistics — directory, entry count, size — are part of `vx info`.',
210
244
  '',
211
245
  'Introspection:',
212
246
  ' vx show List every project: name, dir, declared task count.',
213
- " vx show <project> Print the project's LIVE resolved config (fresh",
214
- ' evaluation, not the lock), one block per task.',
247
+ " vx show <project> Print the project's resolved config as a run reads",
248
+ ' it from the lock, one block per task.',
215
249
  " vx show <pkg>#<task> Print a single task's resolved config.",
216
250
  ' vx show <task> That task in every project declaring it.',
217
251
  " --filter <pattern> The list (or a task's projects) as `vx run --filter` selects.",
@@ -234,25 +268,27 @@ export function helpText(pluginCommands: readonly string[] = []): string {
234
268
  ' --failed Only failed runs: the latest one, or with --list the list.',
235
269
  " --log <task> That task's output in the run (latest that ran it): a failure's",
236
270
  ' or the cached log; none for an uncached pass.',
271
+ ' --stats [<task>] Per-task p50/p99, pass and hit rate, failure mode, peak',
272
+ ' RSS and CPU over the last 50 runs; a task id or name narrows.',
273
+ " --branch <name> The branch --stats' p50 forecasts from (default: the run's).",
237
274
  ' --format <fmt> pretty (default) | json.',
238
275
  ' info, why and last:',
239
276
  ' --cache-dir <path> Read the cache a run with the same flag uses.',
240
277
  '',
241
278
  'Migration:',
242
- ' vx init Generate vx.workspace.ts + one vx.config.ts per package',
243
- ' from package.json scripts. Unmappable settings become',
244
- ' TODO(vx-migrate) comments. Beside turbo.json or nx.json:',
245
- ' runs @vzn/vx-migrate, which asks native or keep.',
279
+ ' vx init With no package.json: write .vx/workspace.ts and an empty',
280
+ ' root vx.config.ts. With one: runs @vzn/vx-migrate, which maps',
281
+ ' turbo.json, an Nx graph or package.json scripts to one',
282
+ ' vx.config.ts per package (asking native or keep in Turbo/Nx).',
246
283
  ' --dry Print the generated files instead of writing them.',
247
284
  ' --format json With --dry: the plan as one JSON document (schemas/init.json).',
248
285
  ' --force Overwrite existing vx.config.* files.',
249
286
  ' --mjs Write .mjs files (no type import) instead of .ts.',
250
287
  ' --native Turbo or Nx: write a vx.config.ts per package (no question).',
251
- ' --keep Turbo or Nx: only vx.workspace.ts, declaring turbo() or nx().',
288
+ ' --keep Turbo or Nx: only .vx/workspace.ts, declaring turbo() or nx().',
252
289
  ' --plugin <seam> Instead: write plugins/<seam>.ts, a runnable plugin, and its',
253
290
  ' test (executor, cache, telemetry, schedule, admit, commands,',
254
291
  ' project, graph, key).',
255
- ' From turbo.json or an Nx project graph: `vx init` runs `@vzn/vx-migrate` (its own package).',
256
292
  '',
257
293
  'Shell completions:',
258
294
  ' vx completions <shell> Print a completion script for bash, zsh or fish: the',
@@ -269,14 +305,15 @@ export function helpText(pluginCommands: readonly string[] = []): string {
269
305
  ' --format json {query, hits: [{page, heading, url, text}]}.',
270
306
  '',
271
307
  'Config lock:',
272
- ' vx lock Evaluate every vx.config in the current environment and',
273
- ' freeze the resolved objects into vx-lock.json. Plain runs',
274
- ' always evaluate live; only `vx run --frozen` consumes the',
275
- ' lock (CI reproducibility, frozen-env semantics).',
308
+ ' vx lock Evaluate every vx.config in the current environment, check',
309
+ ' the graph rules (run.rules) and freeze the resolved objects',
310
+ ' into .vx/projects.lock.json, the only place a run reads configs',
311
+ ' from. A run never evaluates one, never checks the rules',
312
+ ' and never writes the lock: with none it fails.',
276
313
  ' vx lock --check Audit the lock: file-changed hash report PLUS a full',
277
- ' re-evaluation compared against the frozen objects. Exits 1',
278
- ' on drift (e.g. configs that read env vars). Run it before',
279
- ' `vx run --frozen` in CI. With --format json it prints',
314
+ ' re-evaluation compared against the frozen objects, and the',
315
+ ' graph rules. Exits 1 on drift (e.g. configs that read env',
316
+ ' vars). Run it before the run in CI. With --format json it prints',
280
317
  ' {upToDate, audited, notAudited, problems}.',
281
318
  '',
282
319
  ...(pluginCommands.length > 0 ? ['Plugin commands:', ...pluginCommands, ''] : []),
@@ -315,7 +352,7 @@ export function refusedWord(arg: string): string {
315
352
  * `(did you mean --concurrency?)` for a flag within two edits of one `verb`
316
353
  * accepts; empty for anything else (a positional included). A third edit
317
354
  * is allowed only between flags that share their first five characters,
318
- * so `--retries` reaches `--retry` while `--zzz` does not reach `--all`.
355
+ * so `--timeouts` reaches `--timeout` while `--zzz` does not reach `--all`.
319
356
  */
320
357
  export function flagHint(verb: string, arg: string): string {
321
358
  if (!arg.startsWith('-')) return ''
@@ -343,8 +380,8 @@ export function flagHint(verb: string, arg: string): string {
343
380
  * The documented `--flags` of one verb, read from the help text's
344
381
  * sections headed `… (for <verb>):` so there is no second list to drift —
345
382
  * and no hint that names another verb's flag. Only the flag an option line
346
- * OPENS with is one: `--frozen`'s line says "pair with vx lock --check", and
347
- * reading every flag on it put `--check` in the list, so `vx run --chek`
383
+ * OPENS with is one: an option line that named `vx lock --check` once put
384
+ * `--check` in the list, so `vx run --chek`
348
385
  * was told "did you mean --check?" and `--check` was then refused.
349
386
  */
350
387
  export function documentedFlags(verb: string, text = helpText()): string[] {
@@ -354,8 +391,9 @@ export function documentedFlags(verb: string, text = helpText()): string[] {
354
391
  if (/^[A-Z][A-Za-z ]*(?: \(for [a-z]+\))?:$/.test(line))
355
392
  inVerb = line.endsWith(`(for ${verb}):`)
356
393
  if (!inVerb) continue
357
- const own = /^\s+(--[a-zA-Z][a-zA-Z-]*)/.exec(line)
358
- if (own !== null) flags.add(own[1]!)
394
+ // `--x, --no-x`: a flag and its negation.
395
+ const own = /^\s+(--[a-zA-Z][a-zA-Z-]*)(?:, (--[a-zA-Z][a-zA-Z-]*))?/.exec(line)
396
+ if (own !== null) for (const f of own.slice(1)) if (f !== undefined) flags.add(f)
359
397
  }
360
398
  return [...flags]
361
399
  }
@@ -414,7 +452,9 @@ export function refuse(
414
452
  code: string,
415
453
  ): number {
416
454
  const line = `vx ${verb}: ${message}`
417
- process.stderr.write(`${line}\n`)
455
+ ui.print('err', refusalNotice(`vx ${verb}`, message))
418
456
  if (wantsJson(args)) process.stdout.write(errorDocument(code, maskedLine(line)))
419
457
  return 1
420
458
  }
459
+
460
+ type Paint = typeof paint
package/src/cli/index.ts CHANGED
@@ -2,14 +2,20 @@
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
+ sayNotice,
12
+ isFsRefusal,
13
+ isOutOfFds,
9
14
  isUserError,
10
15
  maskedLine,
11
- MOVED_VERBS,
12
16
  nearest,
17
+ refusalNotice,
18
+ ui,
13
19
  UserError,
14
20
  wantsJson,
15
21
  } from '../util/index.js'
@@ -24,6 +30,7 @@ const plugins = () => import('./plugin-commands.js')
24
30
 
25
31
  export async function run(argv: readonly string[]): Promise<number> {
26
32
  const [command, ...rest] = argv
33
+ await hideEnvFromTasks()
27
34
 
28
35
  // `vx <verb> --help` is the universal reflex, and every verb answered
29
36
  // `unknown flag: --help` and exited 1 (walkthrough, 2026-09-04). Only args
@@ -34,7 +41,33 @@ export async function run(argv: readonly string[]): Promise<number> {
34
41
  await printHelp(await (await plugins()).pluginCommandHelp(), command)
35
42
  return 0
36
43
  }
44
+ // Each setting's variable once, before any verb: a renamed one is named,
45
+ // and a deep reader trusts what it reads.
46
+ checkSettingsEnv()
37
47
 
48
+ try {
49
+ return await dispatch(command, rest)
50
+ } catch (err) {
51
+ throw await brokenLockNamed(err)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * A crash from a hand-broken lock entry is that entry's fault: runs validate
57
+ * no entry, so it surfaced as a raw TypeError (M, 2026-10-14). The lock is
58
+ * read only on the crash path, so a normal run pays nothing.
59
+ */
60
+ async function brokenLockNamed(err: unknown): Promise<unknown> {
61
+ if (isUserError(err) || isFsRefusal(err) || isOutOfFds(err)) return err
62
+ try {
63
+ const { findWorkspaceRoot, LOCK_INVALID, lockEntryBroken } =
64
+ await import('../workspace/index.js')
65
+ if (lockEntryBroken(await findWorkspaceRoot(process.cwd()))) return new UserError(LOCK_INVALID)
66
+ } catch {}
67
+ return err
68
+ }
69
+
70
+ async function dispatch(command: string | undefined, rest: string[]): Promise<number> {
38
71
  switch (command) {
39
72
  case undefined: {
40
73
  // A bare `vx` where no workspace is printed 151 lines of help, none
@@ -44,7 +77,8 @@ export async function run(argv: readonly string[]): Promise<number> {
44
77
  await findWorkspaceRoot(process.cwd())
45
78
  } catch (err) {
46
79
  if (!isUserError(err)) throw err
47
- process.stderr.write(`vx: ${err.message}; \`vx help\` lists the verbs\n`)
80
+ const parts = err.parts ?? { headline: err.message }
81
+ ui.print('err', refusalNotice('vx', err.message, { ...parts, why: 'see vx help' }))
48
82
  return 1
49
83
  }
50
84
  await printHelp(await (await plugins()).pluginCommandHelp())
@@ -65,21 +99,25 @@ export async function run(argv: readonly string[]): Promise<number> {
65
99
  // for the whole reference: it printed 145 lines and no hint.
66
100
  const resolved = await (await plugins()).resolvePluginCommand(verb)
67
101
  const unknown = resolved === null || 'declaredVerbs' in resolved
68
- const pointer = MOVED_VERBS[verb] ?? FOREIGN_VERBS[verb]
102
+ const pointer = FOREIGN_VERBS[verb]
69
103
  if (unknown && pointer !== undefined) {
70
- process.stderr.write(`${pointer}\n`)
104
+ sayNotice('info', 'vx', pointer)
71
105
  return 1
72
106
  }
73
107
  if (unknown && verb.startsWith('-')) {
74
- process.stderr.write(
75
- `vx help: unknown flag: ${verb}${flagHint('help', verb)} (see \`vx help\`)\n`,
108
+ sayNotice(
109
+ 'error',
110
+ 'vx help',
111
+ `unknown flag: ${verb}${flagHint('help', verb)} (see \`vx help\`)`,
76
112
  )
77
113
  return 1
78
114
  }
79
115
  if (unknown) {
80
116
  const declared = resolved === null ? [] : resolved.declaredVerbs
81
- process.stderr.write(
82
- `vx help: unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)\n`,
117
+ sayNotice(
118
+ 'error',
119
+ 'vx help',
120
+ `unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)`,
83
121
  )
84
122
  return 1
85
123
  }
@@ -93,8 +131,10 @@ export async function run(argv: readonly string[]): Promise<number> {
93
131
  // takes no more of is refused, as every other verb refuses one.
94
132
  const extra = rest[0]
95
133
  if (extra !== undefined) {
96
- process.stderr.write(
97
- `vx version: ${refusedWord(extra)}: ${extra}${flagHint('version', extra)}${seeHelp('version')}\n`,
134
+ sayNotice(
135
+ 'error',
136
+ 'vx version',
137
+ `${refusedWord(extra)}: ${extra}${flagHint('version', extra)}${seeHelp('version')}`,
98
138
  )
99
139
  return 1
100
140
  }
@@ -163,22 +203,25 @@ export async function run(argv: readonly string[]): Promise<number> {
163
203
  // points at the file that broke it.
164
204
  // Every answer below is "no such verb here": under --format json the
165
205
  // agent reads it as one code, as a verb's own refusal gives one.
166
- const unknown = (text: string, code = 'VX_E_UNKNOWN_COMMAND'): number => {
167
- process.stderr.write(text)
206
+ const unknown = (
207
+ text: string,
208
+ code = 'VX_E_UNKNOWN_COMMAND',
209
+ tone: 'error' | 'info' = 'error',
210
+ ): number => {
211
+ sayNotice(tone, 'vx', text.replace(/^vx: /, '').trimEnd())
168
212
  if (wantsJson(rest))
169
213
  process.stdout.write(errorDocument(code, maskedLine(text.split('\n')[0]!)))
170
214
  return 1
171
215
  }
172
216
  const loadNote =
173
217
  resolved !== null && 'loadError' in resolved
174
- ? `\n (plugin verbs could not be looked up: vx.workspace failed to load: ${resolved.loadError})`
218
+ ? `\n (plugin verbs could not be looked up: .vx/workspace failed to load: ${resolved.loadError})`
175
219
  : ''
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]
220
+ // Another tool's verb: the pointer, but only after the plugins had
221
+ // their chance — a workspace that declares a plugin verb of that name keeps it.
222
+ const moved = FOREIGN_VERBS[command]
180
223
  if (moved !== undefined) {
181
- return unknown(`${moved}${loadNote}\n`)
224
+ return unknown(`${moved}${loadNote}\n`, undefined, 'info')
182
225
  }
183
226
  // A task typed where the verb goes (`turbo build`, `nx build app`),
184
227
  // `turbo dev` included: a repo's own `dev` task beats the no-service note.
@@ -198,7 +241,7 @@ export async function run(argv: readonly string[]): Promise<number> {
198
241
  if (command === 'serve' || command === 'dev') {
199
242
  // vx core is only a task runner — it has no service layer of its
200
243
  // own. A dashboard, remote cache, distributed execution, etc. are
201
- // provided by PLUGINS (declared in vx.workspace.ts), never by core.
244
+ // provided by PLUGINS (declared in .vx/workspace.ts), never by core.
202
245
  // We keep this neutral hint for the common muscle-memory verbs, but
203
246
  // core names no specific plugin package: any package can provide
204
247
  // these.
@@ -213,10 +256,10 @@ export async function run(argv: readonly string[]): Promise<number> {
213
256
  // help after a typo was a hundred lines past the hint that mattered.
214
257
  // The verbs this workspace's plugins declare are verbs HERE, so they
215
258
  // 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).
259
+ // line says where a verb can come from: a typo of a plugin verb read
260
+ // as a plain unknown command, and a plugin verb before the plugin was
261
+ // declared said nothing about the file that would declare it
262
+ // (2026-09-20).
220
263
  // A word that opens with a dash is a flag, and vx itself takes only
221
264
  // --help and --version: "unknown command: --bogus" named the wrong
222
265
  // thing (M-60).
@@ -268,5 +311,5 @@ function verbSourceNote(resolved: unknown, declaredVerbs: readonly string[]): st
268
311
  return ` This workspace's plugins declare: ${[...declaredVerbs].sort().join(', ')}.\n`
269
312
  }
270
313
  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`
314
+ return ` A plugin declared in .vx/workspace.ts can add verbs; ${here}.\n`
272
315
  }