@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.
- package/LICENSE +1 -1
- package/README.md +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/local-executor/index.ts +0 -45
- 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.
|
|
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`
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// the
|
|
41
|
-
//
|
|
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
|
|
45
|
-
//
|
|
46
|
-
// this façade, which is what put these here).
|
|
47
|
-
export {
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. `
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
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
|
|
196
|
-
export {
|
|
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
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
28
|
-
* FORCE_COLOR=
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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)
|