Stability & compatibility
August 4, 2026 · View on GitHub
gwm follows Semantic Versioning
(MAJOR.MINOR.PATCH). This page is the explicit, published compatibility
contract that backs the 1.0 line: it states which surfaces are covered by
that promise (a breaking change there forces a major bump) and which are
deliberately left free to change in a minor or patch.
The rule of thumb: anything a machine parses is covered; anything a human reads on screen is not.
Covered by SemVer (breaking change → major)
These surfaces are part of the public contract. A backward-incompatible change (renaming or removing something, or changing its type or documented meaning) is a conscious major version decision.
- CLI surface: the subcommands, their flags, and their documented argument shapes. Adding a subcommand or an optional flag is additive (minor); renaming or removing one is breaking.
- Exit codes: the deterministic
0/1/2contract documented per command (e.g.gwm doctor's severity-derived code). Scripts and CI jobs key off these, so a code's meaning is frozen under this promise. --format=jsonoutput schemas: the JSON payloads ofgwm list,gwm doctor,gwm path, andgwm status --json, documented underdocs/schema/and pinned bytests/contract_tests.rs.- Daemon JSON-RPC 2.0 protocol: the
list/doctor/path/subscribemethods, theworktrees.changednotification, and the standard JSON-RPC error codes. A daemonlistresult is byte-identical togwm list --format=json, so the two share oneSCHEMA_VERSION. .gwm.tomlschema: the top-level key set (forge,worktree,bootstrap,hooks,doctor,tui,theme,git_tui,review,labels,milestones,branch_types,aliases,gitmoji,issue_template,pr_template,exec,clean). A renamed or removed stable key is breaking; adding an optional one (asforgewas in #419) is not.
Frozen by test vs. covered by promise
Three of these surfaces are mechanically frozen, and a rename fails CI before
it can ship: the JSON schemas, the daemon method/notification names,
and the .gwm.toml section set, all pinned by tests/contract_tests.rs
against the single source of truth in
src/contract.rs.
The CLI subcommands/flags and the exit-code meanings are not freeze-
tested end-to-end (only doctor's exit_code field rides the JSON schema);
they are covered by this written SemVer promise and reviewed per PR. Treat
them as just as binding: the absence of a guard test is not a licence to
break them silently.
The machine-contract detail lives elsewhere
The per-field tiers (which exact fields are stable vs experimental),
the drift-detection mechanism (SCHEMA_VERSION on the daemon notification,
gwm --version for one-shot CLI consumers), and the additionalProperties
rules are documented in full in
docs/schema/README.md.
Notably, a few fields are experimental and may change without a major bump,
among them the workspace-only repo field on a list row and the top-level
repo on status --json. When in doubt about a specific field, that tiers
table is authoritative.
NOT covered by SemVer (may change in minor/patch)
These are free to change without a major bump. Do not build automation on top of them.
- TUI layout & colours: pane arrangement, widget placement, the theme / colour scheme, and any visual detail of the ratatui interface. Scripting against the rendered TUI is unsupported.
- Human-readable strings: log lines, status-bar messages, help blurbs,
the human (non-
--format=json) output of any command. Parse the JSON surface instead; the prose is allowed to be reworded at any time. - Internal Rust API: the
gwm-clicrate publishes a[lib]target (namedgwm) alongside the binary, but only as a byproduct: the binary and thetests/integration suite share one module tree, and Rust integration tests can reach it only through apublib. That surface (~460pubitems across ~33 modules) is an internal test seam, not a public API: it is# and carries no SemVer guarantee. Do notcargo add gwm-clito depend ongwm::*; those items may change in any release. (Decision recorded for #342: the library API is disclaimed, not gated withcargo-semver-checks, because owning ~460 items as a frozen contract would trip a major bump on every routine internal refactor, which is the wrong trade-off for a seam that exists to be tested, not consumed.)
MSRV policy
The Minimum Supported Rust Version is declared as rust-version in
Cargo.toml
(currently 1.95), the floor the crate is expected to compile against.
Two CI jobs hold that floor. The clippy job runs on the stable toolchain with
-D warnings, and clippy::incompatible_msrv is warn-by-default, so an
accidental use of a std API newer than the declared floor fails CI. The
msrv job installs the declared toolchain itself (read out of Cargo.toml,
never hardcoded) and runs cargo check --all-targets --locked, which covers
what clippy cannot: a newer language / edition feature, or a dependency
whose own floor is higher than ours. --locked matters twice over. Cargo's
rust-version gate is evaluated at resolve time against the committed
lockfile, so a dependency that declares a higher floor fails before
anything is built; and the compile that follows is the only thing that catches
a dependency which declares nothing at all.
That last case is not hypothetical, and it is why this section no longer
recommends reading the floor out of cargo metadata. Until
#491 the declared floor read
1.86 with nothing enforcing it. Metadata put the real floor at 1.88 (the
ratatui 0.30 stack, time 0.3.47); compiling put it at 1.95, because
libsqlite3-sys 0.38.1 (a normal dependency, via rusqlite with bundled)
declares no rust-version and its build script uses cfg_select!, stable
since 1.95.0. A crate that declares nothing is invisible to every
metadata-based check, so the floor is whatever a build says it is.
In practice an MSRV bump rides a minor release, not a major one. It has
historically been driven by a dependency raising its own floor (the 1.86 bump
came in with tui-term / portable-pty when the PTY overlay landed, the
1.95 bump with rusqlite's bundled libsqlite3-sys), and is treated as a
routine toolchain update rather than a breaking change to the public contract.
Bumps are called out in the changelog so packagers are not surprised.
Deprecation process
When a covered surface has to change in a backward-incompatible way:
- Announce: document the deprecation in the changelog under the release that introduces it, and (where the surface supports it) emit a runtime warning pointing at the replacement.
- Keep the old path working through the rest of the current major line: a deprecation is a heads-up, not an immediate removal.
- Remove only on a major bump, with the removal listed in that release's notes alongside the migration path.
Additive changes (a new subcommand, an optional flag, a new optional JSON field, a new daemon method) are not deprecations: they ship in a minor release and require no warning, because existing consumers keep working unchanged (consumers MUST ignore unknown JSON fields).
See also
docs/schema/README.mdfor the per-field stable/experimental tiers and the drift-detection contract.src/contract.rsfor the single source of truth onSCHEMA_VERSIONand the frozen method/section sets.- Contributing → Releases and
CONTRIBUTING.mdfor the SemVer release process and tagging workflow.