triflux 6.0.18 → 6.0.20
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/headless.mjs
CHANGED
|
@@ -238,11 +238,9 @@ export async function runHeadlessWithCleanup(assignments, opts = {}) {
|
|
|
238
238
|
try {
|
|
239
239
|
return await runHeadless(sessionName, assignments, runOpts);
|
|
240
240
|
} finally {
|
|
241
|
-
try {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// 이미 종료된 세션 — 무시
|
|
245
|
-
}
|
|
241
|
+
try { killPsmuxSession(sessionName); } catch { /* 무시 */ }
|
|
242
|
+
// WT split pane은 psmux 종료 시 셸이 끝나면서 자동으로 닫힘
|
|
243
|
+
// 수동 close-pane 불필요 (레이스 컨디션으로 WT 에러 발생)
|
|
246
244
|
}
|
|
247
245
|
}
|
|
248
246
|
|
|
@@ -369,28 +367,36 @@ export function autoAttachTerminal(sessionName, opts = {}, workerCount = 2) {
|
|
|
369
367
|
ensureWtProfile(workerCount);
|
|
370
368
|
|
|
371
369
|
const shells = ["pwsh.exe", "powershell.exe"];
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
370
|
+
const mode = opts.mode || "auto"; // "split" | "window" | "auto"
|
|
371
|
+
|
|
372
|
+
// v6.0.20: 기본 popup(별도 창). split은 --split 명시 시에만.
|
|
373
|
+
// 이유: WT sp는 포커스된 pane을 분할하므로, 사용자가 다른 탭에 있으면
|
|
374
|
+
// 엉뚱한 곳이 분할됨. 별도 창은 포커스 무관하게 안정적.
|
|
375
|
+
const preferSplit = mode === "split";
|
|
376
|
+
const preferWindow = mode !== "split";
|
|
377
|
+
|
|
378
|
+
if (preferSplit && !preferWindow && process.env.WT_SESSION) {
|
|
379
|
+
// inner split — 같은 WT 창에서 가로 분할
|
|
380
|
+
const splitSize = "0.50";
|
|
376
381
|
for (const shell of shells) {
|
|
377
382
|
try {
|
|
378
|
-
// 1) 하단 분할 생성
|
|
379
383
|
execSync(
|
|
380
384
|
`wt.exe -w 0 sp -H --size ${splitSize} --profile triflux --title triflux -- ${shell} -Command "psmux attach -t ${sessionName}"`,
|
|
381
385
|
{ stdio: "ignore", timeout: 5000 },
|
|
382
386
|
);
|
|
383
|
-
// 2) 포커스를 Claude Code(위 pane)로 되돌림
|
|
384
387
|
try { execSync(`wt.exe -w 0 mf up`, { stdio: "ignore", timeout: 2000 }); } catch { /* 무시 */ }
|
|
385
388
|
return true;
|
|
386
389
|
} catch { /* 다음 shell */ }
|
|
387
390
|
}
|
|
388
391
|
}
|
|
389
|
-
//
|
|
392
|
+
// 별도 WT 창 — PowerShell 콘솔 크기 지정 후 psmux attach
|
|
393
|
+
const cols = Math.max(100, 60 + workerCount * 15);
|
|
394
|
+
const rows = Math.max(25, 15 + workerCount * 4);
|
|
395
|
+
const resizePs = `$s=$Host.UI.RawUI;$b=$s.BufferSize;$b.Width=${cols};$s.BufferSize=$b;$w=$s.WindowSize;$w.Width=${cols};$w.Height=${rows};$s.WindowSize=$w`;
|
|
390
396
|
for (const shell of shells) {
|
|
391
397
|
try {
|
|
392
398
|
execSync(
|
|
393
|
-
`start "" /b wt.exe
|
|
399
|
+
`start "" /b wt.exe -w new --profile triflux --title "triflux workers (${workerCount})" -- ${shell} -NoProfile -Command "${resizePs};psmux attach -t ${sessionName}"`,
|
|
394
400
|
{ stdio: "ignore", shell: true, timeout: 5000 },
|
|
395
401
|
);
|
|
396
402
|
return true;
|
|
@@ -460,7 +466,8 @@ export async function runHeadlessInteractive(sessionName, assignments, opts = {}
|
|
|
460
466
|
runOpts.onProgress = (event) => {
|
|
461
467
|
if (autoAttach && event.type === "session_created" && !terminalAttached) {
|
|
462
468
|
terminalAttached = true;
|
|
463
|
-
|
|
469
|
+
// v6.0.20: 항상 별도 창 (포커스 문제 회피)
|
|
470
|
+
autoAttachTerminal(sessionName, { mode: "window" }, assignments.length);
|
|
464
471
|
}
|
|
465
472
|
if (userOnProgress) userOnProgress(event);
|
|
466
473
|
};
|
|
@@ -531,11 +538,13 @@ export async function runHeadlessInteractive(sessionName, assignments, opts = {}
|
|
|
531
538
|
return psmuxSessionExists(sessionName);
|
|
532
539
|
},
|
|
533
540
|
|
|
534
|
-
/** 세션 종료 */
|
|
541
|
+
/** 세션 종료 — WT pane은 psmux 종료 시 자동으로 닫힘 */
|
|
535
542
|
kill() {
|
|
536
543
|
if (this._killed) return;
|
|
537
544
|
this._killed = true;
|
|
538
545
|
try { killPsmuxSession(sessionName); } catch { /* 무시 */ }
|
|
546
|
+
// WT split pane은 psmux 종료 → 셸 종료 → 자동 닫힘
|
|
547
|
+
// 수동 close-pane 불필요 (레이스 컨디션으로 WT 0x80070002 에러 발생)
|
|
539
548
|
},
|
|
540
549
|
};
|
|
541
550
|
|
package/package.json
CHANGED
|
@@ -1,62 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* headless-guard.mjs — PreToolUse 훅 (auto-route
|
|
3
|
+
* headless-guard.mjs — PreToolUse 훅 (상시 활성 auto-route)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* psmux가 설치된 환경에서 Bash(tfx-route.sh) 개별 호출을
|
|
6
|
+
* 자동으로 headless 명령으로 변환한다.
|
|
7
|
+
*
|
|
8
|
+
* v2: 마커 파일 의존 제거. psmux 설치 여부만으로 판단.
|
|
9
|
+
* Opus가 SKILL.md를 무시해도 auto-route가 작동한다.
|
|
7
10
|
*
|
|
8
11
|
* 동작:
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
12
|
+
* - psmux 설치 + Bash(tfx-route.sh) → updatedInput: tfx multi --headless --assign
|
|
13
|
+
* - psmux 설치 + Bash(codex exec / gemini -p) → deny
|
|
14
|
+
* - psmux 설치 + Agent(codex/gemini CLI 래핑) → deny
|
|
15
|
+
* - psmux 미설치 → 전부 통과
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
* Exit 2 + stderr: deny (Agent CLI 래핑만)
|
|
16
|
-
* Exit 0 (no stdout): allow
|
|
17
|
+
* 성능: psmux 감지 결과를 5분간 캐시 ($TMPDIR/tfx-psmux-check.json)
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
|
-
import { existsSync, readFileSync,
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
20
22
|
import { tmpdir } from "node:os";
|
|
21
23
|
import { join } from "node:path";
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
+
const CACHE_FILE = join(tmpdir(), "tfx-psmux-check.json");
|
|
26
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
|
|
25
27
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
+
function isPsmuxInstalled() {
|
|
29
|
+
// 캐시 확인
|
|
28
30
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return false;
|
|
31
|
+
if (existsSync(CACHE_FILE)) {
|
|
32
|
+
const cache = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
33
|
+
if (Date.now() - cache.ts < CACHE_TTL_MS) return cache.ok;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
} catch { /* cache miss */ }
|
|
36
|
+
|
|
37
|
+
// psmux -V 실행
|
|
38
|
+
let ok = false;
|
|
39
|
+
try {
|
|
40
|
+
execFileSync("psmux", ["-V"], { timeout: 2000, stdio: "ignore" });
|
|
41
|
+
ok = true;
|
|
42
|
+
} catch { /* not installed */ }
|
|
43
|
+
|
|
44
|
+
// 캐시 저장
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), ok }));
|
|
47
|
+
} catch { /* ignore */ }
|
|
48
|
+
|
|
49
|
+
return ok;
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
53
|
* tfx-route.sh 명령에서 agent, prompt를 파싱한다.
|
|
42
|
-
* 형식: bash ~/.claude/scripts/tfx-route.sh {agent} '{prompt}' {mcp} [timeout] [context]
|
|
43
54
|
*/
|
|
44
55
|
function parseRouteCommand(cmd) {
|
|
45
|
-
// MCP 프로필 목록 (tfx-route.sh의 마지막 위치 인자)
|
|
46
56
|
const MCP_PROFILES = ["implement", "analyze", "review", "docs"];
|
|
47
57
|
|
|
48
|
-
// 전략: agent명 추출 후, 나머지에서 MCP 프로필을 역방향으로 찾아 프롬프트 경계를 결정
|
|
49
58
|
const agentMatch = cmd.match(/tfx-route\.sh\s+(\S+)\s+/);
|
|
50
59
|
if (!agentMatch) return null;
|
|
51
60
|
|
|
52
61
|
const agent = agentMatch[1];
|
|
53
62
|
const afterAgent = cmd.slice(agentMatch.index + agentMatch[0].length);
|
|
54
63
|
|
|
55
|
-
// MCP 프로필을 역방향으로 찾기
|
|
56
64
|
let mcp = "";
|
|
57
65
|
let promptRaw = afterAgent;
|
|
58
66
|
for (const profile of MCP_PROFILES) {
|
|
59
|
-
// 프롬프트 뒤에 오는 MCP 프로필 (공백 구분)
|
|
60
67
|
const profileIdx = afterAgent.lastIndexOf(` ${profile}`);
|
|
61
68
|
if (profileIdx >= 0) {
|
|
62
69
|
mcp = profile;
|
|
@@ -65,26 +72,24 @@ function parseRouteCommand(cmd) {
|
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
// 프롬프트에서 바깥쪽 따옴표 제거
|
|
69
75
|
const prompt = promptRaw
|
|
70
76
|
.replace(/^['"]/, "")
|
|
71
77
|
.replace(/['"]$/, "")
|
|
72
|
-
.replace(/'\\''/g, "'")
|
|
73
|
-
.replace(/'"'"'/g, "'")
|
|
78
|
+
.replace(/'\\''/g, "'")
|
|
79
|
+
.replace(/'"'"'/g, "'")
|
|
74
80
|
.trim();
|
|
75
81
|
|
|
76
82
|
return { agent, prompt, mcp };
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
function autoRoute(updatedCommand, reason) {
|
|
80
|
-
|
|
86
|
+
process.stdout.write(JSON.stringify({
|
|
81
87
|
hookSpecificOutput: {
|
|
82
88
|
hookEventName: "PreToolUse",
|
|
83
89
|
updatedInput: { command: updatedCommand },
|
|
84
90
|
additionalContext: reason,
|
|
85
91
|
},
|
|
86
|
-
};
|
|
87
|
-
process.stdout.write(JSON.stringify(output));
|
|
92
|
+
}));
|
|
88
93
|
process.exit(0);
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -94,7 +99,8 @@ function deny(reason) {
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
async function main() {
|
|
97
|
-
|
|
102
|
+
// psmux 미설치 → 전부 통과
|
|
103
|
+
if (!isPsmuxInstalled()) process.exit(0);
|
|
98
104
|
|
|
99
105
|
let raw = "";
|
|
100
106
|
for await (const chunk of process.stdin) raw += chunk;
|
|
@@ -109,58 +115,45 @@ async function main() {
|
|
|
109
115
|
const toolName = input.tool_name || "";
|
|
110
116
|
const toolInput = input.tool_input || {};
|
|
111
117
|
|
|
112
|
-
// ── Bash
|
|
118
|
+
// ── Bash ──
|
|
113
119
|
if (toolName === "Bash") {
|
|
114
120
|
const cmd = toolInput.command || "";
|
|
115
121
|
|
|
116
|
-
//
|
|
122
|
+
// headless 명령은 통과
|
|
117
123
|
if (cmd.includes("tfx multi") || cmd.includes("triflux.mjs multi")) {
|
|
118
124
|
process.exit(0);
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
//
|
|
122
|
-
if (cmd.includes("tfx-headless-guard")) {
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// codex/gemini 직접 CLI 호출 감지 → deny (auto-route 불가: 원본 agent/role 정보 없음)
|
|
127
|
+
// codex/gemini 직접 CLI 호출 → deny
|
|
127
128
|
if (/\bcodex\s+exec\b/.test(cmd) || /\bgemini\s+(-p|--prompt)\b/.test(cmd)) {
|
|
128
129
|
deny(
|
|
129
|
-
"[headless-guard]
|
|
130
|
-
'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")
|
|
130
|
+
"[headless-guard] codex/gemini 직접 호출 대신 headless를 사용하세요. " +
|
|
131
|
+
'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")',
|
|
131
132
|
);
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
// tfx-route.sh
|
|
135
|
-
if (
|
|
135
|
+
// tfx-route.sh 실행만 감지: 명령이 bash로 시작할 때만 (커밋 메시지/echo 등 무시)
|
|
136
|
+
if (/^\s*bash\s+.*tfx-route\.sh\s/.test(cmd)) {
|
|
136
137
|
const parsed = parseRouteCommand(cmd);
|
|
137
138
|
if (parsed) {
|
|
138
|
-
const role = parsed.agent;
|
|
139
|
-
// 프롬프트에서 싱글쿼트 이스케이프
|
|
140
139
|
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
140
|
autoRoute(
|
|
145
|
-
|
|
146
|
-
`[headless-guard] auto-route: tfx-route.sh
|
|
141
|
+
`tfx multi --teammate-mode headless --auto-attach --assign '${parsed.agent}:${safePrompt}:${parsed.agent}' --timeout 600`,
|
|
142
|
+
`[headless-guard] auto-route: tfx-route.sh ${parsed.agent} → headless. mcp=${parsed.mcp}`,
|
|
147
143
|
);
|
|
148
144
|
}
|
|
149
|
-
// 파싱 실패 시 deny fallback
|
|
150
145
|
deny(
|
|
151
|
-
"[headless-guard]
|
|
152
|
-
'Bash("tfx multi --teammate-mode headless --
|
|
146
|
+
"[headless-guard] tfx-route.sh를 headless로 변환 실패. " +
|
|
147
|
+
'Bash("tfx multi --teammate-mode headless --assign \'cli:prompt:role\' ...") 형식을 사용하세요.',
|
|
153
148
|
);
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
// ── Agent: CLI 워커 래핑
|
|
152
|
+
// ── Agent: CLI 워커 래핑 → deny ──
|
|
158
153
|
if (toolName === "Agent") {
|
|
159
|
-
const
|
|
160
|
-
const desc = (toolInput.description || "").toLowerCase();
|
|
161
|
-
const combined = `${prompt} ${desc}`;
|
|
154
|
+
const combined = `${(toolInput.prompt || "").toLowerCase()} ${(toolInput.description || "").toLowerCase()}`;
|
|
162
155
|
|
|
163
|
-
const
|
|
156
|
+
const cliPatterns = [
|
|
164
157
|
/codex\s+(exec|run|실행)/,
|
|
165
158
|
/gemini\s+(-p|run|실행)/,
|
|
166
159
|
/tfx-route/,
|
|
@@ -168,10 +161,9 @@ async function main() {
|
|
|
168
161
|
/bash.*gemini/,
|
|
169
162
|
];
|
|
170
163
|
|
|
171
|
-
if (
|
|
164
|
+
if (cliPatterns.some((p) => p.test(combined))) {
|
|
172
165
|
deny(
|
|
173
|
-
"[headless-guard]
|
|
174
|
-
"Codex/Gemini를 Agent()로 래핑하지 말고 headless --assign으로 전달하세요. " +
|
|
166
|
+
"[headless-guard] Codex/Gemini를 Agent()로 래핑하지 마세요. " +
|
|
175
167
|
'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")',
|
|
176
168
|
);
|
|
177
169
|
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -117,6 +117,11 @@ const SYNC_MAP = [
|
|
|
117
117
|
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
118
118
|
label: "lib/keyword-rules.mjs",
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
src: join(PLUGIN_ROOT, "scripts", "headless-guard.mjs"),
|
|
122
|
+
dst: join(CLAUDE_DIR, "scripts", "headless-guard.mjs"),
|
|
123
|
+
label: "headless-guard.mjs",
|
|
124
|
+
},
|
|
120
125
|
];
|
|
121
126
|
|
|
122
127
|
function getVersion(filePath) {
|
|
@@ -549,6 +554,55 @@ try {
|
|
|
549
554
|
writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
|
|
550
555
|
synced++;
|
|
551
556
|
}
|
|
557
|
+
|
|
558
|
+
// ── PreToolUse 훅: headless-guard (auto-route) ──
|
|
559
|
+
// Phase 3 headless 모드 활성 중 tfx-route.sh 개별 호출을
|
|
560
|
+
// headless 명령으로 자동 변환한다.
|
|
561
|
+
if (!Array.isArray(hookSettings.hooks.PreToolUse)) {
|
|
562
|
+
hookSettings.hooks.PreToolUse = [];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const guardScriptPath = join(CLAUDE_DIR, "scripts", "headless-guard.mjs").replace(/\\/g, "/");
|
|
566
|
+
const hasGuardHook = hookSettings.hooks.PreToolUse.some((entry) =>
|
|
567
|
+
Array.isArray(entry.hooks) &&
|
|
568
|
+
entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("headless-guard")),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (!hasGuardHook && existsSync(guardScriptPath.replace(/\//g, "\\"))) {
|
|
572
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
573
|
+
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
574
|
+
|
|
575
|
+
hookSettings.hooks.PreToolUse.push({
|
|
576
|
+
matcher: "Bash|Agent",
|
|
577
|
+
hooks: [
|
|
578
|
+
{
|
|
579
|
+
type: "command",
|
|
580
|
+
command: `${nodeRef} "${guardScriptPath}"`,
|
|
581
|
+
timeout: 3,
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
});
|
|
585
|
+
writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
|
|
586
|
+
synced++;
|
|
587
|
+
} else if (hasGuardHook) {
|
|
588
|
+
// 기존 훅 경로를 동기화된 경로로 업데이트
|
|
589
|
+
let updated = false;
|
|
590
|
+
for (const entry of hookSettings.hooks.PreToolUse) {
|
|
591
|
+
if (!Array.isArray(entry.hooks)) continue;
|
|
592
|
+
for (const h of entry.hooks) {
|
|
593
|
+
if (typeof h.command === "string" && h.command.includes("headless-guard") && !h.command.includes(guardScriptPath)) {
|
|
594
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
595
|
+
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
596
|
+
h.command = `${nodeRef} "${guardScriptPath}"`;
|
|
597
|
+
updated = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (updated) {
|
|
602
|
+
writeFileSync(settingsPath, JSON.stringify(hookSettings, null, 2) + "\n", "utf8");
|
|
603
|
+
synced++;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
552
606
|
} catch {
|
|
553
607
|
// settings.json 파싱 실패 시 무시 — 기존 설정 보존
|
|
554
608
|
}
|
|
@@ -75,29 +75,17 @@ preflight와 Agent 생성을 병렬로 실행하여 사용자 체감 지연을
|
|
|
75
75
|
|
|
76
76
|
### Phase 3: Lead-Direct Headless 실행 (v6.0.0, 기본)
|
|
77
77
|
|
|
78
|
-
> **MANDATORY:
|
|
79
|
-
>
|
|
80
|
-
> `
|
|
78
|
+
> **MANDATORY: CLI 워커는 headless 엔진으로 실행**
|
|
79
|
+
> CLI 워커(Codex/Gemini)는 반드시 아래 `Bash()` 명령으로 headless 엔진을 통해 실행한다.
|
|
80
|
+
> `Bash(tfx-route.sh)` 개별 호출이나 `Agent()` CLI 래핑은 PreToolUse 훅이 자동 차단/변환한다.
|
|
81
81
|
> headless 엔진이 psmux 세션 생성 → WT 자동 팝업 → CLI dispatch → 결과 수집을 전부 처리한다.
|
|
82
82
|
|
|
83
|
-
|
|
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가 호출하는 유일한 명령):**
|
|
83
|
+
**실행 명령 (Lead가 호출하는 유일한 명령):**
|
|
90
84
|
|
|
91
85
|
```
|
|
92
86
|
Bash("tfx multi --teammate-mode headless --auto-attach --assign 'codex:{프롬프트1}:{역할1}' --assign 'gemini:{프롬프트2}:{역할2}' --timeout 600")
|
|
93
87
|
```
|
|
94
88
|
|
|
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
89
|
**예시:**
|
|
102
90
|
|
|
103
91
|
```
|