triflux 3.3.0-dev.8 → 4.0.0

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.
Files changed (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -684
  8. package/hub/delegator/contracts.mjs +38 -38
  9. package/hub/delegator/index.mjs +14 -14
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  11. package/hub/delegator/service.mjs +302 -118
  12. package/hub/delegator/tool-definitions.mjs +35 -35
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -367
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -1,516 +0,0 @@
1
- // hub/team/cli-team-start.mjs — 팀 시작 로직
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { spawn } from "node:child_process";
5
-
6
- import {
7
- createSession,
8
- createWtSession,
9
- attachSession,
10
- configureTeammateKeybindings,
11
- detectMultiplexer,
12
- hasWindowsTerminal,
13
- hasWindowsTerminalSession,
14
- } from "./session.mjs";
15
- import { buildCliCommand, startCliInPane } from "./pane.mjs";
16
- import { orchestrate, decomposeTask, buildLeadPrompt, buildPrompt } from "./orchestrator.mjs";
17
- import {
18
- PKG_ROOT,
19
- HUB_PID_DIR,
20
- TEAM_PROFILE,
21
- AMBER,
22
- GREEN,
23
- RED,
24
- DIM,
25
- BOLD,
26
- RESET,
27
- WHITE,
28
- getHubInfo,
29
- startHubDaemon,
30
- getDefaultHubUrl,
31
- saveTeamState,
32
- ok,
33
- warn,
34
- fail,
35
- } from "./cli-team-common.mjs";
36
-
37
- function normalizeTeammateMode(mode = "auto") {
38
- const raw = String(mode).toLowerCase();
39
- if (raw === "inline" || raw === "native") return "in-process";
40
- if (raw === "in-process" || raw === "tmux" || raw === "wt" || raw === "psmux") return raw;
41
- if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
42
- if (raw === "auto") {
43
- if (process.env.TMUX) return "tmux";
44
- const mux = detectMultiplexer();
45
- if (mux === "psmux") return "psmux";
46
- return "in-process";
47
- }
48
- return "in-process";
49
- }
50
-
51
- function normalizeLayout(layout = "2x2") {
52
- const raw = String(layout).toLowerCase();
53
- if (raw === "2x2" || raw === "grid") return "2x2";
54
- if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns") return "1xN";
55
- if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
56
- return "2x2";
57
- }
58
-
59
- function parseTeamArgs() {
60
- const args = process.argv.slice(3);
61
- let agents = ["codex", "gemini"];
62
- let lead = "claude";
63
- let layout = "2x2";
64
- let teammateMode = "auto";
65
- const taskParts = [];
66
-
67
- for (let i = 0; i < args.length; i++) {
68
- const cur = args[i];
69
- if (cur === "--agents" && args[i + 1]) {
70
- agents = args[++i].split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
71
- } else if (cur === "--lead" && args[i + 1]) {
72
- lead = args[++i].trim().toLowerCase();
73
- } else if (cur === "--layout" && args[i + 1]) {
74
- layout = args[++i];
75
- } else if ((cur === "--teammate-mode" || cur === "--mode") && args[i + 1]) {
76
- teammateMode = args[++i];
77
- } else if (!cur.startsWith("-")) {
78
- taskParts.push(cur);
79
- }
80
- }
81
-
82
- return {
83
- agents,
84
- lead,
85
- layout: normalizeLayout(layout),
86
- teammateMode: normalizeTeammateMode(teammateMode),
87
- task: taskParts.join(" ").trim(),
88
- };
89
- }
90
-
91
- function buildTasks(subtasks, workers) {
92
- return subtasks.map((subtask, i) => ({
93
- id: `T${i + 1}`,
94
- title: subtask,
95
- owner: workers[i]?.name || null,
96
- status: "pending",
97
- depends_on: i === 0 ? [] : [`T${i}`],
98
- }));
99
- }
100
-
101
- function ensureTmuxOrExit() {
102
- const mux = detectMultiplexer();
103
- if (mux) return;
104
-
105
- console.log(`
106
- ${RED}${BOLD}tmux 미발견${RESET}
107
-
108
- 현재 선택한 모드는 tmux 기반 팀세션이 필요합니다.
109
-
110
- 설치:
111
- WSL2: ${WHITE}wsl sudo apt install tmux${RESET}
112
- macOS: ${WHITE}brew install tmux${RESET}
113
- Linux: ${WHITE}apt install tmux${RESET}
114
-
115
- Windows에서는 WSL2를 권장합니다:
116
- 1. ${WHITE}wsl --install${RESET}
117
- 2. ${WHITE}wsl sudo apt install tmux${RESET}
118
- 3. ${WHITE}tfx multi "작업"${RESET}
119
- `);
120
- process.exit(1);
121
- }
122
-
123
- function toAgentId(cli, target) {
124
- const suffix = String(target).split(/[:.]/).pop();
125
- return `${cli}-${suffix}`;
126
- }
127
-
128
- function buildNativeCliCommand(cli) {
129
- switch (cli) {
130
- case "codex":
131
- return "codex --dangerously-bypass-approvals-and-sandbox --no-alt-screen";
132
- case "gemini":
133
- return "gemini";
134
- case "claude":
135
- return "claude";
136
- default:
137
- return buildCliCommand(cli);
138
- }
139
- }
140
-
141
- async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
142
- const nativeConfigPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
143
- const nativeRuntimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
144
- const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
145
- mkdirSync(logsDir, { recursive: true });
146
-
147
- const leadMember = {
148
- role: "lead",
149
- name: "lead",
150
- cli: lead,
151
- agentId: `${lead}-lead`,
152
- command: buildNativeCliCommand(lead),
153
- };
154
-
155
- const workers = agents.map((cli, i) => ({
156
- role: "worker",
157
- name: `${cli}-${i + 1}`,
158
- cli,
159
- agentId: `${cli}-w${i + 1}`,
160
- command: buildNativeCliCommand(cli),
161
- subtask: subtasks[i],
162
- }));
163
-
164
- const leadPrompt = buildLeadPrompt(task, {
165
- agentId: leadMember.agentId,
166
- hubUrl,
167
- teammateMode: "in-process",
168
- workers: workers.map((w) => ({ agentId: w.agentId, cli: w.cli, subtask: w.subtask })),
169
- });
170
-
171
- const members = [
172
- { ...leadMember, prompt: leadPrompt },
173
- ...workers.map((w) => ({
174
- ...w,
175
- prompt: buildPrompt(w.subtask, { cli: w.cli, agentId: w.agentId, hubUrl }),
176
- })),
177
- ];
178
-
179
- const config = {
180
- sessionName: sessionId,
181
- hubUrl,
182
- startupDelayMs: 3000,
183
- logsDir,
184
- runtimeFile: nativeRuntimePath,
185
- members,
186
- };
187
- writeFileSync(nativeConfigPath, JSON.stringify(config, null, 2) + "\n");
188
-
189
- const supervisorPath = join(PKG_ROOT, "hub", "team", "native-supervisor.mjs");
190
- const child = spawn(process.execPath, [supervisorPath, "--config", nativeConfigPath], {
191
- detached: true,
192
- stdio: "ignore",
193
- env: { ...process.env },
194
- });
195
- child.unref();
196
-
197
- const deadline = Date.now() + 5000;
198
- while (Date.now() < deadline) {
199
- if (existsSync(nativeRuntimePath)) {
200
- try {
201
- const runtime = JSON.parse(readFileSync(nativeRuntimePath, "utf8"));
202
- return { runtime, members };
203
- } catch {}
204
- }
205
- await new Promise((r) => setTimeout(r, 100));
206
- }
207
-
208
- return { runtime: null, members };
209
- }
210
-
211
- export async function teamStart() {
212
- const { agents, lead, layout, teammateMode, task } = parseTeamArgs();
213
- if (!task) {
214
- console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
215
- console.log(` 사용법: ${WHITE}tfx multi "작업 설명"${RESET}`);
216
- console.log(` ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}`);
217
- console.log(` ${WHITE}tfx multi --teammate-mode psmux "작업"${RESET} ${DIM}(Windows psmux 네이티브)${RESET}`);
218
- console.log(` ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
219
- console.log(` ${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(mux 불필요)${RESET}\n`);
220
- return;
221
- }
222
-
223
- console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
224
-
225
- let hub = await getHubInfo();
226
- if (!hub) {
227
- process.stdout.write(" Hub 시작 중...");
228
- hub = await startHubDaemon();
229
- if (hub) {
230
- console.log(` ${GREEN}✓${RESET}`);
231
- } else {
232
- console.log(` ${RED}✗${RESET}`);
233
- warn("Hub 시작 실패 — 수동으로 실행: tfx hub start");
234
- }
235
- } else {
236
- ok(`Hub: ${DIM}${hub.url}${RESET}`);
237
- }
238
-
239
- const sessionId = `tfx-multi-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`;
240
- const subtasks = decomposeTask(task, agents.length);
241
- const hubUrl = hub?.url || getDefaultHubUrl();
242
- let effectiveTeammateMode = teammateMode;
243
-
244
- if (teammateMode === "wt") {
245
- if (!hasWindowsTerminal()) {
246
- warn("wt.exe 미발견 — in-process 모드로 자동 fallback");
247
- effectiveTeammateMode = "in-process";
248
- } else if (!hasWindowsTerminalSession()) {
249
- warn("WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback");
250
- effectiveTeammateMode = "in-process";
251
- }
252
- }
253
-
254
- console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
255
- console.log(` 모드: ${effectiveTeammateMode}`);
256
- console.log(` 리드: ${AMBER}${lead}${RESET}`);
257
- console.log(` 워커: ${agents.map((a) => `${AMBER}${a}${RESET}`).join(", ")}`);
258
-
259
- if (effectiveTeammateMode === "in-process") {
260
- for (let i = 0; i < subtasks.length; i++) {
261
- const preview = subtasks[i].length > 44 ? subtasks[i].slice(0, 44) + "…" : subtasks[i];
262
- console.log(` ${DIM}[${agents[i]}-${i + 1}] ${preview}${RESET}`);
263
- }
264
- console.log("");
265
-
266
- const { runtime, members } = await startNativeSupervisor({
267
- sessionId,
268
- task,
269
- lead,
270
- agents,
271
- subtasks,
272
- hubUrl,
273
- });
274
-
275
- if (!runtime?.controlUrl) {
276
- fail("in-process supervisor 시작 실패");
277
- return;
278
- }
279
-
280
- const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
281
-
282
- saveTeamState({
283
- sessionName: sessionId,
284
- task,
285
- lead,
286
- agents,
287
- layout: "native",
288
- teammateMode: effectiveTeammateMode,
289
- startedAt: Date.now(),
290
- hubUrl,
291
- members: members.map((m, idx) => ({
292
- role: m.role,
293
- name: m.name,
294
- cli: m.cli,
295
- agentId: m.agentId,
296
- pane: `native:${idx}`,
297
- subtask: m.subtask || null,
298
- })),
299
- panes: {},
300
- tasks,
301
- native: {
302
- controlUrl: runtime.controlUrl,
303
- supervisorPid: runtime.supervisorPid,
304
- },
305
- });
306
-
307
- ok("네이티브 in-process 팀 시작 완료");
308
- console.log(` ${DIM}tmux 없이 실행됨 (직접 CLI 프로세스)${RESET}`);
309
- console.log(` ${DIM}제어: tfx multi send/control/tasks/status${RESET}\n`);
310
- return;
311
- }
312
-
313
- if (effectiveTeammateMode === "wt") {
314
- const paneCount = agents.length + 1;
315
- const effectiveLayout = layout === "Nx1" ? "Nx1" : "1xN";
316
- if (layout !== effectiveLayout) {
317
- warn(`wt 모드에서 ${layout} 레이아웃은 미지원 — ${effectiveLayout}로 대체`);
318
- }
319
- console.log(` 레이아웃: ${effectiveLayout} (${paneCount} panes)`);
320
-
321
- const paneCommands = [
322
- {
323
- title: `${sessionId}-lead`,
324
- command: buildCliCommand(lead),
325
- cwd: PKG_ROOT,
326
- },
327
- ...agents.map((cli, i) => ({
328
- title: `${sessionId}-${cli}-${i + 1}`,
329
- command: buildCliCommand(cli),
330
- cwd: PKG_ROOT,
331
- })),
332
- ];
333
-
334
- const session = createWtSession(sessionId, {
335
- layout: effectiveLayout,
336
- paneCommands,
337
- });
338
-
339
- const members = [
340
- {
341
- role: "lead",
342
- name: "lead",
343
- cli: lead,
344
- pane: session.panes[0] || "wt:0",
345
- agentId: toAgentId(lead, session.panes[0] || "wt:0"),
346
- },
347
- ];
348
-
349
- for (let i = 0; i < agents.length; i++) {
350
- const cli = agents[i];
351
- const target = session.panes[i + 1] || `wt:${i + 1}`;
352
- members.push({
353
- role: "worker",
354
- name: `${cli}-${i + 1}`,
355
- cli,
356
- pane: target,
357
- subtask: subtasks[i],
358
- agentId: toAgentId(cli, target),
359
- });
360
- }
361
-
362
- for (const worker of members.filter((m) => m.role === "worker")) {
363
- const preview = worker.subtask.length > 44 ? worker.subtask.slice(0, 44) + "…" : worker.subtask;
364
- console.log(` ${DIM}[${worker.name}] ${preview}${RESET}`);
365
- }
366
- console.log("");
367
-
368
- const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
369
- const panes = {};
370
- for (const m of members) {
371
- panes[m.pane] = {
372
- role: m.role,
373
- name: m.name,
374
- cli: m.cli,
375
- agentId: m.agentId,
376
- subtask: m.subtask || null,
377
- };
378
- }
379
-
380
- saveTeamState({
381
- sessionName: sessionId,
382
- task,
383
- lead,
384
- agents,
385
- layout: effectiveLayout,
386
- teammateMode: effectiveTeammateMode,
387
- startedAt: Date.now(),
388
- hubUrl,
389
- members,
390
- panes,
391
- tasks,
392
- wt: {
393
- windowId: 0,
394
- layout: effectiveLayout,
395
- paneCount: session.paneCount,
396
- },
397
- });
398
-
399
- ok("Windows Terminal wt 팀 시작 완료");
400
- console.log(` ${DIM}현재 pane 기준으로 ${effectiveLayout} 분할 생성됨${RESET}`);
401
- console.log(` ${DIM}wt 모드는 자동 프롬프트 주입/Hub direct 제어(send/control)가 제한됩니다.${RESET}\n`);
402
- return;
403
- }
404
-
405
- if (effectiveTeammateMode === "tmux") ensureTmuxOrExit();
406
-
407
- const paneCount = agents.length + 1;
408
- const effectiveLayout = paneCount <= 4 ? layout : (layout === "Nx1" ? "Nx1" : "1xN");
409
- console.log(` 레이아웃: ${effectiveLayout} (${paneCount} panes)`);
410
-
411
- const session = createSession(sessionId, {
412
- layout: effectiveLayout,
413
- paneCount,
414
- });
415
-
416
- const leadTarget = session.panes[0];
417
- startCliInPane(leadTarget, buildCliCommand(lead));
418
-
419
- const assignments = [];
420
- const members = [
421
- {
422
- role: "lead",
423
- name: "lead",
424
- cli: lead,
425
- pane: leadTarget,
426
- agentId: toAgentId(lead, leadTarget),
427
- },
428
- ];
429
-
430
- for (let i = 0; i < agents.length; i++) {
431
- const cli = agents[i];
432
- const target = session.panes[i + 1];
433
- startCliInPane(target, buildCliCommand(cli));
434
-
435
- const worker = {
436
- role: "worker",
437
- name: `${cli}-${i + 1}`,
438
- cli,
439
- pane: target,
440
- subtask: subtasks[i],
441
- agentId: toAgentId(cli, target),
442
- };
443
-
444
- members.push(worker);
445
- assignments.push({ target, cli, subtask: subtasks[i] });
446
- }
447
-
448
- for (const worker of members.filter((m) => m.role === "worker")) {
449
- const preview = worker.subtask.length > 44 ? worker.subtask.slice(0, 44) + "…" : worker.subtask;
450
- console.log(` ${DIM}[${worker.name}] ${preview}${RESET}`);
451
- }
452
- console.log("");
453
-
454
- ok("CLI 초기화 대기 (3초)...");
455
- await new Promise((r) => setTimeout(r, 3000));
456
-
457
- await orchestrate(sessionId, assignments, {
458
- hubUrl,
459
- teammateMode: effectiveTeammateMode,
460
- lead: {
461
- target: leadTarget,
462
- cli: lead,
463
- task,
464
- },
465
- });
466
- ok("리드/워커 프롬프트 주입 완료");
467
-
468
- const tasks = buildTasks(subtasks, members.filter((m) => m.role === "worker"));
469
- const panes = {};
470
- for (const m of members) {
471
- panes[m.pane] = {
472
- role: m.role,
473
- name: m.name,
474
- cli: m.cli,
475
- agentId: m.agentId,
476
- subtask: m.subtask || null,
477
- };
478
- }
479
-
480
- saveTeamState({
481
- sessionName: sessionId,
482
- task,
483
- lead,
484
- agents,
485
- layout: effectiveLayout,
486
- teammateMode: effectiveTeammateMode,
487
- startedAt: Date.now(),
488
- hubUrl,
489
- members,
490
- panes,
491
- tasks,
492
- });
493
-
494
- const profilePrefix = TEAM_PROFILE === "team" ? "" : `TFX_TEAM_PROFILE=${TEAM_PROFILE} `;
495
- const taskListCommand = `${profilePrefix}${process.execPath} ${join(PKG_ROOT, "bin", "triflux.mjs")} team tasks`;
496
- configureTeammateKeybindings(sessionId, {
497
- inProcess: false,
498
- taskListCommand,
499
- });
500
-
501
- console.log(`\n ${GREEN}${BOLD}팀 세션 준비 완료${RESET}`);
502
- console.log(` ${DIM}Shift+Down: 다음 팀메이트 전환${RESET}`);
503
- console.log(` ${DIM}Shift+Tab / Shift+Left: 이전 팀메이트 전환${RESET}`);
504
- console.log(` ${DIM}Escape: 현재 팀메이트 인터럽트${RESET}`);
505
- console.log(` ${DIM}Ctrl+T: 태스크 목록${RESET}`);
506
- console.log(` ${DIM}참고: Shift+Up은 Claude Code 미지원 (scroll-up 충돌). Shift+Tab 사용${RESET}`);
507
- console.log(` ${DIM}Ctrl+B → D: 세션 분리 (백그라운드)${RESET}\n`);
508
-
509
- if (process.stdout.isTTY && process.stdin.isTTY) {
510
- attachSession(sessionId);
511
- } else {
512
- warn("TTY 미지원 환경이라 자동 attach를 생략함");
513
- console.log(` ${DIM}수동 연결: tfx multi attach${RESET}\n`);
514
- }
515
- }
516
-