Roadmap to 1.0
This page is the source of record. It previously mirrored a private claude.ai Artifact, which meant every landed item had to be written twice and the copy nobody could see was the authority. Dropped on 2026-09-17: a roadmap that lives beside the code it describes is the one that gets updated when the code moves.
Assembled originally from the working tree at 9bf46e5, CHANGELOG.md history, and the 2026-08-18 architecture review.
What 1.0 has to mean here
Not "more features." Three promises the pre-1.0 releases can't currently keep — every item below exists to close one of them:
- You can install it without cloning a repository, and without installing Python by hand to reach the default (native) path.
- You can prove it worked — which model actually served the turn, what it cost, and whether the cost story the README sells is true for you.
- Your config survives — a
sonata.tomlwritten on launch day still loads at 1.9, with migration that runs itself.
Fourteen items, four movements
Status legend: ✅ shipped · 🔸 in progress · ⏳ not started.
I. Measurement — the promise sonata sells and does not keep
| # | Item | Priority | Size | Status |
|---|---|---|---|---|
| 01 | sonata usage — a token and cost ledger | P0 | M | ✅ shipped in 0.3.0 |
| 02 | Route ledger, and a sonata status that answers "who actually served this?" | P0 | S | ✅ shipped in 0.3.0 |
| 03 | sonata runs — surface the list that already exists | P1 | XS | ✅ shipped in 0.3.0 |
| 04 | Budget guardrails | P1 | S | ✅ shipped in 0.6.0 — [budget] daily_usd (src/budget.ts), enforced at the top of routeRequest above both the tier and direct branches, since a cap on one of two paths is not a cap. It sums the ledger's priced rows for the current UTC day and refuses at or past the cap with a 429 naming the cap, the spend and the file to edit; both halves are re-read per request, so raising the cap frees the router without a restart. The two things it cannot see are stated in the refusal itself rather than papered over — priced volume only (unpriced is never folded in as zero, so real spend can exceed the cap) and, until 0.13.0, the native path only (sonata dispatch never transits the router; since 0.13.0 a dispatch run's tokens are read from the harness's own store when it finishes and count toward the same cap, and sonata dispatch refuses to launch past it). The refusal is deliberately not ledgered: a row records a request that was forwarded, and putting avoided spend into the store that defines spend is how the number stops meaning what it says. What the deferral was waiting for — forecasting, per-role splits, auto-tuning — still has no usage data behind it and is still not built |
II. Reliability — the two things that already bit, on record
| # | Item | Priority | Size | Status |
|---|---|---|---|---|
| 05 | A daemon lifecycle you can trust (instance-id handshake, actionable takeover) | P0 | S–M | ✅ shipped in 0.3.0 |
| 06 | sonata init hardening — the front door is the largest, least-tested file | P0 | L | ✅ shipped in 0.4.0 — init.ts 1502 → 184 lines, decomposed into src/init/ (discover / validate / plan / apply / interactive-state / scripted-state / toml) with end-to-end tests for the pipeline. The interactive TUI has now been hand-driven through /cmux (2026-08-31), which found two functional bugs no test had: a confirm summary promising 8 agent files where sync writes 4, and a gateway reported keyless one line above ✓ stored the key. The Ink stdin-teardown class remains knowingly uncovered |
| 07 | Close the last false-success gap (worktree delta captured at launch and exit) | P1 | S | ✅ shipped in 0.6.0 — src/worktree.ts hashes git rev-parse HEAD + git status --porcelain + a git hash-object blob hash for every not-committed-clean path into meta.worktreeAtLaunch; the launch wrapper writes the same capture before the exit sentinel and tail compares against that rather than sampling live. It annotates rather than degrades ([no worktree change: …] prefixed to the trusted report branch only): degraded means sonata cannot mechanically trust the result, and a run that correctly concluded no change was needed is a legitimate outcome — degrading it would trade this check's false successes for false alarms instead of removing either. Inert outside git: no repo, no git, any failure → undefined, read as unknown, never as "unchanged", because a check for silent failures must not invent one. Read-only roles skip it. Four traps the design avoids: the launch sample is taken after createRun, so sonata's own .sonata/ scaffolding is in both samples and a repo that does not ignore it still compares usefully; HEAD is in the hash, so a run whose only trace is a commit — leaving a clean tree — still registers; the closing sample is captured by the wrapper rather than by tail, which may not look for hours, by which time the tree belongs to whoever is using the repository; and the capture hashes file content, because status alone reports the identical modified-but-unstaged line however many times that path is rewritten |
| 13 | A repeating 400 must cool a candidate down — a permanently-broken model currently absorbs its whole tier (spec) | P0 | S | ✅ fixed in 0.4.1 — a fingerprinted 400 repeated 3× consecutively now cools the candidate the way a 5xx does, while every other 400 is still returned to the caller with its body intact. The signature allow-list carries only measured strings, and now carries two: thought_signature, and — added 2026-09-03, shipped in 0.6.0 — System messages are not allowed, the Codex backend's refusal, captured live as litellm.BadRequestError: ChatgptException … Received Model Group=gpt-5.6-terra on a request the router had already flattened. That reverses a judgement made a session earlier, when the same addition was declined on the grounds its 2026-08-30 evidence predated the 2026-08-28 flattenSystemBlocks + supports_system_message: false fix; fresh evidence after that fix, on a verified-current dist/, says the pair is not sufficient and the remaining hole is unidentified. Cooling the candidate turns that into a survivable fallback ending in a 529 naming sonata dispatch, instead of a bare 400 that kills the subagent |
| 14 | A killed subagent pins routing on for good, and sonata route off does not clear it (spec) | P0 | S | ✅ fixed in 0.4.1 — route off now clears route-subagents.json too, so the documented recovery recovers. Two defects that produced the pin from ordinary use are fixed with it: the writer and cleaner defaulted to different scopes, and guarded one file with two different locks. The deadlock review found in the proposed fix is covered by a test that reproduces it in 2036 ms against withSessionLock's 2000 ms deadline |
III. Distribution — can a stranger actually run this?
| # | Item | Priority | Size | Status |
|---|---|---|---|---|
| 08 | Publish to npm, and automate the release | P0 | S | ✅ shipped in 0.4.0 — npm run release -- <version> promotes [Unreleased], bumps the manifest and lock, and tags; release.yml publishes on the pushed tag via npm trusted publishing (OIDC) with provenance, storing no token. Correction to what this entry previously said: the bootstrap does not require publishing by hand first. npm trust github <pkg> --file release.yml --repo <owner/repo> --allow-publish (npm 11.10+) configures a trusted publisher for a package that does not exist yet — only the npmjs.com web UI has that limitation. 0.4.0 was in fact published manually before this was known. Nothing remains here — the pipeline is proven. 0.4.1 through 0.6.0 all published from a pushed tag with no npm token in the repository; 0.6.0 (2026-09-03) was watched end to end and every step of release.yml passed, including the shared verify action. One detail worth not "fixing": the publish log shows a signed provenance statement even though publishConfig deliberately omits provenance — trusted publishing turns it on by itself in CI, so the manifest can stay quiet and keep a local publish possible, which is exactly the escape hatch you want when the pipeline is what's broken |
| 09 | Own the LiteLLM dependency — manage it, don't replace it (pinned managed venv, doctor installs/repairs) | P0 | M | ✅ shipped in 0.5.0 — and the answer turned out to be better than managing it. A gateway declares its provider; an Anthropic-native one is reached directly by sonata's own router with no LiteLLM in the path, so a config whose gateways all speak Anthropic needs no LiteLLM, no venv and no Python at all. When one is needed, sonata installs its own venv pinned to 1.98.0 (sonata litellm install; init offers it, doctor reports six distinct states, serve refuses rather than installing because it runs headless from a SessionStart hook). litellmRequired counts every routable model, not just tier members — a bare model key never calls resolveTier |
IV. Contract — the part where the number stops being decorative
| # | Item | Priority | Size | Status |
|---|---|---|---|---|
| 10 | Config schema v1, with migration that runs on load | P0 | S–M | ✅ shipped in 0.6.0 — schema_version stamped by init, read by parseConfig, which walks the file forward through an ordered chain (src/migrations.ts) before field-level validation and refuses a stamp newer than it understands. Migration is in-memory: an old file keeps loading, and no read-only command rewrites it. The chain ships empty on purpose — v1 names the shape parseConfig already accepts, so what ships is the mechanism, the stamp and the refusal, with composition proven against a synthetic chain |
| 11 | sonata doctor --json | P1 | S | ✅ shipped in 0.5.1 — cmdDoctor already returned a structured { ok, checks }; the flag just exposes it, so it round-trips exactly what the text output was rendering |
| 12 | One report-contract manifest | P2 | M | ✅ shipped in 0.6.0 — src/report-contract.ts owns report.md, referenced by the prompt, the watcher, the store, sonata runs and tail, where five bare copies of the string previously had nothing forcing them to agree. It owns the file, not the verdict: decide() in tail.ts keeps the degraded / report-impossible rules, because that predicate is a state machine over exit code, timeout and pane output whose every clause was written against an observed failure, and re-expressing it as a one-line helper would have to drop clauses to fit — the simplified version calls a crashed read-only run "report impossible", which is exactly the silent success those clauses exist to catch. fallbackReportFile stays per-adapter for the mirror-image reason: which file a harness writes is harness knowledge |
Three releases, in dependency order
Measurement first because it backs the claim the product is sold on. Distribution second because it is cheap and gates everyone else's ability to try any of it. Freeze last.
0.3 — "Prove it works, and stop lying about the daemon." The cost ledger, the route ledger, the run list, and an instance-identity handshake in
serve. After this release, every claim sonata makes about itself is checkable by its user. Items 01 · 02 · 03 · 05 — all shipped (2026-08-28).0.4 — "Let strangers in." npm publish with an automated release, a managed LiteLLM venv, and a decomposed, end-to-end-tested
init. This is the release that converts a repository into a product — and the one that will generate the bug reports 1.0 needs to have already answered. Two P0 reliability defects found in live use (a broken model absorbing its tier; a killed subagent pinning routing on for good) belong here too — shipping the install path while those defects are open would hand new users the very things that bit the existing ones. Items 06 · 08 · 09 · 13 · 14.0.4.0 shipped 2026-08-31 carrying 06 and 08 only, and 13 · 14 followed in 0.4.1. With 09 in 0.5.0, every item in this milestone is done and released — the version number describes the code it ships, not the completeness of the milestone, and the milestone is what closes here. Item 09's answer turned out better than the one the milestone asked for: rather than only managing the Python dependency, sonata now avoids needing it at all whenever a gateway speaks Anthropic natively.
1.0 — "Freeze the contract." Schema version and load-time migration, structured doctor output, budget caps, the false-success check, and one report manifest. Ship 1.0 only after 0.4 has been in strangers' hands long enough to know what you would regret freezing. Items 04 · 07 · 10 · 11 · 12.
All five are now released — 11 in 0.5.1, and 04 · 07 · 10 · 12 in 0.6.0 on 2026-09-03 — so every one of the fourteen items is shipped, and 1.0 is deliberately still not tagged. The milestone's own gate is not an item list: it is "after 0.4 has been in strangers' hands long enough to know what you would regret freezing." 0.4 published to npm on 2026-08-31, 0.5 on 2026-09-02, 0.6.0 on 2026-09-03; the elapsed time is days, and no external bug report has arrived to be answered. What the code can do is finished. What the gate asks for is exposure, and tagging 1.0 to celebrate an empty checklist would freeze a contract nobody outside this repository has tried to use — which is the one mistake this milestone was written to prevent.
The checklist can no longer move this gate, since there is nothing left on it to tick. Only two things can: an external bug report arriving and being answered, or a decision that the exposure so far is enough. Both are judgement calls for the maintainer, not work items — which is why they are recorded here rather than as a fifteenth row.
After the fourteen items — surface added since the checklist emptied
The item list was complete at 0.6.0 (2026-09-03). Every release since — up to 0.16.0 — has been something other than a roadmap item: every one added surface rather than closing a gap. That matters here, because the 1.0 gate asks what you would regret freezing, and each of these is another thing that would be frozen.
| Since | What it added | Why it is not a roadmap item |
|---|---|---|
| 0.7.x | Multi-tenant routing — one router per machine, per-request tenant resolution | The roadmap assumed one router per project; running two projects proved that wrong |
| 0.8.x | sonata route auto, the session/subagent hook pair, sonata agents as an editor | Routing without losing Remote Control, and a second writer of sonata.toml |
| 0.8.2 | Per-model context windows (from models.dev) and the [1m] tier declaration | — |
| 0.9.0 | Reasoning-effort tiers — the <key>@<effort> candidate grammar | AA scores each effort level separately, so a bare key ranks on one row and runs at another |
| 0.10.0 | A local web UI on the router; the normal tier; sonata init ranking only AA task-costed models | Each is a new surface: an HTTP server, a config key, and a change to what the wizard offers |
| 0.11.0–0.12.x | A terminal config UI; tiers derived from the price-performance frontier | A second front end on sonata.toml, and a ranking rule users will learn to rely on |
| 0.13.0 | gateway_order; sonata dispatch runs measured into the ledger and counted toward [budget] | A new top-level key, and the harness lane joining the money path |
| 0.15.x | Auto-routed tiers ([auto_route], <role>-auto agents) | A new config table, a fourth agent per role, and the first feature that sends task text off the machine |
| 0.16.0 | Claude models as ordinary sonata models (auth = "session"); sonata upgrade; the sonata-loop panel | A credential path that forwards the session's own token, and a new install surface |
The direction of travel is the argument against tagging 1.0 now. [tiers.<role>] gained a key on 2026-09-16 — exactly the kind of thing a frozen contract would have made expensive — and the tier-selection wording was rewritten the same day on the strength of a ledger measurement (74% of tiered requests went to complex) that nobody could have predicted before the data existed. Both are arguments the gate already makes: the exposure that would tell you what to freeze has not happened yet, and the shape is still moving without it.
The gate itself is unchanged. Still two ways to move it, both judgement calls for the maintainer: an external bug report arriving and being answered, or a decision that exposure so far is enough.
Explicitly not in 1.0
Naming these is half the value of the plan. Each has been considered and declined, with the reason attached.
- Native Windows. Sonata drives tmux process groups and job control directly. This is a permanent documented non-goal, not an open question; WSL stays best-effort and untested.
- Mid-stream failover. Retry is pre-first-byte by design, which is exactly why it never corrupts a response in flight. Failing over mid-stream means buffering, which forfeits streaming. The 529-and-retry path already covers the case that matters.
- A rendering or streaming layer. For rendering and streaming, parity with a native Claude subagent remains the ceiling, not a floor. Agent fan-out is the deliberate exception granted on 2026-08-30; it does not reopen work on either declined layer.
- An MCP server. Removed on purpose in 0.2.0. Bash commands with a working allow-list beat RPC tools the permission classifier judges inconsistently — that failure was observed, not theorised.
- More harness adapters. The adapter boundary is proven at five. Additional harnesses are contributor work, not launch-blocking work — and each one added before 1.0 is another surface to freeze.
- Telemetry. A tool whose whole premise is routing your credentials through a local proxy does not get to phone home.