triflux 3.2.0-dev.9 → 3.3.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.
@@ -1,241 +1,241 @@
1
- // hub/team/cli-team-status.mjs — team 상태/조회 로직
2
- import {
3
- capturePaneOutput,
4
- detectMultiplexer,
5
- getSessionAttachedCount,
6
- hasWindowsTerminal,
7
- hasWindowsTerminalSession,
8
- listSessions,
9
- } from "./session.mjs";
10
- import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET, WHITE } from "./shared.mjs";
11
- import {
12
- TEAM_PROFILE,
13
- getDefaultHubUrl,
14
- getHubInfo,
15
- isNativeMode,
16
- isTeamAlive,
17
- isWtMode,
18
- loadTeamState,
19
- nativeGetStatus,
20
- ok,
21
- saveTeamState,
22
- } from "./cli-team-common.mjs";
23
-
24
- async function fetchHubTaskList(state) {
25
- const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
26
- const teamName = state?.native?.teamName || state?.sessionName || null;
27
- if (!teamName) return [];
28
- try {
29
- const res = await fetch(`${hubBase}/bridge/team/task-list`, {
30
- method: "POST",
31
- headers: { "Content-Type": "application/json" },
32
- body: JSON.stringify({ team_name: teamName }),
33
- signal: AbortSignal.timeout(2000),
34
- });
35
- const data = await res.json();
36
- return data?.ok ? (data.data?.tasks || []) : [];
37
- } catch {
38
- return [];
39
- }
40
- }
41
-
42
- function renderTasks(tasks = []) {
43
- if (!tasks.length) {
44
- console.log(`\n ${DIM}태스크 없음${RESET}\n`);
45
- return;
46
- }
47
-
48
- console.log(`\n ${AMBER}${BOLD}⬡ Team Tasks${RESET}\n`);
49
- for (const t of tasks) {
50
- const dep = t.depends_on?.length ? ` ${DIM}(deps: ${t.depends_on.join(",")})${RESET}` : "";
51
- const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
52
- console.log(` ${WHITE}${t.id}${RESET} ${t.status.padEnd(11)} ${t.title}${owner}${dep}`);
53
- }
54
- console.log("");
55
- }
56
-
57
- export async function teamStatus() {
58
- const state = loadTeamState();
59
- if (!state) {
60
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
61
- return;
62
- }
63
-
64
- const alive = isTeamAlive(state);
65
- const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
66
- const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
67
-
68
- console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET} ${status}\n`);
69
- console.log(` 세션: ${state.sessionName}`);
70
- console.log(` 모드: ${state.teammateMode || "tmux"}`);
71
- console.log(` 리드: ${state.lead || "claude"}`);
72
- console.log(` 워커: ${(state.agents || []).join(", ")}`);
73
- console.log(` Uptime: ${uptime}`);
74
- console.log(` 태스크: ${(state.tasks || []).length}`);
75
- if (isWtMode(state) && !hasWindowsTerminalSession()) {
76
- console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
77
- }
78
-
79
- const members = state.members || [];
80
- if (members.length) {
81
- console.log("");
82
- for (const m of members) {
83
- const roleTag = m.role === "lead" ? "lead" : "worker";
84
- console.log(` - ${m.name} (${m.cli}) ${DIM}${roleTag}${RESET} ${DIM}${m.pane}${RESET}`);
85
- }
86
- }
87
-
88
- if (isNativeMode(state) && alive) {
89
- const native = await nativeGetStatus(state);
90
- const nativeMembers = native?.data?.members || [];
91
- if (nativeMembers.length) {
92
- console.log("");
93
- for (const m of nativeMembers) {
94
- console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
95
- }
96
- }
97
- }
98
-
99
- if (alive) {
100
- const hubTasks = await fetchHubTaskList(state);
101
- if (hubTasks.length > 0) {
102
- const completed = hubTasks.filter((t) => t.status === "completed").length;
103
- const failed = hubTasks.filter((t) => t.status === "failed").length;
104
-
105
- console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
106
- for (const t of hubTasks) {
107
- const icon = t.status === "completed" ? `${GREEN}✓${RESET}`
108
- : t.status === "in_progress" ? `${AMBER}●${RESET}`
109
- : t.status === "failed" ? `${RED}✗${RESET}`
110
- : `${GRAY}○${RESET}`;
111
- const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
112
- const subject = t.subject || t.description?.slice(0, 50) || "";
113
- console.log(` ${icon} ${subject}${owner}`);
114
- }
115
- if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
116
- }
117
- }
118
-
119
- console.log("");
120
- }
121
-
122
- export function teamTasks() {
123
- const state = loadTeamState();
124
- if (!state || !isTeamAlive(state)) {
125
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
126
- return;
127
- }
128
- renderTasks(state.tasks || []);
129
- }
130
-
131
- export function teamTaskUpdate() {
132
- const state = loadTeamState();
133
- if (!state || !isTeamAlive(state)) {
134
- console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
135
- return;
136
- }
137
-
138
- const action = (process.argv[4] || "").toLowerCase();
139
- const taskId = (process.argv[5] || "").toUpperCase();
140
- const nextStatus = action === "done" || action === "complete" || action === "completed"
141
- ? "completed"
142
- : action === "progress" || action === "in-progress" || action === "in_progress"
143
- ? "in_progress"
144
- : action === "pending"
145
- ? "pending"
146
- : null;
147
-
148
- if (!nextStatus || !taskId) {
149
- console.log(`\n 사용법: ${WHITE}tfx team task <pending|progress|done> <T1>${RESET}\n`);
150
- return;
151
- }
152
-
153
- const tasks = state.tasks || [];
154
- const target = tasks.find((t) => String(t.id).toUpperCase() === taskId);
155
- if (!target) {
156
- console.log(`\n ${DIM}태스크를 찾을 수 없음: ${taskId}${RESET}\n`);
157
- return;
158
- }
159
-
160
- target.status = nextStatus;
161
- saveTeamState(state);
162
- ok(`${target.id} 상태 갱신: ${nextStatus}`);
163
- console.log("");
164
- }
165
-
1
+ // hub/team/cli-team-status.mjs — team 상태/조회 로직
2
+ import {
3
+ capturePaneOutput,
4
+ detectMultiplexer,
5
+ getSessionAttachedCount,
6
+ hasWindowsTerminal,
7
+ hasWindowsTerminalSession,
8
+ listSessions,
9
+ } from "./session.mjs";
10
+ import { AMBER, BOLD, DIM, GRAY, GREEN, RED, RESET, WHITE } from "./shared.mjs";
11
+ import {
12
+ TEAM_PROFILE,
13
+ getDefaultHubUrl,
14
+ getHubInfo,
15
+ isNativeMode,
16
+ isTeamAlive,
17
+ isWtMode,
18
+ loadTeamState,
19
+ nativeGetStatus,
20
+ ok,
21
+ saveTeamState,
22
+ } from "./cli-team-common.mjs";
23
+
24
+ async function fetchHubTaskList(state) {
25
+ const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
26
+ const teamName = state?.native?.teamName || state?.sessionName || null;
27
+ if (!teamName) return [];
28
+ try {
29
+ const res = await fetch(`${hubBase}/bridge/team/task-list`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ team_name: teamName }),
33
+ signal: AbortSignal.timeout(2000),
34
+ });
35
+ const data = await res.json();
36
+ return data?.ok ? (data.data?.tasks || []) : [];
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function renderTasks(tasks = []) {
43
+ if (!tasks.length) {
44
+ console.log(`\n ${DIM}태스크 없음${RESET}\n`);
45
+ return;
46
+ }
47
+
48
+ console.log(`\n ${AMBER}${BOLD}⬡ Team Tasks${RESET}\n`);
49
+ for (const t of tasks) {
50
+ const dep = t.depends_on?.length ? ` ${DIM}(deps: ${t.depends_on.join(",")})${RESET}` : "";
51
+ const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
52
+ console.log(` ${WHITE}${t.id}${RESET} ${t.status.padEnd(11)} ${t.title}${owner}${dep}`);
53
+ }
54
+ console.log("");
55
+ }
56
+
57
+ export async function teamStatus() {
58
+ const state = loadTeamState();
59
+ if (!state) {
60
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
61
+ return;
62
+ }
63
+
64
+ const alive = isTeamAlive(state);
65
+ const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
66
+ const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
67
+
68
+ console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET} ${status}\n`);
69
+ console.log(` 세션: ${state.sessionName}`);
70
+ console.log(` 모드: ${state.teammateMode || "tmux"}`);
71
+ console.log(` 리드: ${state.lead || "claude"}`);
72
+ console.log(` 워커: ${(state.agents || []).join(", ")}`);
73
+ console.log(` Uptime: ${uptime}`);
74
+ console.log(` 태스크: ${(state.tasks || []).length}`);
75
+ if (isWtMode(state) && !hasWindowsTerminalSession()) {
76
+ console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
77
+ }
78
+
79
+ const members = state.members || [];
80
+ if (members.length) {
81
+ console.log("");
82
+ for (const m of members) {
83
+ const roleTag = m.role === "lead" ? "lead" : "worker";
84
+ console.log(` - ${m.name} (${m.cli}) ${DIM}${roleTag}${RESET} ${DIM}${m.pane}${RESET}`);
85
+ }
86
+ }
87
+
88
+ if (isNativeMode(state) && alive) {
89
+ const native = await nativeGetStatus(state);
90
+ const nativeMembers = native?.data?.members || [];
91
+ if (nativeMembers.length) {
92
+ console.log("");
93
+ for (const m of nativeMembers) {
94
+ console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ if (alive) {
100
+ const hubTasks = await fetchHubTaskList(state);
101
+ if (hubTasks.length > 0) {
102
+ const completed = hubTasks.filter((t) => t.status === "completed").length;
103
+ const failed = hubTasks.filter((t) => t.status === "failed").length;
104
+
105
+ console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
106
+ for (const t of hubTasks) {
107
+ const icon = t.status === "completed" ? `${GREEN}✓${RESET}`
108
+ : t.status === "in_progress" ? `${AMBER}●${RESET}`
109
+ : t.status === "failed" ? `${RED}✗${RESET}`
110
+ : `${GRAY}○${RESET}`;
111
+ const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
112
+ const subject = t.subject || t.description?.slice(0, 50) || "";
113
+ console.log(` ${icon} ${subject}${owner}`);
114
+ }
115
+ if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
116
+ }
117
+ }
118
+
119
+ console.log("");
120
+ }
121
+
122
+ export function teamTasks() {
123
+ const state = loadTeamState();
124
+ if (!state || !isTeamAlive(state)) {
125
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
126
+ return;
127
+ }
128
+ renderTasks(state.tasks || []);
129
+ }
130
+
131
+ export function teamTaskUpdate() {
132
+ const state = loadTeamState();
133
+ if (!state || !isTeamAlive(state)) {
134
+ console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
135
+ return;
136
+ }
137
+
138
+ const action = (process.argv[4] || "").toLowerCase();
139
+ const taskId = (process.argv[5] || "").toUpperCase();
140
+ const nextStatus = action === "done" || action === "complete" || action === "completed"
141
+ ? "completed"
142
+ : action === "progress" || action === "in-progress" || action === "in_progress"
143
+ ? "in_progress"
144
+ : action === "pending"
145
+ ? "pending"
146
+ : null;
147
+
148
+ if (!nextStatus || !taskId) {
149
+ console.log(`\n 사용법: ${WHITE}tfx multi task <pending|progress|done> <T1>${RESET}\n`);
150
+ return;
151
+ }
152
+
153
+ const tasks = state.tasks || [];
154
+ const target = tasks.find((t) => String(t.id).toUpperCase() === taskId);
155
+ if (!target) {
156
+ console.log(`\n ${DIM}태스크를 찾을 수 없음: ${taskId}${RESET}\n`);
157
+ return;
158
+ }
159
+
160
+ target.status = nextStatus;
161
+ saveTeamState(state);
162
+ ok(`${target.id} 상태 갱신: ${nextStatus}`);
163
+ console.log("");
164
+ }
165
+
166
166
  export async function teamDebug() {
167
- const state = loadTeamState();
168
- const linesIdx = process.argv.findIndex((a) => a === "--lines" || a === "-n");
169
- const lines = linesIdx !== -1 ? Math.max(3, parseInt(process.argv[linesIdx + 1] || "20", 10) || 20) : 20;
170
- const mux = detectMultiplexer() || "none";
171
- const hub = await getHubInfo();
172
-
173
- console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
174
- console.log(` platform: ${process.platform}`);
175
- console.log(` node: ${process.version}`);
176
- console.log(` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`);
177
- console.log(` mux: ${mux}`);
178
- console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
179
- console.log(` hub-url: ${hub?.url || "-"}`);
180
-
181
- const sessions = listSessions();
182
- console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
183
-
184
- if (!state) {
185
- console.log(`\n ${DIM}team-state 없음 (활성 세션 없음)${RESET}\n`);
186
- return;
187
- }
188
-
189
- console.log(`\n ${BOLD}state${RESET}`);
190
- console.log(` session: ${state.sessionName}`);
191
- console.log(` profile: ${state.profile || TEAM_PROFILE}`);
192
- console.log(` mode: ${state.teammateMode || "tmux"}`);
193
- console.log(` lead: ${state.lead}`);
194
- console.log(` agents: ${(state.agents || []).join(", ")}`);
195
- console.log(` alive: ${isTeamAlive(state) ? "yes" : "no"}`);
196
- const attached = getSessionAttachedCount(state.sessionName);
197
- console.log(` attached: ${attached == null ? "-" : attached}`);
198
-
199
- if (isWtMode(state)) {
200
- const wtState = state.wt || {};
201
- console.log(`\n ${BOLD}wt-session${RESET}`);
202
- console.log(` window: ${wtState.windowId ?? 0}`);
203
- console.log(` layout: ${wtState.layout || state.layout || "-"}`);
204
- console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
205
- console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
206
- console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
207
- console.log("");
208
- return;
209
- }
210
-
211
- if (isNativeMode(state)) {
212
- const native = await nativeGetStatus(state);
213
- const members = native?.data?.members || [];
214
- console.log(`\n ${BOLD}native-members${RESET}`);
215
- if (!members.length) {
216
- console.log(` ${DIM}(no data)${RESET}`);
217
- } else {
218
- for (const m of members) {
219
- console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
220
- }
221
- }
222
- console.log("");
223
- return;
224
- }
225
-
226
- const members = state.members || [];
227
- console.log(`\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`);
228
- if (!members.length) {
229
- console.log(` ${DIM}(members 없음)${RESET}`);
230
- } else {
231
- for (const m of members) {
232
- const tail = capturePaneOutput(m.pane, lines) || "(empty)";
233
- console.log(`\n [${m.name}] ${m.pane}`);
234
- const tailLines = tail.split("\n").slice(-lines);
235
- for (const line of tailLines) {
236
- console.log(` ${line}`);
237
- }
238
- }
167
+ const state = loadTeamState();
168
+ const linesIdx = process.argv.findIndex((a) => a === "--lines" || a === "-n");
169
+ const lines = linesIdx !== -1 ? Math.max(3, parseInt(process.argv[linesIdx + 1] || "20", 10) || 20) : 20;
170
+ const mux = detectMultiplexer() || "none";
171
+ const hub = await getHubInfo();
172
+
173
+ console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
174
+ console.log(` platform: ${process.platform}`);
175
+ console.log(` node: ${process.version}`);
176
+ console.log(` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`);
177
+ console.log(` mux: ${mux}`);
178
+ console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
179
+ console.log(` hub-url: ${hub?.url || "-"}`);
180
+
181
+ const sessions = listSessions();
182
+ console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
183
+
184
+ if (!state) {
185
+ console.log(`\n ${DIM}team-state 없음 (활성 세션 없음)${RESET}\n`);
186
+ return;
187
+ }
188
+
189
+ console.log(`\n ${BOLD}state${RESET}`);
190
+ console.log(` session: ${state.sessionName}`);
191
+ console.log(` profile: ${state.profile || TEAM_PROFILE}`);
192
+ console.log(` mode: ${state.teammateMode || "tmux"}`);
193
+ console.log(` lead: ${state.lead}`);
194
+ console.log(` agents: ${(state.agents || []).join(", ")}`);
195
+ console.log(` alive: ${isTeamAlive(state) ? "yes" : "no"}`);
196
+ const attached = getSessionAttachedCount(state.sessionName);
197
+ console.log(` attached: ${attached == null ? "-" : attached}`);
198
+
199
+ if (isWtMode(state)) {
200
+ const wtState = state.wt || {};
201
+ console.log(`\n ${BOLD}wt-session${RESET}`);
202
+ console.log(` window: ${wtState.windowId ?? 0}`);
203
+ console.log(` layout: ${wtState.layout || state.layout || "-"}`);
204
+ console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
205
+ console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
206
+ console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
207
+ console.log("");
208
+ return;
209
+ }
210
+
211
+ if (isNativeMode(state)) {
212
+ const native = await nativeGetStatus(state);
213
+ const members = native?.data?.members || [];
214
+ console.log(`\n ${BOLD}native-members${RESET}`);
215
+ if (!members.length) {
216
+ console.log(` ${DIM}(no data)${RESET}`);
217
+ } else {
218
+ for (const m of members) {
219
+ console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
220
+ }
221
+ }
222
+ console.log("");
223
+ return;
224
+ }
225
+
226
+ const members = state.members || [];
227
+ console.log(`\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`);
228
+ if (!members.length) {
229
+ console.log(` ${DIM}(members 없음)${RESET}`);
230
+ } else {
231
+ for (const m of members) {
232
+ const tail = capturePaneOutput(m.pane, lines) || "(empty)";
233
+ console.log(`\n [${m.name}] ${m.pane}`);
234
+ const tailLines = tail.split("\n").slice(-lines);
235
+ for (const line of tailLines) {
236
+ console.log(` ${line}`);
237
+ }
238
+ }
239
239
  }
240
240
  console.log("");
241
241
  }
@@ -266,4 +266,4 @@ export function teamList() {
266
266
  }
267
267
  console.log("");
268
268
  }
269
-
269
+
package/hub/team/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
- // hub/team/cli.mjs — team CLI UI/네비게이션 진입점
2
- // bin/triflux.mjs에서 import하여 사용
1
+ // hub/team/cli.mjs — team CLI UI/네비게이션 진입점
2
+ // bin/triflux.mjs에서 import하여 사용
3
3
  import { AMBER, GRAY, DIM, BOLD, RESET, WHITE } from "./shared.mjs";
4
4
  import { TEAM_SUBCOMMANDS } from "./cli-team-common.mjs";
5
5
  import { teamStart } from "./cli-team-start.mjs";
@@ -13,87 +13,87 @@ import {
13
13
  teamKill,
14
14
  teamSend,
15
15
  } from "./cli-team-control.mjs";
16
-
17
- function teamHelp() {
18
- console.log(`
19
- ${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
20
-
21
- ${BOLD}시작${RESET}
22
- ${WHITE}tfx team "작업 설명"${RESET}
23
- ${WHITE}tfx team --agents codex,gemini --lead claude "작업"${RESET}
24
- ${WHITE}tfx team --teammate-mode tmux "작업"${RESET}
25
- ${WHITE}tfx team --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
26
- ${WHITE}tfx team --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
27
- ${WHITE}tfx team --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
28
- ${WHITE}tfx team --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
29
-
30
- ${BOLD}제어${RESET}
31
- ${WHITE}tfx team status${RESET} ${GRAY}현재 팀 상태${RESET}
32
- ${WHITE}tfx team debug${RESET} ${DIM}[--lines 30]${RESET} ${GRAY}강화 디버그 출력(환경/세션/pane tail)${RESET}
33
- ${WHITE}tfx team tasks${RESET} ${GRAY}공유 태스크 목록${RESET}
34
- ${WHITE}tfx team task${RESET} ${DIM}<pending|progress|done> <T1>${RESET} ${GRAY}태스크 상태 갱신${RESET}
35
- ${WHITE}tfx team attach${RESET} ${DIM}[--wt]${RESET} ${GRAY}세션 재연결 (WT 분할은 opt-in)${RESET}
36
- ${WHITE}tfx team focus${RESET} ${DIM}<lead|이름|번호> [--wt]${RESET} ${GRAY}특정 팀메이트 포커스${RESET}
37
- ${WHITE}tfx team send${RESET} ${DIM}<lead|이름|번호> "msg"${RESET} ${GRAY}팀메이트에 메시지 주입${RESET}
38
- ${WHITE}tfx team interrupt${RESET} ${DIM}<대상>${RESET} ${GRAY}팀메이트 인터럽트(C-c)${RESET}
39
- ${WHITE}tfx team control${RESET} ${DIM}<대상> <cmd>${RESET} ${GRAY}리드 제어명령(interrupt|stop|pause|resume)${RESET}
40
- ${WHITE}tfx team stop${RESET} ${GRAY}graceful 종료${RESET}
41
- ${WHITE}tfx team kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
42
- ${WHITE}tfx team list${RESET} ${GRAY}활성 세션 목록${RESET}
43
-
44
- ${BOLD}키 조작(Claude teammate 스타일, tmux 모드)${RESET}
45
- ${WHITE}Shift+Down${RESET} ${GRAY}다음 팀메이트${RESET}
46
- ${WHITE}Shift+Tab${RESET} ${GRAY}이전 팀메이트 (권장)${RESET}
47
- ${WHITE}Shift+Left${RESET} ${GRAY}이전 팀메이트 (대체)${RESET}
48
- ${WHITE}Shift+Up${RESET} ${GRAY}미지원 (Claude Code가 캡처 불가, scroll-up 충돌)${RESET}
49
- ${WHITE}Escape${RESET} ${GRAY}현재 팀메이트 인터럽트${RESET}
50
- ${WHITE}Ctrl+T${RESET} ${GRAY}태스크 목록 토글${RESET}
51
- `);
52
- }
53
-
54
- /**
55
- * tfx team 서브커맨드 라우터
56
- * bin/triflux.mjs에서 호출
57
- */
58
- export async function cmdTeam() {
59
- const rawSub = process.argv[3];
60
- const sub = typeof rawSub === "string" ? rawSub.toLowerCase() : rawSub;
61
-
62
- switch (sub) {
63
- case "status":
64
- return teamStatus();
65
- case "debug":
66
- return teamDebug();
67
- case "tasks":
68
- return teamTasks();
69
- case "task":
70
- return teamTaskUpdate();
71
- case "attach":
72
- return teamAttach();
73
- case "focus":
74
- return teamFocus();
75
- case "interrupt":
76
- return teamInterrupt();
77
- case "control":
78
- return teamControl();
79
- case "stop":
80
- return teamStop();
81
- case "kill":
82
- return teamKill();
83
- case "send":
84
- return teamSend();
85
- case "list":
86
- return teamList();
87
- case "help":
88
- case "--help":
89
- case "-h":
90
- return teamHelp();
91
- case undefined:
92
- return teamHelp();
93
- default:
94
- if (typeof sub === "string" && !sub.startsWith("-") && TEAM_SUBCOMMANDS.has(sub)) {
95
- return teamHelp();
96
- }
97
- return teamStart();
98
- }
99
- }
16
+
17
+ function teamHelp() {
18
+ console.log(`
19
+ ${AMBER}${BOLD}⬡ tfx multi${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
20
+
21
+ ${BOLD}시작${RESET}
22
+ ${WHITE}tfx multi "작업 설명"${RESET}
23
+ ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}
24
+ ${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
25
+ ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
26
+ ${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
27
+ ${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
28
+ ${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
29
+
30
+ ${BOLD}제어${RESET}
31
+ ${WHITE}tfx multi status${RESET} ${GRAY}현재 팀 상태${RESET}
32
+ ${WHITE}tfx multi debug${RESET} ${DIM}[--lines 30]${RESET} ${GRAY}강화 디버그 출력(환경/세션/pane tail)${RESET}
33
+ ${WHITE}tfx multi tasks${RESET} ${GRAY}공유 태스크 목록${RESET}
34
+ ${WHITE}tfx multi task${RESET} ${DIM}<pending|progress|done> <T1>${RESET} ${GRAY}태스크 상태 갱신${RESET}
35
+ ${WHITE}tfx multi attach${RESET} ${DIM}[--wt]${RESET} ${GRAY}세션 재연결 (WT 분할은 opt-in)${RESET}
36
+ ${WHITE}tfx multi focus${RESET} ${DIM}<lead|이름|번호> [--wt]${RESET} ${GRAY}특정 팀메이트 포커스${RESET}
37
+ ${WHITE}tfx multi send${RESET} ${DIM}<lead|이름|번호> "msg"${RESET} ${GRAY}팀메이트에 메시지 주입${RESET}
38
+ ${WHITE}tfx multi interrupt${RESET} ${DIM}<대상>${RESET} ${GRAY}팀메이트 인터럽트(C-c)${RESET}
39
+ ${WHITE}tfx multi control${RESET} ${DIM}<대상> <cmd>${RESET} ${GRAY}리드 제어명령(interrupt|stop|pause|resume)${RESET}
40
+ ${WHITE}tfx multi stop${RESET} ${GRAY}graceful 종료${RESET}
41
+ ${WHITE}tfx multi kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
42
+ ${WHITE}tfx multi list${RESET} ${GRAY}활성 세션 목록${RESET}
43
+
44
+ ${BOLD}키 조작(Claude teammate 스타일, tmux 모드)${RESET}
45
+ ${WHITE}Shift+Down${RESET} ${GRAY}다음 팀메이트${RESET}
46
+ ${WHITE}Shift+Tab${RESET} ${GRAY}이전 팀메이트 (권장)${RESET}
47
+ ${WHITE}Shift+Left${RESET} ${GRAY}이전 팀메이트 (대체)${RESET}
48
+ ${WHITE}Shift+Up${RESET} ${GRAY}미지원 (Claude Code가 캡처 불가, scroll-up 충돌)${RESET}
49
+ ${WHITE}Escape${RESET} ${GRAY}현재 팀메이트 인터럽트${RESET}
50
+ ${WHITE}Ctrl+T${RESET} ${GRAY}태스크 목록 토글${RESET}
51
+ `);
52
+ }
53
+
54
+ /**
55
+ * tfx multi 서브커맨드 라우터
56
+ * bin/triflux.mjs에서 호출
57
+ */
58
+ export async function cmdTeam() {
59
+ const rawSub = process.argv[3];
60
+ const sub = typeof rawSub === "string" ? rawSub.toLowerCase() : rawSub;
61
+
62
+ switch (sub) {
63
+ case "status":
64
+ return teamStatus();
65
+ case "debug":
66
+ return teamDebug();
67
+ case "tasks":
68
+ return teamTasks();
69
+ case "task":
70
+ return teamTaskUpdate();
71
+ case "attach":
72
+ return teamAttach();
73
+ case "focus":
74
+ return teamFocus();
75
+ case "interrupt":
76
+ return teamInterrupt();
77
+ case "control":
78
+ return teamControl();
79
+ case "stop":
80
+ return teamStop();
81
+ case "kill":
82
+ return teamKill();
83
+ case "send":
84
+ return teamSend();
85
+ case "list":
86
+ return teamList();
87
+ case "help":
88
+ case "--help":
89
+ case "-h":
90
+ return teamHelp();
91
+ case undefined:
92
+ return teamHelp();
93
+ default:
94
+ if (typeof sub === "string" && !sub.startsWith("-") && TEAM_SUBCOMMANDS.has(sub)) {
95
+ return teamHelp();
96
+ }
97
+ return teamStart();
98
+ }
99
+ }