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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "7.3.1",
3
+ "version": "7.4.0",
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": {
@@ -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: CLI 워커 래핑 deny (Claude native는 통과) ──
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
- if (NATIVE_TYPES.has(subType)) process.exit(0);
206
- // oh-my-claudecode 계열도 통과
207
- if (subType.startsWith("oh-my-claudecode:")) process.exit(0);
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
- appendFileSync(
268
- issuesFile,
269
- JSON.stringify({
270
- ts: Date.now(),
271
- cli: cliType,
272
- agent,
273
- pattern: matched.pattern,
274
- msg: matched.msg,
275
- severity: matched.severity,
276
- snippet,
277
- resolved: false,
278
- }) + "\n",
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();
@@ -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
- # ─── 탐색/검증/테스트 (Claude-native 우선, TFX_NO_CLAUDE_NATIVE=1일 때만 Codex 리매핑) ───
673
- explore|verifier|test-engineer|qa-tester)
674
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
675
- case "$agent" in
676
- test-engineer|qa-tester) DEFAULT_TIMEOUT=1200; RUN_MODE="bg" ;;
677
- esac
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
- if [[ "$use_tee_flag" == "true" ]]; then
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() {
@@ -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 `--full-auto` 분류 + Opus 인라인 분해. Agent 스폰 금지.
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 `--full-auto --skip-git-repo-check` 분류 → JSON `{parts: [{description, agent}]}`
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