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
@@ -7,8 +7,8 @@
7
7
  // L1.5: INPUT_WAIT 패턴 감지 (detectInputWait 재사용)
8
8
  // L3: 완료 토큰 감지 (__TRIFLUX_DONE__ 또는 프롬프트 idle)
9
9
 
10
- import { execFileSync } from 'node:child_process';
11
- import { detectInputWait, PROBE_DEFAULTS } from './health-probe.mjs';
10
+ import { execFileSync } from "node:child_process";
11
+ import { detectInputWait, PROBE_DEFAULTS } from "./health-probe.mjs";
12
12
 
13
13
  /** 완료 토큰 패턴 */
14
14
  const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
@@ -27,19 +27,25 @@ const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
27
27
  export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
28
28
  const execFn = deps.execFileSync || execFileSync;
29
29
  try {
30
- const output = execFn('ssh', [
31
- '-o', 'ConnectTimeout=5',
32
- '-o', 'BatchMode=yes',
33
- host,
34
- `psmux capture-pane -t ${paneTarget} -p -S -`,
35
- ], {
36
- encoding: 'utf8',
37
- timeout: 10_000,
38
- windowsHide: true,
39
- stdio: ['ignore', 'pipe', 'ignore'],
40
- });
41
- const nonEmpty = output.split('\n').filter((line) => line.trim() !== '');
42
- return nonEmpty.slice(-lines).join('\n');
30
+ const output = execFn(
31
+ "ssh",
32
+ [
33
+ "-o",
34
+ "ConnectTimeout=5",
35
+ "-o",
36
+ "BatchMode=yes",
37
+ host,
38
+ `psmux capture-pane -t ${paneTarget} -p -S -`,
39
+ ],
40
+ {
41
+ encoding: "utf8",
42
+ timeout: 10_000,
43
+ windowsHide: true,
44
+ stdio: ["ignore", "pipe", "ignore"],
45
+ },
46
+ );
47
+ const nonEmpty = output.split("\n").filter((line) => line.trim() !== "");
48
+ return nonEmpty.slice(-lines).join("\n");
43
49
  } catch {
44
50
  return null;
45
51
  }
