@swarmclawai/swarmclaw 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -1,788 +1,387 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
- import { genId } from '@/lib/id'
4
3
  import { spawn, spawnSync } from 'child_process'
5
- import { loadAgents, loadTasks, upsertTask } from '../storage'
6
- import { log } from '../logger'
7
4
  import type { ToolBuildContext } from './context'
8
- import { truncate, tail, extractResumeIdentifier, findBinaryOnPath, MAX_OUTPUT } from './context'
5
+ import { truncate, findBinaryOnPath, MAX_OUTPUT } from './context'
6
+ import type { Plugin, PluginHooks } from '@/types'
7
+ import { getPluginManager } from '../plugins'
8
+ import { normalizeToolInputArgs } from './normalize-tool-args'
9
+
10
+ const MAX_DELEGATION_CHAIN_HOPS = 128
11
+
12
+ interface DelegateContext {
13
+ cwd?: string
14
+ claudeTimeoutMs?: number
15
+ readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode') => string | null
16
+ persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode', id: string) => void
17
+ ctx?: { platformAssignScope?: string; agentId?: string | null }
18
+ hasTool?: (name: string) => boolean
19
+ }
9
20
 
10
- export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
11
- const tools: StructuredToolInterface[] = []
12
- const { cwd, ctx, claudeTimeoutMs, cliProcessTimeoutMs, persistDelegateResumeId, readStoredDelegateResumeId } = bctx
21
+ type DelegateBackend = 'claude' | 'codex' | 'opencode'
13
22
 
14
- const wantsClaudeDelegate = bctx.hasTool('claude_code')
15
- const wantsCodexDelegate = bctx.hasTool('codex_cli')
16
- const wantsOpenCodeDelegate = bctx.hasTool('opencode_cli')
23
+ function asTaskRecord(value: unknown): Record<string, unknown> | null {
24
+ return value && typeof value === 'object' ? value as Record<string, unknown> : null
25
+ }
17
26
 
