@vzn/vx 0.0.636 → 0.0.637

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 (141) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +213 -207
  3. package/docs/cli.md +699 -655
  4. package/docs/execution.md +67 -75
  5. package/docs/features.md +44 -40
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +376 -198
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +2 -2
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +16 -12
  19. package/src/bin.ts +17 -2
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +105 -53
  22. package/src/cache/git-inputs.ts +277 -63
  23. package/src/cache/index.ts +7 -1
  24. package/src/cache/inputs.ts +125 -35
  25. package/src/cache/key-fold.ts +8 -7
  26. package/src/cache/layer.ts +8 -17
  27. package/src/cache/layered-cache.ts +16 -102
  28. package/src/cache/policy.ts +25 -29
  29. package/src/cache/run-history.ts +36 -14
  30. package/src/cache/schema.ts +21 -23
  31. package/src/cache/scoped-cache.ts +224 -0
  32. package/src/cli/cache.ts +150 -28
  33. package/src/cli/completions.ts +2 -2
  34. package/src/cli/core-alias.ts +1 -1
  35. package/src/cli/foreign-flags.ts +13 -12
  36. package/src/cli/help.ts +94 -63
  37. package/src/cli/index.ts +42 -13
  38. package/src/cli/info.ts +38 -23
  39. package/src/cli/init.ts +115 -406
  40. package/src/cli/last.ts +145 -4
  41. package/src/cli/lock.ts +333 -111
  42. package/src/cli/plan-format.ts +2 -2
  43. package/src/cli/plugin-commands.ts +5 -9
  44. package/src/cli/plugin-templates.ts +10 -10
  45. package/src/cli/run.ts +103 -149
  46. package/src/cli/select.ts +33 -39
  47. package/src/cli/show.ts +17 -19
  48. package/src/cli/task-verb.ts +2 -9
  49. package/src/cli/upgrade.ts +402 -145
  50. package/src/cli/watch-cycle.ts +9 -2
  51. package/src/cli/watch-filter.ts +33 -60
  52. package/src/cli/watch-fs.ts +29 -16
  53. package/src/cli/watch-judge.ts +12 -3
  54. package/src/cli/watch-set.ts +16 -42
  55. package/src/cli/watch.ts +188 -215
  56. package/src/cli/why.ts +28 -17
  57. package/src/cli/workspace-config.ts +31 -68
  58. package/src/config.ts +169 -70
  59. package/src/exec/env.ts +1 -31
  60. package/src/exec/executor.ts +1 -5
  61. package/src/exec/kill-tree.ts +2 -1
  62. package/src/exec/sandbox-runtime.ts +12 -1
  63. package/src/exec/sandbox-violations.ts +9 -1
  64. package/src/graph/dependency-spec.ts +1 -1
  65. package/src/graph/index.ts +1 -0
  66. package/src/graph/priorities.ts +1 -1
  67. package/src/graph/scheduler.ts +19 -7
  68. package/src/graph/task-graph.ts +44 -20
  69. package/src/handoff.ts +169 -0
  70. package/src/index.ts +24 -57
  71. package/src/orchestrator/affected-tasks.ts +7 -5
  72. package/src/orchestrator/command-windows.ts +44 -0
  73. package/src/orchestrator/deferred-outputs.ts +12 -10
  74. package/src/orchestrator/doctor.ts +98 -110
  75. package/src/orchestrator/download-policy.ts +3 -3
  76. package/src/orchestrator/events.ts +19 -3
  77. package/src/orchestrator/excluded-keys.ts +0 -2
  78. package/src/orchestrator/execute-task.ts +158 -63
  79. package/src/orchestrator/fingerprint-watch.ts +18 -15
  80. package/src/orchestrator/forecast.ts +10 -2
  81. package/src/orchestrator/framed-output.ts +8 -3
  82. package/src/orchestrator/graph-stage.ts +5 -5
  83. package/src/orchestrator/history.ts +70 -30
  84. package/src/orchestrator/hit-restore.ts +15 -3
  85. package/src/orchestrator/index.ts +18 -4
  86. package/src/orchestrator/local-shortcircuit.ts +2 -1
  87. package/src/orchestrator/lockfile-claim.ts +79 -20
  88. package/src/orchestrator/logger.ts +71 -39
  89. package/src/orchestrator/metrics.ts +71 -6
  90. package/src/orchestrator/miss-save.ts +3 -0
  91. package/src/orchestrator/options.ts +42 -29
  92. package/src/orchestrator/output-host.ts +72 -0
  93. package/src/orchestrator/plan.ts +6 -8
  94. package/src/orchestrator/plugin-host.ts +87 -63
  95. package/src/orchestrator/plugin.ts +94 -60
  96. package/src/orchestrator/prepare.ts +113 -72
  97. package/src/orchestrator/projects.ts +102 -99
  98. package/src/orchestrator/remote-prefetch.ts +2 -1
  99. package/src/orchestrator/run-context.ts +25 -6
  100. package/src/orchestrator/run-failures.ts +17 -8
  101. package/src/orchestrator/run-lock.ts +1 -1
  102. package/src/orchestrator/run-report.ts +9 -9
  103. package/src/orchestrator/run-settings.ts +48 -0
  104. package/src/orchestrator/run.ts +121 -95
  105. package/src/orchestrator/sandbox-fix.ts +39 -20
  106. package/src/orchestrator/sandbox-request.ts +84 -13
  107. package/src/orchestrator/signals.ts +1 -1
  108. package/src/orchestrator/stable-keys.ts +13 -11
  109. package/src/orchestrator/status-line.ts +48 -6
  110. package/src/orchestrator/summary.ts +46 -62
  111. package/src/orchestrator/task-hash.ts +12 -83
  112. package/src/orchestrator/telemetry.ts +3 -3
  113. package/src/settings.ts +417 -0
  114. package/src/util/errors.ts +1 -1
  115. package/src/util/essential-env.ts +31 -0
  116. package/src/util/hide-env.ts +107 -0
  117. package/src/util/index.ts +8 -2
  118. package/src/util/paths.ts +10 -0
  119. package/src/util/settle.ts +49 -30
  120. package/src/util/size.ts +23 -4
  121. package/src/util/timing.ts +3 -2
  122. package/src/util/ulid.ts +1 -1
  123. package/src/util/verbs.ts +0 -15
  124. package/src/workspace/affected.ts +77 -222
  125. package/src/workspace/config-eval.ts +15 -20
  126. package/src/workspace/config-imports.ts +78 -3
  127. package/src/workspace/config-schema.ts +209 -268
  128. package/src/workspace/dependency-rules.ts +57 -0
  129. package/src/workspace/filter.ts +2 -2
  130. package/src/workspace/fingerprint.ts +11 -121
  131. package/src/workspace/index.ts +20 -27
  132. package/src/workspace/load-reads.ts +4 -4
  133. package/src/workspace/lockfile.ts +96 -140
  134. package/src/workspace/migration.ts +242 -146
  135. package/src/workspace/package-graph.ts +30 -216
  136. package/src/workspace/project-loader.ts +80 -285
  137. package/src/workspace/repo-id.ts +2 -1
  138. package/src/workspace/workspace.ts +362 -831
  139. package/src/cache/config-evals.ts +0 -130
  140. package/src/workspace/config-cache.ts +0 -661
  141. package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/why.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  // `vx why [pkg#task | task]` — answer "why did this task re-run?" from the
