opencastle 0.27.0 → 0.27.1

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 (205) hide show
  1. package/bin/cli.mjs +6 -0
  2. package/dist/cli/agents.d.ts +3 -0
  3. package/dist/cli/agents.d.ts.map +1 -0
  4. package/dist/cli/agents.js +161 -0
  5. package/dist/cli/agents.js.map +1 -0
  6. package/dist/cli/baselines.d.ts +3 -0
  7. package/dist/cli/baselines.d.ts.map +1 -0
  8. package/dist/cli/baselines.js +128 -0
  9. package/dist/cli/baselines.js.map +1 -0
  10. package/dist/cli/convoy/engine.d.ts +68 -2
  11. package/dist/cli/convoy/engine.d.ts.map +1 -1
  12. package/dist/cli/convoy/engine.js +2102 -26
  13. package/dist/cli/convoy/engine.js.map +1 -1
  14. package/dist/cli/convoy/engine.test.js +1572 -70
  15. package/dist/cli/convoy/engine.test.js.map +1 -1
  16. package/dist/cli/convoy/events.d.ts +4 -1
  17. package/dist/cli/convoy/events.d.ts.map +1 -1
  18. package/dist/cli/convoy/events.js +74 -13
  19. package/dist/cli/convoy/events.js.map +1 -1
  20. package/dist/cli/convoy/events.test.js +154 -27
  21. package/dist/cli/convoy/events.test.js.map +1 -1
  22. package/dist/cli/convoy/expertise.d.ts +16 -0
  23. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  24. package/dist/cli/convoy/expertise.js +121 -0
  25. package/dist/cli/convoy/expertise.js.map +1 -0
  26. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  27. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  28. package/dist/cli/convoy/expertise.test.js +96 -0
  29. package/dist/cli/convoy/expertise.test.js.map +1 -0
  30. package/dist/cli/convoy/export.test.js +1 -0
  31. package/dist/cli/convoy/export.test.js.map +1 -1
  32. package/dist/cli/convoy/formula.d.ts +19 -0
  33. package/dist/cli/convoy/formula.d.ts.map +1 -0
  34. package/dist/cli/convoy/formula.js +142 -0
  35. package/dist/cli/convoy/formula.js.map +1 -0
  36. package/dist/cli/convoy/formula.test.d.ts +2 -0
  37. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  38. package/dist/cli/convoy/formula.test.js +342 -0
  39. package/dist/cli/convoy/formula.test.js.map +1 -0
  40. package/dist/cli/convoy/gates.d.ts +128 -0
  41. package/dist/cli/convoy/gates.d.ts.map +1 -0
  42. package/dist/cli/convoy/gates.js +606 -0
  43. package/dist/cli/convoy/gates.js.map +1 -0
  44. package/dist/cli/convoy/gates.test.d.ts +2 -0
  45. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  46. package/dist/cli/convoy/gates.test.js +976 -0
  47. package/dist/cli/convoy/gates.test.js.map +1 -0
  48. package/dist/cli/convoy/health.d.ts +11 -0
  49. package/dist/cli/convoy/health.d.ts.map +1 -1
  50. package/dist/cli/convoy/health.js +54 -0
  51. package/dist/cli/convoy/health.js.map +1 -1
  52. package/dist/cli/convoy/health.test.js +56 -1
  53. package/dist/cli/convoy/health.test.js.map +1 -1
  54. package/dist/cli/convoy/issues.d.ts +8 -0
  55. package/dist/cli/convoy/issues.d.ts.map +1 -0
  56. package/dist/cli/convoy/issues.js +98 -0
  57. package/dist/cli/convoy/issues.js.map +1 -0
  58. package/dist/cli/convoy/issues.test.d.ts +2 -0
  59. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  60. package/dist/cli/convoy/issues.test.js +107 -0
  61. package/dist/cli/convoy/issues.test.js.map +1 -0
  62. package/dist/cli/convoy/knowledge.d.ts +5 -0
  63. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  64. package/dist/cli/convoy/knowledge.js +116 -0
  65. package/dist/cli/convoy/knowledge.js.map +1 -0
  66. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  67. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/knowledge.test.js +87 -0
  69. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  70. package/dist/cli/convoy/lessons.d.ts +17 -0
  71. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  72. package/dist/cli/convoy/lessons.js +149 -0
  73. package/dist/cli/convoy/lessons.js.map +1 -0
  74. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  75. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/lessons.test.js +135 -0
  77. package/dist/cli/convoy/lessons.test.js.map +1 -0
  78. package/dist/cli/convoy/lock.d.ts +13 -0
  79. package/dist/cli/convoy/lock.d.ts.map +1 -0
  80. package/dist/cli/convoy/lock.js +88 -0
  81. package/dist/cli/convoy/lock.js.map +1 -0
  82. package/dist/cli/convoy/lock.test.d.ts +2 -0
  83. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/lock.test.js +136 -0
  85. package/dist/cli/convoy/lock.test.js.map +1 -0
  86. package/dist/cli/convoy/merge.d.ts +4 -0
  87. package/dist/cli/convoy/merge.d.ts.map +1 -1
  88. package/dist/cli/convoy/merge.js +18 -1
  89. package/dist/cli/convoy/merge.js.map +1 -1
  90. package/dist/cli/convoy/merge.test.js +6 -7
  91. package/dist/cli/convoy/merge.test.js.map +1 -1
  92. package/dist/cli/convoy/partition.d.ts +51 -0
  93. package/dist/cli/convoy/partition.d.ts.map +1 -0
  94. package/dist/cli/convoy/partition.js +186 -0
  95. package/dist/cli/convoy/partition.js.map +1 -0
  96. package/dist/cli/convoy/partition.test.d.ts +2 -0
  97. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  98. package/dist/cli/convoy/partition.test.js +315 -0
  99. package/dist/cli/convoy/partition.test.js.map +1 -0
  100. package/dist/cli/convoy/pipeline.test.js +6 -0
  101. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  102. package/dist/cli/convoy/store.d.ts +47 -5
  103. package/dist/cli/convoy/store.d.ts.map +1 -1
  104. package/dist/cli/convoy/store.js +525 -19
  105. package/dist/cli/convoy/store.js.map +1 -1
  106. package/dist/cli/convoy/store.test.js +1345 -12
  107. package/dist/cli/convoy/store.test.js.map +1 -1
  108. package/dist/cli/convoy/types.d.ts +156 -2
  109. package/dist/cli/convoy/types.d.ts.map +1 -1
  110. package/dist/cli/run/adapters/claude.d.ts +2 -0
  111. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  112. package/dist/cli/run/adapters/claude.js +89 -49
  113. package/dist/cli/run/adapters/claude.js.map +1 -1
  114. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  115. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  116. package/dist/cli/run/adapters/claude.test.js +205 -0
  117. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  118. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  119. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  120. package/dist/cli/run/adapters/copilot.js +84 -46
  121. package/dist/cli/run/adapters/copilot.js.map +1 -1
  122. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  123. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  124. package/dist/cli/run/adapters/copilot.test.js +195 -0
  125. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  126. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  127. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  128. package/dist/cli/run/adapters/cursor.js +83 -47
  129. package/dist/cli/run/adapters/cursor.js.map +1 -1
  130. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  131. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  132. package/dist/cli/run/adapters/cursor.test.js +129 -0
  133. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  134. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  135. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  136. package/dist/cli/run/adapters/opencode.js +81 -47
  137. package/dist/cli/run/adapters/opencode.js.map +1 -1
  138. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  139. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  140. package/dist/cli/run/adapters/opencode.test.js +119 -0
  141. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  142. package/dist/cli/run/executor.js +1 -1
  143. package/dist/cli/run/executor.js.map +1 -1
  144. package/dist/cli/run/schema.d.ts.map +1 -1
  145. package/dist/cli/run/schema.js +245 -4
  146. package/dist/cli/run/schema.js.map +1 -1
  147. package/dist/cli/run/schema.test.js +669 -0
  148. package/dist/cli/run/schema.test.js.map +1 -1
  149. package/dist/cli/run.d.ts.map +1 -1
  150. package/dist/cli/run.js +362 -22
  151. package/dist/cli/run.js.map +1 -1
  152. package/dist/cli/types.d.ts +85 -2
  153. package/dist/cli/types.d.ts.map +1 -1
  154. package/dist/cli/types.js.map +1 -1
  155. package/dist/cli/watch.d.ts +15 -0
  156. package/dist/cli/watch.d.ts.map +1 -0
  157. package/dist/cli/watch.js +279 -0
  158. package/dist/cli/watch.js.map +1 -0
  159. package/package.json +1 -1
  160. package/src/cli/agents.ts +177 -0
  161. package/src/cli/baselines.ts +143 -0
  162. package/src/cli/convoy/engine.test.ts +1839 -70
  163. package/src/cli/convoy/engine.ts +2417 -38
  164. package/src/cli/convoy/events.test.ts +179 -38
  165. package/src/cli/convoy/events.ts +88 -16
  166. package/src/cli/convoy/expertise.test.ts +128 -0
  167. package/src/cli/convoy/expertise.ts +163 -0
  168. package/src/cli/convoy/export.test.ts +1 -0
  169. package/src/cli/convoy/formula.test.ts +405 -0
  170. package/src/cli/convoy/formula.ts +174 -0
  171. package/src/cli/convoy/gates.test.ts +1169 -0
  172. package/src/cli/convoy/gates.ts +774 -0
  173. package/src/cli/convoy/health.test.ts +64 -2
  174. package/src/cli/convoy/health.ts +80 -2
  175. package/src/cli/convoy/issues.test.ts +143 -0
  176. package/src/cli/convoy/issues.ts +136 -0
  177. package/src/cli/convoy/knowledge.test.ts +101 -0
  178. package/src/cli/convoy/knowledge.ts +132 -0
  179. package/src/cli/convoy/lessons.test.ts +188 -0
  180. package/src/cli/convoy/lessons.ts +164 -0
  181. package/src/cli/convoy/lock.test.ts +181 -0
  182. package/src/cli/convoy/lock.ts +103 -0
  183. package/src/cli/convoy/merge.test.ts +6 -7
  184. package/src/cli/convoy/merge.ts +19 -1
  185. package/src/cli/convoy/partition.test.ts +423 -0
  186. package/src/cli/convoy/partition.ts +232 -0
  187. package/src/cli/convoy/pipeline.test.ts +6 -0
  188. package/src/cli/convoy/store.test.ts +1512 -14
  189. package/src/cli/convoy/store.ts +676 -30
  190. package/src/cli/convoy/types.ts +170 -1
  191. package/src/cli/run/adapters/claude.test.ts +234 -0
  192. package/src/cli/run/adapters/claude.ts +45 -5
  193. package/src/cli/run/adapters/copilot.test.ts +224 -0
  194. package/src/cli/run/adapters/copilot.ts +34 -4
  195. package/src/cli/run/adapters/cursor.test.ts +144 -0
  196. package/src/cli/run/adapters/cursor.ts +33 -2
  197. package/src/cli/run/adapters/opencode.test.ts +135 -0
  198. package/src/cli/run/adapters/opencode.ts +30 -2
  199. package/src/cli/run/executor.ts +1 -1
  200. package/src/cli/run/schema.test.ts +758 -0
  201. package/src/cli/run/schema.ts +300 -25
  202. package/src/cli/run.ts +341 -21
  203. package/src/cli/types.ts +86 -1
  204. package/src/cli/watch.ts +298 -0
  205. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
