Compatibility policy
What commission promises not to break, per surface, and what “break” means for each
one. This document is the contract behind the version number; the entries that
exercise it are in ../CHANGELOG.md CHANGELOG.md, and the process that
enforces it at release time is
release-checklist.md.
The version number
Commission ships one version string, single-sourced in src/version.ts and derived
from package.json. Every surface that reports a version reports that one:
| Surface | How you read it |
|---|---|
| CLI | commission --version |
| MCP | serverInfo.version in the initialize result |
| HTTP | GET /v1/version → { commission, schema, config } |
| Package | the version field in package.json |
tests/version.test.ts asserts all four agree, and fails if any source file
outside src/version.ts reintroduces a hardcoded version literal.
Commission follows semantic versioning across the CLI surface and the HTTP and MCP contracts taken together. There is one number, not one per surface: a breaking change to any of them is a major bump. This is deliberately conservative. Splitting the version per surface would let a “minor” release break the HTTP API for anyone who reads only the CLI’s number, and commission’s whole argument is that one truth beats several that drift.
Two things carry their own independent version, because they describe stored
state rather than a calling convention, and both are reported by
GET /v1/version:
schema— the database schema level (PRAGMA user_version).config— the.commission.jsonformat version.
Pre-1.0
Until 1.0.0 there is no compatibility promise. 0.x is the development line;
surfaces move. The policy below describes what 1.0 onward guarantees, and
Commission is being built to it now so that RC1 can adopt it without a redesign.
For the release candidates: 1.0.0-rc.N speaks the 1.x contracts and is held
to them. Breakage between release candidates is possible but must be listed
under ### Breaking in the changelog with its migration, same as any major.
Per-surface definitions of “breaking”
CLI
The CLI is a machine interface first. Agents parse its output and branch on its exit codes, so cosmetic changes are not automatically safe.
Breaking:
- Removing or renaming a command, subcommand, or flag.
- Removing a flag’s short form, or repointing it at a different flag.
- Changing a flag from optional to required, or adding a required flag or positional argument to an existing command.
- Narrowing an accepted value: fewer enum members, a stricter format, a lower limit.
- Changing a default in a way that changes what an existing invocation does.
- Changing an exit code for an existing outcome (see below).
- Making a command that succeeded refuse, or exit non-zero where it exited 0.
Not breaking:
- Adding a command, subcommand, or optional flag.
- Adding a new accepted value to an input that already accepts several.
- Changing human-readable prose: help text, descriptions, refusal wording,
progress output. Refusal messages are documentation, not contract — what is
guaranteed is that a refusal exits 2 and, under
--json, carriesmissingandhint. - Changing the layout, ordering, colour, or wrapping of non-
--jsonoutput. If you parse commission’s human output, you are on your own — use--json.
JSON output shapes
Everything printed under --json, and every HTTP and MCP response body.
Breaking:
- Removing a field, or renaming one.
- Changing a field’s type, including
string→number, scalar → array, and making a previously always-present field nullable or optional. - Changing the meaning or units of an existing field while keeping its name and type. This is the worst kind, because nothing fails loudly.
- Removing a member from an enum-valued field (
status,error, evidencekind), or repurposing an existing member. - Changing a top-level response from an object to an array, or vice versa.
Not breaking:
- Adding a field. Consumers must ignore unknown fields. This is the one obligation the policy places on clients, and everything else depends on it.
- Adding a new enum member to a field that is documented as extensible. New
statusvalues are not covered by this — the status set is closed and adding one is breaking. - Changing key order, or whitespace and indentation.
Exit codes
Commission’s exit codes are load-bearing: they are how an agent decides whether to retry, fix, or stop. The mapping is fixed for the life of 1.x.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error — something went wrong that is not the caller’s fault to fix |
| 2 | Refusal — the operation is understood and declined; the message says what is missing and how to fix it |
| 3 | Not found — the named project, task, or binding does not exist |
Breaking: changing any of these meanings, moving an existing outcome from one code to another, or introducing a new code in the 0–3 range.
Not breaking: adding a code above 3 for an outcome that has no existing code. Note that a new refusal on an operation that used to succeed is a CLI break even though the code itself is unchanged — the caller’s script still stops working.
Database schema
The database is the user’s data. The promises here are stronger than elsewhere, and none of them are optional.
Commission migrates forward automatically. src/db/ddl.ts holds MIGRATIONS, an
ordered, append-only array of DDL statements; src/db/client.ts compares
PRAGMA user_version against it on every open and applies the missing ones,
each inside a transaction that bumps user_version with it. Migrations ship
inside the binary — there is no migration folder to deploy, and no separate
migrate command to forget.
The promises:
- Forward migration is automatic and transactional. Any commission command opening an older database upgrades it. Each step commits atomically with its version bump, so an interrupted upgrade leaves a consistent database at a known version, never a half-migrated one.
- Commission never silently downgrades. A binary that meets a database newer than the migration chain it carries does not touch it and does not guess at the schema. It refuses with the upgrade instruction. Downgrading a database is not a supported operation at all — restore a backup instead.
- Migrations are append-only. A released migration is never edited or
reordered. Fixing a shipped migration means adding another one; editing it
in place would give two users the same
user_versionwith different schemas, which is unrecoverable. - No data-destroying migration without an explicit gate. A migration that cannot preserve existing rows does not run unattended.
- The database is copied before it is migrated. The first open that has
migrations to apply copies the file — consistently, via
VACUUM INTO— and prints the copy’s path with the exact command that restores it. Migrations are not reversible (v5 drops columns), so this copy is the only way back. If the copy cannot be written, Commission refuses to migrate rather than proceeding without one. - Every migration is exercised against real data before it ships.
tests/migrations.test.tsbuilds a v1 database with rows in every table, walks it through the whole chain one migration at a time, and asserts after each step that row counts, referential integrity, and the meaning of any moved data all survived. A migration added without an entry there fails the suite.
The schema level is MIGRATIONS.length and nothing else — there is no
second constant declaring it, because a second copy of that number can only
drift from the array it describes.
Breaking: dropping a table or column that held user data; changing a column’s type or meaning in place; any change requiring manual intervention before an upgrade; tightening a constraint such that existing valid rows become invalid.
Not breaking: adding a table, adding a nullable column or one with a default, adding an index, or widening a constraint. A schema version bump on its own is routine and is not a breaking change — it is the expected outcome of almost every feature.
commission doctor reports the current schema level against the expected one, and
commission migrate --check reports what is pending without touching the database,
exiting 2 when a database is behind the binary or ahead of it. The operational
side — what the backup is called, how to restore it, what to do when commission
refuses to open a database — is guides/upgrades.md.
.commission.json config version
See reference/config.md for the format itself.
Version 1 is supported for the life of 1.x. A .commission.json written today
against "version": 1 will be read, unchanged and without warnings, by every
1.x commission. If a version 2 becomes necessary, 1.x binaries continue to accept
version 1 files; commission config migrate performs the upgrade, non-destructively
and idempotently, and prints the diff before writing anything.
Two rules make this safe in both directions:
- A config declaring a version the binary does not speak is refused, with the upgrade command — never partially applied. A newer config meeting an older commission fails loudly instead of silently ignoring the keys it cannot parse.
- Unknown keys are refused, not ignored. This is why adding a key to the schema is a compatible change but the file cannot be “forward compatible” by accident: a typo that silently does nothing is precisely the failure the strictness exists to prevent.
Breaking: removing a key, renaming one without accepting the old name, changing a key’s type or default in a way that changes behaviour, or requiring a key that was previously optional.
Not breaking: adding an optional key, adding a value to an extensible enum, or adding a default for something previously unset.
A file with no version at all is the pre-versioned shape and is read as such
— project and integrations only — with a notice pointing at
commission config migrate. That compatibility path predates versioning and is
scheduled for removal; the deprecation policy is relay-32.
What is explicitly not covered
- Human-readable output. Layout, wording, and colour change freely. Parse
--json. - The web view. It is a rendered view over the same API, with no stability promise of its own.
- Anything importing commission as a TypeScript library.
src/coreis the internal core the CLI, HTTP, and MCP surfaces consume. It is not a published API and changes without notice. If you need programmatic access, use the HTTP API. - Adapter behaviour against third-party systems. Jira and GitHub change their own APIs; adapters follow. Commission’s promise is that the interop layer is opt-in and inert until configured, not that a given Jira instance keeps working forever.
- The event log’s payload internals. Event
kindvalues and the envelope are contract; the shape insidepayloadvaries per kind and is documented per kind, not guaranteed uniformly.
Deprecation
Nothing is removed without first being deprecated in a release:
- The deprecation ships in a minor release, listed under
### Deprecatedin the changelog, naming the replacement. - Using the deprecated thing warns on stderr — once per invocation, never on
stdout, and never in
--jsonoutput, so warnings cannot corrupt a parsed response. - The removal happens no earlier than the next major.
A deprecation that has no working replacement on the day it ships is not a deprecation, it is a break with a longer fuse. Do not ship one.
This page is docs/compatibility.md in the Commission
repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.