@triflux/remote 10.50.1 → 10.51.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 (120) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +16 -247
  3. package/cto/current.schema.json +4 -20
  4. package/cto/events.mjs +13 -294
  5. package/cto/hygiene.mjs +26 -346
  6. package/cto/lake-root.mjs +114 -16
  7. package/cto/status.mjs +33 -128
  8. package/hub/index.mjs +0 -8
  9. package/hub/store-adapter.mjs +1 -1
  10. package/hub/team/agent-map.json +0 -1
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/cli/commands/attach.mjs +1 -17
  14. package/hub/team/cli/commands/control.mjs +12 -38
  15. package/hub/team/cli/commands/debug.mjs +1 -37
  16. package/hub/team/cli/commands/focus.mjs +5 -31
  17. package/hub/team/cli/commands/interrupt.mjs +6 -25
  18. package/hub/team/cli/commands/kill.mjs +1 -30
  19. package/hub/team/cli/commands/list.mjs +0 -16
  20. package/hub/team/cli/commands/send.mjs +6 -26
  21. package/hub/team/cli/commands/start/index.mjs +41 -133
  22. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  23. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  24. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  25. package/hub/team/cli/commands/status.mjs +2 -57
  26. package/hub/team/cli/commands/stop.mjs +14 -19
  27. package/hub/team/cli/help.mjs +4 -5
  28. package/hub/team/cli/manifest.mjs +5 -1
  29. package/hub/team/cli/render.mjs +0 -12
  30. package/hub/team/cli/services/hub-client.mjs +2 -88
  31. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  32. package/hub/team/handoff.mjs +3 -34
  33. package/hub/team/headless.mjs +14 -755
  34. package/hub/team/orchestrator.mjs +4 -30
  35. package/hub/team/pane.mjs +6 -28
  36. package/hub/team/psmux.mjs +16 -403
  37. package/hub/team/remote-session.mjs +1 -2
  38. package/hub/team/session.mjs +8 -119
  39. package/hub/team/swarm-intent.mjs +0 -198
  40. package/hub/team/terminal-opener.mjs +11 -35
  41. package/hub/team/tui-core.mjs +1 -1
  42. package/hub/team/tui-viewer.mjs +2 -2
  43. package/hub/team/tui.mjs +8 -269
  44. package/hub/team/wt-manager.mjs +2 -2
  45. package/hub/workers/factory.mjs +4 -37
  46. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  47. package/package.json +1 -1
  48. package/scripts/lib/agent-route-policy.mjs +162 -14
  49. package/scripts/lib/claudemd-scanner.mjs +2 -199
  50. package/scripts/lib/cli-claude.mjs +0 -2
  51. package/scripts/lib/context.mjs +3 -19
  52. package/scripts/lib/env-probe.mjs +5 -64
  53. package/scripts/lib/gemini-profiles.mjs +2 -69
  54. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  55. package/scripts/lib/machine-profile.mjs +3 -17
  56. package/scripts/lib/mcp-filter.mjs +109 -53
  57. package/scripts/lib/mcp-health.mjs +2 -2
  58. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  59. package/scripts/lib/psmux-info.mjs +25 -104
  60. package/scripts/lib/skill-template.mjs +0 -271
  61. package/cto/hygiene-actions.mjs +0 -355
  62. package/cto/hygiene-notify.mjs +0 -170
  63. package/hub/team/build-worker-prompt.mjs +0 -92
  64. package/hub/team/check-mcp-hub.mjs +0 -49
  65. package/hub/team/claude-native-bridge.mjs +0 -987
  66. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  67. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  68. package/hub/team/cli/services/native-control.mjs +0 -145
  69. package/hub/team/codex-review.mjs +0 -520
  70. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  71. package/hub/team/conductor.mjs +0 -1529
  72. package/hub/team/consensus-meta.mjs +0 -176
  73. package/hub/team/cto-pull.mjs +0 -31
  74. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  75. package/hub/team/dashboard.mjs +0 -322
  76. package/hub/team/event-log.mjs +0 -84
  77. package/hub/team/execution-mode.mjs +0 -324
  78. package/hub/team/extract-completion-payload.mjs +0 -110
  79. package/hub/team/headless-bridge-session.mjs +0 -17
  80. package/hub/team/health-probe.mjs +0 -435
  81. package/hub/team/interactive-native-launcher.mjs +0 -119
  82. package/hub/team/interactive-native-worker.mjs +0 -80
  83. package/hub/team/interactive-tui-transport.mjs +0 -479
  84. package/hub/team/launcher-template.mjs +0 -144
  85. package/hub/team/mcp-selector.mjs +0 -145
  86. package/hub/team/native-supervisor.mjs +0 -460
  87. package/hub/team/native.mjs +0 -727
  88. package/hub/team/recovery-store.mjs +0 -149
  89. package/hub/team/remote-probe.mjs +0 -292
  90. package/hub/team/remote-watcher.mjs +0 -506
  91. package/hub/team/runtime-strategy.mjs +0 -200
  92. package/hub/team/schemas/worker-completion.json +0 -39
  93. package/hub/team/sentinel-capture.mjs +0 -127
  94. package/hub/team/swarm-cli.mjs +0 -390
  95. package/hub/team/swarm-hypervisor.mjs +0 -2206
  96. package/hub/team/swarm-locks.mjs +0 -335
  97. package/hub/team/swarm-planner.mjs +0 -371
  98. package/hub/team/swarm-preflight.mjs +0 -366
  99. package/hub/team/swarm-reconciler.mjs +0 -242
  100. package/hub/team/tui-remote-adapter.mjs +0 -377
  101. package/hub/team/worker-completion-validator.mjs +0 -79
  102. package/hub/team/worktree-lifecycle.mjs +0 -639
  103. package/hub/team/wt-templates.json +0 -43
  104. package/hub/workers/antigravity-route-worker.mjs +0 -179
  105. package/hub/workers/gemini-worker.mjs +0 -217
  106. package/scripts/lib/agent-json.mjs +0 -27
  107. package/scripts/lib/async.mjs +0 -174
  108. package/scripts/lib/cli-agy.mjs +0 -85
  109. package/scripts/lib/cli-codex.mjs +0 -62
  110. package/scripts/lib/cli-gemini.mjs +0 -37
  111. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  112. package/scripts/lib/env.mjs +0 -14
  113. package/scripts/lib/hub.mjs +0 -48
  114. package/scripts/lib/pid.mjs +0 -63
  115. package/scripts/lib/quota.mjs +0 -47
  116. package/scripts/lib/skill-state.mjs +0 -220
  117. package/scripts/lib/team.mjs +0 -75
  118. package/scripts/lib/timeout.mjs +0 -66
  119. package/scripts/lib/tmp.mjs +0 -26
  120. package/scripts/lib/toml.mjs +0 -132
