opencastle 0.34.2 → 0.34.3

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 (44) hide show
  1. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  2. package/dist/cli/run/adapters/claude.js +43 -5
  3. package/dist/cli/run/adapters/claude.js.map +1 -1
  4. package/dist/cli/run/adapters/claude.test.js +85 -0
  5. package/dist/cli/run/adapters/claude.test.js.map +1 -1
  6. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  7. package/dist/cli/run/adapters/copilot.js +43 -5
  8. package/dist/cli/run/adapters/copilot.js.map +1 -1
  9. package/dist/cli/run/adapters/copilot.test.js +85 -0
  10. package/dist/cli/run/adapters/copilot.test.js.map +1 -1
  11. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  12. package/dist/cli/run/adapters/cursor.js +43 -5
  13. package/dist/cli/run/adapters/cursor.js.map +1 -1
  14. package/dist/cli/run/adapters/cursor.test.js +85 -0
  15. package/dist/cli/run/adapters/cursor.test.js.map +1 -1
  16. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  17. package/dist/cli/run/adapters/opencode.js +44 -6
  18. package/dist/cli/run/adapters/opencode.js.map +1 -1
  19. package/dist/cli/run/adapters/opencode.test.js +86 -0
  20. package/dist/cli/run/adapters/opencode.test.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/cli/run/adapters/claude.test.ts +85 -0
  23. package/src/cli/run/adapters/claude.ts +41 -7
  24. package/src/cli/run/adapters/copilot.test.ts +85 -0
  25. package/src/cli/run/adapters/copilot.ts +41 -7
  26. package/src/cli/run/adapters/cursor.test.ts +85 -0
  27. package/src/cli/run/adapters/cursor.ts +41 -7
  28. package/src/cli/run/adapters/opencode.test.ts +86 -0
  29. package/src/cli/run/adapters/opencode.ts +42 -8
  30. package/src/dashboard/dist/data/convoys/demo-api-v2.json +9 -9
  31. package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +4 -4
  32. package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +12 -12
  33. package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
  34. package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +1 -1
  35. package/src/dashboard/dist/data/convoys/demo-docs-update.json +3 -3
  36. package/src/dashboard/dist/data/convoys/demo-perf-opt.json +10 -10
  37. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  38. package/src/dashboard/public/data/convoys/demo-api-v2.json +9 -9
  39. package/src/dashboard/public/data/convoys/demo-auth-revamp.json +4 -4
  40. package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +12 -12
  41. package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
  42. package/src/dashboard/public/data/convoys/demo-deploy-ci.json +1 -1
  43. package/src/dashboard/public/data/convoys/demo-docs-update.json +3 -3
  44. package/src/dashboard/public/data/convoys/demo-perf-opt.json +10 -10
@@ -233,4 +233,89 @@ describe('copilot adapter — CLI mode', () => {
233
233
  token: 'abc',
234
234
  })
235
235
  })
