triflux 6.0.4 → 6.0.6
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
|
@@ -171,7 +171,9 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
// dispatch 시 확정된 logPath를 전달 — 셸이 pane 타이틀 변경해도 캡처 로그 매칭 유지
|
|
175
|
+
if (d.logPath) pollOpts.logPath = d.logPath;
|
|
176
|
+
const completion = await waitForCompletion(sessionName, d.paneId || d.paneName, d.token, timeoutSec, pollOpts);
|
|
175
177
|
|
|
176
178
|
const output = completion.matched
|
|
177
179
|
? readResult(d.resultFile, d.paneId)
|
|
@@ -247,6 +249,8 @@ export function applyTrifluxTheme(sessionName) {
|
|
|
247
249
|
["pane-border-style", "fg=#45475a"],
|
|
248
250
|
// Status bar 위치
|
|
249
251
|
["status-position", "bottom"],
|
|
252
|
+
// 셸이 pane 타이틀을 변경하는 것 방지 (캡처 로그 경로 안정성)
|
|
253
|
+
["allow-rename", "off"],
|
|
250
254
|
];
|
|
251
255
|
for (const [key, value] of opts) {
|
|
252
256
|
try { psmuxExec(["set-option", "-t", sessionName, key, value]); } catch { /* 무시 */ }
|
|
@@ -272,18 +276,21 @@ export function autoAttachTerminal(sessionName, opts = {}) {
|
|
|
272
276
|
return false; // wt.exe 미설치 — 사용자에게 수동 attach 안내 필요
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
// PowerShell 래핑 — wt가 psmux를 파일로 인식하는 문제 방지
|
|
280
|
+
// "--" 구분자 필수: -NoExit 등이 wt 옵션으로 해석되는 것 방지
|
|
281
|
+
// pwsh.exe (PS7) 우선, 없으면 powershell.exe (PS5.1) fallback
|
|
282
|
+
const shells = ["pwsh.exe", "powershell.exe"];
|
|
283
|
+
for (const shell of shells) {
|
|
280
284
|
try {
|
|
281
|
-
execFileSync("wt.exe", [
|
|
285
|
+
execFileSync("wt.exe", [
|
|
286
|
+
"nt", "--title", "triflux", "--",
|
|
287
|
+
shell, "-NoExit", "-Command",
|
|
288
|
+
`psmux attach -t ${sessionName}`,
|
|
289
|
+
], { stdio: "ignore" });
|
|
282
290
|
return true;
|
|
283
|
-
} catch {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
291
|
+
} catch { /* 다음 shell 시도 */ }
|
|
286
292
|
}
|
|
293
|
+
return false;
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
/**
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -667,7 +667,10 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
|
|
|
667
667
|
}
|
|
668
668
|
|
|
669
669
|
const paneName = pane.title || paneNameOrTarget;
|
|
670
|
-
|
|
670
|
+
// opts.logPath: dispatch 시 확정된 캡처 로그 경로 직접 지정 (타이틀 변경 내성)
|
|
671
|
+
const logPath = (opts.logPath && existsSync(opts.logPath))
|
|
672
|
+
? opts.logPath
|
|
673
|
+
: getCaptureLogPath(sessionName, paneName);
|
|
671
674
|
if (!existsSync(logPath)) {
|
|
672
675
|
throw new Error(`캡처 로그가 없습니다. 먼저 startCapture(${sessionName}, ${paneName})를 호출하세요.`);
|
|
673
676
|
}
|
|
@@ -679,7 +682,13 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
|
|
|
679
682
|
while (Date.now() <= deadline) {
|
|
680
683
|
// E4 크래시 복구: capture 실패 시 세션 생존 체크
|
|
681
684
|
try {
|
|
682
|
-
|
|
685
|
+
if (opts.logPath) {
|
|
686
|
+
// logPath 직접 지정 시 — 셸 타이틀 변경과 무관하게 올바른 파일에 기록
|
|
687
|
+
const snapshot = psmuxExec(["capture-pane", "-t", pane.paneId, "-p"]);
|
|
688
|
+
writeFileSync(logPath, snapshot, "utf8");
|
|
689
|
+
} else {
|
|
690
|
+
refreshCaptureSnapshot(sessionName, pane.paneId);
|
|
691
|
+
}
|
|
683
692
|
} catch {
|
|
684
693
|
if (!psmuxSessionExists(sessionName)) {
|
|
685
694
|
return {
|