#!/usr/bin/env bash
# CLI 라우팅 래퍼 (triflux)
VERSION="2.7"

# 사용법:
#   tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
#   tfx-route.sh --async <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
#   tfx-route.sh --job-status <job_id>
#   tfx-route.sh --job-result <job_id>
#
# --async: 백그라운드 실행, 즉시 job_id 반환 (Claude Code Bash 600초 제한 우회)
# --job-status: running | done | timeout | failed
# --job-result: 완료된 잡의 전체 출력
#
# 예시:
#   tfx-route.sh executor "코드 구현" implement
#   tfx-route.sh --async scientist "딥 리서치" auto 1440
#   tfx-route.sh --job-status 1742400000-12345-9876
#   tfx-route.sh --job-result 1742400000-12345-9876

set -euo pipefail

_AGY_HEADLESS_BIN=""
_AGY_HEADLESS_SUPPORTED=1
_AGY_CLAUDE_FALLBACK=0

# ── 영속 machine profile (env > profile > runtime observation) ──
# 신뢰 경계: 파일을 source/eval 하지 않고 allowlist literal만 읽는다.
resolve_machine_profile_path() {
  if [[ -n "${TFX_MACHINE_PROFILE_PATH:-}" ]]; then
    printf '%s\n' "$TFX_MACHINE_PROFILE_PATH"
    return 0
  fi
  case "$(uname -s 2>/dev/null || true)" in
    MINGW*|MSYS*|CYGWIN*)
      local appdata_path="${APPDATA:-${HOME}/AppData/Roaming}"
      if command -v cygpath >/dev/null 2>&1; then
        appdata_path="$(cygpath -u "$appdata_path" 2>/dev/null || printf '%s' "$appdata_path")"
      else
        appdata_path="${appdata_path//\\//}"
      fi
      printf '%s/triflux/machine-profile.env\n' "${appdata_path%/}"
      ;;
    *)
      printf '%s/triflux/machine-profile.env\n' "${XDG_CONFIG_HOME:-${HOME}/.config}"
      ;;
  esac
}

load_machine_profile() {
  local profile_path line key value
  profile_path="$(resolve_machine_profile_path)"
  [[ -r "$profile_path" ]] || return 0

  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    [[ -z "$line" || "$line" == \#* ]] && continue
    if [[ "$line" != *=* ]]; then
      echo "[tfx-route] machine profile 형식 무시: KEY=VALUE 아님" >&2
      continue
    fi
    key="${line%%=*}"
    value="${line#*=}"
    if [[ "$key" == "TFX_ANTIGRAVITY_FALLBACK" && -z "${TFX_ANTIGRAVITY_FALLBACK+x}" ]]; then
      case "$value" in
        codex|claude) ;;
        *)
          echo "ERROR: TFX_ANTIGRAVITY_FALLBACK 값은 codex 또는 claude여야 합니다. (machine profile)" >&2
          return 1 ;;
      esac
    fi
    if [[ -z "$value" || "$value" == *[!A-Za-z0-9_.-]* ]]; then
      echo "[tfx-route] machine profile ${key} 값 무시: 안전한 literal 아님" >&2
      continue
    fi
    case "$key" in
      TFX_MACHINE_PROFILE_VERSION)
        [[ -n "${TFX_MACHINE_PROFILE_VERSION+x}" ]] || TFX_MACHINE_PROFILE_VERSION="$value" ;;
      TFX_MACHINE_OS)
        [[ -n "${TFX_MACHINE_OS+x}" ]] || TFX_MACHINE_OS="$value" ;;
      TFX_MULTIPLEXER_POLICY)
        [[ -n "${TFX_MULTIPLEXER_POLICY+x}" ]] || TFX_MULTIPLEXER_POLICY="$value" ;;
      TFX_DISABLE_CODEX)
        [[ -n "${TFX_DISABLE_CODEX+x}" ]] || TFX_DISABLE_CODEX="$value" ;;
      TFX_DISABLE_ANTIGRAVITY)
        [[ -n "${TFX_DISABLE_ANTIGRAVITY+x}" ]] || TFX_DISABLE_ANTIGRAVITY="$value" ;;
      TFX_ANTIGRAVITY_FALLBACK)
        [[ -n "${TFX_ANTIGRAVITY_FALLBACK+x}" ]] || TFX_ANTIGRAVITY_FALLBACK="$value" ;;
      TFX_TIMEOUT_POLICY)
        [[ -n "${TFX_TIMEOUT_POLICY+x}" ]] || TFX_TIMEOUT_POLICY="$value" ;;
      TFX_HARD_CEILING_SEC)
        [[ -n "${TFX_HARD_CEILING_SEC+x}" ]] || TFX_HARD_CEILING_SEC="$value" ;;
      TFX_STALL_THRESHOLD)
        [[ -n "${TFX_STALL_THRESHOLD+x}" ]] || TFX_STALL_THRESHOLD="$value" ;;
      TFX_STALL_KILL)
        [[ -n "${TFX_STALL_KILL+x}" ]] || TFX_STALL_KILL="$value" ;;
      *)
        echo "[tfx-route] machine profile key 무시: $key" >&2 ;;
    esac
  done < "$profile_path"
}

load_machine_profile

# 첫 인자(초)를 버리고 명령을 그대로 실행한다. timeout binary 부재 또는 명시적
# TFX_HARD_CEILING_SEC=0에서 사용한다.
_no_timeout() { shift; "$@"; }

# ── timeout 명령 호환성 — Windows에서 TIMEOUT.exe 대신 Git Bash coreutils timeout 사용 ──
if command -v /usr/bin/timeout >/dev/null 2>&1; then
  TIMEOUT_BIN="/usr/bin/timeout"
elif command -v gtimeout >/dev/null 2>&1; then
  TIMEOUT_BIN="gtimeout"  # macOS homebrew coreutils
elif command -v timeout >/dev/null 2>&1; then
  TIMEOUT_BIN="timeout"   # Linux 기본
else
  if [[ "${TFX_HARD_CEILING_SEC:-21600}" != "0" ]]; then
    echo "[tfx-route] WARNING: timeout 명령을 찾을 수 없습니다 — TFX_HARD_CEILING_SEC 최후 방어선(기본 6h)이 비활성화됩니다. hung 워커 방어는 heartbeat stall kill 뿐입니다. macOS: brew install coreutils (gtimeout 제공)" >&2
  fi
  TIMEOUT_BIN="_no_timeout"
fi

# 활동 기반 수명주기: wall-clock timeout은 hard ceiling에서만 집행한다.
# coreutils timeout -k는 SIGTERM을 무시하는 프로세스를 5초 후 SIGKILL한다.
if [[ "$TIMEOUT_BIN" == "_no_timeout" ]]; then
  TIMEOUT_CMD=("_no_timeout")
else
  TIMEOUT_CMD=("$TIMEOUT_BIN" "-k" "${TFX_HARD_CEILING_KILL_GRACE:-5}")
fi

# ── 임시 디렉토리 정규화 ──
# 공용 /tmp 의 고정 이름은 다른 사용자가 선점하거나 읽을 수 있어 uid 0700 루트를 쓴다.
# hub/lib/private-tmp.mjs 와 같은 규칙이고, 후보 순서도 node os.tmpdir() 와 맞춘다.
# 루트가 남의 것이면 다른 후보로 넘어가지 않고 실패한다. 넘어가면 node 쪽 정리기가 그 경로를 모른다.
# Windows(Git Bash)는 TEMP 가 이미 사용자별이라 그대로 쓴다.
resolve_tmp_dir() {
  local candidate="" root windows=0
  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*) windows=1 ;;
  esac
  local -a candidates=("${TMPDIR:-}" "${TMP:-}" "${TEMP:-}" "/tmp")
  [[ "$windows" == 1 ]] && candidates=("${TEMP:-}" "${TMP:-}" "/tmp")
  for candidate in "${candidates[@]}"; do
    [[ -n "$candidate" ]] || continue
    mkdir -p "$candidate" >/dev/null 2>&1 || continue
    candidate="${candidate%/}"
    if [[ "$windows" == 1 ]]; then
      printf '%s\n' "$candidate"
      return 0
    fi
    root="${candidate}/triflux-$(id -u)"
    mkdir -m 700 "$root" >/dev/null 2>&1
    if [[ -d "$root" && ! -L "$root" && -O "$root" ]]; then
      chmod 700 "$root" 2>/dev/null
      printf '%s\n' "$root"
      return 0
    fi
    echo "[tfx-route] ERROR: $root 가 다른 사용자 소유이거나 링크라 쓸 수 없습니다" >&2
    return 1
  done

  candidate="$(pwd)/.tfx-tmp"
  mkdir -m 700 -p "$candidate" >/dev/null 2>&1 || true
  printf '%s\n' "$candidate"
}

TFX_TMP="$(resolve_tmp_dir)" || exit 1

# ── Worker PID 추적 (EXIT trap에서 정리) ──
_PID_TRACK="${TFX_TMP}/tfx-route-$$-pids"

track_worker_pid() {
  echo "$1" >> "$_PID_TRACK"
}

# tee 파이프라인에서 $! 는 tee 라 실제 워커가 추적되지 않는다. 파이프라인 단계 안에서
# 자기 PID 를 남기고 워커로 exec 한다. 이 PID 는 스크립트의 직속 자식이다.
_exec_tracked() {
  local self_pid="${BASHPID:-}"
  [[ -n "$self_pid" ]] || self_pid="$(exec sh -c 'echo "$PPID"')"
  track_worker_pid "$self_pid"
  [[ "$1" == "_no_timeout" ]] && shift 2
  exec "$@"
}

# --async 서브셸은 job id 를 찍고 끝나는 맨 위 프로세스($$)보다 오래 산다. 추적 파일이
# 끝난 $$ 이름이면 SessionStart 의 session-stale-cleanup 이 소유자 사망으로 보고
# 실행 중인 워커를 SIGTERM 한다. 서브셸 안에서 자기 PID 로 다시 묶는다.
# BASHPID 가 없는 bash 3.2 에서는 sh 의 PPID(= 이 서브셸)를 쓴다.
_rebind_pid_track_to_self() {
  local self_pid="${BASHPID:-}"
  [[ -n "$self_pid" ]] || self_pid="$(exec sh -c 'echo "$PPID"')"
  _PID_TRACK="${TFX_TMP}/tfx-route-${self_pid}-pids"
}

# async 잡 서브셸의 공통 실행부.
# body 는 자체 EXIT trap(cleanup_workers)을 설치하므로 정상 종료는 straight-line 으로
# 기록하고 시그널만 trap 한다. 서브셸 안에서 호출해야 한다.
_run_async_job_body() {
  local body="$1" _ec
  set +e
  trap 'rc=$?; echo "$rc" > "$JOB_DIR/exit_code"; touch "$JOB_DIR/done"; exit "$rc"' INT TERM HUP
  _rebind_pid_track_to_self
  exec > "$JOB_DIR/result.log" 2>"$JOB_DIR/stderr.log"
  "$body"
  _ec=$?
  echo "$_ec" > "$JOB_DIR/exit_code"
  touch "$JOB_DIR/done"
  exit "$_ec"
}

# 그룹 kill 대상 워커의 명령 서명. timeout 래퍼가 리더면 인자에 CLI 이름이 남는다.
_TFX_WORKER_CMD_RE='tfx-route|antigravity|(^|[ /])(codex|agy|claude)([ .]|$)'

# Unix: 추적 PID 를 종료한다. timeout 래퍼가 없거나 tee 가 마지막 단계면 워커가
# 이 스크립트와 같은 그룹이라, 그룹 kill 이 자기와 호출자 그룹을 죽인다(#548).
# 그래서 자기 자식(재사용 PID 아님)만 다루고, 그룹 리더이면서 서명이 맞을 때만 그룹 kill,
# 아니면 그 PID 와 자손만 종료한다.
_kill_tracked_worker_unix() {
  local pid="$1" self_pid="$2" info ppid pgid cmd
  info=$(ps -o ppid=,pgid=,command= -p "$pid" 2>/dev/null) || return 0
  read -r ppid pgid cmd <<< "$info"
  [[ "$ppid" == "$self_pid" ]] || return 0
  if [[ "$pgid" == "$pid" && "$cmd" =~ $_TFX_WORKER_CMD_RE ]]; then
    kill -- "-$pgid" 2>/dev/null || true
    return 0
  fi
  local queue="$pid" all="" cur child
  while [[ -n "$queue" ]]; do
    cur="${queue%% *}"
    [[ "$queue" == *" "* ]] && queue="${queue#* }" || queue=""
    all="$all $cur"
    for child in $(pgrep -P "$cur" 2>/dev/null); do
      queue="${queue:+$queue }$child"
    done
  done
  kill $all 2>/dev/null || true
}

cleanup_workers() {
  deregister_agent 2>/dev/null || true
  [[ ! -f "$_PID_TRACK" ]] && return
  local self_pid="${BASHPID:-}"
  [[ -n "$self_pid" ]] || self_pid="$(exec sh -c 'echo "$PPID"')"
  while IFS= read -r pid; do
    [[ "$pid" =~ ^[0-9]+$ ]] || continue
    kill -0 "$pid" 2>/dev/null || continue
    case "$(uname -s)" in
      MINGW*|MSYS*)
        # Windows: taskkill /T /F로 프로세스 트리 전체 종료
        MSYS_NO_PATHCONV=1 cmd.exe //c "taskkill /T /F /PID $pid" 2>/dev/null || true ;;
      *)
        _kill_tracked_worker_unix "$pid" "$self_pid" ;;
    esac
  done < "$_PID_TRACK"
  rm -f "$_PID_TRACK"
}

# ── Preflight env vars (P0 prompt-dodge): git/npm/gh 자동 응답 ──
# 워커가 dispatch 직후 git credential / npm install / gh auth prompt에서
# stall하지 않도록 환경변수를 선주입한다. 사용자 값이 있으면 존중.
export GIT_TERMINAL_PROMPT="${GIT_TERMINAL_PROMPT:-0}"
export GIT_ASKPASS="${GIT_ASKPASS:-false}"
export npm_config_yes="${npm_config_yes:-true}"

_preflight_check_gh_auth() {
  command -v gh >/dev/null 2>&1 || return 0
  [[ -n "${GH_TOKEN:-}" || -n "${GITHUB_TOKEN:-}" ]] && return 0
  if ! gh auth status >/dev/null 2>&1; then
    echo "[tfx-route] 경고: gh 인증 미설정 (GH_TOKEN/GITHUB_TOKEN 미설정 + 'gh auth status' 실패). gh 명령 실행 시 prompt 발생 가능" >&2
  fi
}
_preflight_check_gh_auth

# MCP override 대상은 이 파일에 정의된 서버로 한정한다.
_CODEX_CONFIG="${TFX_CODEX_CONFIG:-${HOME}/.codex/config.toml}"

_sanitize_codex_legacy_profiles() {
  local config="${1:-$_CODEX_CONFIG}"
  [[ -f "$config" ]] || return 0
  if ! grep -Eq '^[[:space:]]*profile[[:space:]]*=|^[[:space:]]*\[profiles\.' "$config" 2>/dev/null; then
    return 0
  fi
  local route_dir node_bin sanitizer
  route_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  sanitizer="${route_dir}/codex-profile-sanitize.mjs"
  [[ -f "$sanitizer" ]] || {
    echo "[tfx-route] 경고: Codex profile sanitizer 없음: $sanitizer" >&2
    return 0
  }
  node_bin="${NODE_BIN:-$(command -v node 2>/dev/null || command -v node.exe 2>/dev/null || echo node)}"
  if "$node_bin" "$sanitizer" "$config" --quiet; then
    echo "[tfx-route] Codex legacy inline profiles sanitized before exec" >&2
  else
    echo "[tfx-route] 경고: Codex legacy profile sanitizer 실패 — --profile 실행이 실패할 수 있음" >&2
  fi
}

_sanitize_codex_legacy_profiles "$_CODEX_CONFIG"

build_codex_base() {
  local mode="${1:-${TFX_EXECUTION_MODE:-bypass}}"
  case "$mode" in
    read_only)
      echo "--sandbox read-only --skip-git-repo-check"
      ;;
    workspace_auto)
      echo "--sandbox workspace-write --skip-git-repo-check"
      ;;
    *)
      echo "--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
      ;;
  esac
}

build_antigravity_base() {
  local mode="${1:-${TFX_EXECUTION_MODE:-bypass}}"
  case "$mode" in
    read_only|workspace_auto)
      echo "--print --sandbox"
      ;;
    *)
      echo "--print --dangerously-skip-permissions"
      ;;
  esac
}

# ── Async Job 디렉토리 ──
# Honor caller-provided TFX_JOBS_DIR env (used by unit tests for isolated jobs dirs).
TFX_JOBS_DIR="${TFX_JOBS_DIR:-${TFX_TMP}/tfx-jobs}"

# ── --job-status / --job-result 핸들러 (인자 파싱 전에 처리) ──
if [[ "${1:-}" == "--job-status" ]]; then
  job_id="${2:?job_id 필수}"
  job_dir="$TFX_JOBS_DIR/$job_id"
  [[ -d "$job_dir" ]] || { echo "error: job not found"; exit 1; }

  if [[ -f "$job_dir/done" ]]; then
    exit_code=$(cat "$job_dir/exit_code" 2>/dev/null || echo 1)
    if [[ "$exit_code" -eq 0 ]]; then
      echo "done"
    elif [[ "$exit_code" -eq 124 ]]; then
      echo "timeout"
    else
      echo "failed"
    fi
  elif [[ -f "$job_dir/pid" ]]; then
    pid=$(cat "$job_dir/pid")
    if [[ "$pid" == "starting" ]]; then
      echo "starting"
      exit 0
    fi
    if kill -0 "$pid" 2>/dev/null; then
      # 진행 상황 힌트
      local_bytes=$(wc -c < "$job_dir/result.log" 2>/dev/null | tr -d ' ' || echo 0)
      elapsed=$(( $(date +%s) - $(cat "$job_dir/start_time" 2>/dev/null || date +%s) ))
      echo "running elapsed=${elapsed}s output=${local_bytes}B"
    elif [[ -s "$job_dir/child_pids" ]]; then
      # wrapper 죽었지만 codex child 가 살아남았으면 orphan-running 으로 분류 (Issue #176).
      while IFS= read -r child; do
        if [[ -n "$child" ]] && kill -0 "$child" 2>/dev/null; then
          echo "orphan-running pid=$child"
          exit 0
        fi
      done < "$job_dir/child_pids"
      echo "failed"
    else
      # 프로세스 종료됐는데 done 마커 없음 → 비정상 종료
      echo "failed"
    fi
  else
    echo "error: invalid job state"
    exit 1
  fi
  exit 0
