triflux 3.2.0-dev.1 → 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 +185 -43
- package/hooks/hooks.json +12 -0
- package/hub/bridge.mjs +137 -51
- package/hub/server.mjs +100 -29
- package/hub/team/cli.mjs +1080 -113
- 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 +99 -35
- package/hub/team/pane.mjs +18 -9
- package/hub/team/session.mjs +359 -16
- 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 +204 -86
package/hub/team/cli.mjs
CHANGED
|
@@ -5,10 +5,26 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
import {
|
|
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";
|
|
9
26
|
import { buildCliCommand, startCliInPane, injectPrompt, sendKeys } from "./pane.mjs";
|
|
10
|
-
import { orchestrate, decomposeTask } from "./orchestrator.mjs";
|
|
11
|
-
import { detectMultiplexer } from "./session.mjs";
|
|
27
|
+
import { orchestrate, decomposeTask, buildLeadPrompt, buildPrompt } from "./orchestrator.mjs";
|
|
12
28
|
|
|
13
29
|
// ── 상수 ──
|
|
14
30
|
const PKG_ROOT = dirname(dirname(dirname(new URL(import.meta.url).pathname))).replace(/^\/([A-Z]:)/, "$1");
|
|
@@ -16,6 +32,10 @@ const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
|
16
32
|
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
17
33
|
const TEAM_STATE_FILE = join(HUB_PID_DIR, "team-state.json");
|
|
18
34
|
|
|
35
|
+
const TEAM_SUBCOMMANDS = new Set([
|
|
36
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "tasks", "task", "focus", "interrupt", "control", "debug",
|
|
37
|
+
]);
|
|
38
|
+
|
|
19
39
|
// ── 색상 ──
|
|
20
40
|
const AMBER = "\x1b[38;5;214m";
|
|
21
41
|
const GREEN = "\x1b[38;5;82m";
|
|
@@ -90,35 +110,67 @@ function startHubDaemon() {
|
|
|
90
110
|
|
|
91
111
|
// ── 인자 파싱 ──
|
|
92
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
|
+
|
|
93
132
|
function parseTeamArgs() {
|
|
94
133
|
const args = process.argv.slice(3);
|
|
95
|
-
let agents = ["codex", "
|
|
134
|
+
let agents = ["codex", "gemini"]; // 기본: codex + gemini
|
|
135
|
+
let lead = "claude"; // 기본 리드
|
|
96
136
|
let layout = "2x2";
|
|
97
|
-
let
|
|
137
|
+
let teammateMode = "auto";
|
|
138
|
+
const taskParts = [];
|
|
98
139
|
|
|
99
140
|
for (let i = 0; i < args.length; i++) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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]) {
|
|
103
147
|
layout = args[++i];
|
|
104
|
-
} else if (
|
|
105
|
-
|
|
148
|
+
} else if ((cur === "--teammate-mode" || cur === "--mode") && args[i + 1]) {
|
|
149
|
+
teammateMode = args[++i];
|
|
150
|
+
} else if (!cur.startsWith("-")) {
|
|
151
|
+
taskParts.push(cur);
|
|
106
152
|
}
|
|
107
153
|
}
|
|
108
154
|
|
|
109
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
agents,
|
|
157
|
+
lead,
|
|
158
|
+
layout: normalizeLayout(layout),
|
|
159
|
+
teammateMode: normalizeTeammateMode(teammateMode),
|
|
160
|
+
task: taskParts.join(" ").trim(),
|
|
161
|
+
};
|
|
110
162
|
}
|
|
111
163
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
async function teamStart() {
|
|
115
|
-
// 1. tmux 확인
|
|
164
|
+
function ensureTmuxOrExit() {
|
|
116
165
|
const mux = detectMultiplexer();
|
|
117
|
-
if (
|
|
118
|
-
|
|
166
|
+
if (mux) return;
|
|
167
|
+
|
|
168
|
+
console.log(`
|
|
119
169
|
${RED}${BOLD}tmux 미발견${RESET}
|
|
120
170
|
|
|
121
|
-
|
|
171
|
+
현재 선택한 모드는 tmux 기반 팀세션이 필요합니다.
|
|
172
|
+
|
|
173
|
+
설치:
|
|
122
174
|
WSL2: ${WHITE}wsl sudo apt install tmux${RESET}
|
|
123
175
|
macOS: ${WHITE}brew install tmux${RESET}
|
|
124
176
|
Linux: ${WHITE}apt install tmux${RESET}
|
|
@@ -128,156 +180,1011 @@ async function teamStart() {
|
|
|
128
180
|
2. ${WHITE}wsl sudo apt install tmux${RESET}
|
|
129
181
|
3. ${WHITE}tfx team "작업"${RESET}
|
|
130
182
|
`);
|
|
131
|
-
|
|
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;
|
|
132
194
|
}
|
|
133
195
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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`);
|
|
141
279
|
return;
|
|
142
280
|
}
|
|
143
281
|
|
|
144
|
-
|
|
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
|
+
|
|
145
478
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
|
|
479
|
+
|
|
146
480
|
let hub = getHubInfo();
|
|
147
481
|
if (!hub) {
|
|
148
|
-
process.stdout.write(
|
|
482
|
+
process.stdout.write(" Hub 시작 중...");
|
|
149
483
|
hub = startHubDaemon();
|
|
150
484
|
if (hub) {
|
|
151
485
|
console.log(` ${GREEN}✓${RESET}`);
|
|
152
486
|
} else {
|
|
153
487
|
console.log(` ${RED}✗${RESET}`);
|
|
154
488
|
warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
|
|
155
|
-
// Hub 없이도 계속 진행 (통신만 불가)
|
|
156
489
|
}
|
|
157
490
|
} else {
|
|
158
491
|
ok(`Hub: ${DIM}${hub.url}${RESET}`);
|
|
159
492
|
}
|
|
160
493
|
|
|
161
|
-
|
|
162
|
-
const
|
|
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("");
|
|
163
521
|
|
|
164
|
-
|
|
165
|
-
|
|
522
|
+
const { runtime, members } = await startNativeSupervisor({
|
|
523
|
+
sessionId,
|
|
524
|
+
task,
|
|
525
|
+
lead,
|
|
526
|
+
agents,
|
|
527
|
+
subtasks,
|
|
528
|
+
hubUrl,
|
|
529
|
+
});
|
|
166
530
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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)`);
|
|
175
668
|
|
|
176
|
-
// 6. tmux 세션 생성
|
|
177
669
|
const session = createSession(sessionId, {
|
|
178
|
-
layout,
|
|
179
|
-
paneCount
|
|
670
|
+
layout: effectiveLayout,
|
|
671
|
+
paneCount,
|
|
180
672
|
});
|
|
181
673
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
startCliInPane(
|
|
674
|
+
// Pane 0: lead
|
|
675
|
+
const leadTarget = session.panes[0];
|
|
676
|
+
startCliInPane(leadTarget, buildCliCommand(lead));
|
|
185
677
|
|
|
186
|
-
//
|
|
678
|
+
// Pane 1..N: workers
|
|
187
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
|
+
|
|
188
690
|
for (let i = 0; i < agents.length; i++) {
|
|
189
691
|
const cli = agents[i];
|
|
190
692
|
const target = session.panes[i + 1];
|
|
191
|
-
|
|
192
|
-
|
|
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);
|
|
193
705
|
assignments.push({ target, cli, subtask: subtasks[i] });
|
|
194
706
|
}
|
|
195
707
|
|
|
196
|
-
|
|
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
|
+
|
|
197
714
|
ok("CLI 초기화 대기 (3초)...");
|
|
198
715
|
await new Promise((r) => setTimeout(r, 3000));
|
|
199
716
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
717
|
+
await orchestrate(sessionId, assignments, {
|
|
718
|
+
hubUrl,
|
|
719
|
+
teammateMode: effectiveTeammateMode,
|
|
720
|
+
lead: {
|
|
721
|
+
target: leadTarget,
|
|
722
|
+
cli: lead,
|
|
723
|
+
task,
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
ok("리드/워커 프롬프트 주입 완료");
|
|
204
727
|
|
|
205
|
-
|
|
206
|
-
const panes = {
|
|
207
|
-
for (
|
|
208
|
-
panes[
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,
|
|
212
737
|
};
|
|
213
738
|
}
|
|
739
|
+
|
|
214
740
|
saveTeamState({
|
|
215
741
|
sessionName: sessionId,
|
|
216
|
-
agents,
|
|
217
742
|
task,
|
|
218
|
-
|
|
219
|
-
|
|
743
|
+
lead,
|
|
744
|
+
agents,
|
|
745
|
+
layout: effectiveLayout,
|
|
746
|
+
teammateMode: effectiveTeammateMode,
|
|
747
|
+
startedAt: Date.now(),
|
|
220
748
|
hubUrl,
|
|
749
|
+
members,
|
|
221
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,
|
|
222
758
|
});
|
|
223
759
|
|
|
224
|
-
// 12. tmux attach
|
|
225
760
|
console.log(`\n ${GREEN}${BOLD}팀 세션 준비 완료${RESET}`);
|
|
226
|
-
console.log(` ${DIM}
|
|
227
|
-
console.log(` ${DIM}
|
|
228
|
-
|
|
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
|
+
}
|
|
229
772
|
}
|
|
230
773
|
|
|
231
|
-
function teamStatus() {
|
|
774
|
+
async function teamStatus() {
|
|
232
775
|
const state = loadTeamState();
|
|
233
776
|
if (!state) {
|
|
234
777
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
235
778
|
return;
|
|
236
779
|
}
|
|
237
780
|
|
|
238
|
-
const alive =
|
|
781
|
+
const alive = isTeamAlive(state);
|
|
239
782
|
const status = alive ? `${GREEN}● active${RESET}` : `${RED}● dead${RESET}`;
|
|
240
783
|
const uptime = alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-";
|
|
241
784
|
|
|
242
785
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET} ${status}\n`);
|
|
243
786
|
console.log(` 세션: ${state.sessionName}`);
|
|
244
|
-
console.log(`
|
|
245
|
-
console.log(`
|
|
787
|
+
console.log(` 모드: ${state.teammateMode || "tmux"}`);
|
|
788
|
+
console.log(` 리드: ${state.lead || "claude"}`);
|
|
789
|
+
console.log(` 워커: ${(state.agents || []).join(", ")}`);
|
|
246
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
|
+
|
|
247
813
|
console.log("");
|
|
248
814
|
}
|
|
249
815
|
|
|
250
|
-
function
|
|
816
|
+
function teamTasks() {
|
|
251
817
|
const state = loadTeamState();
|
|
252
|
-
if (!state || !
|
|
818
|
+
if (!state || !isTeamAlive(state)) {
|
|
253
819
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
254
820
|
return;
|
|
255
821
|
}
|
|
256
|
-
|
|
822
|
+
renderTasks(state.tasks || []);
|
|
257
823
|
}
|
|
258
824
|
|
|
259
|
-
function
|
|
825
|
+
function teamTaskUpdate() {
|
|
260
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
|
+
|
|
261
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)) {
|
|
262
1083
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
263
1084
|
return;
|
|
264
1085
|
}
|
|
265
1086
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
1114
|
} else {
|
|
270
|
-
|
|
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
|
+
}
|
|
271
1160
|
}
|
|
272
1161
|
|
|
273
|
-
// 상태 파일 정리
|
|
274
1162
|
clearTeamState();
|
|
275
1163
|
console.log("");
|
|
276
1164
|
}
|
|
277
1165
|
|
|
278
|
-
function teamKill() {
|
|
279
|
-
|
|
280
|
-
|
|
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();
|
|
281
1188
|
if (sessions.length === 0) {
|
|
282
1189
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
283
1190
|
return;
|
|
@@ -290,28 +1197,60 @@ function teamKill() {
|
|
|
290
1197
|
console.log("");
|
|
291
1198
|
}
|
|
292
1199
|
|
|
293
|
-
function teamSend() {
|
|
1200
|
+
async function teamSend() {
|
|
294
1201
|
const state = loadTeamState();
|
|
295
|
-
if (!state || !
|
|
1202
|
+
if (!state || !isTeamAlive(state)) {
|
|
296
1203
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
297
1204
|
return;
|
|
298
1205
|
}
|
|
299
1206
|
|
|
300
|
-
const
|
|
1207
|
+
const selector = process.argv[4];
|
|
301
1208
|
const message = process.argv.slice(5).join(" ");
|
|
302
|
-
|
|
303
|
-
|
|
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("");
|
|
304
1230
|
return;
|
|
305
1231
|
}
|
|
306
1232
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
ok(`Pane ${paneIdx}에 메시지 주입 완료`);
|
|
1233
|
+
injectPrompt(member.pane, message);
|
|
1234
|
+
ok(`${member.name}에 메시지 주입 완료`);
|
|
310
1235
|
console.log("");
|
|
311
1236
|
}
|
|
312
1237
|
|
|
313
|
-
function teamList() {
|
|
314
|
-
const
|
|
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();
|
|
315
1254
|
if (sessions.length === 0) {
|
|
316
1255
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
317
1256
|
return;
|
|
@@ -325,20 +1264,36 @@ function teamList() {
|
|
|
325
1264
|
|
|
326
1265
|
function teamHelp() {
|
|
327
1266
|
console.log(`
|
|
328
|
-
${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (
|
|
1267
|
+
${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
|
|
329
1268
|
|
|
330
|
-
${BOLD}시작${RESET}
|
|
331
|
-
${WHITE}tfx team "작업 설명"${RESET}
|
|
332
|
-
${WHITE}tfx team --agents codex,gemini "작업"${RESET}
|
|
333
|
-
${WHITE}tfx team --
|
|
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}
|
|
334
1277
|
|
|
335
1278
|
${BOLD}제어${RESET}
|
|
336
|
-
${WHITE}tfx team status${RESET}
|
|
337
|
-
${WHITE}tfx team
|
|
338
|
-
${WHITE}tfx team
|
|
339
|
-
${WHITE}tfx team
|
|
340
|
-
${WHITE}tfx team
|
|
341
|
-
${WHITE}tfx team
|
|
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}
|
|
342
1297
|
`);
|
|
343
1298
|
}
|
|
344
1299
|
|
|
@@ -352,17 +1307,29 @@ export async function cmdTeam() {
|
|
|
352
1307
|
const sub = process.argv[3];
|
|
353
1308
|
|
|
354
1309
|
switch (sub) {
|
|
355
|
-
case "status":
|
|
356
|
-
case "
|
|
357
|
-
case "
|
|
358
|
-
case "
|
|
359
|
-
case "
|
|
360
|
-
case "
|
|
361
|
-
case "
|
|
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":
|
|
362
1325
|
return teamHelp();
|
|
363
1326
|
case undefined:
|
|
364
1327
|
return teamHelp();
|
|
365
1328
|
default:
|
|
1329
|
+
// 서브커맨드가 아니면 작업 문자열로 간주
|
|
1330
|
+
if (!sub.startsWith("-") && TEAM_SUBCOMMANDS.has(sub)) {
|
|
1331
|
+
return teamHelp();
|
|
1332
|
+
}
|
|
366
1333
|
return teamStart();
|
|
367
1334
|
}
|
|
368
1335
|
}
|