@@ -44,13 +44,12 @@ export function decomposeTask(taskDescription, agentCount) {
44
44
  * @param {string} taskDescription
45
45
  * @param {object} config
46
46
  * @param {string} config.agentId
47
- * @param {string} config.hubUrl
48
47
  * @param {string} config.teammateMode
49
48
  * @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
50
49
  * @returns {string}
51
50
  */
52
51
  export function buildLeadPrompt(taskDescription, config) {
53
- const { agentId, repoRoot, teammateMode = "tmux", workers = [] } = config;
52
+ const { agentId, teammateMode = "tmux", workers = [] } = config;
54
53
 
55
54
  const roster =
56
55
  workers
@@ -59,11 +58,6 @@ export function buildLeadPrompt(taskDescription, config) {
59
58
 
60
59
  const workerIds = workers.map((w) => w.agentId).join(", ");
61
60
 
62
- // TODO: Require repoRoot once all callers pass absolute repository roots.
63
- const bridgePath = repoRoot
64
- ? `node ${String(repoRoot).replace(/\/+$/, "")}/hub/bridge.mjs`
65
- : "node hub/bridge.mjs";
66
-
67
61
  return `리드 에이전트: ${agentId}
68
62
 
69
63
  목표: ${taskDescription}
@@ -74,12 +68,7 @@ ${roster}
74
68
 
75
69
  규칙:
76
70
  - 가능한 짧고 핵심만 지시/요약(토큰 절약)
77
- - 워커 제어:
78
- ${bridgePath} result --agent ${agentId} --topic lead.control
79
- - 워커 결과 수집:
80
- ${bridgePath} context --agent ${agentId} --max 20
81
- - 최종 결과는 topic="task.result"를 모아 통합
82
- - 모든 워커의 task.result 수신 후 결과를 통합하고 종료하라. 워커가 무응답이면 상태를 보고하고 중단하라.
71
+ - 모든 워커의 결과와 출력 파일을 확인하고 통합한다. 무응답 워커는 상태를 보고하고 중단한다.
83
72
  - 증거(커밋/테스트/파일) 없는 완료 주장은 통합하지 말고 해당 워커에 redo 를 지시하라.
84
73
 
85
74
  워커 ID: ${workerIds || "(없음)"}
@@ -92,28 +81,17 @@ ${roster}
92
81
  * @param {object} config
93
82
  * @param {string} config.cli — codex/gemini/claude
94
83
  * @param {string} config.agentId — 에이전트 식별자
95
- * @param {string} config.hubUrl — Hub URL
96
84
  * @returns {string}
97
85
  */
98
86
  export function buildPrompt(subtask, config) {
99
- const { cli, agentId, hubUrl } = config;
100
-
101
- const _hubBase = hubUrl.replace("/mcp", "");
102
-
103
- const bridgePath = "node hub/bridge.mjs";
87
+ const { cli, agentId } = config;
104
88
 
105
89
  return `워커: ${agentId} (${cli})
106
90
  작업: ${subtask}
107
91
 
108
92
  필수 규칙:
109
93
  1) 간결하게 작업(불필요한 장문 설명 금지)
110
- 2) 시작 즉시 등록:
111
- ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
112
- 3) 주기적으로 수신함 확인:
113
- ${bridgePath} context --agent ${agentId} --max 10
114
- 4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
115
- 5) 완료 시 결과 발행:
116
- ${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
94
+ 2) 완료 시 변경 파일, 검증 결과와 출력 파일 경로를 보고한다
117
95
 
