triflux 3.3.0-dev.8 → 4.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.
Files changed (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2427 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -684
  8. package/hub/delegator/contracts.mjs +38 -38
  9. package/hub/delegator/index.mjs +14 -14
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  11. package/hub/delegator/service.mjs +302 -118
  12. package/hub/delegator/tool-definitions.mjs +35 -35
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -367
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +3 -2
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -1,1326 +1,1360 @@
1
- #!/usr/bin/env bash
2
- # tfx-route.sh v2.3 — CLI 라우팅 래퍼 (triflux)
3
- #
4
- # v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
5
- # v2.0: tfx-route.sh 리네임
6
- # - 후처리 전부 tfx-route-post.mjs로 이관 (node 단일 ~100ms)
7
- # - per-process 에이전트 등록 (race condition 구조적 제거)
8
- # - get_mcp_hint 통합 (캐시/비캐시 단일 코드경로)
9
- # - Gemini health check 지수 백오프 (30×1s → 5×exp)
10
- # - 컨텍스트 파일 5번째 인자 지원
11
- #
12
- VERSION="2.3"
13
- #
14
- # 사용법:
15
- # tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
16
- #
17
- # 예시:
18
- # tfx-route.sh executor "코드 구현" implement
19
- # tfx-route.sh architect "아키텍처 분석" analyze '' context.md
20
-
21
- set -euo pipefail
22
-
23
- # ── 인자 파싱 ──
24
- AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
25
- PROMPT="${2:?프롬프트 필수}"
26
- MCP_PROFILE="${3:-auto}"
27
- USER_TIMEOUT="${4:-}"
28
- CONTEXT_FILE="${5:-}"
29
-
30
- # ── CLI 경로 해석 (Windows npm global 대응) ──
31
- NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
32
- CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
33
- GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
34
- CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
35
- GEMINI_BIN_ARGS_JSON="${GEMINI_BIN_ARGS_JSON:-[]}"
36
- CLAUDE_BIN_ARGS_JSON="${CLAUDE_BIN_ARGS_JSON:-[]}"
37
-
38
- # ── 상수 ──
39
- MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
40
- TIMESTAMP=$(date +%s)
41
- RUN_ID="${TIMESTAMP}-$$-${RANDOM}"
42
- STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${RUN_ID}-stderr.log"
43
- STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${RUN_ID}-stdout.log"
44
- TFX_TMP="${TMPDIR:-/tmp}"
45
-
46
- # ── 팀 환경변수 ──
47
- TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
48
- TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
49
- TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
50
- TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
51
- TFX_HUB_PIPE="${TFX_HUB_PIPE:-}"
52
- TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}" # bridge.mjs HTTP fallback hint
53
-
54
- # fallback 시 원래 에이전트 정보 보존
55
- ORIGINAL_AGENT=""
56
- ORIGINAL_CLI_ARGS=""
57
-
58
- # JSON 문자열 이스케이프:
59
- # - "\", """ 필수 이스케이프
60
- # - 제어문자 U+0000..U+001F 이스케이프
61
- # - 비ASCII 문자는 \uXXXX(또는 surrogate pair)로 강제
62
- json_escape() {
63
- local s="${1:-}"
64
-
65
- if command -v "$NODE_BIN" &>/dev/null; then
66
- "$NODE_BIN" -e '
67
- const input = process.argv[1] ?? "";
68
- let out = "";
69
- for (const ch of input) {
70
- const cp = ch.codePointAt(0);
71
- if (cp === 0x22) { out += "\\\""; continue; } // "
72
- if (cp === 0x5c) { out += "\\\\"; continue; } // \
73
- if (cp <= 0x1f) {
74
- if (cp === 0x08) { out += "\\b"; continue; }
75
- if (cp === 0x09) { out += "\\t"; continue; }
76
- if (cp === 0x0a) { out += "\\n"; continue; }
77
- if (cp === 0x0c) { out += "\\f"; continue; }
78
- if (cp === 0x0d) { out += "\\r"; continue; }
79
- out += `\\u${cp.toString(16).padStart(4, "0")}`;
80
- continue;
81
- }
82
- if (cp >= 0x20 && cp <= 0x7e) {
83
- out += ch;
84
- continue;
85
- }
86
- if (cp <= 0xffff) {
87
- out += `\\u${cp.toString(16).padStart(4, "0")}`;
88
- continue;
89
- }
90
- const v = cp - 0x10000;
91
- const hi = 0xd800 + (v >> 10);
92
- const lo = 0xdc00 + (v & 0x3ff);
93
- out += `\\u${hi.toString(16).padStart(4, "0")}\\u${lo.toString(16).padStart(4, "0")}`;
94
- }
95
- process.stdout.write(out);
96
- ' -- "$s"
97
- return
98
- fi
99
-
100
- echo "[tfx-route] ERROR: node 미설치로 안전한 JSON 이스케이프를 수행할 수 없습니다." >&2
101
- return 1
102
- }
103
-
104
- # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
105
- register_agent() {
106
- local agent_file="${TFX_TMP}/tfx-agent-$$.json"
107
- local safe_cli safe_agent started_at
108
- safe_cli=$(json_escape "$CLI_TYPE" 2>/dev/null || true)
109
- safe_agent=$(json_escape "$AGENT_TYPE" 2>/dev/null || true)
110
- started_at=$(date +%s)
111
-
112
- # fail-closed: 안전 인코딩 불가 시 agent 파일을 쓰지 않는다
113
- if [[ -n "$CLI_TYPE" && -z "$safe_cli" ]]; then
114
- return 0
115
- fi
116
- if [[ -n "$AGENT_TYPE" && -z "$safe_agent" ]]; then
117
- return 0
118
- fi
119
-
120
- printf '{"pid":%s,"cli":"%s","agent":"%s","started":%s}\n' "$$" "$safe_cli" "$safe_agent" "$started_at" \
121
- > "$agent_file" 2>/dev/null || true
122
- }
123
-
124
- deregister_agent() {
125
- rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
126
- }
127
-
128
- normalize_script_path() {
129
- local path="${1:-}"
130
- if [[ -z "$path" ]]; then
131
- return 0
132
- fi
133
-
134
- if command -v cygpath &>/dev/null; then
135
- case "$path" in
136
- [A-Za-z]:\\*|[A-Za-z]:/*)
137
- cygpath -u "$path"
138
- return 0
139
- ;;
140
- esac
141
- fi
142
-
143
- printf '%s\n' "$path"
144
- }
145
-
146
- # ── 팀 Hub Bridge 통신 ──
147
- resolve_bridge_script() {
148
- if [[ -n "${TFX_BRIDGE_SCRIPT:-}" && -f "$TFX_BRIDGE_SCRIPT" ]]; then
149
- printf '%s\n' "$TFX_BRIDGE_SCRIPT"
150
- return 0
151
- fi
152
-
153
- local script_dir
154
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
155
- local candidates=(
156
- "$script_dir/../hub/bridge.mjs"
157
- "$script_dir/hub/bridge.mjs"
158
- )
159
-
160
- local candidate
161
- for candidate in "${candidates[@]}"; do
162
- if [[ -f "$candidate" ]]; then
163
- printf '%s\n' "$candidate"
164
- return 0
165
- fi
166
- done
167
-
168
- return 1
169
- }
170
-
171
- bridge_cli() {
172
- if ! command -v "$NODE_BIN" &>/dev/null; then
173
- return 127
174
- fi
175
-
176
- local bridge_script
177
- if ! bridge_script=$(resolve_bridge_script); then
178
- return 127
179
- fi
180
-
181
- TFX_HUB_PIPE="$TFX_HUB_PIPE" TFX_HUB_URL="$TFX_HUB_URL" TFX_HUB_TOKEN="${TFX_HUB_TOKEN:-}" \
182
- "$NODE_BIN" "$bridge_script" "$@" 2>/dev/null
183
- }
184
-
185
- bridge_json_get() {
186
- local json="${1:-}"
187
- local path="${2:-}"
188
- [[ -z "$json" || -z "$path" ]] && return 1
189
-
190
- "$NODE_BIN" -e '
191
- const data = JSON.parse(process.argv[1] || "{}");
192
- const keys = String(process.argv[2] || "").split(".").filter(Boolean);
193
- let value = data;
194
- for (const key of keys) value = value?.[key];
195
- if (value === undefined || value === null) process.exit(1);
196
- process.stdout.write(typeof value === "object" ? JSON.stringify(value) : String(value));
197
- ' -- "$json" "$path" 2>/dev/null
198
- }
199
-
200
- bridge_json_stringify() {
201
- local mode="${1:-}"
202
- shift || true
203
-
204
- case "$mode" in
205
- metadata-patch)
206
- "$NODE_BIN" -e '
207
- process.stdout.write(JSON.stringify({
208
- result: process.argv[1] || "",
209
- summary: process.argv[2] || "",
210
- }));
211
- ' -- "${1:-}" "${2:-}"
212
- ;;
213
- task-result)
214
- "$NODE_BIN" -e '
215
- process.stdout.write(JSON.stringify({
216
- task_id: process.argv[1] || "",
217
- result: process.argv[2] || "",
218
- }));
219
- ' -- "${1:-}" "${2:-}"
220
- ;;
221
- *)
222
- return 1
223
- ;;
224
- esac
225
- }
226
-
227
- team_send_message() {
228
- local text="${1:-}"
229
- local summary="${2:-}"
230
- [[ -z "$TFX_TEAM_NAME" || -z "$text" ]] && return 0
231
-
232
- if ! bridge_cli_with_restart "팀 메시지 전송" "Hub 재시작 후 팀 메시지 전송 성공." \
233
- team-send-message \
234
- --team "$TFX_TEAM_NAME" \
235
- --from "$TFX_TEAM_AGENT_NAME" \
236
- --to "$TFX_TEAM_LEAD_NAME" \
237
- --text "$text" \
238
- --summary "${summary:-status update}"; then
239
- echo "[tfx-route] 경고: 팀 메시지 전송 실패 (team=$TFX_TEAM_NAME, to=$TFX_TEAM_LEAD_NAME)" >&2
240
- return 0
241
- fi
242
-
243
- return 0
244
- }
245
-
246
- # ── Hub 자동 재시작 (슬립 복귀 등으로 Hub 종료 시) ──
247
- try_restart_hub() {
248
- local hub_server script_dir hub_port
249
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
250
- hub_server="$script_dir/../hub/server.mjs"
251
-
252
- if [[ ! -f "$hub_server" ]]; then
253
- echo "[tfx-route] Hub 서버 스크립트 미발견: $hub_server" >&2
254
- return 1
255
- fi
256
-
257
- # TFX_HUB_URL에서 포트 추출 (기본 27888)
258
- hub_port="${TFX_HUB_URL##*:}"
259
- hub_port="${hub_port%%/*}"
260
- [[ -z "$hub_port" || "$hub_port" == "$TFX_HUB_URL" ]] && hub_port=27888
261
-
262
- echo "[tfx-route] Hub 미응답 — 자동 재시작 시도 (port=$hub_port)..." >&2
263
- TFX_HUB_PORT="$hub_port" "$NODE_BIN" "$hub_server" &>/dev/null &
264
- local hub_pid=$!
265
-
266
- # 최대 4초 대기 (0.5초 간격)
267
- local i
268
- for i in 1 2 3 4 5 6 7 8; do
269
- sleep 0.5
270
- if curl -sf "${TFX_HUB_URL}/status" >/dev/null 2>&1; then
271
- echo "[tfx-route] Hub 재시작 성공 (pid=$hub_pid)" >&2
272
- return 0
273
- fi
274
- done
275
-
276
- echo "[tfx-route] Hub 재시작 실패 — claim 없이 계속 실행" >&2
277
- return 1
278
- }
279
-
280
- bridge_cli_with_restart() {
281
- local action_label="${1:-bridge 호출}"
282
- local success_message="${2:-}"
283
- shift 2 || true
284
-
285
- if bridge_cli "$@" >/dev/null 2>&1; then
286
- return 0
287
- fi
288
-
289
- if ! try_restart_hub; then
290
- return 1
291
- fi
292
-
293
- if bridge_cli "$@" >/dev/null 2>&1; then
294
- [[ -n "$success_message" ]] && echo "[tfx-route] ${success_message}" >&2
295
- return 0
296
- fi
297
-
298
- echo "[tfx-route] 경고: Hub 재시작 후 ${action_label} 재시도 실패." >&2
299
- return 1
300
- }
301
-
302
- team_claim_task() {
303
- [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
304
- local response ok error_code error_message owner_before status_before
305
- response=$(bridge_cli team-task-update \
306
- --team "$TFX_TEAM_NAME" \
307
- --task-id "$TFX_TEAM_TASK_ID" \
308
- --claim \
309
- --owner "$TFX_TEAM_AGENT_NAME" \
310
- --status in_progress || true)
311
-
312
- ok=$(bridge_json_get "$response" "ok" || true)
313
- error_code=$(bridge_json_get "$response" "error.code" || true)
314
- error_message=$(bridge_json_get "$response" "error.message" || true)
315
- owner_before=$(bridge_json_get "$response" "error.details.task_before.owner" || true)
316
- status_before=$(bridge_json_get "$response" "error.details.task_before.status" || true)
317
-
318
- case "$ok:$error_code" in
319
- true:*) ;;
320
- false:CLAIM_CONFLICT)
321
- if [[ "$owner_before" == "$TFX_TEAM_AGENT_NAME" && "$status_before" == "in_progress" ]]; then
322
- echo "[tfx-route] 동일 owner(${TFX_TEAM_AGENT_NAME})가 이미 claim한 task ${TFX_TEAM_TASK_ID} — 계속 실행." >&2
323
- return 0
324
- fi
325
- echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨(owner=${owner_before:-unknown}, status=${status_before:-unknown}). 실행 중단." >&2
326
- team_send_message \
327
- "task ${TFX_TEAM_TASK_ID} claim conflict: owner=${owner_before:-unknown}, status=${status_before:-unknown}" \
328
- "task ${TFX_TEAM_TASK_ID} claim conflict"
329
- exit 0 ;;
330
- :|false:)
331
- # Hub 연결 실패 → 자동 재시작 시도 후 claim 재시도
332
- if try_restart_hub; then
333
- response=$(bridge_cli team-task-update \
334
- --team "$TFX_TEAM_NAME" \
335
- --task-id "$TFX_TEAM_TASK_ID" \
336
- --claim \
337
- --owner "$TFX_TEAM_AGENT_NAME" \
338
- --status in_progress || true)
339
- ok=$(bridge_json_get "$response" "ok" || true)
340
- if [[ "$ok" == "true" ]]; then
341
- echo "[tfx-route] Hub 재시작 후 claim 성공." >&2
342
- else
343
- echo "[tfx-route] 경고: Hub 재시작 후 claim 실패. claim 없이 계속 실행." >&2
344
- fi
345
- else
346
- echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2
347
- fi ;;
348
- *)
349
- echo "[tfx-route] 경고: Hub claim 실패 (${error_code:-unknown}${error_message:+: ${error_message}}). claim 없이 계속 실행." >&2 ;;
350
- esac
351
- }
352
-
353
- team_complete_task() {
354
- local result="${1:-success}" # success/failed/timeout
355
- local result_summary="${2:-작업 완료}"
356
- [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
357
-
358
- local summary_trimmed metadata_patch result_payload
359
- summary_trimmed=$(echo "$result_summary" | head -c 4096)
360
- metadata_patch=$(bridge_json_stringify metadata-patch "$result" "$summary_trimmed" 2>/dev/null || true)
361
- result_payload=$(bridge_json_stringify task-result "$TFX_TEAM_TASK_ID" "$result" 2>/dev/null || true)
362
-
363
- # task 상태: 항상 "completed" (Claude Code API는 "failed" 미지원)
364
- # 실제 결과는 metadata.result로 전달
365
- if [[ -n "$metadata_patch" ]]; then
366
- if ! bridge_cli_with_restart "팀 task 완료 보고" "Hub 재시작 후 팀 task 완료 보고 성공." \
367
- team-task-update \
368
- --team "$TFX_TEAM_NAME" \
369
- --task-id "$TFX_TEAM_TASK_ID" \
370
- --status completed \
371
- --owner "$TFX_TEAM_AGENT_NAME" \
372
- --metadata-patch "$metadata_patch"; then
373
- echo "[tfx-route] 경고: 팀 task 완료 보고 실패 (team=$TFX_TEAM_NAME, task=$TFX_TEAM_TASK_ID, result=$result)" >&2
374
- fi
375
- fi
376
-
377
- # 리드에게 메시지 전송
378
- team_send_message "$summary_trimmed" "task ${TFX_TEAM_TASK_ID} ${result}"
379
-
380
- # Hub result 발행 (poll_messages 채널 활성화)
381
- if [[ -n "$result_payload" ]]; then
382
- if ! bridge_cli_with_restart "Hub result 발행" "Hub 재시작 후 Hub result 발행 성공." \
383
- result \
384
- --agent "$TFX_TEAM_AGENT_NAME" \
385
- --topic task.result \
386
- --payload "$result_payload" \
387
- --trace "$TFX_TEAM_NAME"; then
388
- echo "[tfx-route] 경고: Hub result 발행 실패 (agent=$TFX_TEAM_AGENT_NAME, task=$TFX_TEAM_TASK_ID)" >&2
389
- fi
390
- fi
391
-
392
- # 로컬 결과 파일 백업 (세션 끊김 복구용)
393
- # Claude 재로그인 시 Agent 래퍼가 죽어도 이 파일로 결과 수집 가능
394
- local result_dir="${TFX_RESULT_DIR:-${HOME}/.claude/tfx-results/${TFX_TEAM_NAME}}"
395
- if mkdir -p "$result_dir" 2>/dev/null; then
396
- cat > "${result_dir}/${TFX_TEAM_TASK_ID}.json" 2>/dev/null <<RESULT_EOF
397
- {"taskId":"${TFX_TEAM_TASK_ID}","agent":"${TFX_TEAM_AGENT_NAME}","team":"${TFX_TEAM_NAME}","result":"${result}","summary":$(printf '%s' "$summary_trimmed" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.stringify(d)))" 2>/dev/null || echo '""'),"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
398
- RESULT_EOF
399
- [[ $? -eq 0 ]] && echo "[tfx-route] 결과 백업: ${result_dir}/${TFX_TEAM_TASK_ID}.json" >&2
400
- fi
401
- }
402
-
403
- capture_workspace_signature() {
404
- if ! command -v git &>/dev/null; then
405
- return 1
406
- fi
407
-
408
- if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
409
- return 1
410
- fi
411
-
412
- git status --short --untracked-files=all --ignore-submodules=all 2>/dev/null || return 1
413
- }
414
-
415
- # ── 라우팅 테이블 ──
416
- # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
417
- route_agent() {
418
- local agent="$1"
419
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
420
-
421
- case "$agent" in
422
- # ─── 구현 레인 ───
423
- executor)
424
- CLI_TYPE="codex"; CLI_CMD="codex"
425
- CLI_ARGS="exec ${codex_base}"
426
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
427
- build-fixer)
428
- CLI_TYPE="codex"; CLI_CMD="codex"
429
- CLI_ARGS="exec --profile fast ${codex_base}"
430
- CLI_EFFORT="fast"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
431
- debugger)
432
- CLI_TYPE="codex"; CLI_CMD="codex"
433
- CLI_ARGS="exec ${codex_base}"
434
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
435
- deep-executor)
436
- CLI_TYPE="codex"; CLI_CMD="codex"
437
- CLI_ARGS="exec --profile xhigh ${codex_base}"
438
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
439
-
440
- # ─── 설계/분석 레인 ───
441
- architect)
442
- CLI_TYPE="codex"; CLI_CMD="codex"
443
- CLI_ARGS="exec --profile xhigh ${codex_base}"
444
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
445
- planner)
446
- CLI_TYPE="codex"; CLI_CMD="codex"
447
- CLI_ARGS="exec --profile xhigh ${codex_base}"
448
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
449
- critic)
450
- CLI_TYPE="codex"; CLI_CMD="codex"
451
- CLI_ARGS="exec --profile xhigh ${codex_base}"
452
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
453
- analyst)
454
- CLI_TYPE="codex"; CLI_CMD="codex"
455
- CLI_ARGS="exec --profile xhigh ${codex_base}"
456
- CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
457
-
458
- # ─── 리뷰 레인 ───
459
- code-reviewer)
460
- CLI_TYPE="codex"; CLI_CMD="codex"
461
- CLI_ARGS="exec --profile thorough ${codex_base} review"
462
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
463
- security-reviewer)
464
- CLI_TYPE="codex"; CLI_CMD="codex"
465
- CLI_ARGS="exec --profile thorough ${codex_base} review"
466
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
467
- quality-reviewer)
468
- CLI_TYPE="codex"; CLI_CMD="codex"
469
- CLI_ARGS="exec --profile thorough ${codex_base} review"
470
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
471
-
472
- # ─── 리서치 레인 ───
473
- scientist)
474
- CLI_TYPE="codex"; CLI_CMD="codex"
475
- CLI_ARGS="exec ${codex_base}"
476
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
477
- scientist-deep)
478
- CLI_TYPE="codex"; CLI_CMD="codex"
479
- CLI_ARGS="exec --profile thorough ${codex_base}"
480
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
481
- document-specialist)
482
- CLI_TYPE="codex"; CLI_CMD="codex"
483
- CLI_ARGS="exec ${codex_base}"
484
- CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
485
-
486
- # ─── UI/문서 레인 ───
487
- designer)
488
- CLI_TYPE="gemini"; CLI_CMD="gemini"
489
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
490
- CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
491
- writer)
492
- CLI_TYPE="gemini"; CLI_CMD="gemini"
493
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
494
- CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
495
-
496
- # ─── Claude 네이티브 ───
497
- explore)
498
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
499
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
500
- verifier)
501
- CLI_TYPE="codex"; CLI_CMD="codex"
502
- CLI_ARGS="exec --profile thorough ${codex_base} review"
503
- CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
504
- test-engineer)
505
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
506
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
507
- qa-tester)
508
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
509
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
510
-
511
- # ─── 경량 ───
512
- spark)
513
- CLI_TYPE="codex"; CLI_CMD="codex"
514
- CLI_ARGS="exec --profile spark_fast ${codex_base}"
515
- CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
516
- *)
517
- echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
518
- echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
519
- echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
520
- echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
521
- exit 1 ;;
522
- esac
523
- }
524
-
525
- # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
526
- TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
527
- TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
528
- TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
529
- TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
530
- TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
531
- TFX_SEARCH_TOOL="${TFX_SEARCH_TOOL:-}"
532
- case "$TFX_NO_CLAUDE_NATIVE" in
533
- 0|1) ;;
534
- *)
535
- echo "ERROR: TFX_NO_CLAUDE_NATIVE 값은 0 또는 1이어야 합니다. (현재: $TFX_NO_CLAUDE_NATIVE)" >&2
536
- exit 1
537
- ;;
538
- esac
539
- case "$TFX_CODEX_TRANSPORT" in
540
- auto|mcp|exec) ;;
541
- *)
542
- echo "ERROR: TFX_CODEX_TRANSPORT 값은 auto, mcp, exec 중 하나여야 합니다. (현재: $TFX_CODEX_TRANSPORT)" >&2
543
- exit 1
544
- ;;
545
- esac
546
- case "$TFX_VERIFIER_OVERRIDE" in
547
- auto|claude) ;;
548
- *)
549
- echo "ERROR: TFX_VERIFIER_OVERRIDE 값은 auto 또는 claude여야 합니다. (현재: $TFX_VERIFIER_OVERRIDE)" >&2
550
- exit 1
551
- ;;
552
- esac
553
- case "$TFX_WORKER_INDEX" in
554
- "") ;;
555
- *[!0-9]*|0)
556
- echo "ERROR: TFX_WORKER_INDEX 값은 1 이상의 정수여야 합니다. (현재: $TFX_WORKER_INDEX)" >&2
557
- exit 1
558
- ;;
559
- esac
560
- case "$TFX_SEARCH_TOOL" in
561
- ""|brave-search|tavily|exa) ;;
562
- *)
563
- echo "ERROR: TFX_SEARCH_TOOL 값은 brave-search, tavily, exa 중 하나여야 합니다. (현재: $TFX_SEARCH_TOOL)" >&2
564
- exit 1
565
- ;;
566
- esac
567
- CODEX_MCP_TRANSPORT_EXIT_CODE=70
568
-
569
- apply_cli_mode() {
570
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
571
-
572
- case "$TFX_CLI_MODE" in
573
- codex)
574
- if [[ "$CLI_TYPE" == "gemini" ]]; then
575
- CLI_TYPE="codex"; CLI_CMD="codex"
576
- case "$AGENT_TYPE" in
577
- designer)
578
- CLI_ARGS="exec ${codex_base}"; CLI_EFFORT="high"; DEFAULT_TIMEOUT=600 ;;
579
- writer)
580
- CLI_ARGS="exec --profile spark_fast ${codex_base}"; CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180 ;;
581
- esac
582
- echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
583
- fi ;;
584
- gemini)
585
- if [[ "$CLI_TYPE" == "codex" ]]; then
586
- CLI_TYPE="gemini"; CLI_CMD="gemini"
587
- case "$AGENT_TYPE" in
588
- executor|debugger|deep-executor|architect|planner|critic|analyst|\
589
- code-reviewer|security-reviewer|quality-reviewer|scientist-deep)
590
- CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"; CLI_EFFORT="pro" ;;
591
- build-fixer|spark)
592
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash"; DEFAULT_TIMEOUT=180 ;;
593
- *)
594
- CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash" ;;
595
- esac
596
- echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
597
- fi ;;
598
- auto)
599
- if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
600
- if command -v "$GEMINI_BIN" &>/dev/null; then
601
- TFX_CLI_MODE="gemini"; apply_cli_mode; return
602
- else
603
- ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
604
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
605
- echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE claude-native fallback" >&2
606
- fi
607
- elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
608
- if command -v "$CODEX_BIN" &>/dev/null; then
609
- TFX_CLI_MODE="codex"; apply_cli_mode; return
610
- else
611
- ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
612
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
613
- echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
614
- fi
615
- fi ;;
616
- esac
617
- }
618
-
619
- # ── Claude 네이티브 제거 (Codex 리드 환경에서 선택적 활성화) ──
620
- apply_no_claude_native_mode() {
621
- local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
622
-
623
- [[ "$TFX_NO_CLAUDE_NATIVE" != "1" ]] && return
624
- [[ "$TFX_CLI_MODE" == "gemini" ]] && return
625
- [[ "$CLI_TYPE" != "claude-native" ]] && return
626
-
627
- if ! command -v "$CODEX_BIN" &>/dev/null; then
628
- echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1 이지만 codex를 찾지 못해 claude-native 유지" >&2
629
- return
630
- fi
631
-
632
- ORIGINAL_AGENT="${AGENT_TYPE}"
633
- CLI_TYPE="codex"; CLI_CMD="codex"
634
-
635
- case "$AGENT_TYPE" in
636
- explore)
637
- CLI_ARGS="exec --profile fast ${codex_base}"
638
- CLI_EFFORT="fast"
639
- DEFAULT_TIMEOUT=600
640
- RUN_MODE="fg"
641
- OPUS_OVERSIGHT="false"
642
- ;;
643
- verifier)
644
- CLI_ARGS="exec --profile thorough ${codex_base} review"
645
- CLI_EFFORT="thorough"
646
- DEFAULT_TIMEOUT=1200
647
- RUN_MODE="fg"
648
- OPUS_OVERSIGHT="false"
649
- ;;
650
- test-engineer)
651
- CLI_ARGS="exec ${codex_base}"
652
- CLI_EFFORT="high"
653
- DEFAULT_TIMEOUT=1200
654
- RUN_MODE="bg"
655
- OPUS_OVERSIGHT="false"
656
- ;;
657
- qa-tester)
658
- CLI_ARGS="exec --profile thorough ${codex_base} review"
659
- CLI_EFFORT="thorough"
660
- DEFAULT_TIMEOUT=1200
661
- RUN_MODE="bg"
662
- OPUS_OVERSIGHT="false"
663
- ;;
664
- *)
665
- # claude-native 타입 중 위에 없는 경우는 보수적으로 유지
666
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
667
- return
668
- ;;
669
- esac
670
-
671
- echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
672
- }
673
-
674
- apply_verifier_override() {
675
- [[ "$AGENT_TYPE" != "verifier" ]] && return
676
-
677
- case "$TFX_VERIFIER_OVERRIDE" in
678
- auto|"")
679
- return 0
680
- ;;
681
- claude)
682
- ORIGINAL_AGENT="${ORIGINAL_AGENT:-$AGENT_TYPE}"
683
- CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
684
- CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
685
- echo "[tfx-route] TFX_VERIFIER_OVERRIDE=claude: verifier -> claude-native" >&2
686
- ;;
687
- esac
688
-
689
- return 0
690
- }
691
-
692
- # ── MCP 인벤토리 캐시 ──
693
- MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
694
- MCP_FILTER_SCRIPT=""
695
- MCP_PROFILE_REQUESTED="auto"
696
- MCP_RESOLVED_PROFILE="default"
697
- MCP_HINT=""
698
- GEMINI_ALLOWED_SERVERS=()
699
- CODEX_CONFIG_FLAGS=()
700
- CODEX_CONFIG_JSON=""
701
-
702
- get_cached_servers() {
703
- local cli_type="$1"
704
- if [[ -f "$MCP_CACHE" ]]; then
705
- 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
706
- fi
707
- }
708
-
709
- resolve_mcp_filter_script() {
710
- if [[ -n "$MCP_FILTER_SCRIPT" && -f "$MCP_FILTER_SCRIPT" ]]; then
711
- printf '%s\n' "$MCP_FILTER_SCRIPT"
712
- return 0
713
- fi
714
-
715
- local script_ref script_dir candidate
716
- local -a candidates=()
717
-
718
- script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
719
- if [[ -n "$script_ref" ]]; then
720
- script_dir="$(cd "$(dirname "$script_ref")" 2>/dev/null && pwd -P || true)"
721
- [[ -n "$script_dir" ]] && candidates+=("$script_dir/lib/mcp-filter.mjs")
722
- fi
723
-
724
- candidates+=(
725
- "$PWD/scripts/lib/mcp-filter.mjs"
726
- "$PWD/lib/mcp-filter.mjs"
727
- )
728
-
729
- for candidate in "${candidates[@]}"; do
730
- if [[ -f "$candidate" ]]; then
731
- MCP_FILTER_SCRIPT="$candidate"
732
- printf '%s\n' "$MCP_FILTER_SCRIPT"
733
- return 0
734
- fi
735
- done
736
-
737
- return 1
738
- }
739
-
740
- resolve_mcp_policy() {
741
- local filter_script available_servers
742
- if ! filter_script=$(resolve_mcp_filter_script); then
743
- echo "[tfx-route] 경고: mcp-filter.mjs를 찾지 못해 기본 MCP 정책을 사용합니다." >&2
744
- MCP_PROFILE_REQUESTED="$MCP_PROFILE"
745
- MCP_RESOLVED_PROFILE="$MCP_PROFILE"
746
- MCP_HINT=""
747
- GEMINI_ALLOWED_SERVERS=()
748
- CODEX_CONFIG_FLAGS=()
749
- CODEX_CONFIG_JSON=""
750
- return 0
751
- fi
752
-
753
- available_servers=$(get_cached_servers "$CLI_TYPE")
754
- [[ -z "$available_servers" ]] && available_servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
755
-
756
- local -a cmd=(
757
- "$NODE_BIN" "$filter_script" shell
758
- "--agent" "$AGENT_TYPE"
759
- "--profile" "$MCP_PROFILE"
760
- "--available" "$available_servers"
761
- "--inventory-file" "$MCP_CACHE"
762
- "--task-text" "$PROMPT"
763
- )
764
- [[ -n "$TFX_SEARCH_TOOL" ]] && cmd+=("--search-tool" "$TFX_SEARCH_TOOL")
765
- [[ -n "$TFX_WORKER_INDEX" ]] && cmd+=("--worker-index" "$TFX_WORKER_INDEX")
766
-
767
- local shell_exports
768
- if ! shell_exports="$("${cmd[@]}")"; then
769
- echo "[tfx-route] ERROR: MCP 정책 계산 실패" >&2
770
- return 1
771
- fi
772
-
773
- eval "$shell_exports"
774
- }
775
-
776
- get_claude_model() {
777
- case "$AGENT_TYPE" in
778
- explore) echo "haiku" ;;
779
- *) echo "sonnet" ;;
780
- esac
781
- }
782
-
783
- emit_claude_native_metadata() {
784
- local model
785
- model=$(get_claude_model)
786
- echo "ROUTE_TYPE=claude-native"
787
- echo "AGENT=$AGENT_TYPE"
788
- echo "MODEL=$model"
789
- echo "RUN_MODE=$RUN_MODE"
790
- echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
791
- echo "TIMEOUT=$TIMEOUT_SEC"
792
- echo "MCP_PROFILE=$MCP_PROFILE"
793
- [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
794
- echo "PROMPT=$PROMPT"
795
- echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
796
- }
797
-
798
- resolve_worker_runner_script() {
799
- if [[ -n "${TFX_ROUTE_WORKER_RUNNER:-}" && -f "$TFX_ROUTE_WORKER_RUNNER" ]]; then
800
- printf '%s\n' "$TFX_ROUTE_WORKER_RUNNER"
801
- return 0
802
- fi
803
-
804
- local script_ref script_dir
805
- script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
806
- script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
807
- local candidate="$script_dir/tfx-route-worker.mjs"
808
- [[ -f "$candidate" ]] || return 1
809
- printf '%s\n' "$candidate"
810
- }
811
-
812
- run_stream_worker() {
813
- local worker_type="$1"
814
- local prompt="$2"
815
- local use_tee_flag="$3"
816
- shift 3
817
-
818
- local runner_script
819
- if ! runner_script=$(resolve_worker_runner_script); then
820
- echo "[tfx-route] 경고: stream worker runner를 찾지 못했습니다." >&2
821
- return 127
822
- fi
823
-
824
- if ! command -v "$NODE_BIN" &>/dev/null; then
825
- echo "[tfx-route] 경고: node를 찾지 못해 stream worker를 실행할 수 없습니다." >&2
826
- return 127
827
- fi
828
-
829
- local -a worker_cmd=(
830
- "$NODE_BIN"
831
- "$runner_script"
832
- "--type" "$worker_type"
833
- "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
834
- "--cwd" "$PWD"
835
- "$@"
836
- )
837
-
838
- if [[ "$use_tee_flag" == "true" ]]; then
839
- printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
840
- else
841
- printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG"
842
- fi
843
- }
844
-
845
- run_legacy_gemini() {
846
- local prompt="$1"
847
- local use_tee_flag="$2"
848
- local -a gemini_args=()
849
- read -r -a gemini_args <<< "$CLI_ARGS"
850
-
851
- if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
852
- local gemini_mcp_filter prompt_index=-1
853
- gemini_mcp_filter=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")
854
- for i in "${!gemini_args[@]}"; do
855
- if [[ "${gemini_args[$i]}" == "--prompt" ]]; then
856
- prompt_index="$i"
857
- break
858
- fi
859
- done
860
- if [[ "$prompt_index" -ge 0 ]]; then
861
- gemini_args=(
862
- "${gemini_args[@]:0:$prompt_index}"
863
- "--allowed-mcp-server-names" "$gemini_mcp_filter"
864
- "${gemini_args[@]:$prompt_index}"
865
- )
866
- echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
867
- fi
868
- fi
869
-
870
- if [[ "$use_tee_flag" == "true" ]]; then
871
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
872
- else
873
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
874
- fi
875
- local pid=$!
876
-
877
- local health_ok=true
878
- local intervals=(1 2 3 5 8)
879
- for wait_sec in "${intervals[@]}"; do
880
- sleep "$wait_sec"
881
- if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
882
- break
883
- fi
884
- if ! kill -0 "$pid" 2>/dev/null; then
885
- health_ok=false
886
- echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
887
- break
888
- fi
889
- done
890
-
891
- local exit_code_local=0
892
- if [[ "$health_ok" == "false" ]]; then
893
- wait "$pid" 2>/dev/null
894
- echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
895
- if [[ "$use_tee_flag" == "true" ]]; then
896
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
897
- else
898
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
899
- fi
900
- pid=$!
901
- fi
902
-
903
- wait "$pid" || exit_code_local=$?
904
- return "$exit_code_local"
905
- }
906
-
907
- resolve_codex_mcp_script() {
908
- if [[ -n "${TFX_CODEX_MCP_SCRIPT:-}" && -f "$TFX_CODEX_MCP_SCRIPT" ]]; then
909
- printf '%s\n' "$TFX_CODEX_MCP_SCRIPT"
910
- return 0
911
- fi
912
-
913
- local script_ref script_dir
914
- script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
915
- script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
916
- local candidates=(
917
- "$script_dir/hub/workers/codex-mcp.mjs"
918
- "$script_dir/../hub/workers/codex-mcp.mjs"
919
- )
920
-
921
- local candidate
922
- for candidate in "${candidates[@]}"; do
923
- if [[ -f "$candidate" ]]; then
924
- printf '%s\n' "$candidate"
925
- return 0
926
- fi
927
- done
928
-
929
- return 1
930
- }
931
-
932
- run_codex_exec() {
933
- local prompt="$1"
934
- local use_tee_flag="$2"
935
- local exit_code_local=0
936
- local -a codex_args=()
937
- read -r -a codex_args <<< "$CLI_ARGS"
938
- if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
939
- codex_args+=("${CODEX_CONFIG_FLAGS[@]}")
940
- fi
941
-
942
- if [[ "$use_tee_flag" == "true" ]]; then
943
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
944
- else
945
- timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
946
- fi
947
-
948
- if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
949
- # stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
950
- # 1차: "codex" 마커 기반 (Windows \r 제거 후 매칭)
951
- sed 's/\r$//' "$STDERR_LOG" \
952
- | awk '/^codex$/{found=NR;content=""} found && NR>found{content=content RS $0} END{if(content) print substr(content,2)}' \
953
- > "$STDOUT_LOG"
954
-
955
- # 2차: 마커 없을 때 node fallback (MCP/헤더/sandbox 로그 제외, 응답 부분만 추출)
956
- if [[ ! -s "$STDOUT_LOG" ]]; then
957
- node -e '
958
- const fs=require("fs"),lines=fs.readFileSync(process.argv[1],"utf-8").split(/\r?\n/);
959
- const skip=/^(mcp[: ]|OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|tokens used|EXIT:|exec$|"[A-Z]:|succeeded in |\s*$)/;
960
- const out=lines.filter(l=>!skip.test(l));
961
- if(out.length) fs.writeFileSync(process.argv[2],out.join("\n"));
962
- ' -- "$STDERR_LOG" "$STDOUT_LOG" 2>/dev/null || true
963
- fi
964
-
965
- if [[ -s "$STDOUT_LOG" ]]; then
966
- echo "[tfx-route] 경고: codex stdout 비어있음, stderr에서 응답 복구 ($(wc -c < "$STDOUT_LOG") bytes)" >&2
967
- else
968
- echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패" >&2
969
- fi
970
- fi
971
-
972
- return "$exit_code_local"
973
- }
974
-
975
- run_codex_mcp() {
976
- local prompt="$1"
977
- local use_tee_flag="$2"
978
- local mcp_script node_bin
979
- local exit_code_local=0
980
-
981
- if ! mcp_script=$(resolve_codex_mcp_script); then
982
- echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
983
- return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
984
- fi
985
-
986
- node_bin="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
987
- if ! command -v "$node_bin" &>/dev/null; then
988
- echo "[tfx-route] 경고: node를 찾지 못해 Codex MCP 경로를 사용할 없습니다." >&2
989
- return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
990
- fi
991
-
992
- local -a mcp_args=(
993
- "$mcp_script"
994
- "--prompt" "$prompt"
995
- "--cwd" "$PWD"
996
- "--profile" "$CLI_EFFORT"
997
- "--approval-policy" "never"
998
- "--sandbox" "danger-full-access"
999
- "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
1000
- "--codex-command" "$CODEX_BIN"
1001
- )
1002
-
1003
- if [[ -n "$CODEX_CONFIG_JSON" && "$CODEX_CONFIG_JSON" != "{}" ]]; then
1004
- mcp_args+=("--config-json" "$CODEX_CONFIG_JSON")
1005
- fi
1006
-
1007
- case "$AGENT_TYPE" in
1008
- code-reviewer)
1009
- mcp_args+=(
1010
- "--developer-instructions"
1011
- "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라."
1012
- )
1013
- ;;
1014
- security-reviewer)
1015
- mcp_args+=(
1016
- "--developer-instructions"
1017
- "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라."
1018
- )
1019
- ;;
1020
- quality-reviewer)
1021
- mcp_args+=(
1022
- "--developer-instructions"
1023
- "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라."
1024
- )
1025
- ;;
1026
- esac
1027
-
1028
- if [[ "$use_tee_flag" == "true" ]]; then
1029
- timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
1030
- else
1031
- timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
1032
- fi
1033
-
1034
- # 모듈 로드 실패(의존성 누락) → MCP transport exit code로 변환하여 fallback 트리거
1035
- if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
1036
- echo "[tfx-route] Codex MCP 모듈 로드 실패 fallback 가능 exit code로 변환" >&2
1037
- return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1038
- fi
1039
-
1040
- return "$exit_code_local"
1041
- }
1042
-
1043
- # ── 메인 실행 ──
1044
- main() {
1045
- # 종료 시 per-process 에이전트 파일 자동 삭제
1046
- trap 'deregister_agent' EXIT
1047
-
1048
- route_agent "$AGENT_TYPE"
1049
- apply_cli_mode
1050
- apply_no_claude_native_mode
1051
- apply_verifier_override
1052
-
1053
- # CLI 경로 해석
1054
- case "$CLI_CMD" in
1055
- codex) CLI_CMD="$CODEX_BIN" ;;
1056
- gemini) CLI_CMD="$GEMINI_BIN" ;;
1057
- claude) CLI_CMD="$CLAUDE_BIN" ;;
1058
- esac
1059
-
1060
- # 타임아웃 결정 (에이전트별 최소값 보장)
1061
- local MIN_TIMEOUT
1062
- case "$AGENT_TYPE" in
1063
- deep-executor|architect|planner|critic|analyst) MIN_TIMEOUT=900 ;;
1064
- document-specialist|scientist|scientist-deep) MIN_TIMEOUT=900 ;;
1065
- code-reviewer|security-reviewer|quality-reviewer) MIN_TIMEOUT=600 ;;
1066
- executor|debugger) MIN_TIMEOUT=300 ;;
1067
- *) MIN_TIMEOUT=120 ;;
1068
- esac
1069
-
1070
- if [[ -n "$USER_TIMEOUT" ]]; then
1071
- if ! [[ "$USER_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
1072
- echo "[tfx-route] 경고: 유효하지 않은 타임아웃 값 ($USER_TIMEOUT), 기본값 사용" >&2
1073
- USER_TIMEOUT=""
1074
- TIMEOUT_SEC="$DEFAULT_TIMEOUT"
1075
- elif [[ "$USER_TIMEOUT" -lt "$MIN_TIMEOUT" ]]; then
1076
- echo "[tfx-route] 경고: 타임아웃 ${USER_TIMEOUT}s < 최소 ${MIN_TIMEOUT}s ($AGENT_TYPE), 최소값 적용" >&2
1077
- TIMEOUT_SEC="$MIN_TIMEOUT"
1078
- else
1079
- TIMEOUT_SEC="$USER_TIMEOUT"
1080
- fi
1081
- else
1082
- TIMEOUT_SEC="$DEFAULT_TIMEOUT"
1083
- fi
1084
-
1085
- # 컨텍스트 파일 → 프롬프트에 주입
1086
- if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
1087
- local ctx_content
1088
- ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
1089
- PROMPT="${PROMPT}
1090
-
1091
- <prior_context>
1092
- ${ctx_content}
1093
- </prior_context>"
1094
- fi
1095
-
1096
- resolve_mcp_policy
1097
-
1098
- # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
1099
- if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
1100
- if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
1101
- CLI_TYPE="claude"
1102
- CLI_CMD="$CLAUDE_BIN"
1103
- echo "[tfx-route] non-tty 팀 환경: claude-native -> claude stream wrapper 전환" >&2
1104
- else
1105
- echo "[tfx-route] claude stream wrapper 미사용: native metadata 유지" >&2
1106
- fi
1107
- fi
1108
-
1109
- # Claude 네이티브 에이전트는 스크립트로 처리 불가
1110
- if [[ "$CLI_TYPE" == "claude-native" ]]; then
1111
- if [[ -n "$TFX_TEAM_NAME" ]]; then
1112
- # 팀 모드: Hub에 fallback 필요 시그널 전송 후 구조화된 출력
1113
- echo "[tfx-route] claude-native 역할($AGENT_TYPE)은 tfx-route.sh로 실행 불가 — Claude Agent fallback 필요" >&2
1114
- team_complete_task "fallback" "claude-native 역할 실행 불가: ${AGENT_TYPE}. Claude Task(sonnet) 에이전트로 위임하세요."
1115
- cat <<FALLBACK_EOF
1116
- === TFX_NEEDS_FALLBACK ===
1117
- agent_type: ${AGENT_TYPE}
1118
- reason: claude-native roles require Claude Agent tools (Read/Edit/Grep). tfx-route.sh cannot provide these.
1119
- action: Lead should spawn Agent(subagent_type="${AGENT_TYPE}") for this task.
1120
- task_id: ${TFX_TEAM_TASK_ID:-none}
1121
- FALLBACK_EOF
1122
- exit 0
1123
- fi
1124
- emit_claude_native_metadata
1125
- exit 0
1126
- fi
1127
-
1128
- local FULL_PROMPT="$PROMPT"
1129
- [[ -n "$MCP_HINT" ]] && FULL_PROMPT="${PROMPT}. ${MCP_HINT}"
1130
- local codex_transport_effective="n/a"
1131
-
1132
- # 메타정보 (stderr)
1133
- echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
1134
- echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE resolved_profile=$MCP_RESOLVED_PROFILE verifier_override=$TFX_VERIFIER_OVERRIDE" >&2
1135
- if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1136
- echo "[tfx-route] allowed_mcp_servers=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1137
- else
1138
- echo "[tfx-route] allowed_mcp_servers=none" >&2
1139
- fi
1140
- if [[ -n "$TFX_WORKER_INDEX" || -n "$TFX_SEARCH_TOOL" ]]; then
1141
- echo "[tfx-route] worker_index=${TFX_WORKER_INDEX:-auto} search_tool=${TFX_SEARCH_TOOL:-auto}" >&2
1142
- fi
1143
- if [[ "$CLI_TYPE" == "codex" ]]; then
1144
- echo "[tfx-route] codex_transport_request=$TFX_CODEX_TRANSPORT" >&2
1145
- fi
1146
- [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
1147
-
1148
- # Per-process 에이전트 등록
1149
- register_agent
1150
-
1151
- # 팀 모드: task claim
1152
- team_claim_task
1153
- team_send_message "작업 시작: ${TFX_TEAM_AGENT_NAME}" "task ${TFX_TEAM_TASK_ID} started"
1154
-
1155
- # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
1156
- local exit_code=0
1157
- local start_time
1158
- start_time=$(date +%s)
1159
- local workspace_signature_before=""
1160
- local workspace_signature_after=""
1161
- local workspace_probe_supported=false
1162
- if workspace_signature_before=$(capture_workspace_signature); then
1163
- workspace_probe_supported=true
1164
- fi
1165
-
1166
- # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
1167
- # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
1168
- # 실시간 모니터링은 Shift+Down으로 워커 pane 전환 권장
1169
- local use_tee=false
1170
- if [[ -n "$TFX_TEAM_NAME" ]]; then
1171
- if [[ -t 1 ]] || [[ -n "${TMUX:-}" ]]; then
1172
- use_tee=true
1173
- fi
1174
- fi
1175
-
1176
- if [[ "$CLI_TYPE" == "codex" ]]; then
1177
- codex_transport_effective="exec"
1178
- if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
1179
- run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
1180
- if [[ "$exit_code" -eq 0 ]]; then
1181
- codex_transport_effective="mcp"
1182
- elif [[ "$exit_code" -eq "$CODEX_MCP_TRANSPORT_EXIT_CODE" && "$TFX_CODEX_TRANSPORT" == "auto" ]]; then
1183
- echo "[tfx-route] Codex MCP bootstrap 실패(exit=${exit_code}). legacy exec 경로로 fallback합니다." >&2
1184
- : > "$STDOUT_LOG"
1185
- : > "$STDERR_LOG"
1186
- exit_code=0
1187
- run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
1188
- codex_transport_effective="exec-fallback"
1189
- else
1190
- codex_transport_effective="mcp"
1191
- fi
1192
- else
1193
- run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
1194
- codex_transport_effective="exec"
1195
- fi
1196
- echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
1197
-
1198
- elif [[ "$CLI_TYPE" == "gemini" ]]; then
1199
- local gemini_model
1200
- gemini_model=$(awk '{
1201
- for (i = 1; i <= NF; i++) {
1202
- if ($i == "-m" || $i == "--model") {
1203
- print $(i + 1)
1204
- exit
1205
- }
1206
- }
1207
- }' <<< "$CLI_ARGS")
1208
- local -a gemini_worker_args=(
1209
- "--command" "$CLI_CMD"
1210
- "--command-args-json" "$GEMINI_BIN_ARGS_JSON"
1211
- "--model" "$gemini_model"
1212
- "--approval-mode" "yolo"
1213
- )
1214
-
1215
- if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1216
- echo "[tfx-route] Gemini MCP 서버: $(IFS=' '; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1217
- local server_name
1218
- for server_name in "${GEMINI_ALLOWED_SERVERS[@]}"; do
1219
- gemini_worker_args+=("--allowed-mcp-server-name" "$server_name")
1220
- done
1221
- fi
1222
-
1223
- run_stream_worker "gemini" "$FULL_PROMPT" "$use_tee" "${gemini_worker_args[@]}" || exit_code=$?
1224
- if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1225
- echo "[tfx-route] Gemini stream wrapper 실패(exit=${exit_code}). legacy CLI 경로로 fallback합니다." >&2
1226
- : > "$STDOUT_LOG"
1227
- : > "$STDERR_LOG"
1228
- exit_code=0
1229
- run_legacy_gemini "$FULL_PROMPT" "$use_tee" || exit_code=$?
1230
- fi
1231
-
1232
- elif [[ "$CLI_TYPE" == "claude" ]]; then
1233
- local claude_model
1234
- claude_model=$(get_claude_model)
1235
- local -a claude_worker_args=(
1236
- "--command" "$CLI_CMD"
1237
- "--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
1238
- "--model" "$claude_model"
1239
- "--permission-mode" "bypassPermissions"
1240
- "--allow-dangerously-skip-permissions"
1241
- )
1242
-
1243
- run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
1244
- if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1245
- echo "[tfx-route] Claude stream wrapper 실패(exit=${exit_code}). native metadata로 fallback합니다." >&2
1246
- cat > "$STDOUT_LOG" <<EOF
1247
- $(emit_claude_native_metadata)
1248
- EOF
1249
- : > "$STDERR_LOG"
1250
- exit_code=0
1251
- CLI_TYPE="claude-native"
1252
- fi
1253
- fi
1254
-
1255
- local end_time
1256
- end_time=$(date +%s)
1257
- local elapsed=$((end_time - start_time))
1258
-
1259
- if [[ "$exit_code" -eq 0 ]]; then
1260
- local workspace_changed="unknown"
1261
- if [[ "$workspace_probe_supported" == "true" ]]; then
1262
- if workspace_signature_after=$(capture_workspace_signature); then
1263
- if [[ "$workspace_signature_before" != "$workspace_signature_after" ]]; then
1264
- workspace_changed="yes"
1265
- else
1266
- workspace_changed="no"
1267
- fi
1268
- fi
1269
- fi
1270
-
1271
- if [[ ! -s "$STDOUT_LOG" && "$workspace_changed" == "no" ]]; then
1272
- printf '%s\n' "[tfx-route] exit 0 이지만 stdout 비어있고 워크스페이스 변화가 없습니다. no-op 성공을 실패로 승격합니다." >> "$STDERR_LOG"
1273
- exit_code=68
1274
- fi
1275
- fi
1276
-
1277
- # 모드: task complete + 리드 보고
1278
- if [[ -n "$TFX_TEAM_NAME" ]]; then
1279
- if [[ "$exit_code" -eq 0 ]]; then
1280
- local output_preview
1281
- output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
1282
- team_complete_task "success" "$output_preview"
1283
- elif [[ "$exit_code" -eq 124 ]]; then
1284
- team_complete_task "timeout" "타임아웃 (${TIMEOUT_SEC}초)"
1285
- else
1286
- local err_preview
1287
- err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
1288
- team_complete_task "failed" "exit_code=${exit_code}: ${err_preview}"
1289
- fi
1290
- fi
1291
-
1292
- # ── 후처리: 단일 node 프로세스로 위임 ──
1293
- # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
1294
- local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
1295
- if [[ -f "$post_script" ]]; then
1296
- node "$post_script" \
1297
- --agent "$AGENT_TYPE" \
1298
- --cli "$CLI_TYPE" \
1299
- --cli-cmd "$CLI_CMD" \
1300
- --effort "$CLI_EFFORT" \
1301
- --run-mode "$RUN_MODE" \
1302
- --opus "$OPUS_OVERSIGHT" \
1303
- --exit-code "$exit_code" \
1304
- --elapsed "$elapsed" \
1305
- --timeout "$TIMEOUT_SEC" \
1306
- --mcp-profile "$MCP_PROFILE" \
1307
- --stderr-log "$STDERR_LOG" \
1308
- --stdout-log "$STDOUT_LOG" \
1309
- --max-bytes "$MAX_STDOUT_BYTES" \
1310
- --tee-active "$use_tee"
1311
- else
1312
- # post.mjs 없으면 기본 출력 (fallback)
1313
- echo "=== TFX-ROUTE RESULT ==="
1314
- echo "agent: $AGENT_TYPE"
1315
- echo "cli: $CLI_TYPE"
1316
- echo "exit_code: $exit_code"
1317
- echo "elapsed: ${elapsed}s"
1318
- echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
1319
- echo "=== OUTPUT ==="
1320
- cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
1321
- fi
1322
-
1323
- return "$exit_code"
1324
- }
1325
-
1326
- main
1
+ #!/usr/bin/env bash
2
+ # tfx-route.sh v2.3 — CLI 라우팅 래퍼 (triflux)
3
+ #
4
+ # v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
5
+ # v2.0: tfx-route.sh 리네임
6
+ # - 후처리 전부 tfx-route-post.mjs로 이관 (node 단일 ~100ms)
7
+ # - per-process 에이전트 등록 (race condition 구조적 제거)
8
+ # - get_mcp_hint 통합 (캐시/비캐시 단일 코드경로)
9
+ # - Gemini health check 지수 백오프 (30×1s → 5×exp)
10
+ # - 컨텍스트 파일 5번째 인자 지원
11
+ #
12
+ VERSION="2.3"
13
+ #
14
+ # 사용법:
15
+ # tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
16
+ #
17
+ # 예시:
18
+ # tfx-route.sh executor "코드 구현" implement
19
+ # tfx-route.sh architect "아키텍처 분석" analyze '' context.md
20
+
21
+ set -euo pipefail
22
+
23
+ # ── 인자 파싱 ──
24
+ AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
25
+ PROMPT="${2:?프롬프트 필수}"
26
+ MCP_PROFILE="${3:-auto}"
27
+ USER_TIMEOUT="${4:-}"
28
+ CONTEXT_FILE="${5:-}"
29
+
30
+ # ── CLI 경로 해석 (Windows npm global 대응) ──
31
+ NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
32
+ CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
33
+ GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
34
+ CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
35
+ GEMINI_BIN_ARGS_JSON="${GEMINI_BIN_ARGS_JSON:-[]}"
36
+ CLAUDE_BIN_ARGS_JSON="${CLAUDE_BIN_ARGS_JSON:-[]}"
37
+
38
+ # ── 상수 ──
39
+ MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
40
+ TIMESTAMP=$(date +%s)
41
+ RUN_ID="${TIMESTAMP}-$$-${RANDOM}"
42
+ STDERR_LOG="/tmp/tfx-route-${AGENT_TYPE}-${RUN_ID}-stderr.log"
43
+ STDOUT_LOG="/tmp/tfx-route-${AGENT_TYPE}-${RUN_ID}-stdout.log"
44
+ TFX_TMP="${TMPDIR:-/tmp}"
45
+
46
+ # ── 팀 환경변수 ──
47
+ TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
48
+ TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
49
+ TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
50
+ TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
51
+ TFX_HUB_PIPE="${TFX_HUB_PIPE:-}"
52
+ TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}" # bridge.mjs HTTP fallback hint
53
+
54
+ # fallback 시 원래 에이전트 정보 보존
55
+ ORIGINAL_AGENT=""
56
+ ORIGINAL_CLI_ARGS=""
57
+
58
+ # JSON 문자열 이스케이프:
59
+ # - "\", """ 필수 이스케이프
60
+ # - 제어문자 U+0000..U+001F 이스케이프
61
+ # - 비ASCII 문자는 \uXXXX(또는 surrogate pair)로 강제
62
+ json_escape() {
63
+ local s="${1:-}"
64
+
65
+ if command -v "$NODE_BIN" &>/dev/null; then
66
+ "$NODE_BIN" -e '
67
+ const input = process.argv[1] ?? "";
68
+ let out = "";
69
+ for (const ch of input) {
70
+ const cp = ch.codePointAt(0);
71
+ if (cp === 0x22) { out += "\\\""; continue; } // "
72
+ if (cp === 0x5c) { out += "\\\\"; continue; } // \
73
+ if (cp <= 0x1f) {
74
+ if (cp === 0x08) { out += "\\b"; continue; }
75
+ if (cp === 0x09) { out += "\\t"; continue; }
76
+ if (cp === 0x0a) { out += "\\n"; continue; }
77
+ if (cp === 0x0c) { out += "\\f"; continue; }
78
+ if (cp === 0x0d) { out += "\\r"; continue; }
79
+ out += `\\u${cp.toString(16).padStart(4, "0")}`;
80
+ continue;
81
+ }
82
+ if (cp >= 0x20 && cp <= 0x7e) {
83
+ out += ch;
84
+ continue;
85
+ }
86
+ if (cp <= 0xffff) {
87
+ out += `\\u${cp.toString(16).padStart(4, "0")}`;
88
+ continue;
89
+ }
90
+ const v = cp - 0x10000;
91
+ const hi = 0xd800 + (v >> 10);
92
+ const lo = 0xdc00 + (v & 0x3ff);
93
+ out += `\\u${hi.toString(16).padStart(4, "0")}\\u${lo.toString(16).padStart(4, "0")}`;
94
+ }
95
+ process.stdout.write(out);
96
+ ' -- "$s"
97
+ return
98
+ fi
99
+
100
+ echo "[tfx-route] ERROR: node 미설치로 안전한 JSON 이스케이프를 수행할 수 없습니다." >&2
101
+ return 1
102
+ }
103
+
104
+ # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
105
+ register_agent() {
106
+ local agent_file="${TFX_TMP}/tfx-agent-$$.json"
107
+ local safe_cli safe_agent started_at
108
+ safe_cli=$(json_escape "$CLI_TYPE" 2>/dev/null || true)
109
+ safe_agent=$(json_escape "$AGENT_TYPE" 2>/dev/null || true)
110
+ started_at=$(date +%s)
111
+
112
+ # fail-closed: 안전 인코딩 불가 시 agent 파일을 쓰지 않는다
113
+ if [[ -n "$CLI_TYPE" && -z "$safe_cli" ]]; then
114
+ return 0
115
+ fi
116
+ if [[ -n "$AGENT_TYPE" && -z "$safe_agent" ]]; then
117
+ return 0
118
+ fi
119
+
120
+ printf '{"pid":%s,"cli":"%s","agent":"%s","started":%s}\n' "$$" "$safe_cli" "$safe_agent" "$started_at" \
121
+ > "$agent_file" 2>/dev/null || true
122
+ }
123
+
124
+ deregister_agent() {
125
+ rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
126
+ }
127
+
128
+ normalize_script_path() {
129
+ local path="${1:-}"
130
+ if [[ -z "$path" ]]; then
131
+ return 0
132
+ fi
133
+
134
+ if command -v cygpath &>/dev/null; then
135
+ case "$path" in
136
+ [A-Za-z]:\\*|[A-Za-z]:/*)
137
+ cygpath -u "$path"
138
+ return 0
139
+ ;;
140
+ esac
141
+ fi
142
+
143
+ printf '%s\n' "$path"
144
+ }
145
+
146
+ # ── 팀 Hub Bridge 통신 ──
147
+ resolve_bridge_script() {
148
+ if [[ -n "${TFX_BRIDGE_SCRIPT:-}" && -f "$TFX_BRIDGE_SCRIPT" ]]; then
149
+ printf '%s\n' "$TFX_BRIDGE_SCRIPT"
150
+ return 0
151
+ fi
152
+
153
+ local script_dir
154
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
155
+ local candidates=(
156
+ "$script_dir/../hub/bridge.mjs"
157
+ "$script_dir/hub/bridge.mjs"
158
+ )
159
+
160
+ local candidate
161
+ for candidate in "${candidates[@]}"; do
162
+ if [[ -f "$candidate" ]]; then
163
+ printf '%s\n' "$candidate"
164
+ return 0
165
+ fi
166
+ done
167
+
168
+ return 1
169
+ }
170
+
171
+ bridge_cli() {
172
+ if ! command -v "$NODE_BIN" &>/dev/null; then
173
+ return 127
174
+ fi
175
+
176
+ local bridge_script
177
+ if ! bridge_script=$(resolve_bridge_script); then
178
+ return 127
179
+ fi
180
+
181
+ TFX_HUB_PIPE="$TFX_HUB_PIPE" TFX_HUB_URL="$TFX_HUB_URL" TFX_HUB_TOKEN="${TFX_HUB_TOKEN:-}" \
182
+ "$NODE_BIN" "$bridge_script" "$@" 2>/dev/null
183
+ }
184
+
185
+ bridge_json_get() {
186
+ local json="${1:-}"
187
+ local path="${2:-}"
188
+ [[ -z "$json" || -z "$path" ]] && return 1
189
+
190
+ "$NODE_BIN" -e '
191
+ const data = JSON.parse(process.argv[1] || "{}");
192
+ const keys = String(process.argv[2] || "").split(".").filter(Boolean);
193
+ let value = data;
194
+ for (const key of keys) value = value?.[key];
195
+ if (value === undefined || value === null) process.exit(1);
196
+ process.stdout.write(typeof value === "object" ? JSON.stringify(value) : String(value));
197
+ ' -- "$json" "$path" 2>/dev/null
198
+ }
199
+
200
+ bridge_json_stringify() {
201
+ local mode="${1:-}"
202
+ shift || true
203
+
204
+ case "$mode" in
205
+ metadata-patch)
206
+ "$NODE_BIN" -e '
207
+ process.stdout.write(JSON.stringify({
208
+ result: process.argv[1] || "",
209
+ summary: process.argv[2] || "",
210
+ }));
211
+ ' -- "${1:-}" "${2:-}"
212
+ ;;
213
+ task-result)
214
+ "$NODE_BIN" -e '
215
+ process.stdout.write(JSON.stringify({
216
+ task_id: process.argv[1] || "",
217
+ result: process.argv[2] || "",
218
+ }));
219
+ ' -- "${1:-}" "${2:-}"
220
+ ;;
221
+ *)
222
+ return 1
223
+ ;;
224
+ esac
225
+ }
226
+
227
+ team_send_message() {
228
+ local text="${1:-}"
229
+ local summary="${2:-}"
230
+ [[ -z "$TFX_TEAM_NAME" || -z "$text" ]] && return 0
231
+
232
+ if ! bridge_cli_with_restart "팀 메시지 전송" "Hub 재시작 후 팀 메시지 전송 성공." \
233
+ team-send-message \
234
+ --team "$TFX_TEAM_NAME" \
235
+ --from "$TFX_TEAM_AGENT_NAME" \
236
+ --to "$TFX_TEAM_LEAD_NAME" \
237
+ --text "$text" \
238
+ --summary "${summary:-status update}"; then
239
+ echo "[tfx-route] 경고: 팀 메시지 전송 실패 (team=$TFX_TEAM_NAME, to=$TFX_TEAM_LEAD_NAME)" >&2
240
+ return 0
241
+ fi
242
+
243
+ return 0
244
+ }
245
+
246
+ # ── Hub 자동 재시작 (슬립 복귀 등으로 Hub 종료 시) ──
247
+ try_restart_hub() {
248
+ local hub_server script_dir hub_port
249
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
250
+ hub_server="$script_dir/../hub/server.mjs"
251
+
252
+ if [[ ! -f "$hub_server" ]]; then
253
+ echo "[tfx-route] Hub 서버 스크립트 미발견: $hub_server" >&2
254
+ return 1
255
+ fi
256
+
257
+ # TFX_HUB_URL에서 포트 추출 (기본 27888)
258
+ hub_port="${TFX_HUB_URL##*:}"
259
+ hub_port="${hub_port%%/*}"
260
+ [[ -z "$hub_port" || "$hub_port" == "$TFX_HUB_URL" ]] && hub_port=27888
261
+
262
+ echo "[tfx-route] Hub 미응답 — 자동 재시작 시도 (port=$hub_port)..." >&2
263
+ TFX_HUB_PORT="$hub_port" "$NODE_BIN" "$hub_server" &>/dev/null &
264
+ local hub_pid=$!
265
+
266
+ # 최대 4초 대기 (0.5초 간격)
267
+ local i
268
+ for i in 1 2 3 4 5 6 7 8; do
269
+ sleep 0.5
270
+ if curl -sf "${TFX_HUB_URL}/status" >/dev/null 2>&1; then
271
+ echo "[tfx-route] Hub 재시작 성공 (pid=$hub_pid)" >&2
272
+ return 0
273
+ fi
274
+ done
275
+
276
+ echo "[tfx-route] Hub 재시작 실패 — claim 없이 계속 실행" >&2
277
+ return 1
278
+ }
279
+
280
+ bridge_cli_with_restart() {
281
+ local action_label="${1:-bridge 호출}"
282
+ local success_message="${2:-}"
283
+ shift 2 || true
284
+
285
+ if bridge_cli "$@" >/dev/null 2>&1; then
286
+ return 0
287
+ fi
288
+
289
+ if ! try_restart_hub; then
290
+ return 1
291
+ fi
292
+
293
+ if bridge_cli "$@" >/dev/null 2>&1; then
294
+ [[ -n "$success_message" ]] && echo "[tfx-route] ${success_message}" >&2
295
+ return 0
296
+ fi
297
+
298
+ echo "[tfx-route] 경고: Hub 재시작 후 ${action_label} 재시도 실패." >&2
299
+ return 1
300
+ }
301
+
302
+ team_claim_task() {
303
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
304
+ local response ok error_code error_message owner_before status_before
305
+ response=$(bridge_cli team-task-update \
306
+ --team "$TFX_TEAM_NAME" \
307
+ --task-id "$TFX_TEAM_TASK_ID" \
308
+ --claim \
309
+ --owner "$TFX_TEAM_AGENT_NAME" \
310
+ --status in_progress || true)
311
+
312
+ ok=$(bridge_json_get "$response" "ok" || true)
313
+ error_code=$(bridge_json_get "$response" "error.code" || true)
314
+ error_message=$(bridge_json_get "$response" "error.message" || true)
315
+ owner_before=$(bridge_json_get "$response" "error.details.task_before.owner" || true)
316
+ status_before=$(bridge_json_get "$response" "error.details.task_before.status" || true)
317
+
318
+ case "$ok:$error_code" in
319
+ true:*) ;;
320
+ false:CLAIM_CONFLICT)
321
+ if [[ "$owner_before" == "$TFX_TEAM_AGENT_NAME" && "$status_before" == "in_progress" ]]; then
322
+ echo "[tfx-route] 동일 owner(${TFX_TEAM_AGENT_NAME})가 이미 claim한 task ${TFX_TEAM_TASK_ID} — 계속 실행." >&2
323
+ return 0
324
+ fi
325
+ echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨(owner=${owner_before:-unknown}, status=${status_before:-unknown}). 실행 중단." >&2
326
+ team_send_message \
327
+ "task ${TFX_TEAM_TASK_ID} claim conflict: owner=${owner_before:-unknown}, status=${status_before:-unknown}" \
328
+ "task ${TFX_TEAM_TASK_ID} claim conflict"
329
+ exit 0 ;;
330
+ :|false:)
331
+ # Hub 연결 실패 → 자동 재시작 시도 후 claim 재시도
332
+ if try_restart_hub; then
333
+ response=$(bridge_cli team-task-update \
334
+ --team "$TFX_TEAM_NAME" \
335
+ --task-id "$TFX_TEAM_TASK_ID" \
336
+ --claim \
337
+ --owner "$TFX_TEAM_AGENT_NAME" \
338
+ --status in_progress || true)
339
+ ok=$(bridge_json_get "$response" "ok" || true)
340
+ if [[ "$ok" == "true" ]]; then
341
+ echo "[tfx-route] Hub 재시작 후 claim 성공." >&2
342
+ else
343
+ echo "[tfx-route] 경고: Hub 재시작 후 claim 실패. claim 없이 계속 실행." >&2
344
+ fi
345
+ else
346
+ echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2
347
+ fi ;;
348
+ *)
349
+ echo "[tfx-route] 경고: Hub claim 실패 (${error_code:-unknown}${error_message:+: ${error_message}}). claim 없이 계속 실행." >&2 ;;
350
+ esac
351
+ }
352
+
353
+ team_complete_task() {
354
+ local result="${1:-success}" # success/failed/timeout
355
+ local result_summary="${2:-작업 완료}"
356
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
357
+
358
+ local summary_trimmed metadata_patch result_payload
359
+ summary_trimmed=$(echo "$result_summary" | head -c 4096)
360
+ metadata_patch=$(bridge_json_stringify metadata-patch "$result" "$summary_trimmed" 2>/dev/null || true)
361
+ result_payload=$(bridge_json_stringify task-result "$TFX_TEAM_TASK_ID" "$result" 2>/dev/null || true)
362
+
363
+ # task 상태: 항상 "completed" (Claude Code API는 "failed" 미지원)
364
+ # 실제 결과는 metadata.result로 전달
365
+ if [[ -n "$metadata_patch" ]]; then
366
+ if ! bridge_cli_with_restart "팀 task 완료 보고" "Hub 재시작 후 팀 task 완료 보고 성공." \
367
+ team-task-update \
368
+ --team "$TFX_TEAM_NAME" \
369
+ --task-id "$TFX_TEAM_TASK_ID" \
370
+ --status completed \
371
+ --owner "$TFX_TEAM_AGENT_NAME" \
372
+ --metadata-patch "$metadata_patch"; then
373
+ echo "[tfx-route] 경고: 팀 task 완료 보고 실패 (team=$TFX_TEAM_NAME, task=$TFX_TEAM_TASK_ID, result=$result)" >&2
374
+ fi
375
+ fi
376
+
377
+ # 리드에게 메시지 전송
378
+ team_send_message "$summary_trimmed" "task ${TFX_TEAM_TASK_ID} ${result}"
379
+
380
+ # Hub result 발행 (poll_messages 채널 활성화)
381
+ if [[ -n "$result_payload" ]]; then
382
+ if ! bridge_cli_with_restart "Hub result 발행" "Hub 재시작 후 Hub result 발행 성공." \
383
+ result \
384
+ --agent "$TFX_TEAM_AGENT_NAME" \
385
+ --topic task.result \
386
+ --payload "$result_payload" \
387
+ --trace "$TFX_TEAM_NAME"; then
388
+ echo "[tfx-route] 경고: Hub result 발행 실패 (agent=$TFX_TEAM_AGENT_NAME, task=$TFX_TEAM_TASK_ID)" >&2
389
+ fi
390
+ fi
391
+
392
+ # 로컬 결과 파일 백업 (세션 끊김 복구용)
393
+ # Claude 재로그인 시 Agent 래퍼가 죽어도 이 파일로 결과 수집 가능
394
+ local result_dir="${TFX_RESULT_DIR:-${HOME}/.claude/tfx-results/${TFX_TEAM_NAME}}"
395
+ if mkdir -p "$result_dir" 2>/dev/null; then
396
+ cat > "${result_dir}/${TFX_TEAM_TASK_ID}.json" 2>/dev/null <<RESULT_EOF
397
+ {"taskId":"${TFX_TEAM_TASK_ID}","agent":"${TFX_TEAM_AGENT_NAME}","team":"${TFX_TEAM_NAME}","result":"${result}","summary":$(printf '%s' "$summary_trimmed" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.stringify(d)))" 2>/dev/null || echo '""'),"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
398
+ RESULT_EOF
399
+ [[ $? -eq 0 ]] && echo "[tfx-route] 결과 백업: ${result_dir}/${TFX_TEAM_TASK_ID}.json" >&2
400
+ fi
401
+ }
402
+
403
+ capture_workspace_signature() {
404
+ if ! command -v git &>/dev/null; then
405
+ return 1
406
+ fi
407
+
408
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
409
+ return 1
410
+ fi
411
+
412
+ git status --short --untracked-files=all --ignore-submodules=all 2>/dev/null || return 1
413
+ }
414
+
415
+ # ── 라우팅 테이블 ──
416
+ # 반환: CLI_CMD, CLI_ARGS, CLI_TYPE, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
417
+ route_agent() {
418
+ local agent="$1"
419
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
420
+
421
+ case "$agent" in
422
+ # ─── 구현 레인 ───
423
+ executor)
424
+ CLI_TYPE="codex"; CLI_CMD="codex"
425
+ CLI_ARGS="exec ${codex_base}"
426
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
427
+ build-fixer)
428
+ CLI_TYPE="codex"; CLI_CMD="codex"
429
+ CLI_ARGS="exec --profile fast ${codex_base}"
430
+ CLI_EFFORT="fast"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
431
+ debugger)
432
+ CLI_TYPE="codex"; CLI_CMD="codex"
433
+ CLI_ARGS="exec ${codex_base}"
434
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
435
+ deep-executor)
436
+ CLI_TYPE="codex"; CLI_CMD="codex"
437
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
438
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
439
+
440
+ # ─── 설계/분석 레인 ───
441
+ architect)
442
+ CLI_TYPE="codex"; CLI_CMD="codex"
443
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
444
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
445
+ planner)
446
+ CLI_TYPE="codex"; CLI_CMD="codex"
447
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
448
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
449
+ critic)
450
+ CLI_TYPE="codex"; CLI_CMD="codex"
451
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
452
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
453
+ analyst)
454
+ CLI_TYPE="codex"; CLI_CMD="codex"
455
+ CLI_ARGS="exec --profile xhigh ${codex_base}"
456
+ CLI_EFFORT="xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
457
+
458
+ # ─── 리뷰 레인 ───
459
+ code-reviewer)
460
+ CLI_TYPE="codex"; CLI_CMD="codex"
461
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
462
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
463
+ security-reviewer)
464
+ CLI_TYPE="codex"; CLI_CMD="codex"
465
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
466
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
467
+ quality-reviewer)
468
+ CLI_TYPE="codex"; CLI_CMD="codex"
469
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
470
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
471
+
472
+ # ─── 리서치 레인 ───
473
+ scientist)
474
+ CLI_TYPE="codex"; CLI_CMD="codex"
475
+ CLI_ARGS="exec ${codex_base}"
476
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
477
+ scientist-deep)
478
+ CLI_TYPE="codex"; CLI_CMD="codex"
479
+ CLI_ARGS="exec --profile thorough ${codex_base}"
480
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
481
+ document-specialist)
482
+ CLI_TYPE="codex"; CLI_CMD="codex"
483
+ CLI_ARGS="exec ${codex_base}"
484
+ CLI_EFFORT="high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
485
+
486
+ # ─── UI/문서 레인 ───
487
+ designer)
488
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
489
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"
490
+ CLI_EFFORT="pro"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
491
+ writer)
492
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
493
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"
494
+ CLI_EFFORT="flash"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
495
+
496
+ # ─── Claude 네이티브 ───
497
+ explore)
498
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
499
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
500
+ verifier)
501
+ CLI_TYPE="codex"; CLI_CMD="codex"
502
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
503
+ CLI_EFFORT="thorough"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
504
+ test-engineer)
505
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
506
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
507
+ qa-tester)
508
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
509
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=300; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
510
+
511
+ # ─── 경량 ───
512
+ spark)
513
+ CLI_TYPE="codex"; CLI_CMD="codex"
514
+ CLI_ARGS="exec --profile spark_fast ${codex_base}"
515
+ CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
516
+ *)
517
+ echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
518
+ echo "사용 가능: executor, build-fixer, debugger, deep-executor, architect, planner, critic, analyst," >&2
519
+ echo " code-reviewer, security-reviewer, quality-reviewer, scientist, document-specialist," >&2
520
+ echo " designer, writer, explore, verifier, test-engineer, qa-tester, spark" >&2
521
+ exit 1 ;;
522
+ esac
523
+ }
524
+
525
+ # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
526
+ TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
527
+ TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
528
+ TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
529
+ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
530
+ # Codex 요금제 자동 감지 (preflight 캐시 → auth.json JWT)
531
+ # 환경변수 명시 설정 시 우선, 미설정 시 캐시에서 읽기, 캐시도 없으면 pro
532
+ if [[ -z "${TFX_CODEX_PLAN:-}" ]]; then
533
+ _detected_plan=$(node -e '
534
+ try {
535
+ const c = JSON.parse(require("fs").readFileSync(require("path").join(require("os").homedir(),".claude","cache","tfx-preflight.json"),"utf8"));
536
+ const p = c?.codex_plan?.plan;
537
+ if (p && p !== "unknown" && p !== "api") { process.stdout.write(p); }
538
+ } catch {}
539
+ ' 2>/dev/null)
540
+ TFX_CODEX_PLAN="${_detected_plan:-pro}"
541
+ unset _detected_plan
542
+ fi
543
+ TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
544
+ TFX_SEARCH_TOOL="${TFX_SEARCH_TOOL:-}"
545
+ case "$TFX_NO_CLAUDE_NATIVE" in
546
+ 0|1) ;;
547
+ *)
548
+ echo "ERROR: TFX_NO_CLAUDE_NATIVE 값은 0 또는 1이어야 합니다. (현재: $TFX_NO_CLAUDE_NATIVE)" >&2
549
+ exit 1
550
+ ;;
551
+ esac
552
+ case "$TFX_CODEX_PLAN" in
553
+ pro|plus|free) ;;
554
+ *)
555
+ echo "ERROR: TFX_CODEX_PLAN 값은 pro, plus, free 중 하나여야 합니다. (현재: $TFX_CODEX_PLAN)" >&2
556
+ exit 1
557
+ ;;
558
+ esac
559
+ case "$TFX_CODEX_TRANSPORT" in
560
+ auto|mcp|exec) ;;
561
+ *)
562
+ echo "ERROR: TFX_CODEX_TRANSPORT 값은 auto, mcp, exec 중 하나여야 합니다. (현재: $TFX_CODEX_TRANSPORT)" >&2
563
+ exit 1
564
+ ;;
565
+ esac
566
+ case "$TFX_VERIFIER_OVERRIDE" in
567
+ auto|claude) ;;
568
+ *)
569
+ echo "ERROR: TFX_VERIFIER_OVERRIDE 값은 auto 또는 claude여야 합니다. (현재: $TFX_VERIFIER_OVERRIDE)" >&2
570
+ exit 1
571
+ ;;
572
+ esac
573
+ case "$TFX_WORKER_INDEX" in
574
+ "") ;;
575
+ *[!0-9]*|0)
576
+ echo "ERROR: TFX_WORKER_INDEX 값은 1 이상의 정수여야 합니다. (현재: $TFX_WORKER_INDEX)" >&2
577
+ exit 1
578
+ ;;
579
+ esac
580
+ case "$TFX_SEARCH_TOOL" in
581
+ ""|brave-search|tavily|exa) ;;
582
+ *)
583
+ echo "ERROR: TFX_SEARCH_TOOL 값은 brave-search, tavily, exa 중 하나여야 합니다. (현재: $TFX_SEARCH_TOOL)" >&2
584
+ exit 1
585
+ ;;
586
+ esac
587
+ CODEX_MCP_TRANSPORT_EXIT_CODE=70
588
+
589
+ apply_cli_mode() {
590
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
591
+
592
+ case "$TFX_CLI_MODE" in
593
+ codex)
594
+ if [[ "$CLI_TYPE" == "gemini" ]]; then
595
+ CLI_TYPE="codex"; CLI_CMD="codex"
596
+ case "$AGENT_TYPE" in
597
+ designer)
598
+ CLI_ARGS="exec ${codex_base}"; CLI_EFFORT="high"; DEFAULT_TIMEOUT=600 ;;
599
+ writer)
600
+ CLI_ARGS="exec --profile spark_fast ${codex_base}"; CLI_EFFORT="spark_fast"; DEFAULT_TIMEOUT=180 ;;
601
+ esac
602
+ echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
603
+ fi ;;
604
+ gemini)
605
+ if [[ "$CLI_TYPE" == "codex" ]]; then
606
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
607
+ case "$AGENT_TYPE" in
608
+ executor|debugger|deep-executor|architect|planner|critic|analyst|\
609
+ code-reviewer|security-reviewer|quality-reviewer|scientist-deep)
610
+ CLI_ARGS="-m gemini-3.1-pro-preview -y --prompt"; CLI_EFFORT="pro" ;;
611
+ build-fixer|spark)
612
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash"; DEFAULT_TIMEOUT=180 ;;
613
+ *)
614
+ CLI_ARGS="-m gemini-3-flash-preview -y --prompt"; CLI_EFFORT="flash" ;;
615
+ esac
616
+ echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($CLI_EFFORT)로 리매핑" >&2
617
+ fi ;;
618
+ auto)
619
+ if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
620
+ if command -v "$GEMINI_BIN" &>/dev/null; then
621
+ TFX_CLI_MODE="gemini"; apply_cli_mode; return
622
+ else
623
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
624
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
625
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE claude-native fallback" >&2
626
+ fi
627
+ elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
628
+ if command -v "$CODEX_BIN" &>/dev/null; then
629
+ TFX_CLI_MODE="codex"; apply_cli_mode; return
630
+ else
631
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
632
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
633
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
634
+ fi
635
+ fi ;;
636
+ esac
637
+ }
638
+
639
+ # ── Codex 요금제 가드 (fast 프로필은 Pro 전용) ──
640
+ apply_plan_guard() {
641
+ [[ "$CLI_TYPE" != "codex" ]] && return
642
+ [[ "$TFX_CODEX_PLAN" == "pro" ]] && return
643
+
644
+ if [[ "$CLI_EFFORT" == "fast" ]]; then
645
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
646
+ CLI_ARGS="exec ${codex_base}"
647
+ CLI_EFFORT="high"
648
+ echo "[tfx-route] TFX_CODEX_PLAN=$TFX_CODEX_PLAN: --profile fast → high로 다운그레이드 (Pro 전용)" >&2
649
+ fi
650
+ }
651
+
652
+ # ── Claude 네이티브 제거 (Codex 리드 환경에서 선택적 활성화) ──
653
+ apply_no_claude_native_mode() {
654
+ local codex_base="--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
655
+
656
+ [[ "$TFX_NO_CLAUDE_NATIVE" != "1" ]] && return
657
+ [[ "$TFX_CLI_MODE" == "gemini" ]] && return
658
+ [[ "$CLI_TYPE" != "claude-native" ]] && return
659
+
660
+ if ! command -v "$CODEX_BIN" &>/dev/null; then
661
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1 이지만 codex를 찾지 못해 claude-native 유지" >&2
662
+ return
663
+ fi
664
+
665
+ ORIGINAL_AGENT="${AGENT_TYPE}"
666
+ CLI_TYPE="codex"; CLI_CMD="codex"
667
+
668
+ case "$AGENT_TYPE" in
669
+ explore)
670
+ CLI_ARGS="exec --profile fast ${codex_base}"
671
+ CLI_EFFORT="fast"
672
+ DEFAULT_TIMEOUT=600
673
+ RUN_MODE="fg"
674
+ OPUS_OVERSIGHT="false"
675
+ ;;
676
+ verifier)
677
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
678
+ CLI_EFFORT="thorough"
679
+ DEFAULT_TIMEOUT=1200
680
+ RUN_MODE="fg"
681
+ OPUS_OVERSIGHT="false"
682
+ ;;
683
+ test-engineer)
684
+ CLI_ARGS="exec ${codex_base}"
685
+ CLI_EFFORT="high"
686
+ DEFAULT_TIMEOUT=1200
687
+ RUN_MODE="bg"
688
+ OPUS_OVERSIGHT="false"
689
+ ;;
690
+ qa-tester)
691
+ CLI_ARGS="exec --profile thorough ${codex_base} review"
692
+ CLI_EFFORT="thorough"
693
+ DEFAULT_TIMEOUT=1200
694
+ RUN_MODE="bg"
695
+ OPUS_OVERSIGHT="false"
696
+ ;;
697
+ *)
698
+ # claude-native 타입 중 위에 없는 경우는 보수적으로 유지
699
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
700
+ return
701
+ ;;
702
+ esac
703
+
704
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
705
+ }
706
+
707
+ apply_verifier_override() {
708
+ [[ "$AGENT_TYPE" != "verifier" ]] && return
709
+
710
+ case "$TFX_VERIFIER_OVERRIDE" in
711
+ auto|"")
712
+ return 0
713
+ ;;
714
+ claude)
715
+ ORIGINAL_AGENT="${ORIGINAL_AGENT:-$AGENT_TYPE}"
716
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
717
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
718
+ echo "[tfx-route] TFX_VERIFIER_OVERRIDE=claude: verifier -> claude-native" >&2
719
+ ;;
720
+ esac
721
+
722
+ return 0
723
+ }
724
+
725
+ # ── MCP 인벤토리 캐시 ──
726
+ MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
727
+ MCP_FILTER_SCRIPT=""
728
+ MCP_PROFILE_REQUESTED="auto"
729
+ MCP_RESOLVED_PROFILE="default"
730
+ MCP_HINT=""
731
+ GEMINI_ALLOWED_SERVERS=()
732
+ CODEX_CONFIG_FLAGS=()
733
+ CODEX_CONFIG_JSON=""
734
+
735
+ get_cached_servers() {
736
+ local cli_type="$1"
737
+ if [[ -f "$MCP_CACHE" ]]; then
738
+ 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
739
+ fi
740
+ }
741
+
742
+ resolve_mcp_filter_script() {
743
+ if [[ -n "$MCP_FILTER_SCRIPT" && -f "$MCP_FILTER_SCRIPT" ]]; then
744
+ printf '%s\n' "$MCP_FILTER_SCRIPT"
745
+ return 0
746
+ fi
747
+
748
+ local script_ref script_dir candidate
749
+ local -a candidates=()
750
+
751
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
752
+ if [[ -n "$script_ref" ]]; then
753
+ script_dir="$(cd "$(dirname "$script_ref")" 2>/dev/null && pwd -P || true)"
754
+ [[ -n "$script_dir" ]] && candidates+=("$script_dir/lib/mcp-filter.mjs")
755
+ fi
756
+
757
+ candidates+=(
758
+ "$PWD/scripts/lib/mcp-filter.mjs"
759
+ "$PWD/lib/mcp-filter.mjs"
760
+ )
761
+
762
+ for candidate in "${candidates[@]}"; do
763
+ if [[ -f "$candidate" ]]; then
764
+ MCP_FILTER_SCRIPT="$candidate"
765
+ printf '%s\n' "$MCP_FILTER_SCRIPT"
766
+ return 0
767
+ fi
768
+ done
769
+
770
+ return 1
771
+ }
772
+
773
+ resolve_mcp_policy() {
774
+ local filter_script available_servers
775
+ if ! filter_script=$(resolve_mcp_filter_script); then
776
+ echo "[tfx-route] 경고: mcp-filter.mjs를 찾지 못해 기본 MCP 정책을 사용합니다." >&2
777
+ MCP_PROFILE_REQUESTED="$MCP_PROFILE"
778
+ MCP_RESOLVED_PROFILE="$MCP_PROFILE"
779
+ MCP_HINT=""
780
+ GEMINI_ALLOWED_SERVERS=()
781
+ CODEX_CONFIG_FLAGS=()
782
+ CODEX_CONFIG_JSON=""
783
+ return 0
784
+ fi
785
+
786
+ available_servers=$(get_cached_servers "$CLI_TYPE")
787
+ [[ -z "$available_servers" ]] && available_servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
788
+
789
+ local -a cmd=(
790
+ "$NODE_BIN" "$filter_script" shell
791
+ "--agent" "$AGENT_TYPE"
792
+ "--profile" "$MCP_PROFILE"
793
+ "--available" "$available_servers"
794
+ "--inventory-file" "$MCP_CACHE"
795
+ "--task-text" "$PROMPT"
796
+ )
797
+ [[ -n "$TFX_SEARCH_TOOL" ]] && cmd+=("--search-tool" "$TFX_SEARCH_TOOL")
798
+ [[ -n "$TFX_WORKER_INDEX" ]] && cmd+=("--worker-index" "$TFX_WORKER_INDEX")
799
+
800
+ local shell_exports
801
+ if ! shell_exports="$("${cmd[@]}")"; then
802
+ echo "[tfx-route] ERROR: MCP 정책 계산 실패" >&2
803
+ return 1
804
+ fi
805
+
806
+ eval "$shell_exports"
807
+ }
808
+
809
+ get_claude_model() {
810
+ case "$AGENT_TYPE" in
811
+ explore) echo "haiku" ;;
812
+ *) echo "sonnet" ;;
813
+ esac
814
+ }
815
+
816
+ emit_claude_native_metadata() {
817
+ local model
818
+ model=$(get_claude_model)
819
+ echo "ROUTE_TYPE=claude-native"
820
+ echo "AGENT=$AGENT_TYPE"
821
+ echo "MODEL=$model"
822
+ echo "RUN_MODE=$RUN_MODE"
823
+ echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
824
+ echo "TIMEOUT=$TIMEOUT_SEC"
825
+ echo "MCP_PROFILE=$MCP_PROFILE"
826
+ [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
827
+ echo "PROMPT=$PROMPT"
828
+ echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
829
+ }
830
+
831
+ resolve_worker_runner_script() {
832
+ if [[ -n "${TFX_ROUTE_WORKER_RUNNER:-}" && -f "$TFX_ROUTE_WORKER_RUNNER" ]]; then
833
+ printf '%s\n' "$TFX_ROUTE_WORKER_RUNNER"
834
+ return 0
835
+ fi
836
+
837
+ local script_ref script_dir
838
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
839
+ script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
840
+ local candidate="$script_dir/tfx-route-worker.mjs"
841
+ [[ -f "$candidate" ]] || return 1
842
+ printf '%s\n' "$candidate"
843
+ }
844
+
845
+ run_stream_worker() {
846
+ local worker_type="$1"
847
+ local prompt="$2"
848
+ local use_tee_flag="$3"
849
+ shift 3
850
+
851
+ local runner_script
852
+ if ! runner_script=$(resolve_worker_runner_script); then
853
+ echo "[tfx-route] 경고: stream worker runner를 찾지 못했습니다." >&2
854
+ return 127
855
+ fi
856
+
857
+ if ! command -v "$NODE_BIN" &>/dev/null; then
858
+ echo "[tfx-route] 경고: node를 찾지 못해 stream worker를 실행할 수 없습니다." >&2
859
+ return 127
860
+ fi
861
+
862
+ local -a worker_cmd=(
863
+ "$NODE_BIN"
864
+ "$runner_script"
865
+ "--type" "$worker_type"
866
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
867
+ "--cwd" "$PWD"
868
+ "$@"
869
+ )
870
+
871
+ if [[ "$use_tee_flag" == "true" ]]; then
872
+ printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG"
873
+ else
874
+ printf '%s' "$prompt" | timeout "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG"
875
+ fi
876
+ }
877
+
878
+ run_legacy_gemini() {
879
+ local prompt="$1"
880
+ local use_tee_flag="$2"
881
+ local -a gemini_args=()
882
+ read -r -a gemini_args <<< "$CLI_ARGS"
883
+
884
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
885
+ local gemini_mcp_filter prompt_index=-1
886
+ gemini_mcp_filter=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")
887
+ for i in "${!gemini_args[@]}"; do
888
+ if [[ "${gemini_args[$i]}" == "--prompt" ]]; then
889
+ prompt_index="$i"
890
+ break
891
+ fi
892
+ done
893
+ if [[ "$prompt_index" -ge 0 ]]; then
894
+ gemini_args=(
895
+ "${gemini_args[@]:0:$prompt_index}"
896
+ "--allowed-mcp-server-names" "$gemini_mcp_filter"
897
+ "${gemini_args[@]:$prompt_index}"
898
+ )
899
+ echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
900
+ fi
901
+ fi
902
+
903
+ if [[ "$use_tee_flag" == "true" ]]; then
904
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
905
+ else
906
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
907
+ fi
908
+ local pid=$!
909
+
910
+ local health_ok=true
911
+ local intervals=(1 2 3 5 8)
912
+ for wait_sec in "${intervals[@]}"; do
913
+ sleep "$wait_sec"
914
+ if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
915
+ break
916
+ fi
917
+ if ! kill -0 "$pid" 2>/dev/null; then
918
+ health_ok=false
919
+ echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
920
+ break
921
+ fi
922
+ done
923
+
924
+ local exit_code_local=0
925
+ if [[ "$health_ok" == "false" ]]; then
926
+ wait "$pid" 2>/dev/null
927
+ echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
928
+ if [[ "$use_tee_flag" == "true" ]]; then
929
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
930
+ else
931
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
932
+ fi
933
+ pid=$!
934
+ fi
935
+
936
+ wait "$pid" || exit_code_local=$?
937
+ return "$exit_code_local"
938
+ }
939
+
940
+ resolve_codex_mcp_script() {
941
+ if [[ -n "${TFX_CODEX_MCP_SCRIPT:-}" && -f "$TFX_CODEX_MCP_SCRIPT" ]]; then
942
+ printf '%s\n' "$TFX_CODEX_MCP_SCRIPT"
943
+ return 0
944
+ fi
945
+
946
+ local script_ref script_dir
947
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
948
+ script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
949
+ local candidates=(
950
+ "$script_dir/hub/workers/codex-mcp.mjs"
951
+ "$script_dir/../hub/workers/codex-mcp.mjs"
952
+ )
953
+
954
+ local candidate
955
+ for candidate in "${candidates[@]}"; do
956
+ if [[ -f "$candidate" ]]; then
957
+ printf '%s\n' "$candidate"
958
+ return 0
959
+ fi
960
+ done
961
+
962
+ return 1
963
+ }
964
+
965
+ run_codex_exec() {
966
+ local prompt="$1"
967
+ local use_tee_flag="$2"
968
+ local exit_code_local=0
969
+ local -a codex_args=()
970
+ read -r -a codex_args <<< "$CLI_ARGS"
971
+ if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
972
+ codex_args+=("${CODEX_CONFIG_FLAGS[@]}")
973
+ fi
974
+
975
+ if [[ "$use_tee_flag" == "true" ]]; then
976
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
977
+ else
978
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
979
+ fi
980
+
981
+ if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
982
+ # stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
983
+ # 1차: "codex" 마커 기반 (Windows \r 제거 후 매칭)
984
+ sed 's/\r$//' "$STDERR_LOG" \
985
+ | awk '/^codex$/{found=NR;content=""} found && NR>found{content=content RS $0} END{if(content) print substr(content,2)}' \
986
+ > "$STDOUT_LOG"
987
+
988
+ # 2차: 마커 없을 node fallback (MCP/헤더/sandbox 로그 제외, 응답 부분만 추출)
989
+ if [[ ! -s "$STDOUT_LOG" ]]; then
990
+ node -e '
991
+ const fs=require("fs"),lines=fs.readFileSync(process.argv[1],"utf-8").split(/\r?\n/);
992
+ const skip=/^(mcp[: ]|OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|tokens used|EXIT:|exec$|"[A-Z]:|succeeded in |\s*$)/;
993
+ const out=lines.filter(l=>!skip.test(l));
994
+ if(out.length) fs.writeFileSync(process.argv[2],out.join("\n"));
995
+ ' -- "$STDERR_LOG" "$STDOUT_LOG" 2>/dev/null || true
996
+ fi
997
+
998
+ if [[ -s "$STDOUT_LOG" ]]; then
999
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr에서 응답 복구 ($(wc -c < "$STDOUT_LOG") bytes)" >&2
1000
+ else
1001
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패" >&2
1002
+ fi
1003
+ fi
1004
+
1005
+ return "$exit_code_local"
1006
+ }
1007
+
1008
+ run_codex_mcp() {
1009
+ local prompt="$1"
1010
+ local use_tee_flag="$2"
1011
+ local mcp_script node_bin
1012
+ local exit_code_local=0
1013
+
1014
+ if ! mcp_script=$(resolve_codex_mcp_script); then
1015
+ echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
1016
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1017
+ fi
1018
+
1019
+ node_bin="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
1020
+ if ! command -v "$node_bin" &>/dev/null; then
1021
+ echo "[tfx-route] 경고: node를 찾지 못해 Codex MCP 경로를 사용할 수 없습니다." >&2
1022
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1023
+ fi
1024
+
1025
+ local -a mcp_args=(
1026
+ "$mcp_script"
1027
+ "--prompt" "$prompt"
1028
+ "--cwd" "$PWD"
1029
+ "--profile" "$CLI_EFFORT"
1030
+ "--approval-policy" "never"
1031
+ "--sandbox" "danger-full-access"
1032
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
1033
+ "--codex-command" "$CODEX_BIN"
1034
+ )
1035
+
1036
+ if [[ -n "$CODEX_CONFIG_JSON" && "$CODEX_CONFIG_JSON" != "{}" ]]; then
1037
+ mcp_args+=("--config-json" "$CODEX_CONFIG_JSON")
1038
+ fi
1039
+
1040
+ case "$AGENT_TYPE" in
1041
+ code-reviewer)
1042
+ mcp_args+=(
1043
+ "--developer-instructions"
1044
+ "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라."
1045
+ )
1046
+ ;;
1047
+ security-reviewer)
1048
+ mcp_args+=(
1049
+ "--developer-instructions"
1050
+ "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라."
1051
+ )
1052
+ ;;
1053
+ quality-reviewer)
1054
+ mcp_args+=(
1055
+ "--developer-instructions"
1056
+ "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라."
1057
+ )
1058
+ ;;
1059
+ esac
1060
+
1061
+ if [[ "$use_tee_flag" == "true" ]]; then
1062
+ timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
1063
+ else
1064
+ timeout "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
1065
+ fi
1066
+
1067
+ # 모듈 로드 실패(의존성 누락) MCP transport exit code로 변환하여 fallback 트리거
1068
+ if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
1069
+ echo "[tfx-route] Codex MCP 모듈 로드 실패 — fallback 가능 exit code로 변환" >&2
1070
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1071
+ fi
1072
+
1073
+ return "$exit_code_local"
1074
+ }
1075
+
1076
+ # ── 메인 실행 ──
1077
+ main() {
1078
+ # 종료 시 per-process 에이전트 파일 자동 삭제
1079
+ trap 'deregister_agent' EXIT
1080
+
1081
+ route_agent "$AGENT_TYPE"
1082
+ apply_cli_mode
1083
+ apply_no_claude_native_mode
1084
+ apply_plan_guard
1085
+ apply_verifier_override
1086
+
1087
+ # CLI 경로 해석
1088
+ case "$CLI_CMD" in
1089
+ codex) CLI_CMD="$CODEX_BIN" ;;
1090
+ gemini) CLI_CMD="$GEMINI_BIN" ;;
1091
+ claude) CLI_CMD="$CLAUDE_BIN" ;;
1092
+ esac
1093
+
1094
+ # 타임아웃 결정 (에이전트별 최소값 보장)
1095
+ local MIN_TIMEOUT
1096
+ case "$AGENT_TYPE" in
1097
+ deep-executor|architect|planner|critic|analyst) MIN_TIMEOUT=900 ;;
1098
+ document-specialist|scientist|scientist-deep) MIN_TIMEOUT=900 ;;
1099
+ code-reviewer|security-reviewer|quality-reviewer) MIN_TIMEOUT=600 ;;
1100
+ executor|debugger) MIN_TIMEOUT=300 ;;
1101
+ *) MIN_TIMEOUT=120 ;;
1102
+ esac
1103
+
1104
+ if [[ -n "$USER_TIMEOUT" ]]; then
1105
+ if ! [[ "$USER_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
1106
+ echo "[tfx-route] 경고: 유효하지 않은 타임아웃 값 ($USER_TIMEOUT), 기본값 사용" >&2
1107
+ USER_TIMEOUT=""
1108
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
1109
+ elif [[ "$USER_TIMEOUT" -lt "$MIN_TIMEOUT" ]]; then
1110
+ echo "[tfx-route] 경고: 타임아웃 ${USER_TIMEOUT}s < 최소 ${MIN_TIMEOUT}s ($AGENT_TYPE), 최소값 적용" >&2
1111
+ TIMEOUT_SEC="$MIN_TIMEOUT"
1112
+ else
1113
+ TIMEOUT_SEC="$USER_TIMEOUT"
1114
+ fi
1115
+ else
1116
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
1117
+ fi
1118
+
1119
+ # 컨텍스트 파일 프롬프트에 주입
1120
+ if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
1121
+ local ctx_content
1122
+ ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
1123
+ PROMPT="${PROMPT}
1124
+
1125
+ <prior_context>
1126
+ ${ctx_content}
1127
+ </prior_context>"
1128
+ fi
1129
+
1130
+ resolve_mcp_policy
1131
+
1132
+ # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
1133
+ if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
1134
+ if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
1135
+ CLI_TYPE="claude"
1136
+ CLI_CMD="$CLAUDE_BIN"
1137
+ echo "[tfx-route] non-tty 팀 환경: claude-native -> claude stream wrapper 전환" >&2
1138
+ else
1139
+ echo "[tfx-route] claude stream wrapper 미사용: native metadata 유지" >&2
1140
+ fi
1141
+ fi
1142
+
1143
+ # Claude 네이티브 에이전트는 스크립트로 처리 불가
1144
+ if [[ "$CLI_TYPE" == "claude-native" ]]; then
1145
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1146
+ # 모드: Hub에 fallback 필요 시그널 전송 구조화된 출력
1147
+ echo "[tfx-route] claude-native 역할($AGENT_TYPE)은 tfx-route.sh로 실행 불가 — Claude Agent fallback 필요" >&2
1148
+ team_complete_task "fallback" "claude-native 역할 실행 불가: ${AGENT_TYPE}. Claude Task(sonnet) 에이전트로 위임하세요."
1149
+ cat <<FALLBACK_EOF
1150
+ === TFX_NEEDS_FALLBACK ===
1151
+ agent_type: ${AGENT_TYPE}
1152
+ reason: claude-native roles require Claude Agent tools (Read/Edit/Grep). tfx-route.sh cannot provide these.
1153
+ action: Lead should spawn Agent(subagent_type="${AGENT_TYPE}") for this task.
1154
+ task_id: ${TFX_TEAM_TASK_ID:-none}
1155
+ FALLBACK_EOF
1156
+ exit 0
1157
+ fi
1158
+ emit_claude_native_metadata
1159
+ exit 0
1160
+ fi
1161
+
1162
+ local FULL_PROMPT="$PROMPT"
1163
+ [[ -n "$MCP_HINT" ]] && FULL_PROMPT="${PROMPT}. ${MCP_HINT}"
1164
+ local codex_transport_effective="n/a"
1165
+
1166
+ # 메타정보 (stderr)
1167
+ echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
1168
+ echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE resolved_profile=$MCP_RESOLVED_PROFILE verifier_override=$TFX_VERIFIER_OVERRIDE" >&2
1169
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1170
+ echo "[tfx-route] allowed_mcp_servers=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1171
+ else
1172
+ echo "[tfx-route] allowed_mcp_servers=none" >&2
1173
+ fi
1174
+ if [[ -n "$TFX_WORKER_INDEX" || -n "$TFX_SEARCH_TOOL" ]]; then
1175
+ echo "[tfx-route] worker_index=${TFX_WORKER_INDEX:-auto} search_tool=${TFX_SEARCH_TOOL:-auto}" >&2
1176
+ fi
1177
+ if [[ "$CLI_TYPE" == "codex" ]]; then
1178
+ echo "[tfx-route] codex_transport_request=$TFX_CODEX_TRANSPORT" >&2
1179
+ fi
1180
+ [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
1181
+
1182
+ # Per-process 에이전트 등록
1183
+ register_agent
1184
+
1185
+ # 모드: task claim
1186
+ team_claim_task
1187
+ team_send_message "작업 시작: ${TFX_TEAM_AGENT_NAME}" "task ${TFX_TEAM_TASK_ID} started"
1188
+
1189
+ # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
1190
+ local exit_code=0
1191
+ local start_time
1192
+ start_time=$(date +%s)
1193
+ local workspace_signature_before=""
1194
+ local workspace_signature_after=""
1195
+ local workspace_probe_supported=false
1196
+ if workspace_signature_before=$(capture_workspace_signature); then
1197
+ workspace_probe_supported=true
1198
+ fi
1199
+
1200
+ # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
1201
+ # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
1202
+ # 실시간 모니터링은 Shift+Down으로 워커 pane 전환 권장
1203
+ local use_tee=false
1204
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1205
+ if [[ -t 1 ]] || [[ -n "${TMUX:-}" ]]; then
1206
+ use_tee=true
1207
+ fi
1208
+ fi
1209
+
1210
+ if [[ "$CLI_TYPE" == "codex" ]]; then
1211
+ codex_transport_effective="exec"
1212
+ if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
1213
+ run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
1214
+ if [[ "$exit_code" -eq 0 ]]; then
1215
+ codex_transport_effective="mcp"
1216
+ elif [[ "$exit_code" -eq "$CODEX_MCP_TRANSPORT_EXIT_CODE" && "$TFX_CODEX_TRANSPORT" == "auto" ]]; then
1217
+ echo "[tfx-route] Codex MCP bootstrap 실패(exit=${exit_code}). legacy exec 경로로 fallback합니다." >&2
1218
+ : > "$STDOUT_LOG"
1219
+ : > "$STDERR_LOG"
1220
+ exit_code=0
1221
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
1222
+ codex_transport_effective="exec-fallback"
1223
+ else
1224
+ codex_transport_effective="mcp"
1225
+ fi
1226
+ else
1227
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
1228
+ codex_transport_effective="exec"
1229
+ fi
1230
+ echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
1231
+
1232
+ elif [[ "$CLI_TYPE" == "gemini" ]]; then
1233
+ local gemini_model
1234
+ gemini_model=$(awk '{
1235
+ for (i = 1; i <= NF; i++) {
1236
+ if ($i == "-m" || $i == "--model") {
1237
+ print $(i + 1)
1238
+ exit
1239
+ }
1240
+ }
1241
+ }' <<< "$CLI_ARGS")
1242
+ local -a gemini_worker_args=(
1243
+ "--command" "$CLI_CMD"
1244
+ "--command-args-json" "$GEMINI_BIN_ARGS_JSON"
1245
+ "--model" "$gemini_model"
1246
+ "--approval-mode" "yolo"
1247
+ )
1248
+
1249
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1250
+ echo "[tfx-route] Gemini MCP 서버: $(IFS=' '; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1251
+ local server_name
1252
+ for server_name in "${GEMINI_ALLOWED_SERVERS[@]}"; do
1253
+ gemini_worker_args+=("--allowed-mcp-server-name" "$server_name")
1254
+ done
1255
+ fi
1256
+
1257
+ run_stream_worker "gemini" "$FULL_PROMPT" "$use_tee" "${gemini_worker_args[@]}" || exit_code=$?
1258
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1259
+ echo "[tfx-route] Gemini stream wrapper 실패(exit=${exit_code}). legacy CLI 경로로 fallback합니다." >&2
1260
+ : > "$STDOUT_LOG"
1261
+ : > "$STDERR_LOG"
1262
+ exit_code=0
1263
+ run_legacy_gemini "$FULL_PROMPT" "$use_tee" || exit_code=$?
1264
+ fi
1265
+
1266
+ elif [[ "$CLI_TYPE" == "claude" ]]; then
1267
+ local claude_model
1268
+ claude_model=$(get_claude_model)
1269
+ local -a claude_worker_args=(
1270
+ "--command" "$CLI_CMD"
1271
+ "--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
1272
+ "--model" "$claude_model"
1273
+ "--permission-mode" "bypassPermissions"
1274
+ "--allow-dangerously-skip-permissions"
1275
+ )
1276
+
1277
+ run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
1278
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1279
+ echo "[tfx-route] Claude stream wrapper 실패(exit=${exit_code}). native metadata로 fallback합니다." >&2
1280
+ cat > "$STDOUT_LOG" <<EOF
1281
+ $(emit_claude_native_metadata)
1282
+ EOF
1283
+ : > "$STDERR_LOG"
1284
+ exit_code=0
1285
+ CLI_TYPE="claude-native"
1286
+ fi
1287
+ fi
1288
+
1289
+ local end_time
1290
+ end_time=$(date +%s)
1291
+ local elapsed=$((end_time - start_time))
1292
+
1293
+ if [[ "$exit_code" -eq 0 ]]; then
1294
+ local workspace_changed="unknown"
1295
+ if [[ "$workspace_probe_supported" == "true" ]]; then
1296
+ if workspace_signature_after=$(capture_workspace_signature); then
1297
+ if [[ "$workspace_signature_before" != "$workspace_signature_after" ]]; then
1298
+ workspace_changed="yes"
1299
+ else
1300
+ workspace_changed="no"
1301
+ fi
1302
+ fi
1303
+ fi
1304
+
1305
+ if [[ ! -s "$STDOUT_LOG" && "$workspace_changed" == "no" ]]; then
1306
+ printf '%s\n' "[tfx-route] exit 0 이지만 stdout 비어있고 워크스페이스 변화가 없습니다. no-op 성공을 실패로 승격합니다." >> "$STDERR_LOG"
1307
+ exit_code=68
1308
+ fi
1309
+ fi
1310
+
1311
+ # 팀 모드: task complete + 리드 보고
1312
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1313
+ if [[ "$exit_code" -eq 0 ]]; then
1314
+ local output_preview
1315
+ output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
1316
+ team_complete_task "success" "$output_preview"
1317
+ elif [[ "$exit_code" -eq 124 ]]; then
1318
+ team_complete_task "timeout" "타임아웃 (${TIMEOUT_SEC}초)"
1319
+ else
1320
+ local err_preview
1321
+ err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
1322
+ team_complete_task "failed" "exit_code=${exit_code}: ${err_preview}"
1323
+ fi
1324
+ fi
1325
+
1326
+ # ── 후처리: 단일 node 프로세스로 위임 ──
1327
+ # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
1328
+ local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
1329
+ if [[ -f "$post_script" ]]; then
1330
+ node "$post_script" \
1331
+ --agent "$AGENT_TYPE" \
1332
+ --cli "$CLI_TYPE" \
1333
+ --cli-cmd "$CLI_CMD" \
1334
+ --effort "$CLI_EFFORT" \
1335
+ --run-mode "$RUN_MODE" \
1336
+ --opus "$OPUS_OVERSIGHT" \
1337
+ --exit-code "$exit_code" \
1338
+ --elapsed "$elapsed" \
1339
+ --timeout "$TIMEOUT_SEC" \
1340
+ --mcp-profile "$MCP_PROFILE" \
1341
+ --stderr-log "$STDERR_LOG" \
1342
+ --stdout-log "$STDOUT_LOG" \
1343
+ --max-bytes "$MAX_STDOUT_BYTES" \
1344
+ --tee-active "$use_tee"
1345
+ else
1346
+ # post.mjs 없으면 기본 출력 (fallback)
1347
+ echo "=== TFX-ROUTE RESULT ==="
1348
+ echo "agent: $AGENT_TYPE"
1349
+ echo "cli: $CLI_TYPE"
1350
+ echo "exit_code: $exit_code"
1351
+ echo "elapsed: ${elapsed}s"
1352
+ echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
1353
+ echo "=== OUTPUT ==="
1354
+ cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
1355
+ fi
1356
+
1357
+ return "$exit_code"
1358
+ }
1359
+
1360
+ main