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, 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 |
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) | cacheDir, cacheRetention, concurrency, plugins, timeout |
cacheRetention | maxSize, olderThan |
vx.config.ts:
| Level | Fields |
|---|---|
| (top) | tasks |
tasks.<name> | cache, dependsOn, description, exec |
tasks.<name>.exec | command, env, persistent, remote, retries, sandbox, timeout |
tasks.<name>.exec.env | define, passThrough, secret |
tasks.<name>.exec.persistent | readyWhen |
tasks.<name>.exec.sandbox | allow, deny, 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>.cache | inputs, outputs |
tasks.<name>.cache.inputs | env, files, runtime, tasks, workspaceFiles, workspaceRuntime |
tasks.<name>.cache.outputs | 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,project,graph,key,fingerprint,schedule,admit,executor,cache,telemetry,setup,commands,teardown. A plugin isdefinePlugin(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.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
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.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.- 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. - 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). - 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. - 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”A contract surface is removed in three steps:
- 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. - At least one more minor ships with the warning.
- 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.
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.