2
- // terminal. The same persisted data the MCP `whyDidThisRerun` tool reads:
3
- // the runs table names the
4
- // run-over-run hash change, and `entry_inputs` (the input-fingerprint moat)
5
- // names the exact cache-key components that differ. Read-only over cache.db —
6
- // no config evaluation, no re-hash.
2
+ // terminal. The runs table names the run-over-run hash change, and
3
+ // `entry_inputs` (the input-fingerprint moat) names the exact cache-key
4
+ // components that differ. Read-only over cache.db plus a stat of each
5
+ // moved input file — no config evaluation, no re-hash.
7
6
 
8
- import path from 'node:path'
9
7
  import { Cache } from '../cache/index.js'
10
8
  import { flagHint, formatValue, seeHelp } from './help.js'
11
9
  import { splitTaskId } from '../graph/index.js'
12
10
  import {
13
11
  cacheKeyDiff,
14
12
  explainCacheKeyQuery as explainCacheKey,
13
+ inputWriters,
15
14
  latestRunId,
16
15
  rootCauses,
17
16
  whyDidThisRerunQuery as whyDidThisRerun,
@@ -20,7 +19,7 @@ import {
20
19
  import { MASKED, nearMatches, printable, UserError } from '../util/index.js'
21
20
  import { findWorkspaceRoot } from '../workspace/index.js'
22
21
  import { findCwdProject } from './select.js'
23
- import { cliCacheDir, parseCacheDirFlag } from './workspace-config.js'
22
+ import { cliCacheDir, discoverCliProjects, parseCacheDirFlag } from './workspace-config.js'
24
23
 
25
24
  interface WhyArgs {
26
25
  target?: string
@@ -43,9 +42,6 @@ export const WHAT_TO_DO: Readonly<Record<string, string>> = {
43
42
  'ws-runtime':
44
43
  'the workspace command printed something else; drop timestamps and paths from its output',
45
44
  forward: 'the arguments after -- differ; the same arguments share one entry',
46
- package: "the project's package.json changed (a dependency, version or script)",
47
- workspace:
48
- 'a lockfile or root manifest changed, which re-keys every task; a lockfile plugin keys each project on its own dependencies',
49
45
  config:
50
46
  "the task's evaluated config changed (its vx.config or a file it imports); `vx show <that task>` prints it as it is now",
51
47
  upstream: "a dependency's key moved; `vx why <that task>` says why",
@@ -174,14 +170,14 @@ async function resolveTarget(
174
170
 
175
171
  const fmtWhen = (ms: number): string => new Date(ms).toISOString()
176
172
 
177
- /** `//#task`, Turbo's root package, as the root project's id: its package.json name (D-39). */
173
+ /** `//#task`, Turbo's root package, as the root project's id: the name discovery gives it (D-39). */
178
174
  async function rootSpelled(root: string, target: string): Promise<string> {
179
175
  if (!target.startsWith('//#')) return target
180
- const name = await Bun.file(path.join(root, 'package.json'))
181
- .json()
182
- .then((j: { name?: unknown }) => j.name)
183
- .catch(() => undefined)
184
- return typeof name === 'string' && name !== '' ? `${name}#${target.slice(3)}` : target
176
+ const name = await discoverCliProjects(root).then(
177
+ (metas) => metas.find((m) => m.dir === root)?.name,
178
+ () => undefined,
179
+ )
180
+ return name === undefined ? target : `${name}#${target.slice(3)}`
185
181
  }
186
182
 
187
183
  /** ` · up-to-date` or ` · restored` on a hit, as the run's summary said it; nothing otherwise. */
@@ -247,9 +243,10 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
247
243
  // each moved dependency in this run down to the tasks whose own
248
244
  // inputs moved, so one `vx why` names the file, not the next task.
249
245
  const roots = rootCauses(db, runId, taskId, diff.entries)
246
+ const writers = inputWriters(db, root, taskId, diff.entries, roots)
250
247
 
251
248
  if (parsed.format === 'json') {
252
- process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff, roots })}\n`)
249
+ process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff, roots, writers })}\n`)
253
250
  return 0
254
251
  }
255
252
 
@@ -320,6 +317,12 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
320
317
  }
