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,352 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { normalizePath } from './platform.mjs';
4
+ import { withRetry } from './workers/worker-utils.mjs';
5
+
6
+ const ADAPTIVE_FINGERPRINT_VERSION = 1;
7
+ const DEFAULT_SCOPE = 'default';
8
+ const META_PREFIX = 'adaptive_fingerprint:';
9
+ const DEFAULT_RETRY_OPTIONS = Object.freeze({
10
+ maxAttempts: 3,
11
+ baseDelayMs: 50,
12
+ maxDelayMs: 250,
13
+ });
14
+ const TIME_WINDOWS = Object.freeze([
15
+ { name: 'overnight', start: 0, end: 5 },
16
+ { name: 'morning', start: 6, end: 11 },
17
+ { name: 'afternoon', start: 12, end: 17 },
18
+ { name: 'evening', start: 18, end: 23 },
19
+ ]);
20
+ const MEMORY_FINGERPRINT_CACHE = new WeakMap();
21
+
22
+ function clone(value) {
23
+ return value == null ? value : JSON.parse(JSON.stringify(value));
24
+ }
25
+
26
+ function toIsoTimestamp(value = Date.now()) {
27
+ const time = Number.isFinite(Number(value)) ? Number(value) : Date.now();
28
+ return new Date(time).toISOString();
29
+ }
30
+
31
+ function toTimestamp(value) {
32
+ if (value instanceof Date) return value.getTime();
33
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
34
+ if (typeof value === 'string') {
35
+ const parsed = Date.parse(value);
36
+ return Number.isFinite(parsed) ? parsed : null;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function normalizeRetryOptions(options = {}) {
42
+ const maxAttempts = Math.max(1, Number(options.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts) || DEFAULT_RETRY_OPTIONS.maxAttempts);
43
+ const baseDelayMs = Math.max(0, Number(options.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs) || DEFAULT_RETRY_OPTIONS.baseDelayMs);
44
+ const maxDelayMs = Math.max(baseDelayMs, Number(options.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs) || DEFAULT_RETRY_OPTIONS.maxDelayMs);
45
+ return { maxAttempts, baseDelayMs, maxDelayMs };
46
+ }
47
+
48
+ function normalizeScope(scope) {
49
+ const text = String(scope ?? DEFAULT_SCOPE).trim();
50
+ return text || DEFAULT_SCOPE;
51
+ }
52
+
53
+ function metaKey(scope) {
54
+ return `${META_PREFIX}${normalizeScope(scope)}`;
55
+ }
56
+
57
+ function hashValue(value) {
58
+ const hash = createHash('sha256');
59
+ hash.update(JSON.stringify(value));
60
+ return `sha256:${hash.digest('hex')}`;
61
+ }
62
+
63
+ function normalizeContextPath(value) {
64
+ const normalized = normalizePath(String(value ?? ''));
65
+ return normalized.replace(/\/+/gu, '/').replace(/\/+$/u, '') || '/';
66
+ }
67
+
68
+ function toRelativePath(targetPath, cwd) {
69
+ if (!cwd) return targetPath;
70
+ const base = normalizeContextPath(cwd);
71
+ if (targetPath === base) return '.';
72
+ return targetPath.startsWith(`${base}/`) ? targetPath.slice(base.length + 1) : targetPath;
73
+ }
74
+
75
+ function toUniqueList(values) {
76
+ const seen = new Set();
77
+ const next = [];
78
+ for (const value of values) {
79
+ if (!value || seen.has(value)) continue;
80
+ seen.add(value);
81
+ next.push(value);
82
+ }
83
+ return next;
84
+ }
85
+
86
+ function collectRawPathCandidates(context = {}) {
87
+ const direct = [context.file_path, context.filePath, context.path, context.target_path, context.targetPath];
88
+ const fromArrays = [context.files, context.paths, context.targets]
89
+ .filter(Array.isArray)
90
+ .flatMap((entry) => entry)
91
+ .map((entry) => (typeof entry === 'string' ? entry : entry?.path));
92
+ return [...direct, ...fromArrays].filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
93
+ }
94
+
95
+ function collectPathPattern(context = {}) {
96
+ const normalized = toUniqueList(
97
+ collectRawPathCandidates(context)
98
+ .map((entry) => normalizeContextPath(entry))
99
+ .map((entry) => toRelativePath(entry, context.cwd || context.project_root || context.projectRoot)),
100
+ ).sort();
101
+
102
+ const primaryPath = normalized[0] ?? null;
103
+ const extensions = normalized
104
+ .map((entry) => entry.split('/').pop() || '')
105
+ .map((entry) => (entry.includes('.') ? entry.slice(entry.lastIndexOf('.')).toLowerCase() : 'none'));
106
+ const extensionCounts = extensions.reduce((acc, ext) => ({
107
+ ...acc,
108
+ [ext]: (acc[ext] || 0) + 1,
109
+ }), {});
110
+
111
+ return {
112
+ count: normalized.length,
113
+ primary_path: primaryPath,
114
+ sample_paths: normalized.slice(0, 5),
115
+ extension_counts: extensionCounts,
116
+ checksum: hashValue(normalized),
117
+ };
118
+ }
119
+
120
+ function normalizeWorkType(value) {
121
+ const text = String(value ?? '').trim().toLowerCase();
122
+ if (!text) {
123
+ return { raw: null, normalized: 'general' };
124
+ }
125
+ const normalized = text.replace(/\s+/gu, '-').replace(/[^a-z0-9-]/gu, '') || 'general';
126
+ return { raw: value, normalized };
127
+ }
128
+
129
+ function collectActivityTimestamps(context = {}, now = Date.now) {
130
+ const nowValue = typeof now === 'function' ? now() : now;
131
+ const fromList = [context.activity_timestamps, context.activityTimestamps, context.timestamps]
132
+ .filter(Array.isArray)
133
+ .flatMap((entry) => entry)
134
+ .map(toTimestamp)
135
+ .filter((entry) => entry != null);
136
+ const singles = [context.timestamp, context.started_at, context.startedAt]
137
+ .map(toTimestamp)
138
+ .filter((entry) => entry != null);
139
+ return fromList.length || singles.length ? [...fromList, ...singles] : [Number(nowValue)];
140
+ }
141
+
142
+ function classifyHour(hour) {
143
+ const safeHour = Number.isFinite(Number(hour)) ? Number(hour) : 0;
144
+ const matched = TIME_WINDOWS.find((window) => safeHour >= window.start && safeHour <= window.end);
145
+ return matched?.name || 'overnight';
146
+ }
147
+
148
+ function buildWindowHistogram(timestamps = []) {
149
+ const histogram = TIME_WINDOWS.reduce((acc, window) => ({ ...acc, [window.name]: 0 }), {});
150
+ for (const timestamp of timestamps) {
151
+ const bucket = classifyHour(new Date(timestamp).getHours());
152
+ histogram[bucket] = (histogram[bucket] || 0) + 1;
153
+ }
154
+ return histogram;
155
+ }
156
+
157
+ function dominantWindow(histogram) {
158
+ const sorted = Object.entries(histogram)
159
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
160
+ return sorted[0]?.[0] || 'overnight';
161
+ }
162
+
163
+ function resolveTimezoneName(context = {}) {
164
+ const fromContext = typeof context.timezone === 'string' ? context.timezone.trim() : '';
165
+ const fromIntl = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
166
+ return fromContext || fromIntl;
167
+ }
168
+
169
+ function collectTimezonePattern(context = {}, now = Date.now) {
170
+ const timestamps = collectActivityTimestamps(context, now);
171
+ const firstTimestamp = timestamps[0] ?? Date.now();
172
+ const histogram = buildWindowHistogram(timestamps);
173
+ return {
174
+ timezone: resolveTimezoneName(context),
175
+ offset_minutes: -new Date(firstTimestamp).getTimezoneOffset(),
176
+ sample_count: timestamps.length,
177
+ window_histogram: histogram,
178
+ dominant_window: dominantWindow(histogram),
179
+ };
180
+ }
181
+
182
+ function computeFingerprintSignature(input) {
183
+ const source = {
184
+ file_checksum: input.path_pattern.checksum,
185
+ work_type: input.work_type.normalized,
186
+ timezone: input.timezone_pattern.timezone,
187
+ dominant_window: input.timezone_pattern.dominant_window,
188
+ };
189
+ return hashValue(source);
190
+ }
191
+
192
+ function getMemoryStoreMap(store) {
193
+ const current = MEMORY_FINGERPRINT_CACHE.get(store);
194
+ if (current) return current;
195
+ const next = new Map();
196
+ MEMORY_FINGERPRINT_CACHE.set(store, next);
197
+ return next;
198
+ }
199
+
200
+ function readFingerprintFromStore(store, scope) {
201
+ if (!store) return null;
202
+ if (typeof store.loadAdaptiveFingerprint === 'function') {
203
+ return store.loadAdaptiveFingerprint(scope);
204
+ }
205
+ if (store.db?.prepare) {
206
+ const row = store.db.prepare('SELECT value FROM _meta WHERE key = ?').get(metaKey(scope));
207
+ return row?.value ? JSON.parse(row.value) : null;
208
+ }
209
+ return clone(getMemoryStoreMap(store).get(normalizeScope(scope)) || null);
210
+ }
211
+
212
+ function writeFingerprintToStore(store, scope, record) {
213
+ if (!store) return clone(record);
214
+ if (typeof store.saveAdaptiveFingerprint === 'function') {
215
+ return store.saveAdaptiveFingerprint(scope, clone(record));
216
+ }
217
+ if (store.db?.prepare) {
218
+ store.db.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)')
219
+ .run(metaKey(scope), JSON.stringify(record));
220
+ return clone(record);
221
+ }
222
+ getMemoryStoreMap(store).set(normalizeScope(scope), clone(record));
223
+ return clone(record);
224
+ }
225
+
226
+ function buildHealthSnapshot(base, patch = {}) {
227
+ return {
228
+ state: patch.state || base.state || 'healthy',
229
+ retry: { ...base.retry },
230
+ last_success_at: patch.last_success_at ?? base.last_success_at ?? null,
231
+ last_failure_at: patch.last_failure_at ?? base.last_failure_at ?? null,
232
+ last_error: patch.last_error ?? base.last_error ?? null,
233
+ };
234
+ }
235
+
236
+ function createInitialHealth(retryOptions) {
237
+ return {
238
+ state: 'healthy',
239
+ retry: { ...retryOptions },
240
+ last_success_at: null,
241
+ last_failure_at: null,
242
+ last_error: null,
243
+ };
244
+ }
245
+
246
+ function resolveNowValue(now) {
247
+ return typeof now === 'function' ? now() : now;
248
+ }
249
+
250
+ function mergeFingerprintSnapshot(previous, computed, scope) {
251
+ return {
252
+ ...computed,
253
+ scope,
254
+ observation_count: (previous?.observation_count || 0) + 1,
255
+ first_captured_at: previous?.first_captured_at || computed.captured_at,
256
+ };
257
+ }
258
+
259
+ function markHealthyHealth(base, now) {
260
+ return buildHealthSnapshot(base, {
261
+ state: 'healthy',
262
+ last_success_at: toIsoTimestamp(resolveNowValue(now)),
263
+ last_error: null,
264
+ });
265
+ }
266
+
267
+ function markDegradedHealth(base, now, error) {
268
+ return buildHealthSnapshot(base, {
269
+ state: 'degraded',
270
+ last_failure_at: toIsoTimestamp(resolveNowValue(now)),
271
+ last_error: {
272
+ name: error?.name || 'Error',
273
+ message: error?.message || 'unknown adaptive fingerprint error',
274
+ },
275
+ });
276
+ }
277
+
278
+ export function buildAdaptiveFingerprint(sessionContext = {}, options = {}) {
279
+ const now = options.now ?? Date.now;
280
+ const capturedAt = toIsoTimestamp(resolveNowValue(now));
281
+ const pathPattern = collectPathPattern(sessionContext);
282
+ const workType = normalizeWorkType(sessionContext.work_type ?? sessionContext.workType);
283
+ const timezonePattern = collectTimezonePattern(sessionContext, now);
284
+ const fingerprintId = computeFingerprintSignature({
285
+ path_pattern: pathPattern,
286
+ work_type: workType,
287
+ timezone_pattern: timezonePattern,
288
+ });
289
+
290
+ return {
291
+ version: ADAPTIVE_FINGERPRINT_VERSION,
292
+ captured_at: capturedAt,
293
+ scope: normalizeScope(sessionContext.scope),
294
+ fingerprint_id: fingerprintId,
295
+ path_pattern: pathPattern,
296
+ work_type: workType,
297
+ timezone_pattern: timezonePattern,
298
+ };
299
+ }
300
+
301
+ export async function loadAdaptiveFingerprint(store, scope = DEFAULT_SCOPE) {
302
+ const loaded = await Promise.resolve(readFingerprintFromStore(store, scope));
303
+ return clone(loaded);
304
+ }
305
+
306
+ export async function saveAdaptiveFingerprint(store, scope, fingerprint, options = {}) {
307
+ const retryOptions = normalizeRetryOptions(options.retryOptions);
308
+ const normalizedScope = normalizeScope(scope);
309
+ const write = async () => Promise.resolve(writeFingerprintToStore(store, normalizedScope, fingerprint));
310
+ const saved = await withRetry(write, { ...retryOptions });
311
+ return clone(saved);
312
+ }
313
+
314
+ export function createAdaptiveFingerprintService(options = {}) {
315
+ const store = options.store ?? null;
316
+ const retryOptions = normalizeRetryOptions(options.retryOptions);
317
+ const now = options.now ?? Date.now;
318
+ let health = createInitialHealth(retryOptions);
319
+
320
+ async function capture(sessionContext = {}) {
321
+ const computed = buildAdaptiveFingerprint(sessionContext, { now });
322
+ const scope = normalizeScope(sessionContext.scope ?? computed.scope);
323
+ const previous = await loadAdaptiveFingerprint(store, scope);
324
+ const merged = mergeFingerprintSnapshot(previous, computed, scope);
325
+
326
+ try {
327
+ const saved = await saveAdaptiveFingerprint(store, scope, merged, { retryOptions });
328
+ health = markHealthyHealth(health, now);
329
+ return saved;
330
+ } catch (error) {
331
+ health = markDegradedHealth(health, now, error);
332
+ throw error;
333
+ }
334
+ }
335
+
336
+ async function read(scope = DEFAULT_SCOPE) {
337
+ return loadAdaptiveFingerprint(store, scope);
338
+ }
339
+
340
+ function getHealth() {
341
+ return clone(health);
342
+ }
343
+
344
+ return Object.freeze({
345
+ captureFingerprint: capture,
346
+ computeFingerprint: (sessionContext = {}) => buildAdaptiveFingerprint(sessionContext, { now }),
347
+ loadFingerprint: read,
348
+ getHealth,
349
+ });
350
+ }
351
+
352
+ export default createAdaptiveFingerprintService;
package/hub/state.mjs ADDED
@@ -0,0 +1,258 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, renameSync, existsSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url));
8
+ const PID_FILE_NAME = 'hub.pid';
9
+ const LEGACY_STATE_FILE_NAME = 'hub-state.json';
10
+ const LOCK_FILE_NAME = 'hub-start.lock';
11
+
12
+ let heldLockPath = null;
13
+ let heldLockFd = null;
14
+ let cachedVersionHash = null;
15
+
16
+ function getStateDir(options = {}) {
17
+ return options.stateDir || process.env.TFX_HUB_STATE_DIR?.trim() || join(homedir(), '.claude', 'cache', 'tfx-hub');
18
+ }
19
+
20
+ function getStatePath(options = {}) {
21
+ return options.statePath || join(getStateDir(options), PID_FILE_NAME);
22
+ }
23
+
24
+ function getLegacyStatePath(options = {}) {
25
+ return join(getStateDir(options), LEGACY_STATE_FILE_NAME);
26
+ }
27
+
28
+ function getLockPath(options = {}) {
29
+ return options.lockPath || join(getStateDir(options), LOCK_FILE_NAME);
30
+ }
31
+
32
+ function sleep(ms) {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
35
+
36
+ function isPidAlive(pid) {
37
+ if (!Number.isFinite(Number(pid)) || Number(pid) <= 0) return false;
38
+ try {
39
+ process.kill(Number(pid), 0);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function parseJson(text, fallback = null) {
47
+ try {
48
+ return JSON.parse(text);
49
+ } catch {
50
+ return fallback;
51
+ }
52
+ }
53
+
54
+ function safeReplaceFile(tempPath, targetPath) {
55
+ try {
56
+ renameSync(tempPath, targetPath);
57
+ } catch (error) {
58
+ if (!['EEXIST', 'EPERM', 'EACCES'].includes(error?.code)) {
59
+ try { unlinkSync(tempPath); } catch {}
60
+ throw error;
61
+ }
62
+ try { unlinkSync(targetPath); } catch {}
63
+ renameSync(tempPath, targetPath);
64
+ }
65
+ }
66
+
67
+ function writeJsonFile(targetPath, payload) {
68
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
69
+ writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
70
+ safeReplaceFile(tempPath, targetPath);
71
+ }
72
+
73
+ function readJsonFile(filePath) {
74
+ try {
75
+ if (!existsSync(filePath)) return null;
76
+ return parseJson(readFileSync(filePath, 'utf8'), null);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 허브의 현재 상태(PID, 포트, 버전 등)를 파일에 기록합니다.
84
+ * 원자적(atomic) 쓰기를 위해 임시 파일을 생성한 후 교체하는 방식을 사용합니다.
85
+ *
86
+ * @param {object} payload - 상태 데이터
87
+ * @param {number} payload.pid - 허브 프로세스 ID
88
+ * @param {number} payload.port - 허브 서버 포트
89
+ * @param {string} payload.version - 허브 버전
90
+ * @param {string} payload.sessionId - 현재 세션 ID
91
+ * @param {string} payload.startedAt - 시작 시각 (ISO 8601)
92
+ * @param {object} [options] - 옵션
93
+ * @param {string} [options.stateDir] - 상태 파일이 저장될 디렉토리
94
+ * @returns {object} 기록된 상태 데이터
95
+ */
96
+ export function writeState({ pid, port, version, sessionId, startedAt, ...rest }, options = {}) {
97
+ const stateDir = getStateDir(options);
98
+ const statePath = getStatePath(options);
99
+ const payload = { pid, port, version, sessionId, startedAt, ...rest };
100
+
101
+ mkdirSync(stateDir, { recursive: true });
102
+ writeJsonFile(statePath, payload);
103
+ try { unlinkSync(getLegacyStatePath(options)); } catch {}
104
+ return payload;
105
+ }
106
+
107
+ /**
108
+ * 파일로부터 허브의 현재 상태를 읽어옵니다.
109
+ *
110
+ * @param {object} [options] - 옵션
111
+ * @param {string} [options.stateDir] - 상태 파일이 저장된 디렉토리
112
+ * @returns {object|null} 읽어온 상태 데이터 또는 실패 시 null
113
+ */
114
+ export function readState(options = {}) {
115
+ return readJsonFile(getStatePath(options)) ?? readJsonFile(getLegacyStatePath(options));
116
+ }
117
+
118
+ /**
119
+ * 지정된 포트에서 실행 중인 허브 서버의 헬스 체크를 수행합니다.
120
+ *
121
+ * @param {number|string} port - 서버 포트
122
+ * @param {object} [options] - 옵션
123
+ * @param {number} [options.timeoutMs=1000] - 요청 타임아웃
124
+ * @param {string} [options.baseUrl] - 서버 베이스 URL
125
+ * @returns {Promise<boolean>} 서버 정상 작동 여부
126
+ */
127
+ export async function isServerHealthy(port, options = {}) {
128
+ const resolvedPort = Number(port);
129
+ if (!Number.isFinite(resolvedPort) || resolvedPort <= 0) return false;
130
+
131
+ const timeoutMs = Math.max(100, Number(options.timeoutMs) || 1000);
132
+ const baseUrl = options.baseUrl || `http://127.0.0.1:${resolvedPort}`;
133
+
134
+ try {
135
+ const response = await fetch(`${baseUrl}/health`, {
136
+ method: 'GET',
137
+ signal: AbortSignal.timeout(timeoutMs),
138
+ });
139
+ if (!response.ok) return false;
140
+ const body = await response.json().catch(() => null);
141
+ return body?.ok === true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 현재 프로젝트의 버전 해시를 생성합니다.
149
+ * package.json의 버전과 Git commit SHA를 조합합니다.
150
+ *
151
+ * @param {object} [options] - 옵션
152
+ * @param {boolean} [options.force=false] - 캐시를 무시하고 새로 생성할지 여부
153
+ * @returns {string} 버전 해시 문자열
154
+ */
155
+ export function getVersionHash(options = {}) {
156
+ if (cachedVersionHash && !options.force) return cachedVersionHash;
157
+
158
+ const packageJsonPath = join(PROJECT_ROOT, 'package.json');
159
+ const pkg = parseJson(readFileSync(packageJsonPath, 'utf8'), {});
160
+ const version = String(pkg?.version || '0.0.0').trim();
161
+
162
+ let sha = String(process.env.TFX_HUB_GIT_SHA || '').trim();
163
+ if (!sha) {
164
+ try {
165
+ sha = execSync('git rev-parse --short HEAD', {
166
+ cwd: PROJECT_ROOT,
167
+ encoding: 'utf8',
168
+ stdio: ['ignore', 'pipe', 'ignore'],
169
+ windowsHide: true,
170
+ }).trim();
171
+ } catch {
172
+ sha = '';
173
+ }
174
+ }
175
+
176
+ cachedVersionHash = sha ? `${version}-${sha}` : version;
177
+ return cachedVersionHash;
178
+ }
179
+
180
+ /**
181
+ * 허브 시작 시 중복 실행을 방지하기 위한 잠금(lock)을 획득합니다.
182
+ * 이미 실행 중인 다른 프로세스가 있는지 확인하고 유효한 잠금을 획득할 때까지 재시도합니다.
183
+ *
184
+ * @param {object} [options] - 옵션
185
+ * @param {number} [options.timeoutMs=3000] - 최대 대기 시간
186
+ * @param {number} [options.pollMs=50] - 재시도 간격
187
+ * @param {string} [options.lockPath] - 잠금 파일 경로
188
+ * @returns {Promise<{path: string}>} 잠금 파일 경로
189
+ * @throws {Error} 타임아웃 내에 잠금을 획득하지 못한 경우
190
+ */
191
+ export async function acquireLock(options = {}) {
192
+ if (heldLockFd !== null && heldLockPath) {
193
+ return { path: heldLockPath };
194
+ }
195
+
196
+ const lockPath = getLockPath(options);
197
+ const timeoutMs = Math.max(100, Number(options.timeoutMs) || 3000);
198
+ const pollMs = Math.max(10, Number(options.pollMs) || 50);
199
+ const deadline = Date.now() + timeoutMs;
200
+
201
+ mkdirSync(dirname(lockPath), { recursive: true });
202
+
203
+ while (Date.now() <= deadline) {
204
+ try {
205
+ const fd = openSync(lockPath, 'wx', 0o600);
206
+ writeFileSync(fd, `${JSON.stringify({
207
+ pid: process.pid,
208
+ createdAt: new Date().toISOString(),
209
+ }, null, 2)}\n`, 'utf8');
210
+ heldLockFd = fd;
211
+ heldLockPath = lockPath;
212
+ return { path: lockPath };
213
+ } catch (error) {
214
+ if (error?.code !== 'EEXIST') {
215
+ throw error;
216
+ }
217
+
218
+ try {
219
+ const raw = readFileSync(lockPath, 'utf8');
220
+ const data = parseJson(raw, {});
221
+ const stats = statSync(lockPath);
222
+ const staleByPid = !isPidAlive(data?.pid);
223
+ const staleByAge = Date.now() - stats.mtimeMs > timeoutMs;
224
+ if (staleByPid || staleByAge) {
225
+ try { unlinkSync(lockPath); } catch {}
226
+ continue;
227
+ }
228
+ } catch {}
229
+
230
+ await sleep(pollMs);
231
+ }
232
+ }
233
+
234
+ throw new Error(`hub start lock busy: ${lockPath}`);
235
+ }
236
+
237
+ /**
238
+ * 획득했던 잠금을 해제합니다. 잠금 파일을 삭제하고 관련 리소스를 정리합니다.
239
+ *
240
+ * @param {object} [options] - 옵션
241
+ * @param {string} [options.lockPath] - 명시적인 잠금 파일 경로
242
+ */
243
+ export function releaseLock(options = {}) {
244
+ const lockPath = options.lockPath || heldLockPath || getLockPath(options);
245
+
246
+ if (heldLockFd !== null) {
247
+ try { closeSync(heldLockFd); } catch {}
248
+ heldLockFd = null;
249
+ }
250
+
251
+ try {
252
+ if (existsSync(lockPath)) unlinkSync(lockPath);
253
+ } catch {}
254
+
255
+ if (!options.lockPath || options.lockPath === heldLockPath) {
256
+ heldLockPath = null;
257
+ }
258
+ }