@tagma/sdk 0.7.3 → 0.7.5

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 (230) hide show
  1. package/README.md +85 -57
  2. package/dist/approval.d.ts +2 -12
  3. package/dist/approval.d.ts.map +1 -1
  4. package/dist/approval.js +1 -90
  5. package/dist/approval.js.map +1 -1
  6. package/dist/bootstrap.d.ts +1 -1
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/completions/file-exists.js +1 -1
  9. package/dist/completions/file-exists.js.map +1 -1
  10. package/dist/completions/output-check.d.ts.map +1 -1
  11. package/dist/completions/output-check.js +17 -4
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config.d.ts +4 -4
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +2 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/dataflow.d.ts +3 -0
  18. package/dist/dataflow.d.ts.map +1 -0
  19. package/dist/dataflow.js +2 -0
  20. package/dist/dataflow.js.map +1 -0
  21. package/dist/drivers/opencode.d.ts.map +1 -1
  22. package/dist/drivers/opencode.js +23 -71
  23. package/dist/drivers/opencode.js.map +1 -1
  24. package/dist/engine.d.ts +5 -56
  25. package/dist/engine.d.ts.map +1 -1
  26. package/dist/engine.js +7 -297
  27. package/dist/engine.js.map +1 -1
  28. package/dist/index.d.ts +4 -6
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/logger.d.ts +2 -60
  33. package/dist/logger.d.ts.map +1 -1
  34. package/dist/logger.js +1 -153
  35. package/dist/logger.js.map +1 -1
  36. package/dist/middlewares/static-context.d.ts.map +1 -1
  37. package/dist/middlewares/static-context.js +1 -2
  38. package/dist/middlewares/static-context.js.map +1 -1
  39. package/dist/pipeline-runner.d.ts.map +1 -1
  40. package/dist/pipeline-runner.js +2 -2
  41. package/dist/pipeline-runner.js.map +1 -1
  42. package/dist/plugins.d.ts +2 -2
  43. package/dist/plugins.d.ts.map +1 -1
  44. package/dist/plugins.js +1 -1
  45. package/dist/plugins.js.map +1 -1
  46. package/dist/runner.d.ts +1 -35
  47. package/dist/runner.d.ts.map +1 -1
  48. package/dist/runner.js +1 -610
  49. package/dist/runner.js.map +1 -1
  50. package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
  51. package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
  52. package/dist/runtime/adapters/stdin-approval.js +2 -0
  53. package/dist/runtime/adapters/stdin-approval.js.map +1 -0
  54. package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
  55. package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
  56. package/dist/runtime/adapters/websocket-approval.js +2 -0
  57. package/dist/runtime/adapters/websocket-approval.js.map +1 -0
  58. package/dist/runtime/bun-process-runner.d.ts +2 -0
  59. package/dist/runtime/bun-process-runner.d.ts.map +1 -0
  60. package/dist/runtime/bun-process-runner.js +2 -0
  61. package/dist/runtime/bun-process-runner.js.map +1 -0
  62. package/dist/runtime.d.ts +2 -8
  63. package/dist/runtime.d.ts.map +1 -1
  64. package/dist/runtime.js +1 -7
  65. package/dist/runtime.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +3 -4
  68. package/dist/schema.js.map +1 -1
  69. package/dist/tagma.d.ts +3 -4
  70. package/dist/tagma.d.ts.map +1 -1
  71. package/dist/tagma.js +2 -3
  72. package/dist/tagma.js.map +1 -1
  73. package/dist/triggers/file.d.ts.map +1 -1
  74. package/dist/triggers/file.js +74 -108
  75. package/dist/triggers/file.js.map +1 -1
  76. package/dist/triggers/manual.d.ts.map +1 -1
  77. package/dist/triggers/manual.js +1 -2
  78. package/dist/triggers/manual.js.map +1 -1
  79. package/dist/types.d.ts +1 -2
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +1 -12
  82. package/dist/types.js.map +1 -1
  83. package/dist/utils-api.d.ts +1 -1
  84. package/dist/utils-api.d.ts.map +1 -1
  85. package/dist/utils-api.js +1 -1
  86. package/dist/utils-api.js.map +1 -1
  87. package/dist/validate-raw.d.ts.map +1 -1
  88. package/dist/validate-raw.js +5 -12
  89. package/dist/validate-raw.js.map +1 -1
  90. package/package.json +20 -22
  91. package/dist/adapters/stdin-approval.d.ts +0 -6
  92. package/dist/adapters/stdin-approval.d.ts.map +0 -1
  93. package/dist/adapters/stdin-approval.js +0 -90
  94. package/dist/adapters/stdin-approval.js.map +0 -1
  95. package/dist/adapters/websocket-approval.d.ts +0 -28
  96. package/dist/adapters/websocket-approval.d.ts.map +0 -1
  97. package/dist/adapters/websocket-approval.js +0 -147
  98. package/dist/adapters/websocket-approval.js.map +0 -1
  99. package/dist/core/dataflow.d.ts +0 -23
  100. package/dist/core/dataflow.d.ts.map +0 -1
  101. package/dist/core/dataflow.js +0 -99
  102. package/dist/core/dataflow.js.map +0 -1
  103. package/dist/core/log-prune.d.ts +0 -16
  104. package/dist/core/log-prune.d.ts.map +0 -1
  105. package/dist/core/log-prune.js +0 -34
  106. package/dist/core/log-prune.js.map +0 -1
  107. package/dist/core/preflight.d.ts +0 -13
  108. package/dist/core/preflight.d.ts.map +0 -1
  109. package/dist/core/preflight.js +0 -61
  110. package/dist/core/preflight.js.map +0 -1
  111. package/dist/core/run-context.d.ts +0 -55
  112. package/dist/core/run-context.d.ts.map +0 -1
  113. package/dist/core/run-context.js +0 -158
  114. package/dist/core/run-context.js.map +0 -1
  115. package/dist/core/run-state.d.ts +0 -25
  116. package/dist/core/run-state.d.ts.map +0 -1
  117. package/dist/core/run-state.js +0 -93
  118. package/dist/core/run-state.js.map +0 -1
  119. package/dist/core/scheduler.d.ts +0 -13
  120. package/dist/core/scheduler.d.ts.map +0 -1
  121. package/dist/core/scheduler.js +0 -35
  122. package/dist/core/scheduler.js.map +0 -1
  123. package/dist/core/task-executor.d.ts +0 -13
  124. package/dist/core/task-executor.d.ts.map +0 -1
  125. package/dist/core/task-executor.js +0 -601
  126. package/dist/core/task-executor.js.map +0 -1
  127. package/dist/core/trigger-errors.d.ts +0 -9
  128. package/dist/core/trigger-errors.d.ts.map +0 -1
  129. package/dist/core/trigger-errors.js +0 -15
  130. package/dist/core/trigger-errors.js.map +0 -1
  131. package/dist/dag.d.ts +0 -45
  132. package/dist/dag.d.ts.map +0 -1
  133. package/dist/dag.js +0 -177
  134. package/dist/dag.js.map +0 -1
  135. package/dist/hooks.d.ts +0 -73
  136. package/dist/hooks.d.ts.map +0 -1
  137. package/dist/hooks.js +0 -106
  138. package/dist/hooks.js.map +0 -1
  139. package/dist/pipeline-definition.d.ts +0 -3
  140. package/dist/pipeline-definition.d.ts.map +0 -1
  141. package/dist/pipeline-definition.js +0 -4
  142. package/dist/pipeline-definition.js.map +0 -1
  143. package/dist/ports.d.ts +0 -196
  144. package/dist/ports.d.ts.map +0 -1
  145. package/dist/ports.js +0 -688
  146. package/dist/ports.js.map +0 -1
  147. package/dist/prompt-doc.d.ts +0 -70
  148. package/dist/prompt-doc.d.ts.map +0 -1
  149. package/dist/prompt-doc.js +0 -154
  150. package/dist/prompt-doc.js.map +0 -1
  151. package/dist/registry.d.ts +0 -67
  152. package/dist/registry.d.ts.map +0 -1
  153. package/dist/registry.js +0 -293
  154. package/dist/registry.js.map +0 -1
  155. package/dist/task-ref.d.ts +0 -55
  156. package/dist/task-ref.d.ts.map +0 -1
  157. package/dist/task-ref.js +0 -103
  158. package/dist/task-ref.js.map +0 -1
  159. package/dist/utils.d.ts +0 -13
  160. package/dist/utils.d.ts.map +0 -1
  161. package/dist/utils.js +0 -177
  162. package/dist/utils.js.map +0 -1
  163. package/src/adapters/stdin-approval.ts +0 -106
  164. package/src/adapters/websocket-approval.ts +0 -224
  165. package/src/approval.ts +0 -131
  166. package/src/bootstrap.ts +0 -55
  167. package/src/completions/exit-code.ts +0 -34
  168. package/src/completions/file-exists.ts +0 -66
  169. package/src/completions/output-check.test.ts +0 -50
  170. package/src/completions/output-check.ts +0 -92
  171. package/src/config-ops.test.ts +0 -70
  172. package/src/config-ops.ts +0 -328
  173. package/src/config.ts +0 -26
  174. package/src/core/dataflow.test.ts +0 -166
  175. package/src/core/dataflow.ts +0 -161
  176. package/src/core/log-prune.test.ts +0 -58
  177. package/src/core/log-prune.ts +0 -43
  178. package/src/core/preflight.test.ts +0 -49
  179. package/src/core/preflight.ts +0 -89
  180. package/src/core/run-context.test.ts +0 -256
  181. package/src/core/run-context.ts +0 -211
  182. package/src/core/run-state.test.ts +0 -98
  183. package/src/core/run-state.ts +0 -122
  184. package/src/core/scheduler.test.ts +0 -83
  185. package/src/core/scheduler.ts +0 -42
  186. package/src/core/task-executor.ts +0 -743
  187. package/src/core/trigger-errors.ts +0 -15
  188. package/src/dag.test.ts +0 -56
  189. package/src/dag.ts +0 -245
  190. package/src/drivers/opencode.ts +0 -410
  191. package/src/engine-ports-mixed.test.ts +0 -156
  192. package/src/engine-ports.test.ts +0 -166
  193. package/src/engine-task-type.test.ts +0 -56
  194. package/src/engine.ts +0 -458
  195. package/src/hooks.ts +0 -193
  196. package/src/index.ts +0 -33
  197. package/src/logger.ts +0 -182
  198. package/src/middlewares/static-context.ts +0 -49
  199. package/src/pipeline-definition.ts +0 -5
  200. package/src/pipeline-runner.test.ts +0 -91
  201. package/src/pipeline-runner.ts +0 -194
  202. package/src/plugin-registry.test.ts +0 -382
  203. package/src/plugins.ts +0 -21
  204. package/src/ports.test.ts +0 -678
  205. package/src/ports.ts +0 -925
  206. package/src/prompt-doc.test.ts +0 -174
  207. package/src/prompt-doc.ts +0 -169
  208. package/src/registry.ts +0 -353
  209. package/src/runner.test.ts +0 -142
  210. package/src/runner.ts +0 -666
  211. package/src/runtime.ts +0 -20
  212. package/src/schema-ports.test.ts +0 -172
  213. package/src/schema.test.ts +0 -213
  214. package/src/schema.ts +0 -379
  215. package/src/tagma.test.ts +0 -155
  216. package/src/tagma.ts +0 -62
  217. package/src/task-ref.test.ts +0 -401
  218. package/src/task-ref.ts +0 -121
  219. package/src/triggers/file.ts +0 -164
  220. package/src/triggers/manual.ts +0 -86
  221. package/src/types.ts +0 -18
  222. package/src/utils-api.ts +0 -8
  223. package/src/utils.test.ts +0 -28
  224. package/src/utils.ts +0 -203
  225. package/src/validate-raw-plugin-types.test.ts +0 -60
  226. package/src/validate-raw-ports.test.ts +0 -136
  227. package/src/validate-raw.ts +0 -852
  228. package/src/yaml-compiler.test.ts +0 -108
  229. package/src/yaml-compiler.ts +0 -110
  230. package/src/yaml.ts +0 -11
