triflux 6.0.5 → 6.0.7

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.
@@ -38,7 +38,9 @@ function renderTmuxInstallHelp() {
38
38
  export { parseTeamArgs };
39
39
 
40
40
  export async function teamStart(args = []) {
41
- const { agents, lead, layout, teammateMode, task } = parseTeamArgs(args);
41
+ const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec } = parseTeamArgs(args);
42
+ // --assign 사용 시 task를 자동 생성
43
+ const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
42
44
  if (!task) return printStartUsage();
43
45
 
44
46
  console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
@@ -70,7 +72,7 @@ export async function teamStart(args = []) {
70
72
  const state = effectiveMode === "in-process"
71
73
  ? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
72
74
  : effectiveMode === "headless"
73
- ? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout })
75
+ ? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec })
74
76
  : effectiveMode === "wt"
75
77
  ? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
76
78
  : await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
@@ -1,32 +1,52 @@
1
- import { normalizeLayout, normalizeTeammateMode } from "../../services/runtime-mode.mjs";
2
-
3
- export function parseTeamArgs(args = []) {
4
- let agents = ["codex", "gemini"];
5
- let lead = "claude";
6
- let layout = "2x2";
7
- let teammateMode = "auto";
8
- const taskParts = [];
9
-
10
- for (let index = 0; index < args.length; index += 1) {
11
- const current = args[index];
12
- if (current === "--agents" && args[index + 1]) {
13
- agents = args[++index].split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
14
- } else if (current === "--lead" && args[index + 1]) {
15
- lead = args[++index].trim().toLowerCase();
16
- } else if (current === "--layout" && args[index + 1]) {
17
- layout = args[++index];
18
- } else if ((current === "--teammate-mode" || current === "--mode") && args[index + 1]) {
19
- teammateMode = args[++index];
20
- } else if (!current.startsWith("-")) {
21
- taskParts.push(current);
22
- }
23
- }
24
-
25
- return {
26
- agents,
27
- lead,
28
- layout: normalizeLayout(layout),
29
- teammateMode: normalizeTeammateMode(teammateMode),
30
- task: taskParts.join(" ").trim(),
31
- };
32
- }
1
+ import { normalizeLayout, normalizeTeammateMode } from "../../services/runtime-mode.mjs";
2
+
3
+ export function parseTeamArgs(args = []) {
4
+ let agents = ["codex", "gemini"];
5
+ let lead = "claude";
6
+ let layout = "2x2";
7
+ let teammateMode = "auto";
8
+ const taskParts = [];
9
+ const assigns = []; // --assign "codex:프롬프트:역할" 형식
10
+ let autoAttach = false;
11
+ let progressive = true;
12
+ let timeoutSec = 300;
13
+
14
+ for (let index = 0; index < args.length; index += 1) {
15
+ const current = args[index];
16
+ if (current === "--agents" && args[index + 1]) {
17
+ agents = args[++index].split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
18
+ } else if (current === "--lead" && args[index + 1]) {
19
+ lead = args[++index].trim().toLowerCase();
20
+ } else if (current === "--layout" && args[index + 1]) {
21
+ layout = args[++index];
22
+ } else if ((current === "--teammate-mode" || current === "--mode") && args[index + 1]) {
23
+ teammateMode = args[++index];
24
+ } else if (current === "--assign" && args[index + 1]) {
25
+ // "cli:prompt:role" 형식 파싱
26
+ const parts = args[++index].split(":");
27
+ if (parts.length >= 2) {
28
+ assigns.push({ cli: parts[0].trim(), prompt: parts.slice(1, -1).join(":").trim() || parts[1].trim(), role: parts[parts.length - 1]?.trim() || "" });
29
+ }
30
+ } else if (current === "--auto-attach") {
31
+ autoAttach = true;
32
+ } else if (current === "--no-progressive") {
33
+ progressive = false;
34
+ } else if (current === "--timeout" && args[index + 1]) {
35
+ timeoutSec = Number(args[++index]) || 300;
36
+ } else if (!current.startsWith("-")) {
37
+ taskParts.push(current);
38
+ }
39
+ }
40
+
41
+ return {
42
+ agents,
43
+ lead,
44
+ layout: normalizeLayout(layout),
45
+ teammateMode: normalizeTeammateMode(teammateMode),
46
+ task: taskParts.join(" ").trim(),
47
+ assigns,
48
+ autoAttach,
49
+ progressive,
50
+ timeoutSec,
51
+ };
52
+ }
@@ -1,33 +1,43 @@
1
1
  import { BOLD, DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
2
- import { runHeadless } from "../../../headless.mjs";
3
- import { killPsmuxSession } from "../../../psmux.mjs";
2
+ import { runHeadlessInteractive } from "../../../headless.mjs";
4
3
  import { ok, warn } from "../../render.mjs";
5
4
  import { buildTasks } from "../../services/task-model.mjs";
6
5
 
7
- export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout }) {
8
- console.log(` ${AMBER}모드: headless (psmux 헤드리스 CLI 실행)${RESET}`);
6
+ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec }) {
7
+ console.log(` ${AMBER}모드: headless (Lead-Direct v6.0.0)${RESET}`);
9
8
 
10
- const assignments = subtasks.map((subtask, i) => ({
11
- cli: agents[i],
12
- prompt: subtask,
13
- role: `worker-${i + 1}`,
14
- }));
9
+ // --assign이 있으면 그것을 사용, 없으면 agents+subtasks 조합
10
+ const assignments = assigns && assigns.length > 0
11
+ ? assigns.map((a, i) => ({ cli: a.cli, prompt: a.prompt, role: a.role || `worker-${i + 1}` }))
12
+ : subtasks.map((subtask, i) => ({ cli: agents[i] || agents[0], prompt: subtask, role: `worker-${i + 1}` }));
15
13
 
16
- ok("헤드리스 실행 시작...");
17
- const { sessionName, results } = await runHeadless(sessionId, assignments, {
18
- timeoutSec: 300,
14
+ ok(`헤드리스 실행 시작 (${assignments.length}워커, progressive=${progressive !== false})`);
15
+
16
+ const handle = await runHeadlessInteractive(sessionId, assignments, {
17
+ timeoutSec: timeoutSec || 300,
19
18
  layout,
19
+ autoAttach: autoAttach !== false, // 기본 true
20
+ progressive: progressive !== false, // 기본 true
21
+ progressIntervalSec: 10,
20
22
  onProgress(event) {
21
- if (event.type === "dispatched") {
23
+ if (event.type === "session_created") {
24
+ console.log(` ${DIM}세션: ${event.sessionName}${RESET}`);
25
+ } else if (event.type === "worker_added") {
26
+ console.log(` ${DIM}[+] ${event.paneTitle}${RESET}`);
27
+ } else if (event.type === "dispatched") {
22
28
  console.log(` ${DIM}[${event.paneName}] ${event.cli} dispatch${RESET}`);
29
+ } else if (event.type === "progress") {
30
+ const last = (event.snapshot || "").split("\n").filter(l => l.trim()).pop() || "";
31
+ if (last) console.log(` ${DIM}[${event.paneName}] ${last.slice(0, 60)}${RESET}`);
23
32
  } else if (event.type === "completed") {
24
33
  const icon = event.matched && event.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
25
- console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (session dead)" : ""}`);
34
+ console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (dead)" : ""}`);
26
35
  }
27
36
  },
28
37
  });
29
38
 
30
39
  // 결과 요약
40
+ const results = handle.results;
31
41
  const succeeded = results.filter((r) => r.matched && r.exitCode === 0);
32
42
  const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
33
43
 
@@ -36,41 +46,39 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
36
46
 
37
47
  if (failed.length > 0) {
38
48
  warn("실패 워커:");
39
- for (const r of failed) {
40
- console.log(` ${r.paneName} (${r.cli}): exit=${r.exitCode}${r.sessionDead ? " session dead" : ""}`);
41
- }
49
+ for (const r of failed) console.log(` ${r.paneName} (${r.cli}): exit=${r.exitCode}`);
42
50
  }
43
51
 
44
- // 결과 출력 (각 워커의 output 요약)
52
+ // 결과 출력 + JSON stdout
45
53
  for (const r of results) {
46
54
  if (r.output) {
47
55
  const preview = r.output.length > 200 ? `${r.output.slice(0, 200)}…` : r.output;
48
- console.log(`\n ${DIM}── ${r.paneName} (${r.cli}) ──${RESET}`);
56
+ console.log(`\n ${DIM}── ${r.paneName} (${r.cli}${r.role ? `, ${r.role}` : ""}) ──${RESET}`);
49
57
  console.log(` ${preview}`);
50
58
  }
51
59
  }
52
60
 
53
61
  // 세션 정리
54
- try { killPsmuxSession(sessionName); } catch { /* already cleaned */ }
62
+ handle.kill();
55
63
 
56
64
  const members = [
57
- { role: "lead", name: "lead", cli: lead, pane: `${sessionName}:0.0` },
58
- ...results.map((r, i) => ({ role: "worker", name: r.paneName, cli: r.cli, pane: `${sessionName}:0.${i + 1}`, subtask: subtasks[i] })),
65
+ { role: "lead", name: "lead", cli: lead, pane: `${handle.sessionName}:0.0` },
66
+ ...results.map((r, i) => ({ role: "worker", name: r.paneName, cli: r.cli, pane: r.paneId || "", subtask: assignments[i]?.prompt })),
59
67
  ];
60
68
 
61
69
  return {
62
- sessionName,
70
+ sessionName: handle.sessionName,
63
71
  task,
64
72
  lead,
65
- agents,
73
+ agents: assignments.map(a => a.cli),
66
74
  layout,
67
75
  teammateMode: "headless",
68
76
  startedAt: Date.now(),
69
77
  members,
70
78
  headlessResults: results,
71
- tasks: buildTasks(subtasks, members.filter((m) => m.role === "worker")),
79
+ tasks: buildTasks(assignments.map(a => a.prompt), members.filter((m) => m.role === "worker")),
72
80
  postSave() {
73
- console.log(`\n ${DIM}세션 자동 정리 완료. 결과는 위에 표시됨.${RESET}\n`);
81
+ console.log(`\n ${DIM}세션 정리 완료.${RESET}\n`);
74
82
  },
75
83
  };
76
84
  }
@@ -20,6 +20,15 @@ import {
20
20
 
21
21
  const RESULT_DIR = join(tmpdir(), "tfx-headless");
22
22
 
23
+ /** CLI별 브랜드 — 이모지 + ANSI 색상 (시각적 구분) */
24
+ const CLI_BRAND = {
25
+ codex: { emoji: "\u{1F7E2}", label: "Codex", ansi: "\x1b[32m" }, // 🟢 green
26
+ gemini: { emoji: "\u{1F535}", label: "Gemini", ansi: "\x1b[34m" }, // 🔵 blue
27
+ claude: { emoji: "\u{1F7E0}", label: "Claude", ansi: "\x1b[33m" }, // 🟠 yellow/orange
28
+ };
29
+ const ANSI_RESET = "\x1b[0m";
30
+ const ANSI_DIM = "\x1b[2m";
31
+
23
32
  /**
24
33
  * CLI별 헤드리스 명령 빌더
25
34
  * @param {'codex'|'gemini'|'claude'} cli
@@ -96,25 +105,34 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
96
105
 
97
106
  dispatches = assignments.map((assignment, i) => {
98
107
  const paneName = `worker-${i + 1}`;
108
+ const brand = CLI_BRAND[assignment.cli] || { emoji: "\u{25CF}", label: assignment.cli, ansi: "" };
99
109
  const paneTitle = assignment.role
100
- ? `${assignment.cli} (${assignment.role})`
101
- : `${assignment.cli}-${i + 1}`;
102
-
103
- // split-window로 새 pane 추가 — paneId 직접 획득
104
- const newPaneId = psmuxExec([
105
- "split-window", "-t", sessionName, "-P", "-F",
106
- "#{session_name}:#{window_index}.#{pane_index}",
107
- ]);
110
+ ? `${brand.emoji} ${assignment.cli} (${assignment.role})`
111
+ : `${brand.emoji} ${assignment.cli}-${i + 1}`;
112
+
113
+ let newPaneId;
114
+ if (i === 0) {
115
+ // 번째 워커: 빈 lead pane을 직접 사용 (빈 pane 제거)
116
+ newPaneId = `${sessionName}:0.0`;
117
+ } else {
118
+ // 2번째+: split-window로 추가
119
+ newPaneId = psmuxExec([
120
+ "split-window", "-t", sessionName, "-P", "-F",
121
+ "#{session_name}:#{window_index}.#{pane_index}",
122
+ ]);
123
+ }
108
124
 
109
- // 타이틀 설정
125
+ // 타이틀 설정 (이모지 포함)
110
126
  try { psmuxExec(["select-pane", "-t", newPaneId, "-T", paneTitle]); } catch { /* 무시 */ }
111
127
 
112
128
  if (safeProgress) safeProgress({ type: "worker_added", paneName, cli: assignment.cli, paneTitle });
113
129
 
114
- // 캡처 시작 + 명령 dispatch (paneId 직접 사용 — resolvePane race 회피)
130
+ // 캡처 시작 + 컬러 배너 + 명령 dispatch
115
131
  const resultFile = join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
116
132
  const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile);
117
133
  startCapture(sessionName, newPaneId);
134
+ // pane 간 pipe-pane EBUSY 방지 — capture 스크립트 파일 잠금 해제 대기
135
+ if (i > 0) { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300); } catch {} }
118
136
  const dispatch = dispatchCommand(sessionName, newPaneId, cmd);
