triflux 3.2.0-dev.8 → 3.2.0-dev.9

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 (42) hide show
  1. package/bin/triflux.mjs +581 -340
  2. package/hooks/keyword-rules.json +16 -0
  3. package/hub/bridge.mjs +410 -318
  4. package/hub/hitl.mjs +45 -31
  5. package/hub/pipe.mjs +457 -0
  6. package/hub/router.mjs +422 -161
  7. package/hub/server.mjs +429 -424
  8. package/hub/store.mjs +388 -314
  9. package/hub/team/cli-team-common.mjs +348 -0
  10. package/hub/team/cli-team-control.mjs +393 -0
  11. package/hub/team/cli-team-start.mjs +512 -0
  12. package/hub/team/cli-team-status.mjs +269 -0
  13. package/hub/team/cli.mjs +59 -1459
  14. package/hub/team/dashboard.mjs +1 -9
  15. package/hub/team/native.mjs +12 -80
  16. package/hub/team/nativeProxy.mjs +121 -47
  17. package/hub/team/pane.mjs +66 -43
  18. package/hub/team/psmux.mjs +297 -0
  19. package/hub/team/session.mjs +354 -291
  20. package/hub/team/shared.mjs +13 -0
  21. package/hub/team/staleState.mjs +299 -0
  22. package/hub/tools.mjs +41 -52
  23. package/hub/workers/claude-worker.mjs +446 -0
  24. package/hub/workers/codex-mcp.mjs +414 -0
  25. package/hub/workers/factory.mjs +18 -0
  26. package/hub/workers/gemini-worker.mjs +349 -0
  27. package/hub/workers/interface.mjs +41 -0
  28. package/hud/hud-qos-status.mjs +4 -2
  29. package/package.json +4 -1
  30. package/scripts/keyword-detector.mjs +15 -0
  31. package/scripts/lib/keyword-rules.mjs +4 -1
  32. package/scripts/psmux-steering-prototype.sh +368 -0
  33. package/scripts/setup.mjs +128 -70
  34. package/scripts/tfx-route-worker.mjs +161 -0
  35. package/scripts/tfx-route.sh +415 -80
  36. package/skills/tfx-auto/SKILL.md +90 -564
  37. package/skills/tfx-auto-codex/SKILL.md +1 -3
  38. package/skills/tfx-codex/SKILL.md +1 -4
  39. package/skills/tfx-doctor/SKILL.md +1 -0
  40. package/skills/tfx-gemini/SKILL.md +1 -4
  41. package/skills/tfx-setup/SKILL.md +1 -4
  42. package/skills/tfx-team/SKILL.md +53 -62
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # tfx-route.sh v2.0 — CLI 라우팅 래퍼 (triflux)
2
+ # tfx-route.sh v2.2 — 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.2"
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
 
@@ -242,6 +252,7 @@ route_agent() {
242
252
  # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
243
253
  TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
244
254
  TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
255
+ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
245
256
  case "$TFX_NO_CLAUDE_NATIVE" in
246
257
  0|1) ;;
247
258
  *)
@@ -249,6 +260,14 @@ case "$TFX_NO_CLAUDE_NATIVE" in
249
260
  exit 1
250
261
  ;;
251
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
252
271
 
253
272
  apply_cli_mode() {
254
273
  local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
@@ -423,17 +442,267 @@ get_mcp_hint() {
423
442
  }
424
443
 
425
444
  # ── Gemini MCP 서버 선택적 로드 ──
426
- get_gemini_mcp_filter() {
445
+ get_gemini_mcp_servers() {
427
446
  local profile="$1"
428
447
  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" ;;
448
+ implement) echo "context7 brave-search" ;;
449
+ analyze) echo "context7 brave-search exa" ;;
450
+ review) echo "sequential-thinking" ;;
451
+ docs) echo "context7 brave-search" ;;
433
452
  *) echo "" ;;
434
453
  esac
