@vzn/vx 0.0.651 → 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 (177) hide show
  1. package/README.md +3 -4
  2. package/launcher.sh +2 -1
  3. package/package.json +5 -5
  4. package/schemas/error.json +1 -1
  5. package/schemas/show.json +15 -20
  6. package/skills/vx/SKILL.md +3 -3
  7. package/src/bin.ts +37 -3
  8. package/src/cache/archive.ts +197 -32
  9. package/src/cache/cache.ts +882 -230
  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 +151 -0
  16. package/src/cache/history-writer.ts +76 -0
  17. package/src/cache/index.ts +12 -1
  18. package/src/cache/inputs.ts +616 -135
  19. package/src/cache/key-fold.ts +1 -1
  20. package/src/cache/layer.ts +29 -16
  21. package/src/cache/layered-cache.ts +28 -7
  22. package/src/cache/lockfile-memo.ts +2 -1
  23. package/src/cache/output-index.ts +106 -44
  24. package/src/cache/plugin-cache.ts +2 -2
  25. package/src/cache/policy.ts +6 -2
  26. package/src/cache/remote-lane.ts +3 -1
  27. package/src/cache/run-history.ts +94 -25
  28. package/src/cache/schema.ts +46 -10
  29. package/src/cli/cache.ts +149 -36
  30. package/src/cli/completions.ts +2 -2
  31. package/src/cli/core-alias.ts +9 -2
  32. package/src/cli/docs.ts +4 -2
  33. package/src/cli/help.ts +27 -4
  34. package/src/cli/index.ts +62 -7
  35. package/src/cli/info.ts +5 -4
  36. package/src/cli/init.ts +55 -20
  37. package/src/cli/last.ts +218 -43
  38. package/src/cli/lock.ts +187 -24
  39. package/src/cli/plan-format.ts +24 -21
  40. package/src/cli/plugin-commands.ts +6 -1
  41. package/src/cli/plugin-templates.ts +36 -45
  42. package/src/cli/run.ts +68 -34
  43. package/src/cli/select.ts +160 -64
  44. package/src/cli/show.ts +70 -28
  45. package/src/cli/task-verb.ts +12 -7
  46. package/src/cli/upgrade.ts +210 -76
  47. package/src/cli/watch-filter.ts +88 -15
  48. package/src/cli/watch-fs.ts +34 -47
  49. package/src/cli/watch-judge.ts +73 -9
  50. package/src/cli/watch-set.ts +10 -8
  51. package/src/cli/watch.ts +190 -33
  52. package/src/cli/why.ts +107 -33
  53. package/src/cli/workspace-config.ts +11 -1
  54. package/src/config.ts +56 -30
  55. package/src/exec/env.ts +3 -1
  56. package/src/exec/executor.ts +116 -16
  57. package/src/exec/index.ts +1 -0
  58. package/src/exec/kill-tree.ts +7 -7
  59. package/src/exec/runner.ts +270 -108
  60. package/src/exec/sandbox-binds.ts +109 -18
  61. package/src/exec/sandbox-paths.ts +3 -1
  62. package/src/exec/sandbox-runtime.ts +446 -64
  63. package/src/exec/sandbox-violations.ts +250 -46
  64. package/src/exec/spawn-off.ts +10 -5
  65. package/src/exec/task-proxy.ts +93 -0
  66. package/src/exec/warm-shell.ts +29 -7
  67. package/src/graph/definition.ts +16 -3
  68. package/src/graph/folds.ts +89 -16
  69. package/src/graph/index.ts +8 -0
  70. package/src/graph/priorities.ts +8 -5
  71. package/src/graph/scheduler.ts +176 -39
  72. package/src/graph/stand-ins.ts +36 -0
  73. package/src/graph/task-graph.ts +347 -160
  74. package/src/graph/task-status.ts +5 -1
  75. package/src/handoff.ts +45 -10
  76. package/src/history/failure-mode.ts +39 -6
  77. package/src/history/history.ts +103 -30
  78. package/src/history/index.ts +2 -0
  79. package/src/history/metrics.ts +193 -73
  80. package/src/index.ts +6 -3
  81. package/src/orchestrator/admission.ts +14 -10
  82. package/src/orchestrator/affected-tasks.ts +35 -13
  83. package/src/orchestrator/command-windows.ts +8 -5
  84. package/src/orchestrator/deferred-outputs.ts +42 -13
  85. package/src/orchestrator/doctor.ts +21 -9
  86. package/src/orchestrator/download-policy.ts +8 -10
  87. package/src/orchestrator/excluded-keys.ts +15 -3
  88. package/src/orchestrator/execute-args.ts +9 -2
  89. package/src/orchestrator/execute-task.ts +612 -138
  90. package/src/orchestrator/graph-memo.ts +26 -4
  91. package/src/orchestrator/graph-stage.ts +27 -4
  92. package/src/orchestrator/held-servers.ts +36 -14
  93. package/src/orchestrator/hit-restore.ts +141 -93
  94. package/src/orchestrator/index.ts +4 -1
  95. package/src/orchestrator/keyed-projects.ts +53 -0
  96. package/src/orchestrator/local-floor.ts +21 -3
  97. package/src/orchestrator/local-shortcircuit.ts +2 -2
  98. package/src/orchestrator/lockfile-claim.ts +30 -4
  99. package/src/orchestrator/miss-save.ts +60 -61
  100. package/src/orchestrator/options.ts +13 -0
  101. package/src/orchestrator/output-host.ts +16 -4
  102. package/src/orchestrator/output-log.ts +1 -54
  103. package/src/orchestrator/persistent-task.ts +32 -16
  104. package/src/orchestrator/persistent.ts +152 -15
  105. package/src/orchestrator/placement.ts +82 -16
  106. package/src/orchestrator/plan.ts +39 -17
  107. package/src/orchestrator/plugin-host.ts +238 -110
  108. package/src/orchestrator/plugin.ts +59 -27
  109. package/src/orchestrator/prepare.ts +194 -38
  110. package/src/orchestrator/projects.ts +89 -10
  111. package/src/orchestrator/read-only.ts +115 -0
  112. package/src/orchestrator/remote-prefetch.ts +1 -1
  113. package/src/orchestrator/run-context.ts +6 -2
  114. package/src/orchestrator/run-failures.ts +101 -23
  115. package/src/orchestrator/run-id.ts +13 -2
  116. package/src/orchestrator/run-keys.ts +87 -28
  117. package/src/orchestrator/run-live.ts +16 -13
  118. package/src/orchestrator/run-lock.ts +27 -4
  119. package/src/orchestrator/run-records.ts +95 -74
  120. package/src/orchestrator/run-report.ts +3 -2
  121. package/src/orchestrator/run-stop.ts +17 -9
  122. package/src/orchestrator/run.ts +531 -122
  123. package/src/orchestrator/sandbox-fix.ts +40 -12
  124. package/src/orchestrator/sandbox-request.ts +314 -44
  125. package/src/orchestrator/save-lane.ts +24 -13
  126. package/src/orchestrator/shell-verdict.ts +29 -1
  127. package/src/orchestrator/signals.ts +103 -8
  128. package/src/orchestrator/stable-keys.ts +34 -27
  129. package/src/orchestrator/task-artifacts.ts +16 -3
  130. package/src/orchestrator/task-hash.ts +191 -34
  131. package/src/orchestrator/telemetry-host.ts +25 -6
  132. package/src/orchestrator/telemetry.ts +85 -13
  133. package/src/orchestrator/upstream.ts +3 -1
  134. package/src/report/events.ts +23 -6
  135. package/src/report/forecast.ts +103 -37
  136. package/src/report/framed-output.ts +179 -162
  137. package/src/report/index.ts +2 -1
  138. package/src/report/logger.ts +108 -31
  139. package/src/report/plain-output.ts +15 -9
  140. package/src/report/status-line.ts +28 -55
  141. package/src/report/summary.ts +61 -18
  142. package/src/settings.ts +50 -14
  143. package/src/ui/components.ts +36 -42
  144. package/src/ui/index.ts +3 -1
  145. package/src/ui/live-region.ts +67 -8
  146. package/src/ui/terminal.ts +12 -0
  147. package/src/ui/theme.ts +16 -1
  148. package/src/util/config-frame.ts +142 -6
  149. package/src/util/edit-distance.ts +17 -1
  150. package/src/util/errors.ts +23 -7
  151. package/src/util/foreign-cache-dir.ts +60 -0
  152. package/src/util/git-ignored.ts +59 -5
  153. package/src/util/index.ts +15 -1
  154. package/src/util/paths.ts +51 -7
  155. package/src/util/plain-text.ts +19 -0
  156. package/src/util/secret-mask.ts +57 -1
  157. package/src/util/settle.ts +18 -1
  158. package/src/util/size.ts +12 -2
  159. package/src/util/tail.ts +4 -1
  160. package/src/util/write-file.ts +86 -9
  161. package/src/workspace/affected.ts +225 -27
  162. package/src/workspace/bracket-literal.ts +2 -2
  163. package/src/workspace/config-bytecode.ts +23 -4
  164. package/src/workspace/config-eval.ts +90 -16
  165. package/src/workspace/config-imports.ts +96 -41
  166. package/src/workspace/config-schema.ts +192 -86
  167. package/src/workspace/dependencies-lock.ts +7 -14
  168. package/src/workspace/filter.ts +18 -2
  169. package/src/workspace/index.ts +3 -1
  170. package/src/workspace/json-data.ts +34 -8
  171. package/src/workspace/lockfile.ts +86 -37
  172. package/src/workspace/migration.ts +58 -31
  173. package/src/workspace/module-state.ts +62 -0
  174. package/src/workspace/nested-dirs.ts +8 -0
  175. package/src/workspace/project-loader.ts +276 -32
  176. package/src/workspace/repo-id.ts +29 -6
  177. package/src/workspace/workspace.ts +190 -30
@@ -15,18 +15,31 @@ const memo = new WeakMap<TaskNode, string | undefined>()
15
15
  export function taskDefinition(node: TaskNode): string | undefined {
16
16
  if (memo.has(node)) return memo.get(node)
17
17
  const exec = node.config.exec
18
+ const define = exec?.env?.define
18
19
  const def =
19
20
  exec?.command === undefined
20
21
  ? undefined
21
22
  : xxh3hex(
22
- `${node.id}\0${exec.command}\0${JSON.stringify(
23
- Object.entries(exec.env?.define ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
24
- )}`,
23
+ `${node.id}\0${exec.command}\0${
24
+ // `[]` is what the stringify gives for no values: most tasks
25
+ // have none, and stringifying it for each was ~9 ms of an
26
+ // 8,002-hit warm run.
27
+ define === undefined || isEmpty(define)
28
+ ? '[]'
29
+ : JSON.stringify(
30
+ Object.entries(define).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
31
+ )
32
+ }`,
25
33
  )
26
34
  memo.set(node, def)
27
35
  return def
28
36
  }
29
37
 
38
+ function isEmpty(o: object): boolean {
39
+ for (const _ in o) return false
40
+ return true
41
+ }
42
+
30
43
  /** Each task's definition (`taskDefinition`) by id, groups left out: what a history reader matches rows to. */
