triflux 3.2.0-dev.1 → 3.2.0-dev.11

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.
Files changed (53) hide show
  1. package/README.ko.md +26 -18
  2. package/README.md +26 -18
  3. package/bin/triflux.mjs +1614 -1084
  4. package/hooks/hooks.json +12 -0
  5. package/hooks/keyword-rules.json +354 -0
  6. package/hub/bridge.mjs +371 -193
  7. package/hub/hitl.mjs +45 -31
  8. package/hub/pipe.mjs +457 -0
  9. package/hub/router.mjs +422 -161
  10. package/hub/server.mjs +429 -344
  11. package/hub/store.mjs +388 -314
  12. package/hub/team/cli-team-common.mjs +348 -0
  13. package/hub/team/cli-team-control.mjs +393 -0
  14. package/hub/team/cli-team-start.mjs +516 -0
  15. package/hub/team/cli-team-status.mjs +269 -0
  16. package/hub/team/cli.mjs +99 -368
  17. package/hub/team/dashboard.mjs +165 -64
  18. package/hub/team/native-supervisor.mjs +300 -0
  19. package/hub/team/native.mjs +62 -0
  20. package/hub/team/nativeProxy.mjs +534 -0
  21. package/hub/team/orchestrator.mjs +90 -31
  22. package/hub/team/pane.mjs +149 -101
  23. package/hub/team/psmux.mjs +297 -0
  24. package/hub/team/session.mjs +608 -186
  25. package/hub/team/shared.mjs +13 -0
  26. package/hub/team/staleState.mjs +299 -0
  27. package/hub/tools.mjs +140 -53
  28. package/hub/workers/claude-worker.mjs +446 -0
  29. package/hub/workers/codex-mcp.mjs +414 -0
  30. package/hub/workers/factory.mjs +18 -0
  31. package/hub/workers/gemini-worker.mjs +349 -0
  32. package/hub/workers/interface.mjs +41 -0
  33. package/hud/hud-qos-status.mjs +1789 -1732
  34. package/package.json +6 -2
  35. package/scripts/__tests__/keyword-detector.test.mjs +234 -0
  36. package/scripts/hub-ensure.mjs +83 -0
  37. package/scripts/keyword-detector.mjs +272 -0
  38. package/scripts/keyword-rules-expander.mjs +521 -0
  39. package/scripts/lib/keyword-rules.mjs +168 -0
  40. package/scripts/psmux-steering-prototype.sh +368 -0
  41. package/scripts/run.cjs +62 -0
  42. package/scripts/setup.mjs +189 -7
  43. package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
  44. package/scripts/tfx-route-worker.mjs +161 -0
  45. package/scripts/tfx-route.sh +943 -508
  46. package/skills/tfx-auto/SKILL.md +90 -564
  47. package/skills/tfx-auto-codex/SKILL.md +77 -0
  48. package/skills/tfx-codex/SKILL.md +1 -4
  49. package/skills/tfx-doctor/SKILL.md +1 -0
  50. package/skills/tfx-gemini/SKILL.md +1 -4
  51. package/skills/tfx-multi/SKILL.md +296 -0
  52. package/skills/tfx-setup/SKILL.md +1 -4
  53. package/skills/tfx-team/SKILL.md +0 -172