236
+
237
+ it('extracts result from single-line JSON output', async () => {
238
+ mockSpawn.mockImplementation((cmd: string) => {
239
+ if (cmd === 'which') return makeMockProc(0, '')
240
+ return makeMockProc(0, '{"result":"# My PRD\\n\\nThe actual content"}')
241
+ })
242
+ const { execute } = await import('./copilot.js')
243
+ const result = await execute(makeTask(), { cwd: tmpDir })
244
+ expect(result.output).toBe('# My PRD\n\nThe actual content')
245
+ })
246
+
247
+ it('extracts result from JSONL output (last line has result)', async () => {
248
+ const jsonl = [
249
+ '{"type":"progress","content":"thinking..."}',
250
+ '{"type":"tool_use","tool":"read_file","args":{}}',
251
+ '{"type":"result","result":"# My PRD\\n\\nThe actual markdown content","usage":{"input_tokens":500,"output_tokens":1000}}',
252
+ ].join('\n')
253
+ mockSpawn.mockImplementation((cmd: string) => {
254
+ if (cmd === 'which') return makeMockProc(0, '')
255
+ return makeMockProc(0, jsonl)
256
+ })
257
+ const { execute } = await import('./copilot.js')
258
+ const result = await execute(makeTask(), { cwd: tmpDir })
259
+ expect(result.output).toBe('# My PRD\n\nThe actual markdown content')
260
+ expect(result.usage?.prompt_tokens).toBe(500)
261
+ expect(result.usage?.completion_tokens).toBe(1000)
262
+ })
263
+
264
+ it('scans JSONL and finds result line among non-result lines', async () => {
265
+ const jsonl = [
266
+ '{"type":"progress","content":"thinking..."}',
267
+ '{"type":"tool_use","tool":"edit","args":{}}',
268
+ '{"type":"result","result":"# Final Content","usage":{"input_tokens":100,"output_tokens":200}}',
269
+ '{"type":"done"}',
270
+ ].join('\n')
271
+ mockSpawn.mockImplementation((cmd: string) => {
272
+ if (cmd === 'which') return makeMockProc(0, '')
273
+ return makeMockProc(0, jsonl)
274
+ })
275
+ const { execute } = await import('./copilot.js')
276
+ const result = await execute(makeTask(), { cwd: tmpDir })
277
+ expect(result.output).toBe('# Final Content')
278
+ })
279
+
280
+ it('extracts content from Copilot-style assistant.message JSONL', async () => {
281
+ const jsonl = [
282
+ '{"type":"session.tools_updated","data":{"model":"claude-sonnet-4.6"}}',
283
+ '{"type":"user.message","data":{"content":"Generate a PRD"}}',
284
+ '{"type":"assistant.message","data":{"content":"# My PRD\\n\\nThe generated content","outputTokens":50}}',
285
+ '{"type":"assistant.turn_end","data":{"turnId":"0"}}',
286
+ '{"type":"result","sessionId":"abc","exitCode":0,"usage":{"premiumRequests":1}}',
287
+ ].join('\n')
288
+ mockSpawn.mockImplementation((cmd: string) => {
289
+ if (cmd === 'which') return makeMockProc(0, '')
290
+ return makeMockProc(0, jsonl)
291
+ })
292
+ const { execute } = await import('./copilot.js')
293
+ const result = await execute(makeTask(), { cwd: tmpDir })
294
+ expect(result.output).toBe('# My PRD\n\nThe generated content')
295
+ })
296
+
297
+ it('uses last assistant.message when multiple turns exist', async () => {
298
+ const jsonl = [
299
+ '{"type":"assistant.message","data":{"content":"Let me check..."}}',
300
+ '{"type":"assistant.message","data":{"content":"# Final PRD\\n\\nComplete document"}}',
301
+ '{"type":"result","sessionId":"abc","exitCode":0}',
302
+ ].join('\n')
303
+ mockSpawn.mockImplementation((cmd: string) => {
304
+ if (cmd === 'which') return makeMockProc(0, '')
305
+ return makeMockProc(0, jsonl)
306
+ })
307
+ const { execute } = await import('./copilot.js')
308
+ const result = await execute(makeTask(), { cwd: tmpDir })
309
+ expect(result.output).toBe('# Final PRD\n\nComplete document')
310
+ })
311
+
312
+ it('falls back to raw output when JSON has no result field', async () => {
313
+ mockSpawn.mockImplementation((cmd: string) => {
314
+ if (cmd === 'which') return makeMockProc(0, '')
315
+ return makeMockProc(0, '{"status":"ok","data":"something"}')
316
+ })
317
+ const { execute } = await import('./copilot.js')
318
+ const result = await execute(makeTask(), { cwd: tmpDir })
319
+ expect(result.output).toBe('{"status":"ok","data":"something"}')
320
+ })
236
321
  })
@@ -190,14 +190,11 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
190
190
  let textOutput = [stdout, stderr].filter(Boolean).join('\n')
191
191
  let usage: TokenUsage | undefined
