@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.
Files changed (134) hide show
  1. package/cto/brief.mjs +0 -20
  2. package/cto/collect.mjs +1 -200
  3. package/cto/current.schema.json +2 -23
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -187
  7. package/hub/index.mjs +0 -17
  8. package/hub/team/agent-map.json +0 -1
  9. package/hub/team/agents-row-attach.py +120 -0
  10. package/hub/team/agents-row.mjs +200 -0
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/claude-daemon-control.mjs +1 -2
  14. package/hub/team/claude-session-projection.mjs +5 -73
  15. package/hub/team/cli/commands/attach.mjs +1 -17
  16. package/hub/team/cli/commands/control.mjs +12 -38
  17. package/hub/team/cli/commands/debug.mjs +2 -39
  18. package/hub/team/cli/commands/focus.mjs +5 -31
  19. package/hub/team/cli/commands/interrupt.mjs +6 -25
  20. package/hub/team/cli/commands/kill.mjs +1 -30
  21. package/hub/team/cli/commands/list.mjs +0 -16
  22. package/hub/team/cli/commands/send.mjs +6 -26
  23. package/hub/team/cli/commands/start/index.mjs +42 -133
  24. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  25. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  26. package/hub/team/cli/commands/start/start-mux.mjs +2 -7
  27. package/hub/team/cli/commands/status.mjs +2 -57
  28. package/hub/team/cli/commands/stop.mjs +14 -19
  29. package/hub/team/cli/help.mjs +4 -5
  30. package/hub/team/cli/manifest.mjs +5 -1
  31. package/hub/team/cli/render.mjs +0 -12
  32. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  33. package/hub/team/cli/services/state-store.mjs +5 -20
  34. package/hub/team/handoff.mjs +3 -34
  35. package/hub/team/headless.mjs +108 -1296
  36. package/hub/team/orchestrator.mjs +4 -30
  37. package/hub/team/pane.mjs +6 -28
  38. package/hub/team/psmux.mjs +3 -34
  39. package/hub/team/remote-session.mjs +1 -2
  40. package/hub/team/session-context.mjs +1 -1
  41. package/hub/team/session.mjs +7 -91
  42. package/hub/team/tui-core.mjs +1 -1
  43. package/hub/team/tui-viewer.mjs +11 -58
  44. package/hub/team/tui.mjs +8 -306
  45. package/hub/team/uds-orchestrator.mjs +2 -580
  46. package/hub/team/wt-manager.mjs +2 -2
  47. package/hub/workers/factory.mjs +6 -84
  48. package/hub/workers/interface.mjs +1 -7
  49. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  50. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
  51. package/package.json +3 -8
  52. package/scripts/lib/agent-route-policy.mjs +143 -62
  53. package/scripts/lib/cli-claude.mjs +0 -14
  54. package/scripts/lib/env-probe.mjs +2 -131
  55. package/scripts/lib/gemini-profiles.mjs +2 -69
  56. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  57. package/scripts/lib/machine-profile.mjs +3 -17
  58. package/scripts/lib/mcp-filter.mjs +109 -53
  59. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  60. package/scripts/lib/mcp-health.mjs +2 -2
  61. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  62. package/scripts/lib/psmux-info.mjs +11 -59
  63. package/scripts/lib/skill-template.mjs +0 -245
  64. package/hub/hub-lifecycle.mjs +0 -248
  65. package/hub/pipe.mjs +0 -825
  66. package/hub/promote-penalties.mjs +0 -138
  67. package/hub/public/dashboard.html +0 -416
  68. package/hub/schema.sql +0 -209
  69. package/hub/server.mjs +0 -3048
  70. package/hub/store-adapter.mjs +0 -182
  71. package/hub/store.mjs +0 -1736
  72. package/hub/team/build-worker-prompt.mjs +0 -92
  73. package/hub/team/check-mcp-hub.mjs +0 -49
  74. package/hub/team/claude-native-bridge.mjs +0 -987
  75. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  76. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  77. package/hub/team/cli/services/hub-client.mjs +0 -264
  78. package/hub/team/cli/services/native-control.mjs +0 -145
  79. package/hub/team/codex-review.mjs +0 -520
  80. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  81. package/hub/team/conductor-registry.mjs +0 -121
  82. package/hub/team/conductor.mjs +0 -1529
  83. package/hub/team/consensus-meta.mjs +0 -176
  84. package/hub/team/cto-auto-collect.mjs +0 -115
  85. package/hub/team/cto-pull.mjs +0 -31
  86. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  87. package/hub/team/dashboard.mjs +0 -322
  88. package/hub/team/event-log.mjs +0 -84
  89. package/hub/team/execution-mode.mjs +0 -324
  90. package/hub/team/extract-completion-payload.mjs +0 -110
  91. package/hub/team/headless-bridge-session.mjs +0 -17
  92. package/hub/team/health-probe.mjs +0 -435
  93. package/hub/team/interactive-native-launcher.mjs +0 -119
  94. package/hub/team/interactive-native-worker.mjs +0 -80
  95. package/hub/team/interactive-tui-transport.mjs +0 -479
  96. package/hub/team/launcher-template.mjs +0 -144
  97. package/hub/team/lead-control.mjs +0 -191
  98. package/hub/team/mcp-selector.mjs +0 -145
  99. package/hub/team/native-supervisor.mjs +0 -460
  100. package/hub/team/native.mjs +0 -727
  101. package/hub/team/recovery-store.mjs +0 -149
  102. package/hub/team/remote-probe.mjs +0 -292
  103. package/hub/team/remote-watcher.mjs +0 -506
  104. package/hub/team/runtime-strategy.mjs +0 -200
  105. package/hub/team/schemas/worker-completion.json +0 -39
  106. package/hub/team/sentinel-capture.mjs +0 -127
  107. package/hub/team/session-sync.mjs +0 -316
  108. package/hub/team/swarm-cli.mjs +0 -390
  109. package/hub/team/swarm-hypervisor.mjs +0 -2206
  110. package/hub/team/swarm-intent.mjs +0 -216
  111. package/hub/team/swarm-locks.mjs +0 -335
  112. package/hub/team/swarm-planner.mjs +0 -371
  113. package/hub/team/swarm-preflight.mjs +0 -366
  114. package/hub/team/swarm-reconciler.mjs +0 -242
  115. package/hub/team/synapse-cli.mjs +0 -458
  116. package/hub/team/synapse-http.mjs +0 -311
  117. package/hub/team/synapse-registry.mjs +0 -618
  118. package/hub/team/tui-remote-adapter.mjs +0 -377
  119. package/hub/team/tui-synapse.mjs +0 -223
  120. package/hub/team/worker-completion-validator.mjs +0 -79
  121. package/hub/team/worker-sandbox.mjs +0 -125
  122. package/hub/team/worktree-lifecycle.mjs +0 -639
  123. package/hub/team/wt-templates.json +0 -43
  124. package/hub/tools.mjs +0 -900
  125. package/hub/workers/antigravity-route-worker.mjs +0 -179
  126. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  127. package/hub/workers/delegator-mcp.mjs +0 -1098
  128. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  129. package/scripts/lib/claudemd-scanner.mjs +0 -223
  130. package/scripts/lib/context.mjs +0 -67
  131. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  132. package/scripts/lib/handoff.mjs +0 -187
  133. package/scripts/lib/logger.mjs +0 -122
  134. 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
- }
@@ -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
- }