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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} else if (
|
|
19
|
-
|
|
20
|
-
} else if (
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
prompt:
|
|
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
|
-
|
|
18
|
-
|
|
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 === "
|
|
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 ? " (
|
|
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
|
-
// 결과 출력
|
|
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
|
-
|
|
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:
|
|
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(
|
|
79
|
+
tasks: buildTasks(assignments.map(a => a.prompt), members.filter((m) => m.role === "worker")),
|
|
72
80
|
postSave() {
|
|
73
|
-
console.log(`\n ${DIM}세션
|
|
81
|
+
console.log(`\n ${DIM}세션 정리 완료.${RESET}\n`);
|
|
74
82
|
},
|
|
75
83
|
};
|
|
76
84
|
}
|
package/hub/team/headless.mjs
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
// 캡처 시작 +
|
|
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 래핑
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
`psmux attach -t ${sessionName}`,
|
|
287
|
-
|
|
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
|
}
|