@vzn/vx 0.0.16 → 0.0.21

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 (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -124
  3. package/index.ts +10 -0
  4. package/launcher.mjs +10 -11
  5. package/package.json +11 -7
  6. package/src/bin.ts +14 -4
  7. package/src/cache/archive.ts +659 -0
  8. package/src/cache/cache.ts +769 -1101
  9. package/src/cache/chained-cache.ts +194 -0
  10. package/src/cache/config-evals.ts +89 -0
  11. package/src/cache/file-hashes.ts +262 -0
  12. package/src/cache/git-inputs.ts +637 -0
  13. package/src/cache/index.ts +24 -5
  14. package/src/cache/inputs.ts +373 -456
  15. package/src/cache/layer.ts +604 -0
  16. package/src/cache/layered-cache.ts +96 -43
  17. package/src/cache/output-index.ts +224 -0
  18. package/src/cache/policy.ts +84 -0
  19. package/src/cache/run-history.ts +147 -0
  20. package/src/cache/tar-stream.ts +358 -0
  21. package/src/cache/zstd.ts +160 -0
  22. package/src/cli/cache.ts +75 -21
  23. package/src/cli/completions.ts +104 -0
  24. package/src/cli/core-alias.ts +25 -0
  25. package/src/cli/help.ts +195 -128
  26. package/src/cli/index.ts +116 -46
  27. package/src/cli/info.ts +122 -63
  28. package/src/cli/init.ts +75 -0
  29. package/src/cli/last.ts +172 -0
  30. package/src/cli/lock.ts +2 -1
  31. package/src/cli/plan-format.ts +72 -6
  32. package/src/cli/plugin-commands.ts +103 -0
  33. package/src/cli/run.ts +245 -271
  34. package/src/cli/select.ts +332 -0
  35. package/src/cli/show.ts +144 -69
  36. package/src/cli/upgrade.ts +9 -7
  37. package/src/cli/watch.ts +858 -110
  38. package/src/cli/why.ts +223 -0
  39. package/src/cli/workspace-config.ts +126 -0
  40. package/src/config.ts +187 -150
  41. package/src/exec/env.ts +18 -1
  42. package/src/exec/executor.ts +224 -0
  43. package/src/exec/index.ts +24 -2
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +166 -40
  46. package/src/exec/sandbox-binds.ts +182 -0
  47. package/src/exec/sandbox-paths.ts +50 -0
  48. package/src/exec/sandbox-runtime.ts +642 -260
  49. package/src/exec/sandbox-violations.ts +280 -0
  50. package/src/graph/index.ts +4 -10
  51. package/src/graph/scheduler.ts +232 -203
  52. package/src/graph/task-graph.ts +222 -8
  53. package/src/index.ts +146 -153
  54. package/src/orchestrator/admission.ts +137 -0
  55. package/src/orchestrator/deferred-outputs.ts +147 -0
  56. package/src/orchestrator/doctor.ts +255 -0
  57. package/src/orchestrator/download-policy.ts +171 -0
  58. package/src/orchestrator/events.ts +72 -8
  59. package/src/orchestrator/execute-task.ts +374 -549
  60. package/src/orchestrator/failure-mode.ts +202 -0
  61. package/src/orchestrator/framed-output.ts +126 -69
  62. package/src/orchestrator/history.ts +126 -59
  63. package/src/orchestrator/hit-restore.ts +221 -0
  64. package/src/orchestrator/index.ts +77 -93
  65. package/src/orchestrator/local-shortcircuit.ts +22 -0
  66. package/src/orchestrator/lockfile-claim.ts +281 -0
  67. package/src/orchestrator/logger.ts +208 -35
  68. package/src/orchestrator/metrics.ts +102 -1388
  69. package/src/orchestrator/miss-save.ts +169 -0
  70. package/src/orchestrator/options.ts +55 -34
  71. package/src/orchestrator/persistent.ts +79 -0
  72. package/src/orchestrator/placement.ts +182 -0
  73. package/src/orchestrator/plan.ts +144 -1
  74. package/src/orchestrator/plugin-host.ts +357 -120
  75. package/src/orchestrator/plugin.ts +298 -58
  76. package/src/orchestrator/prepare.ts +202 -106
  77. package/src/orchestrator/projects.ts +259 -0
  78. package/src/orchestrator/remote-prefetch.ts +12 -8
  79. package/src/orchestrator/run-artifacts.ts +63 -16
  80. package/src/orchestrator/run-context.ts +167 -29
  81. package/src/orchestrator/run-records.ts +149 -0
  82. package/src/orchestrator/run-report.ts +74 -47
  83. package/src/orchestrator/run.ts +534 -417
  84. package/src/orchestrator/sandbox-request.ts +243 -0
  85. package/src/orchestrator/save-lane.ts +47 -0
  86. package/src/orchestrator/signals.ts +111 -0
  87. package/src/orchestrator/stable-keys.ts +129 -31
  88. package/src/orchestrator/status-line.ts +49 -9
  89. package/src/orchestrator/summary.ts +90 -26
  90. package/src/orchestrator/tally.ts +78 -35
  91. package/src/orchestrator/task-hash.ts +111 -15
  92. package/src/orchestrator/task-log-buffer.ts +288 -0
  93. package/src/orchestrator/telemetry-host.ts +34 -6
  94. package/src/orchestrator/telemetry.ts +182 -36
  95. package/src/orchestrator/upstream.ts +72 -31
  96. package/src/util/cgroup.ts +160 -0
  97. package/src/util/edit-distance.ts +70 -0
  98. package/src/util/errors.ts +15 -1
  99. package/src/util/index.ts +15 -2
  100. package/src/util/num.ts +37 -0
  101. package/src/util/paths.ts +64 -0
  102. package/src/util/settle.ts +67 -0
  103. package/src/util/size.ts +5 -1
  104. package/src/util/tail.ts +66 -0
  105. package/src/util/timing.ts +63 -0
  106. package/src/util/verbs.ts +39 -0
  107. package/src/workspace/affected.ts +250 -23
  108. package/src/workspace/config-cache.ts +313 -0
  109. package/src/workspace/config-eval.ts +217 -0
  110. package/src/workspace/config-imports.ts +162 -0
  111. package/src/workspace/config-schema.ts +837 -0
  112. package/src/workspace/filter.ts +65 -10
  113. package/src/workspace/fingerprint.ts +53 -4
  114. package/src/workspace/index.ts +42 -3
  115. package/src/workspace/lockfile.ts +25 -9
  116. package/src/workspace/migrate-scripts.ts +134 -0
  117. package/src/workspace/migration.ts +314 -0
  118. package/src/workspace/nested-dirs.ts +21 -7
  119. package/src/workspace/package-graph.ts +114 -41
  120. package/src/workspace/project-loader.ts +224 -450
  121. package/src/workspace/workspace.ts +298 -72
  122. package/src/cache/cas-backend.ts +0 -97
  123. package/src/cache/digest.ts +0 -47
  124. package/src/cache/tar.ts +0 -343
  125. package/src/cli/backend.ts +0 -23
  126. package/src/cli/mcp-rpc.ts +0 -292
  127. package/src/cli/mcp.ts +0 -92
  128. package/src/cli/migrate-nx.ts +0 -450
  129. package/src/cli/migrate-turbo.ts +0 -364
  130. package/src/cli/migrate.ts +0 -219
  131. package/src/orchestrator/devframe-surface.ts +0 -74
  132. package/src/orchestrator/predict.ts +0 -106
  133. package/src/orchestrator/protocol.ts +0 -156
  134. package/src/orchestrator/resources.ts +0 -59
  135. package/src/orchestrator/run-state.ts +0 -145
  136. package/src/orchestrator/verify.ts +0 -164
  137. package/src/orchestrator/wire-render.ts +0 -92
  138. package/src/orchestrator/wire.ts +0 -212
@@ -23,8 +23,8 @@ export interface TaskNode {
23
23
  /** Ids of tasks that must complete before this one runs. */
24
24
  deps: string[]
25
25
  /**
26
- * True for the tasks the user actually asked for (via cwd, `-r`, `-F`,
27
- * or `pkg#task`). False for dependencies pulled in by `dependsOn`
26
+ * True for the tasks the user actually asked for (via cwd, `--all`,
27
+ * `--filter`, or `pkg#task`). False for deps pulled in by `dependsOn`
28
28
  * expansion. Used by the orchestrator to scope `forwardArgs` so trailing
29
29
  * CLI args don't leak into upstream tasks the user didn't address.
30
30
  */
@@ -38,12 +38,38 @@ export interface TaskNode {
38
38
  * them `requested`, so `forwardArgs` scoping is unaffected.
39
39
  */
40
40
  surfaced?: boolean
41
+ /**
42
+ * Extra cache-key material from plugins' `key` stage, as sorted
43
+ * `[name, value]` pairs; set by `prepareRun`, folded by `Cache.key`.
44
+ * Absent when no plugin declares the stage.
45
+ */
46
+ keyParts?: ReadonlyArray<readonly [name: string, value: string]>
41
47
  }
42
48
 
43
49
  export function taskId(project: string, task: string): string {
44
50
  return `${project}#${task}`
45
51
  }
46
52
 
53
+ /**
54
+ * The inverse of `taskId`. Splits on the FIRST `#`, so a task name that itself
55
+ * contains one round-trips: `taskId('a', 'b#c')` → `'a#b#c'` → `['a', 'b#c']`.
56
+ *
57
+ * This exists because it kept being written by hand as `id.split('#', 2)`,
58
+ * which is NOT the inverse — it discards everything after the second segment,
59
+ * so `'a#b#c'` reads back as task `'b'`. Six call sites had that form while
60
+ * `parseDependencySpec` (the surface that decides what actually runs) has
61
+ * always split on the first `#`, so the query layer and the graph disagreed
62
+ * about the identity of the same task: a lookup either found nothing or, worse,
63
+ * answered with a different task's history.
64
+ *
65
+ * A `#`-containing task name is legal and pinned elsewhere in the suite, so
66
+ * this is reachable rather than theoretical.
67
+ */
68
+ export function splitTaskId(id: string): [project: string, task: string] {
69
+ const i = id.indexOf('#')
70
+ return i < 0 ? [id, ''] : [id.slice(0, i), id.slice(i + 1)]
71
+ }
72
+
47
73
  /**
48
74
  * A task is a "group" if it has no `exec` — it exists only to chain
49
75
  * `dependsOn` (an umbrella for `vx run ci`). Group tasks:
@@ -134,16 +160,54 @@ export function expandRequested(
134
160
  if (idx >= 0) {
135
161
  const project = spec.slice(0, idx)
136
162
  const task = spec.slice(idx + 1)
137
- if (projects.get(project)?.config.tasks?.[task]) push(project, task)
163
+ if (declaresTask(projects, project, task)) push(project, task)
138
164
  continue
139
165
  }
140
166
  for (const name of candidates) {
141
- if (projects.get(name)?.config.tasks?.[spec]) push(name, spec)
167
+ if (declaresTask(projects, name, spec)) push(name, spec)
142
168
  }
143
169
  }
144
170
  return out
145
171
  }
146
172
 
173
+ /** Shared by `expandRequested` + `unresolvedRequests` so the two can't drift. */
174
+ function declaresTask(projects: Map<string, ProjectEntry>, project: string, task: string): boolean {
175
+ return projects.get(project)?.config.tasks?.[task] !== undefined
176
+ }
177
+
178
+ /**
179
+ * The requested specs `expandRequested` silently dropped — each one
180
+ * matched NO project, so nothing it asked for will run.
181
+ *
182
+ * A bare name matching only SOME projects is normal (sparse tasks across
183
+ * a workspace) and never reported. An empty `candidates` scope is also
184
+ * never reported: that is the legitimate "nothing selected" outcome
185
+ * (`--affected` with nothing changed), not a typo — the CLI's selection
186
+ * layer owns that message.
187
+ *
188
+ * Deduped so `vx run x x` names `x` once.
189
+ */
190
+ export function unresolvedRequests(
191
+ tasks: readonly string[],
192
+ candidates: readonly string[],
193
+ projects: Map<string, ProjectEntry>,
194
+ ): string[] {
195
+ const out: string[] = []
196
+ const seen = new Set<string>()
197
+ for (const spec of tasks) {
198
+ if (seen.has(spec)) continue
199
+ const idx = spec.indexOf('#')
200
+ const resolved =
201
+ idx >= 0
202
+ ? declaresTask(projects, spec.slice(0, idx), spec.slice(idx + 1))
203
+ : candidates.length === 0 || candidates.some((name) => declaresTask(projects, name, spec))
204
+ if (resolved) continue
205
+ seen.add(spec)
206
+ out.push(spec)
207
+ }
208
+ return out
209
+ }
210
+
147
211
  export interface BuildGraphOptions {
148
212
  projects: Map<string, ProjectEntry>
149
213
  packageGraph: PackageGraph
@@ -225,7 +289,7 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
225
289
  `Task ${id}: dependsOn patterns are not supported in the "pkg#task" form (got "${raw}")`,
226
290
  )
227
291
  }
228
- // CLI `--excludeDependencies=name1,name2` drops edges whose target
292
+ // CLI `--exclude-dependencies=name1,name2` drops edges whose target
229
293
  // task name matches, regardless of bucket (self / deps / cross).
230
294
  // Pattern specs re-apply the filter per EXPANDED name below.
231
295
  if (skipNames?.has(spec.task)) continue
@@ -267,9 +331,15 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
267
331
  // With a pattern (`^build.*`), a holder is a package declaring
268
332
  // AT LEAST ONE matching task and it receives edges to ALL its
269
333
  // matches — holder-ness is about declaration, so a holder still
270
- // stops the walk even when every match is --excludeDependencies'd.
334
+ // stops the walk even when every match is --exclude-dependencies'd.
335
+ //
336
+ // The declaring project seeds `visited`: package graphs may legally
337
+ // contain cycles (the common "b devDepends on a for its tests"
338
+ // shape), and a cycle walks the frontier straight back to the
339
+ // origin. Mirrors the self-pattern rule below — a task can never
340
+ // depend on itself.
271
341
  const re = isTaskPattern(spec.task) ? compileTaskPattern(spec.task) : null
272
- const visited = new Set<string>()
342
+ const visited = new Set<string>([projectName])
273
343
  const frontier = [...packageGraph.directDeps(projectName)]
274
344
  while (frontier.length > 0) {
275
345
  const target = frontier.pop()!
@@ -320,10 +390,154 @@ export function buildTaskGraph(options: BuildGraphOptions): Map<string, TaskNode
320
390
  }
321
391
 
322
392
  detectCycle(nodes)
393
+ detectOutputCollisions(nodes)
323
394
  return nodes
324
395
  }
