@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
package/hub/team/shared.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// hub/team/shared.mjs — 팀 모듈 공유 유틸리티
|
|
2
|
-
// cli.mjs, dashboard.mjs 등에서 중복되던 상수/함수를 통합
|
|
3
|
-
|
|
4
|
-
// ── ANSI 색상 상수 ──
|
|
5
|
-
export const AMBER = "\x1b[38;5;214m";
|
|
6
|
-
export const GREEN = "\x1b[38;5;82m";
|
|
7
|
-
export const RED = "\x1b[38;5;196m";
|
|
8
|
-
export const GRAY = "\x1b[38;5;245m";
|
|
9
|
-
export const DIM = "\x1b[2m";
|
|
10
|
-
export const BOLD = "\x1b[1m";
|
|
11
|
-
export const RESET = "\x1b[0m";
|
|
12
|
-
export const WHITE = "\x1b[97m";
|
|
13
|
-
export const YELLOW = "\x1b[33m";
|
|
1
|
+
// hub/team/shared.mjs — 팀 모듈 공유 유틸리티
|
|
2
|
+
// cli.mjs, dashboard.mjs 등에서 중복되던 상수/함수를 통합
|
|
3
|
+
|
|
4
|
+
// ── ANSI 색상 상수 ──
|
|
5
|
+
export const AMBER = "\x1b[38;5;214m";
|
|
6
|
+
export const GREEN = "\x1b[38;5;82m";
|
|
7
|
+
export const RED = "\x1b[38;5;196m";
|
|
8
|
+
export const GRAY = "\x1b[38;5;245m";
|
|
9
|
+
export const DIM = "\x1b[2m";
|
|
10
|
+
export const BOLD = "\x1b[1m";
|
|
11
|
+
export const RESET = "\x1b[0m";
|
|
12
|
+
export const WHITE = "\x1b[97m";
|
|
13
|
+
export const YELLOW = "\x1b[33m";
|
package/hub/team/staleState.mjs
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// hub/team/staleState.mjs
|
|
2
2
|
// .omc/state 아래에 남은 stale team 상태를 탐지/정리한다.
|
|
3
3
|
|
|
4
|
-
import { existsSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
5
4
|
import { execFileSync } from "node:child_process";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
} from "node:fs";
|
|
7
13
|
import { homedir } from "node:os";
|
|
8
|
-
|
|
9
|
-
import { forceCleanupTeam } from "./nativeProxy.mjs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
15
|
import { isPidAlive } from "@triflux/core/hub/lib/process-utils.mjs";
|
|
16
|
+
import { forceCleanupTeam } from "./nativeProxy.mjs";
|
|
11
17
|
|
|
12
18
|
export const TEAM_STATE_FILE_NAME = "team-state.json";
|
|
13
19
|
export const STALE_TEAM_MAX_AGE_MS = 60 * 60 * 1000;
|
|
@@ -99,7 +105,9 @@ function normalizeProcessEntries(processEntries = []) {
|
|
|
99
105
|
|
|
100
106
|
return processEntries.map((entry) => ({
|
|
101
107
|
pid: Number(entry?.pid ?? entry?.ProcessId ?? 0),
|
|
102
|
-
command: String(
|
|
108
|
+
command: String(
|
|
109
|
+
entry?.command ?? entry?.CommandLine ?? entry?.Name ?? "",
|
|
110
|
+
).toLowerCase(),
|
|
103
111
|
}));
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -166,9 +174,11 @@ function resolveLiveness(state, sessionId, liveSessionNames, processEntries) {
|
|
|
166
174
|
|
|
167
175
|
const processTokens = findProcessTokens(state, sessionId);
|
|
168
176
|
if (processTokens.length > 0) {
|
|
169
|
-
const matched = processEntries.find(
|
|
170
|
-
entry
|
|
171
|
-
|
|
177
|
+
const matched = processEntries.find(
|
|
178
|
+
(entry) =>
|
|
179
|
+
entry.pid > 0 &&
|
|
180
|
+
processTokens.some((token) => entry.command.includes(token)),
|
|
181
|
+
);
|
|
172
182
|
if (matched) {
|
|
173
183
|
return { active: true, reason: `command:${matched.pid}` };
|
|
174
184
|
}
|
|
@@ -258,14 +268,23 @@ export function findNearestOmcStateDir(startDir = process.cwd()) {
|
|
|
258
268
|
}
|
|
259
269
|
|
|
260
270
|
export function inspectStaleOmcTeams(options = {}) {
|
|
261
|
-
const stateRoot =
|
|
271
|
+
const stateRoot =
|
|
272
|
+
options.stateRoot !== undefined
|
|
273
|
+
? options.stateRoot
|
|
274
|
+
: findNearestOmcStateDir(options.startDir || process.cwd());
|
|
262
275
|
const requestedTeamsRoot = options.teamsRoot || CLAUDE_TEAMS_ROOT;
|
|
263
|
-
const teamsRoot = safeStat(requestedTeamsRoot)?.isDirectory()
|
|
276
|
+
const teamsRoot = safeStat(requestedTeamsRoot)?.isDirectory()
|
|
277
|
+
? requestedTeamsRoot
|
|
278
|
+
: null;
|
|
264
279
|
|
|
265
280
|
const liveSessionNames = new Set(options.liveSessionNames || []);
|
|
266
|
-
const processEntries = normalizeProcessEntries(
|
|
281
|
+
const processEntries = normalizeProcessEntries(
|
|
282
|
+
options.processEntries || readProcessEntries(),
|
|
283
|
+
);
|
|
267
284
|
const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
|
|
268
|
-
const maxAgeMs = Number.isFinite(options.maxAgeMs)
|
|
285
|
+
const maxAgeMs = Number.isFinite(options.maxAgeMs)
|
|
286
|
+
? options.maxAgeMs
|
|
287
|
+
: STALE_TEAM_MAX_AGE_MS;
|
|
269
288
|
const targets = [
|
|
270
289
|
...(stateRoot ? collectTeamStateTargets(stateRoot) : []),
|
|
271
290
|
...collectClaudeTeamTargets(teamsRoot),
|
|
@@ -291,25 +310,48 @@ export function inspectStaleOmcTeams(options = {}) {
|
|
|
291
310
|
|
|
292
311
|
const fileStat = safeStat(target.stateFile);
|
|
293
312
|
const teamDirStat = target.teamDir ? safeStat(target.teamDir) : null;
|
|
294
|
-
const createdAtMs = Number.isFinite(state?.createdAt)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
??
|
|
299
|
-
??
|
|
300
|
-
??
|
|
313
|
+
const createdAtMs = Number.isFinite(state?.createdAt)
|
|
314
|
+
? state.createdAt
|
|
315
|
+
: null;
|
|
316
|
+
const startedAtMs =
|
|
317
|
+
parseStartedAtMs(state?.started_at) ??
|
|
318
|
+
parseStartedAtMs(state?.startedAt) ??
|
|
319
|
+
createdAtMs ??
|
|
320
|
+
fileStat?.mtimeMs ??
|
|
321
|
+
teamDirStat?.mtimeMs ??
|
|
322
|
+
null;
|
|
301
323
|
const ageMs = startedAtMs == null ? null : Math.max(0, nowMs - startedAtMs);
|
|
302
|
-
const teamName =
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
324
|
+
const teamName =
|
|
325
|
+
state?.teamName ||
|
|
326
|
+
state?.team_name ||
|
|
327
|
+
state?.native?.teamName ||
|
|
328
|
+
state?.name ||
|
|
329
|
+
target.teamName ||
|
|
330
|
+
null;
|
|
331
|
+
const livenessState =
|
|
332
|
+
target.scope === "claude_team"
|
|
333
|
+
? {
|
|
334
|
+
...(state || {}),
|
|
335
|
+
name: teamName,
|
|
336
|
+
teamName,
|
|
337
|
+
sessionName:
|
|
338
|
+
state?.leadSessionId ||
|
|
339
|
+
state?.lead_session_id ||
|
|
340
|
+
state?.sessionName ||
|
|
341
|
+
target.sessionId,
|
|
342
|
+
sessionId:
|
|
343
|
+
state?.leadSessionId ||
|
|
344
|
+
state?.lead_session_id ||
|
|
345
|
+
state?.sessionId ||
|
|
346
|
+
target.sessionId,
|
|
347
|
+
}
|
|
348
|
+
: state;
|
|
349
|
+
const liveness = resolveLiveness(
|
|
350
|
+
livenessState,
|
|
351
|
+
target.sessionId,
|
|
352
|
+
liveSessionNames,
|
|
353
|
+
processEntries,
|
|
354
|
+
);
|
|
313
355
|
const stale = ageMs != null && ageMs >= maxAgeMs && !liveness.active;
|
|
314
356
|
|
|
315
357
|
entries.push({
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// hub/team/swarm-cli.mjs — `tfx swarm` CLI handlers (#93)
|
|
2
|
+
// PRD를 planSwarm으로 분석하고 필요 시 createSwarmHypervisor로 실행한다.
|
|
3
|
+
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { createSwarmHypervisor } from "./swarm-hypervisor.mjs";
|
|
7
|
+
import { planSwarm } from "./swarm-planner.mjs";
|
|
8
|
+
import {
|
|
9
|
+
formatPreflightReport,
|
|
10
|
+
runSwarmPreflight,
|
|
11
|
+
} from "./swarm-preflight.mjs";
|
|
12
|
+
|
|
13
|
+
const RESET = "\u001b[0m";
|
|
14
|
+
const BOLD = "\u001b[1m";
|
|
15
|
+
const GREEN = "\u001b[92m";
|
|
16
|
+
const RED = "\u001b[91m";
|
|
17
|
+
const YELLOW = "\u001b[93m";
|
|
18
|
+
const GRAY = "\u001b[90m";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* #116-C: non-TTY background 환경에서 `tfx swarm` 실행 시 codex worker spawn 이
|
|
22
|
+
* 무한 hang 가능성이 있다 (stdin TTY 대기 또는 hub MCP lease race).
|
|
23
|
+
*
|
|
24
|
+
* Policy (v10.15+):
|
|
25
|
+
* - stdout 또는 stdin 중 하나라도 TTY → silent OK (기존 동작).
|
|
26
|
+
* - 양측 non-TTY → warning + 진행 (기본, 사용자 친화 + CI/background 호환).
|
|
27
|
+
* - `TFX_BLOCK_NON_TTY_SWARM=1` opt-out → fail-fast + 복구 경로 안내 (안전 망).
|
|
28
|
+
* - `TFX_ALLOW_NON_TTY_SWARM=1` 은 silent OK (호환 유지, warning suppress).
|
|
29
|
+
*
|
|
30
|
+
* 기존 fail-fast 정책은 첫 사용자에게 묻기 효과 (실제 user terminal 은 TTY 인데
|
|
31
|
+
* Claude Code run_in_background 같은 spawn 환경에서 child stdio 는 non-TTY).
|
|
32
|
+
* 다른 사용자도 동일 마찰 → 기본 동작을 "proceed with warning" 으로 변경.
|
|
33
|
+
*
|
|
34
|
+
* @param {{
|
|
35
|
+
* stdoutIsTTY?: boolean,
|
|
36
|
+
* stdinIsTTY?: boolean,
|
|
37
|
+
* env?: Record<string,string|undefined>,
|
|
38
|
+
* }} [deps]
|
|
39
|
+
* @returns {{ ok: boolean, optIn: boolean, warnings: string[], reason?: string }}
|
|
40
|
+
*/
|
|
41
|
+
export function assertTtyForSwarm(deps = {}) {
|
|
42
|
+
const stdoutIsTTY =
|
|
43
|
+
typeof deps.stdoutIsTTY === "boolean"
|
|
44
|
+
? deps.stdoutIsTTY
|
|
45
|
+
: Boolean(process.stdout.isTTY);
|
|
46
|
+
const stdinIsTTY =
|
|
47
|
+
typeof deps.stdinIsTTY === "boolean"
|
|
48
|
+
? deps.stdinIsTTY
|
|
49
|
+
: Boolean(process.stdin.isTTY);
|
|
50
|
+
const env = deps.env || process.env;
|
|
51
|
+
const warnings = [];
|
|
52
|
+
|
|
53
|
+
if (stdoutIsTTY || stdinIsTTY) {
|
|
54
|
+
return { ok: true, optIn: false, warnings };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 양측 non-TTY 부터 적용되는 정책.
|
|
58
|
+
if (env.TFX_BLOCK_NON_TTY_SWARM === "1") {
|
|
59
|
+
const reason =
|
|
60
|
+
"tfx swarm 이 차단됨 — non-TTY 환경 + TFX_BLOCK_NON_TTY_SWARM=1 (#116-C).\n" +
|
|
61
|
+
" 복구 경로:\n" +
|
|
62
|
+
" 1) 터미널에서 직접 실행: tfx swarm <prd>\n" +
|
|
63
|
+
" 2) TFX_BLOCK_NON_TTY_SWARM=0 (또는 unset) 으로 차단 해제 후 재시도";
|
|
64
|
+
return { ok: false, optIn: false, warnings, reason };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (env.TFX_ALLOW_NON_TTY_SWARM === "1") {
|
|
68
|
+
// 명시 opt-in — silent OK (기존 호환, warning 미출력).
|
|
69
|
+
return { ok: true, optIn: true, warnings };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
warnings.push(
|
|
73
|
+
"non-TTY 환경 감지 — codex worker spawn hang 가능성 존재 (#116-C). 차단하려면 TFX_BLOCK_NON_TTY_SWARM=1.",
|
|
74
|
+
);
|
|
75
|
+
return { ok: true, optIn: true, warnings };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseFlags(args) {
|
|
79
|
+
const flags = {
|
|
80
|
+
dryRun: false,
|
|
81
|
+
planOnly: false,
|
|
82
|
+
json: false,
|
|
83
|
+
filter: null,
|
|
84
|
+
maxRestarts: 2,
|
|
85
|
+
logsDir: null,
|
|
86
|
+
baseBranch: "main",
|
|
87
|
+
nativeBridge: true,
|
|
88
|
+
};
|
|
89
|
+
const positional = [];
|
|
90
|
+
let nativeBridgeUiEnabled = false;
|
|
91
|
+
let nativeBridgeUiDisabled = false;
|
|
92
|
+
for (let i = 0; i < args.length; i++) {
|
|
93
|
+
const a = args[i];
|
|
94
|
+
if (a === "--dry-run" || a === "--plan-only") flags.dryRun = true;
|
|
95
|
+
else if (a === "--json") flags.json = true;
|
|
96
|
+
else if (a === "--filter") flags.filter = args[++i];
|
|
97
|
+
else if (a === "--max-restarts") flags.maxRestarts = Number(args[++i]) || 2;
|
|
98
|
+
else if (a === "--logs-dir") flags.logsDir = args[++i];
|
|
99
|
+
else if (a === "--base") {
|
|
100
|
+
const v = args[++i];
|
|
101
|
+
if (!v || /\s/.test(v)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"--base requires a non-empty branch name without whitespace",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
flags.baseBranch = v;
|
|
107
|
+
} else if (a === "--native-bridge-ui") {
|
|
108
|
+
nativeBridgeUiEnabled = true;
|
|
109
|
+
flags.nativeBridge = true;
|
|
110
|
+
} else if (a === "--no-native-bridge-ui") {
|
|
111
|
+
nativeBridgeUiDisabled = true;
|
|
112
|
+
flags.nativeBridge = false;
|
|
113
|
+
} else if (a.startsWith("--")) {
|
|
114
|
+
// ignore unknown flags silently
|
|
115
|
+
} else {
|
|
116
|
+
positional.push(a);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (nativeBridgeUiEnabled && nativeBridgeUiDisabled) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"conflicting native bridge flags: --native-bridge-ui and --no-native-bridge-ui",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return { flags, positional };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function planToJson(plan) {
|
|
128
|
+
return {
|
|
129
|
+
totalShards: plan.shards.length,
|
|
130
|
+
shards: plan.shards.map((s) => ({
|
|
131
|
+
name: s.name,
|
|
132
|
+
agent: s.agent,
|
|
133
|
+
host: s.host || null,
|
|
134
|
+
files: s.files,
|
|
135
|
+
depends: s.depends,
|
|
136
|
+
critical: s.critical,
|
|
137
|
+
})),
|
|
138
|
+
leaseMap: Object.fromEntries(plan.leaseMap),
|
|
139
|
+
mcpManifest: Object.fromEntries(plan.mcpManifest),
|
|
140
|
+
mergeOrder: plan.mergeOrder,
|
|
141
|
+
criticalShards: plan.criticalShards,
|
|
142
|
+
conflicts: plan.conflicts,
|
|
143
|
+
remoteSuggestion: plan.remoteSuggestion,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printPlan(plan) {
|
|
148
|
+
console.log(`\n ${BOLD}Swarm plan${RESET}: ${plan.shards.length} shards`);
|
|
149
|
+
for (const s of plan.shards) {
|
|
150
|
+
const hostStr = s.host ? `@${s.host}` : "";
|
|
151
|
+
const critStr = s.critical ? ` ${YELLOW}[critical]${RESET}` : "";
|
|
152
|
+
const depsStr = s.depends.length > 0 ? ` ← ${s.depends.join(", ")}` : "";
|
|
153
|
+
console.log(
|
|
154
|
+
` - ${BOLD}${s.name}${RESET} [${s.agent}${hostStr}] files=${s.files.length}${critStr}${depsStr}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
console.log(`\n ${GRAY}Merge order:${RESET} ${plan.mergeOrder.join(" → ")}`);
|
|
158
|
+
if (plan.criticalShards.length > 0) {
|
|
159
|
+
console.log(
|
|
160
|
+
` ${GRAY}Critical (redundant):${RESET} ${plan.criticalShards.join(", ")}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (plan.conflicts.length > 0) {
|
|
164
|
+
console.log(`\n ${YELLOW}⚠ File conflicts:${RESET}`);
|
|
165
|
+
for (const c of plan.conflicts) {
|
|
166
|
+
console.log(` - ${c.file} → [${c.shards.join(", ")}]`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatCell(value, width) {
|
|
172
|
+
const text = value == null || value === "" ? "-" : String(value);
|
|
173
|
+
return text.padEnd(width).slice(0, width);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function formatIntegrationReport(report = []) {
|
|
177
|
+
const rows = Array.isArray(report) ? report : [];
|
|
178
|
+
const lines = [
|
|
179
|
+
"=== INTEGRATION REPORT ===",
|
|
180
|
+
"SHARD STRATEGY COMMITS FINAL_SHA NOTES",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
let integrated = 0;
|
|
184
|
+
let recoveryPatches = 0;
|
|
185
|
+
for (const row of rows) {
|
|
186
|
+
if (row.strategy !== "failed") integrated += 1;
|
|
187
|
+
if (row.recoveryPatch) recoveryPatches += 1;
|
|
188
|
+
const finalSha = row.finalSha ? String(row.finalSha).slice(0, 7) : "-";
|
|
189
|
+
const notes = row.recoveryPatch
|
|
190
|
+
? `${row.notes || "failed"}; recovery: ${row.recoveryPatch}`
|
|
191
|
+
: row.notes || "-";
|
|
192
|
+
lines.push(
|
|
193
|
+
`${formatCell(row.shard, 9)} ${formatCell(row.strategy, 14)} ${formatCell(row.commits, 8)} ${formatCell(finalSha, 10)} ${notes}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const total = rows.length;
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push(
|
|
200
|
+
`TOTAL: ${integrated}/${total} integrated, ${recoveryPatches} recovery patch${recoveryPatches === 1 ? "" : "es"} saved.`,
|
|
201
|
+
);
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* tfx swarm <prd-path> — PRD 실행
|
|
207
|
+
*/
|
|
208
|
+
export async function cmdSwarmRun(args, { json = false, deps = {} } = {}) {
|
|
209
|
+
const { flags, positional } = parseFlags(args);
|
|
210
|
+
flags.json = flags.json || json;
|
|
211
|
+
|
|
212
|
+
const prdPath = positional[0];
|
|
213
|
+
if (!prdPath) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"PRD path required. Usage: tfx swarm <prd-path> [--dry-run] [--filter <shard>] [--json] [--base <branch>]",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const absPrd = resolve(prdPath);
|
|
219
|
+
if (!existsSync(absPrd)) {
|
|
220
|
+
throw new Error(`PRD file not found: ${absPrd}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (flags.dryRun) {
|
|
224
|
+
const plan = (deps.planSwarm || planSwarm)(absPrd, {
|
|
225
|
+
repoRoot: process.cwd(),
|
|
226
|
+
});
|
|
227
|
+
if (flags.json) {
|
|
228
|
+
process.stdout.write(JSON.stringify(planToJson(plan), null, 2) + "\n");
|
|
229
|
+
} else {
|
|
230
|
+
printPlan(plan);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const preflight = (deps.runSwarmPreflight || runSwarmPreflight)(absPrd, {
|
|
236
|
+
repoRoot: process.cwd(),
|
|
237
|
+
deps,
|
|
238
|
+
});
|
|
239
|
+
if (!preflight.ok) {
|
|
240
|
+
if (flags.json) {
|
|
241
|
+
process.stdout.write(JSON.stringify(preflight, null, 2) + "\n");
|
|
242
|
+
} else {
|
|
243
|
+
process.stderr.write(formatPreflightReport(preflight));
|
|
244
|
+
}
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const plan = (deps.planSwarm || planSwarm)(absPrd, {
|
|
250
|
+
repoRoot: process.cwd(),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const ttyGate = assertTtyForSwarm();
|
|
254
|
+
for (const w of ttyGate.warnings) {
|
|
255
|
+
console.error(` ${YELLOW}⚠${RESET} ${w}`);
|
|
256
|
+
}
|
|
257
|
+
if (!ttyGate.ok) {
|
|
258
|
+
throw new Error(ttyGate.reason);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const logsDir =
|
|
262
|
+
flags.logsDir ||
|
|
263
|
+
join(process.cwd(), ".triflux", "swarm-logs", `run-${Date.now()}`);
|
|
264
|
+
const hyper = (deps.createSwarmHypervisor || createSwarmHypervisor)({
|
|
265
|
+
workdir: process.cwd(),
|
|
266
|
+
logsDir,
|
|
267
|
+
maxRestarts: flags.maxRestarts,
|
|
268
|
+
baseBranch: flags.baseBranch,
|
|
269
|
+
nativeBridge: flags.nativeBridge,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
hyper.on("shardLaunched", ({ shardName, sessionId, remote }) => {
|
|
273
|
+
const tag = remote ? ` ${GRAY}(remote)${RESET}` : "";
|
|
274
|
+
console.log(
|
|
275
|
+
` ${GREEN}▸${RESET} launched: ${shardName}${tag} [${sessionId}]`,
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
hyper.on("shardCompleted", ({ shardName, sessionId, isRedundant }) => {
|
|
279
|
+
const tag = isRedundant ? ` ${GRAY}(redundant)${RESET}` : "";
|
|
280
|
+
console.log(` ${GREEN}✓${RESET} ${shardName}${tag} [${sessionId}]`);
|
|
281
|
+
});
|
|
282
|
+
hyper.on("shardFailed", ({ shardName, failureMode, reason }) => {
|
|
283
|
+
const reasonStr = reason ? ` ${GRAY}(${reason})${RESET}` : "";
|
|
284
|
+
const modeStr = failureMode ? ` ${GRAY}[${failureMode}]${RESET}` : "";
|
|
285
|
+
console.log(` ${RED}✗${RESET} ${shardName}${modeStr}${reasonStr}`);
|
|
286
|
+
});
|
|
287
|
+
hyper.on("warning", ({ type, ...rest }) => {
|
|
288
|
+
console.error(
|
|
289
|
+
` ${YELLOW}⚠${RESET} ${type}: ${JSON.stringify(rest).slice(0, 200)}`,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.log(
|
|
294
|
+
`\n ${BOLD}Launching swarm:${RESET} ${plan.shards.length} shards (logs: ${logsDir})`,
|
|
295
|
+
);
|
|
296
|
+
const run = await hyper.launch(plan);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await run.done;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(` ${RED}integration failed:${RESET} ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const status = hyper.getStatus();
|
|
305
|
+
|
|
306
|
+
// #126: surface integration outcome in summary + exit code so silent
|
|
307
|
+
// success failures (worker exit 0 + integration_failed) can't mask state.
|
|
308
|
+
const ip = status.integrationPromise || {};
|
|
309
|
+
const integratedCount = Array.isArray(ip.integrated)
|
|
310
|
+
? ip.integrated.length
|
|
311
|
+
: 0;
|
|
312
|
+
const integrationFailureCount = Array.isArray(ip.integrationFailures)
|
|
313
|
+
? ip.integrationFailures.length
|
|
314
|
+
: 0;
|
|
315
|
+
const finalState = status.state || "unknown";
|
|
316
|
+
const isFailure =
|
|
317
|
+
status.failedShards > 0 ||
|
|
318
|
+
integrationFailureCount > 0 ||
|
|
319
|
+
finalState === "failed";
|
|
320
|
+
const stateColor = isFailure ? RED : GREEN;
|
|
321
|
+
|
|
322
|
+
console.log(
|
|
323
|
+
`\n ${BOLD}Summary:${RESET} state=${stateColor}${finalState}${RESET} completed=${status.completedShards}/${status.totalShards} failed=${status.failedShards} integrated=${integratedCount} integration_failures=${integrationFailureCount}`,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (integrationFailureCount > 0) {
|
|
327
|
+
console.log(
|
|
328
|
+
` ${RED}integration failures:${RESET} ${ip.integrationFailures.join(", ")}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (Array.isArray(ip.report) && ip.report.length > 0) {
|
|
333
|
+
console.log(`\n${formatIntegrationReport(ip.report)}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (flags.json) {
|
|
337
|
+
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await hyper.shutdown("completed");
|
|
341
|
+
if (isFailure) process.exitCode = 1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* tfx swarm plan <prd-path> — dry-run (실행 없이 계획만 출력)
|
|
346
|
+
*/
|
|
347
|
+
export async function cmdSwarmPlan(args, { json = false } = {}) {
|
|
348
|
+
return cmdSwarmRun(["--dry-run", ...args], { json });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* tfx swarm preflight <prd-path> — PRD 실행 전 no-go 리포트 출력
|
|
353
|
+
*/
|
|
354
|
+
export async function cmdSwarmPreflight(
|
|
355
|
+
args,
|
|
356
|
+
{ json = false, deps = {} } = {},
|
|
357
|
+
) {
|
|
358
|
+
const { flags, positional } = parseFlags(args);
|
|
359
|
+
flags.json = flags.json || json;
|
|
360
|
+
|
|
361
|
+
const prdPath = positional[0];
|
|
362
|
+
if (!prdPath) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
"PRD path required. Usage: tfx swarm preflight <prd-path> [--json]",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const absPrd = resolve(prdPath);
|
|
368
|
+
if (!existsSync(absPrd)) {
|
|
369
|
+
throw new Error(`PRD file not found: ${absPrd}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const report = (deps.runSwarmPreflight || runSwarmPreflight)(absPrd, {
|
|
373
|
+
repoRoot: process.cwd(),
|
|
374
|
+
deps,
|
|
375
|
+
});
|
|
376
|
+
if (flags.json) {
|
|
377
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
378
|
+
} else {
|
|
379
|
+
process.stdout.write(formatPreflightReport(report));
|
|
380
|
+
}
|
|
381
|
+
if (!report.ok) process.exitCode = 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* tfx swarm list — synapse status로 위임 (swarm 세션은 synapse registry에 등록)
|
|
386
|
+
*/
|
|
387
|
+
export async function cmdSwarmList(args, { json = false } = {}) {
|
|
388
|
+
const { cmdSynapseStatus } = await import("./synapse-cli.mjs");
|
|
389
|
+
return cmdSynapseStatus(args, { json });
|
|
390
|
+
}
|