@vzn/vx 0.0.646 → 0.0.649

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
@@ -19,6 +19,7 @@ import {
19
19
  machineMemoryBytes,
20
20
  machineParallelism,
21
21
  treeBytes,
22
+ ui,
22
23
  } from '../util/index.js'
23
24
  import { vxPath } from '../handoff.js'
24
25
  import { VERSION } from '../version.js'
@@ -32,10 +33,18 @@ import {
32
33
  import { flakyTasks, type FlakyTask } from '../history/index.js'
33
34
  import type { VxPlugin } from './plugin.js'
34
35
  import { discoverProjects, loadProjects, loadWorkspacePlugins, lockReader } from './projects.js'
35
- import { atPath, envName, resolveSetting, SETTINGS, type SettingSource } from '../settings.js'
36
+ import {
37
+ atPath,
38
+ envName,
39
+ parseSettingValue,
40
+ resolveSetting,
41
+ settingRefusal,
42
+ SETTINGS,
43
+ type SettingSource,
44
+ } from '../settings.js'
36
45
 
37
46
  const warnToStderr = (message: string): void => {
38
- process.stderr.write(`${message}\n`)
47
+ ui.say('warn', message)
39
48
  }
40
49
 
41
50
  /** The doctor's facts, typed: what `--format json` prints and the pretty rows render. */
@@ -89,6 +98,11 @@ export interface InfoFacts {
89
98
  schemaVersion: string
90
99
  cacheEntries: number
91
100
  cacheBytes: number
101
+ /**
102
+ * Why the cache index cannot be read (a file SQLite cannot open, and its
103
+ * fix); null when it reads. While set, the counts it holds read zero.
104
+ */
105
+ cacheIndexError: string | null
92
106
  /**
93
107
  * Every setting (src/settings.ts) as a run with no flags takes it: its
94
108
  * variable's value, else the workspace's (after the `config` stage),
@@ -96,7 +110,12 @@ export interface InfoFacts {
96
110
  */
97
111
  settings: Record<
98
112
  string,
99
- { value: string | number | boolean | null; source: 'env' | 'workspace' | 'default' }
113
+ {
114
+ value: string | number | boolean | null
115
+ source: 'env' | 'workspace' | 'default'
116
+ /** Why a run refuses this variable's value; absent when it takes it. */
117
+ refused?: string
118
+ }
100
119
  >
101
120
  orphans: { artifacts: number; bytes: number }
102
121
  runs24h: number
@@ -106,6 +125,8 @@ export interface InfoFacts {
106
125
  /** Tasks the retained history shows failing on a key that had passed. */
107
126
  flakyTasks: FlakyTask[]
108
127
  lockfile: boolean
128
+ /** Why a run refuses the lock that is there; null when it reads it (or there is none). */
129
+ lockError: string | null
109
130
  /**
110
131
  * Whether this host can run a task's `exec.sandbox`, and how many loaded
111
132
  * tasks declare one. A declared sandbox whose runtime cannot start is a
@@ -127,6 +148,34 @@ export interface CollectInfoOptions {
127
148
  }
128
149
 
129
150
  export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): Promise<InfoFacts> {
151
+ // A variable a run refuses is a fact of the report, not a refusal of it:
152
+ // set aside while the workspace loads, so no deep reader throws on it.
153
+ const refused = refusedSettings()
154
+ for (const r of refused.values()) delete process.env[r.name]
155
+ try {
156
+ const facts = await collectInfoChecked(cwd, opts)
157
+ for (const [p, r] of refused)
158
+ facts.settings[p] = { value: r.raw, source: 'env', refused: r.reason }
159
+ return facts
160
+ } finally {
161
+ for (const r of refused.values()) process.env[r.name] = r.raw
162
+ }
163
+ }
164
+
165
+ /** Each set setting variable a run refuses, by path: its name, value and why. */
166
+ function refusedSettings(): Map<string, { name: string; raw: string; reason: string }> {
167
+ const out = new Map<string, { name: string; raw: string; reason: string }>()
168
+ for (const s of SETTINGS) {
169
+ if (s.env === false) continue
170
+ const name = envName(s.path)
171
+ const raw = process.env[name]
172
+ if (raw === undefined || raw === '' || parseSettingValue(s.kind, raw) !== null) continue
173
+ out.set(s.path, { name, raw, reason: settingRefusal(s.path, name, raw) })
174
+ }
175
+ return out
176
+ }
177
+
178
+ async function collectInfoChecked(cwd: string, opts: CollectInfoOptions): Promise<InfoFacts> {
130
179
  const warn = opts.warn ?? warnToStderr
131
180
  // Both first: outside a workspace, or under a workspace config that does
132
181
  // not load, the doctor refuses at once, with no probe (~180 ms) ahead.
@@ -162,65 +211,90 @@ async function collectWorkspaceInfo(
162
211
  // The lock's projects: none without a lock, or with one a run would
163
212
  // refuse, which is `vx lock`'s to report.
164
213
  const lock = lockReader(root)
214
+ // A lock a run refuses is said, not swallowed: "projects lock yes" with
215
+ // no task hid why every run failed (2026-10-28).
216
+ let refused: string | null = null
165
217
  const held = await lock().then(
166
218
  () => true,
167
219
  (err: unknown) => {
168
220
  if (!isUserError(err)) throw err
221
+ refused = err.message
169
222
  return false
170
223
  },
171
224
  )
172
225
  const metas = held ? await discoverProjects(root, lock, plugins, cacheDir, warn) : []
173
226
  const storeRoot =
174
227
  opts.cacheDir === undefined ? await resolveStoreRoot(root, workspaceConfig) : null
175
- const cache = new Cache(cacheDir, undefined, undefined, undefined, 'inspect', storeRoot)
176
- let stats
177
- let orphans
178
- let flaky: FlakyTask[]
228
+ // The index is read first and alone: one SQLite cannot read is a fact
229
+ // of the report, with its fix, not a refusal of it — the doctor is where
230
+ // a broken setup is diagnosed.
231
+ let stats: ReturnType<Cache['stats']> = {
232
+ entryCount: 0,
233
+ totalBytes: 0,
234
+ runCountLast24h: 0,
235
+ hitCountLast24h: 0,
236
+ restoredCountLast24h: 0,
237
+ }
238
+ let orphans = { orphans: 0, orphanBytes: 0 }
239
+ let flaky: FlakyTask[] = []
240
+ let indexError: string | null = null
241
+ let indexStore: string | undefined
242
+ try {
243
+ const cache = new Cache(cacheDir, undefined, undefined, undefined, 'inspect', storeRoot)
244
+ try {
245
+ indexStore = cache.storeDir
246
+ stats = cache.stats()
247
+ orphans = await cache.orphanStats()
248
+ flaky = flakyTasks(cache.dbHandle())
249
+ } finally {
250
+ cache.close()
251
+ }
252
+ } catch (err) {
253
+ // An index of another schema is refused as by every reading verb: this
254
+ // vx reads nothing in it, and the next run resets it.
255
+ if (!isUserError(err) || err.code === 'VX_E_CACHE_VERSION') throw err
256
+ indexError = err.message
257
+ }
179
258
  let taskCount = 0
180
259
  let sandboxed = 0
260
+ // The run path's load — the lock, a plugin's `project` stage counted —
261
+ // so the doctor's task count is the number a run would see. No lock, or
262
+ // one a run would refuse, counts zero: broken configs are `vx lock`'s
263
+ // to report.
181
264
  try {
182
- stats = cache.stats()
183
- orphans = await cache.orphanStats()
184
- flaky = flakyTasks(cache.dbHandle())
185
- // The run path's load — the lock, a plugin's `project` stage counted —
186
- // so the doctor's task count is the number a run would see. No lock, or
187
- // one a run would refuse, counts zero: broken configs are `vx lock`'s
188
- // to report.
189
- try {
190
- const loaded = await loadProjects({
191
- workspaceRoot: root,
192
- cacheDir,
193
- plugins,
194
- projectMetas: metas,
195
- seeds: 'all',
196
- closure: false,
197
- configs: lock,
198
- warn,
199
- })
200
- for (const p of loaded.projects.values()) {
201
- const tasks = p.config.tasks ?? {}
202
- taskCount += Object.keys(tasks).length
203
- for (const t of Object.values(tasks)) if (t?.exec?.sandbox !== undefined) sandboxed++
204
- }
205
- } catch (err) {
206
- if (!isUserError(err)) throw err
207
- taskCount = 0
208
- sandboxed = 0
265
+ const loaded = await loadProjects({
266
+ workspaceRoot: root,
267
+ cacheDir,
268
+ plugins,
269
+ projectMetas: metas,
270
+ seeds: 'all',
271
+ closure: false,
272
+ configs: lock,
273
+ warn,
274
+ })
275
+ for (const p of loaded.projects.values()) {
276
+ const tasks = p.config.tasks ?? {}
277
+ taskCount += Object.keys(tasks).length
278
+ for (const t of Object.values(tasks)) if (t?.exec?.sandbox !== undefined) sandboxed++
209
279
  }
210
- } finally {
211
- cache.close()
280
+ } catch (err) {
281
+ if (!isUserError(err)) throw err
282
+ taskCount = 0
283
+ sandboxed = 0
212
284
  }
213
285
 
214
286
  const lockPresent = await Bun.file(lockfilePath(root)).exists()
287
+ // No lock is its own row, `lockfile`.
288
+ const lockError = lockPresent ? refused : null
215
289
  // Asked while the probe runs: each is a synchronous spawn.
216
290
  const git = gitVersion()
217
291
  const statusCache = gitStatusCache(root)
218
292
  const sandbox = { ...(await probing), declared: sandboxed }
219
- const cacheStore = cache.storeDir ?? storeRoot
293
+ const cacheStore = indexStore ?? storeRoot
220
294
  return {
221
295
  vx: VERSION,
222
296
  vxPath: vxPath(),
223
- handedOffFrom: process.env['VX_HANDED_OFF'] ?? null,
297
+ handedOffFrom: process.env['VX_HANDED_OFF'] || null,
224
298
  // An unsupported Bun does not stop a run, it makes the run's ANSWERS
225
299
  // wrong (util/bun-version.ts measures which). The row a reader already
226
300
  // consults for "is my setup sane" is where that belongs: a warning on
@@ -257,6 +331,7 @@ async function collectWorkspaceInfo(
257
331
  schemaVersion: SCHEMA_VERSION,
258
332
  cacheEntries: stats.entryCount,
259
333
  cacheBytes: stats.totalBytes,
334
+ cacheIndexError: indexError,
260
335
  settings: settingsFact(workspaceConfig),
261
336
  // A row-less artifact is indexed again only when its task asks for its
262
337
  // key; the rest only `vx cache clean` reclaims (after an upgrade's
@@ -270,6 +345,7 @@ async function collectWorkspaceInfo(
270
345
  // this is the workspace's standing list.
271
346
  flakyTasks: flaky,
272
347
  lockfile: lockPresent,
348
+ lockError,
273
349
  sandbox,
274
350
  }
275
351
  }
@@ -339,7 +415,7 @@ function memoryFact(): InfoFacts['memory'] {
339
415
  * the warm run (STATUS, waves 5 and the 2026-09-03 refutations) — the
340
416
  * status walk's cost is git's own, and vx already overlaps it.
341
417
  */
342
- function gitStatusCache(root: string): InfoFacts['gitStatusCache'] {
418
+ export function gitStatusCache(root: string): InfoFacts['gitStatusCache'] {
343
419
  try {
344
420
  const p = Bun.spawnSync({
345
421
  cmd: [executablePath('git'), 'config', '--get-regexp', '^core\\.(fsmonitor|untrackedcache)$'],
@@ -351,9 +427,25 @@ function gitStatusCache(root: string): InfoFacts['gitStatusCache'] {
351
427
  // exit 1 is git's "no key matched": both off, still an answer.
352
428
  if (p.exitCode !== 0 && p.exitCode !== 1) return null
353
429
  const out = p.exitCode === 0 ? new TextDecoder().decode(p.stdout) : ''
354
- const on = (key: string): boolean =>
355
- new RegExp(`^core\\.${key} (true|1|yes|on)$`, 'im').test(out)
356
- return { fsmonitor: on('fsmonitor'), untrackedCache: on('untrackedcache') }
430
+ // The last value set wins, as git reads it (a global `true` under a
431
+ // repo's `false` is off), and a key with no `=` is true.
432
+ const last = new Map<string, string | null>()
433
+ for (const line of out.split('\n')) {
434
+ if (line === '') continue
435
+ const sp = line.indexOf(' ')
436
+ last.set(
437
+ (sp === -1 ? line : line.slice(0, sp)).toLowerCase(),
438
+ sp === -1 ? null : line.slice(sp + 1),
439
+ )
440
+ }
441
+ const on = (key: string, hook: boolean): boolean => {
442
+ const v = last.get(`core.${key}`)
443
+ if (v === undefined) return false
444
+ if (v === null || /^(true|yes|on|1)$/i.test(v)) return true
445
+ // core.fsmonitor may name a hook command instead of a boolean.
446
+ return hook && v !== '' && !/^(false|no|off|0)$/i.test(v)
447
+ }
448
+ return { fsmonitor: on('fsmonitor', true), untrackedCache: on('untrackedcache', false) }
357
449
  } catch {
358
450
  return null
359
451
  }
@@ -71,7 +71,10 @@ export function executeArgsOf(
71
71
  if (placements.remoteOnly.has(id)) a.remoteOnly = true
72
72
  if (placements.carried.has(id)) a.carried = true
73
73
  const servers = placements.servers.get(id)
74
- if (servers !== undefined) a.servers = servers
74
+ if (servers !== undefined) {
75
+ a.servers = servers
76
+ a.serversNested = servers.map((s) => facts.nestedDirsByProject.get(s.projectName) ?? [])
77
+ }
75
78
  if (probe !== undefined) a.preProbed = probe
76
79
  if (upfrontKey !== undefined) a.upfrontKey = upfrontKey
77
80
  if (upfrontGroupKey !== undefined) a.upfrontGroupKey = upfrontGroupKey