@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.
- package/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +68 -7
- package/hub/server.mjs +2181 -701
- package/hub/store-adapter.mjs +101 -37
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +127 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +45 -17
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +21 -119
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1067 -264
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +69 -53
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +62 -34
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1526 -198
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +21 -37
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -45
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +936 -449
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +518 -64
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +663 -337
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- 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
|
+
}
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -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 {
|
|
8
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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 {
|
|
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
|
|
24
|
+
const SESSION = argVal("--session");
|
|
25
25
|
const RESULT_DIR = argVal("--result-dir") ?? join(tmpdir(), "tfx-headless");
|
|
26
|
-
const LAYOUT
|
|
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
|
-
|
|
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,
|
|
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) =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
131
|
-
research:0.
|
|
132
|
-
exec:
|
|
133
|
-
verify:
|
|
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))
|
|
143
|
-
else if (/edit|patch|implement|write|update|fix|refactor/.test(text))
|
|
144
|
-
|
|
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
|
-
[
|
|
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();
|
|
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(
|
|
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", () => {
|
|
283
|
+
process.stdin.once("data", () => {
|
|
284
|
+
cleanup();
|
|
285
|
+
process.exit(0);
|
|
286
|
+
});
|
|
269
287
|
} else {
|
|
270
|
-
setTimeout(() => {
|
|
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("🔵"))
|
|
292
|
-
|
|
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 {
|
|
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"))
|
|
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, {
|
|
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 =
|
|
386
|
-
|
|
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(() => {
|
|
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(
|
|
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", () => {
|
|
478
|
+
process.stdin.once("data", () => {
|
|
479
|
+
cleanup();
|
|
480
|
+
process.exit(0);
|
|
481
|
+
});
|
|
435
482
|
} else {
|
|
436
|
-
setTimeout(() => {
|
|
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);
|
|
506
|
+
const ingestTimer = setInterval(ingest, 500); // 2Hz
|
|
457
507
|
startRender();
|
|
458
508
|
|
|
459
509
|
// 타임아웃 (10분)
|
|
460
|
-
setTimeout(
|
|
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", () => {
|
|
519
|
+
process.on("SIGINT", () => {
|
|
520
|
+
cleanup();
|
|
521
|
+
process.exit(0);
|
|
522
|
+
});
|