@triflux/remote 10.51.0 → 10.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/cto/brief.mjs +0 -8
  2. package/cto/collect.mjs +1 -127
  3. package/cto/current.schema.json +2 -19
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -139
  7. package/hub/index.mjs +0 -9
  8. package/hub/team/agents-row-attach.py +120 -0
  9. package/hub/team/agents-row.mjs +200 -0
  10. package/hub/team/claude-daemon-control.mjs +1 -2
  11. package/hub/team/claude-session-projection.mjs +5 -73
  12. package/hub/team/claude-transcript.mjs +19 -0
  13. package/hub/team/cli/commands/debug.mjs +1 -2
  14. package/hub/team/cli/commands/start/index.mjs +2 -1
  15. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  16. package/hub/team/cli/commands/start/start-mux.mjs +2 -4
  17. package/hub/team/cli/services/state-store.mjs +5 -20
  18. package/hub/team/headless.mjs +132 -551
  19. package/hub/team/psmux.mjs +3 -33
  20. package/hub/team/results-index.mjs +92 -0
  21. package/hub/team/session-context.mjs +1 -1
  22. package/hub/team/tui-viewer.mjs +9 -56
  23. package/hub/team/tui.mjs +0 -37
  24. package/hub/team/uds-orchestrator.mjs +20 -599
  25. package/hub/workers/factory.mjs +6 -51
  26. package/hub/workers/interface.mjs +1 -7
  27. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  28. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
  29. package/package.json +3 -8
  30. package/scripts/lib/agent-route-policy.mjs +0 -67
  31. package/scripts/lib/cli-claude.mjs +0 -12
  32. package/scripts/lib/env-probe.mjs +0 -70
  33. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  34. package/scripts/lib/mcp-guard-engine.mjs +42 -101
  35. package/hub/hub-lifecycle.mjs +0 -248
  36. package/hub/pipe.mjs +0 -825
  37. package/hub/promote-penalties.mjs +0 -138
  38. package/hub/public/dashboard.html +0 -416
  39. package/hub/schema.sql +0 -209
  40. package/hub/server.mjs +0 -3048
  41. package/hub/store-adapter.mjs +0 -182
  42. package/hub/store.mjs +0 -1736
  43. package/hub/team/cli/services/hub-client.mjs +0 -178
  44. package/hub/team/conductor-registry.mjs +0 -121
  45. package/hub/team/cto-auto-collect.mjs +0 -115
  46. package/hub/team/lead-control.mjs +0 -191
  47. package/hub/team/session-sync.mjs +0 -316
  48. package/hub/team/swarm-intent.mjs +0 -18
  49. package/hub/team/synapse-cli.mjs +0 -458
  50. package/hub/team/synapse-http.mjs +0 -311
  51. package/hub/team/synapse-registry.mjs +0 -618
  52. package/hub/team/tui-synapse.mjs +0 -223
  53. package/hub/team/worker-sandbox.mjs +0 -125
  54. package/hub/tools.mjs +0 -900
  55. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  56. package/hub/workers/delegator-mcp.mjs +0 -1098
  57. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  58. package/scripts/lib/claudemd-scanner.mjs +0 -26
  59. package/scripts/lib/context.mjs +0 -51
  60. package/scripts/lib/handoff.mjs +0 -187
  61. package/scripts/lib/logger.mjs +0 -122
  62. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -1,1098 +0,0 @@