@@ -55,16 +61,22 @@ export function sshCapturePane(host, paneTarget, lines = 20, deps = {}) {
55
61
  export function sshSessionExists(host, sessionName, deps = {}) {
56
62
  const execFn = deps.execFileSync || execFileSync;
57
63
  try {
58
- execFn('ssh', [
59
- '-o', 'ConnectTimeout=5',
60
- '-o', 'BatchMode=yes',
61
- host,
62
- `psmux has-session -t ${sessionName}`,
63
- ], {
64
- timeout: 10_000,
65
- windowsHide: true,
66
- stdio: ['ignore', 'ignore', 'ignore'],
67
- });
64
+ execFn(
65
+ "ssh",
66
+ [
67
+ "-o",
68
+ "ConnectTimeout=5",
69
+ "-o",
70
+ "BatchMode=yes",
71
+ host,
72
+ `psmux has-session -t ${sessionName}`,
73
+ ],
74
+ {
75
+ timeout: 10_000,
76
+ windowsHide: true,
77
+ stdio: ["ignore", "ignore", "ignore"],
78
+ },
79
+ );
68
80
  return true;
69
81
  } catch {
70
82
  return false;
@@ -92,7 +104,7 @@ export function createRemoteProbe(session, opts = {}) {
92
104
  let started = false;
93
105
 
94
106
  // L1 tracking — 출력 변화 감지
95
- let lastCaptureHash = '';
107
+ let lastCaptureHash = "";
96
108
  let lastOutputChangeAt = Date.now();
97
109
 
98
110
  // L3 tracking — 완료 토큰 / prompt idle
@@ -102,7 +114,7 @@ export function createRemoteProbe(session, opts = {}) {
102
114
  const status = {
103
115
  l0: null,
104
116
  l1: null,
105
- l2: 'skip', // 원격은 MCP L2 미지원
117
+ l2: "skip", // 원격은 MCP L2 미지원
106
118
  l3: null,
107
119
  lastProbeAt: null,
108
120
  inputWaitPattern: null,
@@ -113,7 +125,7 @@ export function createRemoteProbe(session, opts = {}) {
113
125
  */
114
126
  function probeL0() {
115
127
  const exists = sshSessionExists(session.host, session.sessionName, deps);
116
- status.l0 = exists ? 'ok' : 'fail';
128
+ status.l0 = exists ? "ok" : "fail";
117
129
  return status.l0;
118
130
  }
119
131
 
@@ -135,9 +147,9 @@ export function createRemoteProbe(session, opts = {}) {
135
147
  if (hash !== lastCaptureHash) {
136
148
  lastCaptureHash = hash;
137
149
  lastOutputChangeAt = now;
138
- status.l1 = 'ok';
150
+ status.l1 = "ok";
139
151
  status.inputWaitPattern = null;
140
- return 'ok';
152
+ return "ok";
141
153
  }
142
154
 
143
155
  const silenceMs = now - lastOutputChangeAt;
@@ -147,18 +159,18 @@ export function createRemoteProbe(session, opts = {}) {
147
159
  const inputWait = detectInputWait(captured);
148
160
 
149
161
  if (inputWait.detected) {
150
- status.l1 = 'input_wait';
162
+ status.l1 = "input_wait";
151
163
  status.inputWaitPattern = inputWait.pattern;
152
- return 'input_wait';
164
+ return "input_wait";
153
165
  }
154
166
 
155
- status.l1 = 'stall';
167
+ status.l1 = "stall";
156
168
  status.inputWaitPattern = null;
157
- return 'stall';
169
+ return "stall";
158
170
  }
159
171
 
160
- status.l1 = 'ok';
161
- return 'ok';
172
+ status.l1 = "ok";
173
+ return "ok";
162
174
  }
163
175
 
164
176
  /**
@@ -167,28 +179,28 @@ export function createRemoteProbe(session, opts = {}) {
167
179
  */
168
180
  function probeL3(captured) {
169
181
  if (promptAcked) {
170
- status.l3 = 'ok';
171
- return 'ok';
182
+ status.l3 = "ok";
183
+ return "ok";
172
184
  }
173
185
 
174
186
  if (captured != null && captured.length > 0) {
175
187
  // 완료 토큰 감지 → 즉시 ok
176
188
  if (COMPLETION_TOKEN_RE.test(captured)) {
177
189
  promptAcked = true;
178
- status.l3 = 'completed';
179
- return 'completed';
190
+ status.l3 = "completed";
191
+ return "completed";
180
192
  }
181
193
 
182
194
  // 출력이 있으면 prompt acknowledged
183
195
  promptAcked = true;
184
- status.l3 = 'ok';
185
- return 'ok';
196
+ status.l3 = "ok";
197
+ return "ok";
186
198
  }
187
199
 
188
200
  const elapsed = Date.now() - spawnedAt;
189
201
  if (elapsed >= config.l3ThresholdMs) {
190
- status.l3 = 'timeout';
191
- return 'timeout';
202
+ status.l3 = "timeout";
203
+ return "timeout";
192
204
  }
193
205
 
194
206
  status.l3 = null;
@@ -204,7 +216,7 @@ export function createRemoteProbe(session, opts = {}) {
204
216
 
205
217
  // L0 실패 시 나머지 probe 스킵
206
218
  let captured = null;
207
- if (l0 === 'ok') {
219
+ if (l0 === "ok") {
208
220
  captured = sshCapturePane(session.host, session.paneTarget, 20, deps);
209
221
  }
210
222
 
@@ -214,14 +226,14 @@ export function createRemoteProbe(session, opts = {}) {
214
226
  const result = {
215
227
  l0,
216
228
  l1,
217
- l2: 'skip',
229
+ l2: "skip",
218
230
  l3,
219
231
  inputWaitPattern: status.inputWaitPattern,
220
232
  ts: Date.now(),
221
233
  };
222
234
  status.lastProbeAt = result.ts;
223
235
 
224
- if (typeof config.onProbe === 'function') {
236
+ if (typeof config.onProbe === "function") {
225
237
  config.onProbe(result);
226
238
  }
227
239
 
@@ -233,10 +245,12 @@ export function createRemoteProbe(session, opts = {}) {
233
245
  started = true;
234
246
  spawnedAt = Date.now();
235
247
  lastOutputChangeAt = Date.now();
236
- lastCaptureHash = '';
248
+ lastCaptureHash = "";
237
249
  promptAcked = false;
238
250
 
239
- timer = setInterval(() => { void probe(); }, config.intervalMs);
251
+ timer = setInterval(() => {
252
+ void probe();
253
+ }, config.intervalMs);
240
254
  timer.unref?.();
241
255
 
242
256
  // 즉시 첫 probe 실행
@@ -254,13 +268,13 @@ export function createRemoteProbe(session, opts = {}) {
254
268
 
255
269
  /** tracking 리셋 (restart 후 호출) */
256
270
  function resetTracking() {
257
- lastCaptureHash = '';
271
+ lastCaptureHash = "";
258
272
  lastOutputChangeAt = Date.now();
259
273
  promptAcked = false;
260
274
  spawnedAt = Date.now();
261
275
  status.l0 = null;
262
276
  status.l1 = null;
263
- status.l2 = 'skip';
277
+ status.l2 = "skip";
264
278
  status.l3 = null;
265
279
  status.inputWaitPattern = null;
266
280
  }
@@ -271,6 +285,8 @@ export function createRemoteProbe(session, opts = {}) {
271
285
  probe,
272
286
  resetTracking,
273
287
  getStatus: () => ({ ...status }),
274
- get started() { return started; },
288
+ get started() {
289
+ return started;
290
+ },
275
291
  });
276
292
  }
@@ -2,13 +2,18 @@
2
2
  // Extracted from scripts/remote-spawn.mjs for reuse by swarm-hypervisor.
3
3
  // Pure functions + SSH operations. No psmux, no WT, no CLI arg parsing.
4
4
 
5
- import { execFileSync } from 'node:child_process';
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { basename, join, posix as posixPath, win32 as win32Path } from 'node:path';
8
- import { execSshWithRetry } from '../lib/ssh-retry.mjs';
5
+ import { execFileSync } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import {
8
+ basename,
9
+ join,
10
+ posix as posixPath,
11
+ win32 as win32Path,
12
+ } from "node:path";
13
+ import { execSshWithRetry } from "../lib/ssh-retry.mjs";
9
14
 
10
15
  const REMOTE_ENV_TTL_MS = 86_400_000; // 24h
11
- const REMOTE_STAGE_ROOT = 'tfx-remote';
16
+ const REMOTE_STAGE_ROOT = "tfx-remote";
12
17
  const SAFE_HOST_RE = /^[a-zA-Z0-9._-]+$/;
13
18
 
14
19
  // ── Shell quoting utilities ─────────────────────────────────────
@@ -22,11 +27,11 @@ export function escapePwshSingleQuoted(value) {
22
27
  }
23
28
 
24
29
  export function escapePwshDoubleQuoted(value) {
25
- return String(value).replace(/`/g, '``').replace(/"/g, '`"');
30
+ return String(value).replace(/`/g, "``").replace(/"/g, '`"');
26
31
  }
27
32
 
28
33
  function normalizeCommandPath(value) {
29
- return String(value).replace(/\\/g, '/');
34
+ return String(value).replace(/\\/g, "/");
30
35
  }
31
36
 
32
37
  // ── Validation ──────────────────────────────────────────────────
@@ -47,7 +52,7 @@ function parseProbeLines(text) {
47
52
  .map((line) => line.trim())
48
53
  .filter(Boolean)
49
54
  .map((line) => {
50
- const idx = line.indexOf('=');
55
+ const idx = line.indexOf("=");
51
56
  return idx === -1 ? null : [line.slice(0, idx), line.slice(idx + 1)];
52
57
  })
53
58
  .filter(Boolean),
@@ -55,24 +60,27 @@ function parseProbeLines(text) {
55
60
  }
56
61
 
57
62
  function normalizePwshProbeEnv(parsed) {
58
- if (parsed.shell !== 'pwsh' || parsed.os !== 'win32') return null;
63
+ if (parsed.shell !== "pwsh" || parsed.os !== "win32") return null;
59
64
  if (!parsed.home) return null;
60
65
  return Object.freeze({
61
- claudePath: (!parsed.claude || parsed.claude === 'notfound') ? null : parsed.claude,
66
+ claudePath:
67
+ !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
62
68
  home: parsed.home,
63
- os: 'win32',
64
- shell: 'pwsh',
69
+ os: "win32",
70
+ shell: "pwsh",
65
71
  });
66
72
  }
67
73
 
68
74
  function normalizePosixProbeEnv(parsed) {
69
- const os = parsed.os === 'darwin' ? 'darwin' : parsed.os === 'linux' ? 'linux' : null;
75
+ const os =
76
+ parsed.os === "darwin" ? "darwin" : parsed.os === "linux" ? "linux" : null;
70
77
  if (!os || !parsed.home) return null;
71
78
  return Object.freeze({
72
- claudePath: (!parsed.claude || parsed.claude === 'notfound') ? null : parsed.claude,
79
+ claudePath:
80
+ !parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
73
81
  home: parsed.home,
74
82
  os,
75
- shell: parsed.shell === 'zsh' ? 'zsh' : 'bash',
83
+ shell: parsed.shell === "zsh" ? "zsh" : "bash",
76
84
  });
77
85
  }
78
86
 
@@ -81,14 +89,20 @@ function probeRemoteEnvViaPwsh(host) {
81
89
  "Write-Output 'shell=pwsh'",
82
90
  'Write-Output "home=$env:USERPROFILE"',
83
91
  '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\' }',
84
- 'Write-Output "os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? \'win32\' : \'other\')"',
85
- ].join('; ');
92
+ "Write-Output \"os=$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) ? 'win32' : 'other')\"",
93
+ ].join("; ");
86
94
 
87
95
  try {
88
- const output = execSshWithRetry([host, 'pwsh', '-NoProfile', '-Command', command], {
89
- encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
90
- maxRetries: 2, baseDelayMs: 1000,
91
- });
96
+ const output = execSshWithRetry(
97
+ [host, "pwsh", "-NoProfile", "-Command", command],
98
+ {
99
+ encoding: "utf8",
100
+ timeout: 15000,
101
+ stdio: ["pipe", "pipe", "pipe"],
102
+ maxRetries: 2,
103
+ baseDelayMs: 1000,
104
+ },
105
+ );
92
106
  return normalizePwshProbeEnv(parseProbeLines(output));
93
107
  } catch {
94
108
  return null;
@@ -97,16 +111,19 @@ function probeRemoteEnvViaPwsh(host) {
97
111
 
98
112
  function probeRemoteEnvViaPosix(host) {
99
113
  const script = [
100
- 'echo shell=$(basename $SHELL)',
101
- 'echo home=$HOME',
102
- 'command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound',
103
- 'echo os=$(uname -s | tr A-Z a-z)',
104
- ].join('\n');
114
+ "echo shell=$(basename $SHELL)",
115
+ "echo home=$HOME",
116
+ "command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound",
117
+ "echo os=$(uname -s | tr A-Z a-z)",
118
+ ].join("\n");
105
119
 
106
120
  try {
107
- const output = execSshWithRetry([host, 'sh'], {
108
- encoding: 'utf8', timeout: 15000, input: script,
109
- maxRetries: 2, baseDelayMs: 1000,
121
+ const output = execSshWithRetry([host, "sh"], {
122
+ encoding: "utf8",
123
+ timeout: 15000,
124
+ input: script,
125
+ maxRetries: 2,
126
+ baseDelayMs: 1000,
110
127
  });
111
128
  return normalizePosixProbeEnv(parseProbeLines(output));
112
129
  } catch {
@@ -124,8 +141,8 @@ function readEnvCache(host, cacheDir) {
124
141
  const cachePath = getEnvCachePath(host, cacheDir);
125
142
  if (!existsSync(cachePath)) return null;
126
143
  try {
127
- const parsed = JSON.parse(readFileSync(cachePath, 'utf8'));
128
- return parsed && typeof parsed === 'object' ? parsed : null;
144
+ const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
145
+ return parsed && typeof parsed === "object" ? parsed : null;
129
146
  } catch {
130
147
  return null;
131
148
  }
@@ -133,16 +150,20 @@ function readEnvCache(host, cacheDir) {
133
150
 
134
151
  function isEnvCacheFresh(entry) {
135
152
  return Boolean(
136
- entry
137
- && typeof entry.cachedAt === 'number'
138
- && entry.env
139
- && (Date.now() - entry.cachedAt) < REMOTE_ENV_TTL_MS,
153
+ entry &&
154
+ typeof entry.cachedAt === "number" &&
155
+ entry.env &&
156
+ Date.now() - entry.cachedAt < REMOTE_ENV_TTL_MS,
140
157
  );
141
158
  }
142
159
 
143
160
  function writeEnvCache(host, env, cacheDir) {
144
161
  mkdirSync(cacheDir, { recursive: true });
145
- writeFileSync(getEnvCachePath(host, cacheDir), JSON.stringify({ cachedAt: Date.now(), env }, null, 2), 'utf8');
162
+ writeFileSync(
163
+ getEnvCachePath(host, cacheDir),
164
+ JSON.stringify({ cachedAt: Date.now(), env }, null, 2),
165
+ "utf8",
166
+ );
146
167
  }
147
168
 
148
169
  /**
@@ -158,7 +179,7 @@ function writeEnvCache(host, env, cacheDir) {
158
179
  export function probeRemoteEnv(host, opts = {}) {
159
180
  validateHost(host);
160
181
  const force = opts.force === true;
161
- const cacheDir = opts.cacheDir || join('.omc', 'state', 'remote-env');
182
+ const cacheDir = opts.cacheDir || join(".omc", "state", "remote-env");
162
183
 
163
184
  if (!force) {
164
185
  const cached = readEnvCache(host, cacheDir);
@@ -166,10 +187,16 @@ export function probeRemoteEnv(host, opts = {}) {
166
187
  }
167
188
 
168
189
  const pwshEnv = probeRemoteEnvViaPwsh(host);
169
- if (pwshEnv) { writeEnvCache(host, pwshEnv, cacheDir); return pwshEnv; }
190
+ if (pwshEnv) {
191
+ writeEnvCache(host, pwshEnv, cacheDir);
192
+ return pwshEnv;
193
+ }
170
194
 
171
195
  const posixEnv = probeRemoteEnvViaPosix(host);
172
- if (posixEnv) { writeEnvCache(host, posixEnv, cacheDir); return posixEnv; }
196
+ if (posixEnv) {
197
+ writeEnvCache(host, posixEnv, cacheDir);
198
+ return posixEnv;
199
+ }
173
200
 
174
201
  throw new Error(`remote probe failed for ${host}`);
175
202
  }
@@ -177,7 +204,7 @@ export function probeRemoteEnv(host, opts = {}) {
177
204
  // ── Remote directory resolution ─────────────────────────────────
178
205
 
179
206
  function isWindowsAbsolutePath(value) {
180
- return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith('\\\\');
207
+ return /^[a-zA-Z]:[\\/]/u.test(value) || value.startsWith("\\\\");
181
208
  }
182
209
 
183
210
  /**
@@ -191,17 +218,19 @@ function isWindowsAbsolutePath(value) {
191
218
  export function resolveRemoteDir(dir, env) {
192
219
  const requestedDir = dir || env.home;
193
220
 
194
- if (env.os === 'win32') {
195
- const winDir = requestedDir.replace(/\//g, '\\');
196
- if (winDir === '~') return env.home;
197
- if (/^~[\\/]/u.test(winDir)) return win32Path.join(env.home, winDir.slice(2));
221
+ if (env.os === "win32") {
222
+ const winDir = requestedDir.replace(/\//g, "\\");
223
+ if (winDir === "~") return env.home;
224
+ if (/^~[\\/]/u.test(winDir))
225
+ return win32Path.join(env.home, winDir.slice(2));
198
226
  if (isWindowsAbsolutePath(winDir)) return winDir;
199
227
  return win32Path.join(env.home, winDir);
200
228
  }
201
229
 
202
- if (requestedDir === '~') return env.home;
203
- if (requestedDir.startsWith('~/')) return posixPath.join(env.home, requestedDir.slice(2));
204
- if (requestedDir.startsWith('/')) return requestedDir;
230
+ if (requestedDir === "~") return env.home;
231
+ if (requestedDir.startsWith("~/"))
232
+ return posixPath.join(env.home, requestedDir.slice(2));
233
+ if (requestedDir.startsWith("/")) return requestedDir;
205
234
  return posixPath.join(env.home, requestedDir);
206
235
  }
207
236
 
@@ -224,12 +253,26 @@ export function resolveRemoteStageDir(env, stageId) {
224
253
  * @param {string} remoteStageDir
225
254
  */
226
255
  export function ensureRemoteStageDir(host, env, remoteStageDir) {
227
- if (env.os === 'win32') {
256
+ if (env.os === "win32") {
228
257
  const safePath = escapePwshSingleQuoted(remoteStageDir);
229
- execFileSync('ssh', [host, 'pwsh', '-NoProfile', '-Command', `New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`], { timeout: 10000, stdio: 'pipe' });
258
+ execFileSync(
259
+ "ssh",
260
+ [
261
+ host,
262
+ "pwsh",
263
+ "-NoProfile",
264
+ "-Command",
265
+ `New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`,
266
+ ],
267
+ { timeout: 10000, stdio: "pipe" },
268
+ );
230
269
  return;
231
270
  }
232
- execFileSync('ssh', [host, 'sh', '-lc', `mkdir -p ${shellQuote(remoteStageDir)}`], { timeout: 10000, stdio: 'pipe' });
271
+ execFileSync(
272
+ "ssh",
273
+ [host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`],
274
+ { timeout: 10000, stdio: "pipe" },
275
+ );
233
276
  }
234
277
 
235
278
  /**
@@ -239,7 +282,10 @@ export function ensureRemoteStageDir(host, env, remoteStageDir) {
239
282
  * @param {string} remotePath
240
283
  */
241
284
  export function uploadFileToRemote(host, localPath, remotePath) {
242
- execFileSync('scp', [localPath, `${host}:${remotePath}`], { timeout: 15000, stdio: 'pipe' });
285
+ execFileSync("scp", [localPath, `${host}:${remotePath}`], {
286
+ timeout: 15000,
287
+ stdio: "pipe",
288
+ });
243
289
  }
244
290
 
245
291
  /**
@@ -283,17 +329,29 @@ export function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
283
329
  * @returns {string} stdout
284
330
  */
285
331
  export function remoteGit(host, env, gitArgs, cwd) {
286
- const gitCmd = ['git', ...gitArgs].map((a) => shellQuote(a)).join(' ');
332
+ const gitCmd = ["git", ...gitArgs].map((a) => shellQuote(a)).join(" ");
287
333
 
288
- if (env.os === 'win32') {
334
+ if (env.os === "win32") {
289
335
  const cdPath = escapePwshSingleQuoted(cwd);
290
336
  const command = `Set-Location '${cdPath}'; ${gitCmd}`;
291
- return execFileSync('ssh', [host, 'pwsh', '-NoProfile', '-Command', command], {
292
- encoding: 'utf8', timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
293
- }).trim();
337
+ return execFileSync(
338
+ "ssh",
339
+ [host, "pwsh", "-NoProfile", "-Command", command],
340
+ {
341
+ encoding: "utf8",
342
+ timeout: 30_000,
343
+ stdio: ["pipe", "pipe", "pipe"],
344
+ },
345
+ ).trim();
294
346
  }
295
347
 
296
- return execFileSync('ssh', [host, 'sh', '-lc', `cd ${shellQuote(cwd)} && ${gitCmd}`], {
297
- encoding: 'utf8', timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
298
- }).trim();
348
+ return execFileSync(
349
+ "ssh",
350
+ [host, "sh", "-lc", `cd ${shellQuote(cwd)} && ${gitCmd}`],
351
+ {
352
+ encoding: "utf8",
353
+ timeout: 30_000,
354
+ stdio: ["pipe", "pipe", "pipe"],
355
+ },
356
+ ).trim();
299
357
  }