triflux 10.9.22 → 10.9.24

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.
@@ -30,5 +30,5 @@
30
30
  ]
31
31
  }
32
32
  ],
33
- "version": "10.9.21"
33
+ "version": "10.9.24"
34
34
  }
@@ -557,6 +557,7 @@
557
557
  },
558
558
  {
559
559
  "id": "gstack-ship",
560
+ "disabled": true,
560
561
  "patterns": [
561
562
  {
562
563
  "source": "(?:배포해|PR\\s*만들|릴리스\\s*해|머지하고\\s*배포)",
@@ -14,6 +14,7 @@ import * as z from "zod";
14
14
  import { resolveBashExecutable } from "../lib/bash-path.mjs";
15
15
  import { CodexMcpWorker } from "./codex-mcp.mjs";
16
16
  import { GeminiWorker } from "./gemini-worker.mjs";
17
+ import { runHeadlessWithCleanup } from "../team/headless.mjs";
17
18
 
18
19
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
20
 
@@ -321,6 +322,18 @@ const DelegateInputSchema = z.object({
321
322
  teamAgentName: z.string().optional().describe("TFX_TEAM_AGENT_NAME"),
322
323
  teamLeadName: z.string().optional().describe("TFX_TEAM_LEAD_NAME"),
323
324
  hubUrl: z.string().optional().describe("TFX_HUB_URL"),
325
+ workers: z
326
+ .array(
327
+ z.object({
328
+ provider: z.enum(["codex", "gemini"]).describe("워커 provider"),
329
+ agentType: z.string().default("executor").describe("역할명"),
330
+ prompt: z.string().describe("워커별 프롬프트"),
331
+ mcpProfile: z.string().default("auto").describe("MCP 프로필"),
332
+ model: z.string().optional().describe("모델 오버라이드"),
333
+ }),
334
+ )
335
+ .optional()
336
+ .describe("병렬 멀티워커 목록. 지정 시 psmux 기반 병렬 실행"),
324
337
  });
325
338
 
326
339
  const DelegateStatusInputSchema = z.object({
@@ -341,7 +354,7 @@ const DelegateOutputSchema = z.object({
341
354
  jobId: z.string().optional(),
342
355
  job_id: z.string().optional(),
343
356
  mode: z.enum(["sync", "async"]).optional(),
344
- status: z.enum(["running", "completed", "failed"]).optional(),
357
+ status: z.enum(["running", "completed", "failed", "partial"]).optional(),
345
358
  error: z.string().optional(),
346
359
  providerRequested: z.string().optional(),
347
360
  providerResolved: z.string().nullable().optional(),
@@ -357,6 +370,20 @@ const DelegateOutputSchema = z.object({
357
370
  threadId: z.string().nullable().optional(),
358
371
  sessionKey: z.string().nullable().optional(),
359
372
  conversationOpen: z.boolean().optional(),
373
+ workerResults: z
374
+ .array(
375
+ z.object({
376
+ cli: z.string(),
377
+ role: z.string().optional(),
378
+ paneName: z.string(),
379
+ matched: z.boolean(),
380
+ exitCode: z.number().nullable(),
381
+ output: z.string(),
382
+ }),
383
+ )
384
+ .optional()
385
+ .describe("멀티워커 개별 결과"),
386
+ sessionName: z.string().optional().describe("psmux 세션명"),
360
387
  });
361
388
 
362
389
  function isTeamRouteRequested(args) {
@@ -791,6 +818,23 @@ export class DelegatorMcpWorker {
791
818
  "위임 실행을 시작합니다.",
792
819
  );
793
820
 
821
+ // 멀티워커 분기: workers 배열이 있으면 headless psmux 병렬 실행
822
+ if (Array.isArray(args.workers) && args.workers.length > 0) {
823
+ try {
824
+ const result = await this._executeMultiWorker(args, extra);
825
+ await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, "위임이 완료되었습니다.");
826
+ return result;
827
+ } catch (error) {
828
+ const message = error instanceof Error ? error.message : String(error);
829
+ return createErrorPayload(message, {
830
+ mode: "sync",
831
+ providerRequested: "multi",
832
+ agentType: args.agentType,
833
+ transport: "headless-psmux",
834
+ });
835
+ }
836
+ }
837
+
794
838
  const runViaRoute = this._shouldUseRoute(args);
795
839
 
796
840
  try {
@@ -816,6 +860,90 @@ export class DelegatorMcpWorker {
816
860
  }
817
861
  }
818
862
 
863
+ async _executeMultiWorker(args, extra) {
864
+ await emitProgress(
865
+ extra,
866
+ DIRECT_PROGRESS_START,
867
+ 100,
868
+ "멀티워커 psmux 병렬 실행을 시작합니다.",
869
+ );
870
+
871
+ // workers 배열을 headless assignments 형식으로 변환
872
+ const assignments = args.workers.map((w) => {
873
+ // provider를 CLI 이름으로 매핑 (codex/gemini 그대로)
874
+ const cli = w.provider;
875
+ const role = w.agentType || "executor";
876
+ // buildDirectPrompt와 동일한 context 처리
877
+ const prompt = withContext(String(w.prompt || ""), args.contextFile);
878
+ return { cli, prompt, role };
879
+ });
880
+
881
+ // 타임아웃: 워커 중 가장 긴 타임아웃 사용
882
+ const maxTimeoutSec = Math.max(
883
+ ...args.workers.map((w) =>
884
+ Math.ceil(resolveTimeoutMs(w.agentType || "executor", args.timeoutMs) / 1000),
885
+ ),
886
+ 300,
887
+ );
888
+
889
+ try {
890
+ const { results, sessionName } = await runHeadlessWithCleanup(assignments, {
891
+ sessionPrefix: "dlg",
892
+ timeoutSec: maxTimeoutSec,
893
+ layout: assignments.length <= 2 ? "even-horizontal" : "2x2",
894
+ progressive: true,
895
+ dashboard: true,
896
+ dashboardLayout: "single",
897
+ });
898
+
899
+ await emitProgress(
900
+ extra,
901
+ DIRECT_PROGRESS_DONE,
902
+ 100,
903
+ `멀티워커 실행 완료: ${results.length}개 워커`,
904
+ );
905
+
906
+ // 전체 성공 판단: 모든 워커가 matched && exitCode === 0
907
+ const allOk = results.every((r) => r.matched && r.exitCode === 0);
908
+ // 개별 출력을 합산
909
+ const combinedOutput = results
910
+ .map(
911
+ (r, i) =>
912
+ `=== Worker ${i + 1} (${r.cli}/${assignments[i].role}) ===\n${r.output || "(no output)"}`,
913
+ )
914
+ .join("\n\n");
915
+
916
+ return {
917
+ ok: allOk,
918
+ mode: "sync",
919
+ status: allOk ? "completed" : "partial",
920
+ providerRequested: "multi",
921
+ providerResolved: "multi",
922
+ agentType: args.agentType || "executor",
923
+ transport: "headless-psmux",
924
+ exitCode: allOk ? 0 : 1,
925
+ output: combinedOutput,
926
+ workerResults: results.map((r) => ({
927
+ cli: r.cli,
928
+ role: r.role || "",
929
+ paneName: r.paneName,
930
+ matched: r.matched,
931
+ exitCode: r.exitCode,
932
+ output: r.output || "",
933
+ })),
934
+ sessionName,
935
+ };
936
+ } catch (error) {
937
+ const message = error instanceof Error ? error.message : String(error);
938
+ return createErrorPayload(message, {
939
+ mode: "sync",
940
+ providerRequested: "multi",
941
+ agentType: args.agentType || "executor",
942
+ transport: "headless-psmux",
943
+ });
944
+ }
945
+ }
946
+
819
947
  async _executeWorker(args, extra) {
820
948
  await emitProgress(
821
949
  extra,
@@ -2,7 +2,8 @@
2
2
  // ADR-006: --output-format stream-json 기반 단발 실행 워커.
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import { isAbsolute } from "node:path";
5
+ import { existsSync } from "node:fs";
6
+ import { extname } from "node:path";
6
7
  import readline from "node:readline";
7
8
 
8
9
  import { extractText, terminateChild, withRetry } from "./worker-utils.mjs";
@@ -15,6 +16,51 @@ function toStringList(value) {
15
16
  return value.map((item) => String(item ?? "").trim()).filter(Boolean);
16
17
  }
17
18
 
19
+ /**
20
+ * Windows에서 cmd.exe에 전달할 인자를 안전하게 quoting한다.
21
+ * 빈 문자열은 ""로, 특수문자 포함 시 큰따옴표로 감싼다.
22
+ */
23
+ function quoteWindowsCmdArg(value) {
24
+ const raw = String(value ?? "");
25
+ if (raw.length === 0) return '""';
26
+ if (!/[\s"()^&|<>%!]/.test(raw)) return raw;
27
+ const escaped = raw
28
+ .replace(/%/g, "%%")
29
+ .replace(/(\\*)"/g, '$1$1\\"')
30
+ .replace(/(\\+)$/g, "$1$1");
31
+ return `"${escaped}"`;
32
+ }
33
+
34
+ /**
35
+ * Windows npm shim(.cmd) spawn 문제를 해결한다.
36
+ * - command -v가 확장자 없는 경로를 반환하면 .exe → .cmd → .bat 순으로 탐색
37
+ * - .cmd/.bat는 CVE-2024-27980 이후 shell:false에서 실행 불가하므로 cmd.exe /d /s /c 경유
38
+ * - .exe는 직접 실행
39
+ * - non-Windows는 그대로 통과
40
+ */
41
+ function buildSpawnSpec(command, args) {
42
+ if (process.platform !== "win32") {
43
+ return { command, args, shell: false };
44
+ }
45
+
46
+ let resolved = command;
47
+ if (!extname(resolved)) {
48
+ for (const ext of [".exe", ".cmd", ".bat"]) {
49
+ if (existsSync(resolved + ext)) {
50
+ resolved = resolved + ext;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (/\.(cmd|bat)$/i.test(resolved)) {
57
+ const line = [resolved, ...args].map(quoteWindowsCmdArg).join(" ");
58
+ return { command: "cmd.exe", args: ["/d", "/s", "/c", line], shell: false };
59
+ }
60
+
61
+ return { command: resolved, args, shell: false };
62
+ }
63
+
18
64
  function safeJsonParse(line) {
19
65
  try {
20
66
  return JSON.parse(line);
@@ -223,12 +269,17 @@ export class GeminiWorker {
223
269
  }),
224
270
  ];
225
271
 
226
- const child = spawn(this.command, args, {
272
+ const {
273
+ command: spawnCmd,
274
+ args: spawnArgs,
275
+ shell,
276
+ } = buildSpawnSpec(this.command, args);
277
+ const child = spawn(spawnCmd, spawnArgs, {
227
278
  cwd: options.cwd || this.cwd,
228
279
  env: { ...this.env, ...(options.env || {}) },
229
280
  stdio: ["pipe", "pipe", "pipe"],
230
281
  windowsHide: true,
231
- shell: process.platform === "win32" && !isAbsolute(this.command),
282
+ shell,
232
283
  });
233
284
 
234
285
  this.child = child;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.22",
3
+ "version": "10.9.24",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,28 +36,12 @@ const PROFILE_DEFINITIONS = Object.freeze({
36
36
  }),
37
37
  }),
38
38
  executor: Object.freeze({
39
- description: "구현 워커용. 문서/검색/브라우징 보조 MCP 허용",
40
- allowedServers: Object.freeze([
41
- "context7",
42
- "playwright",
43
- "brave-search",
44
- "tavily",
45
- "exa",
46
- ]),
39
+ description: "구현 워커용. 문서 조회 전용 MCP 허용 (검색/브라우징은 Codex stall 유발)",
40
+ allowedServers: Object.freeze(["context7"]),
47
41
  alwaysOnServers: Object.freeze(["context7"]),
48
- maxSearchServers: 2,
42
+ maxSearchServers: 0,
49
43
  allowedToolsByServer: Object.freeze({
50
44
  context7: Object.freeze(["resolve-library-id", "query-docs"]),
51
- "brave-search": Object.freeze(["brave_web_search", "brave_news_search"]),
52
- exa: Object.freeze(["web_search_exa", "get_code_context_exa"]),
53
- tavily: Object.freeze(["tavily_search", "tavily_extract"]),
54
- playwright: Object.freeze([
55
- "browser_navigate",
56
- "browser_navigate_back",
57
- "browser_snapshot",
58
- "browser_take_screenshot",
59
- "browser_wait_for",
60
- ]),
61
45
  }),
62
46
  }),
63
47
  designer: Object.freeze({
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
9
9
  const FACTORY_CANDIDATES = [
10
+ resolve(process.cwd(), "hub/workers/factory.mjs"),
10
11
  resolve(SCRIPT_DIR, "../hub/workers/factory.mjs"),
11
12
  resolve(SCRIPT_DIR, "./hub/workers/factory.mjs"),
12
13
  ];