192
192
  try {
193
+ // Try single JSON object first (claude CLI)
193
194
  const parsedJson = JSON.parse(stdout) as Record<string, unknown>
194
-
195
- // Extract the actual AI text response from the JSON envelope
196
- const result = parsedJson.result as string | undefined
197
- if (typeof result === 'string') {
198
- textOutput = result
195
+ if (typeof parsedJson.result === 'string') {
196
+ textOutput = parsedJson.result
199
197
  }
200
-
201
198
  const u = parsedJson?.usage as Record<string, number> | undefined
202
199
  if (u) {
203
200
  const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
@@ -205,7 +202,44 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
205
202
  const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
206
203
  usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
207
204
  }
208
- } catch { /* not JSON or no usage — graceful degradation */ }
205
+ } catch {
206
+ // Fallback: parse JSONL (one JSON object per line)
207
+ // Claude CLI uses {"result": "text"}, Copilot CLI uses
208
+ // {"type":"assistant.message","data":{"content":"text"}} for the AI
209
+ // response and a separate {"type":"result"} line for session metadata.
210
+ const lines = stdout.split('\n')
211
+ let lastAssistantContent: string | undefined
212
+ for (const rawLine of lines) {
213
+ const line = rawLine.trim()
214
+ if (!line) continue
215
+ try {
216
+ const parsed = JSON.parse(line) as Record<string, unknown>
217
+ // Claude-style: result text in the result line
218
+ if (typeof parsed.result === 'string' && parsed.result) {
219
+ textOutput = parsed.result
220
+ const u = parsed?.usage as Record<string, number> | undefined
221
+ if (u) {
222
+ const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
223
+ const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
224
+ const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
225
+ usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
226
+ }
227
+ lastAssistantContent = undefined // prefer explicit result field
228
+ break
229
+ }
230
+ // Copilot-style: AI response in assistant.message events
231
+ if (parsed.type === 'assistant.message') {
232
+ const data = parsed.data as Record<string, unknown> | undefined
233
+ if (data && typeof data.content === 'string') {
234
+ lastAssistantContent = data.content
235
+ }
236
+ }
237
+ } catch { /* skip non-JSON lines */ }
238
+ }
239
+ if (lastAssistantContent !== undefined) {
240
+ textOutput = lastAssistantContent
241
+ }
242
+ }
209
243
  resolve({
210
244
  success: code === 0,
211
245
  output: textOutput.slice(0, 500_000),
@@ -157,4 +157,89 @@ describe('cursor adapter — MCP support', () => {
157
157
  expect(capturedArgs).toContain('--approve-mcps')
158
158
  expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
159
159
  })
160
+
161
+ it('extracts result from single-line JSON output', async () => {
162
+ mockSpawn.mockImplementation((cmd: string) => {
163
+ if (cmd === 'which') return makeMockProc(0, '')
164
+ return makeMockProc(0, '{"result":"# My PRD\\n\\nThe actual content"}')
165
+ })
166
+ const { execute } = await import('./cursor.js')
167
+ const result = await execute(makeTask(), { cwd: tmpDir })
168
+ expect(result.output).toBe('# My PRD\n\nThe actual content')
169
+ })
170
+
171
+ it('extracts result from JSONL output (last line has result)', async () => {
172
+ const jsonl = [
173
+ '{"type":"progress","content":"thinking..."}',
174
+ '{"type":"tool_use","tool":"read_file","args":{}}',
175
+ '{"type":"result","result":"# My PRD\\n\\nThe actual markdown content","usage":{"input_tokens":500,"output_tokens":1000}}',
176
+ ].join('\n')
177
+ mockSpawn.mockImplementation((cmd: string) => {
178
+ if (cmd === 'which') return makeMockProc(0, '')
179
+ return makeMockProc(0, jsonl)
180
+ })
181
+ const { execute } = await import('./cursor.js')
182
+ const result = await execute(makeTask(), { cwd: tmpDir })
183
+ expect(result.output).toBe('# My PRD\n\nThe actual markdown content')
184
+ expect(result.usage?.prompt_tokens).toBe(500)
185
+ expect(result.usage?.completion_tokens).toBe(1000)
186
+ })
187
+
188
+ it('scans JSONL and finds result line among non-result lines', async () => {
189
+ const jsonl = [
190
+ '{"type":"progress","content":"thinking..."}',
191
+ '{"type":"tool_use","tool":"edit","args":{}}',
192
+ '{"type":"result","result":"# Final Content","usage":{"input_tokens":100,"output_tokens":200}}',
193
+ '{"type":"done"}',
194
+ ].join('\n')
195
+ mockSpawn.mockImplementation((cmd: string) => {
196
+ if (cmd === 'which') return makeMockProc(0, '')
197
+ return makeMockProc(0, jsonl)
198
+ })
199
+ const { execute } = await import('./cursor.js')
200
+ const result = await execute(makeTask(), { cwd: tmpDir })
201
+ expect(result.output).toBe('# Final Content')
202
+ })
203
+
204
+ it('extracts content from Copilot-style assistant.message JSONL', async () => {
205
+ const jsonl = [
206
+ '{"type":"session.tools_updated","data":{"model":"claude-sonnet-4.6"}}',
207
+ '{"type":"user.message","data":{"content":"Generate a PRD"}}',
208
+ '{"type":"assistant.message","data":{"content":"# My PRD\\n\\nThe generated content","outputTokens":50}}',
209
+ '{"type":"assistant.turn_end","data":{"turnId":"0"}}',
210
+ '{"type":"result","sessionId":"abc","exitCode":0,"usage":{"premiumRequests":1}}',
211
+ ].join('\n')
212
+ mockSpawn.mockImplementation((cmd: string) => {
213
+ if (cmd === 'which') return makeMockProc(0, '')
214
+ return makeMockProc(0, jsonl)
215
+ })
216
+ const { execute } = await import('./cursor.js')
217
+ const result = await execute(makeTask(), { cwd: tmpDir })
218
+ expect(result.output).toBe('# My PRD\n\nThe generated content')
219
+ })
220
+
221
+ it('uses last assistant.message when multiple turns exist', async () => {
222
+ const jsonl = [
223
+ '{"type":"assistant.message","data":{"content":"Let me check..."}}',
224
+ '{"type":"assistant.message","data":{"content":"# Final PRD\\n\\nComplete document"}}',
225
+ '{"type":"result","sessionId":"abc","exitCode":0}',
226
+ ].join('\n')
227
+ mockSpawn.mockImplementation((cmd: string) => {
228
+ if (cmd === 'which') return makeMockProc(0, '')
229
+ return makeMockProc(0, jsonl)
230
+ })
231
+ const { execute } = await import('./cursor.js')
232
+ const result = await execute(makeTask(), { cwd: tmpDir })
233
+ expect(result.output).toBe('# Final PRD\n\nComplete document')
234
+ })
235
+
236
+ it('falls back to raw output when JSON has no result field', async () => {
237
+ mockSpawn.mockImplementation((cmd: string) => {
238
+ if (cmd === 'which') return makeMockProc(0, '')
239
+ return makeMockProc(0, '{"status":"ok","data":"something"}')
240
+ })
241
+ const { execute } = await import('./cursor.js')
242
+ const result = await execute(makeTask(), { cwd: tmpDir })
243
+ expect(result.output).toBe('{"status":"ok","data":"something"}')
244
+ })
160
245
  })
