triflux 10.4.0 → 10.7.0

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 (391) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/bin/tfx-doctor-tui.mjs +1 -1
  4. package/bin/tfx-doctor.mjs +6 -1
  5. package/bin/tfx-profile.mjs +1 -1
  6. package/bin/tfx-setup-tui.mjs +1 -1
  7. package/bin/tfx-setup.mjs +6 -1
  8. package/bin/triflux.mjs +2396 -1140
  9. package/config/mcp-registry.json +29 -0
  10. package/hooks/agent-route-guard.mjs +12 -8
  11. package/hooks/cross-review-tracker.mjs +21 -8
  12. package/hooks/error-context.mjs +19 -7
  13. package/hooks/hook-adaptive-collector.mjs +18 -16
  14. package/hooks/hook-manager.mjs +93 -32
  15. package/hooks/hook-orchestrator.mjs +108 -24
  16. package/hooks/hook-registry.json +267 -256
  17. package/hooks/keyword-rules.json +6 -10
  18. package/hooks/lib/resolve-root.mjs +1 -1
  19. package/hooks/mcp-config-watcher.mjs +6 -2
  20. package/hooks/pipeline-stop.mjs +3 -6
  21. package/hooks/safety-guard.mjs +99 -28
  22. package/hooks/session-start-fast.mjs +143 -0
  23. package/hooks/subagent-verifier.mjs +5 -4
  24. package/hub/account-broker.mjs +256 -60
  25. package/hub/adaptive-diagnostic.mjs +75 -48
  26. package/hub/adaptive-inject.mjs +96 -58
  27. package/hub/adaptive-memory.mjs +156 -42
  28. package/hub/adaptive.mjs +60 -31
  29. package/hub/assign-callbacks.mjs +163 -126
  30. package/hub/bridge.mjs +0 -1
  31. package/hub/cli-adapter-base.mjs +200 -48
  32. package/hub/codex-adapter.mjs +76 -96
  33. package/hub/codex-compat.mjs +3 -3
  34. package/hub/codex-preflight.mjs +63 -37
  35. package/hub/delegator/contracts.mjs +19 -23
  36. package/hub/delegator/index.mjs +14 -14
  37. package/hub/delegator/service.mjs +88 -64
  38. package/hub/delegator/tool-definitions.mjs +35 -35
  39. package/hub/fullcycle.mjs +33 -17
  40. package/hub/gemini-adapter.mjs +69 -94
  41. package/hub/hitl.mjs +202 -143
  42. package/hub/intent.mjs +161 -38
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/mcp-response-cache.mjs +66 -32
  45. package/hub/lib/memory-store.mjs +285 -111
  46. package/hub/lib/path-utils.mjs +35 -37
  47. package/hub/lib/process-utils.mjs +106 -37
  48. package/hub/lib/spawn-trace.mjs +527 -0
  49. package/hub/lib/ssh-command.mjs +34 -4
  50. package/hub/lib/ssh-retry.mjs +5 -1
  51. package/hub/lib/uuidv7.mjs +4 -3
  52. package/hub/memory-doctor.mjs +266 -106
  53. package/hub/middleware/request-logger.mjs +61 -34
  54. package/hub/paths.mjs +9 -9
  55. package/hub/pipeline/gates/confidence.mjs +34 -15
  56. package/hub/pipeline/gates/consensus.mjs +27 -15
  57. package/hub/pipeline/gates/index.mjs +7 -3
  58. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  59. package/hub/pipeline/index.mjs +77 -42
  60. package/hub/pipeline/state.mjs +10 -10
  61. package/hub/pipeline/transitions.mjs +40 -23
  62. package/hub/platform.mjs +57 -48
  63. package/hub/promote-penalties.mjs +25 -7
  64. package/hub/quality/deslop.mjs +70 -49
  65. package/hub/research.mjs +32 -25
  66. package/hub/router.mjs +924 -791
  67. package/hub/routing/complexity.mjs +132 -29
  68. package/hub/routing/index.mjs +17 -12
  69. package/hub/routing/q-learning.mjs +76 -28
  70. package/hub/server.mjs +4 -4
  71. package/hub/session-fingerprint.mjs +127 -61
  72. package/hub/state.mjs +84 -43
  73. package/hub/store-adapter.mjs +59 -26
  74. package/hub/store.mjs +356 -153
  75. package/hub/team/agent-map.json +22 -7
  76. package/hub/team/ansi.mjs +186 -122
  77. package/hub/team/backend.mjs +28 -10
  78. package/hub/team/cli/commands/attach.mjs +57 -37
  79. package/hub/team/cli/commands/control.mjs +29 -8
  80. package/hub/team/cli/commands/debug.mjs +95 -74
  81. package/hub/team/cli/commands/focus.mjs +80 -53
  82. package/hub/team/cli/commands/interrupt.mjs +18 -6
  83. package/hub/team/cli/commands/kill.mjs +16 -5
  84. package/hub/team/cli/commands/list.mjs +31 -24
  85. package/hub/team/cli/commands/send.mjs +19 -6
  86. package/hub/team/cli/commands/start/index.mjs +154 -31
  87. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  88. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  89. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  90. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  91. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  92. package/hub/team/cli/commands/status.mjs +43 -14
  93. package/hub/team/cli/commands/stop.mjs +11 -4
  94. package/hub/team/cli/commands/task.mjs +8 -3
  95. package/hub/team/cli/commands/tasks.mjs +13 -13
  96. package/hub/team/cli/index.mjs +2 -2
  97. package/hub/team/cli/manifest.mjs +38 -8
  98. package/hub/team/cli/render.mjs +52 -30
  99. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  100. package/hub/team/cli/services/hub-client.mjs +42 -14
  101. package/hub/team/cli/services/member-selector.mjs +37 -30
  102. package/hub/team/cli/services/native-control.mjs +143 -116
  103. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  104. package/hub/team/cli/services/state-store.mjs +25 -8
  105. package/hub/team/cli/services/task-model.mjs +40 -30
  106. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  107. package/hub/team/conductor.mjs +8 -4
  108. package/hub/team/dashboard-anchor.mjs +4 -5
  109. package/hub/team/dashboard-layout.mjs +3 -1
  110. package/hub/team/dashboard-open.mjs +41 -21
  111. package/hub/team/dashboard.mjs +76 -28
  112. package/hub/team/event-log.mjs +18 -10
  113. package/hub/team/handoff.mjs +31 -15
  114. package/hub/team/headless.mjs +2 -1
  115. package/hub/team/health-probe.mjs +69 -54
  116. package/hub/team/launcher-template.mjs +16 -13
  117. package/hub/team/native-supervisor.mjs +65 -21
  118. package/hub/team/native.mjs +74 -35
  119. package/hub/team/nativeProxy.mjs +184 -113
  120. package/hub/team/notify.mjs +120 -77
  121. package/hub/team/orchestrator.mjs +166 -161
  122. package/hub/team/pane.mjs +12 -7
  123. package/hub/team/process-cleanup.mjs +25 -16
  124. package/hub/team/psmux.mjs +491 -201
  125. package/hub/team/remote-probe.mjs +68 -52
  126. package/hub/team/remote-session.mjs +117 -59
  127. package/hub/team/remote-watcher.mjs +61 -33
  128. package/hub/team/routing.mjs +51 -25
  129. package/hub/team/runtime-strategy.mjs +3 -1
  130. package/hub/team/session.mjs +675 -611
  131. package/hub/team/shared.mjs +13 -13
  132. package/hub/team/staleState.mjs +72 -30
  133. package/hub/team/swarm-locks.mjs +15 -13
  134. package/hub/team/swarm-planner.mjs +32 -21
  135. package/hub/team/swarm-reconciler.mjs +48 -23
  136. package/hub/team/tui-lite.mjs +266 -68
  137. package/hub/team/tui-remote-adapter.mjs +14 -10
  138. package/hub/team/tui-viewer.mjs +99 -43
  139. package/hub/team/tui.mjs +708 -271
  140. package/hub/team/worktree-lifecycle.mjs +152 -58
  141. package/hub/team/wt-manager.mjs +24 -14
  142. package/hub/token-mode.mjs +71 -71
  143. package/hub/tray.mjs +411 -368
  144. package/hub/workers/claude-worker.mjs +162 -118
  145. package/hub/workers/codex-mcp.mjs +558 -507
  146. package/hub/workers/delegator-mcp.mjs +507 -333
  147. package/hub/workers/factory.mjs +21 -21
  148. package/hub/workers/gemini-worker.mjs +115 -84
  149. package/hub/workers/interface.mjs +6 -1
  150. package/hub/workers/worker-utils.mjs +21 -14
  151. package/hud/colors.mjs +27 -9
  152. package/hud/constants.mjs +162 -26
  153. package/hud/context-monitor.mjs +82 -41
  154. package/hud/hud-qos-status.mjs +129 -49
  155. package/hud/mission-board.mjs +6 -3
  156. package/hud/providers/claude.mjs +226 -115
  157. package/hud/providers/codex.mjs +62 -22
  158. package/hud/providers/gemini.mjs +168 -56
  159. package/hud/renderers.mjs +384 -119
  160. package/hud/terminal.mjs +101 -31
  161. package/hud/utils.mjs +78 -38
  162. package/mesh/index.mjs +11 -5
  163. package/mesh/mesh-budget.mjs +18 -9
  164. package/mesh/mesh-heartbeat.mjs +1 -1
  165. package/mesh/mesh-queue.mjs +3 -5
  166. package/mesh/mesh-router.mjs +5 -4
  167. package/package.json +56 -21
  168. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  169. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  170. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  171. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  172. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  173. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  174. package/scripts/__tests__/skill-template.test.mjs +98 -50
  175. package/scripts/__tests__/smoke.test.mjs +1 -1
  176. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  177. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  178. package/scripts/cache-doctor.mjs +11 -4
  179. package/scripts/cache-warmup.mjs +96 -37
  180. package/scripts/claudemd-sync.mjs +27 -17
  181. package/scripts/codex-gateway-preflight.mjs +52 -37
  182. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +139 -120
  193. package/scripts/keyword-detector.mjs +293 -272
  194. package/scripts/keyword-rules-expander.mjs +538 -521
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +669 -554
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +61 -57
  229. package/scripts/tfx-batch-stats.mjs +117 -96
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/idle-notif-cooldown.json +1 -1
  237. package/skills/.omc/state/last-tool-error.json +1 -1
  238. package/skills/.omc/state/subagent-tracking.json +1 -1
  239. package/skills/_templates/base.md +1 -6
  240. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  241. package/skills/shared/telemetry-segment.md +6 -0
  242. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  243. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  244. package/skills/tfx-analysis/skill.json +1 -6
  245. package/skills/tfx-auto/SKILL.md +1 -0
  246. package/skills/{tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md → tfx-auto-codex/SKILL.md.tmpl} +33 -4
  247. package/skills/tfx-auto-codex/skill.json +1 -3
  248. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  249. package/skills/tfx-autopilot/skill.json +1 -5
  250. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  251. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  252. package/skills/tfx-autoroute/skill.json +1 -7
  253. package/skills/tfx-codex/SKILL.md +1 -0
  254. package/skills/tfx-codex/skill.json +1 -3
  255. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  256. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  257. package/skills/tfx-codex-swarm/skill.json +1 -4
  258. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  265. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  266. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  267. package/skills/tfx-debate/skill.json +1 -7
  268. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  269. package/skills/tfx-deep-analysis/skill.json +1 -5
  270. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  271. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  272. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  273. package/skills/tfx-deep-qa/skill.json +1 -6
  274. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  275. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  276. package/skills/tfx-doctor/SKILL.md +21 -0
  277. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  278. package/skills/tfx-doctor/skill.json +1 -3
  279. package/skills/tfx-find/SKILL.md +1 -0
  280. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  281. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  282. package/skills/tfx-fullcycle/skill.json +1 -6
  283. package/skills/{tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md → tfx-gemini/SKILL.md.tmpl} +16 -7
  284. package/skills/tfx-gemini/skill.json +1 -3
  285. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  286. package/skills/tfx-hooks/skill.json +1 -3
  287. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  288. package/skills/tfx-hub/skill.json +1 -3
  289. package/skills/tfx-index/SKILL.md +1 -0
  290. package/skills/tfx-index/skill.json +1 -6
  291. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  292. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  293. package/skills/tfx-multi/skill.json +1 -3
  294. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  295. package/skills/tfx-panel/skill.json +1 -7
  296. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  297. package/skills/tfx-persist/skill.json +1 -7
  298. package/skills/tfx-plan/SKILL.md +1 -0
  299. package/skills/tfx-plan/skill.json +1 -6
  300. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  301. package/skills/tfx-profile/skill.json +1 -3
  302. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  303. package/skills/tfx-prune/skill.json +1 -7
  304. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  305. package/skills/tfx-psmux-rules/skill.json +1 -4
  306. package/skills/tfx-qa/SKILL.md +1 -0
  307. package/skills/tfx-qa/skill.json +1 -6
  308. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  309. package/skills/tfx-ralph/skill.json +1 -4
  310. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  311. package/skills/tfx-remote-setup/skill.json +1 -3
  312. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  313. package/skills/tfx-remote-spawn/references/hosts.json +16 -16
  314. package/skills/tfx-remote-spawn/skill.json +1 -4
  315. package/skills/tfx-research/SKILL.md +1 -0
  316. package/skills/tfx-review/SKILL.md +1 -0
  317. package/skills/tfx-review/skill.json +1 -6
  318. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  319. package/skills/tfx-setup/skill.json +1 -3
  320. package/skills/tfx-swarm/SKILL.md +22 -0
  321. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  322. package/tui/codex-profile.mjs +457 -0
  323. package/tui/core.mjs +266 -0
  324. package/tui/doctor.mjs +375 -0
  325. package/tui/gemini-profile.mjs +299 -0
  326. package/tui/monitor-data.mjs +152 -0
  327. package/tui/monitor.mjs +339 -0
  328. package/tui/setup.mjs +533 -0
  329. package/CLAUDE.md +0 -193
  330. package/references/hosts.json +0 -33
  331. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  332. package/skills/tfx-workspace/evals/evals.json +0 -79
  333. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -162
  334. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  335. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -9
  336. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  337. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  338. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -9
  339. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  340. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  341. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  342. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -9
  343. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  344. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  345. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -9
  346. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  347. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  348. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  349. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -8
  350. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  351. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  352. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -8
  353. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  354. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  355. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  356. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -10
  357. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  358. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  359. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -10
  360. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  361. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  362. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  363. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  364. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -10
  365. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  366. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  367. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -10
  368. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  369. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  370. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  371. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -10
  372. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  373. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  374. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -10
  375. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  376. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  377. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -62
  378. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  379. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -11
  380. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  381. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  382. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -11
  383. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  384. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  385. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  386. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  387. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  388. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  389. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  390. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  391. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
