Versioning and support (2026-09-23, roadmap 3.1–3.4)
This page says what a vx release may change, and what it may not. It
takes effect at 1.0. Until then, releases are 0.x and a minor may
break anything, but every break is listed under “Breaking” in the
release notes (history/release-0.1.0-notes.md is the model).
The contract
Section titled “The contract”From 1.0, these surfaces follow semver. A patch fixes them, a minor adds to them, and only a major removes or changes one.
| Surface | Defined by |
|---|---|
The config schema: every field vx.config.ts and .vx/workspace.ts accept, and what each means |
src/workspace/config-schema.ts, docs/schema.md; recorded in tests/contract/config-schema.json |
The plugin API: the hooks in PLUGIN_HOOKS, definePlugin, CacheLayer / RemoteCacheLayer, TaskExecutor, and the telemetry records (TELEMETRY_SCHEMA_VERSION) |
src/orchestrator/plugin.ts, src/cache/layer.ts, src/cache/layered-cache.ts, src/exec/executor.ts, src/orchestrator/telemetry.ts; recorded in tests/contract/package-api.txt |
| The package’s exports | src/index.ts; names pinned by package-boundaries.unsafe.test.ts, shapes by tests/contract/package-api.txt |
The first-party plugin packages’ exports (@vzn/vx-reapi, @vzn/vx-otel, …) |
each package’s src/index.ts; recorded in tests/contract/plugin-api/, one file per package |
The CLI: verbs, flags, exit codes, and the machine-readable outputs (--dry=json, --graph, --summarize, --profile, vx mcp’s tools) |
docs/cli.md; verbs, flags and their values recorded in tests/contract/cli-surface.json; exit codes recorded in tests/contract/exit-codes.json, tests/contract/exit-codes-states.json and tests/contract/exit-codes-init.json; --dry=json, --summarize and --graph recorded in tests/contract/cli-wire.json; --profile in tests/contract/profile-wire.json; vx mcp’s tools in packages/vx-mcp/tests/contract/tools.json |
The lock file: .vx/lock.json, what vx lock writes and vx lock --check and every run read |
src/workspace/lockfile.ts (LOCKFILE_VERSION), docs/cli.md; recorded in tests/contract/lock.json |
The environment variables vx reads (VX_*) |
docs/cli.md § Environment variables vx reads, held to the source by tests/env-doc-drift.test.ts; recorded in tests/contract/cli-surface.json |
| Workspace discovery: which manifests make a root and list its projects, which root a run from inside a member finds, and which config file names load, in which order | src/workspace/workspace.ts, src/workspace/project-loader.ts; recorded in tests/contract/discovery.json |
The environment variables the first-party plugins read (VX_REAPI_*, OTEL_*, GITHUB_*, TURBO_*, …) |
each package’s src/; recorded in tests/contract/plugin-env.txt |
What @vzn/vx-otel sends: span and metric names, attribute keys and their OTLP value types |
packages/vx-otel/src/otlp.ts; recorded in packages/vx-otel/tests/contract/otlp.txt |
vx-migrate’s command lines, and those of nx-exec and nx-env, which the configs it writes call |
each bin’s usage in packages/vx-migrate/src/; recorded in packages/vx-migrate/tests/contract/cli.txt |
What each npm package ships: the files npm publish uploads for @vzn/vx and every plugin |
scripts/build-npm.ts; recorded in tests/contract/pack/ |
Task-glob semantics: which paths a pattern selects (item 667 made [ literal; that reading is now part of the contract) |
docs/schema.md, docs/caching.md |
What 1.0 freezes, exactly
Section titled “What 1.0 freezes, exactly”The table above names the surfaces. This section lists what is in them,
and tests/contract-versioning-doc.test.ts checks every list here
against the code, in both directions, so the page cannot promise a field
the schema dropped or leave out one it gained.
Config fields
Section titled “Config fields”Each object level of a config and the fields it accepts. A field’s type,
the values it takes and each refusal’s exact words are recorded in
tests/contract/config-schema.json, and what each field means is in
schema.md. <name> is a key the author picks.
.vx/workspace.ts:
| Level | Fields |
|---|---|
| (top) | baseBranch, cache, inputs, lock, outputs, plugins, run, watch |
run |
concurrency, continue, retries, rules, schedule, timeout |
lock |
reviewFiles |
run.rules |
dependencies, exclusiveOutputs |
cache |
concurrency, dir, download, policy, store, upload |
cache.concurrency |
download, restore, upload |
cache.store |
decompress, embeddings |
cache.upload |
failures |
watch |
poll |
inputs |
stability |
inputs.stability |
severity, strategy |
outputs |
clean |
vx.config.ts:
| Level | Fields |
|---|---|
| (top) | name, tags, tasks |
tasks.<name> |
cache, dependsOn, description, exec, inputs, outputs |
tasks.<name>.exec |
command, env, interactive, persistent, platforms, remote, retries, sandbox, timeout, worker |
tasks.<name>.exec.env |
define, passThrough, secret |
tasks.<name>.exec.persistent |
readyWhen |
tasks.<name>.exec.sandbox |
allow, deny, hermetic, ignore, weakerNetworkIsolation, weakerWhenNested |
tasks.<name>.exec.sandbox.allow |
gitConfig, localBinding, machLookup, network, pty, read, systemInfo, unixSockets, write |
tasks.<name>.exec.sandbox.deny |
network |
tasks.<name>.exec.sandbox.ignore |
network, read, systemInfo, write |
tasks.<name>.inputs |
env, files, runtime, stability, tasks, workspaceFiles, workspaceRuntime |
tasks.<name>.inputs.stability |
severity, strategy |
tasks.<name>.outputs |
clean, files, workspaceFiles |
Keyed by name: tasks, tasks.<name>.exec.env.define.
sandbox.ignore takes only the four classes a denial is reported in
(D-4); any other grant name is refused as an unknown field.
The plugin API
Section titled “The plugin API”- Hooks, in pipeline order:
config,discover,project,graph,key,fingerprint,measures,schedule,admit,executor,cache,telemetry,output,setup,commands,teardown. A plugin isdefinePlugin(import.meta, hooks), and its name is its package’s. - Telemetry records, schema version 4. The kinds are
run.start,task.start,task.log,task.sample,task.endandrun.end; each record carries the version asv. - Types. Every type
@vzn/vxexports is frozen with every type it names, whether that one is exported or not.VxPluginand its hook contexts,CacheLayer,RemoteCacheLayer,TaskExecutor,ExecuteRequest/ExecuteResult,TelemetrySinkandTelemetryRecordare the ones a plugin implements or receives. The full text istests/contract/package-api.txt.
The CLI
Section titled “The CLI”The verbs, flags, exit codes and machine-readable outputs in docs/cli.md.
How the contract is held
Section titled “How the contract is held”A surface is frozen only if a change to it fails a test. Each pin records the surface as the code produces it, in a committed file, so a change is a reviewed diff of that file and never a side effect.
A break must also be declared. tests/api-break.unsafe.test.ts diffs
every contract record — each file under tests/contract/ but the
vendored upstream schemas, the --format json schemas in schemas/,
vx-mcp’s tools record, vx-otel’s OTLP wire, vx-migrate’s command
lines — against the same file at the last
v* tag, each read its own way (contractBreaks in
scripts/api-break.ts): the API records by declaration (one moved to
another file is the same declaration), a pack list by
the files it lost outside src/ or among its package’s exports and
bin targets (no other src/ file can be imported, so moving one is
internal), a record of leaves by what it lost (a changed value is lost too), the
config schema by the fields and accepted values it lost (a reworded
refusal is not a break), its rules by the combinations it lost, an
output schema by the properties, required entries, types and enum
values it lost (reworded title or description prose is not), and the
Turbo/Nx table by a key whose status got worse. A break fails unless a
commit since the tag is marked type!: or carries a BREAKING CHANGE:
footer, which also heads the release notes (scripts/release-notes.ts);
on a PR, the same diff between its base and head needs the title marked
type!:. CI’s Linux job checks out full history and sets
VX_REQUIRE_TAGS=1, so a missing tag fails there instead of passing.
- The config schema.
tests/contract-config-schema.test.tsdiscovers every level and field the validator accepts (by injecting an unknown key at each level of a valid seed config and reading the refusal’sallowed:list), probes each field with a fixed set of values, and compares every outcome, acceptance or exact refusal text, withtests/contract/config-schema.json. The same file holds each level’s field list to its exported interface insrc/config.tsthrough the type checker, so the TypeScript surface and the runtime surface cannot drift apart. The rules BETWEEN fields are found the same way: every task field andexecfield, at each seed value, is tried alone and in every pair on a plain task and on a group task, and each pair that disagrees with its halves (a conflict such aspersistentwithcache, or a requirement such asremote: 'only'oncache) is compared withtests/contract/config-schema-rules.json. A deliberate change regenerates the record withVX_UPDATE_CONTRACT=1(the command is in the test’s header) and names the change in the release notes. vx mcp‘s tools.packages/vx-mcp/tests/contract-tools.test.tscalls each toollistTools()names against a workspace with real runs and records its input schema and every key path of its answer inpackages/vx-mcp/tests/contract/tools.json. Key paths, not types: the doctor’s facts are null on one host and numbers on another; the cache tools’ values are held bytools.test.ts.- Turbo and Nx config support.
tests/turbo-nx-support.test.tslists every keyturbo.json,nx.jsonandproject.jsonaccept from vendored upstream schemas (tests/contract/turbo-nx/, versions in itsSOURCE.md) and requires one row per key intests/contract/turbo-nx-support.json: supported, mapped, not supported or not applicable, with a reason. A key a new upstream release adds fails until it is classified;docs/turbo-nx-support.mdis the table rendered. - The documented configs.
tests/config-corpus.unsafe.test.tsfinds everytsfence that callsdefineProject(ordefineWorkspace(indocs/(history and design aside) and the site’s hand-authored pages, and every config underexamples/, imports each and runs the loader’s validator on it. A fence whose first line names a path (// presets/ts-build.ts) is written there, so a config can import what its page showed. A schema change that refuses a documented config fails here; the fences that are not programs are named in the test, with why.vx-migrate’s writers are held by their own suites, which load each config they write. - The plugin API and the package’s exports.
tests/contract-package-api.test.tsreads, from the source, every declarationsrc/index.tsexports and every type those name, transitively (followed through imports and re-exports, so an internal type a plugin meets in a signature is held too): a type in full, a function to its signature, a class to its public members, comments dropped. With the runtime values of the exported constants (PLUGIN_HOOKS,TELEMETRY_SCHEMA_VERSION, …) it is compared withtests/contract/package-api.txt. A second row holdsVxPlugin’s members toPLUGIN_HOOKS. The façade snapshot inpackage-boundaries.unsafe.test.tsstill pins the export names; this pins their shapes. Regenerate the same way. - The plugin packages’ exports.
tests/contract-plugin-api.unsafe.test.tsreads each workspace package with anexportsentry (@vzn/vx-reapi,@vzn/vx-otel, …) the same way, from itssrc/index.ts, and compares it withtests/contract/plugin-api/<package>.txt; a package without a record, or a record without a package, fails. A type the package takes from@vzn/vxis held by core’s record, not again here. A record holds what a package exports; its README says what that is:tests/plugin-exports-documented.unsafe.test.tsrequires every name a record holds in the package’s README, andtests/plugin-options-documented.unsafe.test.tsevery field of its options types, sincerefuseUnknownOptionsaccepts each one. A name only tests use is kept off the entry; tests import the module. - Verbs, flags and variables.
tests/contract-cli-surface.test.tsrecords each core verb with the flags it accepts (verbFlags, whichcompletions.test.tsholds to the verb’s parser both ways) and everyVX_*variable the source reads, intests/contract/cli-surface.json. Each flag’s accepted values are recorded too, asked of its parser:*when it takes a value no parser could (a path, a ref), else the words the CLI’s source spells and fixed grammar probes (numbers, durations, sizes, cache modes) it takes. A dropped verb, flag, value or variable is a removed line there.tests/cli-verb-sections.test.tsholds each verb’sdocs/cli.mdsection to it: every flag the verb accepts is named there, its synopsis names none it refuses, and it states the exit codes the exit-code records hold for it. - The plugins’ variables. A CI workflow, not a config, sets
VX_REAPI_ENDPOINTorOTEL_EXPORTER_OTLP_ENDPOINT, so a rename drops the remote with no config change to review.tests/contract-plugin-env.unsafe.test.tsfinds every variable each plugin package’s source reads (a write or a comment is not a read), by name or through a helper (read('concurrency', 'TURBO_CONCURRENCY')), and holds the set totests/contract/plugin-env.txt, one<package> <NAME>line each; a lost line is a break. A name built from a template is recorded with*per placeholder (OTEL_EXPORTER_OTLP_*_PROTOCOL), which pins its shape, not the words the placeholder takes.tests/plugin-env-documented.unsafe.test.tsrequires each recorded read in its package’s README (a<SIGNAL>family, or a named member for a template). - Workspace discovery.
tests/contract-discovery.test.tsbuilds a fixture for each layout (pnpm-workspace.yaml,workspacesas an array or{ packages }, a negated and a recursive glob, both manifests at once, none), asks the real discovery for its projects and for the root found from inside a member, and loads every precedence of the four project and four workspace config file names. The answers aretests/contract/discovery.json. - The OTLP wire.
vx-otel’stests/contract-otlp.test.tsdrives the sink through one run whose every telemetry field is set (Required<…>), captures the traces, metrics and logs it posts, and compares their shape (each path, attribute key and value type, and the span kind, units, temporality and severity, never a time, id or measurement) withpackages/vx-otel/tests/contract/otlp.txt. - vx-migrate’s command lines.
vx-migrate’stests/contract-cli.test.tsspawnsvx-migrate,nx-execandnx-envfor--helpand for a usage error, and records the exit codes, each bin’s leading positionals and every flag its usage names, and the sources--fromtakes, inpackages/vx-migrate/tests/contract/cli.txt. A config written by vx-migrate 1.0 calls the helper bins, so their argv holds for all 1.x. - Exit codes.
tests/contract-exit-codes.test.tsdrives each documented outcome (a task failing, an unknown task, flag or verb, nothing affected, an unwritable--graph,cache prunewithout a policy,lock --check, …) through the real binary against one fixture workspace and compares each exit code withtests/contract/exit-codes.json.tests/contract-exit-codes-init.test.tsrecordsvx init’s: nopackage.json,--dry, files written, files already there with and without--force, intests/contract/exit-codes-init.json.tests/contract-exit-codes-states.test.tsrecords those that need a state or a signal first:lock --checkandrunwith no lock,lock --checkafter a config drifted,showof an unknown target,whyandlastwith and without a recorded run,completionsof an unknown shell, and a run interrupted by SIGINT, SIGTERM and SIGHUP (130, 143, 129), intests/contract/exit-codes-states.json. - The lock file.
tests/contract-lockfile.test.tslocks one fixture workspace and compares the file withtests/contract/lock.jsonbyte for byte, then writes the committed lock back and requiresvx lock --checkto pass andvx runto run from it (under an environment a live evaluation would read differently). A change to the format or toLOCKFILE_VERSIONfails here; a lock written by 1.x is valid for every later 1.x. - The npm tarballs.
tests/npm-pack.unsafe.test.tsemits every package the release publishes, runsnpm pack --dry-run --jsonon each, and compares the file list withtests/contract/pack/<package>.txt. A test or fixture file, a file over 512 KiB, or anexports/bintarget the tarball lacks fails, naming the file. The packed@vzn/vxand@vzn/vx-migrateare then installed with npm into a Turbo repo, andturbo()builds it, then hits. - The machine-readable run outputs.
--dry=jsonand the--summarizefile are wire objects built field by field (formatPlanJson,writeRunSummary), not a serialized type, so the type pin does not hold them.tests/contract-cli-wire.test.tsrenders each from a fixture that sets every field of its source type (Required<…>, so a new field must be given a value before the file compiles) and compares every key path and the JSON types at it withtests/contract/cli-wire.json. The samplescli.mdprints may show only keys the wire has, with its types.--graph’s DOT is recorded there whole, line by line, for the same fixture plan: its graph name, node ids, labels and edge direction (dependency → dependant) are what a renderer keys on (H-24).--profile’s Chrome trace is read by tools too (Perfetto, a script summingargs.cpuMs):tests/contract-profile-wire.test.tsrecords its key paths and types intests/contract/profile-wire.json, and holdscli.md’s sample to them. - Task-glob semantics.
tests/contract-task-globs.test.tsresolves 32 input lists and 8 output lists against one fixed project tree (dotfiles, a gitignored file,node_modules,[id],(group),{b}and a space in a name) through the real resolvers, and compares every selection, or the refusal it meets, withtests/contract/task-globs.json. The answer comes fromBun.Globas much as from vx, so this is the row a Bun upgrade that reads a pattern differently turns red, before a key silently covers different files.
Not the contract
Section titled “Not the contract”These may change in any release.
- The terminal output: status lines, colours, wording, layout. Scripts
should read
--summarizeor--dry=jsoninstead. --report’s markdown (a step summary people read): its columns and wording may change. A script reads--summarize.- What
vx initwrites: a starting point the user then edits, so a release may write a different one. The config it writes is held to the schema, which is the contract. - Anything not exported from
@vzn/vx: the modules undersrc/are internal. - The cache’s on-disk format and its keys. A
CACHE_VERSIONbump is allowed in a minor, because replaying stale bytes is worse than a cold run. It is never silent: it goes in the release notes, and the first run after the upgrade sayscache format changed: vA → vB(item 671). ASCHEMA_VERSIONreset sayscache index reset. - Performance, including the scheduler’s ordering. A regression is a bug, but it is not a break.
Deprecation
Section titled “Deprecation”Until 1.0 a breaking change ships with no alias, rename notice or migration shim (2026-10-14): an old config key fails the unknown field check, an old flag the unknown flag check, and an old variable is ignored. The release notes and the change’s record name each rename. From 1.0 a contract surface is removed in three steps: a minor deprecates it and warns once per run naming what replaces it, at least one more minor ships with the warning, and the next major removes it.
A fix for a stale hit or wrong bytes ships in a patch, even when it changes what a glob or a key means (item 667 did both). Its release notes say what changed.
Plugins
Section titled “Plugins”Every first-party plugin publishes at the core’s version, in one release
train, with peerDependencies['@vzn/vx'] = ^<version> (item 656).
Plugin authors code against the contract above and nothing else.
Runtime and platforms
Section titled “Runtime and platforms”- Bun. The floor is
engines.buninpackages/vx/package.json. Raising it is a minor, and the release notes announce it. The gate refuses a Bun below the floor (check.bun, item 575). Standalone binaries embed their runtime, so the floor only applies tobunxandbun addinstalls. - Tier 1: Linux and macOS. CI runs every commit on Linux x64
(
ubuntu-latest) and macOS arm64 (macos-latest); release binaries are built for x64 and arm64 on both, and the other two pairs are covered by those builds, not by a test run. - Windows: WSL only, since POSIX shell is the task API. There is no native build, and a Windows-only bug is out of scope unless it also reproduces under WSL.
Releasing 1.0
Section titled “Releasing 1.0”In order; each step says how it is checked.
- Milestone 3 is done and the soak is clean. 3.1–3.4 are merged
(
roadmap-1.0.md); the soak (3.5) is the owner’s call. - The contract records are current. The gate runs every
tests/contract-*.test.ts; each compares the code with its record undertests/contract/, so a green gate means no surface moved unrecorded. This page’s § What 1.0 freezes is held to the same records bycontract-versioning-doc.test.ts, which also checks that every record the contract table names exists. - Every change since the last release is in the notes.
git diff --stat <last-tag> -- packages/vx/tests/contract/lists the records that moved; each move is a line under “Breaking” (a removal or change) or “Added”. - Every plugin is on npm at the core’s version. The release’s
npm.ymlrun is green through all twelve packages. - The README says 1.0. Its status section drops “Pre-alpha” and points here.
- Tag
v1.0.0.release.ymlbuilds and signs the binaries andnpm.ymlpublishes.
When this takes effect
Section titled “When this takes effect”The owner tags 1.0 once roadmap milestone 3 is done and the soak (3.5) is clean. Until then, this page is the plan. When 1.0 is tagged, the README’s status section changes from “Pre-alpha” to point here.