Skip to content
vxvx
GitHubBlueskydev.toRSS

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).

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

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.

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.

  • Hooks, in pipeline order: config, discover, project, graph, key, fingerprint, measures, schedule, admit, executor, cache, telemetry, output, setup, commands, teardown. A plugin is definePlugin(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.end and run.end; each record carries the version as v.
  • Types. Every type @vzn/vx exports is frozen with every type it names, whether that one is exported or not. VxPlugin and its hook contexts, CacheLayer, RemoteCacheLayer, TaskExecutor, ExecuteRequest / ExecuteResult, TelemetrySink and TelemetryRecord are the ones a plugin implements or receives. The full text is tests/contract/package-api.txt.

The verbs, flags, exit codes and machine-readable outputs in docs/cli.md.

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.ts discovers every level and field the validator accepts (by injecting an unknown key at each level of a valid seed config and reading the refusal’s allowed: list), probes each field with a fixed set of values, and compares every outcome, acceptance or exact refusal text, with tests/contract/config-schema.json. The same file holds each level’s field list to its exported interface in src/config.ts through 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 and exec field, 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 as persistent with cache, or a requirement such as remote: 'only' on cache) is compared with tests/contract/config-schema-rules.json. A deliberate change regenerates the record with VX_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.ts calls each tool listTools() names against a workspace with real runs and records its input schema and every key path of its answer in packages/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 by tools.test.ts.
  • Turbo and Nx config support. tests/turbo-nx-support.test.ts lists every key turbo.json, nx.json and project.json accept from vendored upstream schemas (tests/contract/turbo-nx/, versions in its SOURCE.md) and requires one row per key in tests/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.md is the table rendered.
  • The documented configs. tests/config-corpus.unsafe.test.ts finds every ts fence that calls defineProject( or defineWorkspace( in docs/ (history and design aside) and the site’s hand-authored pages, and every config under examples/, 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.ts reads, from the source, every declaration src/index.ts exports 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 with tests/contract/package-api.txt. A second row holds VxPlugin’s members to PLUGIN_HOOKS. The façade snapshot in package-boundaries.unsafe.test.ts still pins the export names; this pins their shapes. Regenerate the same way.
  • The plugin packages’ exports. tests/contract-plugin-api.unsafe.test.ts reads each workspace package with an exports entry (@vzn/vx-reapi, @vzn/vx-otel, …) the same way, from its src/index.ts, and compares it with tests/contract/plugin-api/<package>.txt; a package without a record, or a record without a package, fails. A type the package takes from @vzn/vx is 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.ts requires every name a record holds in the package’s README, and tests/plugin-options-documented.unsafe.test.ts every field of its options types, since refuseUnknownOptions accepts 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.ts records each core verb with the flags it accepts (verbFlags, which completions.test.ts holds to the verb’s parser both ways) and every VX_* variable the source reads, in tests/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.ts holds each verb’s docs/cli.md section 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_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT, so a rename drops the remote with no config change to review. tests/contract-plugin-env.unsafe.test.ts finds 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 to tests/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.ts requires each recorded read in its package’s README (a <SIGNAL> family, or a named member for a template).
  • Workspace discovery. tests/contract-discovery.test.ts builds a fixture for each layout (pnpm-workspace.yaml, workspaces as 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 are tests/contract/discovery.json.
  • The OTLP wire. vx-otel’s tests/contract-otlp.test.ts drives 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) with packages/vx-otel/tests/contract/otlp.txt.
  • vx-migrate’s command lines. vx-migrate’s tests/contract-cli.test.ts spawns vx-migrate, nx-exec and nx-env for --help and for a usage error, and records the exit codes, each bin’s leading positionals and every flag its usage names, and the sources --from takes, in packages/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.ts drives each documented outcome (a task failing, an unknown task, flag or verb, nothing affected, an unwritable --graph, cache prune without a policy, lock --check, …) through the real binary against one fixture workspace and compares each exit code with tests/contract/exit-codes.json. tests/contract-exit-codes-init.test.ts records vx init’s: no package.json, --dry, files written, files already there with and without --force, in tests/contract/exit-codes-init.json. tests/contract-exit-codes-states.test.ts records those that need a state or a signal first: lock --check and run with no lock, lock --check after a config drifted, show of an unknown target, why and last with and without a recorded run, completions of an unknown shell, and a run interrupted by SIGINT, SIGTERM and SIGHUP (130, 143, 129), in tests/contract/exit-codes-states.json.
  • The lock file. tests/contract-lockfile.test.ts locks one fixture workspace and compares the file with tests/contract/lock.json byte for byte, then writes the committed lock back and requires vx lock --check to pass and vx run to run from it (under an environment a live evaluation would read differently). A change to the format or to LOCKFILE_VERSION fails here; a lock written by 1.x is valid for every later 1.x.
  • The npm tarballs. tests/npm-pack.unsafe.test.ts emits every package the release publishes, runs npm pack --dry-run --json on each, and compares the file list with tests/contract/pack/<package>.txt. A test or fixture file, a file over 512 KiB, or an exports / bin target the tarball lacks fails, naming the file. The packed @vzn/vx and @vzn/vx-migrate are then installed with npm into a Turbo repo, and turbo() builds it, then hits.
  • The machine-readable run outputs. --dry=json and the --summarize file 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.ts renders 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 with tests/contract/cli-wire.json. The samples cli.md prints 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 summing args.cpuMs): tests/contract-profile-wire.test.ts records its key paths and types in tests/contract/profile-wire.json, and holds cli.md’s sample to them.
  • Task-glob semantics. tests/contract-task-globs.test.ts resolves 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, with tests/contract/task-globs.json. The answer comes from Bun.Glob as 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.

These may change in any release.

  • The terminal output: status lines, colours, wording, layout. Scripts should read --summarize or --dry=json instead.
  • --report’s markdown (a step summary people read): its columns and wording may change. A script reads --summarize.
  • What vx init writes: 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 under src/ are internal.
  • The cache’s on-disk format and its keys. A CACHE_VERSION bump 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 says cache format changed: vA → vB (item 671). A SCHEMA_VERSION reset says cache index reset.
  • Performance, including the scheduler’s ordering. A regression is a bug, but it is not a break.

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.

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.

  • Bun. The floor is engines.bun in packages/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 to bunx and bun add installs.
  • 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.

In order; each step says how it is checked.

  1. 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.
  2. The contract records are current. The gate runs every tests/contract-*.test.ts; each compares the code with its record under tests/contract/, so a green gate means no surface moved unrecorded. This page’s § What 1.0 freezes is held to the same records by contract-versioning-doc.test.ts, which also checks that every record the contract table names exists.
  3. 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”.
  4. Every plugin is on npm at the core’s version. The release’s npm.yml run is green through all twelve packages.
  5. The README says 1.0. Its status section drops “Pre-alpha” and points here.
  6. Tag v1.0.0. release.yml builds and signs the binaries and npm.yml publishes.

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.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.