@vzn/vx 0.0.18 → 0.0.22

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 (149) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -70
  3. package/package.json +10 -20
  4. package/src/bin.ts +64 -5
  5. package/src/cache/archive.ts +279 -28
  6. package/src/cache/cache.ts +952 -1584
  7. package/src/cache/chained-cache.ts +14 -10
  8. package/src/cache/config-evals.ts +112 -0
  9. package/src/cache/file-hashes.ts +290 -0
  10. package/src/cache/git-inputs.ts +966 -0
  11. package/src/cache/index.ts +23 -7
  12. package/src/cache/inputs.ts +553 -739
  13. package/src/cache/key-fold.ts +232 -0
  14. package/src/cache/layer.ts +674 -0
  15. package/src/cache/layered-cache.ts +211 -49
  16. package/src/cache/output-index.ts +323 -0
  17. package/src/cache/policy.ts +84 -0
  18. package/src/cache/run-history.ts +152 -0
  19. package/src/cache/zstd.ts +183 -0
  20. package/src/cli/cache.ts +88 -30
  21. package/src/cli/completions.ts +104 -0
  22. package/src/cli/core-alias.ts +25 -0
  23. package/src/cli/format.ts +4 -15
  24. package/src/cli/help.ts +76 -38
  25. package/src/cli/index.ts +82 -17
  26. package/src/cli/info.ts +162 -91
  27. package/src/cli/init.ts +87 -0
  28. package/src/cli/last.ts +130 -23
  29. package/src/cli/lock.ts +36 -18
  30. package/src/cli/plan-format.ts +18 -7
  31. package/src/cli/plugin-commands.ts +34 -12
  32. package/src/cli/run.ts +138 -313
  33. package/src/cli/select.ts +342 -0
  34. package/src/cli/show.ts +151 -71
  35. package/src/cli/upgrade.ts +136 -15
  36. package/src/cli/watch.ts +1175 -116
  37. package/src/cli/why.ts +51 -29
  38. package/src/cli/workspace-config.ts +138 -0
  39. package/src/config.ts +183 -126
  40. package/src/exec/env.ts +11 -0
  41. package/src/exec/executor.ts +62 -31
  42. package/src/exec/index.ts +16 -2
  43. package/src/exec/kill-tree.ts +281 -0
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +361 -77
  46. package/src/exec/sandbox-binds.ts +211 -0
  47. package/src/exec/sandbox-paths.ts +70 -0
  48. package/src/exec/sandbox-runtime.ts +1152 -415
  49. package/src/exec/sandbox-violations.ts +293 -0
  50. package/src/graph/dependency-spec.ts +14 -14
  51. package/src/graph/index.ts +6 -4
  52. package/src/graph/priorities.ts +130 -0
  53. package/src/graph/scheduler.ts +332 -329
  54. package/src/graph/task-graph.ts +552 -188
  55. package/src/index.ts +111 -102
  56. package/src/orchestrator/admission.ts +161 -0
  57. package/src/orchestrator/colors.ts +8 -3
  58. package/src/orchestrator/deferred-outputs.ts +25 -7
  59. package/src/orchestrator/doctor.ts +363 -0
  60. package/src/orchestrator/events.ts +111 -8
  61. package/src/orchestrator/excluded-keys.ts +155 -0
  62. package/src/orchestrator/execute-task.ts +450 -565
  63. package/src/orchestrator/failure-mode.ts +169 -28
  64. package/src/orchestrator/failure-recap.ts +121 -0
  65. package/src/orchestrator/fingerprint-watch.ts +89 -0
  66. package/src/orchestrator/framed-output.ts +121 -49
  67. package/src/orchestrator/history.ts +123 -104
  68. package/src/orchestrator/hit-restore.ts +285 -0
  69. package/src/orchestrator/index.ts +42 -5
  70. package/src/orchestrator/keyed-projects.ts +89 -0
  71. package/src/orchestrator/local-shortcircuit.ts +83 -23
  72. package/src/orchestrator/lockfile-claim.ts +401 -0
  73. package/src/orchestrator/logger.ts +128 -41
  74. package/src/orchestrator/metrics.ts +201 -72
  75. package/src/orchestrator/miss-save.ts +232 -0
  76. package/src/orchestrator/options.ts +63 -34
  77. package/src/orchestrator/persistent.ts +99 -0
  78. package/src/orchestrator/placement.ts +212 -0
  79. package/src/orchestrator/plan.ts +24 -11
  80. package/src/orchestrator/plugin-host.ts +200 -49
  81. package/src/orchestrator/plugin.ts +190 -14
  82. package/src/orchestrator/prepare.ts +168 -137
  83. package/src/orchestrator/projects.ts +270 -0
  84. package/src/orchestrator/remote-prefetch.ts +22 -2
  85. package/src/orchestrator/run-artifacts.ts +34 -3
  86. package/src/orchestrator/run-context.ts +20 -5
  87. package/src/orchestrator/run-lock.ts +349 -0
  88. package/src/orchestrator/run-records.ts +132 -0
  89. package/src/orchestrator/run-report.ts +6 -4
  90. package/src/orchestrator/run.ts +467 -611
  91. package/src/orchestrator/sandbox-request.ts +569 -0
  92. package/src/orchestrator/save-lane.ts +44 -0
  93. package/src/orchestrator/shell-verdict.ts +117 -0
  94. package/src/orchestrator/signals.ts +167 -0
  95. package/src/orchestrator/stable-keys.ts +259 -10
  96. package/src/orchestrator/status-line.ts +35 -2
  97. package/src/orchestrator/summary.ts +115 -25
  98. package/src/orchestrator/task-hash.ts +103 -21
  99. package/src/orchestrator/task-log-buffer.ts +15 -18
  100. package/src/orchestrator/telemetry-host.ts +27 -8
  101. package/src/orchestrator/telemetry.ts +71 -44
  102. package/src/orchestrator/upstream.ts +91 -45
  103. package/src/util/bun-version.ts +43 -0
  104. package/src/util/cgroup.ts +160 -0
  105. package/src/util/edit-distance.ts +49 -0
  106. package/src/util/errors.ts +86 -0
  107. package/src/util/hash.ts +10 -1
  108. package/src/util/index.ts +42 -5
  109. package/src/util/paths.ts +168 -5
  110. package/src/util/procfs.ts +20 -0
  111. package/src/util/settle.ts +23 -2
  112. package/src/util/size.ts +33 -0
  113. package/src/util/task-id.ts +20 -0
  114. package/src/util/timing.ts +11 -1
  115. package/src/util/verbs.ts +39 -0
  116. package/src/util/which.ts +48 -0
  117. package/src/workspace/affected.ts +368 -60
  118. package/src/workspace/config-cache.ts +271 -32
  119. package/src/workspace/config-eval.ts +60 -16
  120. package/src/workspace/config-imports.ts +131 -9
  121. package/src/workspace/config-schema.ts +1033 -0
  122. package/src/workspace/filter.ts +69 -17
  123. package/src/workspace/fingerprint.ts +59 -7
  124. package/src/workspace/index.ts +37 -3
  125. package/src/workspace/json-data.ts +90 -0
  126. package/src/workspace/load-reads.ts +32 -0
  127. package/src/workspace/lockfile.ts +41 -14
  128. package/src/workspace/migrate-scripts.ts +187 -0
  129. package/src/workspace/migration.ts +353 -0
  130. package/src/workspace/package-graph.ts +239 -31
  131. package/src/workspace/project-loader.ts +295 -815
  132. package/src/workspace/workspace.ts +266 -31
  133. package/plugins/local-cache/index.ts +0 -2
  134. package/plugins/local-executor/index.ts +0 -2
  135. package/plugins/schedule-history/index.ts +0 -2
  136. package/src/cache/cas-backend.ts +0 -115
  137. package/src/cache/digest.ts +0 -32
  138. package/src/cli/migrate-emit.ts +0 -21
  139. package/src/cli/migrate-nx.ts +0 -473
  140. package/src/cli/migrate-scripts.ts +0 -127
  141. package/src/cli/migrate-turbo.ts +0 -401
  142. package/src/cli/migrate.ts +0 -305
  143. package/src/cli/prune.ts +0 -291
  144. package/src/orchestrator/missing-plugin.ts +0 -9
  145. package/src/orchestrator/resources.ts +0 -45
  146. package/src/orchestrator/verify.ts +0 -174
  147. package/src/plugins/local-cache/index.ts +0 -11
  148. package/src/plugins/local-executor/index.ts +0 -45
  149. package/src/plugins/schedule-history/index.ts +0 -109
