triflux 10.3.4 → 10.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/LICENSE +21 -21
  2. package/bin/tfx-doctor-tui.mjs +1 -1
  3. package/bin/tfx-doctor.mjs +6 -1
  4. package/bin/tfx-profile.mjs +1 -1
  5. package/bin/tfx-setup-tui.mjs +1 -1
  6. package/bin/tfx-setup.mjs +6 -1
  7. package/bin/triflux.mjs +2396 -1140
  8. package/hooks/agent-route-guard.mjs +12 -8
  9. package/hooks/cross-review-tracker.mjs +21 -8
  10. package/hooks/error-context.mjs +19 -7
  11. package/hooks/hook-adaptive-collector.mjs +18 -16
  12. package/hooks/hook-manager.mjs +93 -32
  13. package/hooks/hook-orchestrator.mjs +108 -24
  14. package/hooks/hook-registry.json +11 -0
  15. package/hooks/keyword-rules.json +6 -10
  16. package/hooks/lib/resolve-root.mjs +1 -1
  17. package/hooks/mcp-config-watcher.mjs +6 -2
  18. package/hooks/pipeline-stop.mjs +3 -6
  19. package/hooks/safety-guard.mjs +99 -28
  20. package/hooks/session-start-fast.mjs +143 -0
  21. package/hooks/subagent-verifier.mjs +5 -4
  22. package/hub/account-broker.mjs +256 -60
  23. package/hub/adaptive-diagnostic.mjs +75 -48
  24. package/hub/adaptive-inject.mjs +95 -57
  25. package/hub/adaptive-memory.mjs +156 -42
  26. package/hub/adaptive.mjs +60 -31
  27. package/hub/assign-callbacks.mjs +67 -30
  28. package/hub/bridge.mjs +0 -1
  29. package/hub/cli-adapter-base.mjs +200 -48
  30. package/hub/codex-adapter.mjs +76 -96
  31. package/hub/codex-compat.mjs +3 -3
  32. package/hub/codex-preflight.mjs +63 -37
  33. package/hub/delegator/contracts.mjs +19 -23
  34. package/hub/delegator/index.mjs +3 -3
  35. package/hub/delegator/service.mjs +88 -64
  36. package/hub/delegator/tool-definitions.mjs +5 -5
  37. package/hub/fullcycle.mjs +33 -17
  38. package/hub/gemini-adapter.mjs +69 -94
  39. package/hub/hitl.mjs +89 -30
  40. package/hub/intent.mjs +161 -38
  41. package/hub/lib/cache-guard.mjs +43 -17
  42. package/hub/lib/mcp-response-cache.mjs +66 -32
  43. package/hub/lib/memory-store.mjs +285 -111
  44. package/hub/lib/path-utils.mjs +35 -37
  45. package/hub/lib/process-utils.mjs +106 -37
  46. package/hub/lib/spawn-trace.mjs +527 -0
  47. package/hub/lib/ssh-command.mjs +34 -4
  48. package/hub/lib/ssh-retry.mjs +5 -1
  49. package/hub/lib/uuidv7.mjs +4 -3
  50. package/hub/memory-doctor.mjs +266 -106
  51. package/hub/middleware/request-logger.mjs +61 -34
  52. package/hub/paths.mjs +9 -9
  53. package/hub/pipeline/gates/confidence.mjs +34 -15
  54. package/hub/pipeline/gates/consensus.mjs +27 -15
  55. package/hub/pipeline/gates/index.mjs +7 -3
  56. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  57. package/hub/pipeline/index.mjs +77 -42
  58. package/hub/pipeline/state.mjs +10 -10
  59. package/hub/pipeline/transitions.mjs +40 -23
  60. package/hub/platform.mjs +57 -48
  61. package/hub/promote-penalties.mjs +25 -7
  62. package/hub/quality/deslop.mjs +70 -49
  63. package/hub/research.mjs +32 -25
  64. package/hub/router.mjs +240 -107
  65. package/hub/routing/complexity.mjs +132 -29
  66. package/hub/routing/index.mjs +17 -12
  67. package/hub/routing/q-learning.mjs +76 -28
  68. package/hub/server.mjs +4 -4
  69. package/hub/session-fingerprint.mjs +126 -60
  70. package/hub/state.mjs +84 -43
  71. package/hub/store-adapter.mjs +59 -26
  72. package/hub/store.mjs +356 -153
  73. package/hub/team/agent-map.json +22 -7
  74. package/hub/team/ansi.mjs +186 -122
  75. package/hub/team/backend.mjs +28 -10
  76. package/hub/team/cli/commands/attach.mjs +29 -9
  77. package/hub/team/cli/commands/control.mjs +29 -8
  78. package/hub/team/cli/commands/debug.mjs +32 -11
  79. package/hub/team/cli/commands/focus.mjs +38 -11
  80. package/hub/team/cli/commands/interrupt.mjs +18 -6
  81. package/hub/team/cli/commands/kill.mjs +16 -5
  82. package/hub/team/cli/commands/list.mjs +11 -4
  83. package/hub/team/cli/commands/send.mjs +19 -6
  84. package/hub/team/cli/commands/start/index.mjs +154 -31
  85. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  86. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  87. package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
  88. package/hub/team/cli/commands/start/start-mux.mjs +70 -21
  89. package/hub/team/cli/commands/start/start-wt.mjs +29 -12
  90. package/hub/team/cli/commands/status.mjs +43 -14
  91. package/hub/team/cli/commands/stop.mjs +11 -4
  92. package/hub/team/cli/commands/task.mjs +8 -3
  93. package/hub/team/cli/commands/tasks.mjs +1 -1
  94. package/hub/team/cli/index.mjs +2 -2
  95. package/hub/team/cli/manifest.mjs +38 -8
  96. package/hub/team/cli/render.mjs +30 -8
  97. package/hub/team/cli/services/attach-fallback.mjs +31 -11
  98. package/hub/team/cli/services/hub-client.mjs +42 -14
  99. package/hub/team/cli/services/member-selector.mjs +11 -4
  100. package/hub/team/cli/services/native-control.mjs +48 -21
  101. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  102. package/hub/team/cli/services/state-store.mjs +25 -8
  103. package/hub/team/cli/services/task-model.mjs +16 -6
  104. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  105. package/hub/team/conductor.mjs +8 -4
  106. package/hub/team/dashboard-anchor.mjs +4 -5
  107. package/hub/team/dashboard-layout.mjs +3 -1
  108. package/hub/team/dashboard-open.mjs +41 -21
  109. package/hub/team/dashboard.mjs +76 -28
  110. package/hub/team/event-log.mjs +18 -10
  111. package/hub/team/handoff.mjs +31 -15
  112. package/hub/team/headless.mjs +2 -1
  113. package/hub/team/health-probe.mjs +69 -54
  114. package/hub/team/launcher-template.mjs +16 -13
  115. package/hub/team/native-supervisor.mjs +65 -21
  116. package/hub/team/native.mjs +74 -35
  117. package/hub/team/nativeProxy.mjs +184 -113
  118. package/hub/team/notify.mjs +119 -76
  119. package/hub/team/orchestrator.mjs +9 -4
  120. package/hub/team/pane.mjs +12 -7
  121. package/hub/team/process-cleanup.mjs +25 -16
  122. package/hub/team/psmux.mjs +491 -201
  123. package/hub/team/remote-probe.mjs +68 -52
  124. package/hub/team/remote-session.mjs +117 -59
  125. package/hub/team/remote-watcher.mjs +61 -33
  126. package/hub/team/routing.mjs +51 -25
  127. package/hub/team/runtime-strategy.mjs +3 -1
  128. package/hub/team/session.mjs +98 -34
  129. package/hub/team/staleState.mjs +72 -30
  130. package/hub/team/swarm-locks.mjs +15 -13
  131. package/hub/team/swarm-planner.mjs +32 -21
  132. package/hub/team/swarm-reconciler.mjs +48 -23
  133. package/hub/team/tui-lite.mjs +266 -68
  134. package/hub/team/tui-remote-adapter.mjs +14 -10
  135. package/hub/team/tui-viewer.mjs +99 -43
  136. package/hub/team/tui.mjs +708 -271
  137. package/hub/team/worktree-lifecycle.mjs +152 -58
  138. package/hub/team/wt-manager.mjs +24 -14
  139. package/hub/token-mode.mjs +71 -71
  140. package/hub/tray.mjs +66 -23
  141. package/hub/workers/claude-worker.mjs +162 -118
  142. package/hub/workers/codex-mcp.mjs +192 -141
  143. package/hub/workers/delegator-mcp.mjs +507 -333
  144. package/hub/workers/factory.mjs +8 -8
  145. package/hub/workers/gemini-worker.mjs +115 -84
  146. package/hub/workers/interface.mjs +6 -1
  147. package/hub/workers/worker-utils.mjs +21 -14
  148. package/hud/colors.mjs +27 -9
  149. package/hud/constants.mjs +162 -26
  150. package/hud/context-monitor.mjs +82 -41
  151. package/hud/hud-qos-status.mjs +129 -49
  152. package/hud/mission-board.mjs +6 -3
  153. package/hud/providers/claude.mjs +226 -115
  154. package/hud/providers/codex.mjs +62 -22
  155. package/hud/providers/gemini.mjs +168 -56
  156. package/hud/renderers.mjs +384 -119
  157. package/hud/terminal.mjs +101 -31
  158. package/hud/utils.mjs +78 -38
  159. package/mesh/index.mjs +11 -5
  160. package/mesh/mesh-budget.mjs +18 -9
  161. package/mesh/mesh-heartbeat.mjs +1 -1
  162. package/mesh/mesh-queue.mjs +3 -5
  163. package/mesh/mesh-router.mjs +5 -4
  164. package/package.json +2 -1
  165. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  166. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  167. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  168. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  169. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  170. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  171. package/scripts/__tests__/skill-template.test.mjs +98 -50
  172. package/scripts/__tests__/smoke.test.mjs +1 -1
  173. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  174. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  175. package/scripts/cache-doctor.mjs +11 -4
  176. package/scripts/cache-warmup.mjs +96 -37
  177. package/scripts/claudemd-sync.mjs +27 -17
  178. package/scripts/codex-gateway-preflight.mjs +52 -37
  179. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  180. package/scripts/completions/tfx.bash +47 -47
  181. package/scripts/completions/tfx.fish +44 -44
  182. package/scripts/completions/tfx.zsh +83 -83
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +45 -26
  193. package/scripts/keyword-detector.mjs +41 -20
  194. package/scripts/keyword-rules-expander.mjs +47 -30
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +300 -63
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +199 -84
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
  229. package/scripts/tfx-batch-stats.mjs +32 -11
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  237. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  238. package/skills/.omc/state/last-tool-error.json +7 -0
  239. package/skills/.omc/state/subagent-tracking.json +7 -0
  240. package/skills/_templates/base.md +1 -6
  241. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  242. package/skills/shared/telemetry-segment.md +6 -0
  243. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  244. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  245. package/skills/tfx-analysis/skill.json +1 -6
  246. package/skills/tfx-auto/SKILL.md +1 -0
  247. package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
  248. package/skills/tfx-auto-codex/skill.json +1 -3
  249. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  250. package/skills/tfx-autopilot/skill.json +1 -5
  251. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  252. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  253. package/skills/tfx-autoroute/skill.json +1 -7
  254. package/skills/tfx-codex/SKILL.md +1 -0
  255. package/skills/tfx-codex/skill.json +1 -3
  256. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  257. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  258. package/skills/tfx-codex-swarm/skill.json +1 -4
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  265. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  266. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  267. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  268. package/skills/tfx-debate/skill.json +1 -7
  269. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  270. package/skills/tfx-deep-analysis/skill.json +1 -5
  271. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  272. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  273. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  274. package/skills/tfx-deep-qa/skill.json +1 -6
  275. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  276. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  277. package/skills/tfx-doctor/SKILL.md +21 -0
  278. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  279. package/skills/tfx-doctor/skill.json +1 -3
  280. package/skills/tfx-find/SKILL.md +1 -0
  281. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  282. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  283. package/skills/tfx-fullcycle/skill.json +1 -6
  284. package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
  285. package/skills/tfx-gemini/skill.json +1 -3
  286. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  287. package/skills/tfx-hooks/skill.json +1 -3
  288. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  289. package/skills/tfx-hub/skill.json +1 -3
  290. package/skills/tfx-index/SKILL.md +1 -0
  291. package/skills/tfx-index/skill.json +1 -6
  292. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  293. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  294. package/skills/tfx-multi/skill.json +1 -3
  295. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  296. package/skills/tfx-panel/skill.json +1 -7
  297. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  298. package/skills/tfx-persist/skill.json +1 -7
  299. package/skills/tfx-plan/SKILL.md +1 -0
  300. package/skills/tfx-plan/skill.json +1 -6
  301. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  302. package/skills/tfx-profile/skill.json +1 -3
  303. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  304. package/skills/tfx-prune/skill.json +1 -7
  305. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  306. package/skills/tfx-psmux-rules/skill.json +1 -4
  307. package/skills/tfx-qa/SKILL.md +1 -0
  308. package/skills/tfx-qa/skill.json +1 -6
  309. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  310. package/skills/tfx-ralph/skill.json +1 -4
  311. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  312. package/skills/tfx-remote-setup/skill.json +1 -3
  313. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  314. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
  315. package/skills/tfx-remote-spawn/skill.json +1 -4
  316. package/skills/tfx-research/SKILL.md +1 -0
  317. package/skills/tfx-review/SKILL.md +1 -0
  318. package/skills/tfx-review/skill.json +1 -6
  319. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  320. package/skills/tfx-setup/skill.json +1 -3
  321. package/skills/tfx-swarm/SKILL.md +22 -0
  322. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  323. package/tui/codex-profile.mjs +88 -33
  324. package/tui/core.mjs +45 -15
  325. package/tui/doctor.mjs +75 -28
  326. package/tui/gemini-profile.mjs +74 -29
  327. package/tui/monitor-data.mjs +8 -4
  328. package/tui/monitor.mjs +71 -27
  329. package/tui/setup.mjs +133 -42
