triflux 3.2.0-dev.8 → 3.3.0-dev.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.
Files changed (52) hide show
  1. package/bin/triflux.mjs +1296 -1055
  2. package/hooks/hooks.json +17 -0
  3. package/hooks/keyword-rules.json +20 -4
  4. package/hooks/pipeline-stop.mjs +54 -0
  5. package/hub/bridge.mjs +517 -318
  6. package/hub/hitl.mjs +45 -31
  7. package/hub/pipe.mjs +457 -0
  8. package/hub/pipeline/index.mjs +121 -0
  9. package/hub/pipeline/state.mjs +164 -0
  10. package/hub/pipeline/transitions.mjs +114 -0
  11. package/hub/router.mjs +422 -161
  12. package/hub/schema.sql +14 -0
  13. package/hub/server.mjs +499 -424
  14. package/hub/store.mjs +388 -314
  15. package/hub/team/cli-team-common.mjs +348 -0
  16. package/hub/team/cli-team-control.mjs +393 -0
  17. package/hub/team/cli-team-start.mjs +516 -0
  18. package/hub/team/cli-team-status.mjs +269 -0
  19. package/hub/team/cli.mjs +75 -1475
  20. package/hub/team/dashboard.mjs +1 -9
  21. package/hub/team/native.mjs +190 -130
  22. package/hub/team/nativeProxy.mjs +165 -78
  23. package/hub/team/orchestrator.mjs +15 -20
  24. package/hub/team/pane.mjs +137 -103
  25. package/hub/team/psmux.mjs +506 -0
  26. package/hub/team/session.mjs +393 -330
  27. package/hub/team/shared.mjs +13 -0
  28. package/hub/team/staleState.mjs +299 -0
  29. package/hub/tools.mjs +105 -31
  30. package/hub/workers/claude-worker.mjs +446 -0
  31. package/hub/workers/codex-mcp.mjs +414 -0
  32. package/hub/workers/factory.mjs +18 -0
  33. package/hub/workers/gemini-worker.mjs +349 -0
  34. package/hub/workers/interface.mjs +41 -0
  35. package/hud/hud-qos-status.mjs +1790 -1788
  36. package/package.json +4 -1
  37. package/scripts/__tests__/keyword-detector.test.mjs +8 -8
  38. package/scripts/keyword-detector.mjs +15 -0
  39. package/scripts/lib/keyword-rules.mjs +4 -1
  40. package/scripts/preflight-cache.mjs +72 -0
  41. package/scripts/psmux-steering-prototype.sh +368 -0
  42. package/scripts/setup.mjs +136 -71
  43. package/scripts/tfx-route-worker.mjs +161 -0
  44. package/scripts/tfx-route.sh +485 -91
  45. package/skills/tfx-auto/SKILL.md +90 -564
  46. package/skills/tfx-auto-codex/SKILL.md +1 -3
  47. package/skills/tfx-codex/SKILL.md +1 -4
  48. package/skills/tfx-doctor/SKILL.md +1 -0
  49. package/skills/tfx-gemini/SKILL.md +1 -4
  50. package/skills/tfx-multi/SKILL.md +378 -0
  51. package/skills/tfx-setup/SKILL.md +1 -4
  52. package/skills/tfx-team/SKILL.md +0 -304
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # tfx-route.sh v2.0 — CLI 라우팅 래퍼 (triflux)
2
+ # tfx-route.sh v2.3 — CLI 라우팅 래퍼 (triflux)
3
3
  #
4
4
  # v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
5
5
  # v2.0: tfx-route.sh 리네임
@@ -9,7 +9,7 @@
9
9
  # - Gemini health check 지수 백오프 (30×1s → 5×exp)
10
10
  # - 컨텍스트 파일 5번째 인자 지원
11
11
  #
12
- VERSION="2.0"
12
+ VERSION="2.3"
13
13
  #
14
14
  # 사용법:
15
15
  # tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