package/src/index.ts CHANGED
@@ -3,20 +3,37 @@
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
- // `clampInt` and `parseDecimalInt` ride along for the same reason the status
13
- // predicates do: without them on the façade an integration package writes its
14
- // own, and both had already happened. A bounds helper whose floor is
15
- // load-bearing (a fractional SQL LIMIT is a datatype mismatch, not a smaller
16
- // page) should have one implementation — and so should the ONE strict integer
17
- // parser, whose entire purpose is that `Number()` silently accepts `0x10` and
18
- // `1e3` at a boundary where a typo must be an error, not a different number.
19
- export { clampInt, parseDecimalInt, parseSize, UserError, isUserError } 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'
33
+ // How core reads a task glob — its spelling, and which characters are
34
+ // wildcards (`[` `]` are not; item 667) — for a plugin that turns declared
35
+ // outputs into something else (`@vzn/vx-reapi`'s literal `output_paths`).
36
+ export { isLiteralPattern, normalizeGlob } from './util/index.js'
20
37
 
21
38
  // Schema types and helpers (used by user vx.config files and presets).
22
39
  export type {
@@ -28,36 +45,31 @@ export type {
28
45
  CacheConfig,
29
46
  CacheInputs,
30
47
  CacheOutputs,
31
- ResourcesConfig,
32
48
  SandboxConfig,
33
- SandboxNetworkConfig,
49
+ SandboxGrants,
50
+ SandboxDenials,
34
51
  } from './config.js'
35
52
  export { defineProject, defineWorkspace } from './config.js'
53
+ export { definePlugin } from './orchestrator/index.js'
36
54
 
37
- // Programmatic engine API (run / plan / prepare) + the graph primitives a
38
- // distribution submitter/agent reasons over + the cache-key hashing seam.
39
- // `deriveStableKeys` is THE shared stable-key derivation (remote-prefetch,
40
- // the local short-circuit, and the distributed submitter must never drift
41
- // on the stability gate); `captureGitContext`/`captureWorkspaceIdentity`
42
- // give agents + the submitter identity before/without a telemetry run.
55
+ // Programmatic engine API: run / plan / prepare (docs/cli.md § Programmatic
56
+ // API). The graph primitives, the cache-key hashing seam and the git / host
57
+ // context capture used to sit beside these for a distributed submitter that
58
+ // left the repo (2026-08); they went with it (2026-09-10) — a seam with no
59
+ // consumer is re-added when one appears, shaped by its use.
43
60
  export { run, planRun, prepareRun } from './orchestrator/index.js'
44
- // The per-task duration history a `schedule` plugin learns from (see
45
- // src/plugins/schedule-history — core's own plugins import core only via
46
- // this façade, which is what put these here).
47
- export { EmptyHistoryProvider, LocalHistoryProvider } from './orchestrator/index.js'
61
+ // The per-task duration history a `schedule` plugin learns from
62
+ // (`@vzn/vx-schedule-history` does; a plugin package reaches core only
63
+ // through this façade, which is what put these here).
64
+ export { LocalHistoryProvider } from './orchestrator/index.js'
48
65
  export type { HistoryProvider, HistoryTable, TaskHistory } from './orchestrator/index.js'
49
66
  export type { PreparedRun } from './orchestrator/index.js'
50
- export { computeTaskHash, createHashCache, deriveStableKeys } from './orchestrator/index.js'
51
- export type { DeriveStableKeysArgs, HashCache, StableKey } from './orchestrator/index.js'
52
- export {
53
- captureDefaultBranch,
54
- captureGitContext,
55
- captureHostContext,
56
- captureWorkspaceIdentity,
57
- detectCi,
58
- } from './orchestrator/index.js'
67
+ // `planRun` returns these, and an embedder that cannot NAME a return type
68
+ // cannot hold it: `run` and `prepareRun` both had theirs here from the
69
+ // start, so the omission read as "planRun is not really public" while
70
+ // docs/cli.md § Programmatic API listed all three (item 387, 2026-09-19).
71
+ export type { RunPlan, PlannedTask } from './orchestrator/index.js'
59
72
  export type { CiContext, GitContext, HostContext, WorkspaceIdentity } from './orchestrator/index.js'
60
- export { FULL_CACHE_POLICY, parseCachePolicy } from './orchestrator/index.js'
61
73
  export type {
62
74
  CachePolicy,
63
75
  Logger,
@@ -65,84 +77,96 @@ export type {
65
77
  RunOptions,
66
78
  RunSummary,
67
79
  } from './orchestrator/index.js'
68
- export { defaultLogger, resolveOutputView } from './orchestrator/index.js'
69
80
  // `splitTaskId` is on the façade because the alternative is what happened:
70
81
  // with only `taskId()` to JOIN an id and nothing exported to SPLIT one,
71
82
  // consumers roll their own `split('#', 2)` and drift from the graph, which
72
83
  // splits on the FIRST '#'.
73
- export {
74
- buildTaskGraph,
75
- expandRequested,
76
- isGroupTask,
77
- markSurfacedDeps,
78
- splitTaskId,
79
- } from './graph/index.js'
80
- export type {
81
- OutputFingerprint,
82
- TaskNode,
83
- TaskOutcome,
84
- TaskStatus,
85
- VerifyVerdict,
86
- } from './graph/index.js'
87
- // The one output-tree diff implementation (verify verdicts + a serve's
88
- // cross-machine fingerprint diff both name rels through it).
89
- export { diffOutputTrees } from './orchestrator/index.js'
84
+ export { outputsOverlap, splitTaskId } from './graph/index.js'
85
+ export type { TaskNode, TaskOutcome, TaskStatus } from './graph/index.js'
90
86
 
91
- // Cache classes + the layer interface (the `cache` capability's currency)
92
- // and input-output resolution. The blob-CAS/digest substrate
93
- // (cas-backend.ts / digest.ts) stays module-internal until it has a
94
- // consumer — no speculative public API. `cleanOutputs` is public for the
95
- // distributed submitter's targeted output materialization (wipe declared
96
- // outputs, then `restoreOutputs` the artifact — never a naive re-run).
97
- export {
98
- Cache,
99
- LayeredCache,
100
- GitFilesCache,
101
- cleanOutputs,
102
- resolveInputs,
103
- resolveOutputs,
104
- } from './cache/index.js'
87
+ // Cache classes + the layer interface (the `cache` capability's currency).
88
+ export { Cache, LayeredCache } from './cache/index.js'
105
89
  export type { CacheLayer, RemoteCacheLayer, RunRecord, InvocationRecord } from './cache/index.js'
106
90
 
107
91
  // Workspace discovery + the project/config catalog surface — an
108
- // out-of-process service/CLI needs these. `readLockfile` is THE one reader
109
- // of vx-lock.json (the format carries its own version sentinel; a second
110
- // parser in a sibling package would drift), and the loader chain
111
- // (`loadWorkspace` → `listProjectMetas` → `loadProjectConfig`) is the same
112
- // one `vx show` uses. Workspace's `listProjects` re-exports as
113
- // `listProjectMetas` (the bare name once belonged to a metrics query).
114
- export { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from './workspace/index.js'
115
- export { readLockfile, LOCKFILE_NAME } from './workspace/index.js'
116
- export type { Lockfile, LockfileEntry } from './workspace/index.js'
92
+ // out-of-process service/CLI needs these. `loadProjectConfig` is the RAW
93
+ // per-file load (`vx lock` freezes exactly that); the resolved view a run
94
+ // or `vx show` sees — plugin stages applied — is `loadResolvedProjects`
95
+ // below. Workspace's `listProjects` re-exports as `listProjectMetas` (the
96
+ // bare name once belonged to a metrics query).
97
+ export { findWorkspaceRoot } from './workspace/index.js'
117
98
  export {
118
99
  loadWorkspace,
119
100
  loadProjectConfig,
120
101
  listProjects as listProjectMetas,
121
102
  } from './workspace/index.js'
122
103
  export type { ProjectMeta } from './workspace/index.js'
104
+ // The package graph as a run sees it (workspace deps by manifest), for a tool
105
+ // that needs a project's transitive closure the way `vx run` computes it.
106
+ export { buildPackageGraph } from './workspace/index.js'
107
+ // The run path's RESOLVED view — plugin `config` and `project` stages
108
+ // applied, cached evaluations served — for a reader outside the CLI (the
109
+ // MCP server's `listTasks`, an embedder's task catalog). What `vx show`
110
+ // prints.
111
+ export { loadResolvedProjects } from './orchestrator/index.js'
112
+ export type { ProjectEntry } from './workspace/index.js'
113
+ // The migration seam: how a generated config is planned, rendered, guarded
114
+ // and written. `vx init` (package.json scripts) uses it in core;
115
+ // `@vzn/vx-migrate` (Turbo, Nx) and any other adoption tool use it from here.
116
+ export {
117
+ applyMigration,
118
+ foldScriptHooks,
119
+ PERSISTENT_TASK_NAMES,
120
+ PERSISTENT_TODO,
121
+ quoteTsLiteral,
122
+ } from './workspace/index.js'
123
+ export type {
124
+ ApplyMigrationArgs,
125
+ MigrationFormat,
126
+ GeneratedProject,
127
+ GeneratedTask,
128
+ MigrationPlan,
129
+ RawExpr,
130
+ } from './workspace/index.js'
123
131
 
124
132
  // Plugin API — the run-level extension points. Behavior capabilities
125
133
  // (executor / cache) change WHAT/HOW work runs; the observe-only `telemetry`
126
134
  // capability is the canonical data-export path and cannot change behavior.
127
135
  // A plugin is declared in vx.workspace.ts via defineWorkspace({ plugins }).
128
136
  // See docs/design/observability-architecture-2026-06.md.
137
+ // The claimant's shell for a lockfile plugin (`@vzn/vx-lockfile`):
138
+ // the claim, the per-project key, the memo and the `--affected` diff around
139
+ // a parser; `reachDigests` is the Merkle-over-components digest both use.
140
+ export {
141
+ lockfileClaim,
142
+ reachDigests,
143
+ type LockfileClaimHooks,
144
+ type LockfileClaimOptions,
145
+ type ReachGraph,
146
+ } from './orchestrator/index.js'
129
147
  export type {
130
148
  VxPlugin,
149
+ PluginHooks,
150
+ PluginOrigin,
131
151
  CacheContext,
132
152
  ExecutorContext,
133
153
  CommandContext,
154
+ FingerprintChange,
155
+ FingerprintClaim,
156
+ FingerprintContext,
134
157
  GraphHookContext,
135
158
  KeyHookContext,
136
159
  PluginCommand,
137
160
  ProjectHookContext,
138
161
  ScheduleHookContext,
162
+ AdmitContext,
139
163
  WorkspaceHookContext,
140
164
  PluginSetupContext,
141
165
  } from './orchestrator/index.js'
142
- // Process primitives — what an executor plugin builds on. `@vzn/vx/plugins/local-executor`
143
- // is the reference implementation and imports exactly these.
144
- export { runCommand, runSandboxed } from './exec/index.js'
145
166
  // The per-task execution contract a plugin's `executor` capability returns.
167
+ // (`runCommand` / `runSandboxed`, the local executor's own primitives, left
168
+ // the façade 2026-09-10: no executor plugin built on them — `@vzn/vx-reapi`
169
+ // speaks a wire — and a seam with no consumer is a special case in waiting.)
146
170
  export type {
147
171
  ExecuteRequest,
148
172
  ExecuteResult,
@@ -166,7 +190,6 @@ export type {
166
190
  // plugin that renders a run as a markdown table takes the same unvalidated
167
191
  // task names core does, and the cloud job summary shipped without the escape.
168
192
  export {
169
- assembleRunSummary,
170
193
  escapeMarkdownCell,
171
194
  TELEMETRY_SCHEMA_VERSION,
172
195
  deriveCacheSource,
@@ -180,6 +203,10 @@ export {
180
203
  // a hit's bytes belong to the run that executed) are a decision, not an
181
204
  // implementation detail. Two sinks rolling their own is how they fork.
182
205
  export { LOG_WIRE_VERSION, TaskLogBuffer } from './orchestrator/index.js'
206
+ // `exitSignal` is on the façade on the same demonstrated need: a sink that
207
+ // renders a failed task's exit code faces 137 where the frame and `vx last`
208
+ // say `128 + SIGKILL`; the decode is the runner's convention, one copy.
209
+ export { exitSignal } from './orchestrator/index.js'
183
210
  export type {
184
211
  CacheSource,
185
212
  RunContextRecord,
@@ -192,29 +219,11 @@ export type {
192
219
  TelemetrySink,
193
220
  } from './orchestrator/index.js'
194
221
 
195
- // The serializable event projection.
196
- export { projectNode, projectOutcome } from './orchestrator/index.js'
197
- export type { RunResult } from './orchestrator/index.js'
198
-
199
- // Event bus + wire form — adapters (otel-bridge, custom subscribers) ride this.
200
- export { createEventBus, wireForwarder, toWireEvent } from './orchestrator/index.js'
201
- export type {
202
- EventBus,
203
- RunEvent,
204
- RunEventSubscriber,
205
- WireEvent,
206
- TaskView,
207
- OutcomeView,
208
- } from './orchestrator/index.js'
222
+ // The serializable run result and its per-task views (what `run()` returns).
223
+ export type { RunResult, TaskView, OutcomeView } from './orchestrator/index.js'
209
224
 
210
- // Run-history queries over cache.db — what `vx why` / `vx last` read. An
211
- // out-of-process surface (an MCP server, a dashboard plugin) reads the same.
212
- export {
213
- cacheKeyDiff,
214
- explainCacheKeyQuery,
215
- getInvocation,
216
- getRun,
217
- listInvocations,
218
- listRuns,
219
- whyDidThisRerunQuery,
220
- } from './orchestrator/index.js'
225
+ // The one run-history query a reader outside the CLI takes today: `vx why`'s
226
+ // answer, which `@vzn/vx-mcp` serves. The event bus / wire form and the
227
+ // other history readers (`listRuns`, `getRun`, …) left the façade 2026-09-10
228
+ // with no consumer; the telemetry seam above is the canonical export path.
229
+ export { latestRunId, whyDidThisRerunQuery } from './orchestrator/index.js'
@@ -0,0 +1,161 @@
1
+ // What stands between the scheduler's `execute` callback and executeTask:
2
+ // two rules that decide whether a ready task runs NOW and whether its
3
+ // result may be SAVED. Neither changes what the task is.
4
+ //
5
+ // - In-flight dedup: an embedder running concurrent delegated runs in one
6
+ // process (a daemon built on the façade; core ships none) supplies an
7
+ // `inflight` registry; a task whose key
8
+ // a sibling is already computing waits for it and cache-hits on what it
9
+ // saved. A stateless `vx run` passes none and takes the untouched path.
10
+ // - Continue-taint: under `continueMode: 'always'` a task runs although an
11
+ // upstream failed. Its key is the healthy one (pure-input hashing) but
12
+ // its bytes are not, so the taint is tracked here and propagates through
13
+ // every success built on it — or a grand-dependent would cache the same
14
+ // partial tree one hop later. Only that mode ever executes a task behind
15
+ // a failure; the other modes skip it, so a default run carries no check.
16
+ // `--exclude-dependencies` seeds the same taint: a task whose key folds a
17
+ // dependency that did not run has bytes nothing vouches for
18
+ // (excluded-keys.ts).
19
+ //
20
+ // Split from `run.ts` on 2026-09-10 (pure motion).
21
+
22
+ import type { CachePolicy } from '../cache/index.js'
23
+ import { isGroupTask, RestoreDemoted, type TaskNode, type TaskOutcome } from '../graph/index.js'
24
+ import { executeTask, type ExecuteArgs } from './execute-task.js'
25
+ import type { ShortCircuit } from './local-shortcircuit.js'
26
+ import { computeTaskHash, type ComputeHashArgs } from './task-hash.js'
27
+
28
+ /**
29
+ * Records which tasks ran behind a failure, or on the key of a dependency
30
+ * that did not run (`seeds`), and answers, per task, whether this one does.
31
+ * Disabled (always `false`, nothing recorded) unless the run's
32
+ * `continueMode` is `'always'` or there is a seed.
33
+ */
34
+ export function taintTracker(
35
+ continueAlways: boolean,
36
+ seeds: ReadonlySet<string>,
37
+ ): (node: TaskNode, upstream: TaskOutcome[]) => boolean {
38
+ if (!continueAlways && seeds.size === 0) return () => false
39
+ const tainted = new Set<string>()
40
+ return (node, upstream) => {
41
+ const taint =
42
+ seeds.has(node.id) ||
43
+ upstream.some(
44
+ // A restore-tier task may run before its deps and see holes here;
45
+ // it never saves anyway (a hit restores), so a hole is not taint.
46
+ (u) =>
47
+ u !== undefined &&
48
+ (tainted.has(u.node.id) ||
49
+ (continueAlways &&
50
+ (u.status === 'failed' || u.status === 'aborted' || u.status === 'skipped'))),
51
+ )
52
+ if (taint) tainted.add(node.id)
53
+ return taint
54
+ }
55
+ }
56
+
57
+ export interface AdmissionArgs {
58
+ /** The shared registry, or undefined for a stateless run (no dedup). */
59
+ inflight: Map<string, Promise<void>> | undefined
60
+ policy: CachePolicy
61
+ shortCircuit: ShortCircuit
62
+ /** Everything `computeTaskHash` needs besides the node and its upstream. */
63
+ hashArgs: Omit<ComputeHashArgs, 'node' | 'upstream' | 'nestedProjectDirs'> & {
64
+ nestedDirsByProject: ReadonlyMap<string, string[]>
65
+ }
66
+ /**
67
+ * The run's execute-args builder. `reuseProbe: false` drops the up-front
68
+ * probe: a task joining a sibling must let executeTask probe fresh,
69
+ * because the probe predates the sibling's save.
70
+ */
71
+ buildExecuteArgs: (node: TaskNode, upstream: TaskOutcome[], reuseProbe?: boolean) => ExecuteArgs
72
+ }
73
+
74
+ /** The scheduler's `execute` callback: dedup when it can help, else executeTask. */
75
+ export function admitTasks(
76
+ args: AdmissionArgs,
77
+ ): (node: TaskNode, upstream: TaskOutcome[]) => Promise<TaskOutcome> {
78
+ const { inflight, policy, shortCircuit, hashArgs, buildExecuteArgs } = args
79
+ // A probed hit whose artifact vanished before its restore is thrown back
80
+ // to the scheduler (`RestoreDemoted`, execute-task.ts), which dispatches
81
+ // the task again once its deps are done. That dispatch probes for itself:
82
+ // the up-front probe answered for an artifact that is gone. The task
83
+ // stays in the restore-tier SET, so it still skips dedup below.
84
+ const dropProbe = (err: unknown): never => {
85
+ if (err instanceof RestoreDemoted) shortCircuit.preProbed.delete(err.taskId)
86
+ throw err
87
+ }
88
+ return async (node, upstream) => {
89
+ // Dedup only helps when the sibling will WRITE the artifact and this
90
+ // task can READ it back — i.e. both axes effectively on.
91
+ const canRead = policy.localRead || policy.remoteRead
92
+ const canWrite = policy.localWrite || policy.remoteWrite
93
+ const cacheable =
94
+ !isGroupTask(node) &&
95
+ node.config.exec?.persistent === undefined &&
96
+ node.config.cache !== undefined &&
97
+ canRead &&
98
+ canWrite
99
+ // A restore-tier task (confirmed local hit, may run before its deps)
100
+ // needs no dedup — it's a restore, not an executor, and its live
101
+ // `upstream` is incomplete, so the dedup hash recompute would be
102
+ // wrong. Route it straight to executeTask, which reuses the up-front
103
+ // probe.
104
+ const restorable = shortCircuit.restoreTier.has(node.id)
105
+ // `!cacheable` is an early-out for bookkeeping, not a guard: measured
106
+ // (item 506), a non-cacheable task never finds a sibling barrier,
107
+ // because the hash folds the taskId and a taskId executes once per
108
+ // run. Removing it reddens nothing and a throw on "a non-cacheable
109
+ // task joined a barrier" never fires across the whole suite. It saves
110
+ // the set/delete, and the wait a joiner would spend before running the
111
+ // task anyway — there is no artifact for it to hit.
112
+ if (inflight === undefined || !cacheable || restorable) {
113
+ return executeTask(buildExecuteArgs(node, upstream)).catch(dropProbe)
114
+ }
115
+ const { nestedDirsByProject, ...rest } = hashArgs
116
+ const hash = await computeTaskHash({
117
+ ...rest,
118
+ node,
119
+ upstream,
120
+ nestedProjectDirs: nestedDirsByProject.get(node.projectName) ?? [],
121
+ })
122
+ const existing = inflight.get(hash)
123
+ if (existing !== undefined) {
124
+ // Join a sibling already computing this exact task: wait, then
125
+ // executeTask cache-hits on the artifact it just saved.
126
+ await existing.catch(() => {})
127
+ return executeTask(buildExecuteArgs(node, upstream, false))
128
+ }
129
+ // Become the executor: register a barrier siblings await. get→set has
130
+ // no await between, so registration is atomic — at most one executor
131
+ // per hash. Released on every exit (success / failure / throw).
132
+ let release!: () => void
133
+ inflight.set(
134
+ hash,
135
+ new Promise<void>((resolve) => {
136
+ release = resolve
137
+ }),
138
+ )
139
+ const execArgs = buildExecuteArgs(node, upstream)
140
+ try {
141
+ // `return await`, deliberately: the finally must run after the task
142
+ // settles, not when its promise is handed back.
143
+ return await executeTask(execArgs)
144
+ } catch (err) {
145
+ return dropProbe(err)
146
+ } finally {
147
+ // The barrier lifts when the ENTRY is there, not when the task is:
148
+ // the save runs off the slot (save-lane.ts), and a sibling released
149
+ // before it landed would probe a miss and run the task again. The
150
+ // executor's own return is not held — only the joiners wait.
151
+ const landed = execArgs.deferredSaves?.get(node.id)
152
+ const lift = (): void => {
153
+ execArgs.deferredSaves?.delete(node.id)
154
+ inflight.delete(hash)
155
+ release()
156
+ }
157
+ if (landed !== undefined) void landed.then(lift, lift)
158
+ else lift()
159
+ }
160
+ }
161
+ }
@@ -24,13 +24,18 @@ export interface PaintOptions {
24
24
 
25
25
  /**
26
26
  * Standard env precedence:
27
- * NO_COLOR=anything → off (overrides FORCE_COLOR)
28
- * FORCE_COLOR=anything → on
27
+ * NO_COLOR non-empty → off (overrides FORCE_COLOR)
28
+ * FORCE_COLOR=0 or =false → off, even on a TTY
29
+ * FORCE_COLOR any other value → on (the empty string too)
29
30
  * else: on iff stdout is a TTY.
31
+ * `0` and `false` are the convention's "off" (supports-color, chalk,
32
+ * Node); reading every non-empty value as on painted a piped CI log that
33
+ * set `FORCE_COLOR=0` (nx#35292's bug).
30
34
  */
31
35
  export function detectColors(stream: NodeJS.WriteStream = process.stdout): ColorSupport {
32
36
  if (process.env['NO_COLOR']) return { enabled: false }
33
- if (process.env['FORCE_COLOR']) return { enabled: true }
37
+ const force = process.env['FORCE_COLOR']
38
+ if (force !== undefined) return { enabled: force !== '0' && force !== 'false' }
34
39
  return { enabled: stream.isTTY === true }
35
40
  }
36
41
 
@@ -77,15 +77,24 @@ export class DeferredOutputs {
77
77
  async materializeFor(node: TaskNode): Promise<void> {
78
78
  const needed: string[] = []
79
79
  const seen = new Set<string>()
80
- const walk = (id: string): void => {
81
- for (const dep of this.args.nodes.get(id)?.deps ?? []) {
82
- if (seen.has(dep)) continue
83
- seen.add(dep)
84
- if (this.entries.has(dep)) needed.push(dep)
85
- walk(dep)
80
+ // Pre-order on an explicit stack of dependency lists: a closure is as
81
+ // deep as the graph, and a recursion per edge threw `RangeError` at the
82
+ // 50,000 the builder takes (item 737).
83
+ const stack: Array<[deps: readonly string[], next: number]> = [
84
+ [this.args.nodes.get(node.id)?.deps ?? [], 0],
85
+ ]
86
+ while (stack.length > 0) {
87
+ const frame = stack[stack.length - 1]!
88
+ if (frame[1] === frame[0].length) {
89
+ stack.pop()
90
+ continue
86
91
  }
92
+ const dep = frame[0][frame[1]++]!
93
+ if (seen.has(dep)) continue
94
+ seen.add(dep)
95
+ if (this.entries.has(dep)) needed.push(dep)
96
+ stack.push([this.args.nodes.get(dep)?.deps ?? [], 0])
87
97
  }
88
- walk(node.id)
89
98
  if (needed.length === 0) return
90
99
  await Promise.all(needed.map((id) => this.materializeOne(id)))
91
100
  }
@@ -101,12 +110,21 @@ export class DeferredOutputs {
101
110
  private async run(taskId: string): Promise<void> {
102
111
  const entry = this.entries.get(taskId)!
103
112
  const producer = this.args.nodes.get(taskId)!
113
+ // Project outputs only: a task declaring `outputs.workspaceFiles` is
114
+ // never deferred (`deferralEligibility` forces it eager), so nothing
115
+ // workspace-anchored can reach this path.
104
116
  const outputs = producer.config.cache?.outputs.files ?? []
105
117
  const nestedProjectDirs = this.args.nestedDirsByProject.get(producer.projectName) ?? []
106
118
  const cleanArgs = { projectDir: producer.projectDir, outputs, nestedProjectDirs }
107
119
 
108
120
  // Mirror `restoreHit`'s sequencing so the two paths cannot drift: wipe
109
121
  // the declared outputs, write, then tell the git snapshot what moved.
122
+ // The wipe is a rule (a straggler in the declared tree is gone after
123
+ // the fetch); the two marks are the mirror's cost, not a rule: no
124
+ // reader of these outputs can be in the run — `deferralEligibility`
125
+ // keeps every same-project reader and every workspaceFiles reader
126
+ // eager — so deleting the post-fetch mark survives the whole core
127
+ // suite (item 643).
110
128
  if (outputs.length > 0) {
111
129
  const cleaned = await cleanOutputs(cleanArgs)
112
130
  this.args.gitFilesCache?.markOutputsChanged(producer.projectDir, cleaned)