triflux 3.2.0-dev.2 → 3.2.0-dev.5

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/hub/team/cli.mjs CHANGED
@@ -5,24 +5,24 @@ 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 {
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";
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
 
@@ -110,16 +110,16 @@ function startHubDaemon() {
110
110
 
111
111
  // ── 인자 파싱 ──
112
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
- }
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
123
 
124
124
  function normalizeLayout(layout = "2x2") {
125
125
  const raw = String(layout).toLowerCase();
@@ -183,8 +183,8 @@ function ensureTmuxOrExit() {
183
183
  process.exit(1);
184
184
  }
185
185
 
186
- async function launchAttachInWindowsTerminal(sessionName) {
187
- if (!hasWindowsTerminal()) return false;
186
+ async function launchAttachInWindowsTerminal(sessionName) {
187
+ if (!hasWindowsTerminal()) return false;
188
188
 
189
189
  let attachSpec;
190
190
  try {
@@ -244,10 +244,10 @@ function wantsWtAttachFallback() {
244
244
  || process.env.TFX_ATTACH_WT_AUTO === "1";
245
245
  }
246
246
 
247
- function toAgentId(cli, target) {
248
- const suffix = String(target).split(/[:.]/).pop();
249
- return `${cli}-${suffix}`;
250
- }
247
+ function toAgentId(cli, target) {
248
+ const suffix = String(target).split(/[:.]/).pop();
249
+ return `${cli}-${suffix}`;
250
+ }
251
251
 
252
252
  function buildNativeCliCommand(cli) {
253
253
  switch (cli) {
@@ -303,11 +303,11 @@ function resolveMember(state, selector) {
303
303
  if (workerIdx >= 0 && workerIdx < workers.length) return workers[workerIdx];
304
304
  }
305
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;
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
311
 
312
312
  // teammate 스타일: 1-based 인덱스
313
313
  if (n >= 1 && n <= members.length) return members[n - 1];
@@ -343,30 +343,30 @@ async function publishLeadControl(state, targetMember, command, reason = "") {
343
343
  }
344
344
  }
345
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
- }
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
370
 
371
371
  async function nativeRequest(state, path, body = {}) {
372
372
  if (!isNativeMode(state)) return null;
@@ -466,14 +466,14 @@ async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks,
466
466
 
467
467
  async function teamStart() {
468
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
- }
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
477
 
478
478
  console.log(`\n ${AMBER}${BOLD}⬡ tfx team${RESET}\n`);
479
479
 
@@ -491,28 +491,28 @@ async function teamStart() {
491
491
  ok(`Hub: ${DIM}${hub.url}${RESET}`);
492
492
  }
493
493
 
494
- const sessionId = `tfx-team-${Date.now().toString(36).slice(-4)}`;
495
- const subtasks = decomposeTask(task, agents.length);
496
- const hubUrl = hub?.url || "http://127.0.0.1:27888/mcp";
497
- let effectiveTeammateMode = teammateMode;
498
-
499
- if (teammateMode === "wt") {
500
- if (!hasWindowsTerminal()) {
501
- warn("wt.exe 미발견 — in-process 모드로 자동 fallback");
502
- effectiveTeammateMode = "in-process";
503
- } else if (!hasWindowsTerminalSession()) {
504
- warn("WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback");
505
- effectiveTeammateMode = "in-process";
506
- }
507
- }
508
-
509
- console.log(` 세션: ${WHITE}${sessionId}${RESET}`);
510
- console.log(` 모드: ${effectiveTeammateMode}`);
511
- console.log(` 리드: ${AMBER}${lead}${RESET}`);
512
- console.log(` 워커: ${agents.map((a) => `${AMBER}${a}${RESET}`).join(", ")}`);
513
-
514
- // ── in-process(네이티브): tmux 없이 supervisor가 직접 CLI 프로세스 관리 ──
515
- if (effectiveTeammateMode === "in-process") {
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
516
  for (let i = 0; i < subtasks.length; i++) {
517
517
  const preview = subtasks[i].length > 44 ? subtasks[i].slice(0, 44) + "…" : subtasks[i];
518
518
  console.log(` ${DIM}[${agents[i]}-${i + 1}] ${preview}${RESET}`);
@@ -540,10 +540,10 @@ async function teamStart() {
540
540
  task,
541
541
  lead,
542
542
  agents,
543
- layout: "native",
544
- teammateMode: effectiveTeammateMode,
545
- startedAt: Date.now(),
546
- hubUrl,
543
+ layout: "native",
544
+ teammateMode: effectiveTeammateMode,
545
+ startedAt: Date.now(),
546
+ hubUrl,
547
547
  members: members.map((m, idx) => ({
548
548
  role: m.role,
549
549
  name: m.name,
@@ -564,103 +564,103 @@ async function teamStart() {
564
564
  console.log(` ${DIM}tmux 없이 실행됨 (직접 CLI 프로세스)${RESET}`);
565
565
  console.log(` ${DIM}제어: tfx team send/control/tasks/status${RESET}\n`);
566
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();
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
664
 
665
665
  const paneCount = agents.length + 1; // lead + workers
666
666
  const effectiveLayout = paneCount <= 4 ? layout : (layout === "Nx1" ? "Nx1" : "1xN");
@@ -714,12 +714,12 @@ async function teamStart() {
714
714
  ok("CLI 초기화 대기 (3초)...");
715
715
  await new Promise((r) => setTimeout(r, 3000));
716
716
 
717
- await orchestrate(sessionId, assignments, {
718
- hubUrl,
719
- teammateMode: effectiveTeammateMode,
720
- lead: {
721
- target: leadTarget,
722
- cli: lead,
717
+ await orchestrate(sessionId, assignments, {
718
+ hubUrl,
719
+ teammateMode: effectiveTeammateMode,
720
+ lead: {
721
+ target: leadTarget,
722
+ cli: lead,
723
723
  task,
724
724
  },
725
725
  });
@@ -742,9 +742,9 @@ async function teamStart() {
742
742
  task,
743
743
  lead,
744
744
  agents,
745
- layout: effectiveLayout,
746
- teammateMode: effectiveTeammateMode,
747
- startedAt: Date.now(),
745
+ layout: effectiveLayout,
746
+ teammateMode: effectiveTeammateMode,
747
+ startedAt: Date.now(),
748
748
  hubUrl,
749
749
  members,
750
750
  panes,
@@ -810,9 +810,56 @@ async function teamStatus() {
810
810
  }
811
811
  }
812
812
 
813
+ // Hub task-list 데이터 통합 (v2.2)
814
+ if (alive) {
815
+ const hubTasks = await fetchHubTaskList(state);
816
+ if (hubTasks.length > 0) {
817
+ const completed = hubTasks.filter((t) => t.status === "completed").length;
818
+ const inProgress = hubTasks.filter((t) => t.status === "in_progress").length;
819
+ const failed = hubTasks.filter((t) => t.status === "failed").length;
820
+ const pending = hubTasks.filter((t) => !t.status || t.status === "pending").length;
821
+
822
+ console.log(`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`);
823
+ for (const t of hubTasks) {
824
+ const icon = t.status === "completed" ? `${GREEN}✓${RESET}`
825
+ : t.status === "in_progress" ? `${AMBER}●${RESET}`
826
+ : t.status === "failed" ? `${RED}✗${RESET}`
827
+ : `${GRAY}○${RESET}`;
828
+ const owner = t.owner ? ` ${GRAY}[${t.owner}]${RESET}` : "";
829
+ const subject = t.subject || t.description?.slice(0, 50) || "";
830
+ console.log(` ${icon} ${subject}${owner}`);
831
+ }
832
+ if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
833
+ }
834
+ }
835
+
813
836
  console.log("");
814
837
  }
815
838
 
839
+ /**
840
+ * Hub bridge에서 팀 task-list 조회 (v2.2)
841
+ * @param {object} state — team-state.json
842
+ * @returns {Promise<Array>}
843
+ */
844
+ async function fetchHubTaskList(state) {
845
+ const hubBase = (state?.hubUrl || "http://127.0.0.1:27888/mcp").replace(/\/mcp$/, "");
846
+ // teamName: native 모드는 state에 저장된 팀 이름, SKILL.md 모드는 세션 이름 기반
847
+ const teamName = state?.native?.teamName || state?.sessionName || null;
848
+ if (!teamName) return [];
849
+ try {
850
+ const res = await fetch(`${hubBase}/bridge/team/task-list`, {
851
+ method: "POST",
852
+ headers: { "Content-Type": "application/json" },
853
+ body: JSON.stringify({ team_name: teamName }),
854
+ signal: AbortSignal.timeout(2000),
855
+ });
856
+ const data = await res.json();
857
+ return data?.ok ? (data.data?.tasks || []) : [];
858
+ } catch {
859
+ return [];
860
+ }
861
+ }
862
+
816
863
  function teamTasks() {
817
864
  const state = loadTeamState();
818
865
  if (!state || !isTeamAlive(state)) {
@@ -865,19 +912,19 @@ async function teamAttach() {
865
912
  return;
866
913
  }
867
914
 
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 {
915
+ if (isNativeMode(state)) {
916
+ console.log(`\n ${DIM}in-process 모드는 별도 attach가 없습니다.${RESET}`);
917
+ console.log(` ${DIM}상태 확인: tfx team status${RESET}\n`);
918
+ return;
919
+ }
920
+
921
+ if (isWtMode(state)) {
922
+ console.log(`\n ${DIM}wt 모드는 attach 개념이 없습니다 (Windows Terminal pane가 독립 실행됨).${RESET}`);
923
+ console.log(` ${DIM}재실행/정리는: tfx team stop${RESET}\n`);
924
+ return;
925
+ }
926
+
927
+ try {
881
928
  attachSession(state.sessionName);
882
929
  } catch (e) {
883
930
  const allowWt = wantsWtAttachFallback();
@@ -929,22 +976,22 @@ async function teamDebug() {
929
976
  console.log(` lead: ${state.lead}`);
930
977
  console.log(` agents: ${(state.agents || []).join(", ")}`);
931
978
  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)) {
979
+ const attached = getSessionAttachedCount(state.sessionName);
980
+ console.log(` attached: ${attached == null ? "-" : attached}`);
981
+
982
+ if (isWtMode(state)) {
983
+ const wtState = state.wt || {};
984
+ console.log(`\n ${BOLD}wt-session${RESET}`);
985
+ console.log(` window: ${wtState.windowId ?? 0}`);
986
+ console.log(` layout: ${wtState.layout || state.layout || "-"}`);
987
+ console.log(` panes: ${wtState.paneCount ?? (state.members || []).length}`);
988
+ console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
989
+ console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
990
+ console.log("");
991
+ return;
992
+ }
993
+
994
+ if (isNativeMode(state)) {
948
995
  const native = await nativeGetStatus(state);
949
996
  const members = native?.data?.members || [];
950
997
  console.log(`\n ${BOLD}native-members${RESET}`);
@@ -990,34 +1037,34 @@ async function teamFocus() {
990
1037
  }
991
1038
 
992
1039
  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 {
1040
+ const member = resolveMember(state, selector);
1041
+ if (!member) {
1042
+ console.log(`\n 사용법: ${WHITE}tfx team focus <lead|이름|번호>${RESET}\n`);
1043
+ return;
1044
+ }
1045
+
1046
+ if (isWtMode(state)) {
1047
+ const m = /^wt:(\d+)$/.exec(member.pane || "");
1048
+ const paneIndex = m ? parseInt(m[1], 10) : NaN;
1049
+ if (!Number.isFinite(paneIndex)) {
1050
+ warn(`wt pane 인덱스 파싱 실패: ${member.pane}`);
1051
+ console.log("");
1052
+ return;
1053
+ }
1054
+ const focused = focusWtPane(paneIndex, {
1055
+ layout: state?.wt?.layout || state?.layout || "1xN",
1056
+ });
1057
+ if (focused) {
1058
+ ok(`${member.name} pane 포커스 이동 (wt)`);
1059
+ } else {
1060
+ warn("wt pane 포커스 이동 실패 (WT_SESSION/wt.exe 상태 확인 필요)");
1061
+ }
1062
+ console.log("");
1063
+ return;
1064
+ }
1065
+
1066
+ focusPane(member.pane, { zoom: (state.teammateMode === "in-process") });
1067
+ try {
1021
1068
  attachSession(state.sessionName);
1022
1069
  } catch (e) {
1023
1070
  const allowWt = wantsWtAttachFallback();
@@ -1049,19 +1096,19 @@ async function teamInterrupt() {
1049
1096
 
1050
1097
  const selector = process.argv[4] || "lead";
1051
1098
  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)) {
1099
+ if (!member) {
1100
+ console.log(`\n 사용법: ${WHITE}tfx team interrupt <lead|이름|번호>${RESET}\n`);
1101
+ return;
1102
+ }
1103
+
1104
+ if (isWtMode(state)) {
1105
+ warn("wt 모드에서는 pane stdin 주입이 지원되지 않아 interrupt를 자동 전송할 수 없습니다.");
1106
+ console.log(` ${DIM}수동으로 해당 pane에서 Ctrl+C를 입력하세요.${RESET}`);
1107
+ console.log("");
1108
+ return;
1109
+ }
1110
+
1111
+ if (isNativeMode(state)) {
1065
1112
  const result = await nativeRequest(state, "/interrupt", { member: member.name });
1066
1113
  if (result?.ok) {
1067
1114
  ok(`${member.name} 인터럽트 전송`);
@@ -1090,20 +1137,20 @@ async function teamControl() {
1090
1137
  const member = resolveMember(state, selector);
1091
1138
  const allowed = new Set(["interrupt", "stop", "pause", "resume"]);
1092
1139
 
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;
1140
+ if (!member || !allowed.has(command)) {
1141
+ console.log(`\n 사용법: ${WHITE}tfx team control <lead|이름|번호> <interrupt|stop|pause|resume> [사유]${RESET}\n`);
1142
+ return;
1143
+ }
1144
+
1145
+ if (isWtMode(state)) {
1146
+ warn("wt 모드는 Hub direct/control 주입 경로가 비활성입니다.");
1147
+ console.log(` ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}`);
1148
+ console.log("");
1149
+ return;
1150
+ }
1151
+
1152
+ // 직접 주입: MCP 유무와 무관하게 즉시 전달
1153
+ let directOk = false;
1107
1154
  if (isNativeMode(state)) {
1108
1155
  const direct = await nativeRequest(state, "/control", {
1109
1156
  member: member.name,
@@ -1140,19 +1187,19 @@ async function teamStop() {
1140
1187
  return;
1141
1188
  }
1142
1189
 
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);
1190
+ if (isNativeMode(state)) {
1191
+ await nativeRequest(state, "/stop", {});
1192
+ try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
1193
+ ok(`세션 종료: ${state.sessionName}`);
1194
+ } else if (isWtMode(state)) {
1195
+ const closed = closeWtSession({
1196
+ layout: state?.wt?.layout || state?.layout || "1xN",
1197
+ paneCount: state?.wt?.paneCount ?? (state.members || []).length,
1198
+ });
1199
+ ok(`세션 종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
1200
+ } else {
1201
+ if (sessionExists(state.sessionName)) {
1202
+ killSession(state.sessionName);
1156
1203
  ok(`세션 종료: ${state.sessionName}`);
1157
1204
  } else {
1158
1205
  console.log(` ${DIM}세션 이미 종료됨${RESET}`);
@@ -1163,28 +1210,28 @@ async function teamStop() {
1163
1210
  console.log("");
1164
1211
  }
1165
1212
 
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();
1213
+ async function teamKill() {
1214
+ const state = loadTeamState();
1215
+ if (state && isNativeMode(state) && isTeamAlive(state)) {
1216
+ await nativeRequest(state, "/stop", {});
1217
+ try { process.kill(state.native.supervisorPid, "SIGTERM"); } catch {}
1218
+ clearTeamState();
1219
+ ok(`종료: ${state.sessionName}`);
1220
+ console.log("");
1221
+ return;
1222
+ }
1223
+ if (state && isWtMode(state)) {
1224
+ const closed = closeWtSession({
1225
+ layout: state?.wt?.layout || state?.layout || "1xN",
1226
+ paneCount: state?.wt?.paneCount ?? (state.members || []).length,
1227
+ });
1228
+ clearTeamState();
1229
+ ok(`종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`);
1230
+ console.log("");
1231
+ return;
1232
+ }
1233
+
1234
+ const sessions = listSessions();
1188
1235
  if (sessions.length === 0) {
1189
1236
  console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
1190
1237
  return;
@@ -1207,19 +1254,19 @@ async function teamSend() {
1207
1254
  const selector = process.argv[4];
1208
1255
  const message = process.argv.slice(5).join(" ");
1209
1256
  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)) {
1257
+ if (!member || !message) {
1258
+ console.log(`\n 사용법: ${WHITE}tfx team send <lead|이름|번호> "메시지"${RESET}\n`);
1259
+ return;
1260
+ }
1261
+
1262
+ if (isWtMode(state)) {
1263
+ warn("wt 모드는 pane 프롬프트 자동 주입(send)이 지원되지 않습니다.");
1264
+ console.log(` ${DIM}수동 전달: 선택한 pane에 직접 붙여넣으세요.${RESET}`);
1265
+ console.log("");
1266
+ return;
1267
+ }
1268
+
1269
+ if (isNativeMode(state)) {
1223
1270
  const result = await nativeRequest(state, "/send", { member: member.name, text: message });
1224
1271
  if (result?.ok) {
1225
1272
  ok(`${member.name}에 메시지 주입 완료`);
@@ -1235,22 +1282,22 @@ async function teamSend() {
1235
1282
  console.log("");
1236
1283
  }
1237
1284
 
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();
1285
+ function teamList() {
1286
+ const state = loadTeamState();
1287
+ if (state && isNativeMode(state) && isTeamAlive(state)) {
1288
+ console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
1289
+ console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(in-process)${RESET}`);
1290
+ console.log("");
1291
+ return;
1292
+ }
1293
+ if (state && isWtMode(state) && isTeamAlive(state)) {
1294
+ console.log(`\n ${AMBER}${BOLD}⬡ 팀 세션 목록${RESET}\n`);
1295
+ console.log(` ${GREEN}●${RESET} ${state.sessionName} ${DIM}(wt)${RESET}`);
1296
+ console.log("");
1297
+ return;
1298
+ }
1299
+
1300
+ const sessions = listSessions();
1254
1301
  if (sessions.length === 0) {
1255
1302
  console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
1256
1303
  return;
@@ -1266,14 +1313,14 @@ function teamHelp() {
1266
1313
  console.log(`
1267
1314
  ${AMBER}${BOLD}⬡ tfx team${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
1268
1315
 
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}
1316
+ ${BOLD}시작${RESET}
1317
+ ${WHITE}tfx team "작업 설명"${RESET}
1318
+ ${WHITE}tfx team --agents codex,gemini --lead claude "작업"${RESET}
1319
+ ${WHITE}tfx team --teammate-mode tmux "작업"${RESET}
1320
+ ${WHITE}tfx team --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}
1321
+ ${WHITE}tfx team --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
1322
+ ${WHITE}tfx team --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
1323
+ ${WHITE}tfx team --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
1277
1324
 
1278
1325
  ${BOLD}제어${RESET}
1279
1326
  ${WHITE}tfx team status${RESET} ${GRAY}현재 팀 상태${RESET}