118
96
  지금 작업을 시작하라.`;
119
97
  }
@@ -123,7 +101,6 @@ export function buildPrompt(subtask, config) {
123
101
  * @param {string} sessionName — tmux 세션 이름
124
102
  * @param {Array<{target: string, cli: string, subtask: string}>} assignments
125
103
  * @param {object} opts
126
- * @param {string} opts.hubUrl — Hub URL
127
104
  * @param {{target:string, cli:string, task:string}|null} opts.lead
128
105
  * @param {string} opts.teammateMode
129
106
  * @param {(failure: {target:string, cli:string, role:string, message:string}) => void} [opts.onInjectionFailure]
@@ -131,7 +108,6 @@ export function buildPrompt(subtask, config) {
131
108
  */
132
109
  export async function orchestrate(sessionName, assignments, opts = {}) {
133
110
  const {
134
- hubUrl = "http://127.0.0.1:27888/mcp",
135
111
  lead = null,
136
112
  teammateMode = "tmux",
137
113
  injectPrompt = defaultInjectPrompt,
@@ -165,7 +141,6 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
165
141
  const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
166
142
  const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
167
143
  agentId: leadAgentId,
168
- hubUrl,
169
144
  teammateMode,
170
145
  workers: workers.map((w) => ({
171
146
  agentId: w.agentId,
@@ -188,7 +163,6 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
188
163
  const prompt = buildPrompt(worker.subtask, {
189
164
  cli: worker.cli,
190
165
  agentId: worker.agentId,
191
- hubUrl,
192
166
  sessionName,
193
167
  });
194
168
  try {
package/hub/team/pane.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
  import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { buildExecArgs } from "@triflux/core/hub/codex-adapter.mjs";
7
6
  import { psmuxExec } from "./psmux.mjs";
8
7
  import { detectMultiplexer, tmuxExec } from "./session.mjs";
9
8
 
@@ -49,23 +48,13 @@ function muxExec(args, opts = {}) {
49
48
 
50
49
  /**
51
50
  * CLI 에이전트 시작 커맨드 생성
52
- * @param {'codex'|'gemini'|'claude'} cli
53
- * @param {{ trustMode?: boolean }} [options]
51
+ * @param {'codex'|'antigravity'|'claude'} cli
54
52
  * @returns {string} 실행할 셸 커맨드
55
53
  */
56
- export function buildCliCommand(cli, options = {}) {
57
- const { trustMode = false } = options;
58
-
54
+ export function buildCliCommand(cli) {
59
55
  switch (cli) {
60
56
  case "codex":
61
- // trust 모드에서는 exec 서브커맨드 기반 명령을 사용
62
- if (trustMode) {
63
- return buildExecArgs({});
64
- }
65
57
  return "codex --dangerously-bypass-approvals-and-sandbox";
66
- case "gemini":
67
- // interactive 모드 — MCP는 ~/.gemini/settings.json에 사전 등록
68
- return "gemini";
69
58
  case "claude":
70
59
  // interactive 모드
71
60
  return "claude";
@@ -87,17 +76,8 @@ export function startCliInPane(target, command) {
87
76
  /**
88
77
  * psmux `@file` 참조 주입이 가능한 CLI인지 판정한다.
89
78
  *
90
- * Codex TUI는 `@` 입력이 있으면 sync_file_search_popup 경로로 FileSearchPopup을
91
- * 활성화한다 (call-site `codex-rs/tui/src/bottom_pane/chat_composer.rs:3082`;
92
- * 본체 L3271-3313 에서 `ActivePopup::File(popup)` 분기; 빈 query 상태는
93
- * `file_search_popup.rs:54-63` 의 `set_empty_prompt()` 가 정의). 팝업이 떠 있는
94
- * 동안의 Enter 처리는 같은 파일 composer의 Enter handler (`chat_composer.rs:1585-1645`
95
- * 기준) 에서 popup 선택 path insert 또는 dismiss 로 분기하므로 prompt submit 으로
96
- * 전달되지 않는다. Gemini CLI의 `@path`는 공식 client-side file-content inject
97
- * 이므로 유지. Codex TUI에서 `@path` 선택은 path 문자열만 textarea에 insert할
98
- * 뿐 Gemini처럼 파일 내용을 inject하지 않으며, 별도 file-injection slash
99
- * command 도 존재하지 않음 (slash_commands / prompt_args / skill_popup /
100
- * command_popup 소스 전수 grep 결과 0건, 2026-04-19 검증 — absence-based).
79
+ * Codex TUI의 @path는 파일 검색 팝업을 열어 Enter 제출을 가로막는다.
80
+ * Gemini CLI의 파일 내용 주입은 유지한다.
101
81
  *
102
82
  * @param {{ multiplexer: string, useFileRef: boolean, cli: string|null }} args
103
83
  */
@@ -156,9 +136,7 @@ function waitForComposerReady(target) {
156
136
  * capture 불가 또는 재시도 소진을 성공으로 숨기지 않고 오류로 반환한다.
157
137
  */
158
138
  function isAntigravityCli(cli) {
159
- return ["agy", "antigravity", "gemini"].includes(
160
- String(cli || "").toLowerCase(),
161
- );
139
+ return ["agy", "antigravity"].includes(String(cli || "").toLowerCase());
162
140
  }
163
141
 
164
142
  function promptComposerNeedle(prompt) {
@@ -254,7 +232,7 @@ function confirmSubmit(target, prompt, cli, sendEnter) {
254
232
  * @param {string} prompt — 주입할 텍스트
255
233
  * @param {object} [opts]
256
234
  * @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 요청 (psmux 전용). Codex에서는 자동으로 paste-buffer 경로로 fallback.
257
- * @param {'codex'|'gemini'|'claude'|null} [opts.cli] — 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
235
+ * @param {'codex'|'antigravity'|'claude'|null} [opts.cli]: 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
258
236
  */
259
237
  export function injectPrompt(
260
238
  target,
@@ -29,8 +29,8 @@ import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
29
29
  // OS 별 primary multiplexer 를 명시적으로 결정한다.
30
30
  // - PSMUX_BIN env 가 있으면 user override (가장 우선)
31
31
  // - Windows: psmux 가 primary. PATH + 기본 설치 경로에서 탐색.
32
- // - mac/Linux: tmux 가 primary. PATH 에서만 찾는다 (HomeBrew, apt 등 표준 위치).
33
- // silent fallback 은 두지 않는다 — primary 가 없으면 의도된 fail.
32
+ // - mac/Linux: tmux 가 primary. 탐색 없이 PATH 의 tmux 를 쓴다.
33
+ // silent fallback 은 두지 않는다. primary 가 없으면 의도된 fail.
34
34
  const PSMUX_BIN = (() => {
35
35
  if (process.env.PSMUX_BIN) return process.env.PSMUX_BIN;
36
36
 
@@ -58,20 +58,9 @@ const PSMUX_BIN = (() => {
58
58
  return "psmux"; // 최종 — hasPsmux/hasMultiplexer 가 검증
59
59
  }
60
60
 
61
- // mac/Linux primary: tmux (POSIX 표준). silent fallback 아님 — 정식 primary.
62
- try {
63
- childProcess.execFileSync("tmux", ["-V"], {
64
- stdio: "ignore",
65
- timeout: 2000,
66
- });
67
- return "tmux";
68
- } catch {
69
- /* tmux not in PATH — hasMultiplexer 가 false 반환할 것 */
70
- }
61
+ // mac/Linux primary: tmux
71
62
  return "tmux";
72
63
  })();
73
- const GIT_BASH =
74
- process.env.GIT_BASH_PATH || resolveGitBashExecutable() || "bash";
75
64
 
76
65
  /** Windows psmux 세션의 기본 셸을 PowerShell로 강제한다 (pwsh7 우선, ps5 fallback). */
77
66
  const PWSH_BIN = (() => {
@@ -231,10 +220,10 @@ function randomToken(prefix) {
231
220
  }
232
221
 
233
222
  function ensurePsmuxInstalled() {
234
- if (!hasPsmux()) {
223
+ if (!hasMultiplexer()) {
224
+ const mux = IS_WINDOWS ? "psmux" : "tmux";
235
225
  throw new Error(
236
- "psmux가 설치되어 있지 않습니다.\n\n" +
237
- "psmux는 Codex/Antigravity CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
226
+ `${mux}가 설치되어 있지 않습니다.\n\n` +
238
227
  "설치 방법 (택 1):\n" +
239
228
  `${formatPsmuxInstallGuidance(" ")}\n\n` +
240
229
  "설치 후 터미널을 재시작하세요.",
@@ -539,13 +528,6 @@ function disablePipeCapture(paneId) {
539
528
  }
540
529
  }
541
530
 
542
- function sendLiteralToPane(paneId, text, submit = true) {
543
- psmuxExec(["send-keys", "-t", paneId, "-l", text]);
544
- if (submit) {
545
- psmuxExec(["send-keys", "-t", paneId, "Enter"]);
546
- }
547
- }
548
-
549
531
  export function sendKeysToPane(paneId, text, submit = true) {
550
532
  psmuxExec(["send-keys", "-t", paneId, "-l", text]);
551
533
  if (submit) {
@@ -563,10 +545,6 @@ function toPatternRegExp(pattern) {
563
545
  return new RegExp(String(pattern), "m");
564
546
  }
565
547
 
566
- function escapeRegExp(value) {
567
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
568
- }
569
-
570
548
  function psmux(args, opts = {}) {
571
549
  const normalizedArgs = normalizePsmuxArgs(args);
572
550
  // PSMUX_SESSION 제거 — 기존 psmux 세션 내에서 호출 시 중첩 세션 차단 방지
@@ -614,7 +592,7 @@ export function hasMultiplexer() {
614
592
 
615
593
  /**
616
594
  * backward-compat alias — historical 이름 `hasPsmux` 는 OS 별 primary 의 의미를
617
- * 가린다. 외부 caller (scripts/remote-spawn.mjs, scripts/session-spawn-helper.mjs,
595
+ * 가린다. 외부 caller (scripts/remote-spawn.mjs,
618
596
  * tests) 호환을 위해 alias 로 보존. 신규 코드는 hasMultiplexer 사용.
619
597
  */
620
598
  export const hasPsmux = hasMultiplexer;
@@ -759,7 +737,7 @@ export function createPsmuxSession(sessionName, opts = {}) {
759
737
  "$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
760
738
  ].join("; ");
761
739
  panes.forEach((paneId) => {
762
- sendLiteralToPane(paneId, encodingInit, true);
740
+ sendKeysToPane(paneId, encodingInit, true);
763
741
  });
764
742
  }
765
743
 
@@ -1048,22 +1026,6 @@ function detachAttachedClients(sessionName, waitMs = 750) {
1048
1026
  }
1049
1027
  }
1050
1028
 
1051
- function findFallbackPane(sessionName, excludedPaneId) {
1052
- try {
1053
- const panes = listPaneDetails(sessionName).filter(
1054
- (pane) => pane.paneId !== excludedPaneId && !pane.isDead,
1055
- );
1056
- if (panes.length === 0) return null;
1057
- return (
1058
- panes.find((pane) => pane.title === "lead") ||
1059
- panes.find((pane) => pane.paneIndex === 0) ||
1060
- panes[0]
1061
- );
1062
- } catch {
1063
- return null;
1064
- }
1065
- }
1066
-
1067
1029
  /**
1068
1030
  * psmux 세션 종료
1069
1031
  * 순서: pipe-pane 해제 → pane 프로세스 트리 정리 → 세션 종료 → 고아 정리
@@ -1179,7 +1141,7 @@ export function pruneStale(opts = {}) {
1179
1141
  const result = killSessionByTitle(
1180
1142
  new RegExp(
1181
1143
  `^(?:${sessions
1182
- .map((session) => escapeRegExp(session.title))
1144
+ .map((session) => escapeRegex(session.title))
1183
1145
  .join("|")})$`,
1184
1146
  ),
1185
1147
  );
@@ -1496,7 +1458,7 @@ export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
1496
1458
  wrapped = `{ ${safeCommand}; __ec=$?; echo "${COMPLETION_PREFIX}${token}:$__ec"; }`;
1497
1459
  }
1498
1460
 
1499
- sendLiteralToPane(pane.paneId, wrapped, true);
1461
+ sendKeysToPane(pane.paneId, wrapped, true);
1500
1462
 
1501
1463
  return { paneId: pane.paneId, paneName, token, logPath };
1502
1464
  }
@@ -1661,7 +1623,6 @@ export async function waitForPattern(
1661
1623
 
1662
1624
  /**
1663
1625
  * 완료 토큰이 찍힐 때까지 대기하고 exit code를 파싱한다.
1664
- * NOTE: 주 채널은 headless.waitForCompletionWithStallDetect이며, 본 함수는 fallback 채널이다.
1665
1626
  * @param {string} sessionName
1666
1627
  * @param {string} paneNameOrTarget
1667
1628
  * @param {string} token
@@ -1677,7 +1638,7 @@ export async function waitForCompletion(
1677
1638
  opts = {},
1678
1639
  ) {
1679
1640
  const completionRegex = new RegExp(
1680
- `${escapeRegExp(COMPLETION_PREFIX)}${escapeRegExp(token)}:(\\d+)`,
1641
+ `${escapeRegex(COMPLETION_PREFIX)}${escapeRegex(token)}:(\\d+)`,
1681
1642
  "m",
1682
1643
  );
1683
1644
  const result = await waitForPattern(
@@ -1726,211 +1687,10 @@ export async function waitForCompletion(
1726
1687
  };
1727
1688
  }
1728
1689
 
1729
- // ─── 하이브리드 모드 워커 관리 함수 ───
1730
-
1731
- /**
1732
- * psmux 세션의 새 pane에서 워커 실행
1733
- * @param {string} sessionName - 대상 psmux 세션 이름
1734
- * @param {string} workerName - 워커 식별용 pane 타이틀
1735
- * @param {string} cmd - 실행할 커맨드
1736
- * @returns {{ paneId: string, workerName: string }}
1737
- */
1738
- export function spawnWorker(sessionName, workerName, cmd) {
1739
- if (!hasPsmux()) {
1740
- throw new Error(
1741
- "psmux가 설치되어 있지 않습니다.\n" +
1742
- `설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1743
- );
1744
- }
1745
-
1746
- // remain-on-exit: 종료된 pane이 즉시 사라지는 것 방지
1747
- try {
1748
- psmuxExec(["set-option", "-t", sessionName, "remain-on-exit", "on"]);
1749
- } catch {
1750
- // 미지원 시 무시
1751
- }
1752
-
1753
- // Windows: pane 기본셸이 PowerShell → Git Bash로 래핑
1754
- // psmux가 이스케이프 시퀀스를 처리하므로 포워드 슬래시 경로를 사용한다.
1755
- const shellCmd = IS_WINDOWS
1756
- ? `& '${GIT_BASH.replace(/\\/g, "/")}' -l -c '${cmd.replace(/'/g, "'\\''")}'`
1757
- : cmd;
1758
-
1759
- try {
1760
- const paneTarget = psmuxExec([
1761
- "split-window",
1762
- "-t",
1763
- sessionName,
1764
- "-P",
1765
- "-F",
1766
- "#{session_name}:#{window_index}.#{pane_index}",
1767
- shellCmd,
1768
- ]);
1769
- try {
1770
- psmuxExec(["select-pane", "-t", paneTarget, "-T", workerName]);
1771
- } catch {
1772
- // tmux 2.6 미만: select-pane -T 미지원 — 무시
1773
- }
1774
- return { paneId: paneTarget, workerName };
1775
- } catch (err) {
1776
- throw new Error(
1777
- `워커 생성 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1778
- );
1779
- }
1780
- }
1781
-
1782
- /**
1783
- * 워커 pane 실행 상태 확인
1784
- * @param {string} sessionName - 대상 psmux 세션 이름
1785
- * @param {string} workerName - 워커 pane 타이틀
1786
- * @returns {{ status: "running"|"exited", exitCode: number|null, paneId: string }}
1787
- */
1788
- export function getWorkerStatus(sessionName, workerName) {
1789
- if (!hasPsmux()) {
1790
- throw new Error(
1791
- `psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1792
- );
1793
- }
1794
- try {
1795
- const pane = resolvePane(sessionName, workerName);
1796
- return {
1797
- status: pane.isDead ? "exited" : "running",
1798
- exitCode: pane.isDead ? pane.exitCode : null,
1799
- paneId: pane.paneId,
1800
- };
1801
- } catch (err) {
1802
- if (err.message.includes("Pane을 찾을 수 없습니다")) {
1803
- throw new Error(`워커를 찾을 수 없습니다: ${workerName}`);
1804
- }
1805
- throw new Error(
1806
- `워커 상태 조회 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1807
- );
1808
- }
1809
- }
1810
-
1811
- /**
1812
- * 워커 pane 프로세스 강제 종료
1813
- * @param {string} sessionName - 대상 psmux 세션 이름
1814
- * @param {string} workerName - 워커 pane 타이틀
1815
- * @returns {{ killed: boolean }}
1816
- */
1817
- export function killWorker(sessionName, workerName) {
1818
- if (!hasPsmux()) {
1819
- throw new Error(
1820
- `psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1821
- );
1822
- }
1823
- try {
1824
- const { paneId, status } = getWorkerStatus(sessionName, workerName);
1825
- const attachedCount = getPsmuxSessionAttachedCount(sessionName);
1826
- const fallbackPane =
1827
- attachedCount > 0 ? findFallbackPane(sessionName, paneId) : null;
1828
-
1829
- if (fallbackPane?.paneId) {
1830
- try {
1831
- psmuxExec(["select-pane", "-t", fallbackPane.paneId]);
1832
- } catch {
1833
- // focus 회복 best-effort
1834
- }
1835
- }
1836
-
1837
- // pipe-pane 캡처 해제 — reader 프로세스 정상 종료 유도
1838
- disablePipeCapture(paneId);
1839
-
1840
- // pane PID 수집 → 프로세스 트리 정리 (MCP 서버 좀비 방지)
1841
- try {
1842
- const pidOutput = psmuxExec([
1843
- "list-panes",
1844
- "-t",
1845
- paneId,
1846
- "-F",
1847
- "#{pane_pid}",
1848
- ]);
1849
- const pid = Number.parseInt(pidOutput.trim(), 10);
1850
- if (Number.isFinite(pid) && pid > 0) killProcessTree(pid);
1851
- } catch {
1852
- // PID 조회 실패 — 아래에서 pane만 정리
1853
- }
1854
-
1855
- // 이미 종료된 워커 → pane 정리만 수행
1856
- if (status === "exited") {
1857
- try {
1858
- psmuxExec(["kill-pane", "-t", paneId]);
1859
- } catch {
1860
- // 무시
1861
- }
1862
- return { killed: true };
1863
- }
1864
-
1865
- // running → C-c 우아한 종료 시도
1866
- try {
1867
- psmuxExec(["send-keys", "-t", paneId, "C-c"]);
1868
- } catch {
1869
- // send-keys 실패 무시
1870
- }
1871
-
1872
- if (!fallbackPane) {
1873
- try {
1874
- psmuxExec(["send-keys", "-t", paneId, "exit", "Enter"]);
1875
- } catch {
1876
- // send-keys 실패 무시
1877
- }
1878
- }
1879
-
1880
- sleepMs(2000);
1881
-
1882
- try {
1883
- psmuxExec(["kill-pane", "-t", paneId]);
1884
- } catch {
1885
- // 이미 종료된 pane — 무시
1886
- }
1887
-
1888
- if (fallbackPane?.paneId) {
1889
- try {
1890
- psmuxExec(["select-pane", "-t", fallbackPane.paneId]);
1891
- } catch {
1892
- // pane 정리 후 focus 재선택 best-effort
1893
- }
1894
- }
1895
- return { killed: true };
1896
- } catch (err) {
1897
- if (err.message.includes("워커를 찾을 수 없습니다")) {
1898
- return { killed: true };
1899
- }
1900
- throw new Error(
1901
- `워커 종료 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1902
- );
1903
- }
1904
- }
1905
-
1906
- /**
1907
- * 워커 pane 출력 마지막 N줄 캡처
1908
- * @param {string} sessionName - 대상 psmux 세션 이름
1909
- * @param {string} workerName - 워커 pane 타이틀
1910
- * @param {number} lines - 캡처할 줄 수 (기본 50)
1911
- * @returns {string} 캡처된 출력
1912
- */
1913
- export function captureWorkerOutput(sessionName, workerName, lines = 50) {
1914
- if (!hasPsmux()) {
1915
- throw new Error(
1916
- `psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}`,
1917
- );
1918
- }
1919
- try {
1920
- const { paneId } = getWorkerStatus(sessionName, workerName);
1921
- return psmuxExec(["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`]);
1922
- } catch (err) {
1923
- if (err.message.includes("워커를 찾을 수 없습니다")) throw err;
1924
- throw new Error(
1925
- `출력 캡처 실패 (session=${sessionName}, worker=${workerName}): ${err.message}`,
1926
- );
1927
- }
1928
- }
1929
-
1930
1690
  // ─── CLI 진입점 ───