@@ -1,4 +1,4 @@
1
- export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed'
1
+ export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed' | 'hook-failed'
2
2
 
3
3
  export type ConvoyTaskStatus =
4
4
  | 'pending'
@@ -6,8 +6,13 @@ export type ConvoyTaskStatus =
6
6
  | 'running'
7
7
  | 'done'
8
8
  | 'failed'
9
+ | 'gate-failed'
10
+ | 'review-blocked'
9
11
  | 'timed-out'
10
12
  | 'skipped'
13
+ | 'hook-failed'
14
+ | 'disputed'
15
+ | 'wait-for-input'
11
16
 
12
17
  export type WorkerStatus = 'spawned' | 'running' | 'done' | 'failed' | 'killed'
13
18
 
@@ -26,6 +31,9 @@ export interface ConvoyRecord {
26
31
  total_tokens: number | null
27
32
  total_cost_usd: string | null
28
33
  pipeline_id: string | null
34
+ circuit_state: string | null
35
+ review_tokens_total: number | null
36
+ review_budget: number | null
29
37
  }
30
38
 
31
39
  export interface TaskRecord {
@@ -52,6 +60,24 @@ export interface TaskRecord {
52
60
  completion_tokens: number | null
53
61
  total_tokens: number | null
54
62
  cost_usd: string | null
63
+ gates: string | null
64
+ on_exhausted: 'dlq' | 'skip' | 'stop'
65
+ injected: number
66
+ provenance: string | null
67
+ idempotency_key: string | null
68
+ current_step: number | null
69
+ total_steps: number | null
70
+ review_level: string | null
71
+ review_verdict: string | null
72
+ review_tokens: number | null
73
+ review_model: string | null
74
+ panel_attempts: number
75
+ dispute_id: string | null
76
+ drift_score: number | null
77
+ drift_retried: number
78
+ outputs?: string | null // JSON array of TaskOutput
79
+ inputs?: string | null // JSON array of TaskInput
80
+ discovered_issues?: string | null // JSON array
55
81
  }
56
82
 
57
83
  export interface WorkerRecord {
@@ -90,3 +116,146 @@ export interface PipelineRecord {
90
116
  total_tokens: number | null
91
117
  total_cost_usd: string | null
92
118
  }
119
+
120
+ export interface BuiltInGatesConfig {
121
+ secret_scan?: boolean
122
+ blast_radius?: boolean
123
+ dependency_audit?: 'auto' | boolean
124
+ regression_test?: 'auto' | boolean
125
+ browser_test?: 'auto' | boolean
126
+ gate_timeout?: number
127
+ }
128
+
129
+
130
+ export interface BrowserTestConfig {
131
+ urls: string[]
132
+ check_console_errors?: boolean
133
+ visual_diff_threshold?: number
134
+ a11y?: boolean
135
+ severity_threshold?: 'critical' | 'serious' | 'moderate' | 'minor'
136
+ baselines_dir?: string
137
+ }
138
+ export interface GuardConfig {
139
+ enabled?: boolean // default: true
140
+ agent?: string // optional agent name (e.g. 'session-guard')
141
+ checks?: string[] // e.g. ['observability', 'cleanup', 'cost-report']
142
+ }
143
+
144
+ export interface DlqRecord {
145
+ id: string
146
+ convoy_id: string
147
+ task_id: string
148
+ agent: string
149
+ failure_type: string
150
+ error_output: string | null
151
+ attempts: number
152
+ tokens_spent: number | null
153
+ escalation_task_id: string | null
154
+ resolved: number
155
+ resolution: string | null
156
+ created_at: string
157
+ resolved_at: string | null
158
+ }
159
+
160
+ export interface CircuitBreakerConfig {
161
+ threshold?: number // failures before Open (default: 3)
162
+ cooldown_ms?: number // ms in Open before Half-Open (default: 300000 = 5min)
163
+ fallback_agent?: string // reassign pending tasks when circuit opens
164
+ }
165
+
166
+ export interface TaskOutput {
167
+ name: string
168
+ type: 'file' | 'summary' | 'json'
169
+ description?: string
170
+ }
171
+
172
+ export interface TaskInput {
173
+ from: string
174
+ name: string
175
+ as?: string
176
+ }
177
+
178
+ export interface ArtifactRecord {
179
+ id: string
180
+ convoy_id: string
181
+ task_id: string
182
+ name: string
183
+ type: 'file' | 'summary' | 'json'
184
+ content: string
185
+ created_at: string
186
+ }
187
+
188
+ export interface AgentIdentityRecord {
189
+ id: string
190
+ agent: string
191
+ convoy_id: string
192
+ task_id: string
193
+ summary: string
194
+ created_at: string
195
+ retention_days: number
196
+ }
197
+
198
+ export interface StepCondition {
199
+ step: string // reference previous step by id
200
+ exitCode?: { eq?: number; ne?: number; gt?: number; lt?: number }
201
+ fileExists?: { path: string }
202
+ }
203
+
204
+ export interface TaskStep {
205
+ id?: string
206
+ prompt: string
207
+ gates?: string[]
208
+ max_retries?: number // inherits from task if omitted
209
+ if?: StepCondition
210
+ }
211
+
212
+ export interface Hook {
213
+ type: 'review' | 'guard' | 'agent' | 'command' | 'validate'
214
+ name?: string
215
+ prompt?: string // for agent hooks
216
+ command?: string // for command hooks
217
+ on?: 'pre_task' | 'post_task' | 'post_convoy'
218
+ }
219
+
220
+ export interface TaskStepRecord {
221
+ id: number
222
+ task_id: string
223
+ step_index: number
224
+ prompt: string
225
+ gates: string | null
226
+ status: string
227
+ exit_code: number | null
228
+ output: string | null
229
+ started_at: string | null
230
+ finished_at: string | null
231
+ }
232
+
233
+ export interface WatchTrigger {
234
+ type: 'file-change' | 'cron' | 'git-push'
235
+ glob?: string // for file-change: glob pattern to watch
236
+ schedule?: string // for cron: 5-field cron expression
237
+ branch?: string // for git-push: branch name pattern
238
+ debounce_ms?: number // file-change debounce (default: 500ms)
239
+ }
240
+
241
+ export interface WatchConfig {
242
+ triggers: WatchTrigger[]
243
+ clear_scratchpad?: boolean // clear scratchpad on watch start
244
+ scratchpad_retention_days?: number // auto-clear scratchpad entries older than N days
245
+ }
246
+
247
+ export interface ScratchpadRecord {
248
+ key: string
249
+ value: string
250
+ updated_at: string
251
+ }
252
+
253
+ export interface MCPServerConfig {
254
+ name: string
255
+ type: string
256
+ local?: boolean
257
+ command?: string
258
+ args?: string[]
259
+ url?: string
260
+ config?: Record<string, unknown>
261
+ }
@@ -0,0 +1,234 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { EventEmitter } from 'node:events'
6
+ import type { Task } from '../../types.js'
7
+
8
+ // ── Helpers ───────────────────────────────────────────────────────────────────
9
+
10
+ function makeTask(): Task {
11
+ return {
12
+ id: 'test-task',
13
+ agent: 'developer',
14
+ prompt: 'Do something',
15
+ files: [],
16
+ timeout: '5m',
17
+ depends_on: [],
18
+ description: 'test task',
19
+ max_retries: 0,
20
+ } as unknown as Task
21
+ }
22
+
23
+ function makeMockProc(exitCode = 0, stdoutData = '{"result":"ok"}') {
24
+ const proc = new EventEmitter() as EventEmitter & {
25
+ stdout: EventEmitter
26
+ stderr: EventEmitter
27
+ killed: boolean
28
+ kill: ReturnType<typeof vi.fn>
29
+ }
30
+ proc.stdout = new EventEmitter()
31
+ proc.stderr = new EventEmitter()
32
+ proc.killed = false
33
+ proc.kill = vi.fn()
34
+ process.nextTick(() => {
35
+ if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData))
36
+ proc.emit('close', exitCode)
37
+ })
38
+ return proc
39
+ }
40
+
41
+ // ── SDK mode ──────────────────────────────────────────────────────────────────
42
+
43
+ describe('claude adapter — SDK mode', () => {
44
+ let mockCreateSession: ReturnType<typeof vi.fn>
45
+ let mockSession: {
46
+ sendAndWait: ReturnType<typeof vi.fn>
47
+ on: ReturnType<typeof vi.fn>
48
+ destroy: ReturnType<typeof vi.fn>
49
+ abort: ReturnType<typeof vi.fn>
50
+ }
51
+
52
+ beforeEach(() => {
53
+ vi.resetModules()
54
+ mockSession = {
55
+ sendAndWait: vi.fn().mockResolvedValue({ data: { content: 'I did the task' } }),
56
+ on: vi.fn(),
57
+ destroy: vi.fn().mockResolvedValue(undefined),
58
+ abort: vi.fn().mockResolvedValue(undefined),
59
+ }
60
+ mockCreateSession = vi.fn().mockResolvedValue(mockSession)
61
+ vi.doMock('@anthropic-ai/agent-sdk', () => {
62
+ // Must use a regular function (not arrow) so `new AgentClient()` works
63
+ function MockAgentClient(this: Record<string, unknown>) {
64
+ this.start = vi.fn().mockResolvedValue(undefined)
65
+ this.createSession = mockCreateSession
66
+ }
67
+ return {
68
+ AgentClient: MockAgentClient,
69
+ approveAll: vi.fn(),
70
+ }
71
+ })
72
+ })
73
+
74
+ afterEach(() => {
75
+ vi.restoreAllMocks()
76
+ })
77
+
78
+ it('passes mcpServers to createSession when provided', async () => {
79
+ const { execute } = await import('./claude.js')
80
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
81
+ await execute(makeTask(), { mcpServers })
82
+ expect(mockCreateSession).toHaveBeenCalledWith(
83
+ expect.objectContaining({ mcpServers }),
84
+ )
85
+ })
86
+
87
+ it('does NOT include mcpServers in createSession when not provided', async () => {
88
+ const { execute } = await import('./claude.js')
89
+ await execute(makeTask(), {})
90
+ const callArg = mockCreateSession.mock.calls[0]?.[0] as Record<string, unknown>
91
+ expect(callArg).not.toHaveProperty('mcpServers')
92
+ })
93
+
94
+ it('does NOT include mcpServers when mcpServers is empty array', async () => {
95
+ const { execute } = await import('./claude.js')
96
+ await execute(makeTask(), { mcpServers: [] })
97
+ const callArg = mockCreateSession.mock.calls[0]?.[0] as Record<string, unknown>
98
+ expect(callArg).not.toHaveProperty('mcpServers')
99
+ })
100
+ })
101
+
102
+ // ── CLI mode ──────────────────────────────────────────────────────────────────
103
+
104
+ describe('claude adapter — CLI mode', () => {
105
+ let tmpDir: string
106
+ let mockSpawn: ReturnType<typeof vi.fn>
107
+
108
+ beforeEach(() => {
109
+ vi.resetModules()
110
+ tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'claude-test-')))
111
+
112
+ mockSpawn = vi.fn().mockImplementation((cmd: string) => {
113
+ if (cmd === 'which') return makeMockProc(0, '')
114
+ return makeMockProc(0, '{"result":"ok"}')
115
+ })
116
+ vi.doMock('node:child_process', () => ({ spawn: mockSpawn }))
117
+ })
118
+
119
+ afterEach(() => {
120
+ rmSync(tmpDir, { recursive: true, force: true })
121
+ vi.restoreAllMocks()
122
+ })
123
+
124
+ it('writes mcp.json to cwd with correct format when mcpServers provided', async () => {
125
+ let capturedContent: string | null = null
126
+ mockSpawn.mockImplementation((cmd: string) => {
127
+ if (cmd === 'which') return makeMockProc(0, '')
128
+ const mcpPath = join(tmpDir, 'mcp.json')
129
+ if (existsSync(mcpPath)) {
130
+ capturedContent = readFileSync(mcpPath, 'utf8')
131
+ }
132
+ return makeMockProc(0, '{}')
133
+ })
134
+
135
+ const { executeViaCli } = await import('./claude.js')
136
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
137
+ await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
138
+
139
+ expect(capturedContent).not.toBeNull()
140
+ expect(JSON.parse(capturedContent!)).toEqual({
141
+ mcpServers: { 'my-mcp': { command: 'node', args: ['server.js'] } },
142
+ })
143
+ })
144
+
145
+ it('passes --mcp-config flag pointing to mcp.json path', async () => {
146
+ const capturedArgs: string[] = []
147
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
148
+ if (cmd === 'which') return makeMockProc(0, '')
149
+ capturedArgs.push(...args)
150
+ return makeMockProc(0, '{}')
151
+ })
152
+ const { executeViaCli } = await import('./claude.js')
153
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
154
+ await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
155
+
156
+ const idx = capturedArgs.indexOf('--mcp-config')
157
+ expect(idx).toBeGreaterThanOrEqual(0)
158
+ expect(capturedArgs[idx + 1]).toBe(join(tmpDir, 'mcp.json'))
159
+ })
160
+
161
+ it('cleans up mcp.json after successful execution', async () => {
162
+ const { executeViaCli } = await import('./claude.js')
163
+ const mcpServers = [{ name: 'my-mcp', type: 'local', command: 'node', args: ['server.js'] }]
164
+ await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
165
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
166
+ })
167
+
168
+ it('cleans up mcp.json after failed execution (non-zero exit)', async () => {
169
+ mockSpawn.mockImplementation((cmd: string) => {
170
+ if (cmd === 'which') return makeMockProc(0, '')
171
+ return makeMockProc(1, '')
172
+ })
173
+ const { executeViaCli } = await import('./claude.js')
174
+ const mcpServers = [{ name: 'err-mcp', type: 'local', command: 'node', args: [] }]
175
+ await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
176
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
177
+ })
178
+
179
+ it('includes --approve-mcps flag when mcp_approve_all is true', async () => {
180
+ const capturedArgs: string[] = []
181
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
182
+ if (cmd === 'which') return makeMockProc(0, '')
183
+ capturedArgs.push(...args)
184
+ return makeMockProc(0, '{}')
185
+ })
186
+ const { executeViaCli } = await import('./claude.js')
187
+ await executeViaCli(makeTask(), { mcp_approve_all: true, cwd: tmpDir })
188
+ expect(capturedArgs).toContain('--approve-mcps')
189
+ })
190
+
191
+ it('does NOT write mcp.json when mcpServers not configured', async () => {
192
+ const { executeViaCli } = await import('./claude.js')
193
+ await executeViaCli(makeTask(), { cwd: tmpDir })
194
+ expect(existsSync(join(tmpDir, 'mcp.json'))).toBe(false)
195
+ })
196
+
197
+ it('does NOT add --approve-mcps when mcp_approve_all is not set', async () => {
198
+ const capturedArgs: string[] = []
199
+ mockSpawn.mockImplementation((cmd: string, args: string[]) => {
200
+ if (cmd === 'which') return makeMockProc(0, '')
201
+ capturedArgs.push(...args)
202
+ return makeMockProc(0, '{}')
203
+ })
204
+ const { executeViaCli } = await import('./claude.js')
205
+ await executeViaCli(makeTask(), { cwd: tmpDir })
206
+ expect(capturedArgs).not.toContain('--approve-mcps')
207
+ })
208
+
209
+ it('maps mcpServers with url and config into mcp.json', async () => {
210
+ let capturedContent: string | null = null
211
+ mockSpawn.mockImplementation((cmd: string) => {
212
+ if (cmd === 'which') return makeMockProc(0, '')
213
+ const mcpPath = join(tmpDir, 'mcp.json')
214
+ if (existsSync(mcpPath)) capturedContent = readFileSync(mcpPath, 'utf8')
215
+ return makeMockProc(0, '{}')
216
+ })
217
+ const { executeViaCli } = await import('./claude.js')
218
+ const mcpServers = [
219
+ {
220
+ name: 'remote-mcp',
221
+ type: 'remote',
222
+ url: 'http://localhost:9000',
223
+ config: { token: 'abc' },
224
+ },
225
+ ]
226
+ await executeViaCli(makeTask(), { mcpServers, cwd: tmpDir })
227
+ expect(capturedContent).not.toBeNull()
228
+ const parsed = JSON.parse(capturedContent!) as { mcpServers: Record<string, Record<string, unknown>> }
229
+ expect(parsed.mcpServers['remote-mcp']).toMatchObject({
230
+ url: 'http://localhost:9000',
231
+ token: 'abc',
232
+ })
233
+ })
234
+ })
@@ -1,10 +1,13 @@
1
1
  import { spawn } from 'node:child_process'