435
454
  }
436
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
+
437
706
  # ── 메인 실행 ──
438
707
  main() {
439
708
  # 종료 시 per-process 에이전트 파일 자동 삭제
@@ -447,11 +716,30 @@ main() {
447
716
  case "$CLI_CMD" in
448
717
  codex) CLI_CMD="$CODEX_BIN" ;;
449
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 ;;
450
730
  esac
451
731
 
452
- # 타임아웃 결정
453
732
  if [[ -n "$USER_TIMEOUT" ]]; then
454
- TIMEOUT_SEC="$USER_TIMEOUT"
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
455
743
  else
456
744
  TIMEOUT_SEC="$DEFAULT_TIMEOUT"
457
745
  fi
@@ -467,22 +755,20 @@ ${ctx_content}
467
755
  </prior_context>"
468
756
  fi
469
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
+
470
769
  # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
471
770
  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) 에이전트로 위임하세요 ---"
771
+ emit_claude_native_metadata
486
772
  exit 0
487
773
  fi
488
774
 
@@ -491,10 +777,14 @@ ${ctx_content}
491
777
  mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
492
778
  local FULL_PROMPT="$PROMPT"
493
779
  [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
780
+ local codex_transport_effective="n/a"
494
781
 
495
782
  # 메타정보 (stderr)
496
783
  echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
497
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
498
788
  [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
499
789
 
500
790
  # Per-process 에이전트 등록
@@ -508,50 +798,94 @@ ${ctx_content}
508
798
  local start_time
509
799
  start_time=$(date +%s)
510
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
+
511
811
  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=$?
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
514
832
 
515
833
  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
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
523
858
  fi
524
859
 
525
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
526
- local pid=$!
527
-
528
- # 지수 백오프 health check (v1.x: 30×1s → v2.0: 5×exp, 총 19초)
529
- local health_ok=true
530
- local intervals=(1 2 3 5 8)
531
- for wait_sec in "${intervals[@]}"; do
532
- sleep "$wait_sec"
533
- # 출력 있으면 정상 → 조기 탈출
534
- if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
535
- break
536
- fi
537
- # 프로세스 사망 + 출력 없음 → crash
538
- if ! kill -0 "$pid" 2>/dev/null; then
539
- health_ok=false
540
- echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
541
- break
542
- fi
543
- done
544
-
545
- if [[ "$health_ok" == "false" ]]; then
546
- wait "$pid" 2>/dev/null
547
- echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
548
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$FULL_PROMPT" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
549
- pid=$!
550
- wait "$pid"
551
- exit_code=$?
552
- else
553
- wait "$pid"
554
- exit_code=$?
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"
555
889
  fi
556
890
  fi
557
891
 
@@ -564,9 +898,9 @@ ${ctx_content}
564
898
  if [[ "$exit_code" -eq 0 ]]; then
565
899
  local output_preview
566
900
  output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
567
- team_complete_task "completed" "$output_preview"
901
+ team_complete_task "success" "$output_preview"
568
902
  elif [[ "$exit_code" -eq 124 ]]; then
569
- team_complete_task "failed" "타임아웃 (${TIMEOUT_SEC}초)"
903
+ team_complete_task "timeout" "타임아웃 (${TIMEOUT_SEC}초)"
570
904
  else
571
905
  local err_preview
572
906
  err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
@@ -591,7 +925,8 @@ ${ctx_content}
591
925
  --mcp-profile "$MCP_PROFILE" \
592
926
  --stderr-log "$STDERR_LOG" \
593
927
  --stdout-log "$STDOUT_LOG" \
594
- --max-bytes "$MAX_STDOUT_BYTES"
928
+ --max-bytes "$MAX_STDOUT_BYTES" \
929
+ --tee-active "$use_tee"
595
930
  else
596
931
  # post.mjs 없으면 기본 출력 (fallback)
597
932
  echo "=== TFX-ROUTE RESULT ==="