321
318
  if (roots.length > ROOTS_SHOWN) lines.push(` and ${roots.length - ROOTS_SHOWN} more`)
322
319
  }
320
+ if (writers.length > 0) {
321
+ lines.push('', ' changed during a run:')
322
+ for (const w of writers) {
323
+ lines.push(` ${printable(w.file)} while ${w.writers.join(', ')} ran`)
324
+ }
325
+ }
323
326
  const rootKinds = roots.flatMap((r) => r.entries.map((e) => e.kind))
324
327
  const kinds = [...new Set([...diff.entries.map((e) => e.kind), ...rootKinds])].filter(
325
328
  (k) => k in WHAT_TO_DO && !(k === 'upstream' && roots.length > 0),
@@ -341,6 +344,11 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
341
344
  : WHAT_TO_DO[k]
342
345
  lines.push(` ${k.padEnd(kindW)} ${todo}`)
343
346
  }
347
+ if (writers.length > 0) {
348
+ // A task re-writing a file a key reads moves that key every run:
349
+ // the miss is right, and only moving the write ends it.
350
+ lines.push(` ${'written'.padEnd(kindW)} ${WRITTEN_TODO}`)
351
+ }
344
352
  }
345
353
  } else if (why.hashChanged === true) {
346
354
  lines.push(` detail ${diff.note}`)
@@ -353,3 +361,6 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
353
361
  }
354
362
 
355
363
  const ROOTS_SHOWN = 5
364
+
365
+ const WRITTEN_TODO =
366
+ 'a task that writes a file a key reads re-keys it every run; give that write its own task with the file as an output, or keep the file out of cache.inputs.files'
@@ -8,17 +8,18 @@ import { existsSync } from 'node:fs'
8
8
  import path from 'node:path'
9
9
  import type { WorkspaceConfig } from '../config.js'
10
10
  import { UserError } from '../util/index.js'
11
- import { Cache } from '../cache/index.js'
12
- import { discoverProjects, loadProjects, loadWorkspacePlugins } from '../orchestrator/index.js'
11
+ import {
12
+ discoverProjects,
13
+ loadProjects,
14
+ loadWorkspacePlugins,
15
+ lockOfDiscovery,
16
+ lockReader,
17
+ } from '../orchestrator/index.js'
13
18
  import type { VxPlugin } from '../orchestrator/index.js'
14
19
  import {
15
20
  buildPackageGraph,
16
- computeWorkspaceFingerprint,
17
- FROZEN_WITHOUT_LOCK,
18
- type Workspace,
19
21
  type ProjectEntry,
20
22
  type ProjectMeta,
21
- readLockfile,
22
23
  resolveCacheDir,
23
24
  resolveStoreRoot,
24
25
  } from '../workspace/index.js'
@@ -46,17 +47,18 @@ export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorksp
46
47
  }
47
48
 
48
49
  /**
49
- * The workspace's projects as a run discovers them: core's, then the
50
- * plugin `discover` stage, with the cache dir the run uses.
50
+ * The workspace's projects as a run discovers them: the lock's, then the
51
+ * plugin `discover` stage, with the cache dir the run uses. No lock is
52
+ * `NO_LOCK`.
51
53
  */
