triflux 3.2.0-dev.3 → 3.2.0-dev.6
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/README.ko.md +19 -13
- package/README.md +19 -13
- package/bin/triflux.mjs +144 -67
- package/hooks/hooks.json +2 -2
- package/hooks/keyword-rules.json +338 -0
- package/hub/server.mjs +19 -10
- package/hub/team/cli.mjs +606 -442
- package/hub/team/dashboard.mjs +164 -55
- package/hub/team/native.mjs +38 -0
- package/hub/team/session.mjs +39 -23
- package/hud/hud-qos-status.mjs +56 -1
- package/package.json +3 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +82 -0
- package/scripts/keyword-detector.mjs +257 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +165 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +36 -16
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route.sh +482 -418
- package/skills/tfx-auto-codex/SKILL.md +79 -0
- package/skills/tfx-team/SKILL.md +108 -63
- package/scripts/team-keyword.mjs +0 -35
package/hub/team/cli.mjs
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
// hub/team/cli.mjs — tfx team CLI 진입점
|
|
2
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 {
|
|
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";
|
|
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 { 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
26
|
import { buildCliCommand, startCliInPane, injectPrompt, sendKeys } from "./pane.mjs";
|
|
27
27
|
import { orchestrate, decomposeTask, buildLeadPrompt, buildPrompt } from "./orchestrator.mjs";
|
|
28
28
|
|
|
29
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
|
|
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 LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
34
|
+
const TEAM_PROFILE = (() => {
|
|
35
|
+
const raw = String(process.env.TFX_TEAM_PROFILE || "team").trim().toLowerCase();
|
|
36
|
+
return raw === "codex-team" ? "codex-team" : "team";
|
|
37
|
+
})();
|
|
38
|
+
const TEAM_STATE_FILE = join(
|
|
39
|
+
HUB_PID_DIR,
|
|
40
|
+
TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json",
|
|
41
|
+
);
|
|
34
42
|
|
|
35
43
|
const TEAM_SUBCOMMANDS = new Set([
|
|
36
44
|
"status", "attach", "stop", "kill", "send", "list", "help", "tasks", "task", "focus", "interrupt", "control", "debug",
|
|
@@ -61,66 +69,163 @@ function loadTeamState() {
|
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
function saveTeamState(state) {
|
|
65
|
-
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
function clearTeamState() {
|
|
70
|
-
try { unlinkSync(TEAM_STATE_FILE); } catch {}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Hub 유틸 ──
|
|
72
|
+
function saveTeamState(state) {
|
|
73
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
74
|
+
const nextState = { ...state, profile: TEAM_PROFILE };
|
|
75
|
+
writeFileSync(TEAM_STATE_FILE, JSON.stringify(nextState, null, 2) + "\n");
|
|
76
|
+
}
|
|
74
77
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
function clearTeamState() {
|
|
79
|
+
try { unlinkSync(TEAM_STATE_FILE); } catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Hub 유틸 ──
|
|
83
|
+
|
|
84
|
+
function formatHostForUrl(host) {
|
|
85
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildHubBaseUrl(host, port) {
|
|
89
|
+
return `http://${formatHostForUrl(host)}:${port}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getDefaultHubPort() {
|
|
93
|
+
const envPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
94
|
+
return Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 27888;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getDefaultHubUrl() {
|
|
98
|
+
return `${buildHubBaseUrl("127.0.0.1", getDefaultHubPort())}/mcp`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getDefaultHubBase() {
|
|
102
|
+
return getDefaultHubUrl().replace(/\/mcp$/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeLoopbackHost(host) {
|
|
106
|
+
if (typeof host !== "string") return "127.0.0.1";
|
|
107
|
+
const candidate = host.trim();
|
|
108
|
+
return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function probeHubStatus(host, port, timeoutMs = 1500) {
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
|
|
114
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) return null;
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
return data?.hub ? data : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getHubInfo() {
|
|
125
|
+
const probePort = getDefaultHubPort();
|
|
126
|
+
|
|
127
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
128
|
+
try {
|
|
129
|
+
const raw = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
130
|
+
const pid = Number(raw?.pid);
|
|
131
|
+
if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
|
|
132
|
+
process.kill(pid, 0); // 프로세스 생존 확인
|
|
133
|
+
const host = normalizeLoopbackHost(raw?.host);
|
|
134
|
+
const port = Number(raw.port) || 27888;
|
|
135
|
+
const status = await probeHubStatus(host, port, 1200);
|
|
136
|
+
if (!status) {
|
|
137
|
+
// transient timeout/응답 지연은 stale로 단정하지 않고 기존 PID 정보를 유지한다.
|
|
138
|
+
return {
|
|
139
|
+
...raw,
|
|
140
|
+
pid,
|
|
141
|
+
host,
|
|
142
|
+
port,
|
|
143
|
+
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
144
|
+
degraded: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
...raw,
|
|
149
|
+
pid,
|
|
150
|
+
host,
|
|
151
|
+
port,
|
|
152
|
+
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// PID 파일이 없거나 stale인 경우에도 실제 Hub가 떠 있으면 재사용
|
|
160
|
+
const candidates = Array.from(new Set([probePort, 27888]));
|
|
161
|
+
for (const portCandidate of candidates) {
|
|
162
|
+
const data = await probeHubStatus("127.0.0.1", portCandidate, 1200);
|
|
163
|
+
if (!data) continue;
|
|
164
|
+
const port = Number(data.port) || portCandidate;
|
|
165
|
+
const pid = Number(data.pid);
|
|
166
|
+
const recovered = {
|
|
167
|
+
pid: Number.isFinite(pid) ? pid : null,
|
|
168
|
+
host: "127.0.0.1",
|
|
169
|
+
port,
|
|
170
|
+
url: `${buildHubBaseUrl("127.0.0.1", port)}/mcp`,
|
|
171
|
+
discovered: true,
|
|
172
|
+
};
|
|
173
|
+
if (Number.isFinite(recovered.pid) && recovered.pid > 0) {
|
|
174
|
+
try {
|
|
175
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
176
|
+
writeFileSync(HUB_PID_FILE, JSON.stringify({
|
|
177
|
+
pid: recovered.pid,
|
|
178
|
+
port: recovered.port,
|
|
179
|
+
host: recovered.host,
|
|
180
|
+
url: recovered.url,
|
|
181
|
+
started: Date.now(),
|
|
182
|
+
}));
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
return recovered;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function startHubDaemon() {
|
|
191
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
192
|
+
if (!existsSync(serverPath)) {
|
|
193
|
+
fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
|
|
82
194
|
return null;
|
|
83
195
|
}
|
|
84
|
-
}
|
|
85
196
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
197
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
198
|
+
env: { ...process.env },
|
|
199
|
+
stdio: "ignore",
|
|
200
|
+
detached: true,
|
|
201
|
+
});
|
|
202
|
+
child.unref();
|
|
203
|
+
|
|
204
|
+
const expectedPort = getDefaultHubPort();
|
|
205
|
+
|
|
206
|
+
// Hub 상태 확인 (최대 3초 대기)
|
|
207
|
+
const deadline = Date.now() + 3000;
|
|
208
|
+
while (Date.now() < deadline) {
|
|
209
|
+
const info = await getHubInfo();
|
|
210
|
+
if (info && info.port === expectedPort) return info;
|
|
211
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
92
215
|
|
|
93
|
-
|
|
94
|
-
env: { ...process.env },
|
|
95
|
-
stdio: "ignore",
|
|
96
|
-
detached: true,
|
|
97
|
-
});
|
|
98
|
-
child.unref();
|
|
216
|
+
// ── 인자 파싱 ──
|
|
99
217
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
218
|
+
function normalizeTeammateMode(mode = "auto") {
|
|
219
|
+
const raw = String(mode).toLowerCase();
|
|
220
|
+
if (raw === "inline" || raw === "native") return "in-process";
|
|
221
|
+
if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
|
|
222
|
+
if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
|
|
223
|
+
if (raw === "auto") {
|
|
224
|
+
return process.env.TMUX ? "tmux" : "in-process";
|
|
107
225
|
}
|
|
108
|
-
return
|
|
226
|
+
return "in-process";
|
|
109
227
|
}
|
|
110
228
|
|
|
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
229
|
function normalizeLayout(layout = "2x2") {
|
|
125
230
|
const raw = String(layout).toLowerCase();
|
|
126
231
|
if (raw === "2x2" || raw === "grid") return "2x2";
|
|
@@ -183,8 +288,8 @@ function ensureTmuxOrExit() {
|
|
|
183
288
|
process.exit(1);
|
|
184
289
|
}
|
|
185
290
|
|
|
186
|
-
async function launchAttachInWindowsTerminal(sessionName) {
|
|
187
|
-
if (!hasWindowsTerminal()) return false;
|
|
291
|
+
async function launchAttachInWindowsTerminal(sessionName) {
|
|
292
|
+
if (!hasWindowsTerminal()) return false;
|
|
188
293
|
|
|
189
294
|
let attachSpec;
|
|
190
295
|
try {
|
|
@@ -244,10 +349,10 @@ function wantsWtAttachFallback() {
|
|
|
244
349
|
|| process.env.TFX_ATTACH_WT_AUTO === "1";
|
|
245
350
|
}
|
|
246
351
|
|
|
247
|
-
function toAgentId(cli, target) {
|
|
248
|
-
const suffix = String(target).split(/[:.]/).pop();
|
|
249
|
-
return `${cli}-${suffix}`;
|
|
250
|
-
}
|
|
352
|
+
function toAgentId(cli, target) {
|
|
353
|
+
const suffix = String(target).split(/[:.]/).pop();
|
|
354
|
+
return `${cli}-${suffix}`;
|
|
355
|
+
}
|
|
251
356
|
|
|
252
357
|
function buildNativeCliCommand(cli) {
|
|
253
358
|
switch (cli) {
|
|
@@ -303,11 +408,11 @@ function resolveMember(state, selector) {
|
|
|
303
408
|
if (workerIdx >= 0 && workerIdx < workers.length) return workers[workerIdx];
|
|
304
409
|
}
|
|
305
410
|
|
|
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;
|
|
411
|
+
const n = parseInt(selector, 10);
|
|
412
|
+
if (!Number.isNaN(n)) {
|
|
413
|
+
// 하위 호환: pane 번호 우선
|
|
414
|
+
const byPane = members.find((m) => m.pane?.endsWith(`.${n}`) || m.pane?.endsWith(`:${n}`));
|
|
415
|
+
if (byPane) return byPane;
|
|
311
416
|
|
|
312
417
|
// teammate 스타일: 1-based 인덱스
|
|
313
418
|
if (n >= 1 && n <= members.length) return members[n - 1];
|
|
@@ -316,8 +421,8 @@ function resolveMember(state, selector) {
|
|
|
316
421
|
return null;
|
|
317
422
|
}
|
|
318
423
|
|
|
319
|
-
async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
320
|
-
const hubBase = (state?.hubUrl ||
|
|
424
|
+
async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
425
|
+
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
321
426
|
const leadAgent = (state?.members || []).find((m) => m.role === "lead")?.agentId || "lead";
|
|
322
427
|
|
|
323
428
|
const payload = {
|
|
@@ -343,27 +448,29 @@ async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
|
343
448
|
}
|
|
344
449
|
}
|
|
345
450
|
|
|
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
|
-
}
|
|
451
|
+
function isNativeMode(state) {
|
|
452
|
+
return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function isWtMode(state) {
|
|
456
|
+
return state?.teammateMode === "wt";
|
|
457
|
+
}
|
|
353
458
|
|
|
354
459
|
function isTeamAlive(state) {
|
|
355
460
|
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
|
-
}
|
|
461
|
+
if (isNativeMode(state)) {
|
|
462
|
+
try {
|
|
463
|
+
process.kill(state.native.supervisorPid, 0);
|
|
464
|
+
return true;
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
364
469
|
if (isWtMode(state)) {
|
|
365
|
-
// WT pane 상태를 신뢰성 있게 조회할 API가
|
|
366
|
-
|
|
470
|
+
// WT pane 상태를 신뢰성 있게 조회할 API가 없어, WT_SESSION은 힌트로만 사용한다.
|
|
471
|
+
if (!hasWindowsTerminal()) return false;
|
|
472
|
+
if (hasWindowsTerminalSession()) return true;
|
|
473
|
+
return Array.isArray(state.members) && state.members.length > 0;
|
|
367
474
|
}
|
|
368
475
|
return sessionExists(state.sessionName);
|
|
369
476
|
}
|
|
@@ -464,23 +571,23 @@ async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks,
|
|
|
464
571
|
|
|
465
572
|
// ── 서브커맨드 ──
|
|
466
573
|
|
|
467
|
-
async function teamStart() {
|
|
574
|
+
async function teamStart() {
|
|
468
575
|
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
|
-
}
|
|
576
|
+
if (!task) {
|
|
577
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
|
|
578
|
+
console.log(` 사용법: ${WHITE}tfx team "작업 설명"${RESET}`);
|
|
579
|
+
console.log(` ${WHITE}tfx team --agents codex,gemini --lead claude "작업"${RESET}`);
|
|
580
|
+
console.log(` ${WHITE}tfx team --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
|
|
581
|
+
console.log(` ${WHITE}tfx team --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}\n`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
477
584
|
|
|
478
585
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
|
|
479
586
|
|
|
480
|
-
let hub = getHubInfo();
|
|
481
|
-
if (!hub) {
|
|
482
|
-
process.stdout.write(" Hub 시작 중...");
|
|
483
|
-
hub = startHubDaemon();
|
|
587
|
+
let hub = await getHubInfo();
|
|
588
|
+
if (!hub) {
|
|
589
|
+
process.stdout.write(" Hub 시작 중...");
|
|
590
|
+
hub = await startHubDaemon();
|
|
484
591
|
if (hub) {
|
|
485
592
|
console.log(` ${GREEN}✓${RESET}`);
|
|
486
593
|
} else {
|
|
@@ -491,28 +598,28 @@ async function teamStart() {
|
|
|
491
598
|
ok(`Hub: ${DIM}${hub.url}${RESET}`);
|
|
492
599
|
}
|
|
493
600
|
|
|
494
|
-
const sessionId = `tfx-team-${Date.now().toString(36).slice(-4)}`;
|
|
495
|
-
const subtasks = decomposeTask(task, agents.length);
|
|
496
|
-
const hubUrl = hub?.url ||
|
|
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") {
|
|
601
|
+
const sessionId = `tfx-team-${Date.now().toString(36).slice(-4)}`;
|
|
602
|
+
const subtasks = decomposeTask(task, agents.length);
|
|
603
|
+
const hubUrl = hub?.url || getDefaultHubUrl();
|
|
604
|
+
let effectiveTeammateMode = teammateMode;
|
|
605
|
+
|
|
606
|
+
if (teammateMode === "wt") {
|
|
607
|
+
if (!hasWindowsTerminal()) {
|
|
608
|
+
warn("wt.exe 미발견 — in-process 모드로 자동 fallback");
|
|
609
|
+
effectiveTeammateMode = "in-process";
|
|
610
|
+
} else if (!hasWindowsTerminalSession()) {
|
|
611
|
+
warn("WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback");
|
|
612
|
+
effectiveTeammateMode = "in-process";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
|
|
617
|
+
console.log(` 모드: ${effectiveTeammateMode}`);
|
|
618
|
+
console.log(` 리드: ${AMBER}${lead}${RESET}`);
|
|
619
|
+
console.log(` 워커: ${agents.map((a) => `${AMBER}${a}${RESET}`).join(", ")}`);
|
|
620
|
+
|
|
621
|
+
// ── in-process(네이티브): tmux 없이 supervisor가 직접 CLI 프로세스 관리 ──
|
|
622
|
+
if (effectiveTeammateMode === "in-process") {
|
|
516
623
|
for (let i = 0; i < subtasks.length; i++) {
|
|
517
624
|
const preview = subtasks[i].length > 44 ? subtasks[i].slice(0, 44) + "…" : subtasks[i];
|
|
518
625
|
console.log(` ${DIM}[${agents[i]}-${i + 1}] ${preview}${RESET}`);
|
|
@@ -540,10 +647,10 @@ async function teamStart() {
|
|
|
540
647
|
task,
|
|
541
648
|
lead,
|
|
542
649
|
agents,
|
|
543
|
-
layout: "native",
|
|
544
|
-
teammateMode: effectiveTeammateMode,
|
|
545
|
-
startedAt: Date.now(),
|
|
546
|
-
hubUrl,
|
|
650
|
+
layout: "native",
|
|
651
|
+
teammateMode: effectiveTeammateMode,
|
|
652
|
+
startedAt: Date.now(),
|
|
653
|
+
hubUrl,
|
|
547
654
|
members: members.map((m, idx) => ({
|
|
548
655
|
role: m.role,
|
|
549
656
|
name: m.name,
|
|
@@ -564,103 +671,103 @@ async function teamStart() {
|
|
|
564
671
|
console.log(` ${DIM}tmux 없이 실행됨 (직접 CLI 프로세스)${RESET}`);
|
|
565
672
|
console.log(` ${DIM}제어: tfx team send/control/tasks/status${RESET}\n`);
|
|
566
673
|
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();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ── wt 모드(Windows Terminal 독립 split-pane) ──
|
|
677
|
+
if (effectiveTeammateMode === "wt") {
|
|
678
|
+
const paneCount = agents.length + 1; // lead + workers
|
|
679
|
+
const effectiveLayout = layout === "Nx1" ? "Nx1" : "1xN";
|
|
680
|
+
if (layout !== effectiveLayout) {
|
|
681
|
+
warn(`wt 모드에서 ${layout} 레이아웃은 미지원 — ${effectiveLayout}로 대체`);
|
|
682
|
+
}
|
|
683
|
+
console.log(` 레이아웃: ${effectiveLayout} (${paneCount} panes)`);
|
|
684
|
+
|
|
685
|
+
const paneCommands = [
|
|
686
|
+
{
|
|
687
|
+
title: `${sessionId}-lead`,
|
|
688
|
+
command: buildCliCommand(lead),
|
|
689
|
+
cwd: PKG_ROOT,
|
|
690
|
+
},
|
|
691
|
+
...agents.map((cli, i) => ({
|
|
692
|
+
title: `${sessionId}-${cli}-${i + 1}`,
|
|
693
|
+
command: buildCliCommand(cli),
|
|
694
|
+
cwd: PKG_ROOT,
|
|
695
|
+
})),
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
const session = createWtSession(sessionId, {
|
|
699
|
+
layout: effectiveLayout,
|
|
700
|
+
paneCommands,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const members = [
|
|
704
|
+
{
|
|
705
|
+
role: "lead",
|
|
706
|
+
name: "lead",
|
|
707
|
+
cli: lead,
|
|
708
|
+
pane: session.panes[0] || "wt:0",
|
|
709
|
+
agentId: toAgentId(lead, session.panes[0] || "wt:0"),
|
|
710
|
+
},
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
for (let i = 0; i < agents.length; i++) {
|
|
714
|
+
const cli = agents[i];
|
|
715
|
+
const target = session.panes[i + 1] || `wt:${i + 1}`;
|
|
716
|
+
members.push({
|
|
717
|
+
role: "worker",
|
|
718
|
+
name: `${cli}-${i + 1}`,
|
|
719
|
+
cli,
|
|
720
|
+
pane: target,
|
|
721
|
+
subtask: subtasks[i],
|
|
722
|
+
agentId: toAgentId(cli, target),
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
for (const worker of members.filter((m) => m.role === "worker")) {
|
|
727
|
+
const preview = worker.subtask.length > 44 ? worker.subtask.slice(0, 44) + "…" : worker.subtask;
|
|
728
|
+
console.log(` ${DIM}[${worker.name}] ${preview}${RESET}`);
|
|
729
|
+
}
|
|
730
|
+
console.log("");
|
|
731
|
+
|
|
732
|
+
const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
|
|
733
|
+
const panes = {};
|
|
734
|
+
for (const m of members) {
|
|
735
|
+
panes[m.pane] = {
|
|
736
|
+
role: m.role,
|
|
737
|
+
name: m.name,
|
|
738
|
+
cli: m.cli,
|
|
739
|
+
agentId: m.agentId,
|
|
740
|
+
subtask: m.subtask || null,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
saveTeamState({
|
|
745
|
+
sessionName: sessionId,
|
|
746
|
+
task,
|
|
747
|
+
lead,
|
|
748
|
+
agents,
|
|
749
|
+
layout: effectiveLayout,
|
|
750
|
+
teammateMode: effectiveTeammateMode,
|
|
751
|
+
startedAt: Date.now(),
|
|
752
|
+
hubUrl,
|
|
753
|
+
members,
|
|
754
|
+
panes,
|
|
755
|
+
tasks,
|
|
756
|
+
wt: {
|
|
757
|
+
windowId: 0,
|
|
758
|
+
layout: effectiveLayout,
|
|
759
|
+
paneCount: session.paneCount,
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
ok("Windows Terminal wt 팀 시작 완료");
|
|
764
|
+
console.log(` ${DIM}현재 pane 기준으로 ${effectiveLayout} 분할 생성됨${RESET}`);
|
|
765
|
+
console.log(` ${DIM}wt 모드는 자동 프롬프트 주입/Hub direct 제어(send/control)가 제한됩니다.${RESET}\n`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── tmux 모드 ──
|
|
770
|
+
ensureTmuxOrExit();
|
|
664
771
|
|
|
665
772
|
const paneCount = agents.length + 1; // lead + workers
|
|
666
773
|
const effectiveLayout = paneCount <= 4 ? layout : (layout === "Nx1" ? "Nx1" : "1xN");
|
|
@@ -714,12 +821,12 @@ async function teamStart() {
|
|
|
714
821
|
ok("CLI 초기화 대기 (3초)...");
|
|
715
822
|
await new Promise((r) => setTimeout(r, 3000));
|
|
716
823
|
|
|
717
|
-
await orchestrate(sessionId, assignments, {
|
|
718
|
-
hubUrl,
|
|
719
|
-
teammateMode: effectiveTeammateMode,
|
|
720
|
-
lead: {
|
|
721
|
-
target: leadTarget,
|
|
722
|
-
cli: lead,
|
|
824
|
+
await orchestrate(sessionId, assignments, {
|
|
825
|
+
hubUrl,
|
|
826
|
+
teammateMode: effectiveTeammateMode,
|
|
827
|
+
lead: {
|
|
828
|
+
target: leadTarget,
|
|
829
|
+
cli: lead,
|
|
723
830
|
task,
|
|
724
831
|
},
|
|
725
832
|
});
|
|
@@ -742,26 +849,29 @@ async function teamStart() {
|
|
|
742
849
|
task,
|
|
743
850
|
lead,
|
|
744
851
|
agents,
|
|
745
|
-
layout: effectiveLayout,
|
|
746
|
-
teammateMode: effectiveTeammateMode,
|
|
747
|
-
startedAt: Date.now(),
|
|
852
|
+
layout: effectiveLayout,
|
|
853
|
+
teammateMode: effectiveTeammateMode,
|
|
854
|
+
startedAt: Date.now(),
|
|
748
855
|
hubUrl,
|
|
749
856
|
members,
|
|
750
857
|
panes,
|
|
751
858
|
tasks,
|
|
752
859
|
});
|
|
753
860
|
|
|
754
|
-
const
|
|
861
|
+
const profilePrefix = TEAM_PROFILE === "team" ? "" : `TFX_TEAM_PROFILE=${TEAM_PROFILE} `;
|
|
862
|
+
const taskListCommand = `${profilePrefix}${process.execPath} ${join(PKG_ROOT, "bin", "triflux.mjs")} team tasks`;
|
|
755
863
|
configureTeammateKeybindings(sessionId, {
|
|
756
864
|
inProcess: false,
|
|
757
865
|
taskListCommand,
|
|
758
866
|
});
|
|
759
867
|
|
|
760
|
-
console.log(`\n ${GREEN}${BOLD}팀 세션 준비 완료${RESET}`);
|
|
761
|
-
console.log(` ${DIM}Shift+Down: 다음 팀메이트 전환${RESET}`);
|
|
762
|
-
console.log(` ${DIM}
|
|
763
|
-
console.log(` ${DIM}
|
|
764
|
-
console.log(` ${DIM}Ctrl+
|
|
868
|
+
console.log(`\n ${GREEN}${BOLD}팀 세션 준비 완료${RESET}`);
|
|
869
|
+
console.log(` ${DIM}Shift+Down: 다음 팀메이트 전환${RESET}`);
|
|
870
|
+
console.log(` ${DIM}Shift+Tab / Shift+Left: 이전 팀메이트 전환${RESET}`);
|
|
871
|
+
console.log(` ${DIM}Escape: 현재 팀메이트 인터럽트${RESET}`);
|
|
872
|
+
console.log(` ${DIM}Ctrl+T: 태스크 목록${RESET}`);
|
|
873
|
+
console.log(` ${DIM}참고: 일부 터미널/호스트에서 Shift+Up 전달이 제한될 수 있음${RESET}`);
|
|
874
|
+
console.log(` ${DIM}Ctrl+B → D: 세션 분리 (백그라운드)${RESET}\n`);
|
|
765
875
|
|
|
766
876
|
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
767
877
|
attachSession(sessionId);
|
|
@@ -785,10 +895,13 @@ async function teamStatus() {
|
|
|
785
895
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET} ${status}\n`);
|
|
786
896
|
console.log(` 세션: ${state.sessionName}`);
|
|
787
897
|
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}`);
|
|
898
|
+
console.log(` 리드: ${state.lead || "claude"}`);
|
|
899
|
+
console.log(` 워커: ${(state.agents || []).join(", ")}`);
|
|
900
|
+
console.log(` Uptime: ${uptime}`);
|
|
901
|
+
console.log(` 태스크: ${(state.tasks || []).length}`);
|
|
902
|
+
if (isWtMode(state) && !hasWindowsTerminalSession()) {
|
|
903
|
+
console.log(` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`);
|
|
904
|
+
}
|
|
792
905
|
|
|
793
906
|
const members = state.members || [];
|
|
794
907
|
if (members.length) {
|
|
@@ -810,9 +923,56 @@ async function teamStatus() {
|
|
|
810
923
|
}
|
|
811
924
|
}
|
|
812
925
|
|
|
926
|
+
// Hub task-list 데이터 통합 (v2.2)
|
|
927
|
+
if (alive) {
|
|
928
|
+
const hubTasks = await fetchHubTaskList(state);
|
|
929
|
+
if (hubTasks.length > 0) {
|
|
930
|
+
const completed = hubTasks.filter((t) => t.status === "completed").length;
|
|
931
|
+
const inProgress = hubTasks.filter((t) => t.status === "in_progress").length;
|
|
932
|
+
const failed = hubTasks.filter((t) => t.status === "failed").length;
|
|
933
|
+
const pending = hubTasks.filter((t) => !t.status || t.status === "pending").length;
|
|
934
|
+
|
|
935
|
+
console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
|
|
936
|
+
for (const t of hubTasks) {
|
|
937
|
+
const icon = t.status === "completed" ? `${GREEN}✓${RESET}`
|
|
938
|
+
: t.status === "in_progress" ? `${AMBER}●${RESET}`
|
|
939
|
+
: t.status === "failed" ? `${RED}✗${RESET}`
|
|
940
|
+
: `${GRAY}○${RESET}`;
|
|
941
|
+
const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
|
|
942
|
+
const subject = t.subject || t.description?.slice(0, 50) || "";
|
|
943
|
+
console.log(` ${icon} ${subject}${owner}`);
|
|
944
|
+
}
|
|
945
|
+
if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
813
949
|
console.log("");
|
|
814
950
|
}
|
|
815
951
|
|
|
952
|
+
/**
|
|
953
|
+
* Hub bridge에서 팀 task-list 조회 (v2.2)
|
|
954
|
+
* @param {object} state — team-state.json
|
|
955
|
+
* @returns {Promise<Array>}
|
|
956
|
+
*/
|
|
957
|
+
async function fetchHubTaskList(state) {
|
|
958
|
+
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
959
|
+
// teamName: native 모드는 state에 저장된 팀 이름, SKILL.md 모드는 세션 이름 기반
|
|
960
|
+
const teamName = state?.native?.teamName || state?.sessionName || null;
|
|
961
|
+
if (!teamName) return [];
|
|
962
|
+
try {
|
|
963
|
+
const res = await fetch(`${hubBase}/bridge/team/task-list`, {
|
|
964
|
+
method: "POST",
|
|
965
|
+
headers: { "Content-Type": "application/json" },
|
|
966
|
+
body: JSON.stringify({ team_name: teamName }),
|
|
967
|
+
signal: AbortSignal.timeout(2000),
|
|
968
|
+
});
|
|
969
|
+
const data = await res.json();
|
|
970
|
+
return data?.ok ? (data.data?.tasks || []) : [];
|
|
971
|
+
} catch {
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
816
976
|
function teamTasks() {
|
|
817
977
|
const state = loadTeamState();
|
|
818
978
|
if (!state || !isTeamAlive(state)) {
|
|
@@ -865,19 +1025,19 @@ async function teamAttach() {
|
|
|
865
1025
|
return;
|
|
866
1026
|
}
|
|
867
1027
|
|
|
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 {
|
|
1028
|
+
if (isNativeMode(state)) {
|
|
1029
|
+
console.log(`\n ${DIM}in-process 모드는 별도 attach가 없습니다.${RESET}`);
|
|
1030
|
+
console.log(` ${DIM}상태 확인: tfx team status${RESET}\n`);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (isWtMode(state)) {
|
|
1035
|
+
console.log(`\n ${DIM}wt 모드는 attach 개념이 없습니다 (Windows Terminal pane가 독립 실행됨).${RESET}`);
|
|
1036
|
+
console.log(` ${DIM}재실행/정리는: tfx team stop${RESET}\n`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
try {
|
|
881
1041
|
attachSession(state.sessionName);
|
|
882
1042
|
} catch (e) {
|
|
883
1043
|
const allowWt = wantsWtAttachFallback();
|
|
@@ -905,7 +1065,7 @@ async function teamDebug() {
|
|
|
905
1065
|
const linesIdx = process.argv.findIndex((a) => a === "--lines" || a === "-n");
|
|
906
1066
|
const lines = linesIdx !== -1 ? Math.max(3, parseInt(process.argv[linesIdx + 1] || "20", 10) || 20) : 20;
|
|
907
1067
|
const mux = detectMultiplexer() || "none";
|
|
908
|
-
const hub = getHubInfo();
|
|
1068
|
+
const hub = await getHubInfo();
|
|
909
1069
|
|
|
910
1070
|
console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
|
|
911
1071
|
console.log(` platform: ${process.platform}`);
|
|
@@ -923,28 +1083,29 @@ async function teamDebug() {
|
|
|
923
1083
|
return;
|
|
924
1084
|
}
|
|
925
1085
|
|
|
926
|
-
console.log(`\n ${BOLD}state${RESET}`);
|
|
927
|
-
console.log(` session: ${state.sessionName}`);
|
|
928
|
-
console.log(`
|
|
1086
|
+
console.log(`\n ${BOLD}state${RESET}`);
|
|
1087
|
+
console.log(` session: ${state.sessionName}`);
|
|
1088
|
+
console.log(` profile: ${state.profile || TEAM_PROFILE}`);
|
|
1089
|
+
console.log(` mode: ${state.teammateMode || "tmux"}`);
|
|
929
1090
|
console.log(` lead: ${state.lead}`);
|
|
930
1091
|
console.log(` agents: ${(state.agents || []).join(", ")}`);
|
|
931
1092
|
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)) {
|
|
1093
|
+
const attached = getSessionAttachedCount(state.sessionName);
|
|
1094
|
+
console.log(` attached: ${attached == null ? "-" : attached}`);
|
|
1095
|
+
|
|
1096
|
+
if (isWtMode(state)) {
|
|
1097
|
+
const wtState = state.wt || {};
|
|
1098
|
+
console.log(`\n ${BOLD}wt-session${RESET}`);
|
|
1099
|
+
console.log(` window: ${wtState.windowId ?? 0}`);
|
|
1100
|
+
console.log(` layout: ${wtState.layout || state.layout || "-"}`);
|
|
1101
|
+
console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
|
|
1102
|
+
console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
|
|
1103
|
+
console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
|
|
1104
|
+
console.log("");
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (isNativeMode(state)) {
|
|
948
1109
|
const native = await nativeGetStatus(state);
|
|
949
1110
|
const members = native?.data?.members || [];
|
|
950
1111
|
console.log(`\n ${BOLD}native-members${RESET}`);
|
|
@@ -990,34 +1151,34 @@ async function teamFocus() {
|
|
|
990
1151
|
}
|
|
991
1152
|
|
|
992
1153
|
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 {
|
|
1154
|
+
const member = resolveMember(state, selector);
|
|
1155
|
+
if (!member) {
|
|
1156
|
+
console.log(`\n 사용법: ${WHITE}tfx team focus <lead|이름|번호>${RESET}\n`);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (isWtMode(state)) {
|
|
1161
|
+
const m = /^wt:(\d+)$/.exec(member.pane || "");
|
|
1162
|
+
const paneIndex = m ? parseInt(m[1], 10) : NaN;
|
|
1163
|
+
if (!Number.isFinite(paneIndex)) {
|
|
1164
|
+
warn(`wt pane 인덱스 파싱 실패: ${member.pane}`);
|
|
1165
|
+
console.log("");
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const focused = focusWtPane(paneIndex, {
|
|
1169
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
1170
|
+
});
|
|
1171
|
+
if (focused) {
|
|
1172
|
+
ok(`${member.name} pane 포커스 이동 (wt)`);
|
|
1173
|
+
} else {
|
|
1174
|
+
warn("wt pane 포커스 이동 실패 (WT_SESSION/wt.exe 상태 확인 필요)");
|
|
1175
|
+
}
|
|
1176
|
+
console.log("");
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
focusPane(member.pane, { zoom: (state.teammateMode === "in-process") });
|
|
1181
|
+
try {
|
|
1021
1182
|
attachSession(state.sessionName);
|
|
1022
1183
|
} catch (e) {
|
|
1023
1184
|
const allowWt = wantsWtAttachFallback();
|
|
@@ -1049,19 +1210,19 @@ async function teamInterrupt() {
|
|
|
1049
1210
|
|
|
1050
1211
|
const selector = process.argv[4] || "lead";
|
|
1051
1212
|
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)) {
|
|
1213
|
+
if (!member) {
|
|
1214
|
+
console.log(`\n 사용법: ${WHITE}tfx team interrupt <lead|이름|번호>${RESET}\n`);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (isWtMode(state)) {
|
|
1219
|
+
warn("wt 모드에서는 pane stdin 주입이 지원되지 않아 interrupt를 자동 전송할 수 없습니다.");
|
|
1220
|
+
console.log(` ${DIM}수동으로 해당 pane에서 Ctrl+C를 입력하세요.${RESET}`);
|
|
1221
|
+
console.log("");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (isNativeMode(state)) {
|
|
1065
1226
|
const result = await nativeRequest(state, "/interrupt", { member: member.name });
|
|
1066
1227
|
if (result?.ok) {
|
|
1067
1228
|
ok(`${member.name} 인터럽트 전송`);
|
|
@@ -1090,20 +1251,20 @@ async function teamControl() {
|
|
|
1090
1251
|
const member = resolveMember(state, selector);
|
|
1091
1252
|
const allowed = new Set(["interrupt", "stop", "pause", "resume"]);
|
|
1092
1253
|
|
|
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;
|
|
1254
|
+
if (!member || !allowed.has(command)) {
|
|
1255
|
+
console.log(`\n 사용법: ${WHITE}tfx team control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (isWtMode(state)) {
|
|
1260
|
+
warn("wt 모드는 Hub direct/control 주입 경로가 비활성입니다.");
|
|
1261
|
+
console.log(` ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}`);
|
|
1262
|
+
console.log("");
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// 직접 주입: MCP 유무와 무관하게 즉시 전달
|
|
1267
|
+
let directOk = false;
|
|
1107
1268
|
if (isNativeMode(state)) {
|
|
1108
1269
|
const direct = await nativeRequest(state, "/control", {
|
|
1109
1270
|
member: member.name,
|
|
@@ -1140,19 +1301,19 @@ async function teamStop() {
|
|
|
1140
1301
|
return;
|
|
1141
1302
|
}
|
|
1142
1303
|
|
|
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);
|
|
1304
|
+
if (isNativeMode(state)) {
|
|
1305
|
+
await nativeRequest(state, "/stop", {});
|
|
1306
|
+
try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
|
|
1307
|
+
ok(`세션 종료: ${state.sessionName}`);
|
|
1308
|
+
} else if (isWtMode(state)) {
|
|
1309
|
+
const closed = closeWtSession({
|
|
1310
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
1311
|
+
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
1312
|
+
});
|
|
1313
|
+
ok(`세션 종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
|
|
1314
|
+
} else {
|
|
1315
|
+
if (sessionExists(state.sessionName)) {
|
|
1316
|
+
killSession(state.sessionName);
|
|
1156
1317
|
ok(`세션 종료: ${state.sessionName}`);
|
|
1157
1318
|
} else {
|
|
1158
1319
|
console.log(` ${DIM}세션 이미 종료됨${RESET}`);
|
|
@@ -1163,28 +1324,28 @@ async function teamStop() {
|
|
|
1163
1324
|
console.log("");
|
|
1164
1325
|
}
|
|
1165
1326
|
|
|
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();
|
|
1327
|
+
async function teamKill() {
|
|
1328
|
+
const state = loadTeamState();
|
|
1329
|
+
if (state && isNativeMode(state) && isTeamAlive(state)) {
|
|
1330
|
+
await nativeRequest(state, "/stop", {});
|
|
1331
|
+
try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
|
|
1332
|
+
clearTeamState();
|
|
1333
|
+
ok(`종료: ${state.sessionName}`);
|
|
1334
|
+
console.log("");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (state && isWtMode(state)) {
|
|
1338
|
+
const closed = closeWtSession({
|
|
1339
|
+
layout: state?.wt?.layout || state?.layout || "1xN",
|
|
1340
|
+
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
1341
|
+
});
|
|
1342
|
+
clearTeamState();
|
|
1343
|
+
ok(`종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
|
|
1344
|
+
console.log("");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const sessions = listSessions();
|
|
1188
1349
|
if (sessions.length === 0) {
|
|
1189
1350
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1190
1351
|
return;
|
|
@@ -1207,19 +1368,19 @@ async function teamSend() {
|
|
|
1207
1368
|
const selector = process.argv[4];
|
|
1208
1369
|
const message = process.argv.slice(5).join(" ");
|
|
1209
1370
|
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)) {
|
|
1371
|
+
if (!member || !message) {
|
|
1372
|
+
console.log(`\n 사용법: ${WHITE}tfx team send <lead|이름|번호> "메시지"${RESET}\n`);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (isWtMode(state)) {
|
|
1377
|
+
warn("wt 모드는 pane 프롬프트 자동 주입(send)이 지원되지 않습니다.");
|
|
1378
|
+
console.log(` ${DIM}수동 전달: 선택한 pane에 직접 붙여넣으세요.${RESET}`);
|
|
1379
|
+
console.log("");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (isNativeMode(state)) {
|
|
1223
1384
|
const result = await nativeRequest(state, "/send", { member: member.name, text: message });
|
|
1224
1385
|
if (result?.ok) {
|
|
1225
1386
|
ok(`${member.name}에 메시지 주입 완료`);
|
|
@@ -1235,22 +1396,22 @@ async function teamSend() {
|
|
|
1235
1396
|
console.log("");
|
|
1236
1397
|
}
|
|
1237
1398
|
|
|
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();
|
|
1399
|
+
function teamList() {
|
|
1400
|
+
const state = loadTeamState();
|
|
1401
|
+
if (state && isNativeMode(state) && isTeamAlive(state)) {
|
|
1402
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
1403
|
+
console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(in-process)${RESET}`);
|
|
1404
|
+
console.log("");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (state && isWtMode(state) && isTeamAlive(state)) {
|
|
1408
|
+
console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
|
|
1409
|
+
console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(wt)${RESET}`);
|
|
1410
|
+
console.log("");
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const sessions = listSessions();
|
|
1254
1415
|
if (sessions.length === 0) {
|
|
1255
1416
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
1256
1417
|
return;
|
|
@@ -1266,14 +1427,14 @@ function teamHelp() {
|
|
|
1266
1427
|
console.log(`
|
|
1267
1428
|
${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
|
|
1268
1429
|
|
|
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}
|
|
1430
|
+
${BOLD}시작${RESET}
|
|
1431
|
+
${WHITE}tfx team "작업 설명"${RESET}
|
|
1432
|
+
${WHITE}tfx team --agents codex,gemini --lead claude "작업"${RESET}
|
|
1433
|
+
${WHITE}tfx team --teammate-mode tmux "작업"${RESET}
|
|
1434
|
+
${WHITE}tfx team --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
|
|
1435
|
+
${WHITE}tfx team --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
|
|
1436
|
+
${WHITE}tfx team --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
|
|
1437
|
+
${WHITE}tfx team --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
|
|
1277
1438
|
|
|
1278
1439
|
${BOLD}제어${RESET}
|
|
1279
1440
|
${WHITE}tfx team status${RESET} ${GRAY}현재 팀 상태${RESET}
|
|
@@ -1289,13 +1450,15 @@ function teamHelp() {
|
|
|
1289
1450
|
${WHITE}tfx team kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
|
|
1290
1451
|
${WHITE}tfx team list${RESET} ${GRAY}활성 세션 목록${RESET}
|
|
1291
1452
|
|
|
1292
|
-
${BOLD}키 조작(Claude teammate 스타일, tmux 모드)${RESET}
|
|
1293
|
-
${WHITE}Shift+Down${RESET} ${GRAY}다음 팀메이트${RESET}
|
|
1294
|
-
${WHITE}Shift+
|
|
1295
|
-
${WHITE}
|
|
1296
|
-
${WHITE}
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1453
|
+
${BOLD}키 조작(Claude teammate 스타일, tmux 모드)${RESET}
|
|
1454
|
+
${WHITE}Shift+Down${RESET} ${GRAY}다음 팀메이트${RESET}
|
|
1455
|
+
${WHITE}Shift+Tab${RESET} ${GRAY}이전 팀메이트 (권장)${RESET}
|
|
1456
|
+
${WHITE}Shift+Left${RESET} ${GRAY}이전 팀메이트 (대체)${RESET}
|
|
1457
|
+
${WHITE}Shift+Up${RESET} ${GRAY}이전 팀메이트 (환경 따라 미동작 가능)${RESET}
|
|
1458
|
+
${WHITE}Escape${RESET} ${GRAY}현재 팀메이트 인터럽트${RESET}
|
|
1459
|
+
${WHITE}Ctrl+T${RESET} ${GRAY}태스크 목록 토글${RESET}
|
|
1460
|
+
`);
|
|
1461
|
+
}
|
|
1299
1462
|
|
|
1300
1463
|
// ── 메인 진입점 ──
|
|
1301
1464
|
|
|
@@ -1303,8 +1466,9 @@ function teamHelp() {
|
|
|
1303
1466
|
* tfx team 서브커맨드 라우터
|
|
1304
1467
|
* bin/triflux.mjs에서 호출
|
|
1305
1468
|
*/
|
|
1306
|
-
export async function cmdTeam() {
|
|
1307
|
-
const
|
|
1469
|
+
export async function cmdTeam() {
|
|
1470
|
+
const rawSub = process.argv[3];
|
|
1471
|
+
const sub = typeof rawSub === "string" ? rawSub.toLowerCase() : rawSub;
|
|
1308
1472
|
|
|
1309
1473
|
switch (sub) {
|
|
1310
1474
|
case "status": return teamStatus();
|
|
@@ -1323,13 +1487,13 @@ export async function cmdTeam() {
|
|
|
1323
1487
|
case "--help":
|
|
1324
1488
|
case "-h":
|
|
1325
1489
|
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
|
-
}
|
|
1490
|
+
case undefined:
|
|
1491
|
+
return teamHelp();
|
|
1492
|
+
default:
|
|
1493
|
+
// 서브커맨드가 아니면 작업 문자열로 간주
|
|
1494
|
+
if (typeof sub === "string" && !sub.startsWith("-") && TEAM_SUBCOMMANDS.has(sub)) {
|
|
1495
|
+
return teamHelp();
|
|
1496
|
+
}
|
|
1497
|
+
return teamStart();
|
|
1498
|
+
}
|
|
1499
|
+
}
|