@triflux/remote 10.0.1 → 10.25.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 (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +68 -7
  3. package/hub/server.mjs +2181 -701
  4. package/hub/store-adapter.mjs +101 -37
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +127 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +45 -17
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +21 -119
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1067 -264
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +69 -53
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +62 -34
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1526 -198
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +21 -37
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -45
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +936 -449
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +518 -64
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +663 -337
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. package/scripts/lib/toml.mjs +113 -0
@@ -1,106 +1,113 @@
1
1
  #!/usr/bin/env node
2
2
  // hub/workers/delegator-mcp.mjs — triflux 위임용 MCP stdio 서버
3
3
 
4
- import { spawn } from 'node:child_process';
5
- import { randomUUID } from 'node:crypto';
6
- import { existsSync, readFileSync } from 'node:fs';
7
- import { dirname, isAbsolute, resolve } from 'node:path';
8
- import process from 'node:process';
9
- import { fileURLToPath, pathToFileURL } from 'node:url';
10
-
11
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
- import * as z from 'zod';
14
-
15
- import { CodexMcpWorker } from './codex-mcp.mjs';
16
- import { GeminiWorker } from './gemini-worker.mjs';
4
+ import { spawn } from "node:child_process";
5
+ import { randomUUID } from "node:crypto";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, resolve } from "node:path";
8
+ import process from "node:process";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import * as z from "zod";
14
+ import { resolveBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
15
+ import { whichCommand } from "@triflux/core/hub/platform.mjs";
16
+ import { runHeadlessWithCleanup } from "../team/headless.mjs";
17
+ import { buildWorkerSandboxEnv } from "../team/worker-sandbox.mjs";
18
+ import { CodexMcpWorker } from "./codex-mcp.mjs";
19
+ import { GeminiWorker } from "./gemini-worker.mjs";
17
20
 
18
21
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
22
 
20
23
  // mcp-filter.mjs 동적 해석 — 프로젝트(hub/workers/)와 배포(scripts/hub/workers/) 양쪽 대응
21
24
  const MCP_FILTER_CANDIDATES = [
22
- resolve(SCRIPT_DIR, '../../scripts/lib/mcp-filter.mjs'), // 프로젝트 원본
23
- resolve(SCRIPT_DIR, '../../lib/mcp-filter.mjs'), // 배포 (~/.claude/scripts/)
25
+ resolve(SCRIPT_DIR, "../../scripts/lib/mcp-filter.mjs"), // 프로젝트 원본
26
+ resolve(SCRIPT_DIR, "../../lib/mcp-filter.mjs"), // 배포 (~/.claude/scripts/)
24
27
  ];
25
28
  const mcpFilterPath = MCP_FILTER_CANDIDATES.find((p) => existsSync(p));
26
29
  if (!mcpFilterPath) {
27
- throw new Error(`mcp-filter.mjs not found. candidates: ${MCP_FILTER_CANDIDATES.join(', ')}`);
30
+ throw new Error(
31
+ `mcp-filter.mjs not found. candidates: ${MCP_FILTER_CANDIDATES.join(", ")}`,
32
+ );
28
33
  }
29
34
  const {
30
35
  buildPromptHint,
31
36
  getCodexMcpConfig,
32
37
  getGeminiAllowedServers,
33
- resolveMcpProfile,
34
38
  SUPPORTED_MCP_PROFILES,
35
39
  } = await import(pathToFileURL(mcpFilterPath).href);
36
- const SERVER_INFO = { name: 'triflux-delegator', version: '1.0.0' };
40
+ const SERVER_INFO = { name: "triflux-delegator", version: "1.0.0" };
37
41
  const DEFAULT_CONTEXT_BYTES = 32 * 1024;
38
42
  const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
39
43
  const DIRECT_PROGRESS_START = 5;
40
44
  const DIRECT_PROGRESS_RUNNING = 60;
41
45
  const DIRECT_PROGRESS_DONE = 100;
42
- const SEARCH_ENGINE_CACHE_PATH = ['.omc', 'state', 'search-engines.json'];
46
+ const SEARCH_ENGINE_CACHE_PATH = [".omc", "state", "search-engines.json"];
43
47
 
44
48
  const AGENT_TIMEOUT_SEC = Object.freeze({
45
49
  executor: 1080,
46
- 'build-fixer': 540,
50
+ "build-fixer": 540,
47
51
  debugger: 900,
48
- 'deep-executor': 3600,
52
+ "deep-executor": 3600,
49
53
  architect: 3600,
50
54
  planner: 3600,
51
55
  critic: 3600,
52
56
  analyst: 3600,
53
- 'code-reviewer': 1800,
54
- 'security-reviewer': 1800,
55
- 'quality-reviewer': 1800,
57
+ "code-reviewer": 1800,
58
+ "security-reviewer": 1800,
59
+ "quality-reviewer": 1800,
56
60
  scientist: 1440,
57
- 'scientist-deep': 3600,
58
- 'document-specialist': 1440,
61
+ "scientist-deep": 3600,
62
+ "document-specialist": 1440,
59
63
  designer: 900,
60
64
  writer: 900,
61
65
  explore: 300,
62
66
  verifier: 1200,
63
- 'test-engineer': 300,
64
- 'qa-tester': 300,
67
+ "test-engineer": 300,
68
+ "qa-tester": 300,
65
69
  spark: 180,
66
70
  });
67
71
 
68
72
  const CODEX_PROFILE_BY_AGENT = Object.freeze({
69
- executor: 'codex53_high',
70
- 'build-fixer': 'codex53_low',
71
- debugger: 'codex53_high',
72
- 'deep-executor': 'gpt54_xhigh',
73
- architect: 'gpt54_xhigh',
74
- planner: 'gpt54_xhigh',
75
- critic: 'gpt54_xhigh',
76
- analyst: 'gpt54_xhigh',
77
- 'code-reviewer': 'codex53_high',
78
- 'security-reviewer': 'codex53_high',
79
- 'quality-reviewer': 'codex53_high',
80
- scientist: 'codex53_high',
81
- 'scientist-deep': 'gpt54_high',
82
- 'document-specialist': 'codex53_high',
83
- verifier: 'codex53_high',
84
- designer: 'gpt54_xhigh', // Gemini primary, codex fallback — UI/UX는 5.4 에이전틱
85
- writer: 'codex53_high', // Gemini primary, codex fallback용
86
- spark: 'spark53_low',
73
+ executor: "codex53_high",
74
+ "build-fixer": "codex53_low",
75
+ debugger: "codex53_high",
76
+ "deep-executor": "gpt54_xhigh",
77
+ architect: "gpt54_xhigh",
78
+ planner: "gpt54_xhigh",
79
+ critic: "gpt54_xhigh",
80
+ analyst: "gpt54_xhigh",
81
+ "code-reviewer": "codex53_high",
82
+ "security-reviewer": "codex53_high",
83
+ "quality-reviewer": "codex53_high",
84
+ scientist: "codex53_high",
85
+ "scientist-deep": "gpt54_high",
86
+ "document-specialist": "codex53_high",
87
+ verifier: "codex53_high",
88
+ designer: "gpt54_xhigh", // Gemini primary, codex fallback — UI/UX는 5.4 에이전틱
89
+ writer: "codex53_high", // Gemini primary, codex fallback용
90
+ spark: "spark53_low",
87
91
  });
88
92
 
89
93
  const GEMINI_MODEL_BY_AGENT = Object.freeze({
90
- 'build-fixer': 'gemini-3-flash-preview',
91
- writer: 'gemini-3-flash-preview',
92
- spark: 'gemini-3-flash-preview',
94
+ "build-fixer": "gemini-3-flash-preview",
95
+ writer: "gemini-3-flash-preview",
96
+ spark: "gemini-3-flash-preview",
93
97
  });
94
98
 
95
99
  const REVIEW_INSTRUCTION_BY_AGENT = Object.freeze({
96
- 'code-reviewer': '코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라.',
97
- 'security-reviewer': '보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라.',
98
- 'quality-reviewer': '품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라.',
100
+ "code-reviewer":
101
+ "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라.",
102
+ "security-reviewer":
103
+ "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라.",
104
+ "quality-reviewer":
105
+ "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라.",
99
106
  });
100
107
 
101
108
  function cloneEnv(env = process.env) {
102
109
  return Object.fromEntries(
103
- Object.entries(env).filter(([, value]) => typeof value === 'string'),
110
+ Object.entries(env).filter(([, value]) => typeof value === "string"),
104
111
  );
105
112
  }
106
113
 
@@ -109,7 +116,7 @@ function parseJsonArray(raw, fallback = []) {
109
116
  try {
110
117
  const parsed = JSON.parse(raw);
111
118
  return Array.isArray(parsed)
112
- ? parsed.map((item) => String(item ?? '')).filter(Boolean)
119
+ ? parsed.map((item) => String(item ?? "")).filter(Boolean)
113
120
  : [...fallback];
114
121
  } catch {
115
122
  return [...fallback];
@@ -118,7 +125,9 @@ function parseJsonArray(raw, fallback = []) {
118
125
 
119
126
  function resolveCandidatePath(candidate, cwd = process.cwd()) {
120
127
  if (!candidate) return null;
121
- const normalized = isAbsolute(candidate) ? candidate : resolve(cwd, candidate);
128
+ const normalized = isAbsolute(candidate)
129
+ ? candidate
130
+ : resolve(cwd, candidate);
122
131
  return existsSync(normalized) ? normalized : null;
123
132
  }
124
133
 
@@ -127,8 +136,8 @@ function resolveRouteScript(explicitPath, cwd = process.cwd()) {
127
136
  explicitPath,
128
137
  process.env.TFX_DELEGATOR_ROUTE_SCRIPT,
129
138
  process.env.TFX_ROUTE_SCRIPT,
130
- resolve(SCRIPT_DIR, '..', '..', 'scripts', 'tfx-route.sh'),
131
- resolve(cwd, 'scripts', 'tfx-route.sh'),
139
+ resolve(SCRIPT_DIR, "..", "..", "scripts", "tfx-route.sh"),
140
+ resolve(cwd, "scripts", "tfx-route.sh"),
132
141
  ];
133
142
 
134
143
  for (const candidate of candidates) {
@@ -140,11 +149,11 @@ function resolveRouteScript(explicitPath, cwd = process.cwd()) {
140
149
  }
141
150
 
142
151
  function resolveCodexProfile(agentType) {
143
- return CODEX_PROFILE_BY_AGENT[agentType] || 'high';
152
+ return CODEX_PROFILE_BY_AGENT[agentType] || "high";
144
153
  }
145
154
 
146
155
  function resolveGeminiModel(agentType) {
147
- return GEMINI_MODEL_BY_AGENT[agentType] || 'gemini-3.1-pro-preview';
156
+ return GEMINI_MODEL_BY_AGENT[agentType] || "gemini-3.1-pro-preview";
148
157
  }
149
158
 
150
159
  function resolveTimeoutMs(agentType, timeoutMs) {
@@ -162,13 +171,13 @@ function resolveTimeoutSec(agentType, timeoutMs) {
162
171
  }
163
172
 
164
173
  function loadContextFromFile(contextFile) {
165
- if (!contextFile) return '';
174
+ if (!contextFile) return "";
166
175
  const resolved = resolveCandidatePath(contextFile);
167
- if (!resolved) return '';
176
+ if (!resolved) return "";
168
177
  try {
169
- return readFileSync(resolved, 'utf8').slice(0, DEFAULT_CONTEXT_BYTES);
178
+ return readFileSync(resolved, "utf8").slice(0, DEFAULT_CONTEXT_BYTES);
170
179
  } catch {
171
- return '';
180
+ return "";
172
181
  }
173
182
  }
174
183
 
@@ -184,7 +193,9 @@ function withPromptHint(prompt, args) {
184
193
  agentType: args.agentType,
185
194
  requestedProfile: args.mcpProfile,
186
195
  searchTool: args.searchTool,
187
- workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
196
+ workerIndex: Number.isInteger(args.workerIndex)
197
+ ? args.workerIndex
198
+ : undefined,
188
199
  taskText: promptWithContext,
189
200
  });
190
201
  if (!hint) return promptWithContext;
@@ -193,46 +204,50 @@ function withPromptHint(prompt, args) {
193
204
 
194
205
  function joinInstructions(...values) {
195
206
  return values
196
- .filter((value) => typeof value === 'string' && value.trim())
197
- .join('\n')
207
+ .filter((value) => typeof value === "string" && value.trim())
208
+ .join("\n")
198
209
  .trim();
199
210
  }
200
211
 
201
- function loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
212
+ function _loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
202
213
  const cacheFile = resolve(cwd, ...SEARCH_ENGINE_CACHE_PATH);
203
214
  if (!existsSync(cacheFile)) return null;
204
215
 
205
216
  try {
206
- const parsed = JSON.parse(readFileSync(cacheFile, 'utf8'));
217
+ const parsed = JSON.parse(readFileSync(cacheFile, "utf8"));
207
218
  if (!Array.isArray(parsed?.engines)) return null;
208
219
  return parsed.engines
209
- .filter((engine) => engine?.status === 'available')
210
- .map((engine) => (typeof engine?.name === 'string' ? engine.name.trim() : ''))
220
+ .filter((engine) => engine?.status === "available")
221
+ .map((engine) =>
222
+ typeof engine?.name === "string" ? engine.name.trim() : "",
223
+ )
211
224
  .filter(Boolean);
212
225
  } catch {
213
226
  return null;
214
227
  }
215
228
  }
216
229
 
217
- function parseRouteType(stderr = '') {
230
+ function parseRouteType(stderr = "") {
218
231
  const match = stderr.match(/type=([a-z-]+)/);
219
232
  if (!match) return null;
220
- if (match[1] === 'codex') return 'codex';
221
- if (match[1] === 'gemini') return 'gemini';
233
+ if (match[1] === "codex") return "codex";
234
+ if (match[1] === "gemini") return "gemini";
222
235
  return match[1];
223
236
  }
224
237
 
225
238
  function summarizePayload(payload) {
226
- if (typeof payload.output === 'string' && payload.output.trim()) return payload.output.trim();
227
- if (payload.mode === 'async' && payload.job_id) return `비동기 위임이 시작되었습니다. jobId=${payload.job_id}`;
239
+ if (typeof payload.output === "string" && payload.output.trim())
240
+ return payload.output.trim();
241
+ if (payload.mode === "async" && payload.job_id)
242
+ return `비동기 위임이 시작되었습니다. jobId=${payload.job_id}`;
228
243
  if (payload.job_id) return `jobId=${payload.job_id} 상태=${payload.status}`;
229
244
  if (payload.status) return `상태=${payload.status}`;
230
- return payload.ok ? '완료되었습니다.' : '실패했습니다.';
245
+ return payload.ok ? "완료되었습니다." : "실패했습니다.";
231
246
  }
232
247
 
233
248
  function createToolResponse(payload, { isError = false } = {}) {
234
249
  return {
235
- content: [{ type: 'text', text: summarizePayload(payload) }],
250
+ content: [{ type: "text", text: summarizePayload(payload) }],
236
251
  structuredContent: payload,
237
252
  isError,
238
253
  };
@@ -241,55 +256,107 @@ function createToolResponse(payload, { isError = false } = {}) {
241
256
  function createErrorPayload(message, extras = {}) {
242
257
  return {
243
258
  ok: false,
244
- status: 'failed',
259
+ status: "failed",
245
260
  error: message,
246
261
  ...extras,
247
262
  };
248
263
  }
249
264
 
250
265
  const DelegateInputSchema = z.object({
251
- prompt: z.string().min(1).describe('위임할 프롬프트'),
252
- provider: z.enum(['auto', 'codex', 'gemini']).default('auto').describe('사용할 provider'),
253
- mode: z.enum(['sync', 'async']).default('sync').describe('동기 또는 비동기 실행'),
254
- agentType: z.string().default('executor').describe('tfx-route 역할명 또는 direct 실행 역할'),
255
- cwd: z.string().optional().describe('작업 디렉터리'),
256
- timeoutMs: z.number().int().positive().optional().describe('요청 타임아웃(ms)'),
257
- sessionKey: z.string().optional().describe('Codex warm session 재사용 키'),
258
- resetSession: z.boolean().optional().describe('기존 Codex 세션 초기화 여부'),
259
- mcpProfile: z.enum(SUPPORTED_MCP_PROFILES).default('auto'),
260
- contextFile: z.string().optional().describe('tfx-route prior_context 파일 경로'),
261
- availableServers: z.array(z.string()).optional().describe('Codex에 등록된 MCP 서버 이름 목록 (config override 대상)'),
262
- searchTool: z.enum(['brave-search', 'tavily', 'exa']).optional().describe('검색 우선 도구'),
263
- workerIndex: z.number().int().positive().optional().describe('병렬 워커 인덱스'),
264
- model: z.string().optional().describe('직접 실행 시 모델 오버라이드'),
265
- developerInstructions: z.string().optional().describe('직접 실행 시 추가 개발자 지침'),
266
- compactPrompt: z.string().optional().describe('Codex compact prompt'),
267
- threadId: z.string().optional().describe('Codex 직접 실행 시 기존 threadId'),
268
- codexTransport: z.enum(['auto', 'mcp', 'exec']).optional().describe('route 경로용 Codex transport'),
269
- noClaudeNative: z.boolean().optional().describe('route 경로용 TFX_NO_CLAUDE_NATIVE'),
270
- teamName: z.string().optional().describe('TFX_TEAM_NAME'),
271
- teamTaskId: z.string().optional().describe('TFX_TEAM_TASK_ID'),
272
- teamAgentName: z.string().optional().describe('TFX_TEAM_AGENT_NAME'),
273
- teamLeadName: z.string().optional().describe('TFX_TEAM_LEAD_NAME'),
274
- hubUrl: z.string().optional().describe('TFX_HUB_URL'),
266
+ prompt: z.string().min(1).describe("위임할 프롬프트"),
267
+ provider: z
268
+ .enum(["auto", "codex", "gemini"])
269
+ .default("auto")
270
+ .describe("사용할 provider"),
271
+ mode: z
272
+ .enum(["sync", "async"])
273
+ .default("sync")
274
+ .describe("동기 또는 비동기 실행"),
275
+ agentType: z
276
+ .string()
277
+ .default("executor")
278
+ .describe("tfx-route 역할명 또는 direct 실행 역할"),
279
+ cwd: z.string().optional().describe("작업 디렉터리"),
280
+ timeoutMs: z
281
+ .number()
282
+ .int()
283
+ .positive()
284
+ .optional()
285
+ .describe("요청 타임아웃(ms)"),
286
+ sessionKey: z.string().optional().describe("Codex warm session 재사용 키"),
287
+ resetSession: z.boolean().optional().describe("기존 Codex 세션 초기화 여부"),
288
+ mcpProfile: z.enum(SUPPORTED_MCP_PROFILES).default("auto"),
289
+ contextFile: z
290
+ .string()
291
+ .optional()
292
+ .describe("tfx-route prior_context 파일 경로"),
293
+ availableServers: z
294
+ .array(z.string())
295
+ .optional()
296
+ .describe("Codex에 등록된 MCP 서버 이름 목록 (config override 대상)"),
297
+ searchTool: z
298
+ .enum(["brave-search", "tavily", "exa"])
299
+ .optional()
300
+ .describe("검색 우선 도구"),
301
+ workerIndex: z
302
+ .number()
303
+ .int()
304
+ .positive()
305
+ .optional()
306
+ .describe("병렬 워커 인덱스"),
307
+ model: z.string().optional().describe("직접 실행 시 모델 오버라이드"),
308
+ developerInstructions: z
309
+ .string()
310
+ .optional()
311
+ .describe("직접 실행 시 추가 개발자 지침"),
312
+ compactPrompt: z.string().optional().describe("Codex compact prompt"),
313
+ threadId: z.string().optional().describe("Codex 직접 실행 시 기존 threadId"),
314
+ codexTransport: z
315
+ .enum(["auto", "mcp", "exec"])
316
+ .optional()
317
+ .describe("route 경로용 Codex transport"),
318
+ noClaudeNative: z
319
+ .boolean()
320
+ .optional()
321
+ .describe("route 경로용 TFX_NO_CLAUDE_NATIVE"),
322
+ teamName: z.string().optional().describe("TFX_TEAM_NAME"),
323
+ teamTaskId: z.string().optional().describe("TFX_TEAM_TASK_ID"),
324
+ teamAgentName: z.string().optional().describe("TFX_TEAM_AGENT_NAME"),
325
+ teamLeadName: z.string().optional().describe("TFX_TEAM_LEAD_NAME"),
326
+ hubUrl: z.string().optional().describe("TFX_HUB_URL"),
327
+ workers: z
328
+ .array(
329
+ z.object({
330
+ provider: z.enum(["codex", "gemini"]).describe("워커 provider"),
331
+ agentType: z.string().default("executor").describe("역할명"),
332
+ prompt: z.string().describe("워커별 프롬프트"),
333
+ mcpProfile: z.string().default("auto").describe("MCP 프로필"),
334
+ model: z.string().optional().describe("모델 오버라이드"),
335
+ }),
336
+ )
337
+ .optional()
338
+ .describe("병렬 멀티워커 목록. 지정 시 psmux 기반 병렬 실행"),
275
339
  });
276
340
 
277
341
  const DelegateStatusInputSchema = z.object({
278
- jobId: z.string().min(1).describe('조회할 비동기 job ID'),
342
+ jobId: z.string().min(1).describe("조회할 비동기 job ID"),
279
343
  });
280
344
 
281
345
  const DelegateReplyInputSchema = z.object({
282
- job_id: z.string().min(1).describe('후속 응답을 보낼 기존 delegate job ID'),
283
- reply: z.string().min(1).describe('후속 사용자 응답'),
284
- done: z.boolean().default(false).describe('true이면 응답 처리 후 대화를 종료'),
346
+ job_id: z.string().min(1).describe("후속 응답을 보낼 기존 delegate job ID"),
347
+ reply: z.string().min(1).describe("후속 사용자 응답"),
348
+ done: z
349
+ .boolean()
350
+ .default(false)
351
+ .describe("true이면 응답 처리 후 대화를 종료"),
285
352
  });
286
353
 
287
354
  const DelegateOutputSchema = z.object({
288
355
  ok: z.boolean(),
289
356
  jobId: z.string().optional(),
290
357
  job_id: z.string().optional(),
291
- mode: z.enum(['sync', 'async']).optional(),
292
- status: z.enum(['running', 'completed', 'failed']).optional(),
358
+ mode: z.enum(["sync", "async"]).optional(),
359
+ status: z.enum(["running", "completed", "failed", "partial"]).optional(),
293
360
  error: z.string().optional(),
294
361
  providerRequested: z.string().optional(),
295
362
  providerResolved: z.string().nullable().optional(),
@@ -305,33 +372,51 @@ const DelegateOutputSchema = z.object({
305
372
  threadId: z.string().nullable().optional(),
306
373
  sessionKey: z.string().nullable().optional(),
307
374
  conversationOpen: z.boolean().optional(),
375
+ workerResults: z
376
+ .array(
377
+ z.object({
378
+ cli: z.string(),
379
+ role: z.string().optional(),
380
+ paneName: z.string(),
381
+ matched: z.boolean(),
382
+ exitCode: z.number().nullable(),
383
+ output: z.string(),
384
+ }),
385
+ )
386
+ .optional()
387
+ .describe("멀티워커 개별 결과"),
388
+ sessionName: z.string().optional().describe("psmux 세션명"),
308
389
  });
309
390
 
310
391
  function isTeamRouteRequested(args) {
311
392
  return Boolean(
312
- args.teamName
313
- || args.teamTaskId
314
- || args.teamAgentName
315
- || args.teamLeadName
316
- || args.hubUrl
393
+ args.teamName ||
394
+ args.teamTaskId ||
395
+ args.teamAgentName ||
396
+ args.teamLeadName ||
397
+ args.hubUrl,
317
398
  );
318
399
  }
319
400
 
320
401
  function pickRouteMode(provider) {
321
- return provider === 'auto' ? 'auto' : provider;
402
+ return provider === "auto" ? "auto" : provider;
322
403
  }
323
404
 
324
405
  function sanitizeDelegateArgs(args = {}) {
325
406
  return {
326
- provider: args.provider || 'auto',
327
- agentType: args.agentType || 'executor',
407
+ provider: args.provider || "auto",
408
+ agentType: args.agentType || "executor",
328
409
  cwd: args.cwd || null,
329
- timeoutMs: Number.isFinite(Number(args.timeoutMs)) ? Math.trunc(Number(args.timeoutMs)) : null,
410
+ timeoutMs: Number.isFinite(Number(args.timeoutMs))
411
+ ? Math.trunc(Number(args.timeoutMs))
412
+ : null,
330
413
  sessionKey: args.sessionKey || null,
331
414
  resetSession: Boolean(args.resetSession),
332
- mcpProfile: args.mcpProfile || 'auto',
415
+ mcpProfile: args.mcpProfile || "auto",
333
416
  contextFile: args.contextFile || null,
334
- availableServers: Array.isArray(args.availableServers) ? args.availableServers : null,
417
+ availableServers: Array.isArray(args.availableServers)
418
+ ? args.availableServers
419
+ : null,
335
420
  searchTool: args.searchTool || null,
336
421
  workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : null,
337
422
  model: args.model || null,
@@ -349,21 +434,21 @@ function sanitizeDelegateArgs(args = {}) {
349
434
  }
350
435
 
351
436
  function formatConversationTranscript(turns = []) {
352
- return turns.map((turn, index) => {
353
- const parts = [
354
- `Turn ${index + 1} user:\n${turn.user}`,
355
- ];
356
- if (typeof turn.assistant === 'string' && turn.assistant.trim()) {
357
- parts.push(`Turn ${index + 1} assistant:\n${turn.assistant}`);
358
- }
359
- return parts.join('\n\n');
360
- }).join('\n\n');
437
+ return turns
438
+ .map((turn, index) => {
439
+ const parts = [`Turn ${index + 1} user:\n${turn.user}`];
440
+ if (typeof turn.assistant === "string" && turn.assistant.trim()) {
441
+ parts.push(`Turn ${index + 1} assistant:\n${turn.assistant}`);
442
+ }
443
+ return parts.join("\n\n");
444
+ })
445
+ .join("\n\n");
361
446
  }
362
447
 
363
448
  async function emitProgress(extra, progress, total, message) {
364
449
  if (extra?._meta?.progressToken === undefined) return;
365
450
  await extra.sendNotification({
366
- method: 'notifications/progress',
451
+ method: "notifications/progress",
367
452
  params: {
368
453
  progressToken: extra._meta.progressToken,
369
454
  progress,
@@ -374,34 +459,39 @@ async function emitProgress(extra, progress, total, message) {
374
459
  }
375
460
 
376
461
  export class DelegatorMcpWorker {
377
- type = 'delegator';
462
+ type = "delegator";
378
463
 
379
464
  constructor(options = {}) {
380
465
  this.cwd = options.cwd || process.cwd();
381
466
  this.env = cloneEnv({ ...cloneEnv(process.env), ...cloneEnv(options.env) });
382
467
  this.routeScript = resolveRouteScript(options.routeScript, this.cwd);
383
- this.bashCommand = options.bashCommand
384
- || this.env.TFX_DELEGATOR_BASH_COMMAND
385
- || this.env.BASH_BIN
386
- || 'bash';
468
+ this.bashCommand =
469
+ options.bashCommand ||
470
+ this.env.TFX_DELEGATOR_BASH_COMMAND ||
471
+ this.env.BASH_BIN ||
472
+ resolveBashExecutable();
387
473
 
388
474
  this.codexWorker = new CodexMcpWorker({
389
- command: options.codexCommand
390
- || this.env.TFX_DELEGATOR_CODEX_COMMAND
391
- || this.env.CODEX_BIN
392
- || 'codex',
393
- args: Array.isArray(options.codexArgs) && options.codexArgs.length
394
- ? options.codexArgs
395
- : parseJsonArray(this.env.TFX_DELEGATOR_CODEX_ARGS_JSON, []),
475
+ command:
476
+ options.codexCommand ||
477
+ this.env.TFX_DELEGATOR_CODEX_COMMAND ||
478
+ this.env.CODEX_BIN ||
479
+ "codex",
480
+ args:
481
+ Array.isArray(options.codexArgs) && options.codexArgs.length
482
+ ? options.codexArgs
483
+ : parseJsonArray(this.env.TFX_DELEGATOR_CODEX_ARGS_JSON, []),
396
484
  cwd: this.cwd,
397
485
  env: this.env,
398
486
  clientInfo: { name: SERVER_INFO.name, version: SERVER_INFO.version },
399
487
  });
400
488
 
401
- this.geminiCommand = options.geminiCommand || this.env.GEMINI_BIN || 'gemini';
402
- this.geminiCommandArgs = Array.isArray(options.geminiArgs) && options.geminiArgs.length
403
- ? [...options.geminiArgs]
404
- : parseJsonArray(this.env.GEMINI_BIN_ARGS_JSON, []);
489
+ this.geminiCommand =
490
+ options.geminiCommand || this.env.GEMINI_BIN || "gemini";
491
+ this.geminiCommandArgs =
492
+ Array.isArray(options.geminiArgs) && options.geminiArgs.length
493
+ ? [...options.geminiArgs]
494
+ : parseJsonArray(this.env.GEMINI_BIN_ARGS_JSON, []);
405
495
 
406
496
  this.server = null;
407
497
  this.transport = null;
@@ -425,32 +515,48 @@ export class DelegatorMcpWorker {
425
515
  capabilities: { logging: {} },
426
516
  });
427
517
 
428
- server.registerTool('triflux-delegate', {
429
- description: '새 위임을 실행합니다. codex/gemini direct 경로와 tfx-route 기반 auto 라우팅을 모두 지원합니다.',
430
- inputSchema: DelegateInputSchema,
431
- outputSchema: DelegateOutputSchema,
432
- }, async (args, extra) => {
433
- const payload = await this.delegate(args, extra);
434
- return createToolResponse(payload, { isError: payload.ok === false && payload.mode !== 'async' });
435
- });
436
-
437
- server.registerTool('triflux-delegate-status', {
438
- description: '비동기 위임 job 상태를 조회합니다.',
439
- inputSchema: DelegateStatusInputSchema,
440
- outputSchema: DelegateOutputSchema,
441
- }, async ({ jobId }, extra) => {
442
- const payload = await this.getJobStatus(jobId, extra);
443
- return createToolResponse(payload, { isError: payload.ok === false });
444
- });
445
-
446
- server.registerTool('triflux-delegate-reply', {
447
- description: '기존 delegate job에 후속 응답을 보내고, Gemini direct job이면 multi-turn 대화를 이어갑니다.',
448
- inputSchema: DelegateReplyInputSchema,
449
- outputSchema: DelegateOutputSchema,
450
- }, async (args, extra) => {
451
- const payload = await this.reply(args, extra);
452
- return createToolResponse(payload, { isError: payload.ok === false });
453
- });
518
+ server.registerTool(
519
+ "triflux-delegate",
520
+ {
521
+ description:
522
+ "새 위임을 실행합니다. codex/gemini direct 경로와 tfx-route 기반 auto 라우팅을 모두 지원합니다.",
523
+ inputSchema: DelegateInputSchema,
524
+ outputSchema: DelegateOutputSchema,
525
+ },
526
+ async (args, extra) => {
527
+ const payload = await this.delegate(args, extra);
528
+ return createToolResponse(payload, {
529
+ isError: payload.ok === false && payload.mode !== "async",
530
+ });
531
+ },
532
+ );
533
+
534
+ server.registerTool(
535
+ "triflux-delegate-status",
536
+ {
537
+ description: "비동기 위임 job 상태를 조회합니다.",
538
+ inputSchema: DelegateStatusInputSchema,
539
+ outputSchema: DelegateOutputSchema,
540
+ },
541
+ async ({ jobId }, extra) => {
542
+ const payload = await this.getJobStatus(jobId, extra);
543
+ return createToolResponse(payload, { isError: payload.ok === false });
544
+ },
545
+ );
546
+
547
+ server.registerTool(
548
+ "triflux-delegate-reply",
549
+ {
550
+ description:
551
+ "기존 delegate job에 후속 응답을 보내고, Gemini direct job이면 multi-turn 대화를 이어갑니다.",
552
+ inputSchema: DelegateReplyInputSchema,
553
+ outputSchema: DelegateOutputSchema,
554
+ },
555
+ async (args, extra) => {
556
+ const payload = await this.reply(args, extra);
557
+ return createToolResponse(payload, { isError: payload.ok === false });
558
+ },
559
+ );
454
560
 
455
561
  this.server = server;
456
562
  this.ready = true;
@@ -468,7 +574,9 @@ export class DelegatorMcpWorker {
468
574
  this.ready = false;
469
575
 
470
576
  for (const child of this.routeChildren) {
471
- try { child.kill(); } catch {}
577
+ try {
578
+ child.kill();
579
+ } catch {}
472
580
  }
473
581
  this.routeChildren.clear();
474
582
 
@@ -497,7 +605,7 @@ export class DelegatorMcpWorker {
497
605
  async execute(prompt, options = {}) {
498
606
  const result = await this._executeDirect({ prompt, ...options });
499
607
  return {
500
- output: result.output || result.error || '',
608
+ output: result.output || result.error || "",
501
609
  exitCode: result.exitCode ?? (result.ok ? 0 : 1),
502
610
  threadId: result.threadId || null,
503
611
  sessionKey: result.sessionKey || null,
@@ -506,7 +614,7 @@ export class DelegatorMcpWorker {
506
614
  }
507
615
 
508
616
  async delegate(args, extra) {
509
- if (args.mode === 'async') {
617
+ if (args.mode === "async") {
510
618
  return this._startAsyncJob(args, extra);
511
619
  }
512
620
  return this._runSyncJob(args, extra);
@@ -519,11 +627,11 @@ export class DelegatorMcpWorker {
519
627
  }
520
628
 
521
629
  const payload = this._serializeJob(job);
522
- if (job.status === 'running') {
630
+ if (job.status === "running") {
523
631
  await emitProgress(extra, 25, 100, `job ${jobId} 실행 중`);
524
- } else if (job.status === 'completed') {
632
+ } else if (job.status === "completed") {
525
633
  await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${jobId} 완료`);
526
- } else if (job.status === 'failed') {
634
+ } else if (job.status === "failed") {
527
635
  await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${jobId} 실패`);
528
636
  }
529
637
  return payload;
@@ -532,28 +640,51 @@ export class DelegatorMcpWorker {
532
640
  async reply({ job_id, reply, done = false }, extra) {
533
641
  const job = this.jobs.get(job_id);
534
642
  if (!job) {
535
- return createErrorPayload(`알 수 없는 jobId: ${job_id}`, { jobId: job_id, job_id });
536
- }
537
- if (job.status === 'running') {
538
- return createErrorPayload(`job ${job_id}가 아직 실행 중입니다.`, { jobId: job_id, job_id });
643
+ return createErrorPayload(`알 수 없는 jobId: ${job_id}`, {
644
+ jobId: job_id,
645
+ job_id,
646
+ });
539
647
  }
540
- if (job.providerRequested !== 'gemini' || job.transport !== 'gemini-worker') {
541
- return createErrorPayload('delegate-reply는 현재 direct Gemini job에만 지원됩니다.', {
648
+ if (job.status === "running") {
649
+ return createErrorPayload(`job ${job_id}가 아직 실행 중입니다.`, {
542
650
  jobId: job_id,
543
651
  job_id,
544
652
  });
545
653
  }
654
+ if (
655
+ job.providerRequested !== "gemini" ||
656
+ job.transport !== "gemini-worker"
657
+ ) {
658
+ return createErrorPayload(
659
+ "delegate-reply는 현재 direct Gemini job에만 지원됩니다.",
660
+ {
661
+ jobId: job_id,
662
+ job_id,
663
+ },
664
+ );
665
+ }
546
666
 
547
667
  const conversation = this.geminiConversations.get(job_id);
548
668
  if (!conversation) {
549
- return createErrorPayload(`Gemini 대화 컨텍스트가 없습니다: ${job_id}`, { jobId: job_id, job_id });
669
+ return createErrorPayload(`Gemini 대화 컨텍스트가 없습니다: ${job_id}`, {
670
+ jobId: job_id,
671
+ job_id,
672
+ });
550
673
  }
551
674
  if (conversation.closed) {
552
- return createErrorPayload(`이미 종료된 대화입니다: ${job_id}`, { jobId: job_id, job_id });
675
+ return createErrorPayload(`이미 종료된 대화입니다: ${job_id}`, {
676
+ jobId: job_id,
677
+ job_id,
678
+ });
553
679
  }
554
680
 
555
- await emitProgress(extra, DIRECT_PROGRESS_START, 100, `job ${job_id} 후속 응답을 시작합니다.`);
556
- job.status = 'running';
681
+ await emitProgress(
682
+ extra,
683
+ DIRECT_PROGRESS_START,
684
+ 100,
685
+ `job ${job_id} 후속 응답을 시작합니다.`,
686
+ );
687
+ job.status = "running";
557
688
  job.updatedAt = new Date().toISOString();
558
689
 
559
690
  const worker = this._createGeminiWorker();
@@ -565,8 +696,10 @@ export class DelegatorMcpWorker {
565
696
  cwd: job.requestArgs.cwd || this.cwd,
566
697
  timeoutMs: resolveTimeoutMs(job.agentType, job.requestArgs.timeoutMs),
567
698
  model: job.requestArgs.model || resolveGeminiModel(job.agentType),
568
- approvalMode: 'yolo',
569
- allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(job.requestArgs)),
699
+ approvalMode: "yolo",
700
+ allowedMcpServerNames: getGeminiAllowedServers(
701
+ this._getMcpPolicyOptions(job.requestArgs),
702
+ ),
570
703
  });
571
704
 
572
705
  conversation.turns.push({
@@ -581,26 +714,34 @@ export class DelegatorMcpWorker {
581
714
 
582
715
  this._applyJobResult(job, {
583
716
  ok: result.exitCode === 0,
584
- status: result.exitCode === 0 ? 'completed' : 'failed',
585
- providerRequested: 'gemini',
586
- providerResolved: 'gemini',
717
+ status: result.exitCode === 0 ? "completed" : "failed",
718
+ providerRequested: "gemini",
719
+ providerResolved: "gemini",
587
720
  agentType: job.agentType,
588
- transport: 'gemini-worker',
721
+ transport: "gemini-worker",
589
722
  exitCode: result.exitCode,
590
723
  output: result.output,
591
724
  sessionKey: result.sessionKey || job.sessionKey || null,
592
725
  });
593
- await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${job_id} 후속 응답이 완료되었습니다.`);
726
+ await emitProgress(
727
+ extra,
728
+ DIRECT_PROGRESS_DONE,
729
+ 100,
730
+ `job ${job_id} 후속 응답이 완료되었습니다.`,
731
+ );
594
732
  return this._serializeJob(job);
595
733
  } catch (error) {
596
734
  const message = error instanceof Error ? error.message : String(error);
597
- this._applyJobResult(job, createErrorPayload(message, {
598
- mode: job.mode,
599
- providerRequested: 'gemini',
600
- providerResolved: 'gemini',
601
- agentType: job.agentType,
602
- transport: 'gemini-worker',
603
- }));
735
+ this._applyJobResult(
736
+ job,
737
+ createErrorPayload(message, {
738
+ mode: job.mode,
739
+ providerRequested: "gemini",
740
+ providerResolved: "gemini",
741
+ agentType: job.agentType,
742
+ transport: "gemini-worker",
743
+ }),
744
+ );
604
745
  return this._serializeJob(job);
605
746
  } finally {
606
747
  await worker.stop().catch(() => {});
@@ -609,8 +750,9 @@ export class DelegatorMcpWorker {
609
750
  }
610
751
 
611
752
  _createGeminiWorker() {
753
+ const resolved = whichCommand(this.geminiCommand) || this.geminiCommand;
612
754
  return new GeminiWorker({
613
- command: this.geminiCommand,
755
+ command: resolved,
614
756
  commandArgs: this.geminiCommandArgs,
615
757
  cwd: this.cwd,
616
758
  env: this.env,
@@ -618,15 +760,17 @@ export class DelegatorMcpWorker {
618
760
  }
619
761
 
620
762
  _buildDirectPrompt(args) {
621
- return withContext(String(args.prompt ?? ''), args.contextFile);
763
+ return withContext(String(args.prompt ?? ""), args.contextFile);
622
764
  }
623
765
 
624
766
  _buildDirectPromptWithHint(args) {
625
- return withPromptHint(String(args.prompt ?? ''), {
626
- agentType: args.agentType || 'executor',
627
- mcpProfile: args.mcpProfile || 'auto',
767
+ return withPromptHint(String(args.prompt ?? ""), {
768
+ agentType: args.agentType || "executor",
769
+ mcpProfile: args.mcpProfile || "auto",
628
770
  searchTool: args.searchTool,
629
- workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
771
+ workerIndex: Number.isInteger(args.workerIndex)
772
+ ? args.workerIndex
773
+ : undefined,
630
774
  contextFile: args.contextFile,
631
775
  });
632
776
  }
@@ -634,26 +778,30 @@ export class DelegatorMcpWorker {
634
778
  _buildGeminiReplyPrompt(conversation, reply) {
635
779
  const transcript = formatConversationTranscript(conversation.turns);
636
780
  return [
637
- 'Continue the conversation using the prior transcript below.',
638
- '',
639
- '<conversation_history>',
781
+ "Continue the conversation using the prior transcript below.",
782
+ "",
783
+ "<conversation_history>",
640
784
  transcript,
641
- '</conversation_history>',
642
- '',
643
- '<latest_user_reply>',
785
+ "</conversation_history>",
786
+ "",
787
+ "<latest_user_reply>",
644
788
  reply,
645
- '</latest_user_reply>',
646
- ].join('\n');
789
+ "</latest_user_reply>",
790
+ ].join("\n");
647
791
  }
648
792
 
649
793
  _getMcpPolicyOptions(args) {
650
794
  return {
651
- agentType: args.agentType || 'executor',
652
- requestedProfile: args.mcpProfile || 'auto',
795
+ agentType: args.agentType || "executor",
796
+ requestedProfile: args.mcpProfile || "auto",
653
797
  searchTool: args.searchTool,
654
- workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : undefined,
655
- taskText: withContext(String(args.prompt ?? ''), args.contextFile),
656
- availableServers: Array.isArray(args.availableServers) ? args.availableServers : undefined,
798
+ workerIndex: Number.isInteger(args.workerIndex)
799
+ ? args.workerIndex
800
+ : undefined,
801
+ taskText: withContext(String(args.prompt ?? ""), args.contextFile),
802
+ availableServers: Array.isArray(args.availableServers)
803
+ ? args.availableServers
804
+ : undefined,
657
805
  };
658
806
  }
659
807
 
@@ -662,11 +810,38 @@ export class DelegatorMcpWorker {
662
810
  }
663
811
 
664
812
  _shouldUseRoute(args) {
665
- return args.provider === 'auto' || isTeamRouteRequested(args);
813
+ return args.provider === "auto" || isTeamRouteRequested(args);
666
814
  }
667
815
 
668
816
  async _executeDirect(args, extra = null) {
669
- await emitProgress(extra, DIRECT_PROGRESS_START, 100, '위임 실행을 시작합니다.');
817
+ await emitProgress(
818
+ extra,
819
+ DIRECT_PROGRESS_START,
820
+ 100,
821
+ "위임 실행을 시작합니다.",
822
+ );
823
+
824
+ // 멀티워커 분기: workers 배열이 있으면 headless psmux 병렬 실행
825
+ if (Array.isArray(args.workers) && args.workers.length > 0) {
826
+ try {
827
+ const result = await this._executeMultiWorker(args, extra);
828
+ await emitProgress(
829
+ extra,
830
+ DIRECT_PROGRESS_DONE,
831
+ 100,
832
+ "위임이 완료되었습니다.",
833
+ );
834
+ return result;
835
+ } catch (error) {
836
+ const message = error instanceof Error ? error.message : String(error);
837
+ return createErrorPayload(message, {
838
+ mode: "sync",
839
+ providerRequested: "multi",
840
+ agentType: args.agentType,
841
+ transport: "headless-psmux",
842
+ });
843
+ }
844
+ }
670
845
 
671
846
  const runViaRoute = this._shouldUseRoute(args);
672
847
 
@@ -675,50 +850,152 @@ export class DelegatorMcpWorker {
675
850
  ? await this._executeRoute(args, extra)
676
851
  : await this._executeWorker(args, extra);
677
852
 
678
- await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, '위임이 완료되었습니다.');
853
+ await emitProgress(
854
+ extra,
855
+ DIRECT_PROGRESS_DONE,
856
+ 100,
857
+ "위임이 완료되었습니다.",
858
+ );
679
859
  return result;
680
860
  } catch (error) {
681
861
  const message = error instanceof Error ? error.message : String(error);
682
862
  return createErrorPayload(message, {
683
- mode: 'sync',
863
+ mode: "sync",
684
864
  providerRequested: args.provider,
685
865
  agentType: args.agentType,
686
- transport: runViaRoute ? 'route-script' : `${args.provider}-worker`,
866
+ transport: runViaRoute ? "route-script" : `${args.provider}-worker`,
687
867
  });
688
868
  }
689
869
  }
690
870
 
691
- async _executeWorker(args, extra) {
692
- await emitProgress(extra, DIRECT_PROGRESS_RUNNING, 100, '직접 워커 경로로 실행 중입니다.');
871
+ async _executeMultiWorker(args, extra) {
872
+ await emitProgress(
873
+ extra,
874
+ DIRECT_PROGRESS_START,
875
+ 100,
876
+ "멀티워커 psmux 병렬 실행을 시작합니다.",
877
+ );
878
+
879
+ // workers 배열을 headless assignments 형식으로 변환
880
+ const assignments = args.workers.map((w) => {
881
+ // provider를 CLI 이름으로 매핑 (codex/gemini 그대로)
882
+ const cli = w.provider;
883
+ const role = w.agentType || "executor";
884
+ // buildDirectPrompt와 동일한 context 처리
885
+ const prompt = withContext(String(w.prompt || ""), args.contextFile);
886
+ return { cli, prompt, role };
887
+ });
693
888
 
694
- if (args.provider === 'codex') {
695
- const result = await this.codexWorker.execute(this._buildDirectPrompt(args), {
696
- cwd: args.cwd || this.cwd,
697
- timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
698
- sessionKey: args.sessionKey,
699
- threadId: args.threadId,
700
- resetSession: args.resetSession,
701
- profile: resolveCodexProfile(args.agentType),
702
- sandbox: 'danger-full-access',
703
- approvalPolicy: 'never',
704
- developerInstructions: joinInstructions(
705
- REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
706
- this._buildPromptHintInstruction(args),
707
- args.developerInstructions,
889
+ // 타임아웃: 워커 중 가장 긴 타임아웃 사용
890
+ const maxTimeoutSec = Math.max(
891
+ ...args.workers.map((w) =>
892
+ Math.ceil(
893
+ resolveTimeoutMs(w.agentType || "executor", args.timeoutMs) / 1000,
708
894
  ),
709
- config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
710
- compactPrompt: args.compactPrompt,
711
- model: args.model,
895
+ ),
896
+ 300,
897
+ );
898
+
899
+ try {
900
+ const { results, sessionName } = await runHeadlessWithCleanup(
901
+ assignments,
902
+ {
903
+ sessionPrefix: "dlg",
904
+ timeoutSec: maxTimeoutSec,
905
+ layout: assignments.length <= 2 ? "even-horizontal" : "2x2",
906
+ progressive: true,
907
+ dashboard: true,
908
+ dashboardLayout: "single",
909
+ },
910
+ );
911
+
912
+ await emitProgress(
913
+ extra,
914
+ DIRECT_PROGRESS_DONE,
915
+ 100,
916
+ `멀티워커 실행 완료: ${results.length}개 워커`,
917
+ );
918
+
919
+ // 전체 성공 판단: 모든 워커가 matched && exitCode === 0
920
+ const allOk = results.every((r) => r.matched && r.exitCode === 0);
921
+ // 개별 출력을 합산
922
+ const combinedOutput = results
923
+ .map(
924
+ (r, i) =>
925
+ `=== Worker ${i + 1} (${r.cli}/${assignments[i].role}) ===\n${r.output || "(no output)"}`,
926
+ )
927
+ .join("\n\n");
928
+
929
+ return {
930
+ ok: allOk,
931
+ mode: "sync",
932
+ status: allOk ? "completed" : "partial",
933
+ providerRequested: "multi",
934
+ providerResolved: "multi",
935
+ agentType: args.agentType || "executor",
936
+ transport: "headless-psmux",
937
+ exitCode: allOk ? 0 : 1,
938
+ output: combinedOutput,
939
+ workerResults: results.map((r) => ({
940
+ cli: r.cli,
941
+ role: r.role || "",
942
+ paneName: r.paneName,
943
+ matched: r.matched,
944
+ exitCode: r.exitCode,
945
+ output: r.output || "",
946
+ })),
947
+ sessionName,
948
+ };
949
+ } catch (error) {
950
+ const message = error instanceof Error ? error.message : String(error);
951
+ return createErrorPayload(message, {
952
+ mode: "sync",
953
+ providerRequested: "multi",
954
+ agentType: args.agentType || "executor",
955
+ transport: "headless-psmux",
712
956
  });
957
+ }
958
+ }
959
+
960
+ async _executeWorker(args, extra) {
961
+ await emitProgress(
962
+ extra,
963
+ DIRECT_PROGRESS_RUNNING,
964
+ 100,
965
+ "직접 워커 경로로 실행 중입니다.",
966
+ );
967
+
968
+ if (args.provider === "codex") {
969
+ const result = await this.codexWorker.execute(
970
+ this._buildDirectPrompt(args),
971
+ {
972
+ cwd: args.cwd || this.cwd,
973
+ timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
974
+ sessionKey: args.sessionKey,
975
+ threadId: args.threadId,
976
+ resetSession: args.resetSession,
977
+ profile: resolveCodexProfile(args.agentType),
978
+ sandbox: "danger-full-access",
979
+ approvalPolicy: "never",
980
+ developerInstructions: joinInstructions(
981
+ REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
982
+ this._buildPromptHintInstruction(args),
983
+ args.developerInstructions,
984
+ ),
985
+ config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
986
+ compactPrompt: args.compactPrompt,
987
+ model: args.model,
988
+ },
989
+ );
713
990
 
714
991
  return {
715
992
  ok: result.exitCode === 0,
716
- mode: 'sync',
717
- status: result.exitCode === 0 ? 'completed' : 'failed',
718
- providerRequested: 'codex',
719
- providerResolved: 'codex',
993
+ mode: "sync",
994
+ status: result.exitCode === 0 ? "completed" : "failed",
995
+ providerRequested: "codex",
996
+ providerResolved: "codex",
720
997
  agentType: args.agentType,
721
- transport: 'codex-mcp',
998
+ transport: "codex-mcp",
722
999
  exitCode: result.exitCode,
723
1000
  output: result.output,
724
1001
  sessionKey: result.sessionKey,
@@ -726,7 +1003,7 @@ export class DelegatorMcpWorker {
726
1003
  };
727
1004
  }
728
1005
 
729
- if (args.provider === 'gemini') {
1006
+ if (args.provider === "gemini") {
730
1007
  const worker = this._createGeminiWorker();
731
1008
  const prompt = this._buildDirectPromptWithHint(args);
732
1009
  try {
@@ -734,18 +1011,20 @@ export class DelegatorMcpWorker {
734
1011
  cwd: args.cwd || this.cwd,
735
1012
  timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
736
1013
  model: args.model || resolveGeminiModel(args.agentType),
737
- approvalMode: 'yolo',
738
- allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(args)),
1014
+ approvalMode: "yolo",
1015
+ allowedMcpServerNames: getGeminiAllowedServers(
1016
+ this._getMcpPolicyOptions(args),
1017
+ ),
739
1018
  });
740
1019
 
741
1020
  return {
742
1021
  ok: result.exitCode === 0,
743
- mode: 'sync',
744
- status: result.exitCode === 0 ? 'completed' : 'failed',
745
- providerRequested: 'gemini',
746
- providerResolved: 'gemini',
1022
+ mode: "sync",
1023
+ status: result.exitCode === 0 ? "completed" : "failed",
1024
+ providerRequested: "gemini",
1025
+ providerResolved: "gemini",
747
1026
  agentType: args.agentType,
748
- transport: 'gemini-worker',
1027
+ transport: "gemini-worker",
749
1028
  exitCode: result.exitCode,
750
1029
  output: result.output,
751
1030
  sessionKey: result.sessionKey,
@@ -756,34 +1035,42 @@ export class DelegatorMcpWorker {
756
1035
  }
757
1036
  }
758
1037
 
759
- return createErrorPayload(`지원하지 않는 direct provider: ${args.provider}`, {
760
- mode: 'sync',
761
- providerRequested: args.provider,
762
- agentType: args.agentType,
763
- transport: 'direct-worker',
764
- });
1038
+ return createErrorPayload(
1039
+ `지원하지 않는 direct provider: ${args.provider}`,
1040
+ {
1041
+ mode: "sync",
1042
+ providerRequested: args.provider,
1043
+ agentType: args.agentType,
1044
+ transport: "direct-worker",
1045
+ },
1046
+ );
765
1047
  }
766
1048
 
767
1049
  async _executeRoute(args, extra) {
768
1050
  if (!this.routeScript) {
769
- return createErrorPayload('tfx-route.sh 경로를 찾지 못했습니다.', {
770
- mode: 'sync',
1051
+ return createErrorPayload("tfx-route.sh 경로를 찾지 못했습니다.", {
1052
+ mode: "sync",
771
1053
  providerRequested: args.provider,
772
1054
  agentType: args.agentType,
773
- transport: 'route-script',
1055
+ transport: "route-script",
774
1056
  });
775
1057
  }
776
1058
 
777
- await emitProgress(extra, DIRECT_PROGRESS_RUNNING, 100, 'tfx-route.sh 경로로 실행 중입니다.');
1059
+ await emitProgress(
1060
+ extra,
1061
+ DIRECT_PROGRESS_RUNNING,
1062
+ 100,
1063
+ "tfx-route.sh 경로로 실행 중입니다.",
1064
+ );
778
1065
  const result = await this._spawnRoute(args);
779
1066
  return {
780
1067
  ok: result.exitCode === 0,
781
- mode: 'sync',
782
- status: result.exitCode === 0 ? 'completed' : 'failed',
1068
+ mode: "sync",
1069
+ status: result.exitCode === 0 ? "completed" : "failed",
783
1070
  providerRequested: args.provider,
784
1071
  providerResolved: parseRouteType(result.stderr) || args.provider,
785
1072
  agentType: args.agentType,
786
- transport: 'route-script',
1073
+ transport: "route-script",
787
1074
  exitCode: result.exitCode,
788
1075
  output: result.stdout.trim() || result.stderr.trim(),
789
1076
  stderr: result.stderr.trim(),
@@ -791,10 +1078,15 @@ export class DelegatorMcpWorker {
791
1078
  }
792
1079
 
793
1080
  async _startAsyncJob(args, extra) {
794
- const job = this._createJob(args, 'async');
1081
+ const job = this._createJob(args, "async");
795
1082
  this.jobs.set(job.jobId, job);
796
1083
 
797
- await emitProgress(extra, DIRECT_PROGRESS_START, 100, `비동기 job ${job.jobId}를 시작합니다.`);
1084
+ await emitProgress(
1085
+ extra,
1086
+ DIRECT_PROGRESS_START,
1087
+ 100,
1088
+ `비동기 job ${job.jobId}를 시작합니다.`,
1089
+ );
798
1090
 
799
1091
  void (async () => {
800
1092
  try {
@@ -803,15 +1095,20 @@ export class DelegatorMcpWorker {
803
1095
  : await this._runAsyncWorker(args, job);
804
1096
  this._applyJobResult(job, result);
805
1097
  } catch (error) {
806
- this._applyJobResult(job, createErrorPayload(
807
- error instanceof Error ? error.message : String(error),
808
- {
809
- mode: 'async',
810
- providerRequested: args.provider,
811
- agentType: args.agentType,
812
- transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
813
- },
814
- ));
1098
+ this._applyJobResult(
1099
+ job,
1100
+ createErrorPayload(
1101
+ error instanceof Error ? error.message : String(error),
1102
+ {
1103
+ mode: "async",
1104
+ providerRequested: args.provider,
1105
+ agentType: args.agentType,
1106
+ transport: this._shouldUseRoute(args)
1107
+ ? "route-script"
1108
+ : `${args.provider}-worker`,
1109
+ },
1110
+ ),
1111
+ );
815
1112
  } finally {
816
1113
  if (job.worker) {
817
1114
  await job.worker.stop().catch(() => {});
@@ -825,29 +1122,32 @@ export class DelegatorMcpWorker {
825
1122
  }
826
1123
 
827
1124
  async _runAsyncWorker(args, job) {
828
- if (args.provider === 'codex') {
829
- const result = await this.codexWorker.execute(this._buildDirectPrompt(args), {
830
- cwd: args.cwd || this.cwd,
831
- timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
832
- sessionKey: args.sessionKey,
833
- threadId: args.threadId,
834
- resetSession: args.resetSession,
835
- profile: resolveCodexProfile(args.agentType),
836
- sandbox: 'danger-full-access',
837
- approvalPolicy: 'never',
838
- developerInstructions: joinInstructions(
839
- REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
840
- this._buildPromptHintInstruction(args),
841
- args.developerInstructions,
842
- ),
843
- config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
844
- compactPrompt: args.compactPrompt,
845
- model: args.model,
846
- });
1125
+ if (args.provider === "codex") {
1126
+ const result = await this.codexWorker.execute(
1127
+ this._buildDirectPrompt(args),
1128
+ {
1129
+ cwd: args.cwd || this.cwd,
1130
+ timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
1131
+ sessionKey: args.sessionKey,
1132
+ threadId: args.threadId,
1133
+ resetSession: args.resetSession,
1134
+ profile: resolveCodexProfile(args.agentType),
1135
+ sandbox: "danger-full-access",
1136
+ approvalPolicy: "never",
1137
+ developerInstructions: joinInstructions(
1138
+ REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
1139
+ this._buildPromptHintInstruction(args),
1140
+ args.developerInstructions,
1141
+ ),
1142
+ config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
1143
+ compactPrompt: args.compactPrompt,
1144
+ model: args.model,
1145
+ },
1146
+ );
847
1147
 
848
1148
  return {
849
1149
  ok: result.exitCode === 0,
850
- providerResolved: 'codex',
1150
+ providerResolved: "codex",
851
1151
  output: result.output,
852
1152
  exitCode: result.exitCode,
853
1153
  threadId: result.threadId,
@@ -855,7 +1155,7 @@ export class DelegatorMcpWorker {
855
1155
  };
856
1156
  }
857
1157
 
858
- if (args.provider === 'gemini') {
1158
+ if (args.provider === "gemini") {
859
1159
  const worker = this._createGeminiWorker();
860
1160
  job.worker = worker;
861
1161
  const prompt = this._buildDirectPromptWithHint(args);
@@ -863,13 +1163,15 @@ export class DelegatorMcpWorker {
863
1163
  cwd: args.cwd || this.cwd,
864
1164
  timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
865
1165
  model: args.model || resolveGeminiModel(args.agentType),
866
- approvalMode: 'yolo',
867
- allowedMcpServerNames: getGeminiAllowedServers(this._getMcpPolicyOptions(args)),
1166
+ approvalMode: "yolo",
1167
+ allowedMcpServerNames: getGeminiAllowedServers(
1168
+ this._getMcpPolicyOptions(args),
1169
+ ),
868
1170
  });
869
1171
 
870
1172
  return {
871
1173
  ok: result.exitCode === 0,
872
- providerResolved: 'gemini',
1174
+ providerResolved: "gemini",
873
1175
  output: result.output,
874
1176
  exitCode: result.exitCode,
875
1177
  sessionKey: result.sessionKey,
@@ -881,14 +1183,26 @@ export class DelegatorMcpWorker {
881
1183
  }
882
1184
 
883
1185
  _buildRouteEnv(args) {
884
- const env = cloneEnv(this.env);
1186
+ const baseEnv = cloneEnv(this.env);
1187
+ const sandbox = buildWorkerSandboxEnv({
1188
+ cwd: args.cwd || this.cwd,
1189
+ sessionId:
1190
+ args.teamTaskId ||
1191
+ args.teamAgentName ||
1192
+ args.workerIndex ||
1193
+ args.agentType ||
1194
+ "route",
1195
+ env: baseEnv,
1196
+ });
1197
+ const env = { ...baseEnv, ...sandbox.env };
885
1198
  env.TFX_CLI_MODE = pickRouteMode(args.provider);
1199
+ if (sandbox.root) env.TFX_WORKER_SANDBOX_SCOPE = "delegator-route";
886
1200
 
887
1201
  if (args.codexTransport) {
888
1202
  env.TFX_CODEX_TRANSPORT = args.codexTransport;
889
1203
  }
890
1204
  if (args.noClaudeNative === true) {
891
- env.TFX_NO_CLAUDE_NATIVE = '1';
1205
+ env.TFX_NO_CLAUDE_NATIVE = "1";
892
1206
  }
893
1207
  if (args.searchTool) {
894
1208
  env.TFX_SEARCH_TOOL = args.searchTool;
@@ -906,19 +1220,19 @@ export class DelegatorMcpWorker {
906
1220
  }
907
1221
 
908
1222
  async _spawnRoute(args, job = null) {
909
- const prompt = withContext(String(args.prompt ?? ''), args.contextFile);
1223
+ const prompt = withContext(String(args.prompt ?? ""), args.contextFile);
910
1224
  const childArgs = [
911
1225
  this.routeScript,
912
- args.agentType || 'executor',
1226
+ args.agentType || "executor",
913
1227
  prompt,
914
- args.mcpProfile || 'auto',
1228
+ args.mcpProfile || "auto",
915
1229
  String(resolveTimeoutSec(args.agentType, args.timeoutMs)),
916
1230
  ];
917
1231
 
918
1232
  const child = spawn(this.bashCommand, childArgs, {
919
1233
  cwd: args.cwd || this.cwd,
920
1234
  env: this._buildRouteEnv(args),
921
- stdio: ['ignore', 'pipe', 'pipe'],
1235
+ stdio: ["ignore", "pipe", "pipe"],
922
1236
  windowsHide: true,
923
1237
  });
924
1238
 
@@ -932,16 +1246,16 @@ export class DelegatorMcpWorker {
932
1246
  const stdoutChunks = [];
933
1247
  const stderrChunks = [];
934
1248
 
935
- child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
936
- child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
937
- child.once('error', (error) => {
1249
+ child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
1250
+ child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
1251
+ child.once("error", (error) => {
938
1252
  this.routeChildren.delete(child);
939
1253
  rejectPromise(error);
940
1254
  });
941
- child.once('close', (code) => {
1255
+ child.once("close", (code) => {
942
1256
  this.routeChildren.delete(child);
943
- const stdout = Buffer.concat(stdoutChunks).toString('utf8');
944
- const stderr = Buffer.concat(stderrChunks).toString('utf8');
1257
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
1258
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
945
1259
  resolvePromise({
946
1260
  ok: code === 0,
947
1261
  providerResolved: parseRouteType(stderr) || args.provider,
@@ -958,7 +1272,7 @@ export class DelegatorMcpWorker {
958
1272
  return {
959
1273
  ok: job.ok,
960
1274
  job_id: job.jobId,
961
- mode: job.mode || 'async',
1275
+ mode: job.mode || "async",
962
1276
  status: job.status,
963
1277
  provider_requested: job.providerRequested,
964
1278
  provider_resolved: job.providerResolved,
@@ -970,7 +1284,7 @@ export class DelegatorMcpWorker {
970
1284
  completed_at: job.completedAt,
971
1285
  output: job.output,
972
1286
  stderr: job.stderr,
973
- error: '',
1287
+ error: "",
974
1288
  thread_id: job.threadId,
975
1289
  session_key: job.sessionKey,
976
1290
  conversation_open: this.geminiConversations.has(job.jobId),
@@ -984,17 +1298,19 @@ export class DelegatorMcpWorker {
984
1298
  ok: true,
985
1299
  jobId,
986
1300
  mode,
987
- status: 'running',
1301
+ status: "running",
988
1302
  providerRequested: args.provider,
989
1303
  providerResolved: null,
990
1304
  agentType: args.agentType,
991
- transport: this._shouldUseRoute(args) ? 'route-script' : `${args.provider}-worker`,
1305
+ transport: this._shouldUseRoute(args)
1306
+ ? "route-script"
1307
+ : `${args.provider}-worker`,
992
1308
  createdAt: now,
993
1309
  startedAt: now,
994
1310
  updatedAt: now,
995
1311
  completedAt: null,
996
- output: '',
997
- stderr: '',
1312
+ output: "",
1313
+ stderr: "",
998
1314
  exitCode: null,
999
1315
  threadId: null,
1000
1316
  sessionKey: args.sessionKey || null,
@@ -1006,28 +1322,34 @@ export class DelegatorMcpWorker {
1006
1322
 
1007
1323
  _applyJobResult(job, result = {}) {
1008
1324
  job.ok = result.ok !== false;
1009
- job.status = job.ok ? 'completed' : 'failed';
1325
+ job.status = job.ok ? "completed" : "failed";
1010
1326
  job.providerResolved = result.providerResolved || job.providerRequested;
1011
1327
  job.transport = result.transport || job.transport;
1012
- job.output = result.output || '';
1013
- job.stderr = result.stderr || result.error || '';
1328
+ job.output = result.output || "";
1329
+ job.stderr = result.stderr || result.error || "";
1014
1330
  job.exitCode = result.exitCode ?? (job.ok ? 0 : 1);
1015
1331
  job.threadId = result.threadId || job.threadId || null;
1016
1332
  job.sessionKey = result.sessionKey || job.sessionKey || null;
1017
1333
  job.completedAt = new Date().toISOString();
1018
1334
  job.updatedAt = job.completedAt;
1019
1335
 
1020
- if (job.providerRequested === 'gemini'
1021
- && job.transport === 'gemini-worker'
1022
- && typeof result._geminiPrompt === 'string') {
1023
- this._storeGeminiConversation(job, result._geminiPrompt, result.output || '');
1336
+ if (
1337
+ job.providerRequested === "gemini" &&
1338
+ job.transport === "gemini-worker" &&
1339
+ typeof result._geminiPrompt === "string"
1340
+ ) {
1341
+ this._storeGeminiConversation(
1342
+ job,
1343
+ result._geminiPrompt,
1344
+ result.output || "",
1345
+ );
1024
1346
  }
1025
1347
  }
1026
1348
 
1027
1349
  _storeGeminiConversation(job, userPrompt, assistantReply) {
1028
1350
  const existing = this.geminiConversations.get(job.jobId);
1029
1351
  if (existing) {
1030
- if (typeof assistantReply === 'string') {
1352
+ if (typeof assistantReply === "string") {
1031
1353
  const lastTurn = existing.turns.at(-1);
1032
1354
  if (lastTurn && lastTurn.assistant !== assistantReply) {
1033
1355
  lastTurn.assistant = assistantReply;
@@ -1041,15 +1363,17 @@ export class DelegatorMcpWorker {
1041
1363
  jobId: job.jobId,
1042
1364
  closed: false,
1043
1365
  updatedAt: new Date().toISOString(),
1044
- turns: [{
1045
- user: userPrompt,
1046
- assistant: assistantReply,
1047
- }],
1366
+ turns: [
1367
+ {
1368
+ user: userPrompt,
1369
+ assistant: assistantReply,
1370
+ },
1371
+ ],
1048
1372
  });
1049
1373
  }
1050
1374
 
1051
1375
  async _runSyncJob(args, extra) {
1052
- const job = this._createJob(args, 'sync');
1376
+ const job = this._createJob(args, "sync");
1053
1377
  this.jobs.set(job.jobId, job);
1054
1378
  const result = await this._executeDirect(args, extra);
1055
1379
  this._applyJobResult(job, result);
@@ -1066,7 +1390,9 @@ export async function runDelegatorMcpCli() {
1066
1390
  try {
1067
1391
  await worker.serveStdio();
1068
1392
  } catch (error) {
1069
- console.error(`[delegator-mcp] ${error instanceof Error ? error.message : String(error)}`);
1393
+ console.error(
1394
+ `[delegator-mcp] ${error instanceof Error ? error.message : String(error)}`,
1395
+ );
1070
1396
  process.exitCode = 1;
1071
1397
  }
1072
1398
  }