@zhihaohong52/sonata 0.4.0

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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +437 -0
  3. package/dist/adapters/claude.js +83 -0
  4. package/dist/adapters/claude.js.map +1 -0
  5. package/dist/adapters/codex.js +382 -0
  6. package/dist/adapters/codex.js.map +1 -0
  7. package/dist/adapters/index.js +21 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/opencode.js +104 -0
  10. package/dist/adapters/opencode.js.map +1 -0
  11. package/dist/adapters/pi.js +152 -0
  12. package/dist/adapters/pi.js.map +1 -0
  13. package/dist/adapters/reasonix.js +358 -0
  14. package/dist/adapters/reasonix.js.map +1 -0
  15. package/dist/adapters/types.js +2 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/agent-markers.js +3 -0
  18. package/dist/agent-markers.js.map +1 -0
  19. package/dist/aipricing.js +109 -0
  20. package/dist/aipricing.js.map +1 -0
  21. package/dist/catalog.js +274 -0
  22. package/dist/catalog.js.map +1 -0
  23. package/dist/cli.js +766 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/commands/approve.js +23 -0
  26. package/dist/commands/approve.js.map +1 -0
  27. package/dist/commands/auth.js +41 -0
  28. package/dist/commands/auth.js.map +1 -0
  29. package/dist/commands/catalog.js +172 -0
  30. package/dist/commands/catalog.js.map +1 -0
  31. package/dist/commands/code.js +113 -0
  32. package/dist/commands/code.js.map +1 -0
  33. package/dist/commands/dispatch.js +148 -0
  34. package/dist/commands/dispatch.js.map +1 -0
  35. package/dist/commands/doctor.js +545 -0
  36. package/dist/commands/doctor.js.map +1 -0
  37. package/dist/commands/gc.js +25 -0
  38. package/dist/commands/gc.js.map +1 -0
  39. package/dist/commands/init-log.js +83 -0
  40. package/dist/commands/init-log.js.map +1 -0
  41. package/dist/commands/init.js +157 -0
  42. package/dist/commands/init.js.map +1 -0
  43. package/dist/commands/log.js +24 -0
  44. package/dist/commands/log.js.map +1 -0
  45. package/dist/commands/route.js +660 -0
  46. package/dist/commands/route.js.map +1 -0
  47. package/dist/commands/run.js +199 -0
  48. package/dist/commands/run.js.map +1 -0
  49. package/dist/commands/runs.js +37 -0
  50. package/dist/commands/runs.js.map +1 -0
  51. package/dist/commands/serve.js +818 -0
  52. package/dist/commands/serve.js.map +1 -0
  53. package/dist/commands/status.js +14 -0
  54. package/dist/commands/status.js.map +1 -0
  55. package/dist/commands/sync.js +258 -0
  56. package/dist/commands/sync.js.map +1 -0
  57. package/dist/commands/tail.js +229 -0
  58. package/dist/commands/tail.js.map +1 -0
  59. package/dist/commands/usage.js +85 -0
  60. package/dist/commands/usage.js.map +1 -0
  61. package/dist/commands/verify.js +69 -0
  62. package/dist/commands/verify.js.map +1 -0
  63. package/dist/commands/wait.js +38 -0
  64. package/dist/commands/wait.js.map +1 -0
  65. package/dist/config.js +588 -0
  66. package/dist/config.js.map +1 -0
  67. package/dist/detect.js +445 -0
  68. package/dist/detect.js.map +1 -0
  69. package/dist/filelock.js +83 -0
  70. package/dist/filelock.js.map +1 -0
  71. package/dist/init/apply.js +96 -0
  72. package/dist/init/apply.js.map +1 -0
  73. package/dist/init/candidates.js +115 -0
  74. package/dist/init/candidates.js.map +1 -0
  75. package/dist/init/discover.js +216 -0
  76. package/dist/init/discover.js.map +1 -0
  77. package/dist/init/helpers.js +429 -0
  78. package/dist/init/helpers.js.map +1 -0
  79. package/dist/init/interactive-state.js +128 -0
  80. package/dist/init/interactive-state.js.map +1 -0
  81. package/dist/init/plan.js +225 -0
  82. package/dist/init/plan.js.map +1 -0
  83. package/dist/init/scripted-state.js +130 -0
  84. package/dist/init/scripted-state.js.map +1 -0
  85. package/dist/init/toml.js +83 -0
  86. package/dist/init/toml.js.map +1 -0
  87. package/dist/init/validate.js +149 -0
  88. package/dist/init/validate.js.map +1 -0
  89. package/dist/ledger.js +123 -0
  90. package/dist/ledger.js.map +1 -0
  91. package/dist/mcp/protocol.js +64 -0
  92. package/dist/mcp/protocol.js.map +1 -0
  93. package/dist/mcp/server.js +96 -0
  94. package/dist/mcp/server.js.map +1 -0
  95. package/dist/mcp/tools.js +210 -0
  96. package/dist/mcp/tools.js.map +1 -0
  97. package/dist/mode.js +64 -0
  98. package/dist/mode.js.map +1 -0
  99. package/dist/native/claude-shim.js +228 -0
  100. package/dist/native/claude-shim.js.map +1 -0
  101. package/dist/native/codex-auth.js +177 -0
  102. package/dist/native/codex-auth.js.map +1 -0
  103. package/dist/native/copilot-auth.js +105 -0
  104. package/dist/native/copilot-auth.js.map +1 -0
  105. package/dist/native/credentials.js +83 -0
  106. package/dist/native/credentials.js.map +1 -0
  107. package/dist/native/hosts.js +60 -0
  108. package/dist/native/hosts.js.map +1 -0
  109. package/dist/native/litellm.js +103 -0
  110. package/dist/native/litellm.js.map +1 -0
  111. package/dist/native/models.js +96 -0
  112. package/dist/native/models.js.map +1 -0
  113. package/dist/native/oauth-login.js +112 -0
  114. package/dist/native/oauth-login.js.map +1 -0
  115. package/dist/native/router.js +469 -0
  116. package/dist/native/router.js.map +1 -0
  117. package/dist/native/tls.js +73 -0
  118. package/dist/native/tls.js.map +1 -0
  119. package/dist/native/usage.js +119 -0
  120. package/dist/native/usage.js.map +1 -0
  121. package/dist/normalize.js +98 -0
  122. package/dist/normalize.js.map +1 -0
  123. package/dist/pricing.js +86 -0
  124. package/dist/pricing.js.map +1 -0
  125. package/dist/roles.js +49 -0
  126. package/dist/roles.js.map +1 -0
  127. package/dist/sessions.js +62 -0
  128. package/dist/sessions.js.map +1 -0
  129. package/dist/settings.js +131 -0
  130. package/dist/settings.js.map +1 -0
  131. package/dist/store.js +70 -0
  132. package/dist/store.js.map +1 -0
  133. package/dist/tmux.js +89 -0
  134. package/dist/tmux.js.map +1 -0
  135. package/dist/tui-ink/app-state.js +340 -0
  136. package/dist/tui-ink/app-state.js.map +1 -0
  137. package/dist/tui-ink/app.js +156 -0
  138. package/dist/tui-ink/app.js.map +1 -0
  139. package/dist/tui-ink/components/byok-step.js +102 -0
  140. package/dist/tui-ink/components/byok-step.js.map +1 -0
  141. package/dist/tui-ink/components/login-screen.js +80 -0
  142. package/dist/tui-ink/components/login-screen.js.map +1 -0
  143. package/dist/tui-ink/components/models-step.js +92 -0
  144. package/dist/tui-ink/components/models-step.js.map +1 -0
  145. package/dist/tui-ink/components/multi-select-state.js +56 -0
  146. package/dist/tui-ink/components/multi-select-state.js.map +1 -0
  147. package/dist/tui-ink/components/multi-select.js +69 -0
  148. package/dist/tui-ink/components/multi-select.js.map +1 -0
  149. package/dist/tui-ink/components/providers-step.js +285 -0
  150. package/dist/tui-ink/components/providers-step.js.map +1 -0
  151. package/dist/tui-ink/components/ranked-select-state.js +41 -0
  152. package/dist/tui-ink/components/ranked-select-state.js.map +1 -0
  153. package/dist/tui-ink/components/ranked-select.js +61 -0
  154. package/dist/tui-ink/components/ranked-select.js.map +1 -0
  155. package/dist/tui-ink/components/search-select.js +47 -0
  156. package/dist/tui-ink/components/search-select.js.map +1 -0
  157. package/dist/tui-ink/components/text-input-state.js +42 -0
  158. package/dist/tui-ink/components/text-input-state.js.map +1 -0
  159. package/dist/tui-ink/components/text-input.js +48 -0
  160. package/dist/tui-ink/components/text-input.js.map +1 -0
  161. package/dist/tui-ink/run.js +38 -0
  162. package/dist/tui-ink/run.js.map +1 -0
  163. package/dist/tui-ink/test-compile.js +4 -0
  164. package/dist/tui-ink/test-compile.js.map +1 -0
  165. package/dist/tui-ink/types.js +2 -0
  166. package/dist/tui-ink/types.js.map +1 -0
  167. package/dist/tui.js +330 -0
  168. package/dist/tui.js.map +1 -0
  169. package/dist/types.js +2 -0
  170. package/dist/types.js.map +1 -0
  171. package/dist/watchdog.js +85 -0
  172. package/dist/watchdog.js.map +1 -0
  173. package/hooks/capture-mode.mjs +56 -0
  174. package/hooks/ensure-serve.mjs +140 -0
  175. package/hooks/hooks.json +12 -0
  176. package/hooks/route-session.mjs +47 -0
  177. package/hooks/route-subagent.mjs +54 -0
  178. package/package.json +64 -0
  179. package/roles/code.md +13 -0
  180. package/roles/explore.md +9 -0
  181. package/roles/plan.md +10 -0
  182. package/roles/review.md +11 -0
  183. package/skills/loop/SKILL.md +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James @zhihaohong52
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,437 @@
1
+ # sonata
2
+
3
+ **Foreign-model subagents for Claude Code.**
4
+
5
+ [![ci](https://github.com/zhihaohong52/sonata/actions/workflows/ci.yml/badge.svg)](https://github.com/zhihaohong52/sonata/actions/workflows/ci.yml)
6
+ [![release](https://img.shields.io/github/v/tag/zhihaohong52/sonata?label=release&sort=semver)](CHANGELOG.md)
7
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ [![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)
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 tier agent per role, each
21
+ backed by a ranked list of models the router tries in order.
22
+ - **Harness** — the foreign model runs in *its own* CLI (OpenCode, Codex, Pi,
23
+ Reasonix), launched in a detached tmux session and driven through
24
+ `sonata dispatch`. No proxy required — this is the fallback lane: when
25
+ every native route for a tier has failed, the router says so and names the
26
+ `sonata dispatch --tier <role>-<tier>` command, which the agent runs itself.
27
+
28
+ Two reasons you might want that:
29
+
30
+ - **Cost.** Mechanical work — bulk refactors, test scaffolding, migrations —
31
+ does not need a frontier model. A cheap high-cache model is an adequate
32
+ substitute, and the difference over a working day is large.
33
+ - **Diversity of judgement.** Asking Claude to review Claude's work surfaces
34
+ fewer problems than asking a different model family. Review is where a second
35
+ opinion actually pays.
36
+
37
+ ```
38
+ ● code-deepseek-v4-flash (running)
39
+ └ sonata: tail a1f3e2
40
+ → read src/parser.ts
41
+ → edit src/parser.ts +31 −12
42
+ → bash pytest -q → 47 passed
43
+ done — "Refactored the parser. Tests pass."
44
+ ```
45
+
46
+ ## Status
47
+
48
+ **Working, early.** The engine and the OpenCode, Codex, Pi and Reasonix
49
+ harness adapters are complete and tested, each verified end to end against a
50
+ real model. The native path (`sonata serve`/`sonata code`/`sonata restart`
51
+ and the `claude` harness adapter) is also complete and verified live —
52
+ routing, permission modes, and the router's Codex-overload handling have all
53
+ been confirmed against real dispatches.
54
+
55
+ Read [Limitations](docs/guide/limitations.md) and
56
+ [Security](docs/guide/security.md) before depending on it.
57
+
58
+ ## Requirements
59
+
60
+ - **Node 22+**
61
+ - **tmux** — every harness runs inside a tmux session (`brew install tmux`,
62
+ `apt install tmux`)
63
+ - **LiteLLM (optional, native path only)** — install with `pip install 'litellm[proxy]'`
64
+ - **macOS or Linux.** Sonata launches bash scripts inside tmux and manages
65
+ process groups directly; Windows is not supported. WSL should work but is
66
+ untested.
67
+ - **A provider.** Either an API key for one of ~30 well-known providers (see
68
+ [BYOK](#byok-bring-your-own-key) — no harness needed), or at least one of the
69
+ harnesses below, authenticated:
70
+ - **[OpenCode](https://opencode.ai)** — any provider it supports
71
+ - **[Codex CLI](https://github.com/openai/codex)** — `codex login`
72
+ - **[Pi](https://github.com/earendil-works/pi-mono)** — any provider it
73
+ supports; model ids take the `provider/id` form
74
+ - **[Reasonix](https://github.com/esengine/DeepSeek-Reasonix)** — `reasonix setup`;
75
+ any OpenAI-compatible endpoint is a config entry, and model ids take the
76
+ `provider/id` form
77
+ - **Claude Code** — the `claude` harness adapter (native path; see the
78
+ [Native path guide](docs/guide/native-path.md))
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ npm install -g @zhihaohong52/sonata
84
+ ```
85
+
86
+ ### From source
87
+
88
+ For working on sonata itself, or to run a change that has not been released:
89
+
90
+ ```bash
91
+ git clone https://github.com/zhihaohong52/sonata.git
92
+ cd sonata
93
+ npm install
94
+ npm run build
95
+ npm link # puts `sonata` on your PATH
96
+ ```
97
+
98
+ `sonata` on your PATH runs `dist/`, not `src/` — after changing anything under
99
+ `src/`, run `npm run build` or the command keeps its old behaviour.
100
+
101
+ Then, in the repository where you want to use it:
102
+
103
+ ```bash
104
+ sonata init
105
+ ```
106
+
107
+ `sonata init` detects your environment, reports anything broken with the command
108
+ that fixes it, and asks which providers, models and roles you want.
109
+
110
+ Provider setup is a menu, modeled on opencode's own `/connect`: bulk-import
111
+ everything you're already logged into elsewhere, or add providers one at a
112
+ time — including one sonata has never heard of.
113
+
114
+ ```
115
+ ███████╗ ██████╗ ███╗ ██╗ █████╗ ████████╗ █████╗
116
+ ██╔════╝██╔═══██╗████╗ ██║██╔══██╗╚══██╔══╝██╔══██╗
117
+ ███████╗██║ ██║██╔██╗ ██║███████║ ██║ ███████║
118
+ ╚════██║██║ ██║██║╚██╗██║██╔══██║ ██║ ██╔══██║
119
+ ███████║╚██████╔╝██║ ╚████║██║ ██║ ██║ ██║ ██║
120
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
121
+
122
+ foreign-model subagents for Claude Code
123
+
124
+ ✓ tmux 3.7b
125
+ ✓ opencode 1.18.19 · 520 models
126
+ ✓ codex codex-cli 0.149.0 · 6 models
127
+
128
+ Set up providers
129
+ ❯ Import from other harnesses
130
+ Add provider
131
+
132
+ ↑↓ choose · enter confirm · ← back · esc cancel
133
+ ```
134
+
135
+ **Import from other harnesses** bulk-selects providers your installed
136
+ harnesses are already authenticated for — nothing to type, nothing to
137
+ re-enter. It lists every provider with a detected credential (an OAuth grant
138
+ or a plain API key sitting in another harness's own store), whether or not
139
+ it is already configured, and pre-checks the ones that are — so the same
140
+ screen doubles as a toggle: unchecking an already-imported provider removes
141
+ it, along with its models and any role assignments that used them:
142
+
143
+ ```
144
+ Import from other harnesses
145
+
146
+ filter: █
147
+
148
+ ❯ ◉ codex · expires in 8d
149
+ ○ openai · expires in 8d
150
+
151
+ 2 of 2 · space toggle · type to filter · enter confirm · esc cancel
152
+ ```
153
+
154
+ **Add provider** searches the merged catalogue of every provider a harness
155
+ knows about plus ~30 well-known BYOK providers — or lets you type in one
156
+ sonata has never seen:
157
+
158
+ ```
159
+ Add provider
160
+
161
+ filter: █
162
+
163
+ ❯ acme · opencode
164
+ anthropic · opencode
165
+ codex · codex
166
+ deepseek · byok
167
+
168
+ Add a custom provider…
169
+
170
+ 38 of 38 · ↑↓ choose · type to filter · enter confirm · ← back · esc cancel
171
+ ```
172
+
173
+ Picking a known provider offers **Run OAuth login** (for `codex` and
174
+ `github-copilot`, the two providers sonata can authenticate on its own — see
175
+ the [Native path guide](docs/guide/native-path.md)) or **Enter an API key**.
176
+ Picking a harness-catalogued provider then shows its models to select from:
177
+
178
+ ```
179
+ Models to enable
180
+
181
+ filter: gpt█
182
+
183
+ ❯ ◉ openai/gpt-5.6-sol · GPT-5.6 Sol
184
+ ○ openai/gpt-5.6-luna · GPT-5.6 Luna
185
+
186
+ 2 of 13 · space toggle · type to filter · enter confirm · esc cancel
187
+ ```
188
+
189
+ Then it asks you to rank your selected models into `simple`/`complex` tiers per
190
+ role — pre-sorted by a cached Artificial Analysis catalog (`sonata catalog
191
+ update`, with a free key from [artificialanalysis.ai](https://artificialanalysis.ai))
192
+ when one exists, or built-in defaults otherwise. `complex` is ordered by raw
193
+ capability and `simple` by capability **per task-dollar**, so demanding work
194
+ goes to the strongest model you picked and grunt work to the one that returns
195
+ the most per dollar rather than merely the cheapest. Set `avoid_gateways` in
196
+ `sonata.toml` to rank a particular gateway's models last without dropping them
197
+ as fallbacks. It then asks whether the config
198
+ applies to this project or the whole machine, writes `sonata.toml`, generates
199
+ one agent per role × tier, offers to install the permission hook, installs the
200
+ `sonata-loop` skill, and offers to run `sonata route auto` so the generated
201
+ agents have a router to reach. Project scope writes `./sonata.toml` and
202
+ `./.claude/agents/`; machine scope writes `~/.config/sonata/sonata.toml` and
203
+ `~/.claude/agents/`, where Claude Code offers the agents in every repository.
204
+
205
+ Claude Code picks up the new agents automatically; no restart is needed.
206
+
207
+ Every prompt has a flag, so it also works unattended:
208
+
209
+ ```bash
210
+ sonata init --yes --providers opencode/openai --models opencode-openai-gpt-5.6-sol --roles code,review --scope project
211
+ ```
212
+
213
+ ### BYOK (bring your own key)
214
+
215
+ Sonata does not need a harness. **Add provider** lists ~30 well-known
216
+ providers alongside anything your harnesses discovered; pick one, enter its
217
+ API key, and choose from the models it reports. A provider not on that
218
+ list — an internal proxy, a self-hosted endpoint — doesn't need to be:
219
+ **Add a custom provider…**, at the bottom of that same search list, asks for
220
+ a name, a base URL, and whether it speaks OpenAI's or Anthropic's wire
221
+ format, then drops into the same key-entry flow.
222
+
223
+ The key is stored in sonata's own key store — never in `sonata.toml`, never in
224
+ an agent file, and never on the command line. It is written only after you
225
+ confirm the summary, so cancelling the wizard stores nothing.
226
+
227
+ Unattended, with the key stored first so it stays out of argv and shell history:
228
+
229
+ ```bash
230
+ sonata auth add deepseek
231
+ sonata init --yes --providers byok/deepseek --models deepseek-deepseek-v4-flash --roles code
232
+ ```
233
+
234
+ BYOK models use the [native path](docs/guide/native-path.md), so they run
235
+ inside Claude Code's own loop, tools and permission modes.
236
+
237
+ Two things worth knowing before you pick a provider:
238
+
239
+ - **Model discovery is a convention, not a guarantee.** Sonata asks the provider
240
+ for `GET <base_url>/models`, which every provider in its list implements. If
241
+ yours does not answer — offline, or a different shape — the wizard asks you to
242
+ type the model ids instead, saying which of those happened. That is a
243
+ fallback, not an error.
244
+ - **A rejected key gets its own screen.** If the provider answers 401 or 403,
245
+ sonata says so and offers to take a different key. It also offers to keep the
246
+ one you gave and type ids by hand, because some providers refuse to list
247
+ models for a key that works perfectly well for inference.
248
+ - **`claude-*` models are not offered.** The router forwards that prefix to
249
+ Anthropic, so a `claude-` id cannot be served through a gateway. Aggregators
250
+ such as OpenRouter list plenty of them; sonata filters them out rather than
251
+ writing a config it would then refuse to load.
252
+
253
+ ### Adding Codex models
254
+
255
+ **The wizard discovers OpenCode, Pi and Reasonix models** by provider then by
256
+ model, and **Codex through `codex app-server`'s `model/list`** (JSON-RPC over
257
+ stdio — codex has no provider dimension and takes a bare model id). Hand-written
258
+ codex entries survive `sonata init`, so a bare-id model can also be added by
259
+ hand:
260
+
261
+ ```toml
262
+ [models."gpt-5-6-sol"]
263
+ harness = "codex"
264
+ id = "gpt-5.6-sol"
265
+ ```
266
+
267
+ Hand-written entries survive `sonata init` — the wizard carries through any
268
+ model whose harness it does not manage.
269
+
270
+ Then run `sonata sync` to regenerate the agent files; Claude Code picks them up automatically.
271
+
272
+ ## Using it
273
+
274
+ The generated agents are ordinary registry entries, so Claude selects them the
275
+ same way it selects any other subagent. Ask for one by name, or describe work
276
+ that suits it:
277
+
278
+ > "Use code-simple to convert these callbacks to async/await."
279
+
280
+ > "Get review-complex to look at the auth refactor."
281
+
282
+ Judging `simple` vs `complex` is a call you make per task — mechanical,
283
+ well-specified, single-file work is `simple`; cross-cutting, ambiguous, or
284
+ design-sensitive work is `complex`. When unsure, use `-complex`. The
285
+ `sonata-loop` skill `sonata init` installs (`skills/loop/SKILL.md`) drives
286
+ this across a whole feature: plan, route each task to a tier, gate behind
287
+ review, escalate a task to `complex` after two failed reviews at `simple`.
288
+
289
+ They compose with everything Claude Code already does — parallel fan-out,
290
+ workflows, and `isolation: "worktree"`.
291
+
292
+ ## How it works
293
+
294
+ `sonata init` generates one agent per role × difficulty tier — `code-simple`,
295
+ `code-complex`, `review-simple`, and so on. Each agent's frontmatter names a
296
+ router alias (`model: sonata-code-simple`), not a specific model:
297
+
298
+ ```
299
+ Claude Code
300
+ │ Agent(subagent_type: "code-simple")
301
+
302
+ sonata-code-simple (native — runs in Claude Code's own loop)
303
+ │ model: sonata-code-simple
304
+
305
+ router (sonata serve)
306
+ │ resolves the alias against [tiers.code].simple, tries each
307
+ │ candidate in rank order, skips one in cooldown after a failure
308
+
309
+ litellm → flash-1 (or the next-ranked model, on failure)
310
+ ```
311
+
312
+ The router is the only place ranking lives — the agent just sends its alias
313
+ and gets an answer from whichever model actually worked. If every native
314
+ candidate for a tier fails, the router returns 529 naming the fallback:
315
+ `sonata dispatch --tier code-simple`, which the agent runs itself to reach a
316
+ model through its own harness (OpenCode, Codex, Pi, or Reasonix) instead:
317
+
318
+ ```
319
+ sonata dispatch --tier code-simple "<task>"
320
+ │ resolves the tier's harness-routed candidates, in rank order
321
+
322
+ sonata CLI
323
+ │ composes role prompt + CLAUDE.md + task
324
+ │ launches harness in a detached tmux session, waits for it
325
+
326
+ opencode → deepseek-v4-flash (next candidate on a degraded/empty finish)
327
+ ```
328
+
329
+ `sonata dispatch` blocks until the run finishes, needs approval, or stalls,
330
+ trying the next ranked candidate on a thrown launch, a degraded finish, or an
331
+ empty report; `sonata wait`/`sonata approve` resume or unblock a specific run
332
+ by id. `sonata tail` remains available as a human/debugging view of any run.
333
+
334
+ Completion is read from an exit sentinel and a report file, never scraped from
335
+ the terminal. If a harness dies without writing a report, sonata returns the
336
+ captured pane and marks the result `degraded` — so you always know when a
337
+ result is untrustworthy.
338
+
339
+ Progress comes from diffing the tmux pane. You can attach to any live run:
340
+
341
+ ```bash
342
+ tmux attach -t sonata-<id> # -r for read-only
343
+ ```
344
+
345
+ Attaching means you can correct a cheap model that is going off the rails
346
+ instead of paying for the rest of a wasted run.
347
+
348
+ `run_timeout_seconds` is a hard cap, enforced by a watchdog inside the launched
349
+ shell rather than by `sonata tail` — a runaway run with nobody watching is
350
+ exactly the case that needs bounding. On expiry the whole process group is
351
+ killed and the run is reported `DONE`, `degraded`, with a report beginning
352
+ `[timed out: …]`.
353
+
354
+ ## Commands
355
+
356
+ | Command | Purpose |
357
+ |---|---|
358
+ | `sonata init` | Set up sonata in this project (interactive); `--prune` removes stale generated agents |
359
+ | `sonata doctor` | Check tmux, harnesses, auth and versions |
360
+ | `sonata sync` | Regenerate agent files from `sonata.toml`; `--prune` removes stale generated agents |
361
+ | `sonata run` | Launch a run, print its id |
362
+ | `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 |
363
+ | `sonata tail` | Human/debugging view of a run |
364
+ | `sonata approve` | Answer a pending approval |
365
+ | `sonata log <id>` | Print a run's whole transcript |
366
+ | `sonata verify <id> [--model <key>]` | Verify a completed run |
367
+ | `sonata auth` | Manage native-path gateway keys (`list`, `add <gateway>`, `remove <gateway>`, `login <gateway>`) |
368
+ | `sonata catalog [update]` | Show the cached Artificial Analysis catalog's age, or refresh it (needs a stored `artificialanalysis` key). `sonata doctor` warns when it goes stale |
369
+ | `sonata serve` | Run the native router and its managed LiteLLM child (`--daemon` detaches) |
370
+ | `sonata restart` | Kill whatever sonata router currently holds the port and start a fresh daemon |
371
+ | `sonata code` | Launch a Claude Code session routed through the local proxy (passes `claude` args through) |
372
+ | `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 |
373
+ | `sonata route auto\|manual [--global]` | Route each session for its lifetime via SessionStart/SessionEnd hooks, keeping Remote Control |
374
+ | `sonata usage [--since 7d] [--by model\|role\|tier\|gateway\|session\|project] [--session <id>] [--json]` | Tokens and cost from the router's ledger (**native path only** — `sonata dispatch` runs never transit the router and are unobservable); unpriced volume is reported separately, never folded into the total |
375
+ | `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` |
376
+ | `sonata runs [--json]` | List this project's dispatch runs |
377
+ | `sonata gc` | Kill finished tmux sessions |
378
+
379
+ ## Documentation
380
+
381
+ The essentials are above. Deep-dive reference lives in
382
+ [`docs/guide/`](docs/guide/):
383
+
384
+ | Guide | Covers |
385
+ |---|---|
386
+ | [Native path](docs/guide/native-path.md) | Running foreign models inside Claude Code's own loop through the local routing proxy |
387
+ | [Codex subscription auth](docs/guide/codex-subscription.md) | Authenticating the `codex-oauth` gateway against a ChatGPT subscription |
388
+ | [Permission modes](docs/guide/permission-modes.md) | How each harness honours Claude Code's permission modes |
389
+ | [Configuration](docs/guide/configuration.md) | The `sonata.toml` schema, resolution order, and roles/tiers |
390
+ | [Troubleshooting](docs/guide/troubleshooting.md) | Symptom → cause table |
391
+ | [Security](docs/guide/security.md) | What sonata does and doesn't protect against |
392
+ | [Limitations](docs/guide/limitations.md) | Known gaps worth knowing before depending on this |
393
+ | [Adding a harness](docs/guide/adding-a-harness.md) | The adapter extension point |
394
+
395
+ Where this is headed: [Roadmap to 1.0](docs/roadmap.md).
396
+
397
+ Design history — every feature's spec and implementation plan, kept as a
398
+ permanent record — is indexed in [`docs/superpowers/`](docs/superpowers/).
399
+ Architecture reviews are in [`docs/reviews/`](docs/reviews/). What using
400
+ sonata to implement sonata taught about sizing and verifying dispatched work
401
+ is in [`docs/dispatching-work-through-sonata.md`](docs/dispatching-work-through-sonata.md).
402
+
403
+ ## Development
404
+
405
+ ```bash
406
+ npm install
407
+ npm test # 1177 tests; needs tmux
408
+ npm run typecheck
409
+ npm run build
410
+ ```
411
+
412
+ The test suite runs against a **fake harness** — a scripted binary replaying a
413
+ normal run, a crash, a real captured approval prompt, a hang that the watchdog
414
+ must kill, a clean exit with no report, and a harness-written report — so the
415
+ whole engine is covered with no API spend and no harness installed.
416
+
417
+ See [Adding a harness](docs/guide/adding-a-harness.md) for the extension
418
+ point, and [Documentation](#documentation) above for the design-history index.
419
+
420
+ ## Contributing
421
+
422
+ Issues and pull requests are welcome.
423
+
424
+ - Run `npm test` and `npm run typecheck` before opening a PR; CI runs both on
425
+ Linux with tmux installed.
426
+ - Add tests for behaviour you change. The suite needs no API keys.
427
+ - Prefer evidence to inference: if a change depends on how a harness behaves,
428
+ say how you verified it. A captured fixture beats a plausible regex.
429
+ - Keep harness-specific knowledge inside its adapter.
430
+
431
+ ## Changelog
432
+
433
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
434
+
435
+ ## License
436
+
437
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,83 @@
1
+ import { isReadOnlyRole, loadConfig } from '../config.js';
2
+ import { homedir } from 'node:os';
3
+ const PROMPT_PATTERNS = [];
4
+ function shellQuote(value) {
5
+ return `'${value.replace(/'/g, `'\\''`)}'`;
6
+ }
7
+ function buildScript(input) {
8
+ const readOnly = isReadOnlyRole(input.role);
9
+ const permissionMode = readOnly ? 'plan' : input.mode;
10
+ const flags = [
11
+ '-p',
12
+ `--model ${shellQuote(input.modelId)}`,
13
+ `--permission-mode ${permissionMode}`,
14
+ ];
15
+ // `--allowedTools <value>` (space form) is variadic in claude's own CLI
16
+ // parser: it keeps consuming every subsequent bare argument — including
17
+ // the prompt itself — until the next `--flag`. Probed directly: the
18
+ // prompt text ended up split on commas/whitespace into garbage
19
+ // "allowedTools" rules, and `-p` then had no prompt argument left at all.
20
+ // The `=` form binds exactly one value and does not swallow what follows.
21
+ if (readOnly)
22
+ flags.push('--allowedTools=Read,Grep,Glob,Bash');
23
+ // Resolve the actual router URL from config rather than inheriting from
24
+ // the parent env — the parent is typically an unproxied session where
25
+ // ANTHROPIC_BASE_URL is unset.
26
+ let routerUrl = '';
27
+ let contextWindow = '';
28
+ try {
29
+ const config = loadConfig(input.cwd, homedir());
30
+ if (config.native) {
31
+ routerUrl = `http://localhost:${config.native.ports.router}`;
32
+ const windows = Object.values(config.native.models).map(m => m.contextWindow);
33
+ if (windows.length > 0)
34
+ contextWindow = String(Math.min(...windows));
35
+ }
36
+ }
37
+ catch {
38
+ // No config or no native table — the script will run claude without
39
+ // proxy routing, which means the API rejects the unknown model id.
40
+ // That failure is legible; swallowing here keeps the adapter from
41
+ // crashing before it can produce it.
42
+ }
43
+ const envLines = routerUrl
44
+ ? [
45
+ `export ANTHROPIC_BASE_URL=${shellQuote(routerUrl)}`,
46
+ ...(contextWindow ? [`export CLAUDE_CODE_MAX_CONTEXT_TOKENS=${shellQuote(contextWindow)}`] : []),
47
+ ]
48
+ : [];
49
+ const script = [
50
+ '#!/bin/bash',
51
+ 'set -o pipefail',
52
+ ...envLines,
53
+ `cd ${shellQuote(input.cwd)} || exit 97`,
54
+ // No tee — both anomalies observed on this adapter (a hang, and a run
55
+ // that fabricated instead of calling tools) happened with claude -p piped
56
+ // through tee, and neither reproduced without it. Stdout also must NOT be
57
+ // report.md: the instructions ask the model to Write report.md itself, and
58
+ // two writers to one file corrupt whichever finishes second. The final
59
+ // message lands in last-message.txt as the fallback report instead.
60
+ `claude ${flags.join(' ')} "$(cat ${shellQuote(input.instructionsPath)})" > ${shellQuote(`${input.runDir}/last-message.txt`)} 2>&1`,
61
+ `echo $? > ${shellQuote(`${input.runDir}/exit`)}`,
62
+ '',
63
+ ].join('\n');
64
+ // silentUntilExit: stdout goes to last-message.txt (see the no-tee comment
65
+ // above), so the pane stays unchanged for the whole run and pane-silence
66
+ // stall detection would mark every long run STALLED.
67
+ return { script, interactive: false, canWriteReport: !readOnly, silentUntilExit: true };
68
+ }
69
+ export const claudeAdapter = {
70
+ name: 'claude',
71
+ versionCommand: ['claude', '--version'],
72
+ supportedVersions: '>=2.1.0 <3.0.0',
73
+ pathPrepend: [],
74
+ plan: buildScript,
75
+ canPromptForApproval: false,
76
+ promptPatterns: PROMPT_PATTERNS,
77
+ describePrompt() {
78
+ return null;
79
+ },
80
+ approveKeys: { yes: [], no: [] },
81
+ fallbackReportFile: 'last-message.txt',
82
+ };
83
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/adapters/claude.ts"],"names":[],"mappings":"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"}