@@ -88,14 +88,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
88
88
  let textOutput = [stdout, stderr].filter(Boolean).join('\n')
89
89
  let usage: TokenUsage | undefined
90
90
  try {
91
+ // Try single JSON object first (claude CLI)
91
92
  const parsed = JSON.parse(stdout) as Record<string, unknown>
92
-
93
- // Extract the actual AI text response from the JSON envelope
94
- const result = parsed.result as string | undefined
95
- if (typeof result === 'string') {
96
- textOutput = result
93
+ if (typeof parsed.result === 'string') {
94
+ textOutput = parsed.result
97
95
  }
98
-
99
96
  const u = parsed?.usage as Record<string, number> | undefined
100
97
  if (u) {
101
98
  const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
@@ -103,7 +100,44 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
103
100
  const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
104
101
  usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
105
102
  }
106
- } catch { /* not JSON or no usage — graceful degradation */ }
103
+ } catch {
104
+ // Fallback: parse JSONL (one JSON object per line)
105
+ // Claude CLI uses {"result": "text"}, Copilot CLI uses
106
+ // {"type":"assistant.message","data":{"content":"text"}} for the AI
107
+ // response and a separate {"type":"result"} line for session metadata.
108
+ const lines = stdout.split('\n')
109
+ let lastAssistantContent: string | undefined
110
+ for (const rawLine of lines) {
111
+ const line = rawLine.trim()
112
+ if (!line) continue
113
+ try {
114
+ const parsed = JSON.parse(line) as Record<string, unknown>
115
+ // Claude-style: result text in the result line
116
+ if (typeof parsed.result === 'string' && parsed.result) {
117
+ textOutput = parsed.result
118
+ const u = parsed?.usage as Record<string, number> | undefined
119
+ if (u) {
120
+ const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
121
+ const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
122
+ const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
123
+ usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
124
+ }
125
+ lastAssistantContent = undefined // prefer explicit result field
126
+ break
127
+ }
128
+ // Copilot-style: AI response in assistant.message events
129
+ if (parsed.type === 'assistant.message') {
130
+ const data = parsed.data as Record<string, unknown> | undefined
131
+ if (data && typeof data.content === 'string') {
132
+ lastAssistantContent = data.content
133
+ }
134
+ }
135
+ } catch { /* skip non-JSON lines */ }
136
+ }
137
+ if (lastAssistantContent !== undefined) {
138
+ textOutput = lastAssistantContent
139
+ }
140
+ }
107
141
  resolve({
108
142
  success: code === 0,
109
143
  output: textOutput.slice(0, 500_000),
@@ -130,6 +130,7 @@ describe('opencode adapter — MCP support', () => {
130
130
  })
131
131
  const { execute } = await import('./opencode.js')
132
132
  await execute(makeTask(), { cwd: tmpDir })
133
+ expect(capturedArgs).toContain('run')
133
134
  expect(capturedArgs).not.toContain('--mcp-config')
134
135
  })