1
- #!/usr/bin/env node
2
- // hub/workers/delegator-mcp.mjs — triflux 위임용 MCP stdio 서버
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
- import { resolveCodexAgentPolicy } from "../../scripts/lib/agent-route-policy.mjs";
15
- import { resolveBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
16
- import { runHeadlessWithCleanup } from "../team/headless.mjs";
17
- import { buildWorkerSandboxEnv } from "../team/worker-sandbox.mjs";
18
- import { CodexMcpWorker } from "./codex-mcp.mjs";
19
-
20
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
21
-
22
- // mcp-filter.mjs 동적 해석 — 프로젝트(hub/workers/)와 배포(scripts/hub/workers/) 양쪽 대응
23
- const MCP_FILTER_CANDIDATES = [
24
- resolve(SCRIPT_DIR, "../../scripts/lib/mcp-filter.mjs"), // 프로젝트 원본
25
- resolve(SCRIPT_DIR, "../../lib/mcp-filter.mjs"), // 배포 (~/.claude/scripts/)
26
- ];
27
- const mcpFilterPath = MCP_FILTER_CANDIDATES.find((p) => existsSync(p));
28
- if (!mcpFilterPath) {
29
- throw new Error(
30
- `mcp-filter.mjs not found. candidates: ${MCP_FILTER_CANDIDATES.join(", ")}`,
31
- );
32
- }
33
- const { buildPromptHint, getCodexMcpConfig, SUPPORTED_MCP_PROFILES } =
34
- await import(pathToFileURL(mcpFilterPath).href);
35
- const SERVER_INFO = { name: "triflux-delegator", version: "1.0.0" };
36
- const DEFAULT_CONTEXT_BYTES = 32 * 1024;
37
- const DIRECT_PROGRESS_START = 5;
38
- const DIRECT_PROGRESS_RUNNING = 60;
39
- const DIRECT_PROGRESS_DONE = 100;
40
- const SEARCH_ENGINE_CACHE_PATH = [".omc", "state", "search-engines.json"];
41
-
42
- const REVIEW_INSTRUCTION_BY_AGENT = Object.freeze({
43
- "code-reviewer":
44
- "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라.",
45
- "security-reviewer":
46
- "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라.",
47
- "quality-reviewer":
48
- "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라.",
49
- });
50
-
51
- function cloneEnv(env = process.env) {
52
- return Object.fromEntries(
53
- Object.entries(env).filter(([, value]) => typeof value === "string"),
54
- );
55
- }
56
-
57
- function parseJsonArray(raw, fallback = []) {
58
- if (!raw) return [...fallback];
59
- try {
60
- const parsed = JSON.parse(raw);
61
- return Array.isArray(parsed)
62
- ? parsed.map((item) => String(item ?? "")).filter(Boolean)
63
- : [...fallback];
64
- } catch {
65
- return [...fallback];
66
- }
67
- }
68
-
69
- function resolveCandidatePath(candidate, cwd = process.cwd()) {
70
- if (!candidate) return null;
71
- const normalized = isAbsolute(candidate)
72
- ? candidate
73
- : resolve(cwd, candidate);
74
- return existsSync(normalized) ? normalized : null;
75
- }
76
-
77
- function resolveRouteScript(explicitPath, cwd = process.cwd()) {
78
- const candidates = [
79
- explicitPath,
80
- process.env.TFX_DELEGATOR_ROUTE_SCRIPT,
81
- process.env.TFX_ROUTE_SCRIPT,
82
- resolve(SCRIPT_DIR, "..", "..", "scripts", "tfx-route.sh"),
83
- resolve(cwd, "scripts", "tfx-route.sh"),
84
- ];
85
-
86
- for (const candidate of candidates) {
87
- const resolved = resolveCandidatePath(candidate, cwd);
88
- if (resolved) return resolved;
89
- }
90
-
91
- return null;
92
- }
93
-
94
- function resolveCodexProfile(agentType) {
95
- return resolveCodexAgentPolicy(agentType).profile;
96
- }
97
-
98
- function normalizeProvider(provider = "auto") {
99
- const value = provider || "auto";
100
- return value === "gemini" ? "antigravity" : value;
101
- }
102
-
103
- function normalizeDelegateArgs(args = {}) {
104
- return { ...args, provider: normalizeProvider(args.provider) };
105
- }
106
-
107
- function resolveTimeoutMs(agentType, timeoutMs) {
108
- if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
109
- return Math.trunc(timeoutMs);
110
- }
111
-
112
- return resolveCodexAgentPolicy(agentType).timeoutSec * 1000;
113
- }
114
-
115
- function resolveTimeoutSec(agentType, timeoutMs) {
116
- const resolved = resolveTimeoutMs(agentType, timeoutMs);
117
- return Math.max(1, Math.ceil(resolved / 1000));
118
- }
119
-
120
- function loadContextFromFile(contextFile) {
121
- if (!contextFile) return "";
122
- const resolved = resolveCandidatePath(contextFile);
123
- if (!resolved) return "";
124
- try {
125
- return readFileSync(resolved, "utf8").slice(0, DEFAULT_CONTEXT_BYTES);
126
- } catch {
127
- return "";
128
- }
129
- }
130
-
131
- function withContext(prompt, contextFile) {
132
- const context = loadContextFromFile(contextFile);
133
- if (!context) return prompt;
134
- return `${prompt}\n\n<prior_context>\n${context}\n</prior_context>`;
135
- }
136
-
137
- function joinInstructions(...values) {
138
- return values
139
- .filter((value) => typeof value === "string" && value.trim())
140
- .join("\n")
141
- .trim();
142
- }
143
-
144
- function _loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
145
- const cacheFile = resolve(cwd, ...SEARCH_ENGINE_CACHE_PATH);
146
- if (!existsSync(cacheFile)) return null;
147
-
148
- try {
149
- const parsed = JSON.parse(readFileSync(cacheFile, "utf8"));
150
- if (!Array.isArray(parsed?.engines)) return null;
151
- return parsed.engines
152
- .filter((engine) => engine?.status === "available")
153
- .map((engine) =>
154
- typeof engine?.name === "string" ? engine.name.trim() : "",
155
- )
156
- .filter(Boolean);
157
- } catch {
158
- return null;
159
- }
160
- }
161
-
162
- function parseRouteType(stderr = "") {
163
- const match = stderr.match(/type=([a-z-]+)/);
164
- if (!match) return null;
165
- if (match[1] === "codex") return "codex";
166
- if (match[1] === "gemini") return "antigravity";
167
- return match[1];
168
- }
169
-
170
- function summarizePayload(payload) {
171
- if (typeof payload.output === "string" && payload.output.trim())
172
- return payload.output.trim();
173
- if (payload.mode === "async" && payload.job_id)
174
- return `비동기 위임이 시작되었습니다. jobId=${payload.job_id}`;
175
- if (payload.job_id) return `jobId=${payload.job_id} 상태=${payload.status}`;
176
- if (payload.status) return `상태=${payload.status}`;
177
- return payload.ok ? "완료되었습니다." : "실패했습니다.";
178
- }
179
-
180
- function createToolResponse(payload, { isError = false } = {}) {
181
- return {
182
- content: [{ type: "text", text: summarizePayload(payload) }],
183
- structuredContent: payload,
184
- isError,
185
- };
186
- }
187
-
188
- function createErrorPayload(message, extras = {}) {
189
- return {
190
- ok: false,
191
- status: "failed",
192
- error: message,
193
- ...extras,
194
- };
195
- }
196
-
197
- const DelegateInputSchema = z.object({
198
- prompt: z.string().min(1).describe("위임할 프롬프트"),
199
- provider: z
200
- .enum(["auto", "codex", "antigravity", "gemini"])
201
- .default("auto")
202
- .describe("사용할 provider"),
203
- mode: z
204
- .enum(["sync", "async"])
205
- .default("sync")
206
- .describe("동기 또는 비동기 실행"),
207
- agentType: z
208
- .string()
209
- .default("executor")
210
- .describe("tfx-route 역할명 또는 direct 실행 역할"),
211
- cwd: z.string().optional().describe("작업 디렉터리"),
212
- timeoutMs: z
213
- .number()
214
- .int()
215
- .positive()
216
- .optional()
217
- .describe("요청 타임아웃(ms)"),
218
- sessionKey: z.string().optional().describe("Codex warm session 재사용 키"),
219
- resetSession: z.boolean().optional().describe("기존 Codex 세션 초기화 여부"),
220
- mcpProfile: z.enum(SUPPORTED_MCP_PROFILES).default("auto"),
221
- contextFile: z
222
- .string()
223
- .optional()
224
- .describe("tfx-route prior_context 파일 경로"),
225
- availableServers: z
226
- .array(z.string())
227
- .optional()
228
- .describe("Codex에 등록된 MCP 서버 이름 목록 (config override 대상)"),
229
- searchTool: z
230
- .enum(["brave-search", "tavily", "exa"])
231
- .optional()
232
- .describe("검색 우선 도구"),
233
- workerIndex: z
234
- .number()
235
- .int()
236
- .positive()
237
- .optional()
238
- .describe("병렬 워커 인덱스"),
239
- model: z.string().optional().describe("직접 실행 시 모델 오버라이드"),
240
- developerInstructions: z
241
- .string()
242
- .optional()
243
- .describe("직접 실행 시 추가 개발자 지침"),
244
- compactPrompt: z.string().optional().describe("Codex compact prompt"),
245
- threadId: z.string().optional().describe("Codex 직접 실행 시 기존 threadId"),
246
- codexTransport: z
247
- .enum(["auto", "mcp", "exec"])
248
- .optional()
249
- .describe("route 경로용 Codex transport"),
250
- noClaudeNative: z
251
- .boolean()
252
- .optional()
253
- .describe("route 경로용 TFX_NO_CLAUDE_NATIVE"),
254
- teamName: z.string().optional().describe("TFX_TEAM_NAME"),
255
- teamTaskId: z.string().optional().describe("TFX_TEAM_TASK_ID"),
256
- teamAgentName: z.string().optional().describe("TFX_TEAM_AGENT_NAME"),
257
- teamLeadName: z.string().optional().describe("TFX_TEAM_LEAD_NAME"),
258
- hubUrl: z.string().optional().describe("TFX_HUB_URL"),
259
- workers: z
260
- .array(
261
- z.object({
262
- provider: z
263
- .enum(["codex", "antigravity", "gemini"])
264
- .describe("워커 provider"),
265
- agentType: z.string().default("executor").describe("역할명"),
266
- prompt: z.string().describe("워커별 프롬프트"),
267
- mcpProfile: z.string().default("auto").describe("MCP 프로필"),
268
- model: z.string().optional().describe("모델 오버라이드"),
269
- }),
270
- )
271
- .optional()
272
- .describe("병렬 멀티워커 목록. 지정 시 psmux 기반 병렬 실행"),
273
- });
274
-
275
- const DelegateStatusInputSchema = z.object({
276
- jobId: z.string().min(1).describe("조회할 비동기 job ID"),
277
- });
278
-
279
- const DelegateReplyInputSchema = z.object({
280
- job_id: z.string().min(1).describe("후속 응답을 보낼 기존 delegate job ID"),
281
- reply: z.string().min(1).describe("후속 사용자 응답"),
282
- done: z
283
- .boolean()
284
- .default(false)
285
- .describe("true이면 응답 처리 후 대화를 종료"),
286
- });
287
-
288
- const DelegateOutputSchema = z.object({
289
- ok: z.boolean(),
290
- jobId: z.string().optional(),
291
- job_id: z.string().optional(),
292
- mode: z.enum(["sync", "async"]).optional(),
293
- status: z.enum(["running", "completed", "failed", "partial"]).optional(),
294
- error: z.string().optional(),
295
- providerRequested: z.string().optional(),
296
- providerResolved: z.string().nullable().optional(),
297
- agentType: z.string().optional(),
298
- transport: z.string().optional(),
299
- createdAt: z.string().optional(),
300
- startedAt: z.string().optional(),
301
- updatedAt: z.string().optional(),
302
- completedAt: z.string().nullable().optional(),
303
- exitCode: z.number().nullable().optional(),
304
- output: z.string().optional(),
305
- stderr: z.string().optional(),
306
- threadId: z.string().nullable().optional(),
307
- sessionKey: z.string().nullable().optional(),
308
- conversationOpen: z.boolean().optional(),
309
- workerResults: z
310
- .array(
311
- z.object({
312
- cli: z.string(),
313
- role: z.string().optional(),
314
- paneName: z.string(),
315
- matched: z.boolean(),
316
- exitCode: z.number().nullable(),
317
- output: z.string(),
318
- }),
319
- )
320
- .optional()
321
- .describe("멀티워커 개별 결과"),
322
- sessionName: z.string().optional().describe("psmux 세션명"),
323
- });
324
-
325
- function isTeamRouteRequested(args) {
326
- return Boolean(
327
- args.teamName ||
328
- args.teamTaskId ||
329
- args.teamAgentName ||
330
- args.teamLeadName ||
331
- args.hubUrl,
332
- );
333
- }
334
-
335
- function pickRouteMode(provider) {
336
- const normalized = normalizeProvider(provider);
337
- return normalized === "auto" ? "auto" : normalized;
338
- }
339
-
340
- function sanitizeDelegateArgs(args = {}) {
341
- return {
342
- provider: normalizeProvider(args.provider),
343
- agentType: args.agentType || "executor",
344
- cwd: args.cwd || null,
345
- timeoutMs: Number.isFinite(Number(args.timeoutMs))
346
- ? Math.trunc(Number(args.timeoutMs))
347
- : null,
348
- sessionKey: args.sessionKey || null,
349
- resetSession: Boolean(args.resetSession),
350
- mcpProfile: args.mcpProfile || "auto",
351
- contextFile: args.contextFile || null,
352
- availableServers: Array.isArray(args.availableServers)
353
- ? args.availableServers
354
- : null,
355
- searchTool: args.searchTool || null,
356
- workerIndex: Number.isInteger(args.workerIndex) ? args.workerIndex : null,
357
- model: args.model || null,
358
- developerInstructions: args.developerInstructions || null,
359
- compactPrompt: args.compactPrompt || null,
360
- threadId: args.threadId || null,
361
- codexTransport: args.codexTransport || null,
362
- noClaudeNative: args.noClaudeNative === true,
363
- teamName: args.teamName || null,
364
- teamTaskId: args.teamTaskId || null,
365
- teamAgentName: args.teamAgentName || null,
366
- teamLeadName: args.teamLeadName || null,
367
- hubUrl: args.hubUrl || null,
368
- };
369
- }
370
-
371
- async function emitProgress(extra, progress, total, message) {
372
- if (extra?._meta?.progressToken === undefined) return;
373
- await extra.sendNotification({
374
- method: "notifications/progress",
375
- params: {
376
- progressToken: extra._meta.progressToken,
377
- progress,
378
- total,
379
- message,
380
- },
381
- });
382
- }
383
-
384
- export class DelegatorMcpWorker {
385
- type = "delegator";
386
-
387
- constructor(options = {}) {
388
- this.cwd = options.cwd || process.cwd();
389
- this.env = cloneEnv({ ...cloneEnv(process.env), ...cloneEnv(options.env) });
390
- this.routeScript = resolveRouteScript(options.routeScript, this.cwd);
391
- this.bashCommand =
392
- options.bashCommand ||
393
- this.env.TFX_DELEGATOR_BASH_COMMAND ||
394
- this.env.BASH_BIN ||
395
- resolveBashExecutable();
396
-
397
- this.codexWorker = new CodexMcpWorker({
398
- command:
399
- options.codexCommand ||
400
- this.env.TFX_DELEGATOR_CODEX_COMMAND ||
401
- this.env.CODEX_BIN ||
402
- "codex",
403
- args:
404
- Array.isArray(options.codexArgs) && options.codexArgs.length
405
- ? options.codexArgs
406
- : parseJsonArray(this.env.TFX_DELEGATOR_CODEX_ARGS_JSON, []),
407
- cwd: this.cwd,
408
- env: this.env,
409
- clientInfo: { name: SERVER_INFO.name, version: SERVER_INFO.version },
410
- });
411
-
412
- this.server = null;
413
- this.transport = null;
414
- this.jobs = new Map();
415
- this.routeChildren = new Set();
416
- this.ready = false;
417
- }
418
-
419
- isReady() {
420
- return this.ready;
421
- }
422
-
423
- async start() {
424
- if (this.server) {
425
- this.ready = true;
426
- return;
427
- }
428
-
429
- const server = new McpServer(SERVER_INFO, {
430
- capabilities: { logging: {} },
431
- });
432
-
433
- server.registerTool(
434
- "triflux-delegate",
435
- {
436
- description:
437
- "새 위임을 실행합니다. codex direct 경로와 antigravity/tfx-route 기반 auto 라우팅을 지원합니다.",
438
- inputSchema: DelegateInputSchema,
439
- outputSchema: DelegateOutputSchema,
440
- },
441
- async (args, extra) => {
442
- const payload = await this.delegate(args, extra);
443
- return createToolResponse(payload, {
444
- isError: payload.ok === false && payload.mode !== "async",
445
- });
446
- },
447
- );
448
-
449
- server.registerTool(
450
- "triflux-delegate-status",
451
- {
452
- description: "비동기 위임 job 상태를 조회합니다.",
453
- inputSchema: DelegateStatusInputSchema,
454
- outputSchema: DelegateOutputSchema,
455
- },
456
- async ({ jobId }, extra) => {
457
- const payload = await this.getJobStatus(jobId, extra);
458
- return createToolResponse(payload, { isError: payload.ok === false });
459
- },
460
- );
461
-
462
- server.registerTool(
463
- "triflux-delegate-reply",
464
- {
465
- description:
466
- "기존 delegate job에 후속 응답을 보냅니다. 현재 route 기반 Antigravity job은 후속 응답을 지원하지 않습니다.",
467
- inputSchema: DelegateReplyInputSchema,
468
- outputSchema: DelegateOutputSchema,
469
- },
470
- async (args, extra) => {
471
- const payload = await this.reply(args, extra);
472
- return createToolResponse(payload, { isError: payload.ok === false });
473
- },
474
- );
475
-
476
- this.server = server;
477
- this.ready = true;
478
- }
479
-
480
- async serveStdio() {
481
- await this.start();
482
- if (this.transport) return;
483
- const transport = new StdioServerTransport();
484
- await this.server.connect(transport);
485
- this.transport = transport;
486
- }
487
-
488
- async stop() {
489
- this.ready = false;
490
-
491
- for (const child of this.routeChildren) {
492
- try {
493
- child.kill();
494
- } catch {}
495
- }
496
- this.routeChildren.clear();
497
-
498
- await this.codexWorker.stop().catch(() => {});
499
- if (this.server) {
500
- await this.server.close().catch(() => {});
501
- }
502
-
503
- this.server = null;
504
- this.transport = null;
505
- }
506
-
507
- async run(prompt, options = {}) {
508
- return this._executeDirect(normalizeDelegateArgs({ prompt, ...options }));
509
- }
510
-
511
- async execute(prompt, options = {}) {
512
- const result = await this._executeDirect(
513
- normalizeDelegateArgs({ prompt, ...options }),
514
- );
515
- return {
516
- output: result.output || result.error || "",
517
- exitCode: result.exitCode ?? (result.ok ? 0 : 1),
518
- threadId: result.threadId || null,
519
- sessionKey: result.sessionKey || null,
520
- raw: result,
521
- };
522
- }
523
-
524
- async delegate(args, extra) {
525
- const normalizedArgs = normalizeDelegateArgs(args);
526
- if (normalizedArgs.mode === "async") {
527
- return this._startAsyncJob(normalizedArgs, extra);
528
- }
529
- return this._runSyncJob(normalizedArgs, extra);
530
- }
531
-
532
- async getJobStatus(jobId, extra) {
533
- const job = this.jobs.get(jobId);
534
- if (!job) {
535
- return createErrorPayload(`알 수 없는 jobId: ${jobId}`, { jobId });
536
- }
537
-
538
- const payload = this._serializeJob(job);
539
- if (job.status === "running") {
540
- await emitProgress(extra, 25, 100, `job ${jobId} 실행 중`);
541
- } else if (job.status === "completed") {
542
- await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${jobId} 완료`);
543
- } else if (job.status === "failed") {
544
- await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, `job ${jobId} 실패`);
545
- }
546
- return payload;
547
- }
548
-
549
- async reply({ job_id }, extra) {
550
- const job = this.jobs.get(job_id);
551
- if (!job) {
552
- return createErrorPayload(`알 수 없는 jobId: ${job_id}`, {
553
- jobId: job_id,
554
- job_id,
555
- });
556
- }
557
- if (job.status === "running") {
558
- return createErrorPayload(`job ${job_id}가 아직 실행 중입니다.`, {
559
- jobId: job_id,
560
- job_id,
561
- });
562
- }
563
- void extra;
564
- return createErrorPayload(
565
- "delegate-reply는 route 기반 Antigravity job에 지원되지 않습니다.",
566
- {
567
- jobId: job_id,
568
- job_id,
569
- },
570
- );
571
- }
572
-
573
- _buildDirectPrompt(args) {
574
- return withContext(String(args.prompt ?? ""), args.contextFile);
575
- }
576
-
577
- _getMcpPolicyOptions(args) {
578
- return {
579
- agentType: args.agentType || "executor",
580
- requestedProfile: args.mcpProfile || "auto",
581
- searchTool: args.searchTool,
582
- workerIndex: Number.isInteger(args.workerIndex)
583
- ? args.workerIndex
584
- : undefined,
585
- taskText: withContext(String(args.prompt ?? ""), args.contextFile),
586
- availableServers: Array.isArray(args.availableServers)
587
- ? args.availableServers
588
- : undefined,
589
- };
590
- }
591
-
592
- _buildPromptHintInstruction(args) {
593
- return buildPromptHint(this._getMcpPolicyOptions(args));
594
- }
595
-
596
- _shouldUseRoute(args) {
597
- return (
598
- args.provider === "auto" ||
599
- args.provider === "antigravity" ||
600
- args.provider === "gemini" ||
601
- isTeamRouteRequested(args)
602
- );
603
- }
604
-
605
- async _executeDirect(args, extra = null) {
606
- await emitProgress(
607
- extra,
608
- DIRECT_PROGRESS_START,
609
- 100,
610
- "위임 실행을 시작합니다.",
611
- );
612
-
613
- // 멀티워커 분기: workers 배열이 있으면 headless psmux 병렬 실행
614
- if (Array.isArray(args.workers) && args.workers.length > 0) {
615
- try {
616
- const result = await this._executeMultiWorker(args, extra);
617
- await emitProgress(
618
- extra,
619
- DIRECT_PROGRESS_DONE,
620
- 100,
621
- "위임이 완료되었습니다.",
622
- );
623
- return result;
624
- } catch (error) {
625
- const message = error instanceof Error ? error.message : String(error);
626
- return createErrorPayload(message, {
627
- mode: "sync",
628
- providerRequested: "multi",
629
- agentType: args.agentType,
630
- transport: "headless-psmux",
631
- });
632
- }
633
- }
634
-
635
- const runViaRoute = this._shouldUseRoute(args);
636
-
637
- try {
638
- const result = runViaRoute
639
- ? await this._executeRoute(args, extra)
640
- : await this._executeWorker(args, extra);
641
-
642
- await emitProgress(
643
- extra,
644
- DIRECT_PROGRESS_DONE,
645
- 100,
646
- "위임이 완료되었습니다.",
647
- );
648
- return result;
649
- } catch (error) {
650
- const message = error instanceof Error ? error.message : String(error);
651
- return createErrorPayload(message, {
652
- mode: "sync",
653
- providerRequested: args.provider,
654
- agentType: args.agentType,
655
- transport: runViaRoute ? "route-script" : `${args.provider}-worker`,
656
- });
657
- }
658
- }
659
-
660
- async _executeMultiWorker(args, extra) {
661
- await emitProgress(
662
- extra,
663
- DIRECT_PROGRESS_START,
664
- 100,
665
- "멀티워커 psmux 병렬 실행을 시작합니다.",
666
- );
667
-
668
- // workers 배열을 headless assignments 형식으로 변환
669
- const assignments = args.workers.map((w) => {
670
- // provider를 CLI 이름으로 매핑. gemini는 deprecated alias로 antigravity에 합류한다.
671
- const cli = normalizeProvider(w.provider);
672
- const role = w.agentType || "executor";
673
- // buildDirectPrompt와 동일한 context 처리
674
- const prompt = withContext(String(w.prompt || ""), args.contextFile);
675
- return { cli, prompt, role };
676
- });
677
-
678
- // 타임아웃: 워커 중 가장 긴 타임아웃 사용
679
- const maxTimeoutSec = Math.max(
680
- ...args.workers.map((w) =>
681
- Math.ceil(
682
- resolveTimeoutMs(w.agentType || "executor", args.timeoutMs) / 1000,
683
- ),
684
- ),
685
- 300,
686
- );
687
-
688
- try {
689
- const { results, sessionName } = await runHeadlessWithCleanup(
690
- assignments,
691
- {
692
- sessionPrefix: "dlg",
693
- timeoutSec: maxTimeoutSec,
694
- layout: assignments.length <= 2 ? "even-horizontal" : "2x2",
695
- progressive: true,
696
- dashboard: true,
697
- dashboardLayout: "single",
698
- },
699
- );
700
-
701
- await emitProgress(
702
- extra,
703
- DIRECT_PROGRESS_DONE,
704
- 100,
705
- `멀티워커 실행 완료: ${results.length}개 워커`,
706
- );
707
-
708
- // 전체 성공 판단: 모든 워커가 matched && exitCode === 0
709
- const allOk = results.every((r) => r.matched && r.exitCode === 0);
710
- // 개별 출력을 합산
711
- const combinedOutput = results
712
- .map(
713
- (r, i) =>
714
- `=== Worker ${i + 1} (${r.cli}/${assignments[i].role}) ===\n${r.output || "(no output)"}`,
715
- )
716
- .join("\n\n");
717
-
718
- return {
719
- ok: allOk,
720
- mode: "sync",
721
- status: allOk ? "completed" : "partial",
722
- providerRequested: "multi",
723
- providerResolved: "multi",
724
- agentType: args.agentType || "executor",
725
- transport: "headless-psmux",
726
- exitCode: allOk ? 0 : 1,
727
- output: combinedOutput,
728
- workerResults: results.map((r) => ({
729
- cli: r.cli,
730
- role: r.role || "",
731
- paneName: r.paneName,
732
- matched: r.matched,
733
- exitCode: r.exitCode,
734
- output: r.output || "",
735
- })),
736
- sessionName,
737
- };
738
- } catch (error) {
739
- const message = error instanceof Error ? error.message : String(error);
740
- return createErrorPayload(message, {
741
- mode: "sync",
742
- providerRequested: "multi",
743
- agentType: args.agentType || "executor",
744
- transport: "headless-psmux",
745
- });
746
- }
747
- }
748
-
749
- async _executeWorker(args, extra) {
750
- await emitProgress(
751
- extra,
752
- DIRECT_PROGRESS_RUNNING,
753
- 100,
754
- "직접 워커 경로로 실행 중입니다.",
755
- );
756
-
757
- if (args.provider === "codex") {
758
- const result = await this.codexWorker.execute(
759
- this._buildDirectPrompt(args),
760
- {
761
- cwd: args.cwd || this.cwd,
762
- timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
763
- sessionKey: args.sessionKey,
764
- threadId: args.threadId,
765
- resetSession: args.resetSession,
766
- profile: resolveCodexProfile(args.agentType),
767
- sandbox: "danger-full-access",
768
- approvalPolicy: "never",
769
- developerInstructions: joinInstructions(
770
- REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
771
- this._buildPromptHintInstruction(args),
772
- args.developerInstructions,
773
- ),
774
- config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
775
- compactPrompt: args.compactPrompt,
776
- model: args.model,
777
- },
778
- );
779
-
780
- return {
781
- ok: result.exitCode === 0,
782
- mode: "sync",
783
- status: result.exitCode === 0 ? "completed" : "failed",
784
- providerRequested: "codex",
785
- providerResolved: "codex",
786
- agentType: args.agentType,
787
- transport: "codex-mcp",
788
- exitCode: result.exitCode,
789
- output: result.output,
790
- sessionKey: result.sessionKey,
791
- threadId: result.threadId,
792
- };
793
- }
794
-
795
- return createErrorPayload(
796
- `지원하지 않는 direct provider: ${args.provider}`,
797
- {
798
- mode: "sync",
799
- providerRequested: args.provider,
800
- agentType: args.agentType,
801
- transport: "direct-worker",
802
- },
803
- );
804
- }
805
-
806
- async _executeRoute(args, extra) {
807
- if (!this.routeScript) {
808
- return createErrorPayload("tfx-route.sh 경로를 찾지 못했습니다.", {
809
- mode: "sync",
810
- providerRequested: args.provider,
811
- agentType: args.agentType,
812
- transport: "route-script",
813
- });
814
- }
815
-
816
- await emitProgress(
817
- extra,
818
- DIRECT_PROGRESS_RUNNING,
819
- 100,
820
- "tfx-route.sh 경로로 실행 중입니다.",
821
- );
822
- const result = await this._spawnRoute(args);
823
- return {
824
- ok: result.exitCode === 0,
825
- mode: "sync",
826
- status: result.exitCode === 0 ? "completed" : "failed",
827
- providerRequested: args.provider,
828
- providerResolved: parseRouteType(result.stderr) || args.provider,
829
- agentType: args.agentType,
830
- transport: "route-script",
831
- exitCode: result.exitCode,
832
- output: result.stdout.trim() || result.stderr.trim(),
833
- stderr: result.stderr.trim(),
834
- };
835
- }
836
-
837
- async _startAsyncJob(args, extra) {
838
- const job = this._createJob(args, "async");
839
- this.jobs.set(job.jobId, job);
840
-
841
- await emitProgress(
842
- extra,
843
- DIRECT_PROGRESS_START,
844
- 100,
845
- `비동기 job ${job.jobId}를 시작합니다.`,
846
- );
847
-
848
- void (async () => {
849
- try {
850
- const result = this._shouldUseRoute(args)
851
- ? await this._spawnRoute(args, job)
852
- : await this._runAsyncWorker(args, job);
853
- this._applyJobResult(job, result);
854
- } catch (error) {
855
- this._applyJobResult(
856
- job,
857
- createErrorPayload(
858
- error instanceof Error ? error.message : String(error),
859
- {
860
- mode: "async",
861
- providerRequested: args.provider,
862
- agentType: args.agentType,
863
- transport: this._shouldUseRoute(args)
864
- ? "route-script"
865
- : `${args.provider}-worker`,
866
- },
867
- ),
868
- );
869
- } finally {
870
- if (job.worker) {
871
- await job.worker.stop().catch(() => {});
872
- job.worker = null;
873
- }
874
- job.child = null;
875
- }
876
- })();
877
-
878
- return this._serializeJob(job);
879
- }
880
-
881
- async _runAsyncWorker(args, job) {
882
- if (args.provider === "codex") {
883
- const result = await this.codexWorker.execute(
884
- this._buildDirectPrompt(args),
885
- {
886
- cwd: args.cwd || this.cwd,
887
- timeoutMs: resolveTimeoutMs(args.agentType, args.timeoutMs),
888
- sessionKey: args.sessionKey,
889
- threadId: args.threadId,
890
- resetSession: args.resetSession,
891
- profile: resolveCodexProfile(args.agentType),
892
- sandbox: "danger-full-access",
893
- approvalPolicy: "never",
894
- developerInstructions: joinInstructions(
895
- REVIEW_INSTRUCTION_BY_AGENT[args.agentType],
896
- this._buildPromptHintInstruction(args),
897
- args.developerInstructions,
898
- ),
899
- config: getCodexMcpConfig(this._getMcpPolicyOptions(args)),
900
- compactPrompt: args.compactPrompt,
901
- model: args.model,
902
- },
903
- );
904
-
905
- return {
906
- ok: result.exitCode === 0,
907
- providerResolved: "codex",
908
- output: result.output,
909
- exitCode: result.exitCode,
910
- threadId: result.threadId,
911
- sessionKey: result.sessionKey,
912
- };
913
- }
914
-
915
- throw new Error(`지원하지 않는 async provider: ${args.provider}`);
916
- }
917
-
918
- _buildRouteEnv(args) {
919
- const baseEnv = cloneEnv(this.env);
920
- const sandbox = buildWorkerSandboxEnv({
921
- cwd: args.cwd || this.cwd,
922
- sessionId:
923
- args.teamTaskId ||
924
- args.teamAgentName ||
925
- args.workerIndex ||
926
- args.agentType ||
927
- "route",
928
- agent: args.provider,
929
- env: baseEnv,
930
- });
931
- const env = { ...baseEnv, ...sandbox.env };
932
- env.TFX_CLI_MODE = pickRouteMode(args.provider);
933
- if (sandbox.root) env.TFX_WORKER_SANDBOX_SCOPE = "delegator-route";
934
-
935
- if (args.codexTransport) {
936
- env.TFX_CODEX_TRANSPORT = args.codexTransport;
937
- }
938
- if (args.noClaudeNative === true) {
939
- env.TFX_NO_CLAUDE_NATIVE = "1";
940
- }
941
- if (args.searchTool) {
942
- env.TFX_SEARCH_TOOL = args.searchTool;
943
- }
944
- if (Number.isInteger(args.workerIndex) && args.workerIndex > 0) {
945
- env.TFX_WORKER_INDEX = String(args.workerIndex);
946
- }
947
- if (args.teamName) env.TFX_TEAM_NAME = args.teamName;
948
- if (args.teamTaskId) env.TFX_TEAM_TASK_ID = args.teamTaskId;
949
- if (args.teamAgentName) env.TFX_TEAM_AGENT_NAME = args.teamAgentName;
950
- if (args.teamLeadName) env.TFX_TEAM_LEAD_NAME = args.teamLeadName;
951
- if (args.hubUrl) env.TFX_HUB_URL = args.hubUrl;
952
-
953
- return env;
954
- }
955
-
956
- async _spawnRoute(args, job = null) {
957
- const prompt = withContext(String(args.prompt ?? ""), args.contextFile);
958
- const childArgs = [
959
- this.routeScript,
960
- args.agentType || "executor",
961
- prompt,
962
- args.mcpProfile || "auto",
963
- String(resolveTimeoutSec(args.agentType, args.timeoutMs)),
964
- ];
965
-
966
- const child = spawn(this.bashCommand, childArgs, {
967
- cwd: args.cwd || this.cwd,
968
- env: this._buildRouteEnv(args),
969
- stdio: ["ignore", "pipe", "pipe"],
970
- windowsHide: true,
971
- });
972
-
973
- if (job) {
974
- job.child = child;
975
- }
976
-
977
- this.routeChildren.add(child);
978
-
979
- return await new Promise((resolvePromise, rejectPromise) => {
980
- const stdoutChunks = [];
981
- const stderrChunks = [];
982
-
983
- child.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
984
- child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
985
- child.once("error", (error) => {
986
- this.routeChildren.delete(child);
987
- rejectPromise(error);
988
- });
989
- child.once("close", (code) => {
990
- this.routeChildren.delete(child);
991
- const stdout = Buffer.concat(stdoutChunks).toString("utf8");
992
- const stderr = Buffer.concat(stderrChunks).toString("utf8");
993
- resolvePromise({
994
- ok: code === 0,
995
- providerResolved: parseRouteType(stderr) || args.provider,
996
- output: stdout.trim() || stderr.trim(),
997
- stdout,
998
- stderr,
999
- exitCode: code ?? 1,
1000
- });
1001
- });
1002
- });
1003
- }
1004
-
1005
- _serializeJob(job) {
1006
- return {
1007
- ok: job.ok,
1008
- job_id: job.jobId,
1009
- mode: job.mode || "async",
1010
- status: job.status,
1011
- provider_requested: job.providerRequested,
1012
- provider_resolved: job.providerResolved,
1013
- agent_type: job.agentType,
1014
- transport: job.transport,
1015
- created_at: job.createdAt,
1016
- started_at: job.startedAt,
1017
- updated_at: job.updatedAt,
1018
- completed_at: job.completedAt,
1019
- output: job.output,
1020
- stderr: job.stderr,
1021
- error: "",
1022
- thread_id: job.threadId,
1023
- session_key: job.sessionKey,
1024
- conversation_open: false,
1025
- };
1026
- }
1027
-
1028
- _createJob(args, mode) {
1029
- const jobId = randomUUID();
1030
- const now = new Date().toISOString();
1031
- return {
1032
- ok: true,
1033
- jobId,
1034
- mode,
1035
- status: "running",
1036
- providerRequested: args.provider,
1037
- providerResolved: null,
1038
- agentType: args.agentType,
1039
- transport: this._shouldUseRoute(args)
1040
- ? "route-script"
1041
- : `${args.provider}-worker`,
1042
- createdAt: now,
1043
- startedAt: now,
1044
- updatedAt: now,
1045
- completedAt: null,
1046
- output: "",
1047
- stderr: "",
1048
- exitCode: null,
1049
- threadId: null,
1050
- sessionKey: args.sessionKey || null,
1051
- worker: null,
1052
- child: null,
1053
- requestArgs: sanitizeDelegateArgs(args),
1054
- };
1055
- }
1056
-
1057
- _applyJobResult(job, result = {}) {
1058
- job.ok = result.ok !== false;
1059
- job.status = job.ok ? "completed" : "failed";
1060
- job.providerResolved = result.providerResolved || job.providerRequested;
1061
- job.transport = result.transport || job.transport;
1062
- job.output = result.output || "";
1063
- job.stderr = result.stderr || result.error || "";
1064
- job.exitCode = result.exitCode ?? (job.ok ? 0 : 1);
1065
- job.threadId = result.threadId || job.threadId || null;
1066
- job.sessionKey = result.sessionKey || job.sessionKey || null;
1067
- job.completedAt = new Date().toISOString();
1068
- job.updatedAt = job.completedAt;
1069
- }
1070
-
1071
- async _runSyncJob(args, extra) {
1072
- const job = this._createJob(args, "sync");
1073
- this.jobs.set(job.jobId, job);
1074
- const result = await this._executeDirect(args, extra);
1075
- this._applyJobResult(job, result);
1076
- return this._serializeJob(job);
1077
- }
1078
- }
1079
-
1080
- export function createDelegatorMcpWorker(options = {}) {
1081
- return new DelegatorMcpWorker(options);
1082
- }
1083
-
1084
- export async function runDelegatorMcpCli() {
1085
- const worker = createDelegatorMcpWorker();
1086
- try {
1087
- await worker.serveStdio();
1088
- } catch (error) {
1089
- console.error(
1090
- `[delegator-mcp] ${error instanceof Error ? error.message : String(error)}`,
1091
- );
1092
- process.exitCode = 1;
1093
- }
1094
- }
1095
-
1096
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
1097
- await runDelegatorMcpCli();
1098
- }