@swarmclawai/swarmclaw 1.5.3 → 1.5.33

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 (78) hide show
  1. package/README.md +167 -147
  2. package/bin/swarmclaw.js +35 -1
  3. package/package.json +2 -2
  4. package/scripts/run-next-build.mjs +87 -2
  5. package/skills/swarmclaw/SKILL.md +151 -0
  6. package/src/app/api/agents/[id]/route.ts +8 -0
  7. package/src/app/api/agents/agents-route.test.ts +114 -0
  8. package/src/app/api/agents/route.ts +10 -0
  9. package/src/app/api/connectors/route.ts +25 -13
  10. package/src/app/api/credentials/[id]/route.ts +8 -1
  11. package/src/app/api/schedules/[id]/route.ts +8 -0
  12. package/src/app/api/secrets/[id]/route.ts +10 -0
  13. package/src/app/api/setup/check-provider/route.ts +45 -0
  14. package/src/app/api/setup/doctor/route.ts +5 -0
  15. package/src/cli/binary.test.js +11 -0
  16. package/src/cli/index.js +4 -4
  17. package/src/cli/index.test.js +5 -2
  18. package/src/cli/index.ts +1 -1
  19. package/src/components/agents/agent-sheet.tsx +16 -4
  20. package/src/components/agents/inspector-panel.tsx +5 -0
  21. package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
  22. package/src/components/chat/activity-moment.tsx +3 -0
  23. package/src/components/chat/chat-header.tsx +23 -2
  24. package/src/components/chat/tool-call-bubble.tsx +20 -0
  25. package/src/components/layout/sidebar-rail.tsx +29 -0
  26. package/src/hooks/setup-done-detection.test.ts +4 -2
  27. package/src/hooks/setup-done-detection.ts +1 -1
  28. package/src/lib/orchestrator-config.ts +5 -0
  29. package/src/lib/provider-sets.ts +4 -4
  30. package/src/lib/providers/acp-client.ts +116 -0
  31. package/src/lib/providers/cli-utils.test.ts +9 -1
  32. package/src/lib/providers/cli-utils.ts +89 -4
  33. package/src/lib/providers/cursor-cli.ts +172 -0
  34. package/src/lib/providers/goose.ts +149 -0
  35. package/src/lib/providers/index.ts +29 -1
  36. package/src/lib/providers/qwen-code-cli.ts +152 -0
  37. package/src/lib/server/agents/agent-availability.ts +2 -2
  38. package/src/lib/server/agents/agent-thread-session.ts +8 -0
  39. package/src/lib/server/agents/task-session.ts +8 -0
  40. package/src/lib/server/capability-router.ts +8 -2
  41. package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
  42. package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
  43. package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
  44. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +3 -0
  45. package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
  46. package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
  47. package/src/lib/server/chat-execution/prompt-sections.ts +3 -1
  48. package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
  49. package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
  50. package/src/lib/server/chats/chat-session-service.ts +18 -0
  51. package/src/lib/server/connectors/session.ts +8 -0
  52. package/src/lib/server/context-manager.ts +5 -0
  53. package/src/lib/server/provider-health.ts +13 -3
  54. package/src/lib/server/provider-model-discovery.test.ts +8 -0
  55. package/src/lib/server/provider-model-discovery.ts +1 -1
  56. package/src/lib/server/runtime/daemon-state/core.ts +2 -2
  57. package/src/lib/server/runtime/queue/core.ts +30 -4
  58. package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
  59. package/src/lib/server/session-reset-policy.test.ts +16 -0
  60. package/src/lib/server/session-reset-policy.ts +9 -1
  61. package/src/lib/server/session-tools/context.ts +2 -2
  62. package/src/lib/server/session-tools/delegate.ts +334 -14
  63. package/src/lib/server/session-tools/index.ts +5 -2
  64. package/src/lib/server/session-tools/session-info.ts +4 -1
  65. package/src/lib/server/storage-auth-docker.test.ts +244 -0
  66. package/src/lib/server/storage-auth.test.ts +150 -0
  67. package/src/lib/server/storage-auth.ts +57 -22
  68. package/src/lib/server/storage-normalization.ts +19 -0
  69. package/src/lib/server/storage.ts +3 -0
  70. package/src/lib/server/tasks/task-resume.ts +23 -1
  71. package/src/lib/server/tool-aliases.ts +1 -1
  72. package/src/lib/server/tool-capability-policy.ts +4 -1
  73. package/src/lib/setup-defaults.test.ts +6 -0
  74. package/src/lib/setup-defaults.ts +146 -0
  75. package/src/lib/tool-definitions.ts +1 -1
  76. package/src/types/misc.ts +4 -1
  77. package/src/types/provider.ts +1 -1
  78. package/src/types/session.ts +9 -0