135
136
 
@@ -156,4 +157,89 @@ describe('opencode adapter — MCP support', () => {
156
157
  await execute(makeTask(), { cwd: tmpDir })
157
158
  expect(capturedArgs).not.toContain('--approve-mcps')
158
159
  })
160
+
161
+ it('extracts result from single-line JSON output', async () => {
162
+ mockSpawn.mockImplementation((cmd: string) => {
163
+ if (cmd === 'which') return makeMockProc(0, '')
164
+ return makeMockProc(0, '{"result":"# My PRD\\n\\nThe actual content"}')
165
+ })
166
+ const { execute } = await import('./opencode.js')
167
+ const result = await execute(makeTask(), { cwd: tmpDir })
168
+ expect(result.output).toBe('# My PRD\n\nThe actual content')
169
+ })
170
+
171
+ it('extracts result from JSONL output (last line has result)', async () => {
172
+ const jsonl = [
173
+ '{"type":"progress","content":"thinking..."}',
174
+ '{"type":"tool_use","tool":"read_file","args":{}}',
175
+ '{"type":"result","result":"# My PRD\\n\\nThe actual markdown content","usage":{"input_tokens":500,"output_tokens":1000}}',
176
+ ].join('\n')
177
+ mockSpawn.mockImplementation((cmd: string) => {
178
+ if (cmd === 'which') return makeMockProc(0, '')
179
+ return makeMockProc(0, jsonl)
180
+ })
181
+ const { execute } = await import('./opencode.js')
182
+ const result = await execute(makeTask(), { cwd: tmpDir })
183
+ expect(result.output).toBe('# My PRD\n\nThe actual markdown content')
184
+ expect(result.usage?.prompt_tokens).toBe(500)
185
+ expect(result.usage?.completion_tokens).toBe(1000)
186
+ })
187
+
188
+ it('scans JSONL and finds result line among non-result lines', async () => {
189
+ const jsonl = [
190
+ '{"type":"progress","content":"thinking..."}',
191
+ '{"type":"tool_use","tool":"edit","args":{}}',
192
+ '{"type":"result","result":"# Final Content","usage":{"input_tokens":100,"output_tokens":200}}',
193
+ '{"type":"done"}',
194
+ ].join('\n')
195
+ mockSpawn.mockImplementation((cmd: string) => {
196
+ if (cmd === 'which') return makeMockProc(0, '')
197
+ return makeMockProc(0, jsonl)
198
+ })
199
+ const { execute } = await import('./opencode.js')
200
+ const result = await execute(makeTask(), { cwd: tmpDir })
201
+ expect(result.output).toBe('# Final Content')
202
+ })
203
+
204
+ it('extracts content from Copilot-style assistant.message JSONL', async () => {
205
+ const jsonl = [
206
+ '{"type":"session.tools_updated","data":{"model":"claude-sonnet-4.6"}}',
207
+ '{"type":"user.message","data":{"content":"Generate a PRD"}}',
208
+ '{"type":"assistant.message","data":{"content":"# My PRD\\n\\nThe generated content","outputTokens":50}}',
209
+ '{"type":"assistant.turn_end","data":{"turnId":"0"}}',
210
+ '{"type":"result","sessionId":"abc","exitCode":0,"usage":{"premiumRequests":1}}',
211
+ ].join('\n')
212
+ mockSpawn.mockImplementation((cmd: string) => {
213
+ if (cmd === 'which') return makeMockProc(0, '')
214
+ return makeMockProc(0, jsonl)
215
+ })
216
+ const { execute } = await import('./opencode.js')
217
+ const result = await execute(makeTask(), { cwd: tmpDir })
218
+ expect(result.output).toBe('# My PRD\n\nThe generated content')
219
+ })
220
+
221
+ it('uses last assistant.message when multiple turns exist', async () => {
222
+ const jsonl = [
223
+ '{"type":"assistant.message","data":{"content":"Let me check..."}}',
224
+ '{"type":"assistant.message","data":{"content":"# Final PRD\\n\\nComplete document"}}',
225
+ '{"type":"result","sessionId":"abc","exitCode":0}',
226
+ ].join('\n')
227
+ mockSpawn.mockImplementation((cmd: string) => {
228
+ if (cmd === 'which') return makeMockProc(0, '')
229
+ return makeMockProc(0, jsonl)
230
+ })
231
+ const { execute } = await import('./opencode.js')
232
+ const result = await execute(makeTask(), { cwd: tmpDir })
233
+ expect(result.output).toBe('# Final PRD\n\nComplete document')
234
+ })
235
+
236
+ it('falls back to raw output when JSON has no result field', async () => {
237
+ mockSpawn.mockImplementation((cmd: string) => {
238
+ if (cmd === 'which') return makeMockProc(0, '')
239
+ return makeMockProc(0, '{"status":"ok","data":"something"}')
240
+ })
241
+ const { execute } = await import('./opencode.js')
242
+ const result = await execute(makeTask(), { cwd: tmpDir })
243
+ expect(result.output).toBe('{"status":"ok","data":"something"}')
244
+ })
159
245
  })