4
  * headless-guard.mjs — PreToolUse 훅 (상시 활성 auto-route)
4
5
  *
@@ -24,11 +25,11 @@
24
25
  * 성능: psmux 감지 결과를 5분간 캐시 ($TMPDIR/tfx-psmux-check.json)
25
26
  */
26
27
 
27
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
28
28
  import { execFileSync } from "node:child_process";
29
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
29
30
  import { tmpdir } from "node:os";
30
31
  import { join } from "node:path";
31
- import { nudge, deny } from "./lib/hook-utils.mjs";
32
+ import { deny, nudge } from "./lib/hook-utils.mjs";
32
33
  import { probePsmuxSupport } from "./lib/psmux-info.mjs";
33
34
 
34
35
  const CACHE_FILE = join(tmpdir(), "tfx-psmux-check.json");
@@ -37,8 +38,8 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
37
38
  // ── tfx-multi 상태 관리 (A+B) ──
38
39
  const MULTI_STATE_FILE = join(tmpdir(), "tfx-multi-state.json");
39
40
  const MULTI_EXPIRE_MS = 30 * 60 * 1000; // 30분 자동 만료
40
- const GATE_THRESHOLD = 2; // A: dispatch 전 허용할 Agent 호출 수
41
- const NUDGE_THRESHOLD = 4; // B: dispatch 후 nudge 트리거 횟수
41
+ const GATE_THRESHOLD = 2; // A: dispatch 전 허용할 Agent 호출 수
42
+ const NUDGE_THRESHOLD = 4; // B: dispatch 후 nudge 트리거 횟수
42
43
 
43
44
  function readMultiState() {
44
45
  try {
@@ -47,17 +48,25 @@ function readMultiState() {
47
48
  if (!state.active) return null;
48
49
  // 자동 만료
49
50
  if (Date.now() - state.activatedAt > MULTI_EXPIRE_MS) {
50
- try { unlinkSync(MULTI_STATE_FILE); } catch { /* ignore */ }
51
+ try {
52
+ unlinkSync(MULTI_STATE_FILE);
53
+ } catch {
54
+ /* ignore */
55
+ }
51
56
  return null;
52
57
  }
53
58
  return state;
54
- } catch { return null; }
59
+ } catch {
60
+ return null;
61
+ }
55
62
  }
56
63
 
57
64
  function writeMultiState(state) {
58
65
  try {
59
66
  writeFileSync(MULTI_STATE_FILE, JSON.stringify(state));
60
- } catch { /* ignore */ }
67
+ } catch {
68
+ /* ignore */
69
+ }
61
70
  }
62
71
 
63
72
  function isPsmuxInstalled() {
@@ -69,15 +78,27 @@ function isPsmuxInstalled() {
69
78
  if (age >= 0 && age < CACHE_TTL_MS) return cache.ok;
70
79
  // age < 0 → 미래 ts (오염) → 캐시 무시하고 재검사
71
80
  }
72
- } catch { /* cache miss */ }
81
+ } catch {
82
+ /* cache miss */
83
+ }
73
84
 
