triflux 3.3.0-dev.1 → 3.3.0-dev.5

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.
@@ -48,7 +48,8 @@ TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
48
48
  TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
49
49
  TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
50
50
  TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
51
- TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}"
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
52
53
 
53
54
  # fallback 시 원래 에이전트 정보 보존
54
55
  ORIGINAL_AGENT=""
@@ -65,45 +66,228 @@ deregister_agent() {
65
66
  rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
66
67
  }
67
68
 
69
+ normalize_script_path() {
70
+ local path="${1:-}"
71
+ if [[ -z "$path" ]]; then
72
+ return 0
73
+ fi
74
+
75
+ if command -v cygpath &>/dev/null; then
76
+ case "$path" in
77
+ [A-Za-z]:\\*|[A-Za-z]:/*)
78
+ cygpath -u "$path"
79
+ return 0
80
+ ;;
81
+ esac
82
+ fi
83
+
84
+ printf '%s\n' "$path"
85
+ }
86
+
68
87
  # ── 팀 Hub Bridge 통신 ──
69
- # JSON 문자열 이스케이프 (큰따옴표, 백슬래시, 개행, 탭, CR)
70
- json_escape() {
71
- local s="${1:-}"
72
- # node로 완전한 JSON 이스케이프 (NUL, 멀티바이트 UTF-8, 제어문자 안전)
73
- if command -v node &>/dev/null; then
74
- node -e 'process.stdout.write(JSON.stringify(process.argv[1]).slice(1,-1))' -- "$s"
75
- return
88
+ resolve_bridge_script() {
89
+ if [[ -n "${TFX_BRIDGE_SCRIPT:-}" && -f "$TFX_BRIDGE_SCRIPT" ]]; then
90
+ printf '%s\n' "$TFX_BRIDGE_SCRIPT"
91
+ return 0
92
+ fi
93
+
94
+ local script_dir
95
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
96
+ local candidates=(
97
+ "$script_dir/../hub/bridge.mjs"
98
+ "$script_dir/hub/bridge.mjs"
99
+ )
100
+
101
+ local candidate
102
+ for candidate in "${candidates[@]}"; do
103
+ if [[ -f "$candidate" ]]; then
104
+ printf '%s\n' "$candidate"
105
+ return 0
106
+ fi
107
+ done
108
+
109
+ return 1
110
+ }
111
+
112
+ bridge_cli() {
113
+ if ! command -v "$NODE_BIN" &>/dev/null; then
114
+ return 127
115
+ fi
116
+
117
+ local bridge_script
118
+ if ! bridge_script=$(resolve_bridge_script); then
119
+ return 127
120
+ fi
121
+
122
+ TFX_HUB_PIPE="$TFX_HUB_PIPE" TFX_HUB_URL="$TFX_HUB_URL" TFX_HUB_TOKEN="${TFX_HUB_TOKEN:-}" \
123
+ "$NODE_BIN" "$bridge_script" "$@" 2>/dev/null
124
+ }
125
+
126
+ bridge_json_get() {
127
+ local json="${1:-}"
128
+ local path="${2:-}"
129
+ [[ -z "$json" || -z "$path" ]] && return 1
130
+
131
+ "$NODE_BIN" -e '
132
+ const data = JSON.parse(process.argv[1] || "{}");
133
+ const keys = String(process.argv[2] || "").split(".").filter(Boolean);
134
+ let value = data;
135
+ for (const key of keys) value = value?.[key];
136
+ if (value === undefined || value === null) process.exit(1);
137
+ process.stdout.write(typeof value === "object" ? JSON.stringify(value) : String(value));
138
+ ' -- "$json" "$path" 2>/dev/null
139
+ }
140
+
141
+ bridge_json_stringify() {
142
+ local mode="${1:-}"
143
+ shift || true
144
+
145
+ case "$mode" in
146
+ metadata-patch)
147
+ "$NODE_BIN" -e '
148
+ process.stdout.write(JSON.stringify({
149
+ result: process.argv[1] || "",
150
+ summary: process.argv[2] || "",
151
+ }));
152
+ ' -- "${1:-}" "${2:-}"
153
+ ;;
154
+ task-result)
155
+ "$NODE_BIN" -e '
156
+ process.stdout.write(JSON.stringify({
157
+ task_id: process.argv[1] || "",
158
+ result: process.argv[2] || "",
159
+ }));
160
+ ' -- "${1:-}" "${2:-}"
161
+ ;;
162
+ *)
163
+ return 1
164
+ ;;
165
+ esac
166
+ }
167
+
168
+ team_send_message() {
169
+ local text="${1:-}"
170
+ local summary="${2:-}"
171
+ [[ -z "$TFX_TEAM_NAME" || -z "$text" ]] && return 0
172
+
173
+ if ! bridge_cli_with_restart "팀 메시지 전송" "Hub 재시작 후 팀 메시지 전송 성공." \
174
+ team-send-message \
175
+ --team "$TFX_TEAM_NAME" \
176
+ --from "$TFX_TEAM_AGENT_NAME" \
177
+ --to "$TFX_TEAM_LEAD_NAME" \
178
+ --text "$text" \
179
+ --summary "${summary:-status update}"; then
180
+ echo "[tfx-route] 경고: 팀 메시지 전송 실패 (team=$TFX_TEAM_NAME, to=$TFX_TEAM_LEAD_NAME)" >&2
181
+ return 0
182
+ fi
183
+
184
+ return 0
185
+ }
186
+
187
+ # ── Hub 자동 재시작 (슬립 복귀 등으로 Hub 종료 시) ──
188
+ try_restart_hub() {
189
+ local hub_server script_dir hub_port
190
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
191
+ hub_server="$script_dir/../hub/server.mjs"
192
+
193
+ if [[ ! -f "$hub_server" ]]; then
194
+ echo "[tfx-route] Hub 서버 스크립트 미발견: $hub_server" >&2
195
+ return 1
76
196
  fi
77
- # node 미설치 fallback: 기본 Bash 치환
78
- s="${s//\\/\\\\}"
79
- s="${s//\"/\\\"}"
80
- s="${s//$'\n'/\\n}"
81
- s="${s//$'\t'/\\t}"
82
- s="${s//$'\r'/\\r}"
83
- printf '%s' "$s"
197
+
198
+ # TFX_HUB_URL에서 포트 추출 (기본 27888)
199
+ hub_port="${TFX_HUB_URL##*:}"
200
+ hub_port="${hub_port%%/*}"
201
+ [[ -z "$hub_port" || "$hub_port" == "$TFX_HUB_URL" ]] && hub_port=27888
202
+
203
+ echo "[tfx-route] Hub 미응답 — 자동 재시작 시도 (port=$hub_port)..." >&2
204
+ TFX_HUB_PORT="$hub_port" "$NODE_BIN" "$hub_server" &>/dev/null &
205
+ local hub_pid=$!
206
+
207
+ # 최대 4초 대기 (0.5초 간격)
208
+ local i
209
+ for i in 1 2 3 4 5 6 7 8; do
210
+ sleep 0.5
211
+ if curl -sf "${TFX_HUB_URL}/status" >/dev/null 2>&1; then
212
+ echo "[tfx-route] Hub 재시작 성공 (pid=$hub_pid)" >&2
213
+ return 0
214
+ fi
215
+ done
216
+
217
+ echo "[tfx-route] Hub 재시작 실패 — claim 없이 계속 실행" >&2
218
+ return 1
219
+ }
220
+
221
+ bridge_cli_with_restart() {
222
+ local action_label="${1:-bridge 호출}"
223
+ local success_message="${2:-}"
224
+ shift 2 || true
225
+
226
+ if bridge_cli "$@" >/dev/null 2>&1; then
227
+ return 0
228
+ fi
229
+
230
+ if ! try_restart_hub; then
231
+ return 1
232
+ fi
233
+
234
+ if bridge_cli "$@" >/dev/null 2>&1; then
235
+ [[ -n "$success_message" ]] && echo "[tfx-route] ${success_message}" >&2
236
+ return 0
237
+ fi
238
+
239
+ echo "[tfx-route] 경고: Hub 재시작 후 ${action_label} 재시도 실패." >&2
240
+ return 1
84
241
  }
85
242
 
86
243
  team_claim_task() {
87
244
  [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
88
- local http_code safe_team_name safe_task_id safe_agent_name
89
- safe_team_name=$(json_escape "$TFX_TEAM_NAME")
90
- safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
91
- safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
92
-
93
- http_code=$(curl -sf -o /dev/null -w "%{http_code}" -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
94
- -H "Content-Type: application/json" \
95
- -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"claim\":true,\"owner\":\"${safe_agent_name}\",\"status\":\"in_progress\"}" \
96
- 2>/dev/null) || http_code="000"
97
-
98
- case "$http_code" in
99
- 200) ;; # 성공
100
- 409)
101
- echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨. 실행 중단." >&2
245
+ local response ok error_code error_message owner_before status_before
246
+ response=$(bridge_cli team-task-update \
247
+ --team "$TFX_TEAM_NAME" \
248
+ --task-id "$TFX_TEAM_TASK_ID" \
249
+ --claim \
250
+ --owner "$TFX_TEAM_AGENT_NAME" \
251
+ --status in_progress || true)
252
+
253
+ ok=$(bridge_json_get "$response" "ok" || true)
254
+ error_code=$(bridge_json_get "$response" "error.code" || true)
255
+ error_message=$(bridge_json_get "$response" "error.message" || true)
256
+ owner_before=$(bridge_json_get "$response" "error.details.task_before.owner" || true)
257
+ status_before=$(bridge_json_get "$response" "error.details.task_before.status" || true)
258
+
259
+ case "$ok:$error_code" in
260
+ true:*) ;;
261
+ false:CLAIM_CONFLICT)
262
+ if [[ "$owner_before" == "$TFX_TEAM_AGENT_NAME" && "$status_before" == "in_progress" ]]; then
263
+ echo "[tfx-route] 동일 owner(${TFX_TEAM_AGENT_NAME})가 이미 claim한 task ${TFX_TEAM_TASK_ID} — 계속 실행." >&2
264
+ return 0
265
+ fi
266
+ echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨(owner=${owner_before:-unknown}, status=${status_before:-unknown}). 실행 중단." >&2
267
+ team_send_message \
268
+ "task ${TFX_TEAM_TASK_ID} claim conflict: owner=${owner_before:-unknown}, status=${status_before:-unknown}" \
269
+ "task ${TFX_TEAM_TASK_ID} claim conflict"
102
270
  exit 0 ;;
103
- 000)
104
- echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2 ;;
271
+ :|false:)
272
+ # Hub 연결 실패 자동 재시작 시도 claim 재시도
273
+ if try_restart_hub; then
274
+ response=$(bridge_cli team-task-update \
275
+ --team "$TFX_TEAM_NAME" \
276
+ --task-id "$TFX_TEAM_TASK_ID" \
277
+ --claim \
278
+ --owner "$TFX_TEAM_AGENT_NAME" \
279
+ --status in_progress || true)
280
+ ok=$(bridge_json_get "$response" "ok" || true)
281
+ if [[ "$ok" == "true" ]]; then
282
+ echo "[tfx-route] Hub 재시작 후 claim 성공." >&2
283
+ else
284
+ echo "[tfx-route] 경고: Hub 재시작 후 claim 실패. claim 없이 계속 실행." >&2
285
+ fi
286
+ else
287
+ echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2
288
+ fi ;;
105
289
  *)
106
- echo "[tfx-route] 경고: Hub claim 응답 HTTP ${http_code}. claim 없이 계속 실행." >&2 ;;
290
+ echo "[tfx-route] 경고: Hub claim 실패 (${error_code:-unknown}${error_message:+: ${error_message}}). claim 없이 계속 실행." >&2 ;;
107
291
  esac
108
292
  }
109
293
 
@@ -112,32 +296,51 @@ team_complete_task() {
112
296
  local result_summary="${2:-작업 완료}"
113
297
  [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
114
298
 
115
- local safe_team_name safe_task_id safe_agent_name safe_result safe_summary safe_lead_name
116
- safe_team_name=$(json_escape "$TFX_TEAM_NAME")
117
- safe_task_id=$(json_escape "$TFX_TEAM_TASK_ID")
118
- safe_agent_name=$(json_escape "$TFX_TEAM_AGENT_NAME")
119
- safe_result=$(json_escape "$result")
120
- safe_summary=$(json_escape "$(echo "$result_summary" | head -c 4096)")
121
- safe_lead_name=$(json_escape "$TFX_TEAM_LEAD_NAME")
299
+ local summary_trimmed metadata_patch result_payload
300
+ summary_trimmed=$(echo "$result_summary" | head -c 4096)
301
+ metadata_patch=$(bridge_json_stringify metadata-patch "$result" "$summary_trimmed" 2>/dev/null || true)
302
+ result_payload=$(bridge_json_stringify task-result "$TFX_TEAM_TASK_ID" "$result" 2>/dev/null || true)
122
303
 
123
304
  # task 상태: 항상 "completed" (Claude Code API는 "failed" 미지원)
124
305
  # 실제 결과는 metadata.result로 전달
125
- curl -sf -X POST "${TFX_HUB_URL}/bridge/team/task-update" \
126
- -H "Content-Type: application/json" \
127
- -d "{\"team_name\":\"${safe_team_name}\",\"task_id\":\"${safe_task_id}\",\"status\":\"completed\",\"owner\":\"${safe_agent_name}\",\"metadata_patch\":{\"result\":\"${safe_result}\",\"summary\":\"${safe_summary}\"}}" \
128
- >/dev/null 2>&1 || true
306
+ if [[ -n "$metadata_patch" ]]; then
307
+ if ! bridge_cli_with_restart " task 완료 보고" "Hub 재시작 후 팀 task 완료 보고 성공." \
308
+ team-task-update \
309
+ --team "$TFX_TEAM_NAME" \
310
+ --task-id "$TFX_TEAM_TASK_ID" \
311
+ --status completed \
312
+ --owner "$TFX_TEAM_AGENT_NAME" \
313
+ --metadata-patch "$metadata_patch"; then
314
+ echo "[tfx-route] 경고: 팀 task 완료 보고 실패 (team=$TFX_TEAM_NAME, task=$TFX_TEAM_TASK_ID, result=$result)" >&2
315
+ fi
316
+ fi
129
317
 
130
318
  # 리드에게 메시지 전송
131
- curl -sf -X POST "${TFX_HUB_URL}/bridge/team/send-message" \
132
- -H "Content-Type: application/json" \
133
- -d "{\"team_name\":\"${safe_team_name}\",\"from\":\"${safe_agent_name}\",\"to\":\"${safe_lead_name}\",\"text\":\"${safe_summary}\",\"summary\":\"task ${safe_task_id} ${safe_result}\"}" \
134
- >/dev/null 2>&1 || true
319
+ team_send_message "$summary_trimmed" "task ${TFX_TEAM_TASK_ID} ${result}"
135
320
 
136
321
  # Hub result 발행 (poll_messages 채널 활성화)
137
- curl -sf -X POST "${TFX_HUB_URL}/bridge/result" \
138
- -H "Content-Type: application/json" \
139
- -d "{\"agent_id\":\"${safe_agent_name}\",\"topic\":\"task.result\",\"payload\":{\"task_id\":\"${safe_task_id}\",\"result\":\"${safe_result}\"},\"trace_id\":\"${safe_team_name}\"}" \
140
- >/dev/null 2>&1 || true
322
+ if [[ -n "$result_payload" ]]; then
323
+ if ! bridge_cli_with_restart "Hub result 발행" "Hub 재시작 후 Hub result 발행 성공." \
324
+ result \
325
+ --agent "$TFX_TEAM_AGENT_NAME" \
326
+ --topic task.result \
327
+ --payload "$result_payload" \
328
+ --trace "$TFX_TEAM_NAME"; then
329
+ echo "[tfx-route] 경고: Hub result 발행 실패 (agent=$TFX_TEAM_AGENT_NAME, task=$TFX_TEAM_TASK_ID)" >&2
330
+ fi
331
+ fi
332
+ }
333
+
334
+ capture_workspace_signature() {
335
+ if ! command -v git &>/dev/null; then
336
+ return 1
337
+ fi
338
+
339
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
340
+ return 1
341
+ fi
342
+
343
+ git status --short --untracked-files=all --ignore-submodules=all 2>/dev/null || return 1
141
344
  }
142
345
 
143
346
  # ── 라우팅 테이블 ──
@@ -253,6 +456,7 @@ route_agent() {
253
456
  # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
254
457
  TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
255
458
  TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
459
+ TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
256
460
  TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-auto}"
257
461
  TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
258
462
  TFX_SEARCH_TOOL="${TFX_SEARCH_TOOL:-}"
@@ -270,6 +474,13 @@ case "$TFX_CODEX_TRANSPORT" in
270
474
  exit 1
271
475
  ;;
272
476
  esac
477
+ case "$TFX_VERIFIER_OVERRIDE" in
478
+ auto|claude) ;;
479
+ *)
480
+ echo "ERROR: TFX_VERIFIER_OVERRIDE 값은 auto 또는 claude여야 합니다. (현재: $TFX_VERIFIER_OVERRIDE)" >&2
481
+ exit 1
482
+ ;;
483
+ esac
273
484
  case "$TFX_WORKER_INDEX" in
274
485
  "") ;;
275
486
  *[!0-9]*|0)
@@ -391,8 +602,33 @@ apply_no_claude_native_mode() {
391
602
  echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
392
603
  }
393
604
 
605
+ apply_verifier_override() {
606
+ [[ "$AGENT_TYPE" != "verifier" ]] && return
607
+
608
+ case "$TFX_VERIFIER_OVERRIDE" in
609
+ auto|"")
610
+ return 0
611
+ ;;
612
+ claude)
613
+ ORIGINAL_AGENT="${ORIGINAL_AGENT:-$AGENT_TYPE}"
614
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
615
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
616
+ echo "[tfx-route] TFX_VERIFIER_OVERRIDE=claude: verifier -> claude-native" >&2
617
+ ;;
618
+ esac
619
+
620
+ return 0
621
+ }
622
+
394
623
  # ── MCP 인벤토리 캐시 ──
395
624
  MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
625
+ MCP_FILTER_SCRIPT=""
626
+ MCP_PROFILE_REQUESTED="auto"
627
+ MCP_RESOLVED_PROFILE="default"
628
+ MCP_HINT=""
629
+ GEMINI_ALLOWED_SERVERS=()
630
+ CODEX_CONFIG_FLAGS=()
631
+ CODEX_CONFIG_JSON=""
396
632
 
397
633
  get_cached_servers() {
398
634
  local cli_type="$1"
@@ -401,119 +637,71 @@ get_cached_servers() {
401
637
  fi
402
638
  }
403
639
 
404
- # ── MCP 프로필 → 프롬프트 힌트 (통합: 캐시 유무 단일 코드경로) ──
405
- get_mcp_hint() {
406
- local profile="$1"
407
- local agent="$2"
408
-
409
- # auto → 구체 프로필 해석
410
- if [[ "$profile" == "auto" ]]; then
411
- case "$agent" in
412
- executor|build-fixer|debugger|deep-executor) profile="implement" ;;
413
- architect|planner|critic|analyst) profile="analyze" ;;
414
- code-reviewer|security-reviewer|quality-reviewer) profile="review" ;;
415
- scientist|document-specialist) profile="analyze" ;;
416
- designer|writer) profile="docs" ;;
417
- *) profile="minimal" ;;
418
- esac
640
+ resolve_mcp_filter_script() {
641
+ if [[ -n "$MCP_FILTER_SCRIPT" && -f "$MCP_FILTER_SCRIPT" ]]; then
642
+ printf '%s\n' "$MCP_FILTER_SCRIPT"
643
+ return 0
419
644
  fi
420
645
 
421
- # 서버 목록: 캐시 있으면 실제, 없으면 전부 가용 가정 (기존 비캐시 동작과 동일)
422
- local servers
423
- servers=$(get_cached_servers "$CLI_TYPE")
424
- [[ -z "$servers" ]] && servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
646
+ local script_ref script_dir candidate
647
+ local -a candidates=()
425
648
 
426
- has_server() { echo ",$servers," | grep -q ",$1,"; }
427
-
428
- get_search_tool_order() {
429
- local available=()
430
- local ordered=()
431
- local tool
649
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
650
+ if [[ -n "$script_ref" ]]; then
651
+ script_dir="$(cd "$(dirname "$script_ref")" 2>/dev/null && pwd -P || true)"
652
+ [[ -n "$script_dir" ]] && candidates+=("$script_dir/lib/mcp-filter.mjs")
653
+ fi
432
654
 
433
- for tool in brave-search tavily exa; do
434
- has_server "$tool" && available+=("$tool")
435
- done
655
+ candidates+=(
656
+ "$PWD/scripts/lib/mcp-filter.mjs"
657
+ "$PWD/lib/mcp-filter.mjs"
658
+ )
436
659
 
437
- if [[ ${#available[@]} -eq 0 ]]; then
660
+ for candidate in "${candidates[@]}"; do
661
+ if [[ -f "$candidate" ]]; then
662
+ MCP_FILTER_SCRIPT="$candidate"
663
+ printf '%s\n' "$MCP_FILTER_SCRIPT"
438
664
  return 0
439
665
  fi
666
+ done
440
667
 
441
- if [[ -n "$TFX_SEARCH_TOOL" ]]; then
442
- for tool in "${available[@]}"; do
443
- [[ "$tool" == "$TFX_SEARCH_TOOL" ]] && ordered+=("$tool")
444
- done
445
- for tool in "${available[@]}"; do
446
- [[ "$tool" != "$TFX_SEARCH_TOOL" ]] && ordered+=("$tool")
447
- done
448
- elif [[ -n "$TFX_WORKER_INDEX" && ${#available[@]} -gt 1 ]]; then
449
- local offset=$(( (TFX_WORKER_INDEX - 1) % ${#available[@]} ))
450
- local i idx
451
- for ((i=0; i<${#available[@]}; i++)); do
452
- idx=$(( (offset + i) % ${#available[@]} ))
453
- ordered+=("${available[$idx]}")
454
- done
455
- else
456
- ordered=("${available[@]}")
457
- fi
458
-
459
- printf '%s\n' "${ordered[*]}"
460
- }
668
+ return 1
669
+ }
461
670
 
462
- local ordered_tools=()
463
- read -r -a ordered_tools <<< "$(get_search_tool_order)"
464
- local ordered_tools_csv=""
465
- if [[ ${#ordered_tools[@]} -gt 0 ]]; then
466
- ordered_tools_csv=$(printf '%s, ' "${ordered_tools[@]}")
467
- ordered_tools_csv="${ordered_tools_csv%, }"
671
+ resolve_mcp_policy() {
672
+ local filter_script available_servers
673
+ if ! filter_script=$(resolve_mcp_filter_script); then
674
+ echo "[tfx-route] 경고: mcp-filter.mjs를 찾지 못해 기본 MCP 정책을 사용합니다." >&2
675
+ MCP_PROFILE_REQUESTED="$MCP_PROFILE"
676
+ MCP_RESOLVED_PROFILE="$MCP_PROFILE"
677
+ MCP_HINT=""
678
+ GEMINI_ALLOWED_SERVERS=()
679
+ CODEX_CONFIG_FLAGS=()
680
+ CODEX_CONFIG_JSON=""
681
+ return 0
468
682
  fi
469
683
 
470
- local hint=""
471
- case "$profile" in
472
- implement)
473
- has_server "context7" && hint+="context7으로 라이브러리 문서를 조회하세요. "
474
- if [[ ${#ordered_tools[@]} -gt 0 ]]; then
475
- hint+="웹 검색은 ${ordered_tools[0]}를 사용하세요. "
476
- fi
477
- hint+="검색 도구 실패 시 402, 429, 432, 433, quota 에러에서 재시도하지 말고 다음 도구로 전환하세요."
478
- ;;
479
- analyze)
480
- has_server "context7" && hint+="context7으로 관련 문서를 조회하세요. "
481
- [[ -n "$ordered_tools_csv" ]] && hint+="웹 검색 우선순위: ${ordered_tools_csv}. 402, 429, 432, 433, quota 에러 시 즉시 다음 도구로 전환. "
482
- has_server "playwright" && hint+="모든 검색 실패 시 playwright로 직접 방문 (최대 3 URL). "
483
- hint+="검색 깊이를 제한하고 결과를 빠르게 요약하세요."
484
- ;;
485
- review)
486
- has_server "sequential-thinking" && hint="sequential-thinking으로 체계적으로 분석하세요."
487
- ;;
488
- docs)
489
- has_server "context7" && hint+="context7으로 공식 문서를 참조하세요. "
490
- if [[ ${#ordered_tools[@]} -gt 0 ]]; then
491
- hint+="추가 검색은 ${ordered_tools[0]}를 사용하세요. "
492
- fi
493
- hint+="검색 결과의 출처 URL을 함께 제시하세요."
494
- ;;
495
- minimal|none) ;;
496
- esac
497
- echo "$hint"
498
- }
684
+ available_servers=$(get_cached_servers "$CLI_TYPE")
685
+ [[ -z "$available_servers" ]] && available_servers="context7,brave-search,exa,tavily,playwright,sequential-thinking"
499
686
 
500
- # ── Gemini MCP 서버 선택적 로드 ──
501
- get_gemini_mcp_servers() {
502
- local profile="$1"
503
- case "$profile" in
504
- implement) echo "context7 brave-search" ;;
505
- analyze) echo "context7 brave-search exa tavily" ;;
506
- review) echo "sequential-thinking" ;;
507
- docs) echo "context7 brave-search" ;;
508
- *) echo "" ;;
509
- esac
510
- }
687
+ local -a cmd=(
688
+ "$NODE_BIN" "$filter_script" shell
689
+ "--agent" "$AGENT_TYPE"
690
+ "--profile" "$MCP_PROFILE"
691
+ "--available" "$available_servers"
692
+ "--inventory-file" "$MCP_CACHE"
693
+ "--task-text" "$PROMPT"
694
+ )
695
+ [[ -n "$TFX_SEARCH_TOOL" ]] && cmd+=("--search-tool" "$TFX_SEARCH_TOOL")
696
+ [[ -n "$TFX_WORKER_INDEX" ]] && cmd+=("--worker-index" "$TFX_WORKER_INDEX")
697
+
698
+ local shell_exports
699
+ if ! shell_exports="$("${cmd[@]}")"; then
700
+ echo "[tfx-route] ERROR: MCP 정책 계산 실패" >&2
701
+ return 1
702
+ fi
511
703
 
512
- get_gemini_mcp_filter() {
513
- local servers
514
- servers=$(get_gemini_mcp_servers "$1")
515
- [[ -z "$servers" ]] && return 0
516
- echo "--allowed-mcp-server-names ${servers// /,}"
704
+ eval "$shell_exports"
517
705
  }
518
706
 
519
707
  get_claude_model() {
@@ -544,8 +732,9 @@ resolve_worker_runner_script() {
544
732
  return 0
545
733
  fi
546
734
 
547
- local script_dir
548
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
735
+ local script_ref script_dir
736
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
737
+ script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
549
738
  local candidate="$script_dir/tfx-route-worker.mjs"
550
739
  [[ -f "$candidate" ]] || return 1
551
740
  printf '%s\n' "$candidate"
@@ -587,19 +776,32 @@ run_stream_worker() {
587
776
  run_legacy_gemini() {
588
777
  local prompt="$1"
589
778
  local use_tee_flag="$2"
590
- local gemini_mcp_filter
591
- gemini_mcp_filter=$(get_gemini_mcp_filter "$MCP_PROFILE")
592
- local gemini_args="$CLI_ARGS"
593
-
594
- if [[ -n "$gemini_mcp_filter" ]]; then
595
- gemini_args="${CLI_ARGS/--prompt/$gemini_mcp_filter --prompt}"
596
- echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
779
+ local -a gemini_args=()
780
+ read -r -a gemini_args <<< "$CLI_ARGS"
781
+
782
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
783
+ local gemini_mcp_filter prompt_index=-1
784
+ gemini_mcp_filter=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")
785
+ for i in "${!gemini_args[@]}"; do
786
+ if [[ "${gemini_args[$i]}" == "--prompt" ]]; then
787
+ prompt_index="$i"
788
+ break
789
+ fi
790
+ done
791
+ if [[ "$prompt_index" -ge 0 ]]; then
792
+ gemini_args=(
793
+ "${gemini_args[@]:0:$prompt_index}"
794
+ "--allowed-mcp-server-names" "$gemini_mcp_filter"
795
+ "${gemini_args[@]:$prompt_index}"
796
+ )
797
+ echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
798
+ fi
597
799
  fi
598
800
 
599
801
  if [[ "$use_tee_flag" == "true" ]]; then
600
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
802
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
601
803
  else
602
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
804
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
603
805
  fi
604
806
  local pid=$!
605
807
 
@@ -622,9 +824,9 @@ run_legacy_gemini() {
622
824
  wait "$pid" 2>/dev/null
623
825
  echo "[tfx-route] Gemini crash 감지, 재시도 중..." >&2
624
826
  if [[ "$use_tee_flag" == "true" ]]; then
625
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
827
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
626
828
  else
627
- timeout "$TIMEOUT_SEC" $CLI_CMD $gemini_args "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
829
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${gemini_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
628
830
  fi
629
831
  pid=$!
630
832
  fi
@@ -639,8 +841,9 @@ resolve_codex_mcp_script() {
639
841
  return 0
640
842
  fi
641
843
 
642
- local script_dir
643
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
844
+ local script_ref script_dir
845
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
846
+ script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
644
847
  local candidates=(
645
848
  "$script_dir/hub/workers/codex-mcp.mjs"
646
849
  "$script_dir/../hub/workers/codex-mcp.mjs"
@@ -661,11 +864,16 @@ run_codex_exec() {
661
864
  local prompt="$1"
662
865
  local use_tee_flag="$2"
663
866
  local exit_code_local=0
867
+ local -a codex_args=()
868
+ read -r -a codex_args <<< "$CLI_ARGS"
869
+ if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
870
+ codex_args+=("${CODEX_CONFIG_FLAGS[@]}")
871
+ fi
664
872
 
665
873
  if [[ "$use_tee_flag" == "true" ]]; then
666
- timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
874
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" || exit_code_local=$?
667
875
  else
668
- timeout "$TIMEOUT_SEC" $CLI_CMD $CLI_ARGS "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
876
+ timeout "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" || exit_code_local=$?
669
877
  fi
670
878
 
671
879
  if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
@@ -723,6 +931,10 @@ run_codex_mcp() {
723
931
  "--codex-command" "$CODEX_BIN"
724
932
  )
725
933
 
934
+ if [[ -n "$CODEX_CONFIG_JSON" && "$CODEX_CONFIG_JSON" != "{}" ]]; then
935
+ mcp_args+=("--config-json" "$CODEX_CONFIG_JSON")
936
+ fi
937
+
726
938
  case "$AGENT_TYPE" in
727
939
  code-reviewer)
728
940
  mcp_args+=(
@@ -767,6 +979,7 @@ main() {
767
979
  route_agent "$AGENT_TYPE"
768
980
  apply_cli_mode
769
981
  apply_no_claude_native_mode
982
+ apply_verifier_override
770
983
 
771
984
  # CLI 경로 해석
772
985
  case "$CLI_CMD" in
@@ -811,6 +1024,8 @@ ${ctx_content}
811
1024
  </prior_context>"
812
1025
  fi
813
1026
 
1027
+ resolve_mcp_policy
1028
+
814
1029
  # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
815
1030
  if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
816
1031
  if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
@@ -822,22 +1037,37 @@ ${ctx_content}
822
1037
  fi
823
1038
  fi
824
1039
 
825
- # Claude 네이티브 에이전트는 이 스크립트로 처리 불가 → 메타데이터만 출력
1040
+ # Claude 네이티브 에이전트는 이 스크립트로 처리 불가
826
1041
  if [[ "$CLI_TYPE" == "claude-native" ]]; then
1042
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1043
+ # 팀 모드: Hub에 fallback 필요 시그널 전송 후 구조화된 출력
1044
+ echo "[tfx-route] claude-native 역할($AGENT_TYPE)은 tfx-route.sh로 실행 불가 — Claude Agent fallback 필요" >&2
1045
+ team_complete_task "fallback" "claude-native 역할 실행 불가: ${AGENT_TYPE}. Claude Task(sonnet) 에이전트로 위임하세요."
1046
+ cat <<FALLBACK_EOF
1047
+ === TFX_NEEDS_FALLBACK ===
1048
+ agent_type: ${AGENT_TYPE}
1049
+ reason: claude-native roles require Claude Agent tools (Read/Edit/Grep). tfx-route.sh cannot provide these.
1050
+ action: Lead should spawn Agent(subagent_type="${AGENT_TYPE}") for this task.
1051
+ task_id: ${TFX_TEAM_TASK_ID:-none}
1052
+ FALLBACK_EOF
1053
+ exit 0
1054
+ fi
827
1055
  emit_claude_native_metadata
828
1056
  exit 0
829
1057
  fi
830
1058
 
831
- # MCP 힌트 주입
832
- local mcp_hint
833
- mcp_hint=$(get_mcp_hint "$MCP_PROFILE" "$AGENT_TYPE")
834
1059
  local FULL_PROMPT="$PROMPT"
835
- [[ -n "$mcp_hint" ]] && FULL_PROMPT="${PROMPT}. ${mcp_hint}"
1060
+ [[ -n "$MCP_HINT" ]] && FULL_PROMPT="${PROMPT}. ${MCP_HINT}"
836
1061
  local codex_transport_effective="n/a"
837
1062
 
838
1063
  # 메타정보 (stderr)
839
1064
  echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
840
- echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE" >&2
1065
+ echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE resolved_profile=$MCP_RESOLVED_PROFILE verifier_override=$TFX_VERIFIER_OVERRIDE" >&2
1066
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1067
+ echo "[tfx-route] allowed_mcp_servers=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1068
+ else
1069
+ echo "[tfx-route] allowed_mcp_servers=none" >&2
1070
+ fi
841
1071
  if [[ -n "$TFX_WORKER_INDEX" || -n "$TFX_SEARCH_TOOL" ]]; then
842
1072
  echo "[tfx-route] worker_index=${TFX_WORKER_INDEX:-auto} search_tool=${TFX_SEARCH_TOOL:-auto}" >&2
843
1073
  fi
@@ -851,11 +1081,18 @@ ${ctx_content}
851
1081
 
852
1082
  # 팀 모드: task claim
853
1083
  team_claim_task
1084
+ team_send_message "작업 시작: ${TFX_TEAM_AGENT_NAME}" "task ${TFX_TEAM_TASK_ID} started"
854
1085
 
855
1086
  # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
856
1087
  local exit_code=0
857
1088
  local start_time
858
1089
  start_time=$(date +%s)
1090
+ local workspace_signature_before=""
1091
+ local workspace_signature_after=""
1092
+ local workspace_probe_supported=false
1093
+ if workspace_signature_before=$(capture_workspace_signature); then
1094
+ workspace_probe_supported=true
1095
+ fi
859
1096
 
860
1097
  # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
861
1098
  # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
@@ -899,8 +1136,6 @@ ${ctx_content}
899
1136
  }
900
1137
  }
901
1138
  }' <<< "$CLI_ARGS")
902
- local gemini_servers
903
- gemini_servers=$(get_gemini_mcp_servers "$MCP_PROFILE")
904
1139
  local -a gemini_worker_args=(
905
1140
  "--command" "$CLI_CMD"
906
1141
  "--command-args-json" "$GEMINI_BIN_ARGS_JSON"
@@ -908,10 +1143,10 @@ ${ctx_content}
908
1143
  "--approval-mode" "yolo"
909
1144
  )
910
1145
 
911
- if [[ -n "$gemini_servers" ]]; then
912
- echo "[tfx-route] Gemini MCP 서버: ${gemini_servers}" >&2
1146
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1147
+ echo "[tfx-route] Gemini MCP 서버: $(IFS=' '; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
913
1148
  local server_name
914
- for server_name in $gemini_servers; do
1149
+ for server_name in "${GEMINI_ALLOWED_SERVERS[@]}"; do
915
1150
  gemini_worker_args+=("--allowed-mcp-server-name" "$server_name")
916
1151
  done
917
1152
  fi
@@ -952,6 +1187,24 @@ EOF
952
1187
  end_time=$(date +%s)
953
1188
  local elapsed=$((end_time - start_time))
954
1189
 
1190
+ if [[ "$exit_code" -eq 0 ]]; then
1191
+ local workspace_changed="unknown"
1192
+ if [[ "$workspace_probe_supported" == "true" ]]; then
1193
+ if workspace_signature_after=$(capture_workspace_signature); then
1194
+ if [[ "$workspace_signature_before" != "$workspace_signature_after" ]]; then
1195
+ workspace_changed="yes"
1196
+ else
1197
+ workspace_changed="no"
1198
+ fi
1199
+ fi
1200
+ fi
1201
+
1202
+ if [[ ! -s "$STDOUT_LOG" && "$workspace_changed" == "no" ]]; then
1203
+ printf '%s\n' "[tfx-route] exit 0 이지만 stdout 비어있고 워크스페이스 변화가 없습니다. no-op 성공을 실패로 승격합니다." >> "$STDERR_LOG"
1204
+ exit_code=68
1205
+ fi
1206
+ fi
1207
+
955
1208
  # 팀 모드: task complete + 리드 보고
956
1209
  if [[ -n "$TFX_TEAM_NAME" ]]; then
957
1210
  if [[ "$exit_code" -eq 0 ]]; then
@@ -997,6 +1250,8 @@ EOF
997
1250
  echo "=== OUTPUT ==="
998
1251
  cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
999
1252
  fi
1253
+
1254
+ return "$exit_code"
1000
1255
  }
1001
1256
 
1002
1257
  main