@@ -1,508 +1,943 @@
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
- # ── Hub 브릿지 (선택적 — Hub 미실행 시 무시) ──
42
- # 패키지 내 브릿지 탐색 (npm global / git local 모두 대응)
43
- find_bridge() {
44
- # 1. 환경변수 지정
45
- [[ -n "${TFX_BRIDGE:-}" && -f "$TFX_BRIDGE" ]] && echo "$TFX_BRIDGE" && return
46
- # 2. 같은 패키지
47
- local script_dir
48
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
49
- local pkg_bridge="${script_dir}/../hub/bridge.mjs"
50
- [[ -f "$pkg_bridge" ]] && echo "$pkg_bridge" && return
51
- # 3. 설치된 triflux 패키지
52
- local npm_bridge
53
- npm_bridge="$(npm root -g 2>/dev/null)/triflux/hub/bridge.mjs"
54
- [[ -f "$npm_bridge" ]] && echo "$npm_bridge" && return
55
- echo ""
56
- }
57
- BRIDGE_BIN="$(find_bridge)"
58
- HUB_ENABLED="false"
59
- if [[ -n "$BRIDGE_BIN" ]]; then
60
- # Hub 핑 (3초 타임아웃, 실패 시 무시)
61
- HUB_PING=$(node "$BRIDGE_BIN" ping 2>/dev/null || echo '{"ok":false}')
62
- if echo "$HUB_PING" | grep -q '"ok":true'; then
63
- HUB_ENABLED="true"
64
- fi
65
- fi
66
-
67
- # Hub 브릿지 래퍼 (Hub 꺼져있으면 아무것도 안 함)
68
- hub_bridge() {
69
- [[ "$HUB_ENABLED" != "true" ]] && return 0
70
- node "$BRIDGE_BIN" "$@" 2>/dev/null || true
71
- }
72
-
73
- # fallback 원래 에이전트 정보 보존
74
- ORIGINAL_AGENT=""
75
- ORIGINAL_CLI_ARGS=""
76
-
77
- # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
78
- register_agent() {
79
- local agent_file="${TFX_TMP}/tfx-agent-$$.json"
80
- echo "{\"pid\":$$,\"cli\":\"$CLI_TYPE\",\"agent\":\"$AGENT_TYPE\",\"started\":$(date +%s)}" \
81
- > "$agent_file" 2>/dev/null || true
82
- }
83
-
84
- deregister_agent() {
85
- rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
86
- }
87
-
88
- # ── 라우팅 테이블 ──
89
- # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
90
- route_agent() {
91
- local agent="$1"
92
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
93
-
94
- case "$agent" in
95
- # ─── 구현 레인 ───
96
- executor)
97
- CLI_TYPE="codex"; CLI_CMD="codex"
98
- CLI_ARGS="exec ${codex_base}"
99
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
100
- build-fixer)
101
- CLI_TYPE="codex"; CLI_CMD="codex"
102
- CLI_ARGS="--profile fast exec ${codex_base}"
103
- CLI_EFFORT="fast"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
104
- debugger)
105
- CLI_TYPE="codex"; CLI_CMD="codex"
106
- CLI_ARGS="exec ${codex_base}"
107
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
108
- deep-executor)
109
- CLI_TYPE="codex"; CLI_CMD="codex"
110
- CLI_ARGS="--profile xhigh exec ${codex_base}"
111
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
112
-
113
- # ─── 설계/분석 레인 ───
114
- architect)
115
- CLI_TYPE="codex"; CLI_CMD="codex"
116
- CLI_ARGS="--profile xhigh exec ${codex_base}"
117
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
118
- planner)
119
- CLI_TYPE="codex"; CLI_CMD="codex"
120
- CLI_ARGS="--profile xhigh exec ${codex_base}"
121
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
122
- critic)
123
- CLI_TYPE="codex"; CLI_CMD="codex"
124
- CLI_ARGS="--profile xhigh exec ${codex_base}"
125
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
126
- analyst)
127
- CLI_TYPE="codex"; CLI_CMD="codex"
128
- CLI_ARGS="--profile xhigh exec ${codex_base}"
129
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
130
-
131
- # ─── 리뷰 레인 ───
132
- code-reviewer)
133
- CLI_TYPE="codex"; CLI_CMD="codex"
134
- CLI_ARGS="--profile thorough exec ${codex_base} review"
135
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
136
- security-reviewer)
137
- CLI_TYPE="codex"; CLI_CMD="codex"
138
- CLI_ARGS="--profile thorough exec ${codex_base} review"
139
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
140
- quality-reviewer)
141
- CLI_TYPE="codex"; CLI_CMD="codex"
142
- CLI_ARGS="--profile thorough exec ${codex_base} review"
143
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
144
-
145
- # ─── 리서치 레인 ───
146
- scientist)
147
- CLI_TYPE="codex"; CLI_CMD="codex"
148
- CLI_ARGS="exec ${codex_base}"
149
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
150
- scientist-deep)
151
- CLI_TYPE="codex"; CLI_CMD="codex"
152
- CLI_ARGS="--profile thorough exec ${codex_base}"
153
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
154
- document-specialist)
155
- CLI_TYPE="codex"; CLI_CMD="codex"
156
- CLI_ARGS="exec ${codex_base}"
157
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
158
-
159
- # ─── UI/문서 레인 ───
160
- designer)
161
- CLI_TYPE="gemini"; CLI_CMD="gemini"
162
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
163
- CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
164
- writer)
165
- CLI_TYPE="gemini"; CLI_CMD="gemini"
166
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
167
- CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
168
-
169
- # ─── Claude 네이티브 ───
170
- explore)
171
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
172
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
173
- verifier)
174
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
175
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
176
- test-engineer)
177
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
178
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
179
- qa-tester)
180
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
181
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
182
-
183
- # ─── 경량 ───
184
- spark)
185
- CLI_TYPE="codex"; CLI_CMD="codex"
186
- CLI_ARGS="--profile spark_fast exec ${codex_base}"
187
- CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
188
- *)
189
- echo "ERROR: 없는 에이전트 타입: $agent" >&2
190
- echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
191
- echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
192
- echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
193
- exit 1 ;;
194
- esac
195
- }
196
-
197
- # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
198
- TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
199
-
200
- apply_cli_mode() {
201
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
202
-
203
- case "$TFX_CLI_MODE" in
204
- codex)
205
- if [[ "$CLI_TYPE" == "gemini" ]]; then
206
- CLI_TYPE="codex"; CLI_CMD="codex"
207
- case "$AGENT_TYPE" in
208
- designer)
209
- CLI_ARGS="exec ${codex_base}"; CLI_EFFORT="high"; DEFAULT_TIMEOUT=600 ;;
210
- writer)
211
- CLI_ARGS="--profile spark_fast exec ${codex_base}"; CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180 ;;
212
- esac
213
- echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
214
- fi ;;
215
- gemini)
216
- if [[ "$CLI_TYPE" == "codex" ]]; then
217
- CLI_TYPE="gemini"; CLI_CMD="gemini"
218
- case "$AGENT_TYPE" in
219
- executor|debugger|deep-executor|architect|planner|critic|analyst|\
220
- code-reviewer|security-reviewer|quality-reviewer|scientist-deep)
221
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"; CLI_EFFORT="pro" ;;
222
- build-fixer|spark)
223
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash"; DEFAULT_TIMEOUT=180 ;;
224
- *)
225
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash" ;;
226
- esac
227
- echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
228
- fi ;;
229
- auto)
230
- if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
231
- if command -v "$GEMINI_BIN" &>/dev/null; then
232
- TFX_CLI_MODE="gemini"; apply_cli_mode; return
233
- else
234
- ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
235
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
236
- echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
237
- fi
238
- elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
239
- if command -v "$CODEX_BIN" &>/dev/null; then
240
- TFX_CLI_MODE="codex"; apply_cli_mode; return
241
- else
242
- ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
243
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
244
- echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE claude-native fallback" >&2
245
- fi
246
- fi ;;
247
- esac
248
- }
249
-
250
- # ── MCP 인벤토리 캐시 ──
251
- MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
252
-
253
- get_cached_servers() {
254
- local cli_type="$1"
255
- if [[ -f "$MCP_CACHE" ]]; then
256
- 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
257
- fi
258
- }
259
-
260
- # ── MCP 프로필 → 프롬프트 힌트 (통합: 캐시 유무 단일 코드경로) ──
261
- get_mcp_hint() {
262
- local profile="$1"
263
- local agent="$2"
264
-
265
- # auto → 구체 프로필 해석
266
- if [[ "$profile" == "auto" ]]; then
267
- case "$agent" in
268
- executor|build-fixer|debugger|deep-executor) profile="implement" ;;
269
- architect|planner|critic|analyst) profile="analyze" ;;
270
- code-reviewer|security-reviewer|quality-reviewer) profile="review" ;;
271
- scientist|document-specialist) profile="analyze" ;;
272
- designer|writer) profile="docs" ;;
273
- *) profile="minimal" ;;
274
- esac
275
- fi
276
-
277
- # 서버 목록: 캐시 있으면 실제, 없으면 전부 가용 가정 (기존 비캐시 동작과 동일)
278
- local servers
279
- servers=$(get_cached_servers "$CLI_TYPE")
280
- [[ -z "$servers" ]] && servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
281
-
282
- has_server() { echo ",$servers," | grep -q ",$1,"; }
283
-
284
- local hint=""
285
- case "$profile" in
286
- implement)
287
- has_server "context7" && hint+="context7으로 라이브러리 문서를 조회하세요. "
288
- if has_server "brave-search"; then hint+=" 검색은 brave-search를 사용하세요. "
289
- elif has_server "exa"; then hint+="웹 검색은 exa를 사용하세요. "
290
- elif has_server "tavily"; then hint+="웹 검색은 tavily를 사용하세요. "
291
- fi
292
- hint+="검색 도구 실패 시 재시도하지 말고 다음 도구로 전환하세요."
293
- ;;
294
- analyze)
295
- has_server "context7" && hint+="context7으로 관련 문서를 조회하세요. "
296
- local search_tools=""
297
- has_server "brave-search" && search_tools+="brave-search, "
298
- has_server "tavily" && search_tools+="tavily, "
299
- has_server "exa" && search_tools+="exa, "
300
- [[ -n "$search_tools" ]] && hint+="웹 검색 우선순위: ${search_tools%, }. 402 에러 시 즉시 다음 도구로 전환. "
301
- has_server "playwright" && hint+="모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
302
- hint+="검색 깊이를 제한하고 결과를 빠르게 요약하세요."
303
- ;;
304
- review)
305
- has_server "sequential-thinking" && hint="sequential-thinking으로 체계적으로 분석하세요."
306
- ;;
307
- docs)
308
- has_server "context7" && hint+="context7으로 공식 문서를 참조하세요. "
309
- has_server "brave-search" && hint+="추가 검색은 brave-search를 사용하세요. "
310
- hint+="검색 결과의 출처 URL을 함께 제시하세요."
311
- ;;
312
- minimal|none) ;;
313
- esac
314
- echo "$hint"
315
- }
316
-
317
- # ── Gemini MCP 서버 선택적 로드 ──
318
- get_gemini_mcp_filter() {
319
- local profile="$1"
320
- case "$profile" in
321
- implement) echo "--allowed-mcp-server-names context7,brave-search" ;;
322
- analyze) echo "--allowed-mcp-server-names context7,brave-search,exa" ;;
323
- review) echo "--allowed-mcp-server-names sequential-thinking" ;;
324
- docs) echo "--allowed-mcp-server-names context7,brave-search" ;;
325
- *) echo "" ;;
326
- esac
327
- }
328
-
329
- # ── 메인 실행 ──
330
- main() {
331
- # 종료 per-process 에이전트 파일 자동 삭제
332
- trap 'deregister_agent' EXIT
333
-
334
- route_agent "$AGENT_TYPE"
335
- apply_cli_mode
336
-
337
- # CLI 경로 해석
338
- case "$CLI_CMD" in
339
- codex) CLI_CMD="$CODEX_BIN" ;;
340
- gemini) CLI_CMD="$GEMINI_BIN" ;;
341
- esac
342
-
343
- # 타임아웃 결정
344
- if [[ -n "$USER_TIMEOUT" ]]; then
345
- TIMEOUT_SEC="$USER_TIMEOUT"
346
- else
347
- TIMEOUT_SEC="$DEFAULT_TIMEOUT"
348
- fi
349
-
350
- # 컨텍스트 파일 → 프롬프트에 주입
351
- if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
352
- local ctx_content
353
- ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
354
- PROMPT="${PROMPT}
355
-
356
- <prior_context>
357
- ${ctx_content}
358
- </prior_context>"
359
- fi
360
-
361
- # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
362
- if [[ "$CLI_TYPE" == "claude-native" ]]; then
363
- local model="sonnet"
364
- case "$AGENT_TYPE" in
365
- explore) model="haiku" ;;
366
- esac
367
- echo "ROUTE_TYPE=claude-native"
368
- echo "AGENT=$AGENT_TYPE"
369
- echo "MODEL=$model"
370
- echo "RUN_MODE=$RUN_MODE"
371
- echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
372
- echo "TIMEOUT=$TIMEOUT_SEC"
373
- echo "MCP_PROFILE=$MCP_PROFILE"
374
- [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
375
- echo "PROMPT=$PROMPT"
376
- echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
377
- exit 0
378
- fi
379
-
380
- # MCP 힌트 주입
381
- local mcp_hint
382
- mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
383
- local FULL_PROMPT="$PROMPT"
384
- [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
385
-
386
- # 메타정보 (stderr)
387
- echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
388
- echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE hub=$HUB_ENABLED" >&2
389
-
390
- # Per-process 에이전트 등록
391
- register_agent
392
-
393
- # Hub 브릿지: 에이전트 등록 (프로세스 수명 기반 lease)
394
- local hub_agent_id="${AGENT_TYPE}-$$"
395
- local hub_topics="${AGENT_TYPE},task.result"
396
- hub_bridge register \
397
- --agent "$hub_agent_id" \
398
- --cli "$CLI_TYPE" \
399
- --timeout "$TIMEOUT_SEC" \
400
- --topics "$hub_topics" \
401
- --capabilities "code,${AGENT_TYPE}"
402
-
403
- # Hub 브릿지: 선행 컨텍스트 폴링 (DAG 의존 태스크용)
404
- if [[ "$HUB_ENABLED" == "true" && -z "$CONTEXT_FILE" ]]; then
405
- local hub_ctx_file="${TFX_TMP}/tfx-hub-ctx-${hub_agent_id}.md"
406
- hub_bridge context --agent "$hub_agent_id" --topics "$hub_topics" --out "$hub_ctx_file"
407
- if [[ -s "$hub_ctx_file" ]]; then
408
- CONTEXT_FILE="$hub_ctx_file"
409
- echo "[tfx-route] hub: 선행 컨텍스트 수신 ($(wc -c < "$hub_ctx_file") bytes)" >&2
410
- fi
411
- fi
412
-
413
- # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
414
- local exit_code=0
415
- local start_time
416
- start_time=$(date +%s)
417
-
418
- if [[ "$CLI_TYPE" == "codex" ]]; then
419
- # Codex: stdout/stderr 모두 파일로 캡처 (post.mjs가 읽음)
420
- timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code=$?
421
-
422
- elif [[ "$CLI_TYPE" == "gemini" ]]; then
423
- # Gemini: MCP 프로필별 서버 필터
424
- local gemini_mcp_filter
425
- gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
426
- local gemini_args="$CLI_ARGS"
427
- if [[ -n "$gemini_mcp_filter" ]]; then
428
- gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
429
- echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
430
- fi
431
-
432
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
433
- local pid=$!
434
-
435
- # 지수 백오프 health check (v1.x: 30×1s → v2.0: 5×exp, 총 19초)
436
- local health_ok=true
437
- local intervals=(1 2 3 5 8)
438
- for wait_sec in "${intervals[@]}"; do
439
- sleep "$wait_sec"
440
- # 출력 있으면 정상 → 조기 탈출
441
- if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
442
- break
443
- fi
444
- # 프로세스 사망 + 출력 없음 crash
445
- if ! kill -0 "$pid" 2>/dev/null; then
446
- health_ok=false
447
- echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
448
- break
449
- fi
450
- done
451
-
452
- if [[ "$health_ok" == "false" ]]; then
453
- wait "$pid" 2>/dev/null
454
- echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
455
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
456
- pid=$!
457
- wait "$pid"
458
- exit_code=$?
459
- else
460
- wait "$pid"
461
- exit_code=$?
462
- fi
463
- fi
464
-
465
- local end_time
466
- end_time=$(date +%s)
467
- local elapsed=$((end_time - start_time))
468
-
469
- # Hub 브릿지: 결과 발행 + 에이전트 해제
470
- hub_bridge result \
471
- --agent "$hub_agent_id" \
472
- --file "$STDOUT_LOG" \
473
- --topic "task.result" \
474
- --exit-code "$exit_code"
475
- hub_bridge deregister --agent "$hub_agent_id"
476
-
477
- # ── 후처리: 단일 node 프로세스로 위임 ──
478
- # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
479
- local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
480
- if [[ -f "$post_script" ]]; then
481
- node "$post_script" \
482
- --agent "$AGENT_TYPE" \
483
- --cli "$CLI_TYPE" \
484
- --cli-cmd "$CLI_CMD" \
485
- --effort "$CLI_EFFORT" \
486
- --run-mode "$RUN_MODE" \
487
- --opus "$OPUS_OVERSIGHT" \
488
- --exit-code "$exit_code" \
489
- --elapsed "$elapsed" \
490
- --timeout "$TIMEOUT_SEC" \
491
- --mcp-profile "$MCP_PROFILE" \
492
- --stderr-log "$STDERR_LOG" \
493
- --stdout-log "$STDOUT_LOG" \
494
- --max-bytes "$MAX_STDOUT_BYTES"
495
- else
496
- # post.mjs 없으면 기본 출력 (fallback)
497
- echo "=== TFX-ROUTE RESULT ==="
498
- echo "agent: $AGENT_TYPE"
499
- echo "cli: $CLI_TYPE"
500
- echo "exit_code: $exit_code"
501
- echo "elapsed: ${elapsed}s"
502
- echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
503
- echo "=== OUTPUT ==="
504
- cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
505
- fi
506
- }
507
-
508
- main
1
+ #!/usr/bin/env bash
2
+ # tfx-route.sh v2.2 — 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.2"
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
+ NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
32
+ CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
33
+ GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
34
+ CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
35
+ GEMINI_BIN_ARGS_JSON="${GEMINI_BIN_ARGS_JSON:-[]}"
36
+ CLAUDE_BIN_ARGS_JSON="${CLAUDE_BIN_ARGS_JSON:-[]}"
37
+
38
+ # ── 상수 ──
39
+ MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
40
+ TIMESTAMP=$(date +%s)
41
+ RUN_ID="${TIMESTAMP}-$$-${RANDOM}"
42
+ STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${RUN_ID}-stderr.log"
43
+ STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${RUN_ID}-stdout.log"
44
+ TFX_TMP="${TMPDIR:-/tmp}"
45
+
46
+ # ── 환경변수 ──
47
+ TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
48
+ TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
49
+ TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
50
+ TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
51
+ TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}"
52
+
53
+ # fallback 원래 에이전트 정보 보존
54
+ ORIGINAL_AGENT=""
55
+ ORIGINAL_CLI_ARGS=""
56
+
57
+ # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
58
+ register_agent() {
59
+ local agent_file="${TFX_TMP}/tfx-agent-$$.json"
60
+ echo "{\"pid\":$$,\"cli\":\"$CLI_TYPE\",\"agent\":\"$AGENT_TYPE\",\"started\":$(date +%s)}" \
61
+ > "$agent_file" 2>/dev/null || true
62
+ }
63
+
64
+ deregister_agent() {
65
+ rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
66
+ }
67
+
68
+ # ── 팀 Hub Bridge 통신 ──
69
+ # JSON 문자열 이스케이프 (큰따옴표, 백슬래시, 개행, 탭, CR)
70
+ json_escape() {
71
+ local s="${1:-}"
72
+ # node로 완전한 JSON 이스케이프 (NUL, 멀티바이트 UTF-8, 제어문자 안전)
73
+ if command -v node &>/dev/null; then
74
+ node -e 'process.stdout.write(JSON.stringify(process.argv[1]).slice(1,-1))' -- "$s"
75
+ return
76
+ fi
77
+ # node 미설치 fallback: 기본 Bash 치환
78
+ s="${s//\\/\\\\}"
79
+ s="${s//\"/\\\"}"
80
+ s="${s//$'\n'/\\n}"
81
+ s="${s//$'\t'/\\t}"
82
+ s="${s//$'\r'/\\r}"
83
+ printf '%s' "$s"
84
+ }
85
+
86
+ team_claim_task() {
87
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
88
+ local http_code safe_team_name safe_task_id safe_agent_name
89
+ safe_team_name=$(json_escape "$TFX_TEAM_NAME")
90
+ safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
91
+ safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
92
+
93
+ http_code=$(curl -sf -o /dev/null -w "%{http_code}" -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
94
+ -H "Content-Type: application/json" \
95
+ -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"claim\":true,\"owner\":\"${safe_agent_name}\",\"status\":\"in_progress\"}" \
96
+ 2>/dev/null) || http_code="000"
97
+
98
+ case "$http_code" in
99
+ 200) ;; # 성공
100
+ 409)
101
+ echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨. 실행 중단." >&2
102
+ exit 0 ;;
103
+ 000)
104
+ echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2 ;;
105
+ *)
106
+ echo "[tfx-route] 경고: Hub claim 응답 HTTP ${http_code}. claim 없이 계속 실행." >&2 ;;
107
+ esac
108
+ }
109
+
110
+ team_complete_task() {
111
+ local result="${1:-success}" # success/failed/timeout
112
+ local result_summary="${2:-작업 완료}"
113
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
114
+
115
+ local safe_team_name safe_task_id safe_agent_name safe_result safe_summary safe_lead_name
116
+ safe_team_name=$(json_escape "$TFX_TEAM_NAME")
117
+ safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
118
+ safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
119
+ safe_result=$(json_escape "$result")
120
+ safe_summary=$(json_escape "$(echo "$result_summary" | head -c 4096)")
121
+ safe_lead_name=$(json_escape "$TFX_TEAM_LEAD_NAME")
122
+
123
+ # task 상태: 항상 "completed" (Claude Code API는 "failed" 미지원)
124
+ # 실제 결과는 metadata.result로 전달
125
+ curl -sf -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
126
+ -H "Content-Type: application/json" \
127
+ -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"status\":\"completed\",\"owner\":\"${safe_agent_name}\",\"metadata_patch\":{\"result\":\"${safe_result}\",\"summary\":\"${safe_summary}\"}}" \
128
+ >/dev/null 2>&1 || true
129
+
130
+ # 리드에게 메시지 전송
131
+ curl -sf -X POST "${TFX_HUB_URL}/bridge/team/send-message" \
132
+ -H "Content-Type: application/json" \
133
+ -d "{\"team_name\":\"${safe_team_name}\",\"from\":\"${safe_agent_name}\",\"to\":\"${safe_lead_name}\",\"text\":\"${safe_summary}\",\"summary\":\"task ${safe_task_id} ${safe_result}\"}" \
134
+ >/dev/null 2>&1 || true
135
+
136
+ # Hub result 발행 (poll_messages 채널 활성화)
137
+ curl -sf -X POST "${TFX_HUB_URL}/bridge/result" \
138
+ -H "Content-Type: application/json" \
139
+ -d "{\"agent_id\":\"${safe_agent_name}\",\"topic\":\"task.result\",\"payload\":{\"task_id\":\"${safe_task_id}\",\"result\":\"${safe_result}\"},\"trace_id\":\"${safe_team_name}\"}" \
140
+ >/dev/null 2>&1 || true
141
+ }
142
+
143
+ # ── 라우팅 테이블 ──
144
+ # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
145
+ route_agent() {
146
+ local agent="$1"
147
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
148
+
149
+ case "$agent" in
150
+ # ─── 구현 레인 ───
151
+ executor)
152
+ CLI_TYPE="codex"; CLI_CMD="codex"
153
+ CLI_ARGS="exec ${codex_base}"
154
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
155
+ build-fixer)
156
+ CLI_TYPE="codex"; CLI_CMD="codex"
157
+ CLI_ARGS="exec --profile fast ${codex_base}"
158
+ CLI_EFFORT="fast"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
159
+ debugger)
160
+ CLI_TYPE="codex"; CLI_CMD="codex"
161
+ CLI_ARGS="exec ${codex_base}"
162
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
163
+ deep-executor)
164
+ CLI_TYPE="codex"; CLI_CMD="codex"
165
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
166
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
167
+
168
+ # ─── 설계/분석 레인 ───
169
+ architect)
170
+ CLI_TYPE="codex"; CLI_CMD="codex"
171
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
172
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
173
+ planner)
174
+ CLI_TYPE="codex"; CLI_CMD="codex"
175
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
176
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
177
+ critic)
178
+ CLI_TYPE="codex"; CLI_CMD="codex"
179
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
180
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
181
+ analyst)
182
+ CLI_TYPE="codex"; CLI_CMD="codex"
183
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
184
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
185
+
186
+ # ─── 리뷰 레인 ───
187
+ code-reviewer)
188
+ CLI_TYPE="codex"; CLI_CMD="codex"
189
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
190
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
191
+ security-reviewer)
192
+ CLI_TYPE="codex"; CLI_CMD="codex"
193
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
194
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
195
+ quality-reviewer)
196
+ CLI_TYPE="codex"; CLI_CMD="codex"
197
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
198
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
199
+
200
+ # ─── 리서치 레인 ───
201
+ scientist)
202
+ CLI_TYPE="codex"; CLI_CMD="codex"
203
+ CLI_ARGS="exec ${codex_base}"
204
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
205
+ scientist-deep)
206
+ CLI_TYPE="codex"; CLI_CMD="codex"
207
+ CLI_ARGS="exec --profile thorough ${codex_base}"
208
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
209
+ document-specialist)
210
+ CLI_TYPE="codex"; CLI_CMD="codex"
211
+ CLI_ARGS="exec ${codex_base}"
212
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
213
+
214
+ # ─── UI/문서 레인 ───
215
+ designer)
216
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
217
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
218
+ CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
219
+ writer)
220
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
221
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
222
+ CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
223
+
224
+ # ─── Claude 네이티브 ───
225
+ explore)
226
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
227
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
228
+ verifier)
229
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
230
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
231
+ test-engineer)
232
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
233
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
234
+ qa-tester)
235
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
236
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
237
+
238
+ # ─── 경량 ───
239
+ spark)
240
+ CLI_TYPE="codex"; CLI_CMD="codex"
241
+ CLI_ARGS="exec --profile spark_fast ${codex_base}"
242
+ CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
243
+ *)
244
+ echo "ERROR: 없는 에이전트 타입: $agent" >&2
245
+ echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
246
+ echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
247
+ echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
248
+ exit 1 ;;
249
+ esac
250
+ }
251
+
252
+ # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
253
+ TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
254
+ TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
255
+ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
256
+ case "$TFX_NO_CLAUDE_NATIVE" in
257
+ 0|1) ;;
258
+ *)
259
+ echo "ERROR: TFX_NO_CLAUDE_NATIVE 값은 0 또는 1이어야 합니다. (현재: $TFX_NO_CLAUDE_NATIVE)" >&2
260
+ exit 1
261
+ ;;
262
+ esac
263
+ case "$TFX_CODEX_TRANSPORT" in
264
+ auto|mcp|exec) ;;
265
+ *)
266
+ echo "ERROR: TFX_CODEX_TRANSPORT 값은 auto, mcp, exec 중 하나여야 합니다. (현재: $TFX_CODEX_TRANSPORT)" >&2
267
+ exit 1
268
+ ;;
269
+ esac
270
+ CODEX_MCP_TRANSPORT_EXIT_CODE=70
271
+
272
+ apply_cli_mode() {
273
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
274
+
275
+ case "$TFX_CLI_MODE" in
276
+ codex)
277
+ if [[ "$CLI_TYPE" == "gemini" ]]; then
278
+ CLI_TYPE="codex"; CLI_CMD="codex"
279
+ case "$AGENT_TYPE" in
280
+ designer)
281
+ CLI_ARGS="exec ${codex_base}"; CLI_EFFORT="high"; DEFAULT_TIMEOUT=600 ;;
282
+ writer)
283
+ CLI_ARGS="exec --profile spark_fast ${codex_base}"; CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180 ;;
284
+ esac
285
+ echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
286
+ fi ;;
287
+ gemini)
288
+ if [[ "$CLI_TYPE" == "codex" ]]; then
289
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
290
+ case "$AGENT_TYPE" in
291
+ executor|debugger|deep-executor|architect|planner|critic|analyst|\
292
+ code-reviewer|security-reviewer|quality-reviewer|scientist-deep)
293
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"; CLI_EFFORT="pro" ;;
294
+ build-fixer|spark)
295
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash"; DEFAULT_TIMEOUT=180 ;;
296
+ *)
297
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash" ;;
298
+ esac
299
+ echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
300
+ fi ;;
301
+ auto)
302
+ if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
303
+ if command -v "$GEMINI_BIN" &>/dev/null; then
304
+ TFX_CLI_MODE="gemini"; apply_cli_mode; return
305
+ else
306
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
307
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
308
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE claude-native fallback" >&2
309
+ fi
310
+ elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
311
+ if command -v "$CODEX_BIN" &>/dev/null; then
312
+ TFX_CLI_MODE="codex"; apply_cli_mode; return
313
+ else
314
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
315
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
316
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
317
+ fi
318
+ fi ;;
319
+ esac
320
+ }
321
+
322
+ # ── Claude 네이티브 제거 (Codex 리드 환경에서 선택적 활성화) ──
323
+ apply_no_claude_native_mode() {
324
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
325
+
326
+ [[ "$TFX_NO_CLAUDE_NATIVE" != "1" ]] && return
327
+ [[ "$TFX_CLI_MODE" == "gemini" ]] && return
328
+ [[ "$CLI_TYPE" != "claude-native" ]] && return
329
+
330
+ if ! command -v "$CODEX_BIN" &>/dev/null; then
331
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1 이지만 codex를 찾지 못해 claude-native 유지" >&2
332
+ return
333
+ fi
334
+
335
+ ORIGINAL_AGENT="${AGENT_TYPE}"
336
+ CLI_TYPE="codex"; CLI_CMD="codex"
337
+
338
+ case "$AGENT_TYPE" in
339
+ explore)
340
+ CLI_ARGS="exec --profile fast ${codex_base}"
341
+ CLI_EFFORT="fast"
342
+ DEFAULT_TIMEOUT=600
343
+ RUN_MODE="fg"
344
+ OPUS_OVERSIGHT="false"
345
+ ;;
346
+ verifier)
347
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
348
+ CLI_EFFORT="thorough"
349
+ DEFAULT_TIMEOUT=1200
350
+ RUN_MODE="fg"
351
+ OPUS_OVERSIGHT="false"
352
+ ;;
353
+ test-engineer)
354
+ CLI_ARGS="exec ${codex_base}"
355
+ CLI_EFFORT="high"
356
+ DEFAULT_TIMEOUT=1200
357
+ RUN_MODE="bg"
358
+ OPUS_OVERSIGHT="false"
359
+ ;;
360
+ qa-tester)
361
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
362
+ CLI_EFFORT="thorough"
363
+ DEFAULT_TIMEOUT=1200
364
+ RUN_MODE="bg"
365
+ OPUS_OVERSIGHT="false"
366
+ ;;
367
+ *)
368
+ # claude-native 타입 중 위에 없는 경우는 보수적으로 유지
369
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
370
+ return
371
+ ;;
372
+ esac
373
+
374
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
375
+ }
376
+
377
+ # ── MCP 인벤토리 캐시 ──
378
+ MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
379
+
380
+ get_cached_servers() {
381
+ local cli_type="$1"
382
+ if [[ -f "$MCP_CACHE" ]]; then
383
+ 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
384
+ fi
385
+ }
386
+
387
+ # ── MCP 프로필 프롬프트 힌트 (통합: 캐시 유무 단일 코드경로) ──
388
+ get_mcp_hint() {
389
+ local profile="$1"
390
+ local agent="$2"
391
+
392
+ # auto → 구체 프로필 해석
393
+ if [[ "$profile" == "auto" ]]; then
394
+ case "$agent" in
395
+ executor|build-fixer|debugger|deep-executor) profile="implement" ;;
396
+ architect|planner|critic|analyst) profile="analyze" ;;
397
+ code-reviewer|security-reviewer|quality-reviewer) profile="review" ;;
398
+ scientist|document-specialist) profile="analyze" ;;
399
+ designer|writer) profile="docs" ;;
400
+ *) profile="minimal" ;;
401
+ esac
402
+ fi
403
+
404
+ # 서버 목록: 캐시 있으면 실제, 없으면 전부 가용 가정 (기존 비캐시 동작과 동일)
405
+ local servers
406
+ servers=$(get_cached_servers "$CLI_TYPE")
407
+ [[ -z "$servers" ]] && servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
408
+
409
+ has_server() { echo ",$servers," | grep -q ",$1,"; }
410
+
411
+ local hint=""
412
+ case "$profile" in
413
+ implement)
414
+ has_server "context7" && hint+="context7으로 라이브러리 문서를 조회하세요. "
415
+ if has_server "brave-search"; then hint+="웹 검색은 brave-search를 사용하세요. "
416
+ elif has_server "exa"; then hint+="웹 검색은 exa를 사용하세요. "
417
+ elif has_server "tavily"; then hint+="웹 검색은 tavily를 사용하세요. "
418
+ fi
419
+ hint+="검색 도구 실패 재시도하지 말고 다음 도구로 전환하세요."
420
+ ;;
421
+ analyze)
422
+ has_server "context7" && hint+="context7으로 관련 문서를 조회하세요. "
423
+ local search_tools=""
424
+ has_server "brave-search" && search_tools+="brave-search, "
425
+ has_server "tavily" && search_tools+="tavily, "
426
+ has_server "exa" && search_tools+="exa, "
427
+ [[ -n "$search_tools" ]] && hint+="웹 검색 우선순위: ${search_tools%, }. 402 에러 시 즉시 다음 도구로 전환. "
428
+ has_server "playwright" && hint+="모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
429
+ hint+="검색 깊이를 제한하고 결과를 빠르게 요약하세요."
430
+ ;;
431
+ review)
432
+ has_server "sequential-thinking" && hint="sequential-thinking으로 체계적으로 분석하세요."
433
+ ;;
434
+ docs)
435
+ has_server "context7" && hint+="context7으로 공식 문서를 참조하세요. "
436
+ has_server "brave-search" && hint+="추가 검색은 brave-search를 사용하세요. "
437
+ hint+="검색 결과의 출처 URL을 함께 제시하세요."
438
+ ;;
439
+ minimal|none) ;;
440
+ esac
441
+ echo "$hint"
442
+ }
443
+
444
+ # ── Gemini MCP 서버 선택적 로드 ──
445
+ get_gemini_mcp_servers() {
446
+ local profile="$1"
447
+ case "$profile" in
448
+ implement) echo "context7 brave-search" ;;
449
+ analyze) echo "context7 brave-search exa" ;;
450
+ review) echo "sequential-thinking" ;;
451
+ docs) echo "context7 brave-search" ;;
452
+ *) echo "" ;;
453
+ esac
454
+ }
455
+
456
+ get_gemini_mcp_filter() {
457
+ local servers
458
+ servers=$(get_gemini_mcp_servers "$1")
459
+ [[ -z "$servers" ]] && return 0
460
+ echo "--allowed-mcp-server-names ${servers// /,}"
461
+ }
462
+
463
+ get_claude_model() {
464
+ case "$AGENT_TYPE" in
465
+ explore) echo "haiku" ;;
466
+ *) echo "sonnet" ;;
467
+ esac
468
+ }
469
+
470
+ emit_claude_native_metadata() {
471
+ local model
472
+ model=$(get_claude_model)
473
+ echo "ROUTE_TYPE=claude-native"
474
+ echo "AGENT=$AGENT_TYPE"
475
+ echo "MODEL=$model"
476
+ echo "RUN_MODE=$RUN_MODE"
477
+ echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
478
+ echo "TIMEOUT=$TIMEOUT_SEC"
479
+ echo "MCP_PROFILE=$MCP_PROFILE"
480
+ [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
481
+ echo "PROMPT=$PROMPT"
482
+ echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
483
+ }
484
+
485
+ resolve_worker_runner_script() {
486
+ if [[ -n "${TFX_ROUTE_WORKER_RUNNER:-}" && -f "$TFX_ROUTE_WORKER_RUNNER" ]]; then
487
+ printf '%s\n' "$TFX_ROUTE_WORKER_RUNNER"
488
+ return 0
489
+ fi
490
+
491
+ local script_dir
492
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
493
+ local candidate="$script_dir/tfx-route-worker.mjs"
494
+ [[ -f "$candidate" ]] || return 1
495
+ printf '%s\n' "$candidate"
496
+ }
497
+
498
+ run_stream_worker() {
499
+ local worker_type="$1"
500
+ local prompt="$2"
501
+ local use_tee_flag="$3"
502
+ shift 3
503
+
504
+ local runner_script
505
+ if ! runner_script=$(resolve_worker_runner_script); then
506
+ echo "[tfx-route] 경고: stream worker runner를 찾지 못했습니다." >&2
507
+ return 127
508
+ fi
509
+
510
+ if ! command -v "$NODE_BIN" &>/dev/null; then
511
+ echo "[tfx-route] 경고: node를 찾지 못해 stream worker를 실행할 수 없습니다." >&2
512
+ return 127
513
+ fi
514
+
515
+ local -a worker_cmd=(
516
+ "$NODE_BIN"
517
+ "$runner_script"
518
+ "--type" "$worker_type"
519
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
520
+ "--cwd" "$PWD"
521
+ "$@"
522
+ )
523
+
524
+ if [[ "$use_tee_flag" == "true" ]]; then
525
+ printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
526
+ else
527
+ printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG"
528
+ fi
529
+ }
530
+
531
+ run_legacy_gemini() {
532
+ local prompt="$1"
533
+ local use_tee_flag="$2"
534
+ local gemini_mcp_filter
535
+ gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
536
+ local gemini_args="$CLI_ARGS"
537
+
538
+ if [[ -n "$gemini_mcp_filter" ]]; then
539
+ gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
540
+ echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
541
+ fi
542
+
543
+ if [[ "$use_tee_flag" == "true" ]]; then
544
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
545
+ else
546
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
547
+ fi
548
+ local pid=$!
549
+
550
+ local health_ok=true
551
+ local intervals=(1 2 3 5 8)
552
+ for wait_sec in "${intervals[@]}"; do
553
+ sleep "$wait_sec"
554
+ if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
555
+ break
556
+ fi
557
+ if ! kill -0 "$pid" 2>/dev/null; then
558
+ health_ok=false
559
+ echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
560
+ break
561
+ fi
562
+ done
563
+
564
+ local exit_code_local=0
565
+ if [[ "$health_ok" == "false" ]]; then
566
+ wait "$pid" 2>/dev/null
567
+ echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
568
+ if [[ "$use_tee_flag" == "true" ]]; then
569
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
570
+ else
571
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
572
+ fi
573
+ pid=$!
574
+ fi
575
+
576
+ wait "$pid" || exit_code_local=$?
577
+ return "$exit_code_local"
578
+ }
579
+
580
+ resolve_codex_mcp_script() {
581
+ if [[ -n "${TFX_CODEX_MCP_SCRIPT:-}" && -f "$TFX_CODEX_MCP_SCRIPT" ]]; then
582
+ printf '%s\n' "$TFX_CODEX_MCP_SCRIPT"
583
+ return 0
584
+ fi
585
+
586
+ local script_dir
587
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
588
+ local candidates=(
589
+ "$script_dir/hub/workers/codex-mcp.mjs"
590
+ "$script_dir/../hub/workers/codex-mcp.mjs"
591
+ )
592
+
593
+ local candidate
594
+ for candidate in "${candidates[@]}"; do
595
+ if [[ -f "$candidate" ]]; then
596
+ printf '%s\n' "$candidate"
597
+ return 0
598
+ fi
599
+ done
600
+
601
+ return 1
602
+ }
603
+
604
+ run_codex_exec() {
605
+ local prompt="$1"
606
+ local use_tee_flag="$2"
607
+ local exit_code_local=0
608
+
609
+ if [[ "$use_tee_flag" == "true" ]]; then
610
+ timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
611
+ else
612
+ timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
613
+ fi
614
+
615
+ if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
616
+ # stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
617
+ # 1차: "codex" 마커 기반 (Windows \r 제거 후 매칭)
618
+ sed 's/\r$//' "$STDERR_LOG" \
619
+ | awk '/^codex$/{found=NR;content=""} found && NR>found{content=content RS $0} END{if(content) print substr(content,2)}' \
620
+ > "$STDOUT_LOG"
621
+
622
+ # 2차: 마커 없을 때 node fallback (MCP/헤더/sandbox 로그 제외, 응답 부분만 추출)
623
+ if [[ ! -s "$STDOUT_LOG" ]]; then
624
+ node -e '
625
+ const fs=require("fs"),lines=fs.readFileSync(process.argv[1],"utf-8").split(/\r?\n/);
626
+ const skip=/^(mcp[: ]|OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|tokens used|EXIT:|exec$|"[A-Z]:|succeeded in |\s*$)/;
627
+ const out=lines.filter(l=>!skip.test(l));
628
+ if(out.length) fs.writeFileSync(process.argv[2],out.join("\n"));
629
+ ' -- "$STDERR_LOG" "$STDOUT_LOG" 2>/dev/null || true
630
+ fi
631
+
632
+ if [[ -s "$STDOUT_LOG" ]]; then
633
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr에서 응답 복구 ($(wc -c < "$STDOUT_LOG") bytes)" >&2
634
+ else
635
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패" >&2
636
+ fi
637
+ fi
638
+
639
+ return "$exit_code_local"
640
+ }
641
+
642
+ run_codex_mcp() {
643
+ local prompt="$1"
644
+ local use_tee_flag="$2"
645
+ local mcp_script node_bin
646
+ local exit_code_local=0
647
+
648
+ if ! mcp_script=$(resolve_codex_mcp_script); then
649
+ echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
650
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
651
+ fi
652
+
653
+ node_bin="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
654
+ if ! command -v "$node_bin" &>/dev/null; then
655
+ echo "[tfx-route] 경고: node를 찾지 못해 Codex MCP 경로를 사용할 수 없습니다." >&2
656
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
657
+ fi
658
+
659
+ local -a mcp_args=(
660
+ "$mcp_script"
661
+ "--prompt" "$prompt"
662
+ "--cwd" "$PWD"
663
+ "--profile" "$CLI_EFFORT"
664
+ "--approval-policy" "never"
665
+ "--sandbox" "danger-full-access"
666
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
667
+ "--codex-command" "$CODEX_BIN"
668
+ )
669
+
670
+ case "$AGENT_TYPE" in
671
+ code-reviewer)
672
+ mcp_args+=(
673
+ "--developer-instructions"
674
+ "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라."
675
+ )
676
+ ;;
677
+ security-reviewer)
678
+ mcp_args+=(
679
+ "--developer-instructions"
680
+ "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라."
681
+ )
682
+ ;;
683
+ quality-reviewer)
684
+ mcp_args+=(
685
+ "--developer-instructions"
686
+ "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라."
687
+ )
688
+ ;;
689
+ esac
690
+
691
+ if [[ "$use_tee_flag" == "true" ]]; then
692
+ timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
693
+ else
694
+ timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
695
+ fi
696
+
697
+ # 모듈 로드 실패(의존성 누락) → MCP transport exit code로 변환하여 fallback 트리거
698
+ if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
699
+ echo "[tfx-route] Codex MCP 모듈 로드 실패 — fallback 가능 exit code로 변환" >&2
700
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
701
+ fi
702
+
703
+ return "$exit_code_local"
704
+ }
705
+
706
+ # ── 메인 실행 ──
707
+ main() {
708
+ # 종료 시 per-process 에이전트 파일 자동 삭제
709
+ trap 'deregister_agent' EXIT
710
+
711
+ route_agent "$AGENT_TYPE"
712
+ apply_cli_mode
713
+ apply_no_claude_native_mode
714
+
715
+ # CLI 경로 해석
716
+ case "$CLI_CMD" in
717
+ codex) CLI_CMD="$CODEX_BIN" ;;
718
+ gemini) CLI_CMD="$GEMINI_BIN" ;;
719
+ claude) CLI_CMD="$CLAUDE_BIN" ;;
720
+ esac
721
+
722
+ # 타임아웃 결정 (에이전트별 최소값 보장)
723
+ local MIN_TIMEOUT
724
+ case "$AGENT_TYPE" in
725
+ deep-executor|architect|planner|critic|analyst) MIN_TIMEOUT=900 ;;
726
+ document-specialist|scientist|scientist-deep) MIN_TIMEOUT=900 ;;
727
+ code-reviewer|security-reviewer|quality-reviewer) MIN_TIMEOUT=600 ;;
728
+ executor|debugger) MIN_TIMEOUT=300 ;;
729
+ *) MIN_TIMEOUT=120 ;;
730
+ esac
731
+
732
+ if [[ -n "$USER_TIMEOUT" ]]; then
733
+ if ! [[ "$USER_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
734
+ echo "[tfx-route] 경고: 유효하지 않은 타임아웃 값 ($USER_TIMEOUT), 기본값 사용" >&2
735
+ USER_TIMEOUT=""
736
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
737
+ elif [[ "$USER_TIMEOUT" -lt "$MIN_TIMEOUT" ]]; then
738
+ echo "[tfx-route] 경고: 타임아웃 ${USER_TIMEOUT}s < 최소 ${MIN_TIMEOUT}s ($AGENT_TYPE), 최소값 적용" >&2
739
+ TIMEOUT_SEC="$MIN_TIMEOUT"
740
+ else
741
+ TIMEOUT_SEC="$USER_TIMEOUT"
742
+ fi
743
+ else
744
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
745
+ fi
746
+
747
+ # 컨텍스트 파일 → 프롬프트에 주입
748
+ if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
749
+ local ctx_content
750
+ ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
751
+ PROMPT="${PROMPT}
752
+
753
+ <prior_context>
754
+ ${ctx_content}
755
+ </prior_context>"
756
+ fi
757
+
758
+ # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
759
+ if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
760
+ if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
761
+ CLI_TYPE="claude"
762
+ CLI_CMD="$CLAUDE_BIN"
763
+ echo "[tfx-route] non-tty 팀 환경: claude-native -> claude stream wrapper 전환" >&2
764
+ else
765
+ echo "[tfx-route] claude stream wrapper 미사용: native metadata 유지" >&2
766
+ fi
767
+ fi
768
+
769
+ # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
770
+ if [[ "$CLI_TYPE" == "claude-native" ]]; then
771
+ emit_claude_native_metadata
772
+ exit 0
773
+ fi
774
+
775
+ # MCP 힌트 주입
776
+ local mcp_hint
777
+ mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
778
+ local FULL_PROMPT="$PROMPT"
779
+ [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
780
+ local codex_transport_effective="n/a"
781
+
782
+ # 메타정보 (stderr)
783
+ echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
784
+ echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE" >&2
785
+ if [[ "$CLI_TYPE" == "codex" ]]; then
786
+ echo "[tfx-route] codex_transport_request=$TFX_CODEX_TRANSPORT" >&2
787
+ fi
788
+ [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
789
+
790
+ # Per-process 에이전트 등록
791
+ register_agent
792
+
793
+ # 팀 모드: task claim
794
+ team_claim_task
795
+
796
+ # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
797
+ local exit_code=0
798
+ local start_time
799
+ start_time=$(date +%s)
800
+
801
+ # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
802
+ # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
803
+ # 실시간 모니터링은 Shift+Down으로 워커 pane 전환 권장
804
+ local use_tee=false
805
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
806
+ if [[ -t 1 ]] || [[ -n "${TMUX:-}" ]]; then
807
+ use_tee=true
808
+ fi
809
+ fi
810
+
811
+ if [[ "$CLI_TYPE" == "codex" ]]; then
812
+ codex_transport_effective="exec"
813
+ if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
814
+ run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
815
+ if [[ "$exit_code" -eq 0 ]]; then
816
+ codex_transport_effective="mcp"
817
+ elif [[ "$exit_code" -eq "$CODEX_MCP_TRANSPORT_EXIT_CODE" && "$TFX_CODEX_TRANSPORT" == "auto" ]]; then
818
+ echo "[tfx-route] Codex MCP bootstrap 실패(exit=${exit_code}). legacy exec 경로로 fallback합니다." >&2
819
+ : > "$STDOUT_LOG"
820
+ : > "$STDERR_LOG"
821
+ exit_code=0
822
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
823
+ codex_transport_effective="exec-fallback"
824
+ else
825
+ codex_transport_effective="mcp"
826
+ fi
827
+ else
828
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
829
+ codex_transport_effective="exec"
830
+ fi
831
+ echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
832
+
833
+ elif [[ "$CLI_TYPE" == "gemini" ]]; then
834
+ local gemini_model
835
+ gemini_model=$(awk '{
836
+ for (i = 1; i <= NF; i++) {
837
+ if ($i == "-m" || $i == "--model") {
838
+ print $(i + 1)
839
+ exit
840
+ }
841
+ }
842
+ }' <<< "$CLI_ARGS")
843
+ local gemini_servers
844
+ gemini_servers=$(get_gemini_mcp_servers "$MCP_PROFILE")
845
+ local -a gemini_worker_args=(
846
+ "--command" "$CLI_CMD"
847
+ "--command-args-json" "$GEMINI_BIN_ARGS_JSON"
848
+ "--model" "$gemini_model"
849
+ "--approval-mode" "yolo"
850
+ )
851
+
852
+ if [[ -n "$gemini_servers" ]]; then
853
+ echo "[tfx-route] Gemini MCP 서버: ${gemini_servers}" >&2
854
+ local server_name
855
+ for server_name in $gemini_servers; do
856
+ gemini_worker_args+=("--allowed-mcp-server-name" "$server_name")
857
+ done
858
+ fi
859
+
860
+ run_stream_worker "gemini" "$FULL_PROMPT" "$use_tee" "${gemini_worker_args[@]}" || exit_code=$?
861
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
862
+ echo "[tfx-route] Gemini stream wrapper 실패(exit=${exit_code}). legacy CLI 경로로 fallback합니다." >&2
863
+ : > "$STDOUT_LOG"
864
+ : > "$STDERR_LOG"
865
+ exit_code=0
866
+ run_legacy_gemini "$FULL_PROMPT" "$use_tee" || exit_code=$?
867
+ fi
868
+
869
+ elif [[ "$CLI_TYPE" == "claude" ]]; then
870
+ local claude_model
871
+ claude_model=$(get_claude_model)
872
+ local -a claude_worker_args=(
873
+ "--command" "$CLI_CMD"
874
+ "--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
875
+ "--model" "$claude_model"
876
+ "--permission-mode" "bypassPermissions"
877
+ "--allow-dangerously-skip-permissions"
878
+ )
879
+
880
+ run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
881
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
882
+ echo "[tfx-route] Claude stream wrapper 실패(exit=${exit_code}). native metadata로 fallback합니다." >&2
883
+ cat > "$STDOUT_LOG" <<EOF
884
+ $(emit_claude_native_metadata)
885
+ EOF
886
+ : > "$STDERR_LOG"
887
+ exit_code=0
888
+ CLI_TYPE="claude-native"
889
+ fi
890
+ fi
891
+
892
+ local end_time
893
+ end_time=$(date +%s)
894
+ local elapsed=$((end_time - start_time))
895
+
896
+ # 팀 모드: task complete + 리드 보고
897
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
898
+ if [[ "$exit_code" -eq 0 ]]; then
899
+ local output_preview
900
+ output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
901
+ team_complete_task "success" "$output_preview"
902
+ elif [[ "$exit_code" -eq 124 ]]; then
903
+ team_complete_task "timeout" "타임아웃 (${TIMEOUT_SEC}초)"
904
+ else
905
+ local err_preview
906
+ err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
907
+ team_complete_task "failed" "exit_code=${exit_code}: ${err_preview}"
908
+ fi
909
+ fi
910
+
911
+ # ── 후처리: 단일 node 프로세스로 위임 ──
912
+ # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
913
+ local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
914
+ if [[ -f "$post_script" ]]; then
915
+ node "$post_script" \
916
+ --agent "$AGENT_TYPE" \
917
+ --cli "$CLI_TYPE" \
918
+ --cli-cmd "$CLI_CMD" \
919
+ --effort "$CLI_EFFORT" \
920
+ --run-mode "$RUN_MODE" \
921
+ --opus "$OPUS_OVERSIGHT" \
922
+ --exit-code "$exit_code" \
923
+ --elapsed "$elapsed" \
924
+ --timeout "$TIMEOUT_SEC" \
925
+ --mcp-profile "$MCP_PROFILE" \
926
+ --stderr-log "$STDERR_LOG" \
927
+ --stdout-log "$STDOUT_LOG" \
928
+ --max-bytes "$MAX_STDOUT_BYTES" \
929
+ --tee-active "$use_tee"
930
+ else
931
+ # post.mjs 없으면 기본 출력 (fallback)
932
+ echo "=== TFX-ROUTE RESULT ==="
933
+ echo "agent: $AGENT_TYPE"
934
+ echo "cli: $CLI_TYPE"
935
+ echo "exit_code: $exit_code"
936
+ echo "elapsed: ${elapsed}s"
937
+ echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
938
+ echo "=== OUTPUT ==="
939
+ cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
940
+ fi
941
+ }
942
+
943
+ main