@@ -28,7 +28,7 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
28
28
  prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
29
29
  }
30
30
 
31
- const args = ['--headless', '-p', prompt, '--output-format', 'json']
31
+ const args = ['run', prompt, '--format', 'json']
32
32
 
33
33
  const cwd = options?.cwd ?? process.cwd()
34
34
  const mcpJsonPath = join(cwd, 'mcp.json')
@@ -82,14 +82,11 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
82
82
  let textOutput = [stdout, stderr].filter(Boolean).join('\n')
83
83
  let usage: TokenUsage | undefined
84
84
  try {
85
+ // Try single JSON object first (claude CLI)
85
86
  const parsed = JSON.parse(stdout) as Record<string, unknown>
86
-
87
- // Extract the actual AI text response from the JSON envelope
88
- const result = parsed.result as string | undefined
89
- if (typeof result === 'string') {
90
- textOutput = result
87
+ if (typeof parsed.result === 'string') {
88
+ textOutput = parsed.result
91
89
  }
92
-
93
90
  const u = parsed?.usage as Record<string, number> | undefined
94
91
  if (u) {
95
92
  const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
@@ -97,7 +94,44 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
97
94
  const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
98
95
  usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
99
96
  }
100
- } catch { /* not JSON or no usage — graceful degradation */ }
97
+ } catch {
98
+ // Fallback: parse JSONL (one JSON object per line)
99
+ // Claude CLI uses {"result": "text"}, Copilot CLI uses
100
+ // {"type":"assistant.message","data":{"content":"text"}} for the AI
101
+ // response and a separate {"type":"result"} line for session metadata.
102
+ const lines = stdout.split('\n')
103
+ let lastAssistantContent: string | undefined
104
+ for (const rawLine of lines) {
105
+ const line = rawLine.trim()
106
+ if (!line) continue
107
+ try {
108
+ const parsed = JSON.parse(line) as Record<string, unknown>
109
+ // Claude-style: result text in the result line
110
+ if (typeof parsed.result === 'string' && parsed.result) {
111
+ textOutput = parsed.result
112
+ const u = parsed?.usage as Record<string, number> | undefined
113
+ if (u) {
114
+ const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
115
+ const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
116
+ const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
117
+ usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
118
+ }
119
+ lastAssistantContent = undefined // prefer explicit result field
120
+ break
121
+ }
122
+ // Copilot-style: AI response in assistant.message events
123
+ if (parsed.type === 'assistant.message') {
124
+ const data = parsed.data as Record<string, unknown> | undefined
125
+ if (data && typeof data.content === 'string') {
126
+ lastAssistantContent = data.content
127
+ }
128
+ }
129
+ } catch { /* skip non-JSON lines */ }
130
+ }
131
+ if (lastAssistantContent !== undefined) {
132
+ textOutput = lastAssistantContent
133
+ }
134
+ }
101
135
  resolve({
102
136
  success: code === 0,
103
137
  output: textOutput.slice(0, 500_000),
@@ -51,21 +51,21 @@
51
51
  "name": "docs/api-v2-contract.json",
52
52
  "type": "json",
53
53
  "task_id": "api-t1",
54
- "created_at": "2026-04-10T09:30:29.641Z"
55
- },
56
- {
57
- "id": "artifact-demo-api-v2-reports-security-gate-failure-md",
58
- "name": "reports/security-gate-failure.md",
59
- "type": "summary",
60
- "task_id": "api-t3",
61
- "created_at": "2026-04-10T09:30:29.641Z"
54
+ "created_at": "2026-04-10T12:41:00.577Z"
62
55
  },
63
56
  {
64
57
  "id": "artifact-demo-api-v2-src-api-rate-limiter-ts",
65
58
  "name": "src/api/rate-limiter.ts",
66
59
  "type": "file",
67
60
  "task_id": "api-t2",
68
- "created_at": "2026-04-10T09:30:29.641Z"
61
+ "created_at": "2026-04-10T12:41:00.577Z"
62
+ },
63
+ {
64
+ "id": "artifact-demo-api-v2-reports-security-gate-failure-md",
65
+ "name": "reports/security-gate-failure.md",
66
+ "type": "summary",
67
+ "task_id": "api-t3",
68
+ "created_at": "2026-04-10T12:41:00.578Z"
69
69
  }
70
70
  ],