119
137
 
120
138
  if (safeProgress) safeProgress({ type: "dispatched", paneName, cli: assignment.cli });
@@ -276,15 +294,16 @@ export function autoAttachTerminal(sessionName, opts = {}) {
276
294
  return false; // wt.exe 미설치 — 사용자에게 수동 attach 안내 필요
277
295
  }
278
296
 
279
- // PowerShell 래핑 wt가 psmux를 파일로 인식하는 문제 방지
297
+ // PowerShell 래핑 + "--" 구분자 + 포커스 비탈취
280
298
  // pwsh.exe (PS7) 우선, 없으면 powershell.exe (PS5.1) fallback
281
299
  const shells = ["pwsh.exe", "powershell.exe"];
282
300
  for (const shell of shells) {
283
301
  try {
284
- execFileSync("wt.exe", [
285
- "nt", shell, "-NoExit", "-Command",
286
- `psmux attach -t ${sessionName}`,
287
- ], { stdio: "ignore" });
302
+ // start "" /b — 포커스를 현재 창에 유지 (사용자 타이핑 보호)
303
+ execSync(
304
+ `start "" /b wt.exe nt --title triflux -- ${shell} -NoExit -Command "psmux attach -t ${sessionName}"`,
305
+ { stdio: "ignore", shell: true, timeout: 5000 },
306
+ );
288
307
  return true;
289
308
  } catch { /* 다음 shell 시도 */ }
290
309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "6.0.5",
3
+ "version": "6.0.7",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {