triflux 3.1.0-dev.5 → 3.2.0-dev.2
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 +192 -43
- package/hooks/hooks.json +12 -0
- package/hub/bridge.mjs +137 -51
- package/hub/server.mjs +100 -29
- package/hub/team/cli.mjs +1335 -0
- package/hub/team/dashboard.mjs +166 -0
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +92 -0
- package/hub/team/nativeProxy.mjs +455 -0
- package/hub/team/orchestrator.mjs +166 -0
- package/hub/team/pane.mjs +110 -0
- package/hub/team/session.mjs +529 -0
- package/hub/tools.mjs +113 -15
- package/package.json +1 -1
- package/scripts/setup.mjs +95 -0
- package/scripts/team-keyword.mjs +35 -0
- package/scripts/tfx-route.sh +13 -73
- package/skills/tfx-team/SKILL.md +290 -0
package/hub/team/cli.mjs
ADDED
|
@@ -0,0 +1,1335 @@
|
|
|
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 {
|
|
9
|
+
createSession,
|
|
10
|
+
createWtSession,
|
|
11
|
+
attachSession,
|
|
12
|
+
resolveAttachCommand,
|
|
13
|
+
killSession,
|
|
14
|
+
closeWtSession,
|
|
15
|
+
sessionExists,
|
|
16
|
+
getSessionAttachedCount,
|
|
17
|
+
listSessions,
|
|
18
|
+
capturePaneOutput,
|
|
19
|
+
focusPane,
|
|
20
|
+
focusWtPane,
|
|
21
|
+
configureTeammateKeybindings,
|
|
22
|
+
detectMultiplexer,
|
|
23
|
+
hasWindowsTerminal,
|
|
24
|
+
hasWindowsTerminalSession,
|
|
25
|
+
} from "./session.mjs";
|
|
26
|
+
import { buildCliCommand, startCliInPane, injectPrompt, sendKeys } from "./pane.mjs";
|
|
27
|
+
import { orchestrate, decomposeTask, buildLeadPrompt, buildPrompt } from "./orchestrator.mjs";
|
|
28
|
+
|
|
29
|
+
// ── 상수 ──
|
|
30
|
+
const PKG_ROOT = dirname(dirname(dirname(new URL(import.meta.url).pathname))).replace(/^\/([A-Z]:)/, "$1");
|
|
31
|
+
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
32
|
+
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
33
|
+
const TEAM_STATE_FILE = join(HUB_PID_DIR, "team-state.json");
|
|
34
|
+
|
|
35
|
+
const TEAM_SUBCOMMANDS = new Set([
|
|
36
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "tasks", "task", "focus", "interrupt", "control", "debug",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// ── 색상 ──
|
|
40
|
+
const AMBER = "\x1b[38;5;214m";
|
|
41
|
+
const GREEN = "\x1b[38;5;82m";
|
|
42
|
+
const RED = "\x1b[38;5;196m";
|
|
43
|
+
const GRAY = "\x1b[38;5;245m";
|
|
44
|
+
const DIM = "\x1b[2m";
|
|
45
|
+
const BOLD = "\x1b[1m";
|
|
46
|
+
const RESET = "\x1b[0m";
|
|
47
|
+
const WHITE = "\x1b[97m";
|
|
48
|
+
const YELLOW = "\x1b[33m";
|
|
49
|
+
|
|
50
|
+
function ok(msg) { console.log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
51
|
+
function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
52
|
+
function fail(msg) { console.log(` ${RED}✗${RESET} ${msg}`); }
|
|
53
|
+
|
|
54
|
+
// ── 팀 상태 관리 ──
|
|
55
|
+
|
|
56
|
+
function loadTeamState() {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(TEAM_STATE_FILE, "utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveTeamState(state) {
|
|
65
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
66
|
+
writeFileSync(TEAM_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clearTeamState() {
|
|
70
|
+
try { unlinkSync(TEAM_STATE_FILE); } catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Hub 유틸 ──
|
|
74
|
+
|
|
75
|
+
function getHubInfo() {
|
|
76
|
+
if (!existsSync(HUB_PID_FILE)) return null;
|
|
77
|
+
try {
|
|
78
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
79
|
+
process.kill(info.pid, 0); // 프로세스 생존 확인
|
|
80
|
+
return info;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function startHubDaemon() {
|
|
87
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
88
|
+
if (!existsSync(serverPath)) {
|
|
89
|
+
fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
94
|
+
env: { ...process.env },
|
|
95
|
+
stdio: "ignore",
|
|
96
|
+
detached: true,
|
|
97
|
+
});
|
|
98
|
+
child.unref();
|
|
99
|
+
|
|
100
|
+
// PID 파일 확인 (최대 3초 대기)
|
|
101
|
+
const deadline = Date.now() + 3000;
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
104
|
+
return JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
105
|
+
}
|
|
106
|
+
execSync('node -e "setTimeout(()=>{},100)"', { stdio: "ignore", timeout: 500 });
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 인자 파싱 ──
|
|
112
|
+
|
|
113
|
+
function normalizeTeammateMode(mode = "auto") {
|
|
114
|
+
const raw = String(mode).toLowerCase();
|
|
115
|
+
if (raw === "inline" || raw === "native") return "in-process";
|
|
116
|
+
if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
|
|
117
|
+
if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
|
|
118
|
+
if (raw === "auto") {
|
|
119
|
+
return process.env.TMUX ? "tmux" : "in-process";
|
|
120
|
+
}
|
|
121
|
+
return "in-process";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeLayout(layout = "2x2") {
|
|
125
|
+
const raw = String(layout).toLowerCase();
|
|
126
|
+
if (raw === "2x2" || raw === "grid") return "2x2";
|
|
127
|
+
if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns") return "1xN";
|
|
128
|
+
if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
|
|
129
|
+
return "2x2";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseTeamArgs() {
|
|
133
|
+
const args = process.argv.slice(3);
|
|
134
|
+
let agents = ["codex", "gemini"]; // 기본: codex + gemini
|
|
135
|
+
let lead = "claude"; // 기본 리드
|
|
136
|
+
let layout = "2x2";
|
|
137
|
+
let teammateMode = "auto";
|
|
138
|
+
const taskParts = [];
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < args.length; i++) {
|
|
141
|
+
const cur = args[i];
|
|
142
|
+
if (cur === "--agents" && args[i + 1]) {
|
|
143
|
+
agents = args[++i].split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
144
|
+
} else if (cur === "--lead" && args[i + 1]) {
|
|
145
|
+
lead = args[++i].trim().toLowerCase();
|
|
146
|
+
} else if (cur === "--layout" && args[i + 1]) {
|
|
147
|
+
layout = args[++i];
|
|
148
|
+
} else if ((cur === "--teammate-mode" || cur === "--mode") && args[i + 1]) {
|
|
149
|
+
teammateMode = args[++i];
|
|
150
|
+
} else if (!cur.startsWith("-")) {
|
|
151
|
+
taskParts.push(cur);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
agents,
|
|
157
|
+
lead,
|
|
158
|
+
layout: normalizeLayout(layout),
|
|
159
|
+
teammateMode: normalizeTeammateMode(teammateMode),
|
|
160
|
+
task: taskParts.join(" ").trim(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function ensureTmuxOrExit() {
|
|
165
|
+
const mux = detectMultiplexer();
|
|
166
|
+
if (mux) return;
|
|
167
|
+
|
|
168
|
+
console.log(`
|
|
169
|
+
${RED}${BOLD}tmux 미발견${RESET}
|
|
170
|
+
|
|
171
|
+
현재 선택한 모드는 tmux 기반 팀세션이 필요합니다.
|
|
172
|
+
|
|
173
|
+
설치:
|
|
174
|
+
WSL2: ${WHITE}wsl sudo apt install tmux${RESET}
|
|
175
|
+
macOS: ${WHITE}brew install tmux${RESET}
|
|
176
|
+
Linux: ${WHITE}apt install tmux${RESET}
|
|
177
|
+
|
|
178
|
+
Windows에서는 WSL2를 권장합니다:
|
|
179
|
+
1. ${WHITE}wsl --install${RESET}
|
|
180
|
+
2. ${WHITE}wsl sudo apt install tmux${RESET}
|
|
181
|
+
3. ${WHITE}tfx team "작업"${RESET}
|
|
182
|
+
`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function launchAttachInWindowsTerminal(sessionName) {
|
|
187
|
+
if (!hasWindowsTerminal()) return false;
|
|
188
|
+
|
|
189
|
+
let attachSpec;
|
|
190
|
+
try {
|
|
191
|
+
attachSpec = resolveAttachCommand(sessionName);
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const launch = (args) => {
|
|
197
|
+
const child = spawn("wt", args, {
|
|
198
|
+
detached: true,
|
|
199
|
+
stdio: "ignore",
|
|
200
|
+
windowsHide: false,
|
|
201
|
+
});
|
|
202
|
+
child.unref();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const beforeAttached = getSessionAttachedCount(sessionName);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// 분할선이 세로(좌/우)가 되도록 -V 우선
|
|
209
|
+
launch(["-w", "0", "split-pane", "-V", "-d", PKG_ROOT, attachSpec.command, ...attachSpec.args]);
|
|
210
|
+
if (beforeAttached == null) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const deadline = Date.now() + 3500;
|
|
215
|
+
while (Date.now() < deadline) {
|
|
216
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
217
|
+
const nowAttached = getSessionAttachedCount(sessionName);
|
|
218
|
+
if (typeof nowAttached === "number" && nowAttached > beforeAttached) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildManualAttachCommand(sessionName) {
|
|
229
|
+
try {
|
|
230
|
+
const spec = resolveAttachCommand(sessionName);
|
|
231
|
+
const quoted = [spec.command, ...spec.args].map((s) => {
|
|
232
|
+
const v = String(s);
|
|
233
|
+
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
234
|
+
});
|
|
235
|
+
return quoted.join(" ");
|
|
236
|
+
} catch {
|
|
237
|
+
return `tmux attach-session -t ${sessionName}`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function wantsWtAttachFallback() {
|
|
242
|
+
return process.argv.includes("--wt")
|
|
243
|
+
|| process.argv.includes("--spawn-wt")
|
|
244
|
+
|| process.env.TFX_ATTACH_WT_AUTO === "1";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toAgentId(cli, target) {
|
|
248
|
+
const suffix = String(target).split(/[:.]/).pop();
|
|
249
|
+
return `${cli}-${suffix}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildNativeCliCommand(cli) {
|
|
253
|
+
switch (cli) {
|
|
254
|
+
case "codex":
|
|
255
|
+
// 비-TTY supervisor 환경에서 확인 프롬프트/alt-screen 의존을 줄임
|
|
256
|
+
return "codex --dangerously-bypass-approvals-and-sandbox --no-alt-screen";
|
|
257
|
+
case "gemini":
|
|
258
|
+
return "gemini";
|
|
259
|
+
case "claude":
|
|
260
|
+
return "claude";
|
|
261
|
+
default:
|
|
262
|
+
return buildCliCommand(cli);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildTasks(subtasks, workers) {
|
|
267
|
+
return subtasks.map((subtask, i) => ({
|
|
268
|
+
id: `T${i + 1}`,
|
|
269
|
+
title: subtask,
|
|
270
|
+
owner: workers[i]?.name || null,
|
|
271
|
+
status: "pending",
|
|
272
|
+
depends_on: i === 0 ? [] : [`T${i}`],
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderTasks(tasks = []) {
|
|
277
|
+
if (!tasks.length) {
|
|
278
|
+
console.log(`\n ${DIM}태스크 없음${RESET}\n`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(`\n ${AMBER}${BOLD}⬡ Team Tasks${RESET}\n`);
|
|
283
|
+
for (const t of tasks) {
|
|
284
|
+
const dep = t.depends_on?.length ? ` ${DIM}(deps: ${t.depends_on.join(",")})${RESET}` : "";
|
|
285
|
+
const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
|
|
286
|
+
console.log(` ${WHITE}${t.id}${RESET} ${t.status.padEnd(11)} ${t.title}${owner}${dep}`);
|
|
287
|
+
}
|
|
288
|
+
console.log("");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolveMember(state, selector) {
|
|
292
|
+
const members = state?.members || [];
|
|
293
|
+
if (!selector) return null;
|
|
294
|
+
|
|
295
|
+
const direct = members.find((m) => m.name === selector || m.role === selector || m.agentId === selector);
|
|
296
|
+
if (direct) return direct;
|
|
297
|
+
|
|
298
|
+
// 스킬 친화 별칭: worker-1, worker-2 ...
|
|
299
|
+
const workerAlias = /^worker-(\d+)$/i.exec(selector);
|
|
300
|
+
if (workerAlias) {
|
|
301
|
+
const workerIdx = parseInt(workerAlias[1], 10) - 1;
|
|
302
|
+
const workers = members.filter((m) => m.role === "worker");
|
|
303
|
+
if (workerIdx >= 0 && workerIdx < workers.length) return workers[workerIdx];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const n = parseInt(selector, 10);
|
|
307
|
+
if (!Number.isNaN(n)) {
|
|
308
|
+
// 하위 호환: pane 번호 우선
|
|
309
|
+
const byPane = members.find((m) => m.pane?.endsWith(`.${n}`) || m.pane?.endsWith(`:${n}`));
|
|
310
|
+
if (byPane) return byPane;
|
|
311
|
+
|
|
312
|
+
// teammate 스타일: 1-based 인덱스
|
|
313
|
+
if (n >= 1 && n <= members.length) return members[n - 1];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
320
|
+
const hubBase = (state?.hubUrl || "http://127.0.0.1:27888/mcp").replace(/\/mcp$/, "");
|
|
321
|
+
const leadAgent = (state?.members || []).find((m) => m.role === "lead")?.agentId || "lead";
|
|
322
|
+
|
|
323
|
+
const payload = {
|
|
324
|
+
from_agent: leadAgent,
|
|
325
|
+
to_agent: targetMember.agentId,
|
|
326
|
+
command,
|
|
327
|
+
reason,
|
|
328
|
+
payload: {
|
|
329
|
+
issued_by: leadAgent,
|
|
330
|
+
issued_at: Date.now(),
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const res = await fetch(`${hubBase}/bridge/control`, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { "Content-Type": "application/json" },
|
|
338
|
+
body: JSON.stringify(payload),
|
|
339
|
+
});
|
|
340
|
+
return !!res.ok;
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isNativeMode(state) {
|
|
347
|
+
return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isWtMode(state) {
|
|
351
|
+
return state?.teammateMode === "wt";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isTeamAlive(state) {
|
|
355
|
+
if (!state) return false;
|
|
356
|
+
if (isNativeMode(state)) {
|
|
357
|
+
try {
|
|
358
|
+
process.kill(state.native.supervisorPid, 0);
|
|
359
|
+
return true;
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (isWtMode(state)) {
|
|
365
|
+
// WT pane 상태를 신뢰성 있게 조회할 API가 없어 세션 환경/실행기 존재 여부로 판정
|
|
366
|
+
return hasWindowsTerminal() && hasWindowsTerminalSession();
|
|
367
|
+
}
|
|
368
|
+
return sessionExists(state.sessionName);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function nativeRequest(state, path, body = {}) {
|
|
372
|
+
if (!isNativeMode(state)) return null;
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch(`${state.native.controlUrl}${path}`, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: { "Content-Type": "application/json" },
|
|
377
|
+
body: JSON.stringify(body),
|
|
378
|
+
});
|
|
379
|
+
return await res.json();
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function nativeGetStatus(state) {
|
|
386
|
+
if (!isNativeMode(state)) return null;
|
|
387
|
+
try {
|
|
388
|
+
const res = await fetch(`${state.native.controlUrl}/status`);
|
|
389
|
+
return await res.json();
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
|
|
396
|
+
const nativeConfigPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
|
|
397
|
+
const nativeRuntimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
|
|
398
|
+
const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
|
|
399
|
+
mkdirSync(logsDir, { recursive: true });
|
|
400
|
+
|
|
401
|
+
const leadMember = {
|
|
402
|
+
role: "lead",
|
|
403
|
+
name: "lead",
|
|
404
|
+
cli: lead,
|
|
405
|
+
agentId: `${lead}-lead`,
|
|
406
|
+
command: buildNativeCliCommand(lead),
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const workers = agents.map((cli, i) => ({
|
|
410
|
+
role: "worker",
|
|
411
|
+
name: `${cli}-${i + 1}`,
|
|
412
|
+
cli,
|
|
413
|
+
agentId: `${cli}-w${i + 1}`,
|
|
414
|
+
command: buildNativeCliCommand(cli),
|
|
415
|
+
subtask: subtasks[i],
|
|
416
|
+
}));
|
|
417
|
+
|
|
418
|
+
const leadPrompt = buildLeadPrompt(task, {
|
|
419
|
+
agentId: leadMember.agentId,
|
|
420
|
+
hubUrl,
|
|
421
|
+
teammateMode: "in-process",
|
|
422
|
+
workers: workers.map((w) => ({ agentId: w.agentId, cli: w.cli, subtask: w.subtask })),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const members = [
|
|
426
|
+
{ ...leadMember, prompt: leadPrompt },
|
|
427
|
+
...workers.map((w) => ({
|
|
428
|
+
...w,
|
|
429
|
+
prompt: buildPrompt(w.subtask, { cli: w.cli, agentId: w.agentId, hubUrl }),
|
|
430
|
+
})),
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const config = {
|
|
434
|
+
sessionName: sessionId,
|
|
435
|
+
hubUrl,
|
|
436
|
+
startupDelayMs: 3000,
|
|
437
|
+
logsDir,
|
|
438
|
+
runtimeFile: nativeRuntimePath,
|
|
439
|
+
members,
|
|
440
|
+
};
|
|
441
|
+
writeFileSync(nativeConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
442
|
+
|
|
443
|
+
const supervisorPath = join(PKG_ROOT, "hub", "team", "native-supervisor.mjs");
|
|
444
|
+
const child = spawn(process.execPath, [supervisorPath, "--config", nativeConfigPath], {
|
|
445
|
+
detached: true,
|
|
446
|
+
stdio: "ignore",
|
|
447
|
+
env: { ...process.env },
|
|
448
|
+
});
|
|
449
|
+
child.unref();
|
|
450
|
+
|
|
451
|
+
const deadline = Date.now() + 5000;
|
|
452
|
+
while (Date.now() < deadline) {
|
|
453
|
+
if (existsSync(nativeRuntimePath)) {
|
|
454
|
+
try {
|
|
455
|
+
const runtime = JSON.parse(readFileSync(nativeRuntimePath, "utf8"));
|
|
456
|
+
return { runtime, members };
|
|
457
|
+
} catch {}
|
|
458
|
+
}
|
|
459
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { runtime: null, members };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── 서브커맨드 ──
|
|
466
|
+
|
|
467
|
+
async function teamStart() {
|
|
468
|
+
const { agents, lead, layout, teammateMode, task } = parseTeamArgs();
|
|
469
|
+
if (!task) {
|
|
470
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
|
|
471
|
+
console.log(` 사용법: ${WHITE}tfx team "작업 설명"${RESET}`);
|
|
472
|
+
console.log(` ${WHITE}tfx team --agents codex,gemini --lead claude "작업"${RESET}`);
|
|
473
|
+
console.log(` ${WHITE}tfx team --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
|
|
474
|
+
console.log(` ${WHITE}tfx team --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}\n`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
|
|
479
|
+
|
|
480
|
+
let hub = getHubInfo();
|
|
481
|
+
if (!hub) {
|
|
482
|
+
process.stdout.write(" Hub 시작 중...");
|
|
483
|
+
hub = startHubDaemon();
|
|
484
|
+
if (hub) {
|
|
485
|
+
console.log(` ${GREEN}✓${RESET}`);
|
|
486
|
+
} else {
|
|
487
|
+
console.log(` ${RED}✗${RESET}`);
|
|
488
|
+
warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
ok(`Hub: ${DIM}${hub.url}${RESET}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const sessionId = `tfx-team-${Date.now().toString(36).slice(-4)}`;
|
|
495
|
+
const subtasks = decomposeTask(task, agents.length);
|
|
496
|
+
const hubUrl = hub?.url || "http://127.0.0.1:27888/mcp";
|
|
497
|
+
let effectiveTeammateMode = teammateMode;
|
|
498
|
+
|
|
499
|
+
if (teammateMode === "wt") {
|
|
500
|
+
if (!hasWindowsTerminal()) {
|
|
501
|
+
warn("wt.exe 미발견 — in-process 모드로 자동 fallback");
|
|
502
|
+
effectiveTeammateMode = "in-process";
|
|
503
|
+
} else if (!hasWindowsTerminalSession()) {
|
|
504
|
+
warn("WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback");
|
|
505
|
+
effectiveTeammateMode = "in-process";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
|
|
510
|
+
console.log(` 모드: ${effectiveTeammateMode}`);
|
|
511
|
+
console.log(` 리드: ${AMBER}${lead}${RESET}`);
|
|
512
|
+
console.log(` 워커: ${agents.map((a) => `${AMBER}${a}${RESET}`).join(", ")}`);
|
|
513
|
+
|
|
514
|
+
// ── in-process(네이티브): tmux 없이 supervisor가 직접 CLI 프로세스 관리 ──
|
|
515
|
+
if (effectiveTeammateMode === "in-process") {
|
|
516
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
517
|
+
const preview = subtasks[i].length > 44 ? subtasks[i].slice(0, 44) + "…" : subtasks[i];
|
|
518
|
+
console.log(` ${DIM}[${agents[i]}-${i + 1}] ${preview}${RESET}`);
|
|
519
|
+
}
|
|
520
|
+
console.log("");
|
|
521
|
+
|
|
522
|
+
const { runtime, members } = await startNativeSupervisor({
|
|
523
|
+
sessionId,
|
|
524
|
+
task,
|
|
525
|
+
lead,
|
|
526
|
+
agents,
|
|
527
|
+
subtasks,
|
|
528
|
+
hubUrl,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
if (!runtime?.controlUrl) {
|
|
532
|
+
fail("in-process supervisor 시작 실패");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
|
|
537
|
+
|
|
538
|
+
saveTeamState({
|
|
539
|
+
sessionName: sessionId,
|
|
540
|
+
task,
|
|
541
|
+
lead,
|
|
542
|
+
agents,
|
|
543
|
+
layout: "native",
|
|
544
|
+
teammateMode: effectiveTeammateMode,
|
|
545
|
+
startedAt: Date.now(),
|
|
546
|
+
hubUrl,
|
|
547
|
+
members: members.map((m, idx) => ({
|
|
548
|
+
role: m.role,
|
|
549
|
+
name: m.name,
|
|
550
|
+
cli: m.cli,
|
|
551
|
+
agentId: m.agentId,
|
|
552
|
+
pane: `native:${idx}`,
|
|
553
|
+
subtask: m.subtask || null,
|
|
554
|
+
})),
|
|
555
|
+
panes: {},
|
|
556
|
+
tasks,
|
|
557
|
+
native: {
|
|
558
|
+
controlUrl: runtime.controlUrl,
|
|
559
|
+
supervisorPid: runtime.supervisorPid,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
ok("네이티브 in-process 팀 시작 완료");
|
|
564
|
+
console.log(` ${DIM}tmux 없이 실행됨 (직접 CLI 프로세스)${RESET}`);
|
|
565
|
+
console.log(` ${DIM}제어: tfx team send/control/tasks/status${RESET}\n`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── wt 모드(Windows Terminal 독립 split-pane) ──
|
|
570
|
+
if (effectiveTeammateMode === "wt") {
|
|
571
|
+
const paneCount = agents.length + 1; // lead + workers
|
|
572
|
+
const effectiveLayout = layout === "Nx1" ? "Nx1" : "1xN";
|
|
573
|
+
if (layout !== effectiveLayout) {
|
|
574
|
+
warn(`wt 모드에서 ${layout} 레이아웃은 미지원 — ${effectiveLayout}로 대체`);
|
|
575
|
+
}
|
|
576
|
+
console.log(` 레이아웃: ${effectiveLayout} (${paneCount} panes)`);
|
|
577
|
+
|
|
578
|
+
const paneCommands = [
|
|
579
|
+
{
|
|
580
|
+
title: `${sessionId}-lead`,
|
|
581
|
+
command: buildCliCommand(lead),
|
|
582
|
+
cwd: PKG_ROOT,
|
|
583
|
+
},
|
|
584
|
+
...agents.map((cli, i) => ({
|
|
585
|
+
title: `${sessionId}-${cli}-${i + 1}`,
|
|
586
|
+
command: buildCliCommand(cli),
|
|
587
|
+
cwd: PKG_ROOT,
|
|
588
|
+
})),
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
const session = createWtSession(sessionId, {
|
|
592
|
+
layout: effectiveLayout,
|
|
593
|
+
paneCommands,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const members = [
|
|
597
|
+
{
|
|
598
|
+
role: "lead",
|
|
599
|
+
name: "lead",
|
|
600
|
+
cli: lead,
|
|
601
|
+
pane: session.panes[0] || "wt:0",
|
|
602
|
+
agentId: toAgentId(lead, session.panes[0] || "wt:0"),
|
|
603
|
+
},
|
|
604
|
+
];
|
|
605
|
+
|
|
606
|
+
for (let i = 0; i < agents.length; i++) {
|
|
607
|
+
const cli = agents[i];
|
|
608
|
+
const target = session.panes[i + 1] || `wt:${i + 1}`;
|
|
609
|
+
members.push({
|
|
610
|
+
role: "worker",
|
|
611
|
+
name: `${cli}-${i + 1}`,
|
|
612
|
+
cli,
|
|
613
|
+
pane: target,
|
|
614
|
+
subtask: subtasks[i],
|
|
615
|
+
agentId: toAgentId(cli, target),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (const worker of members.filter((m) => m.role === "worker")) {
|
|
620
|
+
const preview = worker.subtask.length > 44 ? worker.subtask.slice(0, 44) + "…" : worker.subtask;
|
|
621
|
+
console.log(` ${DIM}[${worker.name}] ${preview}${RESET}`);
|
|
622
|
+
}
|
|
623
|
+
console.log("");
|
|
624
|
+
|
|
625
|
+
const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
|
|
626
|
+
const panes = {};
|
|
627
|
+
for (const m of members) {
|
|
628
|
+
panes[m.pane] = {
|
|
629
|
+
role: m.role,
|
|
630
|
+
name: m.name,
|
|
631
|
+
cli: m.cli,
|
|
632
|
+
agentId: m.agentId,
|
|
633
|
+
subtask: m.subtask || null,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
saveTeamState({
|
|
638
|
+
sessionName: sessionId,
|
|
639
|
+
task,
|
|
640
|
+
lead,
|
|
641
|
+
agents,
|
|
642
|
+
layout: effectiveLayout,
|
|
643
|
+
teammateMode: effectiveTeammateMode,
|
|
644
|
+
startedAt: Date.now(),
|
|
645
|
+
hubUrl,
|
|
646
|
+
members,
|
|
647
|
+
panes,
|
|
648
|
+
tasks,
|
|
649
|
+
wt: {
|
|
650
|
+
windowId: 0,
|
|
651
|
+
layout: effectiveLayout,
|
|
652
|
+
paneCount: session.paneCount,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
ok("Windows Terminal wt 팀 시작 완료");
|
|
657
|
+
console.log(` ${DIM}현재 pane 기준으로 ${effectiveLayout} 분할 생성됨${RESET}`);
|
|
658
|
+
console.log(` ${DIM}wt 모드는 자동 프롬프트 주입/Hub direct 제어(send/control)가 제한됩니다.${RESET}\n`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── tmux 모드 ──
|
|
663
|
+
ensureTmuxOrExit();
|
|
664
|
+
|
|
665
|
+
const paneCount = agents.length + 1; // lead + workers
|
|
666
|
+
const effectiveLayout = paneCount <= 4 ? layout : (layout === "Nx1" ? "Nx1" : "1xN");
|
|
667
|
+
console.log(` 레이아웃: ${effectiveLayout} (${paneCount} panes)`);
|
|
668
|
+
|
|
669
|
+
const session = createSession(sessionId, {
|
|
670
|
+
layout: effectiveLayout,
|
|
671
|
+
paneCount,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Pane 0: lead
|
|
675
|
+
const leadTarget = session.panes[0];
|
|
676
|
+
startCliInPane(leadTarget, buildCliCommand(lead));
|
|
677
|
+
|
|
678
|
+
// Pane 1..N: workers
|
|
679
|
+
const assignments = [];
|
|
680
|
+
const members = [
|
|
681
|
+
{
|
|
682
|
+
role: "lead",
|
|
683
|
+
name: "lead",
|
|
684
|
+
cli: lead,
|
|
685
|
+
pane: leadTarget,
|
|
686
|
+
agentId: toAgentId(lead, leadTarget),
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
for (let i = 0; i < agents.length; i++) {
|
|
691
|
+
const cli = agents[i];
|
|
692
|
+
const target = session.panes[i + 1];
|
|
693
|
+
startCliInPane(target, buildCliCommand(cli));
|
|
694
|
+
|
|
695
|
+
const worker = {
|
|
696
|
+
role: "worker",
|
|
697
|
+
name: `${cli}-${i + 1}`,
|
|
698
|
+
cli,
|
|
699
|
+
pane: target,
|
|
700
|
+
subtask: subtasks[i],
|
|
701
|
+
agentId: toAgentId(cli, target),
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
members.push(worker);
|
|
705
|
+
assignments.push({ target, cli, subtask: subtasks[i] });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
for (const worker of members.filter((m) => m.role === "worker")) {
|
|
709
|
+
const preview = worker.subtask.length > 44 ? worker.subtask.slice(0, 44) + "…" : worker.subtask;
|
|
710
|
+
console.log(` ${DIM}[${worker.name}] ${preview}${RESET}`);
|
|
711
|
+
}
|
|
712
|
+
console.log("");
|
|
713
|
+
|
|
714
|
+
ok("CLI 초기화 대기 (3초)...");
|
|
715
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
716
|
+
|
|
717
|
+
await orchestrate(sessionId, assignments, {
|
|
718
|
+
hubUrl,
|
|
719
|
+
teammateMode: effectiveTeammateMode,
|
|
720
|
+
lead: {
|
|
721
|
+
target: leadTarget,
|
|
722
|
+
cli: lead,
|
|
723
|
+
task,
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
ok("리드/워커 프롬프트 주입 완료");
|
|
727
|
+
|
|
728
|
+
const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
|
|
729
|
+
const panes = {};
|
|
730
|
+
for (const m of members) {
|
|
731
|
+
panes[m.pane] = {
|
|
732
|
+
role: m.role,
|
|
733
|
+
name: m.name,
|
|
734
|
+
cli: m.cli,
|
|
735
|
+
agentId: m.agentId,
|
|
736
|
+
subtask: m.subtask || null,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
saveTeamState({
|
|
741
|
+
sessionName: sessionId,
|
|
742
|
+
task,
|
|
743
|
+
lead,
|
|
744
|
+
agents,
|
|
745
|
+
layout: effectiveLayout,
|
|
746
|
+
teammateMode: effectiveTeammateMode,
|
|
747
|
+
startedAt: Date.now(),
|
|
748
|
+
hubUrl,
|
|
749
|
+
members,
|
|
750
|
+
panes,
|
|
751
|
+
tasks,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const taskListCommand = `${process.execPath} ${join(PKG_ROOT, "bin", "triflux.mjs")} team tasks`;
|
|
755
|
+
configureTeammateKeybindings(sessionId, {
|
|
756
|
+
inProcess: false,
|
|
757
|
+
taskListCommand,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
console.log(`\n ${GREEN}${BOLD}팀 세션 준비 완료${RESET}`);
|
|
761
|
+
console.log(` ${DIM}Shift+Down: 다음 팀메이트 전환${RESET}`);
|
|
762
|
+
console.log(` ${DIM}Escape: 현재 팀메이트 인터럽트${RESET}`);
|
|
763
|
+
console.log(` ${DIM}Ctrl+T: 태스크 목록${RESET}`);
|
|
764
|
+
console.log(` ${DIM}Ctrl+B → D: 세션 분리 (백그라운드)${RESET}\n`);
|
|
765
|
+
|
|
766
|
+
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
767
|
+
attachSession(sessionId);
|
|
768
|
+
} else {
|
|
769
|
+
warn("TTY 미지원 환경이라 자동 attach를 생략함");
|
|
770
|
+
console.log(` ${DIM}수동 연결: tfx team attach${RESET}\n`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function teamStatus() {
|
|
775
|
+
const state = loadTeamState();
|
|
776
|
+
if (!state) {
|
|
777
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const alive = isTeamAlive(state);
|
|
782
|
+
const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
|
|
783
|
+
const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
|
|
784
|
+
|
|
785
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET} ${status}\n`);
|
|
786
|
+
console.log(` 세션: ${state.sessionName}`);
|
|
787
|
+
console.log(` 모드: ${state.teammateMode || "tmux"}`);
|
|
788
|
+
console.log(` 리드: ${state.lead || "claude"}`);
|
|
789
|
+
console.log(` 워커: ${(state.agents || []).join(", ")}`);
|
|
790
|
+
console.log(` Uptime: ${uptime}`);
|
|
791
|
+
console.log(` 태스크: ${(state.tasks || []).length}`);
|
|
792
|
+
|
|
793
|
+
const members = state.members || [];
|
|
794
|
+
if (members.length) {
|
|
795
|
+
console.log("");
|
|
796
|
+
for (const m of members) {
|
|
797
|
+
const roleTag = m.role === "lead" ? "lead" : "worker";
|
|
798
|
+
console.log(` - ${m.name} (${m.cli}) ${DIM}${roleTag}${RESET} ${DIM}${m.pane}${RESET}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (isNativeMode(state) && alive) {
|
|
803
|
+
const native = await nativeGetStatus(state);
|
|
804
|
+
const nativeMembers = native?.data?.members || [];
|
|
805
|
+
if (nativeMembers.length) {
|
|
806
|
+
console.log("");
|
|
807
|
+
for (const m of nativeMembers) {
|
|
808
|
+
console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
console.log("");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function teamTasks() {
|
|
817
|
+
const state = loadTeamState();
|
|
818
|
+
if (!state || !isTeamAlive(state)) {
|
|
819
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
renderTasks(state.tasks || []);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function teamTaskUpdate() {
|
|
826
|
+
const state = loadTeamState();
|
|
827
|
+
if (!state || !isTeamAlive(state)) {
|
|
828
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const action = (process.argv[4] || "").toLowerCase();
|
|
833
|
+
const taskId = (process.argv[5] || "").toUpperCase();
|
|
834
|
+
|
|
835
|
+
const nextStatus = action === "done" || action === "complete" || action === "completed"
|
|
836
|
+
? "completed"
|
|
837
|
+
: action === "progress" || action === "in-progress" || action === "in_progress"
|
|
838
|
+
? "in_progress"
|
|
839
|
+
: action === "pending"
|
|
840
|
+
? "pending"
|
|
841
|
+
: null;
|
|
842
|
+
|
|
843
|
+
if (!nextStatus || !taskId) {
|
|
844
|
+
console.log(`\n 사용법: ${WHITE}tfx team task <pending|progress|done> <T1>${RESET}\n`);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const tasks = state.tasks || [];
|
|
849
|
+
const target = tasks.find((t) => String(t.id).toUpperCase() === taskId);
|
|
850
|
+
if (!target) {
|
|
851
|
+
console.log(`\n ${DIM}태스크를 찾을 수 없음: ${taskId}${RESET}\n`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
target.status = nextStatus;
|
|
856
|
+
saveTeamState(state);
|
|
857
|
+
ok(`${target.id} 상태 갱신: ${nextStatus}`);
|
|
858
|
+
console.log("");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function teamAttach() {
|
|
862
|
+
const state = loadTeamState();
|
|
863
|
+
if (!state || !isTeamAlive(state)) {
|
|
864
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (isNativeMode(state)) {
|
|
869
|
+
console.log(`\n ${DIM}in-process 모드는 별도 attach가 없습니다.${RESET}`);
|
|
870
|
+
console.log(` ${DIM}상태 확인: tfx team status${RESET}\n`);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (isWtMode(state)) {
|
|
875
|
+
console.log(`\n ${DIM}wt 모드는 attach 개념이 없습니다 (Windows Terminal pane가 독립 실행됨).${RESET}`);
|
|
876
|
+
console.log(` ${DIM}재실행/정리는: tfx team stop${RESET}\n`);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
attachSession(state.sessionName);
|
|
882
|
+
} catch (e) {
|
|
883
|
+
const allowWt = wantsWtAttachFallback();
|
|
884
|
+
if (allowWt && await launchAttachInWindowsTerminal(state.sessionName)) {
|
|
885
|
+
warn(`현재 터미널에서 attach 실패: ${e.message}`);
|
|
886
|
+
ok("Windows Terminal split-pane로 attach 재시도 창을 열었습니다.");
|
|
887
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
888
|
+
console.log("");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
fail(`attach 실패: ${e.message}`);
|
|
892
|
+
if (allowWt) {
|
|
893
|
+
fail("WT 분할창 attach 자동 검증 실패 (session_attached 증가 없음)");
|
|
894
|
+
} else {
|
|
895
|
+
warn("자동 WT 분할은 기본 비활성입니다. 필요 시 --wt 옵션으로 실행하세요.");
|
|
896
|
+
}
|
|
897
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
898
|
+
console.log("");
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async function teamDebug() {
|
|
904
|
+
const state = loadTeamState();
|
|
905
|
+
const linesIdx = process.argv.findIndex((a) => a === "--lines" || a === "-n");
|
|
906
|
+
const lines = linesIdx !== -1 ? Math.max(3, parseInt(process.argv[linesIdx + 1] || "20", 10) || 20) : 20;
|
|
907
|
+
const mux = detectMultiplexer() || "none";
|
|
908
|
+
const hub = getHubInfo();
|
|
909
|
+
|
|
910
|
+
console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
|
|
911
|
+
console.log(` platform: ${process.platform}`);
|
|
912
|
+
console.log(` node: ${process.version}`);
|
|
913
|
+
console.log(` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`);
|
|
914
|
+
console.log(` mux: ${mux}`);
|
|
915
|
+
console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
|
|
916
|
+
console.log(` hub-url: ${hub?.url || "-"}`);
|
|
917
|
+
|
|
918
|
+
const sessions = listSessions();
|
|
919
|
+
console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
|
|
920
|
+
|
|
921
|
+
if (!state) {
|
|
922
|
+
console.log(`\n ${DIM}team-state 없음 (활성 세션 없음)${RESET}\n`);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
console.log(`\n ${BOLD}state${RESET}`);
|
|
927
|
+
console.log(` session: ${state.sessionName}`);
|
|
928
|
+
console.log(` mode: ${state.teammateMode || "tmux"}`);
|
|
929
|
+
console.log(` lead: ${state.lead}`);
|
|
930
|
+
console.log(` agents: ${(state.agents || []).join(", ")}`);
|
|
931
|
+
console.log(` alive: ${isTeamAlive(state) ? "yes" : "no"}`);
|
|
932
|
+
const attached = getSessionAttachedCount(state.sessionName);
|
|
933
|
+
console.log(` attached: ${attached == null ? "-" : attached}`);
|
|
934
|
+
|
|
935
|
+
if (isWtMode(state)) {
|
|
936
|
+
const wtState = state.wt || {};
|
|
937
|
+
console.log(`\n ${BOLD}wt-session${RESET}`);
|
|
938
|
+
console.log(` window: ${wtState.windowId ?? 0}`);
|
|
939
|
+
console.log(` layout: ${wtState.layout || state.layout || "-"}`);
|
|
940
|
+
console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
|
|
941
|
+
console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
|
|
942
|
+
console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
|
|
943
|
+
console.log("");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (isNativeMode(state)) {
|
|
948
|
+
const native = await nativeGetStatus(state);
|
|
949
|
+
const members = native?.data?.members || [];
|
|
950
|
+
console.log(`\n ${BOLD}native-members${RESET}`);
|
|
951
|
+
if (!members.length) {
|
|
952
|
+
console.log(` ${DIM}(no data)${RESET}`);
|
|
953
|
+
} else {
|
|
954
|
+
for (const m of members) {
|
|
955
|
+
console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
console.log("");
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const members = state.members || [];
|
|
963
|
+
console.log(`\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`);
|
|
964
|
+
if (!members.length) {
|
|
965
|
+
console.log(` ${DIM}(members 없음)${RESET}`);
|
|
966
|
+
} else {
|
|
967
|
+
for (const m of members) {
|
|
968
|
+
const tail = capturePaneOutput(m.pane, lines) || "(empty)";
|
|
969
|
+
console.log(`\n [${m.name}] ${m.pane}`);
|
|
970
|
+
const tailLines = tail.split("\n").slice(-lines);
|
|
971
|
+
for (const line of tailLines) {
|
|
972
|
+
console.log(` ${line}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
console.log("");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function teamFocus() {
|
|
980
|
+
const state = loadTeamState();
|
|
981
|
+
if (!state || !isTeamAlive(state)) {
|
|
982
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (isNativeMode(state)) {
|
|
987
|
+
console.log(`\n ${DIM}in-process 모드는 focus/attach 개념이 없습니다.${RESET}`);
|
|
988
|
+
console.log(` ${DIM}직접 지시: tfx team send <대상> \"메시지\"${RESET}\n`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const selector = process.argv[4];
|
|
993
|
+
const member = resolveMember(state, selector);
|
|
994
|
+
if (!member) {
|
|
995
|
+
console.log(`\n 사용법: ${WHITE}tfx team focus <lead|이름|번호>${RESET}\n`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (isWtMode(state)) {
|
|
1000
|
+
const m = /^wt:(\d+)$/.exec(member.pane || "");
|
|
1001
|
+
const paneIndex = m ? parseInt(m[1], 10) : NaN;
|
|
1002
|
+
if (!Number.isFinite(paneIndex)) {
|
|
1003
|
+
warn(`wt pane 인덱스 파싱 실패: ${member.pane}`);
|
|
1004
|
+
console.log("");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const focused = focusWtPane(paneIndex, {
|
|
1008
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
1009
|
+
});
|
|
1010
|
+
if (focused) {
|
|
1011
|
+
ok(`${member.name} pane 포커스 이동 (wt)`);
|
|
1012
|
+
} else {
|
|
1013
|
+
warn("wt pane 포커스 이동 실패 (WT_SESSION/wt.exe 상태 확인 필요)");
|
|
1014
|
+
}
|
|
1015
|
+
console.log("");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
focusPane(member.pane, { zoom: (state.teammateMode === "in-process") });
|
|
1020
|
+
try {
|
|
1021
|
+
attachSession(state.sessionName);
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
const allowWt = wantsWtAttachFallback();
|
|
1024
|
+
if (allowWt && await launchAttachInWindowsTerminal(state.sessionName)) {
|
|
1025
|
+
warn(`현재 터미널에서 attach 실패: ${e.message}`);
|
|
1026
|
+
ok("Windows Terminal split-pane로 attach 재시도 창을 열었습니다.");
|
|
1027
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
1028
|
+
console.log("");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
fail(`attach 실패: ${e.message}`);
|
|
1032
|
+
if (allowWt) {
|
|
1033
|
+
fail("WT 분할창 attach 자동 검증 실패 (session_attached 증가 없음)");
|
|
1034
|
+
} else {
|
|
1035
|
+
warn("자동 WT 분할은 기본 비활성입니다. 필요 시 --wt 옵션으로 실행하세요.");
|
|
1036
|
+
}
|
|
1037
|
+
console.log(` ${DIM}수동 attach 명령: ${buildManualAttachCommand(state.sessionName)}${RESET}`);
|
|
1038
|
+
console.log("");
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function teamInterrupt() {
|
|
1044
|
+
const state = loadTeamState();
|
|
1045
|
+
if (!state || !isTeamAlive(state)) {
|
|
1046
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const selector = process.argv[4] || "lead";
|
|
1051
|
+
const member = resolveMember(state, selector);
|
|
1052
|
+
if (!member) {
|
|
1053
|
+
console.log(`\n 사용법: ${WHITE}tfx team interrupt <lead|이름|번호>${RESET}\n`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (isWtMode(state)) {
|
|
1058
|
+
warn("wt 모드에서는 pane stdin 주입이 지원되지 않아 interrupt를 자동 전송할 수 없습니다.");
|
|
1059
|
+
console.log(` ${DIM}수동으로 해당 pane에서 Ctrl+C를 입력하세요.${RESET}`);
|
|
1060
|
+
console.log("");
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (isNativeMode(state)) {
|
|
1065
|
+
const result = await nativeRequest(state, "/interrupt", { member: member.name });
|
|
1066
|
+
if (result?.ok) {
|
|
1067
|
+
ok(`${member.name} 인터럽트 전송`);
|
|
1068
|
+
} else {
|
|
1069
|
+
warn(`${member.name} 인터럽트 실패`);
|
|
1070
|
+
}
|
|
1071
|
+
console.log("");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
sendKeys(member.pane, "C-c");
|
|
1076
|
+
ok(`${member.name} 인터럽트 전송`);
|
|
1077
|
+
console.log("");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function teamControl() {
|
|
1081
|
+
const state = loadTeamState();
|
|
1082
|
+
if (!state || !isTeamAlive(state)) {
|
|
1083
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const selector = process.argv[4];
|
|
1088
|
+
const command = (process.argv[5] || "").toLowerCase();
|
|
1089
|
+
const reason = process.argv.slice(6).join(" ");
|
|
1090
|
+
const member = resolveMember(state, selector);
|
|
1091
|
+
const allowed = new Set(["interrupt", "stop", "pause", "resume"]);
|
|
1092
|
+
|
|
1093
|
+
if (!member || !allowed.has(command)) {
|
|
1094
|
+
console.log(`\n 사용법: ${WHITE}tfx team control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (isWtMode(state)) {
|
|
1099
|
+
warn("wt 모드는 Hub direct/control 주입 경로가 비활성입니다.");
|
|
1100
|
+
console.log(` ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}`);
|
|
1101
|
+
console.log("");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// 직접 주입: MCP 유무와 무관하게 즉시 전달
|
|
1106
|
+
let directOk = false;
|
|
1107
|
+
if (isNativeMode(state)) {
|
|
1108
|
+
const direct = await nativeRequest(state, "/control", {
|
|
1109
|
+
member: member.name,
|
|
1110
|
+
command,
|
|
1111
|
+
reason,
|
|
1112
|
+
});
|
|
1113
|
+
directOk = !!direct?.ok;
|
|
1114
|
+
} else {
|
|
1115
|
+
const controlMsg = `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`;
|
|
1116
|
+
injectPrompt(member.pane, controlMsg);
|
|
1117
|
+
if (command === "interrupt") {
|
|
1118
|
+
sendKeys(member.pane, "C-c");
|
|
1119
|
+
}
|
|
1120
|
+
directOk = true;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Hub direct mailbox에도 발행
|
|
1124
|
+
const published = await publishLeadControl(state, member, command, reason);
|
|
1125
|
+
|
|
1126
|
+
if (directOk && published) {
|
|
1127
|
+
ok(`${member.name} 제어 전송 (${command}, direct + hub)`);
|
|
1128
|
+
} else if (directOk) {
|
|
1129
|
+
ok(`${member.name} 제어 전송 (${command}, direct only)`);
|
|
1130
|
+
} else {
|
|
1131
|
+
warn(`${member.name} 제어 전송 실패 (${command})`);
|
|
1132
|
+
}
|
|
1133
|
+
console.log("");
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
async function teamStop() {
|
|
1137
|
+
const state = loadTeamState();
|
|
1138
|
+
if (!state) {
|
|
1139
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (isNativeMode(state)) {
|
|
1144
|
+
await nativeRequest(state, "/stop", {});
|
|
1145
|
+
try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
|
|
1146
|
+
ok(`세션 종료: ${state.sessionName}`);
|
|
1147
|
+
} else if (isWtMode(state)) {
|
|
1148
|
+
const closed = closeWtSession({
|
|
1149
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
1150
|
+
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
1151
|
+
});
|
|
1152
|
+
ok(`세션 종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
|
|
1153
|
+
} else {
|
|
1154
|
+
if (sessionExists(state.sessionName)) {
|
|
1155
|
+
killSession(state.sessionName);
|
|
1156
|
+
ok(`세션 종료: ${state.sessionName}`);
|
|
1157
|
+
} else {
|
|
1158
|
+
console.log(` ${DIM}세션 이미 종료됨${RESET}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
clearTeamState();
|
|
1163
|
+
console.log("");
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async function teamKill() {
|
|
1167
|
+
const state = loadTeamState();
|
|
1168
|
+
if (state && isNativeMode(state) && isTeamAlive(state)) {
|
|
1169
|
+
await nativeRequest(state, "/stop", {});
|
|
1170
|
+
try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
|
|
1171
|
+
clearTeamState();
|
|
1172
|
+
ok(`종료: ${state.sessionName}`);
|
|
1173
|
+
console.log("");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (state && isWtMode(state)) {
|
|
1177
|
+
const closed = closeWtSession({
|
|
1178
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
1179
|
+
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
1180
|
+
});
|
|
1181
|
+
clearTeamState();
|
|
1182
|
+
ok(`종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
|
|
1183
|
+
console.log("");
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const sessions = listSessions();
|
|
1188
|
+
if (sessions.length === 0) {
|
|
1189
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
for (const s of sessions) {
|
|
1193
|
+
killSession(s);
|
|
1194
|
+
ok(`종료: ${s}`);
|
|
1195
|
+
}
|
|
1196
|
+
clearTeamState();
|
|
1197
|
+
console.log("");
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function teamSend() {
|
|
1201
|
+
const state = loadTeamState();
|
|
1202
|
+
if (!state || !isTeamAlive(state)) {
|
|
1203
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const selector = process.argv[4];
|
|
1208
|
+
const message = process.argv.slice(5).join(" ");
|
|
1209
|
+
const member = resolveMember(state, selector);
|
|
1210
|
+
if (!member || !message) {
|
|
1211
|
+
console.log(`\n 사용법: ${WHITE}tfx team send <lead|이름|번호> "메시지"${RESET}\n`);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (isWtMode(state)) {
|
|
1216
|
+
warn("wt 모드는 pane 프롬프트 자동 주입(send)이 지원되지 않습니다.");
|
|
1217
|
+
console.log(` ${DIM}수동 전달: 선택한 pane에 직접 붙여넣으세요.${RESET}`);
|
|
1218
|
+
console.log("");
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (isNativeMode(state)) {
|
|
1223
|
+
const result = await nativeRequest(state, "/send", { member: member.name, text: message });
|
|
1224
|
+
if (result?.ok) {
|
|
1225
|
+
ok(`${member.name}에 메시지 주입 완료`);
|
|
1226
|
+
} else {
|
|
1227
|
+
warn(`${member.name} 메시지 주입 실패`);
|
|
1228
|
+
}
|
|
1229
|
+
console.log("");
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
injectPrompt(member.pane, message);
|
|
1234
|
+
ok(`${member.name}에 메시지 주입 완료`);
|
|
1235
|
+
console.log("");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function teamList() {
|
|
1239
|
+
const state = loadTeamState();
|
|
1240
|
+
if (state && isNativeMode(state) && isTeamAlive(state)) {
|
|
1241
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
1242
|
+
console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(in-process)${RESET}`);
|
|
1243
|
+
console.log("");
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (state && isWtMode(state) && isTeamAlive(state)) {
|
|
1247
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
1248
|
+
console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(wt)${RESET}`);
|
|
1249
|
+
console.log("");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const sessions = listSessions();
|
|
1254
|
+
if (sessions.length === 0) {
|
|
1255
|
+
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
1259
|
+
for (const s of sessions) {
|
|
1260
|
+
console.log(` ${GREEN}●${RESET} ${s}`);
|
|
1261
|
+
}
|
|
1262
|
+
console.log("");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function teamHelp() {
|
|
1266
|
+
console.log(`
|
|
1267
|
+
${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
|
|
1268
|
+
|
|
1269
|
+
${BOLD}시작${RESET}
|
|
1270
|
+
${WHITE}tfx team "작업 설명"${RESET}
|
|
1271
|
+
${WHITE}tfx team --agents codex,gemini --lead claude "작업"${RESET}
|
|
1272
|
+
${WHITE}tfx team --teammate-mode tmux "작업"${RESET}
|
|
1273
|
+
${WHITE}tfx team --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
|
|
1274
|
+
${WHITE}tfx team --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
|
|
1275
|
+
${WHITE}tfx team --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
|
|
1276
|
+
${WHITE}tfx team --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
|
|
1277
|
+
|
|
1278
|
+
${BOLD}제어${RESET}
|
|
1279
|
+
${WHITE}tfx team status${RESET} ${GRAY}현재 팀 상태${RESET}
|
|
1280
|
+
${WHITE}tfx team debug${RESET} ${DIM}[--lines 30]${RESET} ${GRAY}강화 디버그 출력(환경/세션/pane tail)${RESET}
|
|
1281
|
+
${WHITE}tfx team tasks${RESET} ${GRAY}공유 태스크 목록${RESET}
|
|
1282
|
+
${WHITE}tfx team task${RESET} ${DIM}<pending|progress|done> <T1>${RESET} ${GRAY}태스크 상태 갱신${RESET}
|
|
1283
|
+
${WHITE}tfx team attach${RESET} ${DIM}[--wt]${RESET} ${GRAY}세션 재연결 (WT 분할은 opt-in)${RESET}
|
|
1284
|
+
${WHITE}tfx team focus${RESET} ${DIM}<lead|이름|번호> [--wt]${RESET} ${GRAY}특정 팀메이트 포커스${RESET}
|
|
1285
|
+
${WHITE}tfx team send${RESET} ${DIM}<lead|이름|번호> "msg"${RESET} ${GRAY}팀메이트에 메시지 주입${RESET}
|
|
1286
|
+
${WHITE}tfx team interrupt${RESET} ${DIM}<대상>${RESET} ${GRAY}팀메이트 인터럽트(C-c)${RESET}
|
|
1287
|
+
${WHITE}tfx team control${RESET} ${DIM}<대상> <cmd>${RESET} ${GRAY}리드 제어명령(interrupt|stop|pause|resume)${RESET}
|
|
1288
|
+
${WHITE}tfx team stop${RESET} ${GRAY}graceful 종료${RESET}
|
|
1289
|
+
${WHITE}tfx team kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
|
|
1290
|
+
${WHITE}tfx team list${RESET} ${GRAY}활성 세션 목록${RESET}
|
|
1291
|
+
|
|
1292
|
+
${BOLD}키 조작(Claude teammate 스타일, tmux 모드)${RESET}
|
|
1293
|
+
${WHITE}Shift+Down${RESET} ${GRAY}다음 팀메이트${RESET}
|
|
1294
|
+
${WHITE}Shift+Up${RESET} ${GRAY}이전 팀메이트${RESET}
|
|
1295
|
+
${WHITE}Escape${RESET} ${GRAY}현재 팀메이트 인터럽트${RESET}
|
|
1296
|
+
${WHITE}Ctrl+T${RESET} ${GRAY}태스크 목록 토글${RESET}
|
|
1297
|
+
`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ── 메인 진입점 ──
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* tfx team 서브커맨드 라우터
|
|
1304
|
+
* bin/triflux.mjs에서 호출
|
|
1305
|
+
*/
|
|
1306
|
+
export async function cmdTeam() {
|
|
1307
|
+
const sub = process.argv[3];
|
|
1308
|
+
|
|
1309
|
+
switch (sub) {
|
|
1310
|
+
case "status": return teamStatus();
|
|
1311
|
+
case "debug": return teamDebug();
|
|
1312
|
+
case "tasks": return teamTasks();
|
|
1313
|
+
case "task": return teamTaskUpdate();
|
|
1314
|
+
case "attach": return teamAttach();
|
|
1315
|
+
case "focus": return teamFocus();
|
|
1316
|
+
case "interrupt": return teamInterrupt();
|
|
1317
|
+
case "control": return teamControl();
|
|
1318
|
+
case "stop": return teamStop();
|
|
1319
|
+
case "kill": return teamKill();
|
|
1320
|
+
case "send": return teamSend();
|
|
1321
|
+
case "list": return teamList();
|
|
1322
|
+
case "help":
|
|
1323
|
+
case "--help":
|
|
1324
|
+
case "-h":
|
|
1325
|
+
return teamHelp();
|
|
1326
|
+
case undefined:
|
|
1327
|
+
return teamHelp();
|
|
1328
|
+
default:
|
|
1329
|
+
// 서브커맨드가 아니면 작업 문자열로 간주
|
|
1330
|
+
if (!sub.startsWith("-") && TEAM_SUBCOMMANDS.has(sub)) {
|
|
1331
|
+
return teamHelp();
|
|
1332
|
+
}
|
|
1333
|
+
return teamStart();
|
|
1334
|
+
}
|
|
1335
|
+
}
|