fi

if [[ "${1:-}" == "--job-result" ]]; then
  job_id="${2:?job_id 필수}"
  job_dir="$TFX_JOBS_DIR/$job_id"
  [[ -d "$job_dir" ]] || { echo "error: job not found"; exit 1; }
  [[ -f "$job_dir/done" ]] || { echo "error: job still running"; exit 1; }

  result_bytes=$(wc -c < "$job_dir/result.log" 2>/dev/null | tr -d ' ' || echo 0)
  if [[ "$result_bytes" -eq 0 ]] && [[ -s "$job_dir/stderr.log" ]]; then
    cat "$job_dir/stderr.log" 2>/dev/null
  else
    cat "$job_dir/result.log" 2>/dev/null
  fi
  exit_code=$(cat "$job_dir/exit_code" 2>/dev/null || echo 1)
  exit "$exit_code"
fi

# ── --job-wait: 내부 폴링으로 완료 대기 (Bash 도구 호출 횟수 최소화) ──
# 사용법: tfx-route.sh --job-wait <job_id> [max_seconds=540]
# 출력: 주기적 "waiting elapsed=Ns" + 최종 "done"|"timeout"|"failed"|"still_running"
if [[ "${1:-}" == "--job-wait" ]]; then
  job_id="${2:?job_id 필수}"
  max_wait="${3:-540}"  # 기본 540초 (9분, Bash 도구 600초 제한 이내)
  poll_interval=15
  job_dir="$TFX_JOBS_DIR/$job_id"
  [[ -d "$job_dir" ]] || { echo "error: job not found"; exit 1; }

  elapsed=0
  while [[ "$elapsed" -lt "$max_wait" ]]; do
    if [[ -f "$job_dir/done" ]]; then
      ec=$(cat "$job_dir/exit_code" 2>/dev/null || echo 1)
      if [[ "$ec" -eq 0 ]]; then echo "done"
      elif [[ "$ec" -eq 124 ]]; then echo "timeout"
      else echo "failed (exit=$ec)"
      fi
      exit 0
    fi
    sleep "$poll_interval"
    elapsed=$((elapsed + poll_interval))
    stderr_bytes=$(wc -c < "$job_dir/stderr.log" 2>/dev/null || echo 0)
    echo "waiting elapsed=${elapsed}s progress=${stderr_bytes}B"
  done

  # max_wait 도달했지만 아직 실행 중
  echo "still_running elapsed=${elapsed}s"
  exit 0
fi

# ── --async 플래그 감지 ──
TFX_ASYNC_MODE=0
if [[ "${1:-}" == "--async" ]]; then
  TFX_ASYNC_MODE=1
  shift
fi

# ── 인자 파싱 ──
AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
PROMPT="${2:?프롬프트 필수}"
MCP_PROFILE="${3:-auto}"
USER_TIMEOUT="${4:-}"
CONTEXT_FILE="${5:-}"

case "$AGENT_TYPE" in
  multi|team)
    echo "ERROR: '$AGENT_TYPE' is a tfx CLI subcommand, not a tfx-route agent." >&2
    echo "Use: tfx multi ... for team/headless dispatch." >&2
    echo "macOS/Linux headless uses tmux; Windows uses psmux." >&2
    exit 64
    ;;
esac

# ── CLI 이름은 route_agent()에서 기본 역할 alias로 처리됨 (codex→executor, antigravity/agy→Antigravity, claude→explore) ──

# ── 인자 검증: MCP_PROFILE이 --flag 형태인 경우 거절 ──
if [[ "$MCP_PROFILE" == --* ]]; then
  echo "ERROR: MCP 프로필 위치(3번째 인자)에 플래그 '$MCP_PROFILE'가 들어왔습니다." >&2
  echo "사용법: tfx-route.sh <역할> \"프롬프트\" [mcp_profile] [timeout]" >&2
  echo "지원 프로필: auto, executor, analyze, implement, review, minimal" >&2
  exit 64
fi

# ── CLI 경로 해석 (Windows npm global 대응) ──
NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null || command -v node.exe 2>/dev/null || echo node)}"
CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
AGY_BIN="${AGY_BIN:-$(command -v agy 2>/dev/null || echo agy)}"
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
CLAUDE_BIN_ARGS_JSON="${CLAUDE_BIN_ARGS_JSON:-[]}"

# ── Codex auth/home 명시 라우팅 (Issue #78 race 차단) ──
# TFX_CODEX_HOME 또는 TFX_CODEX_AUTH_FILE 설정 시 codex CLI에 명시적 CODEX_HOME 적용.
# 호출자가 특정 account 를 강제 라우팅하고 싶을 때 사용. 미설정 시 codex default (~/.codex) 유지.
if [[ -n "${TFX_CODEX_HOME:-}" ]]; then
  export CODEX_HOME="$TFX_CODEX_HOME"
elif [[ -n "${TFX_CODEX_AUTH_FILE:-}" && -f "${TFX_CODEX_AUTH_FILE}" ]]; then
  # auth.json 파일 경로만 받으면 그 디렉토리를 CODEX_HOME 으로 사용
  _tfx_codex_auth_dir="$(dirname "$TFX_CODEX_AUTH_FILE")"
  if [[ -f "$_tfx_codex_auth_dir/auth.json" ]]; then
    export CODEX_HOME="$_tfx_codex_auth_dir"
  fi
  unset _tfx_codex_auth_dir
fi

# ── Gemini 프로필 경로 (Codex config.toml 대칭) ──
GEMINI_PROFILES_PATH="${GEMINI_PROFILES_PATH:-${HOME}/.gemini/triflux-profiles.json}"

# ── 상수 ──
MAX_STDOUT_BYTES=51200  # 50KB — Claude 컨텍스트 절약
TIMESTAMP=$(date +%s)
TFX_PROBE_DIR="${TFX_PROBE_DIR:-${TFX_TMP}/tfx-probe}"
mkdir -p "$TFX_PROBE_DIR" 2>/dev/null || true

estimate_expected_duration_sec() {
  local profile="${1:-}" prompt="${2:-}"
  local text
  text=$(printf '%s' "$prompt" | tr '[:upper:]' '[:lower:]')
  local expected="${ROLE_EXPECTED_DURATION:-30}"

  case "$profile" in
    minimal|default) [[ "$expected" -lt 60 ]] && expected=60 ;;
    analyze|review) [[ "$expected" -lt 300 ]] && expected=300 ;;
    implement|executor) [[ "$expected" -lt 300 ]] && expected=300 ;;
  esac

  if [[ "$text" =~ (deep|research|analy[sz]e|분석|리서치|조사|전체|전부|싹다|comprehensive) ]]; then
    [[ "$expected" -lt 600 ]] && expected=600
  fi
  if [[ "$text" =~ (refactor|migration|migrate|리팩터|마이그레이션|대규모|rewrite) ]]; then
    [[ "$expected" -lt 900 ]] && expected=900
  fi
  if [[ "$text" =~ (test|lint|build|npm|pnpm|pytest|검증|테스트) ]]; then
    [[ "$expected" -lt 180 ]] && expected=180
  fi
  if [[ "$text" =~ (mcp|browser|playwright|context7|exa|brave) ]]; then
    [[ "$expected" -lt 120 ]] && expected=120
  fi

  printf '%s\n' "$expected"
}

# 등록된 스킬 본문을 프롬프트 앞에 붙인다. temp file로 리터럴과 줄바꿈을 보존한다.
prepend_skill() {
  local prompt="${1:-}"
  local skill_name="${TFX_INJECT_SKILL:-}"
  if [[ -z "$skill_name" ]]; then
    printf '%s' "$prompt"
    return 0
  fi
  # 경로 탈출 방지: skill_name 은 skills/ 하위 단일 디렉토리 이름만 허용한다.
  if [[ "$skill_name" == *"/"* || "$skill_name" == *".."* ]]; then
    echo "[tfx-route] WARNING: TFX_INJECT_SKILL='${skill_name}' 에 경로 구분자/.. 포함 — 주입 거부" >&2
    printf '%s' "$prompt"
    return 0
  fi

  local skills_dir="${TFX_SKILLS_DIR:-}"
  if [[ -z "$skills_dir" ]]; then
    skills_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)/skills"
  fi
  local skill_file="${skills_dir}/${skill_name}/SKILL.md"
  if [[ ! -r "$skill_file" ]]; then
    echo "[tfx-route] WARNING: TFX_INJECT_SKILL='${skill_name}' 스킬을 찾지 못함 ($skill_file) — 주입 생략" >&2
    printf '%s' "$prompt"
    return 0
  fi

  local workdir="${WORKDIR:-$PWD}"
  local resolved_workdir="$workdir"
  if [[ -d "$workdir" ]]; then
    resolved_workdir="$(cd "$workdir" 2>/dev/null && pwd -P)" || resolved_workdir="$workdir"
  fi

  local tmp_file
  tmp_file="$(mktemp "${TFX_TMP:-${TMPDIR:-/tmp}}/tfx-skill-prompt.XXXXXX" 2>/dev/null)" || {
    printf '%s' "$prompt"
    return 0
  }

  if ! {
    printf '%s\n' "--- SKILL: ${skill_name} (workspace: ${resolved_workdir}; apply this methodology to the task below) ---"
    cat "$skill_file"
    if [[ -s "$skill_file" ]]; then
      local last_byte
      last_byte="$(tail -c 1 "$skill_file" 2>/dev/null || true)"
      [[ -n "$last_byte" ]] && printf '\n'
    fi
    printf '%s\n' "--- END SKILL ---"
    printf '%s' "$prompt"
  } > "$tmp_file"; then
    rm -f "$tmp_file" 2>/dev/null || true
    printf '%s' "$prompt"
    return 0
  fi

  cat "$tmp_file" 2>/dev/null || printf '%s' "$prompt"
  rm -f "$tmp_file" 2>/dev/null || true
}

# append_agy_anti_overclaim: agy(Gemini 3.x) 레인 전용 완료/grounding 규율 블록을
# 프롬프트 *뒤*에 붙인다. Gemini 3.x 는 과신(overconfidence) outlier 라 거짓 완료
# 주장·환각이 잦다 (AA-Omniscience 실증). 부정 제약은 Gemini 3 공식 가이드 권고대로
# 프롬프트 END 에 배치하고, blanket "do not guess" 대신 "주어진 context 로 추론 +
# 정확한 abstention 우선" 형태로 적는다 (open-ended negative 는 역효과). TFX_AGY_ANTI_OVERCLAIM=0 으로 opt-out.
append_agy_anti_overclaim() {
  local prompt="${1:-}"
  local repo_root="${2:-${WORKDIR:-$PWD}}"
  local resolved_repo_root="$repo_root"
  if [[ -d "$repo_root" ]]; then
    resolved_repo_root="$(cd "$repo_root" 2>/dev/null && pwd -P)" || resolved_repo_root="$repo_root"
  fi
  case "${TFX_AGY_ANTI_OVERCLAIM:-1}" in
    0|false|FALSE|off|OFF|no|NO)
      printf '%s' "$prompt"
      return 0
      ;;
  esac

  printf '%s' "$prompt"
  printf '\n\n%s\n' "--- COMPLETION & GROUNDING DISCIPLINE (apply strictly) ---"
  printf '%s\n' "Repository root (absolute): ${resolved_repo_root}. Resolve every relative path against this root only."
  printf '%s\n' "Before any file write, git add/commit/push, verify \`git rev-parse --show-toplevel\` equals the repository root above; if it differs, stop and report the mismatch instead of proceeding."
  printf '%s\n' "- Claim done/fixed/passing ONLY after running the check in this turn and seeing its output. With no fresh evidence, report what is still unverified instead of asserting success."
  printf '%s\n' "- Ground every answer in the provided context and the actual repository. If something is not verifiable from what you can see, say 'No Info / 확인 불가' and stop rather than inventing plausible-but-unconfirmed details."
  printf '%s\n' "- Use the provided context for deductions and prefer accurate abstention over confident guessing."
  printf '%s\n' "The task's own final output/format constraints above remain in effect."
  printf '%s' "--- END DISCIPLINE ---"
}

read_probe_state() {
  local pid="$1"
  local state_file="${TFX_PROBE_STATE_FILE:-${TFX_PROBE_DIR}/${pid}.json}"
  [[ -f "$state_file" ]] || return 1
  # 2-step read (#162): health-probe.mjs 의 atomic write (tmp+rename) 가 도입되었지만
  # writer 쪽 OS race 또는 race-free 보장을 못 받는 환경 (예: 일부 FS) 에서 빈/부분 파일
  # 을 sed 가 읽고 stale state 를 반환하는 것을 방지하기 위해 size 가 너무 작으면 무시.
  local size
  size=$(wc -c < "$state_file" 2>/dev/null || printf '0')
  [[ "$size" -ge 20 ]] || return 1
  sed -n 's/.*"state"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$state_file" 2>/dev/null | head -1
}
RUN_ID="${TIMESTAMP}-$$-${RANDOM}"
STDERR_LOG="${TFX_TMP}/tfx-route-${AGENT_TYPE}-${RUN_ID}-stderr.log"
STDOUT_LOG="${TFX_TMP}/tfx-route-${AGENT_TYPE}-${RUN_ID}-stdout.log"
CODEX_LAST_MESSAGE_LOG="${TFX_TMP}/tfx-route-${AGENT_TYPE}-${RUN_ID}-last-message.log"

# ── 팀 환경변수 ──
TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"

# ── 패키지 루트 해석 (setup.mjs가 기록한 breadcrumb) ──
TFX_PKG_ROOT=""
_tfx_breadcrumb="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.tfx-pkg-root"
if [[ -f "$_tfx_breadcrumb" ]]; then
  TFX_PKG_ROOT="$(head -1 "$_tfx_breadcrumb" 2>/dev/null | tr -d '\r\n')"
fi
unset _tfx_breadcrumb

# fallback 시 원래 에이전트 정보 보존
ORIGINAL_AGENT=""

# ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
register_agent() {
  local agent_file="${TFX_TMP}/tfx-agent-$$.json"
  # route policy가 provider enum과 agent 토큰을 검증했다.
  printf '{"pid":%s,"cli":"%s","agent":"%s","started":%s}\n' "$$" "$CLI_TYPE" "$AGENT_TYPE" "$(date +%s)" \
    > "$agent_file" 2>/dev/null || true
}

deregister_agent() {
  rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
}