@@ -28,14 +28,19 @@ USER_TIMEOUT="${4:-}"
28
28
  CONTEXT_FILE="${5:-}"
29
29
 
30
30
  # ── CLI 경로 해석 (Windows npm global 대응) ──
31
+ NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
31
32
  CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
32
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:-[]}"
33
37
 
34
38
  # ── 상수 ──
35
39
  MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
36
40
  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"
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"
39
44
  TFX_TMP="${TMPDIR:-/tmp}"
40
45
 
41
46
  # ── 팀 환경변수 ──
@@ -64,12 +69,18 @@ deregister_agent() {
64
69
  # JSON 문자열 이스케이프 (큰따옴표, 백슬래시, 개행, 탭, CR)
65
70
  json_escape() {
66
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 치환
67
78
  s="${s//\\/\\\\}"
68
79
  s="${s//\"/\\\"}"
69
80
  s="${s//$'\n'/\\n}"
70
81
  s="${s//$'\t'/\\t}"
71
82
  s="${s//$'\r'/\\r}"
72
- echo "$s"
83
+ printf '%s' "$s"
73
84
  }
74
85
 
75
86
  team_claim_task() {
@@ -97,36 +108,35 @@ team_claim_task() {
97
108
  }
98
109
 
99
110
  team_complete_task() {
100
- local result_status="${1:-completed}"
111
+ local result="${1:-success}" # success/failed/timeout
101
112
  local result_summary="${2:-작업 완료}"
102
- local safe_team_name safe_task_id safe_agent_name safe_status
103
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
104
116
  safe_team_name=$(json_escape "$TFX_TEAM_NAME")
105
117
  safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
106
118
  safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
107
- safe_status=$(json_escape "$result_status")
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")
108
122
 
109
- # task 상태 업데이트
123
+ # task 상태: 항상 "completed" (Claude Code API는 "failed" 미지원)
124
+ # 실제 결과는 metadata.result로 전달
110
125
  curl -sf -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
111
126
  -H "Content-Type: application/json" \
112
- -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"status\":\"${safe_status}\",\"owner\":\"${safe_agent_name}\"}" \
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}\"}}" \
113
128
  >/dev/null 2>&1 || true
114
129
 
115
130
  # 리드에게 메시지 전송
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
131
  curl -sf -X POST "${TFX_HUB_URL}/bridge/team/send-message" \
122
132
  -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}\"}" \
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}\"}" \
124
134
  >/dev/null 2>&1 || true
125
135
 
126
136
  # Hub result 발행 (poll_messages 채널 활성화)
127
137
  curl -sf -X POST "${TFX_HUB_URL}/bridge/result" \
128
138
  -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}\"}" \
139
+ -d "{\"agent_id\":\"${safe_agent_name}\",\"topic\":\"task.result\",\"payload\":{\"task_id\":\"${safe_task_id}\",\"result\":\"${safe_result}\"},\"trace_id\":\"${safe_team_name}\"}" \
130
140
  >/dev/null 2>&1 || true
131
141
  }
132
142
 
@@ -216,8 +226,9 @@ route_agent() {
216
226
  CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
217
227
  CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
218
228
  verifier)
219
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
220
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
229
+ CLI_TYPE="codex"; CLI_CMD="codex"
230
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
231
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
221
232
  test-engineer)
222
233
  CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
223
234
  CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
@@ -242,6 +253,9 @@ route_agent() {
242
253
  # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
243
254
  TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
244
255
  TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
256
+ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
257
+ TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
258
+ TFX_SEARCH_TOOL="${TFX_SEARCH_TOOL:-}"
245
259
  case "$TFX_NO_CLAUDE_NATIVE" in
246
260
  0|1) ;;
247
261
  *)
@@ -249,6 +263,28 @@ case "$TFX_NO_CLAUDE_NATIVE" in
249
263
  exit 1
250
264
  ;;
251
265
  esac
