@vzn/vx 0.0.652 → 0.0.653

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 (167) hide show
  1. package/README.md +2 -2
  2. package/launcher.sh +2 -1
  3. package/package.json +5 -5
  4. package/schemas/error.json +1 -1
  5. package/schemas/show.json +1 -0
  6. package/skills/vx/SKILL.md +3 -3
  7. package/src/bin.ts +25 -3
  8. package/src/cache/archive.ts +196 -31
  9. package/src/cache/cache.ts +863 -225
  10. package/src/cache/checkpointer.ts +102 -0
  11. package/src/cache/file-hashes.ts +2 -1
  12. package/src/cache/fold-memo.ts +24 -4
  13. package/src/cache/fs-clock.ts +26 -0
  14. package/src/cache/git-inputs.ts +188 -22
  15. package/src/cache/history-stream.ts +22 -5
  16. package/src/cache/index.ts +10 -1
  17. package/src/cache/inputs.ts +573 -152
  18. package/src/cache/key-fold.ts +1 -1
  19. package/src/cache/layer.ts +23 -10
  20. package/src/cache/layered-cache.ts +28 -7
  21. package/src/cache/output-index.ts +106 -44
  22. package/src/cache/plugin-cache.ts +2 -2
  23. package/src/cache/policy.ts +6 -2
  24. package/src/cache/remote-lane.ts +3 -1
  25. package/src/cache/run-history.ts +50 -10
  26. package/src/cache/schema.ts +42 -9
  27. package/src/cli/cache.ts +147 -34
  28. package/src/cli/completions.ts +2 -2
  29. package/src/cli/core-alias.ts +9 -2
  30. package/src/cli/docs.ts +4 -2
  31. package/src/cli/help.ts +25 -2
  32. package/src/cli/index.ts +38 -7
  33. package/src/cli/info.ts +4 -3
  34. package/src/cli/init.ts +55 -20
  35. package/src/cli/last.ts +184 -41
  36. package/src/cli/lock.ts +187 -24
  37. package/src/cli/plan-format.ts +24 -21
  38. package/src/cli/plugin-commands.ts +6 -1
  39. package/src/cli/plugin-templates.ts +1 -1
  40. package/src/cli/run.ts +68 -34
  41. package/src/cli/select.ts +160 -64
  42. package/src/cli/show.ts +63 -18
  43. package/src/cli/task-verb.ts +12 -7
  44. package/src/cli/upgrade.ts +210 -76
  45. package/src/cli/watch-filter.ts +88 -15
  46. package/src/cli/watch-fs.ts +34 -47
  47. package/src/cli/watch-judge.ts +72 -8
  48. package/src/cli/watch-set.ts +6 -4
  49. package/src/cli/watch.ts +179 -25
  50. package/src/cli/why.ts +107 -33
  51. package/src/cli/workspace-config.ts +11 -1
  52. package/src/config.ts +9 -0
  53. package/src/exec/env.ts +3 -1
  54. package/src/exec/executor.ts +76 -15
  55. package/src/exec/kill-tree.ts +7 -7
  56. package/src/exec/runner.ts +270 -108
  57. package/src/exec/sandbox-binds.ts +109 -18
  58. package/src/exec/sandbox-runtime.ts +134 -37
  59. package/src/exec/sandbox-violations.ts +250 -46
  60. package/src/exec/spawn-off.ts +10 -5
  61. package/src/exec/warm-shell.ts +29 -7
  62. package/src/graph/folds.ts +89 -16
  63. package/src/graph/index.ts +8 -0
  64. package/src/graph/priorities.ts +8 -5
  65. package/src/graph/scheduler.ts +173 -38
  66. package/src/graph/stand-ins.ts +36 -0
  67. package/src/graph/task-graph.ts +327 -141
  68. package/src/graph/task-status.ts +5 -1
  69. package/src/handoff.ts +45 -10
  70. package/src/history/failure-mode.ts +15 -5
  71. package/src/history/history.ts +103 -30
  72. package/src/history/index.ts +1 -0
  73. package/src/history/metrics.ts +189 -69
  74. package/src/index.ts +2 -1
  75. package/src/orchestrator/admission.ts +13 -9
  76. package/src/orchestrator/affected-tasks.ts +35 -13
  77. package/src/orchestrator/command-windows.ts +8 -5
  78. package/src/orchestrator/deferred-outputs.ts +41 -12
  79. package/src/orchestrator/doctor.ts +21 -9
  80. package/src/orchestrator/download-policy.ts +3 -3
  81. package/src/orchestrator/excluded-keys.ts +14 -2
  82. package/src/orchestrator/execute-args.ts +9 -2
  83. package/src/orchestrator/execute-task.ts +564 -123
  84. package/src/orchestrator/graph-memo.ts +26 -4
  85. package/src/orchestrator/graph-stage.ts +27 -4
  86. package/src/orchestrator/held-servers.ts +36 -14
  87. package/src/orchestrator/hit-restore.ts +136 -87
  88. package/src/orchestrator/index.ts +4 -1
  89. package/src/orchestrator/keyed-projects.ts +25 -0
  90. package/src/orchestrator/local-floor.ts +21 -3
  91. package/src/orchestrator/lockfile-claim.ts +30 -4
  92. package/src/orchestrator/miss-save.ts +33 -53
  93. package/src/orchestrator/options.ts +13 -0
  94. package/src/orchestrator/output-host.ts +16 -4
  95. package/src/orchestrator/output-log.ts +1 -54
  96. package/src/orchestrator/persistent-task.ts +31 -17
  97. package/src/orchestrator/persistent.ts +152 -15
  98. package/src/orchestrator/placement.ts +81 -15
  99. package/src/orchestrator/plan.ts +37 -15
  100. package/src/orchestrator/plugin-host.ts +232 -108
  101. package/src/orchestrator/plugin.ts +53 -27
  102. package/src/orchestrator/prepare.ts +194 -38
  103. package/src/orchestrator/projects.ts +89 -10
  104. package/src/orchestrator/read-only.ts +115 -0
  105. package/src/orchestrator/run-context.ts +6 -2
  106. package/src/orchestrator/run-failures.ts +43 -14
  107. package/src/orchestrator/run-id.ts +13 -2
  108. package/src/orchestrator/run-keys.ts +87 -28
  109. package/src/orchestrator/run-live.ts +16 -13
  110. package/src/orchestrator/run-lock.ts +27 -4
  111. package/src/orchestrator/run-records.ts +12 -10
  112. package/src/orchestrator/run-report.ts +2 -1
  113. package/src/orchestrator/run-stop.ts +17 -9
  114. package/src/orchestrator/run.ts +443 -107
  115. package/src/orchestrator/sandbox-fix.ts +40 -12
  116. package/src/orchestrator/sandbox-request.ts +286 -59
  117. package/src/orchestrator/save-lane.ts +24 -13
  118. package/src/orchestrator/shell-verdict.ts +20 -1
  119. package/src/orchestrator/signals.ts +103 -8
  120. package/src/orchestrator/stable-keys.ts +17 -9
  121. package/src/orchestrator/task-artifacts.ts +15 -2
  122. package/src/orchestrator/task-hash.ts +187 -30
  123. package/src/orchestrator/telemetry-host.ts +25 -6
  124. package/src/orchestrator/telemetry.ts +80 -12
  125. package/src/orchestrator/upstream.ts +3 -1
  126. package/src/report/events.ts +20 -3
  127. package/src/report/forecast.ts +103 -37
  128. package/src/report/framed-output.ts +165 -157
  129. package/src/report/index.ts +2 -1
  130. package/src/report/logger.ts +107 -30
  131. package/src/report/plain-output.ts +13 -9
  132. package/src/report/status-line.ts +28 -55
  133. package/src/report/summary.ts +57 -14
  134. package/src/settings.ts +42 -6
  135. package/src/ui/components.ts +36 -42
  136. package/src/ui/index.ts +3 -1
  137. package/src/ui/live-region.ts +67 -8
  138. package/src/ui/terminal.ts +12 -0
  139. package/src/ui/theme.ts +16 -1
  140. package/src/util/config-frame.ts +17 -3
  141. package/src/util/edit-distance.ts +17 -1
  142. package/src/util/errors.ts +23 -7
  143. package/src/util/foreign-cache-dir.ts +60 -0
  144. package/src/util/git-ignored.ts +59 -5
  145. package/src/util/index.ts +15 -1
  146. package/src/util/paths.ts +50 -6
  147. package/src/util/plain-text.ts +19 -0
  148. package/src/util/secret-mask.ts +57 -1
  149. package/src/util/settle.ts +16 -0
  150. package/src/util/size.ts +12 -2
  151. package/src/util/write-file.ts +86 -9
  152. package/src/workspace/affected.ts +225 -27
  153. package/src/workspace/config-bytecode.ts +23 -4
  154. package/src/workspace/config-eval.ts +90 -16
  155. package/src/workspace/config-imports.ts +96 -41
  156. package/src/workspace/config-schema.ts +126 -25
  157. package/src/workspace/dependencies-lock.ts +7 -14
  158. package/src/workspace/filter.ts +10 -1
  159. package/src/workspace/index.ts +3 -1
  160. package/src/workspace/json-data.ts +34 -8
  161. package/src/workspace/lockfile.ts +86 -37
  162. package/src/workspace/migration.ts +46 -25
  163. package/src/workspace/module-state.ts +62 -0
  164. package/src/workspace/nested-dirs.ts +8 -0
  165. package/src/workspace/project-loader.ts +276 -32
  166. package/src/workspace/repo-id.ts +29 -6
  167. package/src/workspace/workspace.ts +190 -30