325
396
 
326
- function detectCycle(nodes: Map<string, TaskNode>): void {
397
+ /** A glob with no wildcard — it names exactly one path. */
398
+ function isLiteralGlob(g: string): boolean {
399
+ return g.search(/[*?[\]]/) === -1
400
+ }
401
+
402
+ /**
403
+ * True only when two output globs PROVABLY select an overlapping set.
404
+ *
405
+ * Deliberately conservative, because the caller REFUSES the run: a false
406
+ * positive breaks a build that works today, which is worse than the defect
407
+ * being caught. So the three cases are exactly the ones that can be decided
408
+ * without a general glob-intersection algorithm:
409
+ *
410
+ * both literal — equal paths
411
+ * literal vs glob — ask the glob whether it matches the literal (exact)
412
+ * both globs — only identical strings; anything else is undecided
413
+ * here and deliberately allowed through
414
+ *
415
+ * The rejected alternative was comparing each glob's static prefix. It is
416
+ * cheaper and catches more, but it is UNSOUND for a refusal — measured:
417
+ * `dist/vx-*` and `dist/other.txt` share the prefix `dist` while matching
418
+ * disjoint sets, so a prefix check refuses a legitimate config. (vx's own
419
+ * `build.bun.*` tasks escape only because they declare distinct literals.)
420
+ */
421
+ function outputsOverlap(a: string, b: string): boolean {
422
+ if (isLiteralGlob(a) && isLiteralGlob(b)) return a === b
423
+ if (isLiteralGlob(a)) return new Bun.Glob(b).match(a)
424
+ if (isLiteralGlob(b)) return new Bun.Glob(a).match(b)
425
+ return a === b
426
+ }
427
+
428
+ /**
429
+ * Refuse a graph in which two tasks declare overlapping outputs.
430
+ *
431
+ * vx cleans a task's declared outputs before it runs AND before a cache-hit
432
+ * restore, so that the tree ends byte-identical to the cached artifact. That
433
+ * makes output ownership STRICT — and two tasks claiming the same path
434
+ * silently destroy each other's work, in whichever order they happen to run,
435
+ * while the run reports success. It is data loss with a green summary.
436
+ *
437
+ * This is a hazard vx CREATED: Turbo restores additively and cannot hit it,
438
+ * which is why no Turbo test surfaces it and why the parity research had to
439
+ * reproduce it end to end.
440
+ *
441
+ * Scope follows the two namespaces' reach. `outputs.files` is
442
+ * project-relative, so only tasks in the SAME project can collide;
443
+ * `outputs.workspaceFiles` is anchored at the workspace root and ignores
444
+ * project boundaries by design, so ANY two tasks can. No cache key changes —
445
+ * this only refuses a graph that was already destroying files.
446
+ */
447
+ function detectOutputCollisions(nodes: Map<string, TaskNode>): void {
448
+ // INDEX FIRST, then compare — never all-pairs over the graph. A naive
449
+ // pairwise loop that filters by project INSIDE the loop is quadratic in the
450
+ // whole graph: measured 1.6 SECONDS at this project's stated target of 1000
451
+ // projects x 10 tasks, on every run, against a ~120ms warm run. (Same shape
452
+ // as the scheduler's priority closure, which took 8.5s on a 1090-package
453
+ // repo before it was rewritten.)
454
+ //
455
+ // Both namespaces have a much smaller natural domain:
456
+ // files — project-relative, so only same-project tasks can
457
+ // collide. Bucketing makes this O(sum of k^2) over
458
+ // per-project task counts, and k is single digits.
459
+ // workspaceFiles — root-anchored and boundary-free, so any two tasks can
460
+ // collide — but only tasks that DECLARE it participate,
461
+ // and that set is nearly always empty.
462
+ const byProject = new Map<string, TaskNode[]>()
463
+ const wsDeclarers: TaskNode[] = []
464
+ for (const n of nodes.values()) {
465
+ const outs = n.config.cache?.outputs
466
+ if ((outs?.files?.length ?? 0) > 0) {
467
+ const bucket = byProject.get(n.projectName)
468
+ if (bucket) bucket.push(n)
469
+ else byProject.set(n.projectName, [n])
470
+ }
471
+ if ((outs?.workspaceFiles?.length ?? 0) > 0) wsDeclarers.push(n)
472
+ }
473
+
474
+ for (const bucket of byProject.values()) {
475
+ for (let i = 0; i < bucket.length; i++) {
476
+ for (let j = i + 1; j < bucket.length; j++) {
477
+ const a = bucket[i]!
478
+ const b = bucket[j]!
479
+ collide(a, b, a.config.cache?.outputs.files, b.config.cache?.outputs.files, 'files')
480
+ }
481
+ }
482
+ }
483
+ for (let i = 0; i < wsDeclarers.length; i++) {
484
+ for (let j = i + 1; j < wsDeclarers.length; j++) {
485
+ const a = wsDeclarers[i]!
486
+ const b = wsDeclarers[j]!
487
+ collide(
488
+ a,
489
+ b,
490
+ a.config.cache?.outputs.workspaceFiles,
491
+ b.config.cache?.outputs.workspaceFiles,
492
+ 'workspaceFiles',
493
+ )
494
+ }
495
+ }
496
+ }
497
+
498
+ /**
499
+ * A `remote: 'only'` task never materialises its outputs on THIS machine —
500
+ * execute-task turns off both the read and the write axis for it, so there is
501
+ * no probe, no restore, no output clean and no local save. The collision
502
+ * hazard is entirely about local cleaning ("whichever runs second DELETES the
503
+ * other's output"), so a pair including one of these cannot exhibit it: there
504
+ * is nothing of its on disk to delete, and it deletes nothing of anyone's.
505
+ *
506
+ * Found while trying to give two projects their own install-as-an-action:
507
+ * both legitimately capture the workspace-root `node_modules` under a
508
+ * hoisting package manager, and the refusal fired on a pair that cannot
509
+ * exhibit the hazard. That particular layout was abandoned for an unrelated
510
+ * reason (identical definitions still get different cache keys), so this is
511
+ * not load-bearing today — it is a refusal narrowed to what it can prove,
512
+ * which this file's header asks for explicitly.
513
+ */
514
+ function neverWritesLocally(n: TaskNode): boolean {
515
+ return n.config.exec?.remote === 'only'
516
+ }
517
+
518
+ function collide(
519
+ a: TaskNode,
520
+ b: TaskNode,
521
+ aGlobs: readonly string[] | undefined,
522
+ bGlobs: readonly string[] | undefined,
523
+ field: 'files' | 'workspaceFiles',
524
+ ): void {
525
+ if (neverWritesLocally(a) || neverWritesLocally(b)) return
526
+ for (const ga of aGlobs ?? []) {
527
+ for (const gb of bGlobs ?? []) {
528
+ if (!outputsOverlap(ga, gb)) continue
529
+ throw new UserError(
530
+ `${a.id} and ${b.id} both declare the output ${JSON.stringify(ga)}` +
531
+ (ga === gb ? '' : ` / ${JSON.stringify(gb)}`) +
532
+ ` in cache.outputs.${field} — vx cleans a task's declared outputs before it runs and ` +
533
+ `before a cache-hit restore, so whichever of these runs second DELETES the other's ` +
534
+ `output and the run still reports success. Give each task its own output path.`,
535
+ )
536
+ }
537
+ }
538
+ }
539
+
540
+ export function detectCycle(nodes: Map<string, TaskNode>): void {
327
541
  // Iterative DFS over dense-indexed colors. Recursion + `Map<string,
328
542
  // number>` worked, but deep workspaces (long chains of `dependsOn`)
329
543
  // can blow V8's frame budget, and per-node Map lookups dominate the
package/src/index.ts CHANGED
@@ -3,13 +3,33 @@
3
3
  // This is the stable cross-package contract — any plugin or integration
4
4
  // package imports everything it needs from here via the bare
5
5
  // `'@vzn/vx'` specifier (never a deep `src/...` path). The surface is pinned
6
- // by tests/package-boundaries.test.ts; a widening updates that snapshot
7
- // deliberately. See docs/design/core-cloud-split-2026-06.md §3.5.
6
+ // by tests/package-boundaries.unsafe.test.ts; a widening updates that snapshot
7
+ // deliberately. The boundary law: docs/architecture.md § Repository shape.
8
8
 
9
9
  export { VERSION } from './version.js'
10
10
 
11
11
  // Clean error type — user-input failures print a message without a stack.
12
- export { UserError } from './util/index.js'
12
+ // `clampInt` rides along for the same reason the status predicates do:
13
+ // without it on the façade an integration package writes its own, and that
14
+ // had already happened. A bounds helper whose floor is load-bearing (a
15
+ // fractional SQL LIMIT is a datatype mismatch, not a smaller page) should
16
+ // have one implementation.
17
+ export { clampInt, UserError, isUserError } from './util/index.js'
18
+ // "Did you mean": the hint core's own verbs give for a near-miss name, for a
19
+ // plugin verb to give the same one.
20
+ export { nearMatches } from './util/index.js'
21
+ // Every hook a plugin may fill, in pipeline order — the one list the
22
+ // loader, the host and `vx info` read; exported so a doc pin outside core
23
+ // (the site's guides) holds its tables to the same list.
24
+ export { PLUGIN_HOOKS, type PluginHook } from './config.js'
25
+ // The workspace doctor's facts (`vx info`), for a host that answers the
26
+ // same question over another surface — `@vzn/vx-mcp`'s `getWorkspaceInfo`.
27
+ export { collectInfo, type CollectInfoOptions, type InfoFacts } from './orchestrator/index.js'
28
+ // The machine as this process may use it — cores and memory capped by the
29
+ // cgroup a container runs under. Core's default worker count reads the
30
+ // first; a policy that budgets memory (`@vzn/vx-schedule-history`) reads
31
+ // the second, so the cgroup walk has one implementation.
32
+ export { machineMemoryBytes, machineParallelism } from './util/index.js'
13
33
 
14
34
  // Schema types and helpers (used by user vx.config files and presets).
15
35
  export type {
@@ -21,29 +41,26 @@ export type {
21
41
  CacheConfig,
22
42
  CacheInputs,
23
43
  CacheOutputs,
24
- ResourcesConfig,
25
44
  SandboxConfig,
26
- SandboxNetworkConfig,
45
+ SandboxGrants,
46
+ SandboxDenials,
27
47
  } from './config.js'
28
48
  export { defineProject, defineWorkspace } from './config.js'
49
+ export { definePlugin } from './orchestrator/index.js'
29
50
 
30
- // Programmatic engine API (run / plan / prepare) + the graph primitives a
31
- // distribution submitter/agent reasons over + the cache-key hashing seam.
32
- // `deriveStableKeys` is THE shared stable-key derivation (remote-prefetch,
33
- // the local short-circuit, and the distributed submitter must never drift
34
- // on the stability gate); `captureGitContext`/`captureWorkspaceIdentity`
35
- // give agents + the submitter identity before/without a telemetry run.
51
+ // Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
52
+ // API). The graph primitives, the cache-key hashing seam and the git / host
53
+ // context capture used to sit beside these for a distributed submitter that
54
+ // left the repo (2026-08); they went with it (2026-09-10) — a seam with no
55
+ // consumer is re-added when one appears, shaped by its use.
36
56
  export { run, planRun, prepareRun } from './orchestrator/index.js'
57
+ // The per-task duration history a `schedule` plugin learns from
58
+ // (`@vzn/vx-schedule-history` does; a plugin package reaches core only
59
+ // through this façade, which is what put these here).
60
+ export { LocalHistoryProvider } from './orchestrator/index.js'
61
+ export type { HistoryProvider, HistoryTable, TaskHistory } from './orchestrator/index.js'
37
62
  export type { PreparedRun } from './orchestrator/index.js'
38
- export { computeTaskHash, createHashCache, deriveStableKeys } from './orchestrator/index.js'
39
- export type { DeriveStableKeysArgs, HashCache, StableKey } from './orchestrator/index.js'
40
- export {
41
- captureGitContext,
42
- captureWorkspaceIdentity,
43
- resolveCacheScope,
44
- } from './orchestrator/index.js'
45
- export type { GitContext, WorkspaceIdentity } from './orchestrator/index.js'
46
- export { FULL_CACHE_POLICY, parseCachePolicy } from './orchestrator/index.js'
63
+ export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './orchestrator/index.js'
47
64
  export type {
48
65
  CachePolicy,
49
66
  Logger,
@@ -51,172 +68,148 @@ export type {
51
68
  RunOptions,
52
69
  RunSummary,
53
70
  } from './orchestrator/index.js'
54
- export { defaultLogger, resolveOutputView } from './orchestrator/index.js'
55
- export { buildTaskGraph, expandRequested, isGroupTask, markSurfacedDeps } from './graph/index.js'
56
- export type {
57
- OutputFingerprint,
58
- TaskNode,
59
- TaskOutcome,
60
- TaskStatus,
61
- VerifyVerdict,
62
- } from './graph/index.js'
63
- // The one output-tree diff implementation (verify verdicts + a serve's
64
- // cross-machine fingerprint diff both name rels through it).
65
- export { diffOutputTrees } from './orchestrator/index.js'
71
+ // `splitTaskId` is on the façade because the alternative is what happened:
72
+ // with only `taskId()` to JOIN an id and nothing exported to SPLIT one,
73
+ // consumers roll their own `split('#', 2)` and drift from the graph, which
74
+ // splits on the FIRST '#'.
75
+ export { splitTaskId } from './graph/index.js'
76
+ export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
66
77
 
67
- // Cache classes + the layer interface (the `cache` capability's currency)
68
- // and input-output resolution. The blob-CAS/digest substrate
69
- // (cas-backend.ts / digest.ts) stays module-internal until it has a
70
- // consumer — no speculative public API. `cleanOutputs` is public for the
71
- // distributed submitter's targeted output materialization (wipe declared
72
- // outputs, then `restoreOutputs` the artifact — never a naive re-run).
73
- export {
74
- Cache,
75
- LayeredCache,
76
- GitFilesCache,
77
- cleanOutputs,
78
- resolveInputs,
79
- resolveOutputs,
80
- } from './cache/index.js'
78
+ // Cache classes + the layer interface (the `cache` capability's currency).
79
+ export { Cache, LayeredCache } from './cache/index.js'
81
80
  export type { CacheLayer, RemoteCacheLayer, RunRecord, InvocationRecord } from './cache/index.js'
82
81
 
83
82
  // Workspace discovery + the project/config catalog surface — an
84
- // out-of-process service/CLI needs these. `readLockfile` is THE one reader
85
- // of vx-lock.json (the format carries its own version sentinel; a second
86
- // parser in a sibling package would drift), and the loader chain
87
- // (`loadWorkspace` → `listProjectMetas` → `loadProjectConfig`) is the same
88
- // one `vx show` uses. Workspace's `listProjects` re-exports as
89
- // `listProjectMetas` — the bare name belongs to the metrics query below.
90
- export { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from './workspace/index.js'
91
- export { readLockfile, LOCKFILE_NAME } from './workspace/index.js'
92
- export type { Lockfile, LockfileEntry } from './workspace/index.js'
83
+ // out-of-process service/CLI needs these. `loadProjectConfig` is the RAW
84
+ // per-file load (`vx lock` freezes exactly that); the resolved view a run
85
+ // or `vx show` sees — plugin stages applied — is `loadResolvedProjects`
86
+ // below. Workspace's `listProjects` re-exports as `listProjectMetas` (the
87
+ // bare name once belonged to a metrics query).
88
+ export { findWorkspaceRoot } from './workspace/index.js'
93
89
  export {
94
90
  loadWorkspace,
95
91
  loadProjectConfig,
96
92
  listProjects as listProjectMetas,
97
93
  } from './workspace/index.js'
98
94
  export type { ProjectMeta } from './workspace/index.js'
95
+ // The package graph as a run sees it (workspace deps by manifest), for a tool
96
+ // that needs a project's transitive closure the way `vx run` computes it.
97
+ export { buildPackageGraph } from './workspace/index.js'
98
+ // The run path's RESOLVED view — plugin `config` and `project` stages
99
+ // applied, cached evaluations served — for a reader outside the CLI (the
100
+ // MCP server's `listTasks`, an embedder's task catalog). What `vx show`
101
+ // prints.
102
+ export { loadResolvedProjects } from './orchestrator/index.js'
103
+ export type { ProjectEntry } from './workspace/index.js'
104
+ // The migration seam: how a generated config is planned, rendered, guarded
105
+ // and written. `vx init` (package.json scripts) uses it in core;
106
+ // `@vzn/vx-migrate` (Turbo, Nx) and any other adoption tool use it from here.
107
+ export {
108
+ applyMigration,
109
+ PERSISTENT_TASK_NAMES,
110
+ PERSISTENT_TODO,
111
+ quoteTsLiteral,
112
+ } from './workspace/index.js'
113
+ export type {
114
+ ApplyMigrationArgs,
115
+ MigrationFormat,
116
+ GeneratedProject,
117
+ GeneratedTask,
118
+ MigrationPlan,
119
+ RawExpr,
120
+ } from './workspace/index.js'
99
121
 
100
122
  // Plugin API — the run-level extension points. Behavior capabilities
101
- // (backend / cache) change WHAT/HOW work runs; the observe-only `telemetry`
123
+ // (executor / cache) change WHAT/HOW work runs; the observe-only `telemetry`
102
124
  // capability is the canonical data-export path and cannot change behavior.
103
125
  // A plugin is declared in vx.workspace.ts via defineWorkspace({ plugins }).
104
126
  // See docs/design/observability-architecture-2026-06.md.
127
+ // The claimant's shell for a lockfile plugin (`@vzn/vx-lockfile`):
128
+ // the claim, the per-project key, the memo and the `--affected` diff around
129
+ // a parser; `reachDigests` is the Merkle-over-components digest both use.
130
+ export {
131
+ lockfileClaim,
132
+ reachDigests,
133
+ type LockfileClaimHooks,
134
+ type LockfileClaimOptions,
135
+ type ReachGraph,
136
+ } from './orchestrator/index.js'
105
137
  export type {
106
138
  VxPlugin,
107
- EventSink,
108
- BackendContext,
139
+ PluginHooks,
140
+ PluginOrigin,
109
141
  CacheContext,
110
- EventSinkContext,
142
+ ExecutorContext,
143
+ CommandContext,
144
+ FingerprintChange,
145
+ FingerprintClaim,
146
+ FingerprintContext,
147
+ GraphHookContext,
148
+ KeyHookContext,
149
+ PluginCommand,
150
+ ProjectHookContext,
151
+ ScheduleHookContext,
152
+ AdmitContext,
153
+ WorkspaceHookContext,
111
154
  PluginSetupContext,
112
155
  } from './orchestrator/index.js'
156
+ // The per-task execution contract a plugin's `executor` capability returns.
157
+ // (`runCommand` / `runSandboxed`, the local executor's own primitives, left
158
+ // the façade 2026-09-10: no executor plugin built on them — `@vzn/vx-reapi`
159
+ // speaks a wire — and a seam with no consumer is a special case in waiting.)
160
+ export type {
161
+ ExecuteRequest,
162
+ ExecuteResult,
163
+ ExecuteSandbox,
164
+ ResolvedSandboxConfig,
165
+ TaskExecutor,
166
+ TaskPlacement,
167
+ } from './exec/index.js'
113
168
 
114
169
  // Telemetry — THE canonical, versioned data-export contract every exporter
115
170
  // (an OTel exporter, an HTTP sink, or any third-party consumer) reads. A sink implements TelemetrySink and is
116
171
  // returned from VxPlugin.telemetry(); it receives immutable records and holds
117
172
  // no run handle (observe-only by construction).
118
- export { TELEMETRY_SCHEMA_VERSION, deriveCacheSource } from './orchestrator/index.js'
173
+ // `isPassStatus` / `isCacheHit` are on the façade for the reason the sweep
174
+ // that added them found: with only the raw `TaskStatus` union exported, every
175
+ // consumer rolls its own Set of status literals — and a Set has no
176
+ // compile-time tripwire when the union gains a member, so it silently answers
177
+ // "no" for the new one. `TASK_STATUSES` is the union at runtime, for a
178
+ // consumer that needs the list rather than the predicate.
179
+ // `escapeMarkdownCell` is on the façade for the same demonstrated need: a
180
+ // plugin that renders a run as a markdown table takes the same unvalidated
181
+ // task names core does, and the cloud job summary shipped without the escape.
182
+ export {
183
+ escapeMarkdownCell,
184
+ TELEMETRY_SCHEMA_VERSION,
185
+ deriveCacheSource,
186
+ isCacheHit,
187
+ isPassStatus,
188
+ TASK_STATUSES,
189
+ } from './orchestrator/index.js'
190
+ // `TaskLogBuffer` is on the façade on the same demonstrated need: EVERY
191
+ // telemetry sink that ships build output has to bound it, and the retention
192
+ // rules (per-task tail, per-run budget, failures never evicted by successes,
193
+ // a hit's bytes belong to the run that executed) are a decision, not an
194
+ // implementation detail. Two sinks rolling their own is how they fork.
195
+ export { LOG_WIRE_VERSION, TaskLogBuffer } from './orchestrator/index.js'
119
196
  export type {
120
197
  CacheSource,
121
198
  RunContextRecord,
122
199
  RunSummaryRecord,
200
+ TaskLogBundle,
201
+ TaskLogEntry,
123
202
  TaskTelemetry,
124
203
  TelemetryContext,
125
204
  TelemetryRecord,
126
205
  TelemetrySink,
127
206
  } from './orchestrator/index.js'
128
207
 
129
- // The submitter wire contract + backend interface (the `backend`
130
- // capability's currency) and the serializable event projection.
131
- export {
132
- optionsToRequest,
133
- requestToOptions,
134
- projectNode,
135
- projectOutcome,
136
- createWireRenderer,
137
- } from './orchestrator/index.js'
138
- export type {
139
- RunBackend,
140
- RunRequest,
141
- RunResult,
142
- ClientMessage,
143
- ServerMessage,
144
- } from './orchestrator/index.js'
145
-
146
- // Event bus + wire form — adapters (otel-bridge, custom subscribers) ride this.
147
- export {
148
- createEventBus,
149
- wireForwarder,
150
- toWireEvent,
151
- createVxSurface,
152
- } from './orchestrator/index.js'
153
- export type {
154
- EventBus,
155
- RunEvent,
156
- RunEventSubscriber,
157
- WireEvent,
158
- TaskView,
159
- OutcomeView,
160
- } from './orchestrator/index.js'
208
+ // The serializable run result and its per-task views (what `run()` returns).
209
+ export type { RunResult, TaskView, OutcomeView } from './orchestrator/index.js'
161
210
 
162
- // The JSON-RPC 2.0 wire envelope — an out-of-process serve/agent speaks this framing.
163
- export {
164
- clientMessageToEnvelope,
165
- decodeEnvelope,
166
- encodeForNDJSON,
167
- encodeForSSE,
168
- encodeForWS,
169
- ENVELOPE_ERRORS,
170
- envelopeToClientMessage,
171
- envelopeToServerMessage,
172
- isEnvelope,
173
- isNotification,
174
- isRequest,
175
- makeError,
176
- makeNotification,
177
- makeRequest,
178
- makeResponse,
179
- serverMessageToEnvelope,
180
- WIRE_CHANNELS,
181
- WIRE_PROTOCOL_VERSION,
182
- } from './orchestrator/index.js'
183
- export type {
184
- Envelope,
185
- ErrorResponse,
186
- Notification,
187
- WireRequest,
188
- WireResponse,
189
- WireChannel,
190
- } from './orchestrator/index.js'
191
-
192
- // Metrics / analytics query layer over cache.db — a service plugin's HTTP
193
- // surface reads from these (the queries stay in core; a serve exposes them over HTTP).
194
- export {
195
- cacheKeyDiff,
196
- compareRuns,
197
- explainCacheKeyQuery,
198
- getBottlenecks,
199
- getCacheBreakdown,
200
- getCacheSavings,
201
- getCacheStatsSql,
202
- getFlakiestTasks,
203
- getHistory,
204
- getHitRateSplit,
205
- getInvocation,
206
- getParallelismHistory,
207
- getPeriodComparison,
208
- getPrunableEntries,
209
- getRecentFailures,
210
- getRegressions,
211
- getRun,
212
- getRunHeatmap,
213
- getRunTrends,
214
- getStorageGrowth,
215
- getTaskDetail,
216
- getTopTimeBurners,
217
- listCacheEntries,
218
- listInvocations,
219
- listProjects,
220
- listRuns,
221
- whyDidThisRerunQuery,
222
- } from './orchestrator/index.js'
211
+ // The one run-history query a reader outside the CLI takes today: `vx why`'s
212
+ // answer, which `@vzn/vx-mcp` serves. The event bus / wire form and the
213
+ // other history readers (`listRuns`, `getRun`, …) left the façade 2026-09-10
214
+ // with no consumer; the telemetry seam above is the canonical export path.
215
+ export { whyDidThisRerunQuery } from './orchestrator/index.js'