triflux 3.2.0-dev.3 → 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/README.ko.md +19 -13
- package/README.md +19 -13
- package/bin/triflux.mjs +6 -5
- package/hooks/hooks.json +2 -2
- package/hooks/keyword-rules.json +338 -0
- package/hub/team/cli.mjs +406 -359
- package/hub/team/dashboard.mjs +164 -55
- package/hub/team/native.mjs +38 -0
- package/hud/hud-qos-status.mjs +56 -1
- package/package.json +3 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -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 +5 -4
- 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 +90 -63
- package/scripts/team-keyword.mjs +0 -35
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}
|