2
+ import { writeFileSync, unlinkSync } from 'node:fs'
3
+ import { join } from 'node:path'
2
4
  import { parseTimeout } from '../schema.js'
3
5
  import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
4
6
 
5
7
  // Adapter name
6
8
  export const name = 'claude'
7
9
 
10
+ export function supportsSessionContinuity(): boolean { return false }
8
11
  // Module-level state for mode selection
9
12
  let mode: 'sdk' | 'cli' | null = null
10
13
 
@@ -100,6 +103,8 @@ async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<
100
103
  },
101
104
  infiniteSessions: { enabled: false },
102
105
  ...(options.verbose ? { streaming: true } : {}),
106
+ // mcpServers is forward-compatible: field will be recognised by future SDK versions
107
+ ...(options.mcpServers?.length ? { mcpServers: options.mcpServers } : {}),
103
108
  })
104
109
  activeSessions.set(task.id, session)
105
110
  if (options.verbose) {
@@ -108,12 +113,21 @@ async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<
108
113
  process.stdout.write(event.data.deltaContent)
109
114
  })
110
115
  }
116
+ interface SdkResponse {
117
+ data?: {
118
+ content?: string
119
+ usage?: Record<string, number>
120
+ }
121
+ usage?: Record<string, number>
122
+ }
123
+
111
124
  try {
112
125
  const timeoutMs = parseTimeout(task.timeout)
113
126
  const response = await session.sendAndWait({ prompt }, timeoutMs)
114
- const data = (response as any)?.data as Record<string, unknown> | undefined
127
+ const typed = response as SdkResponse
128
+ const data = typed?.data
115
129
  const output = (data?.content as string | undefined) ?? ''
116
- const rawUsage = data?.usage ?? (response as any)?.usage
130
+ const rawUsage = data?.usage ?? typed?.usage
117
131
  const u = rawUsage as Record<string, number> | undefined
118
132
  const usageResult = u
119
133
  ? {
@@ -150,7 +164,7 @@ function killSdk(task: Task): void {
150
164
  }
151
165
 
152
166
  // --- CLI implementation (from claude-code.ts) ---
153
- async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
167
+ export async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
154
168
  let prompt = `You are a ${task.agent}. ${task.prompt}`
155
169
  if (task.files && task.files.length > 0) {
156
170
  prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
@@ -163,11 +177,32 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
163
177
  '--max-turns',
164
178
  '50',
165
179
  ]
166
- return new Promise((resolve) => {
180
+ const cwd = options?.cwd ?? process.cwd()
181
+ const mcpJsonPath = join(cwd, 'mcp.json')
182
+ let wroteJson = false
183
+ if (options.mcpServers?.length) {
184
+ const mcpJson: Record<string, Record<string, unknown>> = {}
185
+ for (const server of options.mcpServers) {
186
+ const entry: Record<string, unknown> = {}
187
+ if (server.command) entry.command = server.command
188
+ if (server.args) entry.args = server.args
189
+ if (server.url) entry.url = server.url
190
+ if (server.config) Object.assign(entry, server.config)
191
+ mcpJson[server.name] = entry
192
+ }
193
+ writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: mcpJson }, null, 2), 'utf8')
194
+ args.push('--mcp-config', mcpJsonPath)
195
+ wroteJson = true
196
+ }
197
+ if (options.mcp_approve_all) {
198
+ args.push('--approve-mcps')
199
+ }
200
+ try {
201
+ return await new Promise<ExecuteResult>((resolve) => {
167
202
  const proc = spawn('claude', args, {
168
203
  stdio: ['ignore', 'pipe', 'pipe'],
169
204
  env: { ...process.env },
170
- cwd: options?.cwd ?? process.cwd(),
205
+ cwd,
171
206
  })
172
207
  let stdout = ''
173
208
  let stderr = ''
@@ -212,6 +247,11 @@ async function executeViaCli(task: Task, options: ExecuteOptions = {}): Promise<
212
247
  })
213
248
  task._process = proc
214
249
  })
250
+ } finally {
251
+ if (wroteJson) {
252
+ try { unlinkSync(mcpJsonPath) } catch { /* ignore */ }
253
+ }
254
+ }
215
255
  }
216
256
 
217
257
  function killCli(task: Task): void {