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