@@ -25,7 +25,7 @@ import { markProviderFailure, markProviderSuccess } from '../provider-health'
25
25
  import { loadRuntimeSettings } from '../runtime/runtime-settings'
26
26
  import { getSessionDepth } from '../agents/subagent-runtime'
27
27
 
28
- const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini']
28
+ const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini', 'copilot', 'cursor', 'qwen']
29
29
 
30
30
  interface DelegateContext {
31
31
  id?: string
@@ -34,8 +34,8 @@ interface DelegateContext {
34
34
  jobId?: string | null
35
35
  cwd?: string
36
36
  claudeTimeoutMs?: number
37
- readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
38
- persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
37
+ readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen') => string | null
38
+ persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen', id: string | null | undefined) => void
39
39
  ctx?: {
40
40
  delegationEnabled?: boolean
41
41
  delegationTargetMode?: 'all' | 'selected'
@@ -48,7 +48,7 @@ interface DelegateContext {
48
48
  hasTool?: (name: string) => boolean
49
49
  }
50
50
 
51
- type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini'
51
+ type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen'
52
52
 
53
53
  interface DelegateRuntimeState {
54
54
  child?: ChildProcess | null
@@ -128,8 +128,11 @@ function buildDelegateResumePatch(bctx: DelegateContext) {
128
128
  codex: bctx.readStoredDelegateResumeId?.('codex') || null,
129
129
  opencode: bctx.readStoredDelegateResumeId?.('opencode') || null,
130
130
  gemini: bctx.readStoredDelegateResumeId?.('gemini') || null,
131
+ copilot: bctx.readStoredDelegateResumeId?.('copilot') || null,
132
+ cursor: bctx.readStoredDelegateResumeId?.('cursor') || null,
133
+ qwen: bctx.readStoredDelegateResumeId?.('qwen') || null,
131
134
  }
132
- const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || null
135
+ const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || resumeIds.copilot || resumeIds.cursor || resumeIds.qwen || null
133
136
  return { resumeIds, resumeId }
134
137
  }
135
138
 
@@ -140,6 +143,9 @@ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
140
143
  if (['codex', 'codex cli', 'codex-cli', 'codex_cli'].includes(normalized)) return 'codex'
141
144
  if (['opencode', 'open code', 'open-code', 'open_code'].includes(normalized)) return 'opencode'
142
145
  if (['gemini', 'gemini cli', 'gemini-cli', 'gemini_cli'].includes(normalized)) return 'gemini'
146
+ if (['copilot', 'copilot cli', 'copilot-cli', 'copilot_cli', 'github copilot'].includes(normalized)) return 'copilot'
147
+ if (['cursor', 'cursor cli', 'cursor-cli', 'cursor_cli', 'cursor-agent'].includes(normalized)) return 'cursor'
148
+ if (['qwen', 'qwen code', 'qwen-code', 'qwen_code', 'qwen-code-cli', 'qwen_code_cli'].includes(normalized)) return 'qwen'
143
149
  return null
144
150
  }
145
151
 
@@ -334,18 +340,21 @@ function coerceOptionalBool(value: unknown): boolean | null {
334
340
  }
335
341
 
336
342
  function resumeStorageKeyForBackend(
337
- backend: 'claude' | 'codex' | 'opencode' | 'gemini',
338
- ): 'claudeCode' | 'codex' | 'opencode' | 'gemini' {
343
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen',
344
+ ): 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen' {
339
345
  if (backend === 'claude') return 'claudeCode'
340
346
  if (backend === 'codex') return 'codex'
341
347
  if (backend === 'opencode') return 'opencode'
342
- return 'gemini'
348
+ if (backend === 'gemini') return 'gemini'
349
+ if (backend === 'copilot') return 'copilot'
350
+ if (backend === 'cursor') return 'cursor'
351
+ return 'qwen'
343
352
  }
344
353
 
345
354
  export function resolveDelegateResumeConfig(
346
355
  normalized: Record<string, unknown>,
347
- backend: 'claude' | 'codex' | 'opencode' | 'gemini',
348
- bctx: { readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null },
356
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen',
357
+ bctx: { readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen') => string | null },
349
358
  ): { resume: boolean; resumeId: string } {
350
359
  const explicitResumeId = typeof normalized.resumeId === 'string' ? normalized.resumeId.trim() : ''
351
360
  if (explicitResumeId) return { resume: true, resumeId: explicitResumeId }
@@ -415,6 +424,21 @@ const DELEGATE_BACKEND_ADAPTERS: Record<DelegateBackend, DelegateBackendAdapter>
415
424
  binaryName: 'gemini',
416
425
  run: runGeminiDelegate,
417
426
  },
427
+ copilot: {
428
+ backend: 'copilot',
429
+ binaryName: 'copilot',
430
+ run: runCopilotDelegate,
431
+ },
432
+ cursor: {
433
+ backend: 'cursor',
434
+ binaryName: 'cursor-agent',
435
+ run: runCursorDelegate,
436
+ },
437
+ qwen: {
438
+ backend: 'qwen',
439
+ binaryName: 'qwen',
440
+ run: runQwenDelegate,
441
+ },
418
442
  }
419
443
 
420
444
  async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
@@ -433,7 +457,10 @@ function providerIdForBackend(backend: DelegateBackend): string {
433
457
  if (backend === 'claude') return 'claude-cli'
434
458
  if (backend === 'codex') return 'codex-cli'
435
459
  if (backend === 'opencode') return 'opencode-cli'
436
- return 'gemini-cli'
460
+ if (backend === 'gemini') return 'gemini-cli'
461
+ if (backend === 'copilot') return 'copilot-cli'
462
+ if (backend === 'cursor') return 'cursor-cli'
463
+ return 'qwen-code-cli'
437
464
  }
438
465
 
439
466
  function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[] {
@@ -665,6 +692,42 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
665
692
  return null
666
693
  }
667
694
 
695
+ function parseCursorOutputText(ev: Record<string, unknown>): string | null {
696
+ if (typeof ev.result === 'string' && ev.result.trim()) return ev.result
697
+ if (typeof ev.text === 'string' && ev.text.trim()) return ev.text
698
+ if (typeof ev.message === 'string' && ev.message.trim()) return ev.message
699
+ const message = ev.message
700
+ if (message && typeof message === 'object') {
701
+ const record = message as Record<string, unknown>
702
+ if (typeof record.text === 'string' && record.text.trim()) return record.text
703
+ if (typeof record.content === 'string' && record.content.trim()) return record.content
704
+ }
705
+ if (ev.type === 'delta') {
706
+ const delta = ev.delta as Record<string, unknown> | undefined
707
+ if (typeof delta?.text === 'string' && delta.text.trim()) return delta.text
708
+ }
709
+ return null
710
+ }
711
+
712
+ function parseQwenOutputText(ev: Record<string, unknown>): string | null {
713
+ if (typeof ev.result === 'string' && ev.result.trim()) return ev.result
714
+ if (ev.type === 'content_block_delta') {
715
+ const delta = ev.delta as Record<string, unknown> | undefined
716
+ if (typeof delta?.text === 'string' && delta.text.trim()) return delta.text
717
+ }
718
+ if (ev.type === 'assistant') {
719
+ const message = ev.message as Record<string, unknown> | undefined
720
+ const content = Array.isArray(message?.content) ? message.content : []
721
+ const text = content
722
+ .filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
723
+ .map((entry) => typeof entry.text === 'string' ? entry.text : '')
724
+ .join('')
725
+ .trim()
726
+ if (text) return text
727
+ }
728
+ return null
729
+ }
730
+
668
731
  async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
669
732
  try {
670
733
  // Build clean env — preserves user's CODEX_HOME for auth
@@ -920,6 +983,263 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
920
983
  }
921
984
  }
