triflux 3.1.0-dev.5 → 3.2.0-dev.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.
package/bin/triflux.mjs CHANGED
@@ -826,6 +826,7 @@ ${updateNotice}
826
826
  ${DIM} --dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
827
827
  ${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
828
828
  ${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
829
+ ${WHITE_BRIGHT}tfx team${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
829
830
  ${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
830
831
  ${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
831
832
 
@@ -1060,6 +1061,12 @@ switch (cmd) {
1060
1061
  case "update": cmdUpdate(); break;
1061
1062
  case "list": case "ls": cmdList(); break;
1062
1063
  case "hub": cmdHub(); break;
1064
+ case "team": {
1065
+ const { pathToFileURL } = await import("node:url");
1066
+ const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
1067
+ await cmdTeam();
1068
+ break;
1069
+ }
1063
1070
  case "notion-read": case "nr": {
1064
1071
  const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
1065
1072
  const nrArgs = process.argv.slice(3).map(a => `"${a}"`).join(" ");
@@ -0,0 +1,368 @@
1
+ // hub/team/cli.mjs — tfx team CLI 진입점
2
+ // bin/triflux.mjs에서 import하여 사용
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { execSync, spawn } from "node:child_process";
7
+
8
+ import { createSession, attachSession, killSession, sessionExists, listSessions, capturePaneOutput } from "./session.mjs";
9
+ import { buildCliCommand, startCliInPane, injectPrompt, sendKeys } from "./pane.mjs";
10
+ import { orchestrate, decomposeTask } from "./orchestrator.mjs";
11
+ import { detectMultiplexer } from "./session.mjs";
12
+
13
+ // ── 상수 ──
14
+ const PKG_ROOT = dirname(dirname(dirname(new URL(import.meta.url).pathname))).replace(/^\/([A-Z]:)/, "$1");
15
+ const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
16
+ const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
17
+ const TEAM_STATE_FILE = join(HUB_PID_DIR, "team-state.json");
18
+
19
+ // ── 색상 ──
20
+ const AMBER = "\x1b[38;5;214m";
21
+ const GREEN = "\x1b[38;5;82m";
22
+ const RED = "\x1b[38;5;196m";
23
+ const GRAY = "\x1b[38;5;245m";
24
+ const DIM = "\x1b[2m";
25
+ const BOLD = "\x1b[1m";
26
+ const RESET = "\x1b[0m";
27
+ const WHITE = "\x1b[97m";
28
+ const YELLOW = "\x1b[33m";
29
+
30
+ function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
31
+ function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
32
+ function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
33
+
34
+ // ── 팀 상태 관리 ──
35
+
36
+ function loadTeamState() {
37
+ try {
38
+ return JSON.parse(readFileSync(TEAM_STATE_FILE, "utf8"));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function saveTeamState(state) {
45
+ mkdirSync(HUB_PID_DIR, { recursive: true });
46
+ writeFileSync(TEAM_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
47
+ }
48
+
49
+ function clearTeamState() {
50
+ try { unlinkSync(TEAM_STATE_FILE); } catch {}
51
+ }
52
+
53
+ // ── Hub 유틸 ──
54
+
55
+ function getHubInfo() {
56
+ if (!existsSync(HUB_PID_FILE)) return null;
57
+ try {
58
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
59
+ process.kill(info.pid, 0); // 프로세스 생존 확인
60
+ return info;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function startHubDaemon() {
67
+ const serverPath = join(PKG_ROOT, "hub", "server.mjs");
68
+ if (!existsSync(serverPath)) {
69
+ fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
70
+ return null;
71
+ }
72
+
73
+ const child = spawn(process.execPath, [serverPath], {
74
+ env: { ...process.env },
75
+ stdio: "ignore",
76
+ detached: true,
77
+ });
78
+ child.unref();
79
+
80
+ // PID 파일 확인 (최대 3초 대기)
81
+ const deadline = Date.now() + 3000;
82
+ while (Date.now() < deadline) {
83
+ if (existsSync(HUB_PID_FILE)) {
84
+ return JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
85
+ }
86
+ execSync('node -e "setTimeout(()=>{},100)"', { stdio: "ignore", timeout: 500 });
87
+ }
88
+ return null;
89
+ }
90
+
91
+ // ── 인자 파싱 ──
92
+
93
+ function parseTeamArgs() {
94
+ const args = process.argv.slice(3);
95
+ let agents = ["codex", "codex", "gemini"]; // 기본: codex x2 + gemini
96
+ let layout = "2x2";
97
+ let task = "";
98
+
99
+ for (let i = 0; i < args.length; i++) {
100
+ if (args[i] === "--agents" && args[i + 1]) {
101
+ agents = args[++i].split(",").map((s) => s.trim().toLowerCase());
102
+ } else if (args[i] === "--layout" && args[i + 1]) {
103
+ layout = args[++i];
104
+ } else if (!args[i].startsWith("-")) {
105
+ task = args[i];
106
+ }
107
+ }
108
+
109
+ return { agents, layout, task };
110
+ }
111
+
112
+ // ── 서브커맨드 ──
113
+
114
+ async function teamStart() {
115
+ // 1. tmux 확인
116
+ const mux = detectMultiplexer();
117
+ if (!mux) {
118
+ console.log(`
119
+ ${RED}${BOLD}tmux 미발견${RESET}
120
+
121
+ tfx team은 tmux가 필요합니다:
122
+ WSL2: ${WHITE}wsl sudo apt install tmux${RESET}
123
+ macOS: ${WHITE}brew install tmux${RESET}
124
+ Linux: ${WHITE}apt install tmux${RESET}
125
+
126
+ Windows에서는 WSL2를 권장합니다:
127
+ 1. ${WHITE}wsl --install${RESET}
128
+ 2. ${WHITE}wsl sudo apt install tmux${RESET}
129
+ 3. ${WHITE}tfx team "작업"${RESET}
130
+ `);
131
+ process.exit(1);
132
+ }
133
+
134
+ // 2. 인자 파싱
135
+ const { agents, layout, task } = parseTeamArgs();
136
+ if (!task) {
137
+ console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
138
+ console.log(` 사용법: ${WHITE}tfx team "작업 설명"${RESET}`);
139
+ console.log(` ${WHITE}tfx team --agents codex,gemini "작업"${RESET}`);
140
+ console.log(` ${WHITE}tfx team --layout 1x3 "작업"${RESET}\n`);
141
+ return;
142
+ }
143
+
144
+ // 3. Hub 확인 + lazy-start
145
+ console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
146
+ let hub = getHubInfo();
147
+ if (!hub) {
148
+ process.stdout.write(` Hub 시작 중...`);
149
+ hub = startHubDaemon();
150
+ if (hub) {
151
+ console.log(` ${GREEN}✓${RESET}`);
152
+ } else {
153
+ console.log(` ${RED}✗${RESET}`);
154
+ warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
155
+ // Hub 없이도 계속 진행 (통신만 불가)
156
+ }
157
+ } else {
158
+ ok(`Hub: ${DIM}${hub.url}${RESET}`);
159
+ }
160
+
161
+ // 4. 세션 ID 생성
162
+ const sessionId = `tfx-team-${Date.now().toString(36).slice(-4)}`;
163
+
164
+ // 5. 작업 분해
165
+ const subtasks = decomposeTask(task, agents.length);
166
+
167
+ console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
168
+ console.log(` 레이아웃: ${layout} (${agents.length + 1} panes)`);
169
+ console.log(` 에이전트: ${agents.map((a) => `${AMBER}${a}${RESET}`).join(", ")}`);
170
+ for (let i = 0; i < subtasks.length; i++) {
171
+ const preview = subtasks[i].length > 40 ? subtasks[i].slice(0, 40) + "…" : subtasks[i];
172
+ console.log(` ${DIM}[${agents[i]}] ${preview}${RESET}`);
173
+ }
174
+ console.log("");
175
+
176
+ // 6. tmux 세션 생성
177
+ const session = createSession(sessionId, {
178
+ layout,
179
+ paneCount: agents.length + 1, // +1 for dashboard
180
+ });
181
+
182
+ // 7. Dashboard 시작 (Pane 0)
183
+ const dashCmd = `node ${PKG_ROOT}/hub/team/dashboard.mjs --session ${sessionId} --interval 2`;
184
+ startCliInPane(session.panes[0], dashCmd);
185
+
186
+ // 8. CLI 에이전트 시작 (Pane 1~N)
187
+ const assignments = [];
188
+ for (let i = 0; i < agents.length; i++) {
189
+ const cli = agents[i];
190
+ const target = session.panes[i + 1];
191
+ const command = buildCliCommand(cli);
192
+ startCliInPane(target, command);
193
+ assignments.push({ target, cli, subtask: subtasks[i] });
194
+ }
195
+
196
+ // 9. CLI 초기화 대기 (3초 — interactive 모드 진입 시간)
197
+ ok("CLI 초기화 대기 (3초)...");
198
+ await new Promise((r) => setTimeout(r, 3000));
199
+
200
+ // 10. 프롬프트 주입
201
+ const hubUrl = hub?.url || "http://127.0.0.1:27888/mcp";
202
+ await orchestrate(sessionId, assignments, { hubUrl });
203
+ ok("프롬프트 주입 완료");
204
+
205
+ // 11. 팀 상태 저장
206
+ const panes = { [session.panes[0]]: { role: "dashboard" } };
207
+ for (let i = 0; i < agents.length; i++) {
208
+ panes[session.panes[i + 1]] = {
209
+ cli: agents[i],
210
+ agentId: `${agents[i]}-${session.panes[i + 1].split(".").pop()}`,
211
+ subtask: subtasks[i],
212
+ };
213
+ }
214
+ saveTeamState({
215
+ sessionName: sessionId,
216
+ agents,
217
+ task,
218
+ layout,
219
+ startedAt: Date.now(),
220
+ hubUrl,
221
+ panes,
222
+ });
223
+
224
+ // 12. tmux attach
225
+ console.log(`\n ${GREEN}${BOLD}팀 세션 준비 완료${RESET}`);
226
+ console.log(` ${DIM}Ctrl+B → 방향키로 pane 전환${RESET}`);
227
+ console.log(` ${DIM}Ctrl+B → D로 세션 분리 (백그라운드)${RESET}\n`);
228
+ attachSession(sessionId);
229
+ }
230
+
231
+ function teamStatus() {
232
+ const state = loadTeamState();
233
+ if (!state) {
234
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
235
+ return;
236
+ }
237
+
238
+ const alive = sessionExists(state.sessionName);
239
+ const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
240
+ const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
241
+
242
+ console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET} ${status}\n`);
243
+ console.log(` 세션: ${state.sessionName}`);
244
+ console.log(` 작업: ${state.task}`);
245
+ console.log(` 에이전트: ${state.agents.join(", ")}`);
246
+ console.log(` Uptime: ${uptime}`);
247
+ console.log("");
248
+ }
249
+
250
+ function teamAttach() {
251
+ const state = loadTeamState();
252
+ if (!state || !sessionExists(state.sessionName)) {
253
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
254
+ return;
255
+ }
256
+ attachSession(state.sessionName);
257
+ }
258
+
259
+ function teamStop() {
260
+ const state = loadTeamState();
261
+ if (!state) {
262
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
263
+ return;
264
+ }
265
+
266
+ if (sessionExists(state.sessionName)) {
267
+ killSession(state.sessionName);
268
+ ok(`세션 종료: ${state.sessionName}`);
269
+ } else {
270
+ console.log(` ${DIM}세션 이미 종료됨${RESET}`);
271
+ }
272
+
273
+ // 상태 파일 정리
274
+ clearTeamState();
275
+ console.log("");
276
+ }
277
+
278
+ function teamKill() {
279
+ // 모든 tfx-team- 세션 강제 종료
280
+ const sessions = listSessions();
281
+ if (sessions.length === 0) {
282
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
283
+ return;
284
+ }
285
+ for (const s of sessions) {
286
+ killSession(s);
287
+ ok(`종료: ${s}`);
288
+ }
289
+ clearTeamState();
290
+ console.log("");
291
+ }
292
+
293
+ function teamSend() {
294
+ const state = loadTeamState();
295
+ if (!state || !sessionExists(state.sessionName)) {
296
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
297
+ return;
298
+ }
299
+
300
+ const paneIdx = parseInt(process.argv[4], 10);
301
+ const message = process.argv.slice(5).join(" ");
302
+ if (isNaN(paneIdx) || !message) {
303
+ console.log(`\n 사용법: ${WHITE}tfx team send <pane번호> "메시지"${RESET}\n`);
304
+ return;
305
+ }
306
+
307
+ const target = `${state.sessionName}:0.${paneIdx}`;
308
+ injectPrompt(target, message);
309
+ ok(`Pane ${paneIdx}에 메시지 주입 완료`);
310
+ console.log("");
311
+ }
312
+
313
+ function teamList() {
314
+ const sessions = listSessions();
315
+ if (sessions.length === 0) {
316
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
317
+ return;
318
+ }
319
+ console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
320
+ for (const s of sessions) {
321
+ console.log(` ${GREEN}●${RESET} ${s}`);
322
+ }
323
+ console.log("");
324
+ }
325
+
326
+ function teamHelp() {
327
+ console.log(`
328
+ ${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
329
+
330
+ ${BOLD}시작${RESET}
331
+ ${WHITE}tfx team "작업 설명"${RESET} ${GRAY}기본 (codex x2 + gemini)${RESET}
332
+ ${WHITE}tfx team --agents codex,gemini "작업"${RESET} ${GRAY}에이전트 지정${RESET}
333
+ ${WHITE}tfx team --layout 1x3 "작업"${RESET} ${GRAY}레이아웃 지정${RESET}
334
+
335
+ ${BOLD}제어${RESET}
336
+ ${WHITE}tfx team status${RESET} ${GRAY}현재 팀 상태${RESET}
337
+ ${WHITE}tfx team attach${RESET} ${GRAY}tmux 세션 연결${RESET}
338
+ ${WHITE}tfx team send${RESET} ${DIM}N "msg"${RESET} ${GRAY}Pane N에 입력${RESET}
339
+ ${WHITE}tfx team stop${RESET} ${GRAY}graceful 종료${RESET}
340
+ ${WHITE}tfx team kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
341
+ ${WHITE}tfx team list${RESET} ${GRAY}활성 세션 목록${RESET}
342
+ `);
343
+ }
344
+
345
+ // ── 메인 진입점 ──
346
+
347
+ /**
348
+ * tfx team 서브커맨드 라우터
349
+ * bin/triflux.mjs에서 호출
350
+ */
351
+ export async function cmdTeam() {
352
+ const sub = process.argv[3];
353
+
354
+ switch (sub) {
355
+ case "status": return teamStatus();
356
+ case "attach": return teamAttach();
357
+ case "stop": return teamStop();
358
+ case "kill": return teamKill();
359
+ case "send": return teamSend();
360
+ case "list": return teamList();
361
+ case "help": case "--help": case "-h":
362
+ return teamHelp();
363
+ case undefined:
364
+ return teamHelp();
365
+ default:
366
+ return teamStart();
367
+ }
368
+ }
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ // hub/team/dashboard.mjs — 실시간 팀 상태 표시
3
+ // 실행: watch -n 1 -c 'node hub/team/dashboard.mjs --session tfx-team-xxx'
4
+ // 또는: node hub/team/dashboard.mjs --session tfx-team-xxx (단일 출력)
5
+ import { get } from "node:http";
6
+ import { capturePaneOutput } from "./session.mjs";
7
+
8
+ // ── 색상 ──
9
+ const AMBER = "\x1b[38;5;214m";
10
+ const GREEN = "\x1b[38;5;82m";
11
+ const RED = "\x1b[38;5;196m";
12
+ const GRAY = "\x1b[38;5;245m";
13
+ const DIM = "\x1b[2m";
14
+ const BOLD = "\x1b[1m";
15
+ const RESET = "\x1b[0m";
16
+
17
+ /**
18
+ * Hub /status 엔드포인트 조회
19
+ * @param {string} hubUrl — 예: http://127.0.0.1:27888
20
+ * @returns {Promise<object|null>}
21
+ */
22
+ function fetchStatus(hubUrl) {
23
+ return new Promise((resolve) => {
24
+ const url = `${hubUrl}/status`;
25
+ const req = get(url, { timeout: 2000 }, (res) => {
26
+ let data = "";
27
+ res.on("data", (chunk) => (data += chunk));
28
+ res.on("end", () => {
29
+ try {
30
+ resolve(JSON.parse(data));
31
+ } catch {
32
+ resolve(null);
33
+ }
34
+ });
35
+ });
36
+ req.on("error", () => resolve(null));
37
+ req.on("timeout", () => {
38
+ req.destroy();
39
+ resolve(null);
40
+ });
41
+ });
42
+ }
43
+
44
+ /**
45
+ * 진행률 바 생성
46
+ * @param {number} pct — 0~100
47
+ * @param {number} width — 바 너비 (기본 8)
48
+ * @returns {string}
49
+ */
50
+ function progressBar(pct, width = 8) {
51
+ const filled = Math.round((pct / 100) * width);
52
+ const empty = width - filled;
53
+ return `${GREEN}${"█".repeat(filled)}${GRAY}${"░".repeat(empty)}${RESET}`;
54
+ }
55
+
56
+ /**
57
+ * 업타임 포맷
58
+ * @param {number} ms
59
+ * @returns {string}
60
+ */
61
+ function formatUptime(ms) {
62
+ if (ms < 60000) return `${Math.round(ms / 1000)}초`;
63
+ if (ms < 3600000) return `${Math.round(ms / 60000)}분`;
64
+ return `${Math.round(ms / 3600000)}시간`;
65
+ }
66
+
67
+ /**
68
+ * 대시보드 렌더링
69
+ * @param {string} sessionName — tmux 세션 이름
70
+ * @param {object} opts
71
+ * @param {string} opts.hubUrl — Hub URL (기본 http://127.0.0.1:27888)
72
+ * @param {object} opts.teamState — team-state.json 내용
73
+ */
74
+ export async function renderDashboard(sessionName, opts = {}) {
75
+ const { hubUrl = "http://127.0.0.1:27888", teamState = {} } = opts;
76
+ const W = 50;
77
+ const border = "─".repeat(W);
78
+
79
+ // Hub 상태 조회
80
+ const status = await fetchStatus(hubUrl);
81
+ const hubOnline = !!status;
82
+ const hubState = hubOnline ? `${GREEN}● online${RESET}` : `${RED}● offline${RESET}`;
83
+ const uptime = status?.hub?.uptime ? formatUptime(status.hub.uptime) : "-";
84
+ const queueSize = status?.hub?.queue_depth ?? 0;
85
+
86
+ // 헤더
87
+ console.log(`${AMBER}┌─ ${sessionName} ${GRAY}${"─".repeat(Math.max(0, W - sessionName.length - 3))}${AMBER}┐${RESET}`);
88
+ console.log(`${AMBER}│${RESET} Hub: ${hubState} Uptime: ${DIM}${uptime}${RESET} Queue: ${DIM}${queueSize}${RESET}`);
89
+ console.log(`${AMBER}│${RESET}`);
90
+
91
+ // 에이전트 상태
92
+ const panes = teamState?.panes || {};
93
+ const paneEntries = Object.entries(panes).filter(([, v]) => v.role !== "dashboard");
94
+
95
+ if (paneEntries.length === 0) {
96
+ console.log(`${AMBER}│${RESET} ${DIM}에이전트 정보 없음${RESET}`);
97
+ } else {
98
+ for (const [paneTarget, paneInfo] of paneEntries) {
99
+ const { cli = "?", agentId = "?", subtask = "" } = paneInfo;
100
+ const label = `[${agentId}]`;
101
+ const cliTag = cli.charAt(0).toUpperCase() + cli.slice(1);
102
+
103
+ // Hub에서 에이전트 상태 확인
104
+ const agentStatus = status?.agents?.find?.((a) => a.agent_id === agentId);
105
+ const online = agentStatus ? `${GREEN}● online${RESET}` : `${GRAY}○ -${RESET}`;
106
+
107
+ // 진행률 추정 (메시지 기반, 단순 휴리스틱)
108
+ const msgCount = agentStatus?.message_count ?? 0;
109
+ const pct = msgCount === 0 ? 0 : Math.min(100, msgCount * 25);
110
+
111
+ console.log(`${AMBER}│${RESET} ${BOLD}${label}${RESET} ${cliTag} ${online} ${progressBar(pct)} ${DIM}${pct}%${RESET}`);
112
+
113
+ // pane 미리보기 (마지막 2줄)
114
+ const preview = capturePaneOutput(paneTarget, 2)
115
+ .split("\n")
116
+ .filter(Boolean)
117
+ .slice(-1)[0] || "";
118
+ if (preview) {
119
+ const truncated = preview.length > W - 8 ? preview.slice(0, W - 11) + "..." : preview;
120
+ console.log(`${AMBER}│${RESET} ${DIM}> ${truncated}${RESET}`);
121
+ }
122
+ console.log(`${AMBER}│${RESET}`);
123
+ }
124
+ }
125
+
126
+ // 푸터
127
+ console.log(`${AMBER}└${GRAY}${border}${AMBER}┘${RESET}`);
128
+ }
129
+
130
+ /** team-state.json 로드 */
131
+ async function loadTeamState() {
132
+ try {
133
+ const { readFileSync } = await import("node:fs");
134
+ const { join } = await import("node:path");
135
+ const { homedir } = await import("node:os");
136
+ const statePath = join(homedir(), ".claude", "cache", "tfx-hub", "team-state.json");
137
+ return JSON.parse(readFileSync(statePath, "utf8"));
138
+ } catch {
139
+ return {};
140
+ }
141
+ }
142
+
143
+ // ── CLI 실행 (자체 갱신 루프 — watch 불필요) ──
144
+ if (process.argv[1]?.includes("dashboard.mjs")) {
145
+ const sessionIdx = process.argv.indexOf("--session");
146
+ const sessionName = sessionIdx !== -1 ? process.argv[sessionIdx + 1] : null;
147
+ const intervalSec = parseInt(process.argv[process.argv.indexOf("--interval") + 1] || "2", 10);
148
+
149
+ if (!sessionName) {
150
+ console.error("사용법: node dashboard.mjs --session <세션이름> [--interval 2]");
151
+ process.exit(1);
152
+ }
153
+
154
+ // Ctrl+C로 종료
155
+ process.on("SIGINT", () => process.exit(0));
156
+
157
+ // 갱신 루프
158
+ while (true) {
159
+ const teamState = await loadTeamState();
160
+ // 화면 클리어 (ANSI)
161
+ process.stdout.write("\x1b[2J\x1b[H");
162
+ await renderDashboard(sessionName, { teamState });
163
+ console.log(`${DIM} ${intervalSec}초 간격 갱신 | Ctrl+C로 종료${RESET}`);
164
+ await new Promise((r) => setTimeout(r, intervalSec * 1000));
165
+ }
166
+ }
@@ -0,0 +1,102 @@
1
+ // hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
2
+ // 의존성: pane.mjs만 사용
3
+ import { injectPrompt } from "./pane.mjs";
4
+
5
+ /**
6
+ * 작업 분해 (LLM 없이 구분자 기반)
7
+ * @param {string} taskDescription — 전체 작업 설명
8
+ * @param {number} agentCount — 에이전트 수
9
+ * @returns {string[]} 각 에이전트의 서브태스크
10
+ */
11
+ export function decomposeTask(taskDescription, agentCount) {
12
+ if (agentCount <= 0) return [];
13
+ if (agentCount === 1) return [taskDescription];
14
+
15
+ // '+', ',', '\n' 기준으로 분리
16
+ const parts = taskDescription
17
+ .split(/[+,\n]+/)
18
+ .map((s) => s.trim())
19
+ .filter(Boolean);
20
+
21
+ if (parts.length === 0) return [taskDescription];
22
+
23
+ // 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
24
+ if (parts.length < agentCount) {
25
+ const result = [...parts];
26
+ while (result.length < agentCount) {
27
+ result.push(taskDescription);
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
33
+ if (parts.length > agentCount) {
34
+ const result = parts.slice(0, agentCount - 1);
35
+ result.push(parts.slice(agentCount - 1).join(" + "));
36
+ return result;
37
+ }
38
+
39
+ return parts;
40
+ }
41
+
42
+ /**
43
+ * 에이전트별 초기 프롬프트 생성
44
+ * @param {string} subtask — 이 에이전트의 서브태스크
45
+ * @param {object} config
46
+ * @param {string} config.cli — codex/gemini/claude
47
+ * @param {string} config.agentId — 에이전트 식별자
48
+ * @param {string} config.hubUrl — Hub URL
49
+ * @param {string} config.sessionName — tmux 세션 이름
50
+ * @returns {string}
51
+ */
52
+ export function buildPrompt(subtask, config) {
53
+ const { cli, agentId, hubUrl } = config;
54
+
55
+ // Hub MCP 도구 사용 안내 (CLI별 차이 없이 공통)
56
+ const hubInstructions = `
57
+ [Hub 메시지 도구]
58
+ tfx-hub MCP 서버(${hubUrl})가 연결되어 있다면 아래 도구를 사용할 수 있다:
59
+ - register: 에이전트 등록 (agent_id: "${agentId}", cli: "${cli}")
60
+ - publish: 결과 발행 (topic: "task.result")
61
+ - poll_messages: 다른 에이전트 메시지 수신
62
+ - ask: 다른 에이전트에게 질문
63
+
64
+ MCP 도구가 없으면 REST API 사용:
65
+ curl -s -X POST ${hubUrl.replace("/mcp", "")}/bridge/register -H 'Content-Type: application/json' -d '{"agent_id":"${agentId}","cli":"${cli}","timeout_sec":600}'
66
+ curl -s -X POST ${hubUrl.replace("/mcp", "")}/bridge/result -H 'Content-Type: application/json' -d '{"agent_id":"${agentId}","topic":"task.result","payload":{"summary":"결과 요약"}}'
67
+ `.trim();
68
+
69
+ return `너는 tfx-hub 팀의 에이전트 ${agentId}이다.
70
+
71
+ [작업]
72
+ ${subtask}
73
+
74
+ [규칙]
75
+ - 작업 완료 후 반드시 결과를 Hub에 발행하라
76
+ - 에이전트 ID: ${agentId}
77
+ - 다른 에이전트 결과가 필요하면 poll_messages로 확인
78
+
79
+ ${hubInstructions}
80
+
81
+ 작업을 시작하라.`;
82
+ }
83
+
84
+ /**
85
+ * 팀 오케스트레이션 실행 — 각 pane에 프롬프트 주입
86
+ * @param {string} sessionName — tmux 세션 이름
87
+ * @param {Array<{target: string, cli: string, subtask: string}>} assignments
88
+ * @param {object} opts
89
+ * @param {string} opts.hubUrl — Hub URL
90
+ * @returns {Promise<void>}
91
+ */
92
+ export async function orchestrate(sessionName, assignments, opts = {}) {
93
+ const { hubUrl = "http://127.0.0.1:27888/mcp" } = opts;
94
+
95
+ for (const { target, cli, subtask } of assignments) {
96
+ const agentId = `${cli}-${target.split(".").pop()}`;
97
+ const prompt = buildPrompt(subtask, { cli, agentId, hubUrl, sessionName });
98
+ injectPrompt(target, prompt);
99
+ // pane 간 100ms 간격 (안정성)
100
+ await new Promise((r) => setTimeout(r, 100));
101
+ }
102
+ }
@@ -0,0 +1,101 @@
1
+ // hub/team/pane.mjs — pane별 CLI 실행 + stdin 주입
2
+ // 의존성: child_process, fs, os, path (Node.js 내장)만 사용
3
+ import { execSync } from "node:child_process";
4
+ import { writeFileSync, unlinkSync, mkdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { detectMultiplexer } from "./session.mjs";
8
+
9
+ /** Windows 경로를 MSYS2/Git Bash tmux용 POSIX 경로로 변환 */
10
+ function toTmuxPath(p) {
11
+ if (process.platform !== "win32") return p;
12
+ // C:\Users\... → /c/Users/...
13
+ return p.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, d) => `/${d.toLowerCase()}`);
14
+ }
15
+
16
+ /** tmux 커맨드 실행 (session.mjs와 동일 패턴) */
17
+ function tmux(args, opts = {}) {
18
+ const mux = detectMultiplexer();
19
+ if (!mux) throw new Error("tmux 미발견");
20
+ const prefix = mux === "wsl-tmux" ? "wsl tmux" : "tmux";
21
+ const result = execSync(`${prefix} ${args}`, {
22
+ encoding: "utf8",
23
+ timeout: 10000,
24
+ stdio: ["pipe", "pipe", "pipe"],
25
+ ...opts,
26
+ });
27
+ return result != null ? result.trim() : "";
28
+ }
29
+
30
+ /**
31
+ * CLI 에이전트 시작 커맨드 생성
32
+ * @param {'codex'|'gemini'|'claude'} cli
33
+ * @returns {string} 실행할 셸 커맨드
34
+ */
35
+ export function buildCliCommand(cli) {
36
+ switch (cli) {
37
+ case "codex":
38
+ // interactive REPL 진입 — MCP는 ~/.codex/config.json에 사전 등록
39
+ return "codex";
40
+ case "gemini":
41
+ // interactive 모드 — MCP는 ~/.gemini/settings.json에 사전 등록
42
+ return "gemini";
43
+ case "claude":
44
+ // interactive 모드
45
+ return "claude";
46
+ default:
47
+ return cli; // 커스텀 CLI 허용
48
+ }
49
+ }
50
+
51
+ /**
52
+ * tmux pane에 CLI 시작
53
+ * @param {string} target — 예: tfx-team-abc:0.1
54
+ * @param {string} command — 실행할 커맨드
55
+ */
56
+ export function startCliInPane(target, command) {
57
+ // 특수문자 이스케이프: 작은따옴표 내부에서 안전하도록
58
+ const escaped = command.replace(/'/g, "'\\''");
59
+ tmux(`send-keys -t ${target} '${escaped}' Enter`);
60
+ }
61
+
62
+ /**
63
+ * pane에 프롬프트 주입 (load-buffer + paste-buffer 방식)
64
+ * 멀티라인 + 특수문자 안전, 크기 제한 없음
65
+ * @param {string} target — 예: tfx-team-abc:0.1
66
+ * @param {string} prompt — 주입할 텍스트
67
+ */
68
+ export function injectPrompt(target, prompt) {
69
+ // 임시 파일에 프롬프트 저장
70
+ const tmpDir = join(tmpdir(), "tfx-team");
71
+ mkdirSync(tmpDir, { recursive: true });
72
+
73
+ // pane ID를 파일명에 포함 (충돌 방지)
74
+ const safeTarget = target.replace(/[:.]/g, "-");
75
+ const tmpFile = join(tmpDir, `prompt-${safeTarget}-${Date.now()}.txt`);
76
+
77
+ try {
78
+ writeFileSync(tmpFile, prompt, "utf8");
79
+
80
+ // tmux load-buffer → paste-buffer → Enter (Windows 경로 변환 필요)
81
+ tmux(`load-buffer ${toTmuxPath(tmpFile)}`);
82
+ tmux(`paste-buffer -t ${target}`);
83
+ tmux(`send-keys -t ${target} Enter`);
84
+ } finally {
85
+ // 임시 파일 정리
86
+ try {
87
+ unlinkSync(tmpFile);
88
+ } catch {
89
+ // 정리 실패 무시
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * pane에 키 입력 전송
96
+ * @param {string} target — 예: tfx-team-abc:0.1
97
+ * @param {string} keys — tmux 키 표현 (예: 'C-c', 'Enter')
98
+ */
99
+ export function sendKeys(target, keys) {
100
+ tmux(`send-keys -t ${target} ${keys}`);
101
+ }
@@ -0,0 +1,186 @@
1
+ // hub/team/session.mjs — tmux 세션 생명주기 관리
2
+ // 의존성: child_process (Node.js 내장)만 사용
3
+ import { execSync } from "node:child_process";
4
+
5
+ /** tmux 실행 가능 여부 확인 */
6
+ function hasTmux() {
7
+ try {
8
+ execSync("tmux -V", { stdio: "ignore", timeout: 3000 });
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ /** WSL2 내 tmux 사용 가능 여부 (Windows 전용) */
16
+ function hasWslTmux() {
17
+ try {
18
+ execSync("wsl tmux -V", { stdio: "ignore", timeout: 5000 });
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * 터미널 멀티플렉서 감지
27
+ * @returns {'tmux'|'wsl-tmux'|null}
28
+ */
29
+ export function detectMultiplexer() {
30
+ if (hasTmux()) return "tmux";
31
+ if (process.platform === "win32" && hasWslTmux()) return "wsl-tmux";
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * tmux 커맨드 실행 (wsl-tmux 투명 지원)
37
+ * @param {string} args — tmux 서브커맨드 + 인자
38
+ * @param {object} opts — execSync 옵션
39
+ * @returns {string} stdout
40
+ */
41
+ function tmux(args, opts = {}) {
42
+ const mux = detectMultiplexer();
43
+ if (!mux) {
44
+ throw new Error(
45
+ "tmux 미발견.\n\n" +
46
+ "tfx team은 tmux가 필요합니다:\n" +
47
+ " WSL2: wsl sudo apt install tmux\n" +
48
+ " macOS: brew install tmux\n" +
49
+ " Linux: apt install tmux\n\n" +
50
+ "Windows에서는 WSL2를 권장합니다:\n" +
51
+ " 1. wsl --install\n" +
52
+ " 2. wsl sudo apt install tmux\n" +
53
+ " 3. tfx team \"작업\" (자동으로 WSL tmux 사용)"
54
+ );
55
+ }
56
+ const prefix = mux === "wsl-tmux" ? "wsl tmux" : "tmux";
57
+ const result = execSync(`${prefix} ${args}`, {
58
+ encoding: "utf8",
59
+ timeout: 10000,
60
+ stdio: ["pipe", "pipe", "pipe"],
61
+ ...opts,
62
+ });
63
+ // stdio: "ignore" 시 execSync가 null 반환 — 안전 처리
64
+ return result != null ? result.trim() : "";
65
+ }
66
+
67
+ /**
68
+ * tmux 세션 생성 + 레이아웃 분할
69
+ * @param {string} sessionName — 세션 이름
70
+ * @param {object} opts
71
+ * @param {'2x2'|'1xN'} opts.layout — 레이아웃 (기본 2x2)
72
+ * @param {number} opts.paneCount — pane 수 (기본 4)
73
+ * @returns {{ sessionName: string, panes: string[] }}
74
+ */
75
+ export function createSession(sessionName, opts = {}) {
76
+ const { layout = "2x2", paneCount = 4 } = opts;
77
+
78
+ // 기존 세션 정리
79
+ if (sessionExists(sessionName)) {
80
+ killSession(sessionName);
81
+ }
82
+
83
+ // 새 세션 생성 (detached)
84
+ tmux(`new-session -d -s ${sessionName} -x 220 -y 55`);
85
+
86
+ const panes = [`${sessionName}:0.0`];
87
+
88
+ if (layout === "2x2" && paneCount >= 3) {
89
+ // 2x2 그리드: 좌|우 → 좌상/좌하 → 우상/우하
90
+ tmux(`split-window -h -t ${sessionName}:0`);
91
+ tmux(`split-window -v -t ${sessionName}:0.0`);
92
+ if (paneCount >= 4) {
93
+ tmux(`split-window -v -t ${sessionName}:0.2`);
94
+ }
95
+ // pane ID 재수집
96
+ panes.length = 0;
97
+ for (let i = 0; i < Math.min(paneCount, 4); i++) {
98
+ panes.push(`${sessionName}:0.${i}`);
99
+ }
100
+ } else {
101
+ // 1xN 수직 분할
102
+ for (let i = 1; i < paneCount; i++) {
103
+ tmux(`split-window -v -t ${sessionName}:0`);
104
+ }
105
+ // even-vertical 레이아웃 적용
106
+ tmux(`select-layout -t ${sessionName}:0 even-vertical`);
107
+ panes.length = 0;
108
+ for (let i = 0; i < paneCount; i++) {
109
+ panes.push(`${sessionName}:0.${i}`);
110
+ }
111
+ }
112
+
113
+ return { sessionName, panes };
114
+ }
115
+
116
+ /**
117
+ * tmux 세션 연결 (포그라운드 전환)
118
+ * @param {string} sessionName
119
+ */
120
+ export function attachSession(sessionName) {
121
+ const mux = detectMultiplexer();
122
+ const prefix = mux === "wsl-tmux" ? "wsl tmux" : "tmux";
123
+ // stdio: inherit로 사용자에게 제어권 반환
124
+ execSync(`${prefix} attach-session -t ${sessionName}`, {
125
+ stdio: "inherit",
126
+ timeout: 0, // 타임아웃 없음 (사용자가 detach할 때까지)
127
+ });
128
+ }
129
+
130
+ /**
131
+ * tmux 세션 존재 확인
132
+ * @param {string} sessionName
133
+ * @returns {boolean}
134
+ */
135
+ export function sessionExists(sessionName) {
136
+ try {
137
+ tmux(`has-session -t ${sessionName}`, { stdio: "ignore" });
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * tmux 세션 종료
146
+ * @param {string} sessionName
147
+ */
148
+ export function killSession(sessionName) {
149
+ try {
150
+ tmux(`kill-session -t ${sessionName}`, { stdio: "ignore" });
151
+ } catch {
152
+ // 이미 종료된 세션 — 무시
153
+ }
154
+ }
155
+
156
+ /**
157
+ * tfx-team- 접두사 세션 목록
158
+ * @returns {string[]}
159
+ */
160
+ export function listSessions() {
161
+ try {
162
+ const output = tmux('list-sessions -F "#{session_name}"');
163
+ return output
164
+ .split("\n")
165
+ .filter((s) => s.startsWith("tfx-team-"));
166
+ } catch {
167
+ return [];
168
+ }
169
+ }
170
+
171
+ /**
172
+ * pane 마지막 N줄 캡처
173
+ * @param {string} target — 예: tfx-team-abc:0.1
174
+ * @param {number} lines — 캡처할 줄 수 (기본 5)
175
+ * @returns {string}
176
+ */
177
+ export function capturePaneOutput(target, lines = 5) {
178
+ try {
179
+ // -l 플래그는 일부 tmux 빌드(MSYS2)에서 미지원 → 전체 캡처 후 JS에서 절삭
180
+ const full = tmux(`capture-pane -t ${target} -p`);
181
+ const nonEmpty = full.split("\n").filter((l) => l.trim() !== "");
182
+ return nonEmpty.slice(-lines).join("\n");
183
+ } catch {
184
+ return "";
185
+ }
186
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "3.1.0-dev.5",
3
+ "version": "3.2.0-dev.1",
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": {
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: tfx-team
3
+ description: 멀티-CLI 팀 모드. tfx-auto와 동일한 트리아지/분해, 실행은 tmux + Hub 기반 interactive 세션.
4
+ triggers:
5
+ - tfx-team
6
+ argument-hint: '"작업 설명" | --agents codex,gemini "작업" | status | stop'
7
+ ---
8
+
9
+ # tfx-team — tmux + Hub 기반 멀티-CLI 팀 오케스트레이터
10
+
11
+ > tfx-auto와 **동일한 트리아지/분해 로직**, 실행 백엔드만 다름.
12
+ >
13
+ > | | tfx-auto | tfx-team |
14
+ > |--|----------|----------|
15
+ > | 트리아지 | Codex 분류 → Opus 분해 | **동일** |
16
+ > | 실행 | `tfx-route.sh` one-shot | **tmux pane interactive** |
17
+ > | 관찰 | stdout 반환 후 종료 | **실시간 네이티브 터미널** |
18
+ > | 통신 | 없음 (독립 실행) | **Hub MCP 메시지 버스** |
19
+ > | 개입 | 불가 | **`tfx team send N "추가 지시"`** |
20
+
21
+ ## 사용법
22
+
23
+ ```
24
+ /tfx-team "인증 리팩터링 + UI 개선 + 보안 리뷰"
25
+ /tfx-team --agents codex,gemini "프론트+백엔드"
26
+ /tfx-team status
27
+ /tfx-team stop
28
+ ```
29
+
30
+ ## 실행 워크플로우
31
+
32
+ ### Phase 1: 입력 파싱
33
+
34
+ ```
35
+ 입력: "3:codex 리뷰" → 수동 모드: N=3, agent=codex
36
+ 입력: "인증 + UI + 테스트" → 자동 모드: Codex 분류 → Opus 분해
37
+ 입력: "status" → 제어 커맨드 (tfx team status)
38
+ 입력: "stop" → 제어 커맨드 (tfx team stop)
39
+ ```
40
+
41
+ **제어 커맨드 감지:**
42
+ - `status`, `stop`, `kill`, `attach`, `list`, `send` → `Bash("node bin/triflux.mjs team {cmd}")` 직행
43
+ - 그 외 → Phase 2 트리아지
44
+
45
+ ### Phase 2: 트리아지 (tfx-auto와 동일)
46
+
47
+ #### 자동 모드 — Codex 분류 → Opus 분해
48
+
49
+ ```bash
50
+ # Step 2a: Codex 분류 (무료)
51
+ Bash("codex exec --full-auto --skip-git-repo-check '다음 작업을 분석하고 각 부분에 적합한 agent를 분류하라.
52
+
53
+ agent 선택:
54
+ - codex: 코드 구현/수정/분석/리뷰/디버깅/설계 (기본값)
55
+ - gemini: 문서/UI/디자인/멀티모달
56
+ - claude: 코드베이스 탐색/테스트 실행/검증 (최후 수단)
57
+
58
+ 작업: {task}
59
+
60
+ JSON:
61
+ { \"parts\": [{ \"description\": \"...\", \"agent\": \"codex|gemini|claude\" }] }
62
+ '")
63
+ ```
64
+
65
+ > Codex 분류 실패 시 → Opus(오케스트레이터)가 직접 분류+분해
66
+
67
+ ```
68
+ # Step 2b: Opus 인라인 분해
69
+ 분류 결과 → 에이전트 배정:
70
+ codex → --agents에 codex 추가
71
+ gemini → --agents에 gemini 추가
72
+ claude → --agents에 claude 추가 (또는 codex로 대체 — claude는 최후 수단)
73
+
74
+ 결과: agents 배열 + subtasks 배열
75
+ ```
76
+
77
+ #### 수동 모드 (`N:agent_type`)
78
+
79
+ Codex 분류 건너뜀 → Opus가 직접 N개 서브태스크 분해.
80
+
81
+ ### Phase 3: tmux 팀 실행
82
+
83
+ 트리아지 결과를 `tfx team` CLI로 실행:
84
+
85
+ ```bash
86
+ # agents 배열과 작업을 tfx team에 전달
87
+ Bash("node {PKG_ROOT}/bin/triflux.mjs team --agents {agents.join(',')} \"{task}\"")
88
+ ```
89
+
90
+ **내부 동작 (hub/team/ 모듈):**
91
+ 1. Hub lazy-start (`hub/server.mjs`)
92
+ 2. tmux 세션 생성 (2x2 or 1xN 레이아웃)
93
+ 3. Pane 0: Dashboard (실시간 상태)
94
+ 4. Pane 1~N: 각 CLI interactive 모드 시작 (codex/gemini/claude)
95
+ 5. 3초 대기 (CLI 초기화)
96
+ 6. 각 pane에 서브태스크 프롬프트 주입 (load-buffer + paste-buffer)
97
+ 7. tmux attach → 사용자에게 제어권
98
+
99
+ ### Phase 4: 실시간 관찰 + 개입
100
+
101
+ tmux 세션 내에서:
102
+ - `Ctrl+B → 방향키`: pane 전환
103
+ - `Ctrl+B → D`: 세션 분리 (백그라운드)
104
+ - `Ctrl+B → Z`: pane 전체화면
105
+
106
+ 세션 분리 후 제어:
107
+ ```bash
108
+ /tfx-team status # 팀 상태 확인
109
+ /tfx-team send 1 "추가 지시" # Pane 1에 입력
110
+ /tfx-team attach # 세션 재연결
111
+ /tfx-team stop # graceful 종료
112
+ ```
113
+
114
+ ### Phase 5: 에이전트 간 통신
115
+
116
+ Hub MCP 도구가 각 CLI에 등록되어 있으면 자동 통신:
117
+ - `register`: 에이전트 등록
118
+ - `publish`: 결과 발행 (topic: task.result)
119
+ - `poll_messages`: 다른 에이전트 메시지 수신
120
+ - `ask`: 다른 에이전트에게 질문
121
+
122
+ MCP 미등록 시 REST 폴백 (프롬프트에 curl 명령 포함).
123
+
124
+ ## 에이전트 매핑
125
+
126
+ | 분류 결과 | CLI | 비고 |
127
+ |----------|-----|------|
128
+ | codex | `codex` (interactive) | MCP: ~/.codex/config.json |
129
+ | gemini | `gemini` (interactive) | MCP: ~/.gemini/settings.json |
130
+ | claude | `claude` (interactive) | MCP: .mcp.json |
131
+
132
+ > **중요:** tfx-auto와 달리 세부 에이전트(executor, debugger 등)로 분류하지 않음.
133
+ > tmux pane에는 CLI 단위(codex/gemini/claude)로 실행하고,
134
+ > 프롬프트에 역할(구현/리뷰/디버깅)을 명시하여 CLI가 알아서 수행.
135
+
136
+ ## tfx-auto와의 차이 요약
137
+
138
+ | 항목 | tfx-auto | tfx-team |
139
+ |------|----------|----------|
140
+ | 트리아지 | Codex 분류 → Opus 분해 | **동일** |
141
+ | 실행 단위 | 에이전트(executor, reviewer 등) | CLI(codex, gemini, claude) |
142
+ | 실행 방식 | `tfx-route.sh` (one-shot, 블랙박스) | tmux pane (interactive, 관찰 가능) |
143
+ | 결과 수집 | stdout 파싱 | Hub publish/poll |
144
+ | 개입 | 불가 | `tfx team send` |
145
+ | 통신 | 없음 | Hub MCP 메시지 버스 |
146
+ | Dashboard | 없음 | Pane 0 실시간 상태 |
147
+ | tmux 필요 | 아니오 | **예** |
148
+ | 종료 | 자동 (실행 완료) | 수동 (`tfx team stop`) |
149
+
150
+ ## 전제 조건
151
+
152
+ - **tmux** — 필수 (Git Bash: v3.6a, WSL2, macOS, Linux)
153
+ - **codex/gemini CLI** — 해당 에이전트 사용 시
154
+ - **tfx setup** — Hub MCP 자동 등록 (사전 실행 권장)
155
+
156
+ ## 에러 처리
157
+
158
+ | 에러 | 처리 |
159
+ |------|------|
160
+ | tmux 미설치 | 안내 메시지 + WSL2 설치 가이드 |
161
+ | Hub 시작 실패 | `tfx hub start` 수동 실행 안내 |
162
+ | CLI 미설치 | 해당 pane 건너뜀 + 경고 |
163
+ | MCP 미등록 | REST 폴백 (curl) |
164
+ | Codex 분류 실패 | Opus 직접 분류+분해 |
165
+
166
+ ## 관련
167
+
168
+ | 항목 | 설명 |
169
+ |------|------|
170
+ | `hub/team/` | tmux + Hub 팀 모듈 (session, pane, orchestrator, dashboard, cli) |
171
+ | `tfx-auto` | one-shot 실행 오케스트레이터 (기존, 병행 유지) |
172
+ | `tfx-hub` | MCP 메시지 버스 관리 (start/stop/status) |