triflux 3.2.0-dev.14 → 3.2.0-dev.16
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/hub/team/native.mjs +71 -0
- package/hub/team/psmux.mjs +34 -10
- package/package.json +1 -1
package/hub/team/native.mjs
CHANGED
|
@@ -80,6 +80,77 @@ Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM
|
|
|
80
80
|
실패 여부는 metadata.result로 구분. Bash 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* v3 하이브리드 래퍼 프롬프트 생성
|
|
85
|
+
* psmux pane 기반 비동기 실행 + polling 패턴.
|
|
86
|
+
* Agent가 idle 상태를 유지하여 인터럽트 수신이 가능하다.
|
|
87
|
+
*
|
|
88
|
+
* @param {'codex'|'gemini'} cli — CLI 타입
|
|
89
|
+
* @param {object} opts
|
|
90
|
+
* @param {string} opts.subtask — 서브태스크 설명
|
|
91
|
+
* @param {string} [opts.role] — 역할
|
|
92
|
+
* @param {string} [opts.teamName] — 팀 이름
|
|
93
|
+
* @param {string} [opts.taskId] — Hub task ID
|
|
94
|
+
* @param {string} [opts.agentName] — 워커 표시 이름
|
|
95
|
+
* @param {string} [opts.leadName] — 리드 수신자 이름
|
|
96
|
+
* @param {string} [opts.mcp_profile] — MCP 프로필
|
|
97
|
+
* @param {string} [opts.sessionName] — psmux 세션 이름
|
|
98
|
+
* @param {string} [opts.pipelinePhase] — 파이프라인 단계
|
|
99
|
+
* @param {string} [opts.psmuxPath] — psmux.mjs 경로
|
|
100
|
+
* @returns {string} 하이브리드 래퍼 프롬프트
|
|
101
|
+
*/
|
|
102
|
+
export function buildHybridWrapperPrompt(cli, opts = {}) {
|
|
103
|
+
const {
|
|
104
|
+
subtask,
|
|
105
|
+
role = "executor",
|
|
106
|
+
teamName = "tfx-multi",
|
|
107
|
+
taskId = "",
|
|
108
|
+
agentName = "",
|
|
109
|
+
leadName = "team-lead",
|
|
110
|
+
mcp_profile = "auto",
|
|
111
|
+
sessionName = teamName,
|
|
112
|
+
pipelinePhase = "",
|
|
113
|
+
psmuxPath = "hub/team/psmux.mjs",
|
|
114
|
+
} = opts;
|
|
115
|
+
|
|
116
|
+
const escaped = subtask.replace(/'/g, "'\\''");
|
|
117
|
+
const pipelineHint = pipelinePhase ? `\n파이프라인 단계: ${pipelinePhase}` : "";
|
|
118
|
+
const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
|
|
119
|
+
const taskIdArg = taskIdRef ? `${taskIdRef}, ` : "";
|
|
120
|
+
|
|
121
|
+
const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}" bash ${ROUTE_SCRIPT} "${role}" '${escaped}' ${mcp_profile}`;
|
|
122
|
+
|
|
123
|
+
return `하이브리드 psmux 워커 프로토콜:
|
|
124
|
+
|
|
125
|
+
1. TaskUpdate(${taskIdArg}status: in_progress) + SendMessage(to: ${leadName}, "작업 시작: ${agentName}")
|
|
126
|
+
|
|
127
|
+
2. pane 생성 (비동기 실행):
|
|
128
|
+
Bash: node ${psmuxPath} spawn --session "${sessionName}" --name "${agentName}" --cmd "${routeCmd}"
|
|
129
|
+
|
|
130
|
+
3. 폴링 루프 (10초 간격, idle 유지 → 인터럽트 수신 가능):
|
|
131
|
+
Bash: node ${psmuxPath} status --session "${sessionName}" --name "${agentName}"
|
|
132
|
+
- status: "running" → 10초 대기 후 재확인
|
|
133
|
+
- status: "exited" → 5단계로
|
|
134
|
+
|
|
135
|
+
4. 인터럽트 수신 시:
|
|
136
|
+
Bash: node ${psmuxPath} kill --session "${sessionName}" --name "${agentName}"
|
|
137
|
+
→ SendMessage(to: ${leadName}, "인터럽트 수신, 방향 전환")
|
|
138
|
+
→ 새 지시에 따라 2단계부터 재실행
|
|
139
|
+
|
|
140
|
+
5. 완료 시:
|
|
141
|
+
Bash: node ${psmuxPath} output --session "${sessionName}" --name "${agentName}" --lines 100
|
|
142
|
+
→ 결과를 TaskUpdate + SendMessage로 보고
|
|
143
|
+
${pipelineHint}
|
|
144
|
+
gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통해 실행한다.
|
|
145
|
+
프롬프트를 파일로 저장하지 마라. psmux spawn --cmd 인자로 전달된다.
|
|
146
|
+
|
|
147
|
+
성공 → TaskUpdate(${taskIdArg}status: completed, metadata: {result: "success"}) + SendMessage(to: ${leadName}).
|
|
148
|
+
실패 → TaskUpdate(${taskIdArg}status: completed, metadata: {result: "failed", error: "에러 요약"}) + SendMessage(to: ${leadName}).
|
|
149
|
+
|
|
150
|
+
중요: TaskUpdate의 status는 "completed"만 사용. "failed"는 API 미지원.
|
|
151
|
+
실패 여부는 metadata.result로 구분. pane 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
83
154
|
/**
|
|
84
155
|
* 팀 이름 생성 (타임스탬프 기반)
|
|
85
156
|
* @returns {string}
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { execSync, spawnSync } from "node:child_process";
|
|
4
4
|
|
|
5
5
|
const PSMUX_BIN = process.env.PSMUX_BIN || "psmux";
|
|
6
|
+
const GIT_BASH = process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
7
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
6
8
|
|
|
7
9
|
function quoteArg(value) {
|
|
8
10
|
const str = String(value);
|
|
@@ -309,12 +311,27 @@ export function spawnWorker(sessionName, workerName, cmd) {
|
|
|
309
311
|
if (!hasPsmux()) {
|
|
310
312
|
throw new Error("psmux가 설치되어 있지 않습니다. psmux를 먼저 설치하세요.");
|
|
311
313
|
}
|
|
314
|
+
|
|
315
|
+
// remain-on-exit: 종료된 pane이 즉시 사라지는 것 방지
|
|
312
316
|
try {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
psmuxExec(`set-option -t ${quoteArg(sessionName)} remain-on-exit on`);
|
|
318
|
+
} catch { /* 미지원 시 무시 */ }
|
|
319
|
+
|
|
320
|
+
// Windows: pane 기본셸이 PowerShell → Git Bash로 래핑
|
|
321
|
+
// psmux가 이스케이프 시퀀스를 처리하므로 포워드 슬래시로 변환
|
|
322
|
+
const shellCmd = IS_WINDOWS
|
|
323
|
+
? `& '${GIT_BASH.replace(/\\/g, '/')}' -l -c '${cmd.replace(/'/g, "'\\''")}'`
|
|
324
|
+
: cmd;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// 배열 형태 spawnSync → 쉘 해석 우회 (백슬래시 경로 보존)
|
|
328
|
+
const paneTarget = psmux([
|
|
329
|
+
"split-window", "-t", sessionName,
|
|
330
|
+
"-P", "-F", "#{session_name}:#{window_index}.#{pane_index}",
|
|
331
|
+
shellCmd,
|
|
332
|
+
]);
|
|
333
|
+
psmux(["select-pane", "-t", paneTarget, "-T", workerName]);
|
|
334
|
+
return { paneId: paneTarget, workerName };
|
|
318
335
|
} catch (err) {
|
|
319
336
|
throw new Error(`워커 생성 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
320
337
|
}
|
|
@@ -332,7 +349,7 @@ export function getWorkerStatus(sessionName, workerName) {
|
|
|
332
349
|
}
|
|
333
350
|
try {
|
|
334
351
|
const output = psmuxExec(
|
|
335
|
-
`list-panes -t ${quoteArg(sessionName)} -F "#{pane_title}\t#{
|
|
352
|
+
`list-panes -t ${quoteArg(sessionName)} -F "#{pane_title}\t#{session_name}:#{window_index}.#{pane_index}\t#{pane_dead}\t#{pane_dead_status}"`
|
|
336
353
|
);
|
|
337
354
|
const lines = output.split("\n").filter(Boolean);
|
|
338
355
|
for (const line of lines) {
|
|
@@ -364,9 +381,15 @@ export function killWorker(sessionName, workerName) {
|
|
|
364
381
|
throw new Error("psmux가 설치되어 있지 않습니다.");
|
|
365
382
|
}
|
|
366
383
|
try {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
//
|
|
384
|
+
const { paneId, status } = getWorkerStatus(sessionName, workerName);
|
|
385
|
+
|
|
386
|
+
// 이미 종료된 워커 → pane 정리만 수행
|
|
387
|
+
if (status === "exited") {
|
|
388
|
+
try { psmuxExec(`kill-pane -t ${quoteArg(paneId)}`); } catch { /* 무시 */ }
|
|
389
|
+
return { killed: true };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// running → C-c 우아한 종료 시도
|
|
370
393
|
try {
|
|
371
394
|
psmuxExec(`send-keys -t ${quoteArg(paneId)} C-c`);
|
|
372
395
|
} catch {
|
|
@@ -381,8 +404,9 @@ export function killWorker(sessionName, workerName) {
|
|
|
381
404
|
}
|
|
382
405
|
return { killed: true };
|
|
383
406
|
} catch (err) {
|
|
407
|
+
// 워커를 찾을 수 없음 → 이미 종료된 것으로 간주
|
|
384
408
|
if (err.message.includes("워커를 찾을 수 없습니다")) {
|
|
385
|
-
return { killed:
|
|
409
|
+
return { killed: true };
|
|
386
410
|
}
|
|
387
411
|
throw new Error(`워커 종료 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
|
|
388
412
|
}
|