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.
- package/bin/triflux.mjs +581 -340
- package/hooks/keyword-rules.json +16 -0
- package/hub/bridge.mjs +410 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +512 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +59 -1459
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +12 -80
- package/hub/team/nativeProxy.mjs +121 -47
- package/hub/team/pane.mjs +66 -43
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +354 -291
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +41 -52
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +4 -2
- package/package.json +4 -1
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +128 -70
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +415 -80
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +53 -62
package/scripts/tfx-route.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# tfx-route.sh v2.
|
|
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.
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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\":\"${
|
|
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\":\"${
|
|
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}\",\"
|
|
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
|
-
|
|
445
|
+
get_gemini_mcp_servers() {
|
|
427
446
|
local profile="$1"
|
|
428
447
|
case "$profile" in
|
|
429
|
-
implement) echo "
|
|
430
|
-
analyze) echo "
|
|
431
|
-
review) echo "
|
|
432
|
-
docs) echo "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
echo "[tfx-route]
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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 "
|
|
901
|
+
team_complete_task "success" "$output_preview"
|
|
568
902
|
elif [[ "$exit_code" -eq 124 ]]; then
|
|
569
|
-
team_complete_task "
|
|
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 ==="
|