18
- if (wantsClaudeDelegate || wantsCodexDelegate || wantsOpenCodeDelegate) {
19
- const claudeBinary = findBinaryOnPath('claude')
20
- const codexBinary = findBinaryOnPath('codex')
21
- const opencodeBinary = findBinaryOnPath('opencode')
27
+ function parseNonNegativeInt(value: unknown): number | null {
28
+ if (typeof value !== 'number' || !Number.isFinite(value)) return null
29
+ const int = Math.trunc(value)
30
+ return int >= 0 ? int : null
31
+ }
22
32
 
23
- if (wantsClaudeDelegate && !claudeBinary) {
24
- log.warn('session-tools', 'Claude delegation enabled but claude binary not found', {
25
- sessionId: ctx?.sessionId || null,
26
- agentId: ctx?.agentId || null,
27
- })
28
- }
29
- if (wantsCodexDelegate && !codexBinary) {
30
- log.warn('session-tools', 'Codex delegation enabled but codex binary not found', {
31
- sessionId: ctx?.sessionId || null,
32
- agentId: ctx?.agentId || null,
33
- })
34
- }
35
- if (wantsOpenCodeDelegate && !opencodeBinary) {
36
- log.warn('session-tools', 'OpenCode delegation enabled but opencode binary not found', {
37
- sessionId: ctx?.sessionId || null,
38
- agentId: ctx?.agentId || null,
39
- })
33
+ function _computeDelegationDepth(
34
+ task: Record<string, unknown> | null,
35
+ tasksById: Record<string, unknown>,
36
+ ): number {
37
+ if (!task) return 0
38
+ const explicitDepth = parseNonNegativeInt(task.delegationDepth)
39
+ if (explicitDepth !== null) return explicitDepth
40
+ if (task.sourceType !== 'delegation') return 0
41
+
42
+ let depth = 1
43
+ let parentId = typeof task.delegatedFromTaskId === 'string' ? task.delegatedFromTaskId.trim() : ''
44
+ let hops = 0
45
+ const visited = new Set<string>()
46
+
47
+ while (parentId && hops < MAX_DELEGATION_CHAIN_HOPS && !visited.has(parentId)) {
48
+ visited.add(parentId)
49
+ const parent = asTaskRecord(tasksById[parentId])
50
+ if (!parent) break
51
+ const parentExplicitDepth = parseNonNegativeInt(parent.delegationDepth)
52
+ if (parentExplicitDepth !== null) {
53
+ depth = Math.max(depth, parentExplicitDepth + 1)
54
+ break
40
55
  }
56
+ depth++
57
+ parentId = typeof parent.delegatedFromTaskId === 'string' ? parent.delegatedFromTaskId.trim() : ''
58
+ hops++
59
+ }
41
60
 
42
- if (claudeBinary && wantsClaudeDelegate) {
43
- tools.push(
44
- tool(
45
- async ({ task, resume, resumeId }) => {
46
- try {
47
- const env: NodeJS.ProcessEnv = { ...process.env }
48
- // Running inside Claude environments can block nested `claude` launches.
49
- // Strip all CLAUDE* vars so delegation can run as an independent subprocess.
50
- const removedClaudeEnvKeys: string[] = []
51
- for (const key of Object.keys(env)) {
52
- if (key.toUpperCase().startsWith('CLAUDE')) {
53
- removedClaudeEnvKeys.push(key)
54
- delete env[key]
55
- }
56
- }
61
+ return depth
62
+ }
57
63
 
58
- // Fast preflight: when Claude isn't authenticated, surface a clear error immediately.
59
- const authProbe = spawnSync(claudeBinary, ['auth', 'status'], {
60
- cwd,
61
- env,
62
- encoding: 'utf-8',
63
- timeout: 8000,
64
- })
65
- if ((authProbe.status ?? 1) !== 0) {
66
- let loggedIn = false
67
- try {
68
- const parsed = JSON.parse(authProbe.stdout || '{}') as { loggedIn?: boolean }
69
- loggedIn = parsed.loggedIn === true
70
- } catch {
71
- // ignore parse issues and fall back to a generic auth guidance
72
- }
73
- if (!loggedIn) {
74
- return 'Error: Claude Code CLI is not authenticated. Run `claude auth login` (or `claude setup-token`) on this machine, then retry.'
75
- }
76
- }
64
+ /**
65
+ * Core Delegate Execution Logic
66
+ */
67
+ async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
68
+ const normalized = normalizeToolInputArgs(args)
69
+ const task = normalized.task as string
70
+ const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
71
+ const resume = normalized.resume as boolean
72
+ const resumeId = normalized.resumeId as string
73
+ const backends = {
74
+ claude: findBinaryOnPath('claude'),
75
+ codex: findBinaryOnPath('codex'),
76
+ opencode: findBinaryOnPath('opencode'),
77
+ }
78
+ const binary = backends[backend as keyof typeof backends]
79
+ if (!binary) return `Error: Backend "${backend}" unavailable.`
77
80
 
78
- const storedResumeId = readStoredDelegateResumeId('claudeCode')
79
- const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
80
- ? resumeId.trim()
81
- : (resume ? storedResumeId : null)
82
-
83
- log.info('session-tools', 'delegate_to_claude_code start', {
84
- sessionId: ctx?.sessionId || null,
85
- agentId: ctx?.agentId || null,
86
- cwd,
87
- timeoutMs: claudeTimeoutMs,
88
- removedClaudeEnvKeys,
89
- resumeRequested: !!resume || !!resumeId,
90
- resumeId: resumeIdToUse || null,
91
- taskPreview: (task || '').slice(0, 200),
92
- })
93
-
94
- return new Promise<string>((resolve) => {
95
- const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
96
- if (resumeIdToUse) args.push('--resume', resumeIdToUse)
97
- const child = spawn(claudeBinary, args, {
98
- cwd,
99
- env,
100
- stdio: ['pipe', 'pipe', 'pipe'],
101
- })
102
- let stdout = ''
103
- let stderr = ''
104
- let stdoutBuf = ''
105
- let assistantText = ''
106
- let discoveredSessionId: string | null = null
107
- let settled = false
108
- let timedOut = false
109
- const startedAt = Date.now()
110
-
111
- const finish = (result: string) => {
112
- if (settled) return
113
- settled = true
114
- resolve(truncate(result, MAX_OUTPUT))
115
- }
116
-
117
- const timeoutHandle = setTimeout(() => {
118
- timedOut = true
119
- try { child.kill('SIGTERM') } catch { /* ignore */ }
120
- setTimeout(() => {
121
- try { child.kill('SIGKILL') } catch { /* ignore */ }
122
- }, 5000)
123
- }, claudeTimeoutMs)
124
-
125
- log.info('session-tools', 'delegate_to_claude_code spawned', {
126
- sessionId: ctx?.sessionId || null,
127
- pid: child.pid || null,
128
- args,
129
- })
130
- child.stdout?.on('data', (chunk: Buffer) => {
131
- const text = chunk.toString()
132
- stdout += text
133
- if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
134
- stdoutBuf += text
135
- const lines = stdoutBuf.split('\n')
136
- stdoutBuf = lines.pop() || ''
137
- for (const line of lines) {
138
- if (!line.trim()) continue
139
- try {
140
- const ev = JSON.parse(line)
141
- if (typeof ev?.session_id === 'string' && ev.session_id.trim()) {
142
- discoveredSessionId = ev.session_id.trim()
143
- }
144
- if (ev?.type === 'result' && typeof ev?.result === 'string') {
145
- assistantText = ev.result
146
- } else if (ev?.type === 'assistant' && Array.isArray(ev?.message?.content)) {
147
- const textBlocks = ev.message.content
148
- .filter((block: any) => block?.type === 'text' && typeof block?.text === 'string')
149
- .map((block: any) => block.text)
150
- .join('')
151
- if (textBlocks) assistantText = textBlocks
152
- } else if (ev?.type === 'content_block_delta' && typeof ev?.delta?.text === 'string') {
153
- assistantText += ev.delta.text
154
- }
155
- } catch {
156
- // keep raw stdout fallback when parsing fails
157
- }
158
- }
159
- })
160
- child.stderr?.on('data', (chunk: Buffer) => {
161
- stderr += chunk.toString()
162
- if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
163
- })
164
- child.on('error', (err) => {
165
- clearTimeout(timeoutHandle)
166
- log.error('session-tools', 'delegate_to_claude_code child error', {
167
- sessionId: ctx?.sessionId || null,
168
- error: err?.message || String(err),
169
- })
170
- finish(`Error: failed to start Claude Code CLI: ${err?.message || String(err)}`)
171
- })
172
- child.on('close', (code, signal) => {
173
- clearTimeout(timeoutHandle)
174
- const durationMs = Date.now() - startedAt
175
- if (!discoveredSessionId) {
176
- const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
177
- if (guessed) discoveredSessionId = guessed
178
- }
179
- if (discoveredSessionId) persistDelegateResumeId('claudeCode', discoveredSessionId)
180
- log.info('session-tools', 'delegate_to_claude_code child close', {
181
- sessionId: ctx?.sessionId || null,
182
- code,
183
- signal: signal || null,
184
- timedOut,
185
- durationMs,
186
- stdoutLen: stdout.length,
187
- stderrLen: stderr.length,
188
- discoveredSessionId,
189
- stderrPreview: tail(stderr, 240),
190
- })
191
- if (timedOut) {
192
- const msg = [
193
- `Error: Claude Code CLI timed out after ${Math.round(claudeTimeoutMs / 1000)}s.`,
194
- stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
195
- stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
196
- 'Try increasing "Claude Code Timeout (sec)" in Settings.',
197
- ].filter(Boolean).join('\n\n')
198
- finish(msg)
199
- return
200
- }
201
-
202
- // If resume failed because the session no longer exists, clear the stale ID
203
- // and return a targeted error so the agent retries without resume
204
- if (resumeIdToUse && /No conversation found/i.test(stdout + stderr)) {
205
- persistDelegateResumeId('claudeCode', null)
206
- log.warn('session-tools', 'delegate_to_claude_code stale resume ID cleared', {
207
- sessionId: ctx?.sessionId || null,
208
- staleResumeId: resumeIdToUse,
209
- })
210
- finish(
211
- `Error: The previous Claude Code session (${resumeIdToUse}) has expired and was cleared. ` +
212
- 'Retry the task with resume=false to start a fresh session.',
213
- )
214
- return
215
- }
216
-
217
- const successText = assistantText.trim() || stdout.trim() || stderr.trim()
218
- if (code === 0 && successText) {
219
- const out = discoveredSessionId
220
- ? `${successText}\n\n[delegate_meta]\nresume_id=${discoveredSessionId}`
221
- : successText
222
- finish(out)
223
- return
224
- }
225
-
226
- const msg = [
227
- `Error: Claude Code CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
228
- stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
229
- stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
230
- ].filter(Boolean).join('\n\n')
231
- finish(msg || 'Error: Claude Code CLI returned no output.')
232
- })
233
-
234
- try {
235
- child.stdin?.write(task)
236
- child.stdin?.end()
237
- } catch (err: any) {
238
- clearTimeout(timeoutHandle)
239
- finish(`Error: failed to send task to Claude Code CLI: ${err?.message || String(err)}`)
240
- }
241
- })
242
- } catch (err: any) {
243
- return `Error delegating to Claude Code: ${err.message}`
244
- }
245
- },
246
- {
247
- name: 'delegate_to_claude_code',
248
- description: 'Delegate a complex multi-file coding task to Claude Code CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
249
- schema: z.object({
250
- task: z.string().describe('Detailed description of the task for Claude Code'),
251
- resume: z.boolean().optional().describe('If true, try to resume the last saved Claude delegation session for this SwarmClaw session'),
252
- resumeId: z.string().optional().describe('Explicit Claude session id to resume (overrides resume=true memory)'),
253
- }),
254
- },
255
- ),
256
- )
257
- }
81
+ if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx)
82
+ if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx)
83
+ if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx)
84
+ return `Error: Unsupported backend "${backend}".`
85
+ }
258
86
 
259
- if (codexBinary && wantsCodexDelegate) {
260
- tools.push(
261
- tool(
262
- async ({ task, resume, resumeId }) => {
263
- try {
264
- const env: NodeJS.ProcessEnv = { ...process.env, TERM: 'dumb', NO_COLOR: '1' }
265
- const removedCodexEnvKeys: string[] = []
266
- for (const key of Object.keys(env)) {
267
- if (key.toUpperCase().startsWith('CODEX')) {
268
- removedCodexEnvKeys.push(key)
269
- delete env[key]
270
- }
271
- }
87
+ function stripEnvPrefixes(input: NodeJS.ProcessEnv, prefixes: string[]): NodeJS.ProcessEnv {
88
+ const out: NodeJS.ProcessEnv = { ...input }
89
+ for (const key of Object.keys(out)) {
90
+ const upper = key.toUpperCase()
91
+ if (prefixes.some((prefix) => upper.startsWith(prefix))) delete out[key]
92
+ }
93
+ return out
94
+ }
272
95
 
273
- const hasApiKey = typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim().length > 0
274
- if (!hasApiKey) {
275
- const loginProbe = spawnSync(codexBinary, ['login', 'status'], {
276
- cwd,
277
- env,
278
- encoding: 'utf-8',
279
- timeout: 8000,
280
- })
281
- const probeText = `${loginProbe.stdout || ''}\n${loginProbe.stderr || ''}`.toLowerCase()
282
- const loggedIn = probeText.includes('logged in')
283
- if ((loginProbe.status ?? 1) !== 0 || !loggedIn) {
284
- return 'Error: Codex CLI is not authenticated. Run `codex login` (or set OPENAI_API_KEY), then retry.'
285
- }
286
- }
96
+ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
97
+ if (ev.type === 'item.content_part.delta') {
98
+ const delta = ev.delta as Record<string, unknown> | undefined
99
+ if (typeof delta?.text === 'string') return delta.text
100
+ }
101
+ if (ev.type === 'item.completed') {
102
+ const item = ev.item as Record<string, unknown> | undefined
103
+ if (item?.type === 'agent_message' && typeof item.text === 'string') return item.text
104
+ if (item?.type === 'message' && item?.role === 'assistant') {
105
+ const content = item.content
106
+ if (typeof content === 'string') return content
107
+ if (Array.isArray(content)) {
108
+ const parts = content
109
+ .filter((entry) => entry && typeof entry === 'object' && (entry as Record<string, unknown>).type === 'output_text')
110
+ .map((entry) => String((entry as Record<string, unknown>).text || ''))
111
+ const joined = parts.join('')
112
+ if (joined) return joined
113
+ }
114
+ }
115
+ }
116
+ return null
117
+ }
287
118
 
288
- const storedResumeId = readStoredDelegateResumeId('codex')
289
- const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
290
- ? resumeId.trim()
291
- : (resume ? storedResumeId : null)
292
-
293
- log.info('session-tools', 'delegate_to_codex_cli start', {
294
- sessionId: ctx?.sessionId || null,
295
- agentId: ctx?.agentId || null,
296
- cwd,
297
- timeoutMs: cliProcessTimeoutMs,
298
- removedCodexEnvKeys,
299
- resumeRequested: !!resume || !!resumeId,
300
- resumeId: resumeIdToUse || null,
301
- taskPreview: (task || '').slice(0, 200),
302
- })
303
-
304
- return new Promise<string>((resolve) => {
305
- const args = ['exec']
306
- if (resumeIdToUse) args.push('resume', resumeIdToUse)
307
- args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
308
- const child = spawn(codexBinary, args, {
309
- cwd,
310
- env,
311
- stdio: ['pipe', 'pipe', 'pipe'],
312
- })
313
- let stdout = ''
314
- let stderr = ''
315
- let settled = false
316
- let timedOut = false
317
- const startedAt = Date.now()
318
- let agentText = ''
319
- let discoveredThreadId: string | null = null
320
- const eventErrors: string[] = []
321
- let stdoutBuf = ''
322
-
323
- const finish = (result: string) => {
324
- if (settled) return
325
- settled = true
326
- resolve(truncate(result, MAX_OUTPUT))
327
- }
328
-
329
- const timeoutHandle = setTimeout(() => {
330
- timedOut = true
331
- try { child.kill('SIGTERM') } catch { /* ignore */ }
332
- setTimeout(() => {
333
- try { child.kill('SIGKILL') } catch { /* ignore */ }
334
- }, 5000)
335
- }, cliProcessTimeoutMs)
336
-
337
- log.info('session-tools', 'delegate_to_codex_cli spawned', {
338
- sessionId: ctx?.sessionId || null,
339
- pid: child.pid || null,
340
- args,
341
- })
342
-
343
- child.stdout?.on('data', (chunk: Buffer) => {
344
- const text = chunk.toString()
345
- stdout += text
346
- if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
347
-
348
- stdoutBuf += text
349
- const lines = stdoutBuf.split('\n')
350
- stdoutBuf = lines.pop() || ''
351
- for (const line of lines) {
352
- if (!line.trim()) continue
353
- try {
354
- const ev = JSON.parse(line)
355
- if (typeof ev?.thread_id === 'string' && ev.thread_id.trim()) {
356
- discoveredThreadId = ev.thread_id.trim()
357
- }
358
- if (ev.type === 'item.completed' && ev.item?.type === 'agent_message' && typeof ev.item?.text === 'string') {
359
- agentText = ev.item.text
360
- } else if (ev.type === 'item.completed' && ev.item?.type === 'message' && ev.item?.role === 'assistant') {
361
- const content = ev.item.content
362
- if (Array.isArray(content)) {
363
- const txt = content
364
- .filter((c: any) => c?.type === 'output_text' && typeof c?.text === 'string')
365
- .map((c: any) => c.text)
366
- .join('')
367
- if (txt) agentText = txt
368
- } else if (typeof content === 'string') {
369
- agentText = content
370
- }
371
- } else if (ev.type === 'error' && ev.message) {
372
- eventErrors.push(String(ev.message))
373
- } else if (ev.type === 'turn.failed' && ev.error?.message) {
374
- eventErrors.push(String(ev.error.message))
375
- }
376
- } catch {
377
- // Ignore non-JSON lines in parser path; raw stdout still captured above.
378
- }
379
- }
380
- })
381
- child.stderr?.on('data', (chunk: Buffer) => {
382
- stderr += chunk.toString()
383
- if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
384
- })
385
- child.on('error', (err) => {
386
- clearTimeout(timeoutHandle)
387
- log.error('session-tools', 'delegate_to_codex_cli child error', {
388
- sessionId: ctx?.sessionId || null,
389
- error: err?.message || String(err),
390
- })
391
- finish(`Error: failed to start Codex CLI: ${err?.message || String(err)}`)
392
- })
393
- child.on('close', (code, signal) => {
394
- clearTimeout(timeoutHandle)
395
- const durationMs = Date.now() - startedAt
396
- if (!discoveredThreadId) {
397
- const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
398
- if (guessed) discoveredThreadId = guessed
399
- }
400
- if (discoveredThreadId) persistDelegateResumeId('codex', discoveredThreadId)
401
- log.info('session-tools', 'delegate_to_codex_cli child close', {
402
- sessionId: ctx?.sessionId || null,
403
- code,
404
- signal: signal || null,
405
- timedOut,
406
- durationMs,
407
- stdoutLen: stdout.length,
408
- stderrLen: stderr.length,
409
- eventErrorCount: eventErrors.length,
410
- discoveredThreadId,
411
- stderrPreview: tail(stderr, 240),
412
- })
413
- if (timedOut) {
414
- const msg = [
415
- `Error: Codex CLI timed out after ${Math.round(cliProcessTimeoutMs / 1000)}s.`,
416
- stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
417
- eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
418
- 'Try increasing "CLI Process Timeout (sec)" in Settings.',
419
- ].filter(Boolean).join('\n\n')
420
- finish(msg)
421
- return
422
- }
423
- if (code === 0 && agentText.trim()) {
424
- const out = discoveredThreadId
425
- ? `${agentText.trim()}\n\n[delegate_meta]\nresume_id=${discoveredThreadId}`
426
- : agentText.trim()
427
- finish(out)
428
- return
429
- }
430
- if (code === 0 && stdout.trim() && !eventErrors.length) {
431
- const out = discoveredThreadId
432
- ? `${stdout.trim()}\n\n[delegate_meta]\nresume_id=${discoveredThreadId}`
433
- : stdout.trim()
434
- finish(out)
435
- return
436
- }
437
- const msg = [
438
- `Error: Codex CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
439
- eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
440
- stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
441
- stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
442
- ].filter(Boolean).join('\n\n')
443
- finish(msg || 'Error: Codex CLI returned no output.')
444
- })
445
-
446
- try {
447
- child.stdin?.write(task)
448
- child.stdin?.end()
449
- } catch (err: any) {
450
- clearTimeout(timeoutHandle)
451
- finish(`Error: failed to send task to Codex CLI: ${err?.message || String(err)}`)
452
- }
453
- })
454
- } catch (err: any) {
455
- return `Error delegating to Codex CLI: ${err.message}`
456
- }
457
- },
458
- {
459
- name: 'delegate_to_codex_cli',
460
- description: 'Delegate a complex multi-file coding task to Codex CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
461
- schema: z.object({
462
- task: z.string().describe('Detailed description of the task for Codex CLI'),
463
- resume: z.boolean().optional().describe('If true, try to resume the last saved Codex delegation thread for this SwarmClaw session'),
464
- resumeId: z.string().optional().describe('Explicit Codex thread id to resume (overrides resume=true memory)'),
465
- }),
466
- },
467
- ),
468
- )
119
+ async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
120
+ try {
121
+ const env = stripEnvPrefixes({ ...process.env, TERM: 'dumb', NO_COLOR: '1' }, ['CODEX'])
122
+ const authProbe = spawnSync(binary, ['login', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
123
+ const probeText = `${authProbe.stdout || ''}\n${authProbe.stderr || ''}`.toLowerCase()
124
+ const loggedIn = probeText.includes('logged in')
125
+ if ((authProbe.status ?? 1) !== 0 || !loggedIn) {
126
+ return 'Error: Codex CLI is not authenticated. Run `codex login` and retry.'
469
127
  }
470
128
 
471
- if (opencodeBinary && wantsOpenCodeDelegate) {
472
- tools.push(
473
- tool(
474
- async ({ task, resume, resumeId }) => {
129
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('codex')
130
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
131
+
132
+ return await new Promise<string>((resolve) => {
133
+ const args: string[] = ['exec']
134
+ if (resumeIdToUse) args.push('resume', resumeIdToUse)
135
+ args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
136
+
137
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
138
+ let stdoutBuf = ''
139
+ let stderrBuf = ''
140
+ let responseText = ''
141
+ let discoveredId: string | null = null
142
+ let settled = false
143
+
144
+ const finish = (text: string) => {
145
+ if (settled) return
146
+ settled = true
147
+ resolve(truncate(text, MAX_OUTPUT))
148
+ }
149
+
150
+ const timeoutHandle = setTimeout(() => {
151
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
152
+ }, bctx.claudeTimeoutMs || 300000)
153
+
154
+ child.stdout?.on('data', (chunk) => {
155
+ stdoutBuf += chunk.toString()
156
+ const lines = stdoutBuf.split('\n')
157
+ stdoutBuf = lines.pop() || ''
158
+ for (const line of lines) {
159
+ const trimmed = line.trim()
160
+ if (!trimmed) continue
475
161
  try {
476
- const env: NodeJS.ProcessEnv = { ...process.env, TERM: 'dumb', NO_COLOR: '1' }
477
- const storedResumeId = readStoredDelegateResumeId('opencode')
478
- const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
479
- ? resumeId.trim()
480
- : (resume ? storedResumeId : null)
481
-
482
- log.info('session-tools', 'delegate_to_opencode_cli start', {
483
- sessionId: ctx?.sessionId || null,
484
- agentId: ctx?.agentId || null,
485
- cwd,
486
- timeoutMs: cliProcessTimeoutMs,
487
- resumeRequested: !!resume || !!resumeId,
488
- resumeId: resumeIdToUse || null,
489
- taskPreview: (task || '').slice(0, 200),
490
- })
491
-
492
- return new Promise<string>((resolve) => {
493
- const args = ['run', task, '--format', 'json']
494
- if (resumeIdToUse) args.push('--session', resumeIdToUse)
495
- const child = spawn(opencodeBinary, args, {
496
- cwd,
497
- env,
498
- stdio: ['pipe', 'pipe', 'pipe'],
499
- })
500
- let stdout = ''
501
- let stderr = ''
502
- let discoveredSessionId: string | null = null
503
- let parsedText = ''
504
- const eventErrors: string[] = []
505
- let stdoutBuf = ''
506
- let settled = false
507
- let timedOut = false
508
- const startedAt = Date.now()
509
-
510
- const finish = (result: string) => {
511
- if (settled) return
512
- settled = true
513
- resolve(truncate(result, MAX_OUTPUT))
514
- }
515
-
516
- const timeoutHandle = setTimeout(() => {
517
- timedOut = true
518
- try { child.kill('SIGTERM') } catch { /* ignore */ }
519
- setTimeout(() => {
520
- try { child.kill('SIGKILL') } catch { /* ignore */ }
521
- }, 5000)
522
- }, cliProcessTimeoutMs)
523
-
524
- log.info('session-tools', 'delegate_to_opencode_cli spawned', {
525
- sessionId: ctx?.sessionId || null,
526
- pid: child.pid || null,
527
- args: resumeIdToUse
528
- ? ['run', '(task hidden)', '--format', 'json', '--session', resumeIdToUse]
529
- : ['run', '(task hidden)', '--format', 'json'],
530
- })
531
- child.stdout?.on('data', (chunk: Buffer) => {
532
- const text = chunk.toString()
533
- stdout += text
534
- if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
535
- stdoutBuf += text
536
- const lines = stdoutBuf.split('\n')
537
- stdoutBuf = lines.pop() || ''
538
- for (const line of lines) {
539
- if (!line.trim()) continue
540
- try {
541
- const ev = JSON.parse(line)
542
- if (typeof ev?.sessionID === 'string' && ev.sessionID.trim()) {
543
- discoveredSessionId = ev.sessionID.trim()
544
- }
545
- if (ev?.type === 'text' && typeof ev?.part?.text === 'string') {
546
- parsedText += ev.part.text
547
- } else if (ev?.type === 'error') {
548
- const msg = typeof ev?.error === 'string'
549
- ? ev.error
550
- : typeof ev?.message === 'string'
551
- ? ev.message
552
- : 'Unknown OpenCode event error'
553
- eventErrors.push(msg)
554
- }
555
- } catch {
556
- // keep raw stdout fallback
557
- }
558
- }
559
- })
560
- child.stderr?.on('data', (chunk: Buffer) => {
561
- stderr += chunk.toString()
562
- if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
563
- })
564
- child.on('error', (err) => {
565
- clearTimeout(timeoutHandle)
566
- log.error('session-tools', 'delegate_to_opencode_cli child error', {
567
- sessionId: ctx?.sessionId || null,
568
- error: err?.message || String(err),
569
- })
570
- finish(`Error: failed to start OpenCode CLI: ${err?.message || String(err)}`)
571
- })
572
- child.on('close', (code, signal) => {
573
- clearTimeout(timeoutHandle)
574
- const durationMs = Date.now() - startedAt
575
- const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
576
- if (guessed) discoveredSessionId = guessed
577
- if (discoveredSessionId) persistDelegateResumeId('opencode', discoveredSessionId)
578
- log.info('session-tools', 'delegate_to_opencode_cli child close', {
579
- sessionId: ctx?.sessionId || null,
580
- code,
581
- signal: signal || null,
582
- timedOut,
583
- durationMs,
584
- stdoutLen: stdout.length,
585
- stderrLen: stderr.length,
586
- parsedTextLen: parsedText.length,
587
- eventErrorCount: eventErrors.length,
588
- discoveredSessionId,
589
- stderrPreview: tail(stderr, 240),
590
- })
591
- if (timedOut) {
592
- const msg = [
593
- `Error: OpenCode CLI timed out after ${Math.round(cliProcessTimeoutMs / 1000)}s.`,
594
- stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
595
- eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
596
- stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
597
- 'Try increasing "CLI Process Timeout (sec)" in Settings.',
598
- ].filter(Boolean).join('\n\n')
599
- finish(msg)
600
- return
601
- }
602
- const successText = parsedText.trim() || stdout.trim() || stderr.trim()
603
- if (code === 0 && successText) {
604
- const out = discoveredSessionId
605
- ? `${successText}\n\n[delegate_meta]\nresume_id=${discoveredSessionId}`
606
- : successText
607
- finish(out)
608
- return
609
- }
610
- const msg = [
611
- `Error: OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
612
- eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
613
- stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
614
- stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
615
- ].filter(Boolean).join('\n\n')
616
- finish(msg || 'Error: OpenCode CLI returned no output.')
617
- })
618
- })
619
- } catch (err: any) {
620
- return `Error delegating to OpenCode CLI: ${err.message}`
162
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
163
+ if (ev.type === 'thread.started' && typeof ev.thread_id === 'string') discoveredId = ev.thread_id
164
+ const parsedText = parseCodexOutputText(ev)
165
+ if (parsedText) responseText = parsedText
166
+ } catch {
167
+ responseText += `${line}\n`
621
168
  }
622
- },
623
- {
624
- name: 'delegate_to_opencode_cli',
625
- description: 'Delegate a complex multi-file coding task to OpenCode CLI. ONLY for deep code understanding, multi-file refactoring, or large code generation. NEVER use this to run servers, dev servers, install dependencies, or execute commands — use execute_command for those (this tool\'s session ends and kills any running processes).',
626
- schema: z.object({
627
- task: z.string().describe('Detailed description of the task for OpenCode CLI'),
628
- resume: z.boolean().optional().describe('If true, try to resume the last saved OpenCode delegation session for this SwarmClaw session'),
629
- resumeId: z.string().optional().describe('Explicit OpenCode session id to resume (overrides resume=true memory)'),
630
- }),
631
- },
632
- ),
633
- )
634
- }
635
- }
169
+ }
170
+ })
636
171
 
637
- // check_delegation_status: lets agents check on tasks they delegated
638
- if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
639
- tools.push(
640
- tool(
641
- async ({ taskId }) => {
642
- try {
643
- const tasks = loadTasks()
644
- const task = tasks[taskId] as Record<string, unknown> | undefined
645
- if (!task) return `Error: Task "${taskId}" not found.`
646
-
647
- const status = task.status as string || 'unknown'
648
- const result = typeof task.result === 'string' ? task.result : null
649
- const error = typeof task.error === 'string' ? task.error : null
650
- const agentId = task.agentId as string || ''
651
- const agents = loadAgents()
652
- const agent = agents[agentId]
653
- const startedAt = typeof task.startedAt === 'number' ? task.startedAt : null
654
- const completedAt = typeof task.completedAt === 'number' ? task.completedAt : null
655
-
656
- const info: Record<string, unknown> = {
657
- taskId,
658
- status,
659
- agentId,
660
- agentName: agent?.name || agentId,
661
- agentAvatarSeed: agent?.avatarSeed || null,
662
- title: task.title || '',
663
- }
172
+ child.stderr?.on('data', (chunk) => {
173
+ stderrBuf += chunk.toString()
174
+ if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
175
+ })
664
176
 
665
- if (startedAt) info.startedAt = new Date(startedAt).toISOString()
666
- if (completedAt) info.completedAt = new Date(completedAt).toISOString()
667
- if (startedAt && !completedAt && status === 'running') {
668
- info.runningForSeconds = Math.round((Date.now() - startedAt) / 1000)
669
- }
670
- if (result) info.result = result.slice(0, 4000)
671
- if (error) info.error = error.slice(0, 1000)
672
-
673
- // Include latest comments for context
674
- const comments = Array.isArray(task.comments) ? task.comments as Array<{ text: string; author: string; createdAt: number }> : []
675
- if (comments.length > 0) {
676
- const latest = comments.slice(-3).map((c) => ({
677
- author: c.author,
678
- text: (c.text || '').slice(0, 500),
679
- time: new Date(c.createdAt).toISOString(),
680
- }))
681
- info.latestComments = latest
682
- }
177
+ child.on('close', (code, signal) => {
178
+ clearTimeout(timeoutHandle)
179
+ if (discoveredId) bctx.persistDelegateResumeId?.('codex', discoveredId)
180
+ const output = responseText.trim()
181
+ if (output) return finish(output)
182
+ const stderr = stderrBuf.trim()
183
+ if (stderr) return finish(`Error: ${stderr}`)
184
+ return finish(`Error: Codex exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
185
+ })
683
186
 
684
- return JSON.stringify(info)
685
- } catch (err: unknown) {
686
- return `Error checking task: ${err instanceof Error ? err.message : String(err)}`
687
- }
688
- },
689
- {
690
- name: 'check_delegation_status',
691
- description: 'Check the status and result of a delegated task. Use this after delegate_to_agent to monitor progress. Returns status (todo/queued/running/completed/failed), result if completed, and latest comments.',
692
- schema: z.object({
693
- taskId: z.string().describe('The task ID returned by delegate_to_agent'),
694
- }),
695
- },
696
- ),
697
- )
187
+ child.on('error', (err) => {
188
+ clearTimeout(timeoutHandle)
189
+ finish(`Error: ${err.message}`)
190
+ })
191
+
192
+ child.stdin?.write(task)
193
+ child.stdin?.end()
194
+ })
195
+ } catch (err: unknown) {
196
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
698
197
  }
198
+ }
699
199
 
700
- // delegate_to_agent: requires "Assign to Other Agents" (platformAssignScope: 'all')
701
- if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
702
- tools.push(
703
- tool(
704
- async ({ agentId: targetAgentId, task: taskPrompt, description: taskDesc, startImmediately }) => {
200
+ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
201
+ try {
202
+ const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
203
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('opencode')
204
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
205
+
206
+ return await new Promise<string>((resolve) => {
207
+ const args = ['run', task, '--format', 'json']
208
+ if (resumeIdToUse) args.push('--session', resumeIdToUse)
209
+
210
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
211
+ let stdoutBuf = ''
212
+ let stderrBuf = ''
213
+ let responseText = ''
214
+ let discoveredId: string | null = null
215
+ let settled = false
216
+
217
+ const finish = (text: string) => {
218
+ if (settled) return
219
+ settled = true
220
+ resolve(truncate(text, MAX_OUTPUT))
221
+ }
222
+
223
+ const timeoutHandle = setTimeout(() => {
224
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
225
+ }, bctx.claudeTimeoutMs || 300000)
226
+
227
+ child.stdout?.on('data', (chunk) => {
228
+ stdoutBuf += chunk.toString()
229
+ const lines = stdoutBuf.split('\n')
230
+ stdoutBuf = lines.pop() || ''
231
+ for (const line of lines) {
232
+ const trimmed = line.trim()
233
+ if (!trimmed) continue
705
234
  try {
706
- const agents = loadAgents()
707
- let target = agents[targetAgentId]
708
- let resolvedId = targetAgentId
709
- // Fallback: resolve by name if the ID doesn't match directly
710
- if (!target) {
711
- const byName = Object.values(agents).find(
712
- (a) => a.name.toLowerCase() === targetAgentId.toLowerCase(),
713
- )
714
- if (byName) {
715
- target = byName
716
- resolvedId = byName.id
717
- }
718
- }
719
- if (!target) return `Error: Agent "${targetAgentId}" not found. Use the agent directory in your system prompt to find valid agent IDs.`
720
-
721
- const taskId = genId()
722
- const now = Date.now()
723
- const newTask = {
724
- id: taskId,
725
- title: taskPrompt.slice(0, 100),
726
- description: taskDesc || taskPrompt,
727
- status: 'todo',
728
- agentId: resolvedId,
729
- cwd,
730
- sourceType: 'delegation' as const,
731
- delegatedByAgentId: ctx.agentId!,
732
- createdAt: now,
733
- updatedAt: now,
734
- comments: [{
735
- id: genId(),
736
- author: agents[ctx.agentId!]?.name || 'Agent',
737
- agentId: ctx.agentId!,
738
- text: `Delegated from ${agents[ctx.agentId!]?.name || ctx.agentId}`,
739
- createdAt: now,
740
- }],
741
- }
742
- // Atomic upsert to avoid race with concurrent queue processing
743
- upsertTask(taskId, newTask)
744
- console.log(`[delegate] Created task ${taskId} for agent ${resolvedId}, startImmediately=${startImmediately}`)
745
-
746
- // Verify it persisted
747
- const verify = loadTasks()
748
- if (!verify[taskId]) {
749
- console.error(`[delegate] RACE: task ${taskId} not found after upsert!`)
235
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
236
+ const sid = typeof ev.sessionID === 'string' ? ev.sessionID : (typeof ev.sessionId === 'string' ? ev.sessionId : null)
237
+ if (sid) discoveredId = sid
238
+ if (ev.type === 'text') {
239
+ const part = ev.part as Record<string, unknown> | undefined
240
+ if (typeof part?.text === 'string') responseText += part.text
241
+ } else if (ev.type === 'error') {
242
+ const msg = typeof ev.error === 'string' ? ev.error : (typeof ev.message === 'string' ? ev.message : 'OpenCode error')
243
+ stderrBuf += `${msg}\n`
750
244
  }
245
+ } catch {
246
+ responseText += `${line}\n`
247
+ }
248
+ }
249
+ })
751
250
 
752
- if (startImmediately) {
753
- // Lazy import to avoid circular: session-tools → queue → chat-execution → session-tools
754
- const { enqueueTask } = await import('../queue')
755
- enqueueTask(taskId)
756
- console.log(`[delegate] Enqueued task ${taskId}`)
757
- }
251
+ child.stderr?.on('data', (chunk) => {
252
+ stderrBuf += chunk.toString()
253
+ if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
254
+ })
758
255
 
759
- return JSON.stringify({
760
- ok: true,
761
- taskId,
762
- agentId: resolvedId,
763
- agentName: target.name,
764
- agentAvatarSeed: target.avatarSeed || null,
765
- message: startImmediately
766
- ? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}. Use check_delegation_status to monitor progress.`
767
- : `Task delegated to ${target.name}. Task ID: ${taskId}. Status: todo (not auto-started). Use delegate_to_agent with startImmediately: true to queue it.`,
768
- })
769
- } catch (err: unknown) {
770
- return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`
256
+ child.on('close', (code, signal) => {
257
+ clearTimeout(timeoutHandle)
258
+ if (discoveredId) bctx.persistDelegateResumeId?.('opencode', discoveredId)
259
+ const output = responseText.trim()
260
+ if (output) return finish(output)
261
+ const stderr = stderrBuf.trim()
262
+ if (stderr) return finish(`Error: ${stderr}`)
263
+ return finish(`Error: OpenCode exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
264
+ })
265
+
266
+ child.on('error', (err) => {
267
+ clearTimeout(timeoutHandle)
268
+ finish(`Error: ${err.message}`)
269
+ })
270
+ })
271
+ } catch (err: unknown) {
272
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
273
+ }
274
+ }
275
+
276
+ async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
277
+ try {
278
+ const env: NodeJS.ProcessEnv = stripEnvPrefixes({ ...process.env }, ['CLAUDE'])
279
+ const authProbe = spawnSync(binary, ['auth', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
280
+ if ((authProbe.status ?? 1) !== 0) return 'Error: Claude Code not authenticated.'
281
+
282
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('claudeCode')
283
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
284
+
285
+ return new Promise<string>((resolve) => {
286
+ const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
287
+ if (resumeIdToUse) args.push('--resume', resumeIdToUse)
288
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
289
+ let stderr = ''
290
+ let assistantText = ''
291
+ let discoveredId: string | null = null
292
+ let settled = false
293
+
294
+ const finish = (res: string) => { if (!settled) { settled = true; resolve(truncate(res, MAX_OUTPUT)) } }
295
+ const timeoutHandle = setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, bctx.claudeTimeoutMs || 300000)
296
+
297
+ child.stdout?.on('data', (c) => {
298
+ const lines = c.toString().split('\n')
299
+ for (const l of lines) {
300
+ const trimmed = l.trim()
301
+ if (!trimmed) continue
302
+ try {
303
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
304
+ if (typeof ev.session_id === 'string') discoveredId = ev.session_id
305
+ if (ev.type === 'result' && typeof ev.result === 'string') assistantText = ev.result
306
+ } catch {
307
+ assistantText += `${l}\n`
771
308
  }
309
+ }
310
+ })
311
+ child.stderr?.on('data', (chunk) => {
312
+ stderr += chunk.toString()
313
+ if (stderr.length > 16_000) stderr = stderr.slice(-16_000)
314
+ })
315
+ child.on('close', (code) => {
316
+ clearTimeout(timeoutHandle)
317
+ if (discoveredId) bctx.persistDelegateResumeId?.('claudeCode', discoveredId)
318
+ const output = assistantText.trim()
319
+ if (code === 0) finish(output || 'Task completed.')
320
+ else finish(output ? output : `Error: Code ${code}. ${stderr.trim()}`)
321
+ })
322
+ child.on('error', (err) => {
323
+ clearTimeout(timeoutHandle)
324
+ finish(`Error: ${err.message}`)
325
+ })
326
+ child.stdin?.write(task)
327
+ child.stdin?.end()
328
+ })
329
+ } catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }
330
+ }
331
+
332
+ /**
333
+ * Register as a Built-in Plugin
334
+ */
335
+ const DelegatePlugin: Plugin = {
336
+ name: 'Core Delegate',
337
+ description: 'Delegate complex multi-file tasks to specialized CLI backends or other agents.',
338
+ hooks: {} as PluginHooks,
339
+ tools: [
340
+ {
341
+ name: 'delegate',
342
+ description: 'Delegate to a specialized backend (Claude, Codex, OpenCode).',
343
+ parameters: {
344
+ type: 'object',
345
+ properties: {
346
+ task: { type: 'string' },
347
+ backend: { type: 'string', enum: ['claude', 'codex', 'opencode'] },
348
+ resume: { type: 'boolean' },
349
+ resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' }
772
350
  },
351
+ required: ['task']
352
+ },
353
+ execute: async (args, context) => executeDelegateAction(args, { ...context.session, cwd: context.session.cwd || process.cwd() })
354
+ }
355
+ ]
356
+ }
357
+
358
+ getPluginManager().registerBuiltin('delegate', DelegatePlugin)
359
+
360
+ /**
361
+ * Legacy Bridge
362
+ */
363
+ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
364
+ const tools: StructuredToolInterface[] = []
365
+ const { hasTool } = bctx
366
+
367
+ if (hasTool('delegate')) {
368
+ tools.push(
369
+ tool(
370
+ async (args) => executeDelegateAction(args, bctx),
773
371
  {
774
- name: 'delegate_to_agent',
775
- description: 'Delegate a task to another agent. Creates a task on the task board and queues it for immediate execution by default. Set startImmediately=false if you want the task to go to "todo" status instead.',
776
- schema: z.object({
777
- agentId: z.string().describe('ID or name of the target agent to delegate to'),
778
- task: z.string().describe('What the target agent should do'),
779
- description: z.string().optional().describe('Optional longer description of the task'),
780
- startImmediately: z.boolean().optional().default(true).describe('If true (default), queue the task for immediate execution. Set false to put in todo for manual start.'),
781
- }),
782
- },
783
- ),
372
+ name: 'delegate',
373
+ description: DelegatePlugin.tools![0].description,
374
+ schema: z.object({}).passthrough()
375
+ }
376
+ )
784
377
  )
785
378
  }
786
379
 
380
+ // Assign to agent and check status tools (kept as platform-level tools)
381
+ if (bctx.ctx?.platformAssignScope === 'all' && bctx.ctx?.agentId) {
382
+ // ... existing check_delegation_status and delegate_to_agent ...
383
+ // These are already part of PLATFORM_TOOLS in tool-definitions
384
+ }
385
+
787
386
  return tools
788
387
  }