1931
1691
 
1932
1692
  if (process.argv[1]?.endsWith("psmux.mjs")) {
1933
- (async () => {
1693
+ (() => {
1934
1694
  const rawArgs = process.argv.slice(2);
1935
1695
  const internalMode = rawArgs[0] === "--internal";
1936
1696
  const cmd = internalMode ? rawArgs[1] : rawArgs[0];
@@ -2007,157 +1767,10 @@ if (process.argv[1]?.endsWith("psmux.mjs")) {
2007
1767
  return;
2008
1768
  }
2009
1769
 
2010
- switch (cmd) {
2011
- case "spawn": {
2012
- const session = getArg("session");
2013
- const name = getArg("name");
2014
- const workerCmd = getArg("cmd");
2015
- if (!session || !name || !workerCmd) {
2016
- console.error(
2017
- "사용법: node psmux.mjs spawn --session <세션> --name <워커명> --cmd <커맨드>",
2018
- );
2019
- process.exit(1);
2020
- }
2021
- console.log(
2022
- JSON.stringify(spawnWorker(session, name, workerCmd), null, 2),
2023
- );
2024
- break;
2025
- }
2026
- case "status": {
2027
- const session = getArg("session");
2028
- const name = getArg("name");
2029
- if (!session || !name) {
2030
- console.error(
2031
- "사용법: node psmux.mjs status --session <세션> --name <워커명>",
2032
- );
2033
- process.exit(1);
2034
- }
2035
- console.log(JSON.stringify(getWorkerStatus(session, name), null, 2));
2036
- break;
2037
- }
2038
- case "kill": {
2039
- const session = getArg("session");
2040
- const name = getArg("name");
2041
- if (!session || !name) {
2042
- console.error(
2043
- "사용법: node psmux.mjs kill --session <세션> --name <워커명>",
2044
- );
2045
- process.exit(1);
2046
- }
2047
- console.log(JSON.stringify(killWorker(session, name), null, 2));
2048
- break;
2049
- }
2050
- case "output": {
2051
- const session = getArg("session");
2052
- const name = getArg("name");
2053
- const lines = parseInt(getArg("lines") || "50", 10);
2054
- if (!session || !name) {
2055
- console.error(
2056
- "사용법: node psmux.mjs output --session <세션> --name <워커명> [--lines <줄수>]",
2057
- );
2058
- process.exit(1);
2059
- }
2060
- console.log(captureWorkerOutput(session, name, lines));
2061
- break;
2062
- }
2063
- case "capture-start": {
2064
- const session = getArg("session");
2065
- const name = getArg("name");
2066
- if (!session || !name) {
2067
- console.error(
2068
- "사용법: node psmux.mjs capture-start --session <세션> --name <pane>",
2069
- );
2070
- process.exit(1);
2071
- }
2072
- console.log(JSON.stringify(startCapture(session, name), null, 2));
2073
- break;
2074
- }
2075
- case "dispatch": {
2076
- const session = getArg("session");
2077
- const name = getArg("name");
2078
- const commandText = getArg("command");
2079
- if (!session || !name || !commandText) {
2080
- console.error(
2081
- "사용법: node psmux.mjs dispatch --session <세션> --name <pane> --command <PowerShell 명령>",
2082
- );
2083
- process.exit(1);
2084
- }
2085
- console.log(
2086
- JSON.stringify(
2087
- dispatchCommand(session, name, commandText),
2088
- null,
2089
- 2,
2090
- ),
2091
- );
2092
- break;
2093
- }
2094
- case "wait-pattern": {
2095
- const session = getArg("session");
2096
- const name = getArg("name");
2097
- const pattern = getArg("pattern");
2098
- const timeoutSec = parseInt(getArg("timeout") || "300", 10);
2099
- if (!session || !name || !pattern) {
2100
- console.error(
2101
- "사용법: node psmux.mjs wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]",
2102
- );
2103
- process.exit(1);
2104
- }
2105
- const result = await waitForPattern(
2106
- session,
2107
- name,
2108
- pattern,
2109
- timeoutSec,
2110
- );
2111
- console.log(JSON.stringify(result, null, 2));
2112
- if (!result.matched) process.exit(2);
2113
- break;
2114
- }
2115
- case "wait-completion": {
2116
- const session = getArg("session");
2117
- const name = getArg("name");
2118
- const token = getArg("token");
2119
- const timeoutSec = parseInt(getArg("timeout") || "300", 10);
2120
- if (!session || !name || !token) {
2121
- console.error(
2122
- "사용법: node psmux.mjs wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]",
2123
- );
2124
- process.exit(1);
2125
- }
2126
- const result = await waitForCompletion(
2127
- session,
2128
- name,
2129
- token,
2130
- timeoutSec,
2131
- );
2132
- console.log(JSON.stringify(result, null, 2));
2133
- if (!result.matched) process.exit(2);
2134
- break;
2135
- }
2136
- default:
2137
- console.error(
2138
- "사용법: node psmux.mjs spawn|status|kill|output|capture-start|dispatch|wait-pattern|wait-completion [args]",
2139
- );
2140
- console.error("");
2141
- console.error(
2142
- " spawn --session <세션> --name <워커명> --cmd <커맨드>",
2143
- );
2144
- console.error(" status --session <세션> --name <워커명>");
2145
- console.error(" kill --session <세션> --name <워커명>");
2146
- console.error(
2147
- " output --session <세션> --name <워커명> [--lines <줄수>]",
2148
- );
2149
- console.error(" capture-start --session <세션> --name <pane>");
2150
- console.error(
2151
- " dispatch --session <세션> --name <pane> --command <PowerShell 명령>",
2152
- );
2153
- console.error(
2154
- " wait-pattern --session <세션> --name <pane> --pattern <정규식> [--timeout <초>]",
2155
- );
2156
- console.error(
2157
- " wait-completion --session <세션> --name <pane> --token <토큰> [--timeout <초>]",
2158
- );
2159
- process.exit(1);
2160
- }
1770
+ console.error(
1771
+ "사용법: node psmux.mjs --internal list|kill-by-title|prune-stale [args]",
1772
+ );
1773
+ process.exit(1);
2161
1774
  } catch (err) {
2162
1775
  console.error(`오류: ${err.message}`);
2163
1776
  process.exit(1);