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