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
|
|
1619
|
-
// Windows: 자기 자신의 파일 잠금으로 첫 시도 실패 가능 →
|
|
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
|
|
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 === "
|
|
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" ? "
|
|
17
|
+
return detectMultiplexer() === "psmux" ? "headless" : "in-process";
|
|
17
18
|
}
|
|
18
19
|
return "in-process";
|
|
19
20
|
}
|
package/package.json
CHANGED
|
@@ -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));
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
164
|
-
|
|
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
|
-
→
|
|
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 워커
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
78
|
+
> **MANDATORY: 2개+ CLI 워커 실행 시 headless 엔진 필수**
|
|
79
|
+
> 서브태스크가 2개 이상이면 반드시 아래 `Bash()` 명령으로 headless 엔진을 실행한다.
|
|
80
|
+
> `Agent()` 백그라운드나 `Bash(tfx-route.sh)` 개별 호출로 대체하지 않는다.
|
|
81
|
+
> headless 엔진이 psmux 세션 생성 → WT 자동 팝업 → CLI dispatch → 결과 수집을 전부 처리한다.
|
|
82
|
+
|
|
83
|
+
**Step 1 — headless 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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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`이 자동 설정
|