triflux 0.0.1 → 2.0.1

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.
@@ -0,0 +1,937 @@
1
+ #!/usr/bin/env bash
2
+ # cli-route.sh v1.6 — CLI 라우팅 래퍼 (ai-scaffold 템플릿)
3
+ # v1.0: 기본 라우팅 (Codex/Gemini/Claude 분기)
4
+ # v1.1: stderr 분리, 출력 필터링, 타임아웃, MCP 프로필 지원
5
+ # v1.2: effort 동적 라우팅, bg/fg 모드, Opus 직접 수행, Gemini 모델 분기, 실행 로그
6
+ # v1.3: architect/critic Codex 이관, 리뷰어 exec review 전환, multi_agent 활성화
7
+ # v1.4: TFX_CLI_MODE 지원 (codex-only/gemini-only), CLI 미설치 자동 fallback
8
+ # v1.5: MCP 인벤토리 캐싱 — 실제 서버 가용성 기반 동적 힌트 생성
9
+ # v1.6: 토큰 사용량 추출 + sv-accumulator.json 누적
10
+ VERSION="1.6"
11
+ #
12
+ # 설치: cp scripts/cli-route.sh ~/.claude/scripts/cli-route.sh
13
+ #
14
+ # 사용법:
15
+ # cli-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec]
16
+ #
17
+ # 예시:
18
+ # cli-route.sh executor "코드 구현" implement 300
19
+ # cli-route.sh architect "아키텍처 분석" analyze 600
20
+ # cli-route.sh designer "UI 리뷰"
21
+ # cli-route.sh debugger "버그 분석" analyze
22
+
23
+ set -euo pipefail
24
+
25
+ # ── 인자 파싱 ──
26
+ AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
27
+ PROMPT="${2:?프롬프트 필수}"
28
+ MCP_PROFILE="${3:-auto}"
29
+ USER_TIMEOUT="${4:-}"
30
+
31
+ # ── CLI 경로 해석 (Windows npm global 대응) ──
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
+
35
+ # ── 상수 ──
36
+ MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
37
+ TIMESTAMP=$(date +%s)
38
+ STDERR_LOG="/tmp/omc-route-${AGENT_TYPE}-${TIMESTAMP}-stderr.log"
39
+ STDOUT_LOG="/tmp/omc-route-${AGENT_TYPE}-${TIMESTAMP}-stdout.log"
40
+
41
+ # ── 라우팅 테이블 ──
42
+ # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
43
+ #
44
+ # RUN_MODE: bg (백그라운드 — 독립 실행, 결과 나중에 수집)
45
+ # fg (포어그라운드 — 결과가 다음 단계를 블로킹)
46
+ #
47
+ # OPUS_OVERSIGHT: true — Codex 결과를 Claude Opus가 검증/보완해야 함
48
+ # false — Codex/Gemini 결과를 그대로 사용
49
+ #
50
+ route_agent() {
51
+ local agent="$1"
52
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
53
+
54
+ case "$agent" in
55
+ # ─── 구현 레인 ───
56
+
57
+ # Codex — 코드 구현 (effort: high, 360s, fg — 후속 태스크가 의존)
58
+ executor)
59
+ CLI_TYPE="codex"
60
+ CLI_CMD="codex"
61
+ CLI_ARGS="exec ${codex_base}"
62
+ CLI_EFFORT="high"
63
+ DEFAULT_TIMEOUT=360
64
+ RUN_MODE="fg"
65
+ OPUS_OVERSIGHT="false"
66
+ ;;
67
+ # Codex — 빌드 수정 (effort: fast, 180s, fg — 빌드 통과 확인 필요)
68
+ build-fixer)
69
+ CLI_TYPE="codex"
70
+ CLI_CMD="codex"
71
+ CLI_ARGS="--profile fast exec ${codex_base}"
72
+ CLI_EFFORT="fast"
73
+ DEFAULT_TIMEOUT=180
74
+ RUN_MODE="fg"
75
+ OPUS_OVERSIGHT="false"
76
+ ;;
77
+ # Codex — 디버깅 (effort: high, 300s, bg — 분석 결과 나중에 수집)
78
+ debugger)
79
+ CLI_TYPE="codex"
80
+ CLI_CMD="codex"
81
+ CLI_ARGS="exec ${codex_base}"
82
+ CLI_EFFORT="high"
83
+ DEFAULT_TIMEOUT=300
84
+ RUN_MODE="bg"
85
+ OPUS_OVERSIGHT="false"
86
+ ;;
87
+ # Codex — 자율 실행 (effort: xhigh, 900s, bg — 장시간 독립 수행)
88
+ deep-executor)
89
+ CLI_TYPE="codex"
90
+ CLI_CMD="codex"
91
+ CLI_ARGS="--profile xhigh exec ${codex_base}"
92
+ CLI_EFFORT="xhigh"
93
+ DEFAULT_TIMEOUT=1200
94
+ RUN_MODE="bg"
95
+ OPUS_OVERSIGHT="true"
96
+ ;;
97
+
98
+ # ─── 설계/분석 레인 ───
99
+
100
+ # Codex — 아키텍처 (effort: xhigh, 900s, bg — Opus가 설계 품질 검증)
101
+ architect)
102
+ CLI_TYPE="codex"
103
+ CLI_CMD="codex"
104
+ CLI_ARGS="--profile xhigh exec ${codex_base}"
105
+ CLI_EFFORT="xhigh"
106
+ DEFAULT_TIMEOUT=1200
107
+ RUN_MODE="bg"
108
+ OPUS_OVERSIGHT="true"
109
+ ;;
110
+ # Codex — 태스크 분해 (effort: xhigh, 900s, fg — Opus가 검증)
111
+ planner)
112
+ CLI_TYPE="codex"
113
+ CLI_CMD="codex"
114
+ CLI_ARGS="--profile xhigh exec ${codex_base}"
115
+ CLI_EFFORT="xhigh"
116
+ DEFAULT_TIMEOUT=1200
117
+ RUN_MODE="fg"
118
+ OPUS_OVERSIGHT="true"
119
+ ;;
120
+ # Codex — 비판적 검토 (effort: xhigh, 900s, bg — Opus가 비판 품질 검증)
121
+ critic)
122
+ CLI_TYPE="codex"
123
+ CLI_CMD="codex"
124
+ CLI_ARGS="--profile xhigh exec ${codex_base}"
125
+ CLI_EFFORT="xhigh"
126
+ DEFAULT_TIMEOUT=1200
127
+ RUN_MODE="bg"
128
+ OPUS_OVERSIGHT="true"
129
+ ;;
130
+ # Codex — 요구사항 분석 (effort: xhigh, 900s, fg — Opus가 검증)
131
+ analyst)
132
+ CLI_TYPE="codex"
133
+ CLI_CMD="codex"
134
+ CLI_ARGS="--profile xhigh exec ${codex_base}"
135
+ CLI_EFFORT="xhigh"
136
+ DEFAULT_TIMEOUT=1200
137
+ RUN_MODE="fg"
138
+ OPUS_OVERSIGHT="true"
139
+ ;;
140
+
141
+ # ─── 리뷰 레인 ───
142
+
143
+ # Codex — 코드 리뷰 (exec review, effort: thorough, 600s, bg — 전용 리뷰 커맨드)
144
+ code-reviewer)
145
+ CLI_TYPE="codex"
146
+ CLI_CMD="codex"
147
+ CLI_ARGS="--profile thorough exec ${codex_base} review"
148
+ CLI_EFFORT="thorough"
149
+ DEFAULT_TIMEOUT=600
150
+ RUN_MODE="bg"
151
+ OPUS_OVERSIGHT="false"
152
+ ;;
153
+ # Codex — 보안 리뷰 (exec review, effort: thorough, 600s, bg — Opus 검증 권장)
154
+ security-reviewer)
155
+ CLI_TYPE="codex"
156
+ CLI_CMD="codex"
157
+ CLI_ARGS="--profile thorough exec ${codex_base} review"
158
+ CLI_EFFORT="thorough"
159
+ DEFAULT_TIMEOUT=600
160
+ RUN_MODE="bg"
161
+ OPUS_OVERSIGHT="true"
162
+ ;;
163
+ # Codex — 품질 리뷰 (exec review, effort: thorough, 600s, bg — 전용 리뷰 커맨드)
164
+ quality-reviewer)
165
+ CLI_TYPE="codex"
166
+ CLI_CMD="codex"
167
+ CLI_ARGS="--profile thorough exec ${codex_base} review"
168
+ CLI_EFFORT="thorough"
169
+ DEFAULT_TIMEOUT=600
170
+ RUN_MODE="bg"
171
+ OPUS_OVERSIGHT="false"
172
+ ;;
173
+
174
+ # ─── 리서치 레인 ───
175
+
176
+ # Codex — 일반 리서치 (effort: high, 480s, bg — 빠른 검색+요약)
177
+ scientist)
178
+ CLI_TYPE="codex"
179
+ CLI_CMD="codex"
180
+ CLI_ARGS="exec ${codex_base}"
181
+ CLI_EFFORT="high"
182
+ DEFAULT_TIMEOUT=480
183
+ RUN_MODE="bg"
184
+ OPUS_OVERSIGHT="false"
185
+ ;;
186
+ # Codex — 심층 리서치 (effort: thorough, 1200s, bg — 논문 심층 분석)
187
+ scientist-deep)
188
+ CLI_TYPE="codex"
189
+ CLI_CMD="codex"
190
+ CLI_ARGS="--profile thorough exec ${codex_base}"
191
+ CLI_EFFORT="thorough"
192
+ DEFAULT_TIMEOUT=1200
193
+ RUN_MODE="bg"
194
+ OPUS_OVERSIGHT="false"
195
+ ;;
196
+ # Codex — 문서 조사 (effort: high, 480s, bg — 웹 검색 폴백 체인)
197
+ document-specialist)
198
+ CLI_TYPE="codex"
199
+ CLI_CMD="codex"
200
+ CLI_ARGS="exec ${codex_base}"
201
+ CLI_EFFORT="high"
202
+ DEFAULT_TIMEOUT=480
203
+ RUN_MODE="bg"
204
+ OPUS_OVERSIGHT="false"
205
+ ;;
206
+
207
+ # ─── UI/문서 레인 ───
208
+
209
+ # Gemini Pro 3.1 — UI/디자인 (높은 품질, 시각적 추론)
210
+ designer)
211
+ CLI_TYPE="gemini"
212
+ CLI_CMD="gemini"
213
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
214
+ CLI_EFFORT="pro"
215
+ DEFAULT_TIMEOUT=600
216
+ RUN_MODE="bg"
217
+ OPUS_OVERSIGHT="false"
218
+ ;;
219
+ # Gemini Flash 3 — 문서/가이드 작성 (빠른 생성)
220
+ writer)
221
+ CLI_TYPE="gemini"
222
+ CLI_CMD="gemini"
223
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
224
+ CLI_EFFORT="flash"
225
+ DEFAULT_TIMEOUT=600
226
+ RUN_MODE="bg"
227
+ OPUS_OVERSIGHT="false"
228
+ ;;
229
+
230
+ # ─── Claude 네이티브 ───
231
+
232
+ # Claude Haiku — 코드베이스 탐색 (fg — 탐색 결과가 분해에 필요)
233
+ explore)
234
+ CLI_TYPE="claude-native"
235
+ CLI_CMD=""
236
+ CLI_ARGS=""
237
+ CLI_EFFORT="n/a"
238
+ DEFAULT_TIMEOUT=300
239
+ RUN_MODE="fg"
240
+ OPUS_OVERSIGHT="false"
241
+ ;;
242
+ # Claude Sonnet — 검증 (fg — 검증 결과가 다음 단계 결정)
243
+ verifier)
244
+ CLI_TYPE="claude-native"
245
+ CLI_CMD=""
246
+ CLI_ARGS=""
247
+ CLI_EFFORT="n/a"
248
+ DEFAULT_TIMEOUT=300
249
+ RUN_MODE="fg"
250
+ OPUS_OVERSIGHT="false"
251
+ ;;
252
+ # Claude Sonnet — 테스트 (bg — 테스트 독립 실행 가능)
253
+ test-engineer)
254
+ CLI_TYPE="claude-native"
255
+ CLI_CMD=""
256
+ CLI_ARGS=""
257
+ CLI_EFFORT="n/a"
258
+ DEFAULT_TIMEOUT=300
259
+ RUN_MODE="bg"
260
+ OPUS_OVERSIGHT="false"
261
+ ;;
262
+ # Claude Sonnet — QA (bg — QA 독립 실행 가능)
263
+ qa-tester)
264
+ CLI_TYPE="claude-native"
265
+ CLI_CMD=""
266
+ CLI_ARGS=""
267
+ CLI_EFFORT="n/a"
268
+ DEFAULT_TIMEOUT=300
269
+ RUN_MODE="bg"
270
+ OPUS_OVERSIGHT="false"
271
+ ;;
272
+
273
+ # ─── 경량 ───
274
+
275
+ # Spark — 린트/보일러플레이트 (120s, fg — 즉시 완료 기대)
276
+ spark)
277
+ CLI_TYPE="codex"
278
+ CLI_CMD="codex"
279
+ CLI_ARGS="--profile spark_fast exec ${codex_base}"
280
+ CLI_EFFORT="spark_fast"
281
+ DEFAULT_TIMEOUT=120
282
+ RUN_MODE="fg"
283
+ OPUS_OVERSIGHT="false"
284
+ ;;
285
+ *)
286
+ echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
287
+ echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
288
+ echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
289
+ echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
290
+ exit 1
291
+ ;;
292
+ esac
293
+ }
294
+
295
+ # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
296
+ # TFX_CLI_MODE: auto (기본), codex (Codex-only), gemini (Gemini-only)
297
+ TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
298
+
299
+ apply_cli_mode() {
300
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
301
+
302
+ case "$TFX_CLI_MODE" in
303
+ codex)
304
+ # Gemini 에이전트를 Codex로 리매핑
305
+ if [[ "$CLI_TYPE" == "gemini" ]]; then
306
+ CLI_TYPE="codex"
307
+ CLI_CMD="codex"
308
+ case "$AGENT_TYPE" in
309
+ designer)
310
+ # UI/디자인은 코드 생성이 필요 → high effort
311
+ CLI_ARGS="exec ${codex_base}"
312
+ CLI_EFFORT="high"
313
+ DEFAULT_TIMEOUT=600
314
+ ;;
315
+ writer)
316
+ # 문서/가이드 작성은 경량 → spark
317
+ CLI_ARGS="--profile spark_fast exec ${codex_base}"
318
+ CLI_EFFORT="spark_fast"
319
+ DEFAULT_TIMEOUT=180
320
+ ;;
321
+ esac
322
+ echo "[cli-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
323
+ fi
324
+ ;;
325
+ gemini)
326
+ # Codex 에이전트를 Gemini로 리매핑
327
+ if [[ "$CLI_TYPE" == "codex" ]]; then
328
+ CLI_TYPE="gemini"
329
+ CLI_CMD="gemini"
330
+ # 복잡한 작업(구현/설계/리뷰/심층분석) → Pro 3.1
331
+ # 경량 작업(빌드/린트/검색/문서) → Flash 3
332
+ case "$AGENT_TYPE" in
333
+ # Pro 3.1 — 깊이 필요한 작업
334
+ executor|debugger|deep-executor)
335
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
336
+ CLI_EFFORT="pro"
337
+ ;;
338
+ architect|planner|critic|analyst)
339
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
340
+ CLI_EFFORT="pro"
341
+ ;;
342
+ code-reviewer|security-reviewer|quality-reviewer)
343
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
344
+ CLI_EFFORT="pro"
345
+ ;;
346
+ scientist-deep)
347
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
348
+ CLI_EFFORT="pro"
349
+ ;;
350
+ # Flash 3 — 경량/빠른 작업
351
+ build-fixer|spark)
352
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
353
+ CLI_EFFORT="flash"
354
+ DEFAULT_TIMEOUT=180
355
+ ;;
356
+ scientist|document-specialist)
357
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
358
+ CLI_EFFORT="flash"
359
+ ;;
360
+ *)
361
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
362
+ CLI_EFFORT="flash"
363
+ ;;
364
+ esac
365
+ echo "[cli-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
366
+ fi
367
+ ;;
368
+ auto)
369
+ # 자동 감지: CLI 미설치 시 대체
370
+ if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
371
+ if command -v "$GEMINI_BIN" &>/dev/null; then
372
+ TFX_CLI_MODE="gemini"
373
+ apply_cli_mode
374
+ return
375
+ else
376
+ CLI_TYPE="claude-native"
377
+ CLI_CMD=""
378
+ CLI_ARGS=""
379
+ echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
380
+ fi
381
+ elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
382
+ if command -v "$CODEX_BIN" &>/dev/null; then
383
+ TFX_CLI_MODE="codex"
384
+ apply_cli_mode
385
+ return
386
+ else
387
+ CLI_TYPE="claude-native"
388
+ CLI_CMD=""
389
+ CLI_ARGS=""
390
+ echo "[cli-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
391
+ fi
392
+ fi
393
+ ;;
394
+ esac
395
+ }
396
+
397
+ # ── MCP 인벤토리 캐시 읽기 ──
398
+ MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
399
+
400
+ # 캐시에서 특정 CLI의 서버 목록 추출 (캐시 없으면 빈 문자열)
401
+ get_cached_servers() {
402
+ local cli_type="$1"
403
+ if [[ -f "$MCP_CACHE" ]]; then
404
+ # node로 JSON 파싱 — 인자 전달 방식 (Windows 호환)
405
+ 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
406
+ fi
407
+ }
408
+
409
+ # ── MCP 프로필 → 프롬프트 접미사 ──
410
+ get_mcp_hint() {
411
+ local profile="$1"
412
+ local agent="$2"
413
+
414
+ # auto 모드: 에이전트에 따라 자동 결정
415
+ if [[ "$profile" == "auto" ]]; then
416
+ case "$agent" in
417
+ executor|build-fixer|debugger)
418
+ profile="implement"
419
+ ;;
420
+ architect|planner|critic|analyst)
421
+ profile="analyze"
422
+ ;;
423
+ code-reviewer|security-reviewer|quality-reviewer)
424
+ profile="review"
425
+ ;;
426
+ scientist|document-specialist)
427
+ profile="analyze"
428
+ ;;
429
+ deep-executor)
430
+ profile="implement"
431
+ ;;
432
+ designer|writer)
433
+ profile="docs"
434
+ ;;
435
+ *)
436
+ profile="minimal"
437
+ ;;
438
+ esac
439
+ fi
440
+
441
+ # 동적 힌트: 캐시가 있으면 실제 서버 목록 기반으로 생성
442
+ local cached_servers=""
443
+ cached_servers=$(get_cached_servers "$CLI_TYPE")
444
+
445
+ # 서버 존재 여부 헬퍼
446
+ has_server() { echo ",$cached_servers," | grep -q ",$1,"; }
447
+
448
+ # 캐시가 있으면 동적 힌트, 없으면 기본 힌트
449
+ if [[ -n "$cached_servers" ]]; then
450
+ local hint=""
451
+ case "$profile" in
452
+ implement)
453
+ has_server "context7" && hint="${hint}context7으로 라이브러리 문서를 조회하세요. "
454
+ if has_server "brave-search"; then
455
+ hint="${hint}웹 검색은 brave-search를 사용하세요. "
456
+ elif has_server "exa"; then
457
+ hint="${hint}웹 검색은 exa를 사용하세요. "
458
+ elif has_server "tavily"; then
459
+ hint="${hint}웹 검색은 tavily를 사용하세요. "
460
+ fi
461
+ hint="${hint}검색 도구 실패 시 재시도하지 말고 다음 도구로 전환하세요."
462
+ echo "$hint"
463
+ ;;
464
+ analyze)
465
+ has_server "context7" && hint="${hint}context7으로 관련 문서를 조회하세요. "
466
+ # 검색 도구 우선순위 동적 구성
467
+ local search_tools=""
468
+ has_server "brave-search" && search_tools="${search_tools}brave-search, "
469
+ has_server "tavily" && search_tools="${search_tools}tavily, "
470
+ has_server "exa" && search_tools="${search_tools}exa, "
471
+ if [[ -n "$search_tools" ]]; then
472
+ hint="${hint}웹 검색 우선순위: ${search_tools%%, }. 402 에러 시 즉시 다음 도구로 전환. "
473
+ fi
474
+ has_server "playwright" && hint="${hint}모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
475
+ hint="${hint}검색 깊이를 제한하고 결과를 빠르게 요약하세요."
476
+ echo "$hint"
477
+ ;;
478
+ review)
479
+ has_server "sequential-thinking" && echo "sequential-thinking으로 체계적으로 분석하세요." || echo ""
480
+ ;;
481
+ docs)
482
+ has_server "context7" && hint="${hint}context7으로 공식 문서를 참조하세요. "
483
+ has_server "brave-search" && hint="${hint}추가 검색은 brave-search를 사용하세요. "
484
+ hint="${hint}검색 결과의 출처 URL을 함께 제시하세요."
485
+ echo "$hint"
486
+ ;;
487
+ minimal|none) echo "" ;;
488
+ *) echo "" ;;
489
+ esac
490
+ else
491
+ # 캐시 없음 → 기본 힌트 (기존 동작 유지)
492
+ case "$profile" in
493
+ implement)
494
+ echo "context7으로 라이브러리 문서를 조회하세요. 웹 검색이 필요하면 brave-search를 우선 사용하고, 실패 시 exa를 시도하세요. exa/tavily가 402 에러를 반환하면 즉시 brave-search로 전환하세요."
495
+ ;;
496
+ analyze)
497
+ echo "context7으로 관련 문서를 조회하세요. 웹 검색은 다음 우선순위로 사용: 1) brave-search (무료, 우선), 2) tavily (실패 시), 3) exa (최후 수단). 402 에러 발생 시 해당 도구를 재시도하지 말고 즉시 다음 도구로 전환하세요. 모든 검색 도구가 실패하면 playwright로 직접 웹페이지를 방문하여 정보를 수집하세요. 주의: URL 크롤링은 최대 3개까지만. 논문 전문 대신 제목/초록/핵심 수치만 정리하세요. 검색 깊이를 제한하고 결과를 빠르게 요약하세요."
498
+ ;;
499
+ review)
500
+ echo "sequential-thinking으로 체계적으로 분석하세요."
501
+ ;;
502
+ docs)
503
+ echo "context7으로 공식 문서를 참조하세요. 추가 검색이 필요하면 brave-search를 사용하세요. 사실 확인이 필요한 내용은 반드시 Google Search를 활용하여 검증하세요 (google_search 도구 사용). 검색 결과의 출처 URL을 함께 제시하세요."
504
+ ;;
505
+ minimal|none) echo "" ;;
506
+ *) echo "" ;;
507
+ esac
508
+ fi
509
+ }
510
+
511
+ # ── Gemini MCP 서버 선택적 로드 (병렬 경합 감소) ──
512
+ # MCP 프로필별 필요한 서버만 로드하여 초기화 시간 단축
513
+ get_gemini_mcp_filter() {
514
+ local profile="$1"
515
+ case "$profile" in
516
+ implement) echo "--allowed-mcp-server-names context7,brave-search" ;;
517
+ analyze) echo "--allowed-mcp-server-names context7,brave-search,exa" ;;
518
+ review) echo "--allowed-mcp-server-names sequential-thinking" ;;
519
+ docs) echo "--allowed-mcp-server-names context7,brave-search" ;;
520
+ *) echo "" ;; # 필터 없음 — 모든 서버 로드
521
+ esac
522
+ }
523
+
524
+ # ── 토큰 사용량 추출 ──
525
+ # Codex JSON-line에서 usage 필드를 파싱하여 "input output" 반환
526
+ # Gemini는 세션 파일에서 추출하므로 여기선 0 반환
527
+ extract_tokens() {
528
+ local raw="$1"
529
+ local cli_type="$2"
530
+
531
+ if [[ "$cli_type" != "codex" ]] || [[ -z "$raw" ]]; then
532
+ echo "0 0"
533
+ return
534
+ fi
535
+
536
+ local result
537
+ result=$(echo "$raw" | python3 -c "
538
+ import sys, json
539
+ inp, out = 0, 0
540
+ for line in sys.stdin:
541
+ line = line.strip()
542
+ if not line:
543
+ continue
544
+ try:
545
+ obj = json.loads(line)
546
+ u = obj.get('usage', {})
547
+ if u:
548
+ inp = max(inp, u.get('input_tokens', 0))
549
+ out = max(out, u.get('output_tokens', 0))
550
+ except:
551
+ pass
552
+ print(f'{inp} {out}')
553
+ " 2>/dev/null) || result="0 0"
554
+ echo "$result"
555
+ }
556
+
557
+ # ── Codex JSON-line 출력 파서 ──
558
+ filter_codex_output() {
559
+ local raw="$1"
560
+
561
+ # JSON-line 형식이면 파싱, 아니면 그대로 반환
562
+ if echo "$raw" | head -1 | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
563
+ echo "$raw" | python3 -c "
564
+ import sys, json
565
+ for line in sys.stdin:
566
+ line = line.strip()
567
+ if not line:
568
+ continue
569
+ try:
570
+ obj = json.loads(line)
571
+ if obj.get('type') in ('message', 'completed', 'output_text'):
572
+ text = obj.get('text', obj.get('content', obj.get('output', '')))
573
+ if text:
574
+ print(text)
575
+ except json.JSONDecodeError:
576
+ print(line)
577
+ " 2>/dev/null || echo "$raw"
578
+ else
579
+ echo "$raw"
580
+ fi
581
+ }
582
+
583
+ # ── 실행 로그 기록 ──
584
+ # 각 실행의 에이전트, effort, 소요시간, 상태를 로컬에 누적 기록
585
+ LOG_DIR="${HOME}/.claude/logs"
586
+ LOG_FILE="${LOG_DIR}/cli-route-stats.jsonl"
587
+
588
+ log_execution() {
589
+ local agent="$1"
590
+ local cli_type="$2"
591
+ local effort="$3"
592
+ local run_mode="$4"
593
+ local opus="$5"
594
+ local exit_code="$6"
595
+ local elapsed="$7"
596
+ local timeout="$8"
597
+ local mcp_profile="$9"
598
+ local input_tokens="${10:-0}"
599
+ local output_tokens="${11:-0}"
600
+ local total_tokens="${12:-0}"
601
+
602
+ mkdir -p "$LOG_DIR"
603
+
604
+ local ts
605
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%S")
606
+ local status="success"
607
+ if [[ $exit_code -eq 124 ]]; then
608
+ status="timeout"
609
+ elif [[ $exit_code -ne 0 ]]; then
610
+ status="failed"
611
+ fi
612
+
613
+ # JSONL 한 줄 추가 (jq 없이 수동 구성)
614
+ printf '{"ts":"%s","agent":"%s","cli":"%s","effort":"%s","run_mode":"%s","opus_oversight":"%s","status":"%s","exit_code":%d,"elapsed_sec":%d,"timeout_sec":%d,"mcp_profile":"%s","input_tokens":%d,"output_tokens":%d,"total_tokens":%d}\n' \
615
+ "$ts" "$agent" "$cli_type" "$effort" "$run_mode" "$opus" "$status" "$exit_code" "$elapsed" "$timeout" "$mcp_profile" \
616
+ "$input_tokens" "$output_tokens" "$total_tokens" \
617
+ >> "$LOG_FILE" 2>/dev/null || true
618
+ }
619
+
620
+ # ── 토큰 누적 (sv-accumulator.json) ──
621
+ # 실행 성공 시 추출된 토큰을 ~/.claude/cache/sv-accumulator.json에 누적
622
+ accumulate_tokens() {
623
+ local cli_type="$1"
624
+ local input_tokens="$2"
625
+ local output_tokens="$3"
626
+ local total=$((input_tokens + output_tokens))
627
+
628
+ # 토큰 0이면 건너뜀
629
+ if [[ $total -eq 0 ]]; then return; fi
630
+
631
+ local acc_file="${HOME}/.claude/cache/sv-accumulator.json"
632
+ mkdir -p "$(dirname "$acc_file")"
633
+
634
+ # node로 JSON 읽기/수정/쓰기 (jq 의존성 없이)
635
+ node -e '
636
+ const fs = require("fs");
637
+ const [,, file, cliType, inp, out] = process.argv;
638
+ let data;
639
+ try { data = JSON.parse(fs.readFileSync(file, "utf-8")); } catch { data = {}; }
640
+ if (!data.codex) data.codex = { tokens: 0, calls: 0 };
641
+ if (!data.gemini) data.gemini = { tokens: 0, calls: 0 };
642
+ const key = cliType === "gemini" ? "gemini" : "codex";
643
+ data[key].tokens += Number(inp) + Number(out);
644
+ data[key].calls += 1;
645
+ data.lastUpdated = new Date().toISOString();
646
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
647
+ ' -- "$acc_file" "$cli_type" "$input_tokens" "$output_tokens" 2>/dev/null || true
648
+ }
649
+
650
+ # ── CLI 이슈 자동 수집 ──
651
+ # stderr에서 알려진 에러 패턴을 감지하여 ~/.claude/cache/cli-issues.jsonl에 기록
652
+ track_cli_issue() {
653
+ local cli_type="$1" agent="$2" stderr_text="$3" exit_code="$4"
654
+ [[ -z "$stderr_text" && "$exit_code" -eq 0 ]] && return
655
+
656
+ local issues_file="${HOME}/.claude/cache/cli-issues.jsonl"
657
+ mkdir -p "$(dirname "$issues_file")"
658
+
659
+ local pattern="" msg="" severity="warn"
660
+
661
+ # 패턴 매칭 (가장 구체적인 것 우선)
662
+ if echo "$stderr_text" | grep -qi "sandbox image.*missing"; then
663
+ pattern="sandbox_missing"; msg="Docker sandbox image not found"; severity="warn"
664
+ elif echo "$stderr_text" | grep -qi "rate.limit\|429\|too many requests"; then
665
+ pattern="rate_limit"; msg="API rate limit exceeded"; severity="warn"
666
+ elif echo "$stderr_text" | grep -qi "ECONNREFUSED\|ENOTFOUND\|network"; then
667
+ pattern="network_error"; msg="Network connection failed"; severity="error"
668
+ elif echo "$stderr_text" | grep -qi "deprecated"; then
669
+ pattern="deprecated_flag"; msg="Deprecated flag/feature detected"; severity="warn"
670
+ elif echo "$stderr_text" | grep -qi "API_KEY.*not.set\|auth.*fail\|unauthorized\|401"; then
671
+ pattern="auth_error"; msg="Authentication failed"; severity="error"
672
+ elif echo "$stderr_text" | grep -qi "ENOMEM\|out of memory\|heap"; then
673
+ pattern="oom"; msg="Out of memory"; severity="error"
674
+ elif [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
675
+ pattern="unknown_error"; msg="Exit code $exit_code"; severity="warn"
676
+ fi
677
+
678
+ [[ -z "$pattern" ]] && return
679
+
680
+ # 중복 방지: 같은 패턴+cli가 최근 5분 내 기록됐으면 건너뜀
681
+ if [[ -f "$issues_file" ]]; then
682
+ local now_ms=$(($(date +%s) * 1000))
683
+ local dedup
684
+ dedup=$(tail -5 "$issues_file" 2>/dev/null | grep "\"$pattern\"" | grep "\"$cli_type\"" | tail -1)
685
+ if [[ -n "$dedup" ]]; then
686
+ local last_ts
687
+ last_ts=$(echo "$dedup" | sed 's/.*"ts":\([0-9]*\).*/\1/' 2>/dev/null)
688
+ if [[ -n "$last_ts" ]] && (( now_ms - last_ts < 300000 )); then
689
+ return # 5분 이내 동일 이슈 → 건너뜀
690
+ fi
691
+ fi
692
+ fi
693
+
694
+ # stderr 첫 200자만 기록 (개인정보 최소화)
695
+ local snippet
696
+ snippet=$(echo "$stderr_text" | head -3 | cut -c1-200 | tr '\n' ' ')
697
+
698
+ # CLI 버전 추출
699
+ local cli_ver=""
700
+ cli_ver=$($cli_type --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
701
+
702
+ node -e '
703
+ const fs = require("fs");
704
+ const [,, file, cli, agent, pattern, msg, severity, snippet, ver] = process.argv;
705
+ const entry = JSON.stringify({
706
+ ts: Date.now(), cli, agent, pattern, msg, severity,
707
+ snippet: snippet.substring(0, 200), ver: ver || null, resolved: false
708
+ });
709
+ fs.appendFileSync(file, entry + "\n");
710
+ // 자동 회전: 200줄 초과 시 최근 100줄만 유지
711
+ const lines = fs.readFileSync(file, "utf-8").trim().split("\n");
712
+ if (lines.length > 200) {
713
+ fs.writeFileSync(file, lines.slice(-100).join("\n") + "\n");
714
+ }
715
+ ' -- "$issues_file" "$cli_type" "$agent" "$pattern" "$msg" "$severity" "$snippet" "$cli_ver" 2>/dev/null || true
716
+ }
717
+
718
+ # ── 출력 크기 제한 ──
719
+ truncate_output() {
720
+ local input="$1"
721
+ local max_bytes="$2"
722
+ local byte_count
723
+ byte_count=$(echo "$input" | wc -c)
724
+
725
+ if [[ $byte_count -gt $max_bytes ]]; then
726
+ echo "$input" | head -c "$max_bytes"
727
+ echo ""
728
+ echo "--- [출력 ${byte_count}B → ${max_bytes}B로 절삭됨] ---"
729
+ else
730
+ echo "$input"
731
+ fi
732
+ }
733
+
734
+ # ── 메인 실행 ──
735
+ main() {
736
+ route_agent "$AGENT_TYPE"
737
+
738
+ # CLI 모드 오버라이드 적용 (tfx-codex/tfx-gemini 또는 auto-fallback)
739
+ apply_cli_mode
740
+
741
+ # CLI 경로 해석 (bare command → 절대경로)
742
+ case "$CLI_CMD" in
743
+ codex) CLI_CMD="$CODEX_BIN" ;;
744
+ gemini) CLI_CMD="$GEMINI_BIN" ;;
745
+ esac
746
+
747
+ # 사용자 지정 타임아웃이 없으면 에이전트별 기본값 사용
748
+ if [[ -n "$USER_TIMEOUT" ]]; then
749
+ TIMEOUT_SEC="$USER_TIMEOUT"
750
+ else
751
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
752
+ fi
753
+
754
+ # kteam 안정화: Gemini 에이전트 기본 타임아웃 축소 (사용자 미지정 시만)
755
+ if [[ -z "$USER_TIMEOUT" ]]; then
756
+ case "$AGENT_TYPE" in
757
+ designer|writer)
758
+ if [[ "$DEFAULT_TIMEOUT" -gt 60 ]]; then
759
+ TIMEOUT_SEC=60
760
+ fi
761
+ ;;
762
+ esac
763
+ fi
764
+
765
+ # Claude 네이티브 에이전트는 이 스크립트로 처리 불가
766
+ if [[ "$CLI_TYPE" == "claude-native" ]]; then
767
+ # 에이전트별 모델 결정
768
+ local model="sonnet"
769
+ case "$AGENT_TYPE" in
770
+ explore) model="haiku" ;;
771
+ verifier|test-engineer|qa-tester) model="sonnet" ;;
772
+ esac
773
+
774
+ echo "ROUTE_TYPE=claude-native"
775
+ echo "AGENT=$AGENT_TYPE"
776
+ echo "MODEL=$model"
777
+ echo "RUN_MODE=$RUN_MODE"
778
+ echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
779
+ echo "TIMEOUT=$TIMEOUT_SEC"
780
+ echo "PROMPT=$PROMPT"
781
+ echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
782
+ exit 0
783
+ fi
784
+
785
+ # MCP 힌트를 프롬프트에 주입
786
+ MCP_HINT=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
787
+ if [[ -n "$MCP_HINT" ]]; then
788
+ FULL_PROMPT="${PROMPT}. ${MCP_HINT}"
789
+ else
790
+ FULL_PROMPT="$PROMPT"
791
+ fi
792
+
793
+ # 메타정보 출력 (stderr로)
794
+ echo "[cli-route] type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
795
+ echo "[cli-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE stderr_log=$STDERR_LOG" >&2
796
+
797
+ # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
798
+ local exit_code=0
799
+ local raw_output=""
800
+ local start_time
801
+ start_time=$(date +%s)
802
+
803
+ if [[ "$CLI_TYPE" == "codex" ]]; then
804
+ raw_output=$(timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$FULL_PROMPT" 2>"$STDERR_LOG") || exit_code=$?
805
+ elif [[ "$CLI_TYPE" == "gemini" ]]; then
806
+ # Gemini 안정화 v2: 프로세스 생존 기반 health check + MCP 선택적 로드
807
+ # - 프로세스 살아있음 → 정상 (MCP 초기화 중일 수 있음)
808
+ # - 프로세스 죽음 + 출력 없음 → crash → 재시도
809
+ # - 메인 timeout이 진짜 hang을 처리
810
+
811
+ # Fix 4: MCP 프로필별 필요한 서버만 로드 (병렬 경합 감소)
812
+ local gemini_mcp_filter
813
+ gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
814
+ local gemini_args="$CLI_ARGS"
815
+ if [[ -n "$gemini_mcp_filter" ]]; then
816
+ gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
817
+ echo "[cli-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
818
+ fi
819
+
820
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
821
+ local pid=$!
822
+
823
+ # Fix 2+3: 프로세스 생존 기반 health check (30초)
824
+ local health_ok=true
825
+ local HEALTH_TIMEOUT=30
826
+ for i in $(seq 1 $HEALTH_TIMEOUT); do
827
+ sleep 1
828
+ # 출력 있으면 확실히 정상 → 조기 탈출
829
+ if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
830
+ break
831
+ fi
832
+ # 프로세스 사망 + 출력 없음 → crash
833
+ if ! kill -0 "$pid" 2>/dev/null; then
834
+ health_ok=false
835
+ echo "[cli-route] Gemini: 출력 없이 프로세스 종료 (${i}초)" >&2
836
+ break
837
+ fi
838
+ # 프로세스 살아있고 출력 없음 → MCP 초기화 중, 계속 대기
839
+ done
840
+
841
+ if [[ "$health_ok" == "false" ]]; then
842
+ # crash 감지 → 1회 재시도
843
+ wait "$pid" 2>/dev/null
844
+ echo "[cli-route] Gemini crash 감지, 재시도 중..." >&2
845
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
846
+ pid=$!
847
+ wait "$pid"
848
+ exit_code=$?
849
+ else
850
+ wait "$pid"
851
+ exit_code=$?
852
+ fi
853
+
854
+ raw_output=$(cat "$STDOUT_LOG" 2>/dev/null)
855
+ fi
856
+
857
+ local end_time
858
+ end_time=$(date +%s)
859
+ local elapsed=$((end_time - start_time))
860
+
861
+ # 토큰 추출
862
+ local token_info input_tokens output_tokens total_tokens
863
+ token_info=$(extract_tokens "$raw_output" "$CLI_TYPE") || token_info="0 0"
864
+ input_tokens=$(echo "$token_info" | awk '{print $1}')
865
+ output_tokens=$(echo "$token_info" | awk '{print $2}')
866
+ total_tokens=$((input_tokens + output_tokens))
867
+
868
+ # 실행 로그 기록 (토큰 포함)
869
+ log_execution "$AGENT_TYPE" "$CLI_TYPE" "$CLI_EFFORT" "$RUN_MODE" "$OPUS_OVERSIGHT" \
870
+ "$exit_code" "$elapsed" "$TIMEOUT_SEC" "$MCP_PROFILE" \
871
+ "$input_tokens" "$output_tokens" "$total_tokens"
872
+
873
+ # 성공 시 토큰 누적
874
+ if [[ $exit_code -eq 0 ]]; then
875
+ accumulate_tokens "$CLI_TYPE" "$input_tokens" "$output_tokens" || true
876
+ fi
877
+
878
+ # CLI 이슈 자동 수집
879
+ local _stderr_for_track=""
880
+ [[ -f "$STDERR_LOG" ]] && _stderr_for_track=$(cat "$STDERR_LOG" 2>/dev/null || echo "")
881
+ track_cli_issue "$CLI_TYPE" "$AGENT_TYPE" "$_stderr_for_track" "$exit_code" || true
882
+
883
+ # 결과 처리
884
+ local stderr_content=""
885
+ if [[ -f "$STDERR_LOG" ]]; then
886
+ stderr_content=$(cat "$STDERR_LOG" 2>/dev/null || echo "")
887
+ fi
888
+
889
+ # 헤더 (구조화된 메타데이터)
890
+ echo "=== CLI-ROUTE RESULT ==="
891
+ echo "agent: $AGENT_TYPE"
892
+ echo "cli: $CLI_TYPE ($CLI_CMD)"
893
+ echo "effort: $CLI_EFFORT"
894
+ echo "run_mode: $RUN_MODE"
895
+ echo "opus_oversight: $OPUS_OVERSIGHT"
896
+ echo "exit_code: $exit_code"
897
+ echo "timeout: ${TIMEOUT_SEC}s"
898
+ echo "elapsed: ${elapsed}s"
899
+ echo "mcp_profile: $MCP_PROFILE"
900
+ echo "stderr_log: $STDERR_LOG"
901
+
902
+ # exit code 분석
903
+ if [[ $exit_code -eq 0 ]]; then
904
+ if [[ -n "$stderr_content" ]]; then
905
+ echo "status: success_with_warnings"
906
+ echo "warnings: $(echo "$stderr_content" | head -3)"
907
+ else
908
+ echo "status: success"
909
+ fi
910
+ echo "=== OUTPUT ==="
911
+
912
+ if [[ "$CLI_TYPE" == "codex" ]]; then
913
+ filtered=$(filter_codex_output "$raw_output")
914
+ truncate_output "$filtered" "$MAX_STDOUT_BYTES"
915
+ else
916
+ truncate_output "$raw_output" "$MAX_STDOUT_BYTES"
917
+ fi
918
+
919
+ elif [[ $exit_code -eq 124 ]]; then
920
+ echo "status: timeout (${TIMEOUT_SEC}s 초과)"
921
+ echo "=== PARTIAL OUTPUT ==="
922
+ truncate_output "$raw_output" "$MAX_STDOUT_BYTES"
923
+ echo "=== STDERR ==="
924
+ echo "$stderr_content" | tail -10
925
+
926
+ else
927
+ echo "status: failed (exit_code=$exit_code)"
928
+ echo "=== STDERR ==="
929
+ echo "$stderr_content" | tail -20
930
+ if [[ -n "$raw_output" ]]; then
931
+ echo "=== PARTIAL OUTPUT ==="
932
+ truncate_output "$raw_output" "$MAX_STDOUT_BYTES"
933
+ fi
934
+ fi
935
+ }
936
+
937
+ main