71
71
  "has_more_events": false,
@@ -42,28 +42,28 @@
42
42
  "name": "libs/auth/src/jwt-middleware.ts",
43
43
  "type": "file",
44
44
  "task_id": "auth-t2",
45
- "created_at": "2026-04-10T09:30:29.640Z"
45
+ "created_at": "2026-04-10T12:41:00.577Z"
46
46
  },
47
47
  {
48
48
  "id": "artifact-demo-auth-revamp-libs-auth-src-rls-policies-sql",
49
49
  "name": "libs/auth/src/rls-policies.sql",
50
50
  "type": "file",
51
51
  "task_id": "auth-t3",
52
- "created_at": "2026-04-10T09:30:29.640Z"
52
+ "created_at": "2026-04-10T12:41:00.577Z"
53
53
  },
54
54
  {
55
55
  "id": "artifact-demo-auth-revamp-reports-auth-review-summary-md",
56
56
  "name": "reports/auth-review-summary.md",
57
57
  "type": "summary",
58
58
  "task_id": "auth-t5",
59
- "created_at": "2026-04-10T09:30:29.640Z"
59
+ "created_at": "2026-04-10T12:41:00.577Z"
60
60
  },
61
61
  {
62
62
  "id": "artifact-demo-auth-revamp-tests-auth-integration-test-ts",
63
63
  "name": "tests/auth/integration.test.ts",
64
64
  "type": "file",
65
65
  "task_id": "auth-t4",
66
- "created_at": "2026-04-10T09:30:29.640Z"
66
+ "created_at": "2026-04-10T12:41:00.577Z"
67
67
  }
68
68
  ],
69
69
  "has_more_events": false,
@@ -46,47 +46,47 @@
46
46
  ],
