@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
|
@@ -14,17 +14,18 @@
|
|
|
14
14
|
// - prompt: |
|
|
15
15
|
// multi-line prompt text
|
|
16
16
|
|
|
17
|
-
import { readFileSync } from
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { selectHostForCapability } from "@triflux/core/hub/lib/ssh-command.mjs";
|
|
18
19
|
|
|
19
20
|
/** Shard schema defaults */
|
|
20
21
|
const SHARD_DEFAULTS = Object.freeze({
|
|
21
|
-
agent:
|
|
22
|
+
agent: "codex",
|
|
22
23
|
files: [],
|
|
23
24
|
mcp: [],
|
|
24
25
|
depends: [],
|
|
25
26
|
critical: false,
|
|
26
|
-
prompt:
|
|
27
|
-
host:
|
|
27
|
+
prompt: "",
|
|
28
|
+
host: "",
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -41,7 +42,7 @@ export function parseShards(content) {
|
|
|
41
42
|
|
|
42
43
|
function flushPrompt() {
|
|
43
44
|
if (current && promptLines.length > 0) {
|
|
44
|
-
current.prompt = promptLines.join(
|
|
45
|
+
current.prompt = promptLines.join("\n").trim();
|
|
45
46
|
promptLines = [];
|
|
46
47
|
}
|
|
47
48
|
inPrompt = false;
|
|
@@ -91,26 +92,38 @@ export function parseShards(content) {
|
|
|
91
92
|
const value = rawValue.trim();
|
|
92
93
|
|
|
93
94
|
switch (key.toLowerCase()) {
|
|
94
|
-
case
|
|
95
|
+
case "agent":
|
|
95
96
|
current.agent = value.toLowerCase();
|
|
96
97
|
break;
|
|
97
|
-
case
|
|
98
|
-
current.files = value
|
|
98
|
+
case "files":
|
|
99
|
+
current.files = value
|
|
100
|
+
.split(/,\s*/)
|
|
101
|
+
.map((f) => f.trim())
|
|
102
|
+
.filter(Boolean);
|
|
99
103
|
break;
|
|
100
|
-
case
|
|
101
|
-
current.mcp = value
|
|
104
|
+
case "mcp":
|
|
105
|
+
current.mcp = value
|
|
106
|
+
.split(/,\s*/)
|
|
107
|
+
.map((s) => s.trim())
|
|
108
|
+
.filter(Boolean);
|
|
102
109
|
break;
|
|
103
|
-
case
|
|
104
|
-
current.depends = value
|
|
110
|
+
case "depends":
|
|
111
|
+
current.depends = value
|
|
112
|
+
.split(/,\s*/)
|
|
113
|
+
.map((d) => d.trim())
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.filter(
|
|
116
|
+
(d) => !["none", "-", "—", "n/a", "없음"].includes(d.toLowerCase()),
|
|
117
|
+
);
|
|
105
118
|
break;
|
|
106
|
-
case
|
|
119
|
+
case "critical":
|
|
107
120
|
current.critical = /^(true|yes|1)$/i.test(value);
|
|
108
121
|
break;
|
|
109
|
-
case
|
|
122
|
+
case "host":
|
|
110
123
|
current.host = value;
|
|
111
124
|
break;
|
|
112
|
-
case
|
|
113
|
-
if (value && !value.startsWith(
|
|
125
|
+
case "prompt":
|
|
126
|
+
if (value && !value.startsWith("|")) {
|
|
114
127
|
current.prompt = value;
|
|
115
128
|
} else {
|
|
116
129
|
inPrompt = true;
|
|
@@ -178,8 +191,8 @@ export function buildMcpManifest(shards) {
|
|
|
178
191
|
*/
|
|
179
192
|
export function computeMergeOrder(shards) {
|
|
180
193
|
const nameSet = new Set(shards.map((s) => s.name));
|
|
181
|
-
const adj = new Map();
|
|
182
|
-
const inDeg = new Map();
|
|
194
|
+
const adj = new Map(); // name → [dependents]
|
|
195
|
+
const inDeg = new Map(); // name → number
|
|
183
196
|
|
|
184
197
|
for (const shard of shards) {
|
|
185
198
|
adj.set(shard.name, []);
|
|
@@ -234,11 +247,25 @@ export function computeMergeOrder(shards) {
|
|
|
234
247
|
* @returns {SwarmPlan}
|
|
235
248
|
*/
|
|
236
249
|
export function planSwarm(prdPath, opts = {}) {
|
|
237
|
-
const content = opts.content || readFileSync(prdPath,
|
|
250
|
+
const content = opts.content || readFileSync(prdPath, "utf8");
|
|
238
251
|
const shards = parseShards(content);
|
|
239
252
|
|
|
240
253
|
if (shards.length === 0) {
|
|
241
|
-
throw new Error(
|
|
254
|
+
throw new Error(
|
|
255
|
+
'No shards found in PRD. Expected "## Shard: <name>" sections.',
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const emptyPromptShards = shards.filter(
|
|
260
|
+
(s) => !s.prompt || s.prompt.trim() === "",
|
|
261
|
+
);
|
|
262
|
+
if (emptyPromptShards.length > 0) {
|
|
263
|
+
const names = emptyPromptShards.map((s) => s.name).join(", ");
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Shard(s) with empty prompt: ${names}. ` +
|
|
266
|
+
`Each shard must include a non-empty "- prompt: |" block. ` +
|
|
267
|
+
`See docs/prd/_template.md for PRD format.`,
|
|
268
|
+
);
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
const { leaseMap, conflicts } = buildFileLeaseMap(shards);
|
|
@@ -246,9 +273,13 @@ export function planSwarm(prdPath, opts = {}) {
|
|
|
246
273
|
const { order: mergeOrder, cycles } = computeMergeOrder(shards);
|
|
247
274
|
|
|
248
275
|
if (cycles.length > 0) {
|
|
249
|
-
throw new Error(`Dependency cycle detected: ${cycles[0].join(
|
|
276
|
+
throw new Error(`Dependency cycle detected: ${cycles[0].join(" → ")}`);
|
|
250
277
|
}
|
|
251
278
|
|
|
279
|
+
// Auto-remote suggestion: PRD에 host 미지정 shard가 있고,
|
|
280
|
+
// hosts.json에 가용 원격 호스트가 있으면 제안 데이터를 생성한다.
|
|
281
|
+
const remoteSuggestion = buildRemoteSuggestion(shards, opts.repoRoot);
|
|
282
|
+
|
|
252
283
|
return Object.freeze({
|
|
253
284
|
shards: Object.freeze(shards.map((s) => Object.freeze({ ...s }))),
|
|
254
285
|
leaseMap,
|
|
@@ -256,5 +287,74 @@ export function planSwarm(prdPath, opts = {}) {
|
|
|
256
287
|
mergeOrder,
|
|
257
288
|
conflicts,
|
|
258
289
|
criticalShards: shards.filter((s) => s.critical).map((s) => s.name),
|
|
290
|
+
remoteSuggestion,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* PRD shard에 host가 없고 원격 호스트가 가용하면 분배 제안을 생성한다.
|
|
296
|
+
* 실제 AskUserQuestion 호출은 스킬(tfx-swarm)에서 수행. 여기는 데이터만.
|
|
297
|
+
* @param {Shard[]} shards
|
|
298
|
+
* @param {string} [repoRoot]
|
|
299
|
+
* @returns {object|null} 제안 데이터 또는 null (제안 없음)
|
|
300
|
+
*/
|
|
301
|
+
function buildRemoteSuggestion(shards, repoRoot) {
|
|
302
|
+
const localShards = shards.filter((s) => !s.host);
|
|
303
|
+
if (localShards.length === 0) return null; // 모든 shard에 host 지정됨
|
|
304
|
+
|
|
305
|
+
// 각 shard의 agent에 대해 가용 원격 호스트 조회
|
|
306
|
+
const agentTypes = [...new Set(localShards.map((s) => s.agent))];
|
|
307
|
+
const availableHosts = [];
|
|
308
|
+
|
|
309
|
+
for (const agent of agentTypes) {
|
|
310
|
+
try {
|
|
311
|
+
const hosts = selectHostForCapability(agent, repoRoot);
|
|
312
|
+
for (const h of hosts) {
|
|
313
|
+
if (!availableHosts.find((ah) => ah.name === h.name)) {
|
|
314
|
+
availableHosts.push(h);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// hosts.json 없거나 파싱 실패 → 무시
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (availableHosts.length === 0) return null;
|
|
323
|
+
|
|
324
|
+
// 분배 제안: 로컬 shard 중 절반(내림)을 원격에 배치
|
|
325
|
+
const remoteCount = Math.min(
|
|
326
|
+
Math.floor(localShards.length / 2),
|
|
327
|
+
availableHosts.length,
|
|
328
|
+
);
|
|
329
|
+
if (remoteCount === 0) return null;
|
|
330
|
+
|
|
331
|
+
// 의존성 없는 shard를 우선 원격 후보로 선택
|
|
332
|
+
const candidates = localShards
|
|
333
|
+
.filter((s) => s.depends.length === 0)
|
|
334
|
+
.concat(localShards.filter((s) => s.depends.length > 0));
|
|
335
|
+
|
|
336
|
+
const suggested = [];
|
|
337
|
+
for (let i = 0; i < remoteCount && i < candidates.length; i++) {
|
|
338
|
+
const shard = candidates[i];
|
|
339
|
+
const host = availableHosts[i % availableHosts.length];
|
|
340
|
+
suggested.push({
|
|
341
|
+
shardName: shard.name,
|
|
342
|
+
host: host.name,
|
|
343
|
+
hostDescription: host.config.description,
|
|
344
|
+
specs: host.specs,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return Object.freeze({
|
|
349
|
+
localCount: localShards.length - remoteCount,
|
|
350
|
+
remoteCount,
|
|
351
|
+
totalShards: shards.length,
|
|
352
|
+
availableHosts: availableHosts.map((h) => ({
|
|
353
|
+
name: h.name,
|
|
354
|
+
description: h.config.description,
|
|
355
|
+
specs: h.specs,
|
|
356
|
+
capabilities: h.config.capabilities,
|
|
357
|
+
})),
|
|
358
|
+
suggested: Object.freeze(suggested),
|
|
259
359
|
});
|
|
260
360
|
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// hub/team/swarm-preflight.mjs — PRD swarm preflight report/gate (#188)
|
|
2
|
+
// Predicts launch-time blockers before any worker session or worktree spawn.
|
|
3
|
+
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { readHosts, resolveHost } from "@triflux/core/hub/lib/hosts-compat.mjs";
|
|
6
|
+
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
7
|
+
import { MCP_CATALOG } from "./mcp-selector.mjs";
|
|
8
|
+
import { DEFAULT_SENSITIVE_DENY } from "./swarm-locks.mjs";
|
|
9
|
+
import { planSwarm } from "./swarm-planner.mjs";
|
|
10
|
+
|
|
11
|
+
const AGENT_COMMANDS = Object.freeze({
|
|
12
|
+
codex: "codex",
|
|
13
|
+
gemini: "gemini",
|
|
14
|
+
claude: "claude",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function unique(values) {
|
|
18
|
+
return [...new Set(values.filter(Boolean))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizePath(filePath) {
|
|
22
|
+
return String(filePath || "")
|
|
23
|
+
.replace(/\\/g, "/")
|
|
24
|
+
.replace(/^\.\//u, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isSensitiveFile(filePath, sensitiveDeny = DEFAULT_SENSITIVE_DENY) {
|
|
28
|
+
const path = normalizePath(filePath);
|
|
29
|
+
const files = new Set(sensitiveDeny.files || []);
|
|
30
|
+
const prefixes = sensitiveDeny.prefixes || [];
|
|
31
|
+
return files.has(path) || prefixes.some((prefix) => path.startsWith(prefix));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function planToJson(plan) {
|
|
35
|
+
return {
|
|
36
|
+
totalShards: plan.shards.length,
|
|
37
|
+
shards: plan.shards.map((s) => ({
|
|
38
|
+
name: s.name,
|
|
39
|
+
agent: s.agent,
|
|
40
|
+
host: s.host || null,
|
|
41
|
+
files: [...s.files],
|
|
42
|
+
mcp: [...s.mcp],
|
|
43
|
+
depends: [...s.depends],
|
|
44
|
+
critical: s.critical,
|
|
45
|
+
})),
|
|
46
|
+
leaseMap: Object.fromEntries(plan.leaseMap),
|
|
47
|
+
mcpManifest: Object.fromEntries(plan.mcpManifest),
|
|
48
|
+
mergeOrder: [...plan.mergeOrder],
|
|
49
|
+
criticalShards: [...plan.criticalShards],
|
|
50
|
+
conflicts: [...plan.conflicts],
|
|
51
|
+
remoteSuggestion: plan.remoteSuggestion,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeCheck(name, ok, details = {}) {
|
|
56
|
+
return { name, ok: Boolean(ok), ...details };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildLeaseTable(plan, repoRoot) {
|
|
60
|
+
return plan.shards.map((shard) => ({
|
|
61
|
+
shard: shard.name,
|
|
62
|
+
host: shard.host || null,
|
|
63
|
+
worktreePath: resolve(repoRoot, ".codex-swarm", `wt-${shard.name}`),
|
|
64
|
+
files: [...shard.files],
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkSensitive(plan, sensitiveDeny) {
|
|
69
|
+
const matches = [];
|
|
70
|
+
for (const shard of plan.shards) {
|
|
71
|
+
for (const file of shard.files) {
|
|
72
|
+
if (!isSensitiveFile(file, sensitiveDeny)) continue;
|
|
73
|
+
matches.push({
|
|
74
|
+
shard: shard.name,
|
|
75
|
+
file,
|
|
76
|
+
kind: "sensitive-planned-lease",
|
|
77
|
+
severity: "warning",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return makeCheck("sensitiveDeny", true, { matches });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkHosts(plan, repoRoot) {
|
|
85
|
+
const registry = readHosts(repoRoot);
|
|
86
|
+
const missing = [];
|
|
87
|
+
const capabilityWarnings = [];
|
|
88
|
+
|
|
89
|
+
for (const shard of plan.shards) {
|
|
90
|
+
if (!shard.host) continue;
|
|
91
|
+
const resolvedHost = resolveHost(shard.host, repoRoot);
|
|
92
|
+
if (!resolvedHost) {
|
|
93
|
+
missing.push({ shard: shard.name, host: shard.host });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const capabilities = resolvedHost.host.capabilities || [];
|
|
97
|
+
if (shard.agent && !capabilities.includes(shard.agent)) {
|
|
98
|
+
capabilityWarnings.push({
|
|
99
|
+
shard: shard.name,
|
|
100
|
+
host: shard.host,
|
|
101
|
+
agent: shard.agent,
|
|
102
|
+
capabilities,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return makeCheck("hosts", missing.length === 0, {
|
|
108
|
+
registryPath: registry.path,
|
|
109
|
+
missing,
|
|
110
|
+
warnings: capabilityWarnings,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function checkLocalWorkerClis(plan, deps = {}) {
|
|
115
|
+
const which = deps.whichCommand || whichCommand;
|
|
116
|
+
const missing = [];
|
|
117
|
+
const present = [];
|
|
118
|
+
const checked = new Map();
|
|
119
|
+
|
|
120
|
+
for (const shard of plan.shards) {
|
|
121
|
+
if (shard.host) continue;
|
|
122
|
+
const command = AGENT_COMMANDS[shard.agent] || shard.agent;
|
|
123
|
+
if (!command) {
|
|
124
|
+
missing.push({ shard: shard.name, agent: shard.agent, command: null });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!checked.has(command)) {
|
|
128
|
+
checked.set(command, which(command, deps.whichOptions || {}));
|
|
129
|
+
}
|
|
130
|
+
const path = checked.get(command);
|
|
131
|
+
if (path)
|
|
132
|
+
present.push({ shard: shard.name, agent: shard.agent, command, path });
|
|
133
|
+
else missing.push({ shard: shard.name, agent: shard.agent, command });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return makeCheck("workerCli", missing.length === 0, { present, missing });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function checkMcp(plan) {
|
|
140
|
+
const catalog = new Map(MCP_CATALOG.map((entry) => [entry.name, entry]));
|
|
141
|
+
const unknown = [];
|
|
142
|
+
const incompatible = [];
|
|
143
|
+
|
|
144
|
+
for (const shard of plan.shards) {
|
|
145
|
+
for (const server of shard.mcp) {
|
|
146
|
+
const entry = catalog.get(server);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
unknown.push({ shard: shard.name, server });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
["codex", "gemini"].includes(shard.agent) &&
|
|
153
|
+
!entry.cli.includes(shard.agent)
|
|
154
|
+
) {
|
|
155
|
+
incompatible.push({
|
|
156
|
+
shard: shard.name,
|
|
157
|
+
agent: shard.agent,
|
|
158
|
+
server,
|
|
159
|
+
supportedCli: [...entry.cli],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return makeCheck("mcp", incompatible.length === 0, { unknown, incompatible });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectMessages(checks) {
|
|
169
|
+
const errors = [];
|
|
170
|
+
const warnings = [];
|
|
171
|
+
|
|
172
|
+
const leases = checks.leases;
|
|
173
|
+
for (const conflict of leases.conflicts || []) {
|
|
174
|
+
errors.push(
|
|
175
|
+
`lease conflict: ${conflict.file} assigned to ${conflict.shards.join(", ")}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hosts = checks.hosts;
|
|
180
|
+
for (const missing of hosts.missing || []) {
|
|
181
|
+
errors.push(
|
|
182
|
+
`missing host: shard ${missing.shard} references ${missing.host}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
for (const warn of hosts.warnings || []) {
|
|
186
|
+
warnings.push(
|
|
187
|
+
`host capability warning: ${warn.host} does not advertise ${warn.agent} for shard ${warn.shard}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const workerCli = checks.workerCli;
|
|
192
|
+
for (const missing of workerCli.missing || []) {
|
|
193
|
+
errors.push(
|
|
194
|
+
`missing local worker CLI: shard ${missing.shard} requires ${missing.command || missing.agent}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const mcp = checks.mcp;
|
|
199
|
+
for (const item of mcp.incompatible || []) {
|
|
200
|
+
errors.push(
|
|
201
|
+
`MCP incompatibility: ${item.server} is not supported by ${item.agent} for shard ${item.shard}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
for (const item of mcp.unknown || []) {
|
|
205
|
+
warnings.push(
|
|
206
|
+
`unknown MCP server: ${item.server} requested by shard ${item.shard}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sensitive = checks.sensitiveDeny;
|
|
211
|
+
for (const item of sensitive.matches || []) {
|
|
212
|
+
warnings.push(
|
|
213
|
+
`sensitive path planned lease: ${item.file} owned by shard ${item.shard}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { errors: unique(errors), warnings: unique(warnings) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build a no-side-effect preflight report for a swarm PRD.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} prdPath
|
|
224
|
+
* @param {object} [opts]
|
|
225
|
+
* @param {string} [opts.repoRoot]
|
|
226
|
+
* @param {object} [opts.deps]
|
|
227
|
+
* @returns {object}
|
|
228
|
+
*/
|
|
229
|
+
export function runSwarmPreflight(prdPath, opts = {}) {
|
|
230
|
+
const repoRoot = opts.repoRoot || process.cwd();
|
|
231
|
+
const absPrd = resolve(prdPath);
|
|
232
|
+
const sensitiveDeny = opts.sensitiveDeny || DEFAULT_SENSITIVE_DENY;
|
|
233
|
+
let plan;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
plan = (opts.deps?.planSwarm || planSwarm)(absPrd, { repoRoot });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
verdict: "no-go",
|
|
242
|
+
prdPath: absPrd,
|
|
243
|
+
repoRoot,
|
|
244
|
+
checks: {
|
|
245
|
+
plan: makeCheck("plan", false, { error: message }),
|
|
246
|
+
},
|
|
247
|
+
errors: [message],
|
|
248
|
+
warnings: [],
|
|
249
|
+
plan: null,
|
|
250
|
+
leaseTable: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const checks = {
|
|
255
|
+
plan: makeCheck("plan", true),
|
|
256
|
+
leases: makeCheck("leases", plan.conflicts.length === 0, {
|
|
257
|
+
conflicts: [...plan.conflicts],
|
|
258
|
+
}),
|
|
259
|
+
sensitiveDeny: checkSensitive(plan, sensitiveDeny),
|
|
260
|
+
hosts: checkHosts(plan, repoRoot),
|
|
261
|
+
workerCli: checkLocalWorkerClis(plan, opts.deps),
|
|
262
|
+
mcp: checkMcp(plan),
|
|
263
|
+
};
|
|
264
|
+
const { errors, warnings } = collectMessages(checks);
|
|
265
|
+
const ok = Object.values(checks).every((check) => check.ok);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
ok,
|
|
269
|
+
verdict: ok ? "go" : "no-go",
|
|
270
|
+
prdPath: absPrd,
|
|
271
|
+
repoRoot,
|
|
272
|
+
checks,
|
|
273
|
+
errors,
|
|
274
|
+
warnings,
|
|
275
|
+
plan: planToJson(plan),
|
|
276
|
+
leaseTable: buildLeaseTable(plan, repoRoot),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function formatPreflightReport(report) {
|
|
281
|
+
const lines = [`SWARM PREFLIGHT: ${report.verdict.toUpperCase()}`];
|
|
282
|
+
|
|
283
|
+
if (report.plan) {
|
|
284
|
+
lines.push(`PRD: ${report.prdPath}`);
|
|
285
|
+
lines.push(`Shards: ${report.plan.totalShards}`);
|
|
286
|
+
lines.push("");
|
|
287
|
+
lines.push("Lease table:");
|
|
288
|
+
for (const lease of report.leaseTable) {
|
|
289
|
+
const host = lease.host ? ` @${lease.host}` : "";
|
|
290
|
+
lines.push(` - ${lease.shard}${host}`);
|
|
291
|
+
lines.push(` worktree: ${lease.worktreePath}`);
|
|
292
|
+
lines.push(
|
|
293
|
+
` files: ${lease.files.length ? lease.files.join(", ") : "(none)"}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
lines.push("");
|
|
299
|
+
lines.push("Checks:");
|
|
300
|
+
for (const [name, check] of Object.entries(report.checks || {})) {
|
|
301
|
+
lines.push(` ${check.ok ? "✓" : "✗"} ${name}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (report.errors?.length) {
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("Blocking errors:");
|
|
307
|
+
for (const error of report.errors) lines.push(` - ${error}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (report.warnings?.length) {
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push("Warnings:");
|
|
313
|
+
for (const warning of report.warnings) lines.push(` - ${warning}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return `${lines.join("\n")}\n`;
|
|
317
|
+
}
|