triflux 6.0.16 → 6.0.18

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/bin/triflux.mjs CHANGED
@@ -1615,10 +1615,10 @@ function cmdUpdate() {
1615
1615
  stdio: ["pipe", "pipe", "pipe"],
1616
1616
  windowsHide: true,
1617
1617
  }).trim().split(/\r?\n/)[0];
1618
- } catch (retryErr) {
1619
- // Windows: 자기 자신의 파일 잠금으로 첫 시도 실패 가능 → 1회 재시도
1620
- info("첫 시도 실패, 재시도 중...");
1621
- result = execSync(npmCmd, {
1618
+ } catch {
1619
+ // Windows: 자기 자신의 파일 잠금으로 첫 시도 실패 가능 → --force 재시도
1620
+ info("첫 시도 실패, --force 재시도 중...");
1621
+ result = execSync(`${npmCmd} --force`, {
1622
1622
  encoding: "utf8",
1623
1623
  timeout: 90000,
1624
1624
  stdio: ["pipe", "pipe", "pipe"],
@@ -15,7 +15,7 @@ function printStartUsage() {
15
15
  console.log(`\n ${AMBER}${BOLD}⬡ tfx multi${RESET}\n`);
16
16
  console.log(` 사용법: ${WHITE}tfx multi "작업 설명"${RESET}`);
17
17
  console.log(` ${WHITE}tfx multi --agents codex,gemini --lead claude "작업"${RESET}`);
18
- console.log(` ${WHITE}tfx multi --teammate-mode psmux "작업"${RESET} ${DIM}(Windows psmux 네이티브)${RESET}`);
18
+ console.log(` ${WHITE}tfx multi --teammate-mode headless "작업"${RESET} ${DIM}(psmux 헤드리스, 기본)${RESET}`);
19
19
  console.log(` ${WHITE}tfx multi --teammate-mode wt "작업"${RESET} ${DIM}(Windows Terminal split-pane)${RESET}`);
20
20
  console.log(` ${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(mux 불필요)${RESET}\n`);
21
21
  }
@@ -9,11 +9,12 @@ export function normalizeTeammateMode(mode = "auto") {
9
9
  const raw = String(mode).toLowerCase();
10
10
  if (raw === "inline" || raw === "native") return "in-process";
11
11
  if (raw === "headless" || raw === "hl") return "headless";
12
- if (raw === "in-process" || raw === "tmux" || raw === "wt" || raw === "psmux") return raw;
12
+ if (raw === "psmux") return "headless";
13
+ if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
13
14
  if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
14
15
  if (raw === "auto") {
15
16
  if (process.env.TMUX) return "tmux";
16
- return detectMultiplexer() === "psmux" ? "psmux" : "in-process";
17
+ return detectMultiplexer() === "psmux" ? "headless" : "in-process";
17
18
  }
18
19
  return "in-process";
19
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "6.0.16",
3
+ "version": "6.0.18",
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": {
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * headless-guard.mjs — PreToolUse 훅 (auto-route 모드)
4
+ *
5
+ * Phase 3 headless 모드 활성 중 Lead가 Bash(tfx-route.sh)로 개별 호출하면
6
+ * deny 대신 자동으로 headless 명령으로 변환한다.
7
+ *
8
+ * 동작:
9
+ * - 마커 존재 + Bash(tfx-route.sh agent prompt mcp) → updatedInput: tfx multi --headless --assign
10
+ * - 마커 존재 + Agent(codex/gemini CLI 워커) → deny (tool 타입 변환 불가, 안내 메시지)
11
+ * - 마커 없음 → 전부 통과
12
+ * - 마커 30분 초과 → 자동 만료 (stale 방지)
13
+ *
14
+ * Exit 0 + stdout JSON: auto-route (updatedInput)
15
+ * Exit 2 + stderr: deny (Agent CLI 래핑만)
16
+ * Exit 0 (no stdout): allow
17
+ */
18
+
19
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+
23
+ const LOCK_FILE = join(tmpdir(), "tfx-headless-guard.lock");
24
+ const MAX_AGE_MS = 30 * 60 * 1000; // 30분
25
+
26
+ function isLockActive() {
27
+ if (!existsSync(LOCK_FILE)) return false;
28
+ try {
29
+ const ts = Number(readFileSync(LOCK_FILE, "utf8").trim());
30
+ if (Date.now() - ts > MAX_AGE_MS) {
31
+ unlinkSync(LOCK_FILE);
32
+ return false;
33
+ }
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * tfx-route.sh 명령에서 agent, prompt를 파싱한다.
42
+ * 형식: bash ~/.claude/scripts/tfx-route.sh {agent} '{prompt}' {mcp} [timeout] [context]
43
+ */
44
+ function parseRouteCommand(cmd) {
45
+ // MCP 프로필 목록 (tfx-route.sh의 마지막 위치 인자)
46
+ const MCP_PROFILES = ["implement", "analyze", "review", "docs"];
47
+
48
+ // 전략: agent명 추출 후, 나머지에서 MCP 프로필을 역방향으로 찾아 프롬프트 경계를 결정
49
+ const agentMatch = cmd.match(/tfx-route\.sh\s+(\S+)\s+/);
50
+ if (!agentMatch) return null;
51
+
52
+ const agent = agentMatch[1];
53
+ const afterAgent = cmd.slice(agentMatch.index + agentMatch[0].length);
54
+
55
+ // MCP 프로필을 역방향으로 찾기
56
+ let mcp = "";
57
+ let promptRaw = afterAgent;
58
+ for (const profile of MCP_PROFILES) {
59
+ // 프롬프트 뒤에 오는 MCP 프로필 (공백 구분)
60
+ const profileIdx = afterAgent.lastIndexOf(` ${profile}`);
61
+ if (profileIdx >= 0) {
62
+ mcp = profile;
63
+ promptRaw = afterAgent.slice(0, profileIdx);
64
+ break;
65
+ }
66
+ }
67
+
68
+ // 프롬프트에서 바깥쪽 따옴표 제거
69
+ const prompt = promptRaw
70
+ .replace(/^['"]/, "")
71
+ .replace(/['"]$/, "")
72
+ .replace(/'\\''/g, "'") // bash '\'' → '
73
+ .replace(/'"'"'/g, "'") // bash '"'"' → '
74
+ .trim();
75
+
76
+ return { agent, prompt, mcp };
77
+ }
78
+
79
+ function autoRoute(updatedCommand, reason) {
80
+ const output = {
81
+ hookSpecificOutput: {
82
+ hookEventName: "PreToolUse",
83
+ updatedInput: { command: updatedCommand },
84
+ additionalContext: reason,
85
+ },
86
+ };
87
+ process.stdout.write(JSON.stringify(output));
88
+ process.exit(0);
89
+ }
90
+
91
+ function deny(reason) {
92
+ process.stderr.write(reason);
93
+ process.exit(2);
94
+ }
95
+
96
+ async function main() {
97
+ if (!isLockActive()) process.exit(0);
98
+
99
+ let raw = "";
100
+ for await (const chunk of process.stdin) raw += chunk;
101
+
102
+ let input;
103
+ try {
104
+ input = JSON.parse(raw);
105
+ } catch {
106
+ process.exit(0);
107
+ }
108
+
109
+ const toolName = input.tool_name || "";
110
+ const toolInput = input.tool_input || {};
111
+
112
+ // ── Bash: tfx-route.sh → headless auto-route ──
113
+ if (toolName === "Bash") {
114
+ const cmd = toolInput.command || "";
115
+
116
+ // 이미 headless 명령이면 통과
117
+ if (cmd.includes("tfx multi") || cmd.includes("triflux.mjs multi")) {
118
+ process.exit(0);
119
+ }
120
+
121
+ // 마커 조작 통과
122
+ if (cmd.includes("tfx-headless-guard")) {
123
+ process.exit(0);
124
+ }
125
+
126
+ // codex/gemini 직접 CLI 호출 감지 → deny (auto-route 불가: 원본 agent/role 정보 없음)
127
+ if (/\bcodex\s+exec\b/.test(cmd) || /\bgemini\s+(-p|--prompt)\b/.test(cmd)) {
128
+ deny(
129
+ "[headless-guard] Phase 3 활성 중. codex/gemini를 직접 호출하지 마세요. " +
130
+ 'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...") 로 headless 엔진에 위임하세요.',
131
+ );
132
+ }
133
+
134
+ // tfx-route.sh 개별 호출 → headless 자동 변환
135
+ if (cmd.includes("tfx-route.sh")) {
136
+ const parsed = parseRouteCommand(cmd);
137
+ if (parsed) {
138
+ const role = parsed.agent;
139
+ // 프롬프트에서 싱글쿼트 이스케이프
140
+ const safePrompt = parsed.prompt.replace(/'/g, "'\\''");
141
+ const headlessCmd =
142
+ `tfx multi --teammate-mode headless --auto-attach ` +
143
+ `--assign '${parsed.agent}:${safePrompt}:${role}' --timeout 600`;
144
+ autoRoute(
145
+ headlessCmd,
146
+ `[headless-guard] auto-route: tfx-route.sh → headless 변환. 원본 agent=${parsed.agent}, mcp=${parsed.mcp}`,
147
+ );
148
+ }
149
+ // 파싱 실패 시 deny fallback
150
+ deny(
151
+ "[headless-guard] Phase 3 활성 중. tfx-route.sh 명령을 headless로 변환할 수 없습니다. " +
152
+ 'Bash("tfx multi --teammate-mode headless --auto-attach --assign \'cli:prompt:role\' ...") 형식을 사용하세요.',
153
+ );
154
+ }
155
+ }
156
+
157
+ // ── Agent: CLI 워커 래핑 시도 → deny (tool 타입 변환 불가) ──
158
+ if (toolName === "Agent") {
159
+ const prompt = (toolInput.prompt || "").toLowerCase();
160
+ const desc = (toolInput.description || "").toLowerCase();
161
+ const combined = `${prompt} ${desc}`;
162
+
163
+ const cliWorkerPatterns = [
164
+ /codex\s+(exec|run|실행)/,
165
+ /gemini\s+(-p|run|실행)/,
166
+ /tfx-route/,
167
+ /bash.*codex/,
168
+ /bash.*gemini/,
169
+ ];
170
+
171
+ if (cliWorkerPatterns.some((p) => p.test(combined))) {
172
+ deny(
173
+ "[headless-guard] Phase 3 활성 중. " +
174
+ "Codex/Gemini를 Agent()로 래핑하지 말고 headless --assign으로 전달하세요. " +
175
+ 'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")',
176
+ );
177
+ }
178
+ }
179
+
180
+ process.exit(0);
181
+ }
182
+
183
+ main().catch(() => process.exit(0));
@@ -160,19 +160,18 @@ Plan/PRD/Approval은 tfx-auto에서 실행, 그 후 tfx-multi Phase 3로 전환.
160
160
  | 2개+ + thorough | Plan/PRD/Approval 후 → headless + verify/fix | headless.mjs |
161
161
  | psmux 미설치 fallback | Native Teams (Agent slim wrapper) | native.mjs |
162
162
 
163
- **전환 방법:** 트리아지 완료 후 서브태스크 배열을 headless.runHeadlessInteractive()에 전달.
164
- Windows Terminal에 psmux 세션이 자동 팝업되어 사용자가 실시간으로 CLI 출력 확인.
163
+ > **MANDATORY: 2개+ 서브태스크 headless 엔진 필수**
164
+ > `Agent()` 백그라운드나 `Bash(tfx-route.sh)` 개별 호출로 대체 금지.
165
+ > 반드시 아래 `Bash("tfx multi ...")` 명령으로 headless 엔진에 위임한다.
166
+
167
+ **전환 방법:**
165
168
 
166
169
  ```
167
170
  thorough = args에 -t 또는 --thorough 포함
168
171
 
169
172
  if subtasks.length >= 2:
170
173
  if psmux 설치됨:
171
- headless.runHeadlessInteractive(assignments, {
172
- autoAttach: true, // WT 자동 팝업
173
- progressive: true, // 실시간 스플릿
174
- progressIntervalSec: 10,
175
- })
174
+ Bash("tfx multi --teammate-mode headless --auto-attach --assign 'cli:prompt:role' ...")
176
175
  → if thorough: verify → fix loop
177
176
  else:
178
177
  → fallback: tfx-multi Phase 3 Native Teams (Agent slim wrapper)
@@ -75,60 +75,73 @@ preflight와 Agent 생성을 병렬로 실행하여 사용자 체감 지연을
75
75
 
76
76
  ### Phase 3: Lead-Direct Headless 실행 (v6.0.0, 기본)
77
77
 
78
- CLI 워커(Codex/Gemini/Claude)를 Agent 래퍼 없이 Lead가 headless.mjs로 직접 실행.
79
- Windows Terminal에 psmux 세션이 자동 팝업되어 사용자가 실시간으로 CLI 출력을 확인.
80
-
81
- **핵심 기능:**
82
- - `progressive: true` (기본) — pane이 하나씩 split-window로 추가 (실시간 스플릿)
83
- - `autoAttach: true` 세션 생성 즉시 Windows Terminal 자동 팝업
84
- - `progressIntervalSec` — N초마다 각 pane 스냅샷을 onProgress로 전달
85
- - `applyTrifluxTheme()` — status bar + pane border 테마 자동 적용
86
- - 피드백 재실행 — 같은 pane에 후속 명령 dispatch (세션 유지)
87
-
88
- **Lead 오케스트레이션 패턴:**
89
-
90
- ```javascript
91
- // headless.mjs의 runHeadlessInteractive()를 Bash 내에서 호출
92
- // Lead는 Bash의 결과를 직접 파싱 Agent 래퍼 불필요
93
- const handle = await runHeadlessInteractive("tfx-session", [
94
- { cli: "codex", prompt: "코드 리뷰", role: "reviewer" },
95
- { cli: "gemini", prompt: "문서 작성", role: "writer" },
96
- { cli: "claude", prompt: "테스트 실행", role: "tester" },
97
- ], {
98
- timeoutSec: 300,
99
- autoAttach: true, // WT 자동 팝업
100
- progressive: true, // 실시간 스플릿 (기본)
101
- progressIntervalSec: 10, // 10초마다 진행 스냅샷
102
- });
103
- // handle: { results, dispatch(), capture(), snapshots(), waitFor(), kill() }
78
+ > **MANDATORY: 2개+ CLI 워커 실행 headless 엔진 필수**
79
+ > 서브태스크가 2개 이상이면 반드시 아래 `Bash()` 명령으로 headless 엔진을 실행한다.
80
+ > `Agent()` 백그라운드나 `Bash(tfx-route.sh)` 개별 호출로 대체하지 않는다.
81
+ > headless 엔진이 psmux 세션 생성 → WT 자동 팝업 → CLI dispatch → 결과 수집을 전부 처리한다.
82
+
83
+ **Step 1headless guard 활성화 (Phase 3 진입 시 반드시 먼저 실행):**
84
+
85
+ ```
86
+ Bash("node -e \"require('fs').writeFileSync(require('os').tmpdir()+'/tfx-headless-guard.lock', String(Date.now()))\"")
87
+ ```
88
+
89
+ **Step 2 — headless 엔진 실행 (Lead가 호출하는 유일한 명령):**
90
+
91
+ ```
92
+ Bash("tfx multi --teammate-mode headless --auto-attach --assign 'codex:{프롬프트1}:{역할1}' --assign 'gemini:{프롬프트2}:{역할2}' --timeout 600")
93
+ ```
94
+
95
+ **Step 3 headless guard 해제 (Phase 3 완료 후):**
96
+
97
+ ```
98
+ Bash("node -e \"try{require('fs').unlinkSync(require('os').tmpdir()+'/tfx-headless-guard.lock')}catch{}\"")
99
+ ```
100
+
101
+ **예시:**
102
+
103
+ ```
104
+ Bash("tfx multi --teammate-mode headless --auto-attach --assign 'codex:코드 리뷰하고 개선 사항 제안:reviewer' --assign 'gemini:API 문서 작성:writer' --assign 'codex:보안 취약점 분석:security' --timeout 600")
105
+ ```
106
+
107
+ **Claude 워커가 Read/Edit 필요한 경우 (하이브리드):**
108
+
109
+ Claude 워커는 headless에서 실행 불가 (Read/Edit 도구 필요). CLI 워커만 headless로 보내고 Claude 워커는 Agent로 병렬 실행.
110
+
111
+ ```
112
+ # 1. CLI 워커를 headless로 dispatch
113
+ Bash("tfx multi --teammate-mode headless --auto-attach --assign 'codex:{프롬프트}:{역할}' --assign 'gemini:{프롬프트}:{역할}' --timeout 600")
114
+
115
+ # 2. Claude 워커를 Agent로 병렬 실행 (headless Bash와 동시에 spawn)
116
+ Agent(subagent_type="...", prompt="...", run_in_background=true)
104
117
  ```
105
118
 
106
119
  **결정 로직:**
107
120
  ```
108
- Phase 3 선택:
109
- assignments.every(a => a.cli !== 'claude')
110
- Phase 3-direct (headless, 전부 CLI)
111
- assignments.some(a => a.cli === 'claude') AND Claude 워커가 Read/Edit 필요
112
- → Claude 워커: Agent(subagent_type), CLI 워커: Phase 3-direct
113
- fallback (psmux 미설치)
114
- → Phase 3 Native Teams (기존 slim wrapper)
115
- ```
116
-
117
- **CLI 헤드리스 명령 패턴:**
118
- | CLI | 명령 | 출력 |
119
- |-----|-------|------|
120
- | Codex | `codex exec 'prompt' -o result.txt --color never` | 파일 |
121
- | Gemini | `gemini -p 'prompt' -o text > result.txt 2>result.txt.err` | 리다이렉트 |
122
- | Claude | `claude -p 'prompt' --output-format text > result.txt 2>&1` | 리다이렉트 |
123
-
124
- **E4 크래시 복구:** `waitForCompletion`이 세션 사망 시 `{sessionDead: true}` 반환 (throw 대신).
125
- **elevation 불필요:** psmux IPC는 TCP 기반. 비-elevated 환경에서 정상 실행. (v5.2.0 검증 완료)
126
- **시각적 확인:** Windows Terminal 자동 팝업 + pane 타이틀 `codex (reviewer)` + triflux 테마.
121
+ if 모든 워커가 CLI (codex/gemini):
122
+ → Bash("tfx multi --teammate-mode headless --auto-attach --assign ...")
123
+ elif CLI + Claude 혼합:
124
+ CLI 워커: Bash("tfx multi --teammate-mode headless --assign ...")
125
+ → Claude 워커: Agent(subagent_type, run_in_background=true)
126
+ elif psmux 미설치:
127
+ → Phase 3-fallback (아래)
128
+ ```
129
+
130
+ **headless 엔진이 자동으로 수행하는 것:**
131
+ - psmux 세션 생성 + progressive split-window
132
+ - Windows Terminal 자동 팝업 (autoAttach)
133
+ - triflux 테마 적용 (Catppuccin Mocha status bar)
134
+ - CLI 명령 dispatch + 완료 토큰 폴링
135
+ - 결과 수집 + JSON stdout 출력
136
+ - 세션 정리
137
+
138
+ **출력 파싱:** headless 완료 stdout에 JSON 결과가 출력된다. 성공/실패 워커 수, 각 워커 출력을 파싱.
139
+ **크래시 복구:** 세션 사망 `{sessionDead: true}` 반환 (throw 대신).
127
140
  **실수로 닫아도:** psmux 세션은 독립적. `psmux attach -t 세션이름`으로 재연결.
128
141
 
129
142
  ### Phase 4: 결과 수집 + 정리
130
143
 
131
- headless 완료 `handle.results`에서 직접 수집. `handle.kill()`로 세션 정리.
144
+ headless stdout 출력에서 성공/실패 워커를 파싱.
132
145
  실패 워커(`exitCode !== 0`)는 Claude fallback 재시도.
133
146
 
134
147
  ### Phase 3-fallback: Native Teams (psmux 미설치 시)
@@ -138,8 +151,6 @@ psmux가 없는 환경에서만 사용. Agent slim wrapper로 CLI를 실행.
138
151
 
139
152
  > 래퍼 규칙 상세 → [`references/agent-wrapper-rules.md`](references/agent-wrapper-rules.md)
140
153
 
141
- **레거시 인터랙티브 모드:** `Bash("node {PKG_ROOT}/bin/triflux.mjs multi --no-attach --agents {agents} \\\"{task}\\\"")`
142
-
143
154
  ## 전제 조건
144
155
 
145
156
  - **CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1** — `tfx setup`이 자동 설정