@triflux/remote 10.50.1 → 10.51.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 -12
- package/cto/collect.mjs +16 -247
- package/cto/current.schema.json +4 -20
- package/cto/events.mjs +13 -294
- package/cto/hygiene.mjs +26 -346
- package/cto/lake-root.mjs +114 -16
- package/cto/status.mjs +33 -128
- package/hub/index.mjs +0 -8
- package/hub/store-adapter.mjs +1 -1
- package/hub/team/agent-map.json +0 -1
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- 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 +1 -37
- 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 +41 -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 +0 -3
- 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/hub-client.mjs +2 -88
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +14 -755
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +16 -403
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session.mjs +8 -119
- package/hub/team/swarm-intent.mjs +0 -198
- package/hub/team/terminal-opener.mjs +11 -35
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +8 -269
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +4 -37
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/skill-template.mjs +0 -271
- package/cto/hygiene-actions.mjs +0 -355
- package/cto/hygiene-notify.mjs +0 -170
- 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/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.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- 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/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/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- 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/tui-remote-adapter.mjs +0 -377
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/gemini-worker.mjs +0 -217
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -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,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
|
-
}
|