triflux 3.2.0-dev.1 → 3.2.0-dev.10

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