triflux 10.0.0 → 10.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/CLAUDE.md +171 -0
  2. package/README.md +32 -15
  3. package/bin/triflux.mjs +62 -5
  4. package/hooks/agent-route-guard.mjs +109 -0
  5. package/hooks/cross-review-tracker.mjs +122 -0
  6. package/hooks/error-context.mjs +148 -0
  7. package/hooks/hook-adaptive-collector.mjs +86 -0
  8. package/hooks/hook-manager.mjs +365 -0
  9. package/hooks/hook-orchestrator.mjs +312 -0
  10. package/hooks/hook-registry.json +246 -0
  11. package/hooks/hooks.json +89 -0
  12. package/hooks/keyword-rules.json +574 -0
  13. package/hooks/lib/resolve-root.mjs +59 -0
  14. package/hooks/mcp-config-watcher.mjs +80 -0
  15. package/hooks/pipeline-stop.mjs +76 -0
  16. package/hooks/safety-guard.mjs +169 -0
  17. package/hooks/subagent-verifier.mjs +80 -0
  18. package/hub/account-broker.mjs +251 -0
  19. package/hub/adaptive-diagnostic.mjs +323 -0
  20. package/hub/adaptive-inject.mjs +186 -0
  21. package/hub/adaptive-memory.mjs +163 -0
  22. package/hub/adaptive.mjs +143 -0
  23. package/hub/assign-callbacks.mjs +133 -0
  24. package/hub/bridge.mjs +799 -0
  25. package/hub/cli-adapter-base.mjs +280 -0
  26. package/hub/codex-adapter.mjs +199 -0
  27. package/hub/codex-compat.mjs +11 -0
  28. package/hub/codex-preflight.mjs +166 -0
  29. package/hub/delegator/contracts.mjs +37 -0
  30. package/hub/delegator/index.mjs +14 -0
  31. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  32. package/hub/delegator/service.mjs +307 -0
  33. package/hub/delegator/tool-definitions.mjs +35 -0
  34. package/hub/fullcycle.mjs +96 -0
  35. package/hub/gemini-adapter.mjs +180 -0
  36. package/hub/hitl.mjs +143 -0
  37. package/hub/intent.mjs +193 -0
  38. package/hub/lib/cache-guard.mjs +114 -0
  39. package/hub/lib/known-errors.json +72 -0
  40. package/hub/lib/memory-store.mjs +748 -0
  41. package/hub/lib/process-utils.mjs +361 -0
  42. package/hub/lib/ssh-command.mjs +211 -0
  43. package/hub/lib/ssh-retry.mjs +59 -0
  44. package/hub/lib/uuidv7.mjs +44 -0
  45. package/hub/memory-doctor.mjs +480 -0
  46. package/hub/middleware/request-logger.mjs +161 -0
  47. package/hub/paths.mjs +30 -0
  48. package/hub/pipe.mjs +664 -0
  49. package/hub/pipeline/gates/confidence.mjs +56 -0
  50. package/hub/pipeline/gates/consensus.mjs +94 -0
  51. package/hub/pipeline/gates/index.mjs +5 -0
  52. package/hub/pipeline/gates/selfcheck.mjs +82 -0
  53. package/hub/pipeline/index.mjs +318 -0
  54. package/hub/pipeline/state.mjs +191 -0
  55. package/hub/pipeline/transitions.mjs +124 -0
  56. package/hub/platform.mjs +225 -0
  57. package/hub/public/dashboard.html +355 -0
  58. package/hub/public/tray-icon.ico +0 -0
  59. package/hub/public/tray-icon.png +0 -0
  60. package/hub/quality/deslop.mjs +253 -0
  61. package/hub/reflexion.mjs +372 -0
  62. package/hub/research.mjs +146 -0
  63. package/hub/router.mjs +791 -0
  64. package/hub/routing/complexity.mjs +166 -0
  65. package/hub/routing/index.mjs +117 -0
  66. package/hub/routing/q-learning.mjs +336 -0
  67. package/hub/schema.sql +148 -0
  68. package/hub/server.mjs +1264 -0
  69. package/hub/session-fingerprint.mjs +352 -0
  70. package/hub/state.mjs +258 -0
  71. package/hub/store-adapter.mjs +118 -0
  72. package/hub/store.mjs +857 -0
  73. package/hub/team/agent-map.json +11 -0
  74. package/hub/team/ansi.mjs +379 -0
  75. package/hub/team/backend.mjs +90 -0
  76. package/hub/team/cli/commands/attach.mjs +37 -0
  77. package/hub/team/cli/commands/control.mjs +43 -0
  78. package/hub/team/cli/commands/debug.mjs +74 -0
  79. package/hub/team/cli/commands/focus.mjs +53 -0
  80. package/hub/team/cli/commands/interrupt.mjs +36 -0
  81. package/hub/team/cli/commands/kill.mjs +37 -0
  82. package/hub/team/cli/commands/list.mjs +24 -0
  83. package/hub/team/cli/commands/send.mjs +37 -0
  84. package/hub/team/cli/commands/start/index.mjs +106 -0
  85. package/hub/team/cli/commands/start/parse-args.mjs +130 -0
  86. package/hub/team/cli/commands/start/start-headless.mjs +109 -0
  87. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  88. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  89. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  90. package/hub/team/cli/commands/status.mjs +87 -0
  91. package/hub/team/cli/commands/stop.mjs +31 -0
  92. package/hub/team/cli/commands/task.mjs +30 -0
  93. package/hub/team/cli/commands/tasks.mjs +13 -0
  94. package/hub/team/cli/help.mjs +42 -0
  95. package/hub/team/cli/index.mjs +41 -0
  96. package/hub/team/cli/manifest.mjs +29 -0
  97. package/hub/team/cli/render.mjs +30 -0
  98. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  99. package/hub/team/cli/services/hub-client.mjs +227 -0
  100. package/hub/team/cli/services/member-selector.mjs +30 -0
  101. package/hub/team/cli/services/native-control.mjs +117 -0
  102. package/hub/team/cli/services/runtime-mode.mjs +62 -0
  103. package/hub/team/cli/services/state-store.mjs +48 -0
  104. package/hub/team/cli/services/task-model.mjs +30 -0
  105. package/hub/team/conductor-mesh-bridge.mjs +121 -0
  106. package/hub/team/conductor.mjs +671 -0
  107. package/hub/team/dashboard-anchor.mjs +14 -0
  108. package/hub/team/dashboard-layout.mjs +33 -0
  109. package/hub/team/dashboard-open.mjs +153 -0
  110. package/hub/team/dashboard.mjs +274 -0
  111. package/hub/team/event-log.mjs +76 -0
  112. package/hub/team/handoff.mjs +303 -0
  113. package/hub/team/headless.mjs +1156 -0
  114. package/hub/team/health-probe.mjs +272 -0
  115. package/hub/team/launcher-template.mjs +95 -0
  116. package/hub/team/lead-control.mjs +104 -0
  117. package/hub/team/native-supervisor.mjs +392 -0
  118. package/hub/team/native.mjs +649 -0
  119. package/hub/team/nativeProxy.mjs +688 -0
  120. package/hub/team/notify.mjs +293 -0
  121. package/hub/team/orchestrator.mjs +161 -0
  122. package/hub/team/pane.mjs +153 -0
  123. package/hub/team/process-cleanup.mjs +342 -0
  124. package/hub/team/psmux.mjs +1354 -0
  125. package/hub/team/remote-probe.mjs +276 -0
  126. package/hub/team/remote-session.mjs +299 -0
  127. package/hub/team/remote-watcher.mjs +478 -0
  128. package/hub/team/routing.mjs +223 -0
  129. package/hub/team/session-sync.mjs +169 -0
  130. package/hub/team/session.mjs +611 -0
  131. package/hub/team/shared.mjs +13 -0
  132. package/hub/team/staleState.mjs +361 -0
  133. package/hub/team/swarm-hypervisor.mjs +589 -0
  134. package/hub/team/swarm-locks.mjs +204 -0
  135. package/hub/team/swarm-planner.mjs +260 -0
  136. package/hub/team/swarm-reconciler.mjs +137 -0
  137. package/hub/team/tui-lite.mjs +380 -0
  138. package/hub/team/tui-remote-adapter.mjs +393 -0
  139. package/hub/team/tui-viewer.mjs +463 -0
  140. package/hub/team/tui.mjs +1449 -0
  141. package/hub/team/worktree-lifecycle.mjs +193 -0
  142. package/hub/team/wt-manager.mjs +407 -0
  143. package/hub/team/wt-templates.json +43 -0
  144. package/hub/team-bridge.mjs +27 -0
  145. package/hub/token-mode.mjs +224 -0
  146. package/hub/tools.mjs +636 -0
  147. package/hub/tray.mjs +376 -0
  148. package/hub/workers/claude-worker.mjs +475 -0
  149. package/hub/workers/codex-mcp.mjs +507 -0
  150. package/hub/workers/delegator-mcp.mjs +1076 -0
  151. package/hub/workers/factory.mjs +21 -0
  152. package/hub/workers/gemini-worker.mjs +374 -0
  153. package/hub/workers/interface.mjs +52 -0
  154. package/hub/workers/worker-utils.mjs +104 -0
  155. package/hud/colors.mjs +88 -0
  156. package/hud/constants.mjs +88 -0
  157. package/hud/context-monitor.mjs +403 -0
  158. package/hud/hud-qos-status.mjs +210 -0
  159. package/hud/providers/claude.mjs +314 -0
  160. package/hud/providers/codex.mjs +151 -0
  161. package/hud/providers/gemini.mjs +320 -0
  162. package/hud/renderers.mjs +442 -0
  163. package/hud/terminal.mjs +140 -0
  164. package/hud/utils.mjs +313 -0
  165. package/mesh/index.mjs +63 -0
  166. package/mesh/mesh-budget.mjs +128 -0
  167. package/mesh/mesh-heartbeat.mjs +100 -0
  168. package/mesh/mesh-protocol.mjs +96 -0
  169. package/mesh/mesh-queue.mjs +165 -0
  170. package/mesh/mesh-registry.mjs +78 -0
  171. package/mesh/mesh-router.mjs +76 -0
  172. package/package.json +8 -1
  173. package/references/hosts.json +33 -0
  174. package/scripts/__tests__/gen-skill-docs.test.mjs +87 -0
  175. package/scripts/__tests__/keyword-detector.test.mjs +234 -0
  176. package/scripts/__tests__/mcp-guard-engine.test.mjs +118 -0
  177. package/scripts/__tests__/remote-spawn-transfer.test.mjs +117 -0
  178. package/scripts/__tests__/remote-spawn.test.mjs +92 -0
  179. package/scripts/__tests__/skill-template.test.mjs +193 -0
  180. package/scripts/__tests__/smoke.test.mjs +34 -0
  181. package/scripts/cache-buildup.mjs +30 -0
  182. package/scripts/cache-doctor.mjs +149 -0
  183. package/scripts/cache-warmup.mjs +557 -0
  184. package/scripts/claudemd-sync.mjs +148 -0
  185. package/scripts/cli-route.sh +3 -0
  186. package/scripts/completions/tfx.bash +47 -0
  187. package/scripts/completions/tfx.fish +44 -0
  188. package/scripts/completions/tfx.zsh +83 -0
  189. package/scripts/cross-review-gate.mjs +126 -0
  190. package/scripts/cross-review-tracker.mjs +238 -0
  191. package/scripts/gen-skill-docs.mjs +111 -0
  192. package/scripts/headless-guard-fast.sh +21 -0
  193. package/scripts/headless-guard.mjs +360 -0
  194. package/scripts/hub-ensure.mjs +120 -0
  195. package/scripts/keyword-detector.mjs +272 -0
  196. package/scripts/keyword-rules-expander.mjs +521 -0
  197. package/scripts/lib/claudemd-scanner.mjs +218 -0
  198. package/scripts/lib/context.mjs +67 -0
  199. package/scripts/lib/cross-review-utils.mjs +51 -0
  200. package/scripts/lib/env-probe.mjs +241 -0
  201. package/scripts/lib/gemini-profiles.mjs +85 -0
  202. package/scripts/lib/handoff.mjs +171 -0
  203. package/scripts/lib/hook-utils.mjs +14 -0
  204. package/scripts/lib/keyword-rules.mjs +166 -0
  205. package/scripts/lib/logger.mjs +105 -0
  206. package/scripts/lib/mcp-filter.mjs +739 -0
  207. package/scripts/lib/mcp-guard-engine.mjs +954 -0
  208. package/scripts/lib/mcp-manifest.mjs +79 -0
  209. package/scripts/lib/mcp-server-catalog.mjs +118 -0
  210. package/scripts/lib/psmux-info.mjs +119 -0
  211. package/scripts/lib/remote-spawn-transfer.mjs +196 -0
  212. package/scripts/lib/skill-template.mjs +326 -0
  213. package/scripts/mcp-check.mjs +237 -0
  214. package/scripts/mcp-cleanup.ps1 +17 -0
  215. package/scripts/mcp-gateway-config.mjs +207 -0
  216. package/scripts/mcp-gateway-ensure.mjs +85 -0
  217. package/scripts/mcp-gateway-integration-test.mjs +228 -0
  218. package/scripts/mcp-gateway-start.mjs +226 -0
  219. package/scripts/mcp-gateway-start.ps1 +141 -0
  220. package/scripts/mcp-gateway-verify.mjs +77 -0
  221. package/scripts/mcp-safety-guard.mjs +44 -0
  222. package/scripts/notion-read.mjs +556 -0
  223. package/scripts/pack.mjs +295 -0
  224. package/scripts/preflight-cache.mjs +69 -0
  225. package/scripts/preinstall.mjs +96 -0
  226. package/scripts/remote-spawn.mjs +1376 -0
  227. package/scripts/run.cjs +79 -0
  228. package/scripts/session-spawn-helper.mjs +185 -0
  229. package/scripts/setup.mjs +1178 -0
  230. package/scripts/test-lock.mjs +71 -0
  231. package/scripts/test-tfx-route-no-claude-native.mjs +57 -0
  232. package/scripts/tfx-batch-stats.mjs +96 -0
  233. package/scripts/tfx-gate-activate.mjs +89 -0
  234. package/scripts/tfx-route-post.mjs +505 -0
  235. package/scripts/tfx-route-worker.mjs +223 -0
  236. package/scripts/tfx-route.sh +2014 -0
  237. package/scripts/tmp-cleanup.mjs +103 -0
  238. package/scripts/token-snapshot.mjs +575 -0
  239. package/skills/tfx-auto/SKILL.md.tmpl +2 -3
  240. package/skills/tfx-autoresearch/SKILL.md +6 -5
  241. package/skills/tfx-codex/SKILL.md.tmpl +2 -3
  242. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +33 -0
  243. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +42 -0
  244. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +11 -0
  245. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +87 -0
  246. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +35 -0
  247. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +275 -0
  248. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +56 -0
  249. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +5 -0
  250. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +11 -0
  251. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +92 -0
  252. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +71 -0
  253. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +264 -0
  254. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +113 -0
  255. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +5 -0
  256. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +32 -0
  257. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +9 -0
  258. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +96 -0
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +38 -0
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +151 -0
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +51 -0
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +5 -0
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +9 -0
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +127 -0
  265. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +57 -0
  266. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +129 -0
  267. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +84 -0
  268. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +5 -0
  269. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +27 -0
  270. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +8 -0
  271. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +98 -0
  272. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +65 -0
  273. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +123 -0
  274. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +66 -0
  275. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +5 -0
  276. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +8 -0
  277. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +88 -0
  278. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +40 -0
  279. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +130 -0
  280. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +61 -0
  281. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +5 -0
  282. package/skills/tfx-deep-interview/SKILL.md +1 -2
  283. package/skills/tfx-plan/SKILL.md.tmpl +2 -3
  284. package/skills/tfx-psmux-rules/SKILL.md +11 -2
  285. package/skills/tfx-qa/SKILL.md.tmpl +2 -3
  286. package/skills/tfx-remote-spawn/SKILL.md +8 -11
  287. package/skills/tfx-research/SKILL.md.tmpl +2 -3
  288. package/skills/tfx-review/SKILL.md.tmpl +2 -3
  289. package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
  290. package/skills/tfx-workspace/evals/evals.json +79 -0
  291. package/skills/tfx-workspace/iteration-1/benchmark.json +162 -0
  292. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
  293. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +9 -0
  294. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
  295. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
  296. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +9 -0
  297. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
  298. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
  299. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
  300. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +9 -0
  301. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
  302. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
  303. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +9 -0
  304. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
  305. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
  306. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
  307. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +8 -0
  308. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
  309. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
  310. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +8 -0
  311. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
  312. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
  313. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
  314. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +10 -0
  315. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
  316. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
  317. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +10 -0
  318. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
  319. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
  320. package/skills/tfx-workspace/iteration-1/review.html +1325 -0
  321. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
  322. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +10 -0
  323. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
  324. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
  325. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +10 -0
  326. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
  327. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
  328. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
  329. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +10 -0
  330. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
  331. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
  332. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +10 -0
  333. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
  334. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
  335. package/skills/tfx-workspace/iteration-2/benchmark.json +62 -0
  336. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
  337. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +11 -0
  338. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
  339. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
  340. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +11 -0
  341. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
  342. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
  343. package/skills/tfx-workspace/iteration-2/review.html +1325 -0
  344. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
  345. package/skills/{tfx-auto-codex/SKILL.md.tmpl → tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md} +3 -31
  346. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
  347. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
  348. package/skills/{tfx-gemini/SKILL.md.tmpl → tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md} +6 -14
  349. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
  350. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
  351. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
  352. package/skills/merge-worktree/SKILL.md.tmpl +0 -144
  353. package/skills/shared/arguments-processing.md +0 -2
  354. package/skills/shared/mandatory-rules.md +0 -6
  355. package/skills/shared/telemetry-segment.md +0 -6
  356. package/skills/star-prompt/SKILL.md.tmpl +0 -122
  357. package/skills/tfx-analysis/SKILL.md.tmpl +0 -106
  358. package/skills/tfx-analysis/skill.json +0 -11
  359. package/skills/tfx-auto/skill.json +0 -26
  360. package/skills/tfx-auto-codex/skill.json +0 -8
  361. package/skills/tfx-autopilot/SKILL.md.tmpl +0 -115
  362. package/skills/tfx-autopilot/skill.json +0 -10
  363. package/skills/tfx-autoresearch/SKILL.md.tmpl +0 -135
  364. package/skills/tfx-autoresearch/skill.json +0 -14
  365. package/skills/tfx-autoroute/SKILL.md.tmpl +0 -188
  366. package/skills/tfx-autoroute/skill.json +0 -12
  367. package/skills/tfx-codex/skill.json +0 -8
  368. package/skills/tfx-codex-swarm/SKILL.md.tmpl +0 -16
  369. package/skills/tfx-codex-swarm/skill.json +0 -5
  370. package/skills/tfx-consensus/SKILL.md.tmpl +0 -145
  371. package/skills/tfx-consensus/skill.json +0 -8
  372. package/skills/tfx-debate/SKILL.md.tmpl +0 -191
  373. package/skills/tfx-debate/skill.json +0 -12
  374. package/skills/tfx-deep-analysis/SKILL.md.tmpl +0 -227
  375. package/skills/tfx-deep-analysis/skill.json +0 -10
  376. package/skills/tfx-deep-interview/SKILL.md.tmpl +0 -203
  377. package/skills/tfx-deep-interview/skill.json +0 -12
  378. package/skills/tfx-deep-plan/SKILL.md.tmpl +0 -281
  379. package/skills/tfx-deep-plan/skill.json +0 -13
  380. package/skills/tfx-deep-qa/SKILL.md.tmpl +0 -164
  381. package/skills/tfx-deep-qa/skill.json +0 -11
  382. package/skills/tfx-deep-research/SKILL.md.tmpl +0 -216
  383. package/skills/tfx-deep-research/skill.json +0 -14
  384. package/skills/tfx-deep-review/SKILL.md.tmpl +0 -178
  385. package/skills/tfx-deep-review/skill.json +0 -12
  386. package/skills/tfx-doctor/SKILL.md.tmpl +0 -172
  387. package/skills/tfx-doctor/skill.json +0 -8
  388. package/skills/tfx-find/skill.json +0 -12
  389. package/skills/tfx-forge/SKILL.md.tmpl +0 -187
  390. package/skills/tfx-forge/skill.json +0 -12
  391. package/skills/tfx-fullcycle/SKILL.md.tmpl +0 -285
  392. package/skills/tfx-fullcycle/skill.json +0 -11
  393. package/skills/tfx-gemini/skill.json +0 -8
  394. package/skills/tfx-hooks/SKILL.md.tmpl +0 -216
  395. package/skills/tfx-hooks/skill.json +0 -8
  396. package/skills/tfx-hub/SKILL.md.tmpl +0 -212
  397. package/skills/tfx-hub/skill.json +0 -8
  398. package/skills/tfx-index/skill.json +0 -11
  399. package/skills/tfx-interview/SKILL.md.tmpl +0 -284
  400. package/skills/tfx-interview/skill.json +0 -12
  401. package/skills/tfx-multi/SKILL.md.tmpl +0 -183
  402. package/skills/tfx-multi/skill.json +0 -8
  403. package/skills/tfx-panel/SKILL.md.tmpl +0 -188
  404. package/skills/tfx-panel/skill.json +0 -12
  405. package/skills/tfx-persist/SKILL.md.tmpl +0 -269
  406. package/skills/tfx-persist/skill.json +0 -12
  407. package/skills/tfx-plan/skill.json +0 -11
  408. package/skills/tfx-profile/SKILL.md.tmpl +0 -239
  409. package/skills/tfx-profile/skill.json +0 -8
  410. package/skills/tfx-prune/SKILL.md.tmpl +0 -199
  411. package/skills/tfx-prune/skill.json +0 -12
  412. package/skills/tfx-psmux-rules/SKILL.md.tmpl +0 -317
  413. package/skills/tfx-psmux-rules/skill.json +0 -8
  414. package/skills/tfx-qa/skill.json +0 -11
  415. package/skills/tfx-ralph/SKILL.md.tmpl +0 -27
  416. package/skills/tfx-ralph/skill.json +0 -8
  417. package/skills/tfx-remote-setup/SKILL.md.tmpl +0 -576
  418. package/skills/tfx-remote-setup/skill.json +0 -8
  419. package/skills/tfx-remote-spawn/SKILL.md.tmpl +0 -263
  420. package/skills/tfx-remote-spawn/skill.json +0 -9
  421. package/skills/tfx-research/skill.json +0 -13
  422. package/skills/tfx-review/skill.json +0 -11
  423. package/skills/tfx-setup/SKILL.md.tmpl +0 -380
  424. package/skills/tfx-setup/skill.json +0 -8
  425. package/skills/tfx-swarm/SKILL.md.tmpl +0 -154
  426. package/skills/tfx-swarm/skill.json +0 -5
