@triflux/remote 10.1.0 → 10.25.1

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 (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2165 -708
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +125 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +44 -16
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +20 -118
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1020 -279
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +68 -52
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +61 -33
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1522 -193
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +18 -34
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -44
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +927 -437
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +517 -63
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +662 -335
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. package/scripts/lib/toml.mjs +113 -0
@@ -1,161 +1,173 @@
1
- // hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
2
- // 의존성: pane.mjs만 사용
3
- import { injectPrompt } from "./pane.mjs";
4
-
5
- /**
6
- * 작업 분해 (LLM 없이 구분자 기반)
7
- * @param {string} taskDescription — 전체 작업 설명
8
- * @param {number} agentCount — 에이전트 수
9
- * @returns {string[]} 각 에이전트의 서브태스크
10
- */
11
- export function decomposeTask(taskDescription, agentCount) {
12
- if (agentCount <= 0) return [];
13
- if (agentCount === 1) return [taskDescription];
14
-
15
- // '+', ',', '\n' 기준으로 분리
16
- const parts = taskDescription
17
- .split(/[+,\n]+/)
18
- .map((s) => s.trim())
19
- .filter(Boolean);
20
-
21
- if (parts.length === 0) return [taskDescription];
22
-
23
- // 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
24
- if (parts.length < agentCount) {
25
- const result = [...parts];
26
- while (result.length < agentCount) {
27
- result.push(taskDescription);
28
- }
29
- return result;
30
- }
31
-
32
- // 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
33
- if (parts.length > agentCount) {
34
- const result = parts.slice(0, agentCount - 1);
35
- result.push(parts.slice(agentCount - 1).join(" + "));
36
- return result;
37
- }
38
-
39
- return parts;
40
- }
41
-
42
- /**
43
- * 리드(보통 claude) 초기 프롬프트 생성
44
- * @param {string} taskDescription
45
- * @param {object} config
46
- * @param {string} config.agentId
47
- * @param {string} config.hubUrl
48
- * @param {string} config.teammateMode
49
- * @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
50
- * @returns {string}
51
- */
52
- export function buildLeadPrompt(taskDescription, config) {
53
- const { agentId, teammateMode = "tmux", workers = [] } = config;
54
-
55
- const roster = workers
56
- .map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
57
- .join("\n") || "- (워커 없음)";
58
-
59
- const workerIds = workers.map((w) => w.agentId).join(", ");
60
-
61
- const bridgePath = "node hub/bridge.mjs";
62
-
63
- return `리드 에이전트: ${agentId}
64
-
65
- 목표: ${taskDescription}
66
- 모드: ${teammateMode}
67
-
68
- 워커:
69
- ${roster}
70
-
71
- 규칙:
72
- - 가능한 짧고 핵심만 지시/요약(토큰 절약)
73
- - 워커 제어:
74
- ${bridgePath} result --agent ${agentId} --topic lead.control
75
- - 워커 결과 수집:
76
- ${bridgePath} context --agent ${agentId} --max 20
77
- - 최종 결과는 topic="task.result"를 모아 통합
78
-
79
- 워커 ID: ${workerIds || "(없음)"}
80
- 지금 즉시 워커를 배정하고 병렬 진행을 관리하라.`;
81
- }
82
-
83
- /**
84
- * 워커 초기 프롬프트 생성
85
- * @param {string} subtask — 이 에이전트의 서브태스크
86
- * @param {object} config
87
- * @param {string} config.cli — codex/gemini/claude
88
- * @param {string} config.agentId — 에이전트 식별자
89
- * @param {string} config.hubUrl — Hub URL
90
- * @returns {string}
91
- */
92
- export function buildPrompt(subtask, config) {
93
- const { cli, agentId, hubUrl } = config;
94
-
95
- const _hubBase = hubUrl.replace("/mcp", "");
96
-
97
- const bridgePath = "node hub/bridge.mjs";
98
-
99
- return `워커: ${agentId} (${cli})
100
- 작업: ${subtask}
101
-
102
- 필수 규칙:
103
- 1) 간결하게 작업(불필요한 장문 설명 금지)
104
- 2) 시작 즉시 등록:
105
- ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
106
- 3) 주기적으로 수신함 확인:
107
- ${bridgePath} context --agent ${agentId} --max 10
108
- 4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
109
- 5) 완료 시 결과 발행:
110
- ${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
111
-
112
- 지금 작업을 시작하라.`;
113
- }
114
-
115
- /**
116
- * 팀 오케스트레이션 실행 — 각 pane에 프롬프트 주입
117
- * @param {string} sessionName — tmux 세션 이름
118
- * @param {Array<{target: string, cli: string, subtask: string}>} assignments
119
- * @param {object} opts
120
- * @param {string} opts.hubUrl — Hub URL
121
- * @param {{target:string, cli:string, task:string}|null} opts.lead
122
- * @param {string} opts.teammateMode
123
- * @returns {Promise<void>}
124
- */
125
- export async function orchestrate(sessionName, assignments, opts = {}) {
126
- const {
127
- hubUrl = "http://127.0.0.1:27888/mcp",
128
- lead = null,
129
- teammateMode = "tmux",
130
- } = opts;
131
-
132
- const workers = assignments.map(({ target, cli, subtask }) => ({
133
- target,
134
- cli,
135
- subtask,
136
- agentId: `${cli}-${target.split(".").pop()}`,
137
- }));
138
-
139
- if (lead?.target) {
140
- const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
141
- const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
142
- agentId: leadAgentId,
143
- hubUrl,
144
- teammateMode,
145
- workers: workers.map((w) => ({ agentId: w.agentId, cli: w.cli, subtask: w.subtask })),
146
- });
147
- injectPrompt(lead.target, leadPrompt, { useFileRef: true });
148
- await new Promise((r) => setTimeout(r, 100));
149
- }
150
-
151
- for (const worker of workers) {
152
- const prompt = buildPrompt(worker.subtask, {
153
- cli: worker.cli,
154
- agentId: worker.agentId,
155
- hubUrl,
156
- sessionName,
157
- });
158
- injectPrompt(worker.target, prompt, { useFileRef: true });
159
- await new Promise((r) => setTimeout(r, 100));
160
- }
161
- }
1
+ // hub/team/orchestrator.mjs — 작업 분배 + 프롬프트 구성
2
+ // 의존성: pane.mjs만 사용
3
+ import { injectPrompt as defaultInjectPrompt } from "./pane.mjs";
4
+
5
+ /**
6
+ * 작업 분해 (LLM 없이 구분자 기반)
7
+ * @param {string} taskDescription — 전체 작업 설명
8
+ * @param {number} agentCount — 에이전트 수
9
+ * @returns {string[]} 각 에이전트의 서브태스크
10
+ */
11
+ export function decomposeTask(taskDescription, agentCount) {
12
+ if (agentCount <= 0) return [];
13
+ if (agentCount === 1) return [taskDescription];
14
+
15
+ // '+', ',', '\n' 기준으로 분리
16
+ const parts = taskDescription
17
+ .split(/[+,\n]+/)
18
+ .map((s) => s.trim())
19
+ .filter(Boolean);
20
+
21
+ if (parts.length === 0) return [taskDescription];
22
+
23
+ // 에이전트보다 서브태스크가 적으면 마지막 에이전트에 전체 태스크 부여
24
+ if (parts.length < agentCount) {
25
+ const result = [...parts];
26
+ while (result.length < agentCount) {
27
+ result.push(taskDescription);
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // 에이전트보다 서브태스크가 많으면 앞에서부터 N개, 나머지는 마지막에 합침
33
+ if (parts.length > agentCount) {
34
+ const result = parts.slice(0, agentCount - 1);
35
+ result.push(parts.slice(agentCount - 1).join(" + "));
36
+ return result;
37
+ }
38
+
39
+ return parts;
40
+ }
41
+
42
+ /**
43
+ * 리드(보통 claude) 초기 프롬프트 생성
44
+ * @param {string} taskDescription
45
+ * @param {object} config
46
+ * @param {string} config.agentId
47
+ * @param {string} config.hubUrl
48
+ * @param {string} config.teammateMode
49
+ * @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
50
+ * @returns {string}
51
+ */
52
+ export function buildLeadPrompt(taskDescription, config) {
53
+ const { agentId, teammateMode = "tmux", workers = [] } = config;
54
+
55
+ const roster =
56
+ workers
57
+ .map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
58
+ .join("\n") || "- (워커 없음)";
59
+
60
+ const workerIds = workers.map((w) => w.agentId).join(", ");
61
+
62
+ const bridgePath = "node hub/bridge.mjs";
63
+
64
+ return `리드 에이전트: ${agentId}
65
+
66
+ 목표: ${taskDescription}
67
+ 모드: ${teammateMode}
68
+
69
+ 워커:
70
+ ${roster}
71
+
72
+ 규칙:
73
+ - 가능한 짧고 핵심만 지시/요약(토큰 절약)
74
+ - 워커 제어:
75
+ ${bridgePath} result --agent ${agentId} --topic lead.control
76
+ - 워커 결과 수집:
77
+ ${bridgePath} context --agent ${agentId} --max 20
78
+ - 최종 결과는 topic="task.result"를 모아 통합
79
+
80
+ 워커 ID: ${workerIds || "(없음)"}
81
+ 지금 즉시 워커를 배정하고 병렬 진행을 관리하라.`;
82
+ }
83
+
84
+ /**
85
+ * 워커 초기 프롬프트 생성
86
+ * @param {string} subtask — 이 에이전트의 서브태스크
87
+ * @param {object} config
88
+ * @param {string} config.cli — codex/gemini/claude
89
+ * @param {string} config.agentId — 에이전트 식별자
90
+ * @param {string} config.hubUrl — Hub URL
91
+ * @returns {string}
92
+ */
93
+ export function buildPrompt(subtask, config) {
94
+ const { cli, agentId, hubUrl } = config;
95
+
96
+ const _hubBase = hubUrl.replace("/mcp", "");
97
+
98
+ const bridgePath = "node hub/bridge.mjs";
99
+
100
+ return `워커: ${agentId} (${cli})
101
+ 작업: ${subtask}
102
+
103
+ 필수 규칙:
104
+ 1) 간결하게 작업(불필요한 장문 설명 금지)
105
+ 2) 시작 즉시 등록:
106
+ ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
107
+ 3) 주기적으로 수신함 확인:
108
+ ${bridgePath} context --agent ${agentId} --max 10
109
+ 4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
110
+ 5) 완료 시 결과 발행:
111
+ ${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
112
+
113
+ 지금 작업을 시작하라.`;
114
+ }
115
+
116
+ /**
117
+ * 팀 오케스트레이션 실행 — 각 pane에 프롬프트 주입
118
+ * @param {string} sessionName — tmux 세션 이름
119
+ * @param {Array<{target: string, cli: string, subtask: string}>} assignments
120
+ * @param {object} opts
121
+ * @param {string} opts.hubUrl — Hub URL
122
+ * @param {{target:string, cli:string, task:string}|null} opts.lead
123
+ * @param {string} opts.teammateMode
124
+ * @returns {Promise<void>}
125
+ */
126
+ export async function orchestrate(sessionName, assignments, opts = {}) {
127
+ const {
128
+ hubUrl = "http://127.0.0.1:27888/mcp",
129
+ lead = null,
130
+ teammateMode = "tmux",
131
+ injectPrompt = defaultInjectPrompt,
132
+ } = opts;
133
+
134
+ const workers = assignments.map(({ target, cli, subtask }) => ({
135
+ target,
136
+ cli,
137
+ subtask,
138
+ agentId: `${cli}-${target.split(".").pop()}`,
139
+ }));
140
+
141
+ if (lead?.target) {
142
+ const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
143
+ const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
144
+ agentId: leadAgentId,
145
+ hubUrl,
146
+ teammateMode,
147
+ workers: workers.map((w) => ({
148
+ agentId: w.agentId,
149
+ cli: w.cli,
150
+ subtask: w.subtask,
151
+ })),
152
+ });
153
+ injectPrompt(lead.target, leadPrompt, {
154
+ useFileRef: true,
155
+ cli: lead.cli,
156
+ });
157
+ await new Promise((r) => setTimeout(r, 100));
158
+ }
159
+
160
+ for (const worker of workers) {
161
+ const prompt = buildPrompt(worker.subtask, {
162
+ cli: worker.cli,
163
+ agentId: worker.agentId,
164
+ hubUrl,
165
+ sessionName,
166
+ });
167
+ injectPrompt(worker.target, prompt, {
168
+ useFileRef: true,
169
+ cli: worker.cli,
170
+ });
171
+ await new Promise((r) => setTimeout(r, 100));
172
+ }
173
+ }
package/hub/team/pane.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  // hub/team/pane.mjs — pane별 CLI 실행 + stdin 주입
2
2
  // 의존성: child_process, fs, os, path (Node.js 내장)만 사용