normalize_script_path() {
  local path="${1:-}"
  if [[ -z "$path" ]]; then
    return 0
  fi

  if command -v cygpath &>/dev/null; then
    case "$path" in
      [A-Za-z]:\\*|[A-Za-z]:/*)
        cygpath -u "$path"
        return 0
        ;;
    esac
  fi

  printf '%s\n' "$path"
}

# ── 스크립트 경로 해석 공통 인프라 ──
_tfx_script_dir=""
_get_script_dir() {
  if [[ -z "$_tfx_script_dir" ]]; then
    local ref; ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
    _tfx_script_dir="$(cd "$(dirname "$ref")" && pwd -P)"
  fi
  printf '%s\n' "$_tfx_script_dir"
}

# _resolve_script ENV_VAR_VALUE CANDIDATE... → 첫 번째 존재하는 파일 경로 반환
_resolve_script() {
  local env_val="${1:-}"; shift
  [[ -n "$env_val" && -f "$env_val" ]] && { printf '%s\n' "$env_val"; return 0; }
  local c; for c in "$@"; do [[ -f "$c" ]] && { printf '%s\n' "$c"; return 0; }; done
  return 1
}

# ── 팀 Bridge 통신 ──
resolve_bridge_script() {
  local sd; sd="$(_get_script_dir)"
  _resolve_script "${TFX_BRIDGE_SCRIPT:-}" \
    ${TFX_PKG_ROOT:+"$TFX_PKG_ROOT/hub/bridge.mjs"} \
    "$sd/../hub/bridge.mjs" "$sd/hub/bridge.mjs"
}

bridge_cli() {
  if ! command -v "$NODE_BIN" &>/dev/null; then
    return 127
  fi

  local bridge_script
  if ! bridge_script=$(resolve_bridge_script); then
    return 127
  fi

  "$NODE_BIN" "$bridge_script" "$@" 2>/dev/null
}

# 키는 JS에서만 읽는다. 셸에는 활성 여부만 전달한다.
jev_enabled() {
  local bridge_script module_path enabled flag="${TFX_JEV:-}"
  flag="${flag#"${flag%%[![:space:]]*}"}"
  flag="${flag%"${flag##*[![:space:]]}"}"
  case "$flag" in
    0|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]) return 1 ;;
  esac
  [[ -n "${JEV_API_KEY:-}${TYPESAFE_API_KEY:-}${TFX_JEV_CLI:-}${JEV_CLI_BIN:-}" ]] && return 0
  case "$flag" in
    1|[Tt][Rr][Uu][Ee]|[Oo][Nn]) return 0 ;;
  esac
  bridge_script="$(resolve_bridge_script 2>/dev/null)" || return 1
  module_path="$(dirname "$bridge_script")/lib/jev-addon.mjs"
  enabled="$("${NODE_BIN:-node}" --input-type=module -e '
    import { pathToFileURL } from "node:url";
    const { isJevEnabled } = await import(pathToFileURL(process.argv[1]));
    process.stdout.write(isJevEnabled() ? "true" : "false");
  ' "$module_path" 2>/dev/null)" || return 1
  [[ "$enabled" == "true" ]]
}

bridge_json_get() {
  local json="${1:-}"
  local path="${2:-}"
  [[ -z "$json" || -z "$path" ]] && return 1

  "$NODE_BIN" -e '
    const data = JSON.parse(process.argv[1] || "{}");
    const keys = String(process.argv[2] || "").split(".").filter(Boolean);
    let value = data;
    for (const key of keys) value = value?.[key];
    if (value === undefined || value === null) process.exit(1);
    process.stdout.write(typeof value === "object" ? JSON.stringify(value) : String(value));
  ' -- "$json" "$path" 2>/dev/null
}

team_send_message() {
  local text="${1:-}"
  local summary="${2:-}"
  [[ -z "$TFX_TEAM_NAME" || -z "$text" ]] && return 0

  if ! bridge_cli team-send-message \
    --team "$TFX_TEAM_NAME" \
    --from "$TFX_TEAM_AGENT_NAME" \
    --to "$TFX_TEAM_LEAD_NAME" \
    --text "$text" \
    --summary "${summary:-status update}" >/dev/null 2>&1; then
    echo "[tfx-route] 경고: 팀 메시지 전송 실패 (team=$TFX_TEAM_NAME, to=$TFX_TEAM_LEAD_NAME)" >&2
    return 0
  fi

  return 0
}

team_claim_task() {
  [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
  local response ok error_code error_message owner_before status_before
  response=$(bridge_cli team-task-update \
    --team "$TFX_TEAM_NAME" \
    --task-id "$TFX_TEAM_TASK_ID" \
    --claim \
    --owner "$TFX_TEAM_AGENT_NAME" \
    --status in_progress || true)

  ok=$(bridge_json_get "$response" "ok" || true)
  error_code=$(bridge_json_get "$response" "error.code" || true)
  error_message=$(bridge_json_get "$response" "error.message" || true)
  owner_before=$(bridge_json_get "$response" "error.details.task_before.owner" || true)
  status_before=$(bridge_json_get "$response" "error.details.task_before.status" || true)

  case "$ok:$error_code" in
    true:*) ;;
    false:CLAIM_CONFLICT)
      if [[ "$owner_before" == "$TFX_TEAM_AGENT_NAME" && "$status_before" == "in_progress" ]]; then
        echo "[tfx-route] 동일 owner(${TFX_TEAM_AGENT_NAME})가 이미 claim한 task ${TFX_TEAM_TASK_ID} — 계속 실행." >&2
        return 0
      fi
      echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨(owner=${owner_before:-unknown}, status=${status_before:-unknown}). 실행 중단." >&2
      team_send_message \
        "task ${TFX_TEAM_TASK_ID} claim conflict: owner=${owner_before:-unknown}, status=${status_before:-unknown}" \
        "task ${TFX_TEAM_TASK_ID} claim conflict"
      exit 0 ;;
    :|false:)
      echo "[tfx-route] 경고: bridge 연결 실패. claim 없이 계속 실행." >&2 ;;
    *)
      echo "[tfx-route] 경고: claim 실패 (${error_code:-unknown}${error_message:+: ${error_message}}). claim 없이 계속 실행." >&2 ;;
  esac
}

team_complete_task() {
  local result="${1:-success}"            # success/failed/timeout
  local result_summary="${2:-작업 완료}"
  [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0

  local summary_trimmed
  summary_trimmed=$(echo "$result_summary" | head -c 4096)

  # task 파일 completion 쓰기는 Worker Step 6 TaskUpdate가 authority다.
  # route 레벨에서는 로컬 backup만 유지한다.

  # 로컬 결과 파일 백업 (세션 끊김 복구용)
  # Claude 재로그인 시 Agent 래퍼가 죽어도 이 파일로 결과 수집 가능
  local result_dir="${TFX_RESULT_DIR:-${HOME}/.claude/tfx-results/${TFX_TEAM_NAME}}"
  if mkdir -p "$result_dir" 2>/dev/null; then
    # 전체를 Node.js로 안전하게 stringify — 변수 직접 삽입 인젝션 방지
    "$NODE_BIN" -e '
      const [,taskId,agent,team,result,summary,ts] = process.argv;
      process.stdout.write(JSON.stringify({taskId,agent,team,result,summary,timestamp:ts}));
    ' -- "$TFX_TEAM_TASK_ID" "$TFX_TEAM_AGENT_NAME" "$TFX_TEAM_NAME" "$result" "$summary_trimmed" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
      > "${result_dir}/${TFX_TEAM_TASK_ID}.json" 2>/dev/null \
      && echo "[tfx-route] 결과 백업: ${result_dir}/${TFX_TEAM_TASK_ID}.json" >&2
  fi
}

detect_quota_exceeded() {
  local stdout_file="$1"
  local stderr_file="$2"
  local -a patterns=(
    "usage limit exceeded" "rate limit exceeded" "rate limit reached"
    "try again at" "purchase more credits"
    "quota exceeded" "Quota pool exhausted" "RESOURCE_EXHAUSTED" "rateLimitExceeded" "Too Many Requests"
    "rate_limit_error" "overloaded_error" "insufficient_quota"
  )
  local pattern
  for pattern in "${patterns[@]}"; do
    if grep -qi "$pattern" "$stdout_file" 2>/dev/null || grep -qi "$pattern" "$stderr_file" 2>/dev/null; then
      echo "[tfx-quota] 감지: '$pattern' in $CLI_TYPE" >&2
      return 0
    fi
  done
  return 1
}

agy_output_is_quota_error() {
  awk '
    {
      line = tolower($0)
      gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
      if (line ~ /^(error: )?quota pool exhausted([.[:space:]]|$)/) quota = 1
      else if (line != "" && line !~ /^error id([:[:space:]]|$)/) content = 1
    }
    END { exit !(quota && !content) }
  ' "$1" "$2"
}

record_agy_quota_reset() {
  local output delay=3600 reset_epoch temp_file
  local reset_pattern='Resets in ([0-9]{1,9})h([0-9]{1,2})m([0-9]{1,2})s'
  output="$(cat "$1" "$2" 2>/dev/null || true)"
  if [[ "$output" =~ $reset_pattern ]]; then
    delay=$((10#${BASH_REMATCH[1]} * 3600 + 10#${BASH_REMATCH[2]} * 60 + 10#${BASH_REMATCH[3]}))
  fi
  reset_epoch=$(( $(date +%s) + delay ))
  temp_file="$(mktemp "${TFX_TMP}/tfx-agy-quota-reset.XXXXXX")" || return 1
  if printf '%s\n' "$reset_epoch" > "$temp_file" \
    && mv -f "$temp_file" "${TFX_TMP}/tfx-agy-quota-reset"; then
    return 0
  fi
  rm -f "$temp_file"
  return 1
}

agy_quota_cooldown_active() {
  AGY_QUOTA_RESET_EPOCH=""
  local state_file="${TFX_TMP}/tfx-agy-quota-reset"
  [[ -r "$state_file" ]] || return 1
  IFS= read -r AGY_QUOTA_RESET_EPOCH < "$state_file" || return 1
  [[ "$AGY_QUOTA_RESET_EPOCH" =~ ^[0-9]{1,12}$ ]] || return 1
  [[ "$((10#$AGY_QUOTA_RESET_EPOCH))" -gt "$(date +%s)" ]]
}

log_agy_quota_fallback() {
  agy_quota_cooldown_active || return 0
  local reset_time
  reset_time="$("$NODE_BIN" -e 'process.stdout.write(new Date(Number(process.argv[1]) * 1000).toISOString())' "$AGY_QUOTA_RESET_EPOCH")"
  echo "[tfx-route] agy 쿼터 소진, ${reset_time}까지 $1으로" >&2
}

agy_supports_headless() {
  local agy_bin="$1" help_text
  if [[ "${_AGY_HEADLESS_BIN:-}" == "$agy_bin" ]]; then
    return "${_AGY_HEADLESS_SUPPORTED:-1}"
  fi
  _AGY_HEADLESS_BIN="$agy_bin"
  _AGY_HEADLESS_SUPPORTED=1
  if command -v "$agy_bin" &>/dev/null && help_text=$("$agy_bin" --help 2>&1); then
    help_text="${help_text:0:20000}"
    if [[ "$help_text" == *"--print"* && "$help_text" == *"--dangerously-skip-permissions"* ]]; then
      _AGY_HEADLESS_SUPPORTED=0
    fi
  fi
  return "${_AGY_HEADLESS_SUPPORTED:-1}"
}

# agy는 SSH 환경을 감지하면 macOS Keychain 대신 file token store를 사용한다.
# 이 cache는 한 route invocation에서 Keychain probe를 한 번만 수행하게 한다.
_AGY_KEYCHAIN_SSH_SCRUB=""
agy_should_scrub_ssh_env() {
  if [[ -n "$_AGY_KEYCHAIN_SSH_SCRUB" ]]; then
    [[ "$_AGY_KEYCHAIN_SSH_SCRUB" == "1" ]]
    return
  fi
  _AGY_KEYCHAIN_SSH_SCRUB="0"

  [[ "${TFX_AGY_KEEP_SSH_ENV:-0}" != "1" ]] || return 1
  [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] || return 1
  command -v security >/dev/null 2>&1 || return 1

  # probe 는 백그라운드 job 을 남기지 않는다. watchdog 서브셸이 호출자의
  # stdout/stderr 파이프를 상속하면 route 출력을 읽는 쪽이 EOF 를 받지 못한다.
  local -a _probe_cmd=()
  if command -v gtimeout >/dev/null 2>&1; then
    _probe_cmd=(gtimeout 2)
  elif command -v timeout >/dev/null 2>&1; then
    _probe_cmd=(timeout 2)
  fi

  # bash 3.2(macOS 기본)는 set -u 에서 빈 배열의 "${a[@]}" 확장을 unbound 로 본다.
  if ${_probe_cmd[@]+"${_probe_cmd[@]}"} security find-generic-password -s gemini -a antigravity -w \
    </dev/null >/dev/null 2>&1; then
    _AGY_KEYCHAIN_SSH_SCRUB="1"
    return 0
  fi
  return 1
}

codex_is_available() {
  [[ "${TFX_CODEX_OK:-0}" == "1" ]] \
    && command -v "${CODEX_BIN:-codex}" &>/dev/null
}

auto_reroute() {
  # Issue #281: code-change dispatch escalation is decided in the JS router
  # before CLI execution. This shell function only handles quota-driven
  # CLI fallback and must stay a transparent passthrough for dispatch mode.
  local failed_cli="$1"
  local target_cli=""
  local -a candidates=()
  if [[ -n "${REVIEW_AUTHOR:-}" ]]; then
    candidates=("claude" "codex" "antigravity")
  else
    case "$failed_cli" in
      codex) candidates=("antigravity") ;;
      antigravity) candidates=("${TFX_ANTIGRAVITY_FALLBACK:-codex}") ;;
      *) echo "[tfx-quota] $failed_cli 대체 CLI 없음" >&2; return 1 ;;
    esac
  fi

  # 대상 CLI 존재 확인 (P2: command not found 방지)
  local candidate
  for candidate in "${candidates[@]}"; do
    [[ "$candidate" != "$failed_cli" && "$candidate" != "${REVIEW_AUTHOR:-}" ]] || continue
    if is_cli_disabled "$candidate"; then
      echo "[tfx-quota] ${candidate} disabled — 자동 전환 후보에서 제외" >&2
      continue
    fi
    if is_cli_available "$candidate"; then
      target_cli="$candidate"
      break
    fi
  done

  if [[ -z "$target_cli" ]]; then
    echo "[tfx-quota] $failed_cli 대체 CLI 미설치 — 자동 전환 불가" >&2
    return 78
  fi

  case "$failed_cli:$target_cli" in
    codex:antigravity) echo "[tfx-quota] Codex → Antigravity 자동 전환" >&2 ;;
    antigravity:codex) echo "[tfx-quota] Antigravity → Codex 자동 전환" >&2 ;;
    antigravity:claude) echo "[tfx-quota] Antigravity → Claude 자동 전환" >&2 ;;
  esac
  [[ "$failed_cli" != "antigravity" ]] || log_agy_quota_fallback "$target_cli"

  local quota_marker="$TFX_TMP/tfx-quota-${failed_cli}-$(date +%Y%m%d)"
  echo "$(date +%s)" >> "$quota_marker"
  ORIGINAL_AGENT="$AGENT_TYPE"
  export TFX_REROUTED_FROM="$CLI_TYPE"
  # EXIT trap 정리 — exec는 현재 프로세스를 교체하므로 trap이 실행되지 않음
  declare -F cleanup_workers >/dev/null && cleanup_workers
  TFX_CLI_MODE="$target_cli" exec bash "${BASH_SOURCE[0]}" \
    "$AGENT_TYPE" "$PROMPT" "$MCP_PROFILE" "${USER_TIMEOUT:-}" "${CONTEXT_FILE:-}"
}

capture_workspace_signature() {
  if ! command -v git &>/dev/null; then
    return 1
  fi

  if ! git -c core.fsmonitor=false rev-parse --is-inside-work-tree < /dev/null >/dev/null 2>&1; then
    return 1
  fi

  git -c core.fsmonitor=false status --short --untracked-files=all --ignore-submodules=all < /dev/null 2>/dev/null || return 1
}

# ── Gemini 모델 프로필 해석 ──
resolve_gemini_profile() {
  "$NODE_BIN" --input-type=module -e '
    import { pathToFileURL } from "node:url";
    const { ensureGeminiProfiles, resolveGeminiModel } = await import(pathToFileURL(process.argv[1]).href);
    const { modelEffortFromProfile, resolveAgyModelEffort } = await import(new URL("./model-effort.mjs", pathToFileURL(process.argv[1])).href);
    const opts = { profilesPath: process.argv[3], command: process.argv[4] };
    ensureGeminiProfiles(opts);
    const model = resolveGeminiModel(process.argv[2], opts);
    const match = /^gemini-\d+(?:\.\d+)*-argon(?:-([a-z]+))?$/i.exec(model)
      || /^Gemini \d+(?:\.\d+)* Argon \(([a-z]+)\)$/i.exec(model);
    const requested = modelEffortFromProfile("agy", process.argv[2])
      || (match ? { family: "argon", effort: (match[1] || "medium").toLowerCase() } : null);
    if (!requested) {
      process.stdout.write(`${model}\x1e`);
    } else {
      try {
        const resolved = resolveAgyModelEffort(requested, opts);
        if (resolved.effort !== requested.effort)
          console.error(`[tfx-route] agy effort: requested=${requested.effort} applied=${resolved.effort} model=${resolved.model}`);
        process.stdout.write([resolved.model, resolved.effort].join("\x1e"));
      } catch (error) {
        console.error(`[tfx-route] ${error.message}`);
        process.exitCode = error.code === "unsupported-effort" ? 78 : 1;
      }
    }
  ' "${CODEX_PROFILE_MODULE%/*}/gemini-profiles.mjs" "$1" "$GEMINI_PROFILES_PATH" "$CLI_CMD"
}

refresh_codex_profiles() {
  "$NODE_BIN" --input-type=module -e '
    import { pathToFileURL } from "node:url";
    const { ensureManagedCodexProfiles } = await import(pathToFileURL(process.argv[1]).href);
    ensureManagedCodexProfiles({ command: process.argv[2] });
  ' "$CODEX_PROFILE_MODULE" "$CLI_CMD" 2>/dev/null || true
}

# ── 라우팅 테이블 ──
# CLI_TYPE/CLI_CMD는 agent-map, 역할별 설정은 agent-route-policy에서 조회한다.
# 반환: CLI_TYPE, CLI_CMD, CLI_ARGS, CLI_EFFORT, DEFAULT_TIMEOUT
route_agent() {
  local agent="$1" sd map_file policy_file policy_row gemini_module
  sd="$(_get_script_dir)"
  map_file="$sd/../hub/team/agent-map.json"
  [[ -f "$map_file" || -z "$TFX_PKG_ROOT" ]] || map_file="$TFX_PKG_ROOT/hub/team/agent-map.json"
  if [[ ! -f "$map_file" ]]; then
    echo "ERROR: agent-map.json 미발견 (경로: $map_file, TFX_PKG_ROOT=${TFX_PKG_ROOT:-unset})" >&2
    exit 1
  fi
  policy_file="$sd/lib/agent-route-policy.mjs"
  [[ -f "$policy_file" || -z "$TFX_PKG_ROOT" ]] || policy_file="$TFX_PKG_ROOT/scripts/lib/agent-route-policy.mjs"
  if [[ ! -f "$policy_file" ]]; then
    echo "ERROR: agent route policy 미발견 (경로: $policy_file)" >&2
    exit 1
  fi
  CODEX_PROFILE_MODULE="${policy_file%/*}/codex-profile-config.mjs"
  local -a query=("$NODE_BIN" "$policy_file" --format route --map-file "$map_file" --agent "$agent" --profile-override "$TFX_CODEX_PROFILE")
  gemini_module="$(_resolve_script "${TFX_GEMINI_PROFILES_MODULE:-}" \
    "$sd/lib/gemini-profiles.mjs" \
    ${TFX_PKG_ROOT:+"$TFX_PKG_ROOT/scripts/lib/gemini-profiles.mjs"} || true)"
  [[ -z "$gemini_module" ]] || query+=(--gemini-profiles-module "$gemini_module")
  is_nested_codex_runtime && query+=(--nested)
  if ! policy_row=$("${query[@]}"); then
    echo "ERROR: agent route policy 조회 실패: $agent (TFX_CODEX_PROFILE=$TFX_CODEX_PROFILE)" >&2
    exit 1
  fi
  local normalized_profile _policy_mcp
  IFS=$'\x1e' read -r CLI_TYPE CODEX_POLICY_PROFILE CODEX_POLICY_TIMEOUT \
    _policy_mcp CODEX_POLICY_SUBCOMMAND normalized_profile CODEX_OVERRIDE_PROFILE GEMINI_PROFILE \
    ROLE_EXPECTED_DURATION NATIVE_CODEX_ALLOWED REVIEW_AUTHOR REVIEW_USAGE_SWITCH \
    CODEX_POLICY_EFFORT CLI_FAMILY <<< "$policy_row"
  if [[ "$normalized_profile" != "$TFX_CODEX_PROFILE" ]]; then
    echo "[tfx-route] legacy Codex profile remapped: ${TFX_CODEX_PROFILE} -> ${normalized_profile}" >&2
  fi
  TFX_CODEX_PROFILE="$normalized_profile"
  case "$CLI_TYPE" in
    codex) apply_codex_agent_policy ;;
    antigravity)
      CLI_CMD="agy"; CLI_ARGS="$(build_antigravity_base)"
      CLI_EFFORT=""; CLI_FAMILY=""; DEFAULT_TIMEOUT=900 ;;
    claude)
      if [[ -n "$REVIEW_AUTHOR" ]]; then
        CLI_CMD="claude"; CLI_ARGS=""
        CLI_EFFORT="high"; CLI_FAMILY=""; DEFAULT_TIMEOUT="$CODEX_POLICY_TIMEOUT"
      else
        CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
        CLI_EFFORT=""; CLI_FAMILY=""; DEFAULT_TIMEOUT=600
      fi ;;
  esac
}

apply_codex_agent_policy() {
  CLI_TYPE="codex"; CLI_CMD="codex"
  CLI_ARGS="exec --profile ${CODEX_POLICY_PROFILE} $(build_codex_base)"
  [[ "$CODEX_POLICY_SUBCOMMAND" != "review" ]] || CLI_ARGS+=" review"
  CLI_EFFORT="$CODEX_POLICY_EFFORT"; DEFAULT_TIMEOUT="$CODEX_POLICY_TIMEOUT"
}

# ── CLI 모드 오버라이드 ──
TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
TFX_DISABLE_CODEX="${TFX_DISABLE_CODEX:-0}"
TFX_DISABLE_ANTIGRAVITY="${TFX_DISABLE_ANTIGRAVITY:-0}"
TFX_ANTIGRAVITY_FALLBACK="${TFX_ANTIGRAVITY_FALLBACK-codex}"
TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
TFX_CODEX_PROFILE="${TFX_CODEX_PROFILE:-auto}"
TFX_EXECUTION_MODE="${TFX_EXECUTION_MODE:-auto}"
JEV_NEEDED_MCP_SERVERS=""
JEV_CLAUDE_MODEL=""
JEV_CLAUDE_EFFORT=""

# Preflight 캐시의 CLI 가용성과 Codex 요금제를 환경변수로 내보냄
# 하위 프로세스(스킬 포함)가 CLI 가용성을 즉시 참조 가능
if [[ -z "${TFX_PREFLIGHT_LOADED:-}" ]]; then
  # SessionStart 훅 없이도 route 진입 시 없거나 만료된 캐시를 갱신한다.
  _preflight_script="$(_resolve_script "" \
    ${TFX_PKG_ROOT:+"$TFX_PKG_ROOT/scripts/preflight-cache.mjs"} \
    "$(_get_script_dir)/preflight-cache.mjs" || true)"
  if [[ -n "$_preflight_script" ]]; then
    "$NODE_BIN" "$_preflight_script" --if-stale >/dev/null 2>&1 || true
  fi
  unset _preflight_script
  # eval 제거 — \x1e (ASCII 30, Record Separator) delimited read로 인젝션 위험 차단
  # F05: `|`에서 `\x1e`로 변경 — 계정 tier/agent 이름 등 값에 `|` 포함 시 필드 분리 오류 방지
  IFS=$'\x1e' read -r _pf_codex _pf_antigravity _pf_plan _pf_agents _pf_antigravity_status _pf_antigravity_source _pf_antigravity_reason < <(
    "$NODE_BIN" -e '
      try {
        const c = JSON.parse(require("fs").readFileSync(require("path").join(require("os").homedir(),".claude","cache","tfx-preflight.json"),"utf8"));
        const parts = [
          c?.codex?.ok ? "1" : "0",
          c?.antigravity?.ok ? "1" : "0",
          (c?.codex_plan?.plan && c.codex_plan.plan !== "unknown" && c.codex_plan.plan !== "api") ? c.codex_plan.plan : "",
          Array.isArray(c?.available_agents) ? c.available_agents.join(",") : "",
          c?.antigravity?.status || "",
          c?.antigravity?.auth_source || "",
          c?.antigravity?.reason || ""
        ];
        process.stdout.write(parts.join("\x1e"));
      } catch { process.stdout.write("0\x1e0\x1e\x1e\x1e\x1e\x1e"); }
    ' 2>/dev/null
  ) || true
  export TFX_CODEX_OK="${TFX_CODEX_OK:-${_pf_codex:-0}}"
  export TFX_ANTIGRAVITY_OK="${TFX_ANTIGRAVITY_OK:-${_pf_antigravity:-0}}"
  [[ -n "${_pf_antigravity_status:-}" ]] && export TFX_ANTIGRAVITY_STATUS="$_pf_antigravity_status"
  [[ -n "${_pf_antigravity_source:-}" ]] && export TFX_ANTIGRAVITY_AUTH_SOURCE="$_pf_antigravity_source"
  [[ -n "${_pf_antigravity_reason:-}" ]] && export TFX_ANTIGRAVITY_REASON="$_pf_antigravity_reason"
  [[ -n "${_pf_agents:-}" ]] && export TFX_AVAILABLE_AGENTS="$_pf_agents"
  export TFX_PREFLIGHT_LOADED=1
  unset _pf_codex _pf_antigravity _pf_plan _pf_agents _pf_antigravity_status _pf_antigravity_source _pf_antigravity_reason
fi
TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
TFX_SEARCH_TOOL="${TFX_SEARCH_TOOL:-}"
case "$TFX_NO_CLAUDE_NATIVE" in
  0|1) ;;
  *)
    echo "ERROR: TFX_NO_CLAUDE_NATIVE 값은 0 또는 1이어야 합니다. (현재: $TFX_NO_CLAUDE_NATIVE)" >&2
    exit 1
    ;;
esac
case "$TFX_DISABLE_CODEX" in
  0|1) ;;
  *)
    echo "ERROR: TFX_DISABLE_CODEX 값은 0 또는 1이어야 합니다. (현재: $TFX_DISABLE_CODEX)" >&2
    exit 1
    ;;
esac
case "$TFX_DISABLE_ANTIGRAVITY" in
  0|1) ;;
  *)
    echo "ERROR: TFX_DISABLE_ANTIGRAVITY 값은 0 또는 1이어야 합니다. (현재: $TFX_DISABLE_ANTIGRAVITY)" >&2
    exit 1
    ;;
esac
case "$TFX_VERIFIER_OVERRIDE" in
  auto|claude) ;;
  *)
    echo "ERROR: TFX_VERIFIER_OVERRIDE 값은 auto 또는 claude여야 합니다. (현재: $TFX_VERIFIER_OVERRIDE)" >&2
    exit 1
    ;;
esac
case "$TFX_ANTIGRAVITY_FALLBACK" in
  codex|claude) ;;
  *)
    echo "ERROR: TFX_ANTIGRAVITY_FALLBACK 값은 codex 또는 claude여야 합니다. (현재: $TFX_ANTIGRAVITY_FALLBACK)" >&2
    exit 1 ;;
esac
case "$TFX_WORKER_INDEX" in
  "") ;;
  *[!0-9]*|0)
    echo "ERROR: TFX_WORKER_INDEX 값은 1 이상의 정수여야 합니다. (현재: $TFX_WORKER_INDEX)" >&2
    exit 1
    ;;
esac
case "$TFX_SEARCH_TOOL" in
  ""|brave-search|exa) ;;
  *)
    echo "ERROR: TFX_SEARCH_TOOL 값은 brave-search, exa 중 하나여야 합니다. (현재: $TFX_SEARCH_TOOL)" >&2
    exit 1
    ;;
esac

apply_cli_mode() {
  case "$TFX_CLI_MODE" in
    codex)
      if [[ "$CLI_TYPE" == "antigravity" || ( "$CLI_TYPE" == "claude" && -n "$REVIEW_AUTHOR" ) ]]; then
        apply_codex_agent_policy
        echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
      fi ;;
    antigravity)
      if [[ "$CLI_TYPE" != "claude-native" && ( "$CLI_TYPE" != "claude" || -n "$REVIEW_AUTHOR" ) ]]; then
        CLI_TYPE="antigravity"
        CLI_CMD="agy"
        CLI_ARGS="$(build_antigravity_base)"
        CLI_EFFORT=""; CLI_FAMILY=""
        DEFAULT_TIMEOUT=900
        echo "[tfx-route] TFX_CLI_MODE=antigravity: $AGENT_TYPE -> antigravity($CLI_EFFORT)로 리매핑" >&2
      fi ;;
    claude)
      if [[ -n "$REVIEW_AUTHOR" ]]; then
        CLI_TYPE="claude"; CLI_CMD="claude"; CLI_ARGS=""
        CLI_EFFORT="high"; CLI_FAMILY=""; DEFAULT_TIMEOUT="$CODEX_POLICY_TIMEOUT"
      elif [[ "${TFX_REROUTED_FROM:-}" == "antigravity" && "$TFX_ANTIGRAVITY_FALLBACK" == "claude" ]]; then
        CLI_TYPE="claude"; CLI_CMD="claude"; CLI_ARGS=""
        CLI_EFFORT=""; CLI_FAMILY=""; DEFAULT_TIMEOUT=900
        case "$AGENT_TYPE" in
          security-reviewer|designer) CLI_EFFORT="high" ;;
        esac
        _AGY_CLAUDE_FALLBACK=1
      fi ;;
    auto)
      # 리뷰의 가용성 폴백은 작성 벤더를 제외하는 최종 정책에서 처리한다.
      [[ -z "$REVIEW_AUTHOR" ]] || return 0
      # Issue #281: JS layer is the single source of truth for auto router
      # Shell auto mode normalizes CLI availability; JS selects single/multi.
      if [[ "$CLI_TYPE" == "codex" ]] && ! codex_is_available; then
        if is_cli_available antigravity; then
          TFX_CLI_MODE="antigravity"; apply_cli_mode; return
        else
          echo "[tfx-route] codex/antigravity 모두 불가: hard routing 검증으로 전달" >&2
        fi
      elif [[ "$CLI_TYPE" == "antigravity" && "$TFX_ANTIGRAVITY_FALLBACK" == "codex" ]] && ! agy_supports_headless "${AGY_BIN:-agy}"; then
        if codex_is_available; then
          TFX_CLI_MODE="codex"; apply_cli_mode; return
        fi
        echo "[tfx-route] antigravity headless 불가: hard routing 검증으로 전달" >&2
      fi ;;
  esac
}

# ── CLI disable 정책 (강제 모드/동적 오버라이드보다 우선) ──
# TFX_CLI_MODE는 선호/강제 선택이고 TFX_*_OK는 preflight 관측값이다. 이 정책은
# 운영자가 명시한 실행 금지이므로, 최종 CLI 선택 후 한 번만 적용한다.
is_cli_disabled() {
  case "$1" in
    codex) [[ "$TFX_DISABLE_CODEX" == "1" ]] ;;
    antigravity) [[ "$TFX_DISABLE_ANTIGRAVITY" == "1" ]] ;;
    *) return 1 ;;
  esac
}

is_cli_available() {
  case "$1" in
    codex)
      codex_is_available
      ;;
    antigravity)
      ! agy_quota_cooldown_active \
        && [[ "${TFX_ANTIGRAVITY_OK:-0}" == "1" ]] \
        && agy_supports_headless "${AGY_BIN:-agy}"
      ;;
    claude)
      command -v "$CLAUDE_BIN" >/dev/null 2>&1
      ;;
    *)
      return 1
      ;;
  esac
}

apply_cli_disable_policy() {
  local selected="$CLI_TYPE"
  if [[ -n "$REVIEW_AUTHOR" ]]; then
    if ! is_cli_disabled "$selected" && is_cli_available "$selected"; then
      return 0
    fi
    local candidate
    for candidate in claude codex antigravity; do
      [[ "$candidate" != "$REVIEW_AUTHOR" && "$candidate" != "$selected" ]] || continue
      if ! is_cli_disabled "$candidate" && is_cli_available "$candidate"; then
        [[ "$selected" != "antigravity" ]] || log_agy_quota_fallback "$candidate"
        echo "[tfx-route] ${selected} -> ${candidate} 리뷰 fallback (작성 벤더 ${REVIEW_AUTHOR} 제외)" >&2
        TFX_CLI_MODE="$candidate"
        apply_cli_mode
        return 0
      fi
    done
    echo "ERROR: 작성 벤더 ${REVIEW_AUTHOR}를 제외하면 허용되고 사용 가능한 리뷰 CLI가 없습니다." >&2
    return 78
  fi
  [[ "$selected" == "codex" || "$selected" == "antigravity" ]] || return 0

  local blocked_reason=""
  if is_cli_disabled "$selected"; then
    case "$selected" in
      codex) blocked_reason="TFX_DISABLE_CODEX=1: codex 선택 차단" ;;
      antigravity) blocked_reason="TFX_DISABLE_ANTIGRAVITY=1: antigravity 선택 차단" ;;
    esac
  elif ! is_cli_available "$selected"; then
    blocked_reason="${selected} 미가용: 선택 차단"
  else
    return 0
  fi
  if [[ "$selected" == "antigravity" && "$TFX_ANTIGRAVITY_FALLBACK" == "claude" ]]; then
    echo "[tfx-route] ${blocked_reason}; antigravity -> claude 명시적 fallback" >&2
    if ! is_cli_available claude; then
      echo "ERROR: TFX_ANTIGRAVITY_FALLBACK=claude 이지만 Claude CLI가 없습니다." >&2
      return 78
    fi
    log_agy_quota_fallback claude
    CLI_TYPE="claude"; CLI_CMD="claude"; CLI_ARGS=""
    case "$AGENT_TYPE" in
      security-reviewer|designer) CLI_EFFORT="high" ;;
    esac
    _AGY_CLAUDE_FALLBACK=1
    return 0
  fi
  echo "[tfx-route] $blocked_reason" >&2

  local candidate=""
  case "$selected" in
    codex) candidate="antigravity" ;;
    antigravity) candidate="codex" ;;
  esac
  if ! is_cli_disabled "$candidate" && is_cli_available "$candidate"; then
    [[ "$selected" != "antigravity" ]] || log_agy_quota_fallback "$candidate"
    echo "[tfx-route] ${selected} → ${candidate} 명시적 fallback (금지 CLI 제외)" >&2
    TFX_CLI_MODE="$candidate"
    apply_cli_mode
    if [[ "$CLI_TYPE" == "$candidate" ]] \
      && ! is_cli_disabled "$CLI_TYPE" \
      && is_cli_available "$CLI_TYPE"; then
      return 0
    fi
  fi

  echo "ERROR: 허용되고 사용 가능한 외부 CLI가 없습니다. codex(disabled=${TFX_DISABLE_CODEX}, available=$(is_cli_available codex && echo 1 || echo 0)), antigravity(disabled=${TFX_DISABLE_ANTIGRAVITY}, available=$(is_cli_available antigravity && echo 1 || echo 0)). Claude native로 조용히 강등하지 않습니다." >&2
  return 78
}

# ── Claude 네이티브 제거 (Codex 리드 환경에서 선택적 활성화) ──
apply_no_claude_native_mode() {
  [[ "$TFX_NO_CLAUDE_NATIVE" == "1" && "$TFX_CLI_MODE" != "antigravity" ]] || return 0
  [[ "$CLI_TYPE" == "claude-native" && "$NATIVE_CODEX_ALLOWED" == "true" ]] || return 0
  if ! codex_is_available; then
    echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1 이지만 codex가 preflight에서 가용하지 않아 claude-native 유지" >&2
    return 0
  fi
  ORIGINAL_AGENT="$AGENT_TYPE"
  apply_codex_agent_policy
  echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
}

## Retry snapshot profile plumbing.
## bridge retry-run/status exposes cliInvocation.argv for escalation-chain
## profile steps. Consume those argv directly instead of duplicating profile
## resolution here.
resolve_safe_retry_codex_profile() {
  local profile
  profile="$1"
  case "$profile" in
    sol|astra|terra|luna)
      echo "$profile"
      return
      ;;
    ultra|astra_ultra)
      echo "astra_max"
      return
      ;;
  esac
  local effort
  if ! effort="$(read_codex_profile_effort "$profile")"; then return 78; fi
  case "$effort" in
    ultra) echo "astra_max" ;;
    *) echo "$profile" ;;
  esac
}

read_codex_profile_effort() {
  local profile="$1"
  "$NODE_BIN" --input-type=module -e '
    import { pathToFileURL } from "node:url";
    const { resolveCodexProfileConfigValues } = await import(pathToFileURL(process.argv[1]).href);
    try {
      const { effort } = resolveCodexProfileConfigValues(process.argv[2], {
        codexHome: process.env.CODEX_HOME || process.env.TFX_CODEX_HOME,
      });
      process.stdout.write(effort?.toLowerCase() || "");
    } catch (error) {
      console.error(`[tfx-route] ${error.message}`);
      process.exit(error.code === "unsupported-effort" ? 78 : 1);
    }
  ' "$CODEX_PROFILE_MODULE" "$profile" || {
    local resolver_status=$?
    [[ "$resolver_status" -ne 78 ]] || return 78
    echo "[tfx-route] 경고: Codex effort 해석 실패, 기존 프로필 유지: $profile" >&2
    return 0
  }
}

apply_codex_concrete_effort_guard() {
  [[ "${CLI_TYPE:-}" == "codex" ]] || return 0
  CODEX_COMPAT_PROFILE=""

  local -a args=()
  read -r -a args <<< "$CLI_ARGS"
  local profile="" i
  for ((i = 0; i < ${#args[@]}; i++)); do
    case "${args[$i]}" in
      --profile) profile="${args[$((i + 1))]:-}" ;;
      --profile=*) profile="${args[$i]#--profile=}" ;;
    esac
  done
  [[ -n "$profile" ]] || return 0

  local allow_ultra=0
  if [[ "$CODEX_OVERRIDE_PROFILE" == "astra_ultra" && -z "${TFX_RETRY_SNAPSHOT:-${TFX_RETRY_SNAPSHOT_FILE:-}}" ]]; then
    allow_ultra=1
  fi
  local effort target_profile=""
  effort=""
  case "$profile" in
    sol|astra|terra|luna) ;;
    *) if ! effort="$(read_codex_profile_effort "$profile")"; then return 78; fi ;;
  esac
  case "$profile" in
    astra_max) target_profile="astra_max" ;;
    astra_ultra) target_profile="astra_max" ;;
    *) [[ "$effort" != "ultra" ]] || target_profile="astra_max" ;;
  esac
  if [[ -n "$target_profile" ]]; then
    [[ "$allow_ultra" -ne 1 ]] || target_profile="astra_ultra"
    for ((i = 0; i < ${#args[@]}; i++)); do
      case "${args[$i]}" in
        --profile) args[$((i + 1))]="$target_profile" ;;
        --profile=*) args[$i]="--profile=$target_profile" ;;
      esac
    done
    if [[ "$profile" != "$target_profile" ]]; then
      echo "[tfx-route] Codex ultra policy: ${profile} -> ${target_profile}" >&2
    fi
    profile="$target_profile"
  fi
  local managed_spec managed_family managed_effort managed_profile
  CLI_EFFORT=""; CLI_FAMILY=""
  if managed_spec=$("$NODE_BIN" --input-type=module -e '
    import { pathToFileURL } from "node:url";
    const { modelEffortFromProfile, profileFromModelEffort, resolveCodexModelEffort } = await import(pathToFileURL(process.argv[1]).href);
    const argv = process.argv.slice(4);
    let explicitEffort;
    for (let i = 0; i < argv.length; i++) {
      if (argv[i] === "-c" && argv[i + 1]?.startsWith("model_reasoning_effort=")) {
        explicitEffort = argv[i + 1].slice("model_reasoning_effort=".length).replace(/^"|"$/g, "");
      }
    }
    let requested = modelEffortFromProfile("codex", process.argv[2], explicitEffort) || modelEffortFromProfile("codex", process.argv[2]);
    if (!requested) process.exit(0);
    if (requested.effort === "ultra" && process.argv[3] !== "1") requested = { family: "astra", effort: "max" };
    try {
      const resolved = resolveCodexModelEffort(requested, {
        codexHome: process.env.CODEX_HOME || process.env.TFX_CODEX_HOME,
      });
      if (resolved.effort !== requested.effort) {
        console.error(`[tfx-route] Codex effort: requested=${requested.effort} applied=${resolved.effort} model=${resolved.model}`);
      }
      const legacy = profileFromModelEffort("codex", requested) || "";
      process.stdout.write([resolved.family, resolved.effort, legacy].join("\x1e"));
    } catch (error) {
      console.error(`[tfx-route] ${error.message}`);
      process.exit(error.code === "unsupported-effort" ? 78 : 1);
    }
  ' "${CODEX_PROFILE_MODULE%/*}/model-effort.mjs" "$profile" "$allow_ultra" "${args[@]}"); then
    IFS=$'\x1e' read -r managed_family managed_effort managed_profile <<< "$managed_spec"
  else
    local resolver_status=$?
    [[ "$resolver_status" -ne 78 ]] || return 78
    echo "[tfx-route] 경고: 관리 Codex effort 해석 실패, 기존 프로필 유지: $profile" >&2
    managed_effort=""
    case "$profile" in sol|astra|terra|luna) CLI_FAMILY="$profile" ;; esac
  fi
  if [[ -n "$managed_effort" ]]; then
    local -a guarded_args=()
    for ((i = 0; i < ${#args[@]}; i++)); do
      if [[ "${args[$i]}" == "-c" && "${args[$((i + 1))]:-}" == model_reasoning_effort=* ]]; then
        i=$((i + 1))
      elif [[ "${args[$i]}" == "--profile" ]]; then
        guarded_args+=(--profile "$managed_family")
        i=$((i + 1))
      elif [[ "${args[$i]}" == --profile=* ]]; then
        guarded_args+=("--profile=$managed_family")
      else
        guarded_args+=("${args[$i]}")
      fi
    done
    args=("${guarded_args[@]}" -c "model_reasoning_effort=\"${managed_effort}\"")
    CLI_FAMILY="$managed_family"; CLI_EFFORT="$managed_effort"
    CODEX_COMPAT_PROFILE="$managed_profile"
  fi
  CLI_ARGS="${args[*]}"
}

apply_retry_snapshot_cli_invocation() {
  [[ "${CLI_TYPE:-}" == "codex" ]] || return 0

  local snapshot="${TFX_RETRY_SNAPSHOT:-${TFX_RETRY_SNAPSHOT_FILE:-}}"
  [[ -n "$snapshot" ]] || return 0

  local status_json
  if ! status_json=$(bridge_cli retry-status --snapshot "$snapshot" --json); then
    echo "[tfx-route] WARNING: retry snapshot status unavailable; keeping existing codex args" >&2
    return
  fi

  local retry_argv
  if ! retry_argv=$("$NODE_BIN" --input-type=module -e '
    import { pathToFileURL } from "node:url";
    const status = JSON.parse(process.argv[1] || "{}");
    if (status.cliInvocation?.cli !== "codex") process.exit(0);
    const argv = status.cliInvocation.argv;
    if (!Array.isArray(argv)) process.exit(1);
    for (const arg of argv) {
      if (typeof arg !== "string" || arg.length === 0 || /\s/.test(arg)) process.exit(2);
    }
    const { normalizeCodexProfileName } = await import(pathToFileURL(process.argv[2]).href);
    for (let i = 0; i < argv.length; i++) {
      if (argv[i] === "--profile" && argv[i + 1]) {
        argv[i + 1] = normalizeCodexProfileName(argv[i + 1]);
      } else if (argv[i].startsWith("--profile=")) {
        argv[i] = `--profile=${normalizeCodexProfileName(argv[i].slice(10))}`;
      }
    }
    process.stdout.write(argv.join(" "));
  ' -- "$status_json" "$CODEX_PROFILE_MODULE" 2>/dev/null); then
    echo "[tfx-route] WARNING: retry snapshot argv unsupported; keeping existing codex args" >&2
    return 0
  fi
  [[ -n "$retry_argv" ]] || return 0

  local -a base_args=()
  local -a retry_args=()
  local -a merged_args=()
  read -r -a base_args <<< "$CLI_ARGS"
  read -r -a retry_args <<< "$retry_argv"

  local retry_has_profile=0
  local retry_profile=""
  local i
  for ((i = 0; i < ${#retry_args[@]}; i++)); do
    case "${retry_args[$i]}" in
      --profile)
        retry_has_profile=1
        retry_profile="${retry_args[$((i + 1))]:-}"
        local safe_retry_profile
        safe_retry_profile="$(resolve_safe_retry_codex_profile "$retry_profile")"
        if [[ "$safe_retry_profile" != "$retry_profile" ]]; then
          echo "[tfx-route] retry Codex profile guard: ${retry_profile} -> ${safe_retry_profile}" >&2
          retry_profile="$safe_retry_profile"
          retry_args[$((i + 1))]="$safe_retry_profile"
        fi
        ;;
      --profile=*)
        retry_has_profile=1
        retry_profile="${retry_args[$i]#--profile=}"
        local safe_retry_profile
        safe_retry_profile="$(resolve_safe_retry_codex_profile "$retry_profile")"
        if [[ "$safe_retry_profile" != "$retry_profile" ]]; then
          echo "[tfx-route] retry Codex profile guard: ${retry_profile} -> ${safe_retry_profile}" >&2
          retry_profile="$safe_retry_profile"
          retry_args[$i]="--profile=$safe_retry_profile"
        fi
        ;;
    esac
  done

  if [[ "$retry_has_profile" -eq 1 ]]; then
    local skip_next=0
    for ((i = 0; i < ${#base_args[@]}; i++)); do
      if [[ "$skip_next" -eq 1 ]]; then
        skip_next=0
        continue
      fi
      case "${base_args[$i]}" in
        --profile)
          skip_next=1
          continue
          ;;
        --profile=*)
          continue
          ;;
      esac
      merged_args+=("${base_args[$i]}")
    done
  else
    merged_args=("${base_args[@]}")
  fi

  merged_args+=("${retry_args[@]}")
  CLI_ARGS="${merged_args[*]}"
  echo "[tfx-route] retry snapshot cliInvocation.argv applied" >&2
}

is_nested_codex_runtime() {
  [[ -n "${TFX_TEAM_NAME:-}" || -n "${TFX_WORKER_INDEX:-}" || -n "${TFX_WORKER_SANDBOX_SCOPE:-}" ]]
}

apply_codex_profile_override() {
  [[ "$CLI_TYPE" == "codex" && "$TFX_CODEX_PROFILE" != "auto" ]] || return 0
  local review_suffix=""
  [[ "$CLI_ARGS" != *" review" ]] || review_suffix=" review"
  CLI_ARGS="exec --profile ${CODEX_OVERRIDE_PROFILE} $(build_codex_base)${review_suffix}"
  echo "[tfx-route] Codex profile override: ${TFX_CODEX_PROFILE} -> ${CODEX_OVERRIDE_PROFILE}" >&2
}

apply_verifier_override() {
  [[ "$AGENT_TYPE" != "verifier" ]] && return

  case "$TFX_VERIFIER_OVERRIDE" in
    auto|"")
      return 0
      ;;
    claude)
      ORIGINAL_AGENT="${ORIGINAL_AGENT:-$AGENT_TYPE}"
      CLI_TYPE="claude-native"
      CLI_CMD=""
      CLI_ARGS=""
      CLI_EFFORT=""; CLI_FAMILY=""; DEFAULT_TIMEOUT=1200
      echo "[tfx-route] TFX_VERIFIER_OVERRIDE=claude: verifier -> claude-native" >&2
      ;;
  esac

  return 0
}

# ── MCP 인벤토리 캐시 ──
MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
MCP_FILTER_SCRIPT=""
MCP_PROFILE_REQUESTED="auto"
MCP_RESOLVED_PROFILE="default"
MCP_HINT=""
ALLOWED_MCP_SERVERS=()
CODEX_CONFIG_FLAGS=()

resolve_mcp_filter_script() {
  [[ -n "$MCP_FILTER_SCRIPT" && -f "$MCP_FILTER_SCRIPT" ]] && { printf '%s\n' "$MCP_FILTER_SCRIPT"; return 0; }
  local sd; sd="$(_get_script_dir)"
  MCP_FILTER_SCRIPT=$(_resolve_script "" \
    ${sd:+"$sd/lib/mcp-filter.mjs"} \
    "$PWD/scripts/lib/mcp-filter.mjs" "$PWD/lib/mcp-filter.mjs") || return 1
  printf '%s\n' "$MCP_FILTER_SCRIPT"
}

resolve_mcp_policy() {
  local filter_script
  if ! filter_script=$(resolve_mcp_filter_script); then
    echo "[tfx-route] 경고: mcp-filter.mjs를 찾지 못해 기본 MCP 정책을 사용합니다." >&2
    MCP_PROFILE_REQUESTED="$MCP_PROFILE"
    MCP_RESOLVED_PROFILE="$MCP_PROFILE"
    MCP_HINT=""
    ALLOWED_MCP_SERVERS=()
    CODEX_CONFIG_FLAGS=()
    return 0
  fi

  local -a cmd=(
    "$NODE_BIN" "$filter_script" delimited
    "--agent" "$AGENT_TYPE"
    "--profile" "$MCP_PROFILE"
    "--cli-type" "$CLI_TYPE"
    "--codex-config" "$_CODEX_CONFIG"
    "--inventory-file" "$MCP_CACHE"
    "--task-text" "$PROMPT"
  )
  [[ -n "$TFX_SEARCH_TOOL" ]] && cmd+=("--search-tool" "$TFX_SEARCH_TOOL")
  [[ -n "$TFX_WORKER_INDEX" ]] && cmd+=("--worker-index" "$TFX_WORKER_INDEX")
  [[ -n "${JEV_NEEDED_MCP_SERVERS:-}" ]] && cmd+=("--available" "$JEV_NEEDED_MCP_SERVERS")

  local _raw
  if ! _raw="$("${cmd[@]}" < /dev/null)"; then
    echo "[tfx-route] ERROR: MCP 정책 계산 실패" >&2
    return 1
  fi

  # mcp-filter delimited 출력의 6필드. 필드 수가 바뀌면 마지막 변수에 나머지가 섞인다.
  local _allowed_servers _codex_flags _codex_config_json
  IFS=$'\x1e' read -r MCP_PROFILE_REQUESTED MCP_RESOLVED_PROFILE MCP_HINT \
    _allowed_servers _codex_flags _codex_config_json <<< "$_raw"
  IFS=',' read -r -a ALLOWED_MCP_SERVERS <<< "$_allowed_servers"
  IFS=$'\x1f' read -r -a CODEX_CONFIG_FLAGS <<< "$_codex_flags"

  return 0
}

get_claude_model() {
  if [[ -n "${JEV_CLAUDE_MODEL:-}" ]]; then
    echo "$JEV_CLAUDE_MODEL"
    return
  fi
  case "$AGENT_TYPE" in
    explore) echo "haiku" ;;
    security-reviewer) echo "fable" ;;
    document-specialist|designer|writer|antigravity|agy) echo "opus" ;;
    code-reviewer|quality-reviewer) echo "opus" ;;
    *) echo "sonnet" ;;
  esac
}

get_claude_effort() {
  local effort="${CLI_EFFORT:-${JEV_CLAUDE_EFFORT:-}}"
  # 명시한 프로필은 해석된 정책 effort를 쓰고, 기본 native/폴백의 빈 값은 보존한다.
  [[ "$TFX_CODEX_PROFILE" == "auto" ]] || effort="$CODEX_POLICY_EFFORT"
  case "$effort" in
    low|medium|high|xhigh|max) echo "$effort" ;;
    ultra) echo "max" ;;
    *) echo "" ;;
  esac
}

emit_claude_native_metadata() {
  local model
  model=$(get_claude_model)
  local effort
  effort=$(get_claude_effort)
  echo "ROUTE_TYPE=claude-native"
  echo "AGENT=$AGENT_TYPE"
  echo "MODEL=$model"
  echo "EFFORT=$effort"
  echo "TIMEOUT=$TIMEOUT_SEC"
  echo "MCP_PROFILE=$MCP_PROFILE"
  [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
  echo "PROMPT=$PROMPT"
  echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
}

# _find_fork_pids PID — cross-platform child PID lookup
# pgrep -P (Linux/macOS) → Git Bash ps fallback (PPID/PGID column)
_find_fork_pids() {
  local parent="$1"
  if command -v pgrep &>/dev/null; then
    pgrep -P "$parent" 2>/dev/null || true
    return
  fi
  # Git Bash: PID PPID PGID WINPID ... — match by PPID or PGID
  ps 2>/dev/null | awk -v p="$parent" 'NR>1 && ($2==p || ($3==p && $1!=p)) {print $1}' | sort -un | tr '\n' ' '
}

_codex_rollout_activity_bytes() {
  [[ "${CLI_TYPE:-}" == "codex" ]] || { printf '0\n'; return 0; }
  local codex_home="${CODEX_HOME:-${TFX_CODEX_HOME:-${HOME:-}/.codex}}"
  [[ -n "$codex_home" && "$codex_home" != "/.codex" ]] || { printf '0\n'; return 0; }
  local codex_home_real="$codex_home"
  [[ -d "$codex_home" ]] && codex_home_real=$(cd "$codex_home" 2>/dev/null && pwd -P || printf '%s' "$codex_home")

  local total=0 seen="" proc_pid fd target candidate size
  for proc_pid in "$@"; do
    [[ "$proc_pid" =~ ^[0-9]+$ ]] || continue

    if [[ -d "/proc/$proc_pid/fd" ]]; then
      for fd in "/proc/$proc_pid/fd"/*; do
        [[ -e "$fd" ]] || continue
        target=$(readlink "$fd" 2>/dev/null || true)
        case "$target" in
          "$codex_home"/sessions/*/rollout-*.jsonl|"$codex_home_real"/sessions/*/rollout-*.jsonl)
            seen="${seen}${target}"$'\n'
            ;;
        esac
      done
    fi

    if command -v lsof >/dev/null 2>&1; then
      while IFS= read -r candidate; do
        case "$candidate" in
          n"$codex_home"/sessions/*/rollout-*.jsonl|n"$codex_home_real"/sessions/*/rollout-*.jsonl)
            seen="${seen}${candidate#n}"$'\n'
            ;;
        esac
      done < <(lsof -Fn -p "$proc_pid" 2>/dev/null || true)
    fi
  done

  local raw_paths="$seen"
  seen=$'\n'
  while IFS= read -r candidate; do
    [[ -f "$candidate" ]] || continue
    [[ "$seen" == *$'\n'"$candidate"$'\n'* ]] && continue
    seen="${seen}${candidate}"$'\n'
    size=$(wc -c < "$candidate" 2>/dev/null || printf '0')
    size="${size//[[:space:]]/}"
    [[ "$size" =~ ^[0-9]+$ ]] || size=0
    total=$((total + size))
  done <<< "$raw_paths"
  printf '%s\n' "$total"
}

# heartbeat_monitor PID [INTERVAL] [STALL_THRESHOLD]
# - PID: 감시할 워커 프로세스 PID
# - INTERVAL: heartbeat 출력 간격 (초, 기본 10)
# - STALL_THRESHOLD: stall 경고 임계값 (초, 기본 1200)
# 환경변수: TFX_HEARTBEAT (0이면 비활성화), TFX_HEARTBEAT_INTERVAL, TFX_STALL_THRESHOLD
heartbeat_monitor() {
  [[ "${TFX_HEARTBEAT:-1}" -eq 0 ]] && return 0
  local pid="$1"
  local interval="${2:-${TFX_HEARTBEAT_INTERVAL:-10}}"
  local stall_threshold="${3:-${TFX_STALL_THRESHOLD:-1200}}"
  local expected_duration="${TFX_EXPECTED_DURATION_SEC:-}"
  local last_size=0 stall_count=0 jev_stall_hits=0 jev_grace_granted=0
  local pid_gone=false
  local post_exit_checks=0
  local max_post_exit_checks=6  # fallback drain: 6 intervals (fork PID 미발견 시)
  local last_known_forks=""     # direct fork PID tracking

  while true; do
    sleep "$interval"

    # Check if the tracked PID is still alive; snapshot forks while alive
    if ! kill -0 "$pid" 2>/dev/null; then
      if [[ "$pid_gone" == "false" ]]; then
        pid_gone=true
        local _imm; _imm=$(_find_fork_pids "$pid") || true
        [[ -n "$_imm" ]] && last_known_forks="$_imm"
        [[ -n "$last_known_forks" ]] && \
          echo "[tfx-heartbeat] pid=$pid exited, tracking forks: $last_known_forks" >&2
      fi
    else
      local _cf; _cf=$(_find_fork_pids "$pid") || true
      [[ -n "$_cf" ]] && last_known_forks="$_cf"
    fi

    local current_size=0
    [[ -f "$STDOUT_LOG" ]] && current_size=$(wc -c < "$STDOUT_LOG" 2>/dev/null || echo 0)
    # P3: stderr 활동도 포함하여 거짓 STALL 방지
    local stderr_size=0
    [[ -f "$STDERR_LOG" ]] && stderr_size=$(wc -c < "$STDERR_LOG" 2>/dev/null || echo 0)
    local codex_rollout_size=0
    codex_rollout_size=$(_codex_rollout_activity_bytes "$pid" $last_known_forks 2>/dev/null || echo 0)
    [[ "$codex_rollout_size" =~ ^[0-9]+$ ]] || codex_rollout_size=0
    current_size=$((current_size + stderr_size + codex_rollout_size))
    local elapsed=$(($(date +%s) - TIMESTAMP))
    local expected_suffix=""
    if [[ -n "$expected_duration" && "$expected_duration" =~ ^[0-9]+$ && "$expected_duration" -gt 0 ]]; then
      expected_suffix=" expected=${expected_duration}s"
      if [[ "$elapsed" -gt $((expected_duration * 2)) ]]; then
        expected_suffix="${expected_suffix} anomaly=slow"
      fi
    fi

    if [[ "$current_size" -gt "$last_size" ]]; then
      stall_count=0
      jev_stall_hits=0
      jev_grace_granted=0
      if [[ "$pid_gone" == "true" ]]; then
        local _fi="forked"; [[ -n "$last_known_forks" ]] && _fi="forks:${last_known_forks// /,}"
        echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=active(${_fi})" >&2
        post_exit_checks=0  # reset — still producing output
      else
        echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=active" >&2
      fi
    else
      stall_count=$((stall_count + interval))
      local probe_state=""
      probe_state="$(read_probe_state "$pid" 2>/dev/null || true)"
      if [[ "$pid_gone" == "true" ]]; then
        if [[ -n "$last_known_forks" ]]; then
          # Direct fork tracking — terminate when all forks are dead
          local _alive=false
          for _fp in $last_known_forks; do
            kill -0 "$_fp" 2>/dev/null && _alive=true && break
          done
          if [[ "$_alive" == "false" ]]; then
            echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=terminated(forks-exited)" >&2
            break
          fi
          echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=fork-idle(${last_known_forks// /,})" >&2
        else
          # Fallback: output-based drain (no fork PIDs found)
          post_exit_checks=$((post_exit_checks + 1))
          if [[ "$post_exit_checks" -ge "$max_post_exit_checks" ]]; then
            echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=terminated(drain-done)" >&2
            break
          fi
          echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=draining(${post_exit_checks}/${max_post_exit_checks})" >&2
        fi
      elif [[ "$probe_state" =~ ^(mcp_initializing|input_wait)$ ]]; then
        stall_count=0
        jev_stall_hits=0
        echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=${probe_state}(probe-grace)" >&2
      else
        if [[ "$stall_count" -ge 60 && "$stall_count" -lt "$stall_threshold" ]] && jev_enabled; then
          local _jev_bridge="" _jev_out="" _jev_status=""
          if declare -f resolve_bridge_script >/dev/null 2>&1; then
            _jev_bridge="$(resolve_bridge_script 2>/dev/null || true)"
          fi
          if [[ -n "$_jev_bridge" && -f "$_jev_bridge" ]] && command -v "${NODE_BIN:-node}" >/dev/null 2>&1; then
            _jev_out="$("${NODE_BIN:-node}" "$_jev_bridge" judge-stall --stdout-log "$STDOUT_LOG" --stderr-log "$STDERR_LOG" --rollout-file "${TFX_CODEX_ROLLOUT_FILE:-}" --elapsed-sec "$elapsed" 2>/dev/null || true)"
            if [[ "$_jev_out" == *'"status":"waiting_input"'* || "$_jev_out" == *'"status":"looping_error"'* ]]; then
              jev_stall_hits=$((jev_stall_hits + 1))
              if [[ "$jev_stall_hits" -ge 2 ]]; then
                echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s Jev semantic stall 판정으로 즉시 개입 전환" >&2
                stall_count=$stall_threshold
              fi
            else
              jev_stall_hits=0
            fi
          fi
        fi
        if [[ "$stall_count" -ge "$stall_threshold" ]]; then
          if [[ "$jev_stall_hits" -lt 2 && "$jev_grace_granted" -eq 0 ]] && jev_enabled; then
            local _jev_grace_bridge="" _jev_grace_out=""
            if declare -f resolve_bridge_script >/dev/null 2>&1; then
              _jev_grace_bridge="$(resolve_bridge_script 2>/dev/null || true)"
            fi
            if [[ -n "$_jev_grace_bridge" && -f "$_jev_grace_bridge" ]] && command -v "${NODE_BIN:-node}" >/dev/null 2>&1; then
              _jev_grace_out="$("${NODE_BIN:-node}" "$_jev_grace_bridge" judge-stall --stdout-log "$STDOUT_LOG" --stderr-log "$STDERR_LOG" --rollout-file "${TFX_CODEX_ROLLOUT_FILE:-}" --elapsed-sec "$elapsed" 2>/dev/null || true)"
              if [[ "$_jev_grace_out" == *'"status":"progressing"'* ]]; then
                jev_grace_granted=1
                stall_count=0
                last_size=$current_size
                echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s Jev progressing 판정으로 stall 유예 1회 부여" >&2
                continue
              fi
            fi
          fi
        # 외곽 timeout이 hard ceiling으로 이동했으므로 기본 stall 방어는 kill이다.
        # intervene는 bridge의 4단 개입 사다리를 실행하고, 실패하면 아래 kill 블록으로 간다.
        local kill_on_stall="${TFX_STALL_KILL:-kill}"
        [[ -z "$kill_on_stall" ]] && kill_on_stall="kill"
        local kill_grace="${TFX_STALL_KILL_GRACE:-30}"
        local _should_kill=0 _should_intervene=0
        case "$kill_on_stall" in
          1|on|kill) _should_kill=1 ;;
          intervene|ladder) _should_intervene=1 ;;
          classify|0|off|disabled) ;;
          *)
            echo "[tfx-heartbeat] pid=$pid warning TFX_STALL_KILL=$kill_on_stall unknown, fallback classify" >&2
            kill_on_stall="classify"
            ;;
        esac
        if [[ "$_should_intervene" -eq 1 ]]; then
          local _bridge_script="" _intervene_payload="" _rollout_file="${TFX_CODEX_ROLLOUT_FILE:-}"
          if declare -f resolve_bridge_script >/dev/null 2>&1; then
            _bridge_script="$(resolve_bridge_script 2>/dev/null || true)"
          fi
          if [[ -n "$_bridge_script" && -f "$_bridge_script" ]] && command -v "${NODE_BIN:-node}" >/dev/null 2>&1; then
            _intervene_payload="$("${NODE_BIN:-node}" -e '
const [pid, stdoutLog, stderrLog, rolloutFile, cli, codexHome, graceGranted] = process.argv.slice(1);
process.stdout.write(JSON.stringify({
  channel: "process", pid: Number(pid), stdoutLog, stderrLog,
  rolloutFile: rolloutFile || null, cli: cli || undefined, codexHome: codexHome || undefined,
  skipJevHealthCheck: graceGranted === "1",
}));
' "$pid" "$STDOUT_LOG" "$STDERR_LOG" "$_rollout_file" "${CLI_TYPE:-}" "${CODEX_HOME:-${TFX_CODEX_HOME:-}}" "$jev_grace_granted")" || _intervene_payload=""
            echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=STALL_INTERVENE stall=${stall_count}s" >&2
            if [[ -n "$_intervene_payload" ]] && printf '%s\n' "$_intervene_payload" | "${NODE_BIN:-node}" "$_bridge_script" intervene-run --payload-file -; then
              jev_grace_granted=1
              stall_count=0
              last_size=$current_size
              continue
            fi
          fi
          echo "[tfx-heartbeat] pid=$pid intervention 실패 — SIGTERM→SIGKILL fallback으로 진행" >&2
          _should_kill=1
          # intervene 실패는 바로 기존 kill ladder를 실행한다.
          stall_count=$((stall_threshold + kill_grace))
        fi
        if [[ "$_should_kill" -eq 1 && "$stall_count" -ge $((stall_threshold + kill_grace)) ]]; then
          echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=STALL_KILL stall=${stall_count}s — SIGTERM" >&2
          # Snapshot child PIDs before SIGTERM — wrapper 가 SIGTERM 을 수용해 죽으면
          # 부모 소멸 후 taskkill /T 가 자식 트리를 탐색하지 못해 codex 자식이 orphan 으로 남는다.
          # 사용자 보고(2026-04-22): "tfx-route 래퍼 exit 이후에도 Codex 자식이 살아있음".
          local _stall_children
          _stall_children=$(_find_fork_pids "$pid" 2>/dev/null || echo "")
          kill -TERM "$pid" 2>/dev/null || true
          local _grace_waited=0
          while kill -0 "$pid" 2>/dev/null && [[ "$_grace_waited" -lt 5 ]]; do
            sleep 1
            _grace_waited=$((_grace_waited + 1))
          done
          if kill -0 "$pid" 2>/dev/null; then
            # Windows/MSYS: POSIX SIGKILL 이 Win32 자식 트리까지 닿지 않는다.
            # cleanup_workers 와 동일하게 taskkill /T /F 로 트리 종료.
            case "$(uname -s)" in
              MINGW*|MSYS*)
                echo "[tfx-heartbeat] pid=$pid SIGTERM 무시 — taskkill /T /F" >&2
                MSYS_NO_PATHCONV=1 cmd.exe //c "taskkill /T /F /PID $pid" 2>/dev/null || true ;;
              *)
                echo "[tfx-heartbeat] pid=$pid SIGTERM 무시 — SIGKILL 강제" >&2
                kill -KILL "$pid" 2>/dev/null || true ;;
            esac
          fi
          # Orphan sweep: wrapper 가 SIGTERM 을 수용해도 자식 codex 프로세스는 별도
          # Win32 process 이므로 자동 종료되지 않는다. 스냅샷 PID 중 살아있는 것만 tree kill.
          if [[ -n "$_stall_children" ]]; then
            local _orphan_alive=""
            local _cpid
            for _cpid in $_stall_children; do
              kill -0 "$_cpid" 2>/dev/null && _orphan_alive="$_orphan_alive $_cpid"
            done
            if [[ -n "$_orphan_alive" ]]; then
              echo "[tfx-heartbeat] pid=$pid orphan children detected:$_orphan_alive — tree kill" >&2
              case "$(uname -s)" in
                MINGW*|MSYS*)
                  for _cpid in $_orphan_alive; do
                    MSYS_NO_PATHCONV=1 cmd.exe //c "taskkill /T /F /PID $_cpid" 2>/dev/null || true
                  done ;;
                *)
                  for _cpid in $_orphan_alive; do
                    kill -KILL "$_cpid" 2>/dev/null || true
                  done ;;
              esac
            fi
          fi
          break
        fi
        if [[ "$kill_on_stall" == "classify" && "$stall_count" -ge $((stall_threshold + kill_grace)) ]]; then
          echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=STALL_CLASSIFY stall=${stall_count}s (no-kill — TFX_STALL_KILL=classify)" >&2
        fi
        echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=STALL stall=${stall_count}s" >&2
      else
        echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B${expected_suffix} status=quiet stall=${stall_count}s" >&2
      fi
      fi
    fi
    last_size=$current_size
  done
  echo "[tfx-heartbeat] pid=$pid terminated" >&2
}

# _wait_with_heartbeat PID — track + heartbeat + wait + cleanup
_wait_with_heartbeat() {
  local wpid="$1" hb_pid ec=0
  track_worker_pid "$wpid"
  if [[ -t 2 ]]; then
    heartbeat_monitor "$wpid" &
  else
    # < /dev/null 은 git bash sleep 자식의 caller pipe 점유를 막는 핵심 (1b31a63).
    # stderr redirect 는 의도치 않게 caller heartbeat 를 STDERR_LOG 로 흡수했으므로 제거.
    heartbeat_monitor "$wpid" < /dev/null &
  fi
  hb_pid=$!
  wait "$wpid" || ec=$?
  kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
  return "$ec"
}

# Inert self-test surface for unit tests. Bypasses CLI dispatch.
if [[ "${1:-}" == "--inert-self-test" ]]; then
  shift
  case "${1:-}" in
    heartbeat-3s)
      sleep 3 &
      _wait_with_heartbeat $! 1
      exit 0
      ;;
    *)
      echo "[tfx-route] unknown self-test target: ${1:-<empty>}" >&2
      exit 2
      ;;
  esac
fi

resolve_worker_runner_script() {
  _resolve_script "${TFX_ROUTE_WORKER_RUNNER:-}" "$(_get_script_dir)/tfx-route-worker.mjs"
}

run_stream_worker() {
  local worker_type="$1"
  local prompt="$2"
  local use_tee_flag="$3"
  shift 3
  local exit_code_local=0
  local worker_pid

  local runner_script
  if ! runner_script=$(resolve_worker_runner_script); then
    echo "[tfx-route] 경고: stream worker runner를 찾지 못했습니다." >&2
    return 127
  fi

  if ! command -v "$NODE_BIN" &>/dev/null; then
    echo "[tfx-route] 경고: node를 찾지 못해 stream worker를 실행할 수 없습니다." >&2
    return 127
  fi

  local -a worker_cmd=(
    "$NODE_BIN"
    "$runner_script"
    "--type" "$worker_type"
    "--timeout-ms" "$((HARD_CEILING_SEC * 1000))"
    "--stall-ms" "$((STALL_THRESHOLD_SEC * 1000))"
    "--cwd" "$PWD"
    "$@"
  )

  if [[ "$use_tee_flag" == "true" ]]; then
    printf '%s' "$prompt" | _exec_tracked "${TIMEOUT_CMD[@]}" "$HARD_CEILING_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
  else
    printf '%s' "$prompt" | "${TIMEOUT_CMD[@]}" "$HARD_CEILING_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
  fi
  worker_pid=$!
  _wait_with_heartbeat "$worker_pid" || exit_code_local=$?
  return "$exit_code_local"
}

run_antigravity_exec() {
  local prompt="$1"
  local use_tee_flag="$2"
  local exit_code_local=0
  local worker_pid
  local -a agy_args=()
  local -a agy_exec_env=()
  local -a _cli_tokens=()
  local _print_flag=""
  local _tok
  read -r -a _cli_tokens <<< "$CLI_ARGS"
  # ── --print 는 값(프롬프트)을 받는 마지막 인자 ──
  # agy 의 --print 는 값을 받는 플래그라(Go flag) 바로 뒤 토큰을 프롬프트로 삼킨다.
  # agy 1.1.27 실측(2026-09-07): 값 없는 `--print` 는 "flag needs an argument",
  # 빈 값 + stdin 은 "empty prompt" 로 거부된다. 즉 stdin 프롬프트 전달은 더 이상 없다.
  # 그래서 print 계열 플래그를 빼 두었다가 --model 뒤에 `--print "$prompt"` 로 붙인다.
  for _tok in "${_cli_tokens[@]}"; do
    case "$_tok" in
      --print|-p|--prompt) _print_flag="$_tok" ;;
      *) agy_args+=("$_tok") ;;
    esac
  done

  # 모델 ID에 effort를 남기면 별도 --effort와 충돌하므로 해석 결과를 나눠 전달한다.
  if [[ -z "${TFX_GEMINI_NO_MODEL:-}" ]]; then
    local _agy_model _agy_effort _agy_selection
    _agy_selection="$(resolve_gemini_profile "${TFX_GEMINI_PROFILE:-${GEMINI_PROFILE:-argon}}")" || return $?
    IFS=$'\x1e' read -r _agy_model _agy_effort <<< "$_agy_selection"
    if [[ -n "$_agy_model" ]]; then
      agy_args+=("--model" "$_agy_model")
      [[ -z "$_agy_effort" ]] || agy_args+=("--effort" "$_agy_effort")
    fi
  fi
  if [[ -n "$_print_flag" ]]; then
    # argv 상한(macOS ARG_MAX 1MiB) 안에서만 인자로 넘긴다. 초과는 조용히 자르지 않는다.
    if (( ${#prompt} > 900000 )); then
      echo "[tfx-route] Antigravity prompt too large for argv (${#prompt} chars > 900000). Split the task or shorten the context." >"$STDERR_LOG"
      return 2
    fi
    agy_args+=("$_print_flag" "$prompt")
  fi

  if ! agy_supports_headless "$CLI_CMD"; then
    echo "[tfx-route] Antigravity CLI headless flags unsupported or missing: $CLI_CMD" >"$STDERR_LOG"
    return 127
  fi

  if agy_should_scrub_ssh_env; then
    agy_exec_env=(env -u SSH_CONNECTION -u SSH_CLIENT -u SSH_TTY)
    echo "[tfx-route] agy가 Keychain을 읽도록 SSH 변수를 제거했습니다." >&2
  fi

  if [[ "$use_tee_flag" == "true" ]]; then
    _exec_tracked "${TIMEOUT_CMD[@]}" "$HARD_CEILING_SEC" ${agy_exec_env[@]+"${agy_exec_env[@]}"} "$CLI_CMD" "${agy_args[@]}" </dev/null 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
  else
    "${TIMEOUT_CMD[@]}" "$HARD_CEILING_SEC" ${agy_exec_env[@]+"${agy_exec_env[@]}"} "$CLI_CMD" "${agy_args[@]}" </dev/null >"$STDOUT_LOG" 2>"$STDERR_LOG" &
  fi
  worker_pid=$!
  if [[ -n "${JOB_DIR:-}" && -w "${JOB_DIR}" ]]; then
    echo "$worker_pid" >> "$JOB_DIR/child_pids"
  fi
  _wait_with_heartbeat "$worker_pid" || exit_code_local=$?
  return "$exit_code_local"
}

# 응답 없는 MCP는 명시적으로 꺼서 config 기본값으로 재연결하지 않게 한다.
# Opt-out: TFX_MCP_HEALTH_CHECK=0
_mcp_preflight_filter_dead() {
  local opt="${TFX_MCP_HEALTH_CHECK:-1}"
  if [[ "$opt" == "0" || "$opt" == "false" || "$opt" == "off" ]]; then
    return 0
  fi
  if [[ "${#CODEX_CONFIG_FLAGS[@]}" -eq 0 ]]; then
    return 0
  fi

  local sd; sd="$(_get_script_dir)"
  local health_script
  health_script="$(_resolve_script "${TFX_MCP_HEALTH_SCRIPT:-}" \
    ${TFX_PKG_ROOT:+"$TFX_PKG_ROOT/scripts/lib/mcp-health.mjs"} \
    "$sd/lib/mcp-health.mjs" "$sd/../scripts/lib/mcp-health.mjs")" || return 0
  [[ -n "$health_script" && -f "$health_script" ]] || return 0
  command -v "$NODE_BIN" &>/dev/null || return 0

  # CODEX_CONFIG_FLAGS 에서 enabled=true 항목으로부터 후보 서버 이름 수집.
  # #153: parseMcpServersFromToml 은 section 이름에 dot 을 허용 (`[a-zA-Z0-9_.-]+`).
  # `[mcp_servers.foo.bar]` 같은 dotted 서버가 `mcp_servers.foo.bar.enabled=true`
  # 플래그로 전달될 때 과거 `[^.]+` 정규식은 `foo` 만 captur 해 suffix 매치 실패
  # → dotted 서버가 preflight candidate 에서 통째로 누락됐다. `\.enabled=true$`
  # 로 끝 anchor 가 고정돼 있어 `(.+)` greedy 가 반복 보장한다.
  local names=""
  local i=0
  local n="${#CODEX_CONFIG_FLAGS[@]}"
  while (( i < n )); do
    local flag="${CODEX_CONFIG_FLAGS[$i]}"
    if [[ "$flag" == "-c" ]] && (( i + 1 < n )); then
      local value="${CODEX_CONFIG_FLAGS[$((i+1))]}"
      if [[ "$value" =~ ^mcp_servers\.(.+)\.enabled=true$ ]]; then
        [[ -n "$names" ]] && names="${names},"
        names="${names}${BASH_REMATCH[1]}"
      fi
      i=$((i+2))
    else
      i=$((i+1))
    fi
  done
  [[ -z "$names" ]] && return 0

  # Probe — TTL cache 로 재호출 부하 억제
  local probe_output
  if ! probe_output=$("$NODE_BIN" "$health_script" probe \
      --names "$names" --format shell < /dev/null 2>/dev/null); then
    echo "[tfx-route] MCP preflight probe 실패 — 스킵" >&2
    return 0
  fi

  local dead_list=""
  while IFS= read -r line; do
    if [[ "$line" =~ ^MCP_DEAD=\"(.*)\"$ ]]; then
      dead_list="${BASH_REMATCH[1]}"
    fi
  done <<< "$probe_output"
  [[ -z "$dead_list" ]] && return 0

  # dead 서버의 enabled override를 false로 바꾸고 나머지 설정은 보존한다.
  local -a dead_names=()
  IFS=',' read -ra dead_names <<< "$dead_list"
  local -a new_flags=()
  i=0
  while (( i < n )); do
    local flag="${CODEX_CONFIG_FLAGS[$i]}"
    if [[ "$flag" == "-c" ]] && (( i + 1 < n )); then
      local value="${CODEX_CONFIG_FLAGS[$((i+1))]}"
      local dead
      for dead in "${dead_names[@]}"; do
        [[ -z "$dead" ]] && continue
        if [[ "$value" == "mcp_servers.${dead}.enabled="* ]]; then
          value="mcp_servers.${dead}.enabled=false"
          break
        fi
      done
      new_flags+=("-c" "$value")
      i=$((i+2))
    else
      new_flags+=("$flag")
      i=$((i+1))
    fi
  done

  CODEX_CONFIG_FLAGS=(${new_flags[@]+"${new_flags[@]}"})
  echo "[tfx-route] MCP preflight: ${#dead_names[@]}개 dead MCP 제외 (${dead_list})" >&2

  # 전부 dead일 때 명시적 early-fail만 중단하고, 기본은 도구 안내 없이 계속한다.
  local remaining_alive=0
  local rflag
  for rflag in ${CODEX_CONFIG_FLAGS[@]+"${CODEX_CONFIG_FLAGS[@]}"}; do
    # #153 + #170 P1: candidate 추출 정규식 (line 1607) 과 일관 — dotted server 이름
    # (e.g. mcp_servers.foo.bar.enabled=true) 도 alive 로 카운트한다. `[^.]+` 는 첫 dot
    # 에서 끊겨 dotted alive 만 남은 경우 false all-dead 판정 → 불필요 degraded.
    if [[ "$rflag" =~ ^mcp_servers\..+\.enabled=true$ ]]; then
      remaining_alive=$((remaining_alive + 1))
    fi
  done

  if [[ "$remaining_alive" -eq 0 ]]; then
    if [[ "${TFX_MCP_FAIL_ON_ALL_DEAD:-0}" == "1" ]]; then
      echo "[tfx-route] 조기 실패: TFX_MCP_FAIL_ON_ALL_DEAD=1 + MCP 전부 dead — Codex 호출 중단" >&2
      echo "  복구: (1) dead MCP 복구 (2) TFX_MCP_HEALTH_CHECK=0 preflight 비활성 (3) TFX_MCP_FAIL_ON_ALL_DEAD=0 graceful degradation" >&2
      return 78
    fi
    export _TFX_MCP_DEGRADED=1
    echo "[tfx-route] graceful degradation: MCP 전부 dead, 도구 안내 생략 (set TFX_MCP_FAIL_ON_ALL_DEAD=1 to revert to early-fail)" >&2
    return 0
  fi
}

# codex-recovery.sh 의 recover_codex_stdout 헬퍼 사용. STDOUT_LOG/STDERR_LOG env.
_TFX_ROUTE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$_TFX_ROUTE_DIR/lib/codex-recovery.sh" ]]; then
  # shellcheck source=lib/codex-recovery.sh
  source "$_TFX_ROUTE_DIR/lib/codex-recovery.sh"
else
  echo "[tfx-route] WARNING: optional helper missing: $_TFX_ROUTE_DIR/lib/codex-recovery.sh (run: tfx doctor --fix)" >&2
fi

_codex_stdout_has_meaningful_output() {
  local output_file="$1"
  [[ -s "$output_file" ]] || return 1
  awk '
    {
      line = $0
      gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
      if (line != "" && line != "Reading additional input from stdin...") {
        found = 1
        exit
      }
    }
    END { exit !found }
  ' "$output_file" 2>/dev/null
}

run_codex_exec() {
  refresh_codex_profiles
  local prompt="$1"
  local use_tee_flag="$2"
  local exit_code_local=0
  local worker_pid
  local -a codex_args=()
  read -r -a codex_args <<< "$CLI_ARGS"
  codex_args=("${codex_args[0]}" ${CODEX_CONFIG_FLAGS[@]+"${CODEX_CONFIG_FLAGS[@]}"} "${codex_args[@]:1}")
  local codex_home
  codex_home="${CODEX_HOME:-${TFX_CODEX_HOME:-${HOME:-}/.codex}}"
  if [[ -n "${CLI_FAMILY:-}" && ! -f "$codex_home/$CLI_FAMILY.config.toml" ]]; then
    if [[ -z "${CODEX_COMPAT_PROFILE:-}" || ! -f "$codex_home/$CODEX_COMPAT_PROFILE.config.toml" ]]; then
      echo "[tfx-route] 오류: 가족과 결합 프로필 파일이 모두 없습니다: $CLI_FAMILY, $CODEX_COMPAT_PROFILE" >&2
      return 78
    fi
    echo "[tfx-route] 경고: 가족 프로필 파일이 없어 결합 프로필 유지: $CLI_FAMILY -> $CODEX_COMPAT_PROFILE" >&2
    local i
    for ((i = 0; i < ${#codex_args[@]}; i++)); do
      case "${codex_args[$i]}" in
        --profile) codex_args[$((i + 1))]="$CODEX_COMPAT_PROFILE" ;;
        --profile=*) codex_args[$i]="--profile=$CODEX_COMPAT_PROFILE" ;;
      esac
    done
  fi

  _attempt_codex_run() {
    exit_code_local=0
    : > "$CODEX_LAST_MESSAGE_LOG"
    # `--` end-of-options: prompt가 '--'/'---' (front-matter 등)로 시작하면
    # clap이 flag로 파싱하는 것을 방지. fallback path에서 특히 중요.
    if [[ "$use_tee_flag" == "true" ]]; then
      _exec_tracked "${TIMEOUT_CMD[@]}" "$HARD_CEILING_SEC" "$CLI_CMD" "${codex_args[@]}" --output-last-message "$CODEX_LAST_MESSAGE_LOG" -- "$prompt" < /dev/null 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
    else
      "${TIMEOUT_CMD[@]}" "$HARD_CEILING_SEC" "$CLI_CMD" "${codex_args[@]}" --output-last-message "$CODEX_LAST_MESSAGE_LOG" -- "$prompt" < /dev/null >"$STDOUT_LOG" 2>"$STDERR_LOG" &
    fi
    worker_pid=$!
    # Track codex child PID so --job-status can detect orphan-running when wrapper dies (Issue #176).
    if [[ -n "${JOB_DIR:-}" && -w "${JOB_DIR}" ]]; then
      echo "$worker_pid" >> "$JOB_DIR/child_pids"
    fi
    _wait_with_heartbeat "$worker_pid" || exit_code_local=$?
  }

  _attempt_codex_run

  # Tier-fallback: ChatGPT account가 높은 reasoning profile을 거부하면
  # GPT-6 Luna low로만 1회 재시도한다. 구형 gpt-5.4 계열로는 더 이상 다운그레이드하지 않는다.
  # 출처: Issue #211 — codex CLI tier-aware fallback.
  if [[ "$exit_code_local" -ne 0 ]] && grep -qE "is not supported when using Codex with a ChatGPT account" "$STDERR_LOG" 2>/dev/null; then
    local fallback_profile=""
    case "$CLI_EFFORT" in
      ultra|max|xhigh|high|medium) fallback_profile="luna" ;;
    esac
    if [[ -n "$fallback_profile" ]]; then
      echo "[tfx-route] tier fallback: $CLI_EFFORT not supported on ChatGPT account → retry with $fallback_profile" >&2
      local -a new_args=()
      local i arg next_arg
      for ((i = 0; i < ${#codex_args[@]}; i++)); do
        arg="${codex_args[$i]}"
        next_arg="${codex_args[$((i + 1))]:-}"
        if [[ "$arg" == "--profile" ]]; then
          new_args+=("$arg" "$fallback_profile")
          i=$((i + 1))
        elif [[ "$arg" == --profile=* ]]; then
          new_args+=("--profile=$fallback_profile")
        elif [[ "$arg" == "-c" && ( "$next_arg" == model=* || "$next_arg" == model_reasoning_effort=* ) ]]; then
          i=$((i + 1))
        else
          new_args+=("$arg")
        fi
      done
      codex_args=("${new_args[@]}" -c 'model_reasoning_effort="low"')
      CLI_FAMILY="$fallback_profile"; CLI_EFFORT="low"
      _attempt_codex_run
    fi
  fi

  # 공식 -o 계약이 산출물을 제공했으면 raw stdout은 진단 로그 그대로 보존한다.
  # -o가 비었을 때만 기존 stderr 복구 경로로 호환한다.
  if [[ ! -s "$CODEX_LAST_MESSAGE_LOG" ]]; then
    recover_codex_stdout
  fi

  return "$exit_code_local"
}

apply_jev_pre_route() {
  jev_enabled || return 0
  [[ -n "${PROMPT:-}" ]] || return 0

  local jev_row=""
  if ! jev_row=$(bridge_cli judge-route \
    --agent "$AGENT_TYPE" \
    --cli-type "${TFX_CLI_MODE:-codex}" \
    --prompt "$PROMPT" \
    --delimited 2>/dev/null); then
    return 0
  fi
  [[ -n "$jev_row" ]] || return 0

  local _jev_comp _jev_codex_prof _jev_agy_prof _jev_claude_mod _jev_claude_eff
  local _jev_dur _jev_exec_mode _jev_mcp_prof _jev_mcp_srvs _jev_conf
  IFS=$'\x1e' read -r _jev_comp _jev_codex_prof _jev_agy_prof _jev_claude_mod _jev_claude_eff \
    _jev_dur _jev_exec_mode _jev_mcp_prof _jev_mcp_srvs _jev_conf <<< "$jev_row"

  local _jev_codex_reason=explicit_override_preserved _jev_agy_reason=explicit_override_preserved
  [[ "${TFX_CODEX_PROFILE:-auto}" != "auto" ]] || _jev_codex_reason=no_recommendation
  [[ -n "${TFX_GEMINI_PROFILE:-}" ]] || _jev_agy_reason=no_recommendation
  if [[ "${TFX_CODEX_PROFILE:-auto}" == "auto" && -n "$_jev_codex_prof" ]]; then
    TFX_CODEX_PROFILE="$_jev_codex_prof"
    _jev_codex_reason=jev_applied
  fi
  if [[ -z "${TFX_GEMINI_PROFILE:-}" && -n "$_jev_agy_prof" ]]; then
    TFX_GEMINI_PROFILE="$_jev_agy_prof"
    _jev_agy_reason=jev_applied
    export TFX_GEMINI_PROFILE
  fi
  if [[ "${TFX_EXECUTION_MODE:-auto}" == "auto" && -n "$_jev_exec_mode" ]]; then
    TFX_EXECUTION_MODE="$_jev_exec_mode"
  fi
  if [[ "${MCP_PROFILE:-auto}" == "auto" && -n "$_jev_mcp_prof" ]]; then
    MCP_PROFILE="$_jev_mcp_prof"
  fi
  if [[ -n "$_jev_mcp_srvs" ]]; then
    JEV_NEEDED_MCP_SERVERS="$_jev_mcp_srvs"
  fi
  if [[ -z "${TFX_EXPECTED_DURATION_SEC:-}" && -n "$_jev_dur" ]]; then
    TFX_EXPECTED_DURATION_SEC="$_jev_dur"
  fi
  JEV_CLAUDE_MODEL="$_jev_claude_mod"
  JEV_CLAUDE_EFFORT="$_jev_claude_eff"
  echo "[tfx-route] Jev pre-route: complexity=${_jev_comp} recommended_codex=${_jev_codex_prof:-none} effective_codex=${TFX_CODEX_PROFILE:-auto} codex_reason=${_jev_codex_reason} recommended_agy=${_jev_agy_prof:-none} effective_agy=${TFX_GEMINI_PROFILE:-auto} agy_reason=${_jev_agy_reason} mode=${TFX_EXECUTION_MODE} mcp=${MCP_PROFILE} conf=${_jev_conf}" >&2
}

# ── 메인 실행 ──
main() {
  # 종료 시 per-process 에이전트 파일 + 워커 프로세스 정리
  trap 'cleanup_workers' EXIT

  apply_jev_pre_route
  route_agent "$AGENT_TYPE"
  apply_cli_mode
  apply_no_claude_native_mode
  apply_verifier_override
  apply_codex_profile_override
  apply_retry_snapshot_cli_invocation
  apply_cli_disable_policy
  apply_codex_concrete_effort_guard

  # CLI 경로 해석
  case "$CLI_CMD" in
    codex) CLI_CMD="$CODEX_BIN" ;;
    agy) CLI_CMD="$AGY_BIN" ;;
    claude) CLI_CMD="$CLAUDE_BIN" ;;
  esac

  # 타임아웃 결정
  if [[ -n "$USER_TIMEOUT" ]]; then
    if ! [[ "$USER_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
      echo "[tfx-route] 경고: 유효하지 않은 타임아웃 값 ($USER_TIMEOUT), 기본값 사용" >&2
      USER_TIMEOUT=""
      TIMEOUT_SEC="$DEFAULT_TIMEOUT"
    else
      TIMEOUT_SEC="$USER_TIMEOUT"
    fi
  else
    TIMEOUT_SEC="$DEFAULT_TIMEOUT"
  fi

  # TIMEOUT_SEC/DEFAULT_TIMEOUT은 예상 소요 시간(advisory)만 나타낸다.
  # 실제 wall-clock 종료는 아래 hard ceiling만 수행한다.
  HARD_CEILING_SEC="${TFX_HARD_CEILING_SEC:-21600}"
  if ! [[ "$HARD_CEILING_SEC" =~ ^(0|[1-9][0-9]*)$ ]]; then
    echo "[tfx-route] 경고: TFX_HARD_CEILING_SEC='$HARD_CEILING_SEC' 무효 — 21600 사용" >&2
    HARD_CEILING_SEC=21600
  fi
  if [[ "${TFX_TIMEOUT_MODE:-activity}" == "wallclock" ]]; then
    HARD_CEILING_SEC="$TIMEOUT_SEC"
  elif [[ "$HARD_CEILING_SEC" -gt 0 && "$HARD_CEILING_SEC" -lt "$TIMEOUT_SEC" ]]; then
    echo "[tfx-route] 경고: hard ceiling(${HARD_CEILING_SEC}s) < expected(${TIMEOUT_SEC}s) — expected로 승격" >&2
    HARD_CEILING_SEC="$TIMEOUT_SEC"
  fi
  if [[ "$HARD_CEILING_SEC" -eq 0 ]]; then
    TIMEOUT_CMD=("_no_timeout")
  fi
  STALL_THRESHOLD_SEC="${TFX_STALL_THRESHOLD:-1200}"
  if ! [[ "$STALL_THRESHOLD_SEC" =~ ^[1-9][0-9]*$ ]]; then
    echo "[tfx-route] 경고: TFX_STALL_THRESHOLD='$STALL_THRESHOLD_SEC' 무효 — 1200 사용" >&2
    STALL_THRESHOLD_SEC=1200
  fi

  local _est_dur
  _est_dur=$(estimate_expected_duration_sec "$MCP_PROFILE" "$PROMPT")
  [[ "$TIMEOUT_SEC" -gt "$_est_dur" ]] && _est_dur="$TIMEOUT_SEC"
  TFX_EXPECTED_DURATION_SEC="${TFX_EXPECTED_DURATION_SEC:-$_est_dur}"
  export TFX_EXPECTED_DURATION_SEC

  # 컨텍스트 파일 → 프롬프트에 주입
  if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
    local ctx_content
    # 32KB 상한. `cat | head -c` 는 큰 파일에서 cat 이 SIGPIPE 를 받아 pipefail+set -e 로
    # 스크립트가 메시지 없이 141 로 끝나므로 head 가 파일을 직접 읽는다.
    ctx_content=$(head -c 32768 "$CONTEXT_FILE" 2>/dev/null || true)
    PROMPT="${PROMPT}

<prior_context>
${ctx_content}
</prior_context>"
  fi

  resolve_mcp_policy

  # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
  if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
    if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
      CLI_TYPE="claude"
      CLI_CMD="$CLAUDE_BIN"
      echo "[tfx-route] non-tty 팀 환경: claude-native -> claude stream wrapper 전환" >&2
    else
      echo "[tfx-route] claude stream wrapper 미사용: native metadata 유지" >&2
    fi
  fi

  # Claude 네이티브 에이전트는 이 스크립트로 처리 불가
  if [[ "$CLI_TYPE" == "claude-native" ]]; then
    if [[ -n "$TFX_TEAM_NAME" ]]; then
      local native_model native_adapter
      native_adapter="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/cli-claude.mjs"
      [[ -f "$native_adapter" ]] || native_adapter="${TFX_PKG_ROOT:-}/scripts/lib/cli-claude.mjs"
      native_model=$("$NODE_BIN" -e 'import(process.argv[1]).then(({plan}) => process.stdout.write(plan({agent: process.argv[2]}).model || ""))' "$native_adapter" "$AGENT_TYPE" 2>/dev/null)
      if [[ -z "$native_model" ]]; then
        echo "ERROR: Claude native handoff model 해석 실패: $AGENT_TYPE" >&2
        exit 1
      fi
      # 팀 모드: Hub에 fallback 필요 시그널 전송 후 구조화된 출력
      echo "[tfx-route] claude-native 역할($AGENT_TYPE)은 tfx-route.sh로 실행 불가 — Claude Agent fallback 필요" >&2
      team_complete_task "fallback" "claude-native 역할 실행 불가: ${AGENT_TYPE}. Claude Task(sonnet) 에이전트로 위임하세요."
      cat <<FALLBACK_EOF
=== TFX_NEEDS_FALLBACK ===
agent_type: ${AGENT_TYPE}
reason: claude-native roles require Claude Agent tools (Read/Edit/Grep). tfx-route.sh cannot provide these.
action: Lead should spawn Agent(subagent_type="${AGENT_TYPE}", model="${native_model}") for this task.
task_id: ${TFX_TEAM_TASK_ID:-none}
FALLBACK_EOF
      exit 0
    fi
    emit_claude_native_metadata
    exit 0
  fi

  local FULL_PROMPT="$PROMPT"
  if [[ -n "$MCP_HINT" ]]; then
    FULL_PROMPT="${PROMPT}"$'\n\n'"[도구 안내] ${MCP_HINT}"
  fi

  # 메타정보 (stderr)
  echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT family=$CLI_FAMILY expected=${TIMEOUT_SEC}s stall=${STALL_THRESHOLD_SEC}s ceiling=${HARD_CEILING_SEC}s" >&2
  if [[ -n "$REVIEW_AUTHOR" ]]; then
    echo "[tfx-route] reviewer=$CLI_TYPE review_author=$REVIEW_AUTHOR usage_switch=$REVIEW_USAGE_SWITCH cli_mode=$TFX_CLI_MODE" >&2
  fi
  echo "[tfx-route] mcp_profile=$MCP_PROFILE resolved_profile=$MCP_RESOLVED_PROFILE verifier_override=$TFX_VERIFIER_OVERRIDE" >&2
  if [[ ${#ALLOWED_MCP_SERVERS[@]} -gt 0 ]]; then
    echo "[tfx-route] allowed_mcp_servers=$(IFS=,; echo "${ALLOWED_MCP_SERVERS[*]}")" >&2
  else
    echo "[tfx-route] allowed_mcp_servers=none" >&2
  fi
  if [[ -n "$TFX_WORKER_INDEX" || -n "$TFX_SEARCH_TOOL" ]]; then
    echo "[tfx-route] worker_index=${TFX_WORKER_INDEX:-auto} search_tool=${TFX_SEARCH_TOOL:-auto}" >&2
  fi
  [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
  [[ -n "${TFX_REROUTED_FROM:-}" ]] && echo "[tfx-route] rerouted_from=$TFX_REROUTED_FROM" >&2

  # Per-process 에이전트 등록
  register_agent

  # 팀 모드: task claim
  team_claim_task
  team_send_message "작업 시작: ${TFX_TEAM_AGENT_NAME}" "task ${TFX_TEAM_TASK_ID} started"

  # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
  local exit_code=0
  local start_time
  start_time=$(date +%s)
  local workspace_signature_before=""
  local workspace_signature_after=""
  local workspace_probe_supported=false
  if workspace_signature_before=$(capture_workspace_signature); then
    workspace_probe_supported=true
  fi
  # recover_codex_stdout 이 복구 진입 시 1로 세운다(dynamic scope로 이 local 이 갱신됨).
  # 매 실행 초기화로 스테일 방지. 진짜 산출물 부재 판정(아래 no-op 가드)에 쓰인다.
  local CODEX_STDOUT_WAS_RECOVERED=0
  local result_reason=""

  # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
  # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
  # 실시간 모니터링은 Shift+Down으로 워커 pane 전환 권장
  local use_tee=false
  if [[ -n "$TFX_TEAM_NAME" ]]; then
    if [[ -t 1 ]] || [[ -n "${TMUX:-}" ]]; then
      use_tee=true
    fi
  fi

  if [[ "$CLI_TYPE" == "codex" ]]; then
    # Degraded is a per-invocation result, not an inherited process contract.
    # Test and wrapper environments can carry stale exported values from prior
    # route calls; clear it before the current MCP preflight decides.
    unset _TFX_MCP_DEGRADED
    local _preflight_rc=0
    _mcp_preflight_filter_dead || _preflight_rc=$?
    if [[ "$_preflight_rc" -eq 78 ]]; then
      exit 78
    fi
    # 사용할 수 없는 MCP를 프롬프트가 안내하지 않게 한다.
    if [[ "${_TFX_MCP_DEGRADED:-0}" == "1" ]]; then
      FULL_PROMPT="$PROMPT"
    fi
    # 스킬 주입은 sentinel로
    # trailing newline 을 보존한다. 미설정이면 prepend_skill 이 no-op.
    if [[ -n "${TFX_INJECT_SKILL:-}" ]]; then
      local _codex_skill_sentinel="__TFX_CODEX_SKILL_END_${$}_${RANDOM}__"
      local _codex_skill_prompt
      _codex_skill_prompt="$(prepend_skill "$FULL_PROMPT"; printf '%s' "$_codex_skill_sentinel")"
      FULL_PROMPT="${_codex_skill_prompt%"$_codex_skill_sentinel"}"
    fi
    run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?

  elif [[ "$CLI_TYPE" == "antigravity" ]]; then
    # Codex degraded branch strips MCP_HINT; keep agy parity when the marker is inherited.
    if [[ "${_TFX_MCP_DEGRADED:-0}" == "1" ]]; then
      FULL_PROMPT="$PROMPT"
    fi
    # Opt-in 스킬 주입 (TFX_INJECT_SKILL) — codex 레인과 동일.
    if [[ -n "${TFX_INJECT_SKILL:-}" ]]; then
      local _agy_skill_sentinel="__TFX_AGY_SKILL_END_${$}_${RANDOM}__"
      local _agy_skill_prompt
      _agy_skill_prompt="$(prepend_skill "$FULL_PROMPT"; printf '%s' "$_agy_skill_sentinel")"
      FULL_PROMPT="${_agy_skill_prompt%"$_agy_skill_sentinel"}"
    fi
    # agy(Gemini 3.x) anti-overclaim 규율 블록을 프롬프트 END 에 append. opt-out: TFX_AGY_ANTI_OVERCLAIM=0.
    local _agy_aoc_sentinel="__TFX_AGY_AOC_END_${$}_${RANDOM}__"
    local _agy_aoc_prompt
    _agy_aoc_prompt="$(append_agy_anti_overclaim "$FULL_PROMPT" "${WORKDIR:-$PWD}"; printf '%s' "$_agy_aoc_sentinel")"
    FULL_PROMPT="${_agy_aoc_prompt%"$_agy_aoc_sentinel"}"
    run_antigravity_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
    if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
      local agy_stderr_bytes=0
      [[ -f "$STDERR_LOG" ]] && agy_stderr_bytes=$(wc -c < "$STDERR_LOG" 2>/dev/null | tr -d ' ')
      echo "[tfx-route] Antigravity CLI 실패(exit=${exit_code}, stderr=${agy_stderr_bytes}B)." >&2
      if [[ "$agy_stderr_bytes" -gt 0 ]]; then
        echo "[tfx-route] Antigravity stderr 보존:" >&2
        tail -c 2048 "$STDERR_LOG" >&2
      fi
    fi

  elif [[ "$CLI_TYPE" == "claude" ]]; then
    local claude_model claude_effort
    claude_model=$(get_claude_model)
    claude_effort=$(get_claude_effort)
    local -a claude_worker_args=(
      "--command" "$CLI_CMD"
      "--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
      "--model" "$claude_model"
    )
    case "${TFX_EXECUTION_MODE:-bypass}" in
      read_only)
        claude_worker_args+=("--permission-mode" "plan")
        ;;
      workspace_auto)
        claude_worker_args+=("--permission-mode" "acceptEdits")
        ;;
      *)
        claude_worker_args+=(
          "--permission-mode" "bypassPermissions"
          "--allow-dangerously-skip-permissions"
        )
        ;;
    esac
    [ -n "$claude_effort" ] && claude_worker_args+=("--effort" "$claude_effort")

    run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
    if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 && "$_AGY_CLAUDE_FALLBACK" == "1" ]]; then
      echo "[tfx-route] Claude fallback 워커 실패(exit=${exit_code})." >&2
      [[ -f "$STDERR_LOG" ]] && tail -c 2048 "$STDERR_LOG" >&2
    elif [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 && -n "$REVIEW_AUTHOR" ]]; then
      # 리뷰는 판정이 결과라 native 안내로 바꾸면 리뷰 없이 성공으로 보인다. 실패로 남긴다.
      echo "[tfx-route] Claude 리뷰 워커 실패(exit=${exit_code}). 리뷰가 실행되지 않았다." >&2
      [[ -f "$STDERR_LOG" ]] && tail -c 2048 "$STDERR_LOG" >&2
    elif [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
      local claude_stderr_bytes=0
      [[ -f "$STDERR_LOG" ]] && claude_stderr_bytes=$(wc -c < "$STDERR_LOG" 2>/dev/null | tr -d ' ')
      echo "[tfx-route] Claude stream wrapper 실패(exit=${exit_code}, stderr=${claude_stderr_bytes}B). native metadata로 fallback합니다." >&2
      if [[ "$claude_stderr_bytes" -gt 0 ]]; then
        echo "[tfx-route] Claude stderr 보존:" >&2
        tail -c 2048 "$STDERR_LOG" >&2
      fi
      cat > "$STDOUT_LOG" <<EOF
$(emit_claude_native_metadata)
EOF
      : > "$STDERR_LOG"
      exit_code=0
      CLI_TYPE="claude-native"
    fi
  fi

  local end_time
  end_time=$(date +%s)
  local elapsed=$((end_time - start_time))

  if [[ "$exit_code" -eq 0 ]]; then
    local workspace_changed="unknown"
    if [[ "$workspace_probe_supported" == "true" ]]; then
      if workspace_signature_after=$(capture_workspace_signature); then
        if [[ "$workspace_signature_before" != "$workspace_signature_after" ]]; then
          workspace_changed="yes"
        else
          workspace_changed="no"
        fi
      fi
    fi

    # 진짜 산출물 부재 판정 — 빈 stdout 이거나, recover_codex_stdout 이 stderr
    # 노이즈로 backfill 한 경우(복구 플래그)면 진짜 워커 출력이 아니다. 복구가
    # `! -s STDOUT_LOG` 를 무력화해 빈손 실행을 success 로 오보고하던 갭을 막는다.
    local no_genuine_output="no"
    if [[ "$CLI_TYPE" == "codex" && -s "$CODEX_LAST_MESSAGE_LOG" ]]; then
      no_genuine_output="no"
    elif [[ "$CLI_TYPE" == "codex" ]] && ! _codex_stdout_has_meaningful_output "$STDOUT_LOG"; then
      no_genuine_output="yes"
    elif [[ ! -s "$STDOUT_LOG" || "${CODEX_STDOUT_WAS_RECOVERED:-0}" == "1" ]]; then
      no_genuine_output="yes"
    fi

    if [[ "$CLI_TYPE" == "codex" && ! -s "$CODEX_LAST_MESSAGE_LOG" && "$no_genuine_output" == "yes" ]]; then
      result_reason="no_final_message"
    fi

    # MCP transport 채널이 실행 중 죽었는지(exit 0 이어도) stderr 서명으로 판정.
    local mcp_transport_crashed="no"
    if declare -F codex_stdout_transport_crashed >/dev/null 2>&1 \
       && codex_stdout_transport_crashed "$STDERR_LOG"; then
      mcp_transport_crashed="yes"
    fi

    # 승격 조건: 진짜 출력이 없고(무근거 성공) + (워크스페이스 무변화 = 완전 no-op
    # 이거나 transport 채널 사망 = 결과 신뢰 불가). antigravity 는 stdout 계약이
    # 달라 제외. 진짜 출력이 있는 성공은 no_genuine_output=no 라 절대 승격되지 않는다.
    # ★의도된 보수적 트레이드오프: codex 가 stdout 없이 stderr 로만 답을 쓰고 recover
    # 가 salvage 한 경우(no_genuine_output=yes)라도 transport 채널이 죽었으면 결과를
    # 신뢰 불가로 보고 실패로 승격한다. 채널 사망 후 salvage 는 완결 보장이 없기
    # 때문이며, 극히 드문 stdout-없는 정상완료보다 false-success 방지를 우선한다.
    if [[ "$CLI_TYPE" != "antigravity" && "$no_genuine_output" == "yes" ]] \
       && { [[ "$workspace_changed" == "no" ]] || [[ "$mcp_transport_crashed" == "yes" ]]; }; then
      printf '%s\n' "[tfx-route] exit 0 이지만 진짜 산출물이 없습니다 (recovered=${CODEX_STDOUT_WAS_RECOVERED:-0}, workspace_changed=${workspace_changed}, mcp_transport_crash=${mcp_transport_crashed}). no-op/실패 실행을 실패로 승격합니다." >> "$STDERR_LOG"
      exit_code=68
    fi
  fi

  local quota_exceeded=0
  if [[ "$exit_code" -ne 124 ]] \
    && { [[ "$exit_code" -ne 0 ]] || { [[ "$CLI_TYPE" == "antigravity" ]] && agy_output_is_quota_error "$STDOUT_LOG" "$STDERR_LOG"; }; } \
    && detect_quota_exceeded "$STDOUT_LOG" "$STDERR_LOG"; then
    quota_exceeded=1
    [[ "$exit_code" -ne 0 ]] || exit_code=1
    if [[ "$CLI_TYPE" == "antigravity" ]]; then
      record_agy_quota_reset "$STDOUT_LOG" "$STDERR_LOG" \
        || echo "[tfx-quota] agy 쿼터 리셋 시각 저장 실패" >&2
    fi
  fi

  # 쿼타 감지 + 자동 re-route
  if [[ "$quota_exceeded" == "1" ]]; then
    if [[ "${TFX_QUOTA_REROUTE:-1}" -ne 0 ]] && [[ -z "${TFX_REROUTED_FROM:-}" ]]; then
      export TFX_REROUTED_FROM="$CLI_TYPE"
      auto_reroute "$CLI_TYPE"
    fi
  fi

  # 팀 모드: task complete + 리드 보고
  if [[ -n "$TFX_TEAM_NAME" ]]; then
    if [[ "$exit_code" -eq 0 ]]; then
      local output_preview
      output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
      team_complete_task "success" "$output_preview"
    elif [[ "$exit_code" -eq 124 ]]; then
      team_complete_task "timeout" "하드실링/무활동 종료 (ceiling=${HARD_CEILING_SEC}s, expected=${TIMEOUT_SEC}s)"
    elif [[ "$exit_code" -eq 143 ]]; then
      team_complete_task "timeout" "외부 시그널로 종료 (SIGTERM, ceiling=${HARD_CEILING_SEC}s, expected=${TIMEOUT_SEC}s)"
    elif [[ "$exit_code" -eq 137 ]]; then
      team_complete_task "timeout" "외부 시그널로 종료 (SIGKILL, ceiling=${HARD_CEILING_SEC}s, expected=${TIMEOUT_SEC}s)"
    elif [[ "$exit_code" -eq 130 ]]; then
      team_complete_task "failed" "사용자 인터럽트 (SIGINT)"
    else
      local err_preview
      err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
      team_complete_task "failed" "exit_code=${exit_code}: ${err_preview}"
    fi
  fi

  # ── 후처리: 단일 node 프로세스로 위임 ──
  # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
  # 후처리기는 이 라우터와 같은 디렉터리의 것을 먼저 쓴다. 설치본 경로를 고정하면 라우터와 후처리기의
  # 버전이 어긋나 새 인자(--last-message-log 등)를 옛 후처리기가 무시한다. 설치본은 폴백이다.
  local _route_dir post_script
  _route_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
  post_script="${_route_dir}/tfx-route-post.mjs"
  if [[ ! -f "$post_script" ]]; then
    post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
  fi
  if [[ -f "$post_script" ]]; then
    node "$post_script" \
      --agent "$AGENT_TYPE" \
      --cli "$CLI_TYPE" \
      --cli-cmd "$CLI_CMD" \
      --effort "$CLI_EFFORT" \
      --exit-code "$exit_code" \
      --elapsed "$elapsed" \
      --timeout "$TIMEOUT_SEC" \
      --mcp-profile "$MCP_PROFILE" \
      --stderr-log "$STDERR_LOG" \
      --stdout-log "$STDOUT_LOG" \
      --last-message-log "$CODEX_LAST_MESSAGE_LOG" \
      --result-reason "$result_reason" \
      --rerouted-from "${TFX_REROUTED_FROM:-}" \
      --max-bytes "$MAX_STDOUT_BYTES" \
      --tee-active "$use_tee" \
      --clean-tui "${TFX_CLEAN_TUI:-true}"
  else
    echo "[tfx-route] ERROR: 후처리기를 찾지 못했습니다: $post_script" >&2
    return 1
  fi

  return "$exit_code"
}

# ── Async 모드: 백그라운드 실행 + 즉시 job_id 반환 ──
if [[ "$TFX_ASYNC_MODE" -eq 1 ]]; then
  mkdir -p "$TFX_JOBS_DIR"
  JOB_ID="$TIMESTAMP-$$-${RANDOM}"
  JOB_DIR="$TFX_JOBS_DIR/$JOB_ID"
  mkdir -p "$JOB_DIR"
  echo "$AGENT_TYPE" > "$JOB_DIR/agent_type"
  date +%s > "$JOB_DIR/start_time"

  # 백그라운드 서브쉘: main 실행 → 반환 코드로 마커 기록
  # H1' 수정 (Track 3 v2): POSIX 2017 wait spec 은 subshell 환경에서
  # wait 가 즉시 리턴하도록 강제하므로 기존 데몬 블록 (wait $bg_pid; touch done)
  # 은 main 시작 전에 done 마커를 찍는 dead code 였다. main() 이 자체 EXIT
  # trap 을 설치하므로 정상 종료는 straight-line write 로 기록하고, 시그널만 trap 한다.
  echo "starting" > "$JOB_DIR/pid"
  ( _run_async_job_body main ) &
  bg_pid=$!
  echo "$bg_pid" > "$JOB_DIR/pid"
  disown "$bg_pid"          # explicit PID — H1' fix (was missing arg, disowning daemon only)

  # 즉시 리턴: 1초 이내에 Claude Code Bash 도구 완료
  echo "$JOB_ID"
  exit 0
fi

main