@@ -0,0 +1,2014 @@
1
+ #!/usr/bin/env bash
2
+ # tfx-route.sh v2.4 — CLI 라우팅 래퍼 (triflux)
3
+ #
4
+ # v1.x: cli-route.sh (jq+python3+node 혼재, 동기 후처리 ~1s)
5
+ # v2.0: tfx-route.sh 리네임
6
+ # - 후처리 전부 tfx-route-post.mjs로 이관 (node 단일 ~100ms)
7
+ # - per-process 에이전트 등록 (race condition 구조적 제거)
8
+ # - get_mcp_hint 통합 (캐시/비캐시 단일 코드경로)
9
+ # - Gemini health check 지수 백오프 (30×1s → 5×exp)
10
+ # - 컨텍스트 파일 5번째 인자 지원
11
+ #
12
+ VERSION="2.5"
13
+ #
14
+ # 사용법:
15
+ # tfx-route.sh <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
16
+ # tfx-route.sh --async <agent_type> <prompt> [mcp_profile] [timeout_sec] [context_file]
17
+ # tfx-route.sh --job-status <job_id>
18
+ # tfx-route.sh --job-result <job_id>
19
+ #
20
+ # --async: 백그라운드 실행, 즉시 job_id 반환 (Claude Code Bash 600초 제한 우회)
21
+ # --job-status: running | done | timeout | failed
22
+ # --job-result: 완료된 잡의 전체 출력
23
+ #
24
+ # 예시:
25
+ # tfx-route.sh executor "코드 구현" implement
26
+ # tfx-route.sh --async scientist "딥 리서치" auto 1440
27
+ # tfx-route.sh --job-status 1742400000-12345-9876
28
+ # tfx-route.sh --job-result 1742400000-12345-9876
29
+
30
+ set -euo pipefail
31
+
32
+ # ── timeout 명령 호환성 — Windows에서 TIMEOUT.exe 대신 Git Bash coreutils timeout 사용 ──
33
+ if command -v /usr/bin/timeout >/dev/null 2>&1; then
34
+ TIMEOUT_BIN="/usr/bin/timeout"
35
+ elif command -v gtimeout >/dev/null 2>&1; then
36
+ TIMEOUT_BIN="gtimeout" # macOS homebrew
37
+ else
38
+ TIMEOUT_BIN="timeout" # Linux 기본
39
+ fi
40
+
41
+ # ── 임시 디렉토리 정규화 ──
42
+ resolve_tmp_dir() {
43
+ local candidate=""
44
+ for candidate in "${TMPDIR:-}" "${TEMP:-}" "${TMP:-}" "/tmp"; do
45
+ [[ -n "$candidate" ]] || continue
46
+ if mkdir -p "$candidate" >/dev/null 2>&1; then
47
+ printf '%s\n' "$candidate"
48
+ return 0
49
+ fi
50
+ done
51
+
52
+ candidate="$(pwd)/.tfx-tmp"
53
+ mkdir -p "$candidate" >/dev/null 2>&1 || true
54
+ printf '%s\n' "$candidate"
55
+ }
56
+
57
+ TFX_TMP="$(resolve_tmp_dir)"
58
+
59
+ # ── config.toml sandbox/approval_mode 감지 ──
60
+ # config.toml에 이미 설정되어 있으면 CLI 플래그 중복 시 Codex가 에러를 던짐
61
+ _CODEX_CONFIG="${HOME}/.codex/config.toml"
62
+ _CODEX_HAS_SANDBOX=""
63
+ if [[ -f "$_CODEX_CONFIG" ]] && grep -qE '^\s*(sandbox|approval_mode)\s*=' "$_CODEX_CONFIG" 2>/dev/null; then
64
+ _CODEX_HAS_SANDBOX="1"
65
+ fi
66
+
67
+ build_codex_base() {
68
+ if [[ -n "$_CODEX_HAS_SANDBOX" ]]; then
69
+ echo "--skip-git-repo-check"
70
+ else
71
+ echo "--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
72
+ fi
73
+ }
74
+
75
+ # ── Async Job 디렉토리 ──
76
+ TFX_JOBS_DIR="${TFX_TMP}/tfx-jobs"
77
+
78
+ # ── --job-status / --job-result 핸들러 (인자 파싱 전에 처리) ──
79
+ if [[ "${1:-}" == "--job-status" ]]; then
80
+ job_id="${2:?job_id 필수}"
81
+ job_dir="$TFX_JOBS_DIR/$job_id"
82
+ [[ -d "$job_dir" ]] || { echo "error: job not found"; exit 1; }
83
+
84
+ if [[ -f "$job_dir/done" ]]; then
85
+ exit_code=$(cat "$job_dir/exit_code" 2>/dev/null || echo 1)
86
+ if [[ "$exit_code" -eq 0 ]]; then
87
+ echo "done"
88
+ elif [[ "$exit_code" -eq 124 ]]; then
89
+ echo "timeout"
90
+ else
91
+ echo "failed"
92
+ fi
93
+ elif [[ -f "$job_dir/pid" ]]; then
94
+ pid=$(cat "$job_dir/pid")
95
+ if [[ "$pid" == "starting" ]]; then
96
+ echo "starting"
97
+ exit 0
98
+ fi
99
+ if kill -0 "$pid" 2>/dev/null; then
100
+ # 진행 상황 힌트
101
+ local_bytes=$(wc -c < "$job_dir/result.log" 2>/dev/null | tr -d ' ' || echo 0)
102
+ elapsed=$(( $(date +%s) - $(cat "$job_dir/start_time" 2>/dev/null || date +%s) ))
103
+ echo "running elapsed=${elapsed}s output=${local_bytes}B"
104
+ else
105
+ # 프로세스 종료됐는데 done 마커 없음 → 비정상 종료
106
+ echo "failed"
107
+ fi
108
+ else
109
+ echo "error: invalid job state"
110
+ exit 1
111
+ fi
112
+ exit 0
113
+ fi
114
+
115
+ if [[ "${1:-}" == "--job-result" ]]; then
116
+ job_id="${2:?job_id 필수}"
117
+ job_dir="$TFX_JOBS_DIR/$job_id"
118
+ [[ -d "$job_dir" ]] || { echo "error: job not found"; exit 1; }
119
+ [[ -f "$job_dir/done" ]] || { echo "error: job still running"; exit 1; }
120
+
121
+ result_bytes=$(wc -c < "$job_dir/result.log" 2>/dev/null | tr -d ' ' || echo 0)
122
+ if [[ "$result_bytes" -eq 0 ]] && [[ -s "$job_dir/stderr.log" ]]; then
123
+ cat "$job_dir/stderr.log" 2>/dev/null
124
+ else
125
+ cat "$job_dir/result.log" 2>/dev/null
126
+ fi
127
+ exit_code=$(cat "$job_dir/exit_code" 2>/dev/null || echo 1)
128
+ exit "$exit_code"
129
+ fi
130
+
131
+ # ── --job-wait: 내부 폴링으로 완료 대기 (Bash 도구 호출 횟수 최소화) ──
132
+ # 사용법: tfx-route.sh --job-wait <job_id> [max_seconds=540]
133
+ # 출력: 주기적 "waiting elapsed=Ns" + 최종 "done"|"timeout"|"failed"|"still_running"
134
+ if [[ "${1:-}" == "--job-wait" ]]; then
135
+ job_id="${2:?job_id 필수}"
136
+ max_wait="${3:-540}" # 기본 540초 (9분, Bash 도구 600초 제한 이내)
137
+ poll_interval=15
138
+ job_dir="$TFX_JOBS_DIR/$job_id"
139
+ [[ -d "$job_dir" ]] || { echo "error: job not found"; exit 1; }
140
+
141
+ elapsed=0
142
+ while [[ "$elapsed" -lt "$max_wait" ]]; do
143
+ if [[ -f "$job_dir/done" ]]; then
144
+ ec=$(cat "$job_dir/exit_code" 2>/dev/null || echo 1)
145
+ if [[ "$ec" -eq 0 ]]; then echo "done"
146
+ elif [[ "$ec" -eq 124 ]]; then echo "timeout"
147
+ else echo "failed (exit=$ec)"
148
+ fi
149
+ exit 0
150
+ fi
151
+ sleep "$poll_interval"
152
+ elapsed=$((elapsed + poll_interval))
153
+ stderr_bytes=$(wc -c < "$job_dir/stderr.log" 2>/dev/null || echo 0)
154
+ echo "waiting elapsed=${elapsed}s progress=${stderr_bytes}B"
155
+ done
156
+
157
+ # max_wait 도달했지만 아직 실행 중
158
+ echo "still_running elapsed=${elapsed}s"
159
+ exit 0
160
+ fi
161
+
162
+ # ── --async 플래그 감지 ──
163
+ TFX_ASYNC_MODE=0
164
+ if [[ "${1:-}" == "--async" ]]; then
165
+ TFX_ASYNC_MODE=1
166
+ shift
167
+ fi
168
+
169
+ # ── 인자 파싱 ──
170
+ AGENT_TYPE="${1:?에이전트 타입 필수 (executor, debugger, designer 등)}"
171
+ PROMPT="${2:?프롬프트 필수}"
172
+ MCP_PROFILE="${3:-auto}"
173
+ USER_TIMEOUT="${4:-}"
174
+ CONTEXT_FILE="${5:-}"
175
+
176
+ # ── CLI 이름은 route_agent()에서 기본 역할 alias로 처리됨 (codex→executor, gemini→designer, claude→explore) ──
177
+
178
+ # ── 인자 검증: MCP_PROFILE이 --flag 형태인 경우 거절 ──
179
+ if [[ "$MCP_PROFILE" == --* ]]; then
180
+ echo "ERROR: MCP 프로필 위치(3번째 인자)에 플래그 '$MCP_PROFILE'가 들어왔습니다." >&2
181
+ echo "사용법: tfx-route.sh <역할> \"프롬프트\" [mcp_profile] [timeout]" >&2
182
+ echo "지원 프로필: auto, executor, analyze, implement, review, minimal, full" >&2
183
+ exit 64
184
+ fi
185
+
186
+ # ── CLI 경로 해석 (Windows npm global 대응) ──
187
+ NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
188
+ CODEX_BIN="${CODEX_BIN:-$(command -v codex 2>/dev/null || echo codex)}"
189
+ GEMINI_BIN="${GEMINI_BIN:-$(command -v gemini 2>/dev/null || echo gemini)}"
190
+ CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
191
+ GEMINI_BIN_ARGS_JSON="${GEMINI_BIN_ARGS_JSON:-[]}"
192
+ CLAUDE_BIN_ARGS_JSON="${CLAUDE_BIN_ARGS_JSON:-[]}"
193
+
194
+ # ── Gemini 프로필 경로 (Codex config.toml 대칭) ──
195
+ GEMINI_PROFILES_PATH="${GEMINI_PROFILES_PATH:-$(eval echo ~)/.gemini/triflux-profiles.json}"
196
+
197
+ # ── 상수 ──
198
+ MAX_STDOUT_BYTES=51200 # 50KB — Claude 컨텍스트 절약
199
+ TIMESTAMP=$(date +%s)
200
+ RUN_ID="${TIMESTAMP}-$$-${RANDOM}"
201
+ STDERR_LOG="${TFX_TMP}/tfx-route-${AGENT_TYPE}-${RUN_ID}-stderr.log"
202
+ STDOUT_LOG="${TFX_TMP}/tfx-route-${AGENT_TYPE}-${RUN_ID}-stdout.log"
203
+
204
+ # ── 팀 환경변수 ──
205
+ TFX_TEAM_NAME="${TFX_TEAM_NAME:-}"
206
+ TFX_TEAM_TASK_ID="${TFX_TEAM_TASK_ID:-}"
207
+ TFX_TEAM_AGENT_NAME="${TFX_TEAM_AGENT_NAME:-${AGENT_TYPE}-worker-$$}"
208
+ TFX_TEAM_LEAD_NAME="${TFX_TEAM_LEAD_NAME:-team-lead}"
209
+ TFX_HUB_PIPE="${TFX_HUB_PIPE:-}"
210
+ TFX_HUB_URL="${TFX_HUB_URL:-http://127.0.0.1:27888}" # bridge.mjs HTTP fallback hint
211
+
212
+ # ── 패키지 루트 해석 (setup.mjs가 기록한 breadcrumb) ──
213
+ TFX_PKG_ROOT=""
214
+ _tfx_breadcrumb="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.tfx-pkg-root"
215
+ if [[ -f "$_tfx_breadcrumb" ]]; then
216
+ TFX_PKG_ROOT="$(head -1 "$_tfx_breadcrumb" 2>/dev/null | tr -d '\r\n')"
217
+ fi
218
+ unset _tfx_breadcrumb
219
+
220
+ # fallback 시 원래 에이전트 정보 보존
221
+ ORIGINAL_AGENT=""
222
+ ORIGINAL_CLI_ARGS=""
223
+
224
+ # JSON 문자열 이스케이프:
225
+ # - "\", """ 필수 이스케이프
226
+ # - 제어문자 U+0000..U+001F 이스케이프
227
+ # - 비ASCII 문자는 \uXXXX(또는 surrogate pair)로 강제
228
+ json_escape() {
229
+ local s="${1:-}"
230
+
231
+ if command -v "$NODE_BIN" &>/dev/null; then
232
+ "$NODE_BIN" -e '
233
+ const input = process.argv[1] ?? "";
234
+ let out = "";
235
+ for (const ch of input) {
236
+ const cp = ch.codePointAt(0);
237
+ if (cp === 0x22) { out += "\\\""; continue; } // "
238
+ if (cp === 0x5c) { out += "\\\\"; continue; } // \
239
+ if (cp <= 0x1f) {
240
+ if (cp === 0x08) { out += "\\b"; continue; }
241
+ if (cp === 0x09) { out += "\\t"; continue; }
242
+ if (cp === 0x0a) { out += "\\n"; continue; }
243
+ if (cp === 0x0c) { out += "\\f"; continue; }
244
+ if (cp === 0x0d) { out += "\\r"; continue; }
245
+ out += `\\u${cp.toString(16).padStart(4, "0")}`;
246
+ continue;
247
+ }
248
+ if (cp >= 0x20 && cp <= 0x7e) {
249
+ out += ch;
250
+ continue;
251
+ }
252
+ if (cp <= 0xffff) {
253
+ out += `\\u${cp.toString(16).padStart(4, "0")}`;
254
+ continue;
255
+ }
256
+ const v = cp - 0x10000;
257
+ const hi = 0xd800 + (v >> 10);
258
+ const lo = 0xdc00 + (v & 0x3ff);
259
+ out += `\\u${hi.toString(16).padStart(4, "0")}\\u${lo.toString(16).padStart(4, "0")}`;
260
+ }
261
+ process.stdout.write(out);
262
+ ' -- "$s"
263
+ return
264
+ fi
265
+
266
+ echo "[tfx-route] ERROR: node 미설치로 안전한 JSON 이스케이프를 수행할 수 없습니다." >&2
267
+ return 1
268
+ }
269
+
270
+ # ── Per-process 에이전트 등록 (원자적, 락 불필요) ──
271
+ register_agent() {
272
+ local agent_file="${TFX_TMP}/tfx-agent-$$.json"
273
+ local safe_cli safe_agent started_at
274
+ safe_cli=$(json_escape "$CLI_TYPE" 2>/dev/null || true)
275
+ safe_agent=$(json_escape "$AGENT_TYPE" 2>/dev/null || true)
276
+ started_at=$(date +%s)
277
+
278
+ # fail-closed: 안전 인코딩 불가 시 agent 파일을 쓰지 않는다
279
+ if [[ -n "$CLI_TYPE" && -z "$safe_cli" ]]; then
280
+ return 0
281
+ fi
282
+ if [[ -n "$AGENT_TYPE" && -z "$safe_agent" ]]; then
283
+ return 0
284
+ fi
285
+
286
+ printf '{"pid":%s,"cli":"%s","agent":"%s","started":%s}\n' "$$" "$safe_cli" "$safe_agent" "$started_at" \
287
+ > "$agent_file" 2>/dev/null || true
288
+ }
289
+
290
+ deregister_agent() {
291
+ rm -f "${TFX_TMP}/tfx-agent-$$.json" 2>/dev/null || true
292
+ }
293
+
294
+ normalize_script_path() {
295
+ local path="${1:-}"
296
+ if [[ -z "$path" ]]; then
297
+ return 0
298
+ fi
299
+
300
+ if command -v cygpath &>/dev/null; then
301
+ case "$path" in
302
+ [A-Za-z]:\\*|[A-Za-z]:/*)
303
+ cygpath -u "$path"
304
+ return 0
305
+ ;;
306
+ esac
307
+ fi
308
+
309
+ printf '%s\n' "$path"
310
+ }
311
+
312
+ # ── 팀 Hub Bridge 통신 ──
313
+ resolve_bridge_script() {
314
+ if [[ -n "${TFX_BRIDGE_SCRIPT:-}" && -f "$TFX_BRIDGE_SCRIPT" ]]; then
315
+ printf '%s\n' "$TFX_BRIDGE_SCRIPT"
316
+ return 0
317
+ fi
318
+
319
+ local script_dir
320
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
321
+ local candidates=()
322
+ [[ -n "$TFX_PKG_ROOT" ]] && candidates+=("$TFX_PKG_ROOT/hub/bridge.mjs")
323
+ candidates+=(
324
+ "$script_dir/../hub/bridge.mjs"
325
+ "$script_dir/hub/bridge.mjs"
326
+ )
327
+
328
+ local candidate
329
+ for candidate in "${candidates[@]}"; do
330
+ if [[ -f "$candidate" ]]; then
331
+ printf '%s\n' "$candidate"
332
+ return 0
333
+ fi
334
+ done
335
+
336
+ return 1
337
+ }
338
+
339
+ bridge_cli() {
340
+ if ! command -v "$NODE_BIN" &>/dev/null; then
341
+ return 127
342
+ fi
343
+
344
+ local bridge_script
345
+ if ! bridge_script=$(resolve_bridge_script); then
346
+ return 127
347
+ fi
348
+
349
+ TFX_HUB_PIPE="$TFX_HUB_PIPE" TFX_HUB_URL="$TFX_HUB_URL" TFX_HUB_TOKEN="${TFX_HUB_TOKEN:-}" \
350
+ "$NODE_BIN" "$bridge_script" "$@" 2>/dev/null
351
+ }
352
+
353
+ bridge_json_get() {
354
+ local json="${1:-}"
355
+ local path="${2:-}"
356
+ [[ -z "$json" || -z "$path" ]] && return 1
357
+
358
+ "$NODE_BIN" -e '
359
+ const data = JSON.parse(process.argv[1] || "{}");
360
+ const keys = String(process.argv[2] || "").split(".").filter(Boolean);
361
+ let value = data;
362
+ for (const key of keys) value = value?.[key];
363
+ if (value === undefined || value === null) process.exit(1);
364
+ process.stdout.write(typeof value === "object" ? JSON.stringify(value) : String(value));
365
+ ' -- "$json" "$path" 2>/dev/null
366
+ }
367
+
368
+ bridge_json_stringify() {
369
+ local mode="${1:-}"
370
+ shift || true
371
+
372
+ case "$mode" in
373
+ metadata-patch)
374
+ "$NODE_BIN" -e '
375
+ process.stdout.write(JSON.stringify({
376
+ result: process.argv[1] || "",
377
+ summary: process.argv[2] || "",
378
+ }));
379
+ ' -- "${1:-}" "${2:-}"
380
+ ;;
381
+ task-result)
382
+ "$NODE_BIN" -e '
383
+ process.stdout.write(JSON.stringify({
384
+ task_id: process.argv[1] || "",
385
+ result: process.argv[2] || "",
386
+ }));
387
+ ' -- "${1:-}" "${2:-}"
388
+ ;;
389
+ *)
390
+ return 1
391
+ ;;
392
+ esac
393
+ }
394
+
395
+ team_send_message() {
396
+ local text="${1:-}"
397
+ local summary="${2:-}"
398
+ [[ -z "$TFX_TEAM_NAME" || -z "$text" ]] && return 0
399
+
400
+ if ! bridge_cli_with_restart "팀 메시지 전송" "Hub 재시작 후 팀 메시지 전송 성공." \
401
+ team-send-message \
402
+ --team "$TFX_TEAM_NAME" \
403
+ --from "$TFX_TEAM_AGENT_NAME" \
404
+ --to "$TFX_TEAM_LEAD_NAME" \
405
+ --text "$text" \
406
+ --summary "${summary:-status update}"; then
407
+ echo "[tfx-route] 경고: 팀 메시지 전송 실패 (team=$TFX_TEAM_NAME, to=$TFX_TEAM_LEAD_NAME)" >&2
408
+ return 0
409
+ fi
410
+
411
+ return 0
412
+ }
413
+
414
+ # ── Hub 자동 재시작 (슬립 복귀 등으로 Hub 종료 시) ──
415
+ try_restart_hub() {
416
+ local hub_server script_dir hub_port
417
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
418
+ hub_server=""
419
+ local _hub_candidates=()
420
+ [[ -n "$TFX_PKG_ROOT" ]] && _hub_candidates+=("$TFX_PKG_ROOT/hub/server.mjs")
421
+ _hub_candidates+=("$script_dir/../hub/server.mjs")
422
+ for _hc in "${_hub_candidates[@]}"; do
423
+ if [[ -f "$_hc" ]]; then hub_server="$_hc"; break; fi
424
+ done
425
+ unset _hub_candidates _hc
426
+
427
+ if [[ -z "$hub_server" ]]; then
428
+ echo "[tfx-route] Hub 서버 스크립트 미발견 (pkg_root=${TFX_PKG_ROOT:-unset}, script_dir=$script_dir)" >&2
429
+ return 1
430
+ fi
431
+
432
+ # TFX_HUB_URL에서 포트 추출 (기본 27888)
433
+ hub_port="${TFX_HUB_URL##*:}"
434
+ hub_port="${hub_port%%/*}"
435
+ [[ -z "$hub_port" || "$hub_port" == "$TFX_HUB_URL" ]] && hub_port=27888
436
+
437
+ echo "[tfx-route] Hub 미응답 — 자동 재시작 시도 (port=$hub_port)..." >&2
438
+ TFX_HUB_PORT="$hub_port" "$NODE_BIN" "$hub_server" &>/dev/null &
439
+ local hub_pid=$!
440
+
441
+ # 최대 4초 대기 (0.5초 간격)
442
+ local i
443
+ for i in 1 2 3 4 5 6 7 8; do
444
+ sleep 0.5
445
+ if curl -sf "${TFX_HUB_URL}/status" >/dev/null 2>&1; then
446
+ echo "[tfx-route] Hub 재시작 성공 (pid=$hub_pid)" >&2
447
+ return 0
448
+ fi
449
+ done
450
+
451
+ echo "[tfx-route] Hub 재시작 실패 — claim 없이 계속 실행" >&2
452
+ return 1
453
+ }
454
+
455
+ bridge_cli_with_restart() {
456
+ local action_label="${1:-bridge 호출}"
457
+ local success_message="${2:-}"
458
+ shift 2 || true
459
+
460
+ if bridge_cli "$@" >/dev/null 2>&1; then
461
+ return 0
462
+ fi
463
+
464
+ if ! try_restart_hub; then
465
+ return 1
466
+ fi
467
+
468
+ if bridge_cli "$@" >/dev/null 2>&1; then
469
+ [[ -n "$success_message" ]] && echo "[tfx-route] ${success_message}" >&2
470
+ return 0
471
+ fi
472
+
473
+ echo "[tfx-route] 경고: Hub 재시작 후 ${action_label} 재시도 실패." >&2
474
+ return 1
475
+ }
476
+
477
+ team_claim_task() {
478
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
479
+ local response ok error_code error_message owner_before status_before
480
+ response=$(bridge_cli team-task-update \
481
+ --team "$TFX_TEAM_NAME" \
482
+ --task-id "$TFX_TEAM_TASK_ID" \
483
+ --claim \
484
+ --owner "$TFX_TEAM_AGENT_NAME" \
485
+ --status in_progress || true)
486
+
487
+ ok=$(bridge_json_get "$response" "ok" || true)
488
+ error_code=$(bridge_json_get "$response" "error.code" || true)
489
+ error_message=$(bridge_json_get "$response" "error.message" || true)
490
+ owner_before=$(bridge_json_get "$response" "error.details.task_before.owner" || true)
491
+ status_before=$(bridge_json_get "$response" "error.details.task_before.status" || true)
492
+
493
+ case "$ok:$error_code" in
494
+ true:*) ;;
495
+ false:CLAIM_CONFLICT)
496
+ if [[ "$owner_before" == "$TFX_TEAM_AGENT_NAME" && "$status_before" == "in_progress" ]]; then
497
+ echo "[tfx-route] 동일 owner(${TFX_TEAM_AGENT_NAME})가 이미 claim한 task ${TFX_TEAM_TASK_ID} — 계속 실행." >&2
498
+ return 0
499
+ fi
500
+ echo "[tfx-route] CLAIM_CONFLICT: task ${TFX_TEAM_TASK_ID}가 이미 claim됨(owner=${owner_before:-unknown}, status=${status_before:-unknown}). 실행 중단." >&2
501
+ team_send_message \
502
+ "task ${TFX_TEAM_TASK_ID} claim conflict: owner=${owner_before:-unknown}, status=${status_before:-unknown}" \
503
+ "task ${TFX_TEAM_TASK_ID} claim conflict"
504
+ exit 0 ;;
505
+ :|false:)
506
+ # Hub 연결 실패 → 자동 재시작 시도 후 claim 재시도
507
+ if try_restart_hub; then
508
+ response=$(bridge_cli team-task-update \
509
+ --team "$TFX_TEAM_NAME" \
510
+ --task-id "$TFX_TEAM_TASK_ID" \
511
+ --claim \
512
+ --owner "$TFX_TEAM_AGENT_NAME" \
513
+ --status in_progress || true)
514
+ ok=$(bridge_json_get "$response" "ok" || true)
515
+ if [[ "$ok" == "true" ]]; then
516
+ echo "[tfx-route] Hub 재시작 후 claim 성공." >&2
517
+ else
518
+ echo "[tfx-route] 경고: Hub 재시작 후 claim 실패. claim 없이 계속 실행." >&2
519
+ fi
520
+ else
521
+ echo "[tfx-route] 경고: Hub 연결 실패 (미실행?). claim 없이 계속 실행." >&2
522
+ fi ;;
523
+ *)
524
+ echo "[tfx-route] 경고: Hub claim 실패 (${error_code:-unknown}${error_message:+: ${error_message}}). claim 없이 계속 실행." >&2 ;;
525
+ esac
526
+ }
527
+
528
+ team_complete_task() {
529
+ local result="${1:-success}" # success/failed/timeout
530
+ local result_summary="${2:-작업 완료}"
531
+ [[ -z "$TFX_TEAM_NAME" || -z "$TFX_TEAM_TASK_ID" ]] && return 0
532
+
533
+ local summary_trimmed result_payload
534
+ summary_trimmed=$(echo "$result_summary" | head -c 4096)
535
+ result_payload=$(bridge_json_stringify task-result "$TFX_TEAM_TASK_ID" "$result" 2>/dev/null || true)
536
+
537
+ # task 파일 completion 쓰기는 Worker Step 6 TaskUpdate가 authority다.
538
+ # route 레벨에서는 task.result 발행 + 로컬 backup만 유지한다.
539
+
540
+ # Hub result 발행 (poll_messages 채널 활성화)
541
+ if [[ -n "$result_payload" ]]; then
542
+ if ! bridge_cli_with_restart "Hub result 발행" "Hub 재시작 후 Hub result 발행 성공." \
543
+ result \
544
+ --agent "$TFX_TEAM_AGENT_NAME" \
545
+ --topic task.result \
546
+ --payload "$result_payload" \
547
+ --trace "$TFX_TEAM_NAME"; then
548
+ echo "[tfx-route] 경고: Hub result 발행 실패 (agent=$TFX_TEAM_AGENT_NAME, task=$TFX_TEAM_TASK_ID)" >&2
549
+ fi
550
+ fi
551
+
552
+ # 로컬 결과 파일 백업 (세션 끊김 복구용)
553
+ # Claude 재로그인 시 Agent 래퍼가 죽어도 이 파일로 결과 수집 가능
554
+ local result_dir="${TFX_RESULT_DIR:-${HOME}/.claude/tfx-results/${TFX_TEAM_NAME}}"
555
+ if mkdir -p "$result_dir" 2>/dev/null; then
556
+ cat > "${result_dir}/${TFX_TEAM_TASK_ID}.json" 2>/dev/null <<RESULT_EOF
557
+ {"taskId":"${TFX_TEAM_TASK_ID}","agent":"${TFX_TEAM_AGENT_NAME}","team":"${TFX_TEAM_NAME}","result":"${result}","summary":$(printf '%s' "$summary_trimmed" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.stringify(d)))" 2>/dev/null || echo '""'),"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
558
+ RESULT_EOF
559
+ [[ $? -eq 0 ]] && echo "[tfx-route] 결과 백업: ${result_dir}/${TFX_TEAM_TASK_ID}.json" >&2
560
+ fi
561
+ }
562
+
563
+ detect_quota_exceeded() {
564
+ local stdout_file="$1"
565
+ local stderr_file="$2"
566
+ local -a patterns=(
567
+ "usage limit exceeded" "rate limit exceeded" "rate limit reached"
568
+ "try again at" "purchase more credits"
569
+ "quota exceeded" "RESOURCE_EXHAUSTED" "rateLimitExceeded" "Too Many Requests"
570
+ "rate_limit_error" "overloaded_error" "insufficient_quota"
571
+ )
572
+ local pattern
573
+ for pattern in "${patterns[@]}"; do
574
+ if grep -qi "$pattern" "$stdout_file" 2>/dev/null || grep -qi "$pattern" "$stderr_file" 2>/dev/null; then
575
+ echo "[tfx-quota] 감지: '$pattern' in $CLI_TYPE" >&2
576
+ return 0
577
+ fi
578
+ done
579
+ return 1
580
+ }
581
+
582
+ auto_reroute() {
583
+ local failed_cli="$1"
584
+ local target_cli=""
585
+ case "$failed_cli" in
586
+ codex) target_cli="gemini"; echo "[tfx-quota] Codex → Gemini 자동 전환" >&2 ;;
587
+ gemini) target_cli="codex"; echo "[tfx-quota] Gemini → Codex 자동 전환" >&2 ;;
588
+ *) echo "[tfx-quota] $failed_cli 대체 CLI 없음" >&2; return 1 ;;
589
+ esac
590
+
591
+ # 대상 CLI 존재 확인 (P2: command not found 방지)
592
+ local target_bin
593
+ case "$target_cli" in
594
+ codex) target_bin="$CODEX_BIN" ;;
595
+ gemini) target_bin="$GEMINI_BIN" ;;
596
+ esac
597
+ if ! command -v "$target_bin" &>/dev/null; then
598
+ echo "[tfx-quota] $target_cli CLI 미설치 — 자동 전환 불가" >&2
599
+ return 1
600
+ fi
601
+
602
+ local quota_marker="$TFX_TMP/tfx-quota-${failed_cli}-$(date +%Y%m%d)"
603
+ echo "$(date +%s)" >> "$quota_marker"
604
+ ORIGINAL_AGENT="$AGENT_TYPE"
605
+ ORIGINAL_CLI_ARGS="$CLI_ARGS"
606
+ export TFX_REROUTED_FROM="$CLI_TYPE"
607
+ TFX_CLI_MODE="$target_cli" exec bash "${BASH_SOURCE[0]}" \
608
+ "$AGENT_TYPE" "$PROMPT" "$MCP_PROFILE" "$USER_TIMEOUT" "$CONTEXT_FILE"
609
+ }
610
+
611
+ capture_workspace_signature() {
612
+ if ! command -v git &>/dev/null; then
613
+ return 1
614
+ fi
615
+
616
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
617
+ return 1
618
+ fi
619
+
620
+ git status --short --untracked-files=all --ignore-submodules=all 2>/dev/null || return 1
621
+ }
622
+
623
+ # ── Codex CLI 버전 감지 (캐시) ──
624
+ _CODEX_VERSION=""
625
+ get_codex_version() {
626
+ if [[ -n "$_CODEX_VERSION" ]]; then echo "$_CODEX_VERSION"; return; fi
627
+ local raw
628
+ raw=$("$CODEX_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
629
+ _CODEX_VERSION="${raw:-0.0.0}"
630
+ echo "$_CODEX_VERSION"
631
+ }
632
+
633
+ # codex_gte <min_version>: 현재 버전이 min 이상이면 true(0), 아니면 false(1)
634
+ codex_gte() {
635
+ local min="$1"
636
+ local cur
637
+ cur=$(get_codex_version)
638
+ printf '%s\n%s' "$min" "$cur" | sort -V | head -1 | grep -q "^${min}$"
639
+ }
640
+
641
+ # ── Gemini 프로필 해석 (Codex --profile 대칭) ──
642
+ _GEMINI_PROFILE_CACHE=""
643
+ resolve_gemini_profile() {
644
+ local profile="$1"
645
+ if [[ "$profile" == gemini-* ]]; then
646
+ echo "$profile"
647
+ return
648
+ fi
649
+ if [[ -z "$_GEMINI_PROFILE_CACHE" && -f "$GEMINI_PROFILES_PATH" ]]; then
650
+ _GEMINI_PROFILE_CACHE=$(cat "$GEMINI_PROFILES_PATH" 2>/dev/null || echo "{}")
651
+ fi
652
+ local settings_path="${HOME}/.gemini/settings.json"
653
+ local settings_cache="{}"
654
+ if [[ -f "$settings_path" ]]; then
655
+ settings_cache=$(cat "$settings_path" 2>/dev/null || echo "{}")
656
+ fi
657
+ local result
658
+ result=$("$NODE_BIN" -e "
659
+ const name = process.argv[1];
660
+ const primaryRaw = process.argv[2] || '{}';
661
+ const settingsRaw = process.argv[3] || '{}';
662
+ const defaults = {
663
+ pro31: 'gemini-3.1-pro-preview',
664
+ flash3: 'gemini-3-flash-preview',
665
+ pro25: 'gemini-2.5-pro',
666
+ flash25: 'gemini-2.5-flash',
667
+ lite25: 'gemini-2.5-flash-lite'
668
+ };
669
+
670
+ if (typeof name === 'string' && name.startsWith('gemini-')) {
671
+ process.stdout.write(name);
672
+ process.exit(0);
673
+ }
674
+
675
+ const parseJson = (raw) => {
676
+ try {
677
+ const parsed = JSON.parse(raw);
678
+ return parsed && typeof parsed === 'object' ? parsed : {};
679
+ } catch {
680
+ return {};
681
+ }
682
+ };
683
+
684
+ const getModelValue = (entry) => {
685
+ if (!entry) return '';
686
+ if (typeof entry === 'string') return entry;
687
+ if (typeof entry !== 'object') return '';
688
+ if (typeof entry.model === 'string') return entry.model;
689
+ if (typeof entry.name === 'string' && entry.name.startsWith('gemini-')) return entry.name;
690
+ if (entry.model && typeof entry.model.name === 'string') return entry.model.name;
691
+ return '';
692
+ };
693
+
694
+ const getProfileBuckets = (cfg) => {
695
+ const buckets = [];
696
+ if (cfg.profiles && typeof cfg.profiles === 'object') buckets.push(cfg.profiles);
697
+ if (cfg.model?.profiles && typeof cfg.model.profiles === 'object') buckets.push(cfg.model.profiles);
698
+ if (cfg.modelProfiles && typeof cfg.modelProfiles === 'object') buckets.push(cfg.modelProfiles);
699
+ if (cfg.models && typeof cfg.models === 'object') buckets.push(cfg.models);
700
+ return buckets;
701
+ };
702
+
703
+ const getDefaultModel = (cfg) => {
704
+ return (
705
+ (typeof cfg.defaultModel === 'string' && cfg.defaultModel) ||
706
+ (typeof cfg.default_profile === 'string' && cfg.default_profile) ||
707
+ (typeof cfg.defaultProfile === 'string' && cfg.defaultProfile) ||
708
+ (typeof cfg.model === 'string' && cfg.model) ||
709
+ (typeof cfg.model?.default === 'string' && cfg.model.default) ||
710
+ ''
711
+ );
712
+ };
713
+
714
+ const sources = [parseJson(primaryRaw), parseJson(settingsRaw)];
715
+ for (const cfg of sources) {
716
+ for (const bucket of getProfileBuckets(cfg)) {
717
+ const value = getModelValue(bucket[name]);
718
+ if (value) {
719
+ process.stdout.write(value);
720
+ process.exit(0);
721
+ }
722
+ }
723
+ }
724
+
725
+ if (name === 'default') {
726
+ for (const cfg of sources) {
727
+ const value = getDefaultModel(cfg);
728
+ if (value) {
729
+ process.stdout.write(value);
730
+ process.exit(0);
731
+ }
732
+ }
733
+ }
734
+
735
+ process.stdout.write(defaults[name] || defaults.pro31);
736
+ " "$profile" "$_GEMINI_PROFILE_CACHE" "$settings_cache" 2>/dev/null)
737
+ echo "${result:-gemini-3.1-pro-preview}"
738
+ }
739
+
740
+ # ── 라우팅 테이블 ──
741
+ # CLI_TYPE/CLI_CMD: agent-map.json 단일 소스. 상세 설정: 아래 case 문.
742
+ # 반환: CLI_TYPE, CLI_CMD, CLI_ARGS, CLI_EFFORT, DEFAULT_TIMEOUT, RUN_MODE, OPUS_OVERSIGHT
743
+ route_agent() {
744
+ local agent="$1"
745
+ local codex_base
746
+ codex_base="$(build_codex_base)"
747
+ echo "[tfx-route] Codex 버전: $(get_codex_version)" >&2
748
+ local map_file
749
+ map_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../hub/team/agent-map.json"
750
+ # ── breadcrumb 폴백 (synced 환경: ~/.claude/scripts/) ──
751
+ if [[ ! -f "$map_file" && -n "$TFX_PKG_ROOT" ]]; then
752
+ map_file="$TFX_PKG_ROOT/hub/team/agent-map.json"
753
+ fi
754
+ if [[ ! -f "$map_file" ]]; then
755
+ echo "ERROR: agent-map.json 미발견 (경로: $map_file, TFX_PKG_ROOT=${TFX_PKG_ROOT:-unset})" >&2
756
+ exit 1
757
+ fi
758
+
759
+ # ── CLI_TYPE: 단일 소스 (agent-map.json) ──
760
+ local _raw_type
761
+ _raw_type=$(node -e "
762
+ const p=require('path').resolve(process.argv[1]);
763
+ const m=JSON.parse(require('fs').readFileSync(p,'utf8'));
764
+ const t=m[process.argv[2]];
765
+ if(t)process.stdout.write(t);
766
+ " "$map_file" "$agent" 2>/dev/null)
767
+
768
+ if [[ -z "$_raw_type" ]]; then
769
+ echo "ERROR: 알 수 없는 에이전트 타입: $agent" >&2
770
+ echo "사용 가능: $(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync(require('path').resolve(process.argv[1]),'utf8'))).join(', '))" "$map_file" 2>/dev/null)" >&2
771
+ exit 1
772
+ fi
773
+
774
+ # "claude" → "claude-native" (headless.mjs는 "claude", route.sh는 "claude-native")
775
+ CLI_TYPE="$_raw_type"
776
+ [[ "$CLI_TYPE" == "claude" ]] && CLI_TYPE="claude-native"
777
+
778
+ # ── CLI_CMD: CLI_TYPE에서 파생 ──
779
+ case "$CLI_TYPE" in
780
+ codex) CLI_CMD="codex" ;;
781
+ gemini) CLI_CMD="gemini" ;;
782
+ claude-native) CLI_CMD=""; CLI_ARGS="" ;;
783
+ esac
784
+
785
+ # ── 에이전트별 상세 설정 ──
786
+ case "$agent" in
787
+ # ─── 구현 레인 ───
788
+ executor)
789
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
790
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
791
+ build-fixer)
792
+ CLI_ARGS="exec --profile codex53_low ${codex_base}"
793
+ CLI_EFFORT="codex53_low"; DEFAULT_TIMEOUT=540; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
794
+ debugger)
795
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
796
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
797
+ deep-executor)
798
+ CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
799
+ CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
800
+
801
+ # ─── 설계/분석 레인 (5.4: 1M 컨텍스트, 에이전틱) ───
802
+ architect)
803
+ CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
804
+ CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
805
+ planner)
806
+ CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
807
+ CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
808
+ critic)
809
+ CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
810
+ CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
811
+ analyst)
812
+ CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"
813
+ CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=3600; RUN_MODE="fg"; OPUS_OVERSIGHT="true" ;;
814
+
815
+ # ─── 리뷰 레인 (5.3-codex: SWE-Bench 72%) ───
816
+ code-reviewer)
817
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
818
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
819
+ security-reviewer)
820
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
821
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="true" ;;
822
+ quality-reviewer)
823
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
824
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1800; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
825
+
826
+ # ─── 리서치 레인 ───
827
+ scientist)
828
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
829
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
830
+ scientist-deep)
831
+ CLI_ARGS="exec --profile gpt54_high ${codex_base}"
832
+ CLI_EFFORT="gpt54_high"; DEFAULT_TIMEOUT=3600; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
833
+ document-specialist)
834
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
835
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1440; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
836
+
837
+ # ─── UI/문서 레인 ───
838
+ designer)
839
+ CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"
840
+ CLI_EFFORT="pro31"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
841
+ writer)
842
+ CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"
843
+ CLI_EFFORT="flash3"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
844
+
845
+ # ─── 탐색 (Claude-native: Glob/Grep/Read 직접 접근) ───
846
+ explore)
847
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
848
+
849
+ # ─── 검증/테스트 (Codex: 무료 + 파일 쓰기 가능) ───
850
+ verifier)
851
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
852
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
853
+ test-engineer)
854
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
855
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
856
+ qa-tester)
857
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
858
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
859
+
860
+ # ─── 경량 ───
861
+ spark)
862
+ CLI_ARGS="exec --profile spark53_low ${codex_base}"
863
+ CLI_EFFORT="spark53_low"; DEFAULT_TIMEOUT=180; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
864
+ # ─── CLI 이름 alias (사용자 편의) ───
865
+ codex)
866
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
867
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
868
+ gemini)
869
+ CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"
870
+ CLI_EFFORT="pro31"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
871
+ claude)
872
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
873
+ # ─── agent-map.json에만 정의된 신규 에이전트 (CLI_TYPE별 기본값) ───
874
+ *)
875
+ case "$CLI_TYPE" in
876
+ codex)
877
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
878
+ CLI_EFFORT="codex53_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
879
+ gemini)
880
+ CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"
881
+ CLI_EFFORT="pro31"; DEFAULT_TIMEOUT=900; RUN_MODE="bg"; OPUS_OVERSIGHT="false" ;;
882
+ claude-native)
883
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=600; RUN_MODE="fg"; OPUS_OVERSIGHT="false" ;;
884
+ esac ;;
885
+ esac
886
+ }
887
+
888
+ # ── CLI 모드 오버라이드 (tfx-codex / tfx-gemini 스킬용) ──
889
+ TFX_CLI_MODE="${TFX_CLI_MODE:-auto}"
890
+ TFX_NO_CLAUDE_NATIVE="${TFX_NO_CLAUDE_NATIVE:-0}"
891
+ TFX_VERIFIER_OVERRIDE="${TFX_VERIFIER_OVERRIDE:-auto}"
892
+ TFX_CODEX_TRANSPORT="${TFX_CODEX_TRANSPORT:-exec}"
893
+ # Preflight 캐시 일괄 로드 — CLI/Hub 가용성 + Codex 요금제를 환경변수로 내보냄
894
+ # 하위 프로세스(스킬 포함)가 TFX_CODEX_OK, TFX_GEMINI_OK, TFX_HUB_OK로 즉시 참조 가능
895
+ if [[ -z "${TFX_PREFLIGHT_LOADED:-}" ]]; then
896
+ eval "$(node -e '
897
+ try {
898
+ const c = JSON.parse(require("fs").readFileSync(require("path").join(require("os").homedir(),".claude","cache","tfx-preflight.json"),"utf8"));
899
+ const lines = [];
900
+ lines.push("export TFX_CODEX_OK=" + (c?.codex?.ok ? "1" : "0"));
901
+ lines.push("export TFX_GEMINI_OK=" + (c?.gemini?.ok ? "1" : "0"));
902
+ lines.push("export TFX_HUB_OK=" + (c?.hub?.ok ? "1" : "0"));
903
+ const p = c?.codex_plan?.plan;
904
+ if (p && p !== "unknown" && p !== "api") lines.push("export TFX_CODEX_PLAN=" + p);
905
+ const agents = c?.available_agents;
906
+ if (Array.isArray(agents)) lines.push("export TFX_AVAILABLE_AGENTS=" + agents.join(","));
907
+ lines.push("export TFX_PREFLIGHT_LOADED=1");
908
+ process.stdout.write(lines.join("\n") + "\n");
909
+ } catch { process.stdout.write("export TFX_PREFLIGHT_LOADED=1\n"); }
910
+ ' 2>/dev/null)"
911
+ TFX_CODEX_PLAN="${TFX_CODEX_PLAN:-pro}"
912
+ fi
913
+ TFX_WORKER_INDEX="${TFX_WORKER_INDEX:-}"
914
+ TFX_SEARCH_TOOL="${TFX_SEARCH_TOOL:-}"
915
+ case "$TFX_NO_CLAUDE_NATIVE" in
916
+ 0|1) ;;
917
+ *)
918
+ echo "ERROR: TFX_NO_CLAUDE_NATIVE 값은 0 또는 1이어야 합니다. (현재: $TFX_NO_CLAUDE_NATIVE)" >&2
919
+ exit 1
920
+ ;;
921
+ esac
922
+ case "$TFX_CODEX_PLAN" in
923
+ pro|plus|free) ;;
924
+ *)
925
+ echo "ERROR: TFX_CODEX_PLAN 값은 pro, plus, free 중 하나여야 합니다. (현재: $TFX_CODEX_PLAN)" >&2
926
+ exit 1
927
+ ;;
928
+ esac
929
+ case "$TFX_CODEX_TRANSPORT" in
930
+ auto|mcp|exec) ;;
931
+ *)
932
+ echo "ERROR: TFX_CODEX_TRANSPORT 값은 auto, mcp, exec 중 하나여야 합니다. (현재: $TFX_CODEX_TRANSPORT)" >&2
933
+ exit 1
934
+ ;;
935
+ esac
936
+ case "$TFX_VERIFIER_OVERRIDE" in
937
+ auto|claude) ;;
938
+ *)
939
+ echo "ERROR: TFX_VERIFIER_OVERRIDE 값은 auto 또는 claude여야 합니다. (현재: $TFX_VERIFIER_OVERRIDE)" >&2
940
+ exit 1
941
+ ;;
942
+ esac
943
+ case "$TFX_WORKER_INDEX" in
944
+ "") ;;
945
+ *[!0-9]*|0)
946
+ echo "ERROR: TFX_WORKER_INDEX 값은 1 이상의 정수여야 합니다. (현재: $TFX_WORKER_INDEX)" >&2
947
+ exit 1
948
+ ;;
949
+ esac
950
+ case "$TFX_SEARCH_TOOL" in
951
+ ""|brave-search|tavily|exa) ;;
952
+ *)
953
+ echo "ERROR: TFX_SEARCH_TOOL 값은 brave-search, tavily, exa 중 하나여야 합니다. (현재: $TFX_SEARCH_TOOL)" >&2
954
+ exit 1
955
+ ;;
956
+ esac
957
+ CODEX_MCP_TRANSPORT_EXIT_CODE=70
958
+
959
+ apply_cli_mode() {
960
+ local codex_base
961
+ codex_base="$(build_codex_base)"
962
+ local gemini_tier=""
963
+
964
+ case "$TFX_CLI_MODE" in
965
+ codex)
966
+ if [[ "$CLI_TYPE" == "gemini" ]]; then
967
+ CLI_TYPE="codex"; CLI_CMD="codex"
968
+ case "$AGENT_TYPE" in
969
+ designer)
970
+ CLI_ARGS="exec --profile gpt54_xhigh ${codex_base}"; CLI_EFFORT="gpt54_xhigh"; DEFAULT_TIMEOUT=600 ;;
971
+ writer)
972
+ CLI_ARGS="exec --profile spark53_low ${codex_base}"; CLI_EFFORT="spark53_low"; DEFAULT_TIMEOUT=180 ;;
973
+ esac
974
+ echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
975
+ fi ;;
976
+ gemini)
977
+ if [[ "$CLI_TYPE" == "codex" ]]; then
978
+ case "$AGENT_TYPE" in
979
+ verifier)
980
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
981
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
982
+ echo "[tfx-route] TFX_CLI_MODE=gemini: verifier는 claude-native 유지" >&2
983
+ return 0
984
+ ;;
985
+ test-engineer)
986
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
987
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=1200; RUN_MODE="bg"; OPUS_OVERSIGHT="false"
988
+ echo "[tfx-route] TFX_CLI_MODE=gemini: test-engineer는 claude-native 유지" >&2
989
+ return 0
990
+ ;;
991
+ esac
992
+ CLI_TYPE="gemini"; CLI_CMD="gemini"
993
+ case "$AGENT_TYPE" in
994
+ executor|debugger|deep-executor|architect|planner|critic|analyst|\
995
+ code-reviewer|security-reviewer|quality-reviewer|scientist-deep|designer)
996
+ CLI_ARGS="-m $(resolve_gemini_profile pro31) -y --prompt"; CLI_EFFORT="pro31" ;;
997
+ build-fixer|spark)
998
+ CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"; CLI_EFFORT="flash3"; DEFAULT_TIMEOUT=180 ;;
999
+ writer)
1000
+ CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"; CLI_EFFORT="flash3" ;;
1001
+ *)
1002
+ CLI_ARGS="-m $(resolve_gemini_profile flash3) -y --prompt"; CLI_EFFORT="flash3" ;;
1003
+ esac
1004
+ case "$CLI_EFFORT" in
1005
+ pro*) gemini_tier="pro" ;;
1006
+ flash*|lite*) gemini_tier="flash" ;;
1007
+ *) gemini_tier="$CLI_EFFORT" ;;
1008
+ esac
1009
+ echo "[tfx-route] TFX_CLI_MODE=gemini: $AGENT_TYPE → gemini($gemini_tier)로 리매핑" >&2
1010
+ fi ;;
1011
+ auto)
1012
+ if [[ "$CLI_TYPE" == "codex" ]] && ! command -v "$CODEX_BIN" &>/dev/null; then
1013
+ if command -v "$GEMINI_BIN" &>/dev/null; then
1014
+ TFX_CLI_MODE="gemini"; apply_cli_mode; return
1015
+ else
1016
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
1017
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
1018
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
1019
+ fi
1020
+ elif [[ "$CLI_TYPE" == "gemini" ]] && ! command -v "$GEMINI_BIN" &>/dev/null; then
1021
+ if command -v "$CODEX_BIN" &>/dev/null; then
1022
+ TFX_CLI_MODE="codex"; apply_cli_mode; return
1023
+ else
1024
+ ORIGINAL_AGENT="${AGENT_TYPE}"; ORIGINAL_CLI_ARGS="$CLI_ARGS"
1025
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
1026
+ echo "[tfx-route] codex/gemini 모두 미설치: $AGENT_TYPE → claude-native fallback" >&2
1027
+ fi
1028
+ fi ;;
1029
+ esac
1030
+ }
1031
+
1032
+ # ── Codex 요금제 가드 (spark 프로필은 Pro 전용) ──
1033
+ apply_plan_guard() {
1034
+ [[ "$CLI_TYPE" != "codex" ]] && return
1035
+ [[ "$TFX_CODEX_PLAN" == "pro" ]] && return
1036
+
1037
+ if [[ "$CLI_EFFORT" == spark53_* ]]; then
1038
+ local codex_base
1039
+ codex_base="$(build_codex_base)"
1040
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
1041
+ CLI_EFFORT="codex53_high"
1042
+ echo "[tfx-route] TFX_CODEX_PLAN=$TFX_CODEX_PLAN: spark → codex53_high로 다운그레이드 (Pro 전용)" >&2
1043
+ fi
1044
+ }
1045
+
1046
+ # ── Claude 네이티브 제거 (Codex 리드 환경에서 선택적 활성화) ──
1047
+ apply_no_claude_native_mode() {
1048
+ local codex_base
1049
+ codex_base="$(build_codex_base)"
1050
+
1051
+ [[ "$TFX_NO_CLAUDE_NATIVE" != "1" ]] && return
1052
+ [[ "$TFX_CLI_MODE" == "gemini" ]] && return
1053
+ [[ "$CLI_TYPE" != "claude-native" ]] && return
1054
+
1055
+ if ! command -v "$CODEX_BIN" &>/dev/null; then
1056
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1 이지만 codex를 찾지 못해 claude-native 유지" >&2
1057
+ return
1058
+ fi
1059
+
1060
+ ORIGINAL_AGENT="${AGENT_TYPE}"
1061
+ CLI_TYPE="codex"; CLI_CMD="codex"
1062
+
1063
+ case "$AGENT_TYPE" in
1064
+ explore)
1065
+ CLI_ARGS="exec --profile codex53_low ${codex_base}"
1066
+ CLI_EFFORT="codex53_low"
1067
+ DEFAULT_TIMEOUT=600
1068
+ RUN_MODE="fg"
1069
+ OPUS_OVERSIGHT="false"
1070
+ ;;
1071
+ verifier)
1072
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
1073
+ CLI_EFFORT="codex53_high"
1074
+ DEFAULT_TIMEOUT=1200
1075
+ RUN_MODE="fg"
1076
+ OPUS_OVERSIGHT="false"
1077
+ ;;
1078
+ test-engineer)
1079
+ CLI_ARGS="exec --profile codex53_high ${codex_base}"
1080
+ CLI_EFFORT="codex53_high"
1081
+ DEFAULT_TIMEOUT=1200
1082
+ RUN_MODE="bg"
1083
+ OPUS_OVERSIGHT="false"
1084
+ ;;
1085
+ qa-tester)
1086
+ CLI_ARGS="exec --profile codex53_high ${codex_base} review"
1087
+ CLI_EFFORT="codex53_high"
1088
+ DEFAULT_TIMEOUT=1200
1089
+ RUN_MODE="bg"
1090
+ OPUS_OVERSIGHT="false"
1091
+ ;;
1092
+ *)
1093
+ # claude-native 타입 중 위에 없는 경우는 보수적으로 유지
1094
+ CLI_TYPE="claude-native"; CLI_CMD=""; CLI_ARGS=""
1095
+ return
1096
+ ;;
1097
+ esac
1098
+
1099
+ echo "[tfx-route] TFX_NO_CLAUDE_NATIVE=1: $AGENT_TYPE -> codex($CLI_EFFORT) 리매핑" >&2
1100
+ }
1101
+
1102
+ apply_verifier_override() {
1103
+ [[ "$AGENT_TYPE" != "verifier" ]] && return
1104
+
1105
+ case "$TFX_VERIFIER_OVERRIDE" in
1106
+ auto|"")
1107
+ return 0
1108
+ ;;
1109
+ claude)
1110
+ ORIGINAL_AGENT="${ORIGINAL_AGENT:-$AGENT_TYPE}"
1111
+ CLI_TYPE="claude-native"
1112
+ CLI_CMD=""
1113
+ CLI_ARGS=""
1114
+ CLI_EFFORT="n/a"; DEFAULT_TIMEOUT=1200; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
1115
+ echo "[tfx-route] TFX_VERIFIER_OVERRIDE=claude: verifier -> claude-native" >&2
1116
+ ;;
1117
+ esac
1118
+
1119
+ return 0
1120
+ }
1121
+
1122
+ # ── MCP 인벤토리 캐시 ──
1123
+ MCP_CACHE="${HOME}/.claude/cache/mcp-inventory.json"
1124
+ MCP_FILTER_SCRIPT=""
1125
+ MCP_PROFILE_REQUESTED="auto"
1126
+ MCP_RESOLVED_PROFILE="default"
1127
+ MCP_HINT=""
1128
+ GEMINI_ALLOWED_SERVERS=()
1129
+ CODEX_CONFIG_FLAGS=()
1130
+ CODEX_CONFIG_JSON=""
1131
+
1132
+ get_cached_servers() {
1133
+ local cli_type="$1"
1134
+ if [[ -f "$MCP_CACHE" ]]; then
1135
+ node -e 'const[,f,t]=process.argv;const inv=JSON.parse(require("fs").readFileSync(f,"utf8"));const s=(inv[t]||{}).servers||[];console.log(s.filter(x=>x.status==="enabled"||x.status==="configured").map(x=>x.name).join(","))' -- "$MCP_CACHE" "$cli_type" 2>/dev/null
1136
+ fi
1137
+ }
1138
+
1139
+ resolve_mcp_filter_script() {
1140
+ if [[ -n "$MCP_FILTER_SCRIPT" && -f "$MCP_FILTER_SCRIPT" ]]; then
1141
+ printf '%s\n' "$MCP_FILTER_SCRIPT"
1142
+ return 0
1143
+ fi
1144
+
1145
+ local script_ref script_dir candidate
1146
+ local -a candidates=()
1147
+
1148
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
1149
+ if [[ -n "$script_ref" ]]; then
1150
+ script_dir="$(cd "$(dirname "$script_ref")" 2>/dev/null && pwd -P || true)"
1151
+ [[ -n "$script_dir" ]] && candidates+=("$script_dir/lib/mcp-filter.mjs")
1152
+ fi
1153
+
1154
+ candidates+=(
1155
+ "$PWD/scripts/lib/mcp-filter.mjs"
1156
+ "$PWD/lib/mcp-filter.mjs"
1157
+ )
1158
+
1159
+ for candidate in "${candidates[@]}"; do
1160
+ if [[ -f "$candidate" ]]; then
1161
+ MCP_FILTER_SCRIPT="$candidate"
1162
+ printf '%s\n' "$MCP_FILTER_SCRIPT"
1163
+ return 0
1164
+ fi
1165
+ done
1166
+
1167
+ return 1
1168
+ }
1169
+
1170
+ resolve_mcp_policy() {
1171
+ local filter_script available_servers
1172
+ if ! filter_script=$(resolve_mcp_filter_script); then
1173
+ echo "[tfx-route] 경고: mcp-filter.mjs를 찾지 못해 기본 MCP 정책을 사용합니다." >&2
1174
+ MCP_PROFILE_REQUESTED="$MCP_PROFILE"
1175
+ MCP_RESOLVED_PROFILE="$MCP_PROFILE"
1176
+ MCP_HINT=""
1177
+ GEMINI_ALLOWED_SERVERS=()
1178
+ CODEX_CONFIG_FLAGS=()
1179
+ CODEX_CONFIG_JSON=""
1180
+ return 0
1181
+ fi
1182
+
1183
+ available_servers=$(get_cached_servers "$CLI_TYPE")
1184
+ if [[ "$CLI_TYPE" == "codex" && "${TFX_CODEX_TRANSPORT:-auto}" != "mcp" ]]; then
1185
+ available_servers=""
1186
+ fi
1187
+ # Codex 0.115+: 미등록 서버에 config override(enabled=true/false 모두)를 보내면
1188
+ # "invalid transport" 에러 발생. 캐시 비어있으면 빈 문자열로 유지하여
1189
+ # mcp-filter가 override를 생성하지 않도록 한다.
1190
+ [[ -z "$available_servers" ]] && available_servers=""
1191
+
1192
+ local -a cmd=(
1193
+ "$NODE_BIN" "$filter_script" shell
1194
+ "--agent" "$AGENT_TYPE"
1195
+ "--profile" "$MCP_PROFILE"
1196
+ "--available" "$available_servers"
1197
+ "--inventory-file" "$MCP_CACHE"
1198
+ "--task-text" "$PROMPT"
1199
+ )
1200
+ [[ -n "$TFX_SEARCH_TOOL" ]] && cmd+=("--search-tool" "$TFX_SEARCH_TOOL")
1201
+ [[ -n "$TFX_WORKER_INDEX" ]] && cmd+=("--worker-index" "$TFX_WORKER_INDEX")
1202
+
1203
+ local shell_exports
1204
+ if ! shell_exports="$("${cmd[@]}")"; then
1205
+ echo "[tfx-route] ERROR: MCP 정책 계산 실패" >&2
1206
+ return 1
1207
+ fi
1208
+
1209
+ eval "$shell_exports"
1210
+ }
1211
+
1212
+ get_claude_model() {
1213
+ case "$AGENT_TYPE" in
1214
+ explore) echo "haiku" ;;
1215
+ *) echo "sonnet" ;;
1216
+ esac
1217
+ }
1218
+
1219
+ emit_claude_native_metadata() {
1220
+ local model
1221
+ model=$(get_claude_model)
1222
+ echo "ROUTE_TYPE=claude-native"
1223
+ echo "AGENT=$AGENT_TYPE"
1224
+ echo "MODEL=$model"
1225
+ echo "RUN_MODE=$RUN_MODE"
1226
+ echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
1227
+ echo "TIMEOUT=$TIMEOUT_SEC"
1228
+ echo "MCP_PROFILE=$MCP_PROFILE"
1229
+ [[ -n "$ORIGINAL_AGENT" ]] && echo "ORIGINAL_AGENT=$ORIGINAL_AGENT"
1230
+ echo "PROMPT=$PROMPT"
1231
+ echo "--- Claude Task($model) 에이전트로 위임하세요 ---"
1232
+ }
1233
+
1234
+ # _find_fork_pids PID — cross-platform child PID lookup
1235
+ # pgrep -P (Linux/macOS) → Git Bash ps fallback (PPID/PGID column)
1236
+ _find_fork_pids() {
1237
+ local parent="$1"
1238
+ if command -v pgrep &>/dev/null; then
1239
+ pgrep -P "$parent" 2>/dev/null || true
1240
+ return
1241
+ fi
1242
+ # Git Bash: PID PPID PGID WINPID ... — match by PPID or PGID
1243
+ ps 2>/dev/null | awk -v p="$parent" 'NR>1 && ($2==p || ($3==p && $1!=p)) {print $1}' | sort -un | tr '\n' ' '
1244
+ }
1245
+
1246
+ # heartbeat_monitor PID [INTERVAL] [STALL_THRESHOLD]
1247
+ # - PID: 감시할 워커 프로세스 PID
1248
+ # - INTERVAL: heartbeat 출력 간격 (초, 기본 10)
1249
+ # - STALL_THRESHOLD: stall 경고 임계값 (초, 기본 60)
1250
+ # 환경변수: TFX_HEARTBEAT (0이면 비활성화), TFX_HEARTBEAT_INTERVAL, TFX_STALL_THRESHOLD
1251
+ heartbeat_monitor() {
1252
+ [[ "${TFX_HEARTBEAT:-1}" -eq 0 ]] && return 0
1253
+ local pid="$1"
1254
+ local interval="${2:-${TFX_HEARTBEAT_INTERVAL:-10}}"
1255
+ local stall_threshold="${3:-${TFX_STALL_THRESHOLD:-60}}"
1256
+ local last_size=0 stall_count=0
1257
+ local pid_gone=false
1258
+ local post_exit_checks=0
1259
+ local max_post_exit_checks=6 # fallback drain: 6 intervals (fork PID 미발견 시)
1260
+ local last_known_forks="" # direct fork PID tracking
1261
+
1262
+ while true; do
1263
+ sleep "$interval"
1264
+
1265
+ # Check if the tracked PID is still alive; snapshot forks while alive
1266
+ if ! kill -0 "$pid" 2>/dev/null; then
1267
+ if [[ "$pid_gone" == "false" ]]; then
1268
+ pid_gone=true
1269
+ local _imm; _imm=$(_find_fork_pids "$pid") || true
1270
+ [[ -n "$_imm" ]] && last_known_forks="$_imm"
1271
+ [[ -n "$last_known_forks" ]] && \
1272
+ echo "[tfx-heartbeat] pid=$pid exited, tracking forks: $last_known_forks" >&2
1273
+ fi
1274
+ else
1275
+ local _cf; _cf=$(_find_fork_pids "$pid") || true
1276
+ [[ -n "$_cf" ]] && last_known_forks="$_cf"
1277
+ fi
1278
+
1279
+ local current_size=0
1280
+ [[ -f "$STDOUT_LOG" ]] && current_size=$(wc -c < "$STDOUT_LOG" 2>/dev/null || echo 0)
1281
+ # P3: stderr 활동도 포함하여 거짓 STALL 방지
1282
+ local stderr_size=0
1283
+ [[ -f "$STDERR_LOG" ]] && stderr_size=$(wc -c < "$STDERR_LOG" 2>/dev/null || echo 0)
1284
+ current_size=$((current_size + stderr_size))
1285
+ local elapsed=$(($(date +%s) - TIMESTAMP))
1286
+
1287
+ if [[ "$current_size" -gt "$last_size" ]]; then
1288
+ stall_count=0
1289
+ if [[ "$pid_gone" == "true" ]]; then
1290
+ local _fi="forked"; [[ -n "$last_known_forks" ]] && _fi="forks:${last_known_forks// /,}"
1291
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=active(${_fi})" >&2
1292
+ post_exit_checks=0 # reset — still producing output
1293
+ else
1294
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=active" >&2
1295
+ fi
1296
+ else
1297
+ stall_count=$((stall_count + interval))
1298
+ if [[ "$pid_gone" == "true" ]]; then
1299
+ if [[ -n "$last_known_forks" ]]; then
1300
+ # Direct fork tracking — terminate when all forks are dead
1301
+ local _alive=false
1302
+ for _fp in $last_known_forks; do
1303
+ kill -0 "$_fp" 2>/dev/null && _alive=true && break
1304
+ done
1305
+ if [[ "$_alive" == "false" ]]; then
1306
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=terminated(forks-exited)" >&2
1307
+ break
1308
+ fi
1309
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=fork-idle(${last_known_forks// /,})" >&2
1310
+ else
1311
+ # Fallback: output-based drain (no fork PIDs found)
1312
+ post_exit_checks=$((post_exit_checks + 1))
1313
+ if [[ "$post_exit_checks" -ge "$max_post_exit_checks" ]]; then
1314
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=terminated(drain-done)" >&2
1315
+ break
1316
+ fi
1317
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=draining(${post_exit_checks}/${max_post_exit_checks})" >&2
1318
+ fi
1319
+ elif [[ "$stall_count" -ge "$stall_threshold" ]]; then
1320
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=STALL stall=${stall_count}s" >&2
1321
+ else
1322
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=quiet stall=${stall_count}s" >&2
1323
+ fi
1324
+ fi
1325
+ last_size=$current_size
1326
+ done
1327
+ echo "[tfx-heartbeat] pid=$pid terminated" >&2
1328
+ }
1329
+
1330
+ resolve_worker_runner_script() {
1331
+ if [[ -n "${TFX_ROUTE_WORKER_RUNNER:-}" && -f "$TFX_ROUTE_WORKER_RUNNER" ]]; then
1332
+ printf '%s\n' "$TFX_ROUTE_WORKER_RUNNER"
1333
+ return 0
1334
+ fi
1335
+
1336
+ local script_ref script_dir
1337
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
1338
+ script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
1339
+ local candidate="$script_dir/tfx-route-worker.mjs"
1340
+ [[ -f "$candidate" ]] || return 1
1341
+ printf '%s\n' "$candidate"
1342
+ }
1343
+
1344
+ run_stream_worker() {
1345
+ local worker_type="$1"
1346
+ local prompt="$2"
1347
+ local use_tee_flag="$3"
1348
+ shift 3
1349
+ local exit_code_local=0
1350
+ local worker_pid hb_pid
1351
+
1352
+ local runner_script
1353
+ if ! runner_script=$(resolve_worker_runner_script); then
1354
+ echo "[tfx-route] 경고: stream worker runner를 찾지 못했습니다." >&2
1355
+ return 127
1356
+ fi
1357
+
1358
+ if ! command -v "$NODE_BIN" &>/dev/null; then
1359
+ echo "[tfx-route] 경고: node를 찾지 못해 stream worker를 실행할 수 없습니다." >&2
1360
+ return 127
1361
+ fi
1362
+
1363
+ local -a worker_cmd=(
1364
+ "$NODE_BIN"
1365
+ "$runner_script"
1366
+ "--type" "$worker_type"
1367
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
1368
+ "--cwd" "$PWD"
1369
+ "$@"
1370
+ )
1371
+
1372
+ if [[ "$use_tee_flag" == "true" ]]; then
1373
+ printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1374
+ else
1375
+ printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1376
+ fi
1377
+ worker_pid=$!
1378
+
1379
+ heartbeat_monitor "$worker_pid" &
1380
+ hb_pid=$!
1381
+
1382
+ wait "$worker_pid" || exit_code_local=$?
1383
+ kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
1384
+ return "$exit_code_local"
1385
+ }
1386
+
1387
+ # Gemini 429 지수 백오프 재시도 래퍼
1388
+ # 사용: gemini_with_retry <use_tee_flag> <gemini_args_array_name> <prompt>
1389
+ # 429/rate limit 감지 시 최대 3회 재시도 (2→4→8초 백오프)
1390
+ _gemini_run_once() {
1391
+ local use_tee_flag="$1"
1392
+ local prompt="$2"
1393
+ shift 2
1394
+ local -a g_args=("$@")
1395
+
1396
+ if [[ "$use_tee_flag" == "true" ]]; then
1397
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1398
+ else
1399
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1400
+ fi
1401
+ GEMINI_RUN_PID=$!
1402
+ }
1403
+
1404
+ gemini_with_retry() {
1405
+ local use_tee_flag="$1"
1406
+ local prompt="$2"
1407
+ shift 2
1408
+ local -a g_args=("$@")
1409
+
1410
+ local max_retries=3
1411
+ local attempt=0
1412
+ local delay=2
1413
+ local exit_code_local=0
1414
+
1415
+ while (( attempt < max_retries )); do
1416
+ exit_code_local=0
1417
+ local pid
1418
+ _gemini_run_once "$use_tee_flag" "$prompt" "${g_args[@]}"
1419
+ pid="${GEMINI_RUN_PID:-}"
1420
+ if [[ -z "$pid" ]]; then
1421
+ echo "[tfx-route] Gemini: worker pid 획득 실패" >&2
1422
+ return 1
1423
+ fi
1424
+
1425
+ local health_ok=true
1426
+ local intervals=(1 2 3 5 8)
1427
+ for wait_sec in "${intervals[@]}"; do
1428
+ sleep "$wait_sec"
1429
+ if [[ -s "$STDOUT_LOG" ]] || [[ -s "$STDERR_LOG" ]]; then
1430
+ break
1431
+ fi
1432
+ if ! kill -0 "$pid" 2>/dev/null; then
1433
+ health_ok=false
1434
+ echo "[tfx-route] Gemini: 출력 없이 프로세스 종료 (${wait_sec}초 체크)" >&2
1435
+ break
1436
+ fi
1437
+ done
1438
+
1439
+ local hb_pid
1440
+ if [[ "$health_ok" == "false" ]]; then
1441
+ wait "$pid" 2>/dev/null
1442
+ else
1443
+ heartbeat_monitor "$pid" &
1444
+ hb_pid=$!
1445
+ wait "$pid" || exit_code_local=$?
1446
+ kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
1447
+ fi
1448
+
1449
+ # 성공 시 즉시 반환
1450
+ if [[ $exit_code_local -eq 0 ]]; then
1451
+ return 0
1452
+ fi
1453
+
1454
+ # 429 / rate limit 감지
1455
+ if grep -qiE '429|rate.limit|too many requests' "$STDERR_LOG" 2>/dev/null; then
1456
+ attempt=$(( attempt + 1 ))
1457
+ if (( attempt < max_retries )); then
1458
+ echo "[tfx-route] Gemini 429 감지. ${delay}초 후 재시도 ($attempt/$max_retries)..." >&2
1459
+ kill "$pid" 2>/dev/null
1460
+ wait "$pid" 2>/dev/null
1461
+ sleep "$delay"
1462
+ delay=$(( delay * 2 ))
1463
+ : > "$STDOUT_LOG"
1464
+ : > "$STDERR_LOG"
1465
+ continue
1466
+ else
1467
+ echo "[tfx-route] Gemini 429: ${max_retries}회 재시도 실패" >&2
1468
+ fi
1469
+ fi
1470
+
1471
+ # 비-429 에러 또는 최대 재시도 초과 시 즉시 반환
1472
+ return "$exit_code_local"
1473
+ done
1474
+
1475
+ return "$exit_code_local"
1476
+ }
1477
+
1478
+ run_legacy_gemini() {
1479
+ local prompt="$1"
1480
+ local use_tee_flag="$2"
1481
+ local -a gemini_args=()
1482
+ read -r -a gemini_args <<< "$CLI_ARGS"
1483
+
1484
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1485
+ local gemini_mcp_filter prompt_index=-1
1486
+ gemini_mcp_filter=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")
1487
+ for i in "${!gemini_args[@]}"; do
1488
+ if [[ "${gemini_args[$i]}" == "--prompt" ]]; then
1489
+ prompt_index="$i"
1490
+ break
1491
+ fi
1492
+ done
1493
+ if [[ "$prompt_index" -ge 0 ]]; then
1494
+ gemini_args=(
1495
+ "${gemini_args[@]:0:$prompt_index}"
1496
+ "--allowed-mcp-server-names" "$gemini_mcp_filter"
1497
+ "${gemini_args[@]:$prompt_index}"
1498
+ )
1499
+ echo "[tfx-route] Gemini MCP 필터: $gemini_mcp_filter" >&2
1500
+ fi
1501
+ fi
1502
+
1503
+ gemini_with_retry "$use_tee_flag" "$prompt" "${gemini_args[@]}"
1504
+ }
1505
+
1506
+ resolve_codex_mcp_script() {
1507
+ if [[ -n "${TFX_CODEX_MCP_SCRIPT:-}" && -f "$TFX_CODEX_MCP_SCRIPT" ]]; then
1508
+ printf '%s\n' "$TFX_CODEX_MCP_SCRIPT"
1509
+ return 0
1510
+ fi
1511
+
1512
+ local script_ref script_dir
1513
+ script_ref="$(normalize_script_path "${BASH_SOURCE[0]}")"
1514
+ script_dir="$(cd "$(dirname "$script_ref")" && pwd -P)"
1515
+ local candidates=()
1516
+ [[ -n "$TFX_PKG_ROOT" ]] && candidates+=("$TFX_PKG_ROOT/hub/workers/codex-mcp.mjs")
1517
+ candidates+=(
1518
+ "$script_dir/hub/workers/codex-mcp.mjs"
1519
+ "$script_dir/../hub/workers/codex-mcp.mjs"
1520
+ )
1521
+
1522
+ local candidate
1523
+ for candidate in "${candidates[@]}"; do
1524
+ if [[ -f "$candidate" ]]; then
1525
+ printf '%s\n' "$candidate"
1526
+ return 0
1527
+ fi
1528
+ done
1529
+
1530
+ return 1
1531
+ }
1532
+
1533
+ run_codex_exec() {
1534
+ local prompt="$1"
1535
+ local use_tee_flag="$2"
1536
+ local exit_code_local=0
1537
+ local worker_pid hb_pid
1538
+ local -a codex_args=()
1539
+ read -r -a codex_args <<< "$CLI_ARGS"
1540
+ if [[ ${#CODEX_CONFIG_FLAGS[@]} -gt 0 ]]; then
1541
+ codex_args+=("${CODEX_CONFIG_FLAGS[@]}")
1542
+ fi
1543
+
1544
+ if [[ "$use_tee_flag" == "true" ]]; then
1545
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" < /dev/null 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1546
+ else
1547
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" < /dev/null >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1548
+ fi
1549
+ worker_pid=$!
1550
+
1551
+ heartbeat_monitor "$worker_pid" &
1552
+ hb_pid=$!
1553
+
1554
+ wait "$worker_pid" || exit_code_local=$?
1555
+ kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
1556
+
1557
+ if [[ ! -s "$STDOUT_LOG" && -s "$STDERR_LOG" ]]; then
1558
+ # stderr에서 마지막 "codex" 마커 이후의 텍스트를 stdout으로 복구
1559
+ # 1차: "codex" 마커 기반 (Windows \r 제거 후 매칭)
1560
+ sed 's/\r$//' "$STDERR_LOG" \
1561
+ | awk '/^codex$/{found=NR;content=""} found && NR>found{content=content RS $0} END{if(content) print substr(content,2)}' \
1562
+ > "$STDOUT_LOG"
1563
+
1564
+ # 2차: 마커 없을 때 node fallback (MCP/헤더/sandbox 로그 제외, 응답 부분만 추출)
1565
+ if [[ ! -s "$STDOUT_LOG" ]]; then
1566
+ node -e '
1567
+ const fs=require("fs"),lines=fs.readFileSync(process.argv[1],"utf-8").split(/\r?\n/);
1568
+ const skip=/^(mcp[: ]|OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|tokens used|EXIT:|exec$|"[A-Z]:|succeeded in |\s*$)/;
1569
+ const out=lines.filter(l=>!skip.test(l));
1570
+ if(out.length) fs.writeFileSync(process.argv[2],out.join("\n"));
1571
+ ' -- "$STDERR_LOG" "$STDOUT_LOG" 2>/dev/null || true
1572
+ fi
1573
+
1574
+ if [[ -s "$STDOUT_LOG" ]]; then
1575
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr에서 응답 복구 ($(wc -c < "$STDOUT_LOG" | tr -d ' ') bytes)" >&2
1576
+ else
1577
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패" >&2
1578
+ fi
1579
+ fi
1580
+
1581
+ return "$exit_code_local"
1582
+ }
1583
+
1584
+ run_codex_mcp() {
1585
+ local prompt="$1"
1586
+ local use_tee_flag="$2"
1587
+ local mcp_script node_bin
1588
+ local exit_code_local=0
1589
+ local worker_pid hb_pid
1590
+
1591
+ if ! mcp_script=$(resolve_codex_mcp_script); then
1592
+ echo "[tfx-route] 경고: Codex MCP 래퍼를 찾지 못했습니다." >&2
1593
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1594
+ fi
1595
+
1596
+ node_bin="${NODE_BIN:-$(command -v node 2>/dev/null || echo node)}"
1597
+ if ! command -v "$node_bin" &>/dev/null; then
1598
+ echo "[tfx-route] 경고: node를 찾지 못해 Codex MCP 경로를 사용할 수 없습니다." >&2
1599
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1600
+ fi
1601
+
1602
+ local -a mcp_args=(
1603
+ "$mcp_script"
1604
+ "--prompt" "$prompt"
1605
+ "--cwd" "$PWD"
1606
+ "--profile" "$CLI_EFFORT"
1607
+ "--approval-policy" "never"
1608
+ "--sandbox" "danger-full-access"
1609
+ "--timeout-ms" "$((TIMEOUT_SEC * 1000))"
1610
+ "--codex-command" "$CODEX_BIN"
1611
+ )
1612
+
1613
+ if [[ -n "$CODEX_CONFIG_JSON" && "$CODEX_CONFIG_JSON" != "{}" ]]; then
1614
+ mcp_args+=("--config-json" "$CODEX_CONFIG_JSON")
1615
+ fi
1616
+
1617
+ case "$AGENT_TYPE" in
1618
+ code-reviewer)
1619
+ mcp_args+=(
1620
+ "--developer-instructions"
1621
+ "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라."
1622
+ )
1623
+ ;;
1624
+ security-reviewer)
1625
+ mcp_args+=(
1626
+ "--developer-instructions"
1627
+ "보안 리뷰 모드로 동작하라. 취약점, 권한 경계, 비밀정보 노출 가능성을 우선 식별하라."
1628
+ )
1629
+ ;;
1630
+ quality-reviewer)
1631
+ mcp_args+=(
1632
+ "--developer-instructions"
1633
+ "품질 리뷰 모드로 동작하라. 로직 결함, 유지보수성 저하, 테스트 누락을 우선 식별하라."
1634
+ )
1635
+ ;;
1636
+ esac
1637
+
1638
+ if [[ "$use_tee_flag" == "true" ]]; then
1639
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
1640
+ else
1641
+ "$TIMEOUT_BIN" "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
1642
+ fi
1643
+ worker_pid=$!
1644
+
1645
+ heartbeat_monitor "$worker_pid" &
1646
+ hb_pid=$!
1647
+
1648
+ wait "$worker_pid" || exit_code_local=$?
1649
+ kill "$hb_pid" 2>/dev/null; wait "$hb_pid" 2>/dev/null
1650
+
1651
+ # 모듈 로드 실패(의존성 누락) → MCP transport exit code로 변환하여 fallback 트리거
1652
+ if [[ "$exit_code_local" -ne 0 && "$exit_code_local" -ne 124 ]] && grep -q 'ERR_MODULE_NOT_FOUND' "$STDERR_LOG" 2>/dev/null; then
1653
+ echo "[tfx-route] Codex MCP 모듈 로드 실패 — fallback 가능 exit code로 변환" >&2
1654
+ return "$CODEX_MCP_TRANSPORT_EXIT_CODE"
1655
+ fi
1656
+
1657
+ return "$exit_code_local"
1658
+ }
1659
+
1660
+ # ── 메인 실행 ──
1661
+ main() {
1662
+ # 종료 시 per-process 에이전트 파일 자동 삭제
1663
+ trap 'deregister_agent' EXIT
1664
+
1665
+ route_agent "$AGENT_TYPE"
1666
+ apply_cli_mode
1667
+ apply_no_claude_native_mode
1668
+ apply_plan_guard
1669
+ apply_verifier_override
1670
+
1671
+ # CLI 경로 해석
1672
+ case "$CLI_CMD" in
1673
+ codex) CLI_CMD="$CODEX_BIN" ;;
1674
+ gemini) CLI_CMD="$GEMINI_BIN" ;;
1675
+ claude) CLI_CMD="$CLAUDE_BIN" ;;
1676
+ esac
1677
+
1678
+ # 타임아웃 결정 (에이전트별 최소값 보장)
1679
+ local MIN_TIMEOUT
1680
+ case "$AGENT_TYPE" in
1681
+ deep-executor|architect|planner|critic|analyst) MIN_TIMEOUT=900 ;;
1682
+ document-specialist|scientist|scientist-deep) MIN_TIMEOUT=900 ;;
1683
+ code-reviewer|security-reviewer|quality-reviewer) MIN_TIMEOUT=600 ;;
1684
+ executor|debugger) MIN_TIMEOUT=300 ;;
1685
+ *) MIN_TIMEOUT=120 ;;
1686
+ esac
1687
+
1688
+ if [[ -n "$USER_TIMEOUT" ]]; then
1689
+ if ! [[ "$USER_TIMEOUT" =~ ^[1-9][0-9]*$ ]]; then
1690
+ echo "[tfx-route] 경고: 유효하지 않은 타임아웃 값 ($USER_TIMEOUT), 기본값 사용" >&2
1691
+ USER_TIMEOUT=""
1692
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
1693
+ elif [[ "$USER_TIMEOUT" -lt "$MIN_TIMEOUT" ]]; then
1694
+ echo "[tfx-route] 경고: 타임아웃 ${USER_TIMEOUT}s < 최소 ${MIN_TIMEOUT}s ($AGENT_TYPE), 최소값 적용" >&2
1695
+ TIMEOUT_SEC="$MIN_TIMEOUT"
1696
+ else
1697
+ TIMEOUT_SEC="$USER_TIMEOUT"
1698
+ fi
1699
+ else
1700
+ TIMEOUT_SEC="$DEFAULT_TIMEOUT"
1701
+ fi
1702
+
1703
+ # 컨텍스트 파일 → 프롬프트에 주입
1704
+ if [[ -n "$CONTEXT_FILE" && -f "$CONTEXT_FILE" ]]; then
1705
+ local ctx_content
1706
+ ctx_content=$(cat "$CONTEXT_FILE" 2>/dev/null | head -c 32768) # 32KB 상한
1707
+ PROMPT="${PROMPT}
1708
+
1709
+ <prior_context>
1710
+ ${ctx_content}
1711
+ </prior_context>"
1712
+ fi
1713
+
1714
+ resolve_mcp_policy
1715
+
1716
+ # Claude native는 팀 비-TTY 환경에서 subprocess wrapper를 우선 시도
1717
+ if [[ "$CLI_TYPE" == "claude-native" && -n "$TFX_TEAM_NAME" ]]; then
1718
+ if { [[ ! -t 0 ]] || [[ ! -t 1 ]]; } && command -v "$CLAUDE_BIN" &>/dev/null && resolve_worker_runner_script >/dev/null 2>&1; then
1719
+ CLI_TYPE="claude"
1720
+ CLI_CMD="$CLAUDE_BIN"
1721
+ echo "[tfx-route] non-tty 팀 환경: claude-native -> claude stream wrapper 전환" >&2
1722
+ else
1723
+ echo "[tfx-route] claude stream wrapper 미사용: native metadata 유지" >&2
1724
+ fi
1725
+ fi
1726
+
1727
+ # Claude 네이티브 에이전트는 이 스크립트로 처리 불가
1728
+ if [[ "$CLI_TYPE" == "claude-native" ]]; then
1729
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1730
+ # 팀 모드: Hub에 fallback 필요 시그널 전송 후 구조화된 출력
1731
+ echo "[tfx-route] claude-native 역할($AGENT_TYPE)은 tfx-route.sh로 실행 불가 — Claude Agent fallback 필요" >&2
1732
+ team_complete_task "fallback" "claude-native 역할 실행 불가: ${AGENT_TYPE}. Claude Task(sonnet) 에이전트로 위임하세요."
1733
+ cat <<FALLBACK_EOF
1734
+ === TFX_NEEDS_FALLBACK ===
1735
+ agent_type: ${AGENT_TYPE}
1736
+ reason: claude-native roles require Claude Agent tools (Read/Edit/Grep). tfx-route.sh cannot provide these.
1737
+ action: Lead should spawn Agent(subagent_type="${AGENT_TYPE}") for this task.
1738
+ task_id: ${TFX_TEAM_TASK_ID:-none}
1739
+ FALLBACK_EOF
1740
+ exit 0
1741
+ fi
1742
+ emit_claude_native_metadata
1743
+ exit 0
1744
+ fi
1745
+
1746
+ local FULL_PROMPT="$PROMPT"
1747
+ [[ -n "$MCP_HINT" ]] && FULL_PROMPT="${PROMPT}. ${MCP_HINT}"
1748
+ local codex_transport_effective="n/a"
1749
+
1750
+ # 메타정보 (stderr)
1751
+ echo "[tfx-route] v${VERSION} type=$CLI_TYPE agent=$AGENT_TYPE effort=$CLI_EFFORT mode=$RUN_MODE timeout=${TIMEOUT_SEC}s" >&2
1752
+ echo "[tfx-route] opus_oversight=$OPUS_OVERSIGHT mcp_profile=$MCP_PROFILE resolved_profile=$MCP_RESOLVED_PROFILE verifier_override=$TFX_VERIFIER_OVERRIDE" >&2
1753
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1754
+ echo "[tfx-route] allowed_mcp_servers=$(IFS=,; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1755
+ else
1756
+ echo "[tfx-route] allowed_mcp_servers=none" >&2
1757
+ fi
1758
+ if [[ -n "$TFX_WORKER_INDEX" || -n "$TFX_SEARCH_TOOL" ]]; then
1759
+ echo "[tfx-route] worker_index=${TFX_WORKER_INDEX:-auto} search_tool=${TFX_SEARCH_TOOL:-auto}" >&2
1760
+ fi
1761
+ if [[ "$CLI_TYPE" == "codex" ]]; then
1762
+ echo "[tfx-route] codex_transport_request=$TFX_CODEX_TRANSPORT" >&2
1763
+ fi
1764
+ [[ -n "$TFX_TEAM_NAME" ]] && echo "[tfx-route] team=$TFX_TEAM_NAME task=$TFX_TEAM_TASK_ID agent=$TFX_TEAM_AGENT_NAME" >&2
1765
+ [[ -n "${TFX_REROUTED_FROM:-}" ]] && echo "[tfx-route] rerouted_from=$TFX_REROUTED_FROM" >&2
1766
+
1767
+ # Per-process 에이전트 등록
1768
+ register_agent
1769
+
1770
+ # 팀 모드: task claim
1771
+ team_claim_task
1772
+ team_send_message "작업 시작: ${TFX_TEAM_AGENT_NAME}" "task ${TFX_TEAM_TASK_ID} started"
1773
+
1774
+ # CLI 실행 (stderr 분리 + 타임아웃 + 소요시간 측정)
1775
+ local exit_code=0
1776
+ local start_time
1777
+ start_time=$(date +%s)
1778
+ local workspace_signature_before=""
1779
+ local workspace_signature_after=""
1780
+ local workspace_probe_supported=false
1781
+ if workspace_signature_before=$(capture_workspace_signature); then
1782
+ workspace_probe_supported=true
1783
+ fi
1784
+
1785
+ # tee 활성화 조건: 팀 모드 + 실제 터미널(TTY/tmux)
1786
+ # Agent 래퍼 안에서는 가상 stdout 캡처로 tee 출력이 사용자에게 안 보임 → 파일 전용
1787
+ # 실시간 모니터링은 Shift+Down으로 워커 pane 전환 권장
1788
+ local use_tee=false
1789
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1790
+ if [[ -t 1 ]] || [[ -n "${TMUX:-}" ]]; then
1791
+ use_tee=true
1792
+ fi
1793
+ fi
1794
+
1795
+ if [[ "$CLI_TYPE" == "codex" ]]; then
1796
+ codex_transport_effective="exec"
1797
+ if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
1798
+ run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
1799
+ if [[ "$exit_code" -eq 0 ]]; then
1800
+ codex_transport_effective="mcp"
1801
+ elif [[ "$exit_code" -eq "$CODEX_MCP_TRANSPORT_EXIT_CODE" && "$TFX_CODEX_TRANSPORT" == "auto" ]]; then
1802
+ echo "[tfx-route] Codex MCP bootstrap 실패(exit=${exit_code}). legacy exec 경로로 fallback합니다." >&2
1803
+ : > "$STDOUT_LOG"
1804
+ : > "$STDERR_LOG"
1805
+ exit_code=0
1806
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
1807
+ codex_transport_effective="exec-fallback"
1808
+ else
1809
+ codex_transport_effective="mcp"
1810
+ fi
1811
+ else
1812
+ run_codex_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
1813
+ codex_transport_effective="exec"
1814
+ fi
1815
+ echo "[tfx-route] codex_transport_effective=$codex_transport_effective" >&2
1816
+
1817
+ elif [[ "$CLI_TYPE" == "gemini" ]]; then
1818
+ local gemini_model
1819
+ gemini_model=$(awk '{
1820
+ for (i = 1; i <= NF; i++) {
1821
+ if ($i == "-m" || $i == "--model") {
1822
+ print $(i + 1)
1823
+ exit
1824
+ }
1825
+ }
1826
+ }' <<< "$CLI_ARGS")
1827
+ local -a gemini_worker_args=(
1828
+ "--command" "$CLI_CMD"
1829
+ "--command-args-json" "$GEMINI_BIN_ARGS_JSON"
1830
+ "--model" "$gemini_model"
1831
+ "--approval-mode" "yolo"
1832
+ )
1833
+
1834
+ if [[ ${#GEMINI_ALLOWED_SERVERS[@]} -gt 0 ]]; then
1835
+ echo "[tfx-route] Gemini MCP 서버: $(IFS=' '; echo "${GEMINI_ALLOWED_SERVERS[*]}")" >&2
1836
+ local server_name
1837
+ for server_name in "${GEMINI_ALLOWED_SERVERS[@]}"; do
1838
+ gemini_worker_args+=("--allowed-mcp-server-name" "$server_name")
1839
+ done
1840
+ fi
1841
+
1842
+ run_stream_worker "gemini" "$FULL_PROMPT" "$use_tee" "${gemini_worker_args[@]}" || exit_code=$?
1843
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1844
+ echo "[tfx-route] Gemini stream wrapper 실패(exit=${exit_code}). legacy CLI 경로로 fallback합니다." >&2
1845
+ : > "$STDOUT_LOG"
1846
+ : > "$STDERR_LOG"
1847
+ exit_code=0
1848
+ run_legacy_gemini "$FULL_PROMPT" "$use_tee" || exit_code=$?
1849
+ fi
1850
+
1851
+ elif [[ "$CLI_TYPE" == "claude" ]]; then
1852
+ local claude_model
1853
+ claude_model=$(get_claude_model)
1854
+ local -a claude_worker_args=(
1855
+ "--command" "$CLI_CMD"
1856
+ "--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
1857
+ "--model" "$claude_model"
1858
+ "--permission-mode" "bypassPermissions"
1859
+ "--allow-dangerously-skip-permissions"
1860
+ )
1861
+
1862
+ run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
1863
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1864
+ echo "[tfx-route] Claude stream wrapper 실패(exit=${exit_code}). native metadata로 fallback합니다." >&2
1865
+ cat > "$STDOUT_LOG" <<EOF
1866
+ $(emit_claude_native_metadata)
1867
+ EOF
1868
+ : > "$STDERR_LOG"
1869
+ exit_code=0
1870
+ CLI_TYPE="claude-native"
1871
+ fi
1872
+ fi
1873
+
1874
+ local end_time
1875
+ end_time=$(date +%s)
1876
+ local elapsed=$((end_time - start_time))
1877
+
1878
+ if [[ "$exit_code" -eq 0 ]]; then
1879
+ local workspace_changed="unknown"
1880
+ if [[ "$workspace_probe_supported" == "true" ]]; then
1881
+ if workspace_signature_after=$(capture_workspace_signature); then
1882
+ if [[ "$workspace_signature_before" != "$workspace_signature_after" ]]; then
1883
+ workspace_changed="yes"
1884
+ else
1885
+ workspace_changed="no"
1886
+ fi
1887
+ fi
1888
+ fi
1889
+
1890
+ if [[ ! -s "$STDOUT_LOG" && "$workspace_changed" == "no" ]]; then
1891
+ printf '%s\n' "[tfx-route] exit 0 이지만 stdout 비어있고 워크스페이스 변화가 없습니다. no-op 성공을 실패로 승격합니다." >> "$STDERR_LOG"
1892
+ exit_code=68
1893
+ fi
1894
+ fi
1895
+
1896
+ # 쿼타 감지 + 자동 re-route
1897
+ if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
1898
+ if [[ "${TFX_QUOTA_REROUTE:-1}" -ne 0 ]] && [[ -z "${TFX_REROUTED_FROM:-}" ]] && detect_quota_exceeded "$STDOUT_LOG" "$STDERR_LOG"; then
1899
+ export TFX_REROUTED_FROM="$CLI_TYPE"
1900
+ auto_reroute "$CLI_TYPE"
1901
+ fi
1902
+ fi
1903
+
1904
+ # 팀 모드: task complete + 리드 보고
1905
+ if [[ -n "$TFX_TEAM_NAME" ]]; then
1906
+ if [[ "$exit_code" -eq 0 ]]; then
1907
+ local output_preview
1908
+ output_preview=$(head -c 2048 "$STDOUT_LOG" 2>/dev/null || echo "출력 없음")
1909
+ team_complete_task "success" "$output_preview"
1910
+ elif [[ "$exit_code" -eq 124 ]]; then
1911
+ team_complete_task "timeout" "타임아웃 (${TIMEOUT_SEC}초)"
1912
+ else
1913
+ local err_preview
1914
+ err_preview=$(tail -c 1024 "$STDERR_LOG" 2>/dev/null || echo "에러 정보 없음")
1915
+ team_complete_task "failed" "exit_code=${exit_code}: ${err_preview}"
1916
+ fi
1917
+ fi
1918
+
1919
+ # ── 후처리: 단일 node 프로세스로 위임 ──
1920
+ # 토큰 추출, 출력 필터링, 로그, 토큰 누적, AIMD, 이슈 추적, 결과 출력 전부 처리
1921
+ local post_script="${HOME}/.claude/scripts/tfx-route-post.mjs"
1922
+ if [[ -f "$post_script" ]]; then
1923
+ node "$post_script" \
1924
+ --agent "$AGENT_TYPE" \
1925
+ --cli "$CLI_TYPE" \
1926
+ --cli-cmd "$CLI_CMD" \
1927
+ --effort "$CLI_EFFORT" \
1928
+ --run-mode "$RUN_MODE" \
1929
+ --opus "$OPUS_OVERSIGHT" \
1930
+ --exit-code "$exit_code" \
1931
+ --elapsed "$elapsed" \
1932
+ --timeout "$TIMEOUT_SEC" \
1933
+ --mcp-profile "$MCP_PROFILE" \
1934
+ --stderr-log "$STDERR_LOG" \
1935
+ --stdout-log "$STDOUT_LOG" \
1936
+ --rerouted-from "${TFX_REROUTED_FROM:-}" \
1937
+ --max-bytes "$MAX_STDOUT_BYTES" \
1938
+ --tee-active "$use_tee" \
1939
+ --clean-tui "${TFX_CLEAN_TUI:-true}"
1940
+ else
1941
+ # post.mjs 없으면 기본 출력 (fallback)
1942
+ echo "=== TFX-ROUTE RESULT ==="
1943
+ echo "agent: $AGENT_TYPE"
1944
+ echo "cli: $CLI_TYPE"
1945
+ [[ -n "${TFX_REROUTED_FROM:-}" ]] && echo "rerouted_from: $TFX_REROUTED_FROM"
1946
+ echo "exit_code: $exit_code"
1947
+ echo "elapsed: ${elapsed}s"
1948
+ echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
1949
+ echo "=== OUTPUT ==="
1950
+ if [[ "${TFX_CLEAN_TUI:-1}" != "0" ]]; then
1951
+ cat "$STDOUT_LOG" 2>/dev/null \
1952
+ | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
1953
+ | sed '/^[[:space:]]*[╭╮╰╯│─┌┐└┘├┤┬┴┼]/d' \
1954
+ | sed '/^[[:space:]]*[›❯][[:space:]]*$/d' \
1955
+ | head -c "$MAX_STDOUT_BYTES"
1956
+ else
1957
+ cat "$STDOUT_LOG" 2>/dev/null | head -c "$MAX_STDOUT_BYTES"
1958
+ fi
1959
+ fi
1960
+
1961
+ # 결과를 파일에도 저장 — run_in_background에서 TaskOutput이 stdout을 놓칠 때 대비
1962
+ local result_file="${TFX_TMP}/tfx-route-${AGENT_TYPE}-${RUN_ID}-result.log"
1963
+ {
1964
+ echo "agent: $AGENT_TYPE"
1965
+ echo "cli: $CLI_TYPE"
1966
+ echo "exit_code: $exit_code"
1967
+ echo "elapsed: ${elapsed}s"
1968
+ echo "status: $([ $exit_code -eq 0 ] && echo success || echo failed)"
1969
+ echo "stdout_log: $STDOUT_LOG"
1970
+ echo "result_file: $result_file"
1971
+ } > "$result_file" 2>/dev/null
1972
+ echo "[tfx-route] result_file=$result_file" >&2
1973
+
1974
+ return "$exit_code"
1975
+ }
1976
+
1977
+ # ── Async 모드: 백그라운드 실행 + 즉시 job_id 반환 ──
1978
+ if [[ "$TFX_ASYNC_MODE" -eq 1 ]]; then
1979
+ mkdir -p "$TFX_JOBS_DIR"
1980
+ JOB_ID="$TIMESTAMP-$$-${RANDOM}"
1981
+ JOB_DIR="$TFX_JOBS_DIR/$JOB_ID"
1982
+ mkdir -p "$JOB_DIR"
1983
+ echo "$AGENT_TYPE" > "$JOB_DIR/agent_type"
1984
+ date +%s > "$JOB_DIR/start_time"
1985
+
1986
+ # 백그라운드 서브쉘: main 실행 → 결과 저장
1987
+ echo "starting" > "$JOB_DIR/pid"
1988
+ (
1989
+ set +e # main 내부 에러가 exit_code 기록 전에 서브쉘을 죽이는 것 방지
1990
+ exec > "$JOB_DIR/result.log" 2>"$JOB_DIR/stderr.log"
1991
+ main; _ec=$?
1992
+ echo "$_ec" > "$JOB_DIR/exit_code"
1993
+ touch "$JOB_DIR/done"
1994
+ ) &
1995
+ bg_pid=$!
1996
+ echo "$bg_pid" > "$JOB_DIR/pid"
1997
+
1998
+ # 종료 감지 데몬 (main이 signal/crash로 죽어도 done 마커 생성)
1999
+ (
2000
+ wait "$bg_pid" 2>/dev/null
2001
+ ec=$?
2002
+ if [[ ! -f "$JOB_DIR/done" ]]; then
2003
+ echo "$ec" > "$JOB_DIR/exit_code"
2004
+ touch "$JOB_DIR/done"
2005
+ fi
2006
+ ) &
2007
+ disown
2008
+
2009
+ # 즉시 리턴: 1초 이내에 Claude Code Bash 도구 완료
2010
+ echo "$JOB_ID"
2011
+ exit 0
2012
+ fi
2013
+
2014
+ main