Skip to content
GitHubRSS

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.

SurfaceDefined by
The config schema: every field vx.config.ts and vx.workspace.ts accept, and what each meanssrc/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 exportssrc/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, vx mcp’s tools)docs/cli.md; --dry=json, --summarize and --graph recorded in tests/contract/cli-wire.json; vx mcp’s tools in packages/vx-mcp/tests/contract/tools.json
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:

LevelFields
(top)cacheDir, cacheRetention, concurrency, plugins, timeout
cacheRetentionmaxSize, olderThan

vx.config.ts:

LevelFields
(top)tasks
tasks.<name>cache, dependsOn, description, exec
tasks.<name>.execcommand, env, persistent, remote, retries, sandbox, timeout
tasks.<name>.exec.envdefine, passThrough, secret
tasks.<name>.exec.persistentreadyWhen
tasks.<name>.exec.sandboxallow, deny, ignore, weakerNetworkIsolation, weakerWhenNested
tasks.<name>.exec.sandbox.allowgitConfig, localBinding, machLookup, network, pty, read, systemInfo, unixSockets, write
tasks.<name>.exec.sandbox.denynetwork
tasks.<name>.exec.sandbox.ignorenetwork, read, systemInfo, write
tasks.<name>.cacheinputs, outputs
tasks.<name>.cache.inputsenv, files, runtime, tasks, workspaceFiles, workspaceRuntime
tasks.<name>.cache.outputsfiles, 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, project, graph, key, fingerprint, schedule, admit, executor, cache, telemetry, setup, commands, teardown. A plugin is definePlugin(import.meta, hooks), and its name is its package’s.
  • Telemetry records, schema version 3. The kinds are run.start, task.start, task.log, 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 tests/contract/package-api.txt and each tests/contract/plugin-api/*.txt against the same file at the last v* tag: a declaration or a line of one gone is a break (scripts/api-break.ts), and it 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). CI’s Linux job checks out full history and sets VX_REQUIRE_TAGS=1, so a missing tag fails there instead of passing. On a PR, a second row diffs the same records between the PR’s base and head: a break there fails unless the PR’s title is marked type!:, so the reviewer sees it before the merge.

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

A contract surface is removed in three steps:

  1. A minor deprecates it. The surface keeps working and warns once per run, naming what replaces it. A config field’s warning comes from config-schema.ts; a flag’s comes from the CLI’s parser.
  2. At least one more minor ships with the warning.
  3. The next major removes it. When a removed field is used, the refusal names the replacement and the version that removed it.

The removal is held in code. REMOVED_FIELDS in config-schema.ts maps each level’s field set to the fields it once took, each with the version that removed it and what to use instead. The unknown-field check consults it before it calls a key unknown, so a removed field gets a refusal of its own:

vx.config.ts: tasks.build.exec has field "resources", which vx 0.0.19 removed — use
`@vzn/vx-schedule-history`, which learns each task's reservation from its run history
(declare one by hand with its `reservations: { 'pkg#task': { cpus, memory } }`)

An entry stays for good, since a config written against an old release meets it whenever it upgrades. tests/contract-removed-fields.test.ts holds that example word for word and every entry to the same shape (out of its level’s accepted fields, a semver removedIn, a replacement).

No field is deprecated today, so step 1’s warning has no code yet. The first deprecation builds it beside REMOVED_FIELDS, with a row that it prints once per run.

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.