@triflux/remote 10.0.1 → 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 +68 -7
  3. package/hub/server.mjs +2181 -701
  4. package/hub/store-adapter.mjs +101 -37
  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 +127 -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 +45 -17
  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 +21 -119
  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 +1067 -264
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +69 -53
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +62 -34
  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 +1526 -198
  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 +21 -37
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -45
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +936 -449
  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 +518 -64
  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 +663 -337
  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
@@ -0,0 +1,223 @@
1
+ // hub/team/tui-synapse.mjs — Synapse 실시간 관제 (Phase 3)
2
+ // HTTP polling 기반 이벤트 수신 + 메트릭 수집 + 스파크라인 렌더링
3
+
4
+ import { color, dim, MOCHA } from "./ansi.mjs";
5
+ import { sparkline } from "./tui-widgets.mjs";
6
+
7
+ const DEFAULT_SYNAPSE_BASE_URL = "http://127.0.0.1:27888";
8
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
9
+ const MAX_METRIC_SAMPLES = 30;
10
+
11
+ // ── SynapseEventStream ────────────────────────────────────────────────────
12
+ /**
13
+ * Synapse 서버로부터 이벤트를 polling으로 수신
14
+ * @param {object} [opts]
15
+ * @param {string} [opts.baseUrl] - Synapse 서버 URL
16
+ * @param {number} [opts.pollIntervalMs] - polling 간격
17
+ * @param {function} [opts.fetchImpl] - fetch 구현 (테스트용)
18
+ * @param {function} [opts.onEvent] - 이벤트 콜백
19
+ * @param {function} [opts.onError] - 에러 콜백
20
+ */
21
+ export function createSynapseEventStream(opts = {}) {
22
+ const {
23
+ baseUrl = DEFAULT_SYNAPSE_BASE_URL,
24
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
25
+ onEvent,
26
+ onError,
27
+ } = opts;
28
+
29
+ const fetchImpl = opts.fetchImpl || globalThis.fetch?.bind(globalThis);
30
+ let timer = null;
31
+ let lastEventId = 0;
32
+ let running = false;
33
+
34
+ async function poll(force = false) {
35
+ if (!fetchImpl || (!running && !force)) return;
36
+ try {
37
+ const url = new URL("/synapse/events", baseUrl);
38
+ url.searchParams.set("since", String(lastEventId));
39
+ const res = await fetchImpl(url.toString(), {
40
+ method: "GET",
41
+ headers: { accept: "application/json" },
42
+ signal: AbortSignal.timeout?.(5000),
43
+ });
44
+ if (!res.ok) return;
45
+ const data = await res.json();
46
+ if (Array.isArray(data.events)) {
47
+ for (const event of data.events) {
48
+ if (event.id > lastEventId) lastEventId = event.id;
49
+ onEvent?.(event);
50
+ }
51
+ }
52
+ } catch (err) {
53
+ onError?.(err);
54
+ }
55
+ }
56
+
57
+ return {
58
+ start() {
59
+ if (running) return;
60
+ running = true;
61
+ // 즉시 첫 poll + 주기적 반복
62
+ poll();
63
+ timer = setInterval(poll, pollIntervalMs);
64
+ if (timer.unref) timer.unref();
65
+ },
66
+
67
+ stop() {
68
+ running = false;
69
+ if (timer) {
70
+ clearInterval(timer);
71
+ timer = null;
72
+ }
73
+ },
74
+
75
+ get isRunning() {
76
+ return running;
77
+ },
78
+
79
+ /** 테스트/수동 용: 즉시 poll 실행 (running 상태 무관) */
80
+ async pollOnce() {
81
+ await poll(true);
82
+ },
83
+
84
+ reset() {
85
+ lastEventId = 0;
86
+ },
87
+ };
88
+ }
89
+
90
+ // ── MetricsCollector ──────────────────────────────────────────────────────
91
+ /**
92
+ * Synapse 이벤트에서 메트릭(토큰, 지연시간, 성공률)을 수집
93
+ */
94
+ export function createMetricsCollector(opts = {}) {
95
+ const { maxSamples = MAX_METRIC_SAMPLES } = opts;
96
+
97
+ const metrics = {
98
+ tokenRates: [], // 초당 토큰 소비율
99
+ latencies: [], // 요청 지연시간 (ms)
100
+ successRates: [], // 성공률 (0-1)
101
+ eventCount: 0,
102
+ lastEventAt: 0,
103
+ };
104
+
105
+ function pushSample(arr, value) {
106
+ arr.push(value);
107
+ if (arr.length > maxSamples) arr.shift();
108
+ }
109
+
110
+ return {
111
+ /** Synapse 이벤트를 처리하여 메트릭 갱신 */
112
+ ingest(event) {
113
+ if (!event || typeof event !== "object") return;
114
+ metrics.eventCount++;
115
+ metrics.lastEventAt = Date.now();
116
+
117
+ // 토큰 소비율
118
+ if (typeof event.tokens === "number" && event.tokens > 0) {
119
+ const elapsed = event.elapsed || 1;
120
+ pushSample(metrics.tokenRates, event.tokens / elapsed);
121
+ }
122
+
123
+ // 지연시간
124
+ if (typeof event.latencyMs === "number") {
125
+ pushSample(metrics.latencies, event.latencyMs);
126
+ }
127
+
128
+ // 성공률 (이벤트별 ok/fail)
129
+ if (event.status) {
130
+ const ok =
131
+ event.status === "ok" || event.status === "completed" ? 1 : 0;
132
+ pushSample(metrics.successRates, ok);
133
+ }
134
+ },
135
+
136
+ /** 현재 메트릭 스냅샷 반환 */
137
+ snapshot() {
138
+ const avgLatency =
139
+ metrics.latencies.length > 0
140
+ ? metrics.latencies.reduce((a, b) => a + b, 0) /
141
+ metrics.latencies.length
142
+ : 0;
143
+ const avgSuccessRate =
144
+ metrics.successRates.length > 0
145
+ ? metrics.successRates.reduce((a, b) => a + b, 0) /
146
+ metrics.successRates.length
147
+ : 1;
148
+ const lastTokenRate =
149
+ metrics.tokenRates.length > 0
150
+ ? metrics.tokenRates[metrics.tokenRates.length - 1]
151
+ : 0;
152
+
153
+ return {
154
+ tokenRates: [...metrics.tokenRates],
155
+ latencies: [...metrics.latencies],
156
+ successRates: [...metrics.successRates],
157
+ avgLatency: Math.round(avgLatency),
158
+ avgSuccessRate: Math.round(avgSuccessRate * 100),
159
+ lastTokenRate: Math.round(lastTokenRate),
160
+ eventCount: metrics.eventCount,
161
+ lastEventAt: metrics.lastEventAt,
162
+ };
163
+ },
164
+
165
+ reset() {
166
+ metrics.tokenRates.length = 0;
167
+ metrics.latencies.length = 0;
168
+ metrics.successRates.length = 0;
169
+ metrics.eventCount = 0;
170
+ metrics.lastEventAt = 0;
171
+ },
172
+ };
173
+ }
174
+
175
+ // ── Tier1 메트릭 렌더러 ───────────────────────────────────────────────────
176
+ /**
177
+ * Synapse 메트릭을 Tier1에 표시할 한 줄 문자열로 렌더링
178
+ * @param {object} snapshot - MetricsCollector.snapshot() 결과
179
+ * @param {number} [width=60] - 최대 표시 폭
180
+ * @returns {string} 렌더링된 메트릭 행
181
+ */
182
+ export function renderMetricsTier1(snapshot, width = 60) {
183
+ if (!snapshot || snapshot.eventCount === 0) {
184
+ return dim("synapse: waiting for events…");
185
+ }
186
+
187
+ const parts = [];
188
+
189
+ // 토큰 스파크라인
190
+ if (snapshot.tokenRates.length > 0) {
191
+ const spark = sparkline(snapshot.tokenRates, 8, MOCHA.executing);
192
+ parts.push(
193
+ `tok/s ${spark} ${color(String(snapshot.lastTokenRate), MOCHA.executing)}`,
194
+ );
195
+ }
196
+
197
+ // 지연시간
198
+ if (snapshot.latencies.length > 0) {
199
+ const latColor =
200
+ snapshot.avgLatency > 1000
201
+ ? MOCHA.fail
202
+ : snapshot.avgLatency > 500
203
+ ? MOCHA.partial
204
+ : MOCHA.ok;
205
+ parts.push(`lat ${color(`${snapshot.avgLatency}ms`, latColor)}`);
206
+ }
207
+
208
+ // 성공률
209
+ if (snapshot.successRates.length > 0) {
210
+ const srColor =
211
+ snapshot.avgSuccessRate < 80
212
+ ? MOCHA.fail
213
+ : snapshot.avgSuccessRate < 95
214
+ ? MOCHA.partial
215
+ : MOCHA.ok;
216
+ parts.push(`ok ${color(`${snapshot.avgSuccessRate}%`, srColor)}`);
217
+ }
218
+
219
+ // 이벤트 카운트
220
+ parts.push(dim(`ev:${snapshot.eventCount}`));
221
+
222
+ return parts.join(dim(" │ "));
223
+ }
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  // hub/team/tui-viewer.mjs — worker state aggregator v5
3
4
  // psmux capture-pane 기반 워커 상태 집계 + TUI 렌더링