74
85
  const probe = probePsmuxSupport({ execFileSyncFn: execFileSync });
75
86
  const ok = probe.ok;
76
87
 
77
88
  // 캐시 저장
78
89
  try {
79
- writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), ok, version: probe.version, missingCommands: probe.missingCommands }));
80
- } catch { /* ignore */ }
90
+ writeFileSync(
91
+ CACHE_FILE,
92
+ JSON.stringify({
93
+ ts: Date.now(),
94
+ ok,
95
+ version: probe.version,
96
+ missingCommands: probe.missingCommands,
97
+ }),
98
+ );
99
+ } catch {
100
+ /* ignore */
101
+ }
81
102
 
82
103
  return ok;
83
104
  }
@@ -116,7 +137,7 @@ function parseRouteCommand(cmd) {
116
137
  // v3: 원본 명령에서 추가 플래그 추출
117
138
  const flags = {};
118
139
  const afterPrompt = cmd.replace(/'.+?'/gs, "").replace(/".+?"/gs, "");
119
- const timeoutMatch = afterPrompt.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
140
+ const timeoutMatch = afterPrompt.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
120
141
  if (timeoutMatch) flags.timeout = parseInt(timeoutMatch[1], 10);
121
142
 
122
143
  // 환경변수 기반 글로벌 플래그
@@ -127,18 +148,20 @@ function parseRouteCommand(cmd) {
127
148
  }
128
149
 
129
150
  function autoRoute(updatedCommand, reason) {
130
- process.stdout.write(JSON.stringify({
131
- hookSpecificOutput: {
132
- hookEventName: "PreToolUse",
133
- updatedInput: { command: updatedCommand },
134
- additionalContext: reason,
135
- },
136
- }));
151
+ process.stdout.write(
152
+ JSON.stringify({
153
+ hookSpecificOutput: {
154
+ hookEventName: "PreToolUse",
155
+ updatedInput: { command: updatedCommand },
156
+ additionalContext: reason,
157
+ },
158
+ }),
159
+ );
137
160
  process.exit(0);
138
161
  }
139
162
 
140
163
  const HEADLESS_FALLBACK_COMMAND =
141
- 'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")';
164
+ "Bash(\"tfx multi --teammate-mode headless --assign 'codex:prompt:role' ...\")";
142
165
  const DIRECT_CLI_BYPASS_HINT =
143
166
  "로컬 디버깅이 목적이면 TFX_ALLOW_DIRECT_CLI=1로 일시 우회할 수 있습니다.";
144
167
 
@@ -153,7 +176,7 @@ async function main() {
153
176
  for await (const chunk of process.stdin) raw += chunk;
154
177
 
155
178
  if (!raw?.trim()) {
156
- console.error('[headless-guard] stdin이 비어있습니다 — 기본 허용');
179
+ console.error("[headless-guard] stdin이 비어있습니다 — 기본 허용");
157
180
  process.exit(0);
158
181
  }
159
182
 
@@ -192,8 +215,8 @@ async function main() {
192
215
  if (/\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd)) {
193
216
  deny(
194
217
  "[headless-guard] psmux send-keys/split-window에 codex/gemini 직접 호출이 포함되어 있습니다. " +
195
- `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
196
- DIRECT_CLI_BYPASS_HINT,
218
+ `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
219
+ DIRECT_CLI_BYPASS_HINT,
197
220
  );
198
221
  }
199
222
  process.exit(0);
@@ -205,7 +228,7 @@ async function main() {
205
228
  // #37 Bug4: gh/git 명령은 본문에 codex/gemini 문자열이 있어도 차단하지 않음
206
229
  const SAFE_CMD_RE = /^\s*(?:[\w_]+=\S+\s+)*\s*(gh|git)\b/;
207
230
  const cmdParts = cmd.split(/\s*(?:&&|\|\||\||;)\s*/);
208
- let hasDirectCli = cmdParts.some(part => {
231
+ let hasDirectCli = cmdParts.some((part) => {
209
232
  // gh/git 세그먼트는 건너뜀 (이슈 본문/커밋 메시지 내 codex/gemini 언급은 정상)
210
233
  if (SAFE_CMD_RE.test(part)) return false;
211
234
  // 1단계: env var prefix 제거 (FOO=bar ...)
@@ -214,24 +237,31 @@ async function main() {
214
237
  .replace(/^\s*(?:[\w_]+=\S+\s+)*/, "")
215
238
  .replace(/^\s*(?:env|command|nohup)\s+/, "")
216
239
  .replace(/^\s*timeout\s+\d+\s+/, "")
217
- .replace(/^\s*(?:bash|sh)\s+(?:-\w+\s+)*(?:"([^"]*)".*|'([^']*)'.*)/i, "$1$2")
240
+ .replace(
241
+ /^\s*(?:bash|sh)\s+(?:-\w+\s+)*(?:"([^"]*)".*|'([^']*)'.*)/i,
242
+ "$1$2",
243
+ )
218
244
  .replace(/^\s*(?:\/[\w./+-]+\/)(codex|gemini)\b/, " $1");
219
- return /^\s*codex\b.*\bexec\b/i.test(stripped) || /^\s*gemini\s+(-p|--prompt)\b/i.test(stripped);
245
+ return (
246
+ /^\s*codex\b.*\bexec\b/i.test(stripped) ||
247
+ /^\s*gemini\s+(-p|--prompt)\b/i.test(stripped)
248
+ );
220
249
  });
221
250
  // 2차 휴리스틱: 1차 세그먼트 검사를 통과한 간접 실행 패턴 탐지
222
251
  // full AST 파서 대신 현실적 위협 벡터만 커버 — eval, subshell, variable 확장
223
252
  // 2차 휴리스틱: 간접 실행 패턴 탐지 (eval, subshell, variable 확장)
224
253
  if (!hasDirectCli) {
225
- const isAllSafeCmd = cmdParts.every(p => SAFE_CMD_RE.test(p));
254
+ const isAllSafeCmd = cmdParts.every((p) => SAFE_CMD_RE.test(p));
226
255
  if (isAllSafeCmd) {
227
256
  // gh/git 전용: $(codex exec ...) 직접 명령 치환만 차단
228
257
  // $(cat <<'EOF'\n...codex exec text...\nEOF) 같은 heredoc 텍스트는 허용
229
- hasDirectCli = /\$\(\s*(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd);
258
+ hasDirectCli = /\$\(\s*(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(
259
+ cmd,
260
+ );
230
261
  } else {
231
- hasDirectCli = (
262
+ hasDirectCli =
232
263
  /\beval\b.*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd) ||
233
- /\$[({].*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd)
234
- );
264
+ /\$[({].*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd);
235
265
  }
236
266
  }
237
267
 
@@ -242,15 +272,17 @@ async function main() {
242
272
  }
243
273
  deny(
244
274
  "[headless-guard] codex/gemini 직접 호출(파이프/복합 명령 포함)은 headless-guard에서 차단됩니다. " +
245
- `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
246
- DIRECT_CLI_BYPASS_HINT,
275
+ `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
276
+ DIRECT_CLI_BYPASS_HINT,
247
277
  );
248
278
  }
249
279
 
250
280
  // tfx-route.sh 실행만 감지: 명령이 bash로 시작할 때만 (커밋 메시지/echo 등 무시)
251
281
  if (/^\s*bash\s+.*tfx-route\.sh\s/.test(cmd)) {
252
282
  // --async, --job-status, --job-result, --job-wait는 tfx-route.sh 내부 플래그 → 통과
253
- if (/tfx-route\.sh\s+--(async|job-status|job-result|job-wait)\b/.test(cmd)) {
283
+ if (
284
+ /tfx-route\.sh\s+--(async|job-status|job-result|job-wait)\b/.test(cmd)
285
+ ) {
254
286
  process.exit(0);
255
287
  }
256
288
 
@@ -261,7 +293,7 @@ async function main() {
261
293
  if (!process.env.TFX_FORCE_HEADLESS) {
262
294
  const isMultiWorker = /\s--(multi|parallel)\b/.test(cmd);
263
295
  if (!isMultiWorker) {
264
- process.exit(0); // 원본 tfx-route.sh 명령 그대로 통과
296
+ process.exit(0); // 원본 tfx-route.sh 명령 그대로 통과
265
297
  }
266
298
  }
267
299
 
@@ -272,10 +304,11 @@ async function main() {
272
304
  // v3: 플래그 빌더 — 하드코딩 제거, 원본 의도 보존
273
305
  const parts = ["tfx multi --teammate-mode headless"];
274
306
  if (!f.noAutoAttach) parts.push("--auto-attach");
275
- if (!f.noAutoAttach) parts.push("--dashboard"); // 워커 요약 스플릿이 기본
307
+ if (!f.noAutoAttach) parts.push("--dashboard"); // 워커 요약 스플릿이 기본
276
308
  if (f.verbose) parts.push("--verbose");
277
309
  parts.push(`--assign '${parsed.agent}:${safePrompt}:${parsed.agent}'`);
278
- if (parsed.mcp && VALID_MCP.has(parsed.mcp)) parts.push(`--mcp-profile ${parsed.mcp}`);
310
+ if (parsed.mcp && VALID_MCP.has(parsed.mcp))
311
+ parts.push(`--mcp-profile ${parsed.mcp}`);
279
312
  parts.push(`--timeout ${f.timeout || 600}`);
280
313
 
281
314
  const builtCmd = parts.join(" ");
@@ -286,7 +319,7 @@ async function main() {
286
319
  }
287
320
  deny(
288
321
  "[headless-guard] tfx-route.sh를 headless로 변환 실패. " +
289
- 'Bash("tfx multi --teammate-mode headless --assign \'cli:prompt:role\' ...") 형식을 사용하세요.',
322
+ "Bash(\"tfx multi --teammate-mode headless --assign 'cli:prompt:role' ...\") 형식을 사용하세요.",
290
323
  );
291
324
  }
292
325
  }
@@ -307,24 +340,27 @@ async function main() {
307
340
  if (multiState.nativeWorkCalls > GATE_THRESHOLD) {
308
341
  deny(
309
342
  `[headless-guard] tfx-multi gate: ${toolName} 호출 ${multiState.nativeWorkCalls}회 — headless dispatch 먼저 하세요.\n` +
310
- 'Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign \'codex:프롬프트:역할\' --timeout 600")',
343
+ "Bash(\"tfx multi --teammate-mode headless --auto-attach --dashboard --assign 'codex:프롬프트:역할' --timeout 600\")",
311
344
  );
312
345
  }
313
346
 
314
347
  nudge(
315
348
  `[headless-guard] tfx-multi 활성 (${multiState.nativeWorkCalls}/${GATE_THRESHOLD}). ` +
316
- "headless dispatch 후 작업을 시작하세요.",
349
+ "headless dispatch 후 작업을 시작하세요.",
317
350
  );
318
351
  }
319
352
 
320
353
  // H3 fix: Agent gate와 동일하게 NUDGE_THRESHOLD 기반 주기적 nudge
321
- multiState.nativeWorkCallsSinceDispatch = (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
354
+ multiState.nativeWorkCallsSinceDispatch =
355
+ (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
322
356
  writeMultiState(multiState);
323
357
 
324
358
  if (multiState.nativeWorkCallsSinceDispatch >= NUDGE_THRESHOLD) {
325
359
  multiState.nativeWorkCallsSinceDispatch = 0;
326
360
  writeMultiState(multiState);
327
- nudge("[headless-guard] nudge: headless 워커가 코드 수정 중. 직접 수정은 충돌 위험.");
361
+ nudge(
362
+ "[headless-guard] nudge: headless 워커가 코드 수정 중. 직접 수정은 충돌 위험.",
363
+ );
328
364
  }
329
365
  process.exit(0); // threshold 미만이면 조용히 통과
330
366
  }
@@ -333,7 +369,8 @@ async function main() {
333
369
  if (toolName === "Agent") {
334
370
  const subType = (toolInput.subagent_type || "").toLowerCase();
335
371
  const NATIVE_TYPES = new Set(["explore", "plan", "general-purpose", ""]);
336
- const isNative = NATIVE_TYPES.has(subType) || subType.startsWith("oh-my-claudecode:");
372
+ const isNative =
373
+ NATIVE_TYPES.has(subType) || subType.startsWith("oh-my-claudecode:");
337
374
 
338
375
  // ── A+B: tfx-multi 상태 기반 처리 ──
339
376
  const multiState = readMultiState();
@@ -346,17 +383,18 @@ async function main() {
346
383
  if (multiState.nativeWorkCalls > GATE_THRESHOLD) {
347
384
  deny(
348
385
  `[headless-guard] tfx-multi gate: Agent(${subType || "default"}) 호출 ${multiState.nativeWorkCalls}회 — headless에 먼저 dispatch하세요.\n` +
349
- 'Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign \'codex:프롬프트:역할\' --timeout 600")',
386
+ "Bash(\"tfx multi --teammate-mode headless --auto-attach --dashboard --assign 'codex:프롬프트:역할' --timeout 600\")",
350
387
  );
351
388
  }
352
389
  // 허용 범위 내 → 경고 + 통과
353
390
  nudge(
354
391
  `[headless-guard] tfx-multi 활성 (${multiState.nativeWorkCalls}/${GATE_THRESHOLD}). ` +
355
- "headless dispatch 후 작업을 시작하세요.",
392
+ "headless dispatch 후 작업을 시작하세요.",
356
393
  );
357
394
  } else {
358
395
  // ── B: nudge — dispatch 후, 네이티브 드리프트 감지 ──
359
- multiState.nativeWorkCallsSinceDispatch = (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
396
+ multiState.nativeWorkCallsSinceDispatch =
397
+ (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
360
398
  writeMultiState(multiState);
361
399
 
362
400
  if (multiState.nativeWorkCallsSinceDispatch >= NUDGE_THRESHOLD) {
@@ -364,7 +402,7 @@ async function main() {
364
402
  writeMultiState(multiState);
365
403
  nudge(
366
404
  "[headless-guard] nudge: headless 워커가 실행 중입니다. " +
367
- "결과를 기다리거나 추가 --assign으로 위임하세요.",
405
+ "결과를 기다리거나 추가 --assign으로 위임하세요.",
368
406
  );
369
407
  }
370
408
  }
@@ -388,8 +426,8 @@ async function main() {
388
426
  if (cliPatterns.some((p) => p.test(combined))) {
389
427
  deny(
390
428
  "[headless-guard] Codex/Gemini를 Agent()로 래핑하지 마세요. " +
391
- `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
392
- DIRECT_CLI_BYPASS_HINT,
429
+ `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
430
+ DIRECT_CLI_BYPASS_HINT,
393
431
  );
394
432
  }
395
433
  }
@@ -1,120 +1,139 @@
1
- #!/usr/bin/env node
2
- // SessionStart 훅에서 호출되는 Hub 보장 스크립트.
3
- // - /status 기반 헬스체크
4
- // - 비정상 Hub를 detached로 기동
5
-
6
- import { existsSync, readFileSync } from "fs";
7
- import { join, dirname } from "path";
8
- import { homedir } from "os";
9
- import { spawn } from "child_process";
10
- import { fileURLToPath } from "url";
11
-
12
- const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
13
- const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
14
- const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
15
-
16
- function formatHostForUrl(host) {
17
- return host.includes(":") ? `[${host}]` : host;
18
- }
19
-
20
- function buildHubBaseUrl(host, port) {
21
- return `http://${formatHostForUrl(host)}:${port}`;
22
- }
23
-
24
- function resolveHubTarget() {
25
- const envPortRaw = Number(process.env.TFX_HUB_PORT || "");
26
- const envPort = Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : null;
27
- const target = {
28
- host: "127.0.0.1",
29
- port: envPort || 27888,
30
- };
31
-
32
- if (existsSync(HUB_PID_FILE)) {
33
- try {
34
- const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
35
- if (!envPort) {
36
- const pidPort = Number(info?.port);
37
- if (Number.isFinite(pidPort) && pidPort > 0) target.port = pidPort;
38
- }
39
- if (typeof info?.host === "string") {
40
- const host = info.host.trim();
41
- if (LOOPBACK_HOSTS.has(host)) target.host = host;
42
- }
43
- } catch {
44
- // ignore parse errors and use env/default
45
- }
46
- }
47
-
48
- return target;
49
- }
50
-
51
- async function isHubHealthy(host, port) {
52
- try {
53
- const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
54
- signal: AbortSignal.timeout(1000),
55
- });
56
- if (!res.ok) return false;
57
- const data = await res.json();
58
- return data?.hub?.state === "healthy";
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- function startHubDetached(port) {
65
- const serverPath = join(PLUGIN_ROOT, "hub", "server.mjs");
66
- if (!existsSync(serverPath)) return false;
67
-
68
- try {
69
- const env = { ...process.env, TFX_HUB_PORT: String(port) };
70
- if (process.platform === "win32") {
71
- // Windows: cmd.exe /c start /b 완전 독립 프로세스 트리 생성
72
- // hook timeout 프로세스 트리 킬에서 살아남음
73
- const child = spawn("cmd.exe", ["/c", "start", "/b", "", process.execPath, serverPath], {
74
- env,
75
- stdio: "ignore",
76
- windowsHide: true,
77
- });
78
- child.unref();
79
- } else {
80
- const child = spawn(process.execPath, [serverPath], {
81
- env,
82
- detached: true,
83
- stdio: "ignore",
84
- });
85
- child.unref();
86
- }
87
- return true;
88
- } catch {
89
- return false;
90
- }
91
- }
92
-
93
- /** Hub 기동 후 ready 상태까지 대기 (최대 maxWaitMs) */
94
- async function waitForHubReady(host, port, maxWaitMs = 5000) {
95
- const interval = 250;
96
- const deadline = Date.now() + maxWaitMs;
97
- while (Date.now() < deadline) {
98
- if (await isHubHealthy(host, port)) return true;
99
- await new Promise((r) => setTimeout(r, interval));
100
- }
101
- return false;
102
- }
103
-
104
- const { host, port } = resolveHubTarget();
105
- if (!(await isHubHealthy(host, port))) {
106
- const started = startHubDetached(port);
107
- if (started) {
108
- const ready = await waitForHubReady(host, port, 3000);
109
- if (ready) {
110
- process.stdout.write("hub: ok");
111
- } else {
112
- // fire-and-forget: hub이 아직 기동 중일 있음 — 에러가 아닌 경고
113
- process.stdout.write("hub: starting");
114
- }
115
- } else {
116
- process.stderr.write("[hub-ensure] hub 시작 실패");
117
- }
118
- } else {
119
- process.stdout.write("hub: ok");
120
- }
1
+ #!/usr/bin/env node
2
+
3
+ // SessionStart 훅에서 호출되는 Hub 보장 스크립트.
4
+ // - /status 기반 헬스체크
5
+ // - 비정상 시 Hub를 detached로 기동
6
+
7
+ import { spawn } from "child_process";
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { dirname, join } from "path";
11
+ import { fileURLToPath } from "url";
12
+
13
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
14
+ const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
+ const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
16
+
17
+ function formatHostForUrl(host) {
18
+ return host.includes(":") ? `[${host}]` : host;
19
+ }
20
+
21
+ function buildHubBaseUrl(host, port) {
22
+ return `http://${formatHostForUrl(host)}:${port}`;
23
+ }
24
+
25
+ function resolveHubTarget() {
26
+ const envPortRaw = Number(process.env.TFX_HUB_PORT || "");
27
+ const envPort =
28
+ Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : null;
29
+ const target = {
30
+ host: "127.0.0.1",
31
+ port: envPort || 27888,
32
+ };
33
+
34
+ if (existsSync(HUB_PID_FILE)) {
35
+ try {
36
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
37
+ if (!envPort) {
38
+ const pidPort = Number(info?.port);
39
+ if (Number.isFinite(pidPort) && pidPort > 0) target.port = pidPort;
40
+ }
41
+ if (typeof info?.host === "string") {
42
+ const host = info.host.trim();
43
+ if (LOOPBACK_HOSTS.has(host)) target.host = host;
44
+ }
45
+ } catch {
46
+ // ignore parse errors and use env/default
47
+ }
48
+ }
49
+
50
+ return target;
51
+ }
52
+
53
+ async function isHubHealthy(host, port) {
54
+ try {
55
+ const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
56
+ signal: AbortSignal.timeout(1000),
57
+ });
58
+ if (!res.ok) return false;
59
+ const data = await res.json();
60
+ return data?.hub?.state === "healthy";
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function startHubDetached(port) {
67
+ const serverPath = join(PLUGIN_ROOT, "hub", "server.mjs");
68
+ if (!existsSync(serverPath)) return false;
69
+
70
+ try {
71
+ const env = { ...process.env, TFX_HUB_PORT: String(port) };
72
+ if (process.platform === "win32") {
73
+ const child = spawn(
74
+ "cmd.exe",
75
+ ["/c", "start", "/b", "", process.execPath, serverPath],
76
+ {
77
+ env,
78
+ stdio: "ignore",
79
+ windowsHide: true,
80
+ },
81
+ );
82
+ child.unref();
83
+ } else {
84
+ const child = spawn(process.execPath, [serverPath], {
85
+ env,
86
+ detached: true,
87
+ stdio: "ignore",
88
+ });
89
+ child.unref();
90
+ }
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ async function waitForHubReady(host, port, maxWaitMs = 5000) {
98
+ const interval = 250;
99
+ const deadline = Date.now() + maxWaitMs;
100
+ while (Date.now() < deadline) {
101
+ if (await isHubHealthy(host, port)) return true;
102
+ await new Promise((resolve) => setTimeout(resolve, interval));
103
+ }
104
+ return false;
105
+ }
106
+
107
+ export async function run(stdinData) {
108
+ void stdinData;
109
+
110
+ const { host, port } = resolveHubTarget();
111
+ if (await isHubHealthy(host, port)) {
112
+ return { code: 0, stdout: "hub: ok", stderr: "" };
113
+ }
114
+
115
+ const started = startHubDetached(port);
116
+ if (!started) {
117
+ return { code: 0, stdout: "", stderr: "[hub-ensure] hub 시작 실패" };
118
+ }
119
+
120
+ const ready = await waitForHubReady(host, port, 3000);
121
+ return {
122
+ code: 0,
123
+ stdout: ready ? "hub: ok" : "hub: starting",
124
+ stderr: "",
125
+ };
126
+ }
127
+
128
+ const isMain =
129
+ process.argv[1] &&
130
+ import.meta.url.endsWith(
131
+ process.argv[1].replace(/\\/g, "/").split("/").pop(),
132
+ );
133
+
134
+ if (isMain) {
135
+ const result = await run();
136
+ if (result.stdout) process.stdout.write(result.stdout);
137
+ if (result.stderr) process.stderr.write(result.stderr);
138
+ process.exit(result.code);
139
+ }