@triflux/remote 10.1.0 → 10.26.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 (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  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 +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -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 +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -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/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. 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,17 +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
-
14
+ import { createLogDashboard } from "./tui.mjs";
15
+ import { createLiteDashboard } from "./tui-lite.mjs";
15
16
 
16
17
  // ── CLI 인자 파싱 ──
17
18
  const args = process.argv.slice(2);
@@ -20,9 +21,9 @@ function argVal(flag) {
20
21
  return idx >= 0 ? args[idx + 1] : null;
21
22
  }
22
23
 
23
- const SESSION = argVal("--session");
24
+ const SESSION = argVal("--session");
24
25
  const RESULT_DIR = argVal("--result-dir") ?? join(tmpdir(), "tfx-headless");
25
- const LAYOUT = argVal("--layout") ?? "single";
26
+ const LAYOUT = argVal("--layout") ?? "single";
26
27
 
27
28
  if (!SESSION) {
28
29
  process.stderr.write(
@@ -35,7 +36,7 @@ try {
35
36
  execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
36
37
  } catch {
37
38
  process.stderr.write(
38
- "ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n",
39
+ `ERROR: psmux 미설치. 설치 방법:\n${formatPsmuxInstallGuidance(" ")}\n`,
39
40
  );
40
41
  process.exit(1);
41
42
  }
@@ -48,20 +49,22 @@ const MAX_BODY_BYTES = 10240;
48
49
  // forceTTY 시 alternate screen이 WT pane에서 렌더링 안 되는 문제 → append-only 유지
49
50
  const tuiFactory = LAYOUT === "lite" ? createLiteDashboard : createLogDashboard;
50
51
  const tui = tuiFactory({
51
- refreshMs: 0, // render 루프를 직접 제어
52
+ refreshMs: 0, // render 루프를 직접 제어
52
53
  stream: process.stdout,
53
54
  input: process.stdin,
54
55
  columns: process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 120,
55
56
  layout: LAYOUT,
56
- onOpenSelectedWorker: (workerName) => openHeadlessDashboardTarget(SESSION, {
57
- worker: workerName,
58
- openAll: false,
59
- cwd: process.cwd(),
60
- }),
61
- onOpenAllWorkers: () => openHeadlessDashboardTarget(SESSION, {
62
- openAll: true,
63
- cwd: process.cwd(),
64
- }),
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
+ }),
65
68
  });
66
69
  const startTime = Date.now();
67
70
  tui.setStartTime(startTime);
@@ -72,6 +75,9 @@ const INTERNAL_PATTERNS = [
72
75
  /\.err\b/,
73
76
  /completion[-_]token/i,
74
77
  /^---\s*HANDOFF\s*---$/i,
78
+ /Get-Content/,
79
+ /-color\s+never/,
80
+ /AppData[\\/]Local[\\/]Temp[\\/]tfx-headless/,
75
81
  ];
76
82
 
77
83
  function isInternalLine(line) {
@@ -126,10 +132,10 @@ function extractFindings(lines, verdict = "") {
126
132
 
127
133
  // ── Phase 가중치 진행률 (Plan=10%, Research=30%, Exec=50%, Verify=10%) ──
128
134
  const PHASE_WEIGHTS = {
129
- plan: 0.10,
130
- research:0.40, // plan + research
131
- exec: 0.90, // plan + research + exec
132
- verify: 1.00,
135
+ plan: 0.1,
136
+ research: 0.4, // plan + research
137
+ exec: 0.9, // plan + research + exec
138
+ verify: 1.0,
133
139
  };
134
140
 
135
141
  function estimateProgress(lines, context = {}) {
@@ -138,9 +144,11 @@ function estimateProgress(lines, context = {}) {
138
144
  const text = lines.join("\n").toLowerCase();
139
145
  let phase = "plan";
140
146
 
141
- if (/verify|assert|test|check|confirm/.test(text)) phase = "verify";
142
- else if (/edit|patch|implement|write|update|fix|refactor/.test(text)) phase = "exec";
143
- 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";
144
152
 
145
153
  let ratio = PHASE_WEIGHTS[phase];
146
154
 
@@ -161,7 +169,13 @@ function listPanes() {
161
169
  try {
162
170
  const out = execFileSync(
163
171
  "psmux",
164
- ["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
+ ],
165
179
  { encoding: "utf8", timeout: 2000 },
166
180
  );
167
181
  return out
@@ -245,7 +259,7 @@ function splitHandoff(handoff) {
245
259
  }
246
260
 
247
261
  // ── 상태 집계 저장소 ──
248
- const workerState = new Map(); // paneName → 내부 상태
262
+ const workerState = new Map(); // paneName → 내부 상태
249
263
  let emptyPollCount = 0;
250
264
 
251
265
  // ── data ingest (4Hz = 250ms) ──
@@ -260,13 +274,21 @@ function ingest() {
260
274
  clearInterval(ingestTimer);
261
275
  clearInterval(renderTimer);
262
276
  tui.render();
263
- process.stdout.write("\n\x1b[38;5;245m 세션 종료됨 — 아무 키나 누르면 닫힘\x1b[0m");
277
+ process.stdout.write(
278
+ "\n\x1b[38;5;245m 세션 종료됨 — 아무 키나 누르면 닫힘\x1b[0m",
279
+ );
264
280
  if (process.stdin.isTTY) {
265
281
  process.stdin.setRawMode(true);
266
282
  process.stdin.resume();
267
- process.stdin.once("data", () => { cleanup(); process.exit(0); });
283
+ process.stdin.once("data", () => {
284
+ cleanup();
285
+ process.exit(0);
286
+ });
268
287
  } else {
269
- setTimeout(() => { cleanup(); process.exit(0); }, 30000);
288
+ setTimeout(() => {
289
+ cleanup();
290
+ process.exit(0);
291
+ }, 30000);
270
292
  }
271
293
  return;
272
294
  }
@@ -287,8 +309,10 @@ function ingest() {
287
309
 
288
310
  // CLI 타입 감지
289
311
  let cli = "codex";
290
- if (pane.title.includes("gemini") || pane.title.includes("🔵")) cli = "gemini";
291
- 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";
292
316
  ws.title = pane.title;
293
317
  ws.cli = cli;
294
318
 
@@ -334,7 +358,11 @@ function ingest() {
334
358
 
335
359
  const resultFile = join(RESULT_DIR, `${SESSION}-${paneName}.txt`);
336
360
  let resultSize = 0;
337
- try { resultSize = statSync(resultFile).size; } catch { /* missing */ }
361
+ try {
362
+ resultSize = statSync(resultFile).size;
363
+ } catch {
364
+ /* missing */
365
+ }
338
366
 
339
367
  const shellReturned = /^(PS\s|>|\$)\s*/.test(lastLine) && lines.length > 2;
340
368
  const tokens = extractTokenLabel(snapshot);
@@ -346,7 +374,9 @@ function ingest() {
346
374
  : snapshot;
347
375
  const rLines = toLines(resultContent);
348
376
  const verdict = extractFindings(rLines).at(-1) || lastLine || "completed";
349
- 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";
350
380
  const handoff = {
351
381
  status: handoffStatus,
352
382
  lead_action: handoffStatus === "failed" ? "retry" : "accept",
@@ -378,11 +408,22 @@ function ingest() {
378
408
  }
379
409
 
380
410
  // 진행 중
381
- const progress = estimateProgress(lines, { tokens, resultSize, shellReturned, done: false });
411
+ const progress = estimateProgress(lines, {
412
+ tokens,
413
+ resultSize,
414
+ shellReturned,
415
+ done: false,
416
+ });
382
417
  const verdict = lastLine;
383
418
 
384
- ws.raw_body = raw_body.length > MAX_BODY_BYTES ? raw_body.slice(-MAX_BODY_BYTES) : raw_body;
385
- 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;
386
427
  ws.verdict = verdict;
387
428
  ws.findings = extractFindings(lines, lastLine);
388
429
  ws.progress = progress;
@@ -406,16 +447,18 @@ function ingest() {
406
447
  function pushToTui(paneName, cli, paneTitle, update) {
407
448
  // _leadAction은 tui에 노출하지 않음 (내부용)
408
449
  const { _leadAction: _ignored, ...safeUpdate } = update;
409
- // pane title에서 실제 역할만 추출: "⚪ codex (executor)" → "executor"
410
- const roleMatch = paneTitle.match(/\(([^)]+)\)$/);
411
- const role = roleMatch ? roleMatch[1] : "";
412
- tui.updateWorker(paneName, { cli, role, ...safeUpdate });
450
+ // pane title의 괄호 값은 UI 라벨이며 role 의미값으로 승격하지 않는다.
451
+ const labelMatch = paneTitle.match(/\(([^)]+)\)$/);
452
+ const displayName = labelMatch ? labelMatch[1] : paneName;
453
+ tui.updateWorker(paneName, { cli, displayName, role: "", ...safeUpdate });
413
454
  }
414
455
 
415
456
  // ── render 루프 (8-12FPS ≈ 100ms) ──
416
457
  let renderTimer = null;
417
458
  function startRender() {
418
- renderTimer = setInterval(() => { tui.render(); }, 100);
459
+ renderTimer = setInterval(() => {
460
+ tui.render();
461
+ }, 100);
419
462
  if (renderTimer.unref) renderTimer.unref();
420
463
  }
421
464
 
@@ -426,13 +469,21 @@ const doneCheck = setInterval(() => {
426
469
  clearInterval(doneCheck);
427
470
  clearInterval(ingestTimer);
428
471
  clearInterval(renderTimer);
429
- process.stdout.write("\n\x1b[38;5;245m 전체 완료 — 아무 키나 누르면 닫힘\x1b[0m");
472
+ process.stdout.write(
473
+ "\n\x1b[38;5;245m 전체 완료 — 아무 키나 누르면 닫힘\x1b[0m",
474
+ );
430
475
  if (process.stdin.isTTY) {
431
476
  process.stdin.setRawMode(true);
432
477
  process.stdin.resume();
433
- process.stdin.once("data", () => { cleanup(); process.exit(0); });
478
+ process.stdin.once("data", () => {
479
+ cleanup();
480
+ process.exit(0);
481
+ });
434
482
  } else {
435
- setTimeout(() => { cleanup(); process.exit(0); }, 30000);
483
+ setTimeout(() => {
484
+ cleanup();
485
+ process.exit(0);
486
+ }, 30000);
436
487
  }
437
488
  }
438
489
  }, 2000);
@@ -452,11 +503,20 @@ function cleanup() {
452
503
 
453
504
  // ── 진입점 ──
454
505
  tui.render();
455
- const ingestTimer = setInterval(ingest, 500); // 2Hz
506
+ const ingestTimer = setInterval(ingest, 500); // 2Hz
456
507
  startRender();
457
508
 
458
509
  // 타임아웃 (10분)
459
- setTimeout(() => { cleanup(); process.exit(0); }, 10 * 60 * 1000);
510
+ setTimeout(
511
+ () => {
512
+ cleanup();
513
+ process.exit(0);
514
+ },
515
+ 10 * 60 * 1000,
516
+ );
460
517
 
461
518
  // Ctrl-C
462
- process.on("SIGINT", () => { cleanup(); process.exit(0); });
519
+ process.on("SIGINT", () => {
520
+ cleanup();
521
+ process.exit(0);
522
+ });