266
+ case "$TFX_CODEX_TRANSPORT" in
267
+ auto|mcp|exec) ;;
268
+ *)
269
+ echo "ERROR: TFX_CODEX_TRANSPORT 값은 auto, mcp, exec 중 하나여야 합니다. (현재: $TFX_CODEX_TRANSPORT)" >&2
270
+ exit 1
271
+ ;;
272
+ esac
273
+ case "$TFX_WORKER_INDEX" in
274
+ "") ;;
275
+ *[!0-9]*|0)
276
+ echo "ERROR: TFX_WORKER_INDEX 값은 1 이상의 정수여야 합니다. (현재: $TFX_WORKER_INDEX)" >&2
277
+ exit 1
278
+ ;;
279
+ esac
280
+ case "$TFX_SEARCH_TOOL" in
281
+ ""|brave-search|tavily|exa) ;;
282
+ *)
283
+ echo "ERROR: TFX_SEARCH_TOOL 값은 brave-search, tavily, exa 중 하나여야 합니다. (현재: $TFX_SEARCH_TOOL)" >&2
284
+ exit 1
285
+ ;;
286
+ esac
287
+ CODEX_MCP_TRANSPORT_EXIT_CODE=70
252
288
 
253
289
  apply_cli_mode() {
254
290
  local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
@@ -389,23 +425,60 @@ get_mcp_hint() {
389
425
 
390
426
  has_server() { echo ",$servers," | grep -q ",$1,"; }
391
427
 
428
+ get_search_tool_order() {
429
+ local available=()
430
+ local ordered=()
431
+ local tool
432
+
433
+ for tool in brave-search tavily exa; do
434
+ has_server "$tool" && available+=("$tool")
435
+ done
436
+
437
+ if [[ ${#available[@]} -eq 0 ]]; then
438
+ return 0
439
+ fi
440
+
441
+ if [[ -n "$TFX_SEARCH_TOOL" ]]; then
442
+ for tool in "${available[@]}"; do
443
+ [[ "$tool" == "$TFX_SEARCH_TOOL" ]] && ordered+=("$tool")
444
+ done
445
+ for tool in "${available[@]}"; do
446
+ [[ "$tool" != "$TFX_SEARCH_TOOL" ]] && ordered+=("$tool")
447
+ done
448
+ elif [[ -n "$TFX_WORKER_INDEX" && ${#available[@]} -gt 1 ]]; then
449
+ local offset=$(( (TFX_WORKER_INDEX - 1) % ${#available[@]} ))
450
+ local i idx
451
+ for ((i=0; i<${#available[@]}; i++)); do
452
+ idx=$(( (offset + i) % ${#available[@]} ))
453
+ ordered+=("${available[$idx]}")
454
+ done
455
+ else
456
+ ordered=("${available[@]}")
457
+ fi
458
+
459
+ printf '%s\n' "${ordered[*]}"
460
+ }
461
+
462
+ local ordered_tools=()
463
+ read -r -a ordered_tools <<< "$(get_search_tool_order)"
464
+ local ordered_tools_csv=""
465
+ if [[ ${#ordered_tools[@]} -gt 0 ]]; then
466
+ ordered_tools_csv=$(printf '%s, ' "${ordered_tools[@]}")
467
+ ordered_tools_csv="${ordered_tools_csv%, }"
468
+ fi
469
+
392
470
  local hint=""
393
471
  case "$profile" in
394
472
  implement)
395
473
  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를 사용하세요. "
474
+ if [[ ${#ordered_tools[@]} -gt 0 ]]; then
475
+ hint+="웹 검색은 ${ordered_tools[0]}를 사용하세요. "
399
476
  fi
400
- hint+="검색 도구 실패 시 재시도하지 말고 다음 도구로 전환하세요."
477
+ hint+="검색 도구 실패 시 402, 429, 432, 433, quota 에러에서 재시도하지 말고 다음 도구로 전환하세요."
401
478
  ;;
402
479
  analyze)
403
480
  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 에러 시 즉시 다음 도구로 전환. "
481
+ [[ -n "$ordered_tools_csv" ]] && hint+="웹 검색 우선순위: ${ordered_tools_csv}. 402, 429, 432, 433, quota 에러 시 즉시 다음 도구로 전환. "
409
482
  has_server "playwright" && hint+="모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
410
483
  hint+="검색 깊이를 제한하고 결과를 빠르게 요약하세요."
411
484
  ;;
@@ -414,7 +487,9 @@ get_mcp_hint() {
414
487
  ;;
415
488
  docs)
416
489
  has_server "context7" && hint+="context7으로 공식 문서를 참조하세요. "
417
- has_server "brave-search" && hint+="추가 검색은 brave-search를 사용하세요. "
490
+ if [[ ${#ordered_tools[@]} -gt 0 ]]; then
491
+ hint+="추가 검색은 ${ordered_tools[0]}를 사용하세요. "
492
+ fi
418
493
  hint+="검색 결과의 출처 URL을 함께 제시하세요."
419
494
  ;;
420
495
  minimal|none) ;;
@@ -423,17 +498,267 @@ get_mcp_hint() {
423
498
  }
424
499
 
425
500
  # ── Gemini MCP 서버 선택적 로드 ──
426
- get_gemini_mcp_filter() {
501
+ get_gemini_mcp_servers() {
427
502
  local profile="$1"
428
503
  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" ;;
504
+ implement) echo "context7 brave-search" ;;
505
+ analyze) echo "context7 brave-search exa tavily" ;;
506
+ review) echo "sequential-thinking" ;;
507
+ docs) echo "context7 brave-search" ;;
433
508
  *) echo "" ;;
434
509
  esac
435
510
  }
436
511
 
512
+ get_gemini_mcp_filter() {
513
+ local servers
514
+ servers=$(get_gemini_mcp_servers "$1")
515
+ [[ -z "$servers" ]] && return 0
516
+ echo "--allowed-mcp-server-names ${servers// /,}"
517
+ }
518
+
519
+ get_claude_model() {
520
+ case "$AGENT_TYPE" in
521
+ explore) echo "haiku" ;;
522
+ *) echo "sonnet" ;;
523
+ esac
524
+ }
525
+
526
+ emit_claude_native_metadata() {
527
+ local model
528
+ model=$(get_claude_model)
529
+ echo "ROUTE_TYPE=claude-native"
530
+ echo "AGENT=$AGENT_TYPE"
531
+ echo "MODEL=$model"
532
+ echo "RUN_MODE=$RUN_MODE"
533
+ echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
534
+ echo "TIMEOUT=$TIMEOUT_SEC"
535
+ echo "MCP_PROFILE=$MCP_PROFILE"
536
+ [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
537
+ echo "PROMPT=$PROMPT"
538
+ echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
539
+ }
540
+
541
+ resolve_worker_runner_script() {
542
+ if [[ -n "${TFX_ROUTE_WORKER_RUNNER:-}" && -f "$TFX_ROUTE_WORKER_RUNNER" ]]; then
543
+ printf '%s\n' "$TFX_ROUTE_WORKER_RUNNER"
544
+ return 0
545
+ fi
546
+
547
+ local script_dir
548
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
549
+ local candidate="$script_dir/tfx-route-worker.mjs"
550
+ [[ -f "$candidate" ]] || return 1
551
+ printf '%s\n' "$candidate"
552
+ }
553
+
554
+ run_stream_worker() {
555
+ local worker_type="$1"
556
+ local prompt="$2"
557
+ local use_tee_flag="$3"
558
+ shift 3
559
+
560
+ local runner_script
561
+ if ! runner_script=$(resolve_worker_runner_script); then
562
+ echo "[tfx-route] 경고: stream worker runner를 찾지 못했습니다." >&2
563
+ return 127
564
+ fi
565
+
566
+ if ! command -v "$NODE_BIN" &>/dev/null; then
567
+ echo "[tfx-route] 경고: node를 찾지 못해 stream worker를 실행할 수 없습니다." >&2
568
+ return 127
569
+ fi
570
+
571
+ local -a worker_cmd=(
572
+ "$NODE_BIN"
573
+ "$runner_script"
574
+ "--type" "$worker_type"
575
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
576
+ "--cwd" "$PWD"
577
+ "$@"
578
+ )
579
+
580
+ if [[ "$use_tee_flag" == "true" ]]; then
581
+ printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
582
+ else
583
+ printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG"
584
+ fi
585
+ }
586
+
587
+ run_legacy_gemini() {
588
+ local prompt="$1"
589
+ local use_tee_flag="$2"
590
+ local gemini_mcp_filter
591
+ gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
592
+ local gemini_args="$CLI_ARGS"
593
+
594
+ if [[ -n "$gemini_mcp_filter" ]]; then
595
+ gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
596
+ echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
597
+ fi
598
+
599
+ if [[ "$use_tee_flag" == "true" ]]; then
600
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
601
+ else
602
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
603
+ fi
604
+ local pid=$!
605
+
606
+ local health_ok=true
607
+ local intervals=(1 2 3 5 8)
608
+ for wait_sec in "${intervals[@]}"; do
609
+ sleep "$wait_sec"
610
+ if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
611
+ break
612
+ fi
613
+ if ! kill -0 "$pid" 2>/dev/null; then
614
+ health_ok=false
615
+ echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
616
+ break
617
+ fi
618
+ done
619
+
620
+ local exit_code_local=0
621
+ if [[ "$health_ok" == "false" ]]; then
622
+ wait "$pid" 2>/dev/null
623
+ echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
624
+ if [[ "$use_tee_flag" == "true" ]]; then
625
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
626
+ else
627
+ timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
628
+ fi
629
+ pid=$!
630
+ fi
631
+
632
+ wait "$pid" || exit_code_local=$?
633
+ return "$exit_code_local"
634
+ }
635
+
636
+ resolve_codex_mcp_script() {
637
+ if [[ -n "${TFX_CODEX_MCP_SCRIPT:-}" && -f "$TFX_CODEX_MCP_SCRIPT" ]]; then
638
+ printf '%s\n' "$TFX_CODEX_MCP_SCRIPT"
639
+ return 0
640
+ fi
641
+
642
+ local script_dir
643
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
644
+ local candidates=(
645
+ "$script_dir/hub/workers/codex-mcp.mjs"
646
+ "$script_dir/../hub/workers/codex-mcp.mjs"
647
+ )
648
+
649
+ local candidate
650
+ for candidate in "${candidates[@]}"; do
651
+ if [[ -f "$candidate" ]]; then
652
+ printf '%s\n' "$candidate"
653
+ return 0
654
+ fi
655
+ done
656
+
657
+ return 1
658
+ }
659
+
660
+ run_codex_exec() {
661
+ local prompt="$1"
662
+ local use_tee_flag="$2"
663
+ local exit_code_local=0
664
+
665
+ if [[ "$use_tee_flag" == "true" ]]; then
666
+ timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
667
+ else
668
+ timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
669
+ fi
670
+
671
+ if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
672
+ # stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
673
+ # 1차: "codex" 마커 기반 (Windows \r 제거 후 매칭)
674
+ sed 's/\r$//' "$STDERR_LOG" \
675
+ | awk '/^codex$/{found=NR;content=""} found && NR>found{content=content RS $0} END{if(content) print substr(content,2)}' \
676
+ > "$STDOUT_LOG"
677
+
678
+ # 2차: 마커 없을 때 node fallback (MCP/헤더/sandbox 로그 제외, 응답 부분만 추출)
679
+ if [[ ! -s "$STDOUT_LOG" ]]; then
680
+ node -e '
681
+ const fs=require("fs"),lines=fs.readFileSync(process.argv[1],"utf-8").split(/\r?\n/);
682
+ const skip=/^(mcp[: ]|OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|tokens used|EXIT:|exec$|"[A-Z]:|succeeded in |\s*$)/;
683
+ const out=lines.filter(l=>!skip.test(l));
684
+ if(out.length) fs.writeFileSync(process.argv[2],out.join("\n"));
685
+ ' -- "$STDERR_LOG" "$STDOUT_LOG" 2>/dev/null || true
686
+ fi
687
+
688
+ if [[ -s "$STDOUT_LOG" ]]; then
689
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr에서 응답 복구 ($(wc -c < "$STDOUT_LOG") bytes)" >&2
690
+ else
691
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패" >&2
692
+ fi
693
+ fi
694
+
695
+ return "$exit_code_local"
696
+ }
697
+
698
+ run_codex_mcp() {
699
+ local prompt="$1"
700
+ local use_tee_flag="$2"
701
+ local mcp_script node_bin
702
+ local exit_code_local=0
703
+
704
+ if ! mcp_script=$(resolve_codex_mcp_script); then
705
+ echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
706
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
707
+ fi
708
+
709
+ node_bin="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
710
+ if ! command -v "$node_bin" &>/dev/null; then
711
+ echo "[tfx-route] 경고: node를 찾지 못해 Codex MCP 경로를 사용할 수 없습니다." >&2
712
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
713
+ fi
714
+
715
+ local -a mcp_args=(
716
+ "$mcp_script"
717
+ "--prompt" "$prompt"
718
+ "--cwd" "$PWD"
719
+ "--profile" "$CLI_EFFORT"
720
+ "--approval-policy" "never"
721
+ "--sandbox" "danger-full-access"
722
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
723
+ "--codex-command" "$CODEX_BIN"
724
+ )
725
+
726
+ case "$AGENT_TYPE" in
727
+ code-reviewer)
728
+ mcp_args+=(
729
+ "--developer-instructions"
730
+ "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라."
731
+ )
732
+ ;;
733
+ security-reviewer)
734
+ mcp_args+=(
735
+ "--developer-instructions"
736
+ "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라."
737
+ )
738
+ ;;
739
+ quality-reviewer)
740
+ mcp_args+=(
741
+ "--developer-instructions"
742
+ "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라."
743
+ )
744
+ ;;
745
+ esac
746
+
747
+ if [[ "$use_tee_flag" == "true" ]]; then
748
+ timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
749
+ else
750
+ timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
751
+ fi
752
+
753
+ # 모듈 로드 실패(의존성 누락) → MCP transport exit code로 변환하여 fallback 트리거
754
+ if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
755
+ echo "[tfx-route] Codex MCP 모듈 로드 실패 — fallback 가능 exit code로 변환" >&2
756
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
757
+ fi
758
+
759
+ return "$exit_code_local"
760
+ }
761
+
437
762
  # ── 메인 실행 ──
438
763
  main() {
439
764
  # 종료 시 per-process 에이전트 파일 자동 삭제
@@ -447,11 +772,30 @@ main() {
447
772
  case "$CLI_CMD" in
448
773
  codex) CLI_CMD="$CODEX_BIN" ;;
449
774
  gemini) CLI_CMD="$GEMINI_BIN" ;;
775
+ claude) CLI_CMD="$CLAUDE_BIN" ;;
776
+ esac
777
+
778
+ # 타임아웃 결정 (에이전트별 최소값 보장)
779
+ local MIN_TIMEOUT
780
+ case "$AGENT_TYPE" in
781
+ deep-executor|architect|planner|critic|analyst) MIN_TIMEOUT=900 ;;
782
+ document-specialist|scientist|scientist-deep) MIN_TIMEOUT=900 ;;
783
+ code-reviewer|security-reviewer|quality-reviewer) MIN_TIMEOUT=600 ;;
784
+ executor|debugger) MIN_TIMEOUT=300 ;;
785
+ *) MIN_TIMEOUT=120 ;;
450
786
  esac
451
787
 
452
- # 타임아웃 결정
453
788
  if [[ -n "$USER_TIMEOUT" ]]; then
454
- TIMEOUT_SEC="$USER_TIMEOUT"
789
+ if ! [[ "$USER_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
790
+ echo "[tfx-route] 경고: 유효하지 않은 타임아웃 값 ($USER_TIMEOUT), 기본값 사용" >&2
791
+ USER_TIMEOUT=""
792
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
793
+ elif [[ "$USER_TIMEOUT" -lt "$MIN_TIMEOUT" ]]; then
794
+ echo "[tfx-route] 경고: 타임아웃 ${USER_TIMEOUT}s < 최소 ${MIN_TIMEOUT}s ($AGENT_TYPE), 최소값 적용" >&2
795
+ TIMEOUT_SEC="$MIN_TIMEOUT"
796
+ else
797
+ TIMEOUT_SEC="$USER_TIMEOUT"
798
+ fi
455
799
  else
456
800
  TIMEOUT_SEC="$DEFAULT_TIMEOUT"
457
801
  fi
@@ -467,22 +811,20 @@ ${ctx_content}
467
811
  </prior_context>"
468
812
  fi
469
813
 
814
+ # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
815
+ if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
816
+ if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
817
+ CLI_TYPE="claude"
818
+ CLI_CMD="$CLAUDE_BIN"
819
+ echo "[tfx-route] non-tty 팀 환경: claude-native -> claude stream wrapper 전환" >&2
820
+ else
821
+ echo "[tfx-route] claude stream wrapper 미사용: native metadata 유지" >&2
822
+ fi
823
+ fi
824
+
470
825
  # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
471
826
  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) 에이전트로 위임하세요 ---"
827
+ emit_claude_native_metadata
486
828
  exit 0
487
829
  fi
488
830
 
@@ -491,10 +833,17 @@ ${ctx_content}
491
833
  mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
492
834
  local FULL_PROMPT="$PROMPT"
493
835
  [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
836
+ local codex_transport_effective="n/a"
494
837
 
495
838
  # 메타정보 (stderr)
496
839
  echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
497
840
  echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE" >&2
841
+ if [[ -n "$TFX_WORKER_INDEX" || -n "$TFX_SEARCH_TOOL" ]]; then
842
+ echo "[tfx-route] worker_index=${TFX_WORKER_INDEX:-auto} search_tool=${TFX_SEARCH_TOOL:-auto}" >&2
843
+ fi
844
+ if [[ "$CLI_TYPE" == "codex" ]]; then
845
+ echo "[tfx-route] codex_transport_request=$TFX_CODEX_TRANSPORT" >&2
846
+ fi
498
847
  [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
499
848
 
500
849
  # Per-process 에이전트 등록
@@ -508,50 +857,94 @@ ${ctx_content}
508
857
  local start_time
509
858
  start_time=$(date +%s)
510
859
 
860
+ # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
861
+ # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
862
+ # 실시간 모니터링은 Shift+Down으로 워커 pane 전환 권장
863
+ local use_tee=false
864
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
865
+ if [[ -t 1 ]] || [[ -n "${TMUX:-}" ]]; then
866
+ use_tee=true
867
+ fi
868
+ fi
869
+
511
870
  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=$?
871
+ codex_transport_effective="exec"
872
+ if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
873
+ run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
874
+ if [[ "$exit_code" -eq 0 ]]; then
875
+ codex_transport_effective="mcp"
876
+ elif [[ "$exit_code" -eq "$CODEX_MCP_TRANSPORT_EXIT_CODE" && "$TFX_CODEX_TRANSPORT" == "auto" ]]; then
877
+ echo "[tfx-route] Codex MCP bootstrap 실패(exit=${exit_code}). legacy exec 경로로 fallback합니다." >&2
878
+ : > "$STDOUT_LOG"
879
+ : > "$STDERR_LOG"
880
+ exit_code=0
881
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
882
+ codex_transport_effective="exec-fallback"
883
+ else
884
+ codex_transport_effective="mcp"
885
+ fi
886
+ else
887
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
888
+ codex_transport_effective="exec"
889
+ fi
890
+ echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
514
891
 
515
892
  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
893
+ local gemini_model
894
+ gemini_model=$(awk '{
895
+ for (i = 1; i <= NF; i++) {
896
+ if ($i == "-m" || $i == "--model") {
897
+ print $(i + 1)
898
+ exit
899
+ }
900
+ }
901
+ }' <<< "$CLI_ARGS")
902
+ local gemini_servers
903
+ gemini_servers=$(get_gemini_mcp_servers "$MCP_PROFILE")
904
+ local -a gemini_worker_args=(
905
+ "--command" "$CLI_CMD"
906
+ "--command-args-json" "$GEMINI_BIN_ARGS_JSON"
907
+ "--model" "$gemini_model"
908
+ "--approval-mode" "yolo"
909
+ )
910
+
911
+ if [[ -n "$gemini_servers" ]]; then
912
+ echo "[tfx-route] Gemini MCP 서버: ${gemini_servers}" >&2
913
+ local server_name
914
+ for server_name in $gemini_servers; do
915
+ gemini_worker_args+=("--allowed-mcp-server-name" "$server_name")
916
+ done
523
917
  fi
524
918
 
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
919
+ run_stream_worker "gemini" "$FULL_PROMPT" "$use_tee" "${gemini_worker_args[@]}" || exit_code=$?
920
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
921
+ echo "[tfx-route] Gemini stream wrapper 실패(exit=${exit_code}). legacy CLI 경로로 fallback합니다." >&2
922
+ : > "$STDOUT_LOG"
923
+ : > "$STDERR_LOG"
924
+ exit_code=0
925
+ run_legacy_gemini "$FULL_PROMPT" "$use_tee" || exit_code=$?
926
+ fi
544
927
 
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=$?
928
+ elif [[ "$CLI_TYPE" == "claude" ]]; then
929
+ local claude_model
930
+ claude_model=$(get_claude_model)
931
+ local -a claude_worker_args=(
932
+ "--command" "$CLI_CMD"
933
+ "--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
934
+ "--model" "$claude_model"
935
+ "--permission-mode" "bypassPermissions"
936
+ "--allow-dangerously-skip-permissions"
937
+ )
938
+
939
+ run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
940
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
941
+ echo "[tfx-route] Claude stream wrapper 실패(exit=${exit_code}). native metadata로 fallback합니다." >&2
942
+ cat > "$STDOUT_LOG" <<EOF
943
+ $(emit_claude_native_metadata)
944
+ EOF
945
+ : > "$STDERR_LOG"
946
+ exit_code=0
947
+ CLI_TYPE="claude-native"
555
948
  fi
556
949
  fi
557
950
 
@@ -564,9 +957,9 @@ ${ctx_content}
564
957
  if [[ "$exit_code" -eq 0 ]]; then
565
958
  local output_preview
566
959
  output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
567
- team_complete_task "completed" "$output_preview"
960
+ team_complete_task "success" "$output_preview"
568
961
  elif [[ "$exit_code" -eq 124 ]]; then
569
- team_complete_task "failed" "타임아웃 (${TIMEOUT_SEC}초)"
962
+ team_complete_task "timeout" "타임아웃 (${TIMEOUT_SEC}초)"
570
963
  else
571
964
  local err_preview
572
965
  err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
@@ -591,7 +984,8 @@ ${ctx_content}
591
984
  --mcp-profile "$MCP_PROFILE" \
592
985
  --stderr-log "$STDERR_LOG" \
593
986
  --stdout-log "$STDOUT_LOG" \
594
- --max-bytes "$MAX_STDOUT_BYTES"
987
+ --max-bytes "$MAX_STDOUT_BYTES" \
988
+ --tee-active "$use_tee"
595
989
  else
596
990
  # post.mjs 없으면 기본 출력 (fallback)
597
991
  echo "=== TFX-ROUTE RESULT ==="