@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
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook: makes sure the sonata router is up when a session is
3
+ // configured (via `sonata route on`) to route through it. The hook and the
4
+ // router are launched by the same install, so it knows the port directly.
5
+ //
6
+ // Unlike `sonata code`, which auto-starts the daemon as part of launching
7
+ // claude, a routed session is just `claude` — nothing of sonata runs to start
8
+ // the router. Without this hook the first thing such a session does is cache
9
+ // the connection error from a router that is not there.
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+
15
+ const port = Number(process.argv[2]);
16
+ const global = process.argv[3] === '--global';
17
+
18
+ /**
19
+ * The sonata.toml this session should be routed through, mirroring
20
+ * `configPath()` in src/config.ts (project config wins outright over the
21
+ * machine one) — duplicated here in plain JS because this hook runs
22
+ * standalone, not through the built TypeScript.
23
+ */
24
+ function expectedConfigPath() {
25
+ const home = homedir();
26
+ if (global) {
27
+ const globalPath = join(home, '.config', 'sonata', 'sonata.toml');
28
+ return existsSync(globalPath) ? globalPath : null;
29
+ }
30
+ const localPath = join(process.cwd(), 'sonata.toml');
31
+ if (existsSync(localPath)) return localPath;
32
+ const globalPath = join(home, '.config', 'sonata', 'sonata.toml');
33
+ return existsSync(globalPath) ? globalPath : null;
34
+ }
35
+
36
+ if (Number.isInteger(port) && port > 0) {
37
+ const probeHealth = async (timeoutMs) => {
38
+ const ctrl = new AbortController();
39
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
40
+ try {
41
+ const res = await fetch(`http://localhost:${port}/__sonata_health`, { signal: ctrl.signal });
42
+ if (!res.ok) return null;
43
+ const body = await res.json();
44
+ return body?.sonata === true ? body : null;
45
+ } catch {
46
+ return null;
47
+ } finally {
48
+ clearTimeout(timer);
49
+ }
50
+ };
51
+
52
+ const existing = await probeHealth(1000);
53
+ if (existing) {
54
+ const expected = expectedConfigPath();
55
+ // When this session can resolve its own expected config, the existing
56
+ // router must prove it is the same one — a router that cannot or does
57
+ // not report its own configPath (an older sonata build, or one whose own
58
+ // resolution failed) is treated the same as a mismatch, not silently
59
+ // trusted. When this session cannot resolve ANY config of its own
60
+ // (`expected === null`), there is nothing to enforce and the existing
61
+ // router is accepted unchanged.
62
+ if (expected !== null) {
63
+ if (typeof existing.configPath !== 'string') {
64
+ console.error(
65
+ `sonata: router port ${port} answered but did not report which sonata configuration ` +
66
+ 'it is running (too old, or its own config resolution failed) — refusing to trust it. ' +
67
+ 'Restart it with `sonata restart` once confirmed to be this project\'s own router.',
68
+ );
69
+ process.exit(1);
70
+ }
71
+ if (existing.configPath !== expected) {
72
+ console.error(
73
+ `sonata: router port ${port} is already serving a different sonata configuration ` +
74
+ `(${existing.configPath}) than this session resolves to (${expected}). Two ` +
75
+ 'projects cannot share one router port — set a different [native.ports].router ' +
76
+ 'in one of the two configs, then restart the router with `sonata restart`.',
77
+ );
78
+ process.exit(1);
79
+ }
80
+ }
81
+ process.exit(0);
82
+ }
83
+
84
+ try {
85
+ // Global routing is one shared router for every project — it has to
86
+ // resolve the *machine* config, not whichever project's session happens
87
+ // to trigger this hook first. Starting it from the machine config's own
88
+ // directory (~/.config/sonata) — not `home` itself — forces that
89
+ // resolution deterministically even when a stray `~/sonata.toml` (a
90
+ // known leftover some upgrades still have) exists: configPath()'s first
91
+ // check is `join(cwd, 'sonata.toml')`, so pointing `cwd` at
92
+ // `~/.config/sonata` makes that check land exactly on the real machine
93
+ // config, with the stray file never in the search path at all. A plain
94
+ // (project-scoped) install keeps inheriting this hook's own cwd.
95
+ const daemon = spawn('sonata', ['serve', '--daemon'], {
96
+ detached: true,
97
+ stdio: 'ignore',
98
+ ...(global ? { cwd: join(homedir(), '.config', 'sonata') } : {}),
99
+ });
100
+ daemon.unref();
101
+
102
+ const deadline = Date.now() + 10_000;
103
+ let started = null;
104
+ while (Date.now() < deadline) {
105
+ started = await probeHealth(1000);
106
+ if (started) break;
107
+ await new Promise((r) => setTimeout(r, 500));
108
+ }
109
+ if (started) {
110
+ // A concurrent `route on` in another project could have won the race
111
+ // to bind this same default port with ITS daemon between the probe
112
+ // above and this wait loop breaking — verify identity again now that
113
+ // something is confirmed to be listening, the same check the
114
+ // pre-existing branch above already does.
115
+ const expected = expectedConfigPath();
116
+ if (expected !== null) {
117
+ if (typeof started.configPath !== 'string') {
118
+ console.error(
119
+ `sonata: router port ${port} answered but did not report which sonata configuration ` +
120
+ 'it is running (too old, or its own config resolution failed) — refusing to trust it. ' +
121
+ 'Restart it with `sonata restart` once confirmed to be this project\'s own router.',
122
+ );
123
+ process.exit(1);
124
+ }
125
+ if (started.configPath !== expected) {
126
+ console.error(
127
+ `sonata: router port ${port} is already serving a different sonata configuration ` +
128
+ `(${started.configPath}) than this session resolves to (${expected}). Two ` +
129
+ 'projects cannot share one router port — set a different [native.ports].router ' +
130
+ 'in one of the two configs, then restart the router with `sonata restart`.',
131
+ );
132
+ process.exit(1);
133
+ }
134
+ }
135
+ }
136
+ } catch {
137
+ // A hook must never break the session it observes.
138
+ }
139
+ }
140
+ process.exit(0);
@@ -0,0 +1,12 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/capture-mode.mjs\"" }
8
+ ]
9
+ }
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart / SessionEnd hook for `sonata route auto`: turns routing on for
3
+ // the life of a session and off again when the last one ends, so a session
4
+ // launches with a clean settings file (keeping Remote Control) and is routed
5
+ // anyway from its first request onward.
6
+ //
7
+ // All of the work is `sonata route session-<phase>` in the CLI, where it is
8
+ // ordinary tested code. This script only finds the session id and stays quiet:
9
+ // a hook that throws is a hook that breaks the session it was meant to serve,
10
+ // so it always exits 0.
11
+ import { spawn } from 'node:child_process';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ const phase = process.argv[2] === 'end' ? 'end' : 'start';
16
+ const global = process.argv[3] === '--global';
17
+ const cli = join(dirname(dirname(fileURLToPath(import.meta.url))), 'dist', 'cli.js');
18
+
19
+ /** Claude Code sends the hook a JSON payload on stdin; `session_id` is in it. */
20
+ async function readSessionId() {
21
+ try {
22
+ const chunks = [];
23
+ for await (const chunk of process.stdin) chunks.push(chunk);
24
+ const doc = JSON.parse(Buffer.concat(chunks).toString('utf8'));
25
+ return typeof doc.session_id === 'string' ? doc.session_id : '';
26
+ } catch {
27
+ return '';
28
+ }
29
+ }
30
+
31
+ const sessionId = await readSessionId();
32
+ if (sessionId === '') process.exit(0);
33
+
34
+ await new Promise((resolve) => {
35
+ try {
36
+ const args = [cli, 'route', `session-${phase}`, '--id', sessionId];
37
+ if (global) args.push('--global');
38
+ const child = spawn(process.execPath, args, {
39
+ stdio: 'ignore',
40
+ });
41
+ child.on('exit', resolve);
42
+ child.on('error', resolve);
43
+ } catch {
44
+ resolve();
45
+ }
46
+ });
47
+ process.exit(0);
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // SubagentStart / SubagentStop hook for `sonata route auto`.
3
+ //
4
+ // Routing is off at rest, so every session launches from a settings file with
5
+ // no ANTHROPIC_BASE_URL in it and keeps Remote Control. It is turned on only
6
+ // while a foreign-model subagent is actually running, and off again when the
7
+ // last one finishes.
8
+ //
9
+ // This replaced turning routing on for the whole life of a session. Two things
10
+ // were measured to get here: adding the env is picked up by a running session
11
+ // within seconds — which is why a subagent's very first request is already
12
+ // routed — while removing it is only observed eventually, which is why routing
13
+ // must stay on for as long as the subagent runs rather than being cleaned up
14
+ // on a timer.
15
+ //
16
+ // All of the work is `sonata route subagent-<phase>` in the CLI, where it is
17
+ // ordinary tested code. This script only finds the agent id and stays quiet: a
18
+ // hook that throws is a hook that breaks the subagent it was meant to serve,
19
+ // so it always exits 0.
20
+ import { spawn } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { dirname, join } from 'node:path';
23
+
24
+ const phase = process.argv[2] === 'stop' ? 'stop' : 'start';
25
+ const global = process.argv[3] === '--global';
26
+ const cli = join(dirname(dirname(fileURLToPath(import.meta.url))), 'dist', 'cli.js');
27
+
28
+ /** Claude Code sends the hook a JSON payload on stdin; `agent_id` is in it. */
29
+ async function readAgentId() {
30
+ try {
31
+ const chunks = [];
32
+ for await (const chunk of process.stdin) chunks.push(chunk);
33
+ const doc = JSON.parse(Buffer.concat(chunks).toString('utf8'));
34
+ return typeof doc.agent_id === 'string' ? doc.agent_id : '';
35
+ } catch {
36
+ return '';
37
+ }
38
+ }
39
+
40
+ const agentId = await readAgentId();
41
+ if (agentId === '') process.exit(0);
42
+
43
+ await new Promise((resolve) => {
44
+ try {
45
+ const args = [cli, 'route', `subagent-${phase}`, '--id', agentId];
46
+ if (global) args.push('--global');
47
+ const child = spawn(process.execPath, args, { stdio: 'ignore' });
48
+ child.on('exit', resolve);
49
+ child.on('error', resolve);
50
+ } catch {
51
+ resolve();
52
+ }
53
+ });
54
+ process.exit(0);
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@zhihaohong52/sonata",
3
+ "version": "0.4.0",
4
+ "description": "Foreign-model subagents for Claude Code — dispatch cheaper or different models through their own harnesses, as ordinary subagents",
5
+ "keywords": [
6
+ "claude-code",
7
+ "subagent",
8
+ "opencode",
9
+ "codex",
10
+ "tmux",
11
+ "llm",
12
+ "ai-agent"
13
+ ],
14
+ "homepage": "https://github.com/zhihaohong52/sonata#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/zhihaohong52/sonata/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/zhihaohong52/sonata.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "James (https://github.com/zhihaohong52)",
24
+ "type": "module",
25
+ "bin": {
26
+ "sonata": "dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "roles",
31
+ "hooks",
32
+ "skills",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run",
40
+ "dev": "tsx src/cli.ts",
41
+ "prepublishOnly": "npm run build && npm test",
42
+ "release": "node scripts/release.mjs"
43
+ },
44
+ "engines": {
45
+ "node": ">=22"
46
+ },
47
+ "dependencies": {
48
+ "@inkjs/ui": "^2.0.0",
49
+ "ink": "^7.1.1",
50
+ "react": "^19.2.8",
51
+ "smol-toml": "^1.3.1"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.10.0",
55
+ "@types/react": "^19.2.18",
56
+ "ink-testing-library": "^4.0.0",
57
+ "tsx": "^4.19.2",
58
+ "typescript": "^5.7.2",
59
+ "vitest": "^2.1.8"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }
package/roles/code.md ADDED
@@ -0,0 +1,13 @@
1
+ You are implementing a change in an existing codebase.
2
+
3
+ Work directly in the repository you have been started in. Follow the existing
4
+ patterns, naming, and test conventions of the surrounding code rather than
5
+ importing conventions from elsewhere.
6
+
7
+ Make the smallest change that fully accomplishes the task. Do not refactor
8
+ unrelated code, do not add features that were not requested, and do not leave
9
+ commented-out code behind.
10
+
11
+ If the repository has tests, run them before you finish and make sure the ones
12
+ related to your change pass. If you cannot make them pass, say so explicitly
13
+ rather than describing the work as complete.
@@ -0,0 +1,9 @@
1
+ You are exploring a codebase you have never seen. Do not modify any files.
2
+
3
+ Locate where things live and answer questions about the code. Report what you
4
+ actually find, and say plainly when you could not find something rather than
5
+ guessing.
6
+
7
+ Report concrete paths with `file.ts:123` line references so the reader can jump
8
+ straight to the code. If a search turns up nothing, say so directly instead of
9
+ inventing a location.
package/roles/plan.md ADDED
@@ -0,0 +1,10 @@
1
+ You are producing an implementation plan, not performing it. Do not modify any
2
+ file.
3
+
4
+ Name the exact files to create or modify, and state the order of work and what
5
+ to verify at each step. Flag anything ambiguous in the request rather than
6
+ silently choosing one interpretation.
7
+
8
+ Keep the plan concrete and minimal. If the request leaves a decision open, call
9
+ it out and say what options exist, but do not expand the scope beyond what was
10
+ asked.
@@ -0,0 +1,11 @@
1
+ You are reviewing code you did not write. Do not modify any files.
2
+
3
+ Report only defects you can point at concretely: a file, a line, and what goes
4
+ wrong. For each one, give the specific input or state that triggers it and the
5
+ resulting incorrect behaviour.
6
+
7
+ Rank findings by severity, most severe first. Correctness bugs outrank style.
8
+
9
+ Do not pad the review. If you find nothing serious, say so plainly. A short
10
+ review naming two real bugs is more useful than a long one listing twenty
11
+ preferences.
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: sonata-loop
3
+ description: Use when building a feature end-to-end with sonata tier agents — plans the work, routes each task to a difficulty tier, gates every change behind review, and escalates tiers on repeated failure.
4
+ ---
5
+
6
+ # Loop engineering with sonata tier agents
7
+
8
+ Run feature development as a loop over sonata's tier agents. You (the
9
+ orchestrating session) judge difficulty and drive the loop; the agents do the
10
+ work on foreign models. All of them require a routed session (`sonata route
11
+ auto`) — if a tier agent errors with "all native routes … failed", fall back
12
+ to `sonata dispatch --tier <role>-<tier> --task-file <path>` in Bash (or pass
13
+ the task text directly as the trailing argument).
14
+
15
+ ## Difficulty heuristic
16
+
17
+ - **simple** — mechanical, well-specified, contained: single-file changes,
18
+ bulk edits, scaffolding, test-writing against a clear spec.
19
+ - **complex** — cross-cutting, ambiguous, design-sensitive, or needs
20
+ sustained reasoning: multi-file refactors, API design, debugging unknowns.
21
+ - When unsure, use `-complex`.
22
+
23
+ ## The loop
24
+
25
+ 1. **Plan.** Dispatch `plan-complex` with the feature description. Ask it for
26
+ a numbered task list with per-task difficulty guesses.
27
+ 2. **Route.** For each task, judge difficulty yourself (the plan's guess is
28
+ advice, not binding) and dispatch `code-simple` or `code-complex` with a
29
+ self-contained task description — name the files to touch and the files to
30
+ leave alone; never say "see the plan".
31
+ 3. **Gate.** After each task, dispatch `review-simple` on the diff. Findings →
32
+ dispatch a fix at the same tier, then re-review.
33
+ - **Escalation rule:** a task that fails review twice at `simple` re-runs at
34
+ `complex` from scratch.
35
+ - **Loop bound:** at most 3 fix iterations per task; then stop and surface
36
+ the findings to the user.
37
+ 4. **Final gate.** When every task passed, dispatch `review-complex` over the
38
+ whole change. Findings loop back through step 3.
39
+
40
+ ## When not to loop
41
+
42
+ A single contained change does not need the loop — dispatch one `code-*`
43
+ agent directly, review it yourself or with one `review-simple` pass, done.