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,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  // remote-spawn.mjs — 로컬/원격 Claude 세션 실행 유틸리티
3
4
  //
4
5
  // Usage:
@@ -9,11 +10,25 @@
9
10
  // node remote-spawn.mjs --attach <session>
10
11
  // node remote-spawn.mjs --probe <ssh-host>
11
12
 
13
+ import { execFileSync, execSync } from "child_process";
14
+ import { spawn } from "../hub/lib/spawn-trace.mjs";
12
15
  import { randomUUID } from "crypto";
13
- import { execFileSync, execSync, spawn } from "child_process";
14
- import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
15
- import { homedir, platform as getPlatform, tmpdir } from "os";
16
- import { basename, join, posix as posixPath, resolve, win32 as win32Path } from "path";
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readFileSync,
20
+ statSync,
21
+ unlinkSync,
22
+ writeFileSync,
23
+ } from "fs";
24
+ import { platform as getPlatform, homedir, tmpdir } from "os";
25
+ import {
26
+ basename,
27
+ join,
28
+ posix as posixPath,
29
+ resolve,
30
+ win32 as win32Path,
31
+ } from "path";
17
32
  import { fileURLToPath } from "url";
18
33
  import {
19
34
  attachPsmuxSession,
@@ -81,7 +96,9 @@ function sleepMs(ms) {
81
96
  }
82
97
 
83
98
  function sleepMsAsync(ms) {
84
- return new Promise((resolveSleep) => setTimeout(resolveSleep, Math.max(0, ms)));
99
+ return new Promise((resolveSleep) =>
100
+ setTimeout(resolveSleep, Math.max(0, ms)),
101
+ );
85
102
  }
86
103
 
87
104
  function parsePositiveInt(value, fallback) {
@@ -113,15 +130,31 @@ export function buildRemoteClaudeCommand(env, permissionFlags = "") {
113
130
  return `${shellQuote(env.claudePath)}${permissionFlags ? ` ${permissionFlags}` : ""}; ${buildPosixExitTail()}`;
114
131
  }
115
132
 
116
- export function resolveCleanupWatcherTimingOptions(source = {}, env = process.env) {
133
+ export function resolveCleanupWatcherTimingOptions(
134
+ source = {},
135
+ env = process.env,
136
+ ) {
117
137
  return Object.freeze({
118
- graceMs: parsePositiveInt(source.graceMs ?? env.TFX_SPAWN_CLEANUP_GRACE_MS, DEFAULT_CLEANUP_WATCH_GRACE_MS),
119
- maxMs: parsePositiveInt(source.maxMs ?? env.TFX_SPAWN_CLEANUP_MAX_MS, DEFAULT_CLEANUP_WATCH_MAX_MS),
120
- pollMs: parsePositiveInt(source.pollMs ?? env.TFX_SPAWN_CLEANUP_POLL_MS, DEFAULT_CLEANUP_WATCH_POLL_MS),
138
+ graceMs: parsePositiveInt(
139
+ source.graceMs ?? env.TFX_SPAWN_CLEANUP_GRACE_MS,
140
+ DEFAULT_CLEANUP_WATCH_GRACE_MS,
141
+ ),
142
+ maxMs: parsePositiveInt(
143
+ source.maxMs ?? env.TFX_SPAWN_CLEANUP_MAX_MS,
144
+ DEFAULT_CLEANUP_WATCH_MAX_MS,
145
+ ),
146
+ pollMs: parsePositiveInt(
147
+ source.pollMs ?? env.TFX_SPAWN_CLEANUP_POLL_MS,
148
+ DEFAULT_CLEANUP_WATCH_POLL_MS,
149
+ ),
121
150
  });
122
151
  }
123
152
 
124
- export function buildSpawnCleanupWatcherArgs(sessionName, paneId, timingOptions = {}) {
153
+ export function buildSpawnCleanupWatcherArgs(
154
+ sessionName,
155
+ paneId,
156
+ timingOptions = {},
157
+ ) {
125
158
  const timings = resolveCleanupWatcherTimingOptions(timingOptions);
126
159
  return [
127
160
  SELF_SCRIPT_PATH,
@@ -159,11 +192,15 @@ function buildSessionSlug(customName) {
159
192
  }).trim();
160
193
  if (branch) {
161
194
  // feature/swarm-hypervisor → swarm-hypervisor
162
- const stripped = branch.includes("/") ? branch.split("/").slice(1).join("-") : branch;
195
+ const stripped = branch.includes("/")
196
+ ? branch.split("/").slice(1).join("-")
197
+ : branch;
163
198
  const slug = toSlug(stripped);
164
199
  if (slug) return slug;
165
200
  }
166
- } catch { /* git not available */ }
201
+ } catch {
202
+ /* git not available */
203
+ }
167
204
  return randomUUID().slice(0, 8);
168
205
  }
169
206
 
@@ -176,7 +213,9 @@ function deduplicateSessionName(baseName) {
176
213
  const candidate = `${baseName}-${i}`;
177
214
  if (!existing.includes(candidate)) return candidate;
178
215
  }
179
- } catch { /* psmux not available */ }
216
+ } catch {
217
+ /* psmux not available */
218
+ }
180
219
  return `${baseName}-${randomUUID().slice(0, 4)}`;
