@triflux/remote 10.50.2 → 10.52.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.
- package/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hub/index.mjs +0 -17
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
// hub/team/tui-remote-adapter.mjs — 원격 세션을 TUI 워커 형식으로 변환하는 어댑터
|
|
2
|
-
//
|
|
3
|
-
// conductor.mjs의 stateChange 이벤트(primary) + remote-watcher.mjs(supplemental)를
|
|
4
|
-
// tui.mjs updateWorker() 호환 형식으로 변환한다.
|
|
5
|
-
// 완료/실패 시 notify.mjs 자동 호출.
|
|
6
|
-
|
|
7
|
-
import { EventEmitter } from "node:events";
|
|
8
|
-
|
|
9
|
-
import { readHosts } from "@triflux/core/hub/lib/hosts-compat.mjs";
|
|
10
|
-
import { STATES } from "./conductor.mjs";
|
|
11
|
-
|
|
12
|
-
// ── 상수 ─────────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
const CONDUCTOR_STATE_TO_TUI_STATUS = Object.freeze({
|
|
15
|
-
[STATES.INIT]: "pending",
|
|
16
|
-
[STATES.STARTING]: "pending",
|
|
17
|
-
[STATES.HEALTHY]: "running",
|
|
18
|
-
[STATES.STALLED]: "running",
|
|
19
|
-
[STATES.INPUT_WAIT]: "running",
|
|
20
|
-
[STATES.FAILED]: "running",
|
|
21
|
-
[STATES.RESTARTING]: "running",
|
|
22
|
-
[STATES.COMPLETED]: "completed",
|
|
23
|
-
[STATES.DEAD]: "failed",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const SESSION_PREFIX = "tfx-spawn-";
|
|
27
|
-
|
|
28
|
-
// ── 유틸 ─────────────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
function resolveSshUser(hostsData, host) {
|
|
31
|
-
if (!host || !hostsData?.hosts) return null;
|
|
32
|
-
return hostsData.hosts[host]?.ssh_user || null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* tfx-spawn-{host}-{id} 형식에서 host를 추출한다.
|
|
37
|
-
* @param {string} sessionName
|
|
38
|
-
* @returns {string|null}
|
|
39
|
-
*/
|
|
40
|
-
function resolveHostFromSessionName(sessionName) {
|
|
41
|
-
if (!sessionName?.startsWith(SESSION_PREFIX)) return null;
|
|
42
|
-
const rest = sessionName.slice(SESSION_PREFIX.length);
|
|
43
|
-
const dashIdx = rest.indexOf("-");
|
|
44
|
-
if (dashIdx === -1) return rest || null;
|
|
45
|
-
return rest.slice(0, dashIdx) || null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* sessionName에서 role을 파생한다.
|
|
50
|
-
* conductor config.id가 있으면 우선 사용, 없으면 sessionName 자체.
|
|
51
|
-
*/
|
|
52
|
-
function resolveRole(configId, sessionName) {
|
|
53
|
-
return configId || sessionName || "unknown";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildPaneName(sessionName) {
|
|
57
|
-
return `remote:${sessionName}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function mapConductorStateToStatus(conductorState) {
|
|
61
|
-
return CONDUCTOR_STATE_TO_TUI_STATUS[conductorState] || "pending";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* conductor snapshot 엔트리 → TUI 워커 데이터.
|
|
66
|
-
* @param {object} entry — conductor getSnapshot() 엔트리
|
|
67
|
-
* @param {object} watcherSession — remote-watcher의 세션 레코드 (nullable)
|
|
68
|
-
* @param {object} hostsData — hosts.json 데이터
|
|
69
|
-
* @returns {object}
|
|
70
|
-
*/
|
|
71
|
-
function buildWorkerData(entry, watcherSession, hostsData) {
|
|
72
|
-
const host = entry.host || resolveHostFromSessionName(entry.id);
|
|
73
|
-
const snapshot = watcherSession?.lastOutput || "";
|
|
74
|
-
const probeLevel =
|
|
75
|
-
entry.health?.level || watcherSession?.lastProbeLevel || null;
|
|
76
|
-
|
|
77
|
-
return Object.freeze({
|
|
78
|
-
cli: entry.agent || "claude",
|
|
79
|
-
role: resolveRole(entry.id, entry.id),
|
|
80
|
-
status: mapConductorStateToStatus(entry.state),
|
|
81
|
-
host: host || "unknown",
|
|
82
|
-
remote: true,
|
|
83
|
-
sshUser: resolveSshUser(hostsData, host),
|
|
84
|
-
sessionName: entry.id,
|
|
85
|
-
snapshot,
|
|
86
|
-
conductor: Object.freeze({
|
|
87
|
-
state: entry.state,
|
|
88
|
-
restarts: entry.restarts || 0,
|
|
89
|
-
probeLevel,
|
|
90
|
-
}),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* remote-watcher 전용 엔트리 → TUI 워커 데이터 (conductor 미등록 세션).
|
|
96
|
-
*/
|
|
97
|
-
function buildWatcherOnlyWorkerData(watcherRecord, hostsData) {
|
|
98
|
-
const host =
|
|
99
|
-
watcherRecord.host || resolveHostFromSessionName(watcherRecord.sessionName);
|
|
100
|
-
|
|
101
|
-
return Object.freeze({
|
|
102
|
-
cli: "claude",
|
|
103
|
-
role: resolveRole(null, watcherRecord.sessionName),
|
|
104
|
-
status:
|
|
105
|
-
watcherRecord.state === "completed"
|
|
106
|
-
? "completed"
|
|
107
|
-
: watcherRecord.state === "failed"
|
|
108
|
-
? "failed"
|
|
109
|
-
: "running",
|
|
110
|
-
host: host || "unknown",
|
|
111
|
-
remote: true,
|
|
112
|
-
sshUser: resolveSshUser(hostsData, host),
|
|
113
|
-
sessionName: watcherRecord.sessionName,
|
|
114
|
-
snapshot: watcherRecord.lastOutput || "",
|
|
115
|
-
conductor: null,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── 팩토리 ───────────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 원격 세션 어댑터 팩토리.
|
|
123
|
-
*
|
|
124
|
-
* @param {object} opts
|
|
125
|
-
* @param {object} opts.conductor — createConductor() 인스턴스
|
|
126
|
-
* @param {object} [opts.watcher] — createRemoteWatcher() 인스턴스 (nullable)
|
|
127
|
-
* @param {object} [opts.notifier] — createNotifier() 인스턴스 (nullable)
|
|
128
|
-
* @param {string} [opts.repoRoot] — hosts fallback 탐색용 repo root override
|
|
129
|
-
* @param {number} [opts.pollMs=10000] — conductor snapshot 폴링 간격
|
|
130
|
-
* @param {object} [opts.deps] — 테스트용 의존성 주입
|
|
131
|
-
* @returns {{ start, stop, getWorkers, on, off }}
|
|
132
|
-
*/
|
|
133
|
-
export function createRemoteAdapter(opts = {}) {
|
|
134
|
-
const {
|
|
135
|
-
conductor,
|
|
136
|
-
watcher = null,
|
|
137
|
-
notifier = null,
|
|
138
|
-
pollMs = 10_000,
|
|
139
|
-
deps = {},
|
|
140
|
-
} = opts;
|
|
141
|
-
|
|
142
|
-
if (!conductor) throw new Error("conductor is required");
|
|
143
|
-
|
|
144
|
-
const loadHosts = deps.readHosts || readHosts;
|
|
145
|
-
const setIntervalFn = deps.setInterval || setInterval;
|
|
146
|
-
const clearIntervalFn = deps.clearInterval || clearInterval;
|
|
147
|
-
const _nowFn = deps.now || Date.now;
|
|
148
|
-
|
|
149
|
-
const emitter = new EventEmitter();
|
|
150
|
-
let hostsData = loadHosts(opts.repoRoot);
|
|
151
|
-
let workers = new Map();
|
|
152
|
-
let pollHandle = null;
|
|
153
|
-
let running = false;
|
|
154
|
-
|
|
155
|
-
// ── conductor stateChange 핸들러 (primary) ──
|
|
156
|
-
|
|
157
|
-
function handleStateChange({ sessionId, from, to, reason }) {
|
|
158
|
-
const snapshots = conductor.getSnapshot();
|
|
159
|
-
const entry = snapshots.find((s) => s.id === sessionId);
|
|
160
|
-
if (!entry?.remote) return;
|
|
161
|
-
|
|
162
|
-
const watcherStatus = getWatcherSession(sessionId);
|
|
163
|
-
const workerData = buildWorkerData(entry, watcherStatus, hostsData);
|
|
164
|
-
const paneName = buildPaneName(sessionId);
|
|
165
|
-
|
|
166
|
-
workers = new Map(workers);
|
|
167
|
-
workers.set(paneName, workerData);
|
|
168
|
-
|
|
169
|
-
emitter.emit("workerUpdate", { ...workerData, paneName });
|
|
170
|
-
|
|
171
|
-
if (to === STATES.COMPLETED) {
|
|
172
|
-
emitter.emit("workerCompleted", {
|
|
173
|
-
name: paneName,
|
|
174
|
-
host: workerData.host,
|
|
175
|
-
exitCode: 0,
|
|
176
|
-
});
|
|
177
|
-
notifyIfAvailable({
|
|
178
|
-
type: "completed",
|
|
179
|
-
sessionId,
|
|
180
|
-
host: workerData.host,
|
|
181
|
-
summary: `completed (${reason})`,
|
|
182
|
-
});
|
|
183
|
-
} else if (to === STATES.DEAD) {
|
|
184
|
-
emitter.emit("workerFailed", {
|
|
185
|
-
name: paneName,
|
|
186
|
-
host: workerData.host,
|
|
187
|
-
reason,
|
|
188
|
-
});
|
|
189
|
-
notifyIfAvailable({
|
|
190
|
-
type: "failed",
|
|
191
|
-
sessionId,
|
|
192
|
-
host: workerData.host,
|
|
193
|
-
summary: `dead: ${reason}`,
|
|
194
|
-
});
|
|
195
|
-
} else if (to === STATES.INPUT_WAIT) {
|
|
196
|
-
emitter.emit("workerInputWait", {
|
|
197
|
-
name: paneName,
|
|
198
|
-
host: workerData.host,
|
|
199
|
-
pattern: reason,
|
|
200
|
-
});
|
|
201
|
-
notifyIfAvailable({
|
|
202
|
-
type: "inputWait",
|
|
203
|
-
sessionId,
|
|
204
|
-
host: workerData.host,
|
|
205
|
-
summary: `input_wait: ${reason}`,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── remote-watcher 이벤트 핸들러 (supplemental) ──
|
|
211
|
-
|
|
212
|
-
function handleWatcherCompleted({ sessionName, exitCode, host }) {
|
|
213
|
-
if (isConductorTracked(sessionName)) return;
|
|
214
|
-
|
|
215
|
-
const paneName = buildPaneName(sessionName);
|
|
216
|
-
notifyIfAvailable({
|
|
217
|
-
type: "completed",
|
|
218
|
-
sessionId: sessionName,
|
|
219
|
-
host: host || resolveHostFromSessionName(sessionName),
|
|
220
|
-
summary: `exit ${exitCode ?? 0}`,
|
|
221
|
-
});
|
|
222
|
-
emitter.emit("workerCompleted", {
|
|
223
|
-
name: paneName,
|
|
224
|
-
host: host || resolveHostFromSessionName(sessionName),
|
|
225
|
-
exitCode: exitCode ?? 0,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function handleWatcherFailed({ sessionName, reason, host }) {
|
|
230
|
-
if (isConductorTracked(sessionName)) return;
|
|
231
|
-
|
|
232
|
-
const paneName = buildPaneName(sessionName);
|
|
233
|
-
notifyIfAvailable({
|
|
234
|
-
type: "failed",
|
|
235
|
-
sessionId: sessionName,
|
|
236
|
-
host: host || resolveHostFromSessionName(sessionName),
|
|
237
|
-
summary: reason || "session failed",
|
|
238
|
-
});
|
|
239
|
-
emitter.emit("workerFailed", {
|
|
240
|
-
name: paneName,
|
|
241
|
-
host: host || resolveHostFromSessionName(sessionName),
|
|
242
|
-
reason: reason || "session failed",
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function handleWatcherInputWait({ sessionName, inputWaitPattern, host }) {
|
|
247
|
-
if (isConductorTracked(sessionName)) return;
|
|
248
|
-
|
|
249
|
-
const paneName = buildPaneName(sessionName);
|
|
250
|
-
notifyIfAvailable({
|
|
251
|
-
type: "inputWait",
|
|
252
|
-
sessionId: sessionName,
|
|
253
|
-
host: host || resolveHostFromSessionName(sessionName),
|
|
254
|
-
summary: `input_wait: ${inputWaitPattern || "unknown"}`,
|
|
255
|
-
});
|
|
256
|
-
emitter.emit("workerInputWait", {
|
|
257
|
-
name: paneName,
|
|
258
|
-
host: host || resolveHostFromSessionName(sessionName),
|
|
259
|
-
pattern: inputWaitPattern || "unknown",
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── 내부 헬퍼 ──
|
|
264
|
-
|
|
265
|
-
function isConductorTracked(sessionName) {
|
|
266
|
-
const snapshots = conductor.getSnapshot();
|
|
267
|
-
return snapshots.some((s) => s.id === sessionName && s.remote);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function getWatcherSession(sessionName) {
|
|
271
|
-
if (!watcher) return null;
|
|
272
|
-
const status = watcher.getStatus();
|
|
273
|
-
return status.sessions?.[sessionName] || null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function notifyIfAvailable(event) {
|
|
277
|
-
if (!notifier) return;
|
|
278
|
-
try {
|
|
279
|
-
notifier.notify(event);
|
|
280
|
-
} catch {
|
|
281
|
-
// notify 실패는 adapter를 중단시키지 않는다
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* conductor snapshot을 폴링하여 모든 원격 워커를 갱신.
|
|
287
|
-
*/
|
|
288
|
-
function pollConductorSnapshot() {
|
|
289
|
-
const snapshots = conductor.getSnapshot();
|
|
290
|
-
const nextWorkers = new Map(workers);
|
|
291
|
-
const conductorSessionIds = new Set();
|
|
292
|
-
|
|
293
|
-
for (const entry of snapshots) {
|
|
294
|
-
if (!entry.remote) continue;
|
|
295
|
-
conductorSessionIds.add(entry.id);
|
|
296
|
-
|
|
297
|
-
const watcherSession = getWatcherSession(entry.id);
|
|
298
|
-
const workerData = buildWorkerData(entry, watcherSession, hostsData);
|
|
299
|
-
const paneName = buildPaneName(entry.id);
|
|
300
|
-
nextWorkers.set(paneName, workerData);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// watcher-only 세션 (conductor 미등록)
|
|
304
|
-
if (watcher) {
|
|
305
|
-
const watcherStatus = watcher.getStatus();
|
|
306
|
-
for (const [sessionName, record] of Object.entries(
|
|
307
|
-
watcherStatus.sessions || {},
|
|
308
|
-
)) {
|
|
309
|
-
if (conductorSessionIds.has(sessionName)) continue;
|
|
310
|
-
const paneName = buildPaneName(sessionName);
|
|
311
|
-
const workerData = buildWatcherOnlyWorkerData(record, hostsData);
|
|
312
|
-
nextWorkers.set(paneName, workerData);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
workers = nextWorkers;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ── 공개 API ───────────────────────────────────────────────────────────────
|
|
320
|
-
|
|
321
|
-
function start() {
|
|
322
|
-
if (running) return;
|
|
323
|
-
running = true;
|
|
324
|
-
|
|
325
|
-
// hosts.json 리로드
|
|
326
|
-
hostsData = loadHosts(opts.repoRoot);
|
|
327
|
-
|
|
328
|
-
// conductor stateChange 구독
|
|
329
|
-
conductor.on("stateChange", handleStateChange);
|
|
330
|
-
|
|
331
|
-
// watcher 이벤트 구독
|
|
332
|
-
if (watcher) {
|
|
333
|
-
watcher.on("sessionCompleted", handleWatcherCompleted);
|
|
334
|
-
watcher.on("sessionFailed", handleWatcherFailed);
|
|
335
|
-
watcher.on("sessionInputWait", handleWatcherInputWait);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// 초기 snapshot 로드
|
|
339
|
-
pollConductorSnapshot();
|
|
340
|
-
|
|
341
|
-
// 주기적 폴링 (snapshot 갱신용 — stateChange가 primary)
|
|
342
|
-
pollHandle = setIntervalFn(() => {
|
|
343
|
-
pollConductorSnapshot();
|
|
344
|
-
}, pollMs);
|
|
345
|
-
pollHandle?.unref?.();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function stop() {
|
|
349
|
-
if (!running) return;
|
|
350
|
-
running = false;
|
|
351
|
-
|
|
352
|
-
conductor.off("stateChange", handleStateChange);
|
|
353
|
-
|
|
354
|
-
if (watcher) {
|
|
355
|
-
watcher.off("sessionCompleted", handleWatcherCompleted);
|
|
356
|
-
watcher.off("sessionFailed", handleWatcherFailed);
|
|
357
|
-
watcher.off("sessionInputWait", handleWatcherInputWait);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (pollHandle) {
|
|
361
|
-
clearIntervalFn(pollHandle);
|
|
362
|
-
pollHandle = null;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function getWorkers() {
|
|
367
|
-
return new Map(workers);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return Object.freeze({
|
|
371
|
-
start,
|
|
372
|
-
stop,
|
|
373
|
-
getWorkers,
|
|
374
|
-
on: emitter.on.bind(emitter),
|
|
375
|
-
off: emitter.off.bind(emitter),
|
|
376
|
-
});
|
|
377
|
-
}
|
package/hub/team/tui-synapse.mjs
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
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,79 +0,0 @@
|
|
|
1
|
-
// hub/team/worker-completion-validator.mjs — Enforce worker completion schema.
|
|
2
|
-
// Issue #115 Lane 1 / F7_worker_did_not_commit.
|
|
3
|
-
//
|
|
4
|
-
// A shard worker that reports `status: "ok"` MUST include a non-empty
|
|
5
|
-
// `commits_made` array. Non-ok statuses are valid terminal reports, but they
|
|
6
|
-
// are not allowed to masquerade as successful completion.
|
|
7
|
-
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
|
|
12
|
-
import Ajv from "ajv";
|
|
13
|
-
|
|
14
|
-
const schemaPath = join(
|
|
15
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
16
|
-
"schemas",
|
|
17
|
-
"worker-completion.json",
|
|
18
|
-
);
|
|
19
|
-
const schema = JSON.parse(readFileSync(schemaPath, "utf8"));
|
|
20
|
-
|
|
21
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
22
|
-
const compiled = ajv.compile(schema);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Validate a worker completion payload against worker-completion.json.
|
|
26
|
-
* Returns `{ ok: true }` when the payload satisfies the schema, otherwise
|
|
27
|
-
* `{ ok: false, reason }` with a short human-readable reason string.
|
|
28
|
-
*
|
|
29
|
-
* @param {unknown} payload
|
|
30
|
-
* @returns {{ ok: boolean, reason?: string }}
|
|
31
|
-
*/
|
|
32
|
-
export function validateWorkerCompletion(payload) {
|
|
33
|
-
if (payload === null || typeof payload !== "object") {
|
|
34
|
-
return { ok: false, reason: "payload_not_object" };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const status = payload.status;
|
|
38
|
-
if (!["ok", "failed", "blocked"].includes(status)) {
|
|
39
|
-
return { ok: false, reason: `invalid_status:${status ?? "undefined"}` };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const schemaPayload =
|
|
43
|
-
status === "blocked" ? { ...payload, status: "failed" } : payload;
|
|
44
|
-
const valid = compiled(schemaPayload);
|
|
45
|
-
if (!valid) {
|
|
46
|
-
const firstError = compiled.errors?.[0];
|
|
47
|
-
const reason = formatError(firstError, payload);
|
|
48
|
-
return { ok: false, reason };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { ok: true };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function formatError(err, payload) {
|
|
55
|
-
if (!err) return "schema_validation_failed";
|
|
56
|
-
|
|
57
|
-
// Map Ajv keywords to short, actionable reasons.
|
|
58
|
-
if (
|
|
59
|
-
err.keyword === "required" &&
|
|
60
|
-
err.params?.missingProperty === "commits_made"
|
|
61
|
-
) {
|
|
62
|
-
return "missing_commits_made";
|
|
63
|
-
}
|
|
64
|
-
if (err.keyword === "minItems" && err.instancePath === "/commits_made") {
|
|
65
|
-
return "empty_commits_made";
|
|
66
|
-
}
|
|
67
|
-
if (err.keyword === "enum" && err.instancePath === "/status") {
|
|
68
|
-
return `invalid_status:${payload?.status ?? "undefined"}`;
|
|
69
|
-
}
|
|
70
|
-
if (err.keyword === "type" && err.instancePath === "/commits_made") {
|
|
71
|
-
return "commits_made_not_array";
|
|
72
|
-
}
|
|
73
|
-
if (err.keyword === "required" && err.params?.missingProperty === "status") {
|
|
74
|
-
return "missing_status";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const path = err.instancePath || "(root)";
|
|
78
|
-
return `${err.keyword}@${path}:${err.message}`;
|
|
79
|
-
}
|