47
47
  "artifact_count": 6,
48
48
  "artifacts": [
49
- {
50
- "id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
51
- "name": "src/components/design-tokens.ts",
52
- "type": "file",
53
- "task_id": "ui-t1",
54
- "created_at": "2026-04-10T09:30:29.640Z"
55
- },
56
49
  {
57
50
  "id": "artifact-demo-dashboard-ui-reports-panel-review-dashboard-md",
58
51
  "name": "reports/panel-review-dashboard.md",
59
52
  "type": "summary",
60
53
  "task_id": "ui-t7",
61
- "created_at": "2026-04-10T09:30:29.641Z"
54
+ "created_at": "2026-04-10T12:41:00.577Z"
62
55
  },
63
56
  {
64
57
  "id": "artifact-demo-dashboard-ui-reports-visual-regression-json",
65
58
  "name": "reports/visual-regression.json",
66
59
  "type": "json",
67
60
  "task_id": "ui-t6",
68
- "created_at": "2026-04-10T09:30:29.641Z"
61
+ "created_at": "2026-04-10T12:41:00.577Z"
69
62
  },
70
63
  {
71
64
  "id": "artifact-demo-dashboard-ui-src-components-DonutChart-tsx",
72
65
  "name": "src/components/DonutChart.tsx",
73
66
  "type": "file",
74
67
  "task_id": "ui-t3",
75
- "created_at": "2026-04-10T09:30:29.641Z"
68
+ "created_at": "2026-04-10T12:41:00.577Z"
76
69
  },
77
70
  {
78
71
  "id": "artifact-demo-dashboard-ui-src-components-KpiCard-tsx",
79
72
  "name": "src/components/KpiCard.tsx",
80
73
  "type": "file",
81
74
  "task_id": "ui-t2",
82
- "created_at": "2026-04-10T09:30:29.641Z"
75
+ "created_at": "2026-04-10T12:41:00.577Z"
76
+ },
77
+ {
78
+ "id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
79
+ "name": "src/components/design-tokens.ts",
80
+ "type": "file",
81
+ "task_id": "ui-t1",
82
+ "created_at": "2026-04-10T12:41:00.577Z"
83
83
  },
84
84
  {
85
85
  "id": "artifact-demo-dashboard-ui-src-styles-animations-css",
86
86
  "name": "src/styles/animations.css",
87
87
  "type": "file",
88
88
  "task_id": "ui-t4",
89
- "created_at": "2026-04-10T09:30:29.641Z"
89
+ "created_at": "2026-04-10T12:41:00.577Z"
90
90
  }
91
91
  ],
92
92
  "has_more_events": false,
@@ -42,21 +42,21 @@
42
42
  "name": "src/etl/pipeline.ts",
43
43
  "type": "file",
44
44
  "task_id": "etl-t2",
45
- "created_at": "2026-04-10T09:30:29.642Z"
45
+ "created_at": "2026-04-10T12:41:00.578Z"
46
46
  },
47
47
  {
48
48
  "id": "artifact-demo-data-pipeline-src-etl-schema-ts",
49
49
  "name": "src/etl/schema.ts",
50
50
  "type": "file",
51
51
  "task_id": "etl-t1",
52
- "created_at": "2026-04-10T09:30:29.642Z"
52
+ "created_at": "2026-04-10T12:41:00.578Z"
53
53
  },
54
54
  {
55
55
  "id": "artifact-demo-data-pipeline-tests-etl-pipeline-test-ts",
56
56
  "name": "tests/etl/pipeline.test.ts",
57
57
  "type": "file",
58
58
  "task_id": "etl-t3",
59
- "created_at": "2026-04-10T09:30:29.642Z"
59
+ "created_at": "2026-04-10T12:41:00.578Z"
60
60
  }
61
61
  ],
62
62
  "has_more_events": false,
@@ -51,7 +51,7 @@
51
51
  "name": ".github/workflows/ci.yml",
52
52
  "type": "file",
53
53
  "task_id": "ci-t1",
54
- "created_at": "2026-04-10T09:30:29.642Z"
54
+ "created_at": "2026-04-10T12:41:00.578Z"
55
55
  }
56
56
  ],
57
57
  "has_more_events": false,