181
220
  }
182
221
 
@@ -323,7 +362,8 @@ function parseArgs(argv) {
323
362
  promptParts.push(arg);
324
363
  }
325
364
 
326
- const mergedPrompt = prompt ?? (promptParts.length > 0 ? promptParts.join(" ") : null);
365
+ const mergedPrompt =
366
+ prompt ?? (promptParts.length > 0 ? promptParts.join(" ") : null);
327
367
  return {
328
368
  command,
329
369
  dir,
@@ -345,7 +385,11 @@ function parseArgs(argv) {
345
385
  function parseVersion(versionStr) {
346
386
  const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
347
387
  if (!match) return null;
348
- return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
388
+ return [
389
+ parseInt(match[1], 10),
390
+ parseInt(match[2], 10),
391
+ parseInt(match[3], 10),
392
+ ];
349
393
  }
350
394
 
351
395
  function compareVersions(a, b) {
@@ -360,12 +404,16 @@ function probeVersion(binPath) {
360
404
  if (/\.(cmd|bat)$/iu.test(binPath)) {
361
405
  // .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
362
406
  const out = execSync(`"${binPath}" --version`, {
363
- encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
407
+ encoding: "utf8",
408
+ timeout: 3000,
409
+ stdio: ["ignore", "pipe", "ignore"],
364
410
  });
365
411
  return parseVersion(out);
366
412
  }
367
413
  const out = execFileSync(binPath, ["--version"], {
368
- encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
414
+ encoding: "utf8",
415
+ timeout: 3000,
416
+ stdio: ["ignore", "pipe", "ignore"],
369
417
  });
370
418
  return parseVersion(out);
371
419
  } catch {
@@ -378,7 +426,15 @@ function detectClaudePath() {
378
426
 
379
427
  const candidates = [];
380
428
 
381
- const wingetPath = join(homedir(), "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe");
429
+ const wingetPath = join(
430
+ homedir(),
431
+ "AppData",
432
+ "Local",
433
+ "Microsoft",
434
+ "WinGet",
435
+ "Links",
436
+ "claude.exe",
437
+ );
382
438
  if (existsSync(wingetPath)) candidates.push(wingetPath);
383
439
 
384
440
  const npmPath = join(process.env.APPDATA || "", "npm", "claude.cmd");
@@ -386,7 +442,10 @@ function detectClaudePath() {
386
442
 
387
443
  try {
388
444
  const command = IS_WINDOWS_LOCAL ? "where" : "which";
389
- const result = execFileSync(command, ["claude"], { encoding: "utf8", timeout: 5000 }).trim();
445
+ const result = execFileSync(command, ["claude"], {
446
+ encoding: "utf8",
447
+ timeout: 5000,
448
+ }).trim();
390
449
  if (result) {
391
450
  for (const line of result.split(/\r?\n/u)) {
392
451
  const p = line.trim();
@@ -415,7 +474,9 @@ function detectClaudePath() {
415
474
  }
416
475
 
417
476
  function getPermissionFlag() {
418
- return process.env.TFX_CLAUDE_SAFE_MODE === "1" ? [] : ["--dangerously-skip-permissions"];
477
+ return process.env.TFX_CLAUDE_SAFE_MODE === "1"
478
+ ? []
479
+ : ["--dangerously-skip-permissions"];
419
480
  }
420
481
 
421
482
  function failFast(message) {
@@ -463,7 +524,10 @@ function buildPromptContext(args, options = {}) {
463
524
  let content = "";
464
525
 
465
526
  if (args.handoff) {
466
- const handoffCandidate = validateTransferCandidate(args.handoff, "handoff file");
527
+ const handoffCandidate = validateTransferCandidate(
528
+ args.handoff,
529
+ "handoff file",
530
+ );
467
531
  content = readFileSync(handoffCandidate.localPath, "utf8").trim();
468
532
  candidates.push(handoffCandidate);
469
533
  }
@@ -558,7 +622,11 @@ function spawnLocalFallback(args, claudePath, prompt) {
558
622
  }
559
623
 
560
624
  try {
561
- spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
625
+ spawn("wt.exe", wtArgs, {
626
+ detached: true,
627
+ stdio: "ignore",
628
+ windowsHide: false,
629
+ }).unref();
562
630
  console.log(`spawned local Claude in WT tab → ${dir}`);
563
631
  } catch (error) {
564
632
  console.error("wt.exe spawn failed:", error.message);
@@ -579,13 +647,21 @@ function spawnRemoteFallback(args, promptContext) {
579
647
 
580
648
  let remoteHome;
581
649
  try {
582
- remoteHome = execFileSync("ssh", [host, "echo", "$env:USERPROFILE"], { encoding: "utf8", timeout: 5000 }).trim();
650
+ remoteHome = execFileSync("ssh", [host, "echo", "$env:USERPROFILE"], {
651
+ encoding: "utf8",
652
+ timeout: 5000,
653
+ }).trim();
583
654
  } catch {
584
655
  try {
585
- remoteHome = execFileSync("ssh", [host, "echo", "$HOME"], { encoding: "utf8", timeout: 5000 }).trim();
656
+ remoteHome = execFileSync("ssh", [host, "echo", "$HOME"], {
657
+ encoding: "utf8",
658
+ timeout: 5000,
659
+ }).trim();
586
660
  } catch {
587
661
  // 원격 홈 감지 실패 시 transfer 기능을 사용할 수 없음
588
- console.warn(`[tfx] 원격 홈 디렉토리 감지 실패 (${host}) — file transfer 비활성화`);
662
+ console.warn(
663
+ `[tfx] 원격 홈 디렉토리 감지 실패 (${host}) — file transfer 비활성화`,
664
+ );
589
665
  remoteHome = null;
590
666
  }
591
667
  }
@@ -594,24 +670,33 @@ function spawnRemoteFallback(args, promptContext) {
594
670
  try {
595
671
  const fallbackEnv = { home: remoteHome, os: "win32", shell: "pwsh" };
596
672
  const stageId = `spawn-${randomUUID().slice(0, 8)}`;
597
- const { stagedFiles } = stageRemotePromptFiles(host, fallbackEnv, promptContext.transferCandidates, stageId);
673
+ const { stagedFiles } = stageRemotePromptFiles(
674
+ host,
675
+ fallbackEnv,
676
+ promptContext.transferCandidates,
677
+ stageId,
678
+ );
598
679
  prompt = rewritePromptPaths(prompt, stagedFiles);
599
680
  } catch (error) {
600
- failFast(`failed to stage remote files: ${error?.message || String(error)}`);
681
+ failFast(
682
+ `failed to stage remote files: ${error?.message || String(error)}`,
683
+ );
601
684
  }
602
685
  } else if (promptContext.transferCandidates.length > 0) {
603
686
  console.warn("[tfx] 원격 홈 미감지 — --transfer 파일이 무시됩니다");
604
687
  }
605
688
 
606
- const scriptLines = [
607
- `cd '${dir.replace(/'/g, "''")}'`,
608
- ];
689
+ const scriptLines = [`cd '${dir.replace(/'/g, "''")}'`];
609
690
 
610
691
  if (prompt) {
611
692
  const safePrompt = prompt.replace(/'/g, "''");
612
- scriptLines.push(`& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")} '${safePrompt}'`);
693
+ scriptLines.push(
694
+ `& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")} '${safePrompt}'`,
695
+ );
613
696
  } else {
614
- scriptLines.push(`& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")}`);
697
+ scriptLines.push(
698
+ `& "$env:USERPROFILE\\.local\\bin\\claude.exe" ${permFlags.join(" ")}`,
699
+ );
615
700
  }
616
701
 
617
702
  const scriptContent = scriptLines.join("\n");
@@ -619,7 +704,10 @@ function spawnRemoteFallback(args, promptContext) {
619
704
  writeFileSync(localScript, scriptContent, "utf8");
620
705
 
621
706
  try {
622
- execFileSync("scp", [localScript, `${host}:tfx-remote-spawn.ps1`], { timeout: 10000, stdio: "pipe" });
707
+ execFileSync("scp", [localScript, `${host}:tfx-remote-spawn.ps1`], {
708
+ timeout: 10000,
709
+ stdio: "pipe",
710
+ });
623
711
  } catch (error) {
624
712
  console.error("failed to copy script to remote:", error.message);
625
713
  process.exit(1);
@@ -641,14 +729,20 @@ function spawnRemoteFallback(args, promptContext) {
641
729
  remoteCmd,
642
730
  ];
643
731
  try {
644
- spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
732
+ spawn("wt.exe", wtArgs, {
733
+ detached: true,
734
+ stdio: "ignore",
735
+ windowsHide: false,
736
+ }).unref();
645
737
  console.log(`spawned remote Claude → ${host}:${dir}`);
646
738
  } catch (error) {
647
739
  console.error("wt.exe spawn failed:", error.message);
648
740
  process.exit(1);
649
741
  }
650
742
  } else {
651
- const child = spawn("ssh", ["-t", "--", host, remoteCmd], { stdio: "inherit" });
743
+ const child = spawn("ssh", ["-t", "--", host, remoteCmd], {
744
+ stdio: "inherit",
745
+ });
652
746
  child.on("exit", (code) => process.exit(code || 0));
653
747
  }
654
748
  }
@@ -689,7 +783,8 @@ function normalizePwshProbeEnv(host, parsed) {
689
783
  }
690
784
 
691
785
  return Object.freeze({
692
- claudePath: (!parsed.claude || parsed.claude === "notfound") ? null : parsed.claude,
786
+ claudePath:
787
+ !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
693
788
  home: parsed.home,
694
789
  os: "win32",
695
790
  shell: "pwsh",
@@ -697,13 +792,15 @@ function normalizePwshProbeEnv(host, parsed) {
697
792
  }
698
793
 
699
794
  function normalizePosixProbeEnv(host, parsed) {
700
- const os = parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
795
+ const os =
796
+ parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
701
797
  if (!os || !parsed.home) {
702
798
  return null;
703
799
  }
704
800
 
705
801
  return Object.freeze({
706
- claudePath: (!parsed.claude || parsed.claude === "notfound") ? null : parsed.claude,
802
+ claudePath:
803
+ !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
707
804
  home: parsed.home,
708
805
  os,
709
806
  shell: parsed.shell === "zsh" ? "zsh" : "bash",
@@ -730,10 +827,10 @@ function readRemoteEnvCache(host) {
730
827
 
731
828
  function isRemoteEnvCacheFresh(cacheEntry) {
732
829
  return Boolean(
733
- cacheEntry
734
- && typeof cacheEntry.cachedAt === "number"
735
- && cacheEntry.env
736
- && (Date.now() - cacheEntry.cachedAt) < REMOTE_ENV_TTL_MS,
830
+ cacheEntry &&
831
+ typeof cacheEntry.cachedAt === "number" &&
832
+ cacheEntry.env &&
833
+ Date.now() - cacheEntry.cachedAt < REMOTE_ENV_TTL_MS,
737
834
  );
738
835
  }
739
836
 
@@ -751,7 +848,7 @@ function probeRemoteEnvViaPwsh(host) {
751
848
  "Write-Output 'shell=pwsh'",
752
849
  'Write-Output "home=$env:USERPROFILE"',
753
850
  'if (Test-Path "$env:USERPROFILE\\.local\\bin\\claude.exe") { Write-Output "claude=$env:USERPROFILE\\.local\\bin\\claude.exe" } elseif (Get-Command claude -ErrorAction SilentlyContinue) { Write-Output "claude=$((Get-Command claude).Source)" } else { Write-Output \'claude=notfound\' }',
754
- 'Write-Output "os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? \'win32\' : \'other\')"',
851
+ "Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
755
852
  ].join("; ");
756
853
 
757
854
  let output;
@@ -825,13 +922,15 @@ function resolveRemoteDir(dir, env) {
825
922
  if (env.os === "win32") {
826
923
  const winDir = requestedDir.replace(/\//g, "\\");
827
924
  if (winDir === "~") return env.home;
828
- if (/^~[\\/]/u.test(winDir)) return win32Path.join(env.home, winDir.slice(2));
925
+ if (/^~[\\/]/u.test(winDir))
926
+ return win32Path.join(env.home, winDir.slice(2));
829
927
  if (isWindowsAbsolutePath(winDir)) return winDir;
830
928
  return win32Path.join(env.home, winDir);
831
929
  }
832
930
 
833
931
  if (requestedDir === "~") return env.home;
834
- if (requestedDir.startsWith("~/")) return posixPath.join(env.home, requestedDir.slice(2));
932
+ if (requestedDir.startsWith("~/"))
933
+ return posixPath.join(env.home, requestedDir.slice(2));
835
934
  if (requestedDir.startsWith("/")) return requestedDir;
836
935
  return posixPath.join(env.home, requestedDir);
837
936
  }
@@ -845,20 +944,30 @@ function ensureRemoteStageDir(host, env, remoteStageDir) {
845
944
  if (env.os === "win32") {
846
945
  const safePath = escapePwshSingleQuoted(remoteStageDir);
847
946
  const command = `New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`;
848
- execFileSync("ssh", [host, "pwsh", "-NoProfile", "-Command", command], { timeout: 10000, stdio: "pipe" });
947
+ execFileSync("ssh", [host, "pwsh", "-NoProfile", "-Command", command], {
948
+ timeout: 10000,
949
+ stdio: "pipe",
950
+ });
849
951
  return;
850
952
  }
851
953
 
852
- execFileSync("ssh", [host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`], {
853
- timeout: 10000,
854
- stdio: "pipe",
855
- });
954
+ execFileSync(
955
+ "ssh",
956
+ [host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`],
957
+ {
958
+ timeout: 10000,
959
+ stdio: "pipe",
960
+ },
961
+ );
856
962
  }
857
963
 
858
964
  function uploadFileToRemote(host, localPath, remotePath) {
859
965
  // scp는 remote path를 셸 확장 없이 직접 전달 — shellQuote 불필요
860
966
  // Windows 원격에서 쿼트가 리터럴 문자로 해석되어 경로 오류 발생
861
- execFileSync("scp", [localPath, `${host}:${remotePath}`], { timeout: 15000, stdio: "pipe" });
967
+ execFileSync("scp", [localPath, `${host}:${remotePath}`], {
968
+ timeout: 15000,
969
+ stdio: "pipe",
970
+ });
862
971
  }
863
972
 
864
973
  function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
@@ -896,14 +1005,17 @@ function listSessionNamesFromRawOutput(output) {
896
1005
  }
897
1006
 
898
1007
  function listSpawnSessions() {
899
- const helperSessions = listPsmuxSessions().filter((name) => name.startsWith("tfx-spawn-"));
1008
+ const helperSessions = listPsmuxSessions().filter((name) =>
1009
+ name.startsWith("tfx-spawn-"),
1010
+ );
900
1011
  if (helperSessions.length > 0) {
901
1012
  return helperSessions;
902
1013
  }
903
1014
 
904
1015
  try {
905
- return listSessionNamesFromRawOutput(psmuxExec(["list-sessions"]))
906
- .filter((name) => name.startsWith("tfx-spawn-"));
1016
+ return listSessionNamesFromRawOutput(psmuxExec(["list-sessions"])).filter(
1017
+ (name) => name.startsWith("tfx-spawn-"),
1018
+ );
907
1019
  } catch {
908
1020
  return [];
909
1021
  }
@@ -912,9 +1024,23 @@ function listSpawnSessions() {
912
1024
  function openAttachTab(sessionName, title = null) {
913
1025
  if (IS_WINDOWS_LOCAL) {
914
1026
  const wtArgs = title
915
- ? ["new-tab", "--title", title, "--suppressApplicationTitle", "--", "psmux", "attach", "-t", sessionName]
1027
+ ? [
1028
+ "new-tab",
1029
+ "--title",
1030
+ title,
1031
+ "--suppressApplicationTitle",
1032
+ "--",
1033
+ "psmux",
1034
+ "attach",
1035
+ "-t",
1036
+ sessionName,
1037
+ ]
916
1038
  : ["new-tab", "--", "psmux", "attach", "-t", sessionName];
917
- spawn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false }).unref();
1039
+ spawn("wt.exe", wtArgs, {
1040
+ detached: true,
1041
+ stdio: "ignore",
1042
+ windowsHide: false,
1043
+ }).unref();
918
1044
  return;
919
1045
  }
920
1046
 
@@ -951,14 +1077,25 @@ async function waitForRemotePrompt(sessionName, paneId) {
951
1077
  }
952
1078
  }
953
1079
 
954
- throw new Error(`ssh prompt wait timed out for ${sessionName}: ${capturePsmuxPane(paneId, 20)}`);
1080
+ throw new Error(
1081
+ `ssh prompt wait timed out for ${sessionName}: ${capturePsmuxPane(paneId, 20)}`,
1082
+ );
955
1083
  }
956
1084
 
957
1085
  /** @returns {boolean|null} true=dead, false=alive, null=probe 실패 */
958
1086
  function isPrimaryPaneDead(paneId) {
959
1087
  try {
960
- const output = psmuxExec(["list-panes", "-t", paneId, "-F", "#{pane_dead}"]);
961
- const lines = output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
1088
+ const output = psmuxExec([
1089
+ "list-panes",
1090
+ "-t",
1091
+ paneId,
1092
+ "-F",
1093
+ "#{pane_dead}",
1094
+ ]);
1095
+ const lines = output
1096
+ .split(/\r?\n/u)
1097
+ .map((line) => line.trim())
1098
+ .filter(Boolean);
962
1099
  if (lines.some((line) => line === "1")) return true;
963
1100
  return false;
964
1101
  } catch {
@@ -968,20 +1105,36 @@ function isPrimaryPaneDead(paneId) {
968
1105
 
969
1106
  async function watchSpawnSessionExit(sessionName, options = {}) {
970
1107
  const paneId = options.paneId || `${sessionName}:0.0`;
971
- const pollMs = parsePositiveInt(options.pollMs, DEFAULT_CLEANUP_WATCH_POLL_MS);
972
- const graceMs = parsePositiveInt(options.graceMs, DEFAULT_CLEANUP_WATCH_GRACE_MS);
973
- const maxWaitMs = parsePositiveInt(options.maxWaitMs, DEFAULT_CLEANUP_WATCH_MAX_MS);
974
- const sessionExists = typeof options.sessionExists === "function"
975
- ? options.sessionExists
976
- : psmuxSessionExists;
977
- const getPaneStatus = typeof options.getPaneStatus === "function"
978
- ? options.getPaneStatus
979
- : (targetPaneId) => ({ isDead: isPrimaryPaneDead(targetPaneId), exitCode: null });
980
- const killSession = typeof options.killSession === "function"
981
- ? options.killSession
982
- : killPsmuxSession;
1108
+ const pollMs = parsePositiveInt(
1109
+ options.pollMs,
1110
+ DEFAULT_CLEANUP_WATCH_POLL_MS,
1111
+ );
1112
+ const graceMs = parsePositiveInt(
1113
+ options.graceMs,
1114
+ DEFAULT_CLEANUP_WATCH_GRACE_MS,
1115
+ );
1116
+ const maxWaitMs = parsePositiveInt(
1117
+ options.maxWaitMs,
1118
+ DEFAULT_CLEANUP_WATCH_MAX_MS,
1119
+ );
1120
+ const sessionExists =
1121
+ typeof options.sessionExists === "function"
1122
+ ? options.sessionExists
1123
+ : psmuxSessionExists;
1124
+ const getPaneStatus =
1125
+ typeof options.getPaneStatus === "function"
1126
+ ? options.getPaneStatus
1127
+ : (targetPaneId) => ({
1128
+ isDead: isPrimaryPaneDead(targetPaneId),
1129
+ exitCode: null,
1130
+ });
1131
+ const killSession =
1132
+ typeof options.killSession === "function"
1133
+ ? options.killSession
1134
+ : killPsmuxSession;
983
1135
  const now = typeof options.now === "function" ? options.now : Date.now;
984
- const sleep = typeof options.sleep === "function" ? options.sleep : sleepMsAsync;
1136
+ const sleep =
1137
+ typeof options.sleep === "function" ? options.sleep : sleepMsAsync;
985
1138
  const startedAt = now();
986
1139
  let consecutiveErrors = 0;
987
1140
 
@@ -1045,7 +1198,8 @@ function startSpawnExitWatcher(sessionName, options = {}) {
1045
1198
  pollMs: options.pollMs,
1046
1199
  });
1047
1200
  args[0] = options.scriptPath || args[0];
1048
- const spawnFn = typeof options.spawnFn === "function" ? options.spawnFn : spawn;
1201
+ const spawnFn =
1202
+ typeof options.spawnFn === "function" ? options.spawnFn : spawn;
1049
1203
  const child = spawnFn(options.execPath || process.execPath, args, {
1050
1204
  detached: true,
1051
1205
  stdio: "ignore",
@@ -1057,9 +1211,17 @@ function startSpawnExitWatcher(sessionName, options = {}) {
1057
1211
  return true;
1058
1212
  }
1059
1213
 
1060
- function startSpawnSessionCleanupWatcher(sessionName, paneId, timingOptions = {}) {
1214
+ function startSpawnSessionCleanupWatcher(
1215
+ sessionName,
1216
+ paneId,
1217
+ timingOptions = {},
1218
+ ) {
1061
1219
  try {
1062
- startSpawnExitWatcher(sessionName, { ...timingOptions, force: true, paneId });
1220
+ startSpawnExitWatcher(sessionName, {
1221
+ ...timingOptions,
1222
+ force: true,
1223
+ paneId,
1224
+ });
1063
1225
  } catch {
1064
1226
  // watcher 시작 실패는 spawn 자체 실패로 보지 않는다.
1065
1227
  }
@@ -1096,7 +1258,9 @@ function spawnLocal(args, claudePath, prompt) {
1096
1258
  // 1단계: 프롬프트를 Get-Content -Raw → claude -p (one-shot), 세션 ID 추출
1097
1259
  // 2단계: --resume으로 인터랙티브 세션 이어붙이기
1098
1260
  const tmpFileNorm = normalizeCommandPath(tmpFile);
1099
- const flags = getPermissionFlag().map((f) => `'${escapePwshSingleQuoted(f)}'`).join(", ");
1261
+ const flags = getPermissionFlag()
1262
+ .map((f) => `'${escapePwshSingleQuoted(f)}'`)
1263
+ .join(", ");
1100
1264
  const scriptContent = [
1101
1265
  `$ErrorActionPreference = 'SilentlyContinue'`,
1102
1266
  `$t = '${escapePwshSingleQuoted(tmpFileNorm)}'`,
@@ -1109,9 +1273,15 @@ function spawnLocal(args, claudePath, prompt) {
1109
1273
  `$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }`,
1110
1274
  `exit $trifluxExit`,
1111
1275
  ].join("\n");
1112
- const scriptFile = join(tmpdir(), `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`);
1276
+ const scriptFile = join(
1277
+ tmpdir(),
1278
+ `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`,
1279
+ );
1113
1280
  writeFileSync(scriptFile, scriptContent, { encoding: "utf8" });
1114
- sendKeysToPane(paneId, `pwsh -NoProfile -File '${escapePwshSingleQuoted(normalizeCommandPath(scriptFile))}'; ${buildPwshExitTail()}`);
1281
+ sendKeysToPane(
1282
+ paneId,
1283
+ `pwsh -NoProfile -File '${escapePwshSingleQuoted(normalizeCommandPath(scriptFile))}'; ${buildPwshExitTail()}`,
1284
+ );
1115
1285
  } else {
1116
1286
  const command = buildLocalClaudeCommand(claudePathNorm, permissionFlags);
1117
1287
  sendKeysToPane(paneId, command);
@@ -1121,7 +1291,9 @@ function spawnLocal(args, claudePath, prompt) {
1121
1291
  openAttachTab(sessionName, `local:${slug}`);
1122
1292
  console.log(sessionName);
1123
1293
  } catch (err) {
1124
- try { killPsmuxSession(sessionName); } catch {}
1294
+ try {
1295
+ killPsmuxSession(sessionName);
1296
+ } catch {}
1125
1297
  throw err;
1126
1298
  }
1127
1299
  }
@@ -1140,7 +1312,9 @@ async function spawnRemote(args, promptContext) {
1140
1312
 
1141
1313
  const env = probeRemoteEnv(host);
1142
1314
  if (!env.claudePath) {
1143
- console.error(`claude not found on ${host}. Install Claude Code on the remote host first.`);
1315
+ console.error(
1316
+ `claude not found on ${host}. Install Claude Code on the remote host first.`,
1317
+ );
1144
1318
  process.exit(1);
1145
1319
  }
1146
1320
  const resolvedDir = resolveRemoteDir(args.dir, env);
@@ -1151,10 +1325,17 @@ async function spawnRemote(args, promptContext) {
1151
1325
  let prompt = promptContext.prompt;
1152
1326
 
1153
1327
  try {
1154
- const { stagedFiles } = stageRemotePromptFiles(host, env, promptContext.transferCandidates, sessionName);
1328
+ const { stagedFiles } = stageRemotePromptFiles(
1329
+ host,
1330
+ env,
1331
+ promptContext.transferCandidates,
1332
+ sessionName,
1333
+ );
1155
1334
  prompt = rewritePromptPaths(prompt, stagedFiles);
1156
1335
  } catch (error) {
1157
- failFast(`failed to stage remote files: ${error?.message || String(error)}`);
1336
+ failFast(
1337
+ `failed to stage remote files: ${error?.message || String(error)}`,
1338
+ );
1158
1339
  }
1159
1340
 
1160
1341
  createPsmuxSession(sessionName, { layout: "1xN", paneCount: 1 });
@@ -1174,11 +1355,18 @@ async function spawnRemote(args, promptContext) {
1174
1355
  const stageDir = resolveRemoteStageDir(env, sessionName);
1175
1356
  ensureRemoteStageDir(host, env, stageDir);
1176
1357
 
1177
- const localPromptFile = join(tmpdir(), `tfx-prompt-${randomUUID().slice(0, 8)}.md`);
1358
+ const localPromptFile = join(
1359
+ tmpdir(),
1360
+ `tfx-prompt-${randomUUID().slice(0, 8)}.md`,
1361
+ );
1178
1362
  writeFileSync(localPromptFile, prompt, { encoding: "utf8" });
1179
1363
  const remotePromptFile = `${stageDir}/prompt.md`;
1180
1364
  uploadFileToRemote(host, localPromptFile, remotePromptFile);
1181
- try { unlinkSync(localPromptFile); } catch { /* cleanup best-effort */ }
1365
+ try {
1366
+ unlinkSync(localPromptFile);
1367
+ } catch {
1368
+ /* cleanup best-effort */
1369
+ }
1182
1370
 
1183
1371
  if (env.shell === "pwsh") {
1184
1372
  const remotePromptWin = remotePromptFile.replace(/\//g, "\\");
@@ -1193,16 +1381,29 @@ async function spawnRemote(args, promptContext) {
1193
1381
  `$trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }`,
1194
1382
  `exit $trifluxExit`,
1195
1383
  ].join("\n");
1196
- const localScript = join(tmpdir(), `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`);
1384
+ const localScript = join(
1385
+ tmpdir(),
1386
+ `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`,
1387
+ );
1197
1388
  writeFileSync(localScript, scriptContent, { encoding: "utf8" });
1198
1389
  const remoteScript = `${stageDir}/launch.ps1`;
1199
1390
  uploadFileToRemote(host, localScript, remoteScript);
1200
- try { unlinkSync(localScript); } catch { /* cleanup best-effort */ }
1391
+ try {
1392
+ unlinkSync(localScript);
1393
+ } catch {
1394
+ /* cleanup best-effort */
1395
+ }
1201
1396
 
1202
1397
  const remoteScriptWin = remoteScript.replace(/\//g, "\\");
1203
- sendKeysToPane(paneId, `pwsh -NoProfile -File '${escapePwshSingleQuoted(remoteScriptWin)}'`);
1398
+ sendKeysToPane(
1399
+ paneId,
1400
+ `pwsh -NoProfile -File '${escapePwshSingleQuoted(remoteScriptWin)}'`,
1401
+ );
1204
1402
  } else {
1205
- sendKeysToPane(paneId, `${shellQuote(env.claudePath)} ${permissionFlags} < ${shellQuote(remotePromptFile)} && rm -f ${shellQuote(remotePromptFile)}; ${buildPosixExitTail()}`);
1403
+ sendKeysToPane(
1404
+ paneId,
1405
+ `${shellQuote(env.claudePath)} ${permissionFlags} < ${shellQuote(remotePromptFile)} && rm -f ${shellQuote(remotePromptFile)}; ${buildPosixExitTail()}`,
1406
+ );
1206
1407
  }
1207
1408
  } else {
1208
1409
  const claudeCommand = buildRemoteClaudeCommand(env, permissionFlags);
@@ -1213,7 +1414,9 @@ async function spawnRemote(args, promptContext) {
1213
1414
  openAttachTab(sessionName, `${host}:${slug}`);
1214
1415
  console.log(sessionName);
1215
1416
  } catch (err) {
1216
- try { killPsmuxSession(sessionName); } catch {}
1417
+ try {
1418
+ killPsmuxSession(sessionName);
1419
+ } catch {}
1217
1420
  throw err;
1218
1421
  }
1219
1422
  }
@@ -1253,13 +1456,19 @@ async function waitForClaudeReady(sessionName, timeoutSec = 60) {
1253
1456
 
1254
1457
  while (Date.now() <= deadline) {
1255
1458
  const snapshot = capturePsmuxPane(paneId, 5);
1256
- const lastLine = snapshot.split(/\r?\n/).filter((l) => l.trim()).at(-1) || "";
1459
+ const lastLine =
1460
+ snapshot
1461
+ .split(/\r?\n/)
1462
+ .filter((l) => l.trim())
1463
+ .at(-1) || "";
1257
1464
  if (readyPattern.test(lastLine)) {
1258
1465
  return true;
1259
1466
  }
1260
1467
  sleepMs(1000);
1261
1468
  }
1262
- throw new Error(`claude ready wait timed out after ${timeoutSec}s for ${sessionName}`);
1469
+ throw new Error(
1470
+ `claude ready wait timed out after ${timeoutSec}s for ${sessionName}`,
1471
+ );
1263
1472
  }
1264
1473
 
1265
1474
  async function main() {
@@ -1299,7 +1508,9 @@ async function main() {
1299
1508
  console.error("--probe requires a host");
1300
1509
  process.exit(1);
1301
1510
  }
1302
- console.log(JSON.stringify(probeRemoteEnv(args.probeHost, { force: true }), null, 2));
1511
+ console.log(
1512
+ JSON.stringify(probeRemoteEnv(args.probeHost, { force: true }), null, 2),
1513
+ );
1303
1514
  return;
1304
1515
  }
1305
1516
 
@@ -1328,7 +1539,9 @@ async function main() {
1328
1539
  const prompt = promptContext.prompt;
1329
1540
 
1330
1541
  if (args.local && args.transferFiles.length > 0) {
1331
- console.warn("[tfx] --transfer는 원격 모드에서만 사용 가능합니다 (--local에서는 무시됨)");
1542
+ console.warn(
1543
+ "[tfx] --transfer는 원격 모드에서만 사용 가능합니다 (--local에서는 무시됨)",
1544
+ );
1332
1545
  }
1333
1546
 
1334
1547
  if (args.command === "send") {
@@ -1357,7 +1570,9 @@ async function main() {
1357
1570
  await spawnRemote(args, promptContext);
1358
1571
  }
1359
1572
 
1360
- const selfRun = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
1573
+ const selfRun =
1574
+ process.argv[1] &&
1575
+ fileURLToPath(import.meta.url) === resolve(process.argv[1]);
1361
1576
  if (selfRun) {
1362
1577
  main().catch((error) => {
1363
1578
  console.error(error?.message || String(error));