triflux 3.2.0-dev.2 → 3.2.0-dev.5

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.
@@ -1,448 +1,608 @@
1
- #!/usr/bin/env bash
2
- # tfx-route.sh v2.0 — CLI 라우팅 래퍼 (triflux)
3
- #
4
- # v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
5
- # v2.0: tfx-route.sh 리네임
6
- # - 후처리 전부 tfx-route-post.mjs로 이관 (node 단일 ~100ms)
7
- # - per-process 에이전트 등록 (race condition 구조적 제거)
8
- # - get_mcp_hint 통합 (캐시/비캐시 단일 코드경로)
9
- # - Gemini health check 지수 백오프 (30×1s → 5×exp)
10
- # - 컨텍스트 파일 5번째 인자 지원
11
- #
12
- VERSION="2.0"
13
- #
14
- # 사용법:
15
- # tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
16
- #
17
- # 예시:
18
- # tfx-route.sh executor "코드 구현" implement
19
- # tfx-route.sh architect "아키텍처 분석" analyze '' context.md
20
-
21
- set -euo pipefail
22
-
23
- # ── 인자 파싱 ──
24
- AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
25
- PROMPT="${2:?프롬프트 필수}"
26
- MCP_PROFILE="${3:-auto}"
27
- USER_TIMEOUT="${4:-}"
28
- CONTEXT_FILE="${5:-}"
29
-
30
- # ── CLI 경로 해석 (Windows npm global 대응) ──
31
- CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
32
- GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
33
-
34
- # ── 상수 ──
35
- MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
36
- TIMESTAMP=$(date +%s)
37
- STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stderr.log"
38
- STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stdout.log"
39
- TFX_TMP="${TMPDIR:-/tmp}"
40
-
41
- # fallback 원래 에이전트 정보 보존
42
- ORIGINAL_AGENT=""
43
- ORIGINAL_CLI_ARGS=""
44
-
45
- # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
46
- register_agent() {
47
- local agent_file="${TFX_TMP}/tfx-agent-$$.json"
48
- echo "{\"pid\":$$,\"cli\":\"$CLI_TYPE\",\"agent\":\"$AGENT_TYPE\",\"started\":$(date +%s)}" \
49
- > "$agent_file" 2>/dev/null || true
50
- }
51
-
52
- deregister_agent() {
53
- rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
54
- }
55
-
56
- # ── 라우팅 테이블 ──
57
- # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
58
- route_agent() {
59
- local agent="$1"
60
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
61
-
62
- case "$agent" in
63
- # ─── 구현 레인 ───
64
- executor)
65
- CLI_TYPE="codex"; CLI_CMD="codex"
66
- CLI_ARGS="exec ${codex_base}"
67
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
68
- build-fixer)
69
- CLI_TYPE="codex"; CLI_CMD="codex"
70
- CLI_ARGS="exec --profile fast ${codex_base}"
71
- CLI_EFFORT="fast"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
72
- debugger)
73
- CLI_TYPE="codex"; CLI_CMD="codex"
74
- CLI_ARGS="exec ${codex_base}"
75
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
76
- deep-executor)
77
- CLI_TYPE="codex"; CLI_CMD="codex"
78
- CLI_ARGS="exec --profile xhigh ${codex_base}"
79
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
80
-
81
- # ─── 설계/분석 레인 ───
82
- architect)
83
- CLI_TYPE="codex"; CLI_CMD="codex"
84
- CLI_ARGS="exec --profile xhigh ${codex_base}"
85
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
86
- planner)
87
- CLI_TYPE="codex"; CLI_CMD="codex"
88
- CLI_ARGS="exec --profile xhigh ${codex_base}"
89
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
90
- critic)
91
- CLI_TYPE="codex"; CLI_CMD="codex"
92
- CLI_ARGS="exec --profile xhigh ${codex_base}"
93
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
94
- analyst)
95
- CLI_TYPE="codex"; CLI_CMD="codex"
96
- CLI_ARGS="exec --profile xhigh ${codex_base}"
97
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
98
-
99
- # ─── 리뷰 레인 ───
100
- code-reviewer)
101
- CLI_TYPE="codex"; CLI_CMD="codex"
102
- CLI_ARGS="exec --profile thorough ${codex_base} review"
103
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
104
- security-reviewer)
105
- CLI_TYPE="codex"; CLI_CMD="codex"
106
- CLI_ARGS="exec --profile thorough ${codex_base} review"
107
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
108
- quality-reviewer)
109
- CLI_TYPE="codex"; CLI_CMD="codex"
110
- CLI_ARGS="exec --profile thorough ${codex_base} review"
111
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
112
-
113
- # ─── 리서치 레인 ───
114
- scientist)
115
- CLI_TYPE="codex"; CLI_CMD="codex"
116
- CLI_ARGS="exec ${codex_base}"
117
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
118
- scientist-deep)
119
- CLI_TYPE="codex"; CLI_CMD="codex"
120
- CLI_ARGS="exec --profile thorough ${codex_base}"
121
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
122
- document-specialist)
123
- CLI_TYPE="codex"; CLI_CMD="codex"
124
- CLI_ARGS="exec ${codex_base}"
125
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
126
-
127
- # ─── UI/문서 레인 ───
128
- designer)
129
- CLI_TYPE="gemini"; CLI_CMD="gemini"
130
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
131
- CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
132
- writer)
133
- CLI_TYPE="gemini"; CLI_CMD="gemini"
134
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
135
- CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
136
-
137
- # ─── Claude 네이티브 ───
138
- explore)
139
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
140
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
141
- verifier)
142
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
143
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
144
- test-engineer)
145
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
146
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
147
- qa-tester)
148
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
149
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
150
-
151
- # ─── 경량 ───
152
- spark)
153
- CLI_TYPE="codex"; CLI_CMD="codex"
154
- CLI_ARGS="exec --profile spark_fast ${codex_base}"
155
- CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
156
- *)
157
- echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
158
- echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
159
- echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
160
- echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
161
- exit 1 ;;
162
- esac
163
- }
164
-
165
- # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
166
- TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
167
-
168
- apply_cli_mode() {
169
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
170
-
171
- case "$TFX_CLI_MODE" in
172
- codex)
173
- if [[ "$CLI_TYPE" == "gemini" ]]; then
174
- CLI_TYPE="codex"; CLI_CMD="codex"
175
- case "$AGENT_TYPE" in
176
- designer)
177
- CLI_ARGS="exec ${codex_base}"; CLI_EFFORT="high"; DEFAULT_TIMEOUT=600 ;;
178
- writer)
179
- CLI_ARGS="exec --profile spark_fast ${codex_base}"; CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180 ;;
180
- esac
181
- echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
182
- fi ;;
183
- gemini)
184
- if [[ "$CLI_TYPE" == "codex" ]]; then
185
- CLI_TYPE="gemini"; CLI_CMD="gemini"
186
- case "$AGENT_TYPE" in
187
- executor|debugger|deep-executor|architect|planner|critic|analyst|\
188
- code-reviewer|security-reviewer|quality-reviewer|scientist-deep)
189
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"; CLI_EFFORT="pro" ;;
190
- build-fixer|spark)
191
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash"; DEFAULT_TIMEOUT=180 ;;
192
- *)
193
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash" ;;
194
- esac
195
- echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
196
- fi ;;
197
- auto)
198
- if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
199
- if command -v "$GEMINI_BIN" &>/dev/null; then
200
- TFX_CLI_MODE="gemini"; apply_cli_mode; return
201
- else
202
- ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
203
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
204
- echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
205
- fi
206
- elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
207
- if command -v "$CODEX_BIN" &>/dev/null; then
208
- TFX_CLI_MODE="codex"; apply_cli_mode; return
209
- else
210
- ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
211
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
212
- echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
213
- fi
214
- fi ;;
215
- esac
216
- }
217
-
218
- # ── MCP 인벤토리 캐시 ──
219
- MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
220
-
221
- get_cached_servers() {
222
- local cli_type="$1"
223
- if [[ -f "$MCP_CACHE" ]]; then
224
- node -e 'const[,f,t]=process.argv;const inv=JSON.parse(require("fs").readFileSync(f,"utf8"));const s=(inv[t]||{}).servers||[];console.log(s.filter(x=>x.status==="enabled"||x.status==="configured").map(x=>x.name).join(","))' -- "$MCP_CACHE" "$cli_type" 2>/dev/null
225
- fi
226
- }
227
-
228
- # ── MCP 프로필 → 프롬프트 힌트 (통합: 캐시 유무 단일 코드경로) ──
229
- get_mcp_hint() {
230
- local profile="$1"
231
- local agent="$2"
232
-
233
- # auto → 구체 프로필 해석
234
- if [[ "$profile" == "auto" ]]; then
235
- case "$agent" in
236
- executor|build-fixer|debugger|deep-executor) profile="implement" ;;
237
- architect|planner|critic|analyst) profile="analyze" ;;
238
- code-reviewer|security-reviewer|quality-reviewer) profile="review" ;;
239
- scientist|document-specialist) profile="analyze" ;;
240
- designer|writer) profile="docs" ;;
241
- *) profile="minimal" ;;
242
- esac
243
- fi
244
-
245
- # 서버 목록: 캐시 있으면 실제, 없으면 전부 가용 가정 (기존 비캐시 동작과 동일)
246
- local servers
247
- servers=$(get_cached_servers "$CLI_TYPE")
248
- [[ -z "$servers" ]] && servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
249
-
250
- has_server() { echo ",$servers," | grep -q ",$1,"; }
251
-
252
- local hint=""
253
- case "$profile" in
254
- implement)
255
- has_server "context7" && hint+="context7으로 라이브러리 문서를 조회하세요. "
256
- if has_server "brave-search"; then hint+="웹 검색은 brave-search를 사용하세요. "
257
- elif has_server "exa"; then hint+="웹 검색은 exa를 사용하세요. "
258
- elif has_server "tavily"; then hint+=" 검색은 tavily를 사용하세요. "
259
- fi
260
- hint+="검색 도구 실패 시 재시도하지 말고 다음 도구로 전환하세요."
261
- ;;
262
- analyze)
263
- has_server "context7" && hint+="context7으로 관련 문서를 조회하세요. "
264
- local search_tools=""
265
- has_server "brave-search" && search_tools+="brave-search, "
266
- has_server "tavily" && search_tools+="tavily, "
267
- has_server "exa" && search_tools+="exa, "
268
- [[ -n "$search_tools" ]] && hint+="웹 검색 우선순위: ${search_tools%, }. 402 에러 시 즉시 다음 도구로 전환. "
269
- has_server "playwright" && hint+="모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
270
- hint+="검색 깊이를 제한하고 결과를 빠르게 요약하세요."
271
- ;;
272
- review)
273
- has_server "sequential-thinking" && hint="sequential-thinking으로 체계적으로 분석하세요."
274
- ;;
275
- docs)
276
- has_server "context7" && hint+="context7으로 공식 문서를 참조하세요. "
277
- has_server "brave-search" && hint+="추가 검색은 brave-search를 사용하세요. "
278
- hint+="검색 결과의 출처 URL을 함께 제시하세요."
279
- ;;
280
- minimal|none) ;;
281
- esac
282
- echo "$hint"
283
- }
284
-
285
- # ── Gemini MCP 서버 선택적 로드 ──
286
- get_gemini_mcp_filter() {
287
- local profile="$1"
288
- case "$profile" in
289
- implement) echo "--allowed-mcp-server-names context7,brave-search" ;;
290
- analyze) echo "--allowed-mcp-server-names context7,brave-search,exa" ;;
291
- review) echo "--allowed-mcp-server-names sequential-thinking" ;;
292
- docs) echo "--allowed-mcp-server-names context7,brave-search" ;;
293
- *) echo "" ;;
294
- esac
295
- }
296
-
297
- # ── 메인 실행 ──
298
- main() {
299
- # 종료 시 per-process 에이전트 파일 자동 삭제
300
- trap 'deregister_agent' EXIT
301
-
302
- route_agent "$AGENT_TYPE"
303
- apply_cli_mode
304
-
305
- # CLI 경로 해석
306
- case "$CLI_CMD" in
307
- codex) CLI_CMD="$CODEX_BIN" ;;
308
- gemini) CLI_CMD="$GEMINI_BIN" ;;
309
- esac
310
-
311
- # 타임아웃 결정
312
- if [[ -n "$USER_TIMEOUT" ]]; then
313
- TIMEOUT_SEC="$USER_TIMEOUT"
314
- else
315
- TIMEOUT_SEC="$DEFAULT_TIMEOUT"
316
- fi
317
-
318
- # 컨텍스트 파일 → 프롬프트에 주입
319
- if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
320
- local ctx_content
321
- ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
322
- PROMPT="${PROMPT}
323
-
324
- <prior_context>
325
- ${ctx_content}
326
- </prior_context>"
327
- fi
328
-
329
- # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
330
- if [[ "$CLI_TYPE" == "claude-native" ]]; then
331
- local model="sonnet"
332
- case "$AGENT_TYPE" in
333
- explore) model="haiku" ;;
334
- esac
335
- echo "ROUTE_TYPE=claude-native"
336
- echo "AGENT=$AGENT_TYPE"
337
- echo "MODEL=$model"
338
- echo "RUN_MODE=$RUN_MODE"
339
- echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
340
- echo "TIMEOUT=$TIMEOUT_SEC"
341
- echo "MCP_PROFILE=$MCP_PROFILE"
342
- [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
343
- echo "PROMPT=$PROMPT"
344
- echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
345
- exit 0
346
- fi
347
-
348
- # MCP 힌트 주입
349
- local mcp_hint
350
- mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
351
- local FULL_PROMPT="$PROMPT"
352
- [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
353
-
354
- # 메타정보 (stderr)
355
- echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
356
- echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE" >&2
357
-
358
- # Per-process 에이전트 등록
359
- register_agent
360
-
361
- # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
362
- local exit_code=0
363
- local start_time
364
- start_time=$(date +%s)
365
-
366
- if [[ "$CLI_TYPE" == "codex" ]]; then
367
- # Codex: stdout/stderr 모두 파일로 캡처 (post.mjs가 읽음)
368
- timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code=$?
369
-
370
- elif [[ "$CLI_TYPE" == "gemini" ]]; then
371
- # Gemini: MCP 프로필별 서버 필터
372
- local gemini_mcp_filter
373
- gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
374
- local gemini_args="$CLI_ARGS"
375
- if [[ -n "$gemini_mcp_filter" ]]; then
376
- gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
377
- echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
378
- fi
379
-
380
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
381
- local pid=$!
382
-
383
- # 지수 백오프 health check (v1.x: 30×1s → v2.0: 5×exp, 총 19초)
384
- local health_ok=true
385
- local intervals=(1 2 3 5 8)
386
- for wait_sec in "${intervals[@]}"; do
387
- sleep "$wait_sec"
388
- # 출력 있으면 정상 조기 탈출
389
- if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
390
- break
391
- fi
392
- # 프로세스 사망 + 출력 없음 → crash
393
- if ! kill -0 "$pid" 2>/dev/null; then
394
- health_ok=false
395
- echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
396
- break
397
- fi
398
- done
399
-
400
- if [[ "$health_ok" == "false" ]]; then
401
- wait "$pid" 2>/dev/null
402
- echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
403
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
404
- pid=$!
405
- wait "$pid"
406
- exit_code=$?
407
- else
408
- wait "$pid"
409
- exit_code=$?
410
- fi
411
- fi
412
-
413
- local end_time
414
- end_time=$(date +%s)
415
- local elapsed=$((end_time - start_time))
416
-
417
- # ── 후처리: 단일 node 프로세스로 위임 ──
418
- # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
419
- local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
420
- if [[ -f "$post_script" ]]; then
421
- node "$post_script" \
422
- --agent "$AGENT_TYPE" \
423
- --cli "$CLI_TYPE" \
424
- --cli-cmd "$CLI_CMD" \
425
- --effort "$CLI_EFFORT" \
426
- --run-mode "$RUN_MODE" \
427
- --opus "$OPUS_OVERSIGHT" \
428
- --exit-code "$exit_code" \
429
- --elapsed "$elapsed" \
430
- --timeout "$TIMEOUT_SEC" \
431
- --mcp-profile "$MCP_PROFILE" \
432
- --stderr-log "$STDERR_LOG" \
433
- --stdout-log "$STDOUT_LOG" \
434
- --max-bytes "$MAX_STDOUT_BYTES"
435
- else
436
- # post.mjs 없으면 기본 출력 (fallback)
437
- echo "=== TFX-ROUTE RESULT ==="
438
- echo "agent: $AGENT_TYPE"
439
- echo "cli: $CLI_TYPE"
440
- echo "exit_code: $exit_code"
441
- echo "elapsed: ${elapsed}s"
442
- echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
443
- echo "=== OUTPUT ==="
444
- cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
445
- fi
446
- }
447
-
448
- main
1
+ #!/usr/bin/env bash
2
+ # tfx-route.sh v2.0 — CLI 라우팅 래퍼 (triflux)
3
+ #
4
+ # v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
5
+ # v2.0: tfx-route.sh 리네임
6
+ # - 후처리 전부 tfx-route-post.mjs로 이관 (node 단일 ~100ms)
7
+ # - per-process 에이전트 등록 (race condition 구조적 제거)
8
+ # - get_mcp_hint 통합 (캐시/비캐시 단일 코드경로)
9
+ # - Gemini health check 지수 백오프 (30×1s → 5×exp)
10
+ # - 컨텍스트 파일 5번째 인자 지원
11
+ #
12
+ VERSION="2.0"
13
+ #
14
+ # 사용법:
15
+ # tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
16
+ #
17
+ # 예시:
18
+ # tfx-route.sh executor "코드 구현" implement
19
+ # tfx-route.sh architect "아키텍처 분석" analyze '' context.md
20
+
21
+ set -euo pipefail
22
+
23
+ # ── 인자 파싱 ──
24
+ AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
25
+ PROMPT="${2:?프롬프트 필수}"
26
+ MCP_PROFILE="${3:-auto}"
27
+ USER_TIMEOUT="${4:-}"
28
+ CONTEXT_FILE="${5:-}"
29
+
30
+ # ── CLI 경로 해석 (Windows npm global 대응) ──
31
+ CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
32
+ GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
33
+
34
+ # ── 상수 ──
35
+ MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
36
+ TIMESTAMP=$(date +%s)
37
+ STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stderr.log"
38
+ STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${TIMESTAMP}-stdout.log"
39
+ TFX_TMP="${TMPDIR:-/tmp}"
40
+
41
+ # ── 환경변수 ──
42
+ TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
43
+ TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
44
+ TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
45
+ TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
46
+ TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}"
47
+
48
+ # fallback 원래 에이전트 정보 보존
49
+ ORIGINAL_AGENT=""
50
+ ORIGINAL_CLI_ARGS=""
51
+
52
+ # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
53
+ register_agent() {
54
+ local agent_file="${TFX_TMP}/tfx-agent-$$.json"
55
+ echo "{\"pid\":$$,\"cli\":\"$CLI_TYPE\",\"agent\":\"$AGENT_TYPE\",\"started\":$(date +%s)}" \
56
+ > "$agent_file" 2>/dev/null || true
57
+ }
58
+
59
+ deregister_agent() {
60
+ rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
61
+ }
62
+
63
+ # ── Hub Bridge 통신 ──
64
+ # JSON 문자열 이스케이프 (큰따옴표, 백슬래시, 개행, 탭, CR)
65
+ json_escape() {
66
+ local s="${1:-}"
67
+ s="${s//\\/\\\\}"
68
+ s="${s//\"/\\\"}"
69
+ s="${s//$'\n'/\\n}"
70
+ s="${s//$'\t'/\\t}"
71
+ s="${s//$'\r'/\\r}"
72
+ echo "$s"
73
+ }
74
+
75
+ team_claim_task() {
76
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
77
+ local http_code safe_team_name safe_task_id safe_agent_name
78
+ safe_team_name=$(json_escape "$TFX_TEAM_NAME")
79
+ safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
80
+ safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
81
+
82
+ http_code=$(curl -sf -o /dev/null -w "%{http_code}" -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
83
+ -H "Content-Type: application/json" \
84
+ -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"claim\":true,\"owner\":\"${safe_agent_name}\",\"status\":\"in_progress\"}" \
85
+ 2>/dev/null) || http_code="000"
86
+
87
+ case "$http_code" in
88
+ 200) ;; # 성공
89
+ 409)
90
+ echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨. 실행 중단." >&2
91
+ exit 0 ;;
92
+ 000)
93
+ echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2 ;;
94
+ *)
95
+ echo "[tfx-route] 경고: Hub claim 응답 HTTP ${http_code}. claim 없이 계속 실행." >&2 ;;
96
+ esac
97
+ }
98
+
99
+ team_complete_task() {
100
+ local result_status="${1:-completed}"
101
+ local result_summary="${2:-작업 완료}"
102
+ local safe_team_name safe_task_id safe_agent_name safe_status
103
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
104
+ safe_team_name=$(json_escape "$TFX_TEAM_NAME")
105
+ safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
106
+ safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
107
+ safe_status=$(json_escape "$result_status")
108
+
109
+ # task 상태 업데이트
110
+ curl -sf -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
111
+ -H "Content-Type: application/json" \
112
+ -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"status\":\"${safe_status}\",\"owner\":\"${safe_agent_name}\"}" \
113
+ >/dev/null 2>&1 || true
114
+
115
+ # 리드에게 메시지 전송
116
+ local msg_text safe_text safe_lead_name
117
+ msg_text=$(echo "$result_summary" | head -c 4096)
118
+ safe_text=$(json_escape "$msg_text")
119
+ safe_lead_name=$(json_escape "$TFX_TEAM_LEAD_NAME")
120
+
121
+ curl -sf -X POST "${TFX_HUB_URL}/bridge/team/send-message" \
122
+ -H "Content-Type: application/json" \
123
+ -d "{\"team_name\":\"${safe_team_name}\",\"from\":\"${safe_agent_name}\",\"to\":\"${safe_lead_name}\",\"text\":\"${safe_text}\",\"summary\":\"task ${safe_task_id} ${safe_status}\"}" \
124
+ >/dev/null 2>&1 || true
125
+
126
+ # Hub result 발행 (poll_messages 채널 활성화)
127
+ curl -sf -X POST "${TFX_HUB_URL}/bridge/result" \
128
+ -H "Content-Type: application/json" \
129
+ -d "{\"agent_id\":\"${safe_agent_name}\",\"topic\":\"task.result\",\"payload\":{\"task_id\":\"${safe_task_id}\",\"status\":\"${safe_status}\"},\"trace_id\":\"${safe_team_name}\"}" \
130
+ >/dev/null 2>&1 || true
131
+ }
132
+
133
+ # ── 라우팅 테이블 ──
134
+ # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
135
+ route_agent() {
136
+ local agent="$1"
137
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
138
+
139
+ case "$agent" in
140
+ # ─── 구현 레인 ───
141
+ executor)
142
+ CLI_TYPE="codex"; CLI_CMD="codex"
143
+ CLI_ARGS="exec ${codex_base}"
144
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
145
+ build-fixer)
146
+ CLI_TYPE="codex"; CLI_CMD="codex"
147
+ CLI_ARGS="exec --profile fast ${codex_base}"
148
+ CLI_EFFORT="fast"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
149
+ debugger)
150
+ CLI_TYPE="codex"; CLI_CMD="codex"
151
+ CLI_ARGS="exec ${codex_base}"
152
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
153
+ deep-executor)
154
+ CLI_TYPE="codex"; CLI_CMD="codex"
155
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
156
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
157
+
158
+ # ─── 설계/분석 레인 ───
159
+ architect)
160
+ CLI_TYPE="codex"; CLI_CMD="codex"
161
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
162
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
163
+ planner)
164
+ CLI_TYPE="codex"; CLI_CMD="codex"
165
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
166
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
167
+ critic)
168
+ CLI_TYPE="codex"; CLI_CMD="codex"
169
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
170
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
171
+ analyst)
172
+ CLI_TYPE="codex"; CLI_CMD="codex"
173
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
174
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
175
+
176
+ # ─── 리뷰 레인 ───
177
+ code-reviewer)
178
+ CLI_TYPE="codex"; CLI_CMD="codex"
179
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
180
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
181
+ security-reviewer)
182
+ CLI_TYPE="codex"; CLI_CMD="codex"
183
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
184
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
185
+ quality-reviewer)
186
+ CLI_TYPE="codex"; CLI_CMD="codex"
187
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
188
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
189
+
190
+ # ─── 리서치 레인 ───
191
+ scientist)
192
+ CLI_TYPE="codex"; CLI_CMD="codex"
193
+ CLI_ARGS="exec ${codex_base}"
194
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
195
+ scientist-deep)
196
+ CLI_TYPE="codex"; CLI_CMD="codex"
197
+ CLI_ARGS="exec --profile thorough ${codex_base}"
198
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
199
+ document-specialist)
200
+ CLI_TYPE="codex"; CLI_CMD="codex"
201
+ CLI_ARGS="exec ${codex_base}"
202
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
203
+
204
+ # ─── UI/문서 레인 ───
205
+ designer)
206
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
207
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
208
+ CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
209
+ writer)
210
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
211
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
212
+ CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
213
+
214
+ # ─── Claude 네이티브 ───
215
+ explore)
216
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
217
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
218
+ verifier)
219
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
220
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
221
+ test-engineer)
222
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
223
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
224
+ qa-tester)
225
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
226
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
227
+
228
+ # ─── 경량 ───
229
+ spark)
230
+ CLI_TYPE="codex"; CLI_CMD="codex"
231
+ CLI_ARGS="exec --profile spark_fast ${codex_base}"
232
+ CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
233
+ *)
234
+ echo "ERROR: 수 없는 에이전트 타입: $agent" >&2
235
+ echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
236
+ echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
237
+ echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
238
+ exit 1 ;;
239
+ esac
240
+ }
241
+
242
+ # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
243
+ TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
244
+ TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
245
+ case "$TFX_NO_CLAUDE_NATIVE" in
246
+ 0|1) ;;
247
+ *)
248
+ echo "ERROR: TFX_NO_CLAUDE_NATIVE 값은 0 또는 1이어야 합니다. (현재: $TFX_NO_CLAUDE_NATIVE)" >&2
249
+ exit 1
250
+ ;;
251
+ esac
252
+
253
+ apply_cli_mode() {
254
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
255
+
256
+ case "$TFX_CLI_MODE" in
257
+ codex)
258
+ if [[ "$CLI_TYPE" == "gemini" ]]; then
259
+ CLI_TYPE="codex"; CLI_CMD="codex"
260
+ case "$AGENT_TYPE" in
261
+ designer)
262
+ CLI_ARGS="exec ${codex_base}"; CLI_EFFORT="high"; DEFAULT_TIMEOUT=600 ;;
263
+ writer)
264
+ CLI_ARGS="exec --profile spark_fast ${codex_base}"; CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180 ;;
265
+ esac
266
+ echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
267
+ fi ;;
268
+ gemini)
269
+ if [[ "$CLI_TYPE" == "codex" ]]; then
270
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
271
+ case "$AGENT_TYPE" in
272
+ executor|debugger|deep-executor|architect|planner|critic|analyst|\
273
+ code-reviewer|security-reviewer|quality-reviewer|scientist-deep)
274
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"; CLI_EFFORT="pro" ;;
275
+ build-fixer|spark)
276
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash"; DEFAULT_TIMEOUT=180 ;;
277
+ *)
278
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash" ;;
279
+ esac
280
+ echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT) 리매핑" >&2
281
+ fi ;;
282
+ auto)
283
+ if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
284
+ if command -v "$GEMINI_BIN" &>/dev/null; then
285
+ TFX_CLI_MODE="gemini"; apply_cli_mode; return
286
+ else
287
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
288
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
289
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
290
+ fi
291
+ elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
292
+ if command -v "$CODEX_BIN" &>/dev/null; then
293
+ TFX_CLI_MODE="codex"; apply_cli_mode; return
294
+ else
295
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
296
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
297
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
298
+ fi
299
+ fi ;;
300
+ esac
301
+ }
302
+
303
+ # ── Claude 네이티브 제거 (Codex 리드 환경에서 선택적 활성화) ──
304
+ apply_no_claude_native_mode() {
305
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
306
+
307
+ [[ "$TFX_NO_CLAUDE_NATIVE" != "1" ]] && return
308
+ [[ "$TFX_CLI_MODE" == "gemini" ]] && return
309
+ [[ "$CLI_TYPE" != "claude-native" ]] && return
310
+
311
+ if ! command -v "$CODEX_BIN" &>/dev/null; then
312
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1 이지만 codex를 찾지 못해 claude-native 유지" >&2
313
+ return
314
+ fi
315
+
316
+ ORIGINAL_AGENT="${AGENT_TYPE}"
317
+ CLI_TYPE="codex"; CLI_CMD="codex"
318
+
319
+ case "$AGENT_TYPE" in
320
+ explore)
321
+ CLI_ARGS="exec --profile fast ${codex_base}"
322
+ CLI_EFFORT="fast"
323
+ DEFAULT_TIMEOUT=600
324
+ RUN_MODE="fg"
325
+ OPUS_OVERSIGHT="false"
326
+ ;;
327
+ verifier)
328
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
329
+ CLI_EFFORT="thorough"
330
+ DEFAULT_TIMEOUT=1200
331
+ RUN_MODE="fg"
332
+ OPUS_OVERSIGHT="false"
333
+ ;;
334
+ test-engineer)
335
+ CLI_ARGS="exec ${codex_base}"
336
+ CLI_EFFORT="high"
337
+ DEFAULT_TIMEOUT=1200
338
+ RUN_MODE="bg"
339
+ OPUS_OVERSIGHT="false"
340
+ ;;
341
+ qa-tester)
342
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
343
+ CLI_EFFORT="thorough"
344
+ DEFAULT_TIMEOUT=1200
345
+ RUN_MODE="bg"
346
+ OPUS_OVERSIGHT="false"
347
+ ;;
348
+ *)
349
+ # claude-native 타입 중 위에 없는 경우는 보수적으로 유지
350
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
351
+ return
352
+ ;;
353
+ esac
354
+
355
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
356
+ }
357
+
358
+ # ── MCP 인벤토리 캐시 ──
359
+ MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
360
+
361
+ get_cached_servers() {
362
+ local cli_type="$1"
363
+ if [[ -f "$MCP_CACHE" ]]; then
364
+ node -e 'const[,f,t]=process.argv;const inv=JSON.parse(require("fs").readFileSync(f,"utf8"));const s=(inv[t]||{}).servers||[];console.log(s.filter(x=>x.status==="enabled"||x.status==="configured").map(x=>x.name).join(","))' -- "$MCP_CACHE" "$cli_type" 2>/dev/null
365
+ fi
366
+ }
367
+
368
+ # ── MCP 프로필 프롬프트 힌트 (통합: 캐시 유무 단일 코드경로) ──
369
+ get_mcp_hint() {
370
+ local profile="$1"
371
+ local agent="$2"
372
+
373
+ # auto → 구체 프로필 해석
374
+ if [[ "$profile" == "auto" ]]; then
375
+ case "$agent" in
376
+ executor|build-fixer|debugger|deep-executor) profile="implement" ;;
377
+ architect|planner|critic|analyst) profile="analyze" ;;
378
+ code-reviewer|security-reviewer|quality-reviewer) profile="review" ;;
379
+ scientist|document-specialist) profile="analyze" ;;
380
+ designer|writer) profile="docs" ;;
381
+ *) profile="minimal" ;;
382
+ esac
383
+ fi
384
+
385
+ # 서버 목록: 캐시 있으면 실제, 없으면 전부 가용 가정 (기존 비캐시 동작과 동일)
386
+ local servers
387
+ servers=$(get_cached_servers "$CLI_TYPE")
388
+ [[ -z "$servers" ]] && servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
389
+
390
+ has_server() { echo ",$servers," | grep -q ",$1,"; }
391
+
392
+ local hint=""
393
+ case "$profile" in
394
+ implement)
395
+ has_server "context7" && hint+="context7으로 라이브러리 문서를 조회하세요. "
396
+ if has_server "brave-search"; then hint+="웹 검색은 brave-search를 사용하세요. "
397
+ elif has_server "exa"; then hint+="웹 검색은 exa를 사용하세요. "
398
+ elif has_server "tavily"; then hint+="웹 검색은 tavily를 사용하세요. "
399
+ fi
400
+ hint+="검색 도구 실패 재시도하지 말고 다음 도구로 전환하세요."
401
+ ;;
402
+ analyze)
403
+ has_server "context7" && hint+="context7으로 관련 문서를 조회하세요. "
404
+ local search_tools=""
405
+ has_server "brave-search" && search_tools+="brave-search, "
406
+ has_server "tavily" && search_tools+="tavily, "
407
+ has_server "exa" && search_tools+="exa, "
408
+ [[ -n "$search_tools" ]] && hint+="웹 검색 우선순위: ${search_tools%, }. 402 에러 시 즉시 다음 도구로 전환. "
409
+ has_server "playwright" && hint+="모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
410
+ hint+="검색 깊이를 제한하고 결과를 빠르게 요약하세요."
411
+ ;;
412
+ review)
413
+ has_server "sequential-thinking" && hint="sequential-thinking으로 체계적으로 분석하세요."
414
+ ;;
415
+ docs)
416
+ has_server "context7" && hint+="context7으로 공식 문서를 참조하세요. "
417
+ has_server "brave-search" && hint+="추가 검색은 brave-search를 사용하세요. "
418
+ hint+="검색 결과의 출처 URL을 함께 제시하세요."
419
+ ;;
420
+ minimal|none) ;;
421
+ esac
422
+ echo "$hint"
423
+ }
424
+
425
+ # ── Gemini MCP 서버 선택적 로드 ──
426
+ get_gemini_mcp_filter() {
427
+ local profile="$1"
428
+ case "$profile" in
429
+ implement) echo "--allowed-mcp-server-names context7,brave-search" ;;
430
+ analyze) echo "--allowed-mcp-server-names context7,brave-search,exa" ;;
431
+ review) echo "--allowed-mcp-server-names sequential-thinking" ;;
432
+ docs) echo "--allowed-mcp-server-names context7,brave-search" ;;
433
+ *) echo "" ;;
434
+ esac
435
+ }
436
+
437
+ # ── 메인 실행 ──
438
+ main() {
439
+ # 종료 시 per-process 에이전트 파일 자동 삭제
440
+ trap 'deregister_agent' EXIT
441
+
442
+ route_agent "$AGENT_TYPE"
443
+ apply_cli_mode
444
+ apply_no_claude_native_mode
445
+
446
+ # CLI 경로 해석
447
+ case "$CLI_CMD" in
448
+ codex) CLI_CMD="$CODEX_BIN" ;;
449
+ gemini) CLI_CMD="$GEMINI_BIN" ;;
450
+ esac
451
+
452
+ # 타임아웃 결정
453
+ if [[ -n "$USER_TIMEOUT" ]]; then
454
+ TIMEOUT_SEC="$USER_TIMEOUT"
455
+ else
456
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
457
+ fi
458
+
459
+ # 컨텍스트 파일 → 프롬프트에 주입
460
+ if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
461
+ local ctx_content
462
+ ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
463
+ PROMPT="${PROMPT}
464
+
465
+ <prior_context>
466
+ ${ctx_content}
467
+ </prior_context>"
468
+ fi
469
+
470
+ # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
471
+ if [[ "$CLI_TYPE" == "claude-native" ]]; then
472
+ local model="sonnet"
473
+ case "$AGENT_TYPE" in
474
+ explore) model="haiku" ;;
475
+ esac
476
+ echo "ROUTE_TYPE=claude-native"
477
+ echo "AGENT=$AGENT_TYPE"
478
+ echo "MODEL=$model"
479
+ echo "RUN_MODE=$RUN_MODE"
480
+ echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
481
+ echo "TIMEOUT=$TIMEOUT_SEC"
482
+ echo "MCP_PROFILE=$MCP_PROFILE"
483
+ [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
484
+ echo "PROMPT=$PROMPT"
485
+ echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
486
+ exit 0
487
+ fi
488
+
489
+ # MCP 힌트 주입
490
+ local mcp_hint
491
+ mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
492
+ local FULL_PROMPT="$PROMPT"
493
+ [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
494
+
495
+ # 메타정보 (stderr)
496
+ echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
497
+ echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE" >&2
498
+ [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
499
+
500
+ # Per-process 에이전트 등록
501
+ register_agent
502
+
503
+ # 팀 모드: task claim
504
+ team_claim_task
505
+
506
+ # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
507
+ local exit_code=0
508
+ local start_time
509
+ start_time=$(date +%s)
510
+
511
+ if [[ "$CLI_TYPE" == "codex" ]]; then
512
+ # Codex: stdout/stderr 모두 파일로 캡처 (post.mjs가 읽음)
513
+ timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code=$?
514
+
515
+ elif [[ "$CLI_TYPE" == "gemini" ]]; then
516
+ # Gemini: MCP 프로필별 서버 필터
517
+ local gemini_mcp_filter
518
+ gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
519
+ local gemini_args="$CLI_ARGS"
520
+ if [[ -n "$gemini_mcp_filter" ]]; then
521
+ gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
522
+ echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
523
+ fi
524
+
525
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
526
+ local pid=$!
527
+
528
+ # 지수 백오프 health check (v1.x: 30×1s → v2.0: 5×exp, 총 19초)
529
+ local health_ok=true
530
+ local intervals=(1 2 3 5 8)
531
+ for wait_sec in "${intervals[@]}"; do
532
+ sleep "$wait_sec"
533
+ # 출력 있으면 정상 → 조기 탈출
534
+ if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
535
+ break
536
+ fi
537
+ # 프로세스 사망 + 출력 없음 → crash
538
+ if ! kill -0 "$pid" 2>/dev/null; then
539
+ health_ok=false
540
+ echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
541
+ break
542
+ fi
543
+ done
544
+
545
+ if [[ "$health_ok" == "false" ]]; then
546
+ wait "$pid" 2>/dev/null
547
+ echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
548
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
549
+ pid=$!
550
+ wait "$pid"
551
+ exit_code=$?
552
+ else
553
+ wait "$pid"
554
+ exit_code=$?
555
+ fi
556
+ fi
557
+
558
+ local end_time
559
+ end_time=$(date +%s)
560
+ local elapsed=$((end_time - start_time))
561
+
562
+ # 팀 모드: task complete + 리드 보고
563
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
564
+ if [[ "$exit_code" -eq 0 ]]; then
565
+ local output_preview
566
+ output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
567
+ team_complete_task "completed" "$output_preview"
568
+ elif [[ "$exit_code" -eq 124 ]]; then
569
+ team_complete_task "failed" "타임아웃 (${TIMEOUT_SEC}초)"
570
+ else
571
+ local err_preview
572
+ err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
573
+ team_complete_task "failed" "exit_code=${exit_code}: ${err_preview}"
574
+ fi
575
+ fi
576
+
577
+ # ── 후처리: 단일 node 프로세스로 위임 ──
578
+ # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
579
+ local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
580
+ if [[ -f "$post_script" ]]; then
581
+ node "$post_script" \
582
+ --agent "$AGENT_TYPE" \
583
+ --cli "$CLI_TYPE" \
584
+ --cli-cmd "$CLI_CMD" \
585
+ --effort "$CLI_EFFORT" \
586
+ --run-mode "$RUN_MODE" \
587
+ --opus "$OPUS_OVERSIGHT" \
588
+ --exit-code "$exit_code" \
589
+ --elapsed "$elapsed" \
590
+ --timeout "$TIMEOUT_SEC" \
591
+ --mcp-profile "$MCP_PROFILE" \
592
+ --stderr-log "$STDERR_LOG" \
593
+ --stdout-log "$STDOUT_LOG" \
594
+ --max-bytes "$MAX_STDOUT_BYTES"
595
+ else
596
+ # post.mjs 없으면 기본 출력 (fallback)
597
+ echo "=== TFX-ROUTE RESULT ==="
598
+ echo "agent: $AGENT_TYPE"
599
+ echo "cli: $CLI_TYPE"
600
+ echo "exit_code: $exit_code"
601
+ echo "elapsed: ${elapsed}s"
602
+ echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
603
+ echo "=== OUTPUT ==="
604
+ cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
605
+ fi
606
+ }
607
+
608
+ main