@@ -1,410 +0,0 @@
1
- import type {
2
- DriverPlugin,
3
- DriverCapabilities,
4
- DriverResultMeta,
5
- TaskConfig,
6
- TrackConfig,
7
- DriverContext,
8
- SpawnSpec,
9
- } from '../types';
10
-
11
- const DEFAULT_MODEL = 'opencode/big-pickle';
12
-
13
- // NOTE on Windows multi-line prompts: `opencode` resolves to `opencode.cmd`,
14
- // an npm-generated batch wrapper. cmd.exe silently truncates argv elements
15
- // at the first newline, so a multi-line prompt reaches the model as only
16
- // its first line. The SDK's runner auto-unwraps npm .cmd shims into direct
17
- // `node <js-entry>` invocations so newlines survive, and this driver can
18
- // keep using the bare `opencode` name on every platform.
19
-
20
- // tagma uses a provider-neutral reasoning_effort vocabulary (low|medium|high)
21
- // but opencode's `--variant` is provider-specific (e.g. high|max|minimal).
22
- // Map the tagma values to the closest opencode variant:
23
- // low → minimal (least thinking)
24
- // medium → <no flag, provider default>
25
- // high → high (most thinking)
26
- // Unknown values pass through unchanged so users who target a specific
27
- // opencode variant (e.g. "max") still work.
28
- const EFFORT_TO_VARIANT: Record<string, string | null> = {
29
- low: 'minimal',
30
- medium: null,
31
- high: 'high',
32
- };
33
-
34
- // ── Auto-install + free-model picker ───────────────────────────────────────
35
- //
36
- // The opencode driver is SDK-built-in, but the `opencode` CLI isn't. Two
37
- // provisioning paths:
38
- //
39
- // 1. Desktop app — the Electron shell ships a platform-matched opencode
40
- // binary under resources/opencode/bin/, prepended to the sidecar's PATH
41
- // at launch (see apps/electron/src/runtime-paths.ts). In-app updates
42
- // drop a newer copy into userData/opencode/bin/ which wins via PATH
43
- // precedence. That path resolves on the first `opencode --version`
44
- // probe below; no auto-install ever fires.
45
- //
46
- // 2. SDK direct use — when bun is on PATH we fall through to
47
- // `bun install -g opencode-ai`, identical to the pre-desktop behavior.
48
- //
49
- // When BOTH paths are unavailable (no bundled binary, no bun) we fail with
50
- // an actionable error pointing at the desktop Settings panel instead of
51
- // silently letting `opencode run` ENOENT later — the old behavior swallowed
52
- // the root cause in runCapture's catch and left the user staring at an
53
- // opaque "exit code -1". The result is process-memoized so subsequent
54
- // tasks in the same run surface the same error without re-probing.
55
-
56
- interface OpencodeModelInfo {
57
- id?: string;
58
- providerID?: string;
59
- status?: string;
60
- cost?: { input?: number; output?: number };
61
- limit?: { context?: number };
62
- }
63
-
64
- // Memoize BOTH success and failure. On failure we stash the message so every
65
- // subsequent ensureOpencodeInstalled() throws the identical error — re-running
66
- // the bun-install probe for each task of a failed run would just be slow and
67
- // produce confusing interleaved stderr.
68
- let opencodeReady: boolean | undefined;
69
- let opencodeReadyError: string | undefined;
70
- let cachedDefaultModel: string | undefined;
71
-
72
- async function runCapture(
73
- args: string[],
74
- ): Promise<{ code: number; stdout: string; stderr: string }> {
75
- try {
76
- const proc = Bun.spawn(args, { stdout: 'pipe', stderr: 'pipe' });
77
- const [stdout, stderr, code] = await Promise.all([
78
- new Response(proc.stdout).text(),
79
- new Response(proc.stderr).text(),
80
- proc.exited,
81
- ]);
82
- return { code, stdout, stderr };
83
- } catch {
84
- return { code: -1, stdout: '', stderr: '' };
85
- }
86
- }
87
-
88
- // Shared tail for every failure message — the Tagma desktop app exposes a
89
- // one-click installer at the same npm source path this driver would reach
90
- // for, so point users there first. Users running the SDK as a library still
91
- // see the manual bun/npm hint.
92
- const SETUP_HINT =
93
- 'If you are using the Tagma desktop app, open Editor Settings → OpenCode CLI to install or update the bundled binary. ' +
94
- 'Otherwise install it manually: `bun install -g opencode-ai` or `npm install -g opencode-ai`.';
95
-
96
- async function ensureOpencodeInstalled(): Promise<void> {
97
- if (opencodeReady === true) return;
98
- if (opencodeReady === false && opencodeReadyError) {
99
- throw new Error(opencodeReadyError);
100
- }
101
-
102
- // Probe existing install first — this is the hot path for desktop users
103
- // (bundled binary in PATH) and for anyone who already has opencode.
104
- const probe = await runCapture(['opencode', '--version']);
105
- if (probe.code === 0) {
106
- opencodeReady = true;
107
- return;
108
- }
109
-
110
- // Distinguish "bun is missing" from "bun is here but install failed" so
111
- // the error we surface points at the right next step. If bun is absent we
112
- // skip the install attempt entirely — spawning with `bun` as argv[0]
113
- // would just ENOENT inside runCapture's catch and look identical to a
114
- // failed install.
115
- const bunProbe = await runCapture(['bun', '--version']);
116
- if (bunProbe.code !== 0) {
117
- opencodeReady = false;
118
- opencodeReadyError = `OpenCode CLI is not available and \`bun\` is not installed. ${SETUP_HINT}`;
119
- throw new Error(opencodeReadyError);
120
- }
121
-
122
- console.error(
123
- '[driver:opencode] opencode CLI not found — installing via `bun install -g opencode-ai`... (this may take up to a minute)',
124
- );
125
- // Use inherit here so the user sees bun's own progress during the one-time
126
- // install; runCapture would swallow it.
127
- const install = Bun.spawn(['bun', 'install', '-g', 'opencode-ai'], {
128
- stdout: 'inherit',
129
- stderr: 'inherit',
130
- });
131
- const installCode = await install.exited;
132
- if (installCode !== 0) {
133
- opencodeReady = false;
134
- opencodeReadyError = `\`bun install -g opencode-ai\` failed (exit code ${installCode}). ${SETUP_HINT}`;
135
- throw new Error(opencodeReadyError);
136
- }
137
-
138
- // Bun installs globals under `~/.bun/bin` (or `%USERPROFILE%\.bun\bin`),
139
- // which isn't on this process's cached PATH unless the user already has
140
- // bun set up. Ask bun for the directory and prepend it so bare `opencode`
141
- // resolves in this process without requiring a shell reload.
142
- const bin = await runCapture(['bun', 'pm', 'bin', '-g']);
143
- if (bin.code === 0) {
144
- const dir = bin.stdout.trim();
145
- const sep = process.platform === 'win32' ? ';' : ':';
146
- const current = process.env.PATH ?? '';
147
- if (dir && !current.split(sep).includes(dir)) {
148
- process.env.PATH = `${dir}${sep}${current}`;
149
- }
150
- }
151
-
152
- const verify = await runCapture(['opencode', '--version']);
153
- if (verify.code !== 0) {
154
- opencodeReady = false;
155
- opencodeReadyError =
156
- '`opencode` is not resolvable after `bun install -g opencode-ai` completed. ' +
157
- "Bun's global bin directory is probably not on PATH — add it manually or restart the app.";
158
- throw new Error(opencodeReadyError);
159
- }
160
- opencodeReady = true;
161
- }
162
-
163
- // `opencode models --verbose` emits "<provider>/<id>\n{...json...}\n" pairs.
164
- // Walk balanced braces rather than split on newlines so we survive any
165
- // whitespace oddities in the JSON payload.
166
- function parseVerboseModels(stdout: string): OpencodeModelInfo[] {
167
- const out: OpencodeModelInfo[] = [];
168
- let depth = 0;
169
- let start = -1;
170
- for (let i = 0; i < stdout.length; i++) {
171
- const c = stdout[i];
172
- if (c === '{') {
173
- if (depth === 0) start = i;
174
- depth++;
175
- } else if (c === '}') {
176
- depth--;
177
- if (depth === 0 && start !== -1) {
178
- try {
179
- out.push(JSON.parse(stdout.slice(start, i + 1)) as OpencodeModelInfo);
180
- } catch {
181
- /* skip malformed block */
182
- }
183
- start = -1;
184
- }
185
- }
186
- }
187
- return out;
188
- }
189
-
190
- function pickFreeModel(models: OpencodeModelInfo[]): string | null {
191
- const fullId = (m: OpencodeModelInfo): string =>
192
- `${m.providerID ?? 'opencode'}/${m.id ?? ''}`;
193
- const eligible = models.filter((m) => {
194
- if (!m.id || m.id === 'big-pickle') return false;
195
- if (m.status && m.status !== 'active') return false;
196
- const cost = m.cost;
197
- if (!cost || cost.input !== 0 || cost.output !== 0) return false;
198
- const ctx = m.limit?.context;
199
- if (typeof ctx !== 'number' || ctx <= 128000) return false;
200
- return true;
201
- });
202
- // Prefer models explicitly labelled "-free" by the provider — those are
203
- // a stronger stability signal than "cost happens to be 0 right now".
204
- const preferred = eligible.filter((m) => m.id?.endsWith('-free'));
205
- const pool = preferred.length > 0 ? preferred : eligible;
206
- if (pool.length === 0) return null;
207
- // Deterministic pick: sort by full id so upstream model-list reordering
208
- // doesn't flip our choice between runs.
209
- pool.sort((a, b) => fullId(a).localeCompare(fullId(b)));
210
- return fullId(pool[0]);
211
- }
212
-
213
- async function resolveDefaultModel(): Promise<string> {
214
- if (cachedDefaultModel !== undefined) return cachedDefaultModel;
215
- // ensureOpencodeInstalled now throws with an actionable message when the
216
- // CLI can't be provisioned, so we let the error bubble up to the task
217
- // runner instead of silently falling back to DEFAULT_MODEL (which would
218
- // produce a second confusing ENOENT a few lines later in `opencode run`).
219
- await ensureOpencodeInstalled();
220
- console.error('[driver:opencode] resolving free opencode model...');
221
- const { code, stdout } = await runCapture(['opencode', 'models', '--verbose']);
222
- if (code !== 0) {
223
- cachedDefaultModel = DEFAULT_MODEL;
224
- return cachedDefaultModel;
225
- }
226
- const picked = pickFreeModel(parseVerboseModels(stdout));
227
- cachedDefaultModel = picked ?? DEFAULT_MODEL;
228
- console.error(`[driver:opencode] default model: ${cachedDefaultModel}`);
229
- return cachedDefaultModel;
230
- }
231
-
232
- export const OpenCodeDriver: DriverPlugin = {
233
- name: 'opencode',
234
-
235
- capabilities: {
236
- sessionResume: true, // supports --session
237
- systemPrompt: false, // no --system-prompt flag; prepend to prompt instead
238
- outputFormat: true, // supports --format json
239
- } satisfies DriverCapabilities,
240
-
241
- resolveModel(): string {
242
- return DEFAULT_MODEL;
243
- },
244
-
245
- async buildCommand(task: TaskConfig, track: TrackConfig, ctx: DriverContext): Promise<SpawnSpec> {
246
- const explicitModel = task.model ?? track.model;
247
- // Always make sure the opencode CLI is usable before we spawn it — even
248
- // when the user pinned a model. ensureOpencodeInstalled throws with an
249
- // actionable message when the binary is neither present on PATH (desktop
250
- // bundles it there via runtime-paths.ts) nor installable via bun.
251
- if (explicitModel) await ensureOpencodeInstalled();
252
- // Otherwise resolveDefaultModel both ensures the CLI and picks a free
253
- // model from `opencode models --verbose` (cached per-process).
254
- const model = explicitModel ?? (await resolveDefaultModel());
255
- // Resolve reasoning_effort → opencode --variant. SDK schema layer already
256
- // resolved task → track → pipeline inheritance, so we only need to read
257
- // task.reasoning_effort here.
258
- const rawEffort = task.reasoning_effort ?? track.reasoning_effort;
259
- const variant = rawEffort
260
- ? rawEffort in EFFORT_TO_VARIANT
261
- ? EFFORT_TO_VARIANT[rawEffort]
262
- : rawEffort
263
- : null;
264
-
265
- let prompt = task.prompt!;
266
-
267
- // agent_profile has no dedicated flag; prepend to prompt
268
- const profile = task.agent_profile ?? track.agent_profile;
269
- if (profile) {
270
- prompt = `[Role]\n${profile}\n\n[Task]\n${prompt}`;
271
- }
272
-
273
- // continue_from: prefer session resume, fall back to text injection
274
- let sessionId: string | null = null;
275
- if (task.continue_from) {
276
- sessionId = ctx.sessionMap.get(task.continue_from) ?? null;
277
- if (!sessionId) {
278
- // no session — degrade to text context passthrough
279
- let prev: string | null = null;
280
- if (ctx.normalizedMap.has(task.continue_from)) {
281
- prev = ctx.normalizedMap.get(task.continue_from)!;
282
- }
283
- if (prev !== null) {
284
- prompt = `[Previous Output]\n${prev}\n\n[Current Task]\n${prompt}`;
285
- }
286
- }
287
- }
288
-
289
- // opencode run does not support stdin (no `-` placeholder like codex exec).
290
- // Prompt is always a positional argument. Flags must be declared before `--`;
291
- // the prompt follows after so that leading `--flag` content cannot be
292
- // misread by opencode's argument parser (flag-injection mitigation).
293
- // Shell-level injection is already prevented by Bun.spawn's direct argv array.
294
- // Windows cmd.exe argv truncation on the `.cmd` wrapper is handled by the
295
- // SDK runner's shim unwrapping — see note at the top of this file.
296
- const args: string[] = [
297
- 'opencode',
298
- 'run',
299
- '--model',
300
- model,
301
- '--format',
302
- 'json', // JSON output for parseResult
303
- ];
304
-
305
- // `--variant` must precede `--` like every other flag. opencode rejects
306
- // unknown variant names with a clear error, so we don't pre-validate.
307
- if (variant) {
308
- args.push('--variant', variant);
309
- }
310
-
311
- // session resume (must appear before --)
312
- if (sessionId) {
313
- args.push('--session', sessionId);
314
- }
315
-
316
- // `--` (POSIX end-of-options) isolates prompt from flag parsing
317
- args.push('--', prompt);
318
-
319
- return { args, cwd: task.cwd ?? ctx.workDir };
320
- },
321
-
322
- parseResult(stdout: string): DriverResultMeta {
323
- // opencode --format json emits NDJSON — one JSON object per line
324
- // (step_start / text / step_finish / …). The previous single
325
- // `JSON.parse(stdout)` always threw on this shape and fell through to
326
- // the catch, returning sessionId:null and losing session resume.
327
- // Walk line-by-line, pick up the first sessionID we see, concatenate
328
- // any text-type parts into normalizedOutput, and bail early on error
329
- // payloads.
330
- const lines = stdout.split(/\r?\n/);
331
- let sessionId: string | undefined;
332
- const textParts: string[] = [];
333
- let sawAnyJson = false;
334
- let errorReason: string | null = null;
335
-
336
- for (const raw of lines) {
337
- const line = raw.trim();
338
- if (!line) continue;
339
- let json: Record<string, unknown>;
340
- try {
341
- json = JSON.parse(line) as Record<string, unknown>;
342
- } catch {
343
- continue; // tolerate interleaved non-JSON noise
344
- }
345
- sawAnyJson = true;
346
-
347
- // M12: opencode sometimes emits {type:"error", error:{...}} with
348
- // exit 0 for transient API failures. Force-fail so downstream
349
- // skip_downstream / stop_all kicks in.
350
- if (json.type === 'error') {
351
- const err = json.error as { message?: unknown } | string | undefined;
352
- const msg =
353
- typeof err === 'object' && err !== null && typeof err.message === 'string'
354
- ? err.message
355
- : typeof err === 'string'
356
- ? err
357
- : null;
358
- errorReason = msg
359
- ? `opencode reported error: ${msg}`
360
- : 'opencode emitted an error JSON payload';
361
- // D21: stop at the first error. Continuing meant subsequent text
362
- // lines got accumulated into `textParts` only to be discarded by
363
- // the error-return below, and a later `{type:"error"}` would
364
- // silently overwrite the original cause — operators then debugged
365
- // a downstream symptom while the root-cause line scrolled past.
366
- break;
367
- }
368
-
369
- // Session id — opencode uses `sessionID` (camelCase with capital D).
370
- // Keep `session_id` / `sessionId` as fallbacks for forward/backward
371
- // compatibility with other shapes.
372
- if (!sessionId) {
373
- const sid =
374
- (json.sessionID as string | undefined) ??
375
- (json.session_id as string | undefined) ??
376
- (json.sessionId as string | undefined) ??
377
- null;
378
- if (typeof sid === 'string' && sid.length > 0) sessionId = sid;
379
- }
380
-
381
- // Extract human-readable text from text-type parts.
382
- if (json.type === 'text') {
383
- const part = json.part as { text?: unknown } | undefined;
384
- if (part && typeof part.text === 'string') {
385
- textParts.push(part.text);
386
- }
387
- } else if (typeof json.result === 'string') {
388
- textParts.push(json.result);
389
- } else if (typeof json.content === 'string') {
390
- textParts.push(json.content);
391
- }
392
- }
393
-
394
- if (errorReason) {
395
- return { forceFailure: true, forceFailureReason: errorReason };
396
- }
397
-
398
- // If nothing parsed as JSON, treat stdout as plain text.
399
- const normalizedOutput = !sawAnyJson
400
- ? stdout
401
- : textParts.length > 0
402
- ? textParts.join('\n')
403
- : stdout;
404
-
405
- return {
406
- sessionId,
407
- normalizedOutput,
408
- };
409
- },
410
- };
@@ -1,156 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { bootstrapBuiltins } from './bootstrap';
6
- import { runPipeline, type RunEventPayload } from './engine';
7
- import { PluginRegistry } from './registry';
8
- import type { DriverPlugin, PipelineConfig, TaskConfig } from './types';
9
-
10
- const PERMS = { read: true, write: false, execute: false };
11
-
12
- function makeDir(): string {
13
- return mkdtempSync(join(tmpdir(), 'tagma-bindings-mixed-'));
14
- }
15
-
16
- function writeEmitScript(dir: string, name: string, payload: Record<string, unknown>): string {
17
- const path = join(dir, `${name}.js`);
18
- writeFileSync(
19
- path,
20
- `process.stdout.write(${JSON.stringify(JSON.stringify(payload))});\nprocess.stdout.write('\\n');\n`,
21
- );
22
- return path;
23
- }
24
-
25
- function writeEchoArgsScript(dir: string): string {
26
- const path = join(dir, 'echo.js');
27
- writeFileSync(path, `process.stdout.write(process.argv.slice(2).join('|'));\n`);
28
- return path;
29
- }
30
-
31
- function writeMockDriverScript(dir: string): string {
32
- const path = join(dir, 'mock-driver.js');
33
- writeFileSync(
34
- path,
35
- [
36
- `const fs = require('fs');`,
37
- `let buf = '';`,
38
- `process.stdin.setEncoding('utf8');`,
39
- `process.stdin.on('data', (c) => { buf += c; });`,
40
- `process.stdin.on('end', () => {`,
41
- ` fs.writeFileSync(process.env.MOCK_RECORD_PATH, buf);`,
42
- ` process.stdout.write(process.env.MOCK_RESPONSE + '\\n');`,
43
- `});`,
44
- ].join('\n'),
45
- );
46
- return path;
47
- }
48
-
49
- function registry(script: string, responses: Record<string, Record<string, unknown>>, records: Record<string, string>) {
50
- const reg = new PluginRegistry();
51
- bootstrapBuiltins(reg);
52
- const driver: DriverPlugin = {
53
- name: 'mock',
54
- capabilities: { sessionResume: false, systemPrompt: true, outputFormat: true },
55
- async buildCommand(task) {
56
- return {
57
- args: ['node', script],
58
- stdin: task.prompt ?? '',
59
- env: {
60
- MOCK_RESPONSE: JSON.stringify(responses[task.id] ?? {}),
61
- MOCK_RECORD_PATH: records[task.id] ?? join(process.cwd(), 'prompt.txt'),
62
- },
63
- };
64
- },
65
- parseResult(stdout) {
66
- return { normalizedOutput: stdout.trim() };
67
- },
68
- };
69
- reg.registerPlugin('drivers', 'mock', driver);
70
- return reg;
71
- }
72
-
73
- function task(overrides: Partial<TaskConfig> & { id: string }): TaskConfig {
74
- return { name: overrides.id, permissions: PERMS, driver: 'mock', ...overrides };
75
- }
76
-
77
- function pipeline(tasks: TaskConfig[]): PipelineConfig {
78
- return {
79
- name: 'mixed-bindings-test',
80
- tracks: [{ id: 't', name: 'T', permissions: PERMS, driver: 'mock', tasks }],
81
- };
82
- }
83
-
84
- async function run(config: PipelineConfig, workDir: string, reg: PluginRegistry) {
85
- const events: RunEventPayload[] = [];
86
- const result = await runPipeline(config, workDir, {
87
- registry: reg,
88
- skipPluginLoading: true,
89
- onEvent: (e) => events.push(e),
90
- });
91
- return { events, success: result.success };
92
- }
93
-
94
- function finalUpdateFor(events: RunEventPayload[], qid: string): RunEventPayload | undefined {
95
- let last: RunEventPayload | undefined;
96
- for (const ev of events) {
97
- if (ev.type === 'task_update' && ev.taskId === qid) last = ev;
98
- }
99
- return last;
100
- }
101
-
102
- describe('engine — mixed prompt/command unified bindings', () => {
103
- test('prompt outputs are inferred from downstream command inputs', async () => {
104
- const dir = makeDir();
105
- try {
106
- const driverScript = writeMockDriverScript(dir);
107
- const echo = writeEchoArgsScript(dir);
108
- const record = join(dir, 'prompt.txt');
109
- const reg = registry(driverScript, { plan: { city: 'Paris' } }, { plan: record });
110
- const config = pipeline([
111
- task({ id: 'plan', prompt: 'Pick a city' }),
112
- task({
113
- id: 'fetch',
114
- driver: 'opencode',
115
- depends_on: ['plan'],
116
- command: `node "${echo}" "{{inputs.city}}"`,
117
- inputs: { city: { from: 't.plan.outputs.city', type: 'string', required: true } },
118
- }),
119
- ]);
120
-
121
- const { events, success } = await run(config, dir, reg);
122
- expect(success).toBe(true);
123
- expect(readFileSync(record, 'utf8')).toContain('[Output Format]');
124
- expect(finalUpdateFor(events, 't.plan')?.outputs).toEqual({ city: 'Paris' });
125
- expect(finalUpdateFor(events, 't.fetch')?.inputs).toEqual({ city: 'Paris' });
126
- } finally {
127
- rmSync(dir, { recursive: true, force: true });
128
- }
129
- });
130
-
131
- test('prompt inputs are inferred from upstream command outputs', async () => {
132
- const dir = makeDir();
133
- try {
134
- const emit = writeEmitScript(dir, 'emit', { city: 'Berlin' });
135
- const driverScript = writeMockDriverScript(dir);
136
- const record = join(dir, 'prompt.txt');
137
- const reg = registry(driverScript, { summarize: {} }, { summarize: record });
138
- const config = pipeline([
139
- task({
140
- id: 'up',
141
- driver: 'opencode',
142
- command: `node "${emit}"`,
143
- outputs: { city: { type: 'string' } },
144
- }),
145
- task({ id: 'summarize', depends_on: ['up'], prompt: 'City is {{inputs.city}}' }),
146
- ]);
147
-
148
- const { events, success } = await run(config, dir, reg);
149
- expect(success).toBe(true);
150
- expect(readFileSync(record, 'utf8')).toContain('City is Berlin');
151
- expect(finalUpdateFor(events, 't.summarize')?.inputs).toEqual({ city: 'Berlin' });
152
- } finally {
153
- rmSync(dir, { recursive: true, force: true });
154
- }
155
- });
156
- });