@zhihaohong52/sonata 0.16.0 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,24 +7,19 @@
7
7
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
  [![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)
9
9
 
10
- Claude Code's subagents are excellent, but they are always Claude. Sonata lets
11
- you dispatch a subagent backed by a different model, through the ordinary
12
- Agent tool — same interface, same working directory, same report contract.
13
- Different brain.
14
-
15
- Two ways to run it:
16
-
17
- - **Native** — the foreign model runs *inside Claude Code's own loop*: its
18
- tools, its permission modes, no separate TUI. A local routing proxy
19
- (`sonata serve`) makes this possible; see the [Native path guide](docs/guide/native-path.md).
20
- This is the default: `sonata init` generates one agent per role and present tier
21
- (up to 12 agents), each backed by a ranked list of models the router tries in
22
- order.
23
- - **Harness** — the foreign model runs in *its own* CLI (OpenCode, Codex, Pi,
24
- Reasonix), launched in a detached tmux session and driven through
25
- `sonata dispatch`. No proxy required — this is the fallback lane: when
26
- every native route for a tier has failed, the router says so and names the
27
- `sonata dispatch --tier <role>-<tier>` command, which the agent runs itself.
10
+ Claude Code's subagents always run on Claude. Sonata gives you subagents that
11
+ run on other models — GPT, DeepSeek, GLM, Kimi, or anything else you can reach
12
+ — called through the ordinary Agent tool, in the same working directory, with
13
+ the same tools and permission modes. Your main session stays on Claude; only
14
+ the subagents change.
15
+
16
+ `sonata init` generates agents such as `code-simple`, `code-complex` and
17
+ `review-complex`, each backed by a ranked list of models. A local router
18
+ (`sonata serve`) runs them inside Claude Code's own loop and tries the next
19
+ model when one fails. If every model in the list fails, the agent falls back to
20
+ running the task in that model's own CLI (OpenCode, Codex, Pi or Reasonix).
21
+
22
+ ![The code-complex tier in sonata agents: ranked models with intelligence, cost per task and live state](docs/public/screenshots/tier-ranking.png)
28
23
 
29
24
  Two reasons you might want that:
30
25
 
@@ -35,283 +30,72 @@ Two reasons you might want that:
35
30
  fewer problems than asking a different model family. Review is where a second
36
31
  opinion actually pays.
37
32
 
38
- ```
39
- ● code-deepseek-v4-flash (running)
40
- └ sonata: tail a1f3e2
41
- → read src/parser.ts
42
- → edit src/parser.ts +31 −12
43
- → bash pytest -q → 47 passed
44
- done — "Refactored the parser. Tests pass."
45
- ```
33
+ Sonata itself is free and MIT-licensed; you pay your providers as usual, and
34
+ `sonata usage` shows what each model, tier and role cost you — including work
35
+ a subscription already covers, valued at list price:
36
+
37
+ ![sonata usage by tier over 30 days](docs/public/screenshots/usage.png)
46
38
 
47
39
  ## Status
48
40
 
49
- **Working, early.** The engine and the OpenCode, Codex, Pi and Reasonix
50
- harness adapters are complete and tested, each verified end to end against a
51
- real model. The native path (`sonata serve`/`sonata code`/`sonata restart`
52
- and the `claude` harness adapter) is also complete and verified live —
53
- routing, permission modes, and the router's Codex-overload handling have all
54
- been confirmed against real dispatches.
41
+ **Public beta.** Sonata is pre-1.0 and changing quickly. The native path and
42
+ the OpenCode, Codex, Pi and Reasonix adapters are complete, tested, and verified
43
+ end to end against real models. Expect rough edges in setups nobody has tried
44
+ yet, and please [report them](https://github.com/zhihaohong52/sonata/issues).
55
45
 
56
46
  Read [Limitations](docs/guide/limitations.md) and
57
- [Security](docs/guide/security.md) before depending on it.
58
-
59
- ## Requirements
60
-
61
- - **Node 22+**
62
- - **tmux** — every harness runs inside a tmux session (`brew install tmux`,
63
- `apt install tmux`)
64
- - **LiteLLM — managed, and only when a gateway needs it.** Sonata installs its
65
- own pinned copy into `~/.config/sonata/litellm` (`sonata litellm install`,
66
- offered by `sonata init`), so `pip install` by hand is no longer a step. A
67
- gateway that speaks the Anthropic Messages API natively is reached directly
68
- with no LiteLLM in the path at all — such a config needs no Python whatsoever.
69
- When one *is* needed, `uv` (fastest, and can fetch a conforming interpreter)
70
- or a `python3` in `>=3.10,<3.15` is required.
71
- - **macOS or Linux.** Sonata launches bash scripts inside tmux and manages
72
- process groups directly; Windows is not supported. WSL should work but is
73
- untested.
74
- - **A provider.** Either an API key for one of ~30 well-known providers (see
75
- [BYOK](#byok-bring-your-own-key) — no harness needed), or at least one of the
76
- harnesses below, authenticated:
77
- - **[OpenCode](https://opencode.ai)** — any provider it supports
78
- - **[Codex CLI](https://github.com/openai/codex)** — `codex login`
79
- - **[Pi](https://github.com/earendil-works/pi-mono)** — any provider it
80
- supports; model ids take the `provider/id` form
81
- - **[Reasonix](https://github.com/esengine/DeepSeek-Reasonix)** — `reasonix setup`;
82
- any OpenAI-compatible endpoint is a config entry, and model ids take the
83
- `provider/id` form
84
- - **Claude Code** — the `claude` harness adapter (native path; see the
85
- [Native path guide](docs/guide/native-path.md))
86
-
87
- ## Install
88
-
89
- ```bash
90
- npm install -g @zhihaohong52/sonata
91
- ```
47
+ [Security](docs/guide/security.md) before depending on it. The models sonata
48
+ launches run on your machine, as you, and your code is sent to whichever
49
+ providers you configure.
92
50
 
93
- ### From source
51
+ ## Quickstart
94
52
 
95
- For working on sonata itself, or to run a change that has not been released:
96
-
97
- ```bash
98
- git clone https://github.com/zhihaohong52/sonata.git
99
- cd sonata
100
- npm install
101
- npm run build
102
- npm link # puts `sonata` on your PATH
103
- ```
104
-
105
- `sonata` on your PATH runs `dist/`, not `src/` — after changing anything under
106
- `src/`, run `npm run build` or the command keeps its old behaviour.
107
-
108
- Then, in the repository where you want to use it:
53
+ Requires **Claude Code**, **Node 22+**, **tmux**, and **macOS or Linux**, plus a provider: an
54
+ API key for one of ~30 well-known providers, your Claude Code account, or an
55
+ authenticated harness ([OpenCode](https://opencode.ai),
56
+ [Codex CLI](https://github.com/openai/codex),
57
+ [Pi](https://github.com/earendil-works/pi-mono) or
58
+ [Reasonix](https://github.com/esengine/DeepSeek-Reasonix)). Full requirements
59
+ are in [Getting started](docs/guide/getting-started.md#requirements).
109
60
 
110
61
  ```bash
62
+ npm install -g @zhihaohong52/sonata
63
+ cd your-project
111
64
  sonata init
112
65
  ```
113
66
 
114
- `sonata init` detects your environment, reports anything broken with the command
115
- that fixes it, and asks which providers, models and roles you want.
116
-
117
- Provider setup is a menu, modeled on opencode's own `/connect`: bulk-import
118
- everything you're already logged into elsewhere, or add providers one at a
119
- time — including one sonata has never heard of.
120
-
121
- ```
122
- ███████╗ ██████╗ ███╗ ██╗ █████╗ ████████╗ █████╗
123
- ██╔════╝██╔═══██╗████╗ ██║██╔══██╗╚══██╔══╝██╔══██╗
124
- ███████╗██║ ██║██╔██╗ ██║███████║ ██║ ███████║
125
- ╚════██║██║ ██║██║╚██╗██║██╔══██║ ██║ ██╔══██║
126
- ███████║╚██████╔╝██║ ╚████║██║ ██║ ██║ ██║ ██║
127
- ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
128
-
129
- foreign-model subagents for Claude Code
130
-
131
- ✓ tmux 3.7b
132
- ✓ opencode 1.18.19 · 520 models
133
- ✓ codex codex-cli 0.149.0 · 6 models
134
-
135
- Set up providers
136
- ❯ Import from other harnesses
137
- Add provider
138
-
139
- ↑↓ choose · enter confirm · ← back · esc cancel
140
- ```
141
-
142
- **Import from other harnesses** bulk-selects providers your installed
143
- harnesses are already authenticated for — nothing to type, nothing to
144
- re-enter. It lists every provider with a detected credential (an OAuth grant
145
- or a plain API key sitting in another harness's own store), whether or not
146
- it is already configured, and pre-checks the ones that are — so the same
147
- screen doubles as a toggle: unchecking an already-imported provider removes
148
- it, along with its models and any role assignments that used them:
149
-
150
- ```
151
- Import from other harnesses
152
-
153
- filter: █
154
-
155
- ❯ ◉ codex · expires in 8d
156
- ○ openai · expires in 8d
157
-
158
- 2 of 2 · space toggle · type to filter · enter confirm · esc cancel
159
- ```
160
-
161
- **Add provider** searches the merged catalogue of every provider a harness
162
- knows about plus ~30 well-known BYOK providers — or lets you type in one
163
- sonata has never seen:
164
-
165
- ```
166
- Add provider
167
-
168
- filter: █
169
-
170
- ❯ acme · opencode
171
- anthropic · opencode
172
- codex · codex
173
- deepseek · byok
174
- ⋮
175
- Add a custom provider…
176
-
177
- 38 of 38 · ↑↓ choose · type to filter · enter confirm · ← back · esc cancel
178
- ```
179
-
180
- Picking a known provider offers **Run OAuth login** (for `codex` and
181
- `github-copilot`, the two providers sonata can authenticate on its own — see
182
- the [Native path guide](docs/guide/native-path.md)) or **Enter an API key**.
183
- Picking a harness-catalogued provider then shows its models to select from:
184
-
185
- ```
186
- Models to enable
187
-
188
- filter: gpt█
189
-
190
- ❯ ◉ openai/gpt-5.6-sol · GPT-5.6 Sol
191
- ○ openai/gpt-5.6-luna · GPT-5.6 Luna
192
-
193
- 2 of 13 · space toggle · type to filter · enter confirm · esc cancel
194
- ```
195
-
196
- Then it asks you to rank your selected models into `simple`, `normal` and
197
- `complex` tiers per role — pre-sorted by a cached Artificial Analysis catalog
198
- (`sonata catalog
199
- update`, with a free key from [artificialanalysis.ai](https://artificialanalysis.ai))
200
- when one exists, or built-in defaults otherwise. `complex` is ordered by raw
201
- capability. `normal` is ordered by capability **per task-dollar** without a cost
202
- cap, while `simple` uses that same value order under a cost cap of 12 times the
203
- best-value model's cost per task. Thus `simple` is a cost-capped prefix of
204
- `normal`: demanding work goes to the strongest model you picked, ordinary work
205
- gets the best value, and grunt work stays within the cheaper fallback chain. Set
206
- `avoid_gateways` in
207
- `sonata.toml` to rank a particular gateway's models last without dropping them
208
- as fallbacks. It then asks whether the config
209
- applies to this project or the whole machine, writes `sonata.toml`, generates
210
- one agent per role × tier, offers to install the permission hook, installs the
211
- `sonata-loop` skill, and offers to run `sonata route auto` so the generated
212
- agents have a router to reach. Project scope writes `./sonata.toml` and
213
- `./.claude/agents/`; machine scope writes `~/.config/sonata/sonata.toml` and
214
- `~/.claude/agents/`, where Claude Code offers the agents in every repository.
215
-
216
- Claude Code picks up the new agents automatically; no restart is needed.
217
-
218
- Every prompt has a flag, so it also works unattended:
219
-
220
- ```bash
221
- sonata init --yes --providers opencode/openai --models opencode-openai-gpt-5.6-sol --roles code,review --scope project
222
- ```
223
-
224
- ### BYOK (bring your own key)
67
+ `sonata init` checks your environment, reports anything broken with the
68
+ command that fixes it, then walks you through providers, models, roles and a
69
+ ranking of models into tiers. It writes `sonata.toml` and generates the tier
70
+ agents. Every prompt has a flag, so it also runs unattended.
225
71
 
226
- Sonata does not need a harness. **Add provider** lists ~30 well-known
227
- providers alongside anything your harnesses discovered; pick one, enter its
228
- API key, and choose from the models it reports. A provider not on that
229
- list — an internal proxy, a self-hosted endpoint — doesn't need to be:
230
- **Add a custom provider…**, at the bottom of that same search list, asks for
231
- a name, a base URL, and whether it speaks OpenAI's or Anthropic's wire
232
- format, then drops into the same key-entry flow.
72
+ Accept its offer to route your sessions (`sonata route auto`), then **start a
73
+ new Claude Code session**: routing is set up when a session starts. Without it,
74
+ tier agents fail with `model_not_found`; `sonata doctor` says what is missing.
233
75
 
234
- The key is stored in sonata's own key store — never in `sonata.toml`, never in
235
- an agent file, and never on the command line. It is written only after you
236
- confirm the summary, so cancelling the wizard stores nothing.
237
-
238
- Unattended, with the key stored first so it stays out of argv and shell history:
239
-
240
- ```bash
241
- sonata auth add deepseek
242
- sonata init --yes --providers byok/deepseek --models deepseek-deepseek-v4-flash --roles code
243
- ```
244
-
245
- BYOK models use the [native path](docs/guide/native-path.md), so they run
246
- inside Claude Code's own loop, tools and permission modes.
247
-
248
- Two things worth knowing before you pick a provider:
249
-
250
- - **Model discovery is a convention, not a guarantee.** Sonata asks the provider
251
- for `GET <base_url>/models`, which every provider in its list implements. If
252
- yours does not answer — offline, or a different shape — the wizard asks you to
253
- type the model ids instead, saying which of those happened. That is a
254
- fallback, not an error.
255
- - **A rejected key gets its own screen.** If the provider answers 401 or 403,
256
- sonata says so and offers to take a different key. It also offers to keep the
257
- one you gave and type ids by hand, because some providers refuse to list
258
- models for a key that works perfectly well for inference.
259
- - **`claude-*` models are not offered.** The router forwards that prefix to
260
- Anthropic, so a `claude-` id cannot be served through a gateway. Aggregators
261
- such as OpenRouter list plenty of them; sonata filters them out rather than
262
- writing a config it would then refuse to load.
263
-
264
- ### Adding Codex models
265
-
266
- **The wizard discovers OpenCode, Pi and Reasonix models** by provider then by
267
- model, and **Codex through `codex app-server`'s `model/list`** (JSON-RPC over
268
- stdio — codex has no provider dimension and takes a bare model id). Hand-written
269
- codex entries survive `sonata init`, so a bare-id model can also be added by
270
- hand:
271
-
272
- ```toml
273
- [models."gpt-5-6-sol"]
274
- harness = "codex"
275
- id = "gpt-5.6-sol"
276
- ```
277
-
278
- Hand-written entries survive `sonata init` — the wizard carries through any
279
- model whose harness it does not manage.
280
-
281
- Then run `sonata sync` to regenerate the agent files; Claude Code picks them up automatically.
282
-
283
- ## Using it
284
-
285
- The generated agents are ordinary registry entries, so Claude selects them the
286
- same way it selects any other subagent. Ask for one by name, or describe work
287
- that suits it:
76
+ Then ask Claude Code for one by name, or describe work that suits it:
288
77
 
289
78
  > "Use code-simple to convert these callbacks to async/await."
290
79
 
291
- > "Use code-normal to add this flag and its tests."
292
-
293
80
  > "Get review-complex to look at the auth refactor."
294
81
 
295
82
  Choose a tier by the task, not its size: `simple` is specified closely enough
296
- that the diff can be written without asking a question; `normal` is the default
297
- when you know what to change but need to fit it into the surrounding code;
298
- `complex` needs a design decision or the meaning of done is ambiguous. Size is
299
- not difficulty: a large mechanical change is `simple`, while a three-line
300
- interface decision is `complex`. When unsure, use `-normal`; a task that fails
301
- review twice is re-run one tier up. The `sonata-loop` skill `sonata init` installs
302
- (`skills/loop/SKILL.md`) drives this across a whole feature: plan, route each task
303
- to a tier, gate behind review, and escalate `simple` to `normal` to `complex`
304
- after repeated failures. The same folder is a Claude Code mod with a live panel:
305
- the loop's tasks, each tier agent's current activity, the model the router
306
- served, and an agent's diff when you select its row (`/sonata-loop panel` toggles it).
307
-
308
- They compose with everything Claude Code already does — parallel fan-out,
309
- workflows, and `isolation: "worktree"`.
83
+ that the diff could be written without asking a question; `normal` is the
84
+ default when you know what to change but need to fit it into the surrounding
85
+ code; `complex` needs a design decision, or "done" is still ambiguous. The
86
+ `sonata-loop` skill that `sonata init` installs drives this across a whole
87
+ feature — plan, route each task to a tier, gate behind review, escalate on
88
+ repeated failure.
89
+
90
+ The [Getting started guide](docs/guide/getting-started.md) walks through the
91
+ wizard screen by screen, including BYOK keys and hand-added models.
310
92
 
311
93
  ## How it works
312
94
 
313
- `sonata init` generates one agent per role × present tier — up to 12 agents
314
- across `simple`, `normal` and `complex` — such as `code-simple`, `code-normal`,
95
+ `sonata init` generates one agent per role (`code`, `review`, `explore`,
96
+ `plan`) and tier (`simple`, `normal`, `complex`) — up to 12 — plus, with
97
+ `[auto_route]` configured, a `<role>-auto` agent per role that picks the tier
98
+ itself. Tier agents are named `code-simple`, `code-normal`,
315
99
  `code-complex`, `review-simple`, and so on. Each agent's frontmatter names a
316
100
  router alias (`model: sonata-code-simple`), not a specific model:
317
101
 
@@ -373,45 +157,34 @@ killed and the run is reported `DONE`, `degraded`, with a report beginning
373
157
 
374
158
  ## Commands
375
159
 
160
+ The ones you will use most:
161
+
376
162
  | Command | Purpose |
377
163
  |---|---|
378
- | `sonata upgrade` | Installs the latest published release with `npm install -g`. A development install (`npm link`) is refused and told to `git pull && npm run build` instead. Run `sonata restart` afterwards so the router runs the new version |
379
- | `sonata --version` (`-v`) | The running version and the directory it ran from. The path matters: `sonata` on PATH runs `dist/`, not `src/`, so this is how you tell which install answered |
380
- | `sonata init` | Set up sonata in this project (interactive); `--prune` removes stale generated agents |
381
- | `sonata reset [--global] [--yes]` | Remove sonata's configuration and generated files at one scope — the config, the generated agents, the loop skill, the CLAUDE.md block, and the routing env/hooks/allow-list. Keeps your gateway keys, the usage ledger and the caches, and shows the full plan before touching anything |
382
- | `sonata doctor [--json]` | Check tmux, harnesses, auth and versions; `--json` prints the same checks as structured output |
383
- | `sonata sync` | Regenerate agent files from `sonata.toml`; `--prune` removes stale generated agents |
384
- | `sonata agents [--list] [--json]` | Every generated tier agent, what each of its ranked candidates resolves to, its context window, and whether the alias carries `[1m]`. In a terminal it is also the editor: re-rank a tier without walking the whole wizard |
385
- | `sonata run` | Launch a run, print its id |
386
- | `sonata dispatch (--tier <role>-<tier> \| --model <key>)` | Blocking harness dispatch with ranked fallback — the fallback lane a tier agent reaches for when every native route fails |
387
- | `sonata tail` | Human/debugging view of a run |
388
- | `sonata approve` | Answer a pending approval |
389
- | `sonata log <id>` | Print what a run printed |
390
- | `sonata verify <id> [--model <key>]` | Verify a completed run |
391
- | `sonata auth` | Manage native-path gateway keys (`list`, `add <gateway>`, `remove <gateway>`, `login <gateway>`) |
392
- | `sonata catalog [update]` | Show the cached Artificial Analysis catalog's age, or refresh it (needs a stored `artificialanalysis` key). `sonata init` refreshes it itself when a discovered model is missing from it; `sonata doctor` warns when it goes stale |
393
- | `sonata litellm install\|status` | Install or report sonata's own pinned LiteLLM venv. `status` answering `not-required` is healthy — it means no gateway in this config routes through it |
394
- | `sonata serve` | Run the native router and its managed LiteLLM child (`--daemon` detaches); UI: `http://localhost:4100/` |
395
- | `sonata restart` | Kill whatever sonata router currently holds the port and start a fresh daemon |
396
- | `sonata code` | Launch a Claude Code session routed through the local proxy (passes `claude` args through) |
397
- | `sonata route on\|off\|status [--global]` | Route every plain `claude` session in the project (or, with `--global`, every project) through the proxy via settings.local.json/settings.json |
398
- | `sonata route auto\|manual [--global]` | Route each session for its lifetime via SessionStart/SessionEnd hooks, keeping Remote Control |
399
- | `sonata usage [--since 7d] [--by model\|role\|tier\|effort\|gateway\|lane\|session\|project] [--session <id>] [--json]` | Tokens and cost from the ledger: native requests as the router saw them, and `sonata dispatch` runs read from each harness's own store once they finish (`--by lane` splits the two); unpriced volume is reported separately, never folded into the total |
400
- | `sonata status [--session <id>\|--all]` | Whether the router is up and on which port, then the recent alias → candidate served → tokens → failed attempts decisions from the ledger; reachability and routing state live in `sonata route status` |
401
- | `sonata runs [--json]` | List this project's dispatch runs |
402
- | `sonata gc` | Kill finished tmux sessions |
164
+ | `sonata init` | Set up sonata in this project |
165
+ | `sonata doctor` | Check everything sonata depends on, and say how to fix what's broken |
166
+ | `sonata agents` | See and re-rank the tier agents' models |
167
+ | `sonata route auto` | Route every plain `claude` session in this project through the router, keeping Remote Control — set once, then launch `claude` as usual |
168
+ | `sonata code` | Launch one routed session without changing any settings |
169
+ | `sonata usage` / `sonata status` | Tokens and cost, and what the router served |
170
+ | `sonata dispatch` | Run a task through a model's own harness — the fallback lane |
171
+ | `sonata upgrade` | Install the latest release |
172
+
173
+ Every command and flag: [Commands](docs/guide/commands.md).
403
174
 
404
175
  ## Documentation
405
176
 
406
- The essentials are above. Deep-dive reference lives in
407
- [`docs/guide/`](docs/guide/):
177
+ **[zhihaohong52.github.io/sonata](https://zhihaohong52.github.io/sonata/)** — the user guide, also
178
+ readable here in [`docs/guide/`](docs/guide/):
408
179
 
409
180
  | Guide | Covers |
410
181
  |---|---|
411
- | [Native path](docs/guide/native-path.md) | Running foreign models inside Claude Code's own loop through the local routing proxy |
412
- | [Codex subscription auth](docs/guide/codex-subscription.md) | Authenticating the `codex-oauth` gateway against a ChatGPT subscription |
182
+ | [Getting started](docs/guide/getting-started.md) | Install, `sonata init` screen by screen, BYOK, first dispatch |
183
+ | [Commands](docs/guide/commands.md) | Every command and flag |
184
+ | [Configuration](docs/guide/configuration.md) | The `sonata.toml` schema, resolution order, roles and tiers |
185
+ | [Native path](docs/guide/native-path.md) | Running foreign models inside Claude Code's own loop through the local router |
186
+ | [Codex subscription auth](docs/guide/codex-subscription.md) | Authenticating against a ChatGPT subscription instead of a metered key |
413
187
  | [Permission modes](docs/guide/permission-modes.md) | How each harness honours Claude Code's permission modes |
414
- | [Configuration](docs/guide/configuration.md) | The `sonata.toml` schema, resolution order, and roles/tiers |
415
188
  | [Troubleshooting](docs/guide/troubleshooting.md) | Symptom → cause table |
416
189
  | [Security](docs/guide/security.md) | What sonata does and doesn't protect against |
417
190
  | [Limitations](docs/guide/limitations.md) | Known gaps worth knowing before depending on this |
@@ -419,39 +192,17 @@ The essentials are above. Deep-dive reference lives in
419
192
 
420
193
  Where this is headed: [Roadmap to 1.0](docs/roadmap.md).
421
194
 
422
- Design history — every feature's spec and implementation plan, kept as a
423
- permanent record — is indexed in [`docs/superpowers/`](docs/superpowers/).
424
- Architecture reviews are in [`docs/reviews/`](docs/reviews/). What using
425
- sonata to implement sonata taught about sizing and verifying dispatched work
426
- is in [`docs/dispatching-work-through-sonata.md`](docs/dispatching-work-through-sonata.md).
427
-
428
- ## Development
429
-
430
- ```bash
431
- npm install
432
- npm test # 1523 tests; needs tmux
433
- npm run typecheck
434
- npm run build
435
- ```
436
-
437
- The test suite runs against a **fake harness** — a scripted binary replaying a
438
- normal run, a crash, a real captured approval prompt, a hang that the watchdog
439
- must kill, a clean exit with no report, and a harness-written report — so the
440
- whole engine is covered with no API spend and no harness installed.
441
-
442
- See [Adding a harness](docs/guide/adding-a-harness.md) for the extension
443
- point, and [Documentation](#documentation) above for the design-history index.
195
+ For maintainers and the curious: the design record behind the code is in
196
+ [`docs/internals/`](docs/internals/), every feature's spec and plan in
197
+ [`docs/superpowers/`](docs/superpowers/), and point-in-time audits in
198
+ [`docs/reviews/`](docs/reviews/). `DESIGN.md` and `PRODUCT.md` at the root are
199
+ the design brief for the terminal UI.
444
200
 
445
201
  ## Contributing
446
202
 
447
- Issues and pull requests are welcome.
448
-
449
- - Run `npm test` and `npm run typecheck` before opening a PR; CI runs both on
450
- Linux with tmux installed.
451
- - Add tests for behaviour you change. The suite needs no API keys.
452
- - Prefer evidence to inference: if a change depends on how a harness behaves,
453
- say how you verified it. A captured fixture beats a plausible regex.
454
- - Keep harness-specific knowledge inside its adapter.
203
+ Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md)
204
+ for the development setup and what a good PR looks like. Report security
205
+ issues privately, as described in [SECURITY.md](SECURITY.md).
455
206
 
456
207
  ## Changelog
457
208
 
package/dist/catalog.js CHANGED
@@ -71,6 +71,30 @@ export const SIMPLE_COST_CEILING = 12;
71
71
  * not signal, and cost should call it instead.
72
72
  */
73
73
  export const AA_CAPABILITY_TIE_MARGIN = 1.0;
74
+ /**
75
+ * The tie margin the `complex` tier applies — wider than the general one,
76
+ * because `complex` is the tier cost otherwise never reaches.
77
+ *
78
+ * `complex` sorts by capability and lets price decide only inside a tie, so
79
+ * with the general 1.0 margin it would pay any premium for a point and a half.
80
+ * Measured 2026-10-13: Claude Opus 5.5 led at `max` (intelligence 57.6,
81
+ * $5.98/task) over its own `xhigh` (56.0, $3.46/task) — 73% more per task for
82
+ * 1.6 points, an edge inside the run-to-run spread of the benchmark itself.
83
+ * Classes are counted down from the strongest candidate, so any model less
84
+ * than 3 points below the leader ties with it and the cheaper one leads; 3 or
85
+ * more never does.
86
+ *
87
+ * One exception, by design: the knee split runs first. A model below the
88
+ * frontier's knee sorts after every model at or above it even when it is less
89
+ * than 3 points below the leader — the knee marks where extra capability
90
+ * stops being worth its price, and `complex` must not slide back under it to
91
+ * save money. The tie decides among models on the same side of the knee.
92
+ *
93
+ * Scoped to `complex` on purpose. `normal` and `simple` already weigh cost in
94
+ * their sort key, and use the class only to break an exact price tie, where a
95
+ * wider band would just discard a real capability edge for no saving.
96
+ */
97
+ export const COMPLEX_CAPABILITY_TIE_MARGIN = 3.0;
74
98
  /**
75
99
  * The capability *class* a score falls in — the margin applied transitively.
76
100
  *
@@ -90,8 +114,27 @@ export const AA_CAPABILITY_TIE_MARGIN = 1.0;
90
114
  * by capability where the old code ranked by price, never produce an order
91
115
  * that depends on input.
92
116
  */
93
- export function capabilityClass(index) {
94
- return Math.round(index / AA_CAPABILITY_TIE_MARGIN);
117
+ export function capabilityClass(index, margin = AA_CAPABILITY_TIE_MARGIN) {
118
+ return Math.round(index / margin);
119
+ }
120
+ /**
121
+ * The capability class a score falls in for `complex`, counted down from the
122
+ * strongest candidate rather than from fixed bucket edges.
123
+ *
124
+ * Bucketing on `index / margin` put 58.6 and 57.0 in different classes (20 vs
125
+ * 19) though they are 1.6 apart, so the expensive rung led again. Anchoring on
126
+ * the leader makes the class a per-candidate key (still transitive): less than
127
+ * `margin` below the leader ties with it (class 0); `margin` or more never
128
+ * does. Lower class is stronger.
129
+ *
130
+ * Below class 0 the bands have fixed edges every `margin` points down from the
131
+ * leader, so two close models can fall either side of one. That affects only
132
+ * fallback order, never which model leads — the leader is class 0 by
133
+ * construction. The `+ 1e-9` keeps a gap of exactly `margin` in class 1
134
+ * deterministically, rather than letting float representation decide.
135
+ */
136
+ export function complexClass(index, top, margin = COMPLEX_CAPABILITY_TIE_MARGIN) {
137
+ return Math.floor((top - index) / margin + 1e-9);
95
138
  }
96
139
  /**
97
140
  * Capability, best available measure first.
@@ -765,6 +808,15 @@ export function proposeTiers(modelKeys, aa, providers = [], avoided = new Set(),
765
808
  * introduced an inversion that ranked a weaker frontier rung above a
766
809
  * stronger dominated one inside a capability-ordered tier.
767
810
  */
811
+ // The strongest reasoning score among the candidates `complex` sorts —
812
+ // classes are counted down from it (see `complexClass`). The top is taken
813
+ // only from candidates that can lead the tier, because the earlier
814
+ // comparator terms sort the others after them; anchoring on one of those
815
+ // shifts every class edge. Falls back to every candidate when none can lead.
816
+ const leaders = candidates.filter((c) => capable(c) && !avoided.has(bareKey(c)) && !offScale(c) && !powerGeometry.wasteful.has(c));
817
+ const complexTop = Math.max(0, ...((leaders.length > 0 ? leaders : candidates)
818
+ .map((c) => rankReasoning(c).index)
819
+ .filter(Number.isFinite)));
768
820
  const byCapability = (a, b) => {
769
821
  const ra = rankReasoning(a);
770
822
  const rb = rankReasoning(b);
@@ -787,7 +839,7 @@ export function proposeTiers(modelKeys, aa, providers = [], avoided = new Set(),
787
839
  // because a pairwise one is not transitive: it produced a genuine
788
840
  // 3-cycle on a real fixture, which made the sort depend on input order
789
841
  // and the tier differ run to run.
790
- || capabilityClass(rb.index) - capabilityClass(ra.index)
842
+ || complexClass(ra.index, complexTop) - complexClass(rb.index, complexTop)
791
843
  || ra.price - rb.price
792
844
  // A real capability edge breaks an equal price, after the class has had
793
845
  // its say: where there is no money to save there is nothing to trade,