4
5
  // data ingest: ~2Hz (500ms), render: 8-12FPS (별도 루프)
5
6
 
6
- import { existsSync, readFileSync, statSync } from "node:fs";
7
7
  import { execFileSync } from "node:child_process";
8
- import { join } from "node:path";
8
+ import { existsSync, readFileSync, statSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
- import { createLogDashboard } from "./tui.mjs";
11
- import { createLiteDashboard } from "./tui-lite.mjs";
10
+ import { join } from "node:path";
11
+ import { formatPsmuxInstallGuidance } from "../../scripts/lib/psmux-info.mjs";
12
12
  import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
13
13
  import { processHandoff } from "./handoff.mjs";
14
- import { statusBadge } from "./ansi.mjs";
15
-
14
+ import { createLogDashboard } from "./tui.mjs";
15
+ import { createLiteDashboard } from "./tui-lite.mjs";
16
16
 
17
17
  // ── CLI 인자 파싱 ──
18
18
  const args = process.argv.slice(2);
@@ -21,9 +21,9 @@ function argVal(flag) {
21
21
  return idx >= 0 ? args[idx + 1] : null;
22
22
  }
23
23
 
24
- const SESSION = argVal("--session");
24
+ const SESSION = argVal("--session");
25
25
  const RESULT_DIR = argVal("--result-dir") ?? join(tmpdir(), "tfx-headless");
26
- const LAYOUT = argVal("--layout") ?? "single";
26
+ const LAYOUT = argVal("--layout") ?? "single";
27
27
 
28
28
  if (!SESSION) {
29
29
  process.stderr.write(
@@ -36,7 +36,7 @@ try {
36
36
  execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
37
37
  } catch {
38
38
  process.stderr.write(
39
- "ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n",
39
+ `ERROR: psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}\n`,
40
40
  );
41
41
  process.exit(1);
42
42
  }
@@ -49,20 +49,22 @@ const MAX_BODY_BYTES = 10240;
49
49
  // forceTTY 시 alternate screen이 WT pane에서 렌더링 안 되는 문제 → append-only 유지
50
50
  const tuiFactory = LAYOUT === "lite" ? createLiteDashboard : createLogDashboard;
51
51
  const tui = tuiFactory({
52
- refreshMs: 0, // render 루프를 직접 제어
52
+ refreshMs: 0, // render 루프를 직접 제어
53
53
  stream: process.stdout,
54
54
  input: process.stdin,
55
55
  columns: process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 120,
56
56
  layout: LAYOUT,
57
- onOpenSelectedWorker: (workerName) => openHeadlessDashboardTarget(SESSION, {
58
- worker: workerName,
59
- openAll: false,
60
- cwd: process.cwd(),
61
- }),
62
- onOpenAllWorkers: () => openHeadlessDashboardTarget(SESSION, {
63
- openAll: true,
64
- cwd: process.cwd(),
65
- }),
57
+ onOpenSelectedWorker: (workerName) =>
58
+ openHeadlessDashboardTarget(SESSION, {
59
+ worker: workerName,
60
+ openAll: false,
61
+ cwd: process.cwd(),
62
+ }),
63
+ onOpenAllWorkers: () =>
64
+ openHeadlessDashboardTarget(SESSION, {
65
+ openAll: true,
66
+ cwd: process.cwd(),
67
+ }),
66
68
  });
67
69
  const startTime = Date.now();
68
70
  tui.setStartTime(startTime);
@@ -73,6 +75,9 @@ const INTERNAL_PATTERNS = [
73
75
  /\.err\b/,
74
76
  /completion[-_]token/i,
75
77
  /^---\s*HANDOFF\s*---$/i,
78
+ /Get-Content/,
79
+ /-color\s+never/,
80
+ /AppData[\\/]Local[\\/]Temp[\\/]tfx-headless/,
76
81
  ];
77
82
 
78
83
  function isInternalLine(line) {
@@ -127,10 +132,10 @@ function extractFindings(lines, verdict = "") {
127
132
 
128
133
  // ── Phase 가중치 진행률 (Plan=10%, Research=30%, Exec=50%, Verify=10%) ──
129
134
  const PHASE_WEIGHTS = {
130
- plan: 0.10,
131
- research:0.40, // plan + research
132
- exec: 0.90, // plan + research + exec
133
- verify: 1.00,
135
+ plan: 0.1,
136
+ research: 0.4, // plan + research
137
+ exec: 0.9, // plan + research + exec
138
+ verify: 1.0,
134
139
  };
135
140
 
136
141
  function estimateProgress(lines, context = {}) {
@@ -139,9 +144,11 @@ function estimateProgress(lines, context = {}) {
139
144
  const text = lines.join("\n").toLowerCase();
140
145
  let phase = "plan";
141
146
 
142
- if (/verify|assert|test|check|confirm/.test(text)) phase = "verify";
143
- else if (/edit|patch|implement|write|update|fix|refactor/.test(text)) phase = "exec";
144
- else if (/search|read|inspect|analy|review|research/.test(text)) phase = "research";
147
+ if (/verify|assert|test|check|confirm/.test(text)) phase = "verify";
148
+ else if (/edit|patch|implement|write|update|fix|refactor/.test(text))
149
+ phase = "exec";
150
+ else if (/search|read|inspect|analy|review|research/.test(text))
151
+ phase = "research";
145
152
 
146
153
  let ratio = PHASE_WEIGHTS[phase];
147
154
 
@@ -162,7 +169,13 @@ function listPanes() {
162
169
  try {
163
170
  const out = execFileSync(
164
171
  "psmux",
165
- ["list-panes", "-t", SESSION, "-F", "#{pane_index}:#{pane_title}:#{pane_pid}"],
172
+ [
173
+ "list-panes",
174
+ "-t",
175
+ SESSION,
176
+ "-F",
177
+ "#{pane_index}:#{pane_title}:#{pane_pid}",
178
+ ],
166
179
  { encoding: "utf8", timeout: 2000 },
167
180
  );
168
181
  return out
@@ -246,7 +259,7 @@ function splitHandoff(handoff) {
246
259
  }
247
260
 
248
261
  // ── 상태 집계 저장소 ──
249
- const workerState = new Map(); // paneName → 내부 상태
262
+ const workerState = new Map(); // paneName → 내부 상태
250
263
  let emptyPollCount = 0;
251
264
 
252
265
  // ── data ingest (4Hz = 250ms) ──
@@ -261,13 +274,21 @@ function ingest() {
261
274
  clearInterval(ingestTimer);
262
275
  clearInterval(renderTimer);
263
276
  tui.render();
264
- process.stdout.write("\n\x1b[38;5;245m 세션 종료됨 — 아무 키나 누르면 닫힘\x1b[0m");
277
+ process.stdout.write(
278
+ "\n\x1b[38;5;245m 세션 종료됨 — 아무 키나 누르면 닫힘\x1b[0m",
279
+ );
265
280
  if (process.stdin.isTTY) {
266
281
  process.stdin.setRawMode(true);
267
282
  process.stdin.resume();
268
- process.stdin.once("data", () => { cleanup(); process.exit(0); });
283
+ process.stdin.once("data", () => {
284
+ cleanup();
285
+ process.exit(0);
286
+ });
269
287
  } else {
270
- setTimeout(() => { cleanup(); process.exit(0); }, 30000);
288
+ setTimeout(() => {
289
+ cleanup();
290
+ process.exit(0);
291
+ }, 30000);
271
292
  }
272
293
  return;
273
294
  }
@@ -288,8 +309,10 @@ function ingest() {
288
309
 
289
310
  // CLI 타입 감지
290
311
  let cli = "codex";
291
- if (pane.title.includes("gemini") || pane.title.includes("🔵")) cli = "gemini";
292
- else if (pane.title.includes("claude") || pane.title.includes("🟠")) cli = "claude";
312
+ if (pane.title.includes("gemini") || pane.title.includes("🔵"))
313
+ cli = "gemini";
314
+ else if (pane.title.includes("claude") || pane.title.includes("🟠"))
315
+ cli = "claude";
293
316
  ws.title = pane.title;
294
317
  ws.cli = cli;
295
318
 
@@ -335,7 +358,11 @@ function ingest() {
335
358
 
336
359
  const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
337
360
  let resultSize = 0;
338
- try { resultSize = statSync(resultFile).size; } catch { /* missing */ }
361
+ try {
362
+ resultSize = statSync(resultFile).size;
363
+ } catch {
364
+ /* missing */
365
+ }
339
366
 
340
367
  const shellReturned = /^(PS\s|>|\$)\s*/.test(lastLine) && lines.length > 2;
341
368
  const tokens = extractTokenLabel(snapshot);
@@ -347,7 +374,9 @@ function ingest() {
347
374
  : snapshot;
348
375
  const rLines = toLines(resultContent);
349
376
  const verdict = extractFindings(rLines).at(-1) || lastLine || "completed";
350
- const handoffStatus = /fail|error|exception/i.test(rLines.join("\n")) ? "failed" : "ok";
377
+ const handoffStatus = /fail|error|exception/i.test(rLines.join("\n"))
378
+ ? "failed"
379
+ : "ok";
351
380
  const handoff = {
352
381
  status: handoffStatus,
353
382
  lead_action: handoffStatus === "failed" ? "retry" : "accept",
@@ -379,11 +408,22 @@ function ingest() {
379
408
  }
380
409
 
381
410
  // 진행 중
382
- const progress = estimateProgress(lines, { tokens, resultSize, shellReturned, done: false });
411
+ const progress = estimateProgress(lines, {
412
+ tokens,
413
+ resultSize,
414
+ shellReturned,
415
+ done: false,
416
+ });
383
417
  const verdict = lastLine;
384
418
 
385
- ws.raw_body = raw_body.length > MAX_BODY_BYTES ? raw_body.slice(-MAX_BODY_BYTES) : raw_body;
386
- ws.filtered_body = filtered_body.length > MAX_BODY_BYTES ? filtered_body.slice(-MAX_BODY_BYTES) : filtered_body;
419
+ ws.raw_body =
420
+ raw_body.length > MAX_BODY_BYTES
421
+ ? raw_body.slice(-MAX_BODY_BYTES)
422
+ : raw_body;
423
+ ws.filtered_body =
424
+ filtered_body.length > MAX_BODY_BYTES
425
+ ? filtered_body.slice(-MAX_BODY_BYTES)
426
+ : filtered_body;
387
427
  ws.verdict = verdict;
388
428
  ws.findings = extractFindings(lines, lastLine);
389
429
  ws.progress = progress;
@@ -416,7 +456,9 @@ function pushToTui(paneName, cli, paneTitle, update) {
416
456
  // ── render 루프 (8-12FPS ≈ 100ms) ──
417
457
  let renderTimer = null;
418
458
  function startRender() {
419
- renderTimer = setInterval(() => { tui.render(); }, 100);
459
+ renderTimer = setInterval(() => {
460
+ tui.render();
461
+ }, 100);
420
462
  if (renderTimer.unref) renderTimer.unref();
421
463
  }
422
464
 
@@ -427,13 +469,21 @@ const doneCheck = setInterval(() => {
427
469
  clearInterval(doneCheck);
428
470
  clearInterval(ingestTimer);
429
471
  clearInterval(renderTimer);
430
- process.stdout.write("\n\x1b[38;5;245m 전체 완료 — 아무 키나 누르면 닫힘\x1b[0m");
472
+ process.stdout.write(
473
+ "\n\x1b[38;5;245m 전체 완료 — 아무 키나 누르면 닫힘\x1b[0m",
474
+ );
431
475
  if (process.stdin.isTTY) {
432
476
  process.stdin.setRawMode(true);
433
477
  process.stdin.resume();
434
- process.stdin.once("data", () => { cleanup(); process.exit(0); });
478
+ process.stdin.once("data", () => {
479
+ cleanup();
480
+ process.exit(0);
481
+ });
435
482
  } else {
436
- setTimeout(() => { cleanup(); process.exit(0); }, 30000);
483
+ setTimeout(() => {
484
+ cleanup();
485
+ process.exit(0);
486
+ }, 30000);
437
487
  }
438
488
  }
439
489
  }, 2000);
@@ -453,11 +503,20 @@ function cleanup() {
453
503
 
454
504
  // ── 진입점 ──
455
505
  tui.render();
456
- const ingestTimer = setInterval(ingest, 500); // 2Hz
506
+ const ingestTimer = setInterval(ingest, 500); // 2Hz
457
507
  startRender();
458
508
 
459
509
  // 타임아웃 (10분)
460
- setTimeout(() => { cleanup(); process.exit(0); }, 10 * 60 * 1000);
510
+ setTimeout(
511
+ () => {
512
+ cleanup();
513
+ process.exit(0);
514
+ },
515
+ 10 * 60 * 1000,
516
+ );
461
517
 
462
518
  // Ctrl-C
463
- process.on("SIGINT", () => { cleanup(); process.exit(0); });
519
+ process.on("SIGINT", () => {
520
+ cleanup();
521
+ process.exit(0);
522
+ });