31
44
  export function taskDefinitions(nodes: Iterable<TaskNode>): Map<string, string> {
32
45
  const out = new Map<string, string>()
@@ -10,6 +10,7 @@ import {
10
10
  parseDependencySpec,
11
11
  type DependencySpec,
12
12
  } from '../util/index.js'
13
+ import { standInsOf, type StandIns } from './stand-ins.js'
13
14
  import type { TaskNode } from './task-graph.js'
14
15
 
15
16
  /**
@@ -36,9 +37,13 @@ export function keyedDeps(node: TaskNode): readonly string[] {
36
37
  * A filter selects among the task's own dependencies, never through a
37
38
  * group: `dependsOn: ['build']` over a group `build` of `build.a` and
38
39
  * `build.b` offers the filter `build`, not its members. A pattern that
39
- * matches only a member (`build.*`) therefore selected nothing, and the key stopped moving
40
- * with that member's inputs: a stale hit under a green run. Such a spec
41
- * is refused, once, over the graph every key site reads. One that matches
40
+ * matches a member (`build.*`) the filter folds neither directly nor
41
+ * through a selected group therefore left it out, and the key stopped
42
+ * moving with that member's inputs: a stale hit under a green run. That
43
+ * held when the pattern also matched another dependency directly
44
+ * (`dependsOn: ['build', 'build.a']`), so such a spec is refused, once,
45
+ * over the graph every key site reads, unless it matches the group
46
+ * itself (a negation of the group is a decision). One that matches
42
47
  * nothing anywhere stays legal (a preset's pattern needn't match in every
43
48
  * project). An exact name is refused earlier, at load, unless `dependsOn`
44
49
  * names it (`assertFilterNamesDeclaredDeps`).
@@ -49,20 +54,37 @@ export function refuseFiltersThroughGroups(nodes: ReadonlyMap<string, TaskNode>)
49
54
  if (filter === undefined || filter.length === 0) continue
50
55
  const deps = keyedDeps(node).flatMap((id) => nodes.get(id) ?? [])
51
56
  const groups = deps.filter(isGroupTask)
57
+ const folded = new Set<TaskNode>()
58
+ if (groups.length > 0) {
59
+ const units = deps.map((n) => ({ node: n, unit: n.id }))
60
+ const picked = selectFoldedDeps(units, filter, node.projectName, node.id, standInsOf(node))
61
+ for (const { node: n } of picked) {
62
+ folded.add(n)
63
+ if (isGroupTask(n)) for (const m of groupMembers(n, nodes)) folded.add(m)
64
+ }
65
+ }
52
66
  for (const raw of filter) {
53
67
  const spec = parseSpec(raw, node.id)
54
68
  if (spec.negated) continue
55
69
  const matches = specMatcher(spec)
56
70
  const hit = (n: TaskNode): boolean => matches(n, n.projectName === node.projectName)
57
- if (deps.some(hit)) continue
71
+ const direct = deps.some(hit)
58
72
  for (const group of groups) {
59
- const member = groupMembers(group, nodes).find(hit)
73
+ if (hit(group)) continue
74
+ const member = groupMembers(group, nodes).find(
75
+ (m) => hit(m) && !folded.has(m) && !deps.includes(m),
76
+ )
60
77
  if (member === undefined) continue
78
+ const outcome = direct ? 'so the key leaves it out' : 'so it selects nothing'
61
79
  throw new UserError(
62
- `${node.id}: inputs.tasks: '${raw}' matches ${member.id} only through the group ${group.id}, so it selects nothing; name '${group.taskName}' or depend on ${member.id} directly`,
80
+ `${node.id}: inputs.tasks: '${raw}' matches ${member.id} only through the group ${group.id}, ${outcome}; name '${group.taskName}' or depend on ${member.id} directly`,
63
81
  )
64
82
  }
65
- if (namesNoDependency(spec, node.config.dependsOn ?? [])) {
83
+ if (direct) continue
84
+ if (
85
+ namesNoDependency(spec, node.config.dependsOn ?? []) ||
86
+ namesTransitiveDependency(spec, node, nodes, hit)
87
+ ) {
66
88
  const named = deps.map((d) => d.id).join(', ') || 'none'
67
89
  throw new UserError(
68
90
  `${node.id}: inputs.tasks: '${raw}' names none of its dependencies (${named}), so it selects nothing and the key would not move with them; fix the name, or use [] to decouple on purpose`,
@@ -100,6 +122,34 @@ function namesNoDependency(spec: DependencySpec, dependsOn: readonly string[]):
100
122
  })
101
123
  }
102
124
 
125
+ /**
126
+ * An exact `pkg#task` filter that matched no dependency but names a task the
127
+ * key reaches only through one. `namesNoDependency` lets it through when a
128
+ * `^` entry names the task (the preset whose project lacks that dependency),
129
+ * but `a → b → c` with `dependsOn: ['^build']` and `['c#build']` selected
130
+ * none of a's direct dependencies, and an edit to c replayed a's hit.
131
+ */
132
+ function namesTransitiveDependency(
133
+ spec: DependencySpec,
134
+ node: TaskNode,
135
+ nodes: ReadonlyMap<string, TaskNode>,
136
+ hit: (n: TaskNode) => boolean,
137
+ ): boolean {
138
+ if (spec.kind !== 'cross' || isTaskPattern(spec.project) || isTaskPattern(spec.task)) return false
139
+ const seen = new Set<string>([node.id])
140
+ const stack = [...keyedDeps(node)]
141
+ while (stack.length > 0) {
142
+ const id = stack.pop()!
143
+ if (seen.has(id)) continue
144
+ seen.add(id)
145
+ const n = nodes.get(id)
146
+ if (n === undefined) continue
147
+ if (hit(n)) return true
148
+ stack.push(...keyedDeps(n))
149
+ }
150
+ return false
151
+ }
152
+
103
153
  function parseSpec(raw: string, selfTaskId: string): DependencySpec {
104
154
  try {
105
155
  return parseDependencySpec(raw, 'inputs.tasks')
@@ -162,16 +212,26 @@ export interface FoldCandidate {
162
212
  * - `filter === undefined` → every candidate, as given.
163
213
  * - `filter === []` → none (fully decoupled).
164
214
  *
165
- * Otherwise deduped by `unit`, first candidate kept. The unit is the
166
- * fold's own identity, not the task's: two groups over the same members
167
- * share one hash, so excluding either excludes both, and a selection by
168
- * task id would keep the other while the key folded neither.
215
+ * Otherwise deduped by `unit`, first selected candidate kept. The unit is
216
+ * the fold's own identity, not the task's: two groups over the same members
217
+ * share one hash. A negation drops only the candidates it names, and the
218
+ * dedup runs after every entry, so a unit still selected through another
219
+ * candidate stays folded: deleting by unit let `['^*', '!c#build']` drop
220
+ * b#build's group with c#build's, and a change beneath both replayed the
221
+ * old hit.
222
+ *
223
+ * `standIns` is the task's `standInsOf`: an edge added past a
224
+ * default build on a package cycle is selected whenever an edge it went
225
+ * past is. That build folds nothing of its cycle, so `['build']` on
226
+ * `a ↔ b` selected a#build alone, dropped b#build, and an edit to b
227
+ * replayed the old hit.
169
228
  */
170
229
  export function selectFoldedDeps(
171
230
  deps: readonly FoldCandidate[],
172
231
  filter: readonly string[] | undefined,
173
232
  selfProjectName: string,
174
233
  selfTaskId: string,
234
+ standIns?: StandIns,
175
235
  ): FoldCandidate[] {
176
236
  if (filter === undefined) return [...deps]
177
237
 
@@ -180,18 +240,25 @@ export function selectFoldedDeps(
180
240
  // Per-spec predicate, compiled once (exact compares + patterns).
181
241
  const matchers = specs.map((spec) => specMatcher(spec))
182
242
 
183
- const selected = new Map<string, FoldCandidate>()
243
+ const selected = new Set<FoldCandidate>()
184
244
  for (let i = 0; i < specs.length; i++) {
185
245
  const spec = specs[i]!
186
246
  const matches = matchers[i]!
187
247
  for (const d of deps) {
188
248
  const isSelf = d.node.projectName === selfProjectName
189
249
  if (!matches(d.node, isSelf)) continue
190
- if (spec.negated) selected.delete(d.unit)
191
- else if (!selected.has(d.unit)) selected.set(d.unit, d)
250
+ if (spec.negated) selected.delete(d)
251
+ else selected.add(d)
192
252
  }
193
253
  }
194
- return [...selected.values()]
254
+ if (standIns !== undefined) {
255
+ const ids = new Set<string>()
256
+ for (const d of selected) ids.add(d.node.id)
257
+ for (const d of deps) if (standIns[d.node.id]?.some((p) => ids.has(p))) selected.add(d)
258
+ }
259
+ const byUnit = new Map<string, FoldCandidate>()
260
+ for (const d of selected) if (!byUnit.has(d.unit)) byUnit.set(d.unit, d)
261
+ return [...byUnit.values()]
195
262
  }
196
263
 
197
264
  /** Exact-name compare, or the shared `*`-glob when the name is a pattern. */
@@ -250,7 +317,13 @@ export function foldedDeps(node: TaskNode, nodeOf: (id: string) => TaskNode): Fo
250
317
  candidates.push({ node: dep, unit: foldUnit(dep) })
251
318
  }
252
319
  if (isGroupTask(node) && node.config.inputs === undefined) return candidates
253
- return selectFoldedDeps(candidates, node.config.inputs?.tasks, node.projectName, node.id)
320
+ return selectFoldedDeps(
321
+ candidates,
322
+ node.config.inputs?.tasks,
323
+ node.projectName,
324
+ node.id,
325
+ standInsOf(node),
326
+ )
254
327
  }
255
328
 
256
329
  /**
@@ -18,6 +18,7 @@ export {
18
18
  } from './folds.js'
19
19
  export { DEFAULT_BUILD, isDefaultBuild } from './default-build.js'
20
20
  export { computeReverseDepCount } from './priorities.js'
21
+ export { setStandIns, type StandIns, standInsOf } from './stand-ins.js'
21
22
  export { criticalPath } from './critical-path.js'
22
23
  export { taskDefinition, taskDefinitions } from './definition.js'
23
24
  export {
@@ -26,6 +27,13 @@ export {
26
27
  idleServers,
27
28
  type InputChanges,
28
29
  RestoreDemoted,
30
+ isKeyFailed,
31
+ KeyFailed,
32
+ markKeyFailed,
33
+ markOutputsFailed,
34
+ OutputsFailed,
35
+ outputsFailedOf,
36
+ RestoreFailed,
29
37
  runGraph,
30
38
  type TaskOutcome,
31
39
  } from './scheduler.js'
@@ -347,10 +347,14 @@ export function strategyPriorities(
347
347
  return out
348
348
  }
349
349
 
350
+ /**
351
+ * A task's merged weight, read as asked: a merged copy held an entry per
352
+ * task of the run beside the two maps it was made of.
353
+ */
350
354
  export function mergePriorities(
351
355
  baseline: ReadonlyMap<string, number>,
352
356
  overrides: ReadonlyMap<string, number>,
353
- ): ReadonlyMap<string, number> {
357
+ ): (id: string) => number {
354
358
  // The override caller scored "expected critical-path duration" in ms.
355
359
  // Baseline reverse-deps counts are bounded by N; scale the override so
356
360
  // it sorts above the baseline for any node it covers, and add the
@@ -361,10 +365,9 @@ export function mergePriorities(
361
365
  const SCALE = 1 << 20
362
366
  let lo = 0
363
367
  for (const w of overrides.values()) if (w < lo) lo = w
364
- const out = new Map(baseline)
365
- for (const [id, w] of overrides) {
368
+ return (id) => {
366
369
  const b = baseline.get(id) ?? 0
367
- out.set(id, (w - lo + 1) * SCALE + b)
370
+ const w = overrides.get(id)
371
+ return w === undefined ? b : (w - lo + 1) * SCALE + b
368
372
  }
369
- return out
370
373
  }