@@ -1,8 +1,8 @@
1
1
  // hub/team/psmux.mjs — Windows psmux 세션/키바인딩/캡처/steering 관리
2
2
  // 의존성: child_process, fs, os, path (Node.js 내장)만 사용
3
- import childProcess from "node:child_process";
3
+ import * as childProcess from "../lib/spawn-trace.mjs";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import { tmpdir, homedir } from "node:os";
5
+ import { homedir, tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { formatPsmuxInstallGuidance } from "../../scripts/lib/psmux-info.mjs";
8
8
  import { IS_WINDOWS } from "../platform.mjs";
@@ -11,9 +11,15 @@ const PSMUX_BIN = (() => {
11
11
  if (process.env.PSMUX_BIN) return process.env.PSMUX_BIN;
12
12
  // PATH에서 찾기
13
13
  try {
14
- childProcess.execFileSync("psmux", ["-V"], { stdio: "ignore", timeout: 2000, windowsHide: true });
14
+ childProcess.execFileSync("psmux", ["-V"], {
15
+ stdio: "ignore",
16
+ timeout: 2000,
17
+ windowsHide: true,
18
+ });
15
19
  return "psmux";
16
- } catch { /* not in PATH */ }
20
+ } catch {
21
+ /* not in PATH */
22
+ }
17
23
  // Windows 기본 설치 경로 탐색
18
24
  if (IS_WINDOWS) {
19
25
  const candidates = [
@@ -28,7 +34,8 @@ const PSMUX_BIN = (() => {
28
34
  }
29
35
  return "psmux"; // 최종 fallback — 원래대로
30
36
  })();
31
- const GIT_BASH = process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
37
+ const GIT_BASH =
38
+ process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
32
39
 
33
40
  /** Windows psmux 세션의 기본 셸을 PowerShell로 강제한다 (pwsh7 우선, ps5 fallback). */
34
41
  const PWSH_BIN = (() => {
@@ -36,25 +43,40 @@ const PWSH_BIN = (() => {
36
43
  if (process.env.PSMUX_SHELL) return process.env.PSMUX_SHELL;
37
44
  // pwsh 7 우선
38
45
  try {
39
- childProcess.execFileSync("pwsh", ["-NoLogo", "-NoProfile", "-Command", "exit 0"], { stdio: "ignore", timeout: 3000, windowsHide: true });
46
+ childProcess.execFileSync(
47
+ "pwsh",
48
+ ["-NoLogo", "-NoProfile", "-Command", "exit 0"],
49
+ { stdio: "ignore", timeout: 3000, windowsHide: true },
50
+ );
40
51
  return "pwsh";
41
- } catch { /* not found */ }
52
+ } catch {
53
+ /* not found */
54
+ }
42
55
  // powershell 5 fallback
43
56
  try {
44
- childProcess.execFileSync("powershell.exe", ["-NoLogo", "-NoProfile", "-Command", "exit 0"], { stdio: "ignore", timeout: 3000, windowsHide: true });
57
+ childProcess.execFileSync(
58
+ "powershell.exe",
59
+ ["-NoLogo", "-NoProfile", "-Command", "exit 0"],
60
+ { stdio: "ignore", timeout: 3000, windowsHide: true },
61
+ );
45
62
  return "powershell.exe";
46
- } catch { /* not found */ }
63
+ } catch {
64
+ /* not found */
65
+ }
47
66
  return ""; // 둘 다 없으면 psmux 기본 셸 사용
48
67
  })();
49
68
  const PSMUX_TIMEOUT_MS = 10000;
50
69
  const COMPLETION_PREFIX = "__TRIFLUX_DONE__:";
51
- const CAPTURE_ROOT = process.env.PSMUX_CAPTURE_ROOT || join(tmpdir(), "psmux-steering");
70
+ const CAPTURE_ROOT =
71
+ process.env.PSMUX_CAPTURE_ROOT || join(tmpdir(), "psmux-steering");
52
72
  const CAPTURE_HELPER_PATH = join(CAPTURE_ROOT, "pipe-pane-capture.ps1");
53
73
  const POLL_INTERVAL_MS = (() => {
54
74
  const ms = Number.parseInt(process.env.PSMUX_POLL_INTERVAL_MS || "", 10);
55
75
  if (Number.isFinite(ms) && ms > 0) return ms;
56
76
  const sec = Number.parseFloat(process.env.PSMUX_POLL_INTERVAL_SEC || "1");
57
- return Number.isFinite(sec) && sec > 0 ? Math.max(100, Math.trunc(sec * 1000)) : 1000;
77
+ return Number.isFinite(sec) && sec > 0
78
+ ? Math.max(100, Math.trunc(sec * 1000))
79
+ : 1000;
58
80
  })();
59
81
 
60
82
  function quoteArg(value) {
@@ -126,7 +148,7 @@ function tokenizeCommand(command) {
126
148
  continue;
127
149
  }
128
150
 
129
- if (char === "\\" && next && (/[\s"'\\;]/u.test(next))) {
151
+ if (char === "\\" && next && /[\s"'\\;]/u.test(next)) {
130
152
  current += next;
131
153
  index += 1;
132
154
  continue;
@@ -165,10 +187,10 @@ function ensurePsmuxInstalled() {
165
187
  if (!hasPsmux()) {
166
188
  throw new Error(
167
189
  "psmux가 설치되어 있지 않습니다.\n\n" +
168
- "psmux는 Codex/Gemini CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
169
- "설치 방법 (택 1):\n" +
170
- `${formatPsmuxInstallGuidance(" ")}\n\n` +
171
- "설치 후 터미널을 재시작하세요."
190
+ "psmux는 Codex/Gemini CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
191
+ "설치 방법 (택 1):\n" +
192
+ `${formatPsmuxInstallGuidance(" ")}\n\n` +
193
+ "설치 후 터미널을 재시작하세요.",
172
194
  );
173
195
  }
174
196
  }
@@ -178,7 +200,10 @@ function getCaptureSessionDir(sessionName) {
178
200
  }
179
201
 
180
202
  function getCaptureLogPath(sessionName, paneName) {
181
- return join(getCaptureSessionDir(sessionName), `${sanitizePathPart(paneName)}.log`);
203
+ return join(
204
+ getCaptureSessionDir(sessionName),
205
+ `${sanitizePathPart(paneName)}.log`,
206
+ );
182
207
  }
183
208
 
184
209
  function ensureCaptureHelper() {
@@ -247,7 +272,9 @@ function parseSessionSummaries(output) {
247
272
  }
248
273
 
249
274
  const sessionName = line.slice(0, colonIndex).trim();
250
- const flags = [...line.matchAll(/\(([^)]*)\)/g)].map((match) => match[1]).join(", ");
275
+ const flags = [...line.matchAll(/\(([^)]*)\)/g)]
276
+ .map((match) => match[1])
277
+ .join(", ");
251
278
  const attachedMatch = flags.match(/(\d+)\s+attached/);
252
279
  const attachedCount = attachedMatch
253
280
  ? parseInt(attachedMatch[1], 10)
@@ -255,9 +282,7 @@ function parseSessionSummaries(output) {
255
282
  ? 1
256
283
  : 0;
257
284
 
258
- return sessionName
259
- ? { sessionName, attachedCount }
260
- : null;
285
+ return sessionName ? { sessionName, attachedCount } : null;
261
286
  })
262
287
  .filter(Boolean);
263
288
  }
@@ -270,19 +295,25 @@ function parsePaneDetails(output) {
270
295
  .map((line) => {
271
296
  const parts = line.split("\t");
272
297
  const hasPaneIndex = parts.length >= 5;
273
- const [paneIndexText = "", title = "", paneId = "", dead = "0", deadStatus = ""] = hasPaneIndex
274
- ? parts
275
- : ["", ...parts];
276
- const exitCode = dead === "1"
277
- ? Number.parseInt(deadStatus, 10)
278
- : null;
298
+ const [
299
+ paneIndexText = "",
300
+ title = "",
301
+ paneId = "",
302
+ dead = "0",
303
+ deadStatus = "",
304
+ ] = hasPaneIndex ? parts : ["", ...parts];
305
+ const exitCode = dead === "1" ? Number.parseInt(deadStatus, 10) : null;
279
306
  const paneIndex = Number.parseInt(paneIndexText, 10);
280
307
  return {
281
308
  title,
282
309
  paneId,
283
310
  paneIndex: Number.isFinite(paneIndex) ? paneIndex : null,
284
311
  isDead: dead === "1",
285
- exitCode: Number.isFinite(exitCode) ? exitCode : dead === "1" ? 0 : null,
312
+ exitCode: Number.isFinite(exitCode)
313
+ ? exitCode
314
+ : dead === "1"
315
+ ? 0
316
+ : null,
286
317
  };
287
318
  })
288
319
  .filter((entry) => entry.paneId);
@@ -325,7 +356,9 @@ function resolvePane(sessionName, paneNameOrTarget) {
325
356
  const panes = listPaneDetails(sessionName);
326
357
 
327
358
  // 1차: title 또는 paneId 직접 매칭
328
- const direct = panes.find((entry) => entry.title === wanted || entry.paneId === wanted);
359
+ const direct = panes.find(
360
+ (entry) => entry.title === wanted || entry.paneId === wanted,
361
+ );
329
362
  if (direct) return direct;
330
363
 
331
364
  // 2차: psmux title 미설정 fallback — "lead"→0, "worker-N"→N 인덱스 매칭
@@ -340,7 +373,14 @@ function refreshCaptureSnapshot(sessionName, paneNameOrTarget) {
340
373
  const paneName = pane.title || paneNameOrTarget;
341
374
  const logPath = getCaptureLogPath(sessionName, paneName);
342
375
  mkdirSync(getCaptureSessionDir(sessionName), { recursive: true });
343
- const snapshot = psmuxExec(["capture-pane", "-t", pane.paneId, "-p", "-S", "-"]);
376
+ const snapshot = psmuxExec([
377
+ "capture-pane",
378
+ "-t",
379
+ pane.paneId,
380
+ "-p",
381
+ "-S",
382
+ "-",
383
+ ]);
344
384
  writeFileSync(logPath, snapshot, "utf8");
345
385
  return { paneId: pane.paneId, paneName, logPath, snapshot };
346
386
  }
@@ -369,7 +409,9 @@ export function sendKeysToPane(paneId, text, submit = true) {
369
409
 
370
410
  function toPatternRegExp(pattern) {
371
411
  if (pattern instanceof RegExp) {
372
- const flags = pattern.flags.includes("m") ? pattern.flags : `${pattern.flags}m`;
412
+ const flags = pattern.flags.includes("m")
413
+ ? pattern.flags
414
+ : `${pattern.flags}m`;
373
415
  return new RegExp(pattern.source, flags);
374
416
  }
375
417
  return new RegExp(String(pattern), "m");
@@ -394,13 +436,17 @@ function psmux(args, opts = {}) {
394
436
  });
395
437
  return result != null ? String(result).trim() : "";
396
438
  } catch (error) {
397
- const stderr = typeof error?.stderr === "string"
398
- ? error.stderr
399
- : error?.stderr?.toString?.("utf8") || "";
400
- const stdout = typeof error?.stdout === "string"
401
- ? error.stdout
402
- : error?.stdout?.toString?.("utf8") || "";
403
- const wrapped = new Error((stderr || stdout || error.message || "psmux command failed").trim());
439
+ const stderr =
440
+ typeof error?.stderr === "string"
441
+ ? error.stderr
442
+ : error?.stderr?.toString?.("utf8") || "";
443
+ const stdout =
444
+ typeof error?.stdout === "string"
445
+ ? error.stdout
446
+ : error?.stdout?.toString?.("utf8") || "";
447
+ const wrapped = new Error(
448
+ (stderr || stdout || error.message || "psmux command failed").trim(),
449
+ );
404
450
  wrapped.status = error.status;
405
451
  throw wrapped;
406
452
  }
@@ -439,12 +485,14 @@ export function psmuxExec(args, opts = {}) {
439
485
  * @returns {{ sessionName: string, panes: string[] }}
440
486
  */
441
487
  export function createPsmuxSession(sessionName, opts = {}) {
442
- const layout = opts.layout === "1xN" || opts.layout === "Nx1" ? opts.layout : "2x2";
488
+ const layout =
489
+ opts.layout === "1xN" || opts.layout === "Nx1" ? opts.layout : "2x2";
443
490
  const paneCount = Math.max(
444
491
  1,
445
492
  Number.isFinite(opts.paneCount) ? Math.trunc(opts.paneCount) : 4,
446
493
  );
447
- const limitedPaneCount = layout === "2x2" ? Math.min(paneCount, 4) : paneCount;
494
+ const limitedPaneCount =
495
+ layout === "2x2" ? Math.min(paneCount, 4) : paneCount;
448
496
  const sessionTarget = `${sessionName}:0`;
449
497
 
450
498
  const newSessionArgs = [
@@ -466,7 +514,17 @@ export function createPsmuxSession(sessionName, opts = {}) {
466
514
 
467
515
  // split-window로 생성되는 pane도 동일 셸 사용
468
516
  if (PWSH_BIN) {
469
- try { psmuxExec(["set-option", "-t", sessionName, "default-command", `${PWSH_BIN} -NoLogo -NoProfile`]); } catch { /* 미지원 시 무시 */ }
517
+ try {
518
+ psmuxExec([
519
+ "set-option",
520
+ "-t",
521
+ sessionName,
522
+ "default-command",
523
+ `${PWSH_BIN} -NoLogo -NoProfile`,
524
+ ]);
525
+ } catch {
526
+ /* 미지원 시 무시 */
527
+ }
470
528
  }
471
529
 
472
530
  if (layout === "2x2" && limitedPaneCount >= 3) {
@@ -547,7 +605,11 @@ export function createPsmuxSession(sessionName, opts = {}) {
547
605
  function collectPanePids(sessionName) {
548
606
  try {
549
607
  const output = psmuxExec([
550
- "list-panes", "-t", sessionName, "-F", "#{pane_pid}",
608
+ "list-panes",
609
+ "-t",
610
+ sessionName,
611
+ "-F",
612
+ "#{pane_pid}",
551
613
  ]);
552
614
  return output
553
615
  .split(/\r?\n/)
@@ -601,9 +663,17 @@ function killOrphanPipeHelpers(sessionName) {
601
663
  try {
602
664
  const output = childProcess.execSync(
603
665
  `powershell -NoProfile -WindowStyle Hidden -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'pipe-pane-capture' -and $_.CommandLine -match 'tfx-headless[/\\\\]${safeSession}' } | Select-Object -ExpandProperty ProcessId"`,
604
- { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
666
+ {
667
+ encoding: "utf8",
668
+ timeout: 8000,
669
+ stdio: ["pipe", "pipe", "pipe"],
670
+ windowsHide: true,
671
+ },
605
672
  );
606
- const pids = output.split(/\r?\n/).map((l) => Number.parseInt(l.trim(), 10)).filter((p) => Number.isFinite(p) && p > 0);
673
+ const pids = output
674
+ .split(/\r?\n/)
675
+ .map((l) => Number.parseInt(l.trim(), 10))
676
+ .filter((p) => Number.isFinite(p) && p > 0);
607
677
  for (const pid of pids) {
608
678
  killProcessTree(pid);
609
679
  }
@@ -625,7 +695,13 @@ function killOrphanMcpProcesses(sessionName) {
625
695
  // Hub PID 보호 — Hub 프로세스를 고아로 잘못 식별하지 않도록
626
696
  let hubPid = null;
627
697
  try {
628
- const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
698
+ const hubPidPath = join(
699
+ homedir(),
700
+ ".claude",
701
+ "cache",
702
+ "tfx-hub",
703
+ "hub.pid",
704
+ );
629
705
  if (existsSync(hubPidPath)) {
630
706
  const hubInfo = JSON.parse(readFileSync(hubPidPath, "utf8"));
631
707
  hubPid = Number(hubInfo?.pid);
@@ -636,9 +712,17 @@ function killOrphanMcpProcesses(sessionName) {
636
712
  // 세션 결과 디렉토리 패턴으로 MCP 서버 프로세스 식별
637
713
  const output = childProcess.execSync(
638
714
  `powershell -NoProfile -WindowStyle Hidden -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -match 'tfx-headless[/\\\\]${safeSession}' } | Select-Object -ExpandProperty ProcessId"`,
639
- { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
715
+ {
716
+ encoding: "utf8",
717
+ timeout: 8000,
718
+ stdio: ["pipe", "pipe", "pipe"],
719
+ windowsHide: true,
720
+ },
640
721
  );
641
- const pids = output.split(/\r?\n/).map((l) => Number.parseInt(l.trim(), 10)).filter((p) => Number.isFinite(p) && p > 0 && p !== hubPid);
722
+ const pids = output
723
+ .split(/\r?\n/)
724
+ .map((l) => Number.parseInt(l.trim(), 10))
725
+ .filter((p) => Number.isFinite(p) && p > 0 && p !== hubPid);
642
726
  for (const pid of pids) {
643
727
  killProcessTree(pid);
644
728
  }
@@ -661,12 +745,15 @@ function detachAttachedClients(sessionName, waitMs = 750) {
661
745
 
662
746
  function findFallbackPane(sessionName, excludedPaneId) {
663
747
  try {
664
- const panes = listPaneDetails(sessionName)
665
- .filter((pane) => pane.paneId !== excludedPaneId && !pane.isDead);
748
+ const panes = listPaneDetails(sessionName).filter(
749
+ (pane) => pane.paneId !== excludedPaneId && !pane.isDead,
750
+ );
666
751
  if (panes.length === 0) return null;
667
- return panes.find((pane) => pane.title === "lead")
668
- || panes.find((pane) => pane.paneIndex === 0)
669
- || panes[0];
752
+ return (
753
+ panes.find((pane) => pane.title === "lead") ||
754
+ panes.find((pane) => pane.paneIndex === 0) ||
755
+ panes[0]
756
+ );
670
757
  } catch {
671
758
  return null;
672
759
  }
@@ -760,11 +847,15 @@ export function capturePsmuxPane(target, lines = 5) {
760
847
  * @param {string} sessionName
761
848
  */
762
849
  export function attachPsmuxSession(sessionName) {
763
- const result = childProcess.spawnSync(PSMUX_BIN, ["attach-session", "-t", sessionName], {
764
- stdio: "inherit",
765
- timeout: 0,
766
- windowsHide: false,
767
- });
850
+ const result = childProcess.spawnSync(
851
+ PSMUX_BIN,
852
+ ["attach-session", "-t", sessionName],
853
+ {
854
+ stdio: "inherit",
855
+ timeout: 0,
856
+ windowsHide: false,
857
+ },
858
+ );
768
859
  if ((result.status ?? 1) !== 0) {
769
860
  throw new Error(`psmux attach 실패 (exit=${result.status})`);
770
861
  }
@@ -777,8 +868,9 @@ export function attachPsmuxSession(sessionName) {
777
868
  */
778
869
  export function getPsmuxSessionAttachedCount(sessionName) {
779
870
  try {
780
- const session = parseSessionSummaries(psmuxExec(["list-sessions"]))
781
- .find((entry) => entry.sessionName === sessionName);
871
+ const session = parseSessionSummaries(psmuxExec(["list-sessions"])).find(
872
+ (entry) => entry.sessionName === sessionName,
873
+ );
782
874
  return session ? session.attachedCount : null;
783
875
  } catch {
784
876
  return null;
@@ -812,12 +904,78 @@ export function configurePsmuxKeybindings(sessionName, opts = {}) {
812
904
  }
813
905
  };
814
906
 
815
- bindSafe(["bind-key", "-T", "root", "-n", "S-Down", "if-shell", "-F", cond, bindNext, "send-keys S-Down"]);
816
- bindSafe(["bind-key", "-T", "root", "-n", "S-Up", "if-shell", "-F", cond, bindPrev, "send-keys S-Up"]);
817
- bindSafe(["bind-key", "-T", "root", "-n", "S-Right", "if-shell", "-F", cond, bindNext, "send-keys S-Right"]);
818
- bindSafe(["bind-key", "-T", "root", "-n", "S-Left", "if-shell", "-F", cond, bindPrev, "send-keys S-Left"]);
819
- bindSafe(["bind-key", "-T", "root", "-n", "BTab", "if-shell", "-F", cond, bindPrev, "send-keys BTab"]);
820
- bindSafe(["bind-key", "-T", "root", "-n", "Escape", "if-shell", "-F", cond, "send-keys C-c", "send-keys Escape"]);
907
+ bindSafe([
908
+ "bind-key",
909
+ "-T",
910
+ "root",
911
+ "-n",
912
+ "S-Down",
913
+ "if-shell",
914
+ "-F",
915
+ cond,
916
+ bindNext,
917
+ "send-keys S-Down",
918
+ ]);
919
+ bindSafe([
920
+ "bind-key",
921
+ "-T",
922
+ "root",
923
+ "-n",
924
+ "S-Up",
925
+ "if-shell",
926
+ "-F",
927
+ cond,
928
+ bindPrev,
929
+ "send-keys S-Up",
930
+ ]);
931
+ bindSafe([
932
+ "bind-key",
933
+ "-T",
934
+ "root",
935
+ "-n",
936
+ "S-Right",
937
+ "if-shell",
938
+ "-F",
939
+ cond,
940
+ bindNext,
941
+ "send-keys S-Right",
942
+ ]);
943
+ bindSafe([
944
+ "bind-key",
945
+ "-T",
946
+ "root",
947
+ "-n",
948
+ "S-Left",
949
+ "if-shell",
950
+ "-F",
951
+ cond,
952
+ bindPrev,
953
+ "send-keys S-Left",
954
+ ]);
955
+ bindSafe([
956
+ "bind-key",
957
+ "-T",
958
+ "root",
959
+ "-n",
960
+ "BTab",
961
+ "if-shell",
962
+ "-F",
963
+ cond,
964
+ bindPrev,
965
+ "send-keys BTab",
966
+ ]);
967
+ bindSafe([
968
+ "bind-key",
969
+ "-T",
970
+ "root",
971
+ "-n",
972
+ "Escape",
973
+ "if-shell",
974
+ "-F",
975
+ cond,
976
+ "send-keys C-c",
977
+ "send-keys Escape",
978
+ ]);
821
979
 
822
980
  if (taskListCommand) {
823
981
  bindSafe([
@@ -905,7 +1063,10 @@ function resolveCliAbsPath(name) {
905
1063
  if (_cliPathCache.has(name)) return _cliPathCache.get(name);
906
1064
  try {
907
1065
  const resolved = childProcess
908
- .execSync(`${GIT_BASH_BIN_EXEC} -c "which ${name}"`, { encoding: "utf8", timeout: 3000 })
1066
+ .execSync(`${GIT_BASH_BIN_EXEC} -c "which ${name}"`, {
1067
+ encoding: "utf8",
1068
+ timeout: 3000,
1069
+ })
909
1070
  .trim();
910
1071
  if (resolved) _cliPathCache.set(name, resolved);
911
1072
  return resolved || name;
@@ -922,9 +1083,10 @@ function wrapCliForBash(cmd) {
922
1083
  if (!cliMatch) return cmd;
923
1084
  // Node.js 측에서 절대 경로 resolve → psmux pane의 bash PATH 불일치 문제 해결 (exit 127)
924
1085
  const absPath = resolveCliAbsPath(cliMatch[1]);
925
- const resolved = absPath !== cliMatch[1]
926
- ? trimmed.replace(new RegExp(`^${cliMatch[1]}\\b`), absPath)
927
- : trimmed;
1086
+ const resolved =
1087
+ absPath !== cliMatch[1]
1088
+ ? trimmed.replace(new RegExp(`^${cliMatch[1]}\\b`), absPath)
1089
+ : trimmed;
928
1090
  // 단일 따옴표 이스케이프: ' → '\''
929
1091
  const escaped = resolved.replace(/'/g, "'\\''");
930
1092
  return `${GIT_BASH_BIN_PS} -c '${escaped}'`;
@@ -962,7 +1124,13 @@ export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
962
1124
  * @param {AbortSignal} [opts.signal] — 외부에서 폴링 중단 요청 시 사용
963
1125
  * @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null, aborted?: boolean }}
964
1126
  */
965
- export async function waitForPattern(sessionName, paneNameOrTarget, pattern, timeoutSec = 300, opts = {}) {
1127
+ export async function waitForPattern(
1128
+ sessionName,
1129
+ paneNameOrTarget,
1130
+ pattern,
1131
+ timeoutSec = 300,
1132
+ opts = {},
1133
+ ) {
966
1134
  ensurePsmuxInstalled();
967
1135
 
968
1136
  // E4 크래시 복구: 초기 resolvePane도 세션 사망을 감지
@@ -985,11 +1153,14 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
985
1153
 
986
1154
  const paneName = pane.title || paneNameOrTarget;
987
1155
  // opts.logPath: dispatch 시 확정된 캡처 로그 경로 직접 지정 (타이틀 변경 내성)
988
- const logPath = (opts.logPath && existsSync(opts.logPath))
989
- ? opts.logPath
990
- : getCaptureLogPath(sessionName, paneName);
1156
+ const logPath =
1157
+ opts.logPath && existsSync(opts.logPath)
1158
+ ? opts.logPath
1159
+ : getCaptureLogPath(sessionName, paneName);
991
1160
  if (!existsSync(logPath)) {
992
- throw new Error(`캡처 로그가 없습니다. 먼저 startCapture(${sessionName}, ${paneName})를 호출하세요.`);
1161
+ throw new Error(
1162
+ `캡처 로그가 없습니다. 먼저 startCapture(${sessionName}, ${paneName})를 호출하세요.`,
1163
+ );
993
1164
  }
994
1165
 
995
1166
  const startTime = Date.now();
@@ -997,18 +1168,39 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
997
1168
  const regex = toPatternRegExp(pattern);
998
1169
 
999
1170
  if (opts?.signal?.aborted) {
1000
- return { matched: false, paneId: pane.paneId, paneName, logPath, match: null, aborted: true };
1171
+ return {
1172
+ matched: false,
1173
+ paneId: pane.paneId,
1174
+ paneName,
1175
+ logPath,
1176
+ match: null,
1177
+ aborted: true,
1178
+ };
1001
1179
  }
1002
1180
 
1003
1181
  while (Date.now() <= deadline) {
1004
1182
  if (opts?.signal?.aborted) {
1005
- return { matched: false, paneId: pane.paneId, paneName, logPath, match: null, aborted: true };
1183
+ return {
1184
+ matched: false,
1185
+ paneId: pane.paneId,
1186
+ paneName,
1187
+ logPath,
1188
+ match: null,
1189
+ aborted: true,
1190
+ };
1006
1191
  }
1007
1192
  // E4 크래시 복구: capture 실패 시 세션 생존 체크
1008
1193
  try {
1009
1194
  if (opts.logPath) {
1010
1195
  // logPath 직접 지정 시 — 셸 타이틀 변경과 무관하게 올바른 파일에 기록
1011
- const snapshot = psmuxExec(["capture-pane", "-t", pane.paneId, "-p", "-S", "-"]);
1196
+ const snapshot = psmuxExec([
1197
+ "capture-pane",
1198
+ "-t",
1199
+ pane.paneId,
1200
+ "-p",
1201
+ "-S",
1202
+ "-",
1203
+ ]);
1012
1204
  writeFileSync(logPath, snapshot, "utf8");
1013
1205
  } else {
1014
1206
  refreshCaptureSnapshot(sessionName, pane.paneId);
@@ -1032,8 +1224,15 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
1032
1224
  // onPoll 콜백 — 각 폴링 주기마다 중간 상태 전달
1033
1225
  if (opts.onPoll) {
1034
1226
  try {
1035
- opts.onPoll({ content, paneId: pane.paneId, paneName, elapsed: Date.now() - startTime });
1036
- } catch { /* 콜백 예외는 삼킴 — 폴링 루프 보호 */ }
1227
+ opts.onPoll({
1228
+ content,
1229
+ paneId: pane.paneId,
1230
+ paneName,
1231
+ elapsed: Date.now() - startTime,
1232
+ });
1233
+ } catch {
1234
+ /* 콜백 예외는 삼킴 — 폴링 루프 보호 */
1235
+ }
1037
1236
  }
1038
1237
 
1039
1238
  const match = regex.exec(content);
@@ -1052,7 +1251,14 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
1052
1251
  }
1053
1252
  await sleepMsAsync(POLL_INTERVAL_MS);
1054
1253
  if (opts?.signal?.aborted) {
1055
- return { matched: false, paneId: pane.paneId, paneName, logPath, match: null, aborted: true };
1254
+ return {
1255
+ matched: false,
1256
+ paneId: pane.paneId,
1257
+ paneName,
1258
+ logPath,
1259
+ match: null,
1260
+ aborted: true,
1261
+ };
1056
1262
  }
1057
1263
  }
1058
1264
 
@@ -1074,19 +1280,38 @@ export async function waitForPattern(sessionName, paneNameOrTarget, pattern, tim
1074
1280
  * @param {object} [opts] — waitForPattern에 전달할 옵션 (onPoll 등)
1075
1281
  * @returns {{ matched: boolean, paneId: string, paneName: string, logPath: string, match: string|null, token: string, exitCode: number|null }}
1076
1282
  */
1077
- export async function waitForCompletion(sessionName, paneNameOrTarget, token, timeoutSec = 300, opts = {}) {
1283
+ export async function waitForCompletion(
1284
+ sessionName,
1285
+ paneNameOrTarget,
1286
+ token,
1287
+ timeoutSec = 300,
1288
+ opts = {},
1289
+ ) {
1078
1290
  const completionRegex = new RegExp(
1079
1291
  `${escapeRegExp(COMPLETION_PREFIX)}${escapeRegExp(token)}:(\\d+)`,
1080
1292
  "m",
1081
1293
  );
1082
- const result = await waitForPattern(sessionName, paneNameOrTarget, completionRegex, timeoutSec, opts);
1294
+ const result = await waitForPattern(
1295
+ sessionName,
1296
+ paneNameOrTarget,
1297
+ completionRegex,
1298
+ timeoutSec,
1299
+ opts,
1300
+ );
1083
1301
 
1084
1302
  // 타이밍 이슈 대응: matched=false인 경우 500ms 대기 후 최종 1회 캡처 재시도
1085
1303
  if (!result.matched && !result.sessionDead && result.logPath) {
1086
1304
  await new Promise((r) => setTimeout(r, 500));
1087
1305
  try {
1088
1306
  const pane = resolvePane(sessionName, paneNameOrTarget);
1089
- const snapshot = psmuxExec(["capture-pane", "-t", pane.paneId, "-p", "-S", "-"]);
1307
+ const snapshot = psmuxExec([
1308
+ "capture-pane",
1309
+ "-t",
1310
+ pane.paneId,
1311
+ "-p",
1312
+ "-S",
1313
+ "-",
1314
+ ]);
1090
1315
  writeFileSync(result.logPath, snapshot, "utf8");
1091
1316
  const content = readCaptureLog(result.logPath);
1092
1317
  const retryMatch = completionRegex.exec(content);
@@ -1125,7 +1350,7 @@ export function spawnWorker(sessionName, workerName, cmd) {
1125
1350
  if (!hasPsmux()) {
1126
1351
  throw new Error(
1127
1352
  "psmux가 설치되어 있지 않습니다.\n" +
1128
- `설치 방법:\n${formatPsmuxInstallGuidance(" ")}`
1353
+ `설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1129
1354
  );
1130
1355
  }
1131
1356
 
@@ -1155,7 +1380,9 @@ export function spawnWorker(sessionName, workerName, cmd) {
1155
1380
  psmuxExec(["select-pane", "-t", paneTarget, "-T", workerName]);
1156
1381
  return { paneId: paneTarget, workerName };
1157
1382
  } catch (err) {
1158
- throw new Error(`워커 생성 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
1383
+ throw new Error(
1384
+ `워커 생성 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1385
+ );
1159
1386
  }
1160
1387
  }
1161
1388
 
@@ -1167,7 +1394,9 @@ export function spawnWorker(sessionName, workerName, cmd) {
1167
1394
  */
1168
1395
  export function getWorkerStatus(sessionName, workerName) {
1169
1396
  if (!hasPsmux()) {
1170
- throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
1397
+ throw new Error(
1398
+ `psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1399
+ );
1171
1400
  }
1172
1401
  try {
1173
1402
  const pane = resolvePane(sessionName, workerName);
@@ -1180,7 +1409,9 @@ export function getWorkerStatus(sessionName, workerName) {
1180
1409
  if (err.message.includes("Pane을 찾을 수 없습니다")) {
1181
1410
  throw new Error(`워커를 찾을 수 없습니다: ${workerName}`);
1182
1411
  }
1183
- throw new Error(`워커 상태 조회 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
1412
+ throw new Error(
1413
+ `워커 상태 조회 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1414
+ );
1184
1415
  }
1185
1416
  }
1186
1417
 
@@ -1192,14 +1423,15 @@ export function getWorkerStatus(sessionName, workerName) {
1192
1423
  */
1193
1424
  export function killWorker(sessionName, workerName) {
1194
1425
  if (!hasPsmux()) {
1195
- throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
1426
+ throw new Error(
1427
+ `psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1428
+ );
1196
1429
  }
1197
1430
  try {
1198
1431
  const { paneId, status } = getWorkerStatus(sessionName, workerName);
1199
1432
  const attachedCount = getPsmuxSessionAttachedCount(sessionName);
1200
- const fallbackPane = attachedCount > 0
1201
- ? findFallbackPane(sessionName, paneId)
1202
- : null;
1433
+ const fallbackPane =
1434
+ attachedCount > 0 ? findFallbackPane(sessionName, paneId) : null;
1203
1435
 
1204
1436
  if (fallbackPane?.paneId) {
1205
1437
  try {
@@ -1214,7 +1446,13 @@ export function killWorker(sessionName, workerName) {
1214
1446
 
1215
1447
  // pane PID 수집 → 프로세스 트리 정리 (MCP 서버 좀비 방지)
1216
1448
  try {
1217
- const pidOutput = psmuxExec(["list-panes", "-t", paneId, "-F", "#{pane_pid}"]);
1449
+ const pidOutput = psmuxExec([
1450
+ "list-panes",
1451
+ "-t",
1452
+ paneId,
1453
+ "-F",
1454
+ "#{pane_pid}",
1455
+ ]);
1218
1456
  const pid = Number.parseInt(pidOutput.trim(), 10);
1219
1457
  if (Number.isFinite(pid) && pid > 0) killProcessTree(pid);
1220
1458
  } catch {
@@ -1266,7 +1504,9 @@ export function killWorker(sessionName, workerName) {
1266
1504
  if (err.message.includes("워커를 찾을 수 없습니다")) {
1267
1505
  return { killed: true };
1268
1506
  }
1269
- throw new Error(`워커 종료 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
1507
+ throw new Error(
1508
+ `워커 종료 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1509
+ );
1270
1510
  }
1271
1511
  }
1272
1512
 
@@ -1279,14 +1519,18 @@ export function killWorker(sessionName, workerName) {
1279
1519
  */
1280
1520
  export function captureWorkerOutput(sessionName, workerName, lines = 50) {
1281
1521
  if (!hasPsmux()) {
1282
- throw new Error(`psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`);
1522
+ throw new Error(
1523
+ `psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1524
+ );
1283
1525
  }
1284
1526
  try {
1285
1527
  const { paneId } = getWorkerStatus(sessionName, workerName);
1286
1528
  return psmuxExec(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
1287
1529
  } catch (err) {
1288
1530
  if (err.message.includes("워커를 찾을 수 없습니다")) throw err;
1289
- throw new Error(`출력 캡처 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`);
1531
+ throw new Error(
1532
+ `출력 캡처 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1533
+ );
1290
1534
  }
1291
1535
  }
1292
1536
 
@@ -1294,123 +1538,169 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
1294
1538
 
1295
1539
  if (process.argv[1]?.endsWith("psmux.mjs")) {
1296
1540
  (async () => {
1297
- const [, , cmd, ...args] = process.argv;
1541
+ const [, , cmd, ...args] = process.argv;
1298
1542
 
1299
- // CLI 인자 파싱 헬퍼
1300
- function getArg(name) {
1301
- const idx = args.indexOf(`--${name}`);
1302
- return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
1303
- }
1543
+ // CLI 인자 파싱 헬퍼
1544
+ function getArg(name) {
1545
+ const idx = args.indexOf(`--${name}`);
1546
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
1547
+ }
1304
1548
 
1305
- try {
1306
- switch (cmd) {
1307
- case "spawn": {
1308
- const session = getArg("session");
1309
- const name = getArg("name");
1310
- const workerCmd = getArg("cmd");
1311
- if (!session || !name || !workerCmd) {
1312
- console.error("사용법: node psmux.mjs spawn --session <세션> --name <워커명> --cmd <커맨드>");
1313
- process.exit(1);
1549
+ try {
1550
+ switch (cmd) {
1551
+ case "spawn": {
1552
+ const session = getArg("session");
1553
+ const name = getArg("name");
1554
+ const workerCmd = getArg("cmd");
1555
+ if (!session || !name || !workerCmd) {
1556
+ console.error(
1557
+ "사용법: node psmux.mjs spawn --session <세션> --name <워커명> --cmd <커맨드>",
1558
+ );
1559
+ process.exit(1);
1560
+ }
1561
+ console.log(
1562
+ JSON.stringify(spawnWorker(session, name, workerCmd), null, 2),
1563
+ );
1564
+ break;
1314
1565
  }
1315
- console.log(JSON.stringify(spawnWorker(session, name, workerCmd), null, 2));
1316
- break;
1317
- }
1318
- case "status": {
1319
- const session = getArg("session");
1320
- const name = getArg("name");
1321
- if (!session || !name) {
1322
- console.error("사용법: node psmux.mjs status --session <세션> --name <워커명>");
1323
- process.exit(1);
1566
+ case "status": {
1567
+ const session = getArg("session");
1568
+ const name = getArg("name");
1569
+ if (!session || !name) {
1570
+ console.error(
1571
+ "사용법: node psmux.mjs status --session <세션> --name <워커명>",
1572
+ );
1573
+ process.exit(1);
1574
+ }
1575
+ console.log(JSON.stringify(getWorkerStatus(session, name), null, 2));
1576
+ break;
1324
1577
  }
1325
- console.log(JSON.stringify(getWorkerStatus(session, name), null, 2));
1326
- break;
1327
- }
1328
- case "kill": {
1329
- const session = getArg("session");
1330
- const name = getArg("name");
1331
- if (!session || !name) {
1332
- console.error("사용법: node psmux.mjs kill --session <세션> --name <워커명>");
1333
- process.exit(1);
1578
+ case "kill": {
1579
+ const session = getArg("session");
1580
+ const name = getArg("name");
1581
+ if (!session || !name) {
1582
+ console.error(
1583
+ "사용법: node psmux.mjs kill --session <세션> --name <워커명>",
1584
+ );
1585
+ process.exit(1);
1586
+ }
1587
+ console.log(JSON.stringify(killWorker(session, name), null, 2));
1588
+ break;
1334
1589
  }
1335
- console.log(JSON.stringify(killWorker(session, name), null, 2));
1336
- break;
1337
- }
1338
- case "output": {
1339
- const session = getArg("session");
1340
- const name = getArg("name");
1341
- const lines = parseInt(getArg("lines") || "50", 10);
1342
- if (!session || !name) {
1343
- console.error("사용법: node psmux.mjs output --session <세션> --name <워커명> [--lines <줄수>]");
1344
- process.exit(1);
1590
+ case "output": {
1591
+ const session = getArg("session");
1592
+ const name = getArg("name");
1593
+ const lines = parseInt(getArg("lines") || "50", 10);
1594
+ if (!session || !name) {
1595
+ console.error(
1596
+ "사용법: node psmux.mjs output --session <세션> --name <워커명> [--lines <줄수>]",
1597
+ );
1598
+ process.exit(1);
1599
+ }
1600
+ console.log(captureWorkerOutput(session, name, lines));
1601
+ break;
1345
1602
  }
1346
- console.log(captureWorkerOutput(session, name, lines));
1347
- break;
1348
- }
1349
- case "capture-start": {
1350
- const session = getArg("session");
1351
- const name = getArg("name");
1352
- if (!session || !name) {
1353
- console.error("사용법: node psmux.mjs capture-start --session <세션> --name <pane>");
1354
- process.exit(1);
1603
+ case "capture-start": {
1604
+ const session = getArg("session");
1605
+ const name = getArg("name");
1606
+ if (!session || !name) {
1607
+ console.error(
1608
+ "사용법: node psmux.mjs capture-start --session <세션> --name <pane>",
1609
+ );
1610
+ process.exit(1);
1611
+ }
1612
+ console.log(JSON.stringify(startCapture(session, name), null, 2));
1613
+ break;
1355
1614
  }
1356
- console.log(JSON.stringify(startCapture(session, name), null, 2));
1357
- break;
1358
- }
1359
- case "dispatch": {
1360
- const session = getArg("session");
1361
- const name = getArg("name");
1362
- const commandText = getArg("command");
1363
- if (!session || !name || !commandText) {
1364
- console.error("사용법: node psmux.mjs dispatch --session <세션> --name <pane> --command <PowerShell 명령>");
1365
- process.exit(1);
1615
+ case "dispatch": {
1616
+ const session = getArg("session");
1617
+ const name = getArg("name");
1618
+ const commandText = getArg("command");
1619
+ if (!session || !name || !commandText) {
1620
+ console.error(
1621
+ "사용법: node psmux.mjs dispatch --session <세션> --name <pane> --command <PowerShell 명령>",
1622
+ );
1623
+ process.exit(1);
1624
+ }
1625
+ console.log(
1626
+ JSON.stringify(
1627
+ dispatchCommand(session, name, commandText),
1628
+ null,
1629
+ 2,
1630
+ ),
1631
+ );
1632
+ break;
1366
1633
  }
1367
- console.log(JSON.stringify(dispatchCommand(session, name, commandText), null, 2));
1368
- break;
1369
- }
1370
- case "wait-pattern": {
1371
- const session = getArg("session");
1372
- const name = getArg("name");
1373
- const pattern = getArg("pattern");
1374
- const timeoutSec = parseInt(getArg("timeout") || "300", 10);
1375
- if (!session || !name || !pattern) {
1376
- console.error("사용법: node psmux.mjs wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]");
1377
- process.exit(1);
1634
+ case "wait-pattern": {
1635
+ const session = getArg("session");
1636
+ const name = getArg("name");
1637
+ const pattern = getArg("pattern");
1638
+ const timeoutSec = parseInt(getArg("timeout") || "300", 10);
1639
+ if (!session || !name || !pattern) {
1640
+ console.error(
1641
+ "사용법: node psmux.mjs wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]",
1642
+ );
1643
+ process.exit(1);
1644
+ }
1645
+ const result = await waitForPattern(
1646
+ session,
1647
+ name,
1648
+ pattern,
1649
+ timeoutSec,
1650
+ );
1651
+ console.log(JSON.stringify(result, null, 2));
1652
+ if (!result.matched) process.exit(2);
1653
+ break;
1378
1654
  }
1379
- const result = await waitForPattern(session, name, pattern, timeoutSec);
1380
- console.log(JSON.stringify(result, null, 2));
1381
- if (!result.matched) process.exit(2);
1382
- break;
1383
- }
1384
- case "wait-completion": {
1385
- const session = getArg("session");
1386
- const name = getArg("name");
1387
- const token = getArg("token");
1388
- const timeoutSec = parseInt(getArg("timeout") || "300", 10);
1389
- if (!session || !name || !token) {
1390
- console.error("사용법: node psmux.mjs wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]");
1391
- process.exit(1);
1655
+ case "wait-completion": {
1656
+ const session = getArg("session");
1657
+ const name = getArg("name");
1658
+ const token = getArg("token");
1659
+ const timeoutSec = parseInt(getArg("timeout") || "300", 10);
1660
+ if (!session || !name || !token) {
1661
+ console.error(
1662
+ "사용법: node psmux.mjs wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]",
1663
+ );
1664
+ process.exit(1);
1665
+ }
1666
+ const result = await waitForCompletion(
1667
+ session,
1668
+ name,
1669
+ token,
1670
+ timeoutSec,
1671
+ );
1672
+ console.log(JSON.stringify(result, null, 2));
1673
+ if (!result.matched) process.exit(2);
1674
+ break;
1392
1675
  }
1393
- const result = await waitForCompletion(session, name, token, timeoutSec);
1394
- console.log(JSON.stringify(result, null, 2));
1395
- if (!result.matched) process.exit(2);
1396
- break;
1676
+ default:
1677
+ console.error(
1678
+ "사용법: node psmux.mjs spawn|status|kill|output|capture-start|dispatch|wait-pattern|wait-completion [args]",
1679
+ );
1680
+ console.error("");
1681
+ console.error(
1682
+ " spawn --session <세션> --name <워커명> --cmd <커맨드>",
1683
+ );
1684
+ console.error(" status --session <세션> --name <워커명>");
1685
+ console.error(" kill --session <세션> --name <워커명>");
1686
+ console.error(
1687
+ " output --session <세션> --name <워커명> [--lines <줄수>]",
1688
+ );
1689
+ console.error(" capture-start --session <세션> --name <pane>");
1690
+ console.error(
1691
+ " dispatch --session <세션> --name <pane> --command <PowerShell 명령>",
1692
+ );
1693
+ console.error(
1694
+ " wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]",
1695
+ );
1696
+ console.error(
1697
+ " wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]",
1698
+ );
1699
+ process.exit(1);
1397
1700
  }
1398
- default:
1399
- console.error("사용법: node psmux.mjs spawn|status|kill|output|capture-start|dispatch|wait-pattern|wait-completion [args]");
1400
- console.error("");
1401
- console.error(" spawn --session <세션> --name <워커명> --cmd <커맨드>");
1402
- console.error(" status --session <세션> --name <워커명>");
1403
- console.error(" kill --session <세션> --name <워커명>");
1404
- console.error(" output --session <세션> --name <워커명> [--lines <줄수>]");
1405
- console.error(" capture-start --session <세션> --name <pane>");
1406
- console.error(" dispatch --session <세션> --name <pane> --command <PowerShell 명령>");
1407
- console.error(" wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]");
1408
- console.error(" wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]");
1409
- process.exit(1);
1701
+ } catch (err) {
1702
+ console.error(`오류: ${err.message}`);
1703
+ process.exit(1);
1410
1704
  }
1411
- } catch (err) {
1412
- console.error(`오류: ${err.message}`);
1413
- process.exit(1);
1414
- }
1415
1705
  })();
1416
1706
  }