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.
@@ -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}
@@ -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
- const paneId = psmuxExec(
314
- `split-window -t ${quoteArg(sessionName)} -P -F "#{pane_id}" ${quoteArg(cmd)}`
315
- );
316
- psmuxExec(`select-pane -t ${quoteArg(paneId)} -T ${quoteArg(workerName)}`);
317
- return { paneId, workerName };
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#{pane_id}\t#{pane_dead}\t#{pane_dead_status}"`
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
- // paneId 찾기
368
- const { paneId } = getWorkerStatus(sessionName, workerName);
369
- // C-c로 우아한 종료 시도
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: false };
409
+ return { killed: true };
386
410
  }
387
411
  throw new Error(`워커 종료 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
388
412
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "3.2.0-dev.14",
3
+ "version": "3.2.0-dev.16",
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": {