@triflux/remote 10.1.0 → 10.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. 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 'node:fs';
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: 'codex',
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('\n').trim();
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 'agent':
95
+ case "agent":
95
96
  current.agent = value.toLowerCase();
96
97
  break;
97
- case 'files':
98
- current.files = value.split(/,\s*/).map((f) => f.trim()).filter(Boolean);
98
+ case "files":
99
+ current.files = value
100
+ .split(/,\s*/)
101
+ .map((f) => f.trim())
102
+ .filter(Boolean);
99
103
  break;
100
- case 'mcp':
101
- current.mcp = value.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
104
+ case "mcp":
105
+ current.mcp = value
106
+ .split(/,\s*/)
107
+ .map((s) => s.trim())
108
+ .filter(Boolean);
102
109
  break;
103
- case 'depends':
104
- current.depends = value.split(/,\s*/).map((d) => d.trim()).filter(Boolean);
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 'critical':
119
+ case "critical":
107
120
  current.critical = /^(true|yes|1)$/i.test(value);
108
121
  break;
109
- case 'host':
122
+ case "host":
110
123
  current.host = value;
111
124
  break;
112
- case 'prompt':
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(); // name → [dependents]
182
- const inDeg = new Map(); // name → number
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, 'utf8');
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('No shards found in PRD. Expected "## Shard: <name>" sections.');
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,319 @@
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: "agy",
14
+ antigravity: "agy",
15
+ agy: "agy",
16
+ claude: "claude",
17
+ });
18
+
19
+ function unique(values) {
20
+ return [...new Set(values.filter(Boolean))];
21
+ }
22
+
23
+ function normalizePath(filePath) {
24
+ return String(filePath || "")
25
+ .replace(/\\/g, "/")
26
+ .replace(/^\.\//u, "");
27
+ }
28
+
29
+ function isSensitiveFile(filePath, sensitiveDeny = DEFAULT_SENSITIVE_DENY) {
30
+ const path = normalizePath(filePath);
31
+ const files = new Set(sensitiveDeny.files || []);
32
+ const prefixes = sensitiveDeny.prefixes || [];
33
+ return files.has(path) || prefixes.some((prefix) => path.startsWith(prefix));
34
+ }
35
+
36
+ function planToJson(plan) {
37
+ return {
38
+ totalShards: plan.shards.length,
39
+ shards: plan.shards.map((s) => ({
40
+ name: s.name,
41
+ agent: s.agent,
42
+ host: s.host || null,
43
+ files: [...s.files],
44
+ mcp: [...s.mcp],
45
+ depends: [...s.depends],
46
+ critical: s.critical,
47
+ })),
48
+ leaseMap: Object.fromEntries(plan.leaseMap),
49
+ mcpManifest: Object.fromEntries(plan.mcpManifest),
50
+ mergeOrder: [...plan.mergeOrder],
51
+ criticalShards: [...plan.criticalShards],
52
+ conflicts: [...plan.conflicts],
53
+ remoteSuggestion: plan.remoteSuggestion,
54
+ };
55
+ }
56
+
57
+ function makeCheck(name, ok, details = {}) {
58
+ return { name, ok: Boolean(ok), ...details };
59
+ }
60
+
61
+ function buildLeaseTable(plan, repoRoot) {
62
+ return plan.shards.map((shard) => ({
63
+ shard: shard.name,
64
+ host: shard.host || null,
65
+ worktreePath: resolve(repoRoot, ".codex-swarm", `wt-${shard.name}`),
66
+ files: [...shard.files],
67
+ }));
68
+ }
69
+
70
+ function checkSensitive(plan, sensitiveDeny) {
71
+ const matches = [];
72
+ for (const shard of plan.shards) {
73
+ for (const file of shard.files) {
74
+ if (!isSensitiveFile(file, sensitiveDeny)) continue;
75
+ matches.push({
76
+ shard: shard.name,
77
+ file,
78
+ kind: "sensitive-planned-lease",
79
+ severity: "warning",
80
+ });
81
+ }
82
+ }
83
+ return makeCheck("sensitiveDeny", true, { matches });
84
+ }
85
+
86
+ function checkHosts(plan, repoRoot) {
87
+ const registry = readHosts(repoRoot);
88
+ const missing = [];
89
+ const capabilityWarnings = [];
90
+
91
+ for (const shard of plan.shards) {
92
+ if (!shard.host) continue;
93
+ const resolvedHost = resolveHost(shard.host, repoRoot);
94
+ if (!resolvedHost) {
95
+ missing.push({ shard: shard.name, host: shard.host });
96
+ continue;
97
+ }
98
+ const capabilities = resolvedHost.host.capabilities || [];
99
+ if (shard.agent && !capabilities.includes(shard.agent)) {
100
+ capabilityWarnings.push({
101
+ shard: shard.name,
102
+ host: shard.host,
103
+ agent: shard.agent,
104
+ capabilities,
105
+ });
106
+ }
107
+ }
108
+
109
+ return makeCheck("hosts", missing.length === 0, {
110
+ registryPath: registry.path,
111
+ missing,
112
+ warnings: capabilityWarnings,
113
+ });
114
+ }
115
+
116
+ function checkLocalWorkerClis(plan, deps = {}) {
117
+ const which = deps.whichCommand || whichCommand;
118
+ const missing = [];
119
+ const present = [];
120
+ const checked = new Map();
121
+
122
+ for (const shard of plan.shards) {
123
+ if (shard.host) continue;
124
+ const command = AGENT_COMMANDS[shard.agent] || shard.agent;
125
+ if (!command) {
126
+ missing.push({ shard: shard.name, agent: shard.agent, command: null });
127
+ continue;
128
+ }
129
+ if (!checked.has(command)) {
130
+ checked.set(command, which(command, deps.whichOptions || {}));
131
+ }
132
+ const path = checked.get(command);
133
+ if (path)
134
+ present.push({ shard: shard.name, agent: shard.agent, command, path });
135
+ else missing.push({ shard: shard.name, agent: shard.agent, command });
136
+ }
137
+
138
+ return makeCheck("workerCli", missing.length === 0, { present, missing });
139
+ }
140
+
141
+ function checkMcp(plan) {
142
+ const catalog = new Map(MCP_CATALOG.map((entry) => [entry.name, entry]));
143
+ const unknown = [];
144
+ const incompatible = [];
145
+
146
+ for (const shard of plan.shards) {
147
+ for (const server of shard.mcp) {
148
+ const entry = catalog.get(server);
149
+ if (!entry) {
150
+ unknown.push({ shard: shard.name, server });
151
+ continue;
152
+ }
153
+ if (
154
+ ["codex", "gemini"].includes(shard.agent) &&
155
+ !entry.cli.includes(shard.agent)
156
+ ) {
157
+ incompatible.push({
158
+ shard: shard.name,
159
+ agent: shard.agent,
160
+ server,
161
+ supportedCli: [...entry.cli],
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ return makeCheck("mcp", incompatible.length === 0, { unknown, incompatible });
168
+ }
169
+
170
+ function collectMessages(checks) {
171
+ const errors = [];
172
+ const warnings = [];
173
+
174
+ const leases = checks.leases;
175
+ for (const conflict of leases.conflicts || []) {
176
+ errors.push(
177
+ `lease conflict: ${conflict.file} assigned to ${conflict.shards.join(", ")}`,
178
+ );
179
+ }
180
+
181
+ const hosts = checks.hosts;
182
+ for (const missing of hosts.missing || []) {
183
+ errors.push(
184
+ `missing host: shard ${missing.shard} references ${missing.host}`,
185
+ );
186
+ }
187
+ for (const warn of hosts.warnings || []) {
188
+ warnings.push(
189
+ `host capability warning: ${warn.host} does not advertise ${warn.agent} for shard ${warn.shard}`,
190
+ );
191
+ }
192
+
193
+ const workerCli = checks.workerCli;
194
+ for (const missing of workerCli.missing || []) {
195
+ errors.push(
196
+ `missing local worker CLI: shard ${missing.shard} requires ${missing.command || missing.agent}`,
197
+ );
198
+ }
199
+
200
+ const mcp = checks.mcp;
201
+ for (const item of mcp.incompatible || []) {
202
+ errors.push(
203
+ `MCP incompatibility: ${item.server} is not supported by ${item.agent} for shard ${item.shard}`,
204
+ );
205
+ }
206
+ for (const item of mcp.unknown || []) {
207
+ warnings.push(
208
+ `unknown MCP server: ${item.server} requested by shard ${item.shard}`,
209
+ );
210
+ }
211
+
212
+ const sensitive = checks.sensitiveDeny;
213
+ for (const item of sensitive.matches || []) {
214
+ warnings.push(
215
+ `sensitive path planned lease: ${item.file} owned by shard ${item.shard}`,
216
+ );
217
+ }
218
+
219
+ return { errors: unique(errors), warnings: unique(warnings) };
220
+ }
221
+
222
+ /**
223
+ * Build a no-side-effect preflight report for a swarm PRD.
224
+ *
225
+ * @param {string} prdPath
226
+ * @param {object} [opts]
227
+ * @param {string} [opts.repoRoot]
228
+ * @param {object} [opts.deps]
229
+ * @returns {object}
230
+ */
231
+ export function runSwarmPreflight(prdPath, opts = {}) {
232
+ const repoRoot = opts.repoRoot || process.cwd();
233
+ const absPrd = resolve(prdPath);
234
+ const sensitiveDeny = opts.sensitiveDeny || DEFAULT_SENSITIVE_DENY;
235
+ let plan;
236
+
237
+ try {
238
+ plan = (opts.deps?.planSwarm || planSwarm)(absPrd, { repoRoot });
239
+ } catch (error) {
240
+ const message = error instanceof Error ? error.message : String(error);
241
+ return {
242
+ ok: false,
243
+ verdict: "no-go",
244
+ prdPath: absPrd,
245
+ repoRoot,
246
+ checks: {
247
+ plan: makeCheck("plan", false, { error: message }),
248
+ },
249
+ errors: [message],
250
+ warnings: [],
251
+ plan: null,
252
+ leaseTable: [],
253
+ };
254
+ }
255
+
256
+ const checks = {
257
+ plan: makeCheck("plan", true),
258
+ leases: makeCheck("leases", plan.conflicts.length === 0, {
259
+ conflicts: [...plan.conflicts],
260
+ }),
261
+ sensitiveDeny: checkSensitive(plan, sensitiveDeny),
262
+ hosts: checkHosts(plan, repoRoot),
263
+ workerCli: checkLocalWorkerClis(plan, opts.deps),
264
+ mcp: checkMcp(plan),
265
+ };
266
+ const { errors, warnings } = collectMessages(checks);
267
+ const ok = Object.values(checks).every((check) => check.ok);
268
+
269
+ return {
270
+ ok,
271
+ verdict: ok ? "go" : "no-go",
272
+ prdPath: absPrd,
273
+ repoRoot,
274
+ checks,
275
+ errors,
276
+ warnings,
277
+ plan: planToJson(plan),
278
+ leaseTable: buildLeaseTable(plan, repoRoot),
279
+ };
280
+ }
281
+
282
+ export function formatPreflightReport(report) {
283
+ const lines = [`SWARM PREFLIGHT: ${report.verdict.toUpperCase()}`];
284
+
285
+ if (report.plan) {
286
+ lines.push(`PRD: ${report.prdPath}`);
287
+ lines.push(`Shards: ${report.plan.totalShards}`);
288
+ lines.push("");
289
+ lines.push("Lease table:");
290
+ for (const lease of report.leaseTable) {
291
+ const host = lease.host ? ` @${lease.host}` : "";
292
+ lines.push(` - ${lease.shard}${host}`);
293
+ lines.push(` worktree: ${lease.worktreePath}`);
294
+ lines.push(
295
+ ` files: ${lease.files.length ? lease.files.join(", ") : "(none)"}`,
296
+ );
297
+ }
298
+ }
299
+
300
+ lines.push("");
301
+ lines.push("Checks:");
302
+ for (const [name, check] of Object.entries(report.checks || {})) {
303
+ lines.push(` ${check.ok ? "✓" : "✗"} ${name}`);
304
+ }
305
+
306
+ if (report.errors?.length) {
307
+ lines.push("");
308
+ lines.push("Blocking errors:");
309
+ for (const error of report.errors) lines.push(` - ${error}`);
310
+ }
311
+
312
+ if (report.warnings?.length) {
313
+ lines.push("");
314
+ lines.push("Warnings:");
315
+ for (const warning of report.warnings) lines.push(` - ${warning}`);
316
+ }
317
+
318
+ return `${lines.join("\n")}\n`;
319
+ }