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.
package/hooks/keyword-rules.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
282
|
+
shell,
|
|
232
283
|
});
|
|
233
284
|
|
|
234
285
|
this.child = child;
|
package/package.json
CHANGED
|
@@ -36,28 +36,12 @@ const PROFILE_DEFINITIONS = Object.freeze({
|
|
|
36
36
|
}),
|
|
37
37
|
}),
|
|
38
38
|
executor: Object.freeze({
|
|
39
|
-
description: "구현 워커용.
|
|
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:
|
|
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
|
];
|