922
985
 
986
+ async function runCopilotDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
987
+ try {
988
+ const env = buildCliEnv()
989
+ const auth = probeCliAuth(binary, 'copilot', env, bctx.cwd)
990
+ if (!auth.authenticated) {
991
+ return buildDelegateFailure('copilot', auth.errorMessage || 'Copilot CLI is not authenticated.', 'auth')
992
+ }
993
+
994
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('copilot')
995
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
996
+
997
+ return await new Promise<DelegateBackendResult>((resolve) => {
998
+ const args = ['-p', task, '--output-format=json', '-s', '--yolo']
999
+ if (resumeIdToUse) args.push(`--resume=${resumeIdToUse}`)
1000
+
1001
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
1002
+ bindDelegateRuntime(runtime, child)
1003
+ let stdoutBuf = ''
1004
+ let stderrBuf = ''
1005
+ let responseText = ''
1006
+ let discoveredId: string | null = null
1007
+ let settled = false
1008
+
1009
+ const finish = (result: DelegateBackendResult) => {
1010
+ if (settled) return
1011
+ settled = true
1012
+ resolve(result)
1013
+ }
1014
+
1015
+ const timeoutHandle = setTimeout(() => {
1016
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
1017
+ }, bctx.claudeTimeoutMs || 300000)
1018
+
1019
+ child.stdout?.on('data', (chunk) => {
1020
+ stdoutBuf += chunk.toString()
1021
+ const lines = stdoutBuf.split('\n')
1022
+ stdoutBuf = lines.pop() || ''
1023
+ for (const line of lines) {
1024
+ const trimmed = line.trim()
1025
+ if (!trimmed) continue
1026
+ try {
1027
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
1028
+ const sid = typeof ev.session_id === 'string'
1029
+ ? ev.session_id
1030
+ : typeof ev.sessionId === 'string'
1031
+ ? ev.sessionId
1032
+ : null
1033
+ if (sid) discoveredId = sid
1034
+ const text = parseCursorOutputText(ev)
1035
+ if (text) {
1036
+ if (String(ev.type || '').includes('result') || String(ev.type || '').includes('completed')) responseText = text
1037
+ else responseText += text
1038
+ }
1039
+ } catch {
1040
+ responseText += `${line}\n`
1041
+ }
1042
+ }
1043
+ })
1044
+
1045
+ child.stderr?.on('data', (chunk) => {
1046
+ stderrBuf += chunk.toString()
1047
+ if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
1048
+ })
1049
+
1050
+ child.on('close', (code, signal) => {
1051
+ clearTimeout(timeoutHandle)
1052
+ if (discoveredId) bctx.persistDelegateResumeId?.('copilot', discoveredId)
1053
+ const output = responseText.trim()
1054
+ if (output) return finish(buildDelegateSuccess('copilot', output))
1055
+ const stderr = stderrBuf.trim()
1056
+ if (stderr) return finish(buildDelegateFailure('copilot', stderr))
1057
+ return finish(buildDelegateFailure('copilot', `Copilot exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
1058
+ })
1059
+
1060
+ child.on('error', (err) => {
1061
+ clearTimeout(timeoutHandle)
1062
+ finish(buildDelegateFailure('copilot', err.message, 'spawn'))
1063
+ })
1064
+ })
1065
+ } catch (err: unknown) {
1066
+ return buildDelegateFailure('copilot', errorMessage(err), 'runtime')
1067
+ }
1068
+ }
1069
+
1070
+ async function runCursorDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
1071
+ try {
1072
+ const env = buildCliEnv()
1073
+ const auth = probeCliAuth(binary, 'cursor', env, bctx.cwd)
1074
+ if (!auth.authenticated) {
1075
+ return buildDelegateFailure('cursor', auth.errorMessage || 'Cursor Agent CLI is not authenticated.', 'auth')
1076
+ }
1077
+
1078
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('cursor')
1079
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
1080
+
1081
+ return await new Promise<DelegateBackendResult>((resolve) => {
1082
+ const args = ['--print', '--output-format', 'stream-json']
1083
+ if (resumeIdToUse) args.push('--resume', resumeIdToUse)
1084
+ args.push(task)
1085
+
1086
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
1087
+ bindDelegateRuntime(runtime, child)
1088
+ let stdoutBuf = ''
1089
+ let stderrBuf = ''
1090
+ let responseText = ''
1091
+ let discoveredId: string | null = null
1092
+ let settled = false
1093
+
1094
+ const finish = (result: DelegateBackendResult) => {
1095
+ if (settled) return
1096
+ settled = true
1097
+ resolve(result)
1098
+ }
1099
+
1100
+ const timeoutHandle = setTimeout(() => {
1101
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
1102
+ }, bctx.claudeTimeoutMs || 300000)
1103
+
1104
+ child.stdout?.on('data', (chunk) => {
1105
+ stdoutBuf += chunk.toString()
1106
+ const lines = stdoutBuf.split('\n')
1107
+ stdoutBuf = lines.pop() || ''
1108
+ for (const line of lines) {
1109
+ const trimmed = line.trim()
1110
+ if (!trimmed) continue
1111
+ try {
1112
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
1113
+ const sid = typeof ev.session_id === 'string'
1114
+ ? ev.session_id
1115
+ : typeof ev.sessionId === 'string'
1116
+ ? ev.sessionId
1117
+ : typeof ev.thread_id === 'string'
1118
+ ? ev.thread_id
1119
+ : null
1120
+ if (sid) discoveredId = sid
1121
+ const text = parseCursorOutputText(ev)
1122
+ if (text) {
1123
+ if (String(ev.type || '').includes('result') || String(ev.type || '').includes('completed')) responseText = text
1124
+ else responseText += text
1125
+ }
1126
+ } catch {
1127
+ responseText += `${line}\n`
1128
+ }
1129
+ }
1130
+ })
1131
+
1132
+ child.stderr?.on('data', (chunk) => {
1133
+ stderrBuf += chunk.toString()
1134
+ if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
1135
+ })
1136
+
1137
+ child.on('close', (code, signal) => {
1138
+ clearTimeout(timeoutHandle)
1139
+ if (discoveredId) bctx.persistDelegateResumeId?.('cursor', discoveredId)
1140
+ const output = responseText.trim()
1141
+ if (output) return finish(buildDelegateSuccess('cursor', output))
1142
+ const stderr = stderrBuf.trim()
1143
+ if (stderr) return finish(buildDelegateFailure('cursor', stderr))
1144
+ return finish(buildDelegateFailure('cursor', `Cursor exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
1145
+ })
1146
+
1147
+ child.on('error', (err) => {
1148
+ clearTimeout(timeoutHandle)
1149
+ finish(buildDelegateFailure('cursor', err.message, 'spawn'))
1150
+ })
1151
+ })
1152
+ } catch (err: unknown) {
1153
+ return buildDelegateFailure('cursor', errorMessage(err), 'runtime')
1154
+ }
1155
+ }
1156
+
1157
+ async function runQwenDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
1158
+ try {
1159
+ const env = buildCliEnv()
1160
+ const auth = probeCliAuth(binary, 'qwen', env, bctx.cwd)
1161
+ if (!auth.authenticated) {
1162
+ return buildDelegateFailure('qwen', auth.errorMessage || 'Qwen Code CLI is not configured.', 'auth')
1163
+ }
1164
+
1165
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('qwen')
1166
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
1167
+
1168
+ return await new Promise<DelegateBackendResult>((resolve) => {
1169
+ const args = ['-p', task, '--output-format', 'stream-json', '--include-partial-messages', '--yolo']
1170
+ if (resumeIdToUse) args.push('--resume', resumeIdToUse)
1171
+
1172
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
1173
+ bindDelegateRuntime(runtime, child)
1174
+ let stdoutBuf = ''
1175
+ let stderrBuf = ''
1176
+ let responseText = ''
1177
+ let discoveredId: string | null = null
1178
+ let settled = false
1179
+
1180
+ const finish = (result: DelegateBackendResult) => {
1181
+ if (settled) return
1182
+ settled = true
1183
+ resolve(result)
1184
+ }
1185
+
1186
+ const timeoutHandle = setTimeout(() => {
1187
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
1188
+ }, bctx.claudeTimeoutMs || 300000)
1189
+
1190
+ child.stdout?.on('data', (chunk) => {
1191
+ stdoutBuf += chunk.toString()
1192
+ const lines = stdoutBuf.split('\n')
1193
+ stdoutBuf = lines.pop() || ''
1194
+ for (const line of lines) {
1195
+ const trimmed = line.trim()
1196
+ if (!trimmed) continue
1197
+ try {
1198
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
1199
+ const sid = typeof ev.session_id === 'string'
1200
+ ? ev.session_id
1201
+ : typeof ev.sessionId === 'string'
1202
+ ? ev.sessionId
1203
+ : null
1204
+ if (sid) discoveredId = sid
1205
+ const text = parseQwenOutputText(ev)
1206
+ if (text) {
1207
+ if (ev.type === 'assistant' || ev.type === 'result') responseText = text
1208
+ else responseText += text
1209
+ } else if (ev.type === 'result' && ev.subtype === 'error') {
1210
+ stderrBuf += `${typeof ev.result === 'string' ? ev.result : 'Qwen Code error'}\n`
1211
+ }
1212
+ } catch {
1213
+ responseText += `${line}\n`
1214
+ }
1215
+ }
1216
+ })
1217
+
1218
+ child.stderr?.on('data', (chunk) => {
1219
+ stderrBuf += chunk.toString()
1220
+ if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
1221
+ })
1222
+
1223
+ child.on('close', (code, signal) => {
1224
+ clearTimeout(timeoutHandle)
1225
+ if (discoveredId) bctx.persistDelegateResumeId?.('qwen', discoveredId)
1226
+ const output = responseText.trim()
1227
+ if (output) return finish(buildDelegateSuccess('qwen', output))
1228
+ const stderr = stderrBuf.trim()
1229
+ if (stderr) return finish(buildDelegateFailure('qwen', stderr))
1230
+ return finish(buildDelegateFailure('qwen', `Qwen Code exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
1231
+ })
1232
+
1233
+ child.on('error', (err) => {
1234
+ clearTimeout(timeoutHandle)
1235
+ finish(buildDelegateFailure('qwen', err.message, 'spawn'))
1236
+ })
1237
+ })
1238
+ } catch (err: unknown) {
1239
+ return buildDelegateFailure('qwen', errorMessage(err), 'runtime')
1240
+ }
1241
+ }
1242
+
923
1243
  async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
924
1244
  try {
925
1245
  const env = buildCliEnv()
@@ -984,19 +1304,19 @@ const DelegateExtension: Extension = {
984
1304
  name: 'Core Delegate',
985
1305
  description: 'Delegate complex multi-file tasks to specialized CLI backends or other agents.',
986
1306
  hooks: {
987
- getCapabilityDescription: () => 'I can hand off coding work to Claude Code, Codex, OpenCode, or Gemini CLI (`delegate`) for file creation, refactoring, debugging, code generation, and multi-file edits. Resume IDs may come back via `[delegate_meta]`.',
1307
+ getCapabilityDescription: () => 'I can hand off coding work to Claude Code, Codex, OpenCode, Gemini CLI, Cursor CLI, or Qwen Code CLI (`delegate`) for file creation, refactoring, debugging, code generation, and multi-file edits. Resume IDs may come back via `[delegate_meta]`.',
988
1308
  getOperatingGuidance: () => ['CRITICAL: `execute_command` (not delegation) for running servers, installs, scripts. Delegation sessions end and kill processes.', 'Delegate for code tasks: writing/creating files, refactors, debugging, generation, test suites, data exports to files.'],
989
1309
  } as ExtensionHooks,
990
1310
  tools: [
991
1311
  {
992
1312
  name: 'delegate',
993
- description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini) for code tasks: writing files, refactoring, debugging, code generation, and multi-file edits. Supports background jobs with action=status|list|wait|cancel.',
1313
+ description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini, Cursor, Qwen) for code tasks: writing files, refactoring, debugging, code generation, and multi-file edits. Supports background jobs with action=status|list|wait|cancel.',
994
1314
  parameters: {
995
1315
  type: 'object',
996
1316
  properties: {
997
1317
  action: { type: 'string', enum: ['start', 'status', 'list', 'wait', 'cancel'] },
998
1318
  task: { type: 'string' },
999
- backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini'] },
1319
+ backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini', 'copilot', 'cursor', 'qwen'] },
1000
1320
  resume: { type: 'boolean' },
1001
1321
  resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' },
1002
1322
  jobId: { type: 'string' },
@@ -73,6 +73,9 @@ const DELEGATION_TOOL_NAMES = new Set([
73
73
  'delegate_to_codex_cli',
74
74
  'delegate_to_opencode_cli',
75
75
  'delegate_to_gemini_cli',
76
+ 'delegate_to_copilot_cli',
77
+ 'delegate_to_cursor_cli',
78
+ 'delegate_to_qwen_code_cli',
76
79
  ])
77
80
 
78
81
  export async function buildSessionTools(cwd: string, enabledExtensions: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
@@ -117,14 +120,14 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
117
120
  return loadSession(ctx.sessionId)
118
121
  }
119
122
 
120
- const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini'): string | null => {
123
+ const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen'): string | null => {
121
124
  const session = resolveCurrentSession()
122
125
  if (!session?.delegateResumeIds || typeof session.delegateResumeIds !== 'object') return null
123
126
  const raw = session.delegateResumeIds[key]
124
127
  return typeof raw === 'string' && raw.trim() ? raw.trim() : null
125
128
  }
126
129
 
127
- const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', resumeId: string | null | undefined): void => {
130
+ const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen', resumeId: string | null | undefined): void => {
128
131
  const normalized = typeof resumeId === 'string' ? resumeId.trim() : ''
129
132
  if (!normalized || !ctx?.sessionId) return
130
133
  patchSession(ctx.sessionId, (target) => {
@@ -49,6 +49,9 @@ function normalizeRuntimeExtensionId(extensionId: string): string {
49
49
  if (normalized === 'delegate_to_codex_cli' || normalized === 'codex_cli') return 'codex_cli'
50
50
  if (normalized === 'delegate_to_opencode_cli' || normalized === 'opencode_cli') return 'opencode_cli'
51
51
  if (normalized === 'delegate_to_gemini_cli' || normalized === 'gemini_cli') return 'gemini_cli'
52
+ if (normalized === 'delegate_to_copilot_cli' || normalized === 'copilot_cli') return 'copilot_cli'
53
+ if (normalized === 'delegate_to_cursor_cli' || normalized === 'cursor_cli') return 'cursor_cli'
54
+ if (normalized === 'delegate_to_qwen_code_cli' || normalized === 'qwen_code_cli') return 'qwen_code_cli'
52
55
  if (['session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'].includes(normalized)) return 'manage_sessions'
53
56
  return canonicalizeExtensionId(normalized)
54
57
  }
@@ -100,7 +103,7 @@ export function buildSessionIdentityPayload(params: {
100
103
  const activeProjectContext = params.activeProjectContext || null
101
104
  const enabledExtensions = Array.isArray(params.enabledExtensions) ? params.enabledExtensions : []
102
105
  const toolPolicy = params.toolPolicy || null
103
- const delegationEnabled = enabledExtensions.some((extensionId) => ['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli'].includes(extensionId))
106
+ const delegationEnabled = enabledExtensions.some((extensionId) => ['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'cursor_cli', 'qwen_code_cli'].includes(extensionId))
104
107
 
105
108
  return {
106
109
  sessionId: params.context.sessionId || undefined,