3
- import { writeFileSync, unlinkSync, mkdirSync } from "node:fs";
4
- import { join } from "node:path";
3
+ import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
5
4
  import { tmpdir } from "node:os";
6
- import { detectMultiplexer, tmuxExec } from "./session.mjs";
7
- import { psmuxExec } from "./psmux.mjs";
8
-
5
+ import { join } from "node:path";
9
6
  import { buildExecArgs } from "@triflux/core/hub/codex-adapter.mjs";
7
+ import { psmuxExec } from "./psmux.mjs";
8
+ import { detectMultiplexer, tmuxExec } from "./session.mjs";
10
9
 
11
10
  function quoteArg(value) {
12
11
  return `"${String(value).replace(/"/g, '\\"')}"`;
@@ -91,34 +90,61 @@ export function startCliInPane(target, command) {
91
90
  }
92
91
 
93
92
  /**
94
- * pane에 프롬프트 주입 (load-buffer + paste-buffer 방식)
95
- * 멀티라인 + 특수문자 안전, 크기 제한 없음
96
- * @param {string} target — 예: tfx-multi-abc:0.1
97
- * @param {string} prompt — 주입할 텍스트
93
+ * psmux `@file` 참조 주입이 가능한 CLI인지 판정한다.
94
+ *
95
+ * Codex TUI는 `@` 입력이 있으면 sync_file_search_popup 경로로 FileSearchPopup을
96
+ * 활성화한다 (call-site `codex-rs/tui/src/bottom_pane/chat_composer.rs:3082`;
97
+ * 본체 L3271-3313 에서 `ActivePopup::File(popup)` 분기; 빈 query 상태는
98
+ * `file_search_popup.rs:54-63` 의 `set_empty_prompt()` 가 정의). 팝업이 떠 있는
99
+ * 동안의 Enter 처리는 같은 파일 composer의 Enter handler (`chat_composer.rs:1585-1645`
100
+ * 기준) 에서 popup 선택 path insert 또는 dismiss 로 분기하므로 prompt submit 으로
101
+ * 전달되지 않는다. Gemini CLI의 `@path`는 공식 client-side file-content inject
102
+ * 이므로 유지. Codex TUI에서 `@path` 선택은 path 문자열만 textarea에 insert할
103
+ * 뿐 Gemini처럼 파일 내용을 inject하지 않으며, 별도 file-injection slash
104
+ * command 도 존재하지 않음 (slash_commands / prompt_args / skill_popup /
105
+ * command_popup 소스 전수 grep 결과 0건, 2026-04-19 검증 — absence-based).
106
+ *
107
+ * @param {{ multiplexer: string, useFileRef: boolean, cli: string|null }} args
98
108
  */
109
+ export function shouldUseFileRef({ multiplexer, useFileRef, cli }) {
110
+ return multiplexer === "psmux" && useFileRef && cli !== "codex";
111
+ }
112
+
99
113
  /**
100
114
  * pane에 프롬프트 주입
101
115
  * @param {string} target — 예: tfx-multi-abc:0.1
102
116
  * @param {string} prompt — 주입할 텍스트
103
117
  * @param {object} [opts]
104
- * @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 (psmux 전용)
118
+ * @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 요청 (psmux 전용). Codex에서는 자동으로 paste-buffer 경로로 fallback.
119
+ * @param {'codex'|'gemini'|'claude'|null} [opts.cli] — 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
105
120
  */
106
- export function injectPrompt(target, prompt, { useFileRef = false } = {}) {
121
+ export function injectPrompt(
122
+ target,
123
+ prompt,
124
+ { useFileRef = false, cli = null } = {},
125
+ ) {
107
126
  const tmpDir = join(tmpdir(), "tfx-multi");
108
127
  mkdirSync(tmpDir, { recursive: true });
109
128
 
110
129
  const safeTarget = target.replace(/[:.]/g, "-");
111
130
  const tmpFile = join(tmpDir, `prompt-${safeTarget}-${Date.now()}.txt`);
112
131
 
113
- // psmux + TUI 앱: @file 참조로 주입 (paste-buffer는 TUI와 호환 안 됨)
114
- if (detectMultiplexer() === "psmux" && useFileRef) {
132
+ const multiplexer = detectMultiplexer();
133
+
134
+ // psmux + TUI + CLI별 @file 지원: @path를 literal paste하고 Enter로 확정.
135
+ // Codex는 @가 file search popup을 intercept하므로 paste-buffer 경로로 fallback.
136
+ if (shouldUseFileRef({ multiplexer, useFileRef, cli })) {
115
137
  writeFileSync(tmpFile, prompt, "utf8");
116
138
  const filePath = tmpFile.replace(/\\/g, "/");
117
139
  psmuxExec(["select-pane", "-t", target]);
118
140
  psmuxExec(["send-keys", "-t", target, "-l", `@${filePath}`]);
119
141
  psmuxExec(["send-keys", "-t", target, "Enter"]);
120
142
  // TUI가 파일을 읽을 시간을 주고 정리
121
- setTimeout(() => { try { unlinkSync(tmpFile); } catch {} }, 10000);
143
+ setTimeout(() => {
144
+ try {
145
+ unlinkSync(tmpFile);
146
+ } catch {}
147
+ }, 10000);
122
148
  return;
123
149
  }
124
150
 
@@ -139,7 +165,9 @@ export function injectPrompt(target, prompt, { useFileRef = false } = {}) {
139
165
  muxExec(`paste-buffer -t ${target}`);
140
166
  muxExec(`send-keys -t ${target} Enter`);
141
167
  } finally {
142
- try { unlinkSync(tmpFile); } catch {}
168
+ try {
169
+ unlinkSync(tmpFile);
170
+ } catch {}
143
171
  }
144
172
  }
145
173
 
@@ -1,7 +1,7 @@
1
1
  // hub/team/process-cleanup.mjs — 고아 node/python 프로세스 감지 및 정리
2
2
  // Windows: Get-CimInstance Win32_Process로 parent PID + cmdLine 접근
3
3
  // Unix: ps aux 파싱
4
- import { execFile as nodeExecFile } from "node:child_process";
4
+ import { execFileSync, execFile as nodeExecFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
6
6
  import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
7
7
 
@@ -12,11 +12,37 @@ const execFileAsync = promisify(nodeExecFile);
12
12
  const TARGET_PROCESS_NAMES = ["node", "python", "python3"];
13
13
  const SIGTERM_GRACE_MS = 5000;
14
14
 
15
+ export function forceKillPid(
16
+ pid,
17
+ {
18
+ isWindows = IS_WINDOWS,
19
+ execFileSyncFn = execFileSync,
20
+ killFn = process.kill,
21
+ } = {},
22
+ ) {
23
+ if (isWindows) {
24
+ try {
25
+ execFileSyncFn("taskkill", ["/F", "/T", "/PID", String(pid)], {
26
+ stdio: "ignore",
27
+ timeout: 5000,
28
+ windowsHide: true,
29
+ });
30
+ return;
31
+ } catch (taskkillError) {
32
+ try {
33
+ killFn(pid);
34
+ return;
35
+ } catch {
36
+ throw taskkillError;
37
+ }
38
+ }
39
+ }
40
+
41
+ killFn(pid, "SIGKILL");
42
+ }
43
+
15
44
  // cmdLine 패턴 기반 화이트리스트 (고아 후보에서 제외)
16
- const WHITELIST_CMDLINE = [
17
- /oh-my-claudecode/i,
18
- /triflux[\\/]hub[\\/]s/i,
19
- ];
45
+ const WHITELIST_CMDLINE = [/oh-my-claudecode/i, /triflux[\\/]hub[\\/]s/i];
20
46
 
21
47
  // 프로세스명 기반 화이트리스트
22
48
  const WHITELIST_NAMES = ["claude", "CCXProcess"];
@@ -44,15 +70,13 @@ async function queryWindowsProcesses(execFileFn) {
44
70
  const raw = JSON.parse(stdout.trim());
45
71
  const items = Array.isArray(raw) ? raw : [raw];
46
72
 
47
- return items
48
- .filter(Boolean)
49
- .map((p) => ({
50
- pid: Number(p.ProcessId),
51
- name: String(p.Name || "").replace(/\.exe$/i, ""),
52
- parentPid: Number(p.ParentProcessId) || 0,
53
- cmdLine: String(p.CommandLine || ""),
54
- ramMB: Math.round((Number(p.WorkingSetSize) || 0) / 1024 / 1024),
55
- }));
73
+ return items.filter(Boolean).map((p) => ({
74
+ pid: Number(p.ProcessId),
75
+ name: String(p.Name || "").replace(/\.exe$/i, ""),
76
+ parentPid: Number(p.ParentProcessId) || 0,
77
+ cmdLine: String(p.CommandLine || ""),
78
+ ramMB: Math.round((Number(p.WorkingSetSize) || 0) / 1024 / 1024),
79
+ }));
56
80
  }
57
81
 
58
82
  /**
@@ -63,7 +87,7 @@ async function queryWindowsProcesses(execFileFn) {
63
87
  async function queryUnixProcesses(execFileFn) {
64
88
  const { stdout } = await execFileFn(
65
89
  "ps",
66
- ["-eo", "pid,ppid,rss,comm,args", "--no-headers"],
90
+ ["-eo", "pid=,ppid=,rss=,comm=,args="],
67
91
  { encoding: "utf8", timeout: 10_000 },
68
92
  );
69
93
 
@@ -225,7 +249,11 @@ export async function findOrphanProcesses(opts = {}) {
225
249
  const nameLower = p.name.toLowerCase();
226
250
 
227
251
  // 대상 프로세스만
228
- if (!TARGET_PROCESS_NAMES.some((t) => nameLower === t || nameLower === `${t}.exe`)) {
252
+ if (
253
+ !TARGET_PROCESS_NAMES.some(
254
+ (t) => nameLower === t || nameLower === `${t}.exe`,
255
+ )
256
+ ) {
229
257
  return false;
230
258
  }
231
259
 
@@ -275,6 +303,12 @@ export async function findOrphanProcesses(opts = {}) {
275
303
  export function createProcessCleanup(opts = {}) {
276
304
  const execFileFn = opts.execFileFn ?? execFileAsync;
277
305
  const dryRun = opts.dryRun ?? false;
306
+ const isWindows = opts.isWindows ?? IS_WINDOWS;
307
+ const execFileSyncFn = opts.execFileSyncFn ?? execFileSync;
308
+ const killFn = opts.killFn ?? process.kill;
309
+ const sleepFn =
310
+ opts.sleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
311
+ const sigtermGraceMs = opts.sigtermGraceMs ?? SIGTERM_GRACE_MS;
278
312
 
279
313
  let lastOrphans = [];
280
314
 
@@ -294,35 +328,45 @@ export function createProcessCleanup(opts = {}) {
294
328
  /**
295
329
  * 마지막 scan 결과의 프로세스를 kill한다.
296
330
  * dryRun=true이면 kill 없이 목록만 반환한다.
297
- * SIGTERM → 5s 대기 → SIGKILL 순서.
331
+ * SIGTERM → 5s 대기 → 강제 종료(taskkill/SIGKILL) 순서.
298
332
  * @returns {Promise<Array<{pid,name,killed,error}>>}
299
333
  */
300
334
  async function kill() {
301
335
  if (dryRun) {
302
- return lastOrphans.map((p) => ({ pid: p.pid, name: p.name, killed: false, dryRun: true }));
336
+ return lastOrphans.map((p) => ({
337
+ pid: p.pid,
338
+ name: p.name,
339
+ killed: false,
340
+ dryRun: true,
341
+ }));
303
342
  }
304
343
 
305
344
  const results = await Promise.all(
306
345
  lastOrphans.map(async (p) => {
307
346
  try {
308
347
  // SIGTERM
309
- process.kill(p.pid, "SIGTERM");
348
+ killFn(p.pid, "SIGTERM");
310
349
 
311
- // 5초 대기 후 살아있으면 SIGKILL
312
- await new Promise((resolve) => setTimeout(resolve, SIGTERM_GRACE_MS));
350
+ // 5초 대기 후 살아있으면 강제 종료
351
+ await sleepFn(sigtermGraceMs);
313
352
 
314
353
  try {
315
354
  // 프로세스가 아직 살아있는지 확인 (signal 0)
316
- process.kill(p.pid, 0);
317
- // 여전히 살아있음 → SIGKILL
318
- process.kill(p.pid, "SIGKILL");
355
+ killFn(p.pid, 0);
356
+ // 여전히 살아있음 → Windows는 taskkill/process.kill, 그 외는 SIGKILL
357
+ forceKillPid(p.pid, { isWindows, execFileSyncFn, killFn });
319
358
  } catch {
320
359
  // ESRCH: 이미 종료됨 — 정상
321
360
  }
322
361
 
323
362
  return { pid: p.pid, name: p.name, killed: true };
324
363
  } catch (err) {
325
- return { pid: p.pid, name: p.name, killed: false, error: String(err.message || err) };
364
+ return {
365
+ pid: p.pid,
366
+ name: p.name,
367
+ killed: false,
368
+ error: String(err.message || err),
369
+ };
326
370
  }
327
371
  }),
328
372
  );