@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,152 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { isReadOnlyRole } from '../config.js';
4
+ const run = promisify(execFile);
5
+ /**
6
+ * Pi adapter.
7
+ *
8
+ * Pi "intentionally does not include built-in MCP, sub-agents, permission
9
+ * popups, plan mode, to-dos, or background bash" — there are no approval
10
+ * prompts and no sandbox to lean on. A write-capable role therefore runs with
11
+ * all of pi's built-in tools or not at all, and sonata must map `default` mode
12
+ * the same way it does for opencode: refuse rather than run ungated.
13
+ *
14
+ * `--mode json` emits a working NDJSON event stream, which would give sonata
15
+ * machine-readable output, but text mode keeps `tmux attach` readable — so
16
+ * sonata sticks with `-p`. JSON mode remains a seam for future work.
17
+ */
18
+ /**
19
+ * Empty, and that is the finding rather than an omission.
20
+ *
21
+ * Pi documents that it has no permission popups, so no pattern could ever
22
+ * fire. Its non-interactive modes (`-p`, `--mode json`, `--mode rpc`) never
23
+ * show a project-trust prompt either, so there is no codex-style trust blocker
24
+ * to match.
25
+ */
26
+ const PROMPT_PATTERNS = [];
27
+ function shellQuote(s) {
28
+ return `'${s.replace(/'/g, `'\\''`)}'`;
29
+ }
30
+ function buildScript(input) {
31
+ const readOnly = isReadOnlyRole(input.role) || input.mode === 'plan';
32
+ // `default` means "ask before doing anything irreversible". Pi cannot ask —
33
+ // it has no approval prompts and no sandbox — so honouring the mode is
34
+ // impossible. Running anyway would make a sonata subagent more permissive
35
+ // than the session that dispatched it, silently. Refuse instead, and say why.
36
+ //
37
+ // Read-only roles are exempt: there is nothing for them to ask about.
38
+ if (input.mode === 'default' && !readOnly) {
39
+ throw new Error('sonata: pi cannot ask for approval, so it cannot honour `default` ' +
40
+ 'permission mode. Re-dispatch in acceptEdits or bypassPermissions to run ' +
41
+ 'it ungated, or use a codex model, whose TUI does prompt.');
42
+ }
43
+ // `--model` accepts a combined provider/id form as a single flag, e.g.
44
+ // opencode-go/deepseek-v4-flash; no separate `--provider` flag is needed.
45
+ // `--no-context-files` stops pi re-reading AGENTS.md / CLAUDE.md: sonata
46
+ // composes its own instructions and injects repo context, so pi would just
47
+ // duplicate it.
48
+ const flags = ['-p', `--model ${shellQuote(input.modelId)}`, '--no-context-files'];
49
+ // `--tools` is an allowlist, genuinely enforced: a read-only role gets only
50
+ // the read-side tools and cannot write even in bypassPermissions.
51
+ if (readOnly)
52
+ flags.push('--tools read,grep,find,ls');
53
+ // The instructions path is given in the prompt rather than piped, so stdin
54
+ // stays free and the prompt survives the harness's own formatting.
55
+ const prompt = 'Follow the instructions in the attached file.\n\n' +
56
+ `Instructions file: ${input.instructionsPath}`;
57
+ const script = [
58
+ '#!/bin/bash',
59
+ 'set -o pipefail',
60
+ 'export PATH="$HOME/.local/bin:$PATH"',
61
+ `cd ${shellQuote(input.cwd)} || exit 97`,
62
+ `pi ${flags.join(' ')} ${shellQuote(prompt)} 2>&1 | tee -a ${shellQuote(`${input.runDir}/harness.log`)}`,
63
+ `echo $? > ${shellQuote(`${input.runDir}/exit`)}`,
64
+ '',
65
+ ].join('\n');
66
+ // A read-only allowlist removes the write tool outright, so the model cannot
67
+ // write report.md either. That is the cost of pi's enforcement being real
68
+ // rather than advisory, and sonata must not report such a run as degraded.
69
+ return { script, interactive: false, canWriteReport: !readOnly };
70
+ }
71
+ const NO_PROVIDER = {
72
+ severity: 'error',
73
+ message: 'pi has no usable model provider configured',
74
+ fix: 'pi auth check --provider <name>',
75
+ };
76
+ /**
77
+ * The listing always starts with a `provider model context ...` header, so
78
+ * "output is non-empty" is not evidence that any model exists — the header
79
+ * alone would report a broken install as healthy.
80
+ */
81
+ const PI_HEADER = /^provider\s+model\b/;
82
+ /**
83
+ * Parses `pi --list-models`, a whitespace-aligned table whose first two
84
+ * columns are the provider and the model.
85
+ *
86
+ * Rows are read only AFTER the header, because pi does not always print a
87
+ * table. With no provider configured it prints prose, captured verbatim in
88
+ * `tests/fixtures/pi/no-models.txt`:
89
+ *
90
+ * No models available. Use /login to log into a provider via OAuth or …
91
+ *
92
+ * Splitting that on whitespace yielded the phantom model `No/models`, which
93
+ * `countModelRows` then counted as one real row — so `piHealth` reported an
94
+ * unconfigured pi as healthy, and `sonata init` offered `No/models` for
95
+ * selection. Gating on the header rejects every prose form at once, including
96
+ * `No models matching "<pattern>"` from a search that matched nothing.
97
+ */
98
+ export function parsePiRefs(stdout) {
99
+ const out = [];
100
+ let inTable = false;
101
+ for (const raw of stdout.split('\n')) {
102
+ const line = raw.trim();
103
+ if (line.length === 0)
104
+ continue;
105
+ if (!inTable) {
106
+ // Anything before the header is prose, not a truncated table.
107
+ if (PI_HEADER.test(line))
108
+ inTable = true;
109
+ continue;
110
+ }
111
+ const cols = line.split(/\s+/);
112
+ // A real row carries all six columns. Fewer means the line is not a row,
113
+ // and must not become a ref with an undefined id.
114
+ if (cols.length < 2)
115
+ continue;
116
+ const [provider, id] = cols;
117
+ out.push({ harness: 'pi', provider, id, ref: `${provider}/${id}` });
118
+ }
119
+ return out;
120
+ }
121
+ /** Counts real model rows, for the health check. */
122
+ export function countModelRows(stdout) {
123
+ return parsePiRefs(stdout).length;
124
+ }
125
+ async function piHealth(_env) {
126
+ try {
127
+ // Hard timeout: pi can block when a provider is unreachable, and doctor
128
+ // must never hang. `--list-models` needs a configured provider, so an
129
+ // empty list means there is nothing to run against.
130
+ const { stdout } = await run('pi', ['--list-models'], { timeout: 5_000 });
131
+ return countModelRows(stdout) > 0 ? [] : [NO_PROVIDER];
132
+ }
133
+ catch {
134
+ return [NO_PROVIDER];
135
+ }
136
+ }
137
+ export const piAdapter = {
138
+ name: 'pi',
139
+ versionCommand: ['pi', '--version'],
140
+ supportedVersions: '>=0.84.0 <1.0.0',
141
+ pathPrepend: ['$HOME/.local/bin'],
142
+ plan: buildScript,
143
+ canPromptForApproval: false,
144
+ promptPatterns: PROMPT_PATTERNS,
145
+ describePrompt() {
146
+ return null;
147
+ },
148
+ /** Nothing to send: pi never waits for an answer. */
149
+ approveKeys: { yes: [], no: [] },
150
+ health: piHealth,
151
+ };
152
+ //# sourceMappingURL=pi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi.js","sourceRoot":"","sources":["../../src/adapters/pi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,358 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { isReadOnlyRole } from '../config.js';
6
+ const run = promisify(execFile);
7
+ /**
8
+ * Reasonix adapter.
9
+ *
10
+ * Reasonix is the second harness after codex that can genuinely ask a human for
11
+ * approval, so it is the second that can honour `default` mode instead of
12
+ * refusing it. Everything below was probed against reasonix v1.26.0; the
13
+ * captured panes are in tests/fixtures/panes/reasonix-*.txt.
14
+ *
15
+ * The mode mapping is not the obvious one:
16
+ *
17
+ * - `plan` is REFUSED by `reasonix run` — "--permission-mode plan requires an
18
+ * interactive session", exit 2. Read-only work therefore uses `dontAsk`,
19
+ * which denies without prompting, rather than plan mode.
20
+ * - `dontAsk` is real enforcement. Probed: a run asked to read a file and then
21
+ * write one read it fine and was refused both the write tool AND the shell
22
+ * fallback (`printf … > report.md`), reporting "denied by permission policy".
23
+ * It cannot write report.md either, hence `canWriteReport: false`.
24
+ * - `acceptEdits` and `bypassPermissions` write unprompted, probed both ways.
25
+ * - `-y` / `--auto` is NOT used anywhere here. It is an alias for reasonix's
26
+ * own `auto` mode, which is wider than Claude Code's `auto` — it skips
27
+ * risk-based prompts for things like `git push`. Since sonata maps Claude's
28
+ * `auto` onto `acceptEdits`, reaching for the similarly named flag would
29
+ * silently widen permissions. Always pass `--permission-mode` explicitly.
30
+ */
31
+ /**
32
+ * Written from real captured output, never from the docs. Three shapes block a
33
+ * run, and the third is the one that would otherwise be missed: the model
34
+ * asking the *user* a question stops a dispatch just as hard as an approval.
35
+ *
36
+ * Deliberately narrow. Reasonix relays the model's own prose into the pane, so
37
+ * a loose /allow|deny/ would park a run in PAUSED over a sentence the model
38
+ * merely wrote.
39
+ */
40
+ const PROMPT_PATTERNS = [
41
+ // The tool approval card: "Will call tool write file report.md."
42
+ /^\s*will call tool\b/i,
43
+ // Every reasonix selection list prints this footer, and only when waiting.
44
+ /↑\/↓\s*navigate\s*·\s*enter select/i,
45
+ // The plan-confirmation prompt. The adapter never selects plan mode, but a
46
+ // user attached to the pane can switch into it with Shift+Tab, and a run
47
+ // parked here is stalled until somebody answers.
48
+ /^\s*⏸\s*plan ready above/i,
49
+ // The model asking the user a multi-select question mid-run.
50
+ /↑\/↓\s*move\s*·\s*number to pick/i,
51
+ ];
52
+ /** Longest prompt block worth relaying; reasonix's lists are far shorter. */
53
+ const MAX_PROMPT_LINES = 14;
54
+ /** Line that introduces a block, used to walk back up from a matched footer. */
55
+ const PROMPT_HEAD = /^\s*(will call tool\b|⏸\s*plan ready above)/i;
56
+ /**
57
+ * The pane text reasonix prints only when it is idle and ready for input.
58
+ *
59
+ * Used by the interactive launch script to know when the composer exists.
60
+ * Matching the composer glyph instead would fire on any selection list, which
61
+ * also draws `❯`.
62
+ */
63
+ const READY_MARKER = 'Shift+Tab ask/auto/plan';
64
+ function shellQuote(s) {
65
+ return `'${s.replace(/'/g, `'\\''`)}'`;
66
+ }
67
+ /**
68
+ * `reasonix run --permission-mode <x>`, for everything except `default`.
69
+ *
70
+ * Read-only roles pin `dontAsk` whatever the session mode says, so a review or
71
+ * explore dispatch can never write even under bypassPermissions.
72
+ */
73
+ function permissionModeFor(input, readOnly) {
74
+ if (readOnly)
75
+ return 'dontAsk';
76
+ switch (input.mode) {
77
+ case 'acceptEdits':
78
+ return 'acceptEdits';
79
+ case 'bypassPermissions':
80
+ return 'bypassPermissions';
81
+ default:
82
+ // `plan` never reaches here — it is read-only, so it took the branch
83
+ // above. Anything unrecognised fails closed.
84
+ return 'dontAsk';
85
+ }
86
+ }
87
+ function buildScript(input) {
88
+ // A read-only role must never be able to write, whatever the session mode
89
+ // says. `plan` mode is read-only for every role.
90
+ const readOnly = isReadOnlyRole(input.role) || input.mode === 'plan';
91
+ const interactive = input.mode === 'default' && !readOnly;
92
+ const script = interactive ? interactiveScript(input) : headlessScript(input, readOnly);
93
+ // Under `dontAsk` the model is refused the write tool and the shell fallback
94
+ // alike, so it cannot write report.md. Nothing went wrong; sonata takes the
95
+ // terminal output as the report and must not mark such a run degraded.
96
+ return { script, interactive, canWriteReport: !readOnly };
97
+ }
98
+ function headlessScript(input, readOnly) {
99
+ // The instructions path is given in the prompt rather than piped, so stdin
100
+ // stays free and the prompt survives the harness's own formatting.
101
+ const prompt = 'Follow the instructions in the attached file.\n\n' +
102
+ `Instructions file: ${input.instructionsPath}`;
103
+ // `--output-format text` rather than json: the pane is what `tmux attach` and
104
+ // sonata's progress diffing both read, and json collapses a whole run into a
105
+ // single unreadable line at the end. The final message is the last thing
106
+ // printed either way.
107
+ const flags = [
108
+ 'run',
109
+ `--dir ${shellQuote(input.cwd)}`,
110
+ `--model ${shellQuote(input.modelId)}`,
111
+ `--permission-mode ${permissionModeFor(input, readOnly)}`,
112
+ '--output-format text',
113
+ ];
114
+ return [
115
+ '#!/bin/bash',
116
+ 'set -o pipefail',
117
+ `cd ${shellQuote(input.cwd)} || exit 97`,
118
+ `reasonix ${flags.join(' ')} ${shellQuote(prompt)} 2>&1 | tee -a ${shellQuote(`${input.runDir}/harness.log`)}`,
119
+ `echo $? > ${shellQuote(`${input.runDir}/exit`)}`,
120
+ '',
121
+ ].join('\n');
122
+ }
123
+ /**
124
+ * `default` mode: the interactive TUI, which is the only way reasonix will ask
125
+ * before acting.
126
+ *
127
+ * Two things make this script unlike every other adapter's.
128
+ *
129
+ * First, the TUI ignores positional task text — probed, the composer comes up
130
+ * empty — so the task has to be typed in. The script addresses its own tmux
131
+ * session (`display-message -p '#S'` resolves from inside the pane) and types
132
+ * the prompt once the composer exists. It polls for the ready marker rather
133
+ * than sleeping a fixed interval, because model and machine both move the
134
+ * startup time around.
135
+ *
136
+ * Second, there is no `tee`. Probed: piping the TUI's stdout leaves the pane
137
+ * completely blank — the renderer needs a terminal. A log file is not worth a
138
+ * run nobody can see or answer, and nothing reads harness.log as the report.
139
+ *
140
+ * Third, the TUI is a chat session: it does not exit when the task is done, it
141
+ * waits for the next message. Left alone it never writes the exit sentinel, so
142
+ * a finished run sits at PROGRESS until the stall timeout and is then killed
143
+ * and reported degraded — with its report sitting right there. The same
144
+ * watcher therefore quits the TUI once the report appears, which is the
145
+ * harness's own signal that the task is over.
146
+ */
147
+ function interactiveScript(input) {
148
+ // Single line: send-keys types this literally, so an embedded newline would
149
+ // submit the message halfway through.
150
+ const prompt = `Follow the instructions in the attached file: ${input.instructionsPath}`;
151
+ const reportPath = `${input.runDir}/report.md`;
152
+ const exitPath = `${input.runDir}/exit`;
153
+ const flags = [
154
+ `--dir ${shellQuote(input.cwd)}`,
155
+ `--model ${shellQuote(input.modelId)}`,
156
+ // Explicit, because the user's own config may default to another mode.
157
+ '--permission-mode ask',
158
+ ];
159
+ return [
160
+ '#!/bin/bash',
161
+ 'set -o pipefail',
162
+ `cd ${shellQuote(input.cwd)} || exit 97`,
163
+ `SESSION="$(tmux display-message -p '#S')"`,
164
+ '(',
165
+ // 60 seconds of half-second polls. If the TUI never becomes ready the
166
+ // prompt is simply never typed, and the run stalls visibly rather than
167
+ // typing a task into whatever is on screen instead.
168
+ ' for _ in $(seq 1 120); do',
169
+ ` tmux capture-pane -p -t "$SESSION" | grep -qF ${shellQuote(READY_MARKER)} && break`,
170
+ ' sleep 0.5',
171
+ ' done',
172
+ ` tmux send-keys -t "$SESSION" -l ${shellQuote(prompt)}`,
173
+ // Enter is sent separately: -l types literally, and the composer needs a
174
+ // beat to register the text before it will submit it.
175
+ ' sleep 1',
176
+ ' tmux send-keys -t "$SESSION" Enter',
177
+ // The report is the harness's own "task over" signal. Until it lands, the
178
+ // model may still be working or waiting on an approval, and quitting would
179
+ // throw the run away. If it never lands, this waits forever and the
180
+ // watchdog ends the run — the same outcome as any harness that hangs.
181
+ ` while [ ! -f ${shellQuote(reportPath)} ]; do sleep 2; done`,
182
+ // Ctrl-D, never the documented `exit` + Enter. Typing blind into a TUI
183
+ // races with it: if a selection list happens to be open, the letters are
184
+ // swallowed by the list and the Enter picks whatever row is highlighted.
185
+ // Observed — a run typed `exit` into an approval card and carried on.
186
+ // Ctrl-D cannot select anything, so at worst it is ignored and retried.
187
+ //
188
+ // The exit sentinel is the acknowledgement: it is written the moment
189
+ // reasonix returns, so it doubles as "the quit landed".
190
+ ' for _ in $(seq 1 20); do',
191
+ ` [ -f ${shellQuote(exitPath)} ] && break`,
192
+ // A stray key can remain in the composer after a prompt is answered. A
193
+ // run observed with `1` in the composer ignored every Ctrl-D until the
194
+ // line was cleared, so kill the line before each quit attempt.
195
+ ' tmux send-keys -t "$SESSION" C-u',
196
+ ' tmux send-keys -t "$SESSION" C-d',
197
+ ' sleep 3',
198
+ ' done',
199
+ ') >/dev/null 2>&1 &',
200
+ `reasonix ${flags.join(' ')}`,
201
+ `echo $? > ${shellQuote(`${input.runDir}/exit`)}`,
202
+ '',
203
+ ].join('\n');
204
+ }
205
+ /**
206
+ * Parses `reasonix doctor --json` into refs.
207
+ *
208
+ * Reasonix's `--model` takes `<provider>` or `<provider>/<model>`, and a
209
+ * provider can serve many models — the probe machine had one serving twelve —
210
+ * so the qualified form is what sonata stores. That makes reasonix
211
+ * opencode-shaped rather than codex-shaped, which `parseConfig` enforces.
212
+ *
213
+ * Read `models[]`, never the singular `model`: that key is absent entirely from
214
+ * a provider entry that serves more than one model, so keying off it would drop
215
+ * every multi-model provider and keep only the single-model ones.
216
+ */
217
+ export function parseReasonixRefs(stdout) {
218
+ let parsed;
219
+ try {
220
+ parsed = JSON.parse(stdout);
221
+ }
222
+ catch {
223
+ return [];
224
+ }
225
+ const providers = parsed?.providers;
226
+ if (!Array.isArray(providers))
227
+ return [];
228
+ const out = [];
229
+ for (const p of providers) {
230
+ if (p === null || typeof p !== 'object')
231
+ continue;
232
+ if (typeof p.name !== 'string' || p.name.length === 0)
233
+ continue;
234
+ // A provider whose key is missing cannot run anything. Offering it would
235
+ // put a model in the picker that fails on first dispatch.
236
+ if (p.key_present !== true)
237
+ continue;
238
+ const models = Array.isArray(p.models) ? p.models : [];
239
+ for (const id of models) {
240
+ if (typeof id !== 'string' || id.length === 0)
241
+ continue;
242
+ out.push({
243
+ harness: 'reasonix',
244
+ provider: p.name,
245
+ id,
246
+ ref: `${p.name}/${id}`,
247
+ });
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+ /** Asks reasonix for its own diagnostics, bounded so doctor never hangs. */
253
+ export async function reasonixDoctorJson(timeoutMs = 10_000) {
254
+ try {
255
+ const { stdout } = await run('reasonix', ['doctor', '--json'], { timeout: timeoutMs });
256
+ return stdout;
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ const NO_PROVIDER = {
263
+ severity: 'error',
264
+ message: 'reasonix has no provider with a usable API key',
265
+ fix: 'reasonix setup',
266
+ };
267
+ /**
268
+ * Whether the telemetry consent question has been answered.
269
+ *
270
+ * On a machine that has never answered it, the very first invocation blocks on
271
+ * "Allow anonymous CLI usage statistics? [Y/n]:" before the agent starts at
272
+ * all — so an unattended dispatch hangs there forever, which looks exactly like
273
+ * a model that never said anything. Probed on a fresh install.
274
+ */
275
+ export function telemetryAnswered(configText) {
276
+ return /^\s*cli_metrics\s*=/m.test(configText);
277
+ }
278
+ async function reasonixHealth(env) {
279
+ const problems = [];
280
+ const configPath = join(env.home, '.reasonix', 'config.toml');
281
+ const configText = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
282
+ if (!telemetryAnswered(configText)) {
283
+ problems.push({
284
+ severity: 'error',
285
+ message: 'reasonix has not been told whether it may send usage statistics, so its first run will block on that question before starting',
286
+ fix: 'reasonix config telemetry off',
287
+ });
288
+ }
289
+ const json = await reasonixDoctorJson();
290
+ if (json === null) {
291
+ problems.push({
292
+ severity: 'error',
293
+ message: 'reasonix doctor failed or timed out',
294
+ fix: 'reasonix doctor',
295
+ });
296
+ return problems;
297
+ }
298
+ if (parseReasonixRefs(json).length === 0)
299
+ problems.push(NO_PROVIDER);
300
+ // Reasonix loads the working directory's .mcp.json on top of its own config.
301
+ // In this repository that hands a foreign model sonata's own run/tail/approve
302
+ // tools, so a dispatched model can dispatch further models. Probed: `doctor`
303
+ // in /tmp lists no plugins; the same command here lists `sonata`.
304
+ if (existsSync(join(env.cwd, '.mcp.json'))) {
305
+ problems.push({
306
+ severity: 'warn',
307
+ message: `reasonix will load the MCP servers in ${join(env.cwd, '.mcp.json')} — a dispatched model gets those tools too`,
308
+ fix: 'Check what they expose. If one of them is sonata itself, a run can dispatch further runs.',
309
+ });
310
+ }
311
+ return problems;
312
+ }
313
+ export const reasonixAdapter = {
314
+ name: 'reasonix',
315
+ versionCommand: ['reasonix', '--version'],
316
+ supportedVersions: '>=1.26.0 <2.0.0',
317
+ pathPrepend: [],
318
+ plan: buildScript,
319
+ canPromptForApproval: true,
320
+ promptPatterns: PROMPT_PATTERNS,
321
+ /**
322
+ * Returns the whole block rather than the matched line. The line that matches
323
+ * is often the footer, and "↑/↓ navigate · Enter select" tells the caller
324
+ * nothing about what it is being asked to allow.
325
+ */
326
+ describePrompt(lines) {
327
+ for (let i = lines.length - 1; i >= 0; i--) {
328
+ if (!PROMPT_PATTERNS.some((re) => re.test(lines[i])))
329
+ continue;
330
+ let start = i;
331
+ while (start > 0 && start > i - MAX_PROMPT_LINES && !PROMPT_HEAD.test(lines[start])) {
332
+ start--;
333
+ }
334
+ const block = lines
335
+ .slice(start, start + MAX_PROMPT_LINES)
336
+ .map((l) => l.trimEnd())
337
+ .filter((l) => l.length > 0);
338
+ return block.length > 0 ? block.join('\n') : lines[i].trim();
339
+ }
340
+ return null;
341
+ },
342
+ /**
343
+ * `1` is "Allow once" on the tool approval card, and the digits on that card
344
+ * act immediately — no trailing Enter, which would fall through to the
345
+ * composer and submit an empty message.
346
+ *
347
+ * `Escape` rather than `4` for deny, even though `4` is the Deny row and was
348
+ * probed to work. Escape is the documented deny key on the approval card
349
+ * ("n/Esc deny") AND the only key that behaves correctly on the plan
350
+ * confirmation prompt: there, selecting the third option records
351
+ * `revise_plan` and silently flips the session from Plan to Auto, whether it
352
+ * is chosen by digit or by arrow keys. One key that is right everywhere beats
353
+ * two that are right in one place each.
354
+ */
355
+ approveKeys: { yes: ['1'], no: ['Escape'] },
356
+ health: reasonixHealth,
357
+ };
358
+ //# sourceMappingURL=reasonix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasonix.js","sourceRoot":"","sources":["../../src/adapters/reasonix.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ /** Marker shared by generated tier agents and ownership detection. */
2
+ export const TIER_AGENT_MARKER = 'Generated by `sonata sync` from [tiers] in sonata.toml';
3
+ //# sourceMappingURL=agent-markers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-markers.js","sourceRoot":"","sources":["../src/agent-markers.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,MAAM,CAAC,MAAM,iBAAiB,GAAG,wDAAwD,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Scraped per-token prices from ai-pricing.fyi.
3
+ *
4
+ * The site publishes no licence, so this is treated exactly like the
5
+ * Artificial Analysis catalog: fetched with an explicit `sonata catalog
6
+ * update`, cached under the user's own config directory, and never committed
7
+ * to this repository. Only a hand-written fixture is.
8
+ *
9
+ * Rows are keyed model -> provider, never flattened to model alone: one model
10
+ * spans an 8x price range across serving providers, so which provider a
11
+ * gateway resells is information sonata cannot infer and must be told.
12
+ */
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { normalizeModelName } from './catalog.js';
16
+ export const AI_PRICING_URL = 'https://ai-pricing.fyi/v1/prices/current?limit=1000';
17
+ export const AI_PRICING_ATTRIBUTION = 'Prices from ai-pricing.fyi — https://ai-pricing.fyi';
18
+ export function aiPricingPath(home) {
19
+ return join(home, '.config', 'sonata', 'ai-pricing.json');
20
+ }
21
+ const METRIC = {
22
+ input_token: 'input',
23
+ output_token: 'output',
24
+ cached_input_token: 'cachedInput',
25
+ };
26
+ export function normalizeAiPricingRows(rows) {
27
+ const out = {};
28
+ for (const raw of rows) {
29
+ if (raw === null || typeof raw !== 'object')
30
+ continue;
31
+ const row = raw;
32
+ const field = typeof row.metric === 'string' ? METRIC[row.metric] : undefined;
33
+ if (field === undefined)
34
+ continue;
35
+ // A batch or EUR row priced into the same slot would be a wrong number
36
+ // wearing the right label.
37
+ if (row.unit !== 'per_1m_tokens' || row.currency !== 'USD')
38
+ continue;
39
+ if (row.batch_flag === 1 || (row.tier_key !== null && row.tier_key !== undefined && row.tier_key !== 'standard'))
40
+ continue;
41
+ const slug = row.canonical_slug;
42
+ const provider = row.provider_slug;
43
+ const price = row.price_numeric;
44
+ if (typeof slug !== 'string' || typeof provider !== 'string')
45
+ continue;
46
+ if (typeof price !== 'number' || !Number.isFinite(price) || price < 0)
47
+ continue;
48
+ const model = normalizeModelName(slug);
49
+ out[model] ??= {};
50
+ out[model][provider] ??= {};
51
+ out[model][provider][field] = price;
52
+ }
53
+ return out;
54
+ }
55
+ export function loadAiPricing(home) {
56
+ const path = aiPricingPath(home);
57
+ if (!existsSync(path))
58
+ return undefined;
59
+ try {
60
+ const doc = JSON.parse(readFileSync(path, 'utf8'));
61
+ if (typeof doc.fetchedAt !== 'string')
62
+ return undefined;
63
+ if (doc.models === null || typeof doc.models !== 'object' || Array.isArray(doc.models))
64
+ return undefined;
65
+ if (!modelsAreValid(doc.models))
66
+ return undefined;
67
+ return doc;
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ /**
74
+ * A provider record must be non-empty and contain no unrecognized rate keys
75
+ * and at least one recognized one, with every present rate a finite,
76
+ * non-negative number — otherwise `costOf` would silently price volume at 0
77
+ * (empty record, or one with no recognized key) or produce a wrong `totalUsd`
78
+ * from a string/non-finite value.
79
+ */
80
+ const RATE_KEYS = new Set(['input', 'cachedInput', 'output']);
81
+ function modelsAreValid(models) {
82
+ for (const providers of Object.values(models)) {
83
+ if (providers === null || typeof providers !== 'object' || Array.isArray(providers))
84
+ return false;
85
+ const entries = Object.entries(providers);
86
+ if (entries.length === 0)
87
+ return false;
88
+ for (const [provider, rates] of entries) {
89
+ if (rates === null || typeof rates !== 'object' || Array.isArray(rates))
90
+ return false;
91
+ const entries2 = Object.entries(rates);
92
+ let recognized = 0;
93
+ for (const [key, value] of entries2) {
94
+ // A key outside `Rates` (e.g. `{ unexpected: 1 }`) reads as an empty
95
+ // record to `costOf` — every recognized field defaults to 0 — so it
96
+ // must be rejected rather than silently billing $0.
97
+ if (!RATE_KEYS.has(key))
98
+ return false;
99
+ recognized += 1;
100
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
101
+ return false;
102
+ }
103
+ if (recognized === 0)
104
+ return false; // an empty record prices nothing, so `costOf` would say 0
105
+ }
106
+ }
107
+ return true;
108
+ }
109
+ //# sourceMappingURL=aipricing.js.map