triflux 7.3.1 → 7.4.0
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/.claude-plugin/plugin.json +1 -1
- package/hub/team/agent-map.json +2 -1
- package/hub/team/backend.mjs +4 -3
- package/hub/team/cli/commands/start/index.mjs +2 -2
- package/hub/team/cli/commands/start/parse-args.mjs +41 -5
- package/hub/team/cli/commands/start/start-headless.mjs +3 -3
- package/hub/team/cli/services/native-control.mjs +118 -115
- package/hub/team/codex-compat.mjs +78 -0
- package/hub/team/headless.mjs +4 -4
- package/hub/team/pane.mjs +154 -150
- package/hub/team/psmux.mjs +1041 -1023
- package/hud/constants.mjs +3 -0
- package/package.json +1 -1
- package/scripts/headless-guard.mjs +94 -7
- package/scripts/setup.mjs +65 -0
- package/scripts/tfx-gate-activate.mjs +89 -0
- package/scripts/tfx-route-post.mjs +17 -13
- package/scripts/tfx-route.sh +118 -46
- package/skills/tfx-auto/SKILL.md +1 -1
- package/skills/tfx-multi/SKILL.md +1 -1
package/hud/constants.mjs
CHANGED
|
@@ -20,6 +20,9 @@ export const OMC_PLUGIN_USAGE_CACHE_PATH = join(homedir(), ".claude", "plugins",
|
|
|
20
20
|
export const CLAUDE_USAGE_STALE_MS_SOLO = 5 * 60 * 1000; // OMC 없을 때: 5분 캐시
|
|
21
21
|
export const CLAUDE_USAGE_STALE_MS_WITH_OMC = 15 * 60 * 1000; // OMC 있을 때: 15분 (OMC가 30초마다 갱신)
|
|
22
22
|
export const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
|
|
23
|
+
export const GEMINI_429_BASE_DELAY_MS = 2000;
|
|
24
|
+
export const GEMINI_429_MAX_RETRIES = 3;
|
|
25
|
+
export const GEMINI_429_COOLDOWN_MS = 30000;
|
|
23
26
|
export const CLAUDE_USAGE_ERROR_BACKOFF_MS = 3 * 60 * 1000; // 기타 에러 시 3분 backoff
|
|
24
27
|
export const CLAUDE_API_TIMEOUT_MS = 10_000;
|
|
25
28
|
export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
package/package.json
CHANGED
|
@@ -8,16 +8,23 @@
|
|
|
8
8
|
* v2: 마커 파일 의존 제거. psmux 설치 여부만으로 판단.
|
|
9
9
|
* Opus가 SKILL.md를 무시해도 auto-route가 작동한다.
|
|
10
10
|
*
|
|
11
|
+
* v3: A(gate) + B(nudge) — OMC 패턴 도입
|
|
12
|
+
* A: tfx-multi 활성 시 headless dispatch 전까지 Agent 작업 위임 차단
|
|
13
|
+
* B: dispatch 후 네이티브 드리프트 감지 시 nudge
|
|
14
|
+
* 상태: $TMPDIR/tfx-multi-state.json (tfx-multi-activate.mjs가 생성)
|
|
15
|
+
*
|
|
11
16
|
* 동작:
|
|
12
17
|
* - psmux 설치 + Bash(tfx-route.sh) → updatedInput: tfx multi --headless --assign
|
|
13
18
|
* - psmux 설치 + Bash(codex exec / gemini -p) → deny
|
|
14
19
|
* - psmux 설치 + Agent(codex/gemini CLI 래핑) → deny
|
|
15
20
|
* - psmux 미설치 → 전부 통과
|
|
21
|
+
* - tfx-multi 활성 + Agent(work) before dispatch → deny (A: gate)
|
|
22
|
+
* - tfx-multi 활성 + Agent(work) after dispatch → nudge (B: nudge)
|
|
16
23
|
*
|
|
17
24
|
* 성능: psmux 감지 결과를 5분간 캐시 ($TMPDIR/tfx-psmux-check.json)
|
|
18
25
|
*/
|
|
19
26
|
|
|
20
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
21
28
|
import { execFileSync } from "node:child_process";
|
|
22
29
|
import { tmpdir } from "node:os";
|
|
23
30
|
import { join } from "node:path";
|
|
@@ -25,6 +32,42 @@ import { join } from "node:path";
|
|
|
25
32
|
const CACHE_FILE = join(tmpdir(), "tfx-psmux-check.json");
|
|
26
33
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
|
|
27
34
|
|
|
35
|
+
// ── tfx-multi 상태 관리 (A+B) ──
|
|
36
|
+
const MULTI_STATE_FILE = join(tmpdir(), "tfx-multi-state.json");
|
|
37
|
+
const MULTI_EXPIRE_MS = 30 * 60 * 1000; // 30분 자동 만료
|
|
38
|
+
const GATE_THRESHOLD = 2; // A: dispatch 전 허용할 Agent 호출 수
|
|
39
|
+
const NUDGE_THRESHOLD = 4; // B: dispatch 후 nudge 트리거 횟수
|
|
40
|
+
|
|
41
|
+
function readMultiState() {
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(MULTI_STATE_FILE)) return null;
|
|
44
|
+
const state = JSON.parse(readFileSync(MULTI_STATE_FILE, "utf8"));
|
|
45
|
+
if (!state.active) return null;
|
|
46
|
+
// 자동 만료
|
|
47
|
+
if (Date.now() - state.activatedAt > MULTI_EXPIRE_MS) {
|
|
48
|
+
try { unlinkSync(MULTI_STATE_FILE); } catch { /* ignore */ }
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return state;
|
|
52
|
+
} catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeMultiState(state) {
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(MULTI_STATE_FILE, JSON.stringify(state));
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nudge(message) {
|
|
62
|
+
process.stdout.write(JSON.stringify({
|
|
63
|
+
hookSpecificOutput: {
|
|
64
|
+
hookEventName: "PreToolUse",
|
|
65
|
+
additionalContext: message,
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
function isPsmuxInstalled() {
|
|
29
72
|
// 캐시 확인
|
|
30
73
|
try {
|
|
@@ -135,8 +178,14 @@ async function main() {
|
|
|
135
178
|
if (toolName === "Bash") {
|
|
136
179
|
const cmd = toolInput.command || "";
|
|
137
180
|
|
|
138
|
-
// headless 명령은 통과
|
|
181
|
+
// headless 명령은 통과 + dispatch 감지 (A: gate 해제)
|
|
139
182
|
if (cmd.includes("tfx multi") || cmd.includes("triflux.mjs multi")) {
|
|
183
|
+
const multiState = readMultiState();
|
|
184
|
+
if (multiState && cmd.includes("--assign")) {
|
|
185
|
+
multiState.dispatched = true;
|
|
186
|
+
multiState.nativeWorkCallsSinceDispatch = 0;
|
|
187
|
+
writeMultiState(multiState);
|
|
188
|
+
}
|
|
140
189
|
process.exit(0);
|
|
141
190
|
}
|
|
142
191
|
|
|
@@ -197,15 +246,53 @@ async function main() {
|
|
|
197
246
|
}
|
|
198
247
|
}
|
|
199
248
|
|
|
200
|
-
// ── Agent:
|
|
249
|
+
// ── Agent: A(gate) + B(nudge) + CLI 래핑 deny ──
|
|
201
250
|
if (toolName === "Agent") {
|
|
202
251
|
const subType = (toolInput.subagent_type || "").toLowerCase();
|
|
203
|
-
// Claude native subagent types → 무조건 통과
|
|
204
252
|
const NATIVE_TYPES = new Set(["explore", "plan", "general-purpose", ""]);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
253
|
+
const isNative = NATIVE_TYPES.has(subType) || subType.startsWith("oh-my-claudecode:");
|
|
254
|
+
|
|
255
|
+
// ── A+B: tfx-multi 상태 기반 처리 ──
|
|
256
|
+
const multiState = readMultiState();
|
|
257
|
+
if (multiState && multiState.active && isNative) {
|
|
258
|
+
if (!multiState.dispatched) {
|
|
259
|
+
// ── A: gate — dispatch 전, Agent 작업 위임 제한 ──
|
|
260
|
+
multiState.nativeWorkCalls = (multiState.nativeWorkCalls || 0) + 1;
|
|
261
|
+
writeMultiState(multiState);
|
|
262
|
+
|
|
263
|
+
if (multiState.nativeWorkCalls > GATE_THRESHOLD) {
|
|
264
|
+
deny(
|
|
265
|
+
`[headless-guard] tfx-multi gate: Agent(${subType || "default"}) 호출 ${multiState.nativeWorkCalls}회 — headless에 먼저 dispatch하세요.\n` +
|
|
266
|
+
'Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign \'codex:프롬프트:역할\' --timeout 600")',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
// 허용 범위 내 → 경고 + 통과
|
|
270
|
+
nudge(
|
|
271
|
+
`[headless-guard] tfx-multi 활성 (${multiState.nativeWorkCalls}/${GATE_THRESHOLD}). ` +
|
|
272
|
+
"headless dispatch 후 작업을 시작하세요.",
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
// ── B: nudge — dispatch 후, 네이티브 드리프트 감지 ──
|
|
276
|
+
multiState.nativeWorkCallsSinceDispatch = (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
|
|
277
|
+
writeMultiState(multiState);
|
|
278
|
+
|
|
279
|
+
if (multiState.nativeWorkCallsSinceDispatch >= NUDGE_THRESHOLD) {
|
|
280
|
+
multiState.nativeWorkCallsSinceDispatch = 0;
|
|
281
|
+
writeMultiState(multiState);
|
|
282
|
+
nudge(
|
|
283
|
+
"[headless-guard] nudge: headless 워커가 실행 중입니다. " +
|
|
284
|
+
"결과를 기다리거나 추가 --assign으로 위임하세요.",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// native → 통과 (gate deny 안 걸린 경우)
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// native → 통과 (tfx-multi 비활성)
|
|
293
|
+
if (isNative) process.exit(0);
|
|
208
294
|
|
|
295
|
+
// ── CLI 래핑 체크 (기존) ──
|
|
209
296
|
const combined = `${(toolInput.prompt || "").toLowerCase()} ${(toolInput.description || "").toLowerCase()}`;
|
|
210
297
|
const cliPatterns = [
|
|
211
298
|
/codex\s+(exec|run|실행)/,
|
package/scripts/setup.mjs
CHANGED
|
@@ -30,6 +30,20 @@ function detectDevMode(root = PLUGIN_ROOT) {
|
|
|
30
30
|
const BREADCRUMB_PATH = join(CLAUDE_DIR, "scripts", ".tfx-pkg-root");
|
|
31
31
|
|
|
32
32
|
const REQUIRED_CODEX_PROFILES = [
|
|
33
|
+
{
|
|
34
|
+
name: "fast",
|
|
35
|
+
lines: [
|
|
36
|
+
'model = "gpt-5.3-codex"',
|
|
37
|
+
'model_reasoning_effort = "low"',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "normal",
|
|
42
|
+
lines: [
|
|
43
|
+
'model = "gpt-5.3-codex"',
|
|
44
|
+
'model_reasoning_effort = "medium"',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
33
47
|
{
|
|
34
48
|
name: "high",
|
|
35
49
|
lines: [
|
|
@@ -37,6 +51,14 @@ const REQUIRED_CODEX_PROFILES = [
|
|
|
37
51
|
'model_reasoning_effort = "high"',
|
|
38
52
|
],
|
|
39
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "thorough",
|
|
56
|
+
lines: [
|
|
57
|
+
'model = "gpt-5.3-codex"',
|
|
58
|
+
'model_reasoning_effort = "high"',
|
|
59
|
+
'model_temperature = 0.2',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
40
62
|
{
|
|
41
63
|
name: "xhigh",
|
|
42
64
|
lines: [
|
|
@@ -51,6 +73,13 @@ const REQUIRED_CODEX_PROFILES = [
|
|
|
51
73
|
'model_reasoning_effort = "low"',
|
|
52
74
|
],
|
|
53
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: "spark_balanced",
|
|
78
|
+
lines: [
|
|
79
|
+
'model = "gpt-5.1-codex-mini"',
|
|
80
|
+
'model_reasoning_effort = "medium"',
|
|
81
|
+
],
|
|
82
|
+
},
|
|
54
83
|
];
|
|
55
84
|
|
|
56
85
|
// ── 파일 동기화 ──
|
|
@@ -186,6 +215,11 @@ const SYNC_MAP = [
|
|
|
186
215
|
dst: join(CLAUDE_DIR, "scripts", "headless-guard-fast.sh"),
|
|
187
216
|
label: "headless-guard-fast.sh",
|
|
188
217
|
},
|
|
218
|
+
{
|
|
219
|
+
src: join(PLUGIN_ROOT, "scripts", "tfx-gate-activate.mjs"),
|
|
220
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-gate-activate.mjs"),
|
|
221
|
+
label: "tfx-gate-activate.mjs",
|
|
222
|
+
},
|
|
189
223
|
];
|
|
190
224
|
|
|
191
225
|
function getVersion(filePath) {
|
|
@@ -533,6 +567,37 @@ function applyHooks(s) {
|
|
|
533
567
|
}
|
|
534
568
|
}
|
|
535
569
|
|
|
570
|
+
// ── PreToolUse 훅: tfx-gate-activate (Skill 감지 → A+B gate) ──
|
|
571
|
+
const gateScriptPath = join(CLAUDE_DIR, "scripts", "tfx-gate-activate.mjs").replace(/\\/g, "/");
|
|
572
|
+
const hasGateHook = s.hooks.PreToolUse.some((entry) =>
|
|
573
|
+
Array.isArray(entry.hooks) &&
|
|
574
|
+
entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("tfx-gate-activate")),
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
if (!hasGateHook && existsSync(gateScriptPath.replace(/\//g, "\\"))) {
|
|
578
|
+
s.hooks.PreToolUse.push({
|
|
579
|
+
matcher: "Skill",
|
|
580
|
+
hooks: [
|
|
581
|
+
{
|
|
582
|
+
type: "command",
|
|
583
|
+
command: `node "${gateScriptPath}"`,
|
|
584
|
+
timeout: 2,
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
changed = true;
|
|
589
|
+
} else if (hasGateHook) {
|
|
590
|
+
for (const entry of s.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("tfx-gate-activate") && !h.command.includes(gateScriptPath)) {
|
|
594
|
+
h.command = `node "${gateScriptPath}"`;
|
|
595
|
+
changed = true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
536
601
|
return changed;
|
|
537
602
|
}
|
|
538
603
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tfx-multi-activate.mjs — PreToolUse(Skill) 훅
|
|
4
|
+
*
|
|
5
|
+
* /tfx-multi 스킬 호출을 감지하여 상태 파일을 설정한다.
|
|
6
|
+
* headless-guard.mjs가 이 상태를 읽어 A(gate) + B(nudge)를 수행.
|
|
7
|
+
*
|
|
8
|
+
* 상태 파일: $TMPDIR/tfx-multi-state.json
|
|
9
|
+
* 자동 만료: 30분
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const STATE_FILE = join(tmpdir(), "tfx-multi-state.json");
|
|
17
|
+
const EXPIRE_MS = 30 * 60 * 1000; // 30분
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
let raw = "";
|
|
21
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
22
|
+
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let input;
|
|
28
|
+
try {
|
|
29
|
+
input = JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const toolName = input.tool_name || "";
|
|
35
|
+
const toolInput = input.tool_input || {};
|
|
36
|
+
|
|
37
|
+
if (toolName !== "Skill") {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const skill = (toolInput.skill || "").toLowerCase();
|
|
42
|
+
|
|
43
|
+
// 모든 tfx CLI 라우팅 스킬에 gate 적용
|
|
44
|
+
const TFX_ROUTING_SKILLS = new Set([
|
|
45
|
+
"tfx-multi", "tfx-team", "tfx-auto", "tfx-auto-codex",
|
|
46
|
+
"tfx-codex", "tfx-gemini", "tfx-autoresearch",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
if (TFX_ROUTING_SKILLS.has(skill)) {
|
|
50
|
+
// 활성화: 상태 파일 생성/갱신
|
|
51
|
+
const state = {
|
|
52
|
+
active: true,
|
|
53
|
+
activatedAt: Date.now(),
|
|
54
|
+
dispatched: false,
|
|
55
|
+
nativeWorkCalls: 0,
|
|
56
|
+
nativeWorkCallsSinceDispatch: 0,
|
|
57
|
+
};
|
|
58
|
+
writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
59
|
+
|
|
60
|
+
// additionalContext로 Lead에게 알림
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
hookSpecificOutput: {
|
|
64
|
+
hookEventName: "PreToolUse",
|
|
65
|
+
additionalContext:
|
|
66
|
+
"[tfx-multi] gate 활성화됨. CLI 작업은 headless로 dispatch 필수:\n" +
|
|
67
|
+
'Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign \'codex:프롬프트:역할\' --timeout 600")',
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// /tfx-multi 외 스킬 호출 시: 기존 상태 만료 체크만
|
|
75
|
+
if (existsSync(STATE_FILE)) {
|
|
76
|
+
try {
|
|
77
|
+
const state = JSON.parse(readFileSync(STATE_FILE, "utf8"));
|
|
78
|
+
if (Date.now() - state.activatedAt > EXPIRE_MS) {
|
|
79
|
+
// 만료 → 삭제하지 않고 headless-guard가 처리
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main().catch(() => process.exit(0));
|
|
@@ -264,19 +264,23 @@ function trackCliIssue(cliType, agent, stderrText, exitCode) {
|
|
|
264
264
|
|
|
265
265
|
const snippet = stderrText.substring(0, 200).replace(/\n/g, " ");
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
267
|
+
const retryCount = (matched.pattern === "rate_limit" && cliType === "gemini")
|
|
268
|
+
? parseInt(process.env.TFX_GEMINI_429_RETRIES || "0")
|
|
269
|
+
: undefined;
|
|
270
|
+
|
|
271
|
+
const issueEntry = {
|
|
272
|
+
ts: Date.now(),
|
|
273
|
+
cli: cliType,
|
|
274
|
+
agent,
|
|
275
|
+
pattern: matched.pattern,
|
|
276
|
+
msg: matched.msg,
|
|
277
|
+
severity: matched.severity,
|
|
278
|
+
snippet,
|
|
279
|
+
resolved: false,
|
|
280
|
+
};
|
|
281
|
+
if (retryCount !== undefined) issueEntry.retry_count = retryCount;
|
|
282
|
+
|
|
283
|
+
appendFileSync(issuesFile, JSON.stringify(issueEntry) + "\n");
|
|
280
284
|
|
|
281
285
|
// 자동 회전
|
|
282
286
|
const content = readFileSync(issuesFile, "utf-8").trim();
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -566,12 +566,31 @@ capture_workspace_signature() {
|
|
|
566
566
|
git status --short --untracked-files=all --ignore-submodules=all 2>/dev/null || return 1
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
# ── Codex CLI 버전 감지 (캐시) ──
|
|
570
|
+
_CODEX_VERSION=""
|
|
571
|
+
get_codex_version() {
|
|
572
|
+
if [[ -n "$_CODEX_VERSION" ]]; then echo "$_CODEX_VERSION"; return; fi
|
|
573
|
+
local raw
|
|
574
|
+
raw=$("$CODEX_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
|
575
|
+
_CODEX_VERSION="${raw:-0.0.0}"
|
|
576
|
+
echo "$_CODEX_VERSION"
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
# codex_gte <min_version>: 현재 버전이 min 이상이면 true(0), 아니면 false(1)
|
|
580
|
+
codex_gte() {
|
|
581
|
+
local min="$1"
|
|
582
|
+
local cur
|
|
583
|
+
cur=$(get_codex_version)
|
|
584
|
+
printf '%s\n%s' "$min" "$cur" | sort -V | head -1 | grep -q "^${min}$"
|
|
585
|
+
}
|
|
586
|
+
|
|
569
587
|
# ── 라우팅 테이블 ──
|
|
570
588
|
# CLI_TYPE/CLI_CMD: agent-map.json 단일 소스. 상세 설정: 아래 case 문.
|
|
571
589
|
# 반환: CLI_TYPE, CLI_CMD, CLI_ARGS, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
|
|
572
590
|
route_agent() {
|
|
573
591
|
local agent="$1"
|
|
574
592
|
local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
|
|
593
|
+
echo "[tfx-route] Codex 버전: $(get_codex_version)" >&2
|
|
575
594
|
local map_file
|
|
576
595
|
map_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../hub/team/agent-map.json"
|
|
577
596
|
# ── breadcrumb 폴백 (synced 환경: ~/.claude/scripts/) ──
|
|
@@ -669,13 +688,20 @@ route_agent() {
|
|
|
669
688
|
CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
|
|
670
689
|
CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
671
690
|
|
|
672
|
-
# ───
|
|
673
|
-
explore
|
|
674
|
-
CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
691
|
+
# ─── 탐색 (Claude-native: Glob/Grep/Read 직접 접근) ───
|
|
692
|
+
explore)
|
|
693
|
+
CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
694
|
+
|
|
695
|
+
# ─── 검증/테스트 (Codex: 무료 + 파일 쓰기 가능) ───
|
|
696
|
+
verifier)
|
|
697
|
+
CLI_ARGS="exec --profile thorough ${codex_base} review"
|
|
698
|
+
CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
|
|
699
|
+
test-engineer)
|
|
700
|
+
CLI_ARGS="exec ${codex_base}"
|
|
701
|
+
CLI_EFFORT="high"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
702
|
+
qa-tester)
|
|
703
|
+
CLI_ARGS="exec --profile thorough ${codex_base} review"
|
|
704
|
+
CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
|
|
679
705
|
|
|
680
706
|
# ─── 경량 ───
|
|
681
707
|
spark)
|
|
@@ -1108,6 +1134,90 @@ run_stream_worker() {
|
|
|
1108
1134
|
return "$exit_code_local"
|
|
1109
1135
|
}
|
|
1110
1136
|
|
|
1137
|
+
# Gemini 429 지수 백오프 재시도 래퍼
|
|
1138
|
+
# 사용: gemini_with_retry <use_tee_flag> <gemini_args_array_name> <prompt>
|
|
1139
|
+
# 429/rate limit 감지 시 최대 3회 재시도 (2→4→8초 백오프)
|
|
1140
|
+
_gemini_run_once() {
|
|
1141
|
+
local use_tee_flag="$1"
|
|
1142
|
+
local prompt="$2"
|
|
1143
|
+
shift 2
|
|
1144
|
+
local -a g_args=("$@")
|
|
1145
|
+
|
|
1146
|
+
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1147
|
+
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1148
|
+
else
|
|
1149
|
+
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1150
|
+
fi
|
|
1151
|
+
echo "$!"
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
gemini_with_retry() {
|
|
1155
|
+
local use_tee_flag="$1"
|
|
1156
|
+
local prompt="$2"
|
|
1157
|
+
shift 2
|
|
1158
|
+
local -a g_args=("$@")
|
|
1159
|
+
|
|
1160
|
+
local max_retries=3
|
|
1161
|
+
local attempt=0
|
|
1162
|
+
local delay=2
|
|
1163
|
+
local exit_code_local=0
|
|
1164
|
+
|
|
1165
|
+
while (( attempt < max_retries )); do
|
|
1166
|
+
exit_code_local=0
|
|
1167
|
+
local pid
|
|
1168
|
+
pid=$(_gemini_run_once "$use_tee_flag" "$prompt" "${g_args[@]}")
|
|
1169
|
+
|
|
1170
|
+
local health_ok=true
|
|
1171
|
+
local intervals=(1 2 3 5 8)
|
|
1172
|
+
for wait_sec in "${intervals[@]}"; do
|
|
1173
|
+
sleep "$wait_sec"
|
|
1174
|
+
if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
|
|
1175
|
+
break
|
|
1176
|
+
fi
|
|
1177
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
1178
|
+
health_ok=false
|
|
1179
|
+
echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
|
|
1180
|
+
break
|
|
1181
|
+
fi
|
|
1182
|
+
done
|
|
1183
|
+
|
|
1184
|
+
local hb_pid
|
|
1185
|
+
if [[ "$health_ok" == "false" ]]; then
|
|
1186
|
+
wait "$pid" 2>/dev/null
|
|
1187
|
+
else
|
|
1188
|
+
heartbeat_monitor "$pid" &
|
|
1189
|
+
hb_pid=$!
|
|
1190
|
+
wait "$pid" || exit_code_local=$?
|
|
1191
|
+
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1192
|
+
fi
|
|
1193
|
+
|
|
1194
|
+
# 성공 시 즉시 반환
|
|
1195
|
+
if [[ $exit_code_local -eq 0 ]]; then
|
|
1196
|
+
return 0
|
|
1197
|
+
fi
|
|
1198
|
+
|
|
1199
|
+
# 429 / rate limit 감지
|
|
1200
|
+
if grep -qiE '429|rate.limit|too many requests' "$STDERR_LOG" 2>/dev/null; then
|
|
1201
|
+
attempt=$(( attempt + 1 ))
|
|
1202
|
+
if (( attempt < max_retries )); then
|
|
1203
|
+
echo "[tfx-route] Gemini 429 감지. ${delay}초 후 재시도 ($attempt/$max_retries)..." >&2
|
|
1204
|
+
sleep "$delay"
|
|
1205
|
+
delay=$(( delay * 2 ))
|
|
1206
|
+
: > "$STDOUT_LOG"
|
|
1207
|
+
: > "$STDERR_LOG"
|
|
1208
|
+
continue
|
|
1209
|
+
else
|
|
1210
|
+
echo "[tfx-route] Gemini 429: ${max_retries}회 재시도 실패" >&2
|
|
1211
|
+
fi
|
|
1212
|
+
fi
|
|
1213
|
+
|
|
1214
|
+
# 비-429 에러 또는 최대 재시도 초과 시 즉시 반환
|
|
1215
|
+
return "$exit_code_local"
|
|
1216
|
+
done
|
|
1217
|
+
|
|
1218
|
+
return "$exit_code_local"
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1111
1221
|
run_legacy_gemini() {
|
|
1112
1222
|
local prompt="$1"
|
|
1113
1223
|
local use_tee_flag="$2"
|
|
@@ -1133,45 +1243,7 @@ run_legacy_gemini() {
|
|
|
1133
1243
|
fi
|
|
1134
1244
|
fi
|
|
1135
1245
|
|
|
1136
|
-
|
|
1137
|
-
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1138
|
-
else
|
|
1139
|
-
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1140
|
-
fi
|
|
1141
|
-
local pid=$!
|
|
1142
|
-
|
|
1143
|
-
local health_ok=true
|
|
1144
|
-
local intervals=(1 2 3 5 8)
|
|
1145
|
-
for wait_sec in "${intervals[@]}"; do
|
|
1146
|
-
sleep "$wait_sec"
|
|
1147
|
-
if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
|
|
1148
|
-
break
|
|
1149
|
-
fi
|
|
1150
|
-
if ! kill -0 "$pid" 2>/dev/null; then
|
|
1151
|
-
health_ok=false
|
|
1152
|
-
echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
|
|
1153
|
-
break
|
|
1154
|
-
fi
|
|
1155
|
-
done
|
|
1156
|
-
|
|
1157
|
-
local exit_code_local=0
|
|
1158
|
-
local hb_pid
|
|
1159
|
-
if [[ "$health_ok" == "false" ]]; then
|
|
1160
|
-
wait "$pid" 2>/dev/null
|
|
1161
|
-
echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
|
|
1162
|
-
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1163
|
-
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1164
|
-
else
|
|
1165
|
-
timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1166
|
-
fi
|
|
1167
|
-
pid=$!
|
|
1168
|
-
fi
|
|
1169
|
-
|
|
1170
|
-
heartbeat_monitor "$pid" &
|
|
1171
|
-
hb_pid=$!
|
|
1172
|
-
wait "$pid" || exit_code_local=$?
|
|
1173
|
-
kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
|
|
1174
|
-
return "$exit_code_local"
|
|
1246
|
+
gemini_with_retry "$use_tee_flag" "$prompt" "${gemini_args[@]}"
|
|
1175
1247
|
}
|
|
1176
1248
|
|
|
1177
1249
|
resolve_codex_mcp_script() {
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -31,7 +31,7 @@ argument-hint: "<command|task> [args...]"
|
|
|
31
31
|
> 1. **실행**: CLI 에이전트는 반드시 `Bash("bash ~/.claude/scripts/tfx-route.sh ...")`. Claude 네이티브(explore/verifier/test-engineer/qa-tester)만 `Agent()`.
|
|
32
32
|
> 2. **비용**: Codex 우선 → Gemini → Claude 최후 수단. `claude` 선택 전 "Codex로 가능한가?" 재확인.
|
|
33
33
|
> 3. **DAG**: SEQUENTIAL/DAG이면 레벨 기반 순차 실행. `.omc/context/{sid}/` 생성, context_output 저장, 실패 시 후속 SKIP.
|
|
34
|
-
> 4. **트리아지**: Codex
|
|
34
|
+
> 4. **트리아지**: Codex `exec --full-auto` 분류 + Opus 인라인 분해. Agent 스폰 금지.
|
|
35
35
|
> 5. **thorough**: `-t`/`--thorough` 시 파이프라인 init 필수. 커맨드 숏컷은 항상 quick.
|
|
36
36
|
|
|
37
37
|
## 모드
|
|
@@ -62,7 +62,7 @@ preflight와 Agent 생성을 병렬로 실행하여 사용자 체감 지연을
|
|
|
62
62
|
### Phase 2: 트리아지 (tfx-auto와 동일)
|
|
63
63
|
|
|
64
64
|
**자동 모드:**
|
|
65
|
-
1. Codex
|
|
65
|
+
1. Codex `exec --skip-git-repo-check` 분류 → JSON `{parts: [{description, agent}]}`
|
|
66
66
|
2. Opus 인라인 분해 → 서브태스크 배열 `[{cli, subtask, role}]`
|
|
67
67
|
3. Codex 분류 실패 시 → Opus가 직접 분류+분해
|
|
68
68
|
|