triflux 5.1.2 → 5.2.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.
- package/hub/team/cli/commands/start/index.mjs +6 -3
- package/hub/team/cli/commands/start/start-headless.mjs +76 -0
- package/hub/team/cli/services/runtime-mode.mjs +1 -0
- package/hub/team/headless.mjs +146 -0
- package/hub/team/psmux.mjs +48 -8
- package/package.json +1 -1
- package/scripts/lib/mcp-filter.mjs +4 -1
- package/scripts/tfx-route.sh +15 -9
- package/skills/tfx-multi/SKILL.md +29 -3
|
@@ -8,6 +8,7 @@ import { fail, ok, warn } from "../../render.mjs";
|
|
|
8
8
|
import { parseTeamArgs } from "./parse-args.mjs";
|
|
9
9
|
import { startInProcessTeam } from "./start-in-process.mjs";
|
|
10
10
|
import { startMuxTeam } from "./start-mux.mjs";
|
|
11
|
+
import { startHeadlessTeam } from "./start-headless.mjs";
|
|
11
12
|
import { startWtTeam } from "./start-wt.mjs";
|
|
12
13
|
|
|
13
14
|
function printStartUsage() {
|
|
@@ -68,9 +69,11 @@ export async function teamStart(args = []) {
|
|
|
68
69
|
|
|
69
70
|
const state = effectiveMode === "in-process"
|
|
70
71
|
? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
|
|
71
|
-
: effectiveMode === "
|
|
72
|
-
? await
|
|
73
|
-
:
|
|
72
|
+
: effectiveMode === "headless"
|
|
73
|
+
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout })
|
|
74
|
+
: effectiveMode === "wt"
|
|
75
|
+
? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
|
|
76
|
+
: await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
|
|
74
77
|
|
|
75
78
|
if (!state) return fail("in-process supervisor 시작 실패");
|
|
76
79
|
saveTeamState(state);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BOLD, DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
|
|
2
|
+
import { runHeadless } from "../../../headless.mjs";
|
|
3
|
+
import { killPsmuxSession } from "../../../psmux.mjs";
|
|
4
|
+
import { ok, warn } from "../../render.mjs";
|
|
5
|
+
import { buildTasks } from "../../services/task-model.mjs";
|
|
6
|
+
|
|
7
|
+
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout }) {
|
|
8
|
+
console.log(` ${AMBER}모드: headless (psmux 헤드리스 CLI 실행)${RESET}`);
|
|
9
|
+
|
|
10
|
+
const assignments = subtasks.map((subtask, i) => ({
|
|
11
|
+
cli: agents[i],
|
|
12
|
+
prompt: subtask,
|
|
13
|
+
role: `worker-${i + 1}`,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
ok("헤드리스 실행 시작...");
|
|
17
|
+
const { sessionName, results } = await runHeadless(sessionId, assignments, {
|
|
18
|
+
timeoutSec: 300,
|
|
19
|
+
layout,
|
|
20
|
+
onProgress(event) {
|
|
21
|
+
if (event.type === "dispatched") {
|
|
22
|
+
console.log(` ${DIM}[${event.paneName}] ${event.cli} dispatch${RESET}`);
|
|
23
|
+
} else if (event.type === "completed") {
|
|
24
|
+
const icon = event.matched && event.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
|
|
25
|
+
console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (session dead)" : ""}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 결과 요약
|
|
31
|
+
const succeeded = results.filter((r) => r.matched && r.exitCode === 0);
|
|
32
|
+
const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
|
|
33
|
+
|
|
34
|
+
console.log(`\n ${GREEN}${BOLD}헤드리스 실행 완료${RESET}`);
|
|
35
|
+
console.log(` ${DIM}성공: ${succeeded.length} / 실패: ${failed.length} / 전체: ${results.length}${RESET}`);
|
|
36
|
+
|
|
37
|
+
if (failed.length > 0) {
|
|
38
|
+
warn("실패 워커:");
|
|
39
|
+
for (const r of failed) {
|
|
40
|
+
console.log(` ${r.paneName} (${r.cli}): exit=${r.exitCode}${r.sessionDead ? " session dead" : ""}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 결과 출력 (각 워커의 output 요약)
|
|
45
|
+
for (const r of results) {
|
|
46
|
+
if (r.output) {
|
|
47
|
+
const preview = r.output.length > 200 ? `${r.output.slice(0, 200)}…` : r.output;
|
|
48
|
+
console.log(`\n ${DIM}── ${r.paneName} (${r.cli}) ──${RESET}`);
|
|
49
|
+
console.log(` ${preview}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 세션 정리
|
|
54
|
+
try { killPsmuxSession(sessionName); } catch { /* already cleaned */ }
|
|
55
|
+
|
|
56
|
+
const members = [
|
|
57
|
+
{ role: "lead", name: "lead", cli: lead, pane: `${sessionName}:0.0` },
|
|
58
|
+
...results.map((r, i) => ({ role: "worker", name: r.paneName, cli: r.cli, pane: `${sessionName}:0.${i + 1}`, subtask: subtasks[i] })),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
sessionName,
|
|
63
|
+
task,
|
|
64
|
+
lead,
|
|
65
|
+
agents,
|
|
66
|
+
layout,
|
|
67
|
+
teammateMode: "headless",
|
|
68
|
+
startedAt: Date.now(),
|
|
69
|
+
members,
|
|
70
|
+
headlessResults: results,
|
|
71
|
+
tasks: buildTasks(subtasks, members.filter((m) => m.role === "worker")),
|
|
72
|
+
postSave() {
|
|
73
|
+
console.log(`\n ${DIM}세션 자동 정리 완료. 결과는 위에 표시됨.${RESET}\n`);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
export function normalizeTeammateMode(mode = "auto") {
|
|
9
9
|
const raw = String(mode).toLowerCase();
|
|
10
10
|
if (raw === "inline" || raw === "native") return "in-process";
|
|
11
|
+
if (raw === "headless" || raw === "hl") return "headless";
|
|
11
12
|
if (raw === "in-process" || raw === "tmux" || raw === "wt" || raw === "psmux") return raw;
|
|
12
13
|
if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
|
|
13
14
|
if (raw === "auto") {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// hub/team/headless.mjs — 헤드리스 CLI 오케스트레이션
|
|
2
|
+
// psmux pane에서 CLI를 헤드리스 모드로 실행하고 결과를 수집한다.
|
|
3
|
+
// 의존성: psmux.mjs (Node.js 내장 모듈만 사용)
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import {
|
|
8
|
+
createPsmuxSession,
|
|
9
|
+
killPsmuxSession,
|
|
10
|
+
psmuxSessionExists,
|
|
11
|
+
dispatchCommand,
|
|
12
|
+
waitForCompletion,
|
|
13
|
+
capturePsmuxPane,
|
|
14
|
+
} from "./psmux.mjs";
|
|
15
|
+
|
|
16
|
+
const RESULT_DIR = join(tmpdir(), "tfx-headless");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CLI별 헤드리스 명령 빌더
|
|
20
|
+
* @param {'codex'|'gemini'|'claude'} cli
|
|
21
|
+
* @param {string} prompt — 실행할 프롬프트
|
|
22
|
+
* @param {string} resultFile — 결과 저장 파일 경로
|
|
23
|
+
* @returns {string} PowerShell 명령
|
|
24
|
+
*/
|
|
25
|
+
export function buildHeadlessCommand(cli, prompt, resultFile) {
|
|
26
|
+
// 프롬프트의 단일 인용부호를 이스케이프
|
|
27
|
+
const escaped = prompt.replace(/'/g, "''");
|
|
28
|
+
|
|
29
|
+
switch (cli) {
|
|
30
|
+
case "codex":
|
|
31
|
+
return `codex exec '${escaped}' -o '${resultFile}' --color never`;
|
|
32
|
+
case "gemini":
|
|
33
|
+
return `gemini -p '${escaped}' -o text > '${resultFile}' 2>&1`;
|
|
34
|
+
case "claude":
|
|
35
|
+
return `claude -p '${escaped}' --output-format text > '${resultFile}' 2>&1`;
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`지원하지 않는 CLI: ${cli}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 결과 파일 읽기 (없으면 capture-pane fallback)
|
|
43
|
+
* @param {string} resultFile
|
|
44
|
+
* @param {string} sessionName
|
|
45
|
+
* @param {string} paneName
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function readResult(resultFile, paneId) {
|
|
49
|
+
if (existsSync(resultFile)) {
|
|
50
|
+
return readFileSync(resultFile, "utf8").trim();
|
|
51
|
+
}
|
|
52
|
+
// fallback: capture-pane (paneId = "tfx:0.1" 형태)
|
|
53
|
+
return capturePsmuxPane(paneId, 30);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 헤드리스 CLI 오케스트레이션 실행
|
|
58
|
+
*
|
|
59
|
+
* @param {string} sessionName — psmux 세션 이름
|
|
60
|
+
* @param {Array<{cli: string, prompt: string, role?: string}>} assignments
|
|
61
|
+
* @param {object} [opts]
|
|
62
|
+
* @param {number} [opts.timeoutSec=300] — 각 워커 타임아웃
|
|
63
|
+
* @param {string} [opts.layout='2x2'] — pane 레이아웃
|
|
64
|
+
* @param {(event: object) => void} [opts.onProgress] — 진행 콜백
|
|
65
|
+
* @returns {{ sessionName: string, results: Array<{cli: string, paneName: string, matched: boolean, exitCode: number|null, output: string, sessionDead?: boolean}> }}
|
|
66
|
+
*/
|
|
67
|
+
export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
68
|
+
const {
|
|
69
|
+
timeoutSec = 300,
|
|
70
|
+
layout = "2x2",
|
|
71
|
+
onProgress,
|
|
72
|
+
} = opts;
|
|
73
|
+
|
|
74
|
+
mkdirSync(RESULT_DIR, { recursive: true });
|
|
75
|
+
const paneCount = assignments.length + 1; // +1 for lead pane (unused but reserved)
|
|
76
|
+
const session = createPsmuxSession(sessionName, { layout, paneCount });
|
|
77
|
+
|
|
78
|
+
if (onProgress) onProgress({ type: "session_created", sessionName, panes: session.panes });
|
|
79
|
+
|
|
80
|
+
// 각 워커 pane에 헤드리스 명령 dispatch
|
|
81
|
+
const dispatches = assignments.map((assignment, i) => {
|
|
82
|
+
const paneName = `worker-${i + 1}`;
|
|
83
|
+
const resultFile = join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
|
|
84
|
+
const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile);
|
|
85
|
+
const dispatch = dispatchCommand(sessionName, paneName, cmd);
|
|
86
|
+
|
|
87
|
+
if (onProgress) onProgress({ type: "dispatched", paneName, cli: assignment.cli });
|
|
88
|
+
|
|
89
|
+
return { ...dispatch, paneName, resultFile, cli: assignment.cli, role: assignment.role };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 병렬 대기 (Promise.all — 모든 pane 동시 폴링, 총 시간 = max(개별 시간))
|
|
93
|
+
const results = await Promise.all(dispatches.map(async (d) => {
|
|
94
|
+
const completion = await waitForCompletion(sessionName, d.paneName, d.token, timeoutSec);
|
|
95
|
+
|
|
96
|
+
const output = completion.matched
|
|
97
|
+
? readResult(d.resultFile, d.paneId)
|
|
98
|
+
: "";
|
|
99
|
+
|
|
100
|
+
if (onProgress) {
|
|
101
|
+
onProgress({
|
|
102
|
+
type: "completed",
|
|
103
|
+
paneName: d.paneName,
|
|
104
|
+
cli: d.cli,
|
|
105
|
+
matched: completion.matched,
|
|
106
|
+
exitCode: completion.exitCode,
|
|
107
|
+
sessionDead: completion.sessionDead || false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
cli: d.cli,
|
|
113
|
+
paneName: d.paneName,
|
|
114
|
+
role: d.role,
|
|
115
|
+
matched: completion.matched,
|
|
116
|
+
exitCode: completion.exitCode,
|
|
117
|
+
output,
|
|
118
|
+
sessionDead: completion.sessionDead || false,
|
|
119
|
+
};
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
return { sessionName, results };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 헤드리스 실행 + 자동 정리
|
|
127
|
+
* 성공/실패에 관계없이 세션을 정리한다.
|
|
128
|
+
*
|
|
129
|
+
* @param {Array<{cli: string, prompt: string, role?: string}>} assignments
|
|
130
|
+
* @param {object} [opts] — runHeadless opts + sessionPrefix
|
|
131
|
+
* @returns {{ results: Array, sessionName: string }}
|
|
132
|
+
*/
|
|
133
|
+
export async function runHeadlessWithCleanup(assignments, opts = {}) {
|
|
134
|
+
const { sessionPrefix = "tfx-hl", ...runOpts } = opts;
|
|
135
|
+
const sessionName = `${sessionPrefix}-${Date.now().toString(36).slice(-6)}`;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
return await runHeadless(sessionName, assignments, runOpts);
|
|
139
|
+
} finally {
|
|
140
|
+
try {
|
|
141
|
+
killPsmuxSession(sessionName);
|
|
142
|
+
} catch {
|
|
143
|
+
// 이미 종료된 세션 — 무시
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -37,6 +37,10 @@ function sleepMs(ms) {
|
|
|
37
37
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function sleepMsAsync(ms) {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
function tokenizeCommand(command) {
|
|
41
45
|
const source = String(command || "").trim();
|
|
42
46
|
if (!source) return [];
|
|
@@ -639,9 +643,27 @@ export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
|
|
|
639
643
|
* @param {number} timeoutSec
|
|
640
644
|
* @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null }}
|
|
641
645
|
*/
|
|
642
|
-
export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSec = 300) {
|
|
646
|
+
export async function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSec = 300) {
|
|
643
647
|
ensurePsmuxInstalled();
|
|
644
|
-
|
|
648
|
+
|
|
649
|
+
// E4 크래시 복구: 초기 resolvePane도 세션 사망을 감지
|
|
650
|
+
let pane;
|
|
651
|
+
try {
|
|
652
|
+
pane = resolvePane(sessionName, paneNameOrTarget);
|
|
653
|
+
} catch (resolveError) {
|
|
654
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
655
|
+
return {
|
|
656
|
+
matched: false,
|
|
657
|
+
paneId: "",
|
|
658
|
+
paneName: String(paneNameOrTarget),
|
|
659
|
+
logPath: "",
|
|
660
|
+
match: null,
|
|
661
|
+
sessionDead: true,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
throw resolveError; // 세션은 살아있지만 pane을 못 찾음 → 원래 에러 전파
|
|
665
|
+
}
|
|
666
|
+
|
|
645
667
|
const paneName = pane.title || paneNameOrTarget;
|
|
646
668
|
const logPath = getCaptureLogPath(sessionName, paneName);
|
|
647
669
|
if (!existsSync(logPath)) {
|
|
@@ -652,7 +674,23 @@ export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSe
|
|
|
652
674
|
const regex = toPatternRegExp(pattern);
|
|
653
675
|
|
|
654
676
|
while (Date.now() <= deadline) {
|
|
655
|
-
|
|
677
|
+
// E4 크래시 복구: capture 실패 시 세션 생존 체크
|
|
678
|
+
try {
|
|
679
|
+
refreshCaptureSnapshot(sessionName, pane.paneId);
|
|
680
|
+
} catch {
|
|
681
|
+
if (!psmuxSessionExists(sessionName)) {
|
|
682
|
+
return {
|
|
683
|
+
matched: false,
|
|
684
|
+
paneId: pane.paneId,
|
|
685
|
+
paneName,
|
|
686
|
+
logPath,
|
|
687
|
+
match: null,
|
|
688
|
+
sessionDead: true,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
// 일시적 오류 — 다음 폴링에서 재시도
|
|
692
|
+
}
|
|
693
|
+
|
|
656
694
|
const content = readCaptureLog(logPath);
|
|
657
695
|
const match = regex.exec(content);
|
|
658
696
|
if (match) {
|
|
@@ -668,7 +706,7 @@ export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSe
|
|
|
668
706
|
if (Date.now() > deadline) {
|
|
669
707
|
break;
|
|
670
708
|
}
|
|
671
|
-
|
|
709
|
+
await sleepMsAsync(POLL_INTERVAL_MS);
|
|
672
710
|
}
|
|
673
711
|
|
|
674
712
|
return {
|
|
@@ -688,12 +726,12 @@ export function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSe
|
|
|
688
726
|
* @param {number} timeoutSec
|
|
689
727
|
* @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null, token: string, exitCode: number|null }}
|
|
690
728
|
*/
|
|
691
|
-
export function waitForCompletion(sessionName, paneNameOrTarget, token, timeoutSec = 300) {
|
|
729
|
+
export async function waitForCompletion(sessionName, paneNameOrTarget, token, timeoutSec = 300) {
|
|
692
730
|
const completionRegex = new RegExp(
|
|
693
731
|
`${escapeRegExp(COMPLETION_PREFIX)}${escapeRegExp(token)}:(\\d+)`,
|
|
694
732
|
"m",
|
|
695
733
|
);
|
|
696
|
-
const result = waitForPattern(sessionName, paneNameOrTarget, completionRegex, timeoutSec);
|
|
734
|
+
const result = await waitForPattern(sessionName, paneNameOrTarget, completionRegex, timeoutSec);
|
|
697
735
|
const exitMatch = result.match ? completionRegex.exec(result.match) : null;
|
|
698
736
|
return {
|
|
699
737
|
...result,
|
|
@@ -840,6 +878,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
|
840
878
|
// ─── CLI 진입점 ───
|
|
841
879
|
|
|
842
880
|
if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
881
|
+
(async () => {
|
|
843
882
|
const [, , cmd, ...args] = process.argv;
|
|
844
883
|
|
|
845
884
|
// CLI 인자 파싱 헬퍼
|
|
@@ -922,7 +961,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
922
961
|
console.error("사용법: node psmux.mjs wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]");
|
|
923
962
|
process.exit(1);
|
|
924
963
|
}
|
|
925
|
-
const result = waitForPattern(session, name, pattern, timeoutSec);
|
|
964
|
+
const result = await waitForPattern(session, name, pattern, timeoutSec);
|
|
926
965
|
console.log(JSON.stringify(result, null, 2));
|
|
927
966
|
if (!result.matched) process.exit(2);
|
|
928
967
|
break;
|
|
@@ -936,7 +975,7 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
936
975
|
console.error("사용법: node psmux.mjs wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]");
|
|
937
976
|
process.exit(1);
|
|
938
977
|
}
|
|
939
|
-
const result = waitForCompletion(session, name, token, timeoutSec);
|
|
978
|
+
const result = await waitForCompletion(session, name, token, timeoutSec);
|
|
940
979
|
console.log(JSON.stringify(result, null, 2));
|
|
941
980
|
if (!result.matched) process.exit(2);
|
|
942
981
|
break;
|
|
@@ -958,4 +997,5 @@ if (process.argv[1] && process.argv[1].endsWith("psmux.mjs")) {
|
|
|
958
997
|
console.error(`오류: ${err.message}`);
|
|
959
998
|
process.exit(1);
|
|
960
999
|
}
|
|
1000
|
+
})();
|
|
961
1001
|
}
|
package/package.json
CHANGED
|
@@ -176,7 +176,10 @@ function normalizeProfileName(profile) {
|
|
|
176
176
|
if (raw === 'auto') return raw;
|
|
177
177
|
if (PROFILE_DEFINITIONS[raw]) return raw;
|
|
178
178
|
if (LEGACY_PROFILE_ALIASES[raw]) return LEGACY_PROFILE_ALIASES[raw];
|
|
179
|
-
|
|
179
|
+
// graceful fallback: --flag나 잘못된 프로필 → 'auto'로 폴백 (hard crash 방지)
|
|
180
|
+
if (raw.startsWith('-') || raw.startsWith('/')) return 'auto';
|
|
181
|
+
console.error(`[mcp-filter] 경고: 알 수 없는 프로필 '${raw}', 'auto'로 폴백`);
|
|
182
|
+
return 'auto';
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
function resolveAutoProfile(agentType = '') {
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -121,14 +121,7 @@ MCP_PROFILE="${3:-auto}"
|
|
|
121
121
|
USER_TIMEOUT="${4:-}"
|
|
122
122
|
CONTEXT_FILE="${5:-}"
|
|
123
123
|
|
|
124
|
-
# ──
|
|
125
|
-
case "$AGENT_TYPE" in
|
|
126
|
-
codex|gemini|claude|claude-native)
|
|
127
|
-
echo "ERROR: '$AGENT_TYPE'는 CLI 이름이지 에이전트 역할이 아닙니다." >&2
|
|
128
|
-
echo "올바른 사용법: TFX_CLI_MODE=$AGENT_TYPE bash tfx-route.sh <역할> \"프롬프트\"" >&2
|
|
129
|
-
echo "사용 가능한 역할: executor, code-reviewer, scientist, designer, architect, verifier 등" >&2
|
|
130
|
-
exit 64 ;;
|
|
131
|
-
esac
|
|
124
|
+
# ── CLI 이름은 route_agent()에서 기본 역할 alias로 처리됨 (codex→executor, gemini→designer, claude→explore) ──
|
|
132
125
|
|
|
133
126
|
# ── 인자 검증: MCP_PROFILE이 --flag 형태인 경우 거절 ──
|
|
134
127
|
if [[ "$MCP_PROFILE" == --* ]]; then
|
|
@@ -668,11 +661,24 @@ route_agent() {
|
|
|
668
661
|
CLI_TYPE="codex"; CLI_CMD="codex"
|
|
669
662
|
CLI_ARGS="exec --profile spark_fast ${codex_base}"
|
|
670
663
|
CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
664
|
+
# ─── CLI 이름 alias (사용자 편의) ───
|
|
665
|
+
codex)
|
|
666
|
+
CLI_TYPE="codex"; CLI_CMD="codex"
|
|
667
|
+
CLI_ARGS="exec ${codex_base}"
|
|
668
|
+
CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
669
|
+
gemini)
|
|
670
|
+
CLI_TYPE="gemini"; CLI_CMD="gemini"
|
|
671
|
+
CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
|
|
672
|
+
CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
673
|
+
claude)
|
|
674
|
+
CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
|
|
675
|
+
CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
671
676
|
*)
|
|
672
677
|
echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
|
|
673
678
|
echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
|
|
674
679
|
echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
|
|
675
|
-
echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
|
|
680
|
+
echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark," >&2
|
|
681
|
+
echo " codex, gemini, claude (CLI alias)" >&2
|
|
676
682
|
exit 1 ;;
|
|
677
683
|
esac
|
|
678
684
|
}
|
|
@@ -148,10 +148,36 @@ status는 "completed"만 사용. 실패 여부는 `metadata.result`로 구분.
|
|
|
148
148
|
3. 실패 시 `forceCleanupTeam(teamName)` → 그래도 실패 시 `rm -rf ~/.claude/teams/{teamName}/` 안내
|
|
149
149
|
4. 종합 보고서 출력
|
|
150
150
|
|
|
151
|
-
### Phase 3-mux:
|
|
151
|
+
### Phase 3-mux: psmux 헤드리스 모드
|
|
152
152
|
|
|
153
|
-
`--tmux`/`--psmux` 시 pane 기반 실행.
|
|
154
|
-
|
|
153
|
+
`--tmux`/`--psmux` 시 pane 기반 헤드리스 실행. Agent 래퍼 없이 Lead가 직접 CLI를 제어하여 토큰 76-89% 절감.
|
|
154
|
+
|
|
155
|
+
**핵심 프리미티브** (`hub/team/psmux.mjs`):
|
|
156
|
+
- `createPsmuxSession(name, {layout, paneCount})` — 세션 + pane 분할
|
|
157
|
+
- `dispatchCommand(session, paneName, cmd)` → `{token, paneId, logPath}`
|
|
158
|
+
- `waitForCompletion(session, paneName, token, timeoutSec)` → `{matched, exitCode, sessionDead?}`
|
|
159
|
+
- 완료 마커: `__TRIFLUX_DONE__:token:exitCode` (PowerShell 래핑)
|
|
160
|
+
- pane 이름: `"lead"` → index 0, `"worker-N"` → index N (대소문자 무관)
|
|
161
|
+
|
|
162
|
+
**헤드리스 오케스트레이션** (`hub/team/headless.mjs`):
|
|
163
|
+
```
|
|
164
|
+
import { runHeadlessWithCleanup } from "hub/team/headless.mjs";
|
|
165
|
+
const { results } = runHeadlessWithCleanup([
|
|
166
|
+
{ cli: "codex", prompt: "코드 리뷰", role: "reviewer" },
|
|
167
|
+
{ cli: "gemini", prompt: "문서 작성", role: "writer" },
|
|
168
|
+
], { timeoutSec: 300 });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**CLI 헤드리스 명령 패턴:**
|
|
172
|
+
| CLI | 명령 | 출력 |
|
|
173
|
+
|-----|-------|------|
|
|
174
|
+
| Codex | `codex exec 'prompt' -o result.txt --color never` | 파일 |
|
|
175
|
+
| Gemini | `gemini -p 'prompt' -o text > result.txt` | 리다이렉트 |
|
|
176
|
+
| Claude | `claude -p 'prompt' --output-format text > result.txt` | 리다이렉트 |
|
|
177
|
+
|
|
178
|
+
**E4 크래시 복구:** `waitForCompletion`이 세션 사망 시 `{sessionDead: true}` 반환 (throw 대신).
|
|
179
|
+
|
|
180
|
+
**레거시 인터랙티브 모드:** `Bash("node {PKG_ROOT}/bin/triflux.mjs multi --no-attach --agents {agents} \\\"{task}\\\"")`
|
|
155
181
|
|
|
156
182
|
## 전제 조건
|
|
157
183
|
|