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,671 @@
1
+ // hub/team/conductor.mjs — 세션 오케스트레이션 Conductor
2
+ // native-supervisor.mjs의 spawn/kill을 래핑하되, 상태 머신 + health probe +
3
+ // auto-restart + event log를 추가하여 "조용한 실패"를 구조적으로 불가능하게 만든다.
4
+ //
5
+ // 기존 native-supervisor와의 차이:
6
+ // 1. 상태 머신 (alive/dead → 7 states + 2 terminal)
7
+ // 2. Health probe 4단계 (+ INPUT_WAIT 감지)
8
+ // 3. Auto-restart (maxRestarts=3)
9
+ // 4. JSONL event log (블랙박스 리코더)
10
+
11
+ import { spawn, execFile } from 'node:child_process';
12
+ import { dirname, join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import { mkdirSync, createWriteStream, readFileSync, copyFileSync } from 'node:fs';
15
+ import { EventEmitter } from 'node:events';
16
+
17
+ import { killProcess, IS_WINDOWS } from '../platform.mjs';
18
+ import { createEventLog } from './event-log.mjs';
19
+ import { createHealthProbe } from './health-probe.mjs';
20
+ import { createRemoteProbe } from './remote-probe.mjs';
21
+ import { buildLauncher } from './launcher-template.mjs';
22
+ import { broker } from '../account-broker.mjs';
23
+
24
+ /** 세션 상태 */
25
+ export const STATES = Object.freeze({
26
+ INIT: 'init',
27
+ STARTING: 'starting',
28
+ HEALTHY: 'healthy',
29
+ STALLED: 'stalled',
30
+ INPUT_WAIT: 'input_wait',
31
+ FAILED: 'failed',
32
+ RESTARTING: 'restarting',
33
+ DEAD: 'dead',
34
+ COMPLETED: 'completed',
35
+ });
36
+
37
+ /** 유효한 상태 전이 테이블 */
38
+ const TRANSITIONS = Object.freeze({
39
+ [STATES.INIT]: [STATES.STARTING],
40
+ [STATES.STARTING]: [STATES.HEALTHY, STATES.FAILED],
41
+ [STATES.HEALTHY]: [STATES.STALLED, STATES.INPUT_WAIT, STATES.FAILED, STATES.COMPLETED],
42
+ [STATES.STALLED]: [STATES.HEALTHY, STATES.FAILED],
43
+ [STATES.INPUT_WAIT]: [STATES.HEALTHY, STATES.FAILED],
44
+ [STATES.FAILED]: [STATES.RESTARTING, STATES.DEAD],
45
+ [STATES.RESTARTING]: [STATES.STARTING],
46
+ [STATES.DEAD]: [],
47
+ [STATES.COMPLETED]: [],
48
+ });
49
+
50
+ const TERMINAL_STATES = new Set([STATES.DEAD, STATES.COMPLETED]);
51
+ const DEFAULT_MAX_RESTARTS = 3;
52
+ const DEFAULT_GRACE_MS = 10_000;
53
+
54
+ /**
55
+ * Conductor 팩토리.
56
+ * @param {object} opts
57
+ * @param {string} opts.logsDir — 이벤트 로그 디렉토리
58
+ * @param {number} [opts.maxRestarts=3]
59
+ * @param {number} [opts.graceMs=10000] — shutdown grace period
60
+ * @param {object} [opts.probeOpts] — health-probe 옵션 오버라이드
61
+ * @returns {Conductor}
62
+ */
63
+ export function createConductor(opts = {}) {
64
+ const {
65
+ logsDir,
66
+ maxRestarts = DEFAULT_MAX_RESTARTS,
67
+ graceMs = DEFAULT_GRACE_MS,
68
+ probeOpts = {},
69
+ } = opts;
70
+
71
+ if (!logsDir) throw new Error('logsDir is required');
72
+ mkdirSync(logsDir, { recursive: true });
73
+
74
+ const emitter = new EventEmitter();
75
+ const sessions = new Map();
76
+ let shuttingDown = false;
77
+
78
+ // 공유 event log (모든 세션 이벤트를 하나의 JSONL에)
79
+ const eventLog = createEventLog(join(logsDir, 'conductor-events.jsonl'));
80
+
81
+ /**
82
+ * 세션 상태 전이.
83
+ * @param {object} session
84
+ * @param {string} nextState
85
+ * @param {string} [reason]
86
+ */
87
+ function transition(session, nextState, reason = '') {
88
+ const valid = TRANSITIONS[session.state] || [];
89
+ if (!valid.includes(nextState)) {
90
+ eventLog.append('invalid_transition', {
91
+ session: session.id,
92
+ from: session.state,
93
+ to: nextState,
94
+ reason,
95
+ });
96
+ return false;
97
+ }
98
+
99
+ const prev = session.state;
100
+ session.state = nextState;
101
+
102
+ eventLog.append('stateChange', {
103
+ session: session.id,
104
+ from: prev,
105
+ to: nextState,
106
+ reason,
107
+ restarts: session.restarts,
108
+ });
109
+
110
+ emitter.emit('stateChange', { sessionId: session.id, from: prev, to: nextState, reason });
111
+
112
+ // Terminal state cleanup
113
+ if (TERMINAL_STATES.has(nextState)) {
114
+ session.probe?.stop();
115
+ }
116
+
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * 프로세스를 강제 종료.
122
+ * Windows: taskkill /T /F /PID (프로세스 트리). POSIX: SIGKILL.
123
+ */
124
+ function forceKill(pid) {
125
+ if (!pid || pid <= 0) return;
126
+ killProcess(pid, { signal: 'SIGKILL', tree: true, force: true, timeout: 5000 });
127
+ }
128
+
129
+ /**
130
+ * 원격 세션의 psmux 세션을 SSH 경유로 kill.
131
+ * fire-and-forget: 실패해도 에러 전파 안 함.
132
+ */
133
+ function killRemoteSession(session) {
134
+ const host = session.config.host;
135
+ if (!host) return;
136
+ let sshUser = session.config.sshUser;
137
+ let sshIp = host;
138
+ // hosts.json에서 ssh_user/IP 해결
139
+ try {
140
+ const hostsPath = join(opts.repoRoot || process.cwd(), 'references', 'hosts.json');
141
+ const hosts = JSON.parse(readFileSync(hostsPath, 'utf8'));
142
+ const hostCfg = hosts.hosts?.[host];
143
+ if (hostCfg) {
144
+ sshUser = sshUser || hostCfg.ssh_user;
145
+ sshIp = hostCfg.tailscale?.ip || host;
146
+ }
147
+ } catch { /* hosts.json 없으면 fallback */ }
148
+ if (!sshUser) return;
149
+ const execFn = opts.deps?.execFile || execFile;
150
+ execFn('ssh', [`${sshUser}@${sshIp}`, 'psmux', 'kill-session', '-t', session.id],
151
+ { timeout: 10_000 }, () => {});
152
+ eventLog.append('remote_kill', { session: session.id, host, sshUser, sshIp });
153
+ }
154
+
155
+ /**
156
+ * 단일 세션의 child process를 정리.
157
+ * 원격 세션은 SSH 경유 psmux kill-session으로 정리.
158
+ */
159
+ async function cleanupChild(session) {
160
+ session.probe?.stop();
161
+
162
+ // 원격 세션 — SSH 경유 psmux kill-session
163
+ if (session.config.remote) {
164
+ killRemoteSession(session);
165
+ return;
166
+ }
167
+
168
+ const child = session.child;
169
+ if (!child) return;
170
+
171
+ const pid = child.pid;
172
+ if (!pid) return;
173
+
174
+ // SIGTERM 먼저
175
+ try { child.kill('SIGTERM'); } catch { /* already dead */ }
176
+
177
+ // Grace period 대기
178
+ await new Promise((resolve) => {
179
+ const timer = setTimeout(() => {
180
+ forceKill(pid);
181
+ resolve();
182
+ }, graceMs);
183
+ timer.unref?.();
184
+ child.once('exit', () => {
185
+ clearTimeout(timer);
186
+ resolve();
187
+ });
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Health probe 콜백 — probe 결과에 따라 상태 전이 판단.
193
+ */
194
+ function handleProbeResult(session, result) {
195
+ if (TERMINAL_STATES.has(session.state)) return;
196
+ if (session.state === STATES.INIT || session.state === STATES.RESTARTING) return;
197
+
198
+ eventLog.append('health', {
199
+ session: session.id,
200
+ ...result,
201
+ });
202
+
203
+ // L0 실패 — 로컬: exit handler에서 처리. 원격: probe가 유일한 감지 수단.
204
+ if (result.l0 === 'fail') {
205
+ if (session.config.remote) {
206
+ handleFailure(session, 'remote_L0_fail');
207
+ }
208
+ return;
209
+ }
210
+
211
+ // L3 completed (원격 완료 토큰 감지)
212
+ if (result.l3 === 'completed' && session.config.remote) {
213
+ transition(session, STATES.COMPLETED, 'remote_completion_token');
214
+ emitter.emit('completed', { sessionId: session.id });
215
+ if (typeof session.config.onCompleted === 'function') {
216
+ session.config.onCompleted({ sessionId: session.id });
217
+ }
218
+ maybeAutoShutdown();
219
+ return;
220
+ }
221
+
222
+ // L1 INPUT_WAIT 감지
223
+ if (result.l1 === 'input_wait' && session.state === STATES.HEALTHY) {
224
+ transition(session, STATES.INPUT_WAIT, `input_wait:${result.inputWaitPattern}`);
225
+ emitter.emit('inputWait', {
226
+ sessionId: session.id,
227
+ pattern: result.inputWaitPattern,
228
+ });
229
+ return;
230
+ }
231
+
232
+ // INPUT_WAIT → output 재개 시 HEALTHY 복귀
233
+ if (session.state === STATES.INPUT_WAIT && result.l1 === 'ok') {
234
+ transition(session, STATES.HEALTHY, 'output_resumed');
235
+ return;
236
+ }
237
+
238
+ // L1 stall
239
+ if (result.l1 === 'stall' && session.state === STATES.HEALTHY) {
240
+ transition(session, STATES.STALLED, 'L1_stall');
241
+ return;
242
+ }
243
+
244
+ // STALLED → output 재개 시 HEALTHY 복귀
245
+ if (session.state === STATES.STALLED && result.l1 === 'ok') {
246
+ transition(session, STATES.HEALTHY, 'output_resumed');
247
+ return;
248
+ }
249
+
250
+ // L3 timeout (아직 STARTING 상태)
251
+ if (result.l3 === 'timeout' && session.state === STATES.STARTING) {
252
+ handleFailure(session, 'L3_timeout');
253
+ return;
254
+ }
255
+
256
+ // STARTING → L0 ok + L3 ok → HEALTHY
257
+ if (session.state === STATES.STARTING && result.l0 === 'ok' && result.l3 === 'ok') {
258
+ transition(session, STATES.HEALTHY, 'probe_healthy');
259
+ return;
260
+ }
261
+
262
+ // STARTING → L0 ok (L3 아직 미판정) → STARTING 유지 (대기)
263
+ }
264
+
265
+ /**
266
+ * 실패 처리 — restart 또는 DEAD.
267
+ */
268
+ function handleFailure(session, reason) {
269
+ if (TERMINAL_STATES.has(session.state)) return;
270
+
271
+ transition(session, STATES.FAILED, reason);
272
+
273
+ if (session.restarts < maxRestarts) {
274
+ transition(session, STATES.RESTARTING, `restart_${session.restarts + 1}/${maxRestarts}`);
275
+ session.restarts += 1;
276
+ void respawnSession(session);
277
+ } else {
278
+ transition(session, STATES.DEAD, `maxRestarts(${maxRestarts})_exceeded`);
279
+ emitter.emit('dead', { sessionId: session.id, reason });
280
+
281
+ // broker release on final death
282
+ if (broker && session.config.accountId) {
283
+ broker.release(session.config.accountId, { ok: false, failureMode: session.lastFailureMode });
284
+ if (session.lastFailureMode === 'rate_limited') {
285
+ broker.markRateLimited(session.config.accountId, 5 * 60 * 1000);
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * 세션의 child process를 (재)시작.
293
+ */
294
+ async function respawnSession(session) {
295
+ // 기존 child 정리
296
+ await cleanupChild(session);
297
+
298
+ transition(session, STATES.STARTING, session.restarts > 0 ? 'respawn' : 'initial');
299
+
300
+ const launcher = session.launcher;
301
+ const outPath = join(logsDir, `${session.id}.out.log`);
302
+ const errPath = join(logsDir, `${session.id}.err.log`);
303
+ mkdirSync(logsDir, { recursive: true });
304
+
305
+ const outWs = createWriteStream(outPath, { flags: 'a' });
306
+ const errWs = createWriteStream(errPath, { flags: 'a' });
307
+
308
+ let outputBytes = 0;
309
+ let recentOutput = '';
310
+
311
+ let child;
312
+ try {
313
+ child = spawn(launcher.command, {
314
+ shell: true,
315
+ env: { ...process.env, ...launcher.env, ...(session.config.env || {}) },
316
+ stdio: ['pipe', 'pipe', 'pipe'],
317
+ windowsHide: true,
318
+ });
319
+ } catch (err) {
320
+ eventLog.append('spawn_error', { session: session.id, error: err.message });
321
+ handleFailure(session, `spawn_error:${err.message}`);
322
+ return;
323
+ }
324
+
325
+ session.child = child;
326
+ session.outPath = outPath;
327
+ session.errPath = errPath;
328
+
329
+ eventLog.append('spawn', {
330
+ session: session.id,
331
+ agent: session.config.agent,
332
+ pid: child.pid,
333
+ command: launcher.command,
334
+ restart: session.restarts,
335
+ });
336
+
337
+ // stdout+stderr 통합 추적 (F3 해결: stderr만 출력되는 경우도 advancing 판정)
338
+ const trackOutput = (buf) => {
339
+ outputBytes += buf.length;
340
+ const txt = String(buf);
341
+ // 최근 2KB만 유지 (INPUT_WAIT 패턴 감지용)
342
+ recentOutput += txt;
343
+ if (recentOutput.length > 2048) {
344
+ recentOutput = recentOutput.slice(-2048);
345
+ }
346
+ };
347
+
348
+ child.stdout?.on('data', (buf) => { outWs.write(buf); trackOutput(buf); });
349
+ child.stderr?.on('data', (buf) => { errWs.write(buf); trackOutput(buf); });
350
+
351
+ child.on('exit', (code, signal) => {
352
+ session.alive = false;
353
+ try { outWs.end(); } catch { /* ignore */ }
354
+ try { errWs.end(); } catch { /* ignore */ }
355
+
356
+ eventLog.append('exit', {
357
+ session: session.id,
358
+ code,
359
+ signal,
360
+ restart: session.restarts,
361
+ });
362
+
363
+ if (TERMINAL_STATES.has(session.state)) return;
364
+
365
+ if (code === 0 && !signal) {
366
+ transition(session, STATES.COMPLETED, 'exit_0');
367
+ emitter.emit('completed', { sessionId: session.id });
368
+ if (typeof session.config.onCompleted === 'function') {
369
+ session.config.onCompleted({ sessionId: session.id });
370
+ }
371
+ if (broker && session.config.accountId) {
372
+ broker.release(session.config.accountId, { ok: true });
373
+ }
374
+ } else {
375
+ // detect rate_limited from recent output before handleFailure
376
+ if (/(rate.?limit|quota|throttl|too.many.requests|429|usage.limit)/ui.test(recentOutput)) {
377
+ session.lastFailureMode = 'rate_limited';
378
+ }
379
+ handleFailure(session, `exit_code:${code},signal:${signal}`);
380
+ }
381
+
382
+ maybeAutoShutdown();
383
+ });
384
+
385
+ child.on('error', (err) => {
386
+ session.alive = false;
387
+ eventLog.append('child_error', { session: session.id, error: err.message });
388
+ if (!TERMINAL_STATES.has(session.state)) {
389
+ handleFailure(session, `child_error:${err.message}`);
390
+ }
391
+ });
392
+
393
+ session.alive = true;
394
+
395
+ // Health probe 설정
396
+ session.probe?.stop();
397
+ const probe = createHealthProbe(
398
+ {
399
+ get pid() { return child.pid; },
400
+ get alive() { return session.alive; },
401
+ getOutputBytes: () => outputBytes,
402
+ getRecentOutput: () => recentOutput,
403
+ },
404
+ {
405
+ ...probeOpts,
406
+ onProbe: (result) => handleProbeResult(session, result),
407
+ },
408
+ );
409
+ session.probe = probe;
410
+ probe.start();
411
+ }
412
+
413
+ /**
414
+ * 원격 세션 시작 — child process 대신 SSH capture-pane 폴링.
415
+ * 원격 세션은 remote-spawn.mjs가 이미 psmux 세션을 생성한 상태를 가정.
416
+ */
417
+ function startRemoteSession(session) {
418
+ transition(session, STATES.STARTING, 'remote_initial');
419
+
420
+ const { host, paneTarget, sessionName } = session.config;
421
+ const resolvedPane = paneTarget || `${sessionName || session.id}:0.0`;
422
+ const resolvedSessionName = sessionName || session.id;
423
+
424
+ eventLog.append('remote_start', {
425
+ session: session.id,
426
+ host,
427
+ paneTarget: resolvedPane,
428
+ sessionName: resolvedSessionName,
429
+ });
430
+
431
+ session.alive = true;
432
+
433
+ // Remote health probe 설정
434
+ session.probe?.stop();
435
+ const probe = createRemoteProbe(
436
+ {
437
+ host,
438
+ paneTarget: resolvedPane,
439
+ sessionName: resolvedSessionName,
440
+ },
441
+ {
442
+ ...probeOpts,
443
+ onProbe: (result) => handleProbeResult(session, result),
444
+ },
445
+ );
446
+ session.probe = probe;
447
+ probe.start();
448
+ }
449
+
450
+ /**
451
+ * 모든 세션이 terminal이면 auto-shutdown.
452
+ */
453
+ function maybeAutoShutdown() {
454
+ if (shuttingDown) return;
455
+ const allTerminal = [...sessions.values()].every(
456
+ (s) => TERMINAL_STATES.has(s.state),
457
+ );
458
+ if (allTerminal && sessions.size > 0) {
459
+ emitter.emit('allCompleted');
460
+ }
461
+ }
462
+
463
+ // ── Public API ──────────────────────────────────────────────
464
+
465
+ /**
466
+ * 새 세션 spawn.
467
+ * @param {object} config
468
+ * @param {string} config.id — 세션 ID (unique)
469
+ * @param {'codex'|'gemini'|'claude'} config.agent
470
+ * @param {string} config.prompt
471
+ * @param {string} [config.profile]
472
+ * @param {string} [config.workdir]
473
+ * @param {string} [config.model]
474
+ * @param {boolean} [config.remote=false] — 원격 세션 여부
475
+ * @param {string} [config.host] — SSH 호스트 (remote=true 필수)
476
+ * @param {string} [config.paneTarget] — psmux pane target (remote용)
477
+ * @param {string} [config.sessionName] — psmux 세션 이름 (remote용)
478
+ * @param {function} [config.onCompleted] — 세션 완료 시 콜백 ({sessionId}) => void
479
+ * @returns {string} session ID
480
+ */
481
+ function spawnSession(config) {
482
+ if (shuttingDown) throw new Error('Conductor is shutting down');
483
+ if (!config.id) throw new Error('session id is required');
484
+ if (sessions.has(config.id)) throw new Error(`Session "${config.id}" already exists`);
485
+ if (config.remote && !config.host) throw new Error('host is required for remote sessions');
486
+
487
+ // broker lease (graceful — broker null if accounts.json absent)
488
+ let lease = null;
489
+ if (broker && config.agent && !config.remote) {
490
+ lease = broker.lease({ provider: config.agent });
491
+ if (lease === null) {
492
+ const eta = broker.nextAvailableEta(config.agent);
493
+ eventLog.append('broker_no_lease', {
494
+ session: config.id,
495
+ agent: config.agent,
496
+ eta: eta ? new Date(eta).toISOString() : 'unknown',
497
+ });
498
+ // 계정이 모두 cooldown이어도 세션 생성 자체는 유지한다.
499
+ // 로컬 테스트/단일 계정 없는 환경에서도 상태 머신이 일관되게 동작해야 한다.
500
+ }
501
+ }
502
+
503
+ // apply lease profile/env/auth to config (immutable — new object)
504
+ const resolvedConfig = lease
505
+ ? {
506
+ ...config,
507
+ profile: lease.profile ?? config.profile,
508
+ env: { ...(config.env || {}), ...(lease.env || {}) },
509
+ accountId: lease.id,
510
+ }
511
+ : config;
512
+
513
+ // auth file copy — broker resolved absolute path, conductor does the actual copy
514
+ if (lease?.mode === 'auth' && lease.authFile) {
515
+ const dests = config.agent === 'codex'
516
+ ? [join(homedir(), '.codex', 'auth.json')]
517
+ : [
518
+ join(homedir(), '.gemini', 'oauth_creds.json'),
519
+ join(homedir(), '.gemini', 'gemini-credentials.json'),
520
+ ];
521
+ for (const dest of dests) {
522
+ try {
523
+ mkdirSync(dirname(dest), { recursive: true });
524
+ copyFileSync(lease.authFile, dest);
525
+ eventLog.append('auth_copy', { session: config.id, agent: config.agent, dest });
526
+ } catch (err) {
527
+ eventLog.append('auth_copy_error', { session: config.id, dest, error: err.message });
528
+ }
529
+ }
530
+ }
531
+
532
+ // 원격 세션은 launcher 불필요 (이미 원격에서 실행 중)
533
+ const launcher = resolvedConfig.remote
534
+ ? null
535
+ : buildLauncher({
536
+ agent: resolvedConfig.agent,
537
+ profile: resolvedConfig.profile,
538
+ prompt: resolvedConfig.prompt,
539
+ workdir: resolvedConfig.workdir,
540
+ model: resolvedConfig.model,
541
+ });
542
+
543
+ const session = {
544
+ id: resolvedConfig.id,
545
+ config: resolvedConfig,
546
+ launcher,
547
+ state: STATES.INIT,
548
+ child: null,
549
+ probe: null,
550
+ alive: false,
551
+ restarts: 0,
552
+ outPath: null,
553
+ errPath: null,
554
+ createdAt: Date.now(),
555
+ };
556
+
557
+ sessions.set(resolvedConfig.id, session);
558
+
559
+ if (resolvedConfig.remote) {
560
+ startRemoteSession(session);
561
+ } else {
562
+ void respawnSession(session);
563
+ }
564
+ return resolvedConfig.id;
565
+ }
566
+
567
+ /**
568
+ * 세션 kill.
569
+ * @param {string} id
570
+ * @param {string} [reason]
571
+ */
572
+ async function killSession(id, reason = 'user_kill') {
573
+ const session = sessions.get(id);
574
+ if (!session) return;
575
+ if (TERMINAL_STATES.has(session.state)) return;
576
+
577
+ eventLog.append('kill', { session: id, reason });
578
+ await cleanupChild(session);
579
+ transition(session, STATES.FAILED, reason);
580
+ transition(session, STATES.DEAD, reason);
581
+ }
582
+
583
+ /**
584
+ * 세션에 stdin 입력 전송 (INPUT_WAIT 해소용).
585
+ * @param {string} id
586
+ * @param {string} text
587
+ */
588
+ function sendInput(id, text) {
589
+ const session = sessions.get(id);
590
+ if (!session) return false;
591
+
592
+ // 원격 세션 — stdin 미지원 (psmux send-keys는 별도 경로)
593
+ if (session.config.remote) {
594
+ eventLog.append('stdin_remote_unsupported', { session: id });
595
+ return false;
596
+ }
597
+
598
+ if (!session.child) return false;
599
+ try {
600
+ session.child.stdin.write(`${text}\n`);
601
+ eventLog.append('stdin', { session: id, text: text.slice(0, 100) });
602
+ return true;
603
+ } catch {
604
+ return false;
605
+ }
606
+ }
607
+
608
+ /**
609
+ * 전체 세션 스냅샷.
610
+ * @returns {object[]}
611
+ */
612
+ function getSnapshot() {
613
+ return [...sessions.values()].map((s) => ({
614
+ id: s.id,
615
+ agent: s.config.agent,
616
+ state: s.state,
617
+ pid: s.child?.pid || null,
618
+ remote: s.config.remote || false,
619
+ host: s.config.host || null,
620
+ restarts: s.restarts,
621
+ health: s.probe?.getStatus() || null,
622
+ outPath: s.outPath,
623
+ errPath: s.errPath,
624
+ createdAt: s.createdAt,
625
+ }));
626
+ }
627
+
628
+ /**
629
+ * Graceful shutdown — 전체 세션 종료.
630
+ */
631
+ async function shutdown(reason = 'shutdown') {
632
+ if (shuttingDown) return;
633
+ shuttingDown = true;
634
+
635
+ eventLog.append('shutdown', { reason, sessions: sessions.size });
636
+
637
+ const cleanups = [...sessions.values()]
638
+ .filter((s) => !TERMINAL_STATES.has(s.state))
639
+ .map(async (s) => {
640
+ s.probe?.stop();
641
+ await cleanupChild(s);
642
+ if (!TERMINAL_STATES.has(s.state)) {
643
+ transition(s, STATES.FAILED, reason);
644
+ transition(s, STATES.DEAD, reason);
645
+ }
646
+ });
647
+
648
+ await Promise.allSettled(cleanups);
649
+ await eventLog.flush();
650
+ await eventLog.close();
651
+ emitter.emit('shutdown');
652
+ }
653
+
654
+ // Shutdown traps
655
+ const onSignal = () => { void shutdown('signal'); };
656
+ process.on('SIGINT', onSignal);
657
+ process.on('SIGTERM', onSignal);
658
+
659
+ return Object.freeze({
660
+ spawnSession,
661
+ killSession,
662
+ sendInput,
663
+ getSnapshot,
664
+ shutdown,
665
+ on: emitter.on.bind(emitter),
666
+ off: emitter.off.bind(emitter),
667
+ get sessionCount() { return sessions.size; },
668
+ get isShuttingDown() { return shuttingDown; },
669
+ get eventLogPath() { return eventLog.filePath; },
670
+ });
671
+ }
@@ -0,0 +1,14 @@
1
+ const DASHBOARD_ANCHORS = new Set([
2
+ "window",
3
+ "tab",
4
+ ]);
5
+
6
+ export function normalizeDashboardAnchor(value) {
7
+ const normalized = String(value ?? "").trim().toLowerCase();
8
+ if (!normalized) return "window";
9
+ return DASHBOARD_ANCHORS.has(normalized) ? normalized : "window";
10
+ }
11
+
12
+ export function parseDashboardAnchor(value) {
13
+ return normalizeDashboardAnchor(value);
14
+ }