52
- export async function discoverCliProjects(workspace: Workspace): Promise<ProjectMeta[]> {
53
- const { plugins, cacheDir } = await loadCliWorkspace(workspace.root)
54
- return await discoverProjects(workspace, plugins, cacheDir, warnToStderr)
54
+ export async function discoverCliProjects(root: string): Promise<ProjectMeta[]> {
55
+ const { plugins, cacheDir } = await loadCliWorkspace(root)
56
+ return await discoverProjects(root, lockReader(root), plugins, cacheDir, warnToStderr)
55
57
  }
56
58
 
57
59
  /**
58
60
  * `--cache-dir <path>` / `--cache-dir=<path>` for a verb that reads the
59
- * cache a run wrote — `why`, `last`, `info`, `cache prune`. Same rules as
61
+ * cache a run wrote — `why`, `last`, `info`, `cache clean`. Same rules as
60
62
  * `vx run`'s: a value is required, and the space form refuses a value
61
63
  * that looks like a flag (an unquoted empty shell variable would otherwise
62
64
  * swallow the next flag as the path). Returns null when `args[i]` is not
@@ -104,16 +106,8 @@ export async function cliCacheDir(
104
106
 
105
107
  /** The run flags a verb's staged load must honour to see what the run sees. */
106
108
  export interface CliLoadOptions {
107
- /** `--cache-dir`: where the cached evaluations live. */
109
+ /** `--cache-dir`: the cache dir a plugin's `project` stage is told. */
108
110
  cacheDir?: string
109
- /** `--frozen`: configs come from `vx-lock.json`, not evaluation. */
110
- frozen?: boolean
111
- /**
112
- * A reading verb (`vx show`) makes nothing on disk (item 900): with no
113
- * cache directory yet, it evaluates without the cache rather than
114
- * create one.
115
- */
116
- noCreate?: boolean
117
111
  /**
118
112
  * Load what each seed's package closure reaches too, as a run does: a
119
113
  * config-less package a configured one depends on gets the default
@@ -124,10 +118,9 @@ export interface CliLoadOptions {
124
118
 
125
119
  /**
126
120
  * The run path's project-config load (`loadProjects`) for a verb that only
127
- * reads: the plugin `project` stage applies, and the local cache opens only
128
- * to serve cached evaluations — a pure config costs a stat, not an
129
- * evaluation. `scope` is every project or a list of names; their closure
130
- * only with `closure`, no lock (a verb reads live, as a default run does).
121
+ * reads: configs from the lock, the plugin `project` stage applied. `scope`
122
+ * is every project or a list of names; their closure only with `closure`.
123
+ * No lock is `NO_LOCK`, whatever the scope: only `vx lock` evaluates.
131
124
  */
132
125
  export async function loadCliProjects(
133
126
  workspaceRoot: string,
@@ -137,48 +130,18 @@ export async function loadCliProjects(
137
130
  ): Promise<Map<string, ProjectEntry>> {
138
131
  const ws = await loadCliWorkspace(workspaceRoot)
139
132
  const { plugins } = ws
140
- // The cached evaluations live where the run's do: a verb given
141
- // `--cache-dir` must not open (and create) the workspace's default one.
142
133
  const cacheDir = opts.cacheDir ?? ws.cacheDir
143
- // A frozen run reads its configs from the lock; what it SELECTS must be
144
- // decided from the same configs, or an env-dependent `workspaceFiles`
145
- // glob could select live what the run then treats otherwise.
146
- const lock = opts.frozen === true ? await readLockfile(workspaceRoot) : null
147
- if (opts.frozen === true && lock === null) throw new UserError(FROZEN_WITHOUT_LOCK)
148
- const cache =
149
- opts.noCreate === true && !existsSync(cacheDir)
150
- ? null
151
- : new Cache(
152
- cacheDir,
153
- { read: true, write: true },
154
- workspaceRoot,
155
- undefined,
156
- 'open',
157
- opts.cacheDir === undefined ? await ws.storeRoot : null,
158
- )
159
- try {
160
- const loaded = await loadProjects({
161
- workspaceRoot,
162
- cacheDir,
163
- plugins,
164
- projectMetas: metas,
165
- seeds: scope,
166
- ...(opts.closure === true
167
- ? { closure: true as const, packageGraph: buildPackageGraph([...metas]) }
168
- : { closure: false as const }),
169
- lock: lock === null ? null : async () => lock,
170
- evalCache:
171
- cache === null
172
- ? undefined
173
- : {
174
- store: cache,
175
- workspaceRoot,
176
- workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot),
177
- },
178
- warn: warnToStderr,
179
- })
180
- return loaded.projects
181
- } finally {
182
- cache?.close()
183
- }
134
+ const loaded = await loadProjects({
135
+ workspaceRoot,
136
+ cacheDir,
137
+ plugins,
138
+ projectMetas: metas,
139
+ seeds: scope,
140
+ ...(opts.closure === true
141
+ ? { closure: true as const, packageGraph: buildPackageGraph([...metas]) }
142
+ : { closure: false as const }),
143
+ configs: lockOfDiscovery(metas) ?? lockReader(workspaceRoot),
144
+ warn: warnToStderr,
145
+ })
146
+ return loaded.projects
184
147
  }
package/src/config.ts CHANGED
@@ -1,85 +1,140 @@
1
1
  // Public schema for vx project and workspace configuration.
2
2
 
3
+ /**
4
+ * A duration with its unit: `'500ms'`, `'30s'`, `'5m'`, `'2h'`, `'1d'`. A
5
+ * bare number is refused: a unit in the value keeps the name free of one.
6
+ */
7
+ export type Duration = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}`
8
+
9
+ /**
10
+ * The workspace: settings in groups (`run`, `cache`, `lock`, `watch`) and the
11
+ * plugins. Each setting's environment variable and flag derive from its
12
+ * path (`run.concurrency` → `VX_RUN_CONCURRENCY`, `vx run --concurrency`); a flag beats
13
+ * the variable, which beats this file. `docs/schema.md` lists them all.
14
+ * Nothing here is folded into a cache key.
15
+ */
3
16
  export interface WorkspaceConfig {
4
- /** Maximum concurrent tasks. Defaults to the number of CPUs. */
5
- concurrency?: number
6
- /**
7
- * Cache directory, relative to the workspace root. Named, it holds the whole
8
- * cache and shares nothing; unset, the workspace keeps its index in
9
- * `.vx/cache` and its entries in the user's shared store.
10
- */
11
- cacheDir?: string
17
+ run?: RunSettings
18
+ cache?: CacheSettings
19
+ lock?: LockSettings
20
+ watch?: WatchSettings
12
21
  /**
13
- * Default per-task timeout (ms) — the lowest-precedence fallback for a
14
- * task that declares no `exec.timeout`. Precedence, highest first:
15
- * per-task `exec.timeout` → `VX_TASK_TIMEOUT` env → this workspace
16
- * default. Purely a runaway-process guard; never folded into a cache
17
- * key (a timed-out task fails and is never cached). Omitted → no default.
22
+ * Plugins registered for this workspace, consulted in this order once
23
+ * per `vx run`: each fills any of the pipeline stages and capabilities
24
+ * below, and core's own executor and cache store sit at the tail of
25
+ * every list. See `docs/design/pipeline-2026-09.md`. An `undefined`,
26
+ * `null` or `false` entry is skipped, so a factory that declines
27
+ * (`github()` off Actions) or `cond && plugin()` needs no filter.
18
28
  */
19
- timeout?: number
29
+ plugins?: readonly (Plugin | undefined | null | false)[]
30
+ }
31
+
32
+ /** `WorkspaceConfig.run`: how a run runs. */
33
+ export interface RunSettings {
34
+ /** Tasks at once: a positive integer, or a share of the cores (`'50%'`). Omitted → the cores this process may use. */
35
+ concurrency?: number | `${number}%`
20
36
  /**
21
- * Evict from the local cache at the end of every run, whatever its
22
- * `--cache` policy (a remote hit is ingested under any), by the same policy `vx cache prune` takes as flags: entries not used
23
- * for `olderThan` (`30d`, `12h`), then least-recently-used entries
24
- * until the cache is under `maxSize` (`10G`, `500MB`). Either or both.
25
- * Omitted → nothing is evicted unless you run `vx cache prune`. Never
26
- * folded into a cache key.
37
+ * Which ready task starts first when there are more than workers, with no
38
+ * timings: `most-work` (the default) the one the most work waits on at
39
+ * any depth, `critical-path` the head of the longest chain of tasks,
40
+ * `direct-dependents` the one the most tasks wait on directly (Nx's
41
+ * rule), `ready-order` the one that became ready first (Turbo's). A
42
+ * `schedule` plugin's weights still come first; this breaks their ties.
27
43
  */
28
- cacheRetention?: { olderThan?: string; maxSize?: string }
44
+ schedule?: 'most-work' | 'critical-path' | 'direct-dependents' | 'ready-order'
29
45
  /**
30
46
  * The git ref a bare `--affected` compares with (`origin/develop`).
31
47
  * `--affected=<base>` overrides it. Omitted → `origin/HEAD`, then the
32
48
  * first of `origin/main`, `origin/master`, `main`, `master` that is not
33
- * HEAD, else `HEAD~1`. Never folded into a cache key.
49
+ * HEAD, else `HEAD~1`.
34
50
  */
35
51
  affectedBase?: string
36
52
  /**
37
- * Where this run's remote cache writes land. `'trusted'` reads and
38
- * writes the trusted keys, for the default branch's CI. `'read-only'`
39
- * reads them and writes nothing; omitted, `vx run` off CI takes it, and
40
- * on CI (or with `--cache` naming the remote) `'trusted'`. Any other
41
- * name (`'pr-123'`) is an untrusted scope: reads try the scope's keys,
42
- * then the trusted ones, and writes go to the scope's only, so a PR never
43
- * writes what the default branch reads. A client-side convention, not a
44
- * security boundary: only a cache server that scopes writes by token
45
- * can refuse a run that ignores it. `VX_CACHE_SCOPE` beats it, and
46
- * `vx run --cache-scope` beats both. Never
47
- * folded into a cache key.
53
+ * A task's time limit, for tasks without their own `exec.timeout` (which
54
+ * beats every run-level source). A runaway-process guard: a timed-out
55
+ * task fails and is never cached. Omitted → no limit.
48
56
  */
49
- cacheScope?: string
57
+ timeout?: Duration
58
+ /** Re-runs of a failed task, for tasks without their own `exec.retries`. Omitted → 0. */
59
+ retries?: number
50
60
  /**
51
- * Which ready task starts first when there are more than workers, with no
52
- * timings: `most-work` (the default) the one the most work waits on at
53
- * any depth, `critical-path` the head of the longest chain of tasks,
54
- * `direct-dependents` the one the most tasks wait on directly (Nx's
55
- * rule), `ready-order` the one that became ready first (Turbo's). A
56
- * `schedule` plugin's weights still come first; this breaks their ties.
61
+ * What a failed task takes down: `never` stops the run, `deps-ok` (the
62
+ * default) skips its dependents, `always` runs them too.
57
63
  */
58
- schedule?: 'most-work' | 'critical-path' | 'direct-dependents' | 'ready-order'
64
+ continue?: 'never' | 'deps-ok' | 'always'
65
+ /** How long a task that ignores SIGTERM gets before SIGKILL. Omitted → `'2s'`. */
66
+ killGrace?: Duration
67
+ /** The bound on each plugin's end-of-run flush and teardown. Omitted → `'3s'`. */
68
+ teardownTimeout?: Duration
69
+ /** How long a persistent task may take to match `readyWhen` before vx says it waits. Omitted → `'10s'`. */
70
+ readyNotice?: Duration
59
71
  /**
60
- * The `cache.inputStability` of every task that does not set its own
61
- * field. `VX_INPUT_STABILITY` / `VX_INPUT_STABILITY_SEVERITY` override
62
- * both for the run.
72
+ * Graph rules checked by `vx lock` and `vx lock --check`, never by a
73
+ * run. Each is on
74
+ * unless set to `false`; turning one off allows a shape vx handles
75
+ * correctly but more slowly.
63
76
  */
64
- inputStability?: InputStability
77
+ rules?: WorkspaceRules
78
+ }
79
+
80
+ /** `WorkspaceConfig.cache`: where the cache lives and what a run does with it. */
81
+ export interface CacheSettings {
65
82
  /**
66
- * Graph rules a run checks before it schedules anything. Each is on unless
67
- * set to `false`; turning one off allows a shape vx handles correctly but
68
- * more slowly. Never folded into a cache key.
83
+ * Cache directory, relative to the workspace root. Named, it holds the whole
84
+ * cache and shares nothing; unset, the workspace keeps its index in
85
+ * `.vx/cache` and its entries in the user's shared store.
69
86
  */
70
- rules?: WorkspaceRules
87
+ dir?: string
71
88
  /**
72
- * Plugins registered for this workspace, consulted in this order once
73
- * per `vx run`: each fills any of the pipeline stages and capabilities
74
- * below, and core's own executor and cache store sit at the tail of
75
- * every list. See `docs/design/pipeline-2026-09.md`. An `undefined`,
76
- * `null` or `false` entry is skipped, so a factory that declines
77
- * (`github()` off Actions) or `cond && plugin()` needs no filter.
89
+ * Evict from the local cache at the end of every run, whatever its
90
+ * policy (a remote hit is ingested under any), by the same policy
91
+ * `vx cache clean` takes as flags: entries not used for `olderThan`
92
+ * (`'30d'`, `'12h'`), then least-recently-used entries until the cache
93
+ * is under `maxSize` (`'10GB'`, `'500MB'`). Either or both. Omitted →
94
+ * nothing is evicted unless you run `vx cache clean`.
95
+ */
96
+ retention?: { olderThan?: Duration; maxSize?: string }
97
+ /**
98
+ * Which layers a run reads and writes (`'local:rw,remote:r'`; a layer it
99
+ * leaves out stays on); `--no-cache` and `--force` still apply on top.
100
+ * Omitted → every layer reads and writes. The local layer is always read
101
+ * first, then each remote in declared order.
102
+ */
103
+ policy?: string
104
+ /**
105
+ * Where this run's cache writes land. Omitted, a run reads and writes the
106
+ * default keys. A name (`'pr-123'`) reads the default key first, then the
107
+ * scope's, and writes only the scope's, so a PR never writes what the
108
+ * default branch reads. Whether a run writes at all is `policy`'s call.
109
+ * A client-side convention, not a security boundary: the boundary is a
110
+ * cache token the server limits.
111
+ */
112
+ scope?: string
113
+ /** Which remote hits land on disk: `all` (the default), only the requested tasks' (`toplevel`), or `none`. */
114
+ download?: 'all' | 'toplevel' | 'none'
115
+ /** The `cache.inputStability` of every task that does not set its own. */
116
+ inputStability?: InputStability
117
+ /** The `cache.outputs.clean` of every task that does not set its own. */
118
+ outputs?: { clean?: boolean }
119
+ }
120
+
121
+ /** `WorkspaceConfig.lock`. */
122
+ export interface LockSettings {
123
+ /**
124
+ * `vx lock` writes each project's resolved config, pretty, to
125
+ * `vx.lock.json` beside its `vx.config`, for review; vx never reads it.
126
+ * On unless `false`.
78
127
  */
79
- plugins?: readonly (Plugin | undefined | null | false)[]
128
+ reviewFiles?: boolean
129
+ }
130
+
131
+ /** `WorkspaceConfig.watch`. */
132
+ export interface WatchSettings {
133
+ /** Poll for changes from the start instead of the OS watcher. */
134
+ poll?: boolean
80
135
  }
81
136
 
82
- /** `WorkspaceConfig.rules`. */
137
+ /** `WorkspaceConfig.run.rules`. */
83
138
  export interface WorkspaceRules {
84
139
  /**
85
140
  * Refuse two tasks whose declared outputs overlap even when a `dependsOn`
@@ -94,6 +149,27 @@ export interface WorkspaceRules {
94
149
  * waits for its producer (`caching.md` § Local restore tier).
95
150
  */
96
151
  upfrontKeys?: boolean
152
+ /**
153
+ * Which workspace projects a project may depend on, checked by `vx lock`
154
+ * over each project's direct dependencies (its manifest's and its tasks'
155
+ * cross-project `dependsOn`). Every rule whose `from` selects a project
156
+ * applies to it; a project no rule selects is free.
157
+ */
158
+ dependencies?: readonly DependencyRule[]
159
+ }
160
+
161
+ /**
162
+ * One `run.rules.dependencies` entry. A selector is `tag:<tag>`, matching
163
+ * a project that carries the tag, or a project name; `*` matches any
164
+ * characters in either (`tag:scope:*`, `@acme/*`, `*`).
165
+ */
166
+ export interface DependencyRule {
167
+ /** The projects this rule constrains. */
168
+ from: string
169
+ /** Given, each dependency must match one of these. */
170
+ allow?: readonly string[]
171
+ /** No dependency may match one of these; beats `allow`. */
172
+ deny?: readonly string[]
97
173
  }
98
174
 
99
175
  /**
@@ -110,7 +186,7 @@ export const PLUGIN_PACKAGE: unique symbol = Symbol.for('vx.plugin.package')
110
186
  /**
111
187
  * Structural plugin shape — any subset of the pipeline stages
112
188
  * (`config`/`project`/`graph`/`key`/`schedule`), run-level capabilities
113
- * (`cache`/`executor`/`telemetry`) and CLI `commands`, plus optional
189
+ * (`cache`/`executor`/`telemetry`/`output`) and CLI `commands`, plus optional
114
190
  * `setup`/`teardown`. The fully-typed `VxPlugin` lives in
115
191
  * `src/orchestrator/plugin.ts` and is what users import; this is a
116
192
  * re-declaration with opaque function types so `config.ts` stays a leaf
@@ -131,6 +207,7 @@ export interface Plugin {
131
207
  cache?(ctx: unknown): unknown
132
208
  executor?(ctx: unknown): unknown
133
209
  telemetry?(ctx: unknown): unknown
210
+ output?(ctx: unknown): unknown
134
211
  setup?(ctx: unknown): void | Promise<void>
135
212
  teardown?(): void | Promise<void>
136
213
  readonly fingerprint?: unknown
@@ -156,6 +233,7 @@ export const PLUGIN_HOOKS = [
156
233
  'executor',
157
234
  'cache',
158
235
  'telemetry',
236
+ 'output',
159
237
  'setup',
160
238
  'commands',
161
239
  'teardown',
@@ -175,6 +253,13 @@ const _pluginHooksMatch: [HooksNotListed, HooksNotOnPlugin] extends [never, neve
175
253
  void _pluginHooksMatch
176
254
 
177
255
  export interface ProjectConfig {
256
+ /**
257
+ * The project's name, as task specs, filters and `dependsOn` address it.
258
+ * Unset: the name a `discover` plugin gives (a lockfile plugin's
259
+ * `package.json#name`), else the directory's root-relative path (`//`
260
+ * for the root).
261
+ */
262
+ name?: string
178
263
  /**
179
264
  * Labels for selection: `--filter tag:<name>` picks the projects that
180
265
  * carry one. Not in any cache key: a tag changes no task's behaviour.
@@ -387,7 +472,8 @@ export interface ExecConfig {
387
472
  /** Environment exposed to the child process. */
388
473
  env?: ExecEnv
389
474
  /**
390
- * Upper bound (ms) before vx SIGTERMs the child. A normal task that
475
+ * Upper bound before vx SIGTERMs the child (`'10m'`), beating
476
+ * `run.timeout` and its flag and variable. A normal task that
391
477
  * runs longer is killed and reported `failed` (timed out) — never
392
478
  * cached. Omitted → no limit.
393
479
  *
@@ -398,7 +484,7 @@ export interface ExecConfig {
398
484
  * on spawn (no `readyWhen`) becomes ready before the timer can fire,
399
485
  * so the timeout is a no-op for it.
400
486
  */
401
- timeout?: number
487
+ timeout?: Duration
402
488
  /**
403
489
  * Max ADDITIONAL attempts after a failed attempt. `retries: 2` means
404
490
  * up to 3 executions total. 0 / omitted → no retries (today's
@@ -408,8 +494,8 @@ export interface ExecConfig {
408
494
  * a failed attempt's partial outputs can't leak into the next. The
409
495
  * final outcome (and the cached artifact) is the last attempt's.
410
496
  * Not allowed with `persistent` — a persistent task has no exit to
411
- * retry. Explicit config wins over the run-level `--retry <n>`
412
- * default, including `retries: 0`.
497
+ * retry. Beats `run.retries` and its flag and variable, including
498
+ * `retries: 0`.
413
499
  */
414
500
  retries?: number
415
501
  /**
@@ -511,7 +597,8 @@ export interface InputStability {
511
597
  * withholds the entry, whatever the file holds after, since an edit
512
598
  * undone mid-run cannot be told from no edit. `'mutable'`: a file
513
599
  * rewritten to the bytes the key folded counts as unchanged, for a task
514
- * that rewrites its own input identically (tsdown's `package.json`).
600
+ * that rewrites its own input identically (tsdown's `package.json`),
601
+ * unless another task that may write it ran meanwhile.
515
602
  * `'unchecked'`: no check.
516
603
  */
517
604
  strategy?: 'immutable' | 'mutable' | 'unchecked'
@@ -543,6 +630,14 @@ export interface CacheOutputs {
543
630
  * and before a restore, so the second would delete the first's).
544
631
  */
545
632
  workspaceFiles?: readonly string[]
633
+ /**
634
+ * `false`: a miss leaves the declared outputs in place for the command
635
+ * (tsc's `.tsbuildinfo`) instead of removing them first. A run that
636
+ * leaves a file it found untouched saves nothing, so an entry holds only
637
+ * what a clean run would leave. A retry still starts clean. Omitted →
638
+ * the workspace's `cache.outputs.clean`, else `true`.
639
+ */
640
+ clean?: boolean
546
641
  }
547
642
 
548
643
  export interface CacheInputs {
@@ -604,8 +699,8 @@ export interface CacheInputs {
604
699
  * trimmed (stdout + stderr) output is folded into the cache key.
605
700
  * Deduped per (projectDir, command) within a run. A non-zero exit
606
701
  * fails the run. Modeled on `env`: the COMMANDS are frozen in the
607
- * lock, the OUTPUT is resolved live every run — correct under
608
- * `--frozen`. Use for project-specific runtime probes.
702
+ * lock, the OUTPUT is resolved live every run. Use for
703
+ * project-specific runtime probes.
609
704
  */
610
705
  runtime?: readonly string[]
611
706
  /**
@@ -698,11 +793,15 @@ export function defineProject<const T extends ProjectConfig>(
698
793
  export function defineWorkspace<T extends WorkspaceConfig>(
699
794
  config: T &
700
795
  Known<T, WorkspaceConfig> & {
701
- cacheRetention?: Known<
702
- At<T, 'cacheRetention'>,
703
- NonNullable<WorkspaceConfig['cacheRetention']>
704
- >
705
- rules?: Known<At<T, 'rules'>, WorkspaceRules>
796
+ run?: Known<At<T, 'run'>, RunSettings> & {
797
+ rules?: Known<At<At<T, 'run'>, 'rules'>, WorkspaceRules>
798
+ }
799
+ cache?: Known<At<T, 'cache'>, CacheSettings> & {
800
+ retention?: Known<At<At<T, 'cache'>, 'retention'>, NonNullable<CacheSettings['retention']>>
801
+ inputStability?: Known<At<At<T, 'cache'>, 'inputStability'>, InputStability>
802
+ }
803
+ lock?: Known<At<T, 'lock'>, LockSettings>
804
+ watch?: Known<At<T, 'watch'>, WatchSettings>
706
805
  },
707
806
  ): T {
708
807
  return config
package/src/exec/env.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { ESSENTIAL_ENV } from '../util/index.js'
3
4
 
4
5
  // Build the environment exposed to a task.
5
6
  //
@@ -9,37 +10,6 @@ import path from 'node:path'
9
10
  // 3. define: explicit name=value pairs from the task config.
10
11
  // 4. binPaths prepended to PATH (after all the above resolve PATH).
11
12
 
12
- /**
13
- * Exported so the schema doc's enumeration of it can be PINNED against the
14
- * real list: a hand-copied allowlist is how the two drift, and a reader
15
- * asking "what does my build script actually see?" is asking a
16
- * security-shaped question that a stale list answers wrongly.
17
- */
18
- export const ESSENTIAL_ENV: readonly string[] = [
19
- 'PATH',
20
- 'HOME',
21
- 'SHELL',
22
- 'USER',
23
- 'LOGNAME',
24
- 'TMPDIR',
25
- 'TEMP',
26
- 'TMP',
27
- 'LANG',
28
- 'LC_ALL',
29
- 'LC_CTYPE',
30
- 'TERM',
31
- 'COLORTERM',
32
- 'FORCE_COLOR',
33
- 'NO_COLOR',
34
- 'CI',
35
- 'NODE_OPTIONS',
36
- // Where corepack and pnpm keep their own binaries, as HOME is: a machine
37
- // path, never task output. Without them a corepack shim re-downloads its
38
- // manager (offline or sandboxed: fails) and pnpm loses its global dir.
39
- 'COREPACK_HOME',
40
- 'PNPM_HOME',
41
- ]
42
-
43
13
  /**
44
14
  * The two variables vx sets on every task it spawns, over the layers
45
15
  * above: the workspace root the run is in and the `project#task` running.