@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,371 +0,0 @@
|
|
|
1
|
-
// hub/team/swarm-planner.mjs — PRD → Swarm execution plan generator
|
|
2
|
-
// Parses a PRD markdown document into shards (units of work), each with:
|
|
3
|
-
// - file-lease-map: files the shard is allowed to modify
|
|
4
|
-
// - MCP manifest: MCP servers the shard needs
|
|
5
|
-
// - mergeOrder: topological order for integrating results
|
|
6
|
-
//
|
|
7
|
-
// PRD format expected:
|
|
8
|
-
// ## Shard: <name>
|
|
9
|
-
// - agent: codex|gemini|claude
|
|
10
|
-
// - files: path/a.mjs, path/b.mjs
|
|
11
|
-
// - mcp: server1, server2
|
|
12
|
-
// - depends: shard-name-1, shard-name-2
|
|
13
|
-
// - critical: true|false
|
|
14
|
-
// - redundancy: false|fallback|true|same-cli
|
|
15
|
-
// - prompt: |
|
|
16
|
-
// multi-line prompt text
|
|
17
|
-
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
19
|
-
import { selectHostForCapability } from "@triflux/core/hub/lib/ssh-command.mjs";
|
|
20
|
-
|
|
21
|
-
/** Shard schema defaults */
|
|
22
|
-
const SHARD_DEFAULTS = Object.freeze({
|
|
23
|
-
agent: "codex",
|
|
24
|
-
files: [],
|
|
25
|
-
mcp: [],
|
|
26
|
-
depends: [],
|
|
27
|
-
critical: false,
|
|
28
|
-
redundancy: false,
|
|
29
|
-
prompt: "",
|
|
30
|
-
host: "",
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parse a PRD markdown into shard definitions.
|
|
35
|
-
* @param {string} content — PRD markdown content
|
|
36
|
-
* @returns {Shard[]}
|
|
37
|
-
*/
|
|
38
|
-
export function parseShards(content) {
|
|
39
|
-
const lines = content.split(/\r?\n/);
|
|
40
|
-
const shards = [];
|
|
41
|
-
let current = null;
|
|
42
|
-
let inPrompt = false;
|
|
43
|
-
let promptLines = [];
|
|
44
|
-
|
|
45
|
-
function flushPrompt() {
|
|
46
|
-
if (current && promptLines.length > 0) {
|
|
47
|
-
current.prompt = promptLines.join("\n").trim();
|
|
48
|
-
promptLines = [];
|
|
49
|
-
}
|
|
50
|
-
inPrompt = false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function flushShard() {
|
|
54
|
-
flushPrompt();
|
|
55
|
-
if (current) {
|
|
56
|
-
shards.push({ ...SHARD_DEFAULTS, ...current });
|
|
57
|
-
current = null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (const line of lines) {
|
|
62
|
-
// New shard header: ## Shard: <name>
|
|
63
|
-
const shardMatch = line.match(/^##\s+Shard:\s*(.+)$/i);
|
|
64
|
-
if (shardMatch) {
|
|
65
|
-
flushShard();
|
|
66
|
-
current = { name: shardMatch[1].trim() };
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Non-shard heading ends current shard (e.g. ## Notes)
|
|
71
|
-
if (/^##\s+/.test(line) && !line.match(/^##\s+Shard:/i)) {
|
|
72
|
-
flushShard();
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!current) continue;
|
|
77
|
-
|
|
78
|
-
// Prompt block continuation
|
|
79
|
-
if (inPrompt) {
|
|
80
|
-
if (/^- \w+:/i.test(line)) {
|
|
81
|
-
flushPrompt();
|
|
82
|
-
// fall through to field parsing
|
|
83
|
-
} else {
|
|
84
|
-
promptLines.push(line);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Field parsing: - key: value
|
|
90
|
-
const fieldMatch = line.match(/^-\s+(\w+):\s*(.*)$/i);
|
|
91
|
-
if (!fieldMatch) continue;
|
|
92
|
-
|
|
93
|
-
const [, key, rawValue] = fieldMatch;
|
|
94
|
-
const value = rawValue.trim();
|
|
95
|
-
|
|
96
|
-
switch (key.toLowerCase()) {
|
|
97
|
-
case "agent":
|
|
98
|
-
current.agent = value.toLowerCase();
|
|
99
|
-
break;
|
|
100
|
-
case "files":
|
|
101
|
-
current.files = value
|
|
102
|
-
.split(/,\s*/)
|
|
103
|
-
.map((f) => f.trim())
|
|
104
|
-
.filter(Boolean);
|
|
105
|
-
break;
|
|
106
|
-
case "mcp":
|
|
107
|
-
current.mcp = value
|
|
108
|
-
.split(/,\s*/)
|
|
109
|
-
.map((s) => s.trim())
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
break;
|
|
112
|
-
case "depends":
|
|
113
|
-
current.depends = value
|
|
114
|
-
.split(/,\s*/)
|
|
115
|
-
.map((d) => d.trim())
|
|
116
|
-
.filter(Boolean)
|
|
117
|
-
.filter(
|
|
118
|
-
(d) => !["none", "-", "—", "n/a", "없음"].includes(d.toLowerCase()),
|
|
119
|
-
);
|
|
120
|
-
break;
|
|
121
|
-
case "critical":
|
|
122
|
-
current.critical = /^(true|yes|1)$/i.test(value);
|
|
123
|
-
break;
|
|
124
|
-
case "redundancy":
|
|
125
|
-
if (/^(true|yes|1|fallback)$/i.test(value)) {
|
|
126
|
-
current.redundancy = "fallback";
|
|
127
|
-
} else if (/^(same-cli|same_cli|same|primary)$/i.test(value)) {
|
|
128
|
-
current.redundancy = "same-cli";
|
|
129
|
-
} else {
|
|
130
|
-
current.redundancy = false;
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
case "host":
|
|
134
|
-
current.host = value;
|
|
135
|
-
break;
|
|
136
|
-
case "prompt":
|
|
137
|
-
if (value && !value.startsWith("|")) {
|
|
138
|
-
current.prompt = value;
|
|
139
|
-
} else {
|
|
140
|
-
inPrompt = true;
|
|
141
|
-
promptLines = [];
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
default:
|
|
145
|
-
// store unknown fields as-is
|
|
146
|
-
current[key] = value;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
flushShard();
|
|
151
|
-
return shards;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Build file-lease-map from shards.
|
|
156
|
-
* Maps each shard name to its allowed files.
|
|
157
|
-
* Detects conflicting file assignments across shards.
|
|
158
|
-
* @param {Shard[]} shards
|
|
159
|
-
* @returns {{ leaseMap: Map<string, string[]>, conflicts: Array<{ file: string, shards: string[] }> }}
|
|
160
|
-
*/
|
|
161
|
-
export function buildFileLeaseMap(shards) {
|
|
162
|
-
const leaseMap = new Map();
|
|
163
|
-
const fileOwners = new Map(); // file → [shard names]
|
|
164
|
-
|
|
165
|
-
for (const shard of shards) {
|
|
166
|
-
leaseMap.set(shard.name, [...shard.files]);
|
|
167
|
-
for (const file of shard.files) {
|
|
168
|
-
const owners = fileOwners.get(file) || [];
|
|
169
|
-
owners.push(shard.name);
|
|
170
|
-
fileOwners.set(file, owners);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const conflicts = [];
|
|
175
|
-
for (const [file, owners] of fileOwners) {
|
|
176
|
-
if (owners.length > 1) {
|
|
177
|
-
conflicts.push({ file, shards: owners });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { leaseMap, conflicts };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Build MCP manifest from shards.
|
|
186
|
-
* Maps each shard name to its required MCP servers.
|
|
187
|
-
* @param {Shard[]} shards
|
|
188
|
-
* @returns {Map<string, string[]>}
|
|
189
|
-
*/
|
|
190
|
-
export function buildMcpManifest(shards) {
|
|
191
|
-
const manifest = new Map();
|
|
192
|
-
for (const shard of shards) {
|
|
193
|
-
manifest.set(shard.name, [...shard.mcp]);
|
|
194
|
-
}
|
|
195
|
-
return manifest;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Compute topological merge order based on shard dependencies.
|
|
200
|
-
* @param {Shard[]} shards
|
|
201
|
-
* @returns {{ order: string[], cycles: string[][] }}
|
|
202
|
-
*/
|
|
203
|
-
export function computeMergeOrder(shards) {
|
|
204
|
-
const nameSet = new Set(shards.map((s) => s.name));
|
|
205
|
-
const adj = new Map(); // name → [dependents]
|
|
206
|
-
const inDeg = new Map(); // name → number
|
|
207
|
-
|
|
208
|
-
for (const shard of shards) {
|
|
209
|
-
adj.set(shard.name, []);
|
|
210
|
-
inDeg.set(shard.name, 0);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
for (const shard of shards) {
|
|
214
|
-
for (const dep of shard.depends) {
|
|
215
|
-
if (!nameSet.has(dep)) continue; // ignore unknown deps
|
|
216
|
-
adj.get(dep).push(shard.name);
|
|
217
|
-
inDeg.set(shard.name, inDeg.get(shard.name) + 1);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Kahn's algorithm
|
|
222
|
-
const queue = [];
|
|
223
|
-
for (const [name, deg] of inDeg) {
|
|
224
|
-
if (deg === 0) queue.push(name);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const order = [];
|
|
228
|
-
while (queue.length > 0) {
|
|
229
|
-
// stable sort: alphabetical among same-level nodes
|
|
230
|
-
queue.sort();
|
|
231
|
-
const node = queue.shift();
|
|
232
|
-
order.push(node);
|
|
233
|
-
|
|
234
|
-
for (const next of adj.get(node)) {
|
|
235
|
-
const newDeg = inDeg.get(next) - 1;
|
|
236
|
-
inDeg.set(next, newDeg);
|
|
237
|
-
if (newDeg === 0) queue.push(next);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Detect cycles (nodes not in order)
|
|
242
|
-
const cycles = [];
|
|
243
|
-
if (order.length < shards.length) {
|
|
244
|
-
const remaining = shards
|
|
245
|
-
.filter((s) => !order.includes(s.name))
|
|
246
|
-
.map((s) => s.name);
|
|
247
|
-
cycles.push(remaining);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return { order, cycles };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Full planning pipeline: parse PRD → build plan.
|
|
255
|
-
* @param {string} prdPath — path to PRD markdown file
|
|
256
|
-
* @param {object} [opts]
|
|
257
|
-
* @param {string} [opts.content] — PRD content (if provided, prdPath is ignored)
|
|
258
|
-
* @returns {SwarmPlan}
|
|
259
|
-
*/
|
|
260
|
-
export function planSwarm(prdPath, opts = {}) {
|
|
261
|
-
const content = opts.content || readFileSync(prdPath, "utf8");
|
|
262
|
-
const shards = parseShards(content);
|
|
263
|
-
|
|
264
|
-
if (shards.length === 0) {
|
|
265
|
-
throw new Error(
|
|
266
|
-
'No shards found in PRD. Expected "## Shard: <name>" sections.',
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const emptyPromptShards = shards.filter(
|
|
271
|
-
(s) => !s.prompt || s.prompt.trim() === "",
|
|
272
|
-
);
|
|
273
|
-
if (emptyPromptShards.length > 0) {
|
|
274
|
-
const names = emptyPromptShards.map((s) => s.name).join(", ");
|
|
275
|
-
throw new Error(
|
|
276
|
-
`Shard(s) with empty prompt: ${names}. ` +
|
|
277
|
-
`Each shard must include a non-empty "- prompt: |" block. ` +
|
|
278
|
-
`See docs/prd/_template.md for PRD format.`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const { leaseMap, conflicts } = buildFileLeaseMap(shards);
|
|
283
|
-
const mcpManifest = buildMcpManifest(shards);
|
|
284
|
-
const { order: mergeOrder, cycles } = computeMergeOrder(shards);
|
|
285
|
-
|
|
286
|
-
if (cycles.length > 0) {
|
|
287
|
-
throw new Error(`Dependency cycle detected: ${cycles[0].join(" → ")}`);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Auto-remote suggestion: PRD에 host 미지정 shard가 있고,
|
|
291
|
-
// hosts.json에 가용 원격 호스트가 있으면 제안 데이터를 생성한다.
|
|
292
|
-
const remoteSuggestion = buildRemoteSuggestion(shards, opts.repoRoot);
|
|
293
|
-
|
|
294
|
-
return Object.freeze({
|
|
295
|
-
shards: Object.freeze(shards.map((s) => Object.freeze({ ...s }))),
|
|
296
|
-
leaseMap,
|
|
297
|
-
mcpManifest,
|
|
298
|
-
mergeOrder,
|
|
299
|
-
conflicts,
|
|
300
|
-
criticalShards: shards.filter((s) => s.critical).map((s) => s.name),
|
|
301
|
-
remoteSuggestion,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* PRD shard에 host가 없고 원격 호스트가 가용하면 분배 제안을 생성한다.
|
|
307
|
-
* 실제 AskUserQuestion 호출은 스킬(tfx-swarm)에서 수행. 여기는 데이터만.
|
|
308
|
-
* @param {Shard[]} shards
|
|
309
|
-
* @param {string} [repoRoot]
|
|
310
|
-
* @returns {object|null} 제안 데이터 또는 null (제안 없음)
|
|
311
|
-
*/
|
|
312
|
-
function buildRemoteSuggestion(shards, repoRoot) {
|
|
313
|
-
const localShards = shards.filter((s) => !s.host);
|
|
314
|
-
if (localShards.length === 0) return null; // 모든 shard에 host 지정됨
|
|
315
|
-
|
|
316
|
-
// 각 shard의 agent에 대해 가용 원격 호스트 조회
|
|
317
|
-
const agentTypes = [...new Set(localShards.map((s) => s.agent))];
|
|
318
|
-
const availableHosts = [];
|
|
319
|
-
|
|
320
|
-
for (const agent of agentTypes) {
|
|
321
|
-
try {
|
|
322
|
-
const hosts = selectHostForCapability(agent, repoRoot);
|
|
323
|
-
for (const h of hosts) {
|
|
324
|
-
if (!availableHosts.find((ah) => ah.name === h.name)) {
|
|
325
|
-
availableHosts.push(h);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
} catch {
|
|
329
|
-
// hosts.json 없거나 파싱 실패 → 무시
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (availableHosts.length === 0) return null;
|
|
334
|
-
|
|
335
|
-
// 분배 제안: 로컬 shard 중 절반(내림)을 원격에 배치
|
|
336
|
-
const remoteCount = Math.min(
|
|
337
|
-
Math.floor(localShards.length / 2),
|
|
338
|
-
availableHosts.length,
|
|
339
|
-
);
|
|
340
|
-
if (remoteCount === 0) return null;
|
|
341
|
-
|
|
342
|
-
// 의존성 없는 shard를 우선 원격 후보로 선택
|
|
343
|
-
const candidates = localShards
|
|
344
|
-
.filter((s) => s.depends.length === 0)
|
|
345
|
-
.concat(localShards.filter((s) => s.depends.length > 0));
|
|
346
|
-
|
|
347
|
-
const suggested = [];
|
|
348
|
-
for (let i = 0; i < remoteCount && i < candidates.length; i++) {
|
|
349
|
-
const shard = candidates[i];
|
|
350
|
-
const host = availableHosts[i % availableHosts.length];
|
|
351
|
-
suggested.push({
|
|
352
|
-
shardName: shard.name,
|
|
353
|
-
host: host.name,
|
|
354
|
-
hostDescription: host.config.description,
|
|
355
|
-
specs: host.specs,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return Object.freeze({
|
|
360
|
-
localCount: localShards.length - remoteCount,
|
|
361
|
-
remoteCount,
|
|
362
|
-
totalShards: shards.length,
|
|
363
|
-
availableHosts: availableHosts.map((h) => ({
|
|
364
|
-
name: h.name,
|
|
365
|
-
description: h.config.description,
|
|
366
|
-
specs: h.specs,
|
|
367
|
-
capabilities: h.config.capabilities,
|
|
368
|
-
})),
|
|
369
|
-
suggested: Object.freeze(suggested),
|
|
370
|
-
});
|
|
371
|
-
}
|