@@ -20,6 +20,9 @@ import {
20
20
  type GitFilesCache,
21
21
  } from '../cache/index.js'
22
22
  import type { TaskNode } from '../graph/index.js'
23
+ import { type Logger, warnOf } from '../report/index.js'
24
+ import type { CommandWindows } from './command-windows.js'
25
+ import { bracketLiteralHint } from './miss-save.js'
23
26
 
24
27
  export interface DeferredEntry {
25
28
  /** Fetches this task's outputs onto disk. At-most-once by construction. */
@@ -33,10 +36,14 @@ export interface DeferredOutputsArgs {
33
36
  nodes: Map<string, TaskNode>
34
37
  cache: CacheLayer
35
38
  workspaceRoot: string
36
- nestedDirsByProject: Map<string, string[]>
39
+ /** Where each project's outputs stop (prepare.ts `outputWallsOf`). */
40
+ nestedDirsByProject: ReadonlyMap<string, string[]>
37
41
  gitFilesCache?: GitFilesCache
38
42
  /** False when the run's policy writes no local entries. */
39
43
  localWrite: boolean
44
+ log: Logger
45
+ /** The run's command windows: a fetch writes into the tree as a restore does. */
46
+ commandWindows?: CommandWindows
40
47
  }
41
48
 
42
49
  export class DeferredOutputs {
@@ -121,6 +128,11 @@ export class DeferredOutputs {
121
128
  projectDir: producer.projectDir,
122
129
  outputs,
123
130
  nestedProjectDirs,
131
+ inputs: {
132
+ projectDir: producer.projectDir,
133
+ workspaceRoot: this.args.workspaceRoot,
134
+ declared: producer.config.inputs,
135
+ },
124
136
  committed: committedFiles(this.args.gitFilesCache, producer.projectDir),
125
137
  }
126
138
 
@@ -131,18 +143,26 @@ export class DeferredOutputs {
131
143
  // reader of these outputs can be in the run — `deferralEligibility`
132
144
  // keeps every same-project reader and every workspaceFiles reader
133
145
  // eager — so deleting the post-fetch mark survives the whole core
134
- // suite (item 643).
135
- if (outputs.length > 0) {
136
- const cleaned = await cleanOutputs(cleanArgs)
137
- this.args.gitFilesCache?.markOutputsChanged(producer.projectDir, cleaned)
138
- }
146
+ // suite (item 643). In a window, as a hit's restore (hit-restore.ts):
147
+ // without one, `vx watch` took the fetched outputs for the user's edit
148
+ // and ran one more cycle.
149
+ const closeWindow =
150
+ this.args.commandWindows?.open(taskId, producer.projectDir, 'project') ?? (() => {})
139
151
  try {
140
- await entry.materialize()
141
- } catch (err) {
142
- const msg = err instanceof Error ? err.message : String(err)
143
- throw new Error(
144
- `could not fetch deferred outputs of ${taskId}: ${msg} — re-run it with --force, or use --cache-download=all`,
145
- )
152
+ if (outputs.length > 0) {
153
+ const cleaned = await cleanOutputs(cleanArgs)
154
+ this.args.gitFilesCache?.markOutputsChanged(producer.projectDir, cleaned)
155
+ }
156
+ try {
157
+ await entry.materialize()
158
+ } catch (err) {
159
+ const msg = err instanceof Error ? err.message : String(err)
160
+ throw new Error(
161
+ `could not fetch deferred outputs of ${taskId}: ${msg} — re-run it with --force, or use --cache-download=all`,
162
+ )
163
+ }
164
+ } finally {
165
+ closeWindow()
146
166
  }
147
167
  const { files: outputFiles, emptyDirs } = await resolveOutputTree(cleanArgs)
148
168
  this.args.gitFilesCache?.markOutputsChanged(
@@ -153,6 +173,15 @@ export class DeferredOutputs {
153
173
  // and nothing downstream learns a second shape. Policy still governs —
154
174
  // a run that writes no local entries writes none here either.
155
175
  if (this.args.localWrite && entry.hash !== undefined) {
176
+ // The eager save's warning: a fetch that landed nothing under the
177
+ // declared outputs saves an empty artifact the next run replays.
178
+ if (outputs.length > 0 && outputFiles.length === 0) {
179
+ warnOf(this.args.log)(
180
+ `[vx] ${taskId}: outputs matched no files (${outputs.join(', ')}) — ` +
181
+ `an empty artifact is saved; a later hit restores nothing` +
182
+ bracketLiteralHint(outputs.map((g) => [producer.projectDir, g] as const)),
183
+ )
184
+ }
156
185
  await this.args.cache.save({
157
186
  hash: entry.hash,
158
187
  projectDir: producer.projectDir,
@@ -20,6 +20,7 @@ import {
20
20
  machineParallelism,
21
21
  treeBytes,
22
22
  } from '../util/index.js'
23
+ import { isDefaultBuild } from '../graph/index.js'
23
24
  import * as ui from '../ui/index.js'
24
25
  import { vxPath } from '../handoff.js'
25
26
  import { VERSION } from '../version.js'
@@ -105,14 +106,17 @@ export interface InfoFacts {
105
106
  cacheIndexError: string | null
106
107
  /**
107
108
  * Every setting (src/settings.ts) as a run with no flags takes it: its
108
- * variable's value, else the workspace's (after the `config` stage),
109
+ * variable's value, else the workspace's (after the `config` stage;
110
+ * source `plugin`, naming it, when a plugin's `config` hook set it),
109
111
  * else null with source `default` (docs/schema.md names each default).
110
112
  */
111
113
  settings: Record<
112
114
  string,
113
115
  {
114
116
  value: string | number | boolean | null
115
- source: 'env' | 'workspace' | 'default'
117
+ source: 'env' | 'workspace' | 'plugin' | 'default'
118
+ /** The plugin whose `config` hook set it, with source `plugin`. */
119
+ plugin?: string
116
120
  /** Why a run refuses this variable's value; absent when it takes it. */
117
121
  refused?: string
118
122
  }
@@ -199,7 +203,7 @@ async function collectInfoChecked(cwd: string, opts: CollectInfoOptions): Promis
199
203
  async function collectWorkspaceInfo(
200
204
  cwd: string,
201
205
  root: string,
202
- { workspaceConfig, plugins }: Awaited<ReturnType<typeof loadWorkspacePlugins>>,
206
+ { workspaceConfig, plugins, settingPlugins }: Awaited<ReturnType<typeof loadWorkspacePlugins>>,
203
207
  opts: CollectInfoOptions,
204
208
  probing: Promise<Omit<InfoFacts['sandbox'], 'declared'>>,
205
209
  ): Promise<InfoFacts> {
@@ -274,7 +278,9 @@ async function collectWorkspaceInfo(
274
278
  })
275
279
  for (const p of loaded.projects.values()) {
276
280
  const tasks = p.config.tasks ?? {}
277
- taskCount += Object.keys(tasks).length
281
+ // The default build is no declared task: `vx run build` denies it, and
282
+ // right after `vx init` the doctor said `1 (1 task)` of `tasks: {}`.
283
+ taskCount += Object.values(tasks).filter((t) => !isDefaultBuild(t)).length
278
284
  for (const t of Object.values(tasks)) if (t?.exec?.sandbox !== undefined) sandboxed++
279
285
  }
280
286
  } catch (err) {
@@ -332,7 +338,7 @@ async function collectWorkspaceInfo(
332
338
  cacheEntries: stats.entryCount,
333
339
  cacheBytes: stats.totalBytes,
334
340
  cacheIndexError: indexError,
335
- settings: settingsFact(workspaceConfig),
341
+ settings: settingsFact(workspaceConfig, settingPlugins),
336
342
  // A row-less artifact is indexed again only when its task asks for its
337
343
  // key; the rest only `vx cache clean` reclaims (after an upgrade's
338
344
  // schema reset, most often).
@@ -469,17 +475,23 @@ function gitVersion(): string | null {
469
475
  }
470
476
  }
471
477
 
472
- function settingsFact(config: WorkspaceConfig | null): InfoFacts['settings'] {
478
+ function settingsFact(
479
+ config: WorkspaceConfig | null,
480
+ settingPlugins: ReadonlyMap<string, string>,
481
+ ): InfoFacts['settings'] {
473
482
  const out: InfoFacts['settings'] = {}
474
483
  for (const s of SETTINGS) {
475
484
  const env = s.env === false ? undefined : process.env[envName(s.path)]
476
485
  const declared = s.config === false ? undefined : atPath(config, s.path)
486
+ const plugin = settingPlugins.get(s.path)
477
487
  out[s.path] =
478
488
  env !== undefined && env !== ''
479
489
  ? { value: env, source: 'env' }
480
- : declared !== undefined
481
- ? { value: declared as string | number | boolean, source: 'workspace' }
482
- : { value: null, source: 'default' }
490
+ : declared === undefined
491
+ ? { value: null, source: 'default' }
492
+ : plugin !== undefined
493
+ ? { value: declared as string | number | boolean, source: 'plugin', plugin }
494
+ : { value: declared as string | number | boolean, source: 'workspace' }
483
495
  }
484
496
  return out
485
497
  }
@@ -141,8 +141,8 @@ function prefixesOverlap(a: string, b: string): boolean {
141
141
  export function resolveDownloadModes(args: {
142
142
  nodes: Map<string, TaskNode>
143
143
  policy: 'all' | 'toplevel' | 'none'
144
- /** Task ids placed on a LOCAL executor — they write in place. */
145
- localPlaced: ReadonlySet<string>
144
+ /** Placed on a LOCAL executor: it writes in place. */
145
+ localPlaced: (id: string) => boolean
146
146
  /** Task ids with `exec.remote: 'only'`. */
147
147
  remoteOnly: ReadonlySet<string>
148
148
  }): { modeOf: Map<string, DownloadMode>; downgrades: Map<string, string> } {
@@ -156,7 +156,7 @@ export function resolveDownloadModes(args: {
156
156
  modeOf.set(n.id, 'never')
157
157
  continue
158
158
  }
159
- if (args.policy === 'all' || args.localPlaced.has(n.id)) {
159
+ if (args.policy === 'all' || args.localPlaced(n.id)) {
160
160
  modeOf.set(n.id, 'eager')
161
161
  continue
162
162
  }
@@ -14,7 +14,13 @@
14
14
  // full run looks up.
15
15
 
16
16
  import type { CacheLayer, GitFilesCache } from '../cache/index.js'
17
- import { isGroupTask, keyedDeps, type TaskNode, type TaskOutcome } from '../graph/index.js'
17
+ import {
18
+ isGroupTask,
19
+ keyedDeps,
20
+ standInsOf,
21
+ type TaskNode,
22
+ type TaskOutcome,
23
+ } from '../graph/index.js'
18
24
  import { computeGroupKey, computeTaskHash, type HashCache } from './task-hash.js'
19
25
  import { filterUpstreamHashes, keyedOutcome } from './upstream.js'
20
26
 
@@ -133,6 +139,12 @@ function foldsExcludedKey(node: TaskNode): boolean {
133
139
  if (excluded === undefined) return false
134
140
  if (isGroupTask(node)) return true
135
141
  return (
136
- filterUpstreamHashes(excluded, node.config.inputs?.tasks, node.projectName, node.id).length > 0
142
+ filterUpstreamHashes(
143
+ excluded,
144
+ node.config.inputs?.tasks,
145
+ node.projectName,
146
+ node.id,
147
+ standInsOf(node),
148
+ ).length > 0
137
149
  )
138
150
  }
@@ -10,6 +10,7 @@ import type { DownloadMode } from './download-policy.js'
10
10
  import type { ExecuteArgs } from './execute-task.js'
11
11
  import type { ShortCircuit } from './local-shortcircuit.js'
12
12
  import { UNPLACED_EXECUTOR, type Placements } from './placement.js'
13
+ import { outputWallsOf } from './prepare.js'
13
14
 
14
15
  type TaskField =
15
16
  | 'node'
@@ -17,7 +18,9 @@ type TaskField =
17
18
  | 'log'
18
19
  | 'executor'
19
20
  | 'nestedProjectDirs'
21
+ | 'outputNestedDirs'
20
22
  | 'remoteOnlyNoop'
23
+ | 'belowNoop'
21
24
  | 'remoteOnly'
22
25
  | 'download'
23
26
  | 'preProbed'
@@ -35,7 +38,8 @@ export interface TaskFacts {
35
38
  log: (node: TaskNode) => Logger
36
39
  placements: Placements
37
40
  nestedDirsByProject: ReadonlyMap<string, string[]>
38
- downloadModeOf: ReadonlyMap<string, DownloadMode>
41
+ /** None under `cache.download: all`: every task eager. */
42
+ downloadModeOf: ReadonlyMap<string, DownloadMode> | undefined
39
43
  shortCircuit: Pick<ShortCircuit, 'preProbed' | 'uncachedKeys' | 'groupKeys'>
40
44
  /** Tasks some other task depends on. */
41
45
  dependedOn: ReadonlySet<string>
@@ -53,6 +57,7 @@ export function executeArgsOf(
53
57
  facts: TaskFacts,
54
58
  ): (node: TaskNode, upstream: TaskOutcome[], reuseProbe: boolean, tainted: boolean) => ExecuteArgs {
55
59
  const { placements, shortCircuit } = facts
60
+ const walls = outputWallsOf(facts.nestedDirsByProject)
56
61
  return (node, upstream, reuseProbe, tainted) => {
57
62
  const id = node.id
58
63
  const a: ExecuteArgs = {
@@ -66,8 +71,10 @@ export function executeArgsOf(
66
71
  const probe = reuseProbe ? shortCircuit.preProbed.get(id) : undefined
67
72
  const upfrontKey = shortCircuit.uncachedKeys.get(id)
68
73
  const upfrontGroupKey = shortCircuit.groupKeys.get(id)
69
- if (facts.downloadModeOf.get(id) === 'deferred') a.download = 'deferred'
74
+ if (walls !== facts.nestedDirsByProject) a.outputNestedDirs = walls.get(node.projectName) ?? []
75
+ if (facts.downloadModeOf?.get(id) === 'deferred') a.download = 'deferred'
70
76
  if (placements.remoteOnlyNoop.has(id)) a.remoteOnlyNoop = true
77
+ if (placements.belowNoop.has(id)) a.belowNoop = true
71
78
  if (placements.remoteOnly.has(id)) a.remoteOnly = true
72
79
  if (placements.carried.has(id)) a.carried = true
73
80
  const servers = placements.servers.get(id)