@triflux/remote 10.43.0 → 10.44.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.
@@ -19,6 +19,8 @@ import {
19
19
  import { homedir } from "node:os";
20
20
  import { dirname, join } from "node:path";
21
21
  import { createRegistry } from "@triflux/core/mesh/mesh-registry.mjs";
22
+ import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
23
+ import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
22
24
  import { broker } from "@triflux/core/hub/account-broker.mjs";
23
25
  import { runPreflight as runCodexPreflight } from "@triflux/core/hub/codex-preflight.mjs";
24
26
  import {
@@ -608,7 +610,9 @@ export function createConductor(opts = {}) {
608
610
  cli: session.config.agent,
609
611
  prompt: session.config.prompt,
610
612
  profile: session.config.profile,
613
+ role: session.config.role,
611
614
  model: session.config.model,
615
+ env: session.config.env,
612
616
  mcpServers: session.config.mcpServers,
613
617
  excludeMcpServers,
614
618
  resolveCommand: opts.deps?.resolveCliExecutable,
@@ -902,8 +906,25 @@ export function createConductor(opts = {}) {
902
906
  } else if (agent === "gemini") {
903
907
  remoteBin = "gemini -y";
904
908
  } else {
905
- remoteBin =
906
- "codex exec -s danger-full-access --dangerously-bypass-approvals-and-sandbox";
909
+ const codexHome = session.config.env?.CODEX_HOME;
910
+ const profile = resolveNestedCodexAgentProfile(
911
+ session.config.role || "executor",
912
+ {
913
+ profileOverride: session.config.profile ?? "auto",
914
+ globalProfile:
915
+ session.config.env?.TFX_CODEX_PROFILE ??
916
+ process.env.TFX_CODEX_PROFILE,
917
+ codexHome,
918
+ },
919
+ );
920
+ const configArgs = codexProfileConfigOverrides(profile, {
921
+ codexHome,
922
+ disallowUltra: true,
923
+ enforceCanonicalProfile: true,
924
+ })
925
+ .map((override) => `-c '${override.replace(/'/g, `'\\''`)}'`)
926
+ .join(" ");
927
+ remoteBin = `codex exec${configArgs ? ` ${configArgs}` : ""} -s danger-full-access --dangerously-bypass-approvals-and-sandbox`;
907
928
  }
908
929
 
909
930
  // prompt는 stdin으로 전달 — 셸 이스케이프 문제 완전 회피
@@ -2,12 +2,7 @@
2
2
 
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, resolve as pathResolve } from "node:path";
5
-
6
- // scripts/lib is bundled into @triflux/remote itself (package.json files), so
7
- // import the local mirror relatively rather than via @triflux/core. The core
8
- // peer range (^10.0.0-alpha.1) could resolve to a build that predates this
9
- // export, which would fail at import time; the bundled copy is version-skew
10
- // safe and always carries the matching codexProfileConfigOverrides.
5
+ import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
11
6
  import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
12
7
  import { whichCommand } from "@triflux/core/hub/platform.mjs";
13
8
 
@@ -165,7 +160,17 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
165
160
  // still contains an inline [profiles.X] table (which codex re-injects on
166
161
  // config rewrite). These go straight to spawn argv (shell: false), so push
167
162
  // the raw TOML-scalar overrides without shell-quoting.
168
- for (const override of codexProfileConfigOverrides(opts.profile)) {
163
+ const codexHome = opts.env?.CODEX_HOME;
164
+ const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
165
+ profileOverride: opts.profile ?? "auto",
166
+ globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
167
+ codexHome,
168
+ });
169
+ for (const override of codexProfileConfigOverrides(codexProfile, {
170
+ codexHome,
171
+ disallowUltra: true,
172
+ enforceCanonicalProfile: true,
173
+ })) {
169
174
  args.push("-c", override);
170
175
  }
171
176
  if (Array.isArray(opts.mcpServers)) {
@@ -20,6 +20,7 @@ import net from "node:net";
20
20
  import { tmpdir } from "node:os";
21
21
  import { dirname, join, resolve } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
+ import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
23
24
  import { requestJson } from "@triflux/core/hub/bridge.mjs";
24
25
  import { escapePwshSingleQuoted } from "@triflux/core/hub/cli-adapter-base.mjs";
25
26
  import { getMaxSpawnPerSec } from "@triflux/core/hub/lib/spawn-trace.mjs";
@@ -42,15 +43,15 @@ import {
42
43
  import { startClaudeNativeBridge } from "./claude-native-bridge.mjs";
43
44
  import { removeClaudeSessionProjection } from "./claude-session-projection.mjs";
44
45
  import { resolveDashboardLayout } from "./dashboard-layout.mjs";
45
- import {
46
- createFileActivitySource,
47
- createInterventionLadder,
48
- } from "./intervention.mjs";
49
46
  import {
50
47
  formatHandoffForLead,
51
48
  HANDOFF_INSTRUCTION_SHORT,
52
49
  processHandoff,
53
50
  } from "./handoff.mjs";
51
+ import {
52
+ createFileActivitySource,
53
+ createInterventionLadder,
54
+ } from "./intervention.mjs";
54
55
  import {
55
56
  capturePsmuxPane,
56
57
  createPsmuxSession,
@@ -366,6 +367,14 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
366
367
  writeFileSync(promptFile, fullPrompt, "utf8");
367
368
  void IS_WINDOWS; // referenced for diagnostic guard chain below
368
369
 
370
+ const codexProfile =
371
+ resolvedCli === "codex"
372
+ ? resolveNestedCodexAgentProfile(opts.role || cli, {
373
+ profileOverride: opts.profile ?? "auto",
374
+ globalProfile: process.env.TFX_CODEX_PROFILE,
375
+ codexHome: process.env.CODEX_HOME,
376
+ })
377
+ : undefined;
369
378
  const backendCommand =
370
379
  resolvedCli === "antigravity"
371
380
  ? buildRouteBackedHeadlessCommand(resolvedCli, promptFile, resultFile, {
@@ -375,6 +384,10 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
375
384
  : getBackend(resolvedCli).buildArgs(fullPrompt, resultFile, {
376
385
  ...opts,
377
386
  model,
387
+ profile: codexProfile,
388
+ codexHome: process.env.CODEX_HOME,
389
+ disallowUltra: true,
390
+ enforceCanonicalProfile: true,
378
391
  promptFile,
379
392
  });
380
393
  const safeCwd =
@@ -571,7 +584,8 @@ export function createStallMonitor(paneId, resultFile, config, deps = {}) {
571
584
 
572
585
  function createHeadlessIntervention(dispatch, fallback) {
573
586
  return async (context) => {
574
- if (typeof fallback === "function") return (await fallback(context)) === true;
587
+ if (typeof fallback === "function")
588
+ return (await fallback(context)) === true;
575
589
 
576
590
  const isDaemon = context.channel === "daemon";
577
591
  const readActivitySignature = isDaemon
@@ -606,6 +620,8 @@ function createHeadlessIntervention(dispatch, fallback) {
606
620
  },
607
621
  readActivitySignature,
608
622
  deps: {
623
+ // 이 트랙이 재실행 소유자다. pane 채널은 기존 completion wrapper를 다시
624
+ // dispatch하여 token/result 계약을 보존한다.
609
625
  resumeHandler: async () => {
610
626
  if (isDaemon || !dispatch.command) return { ok: false };
611
627
  const restarted = dispatchCommand(
@@ -631,6 +647,7 @@ export function createActivityPollGuard({
631
647
  const controller = new AbortController();
632
648
  let lastMtime = 0;
633
649
  let checking = false;
650
+
634
651
  const finishIfTimedOut = async () => {
635
652
  if (checking || controller.signal.aborted) return;
636
653
  checking = true;
@@ -640,6 +657,7 @@ export function createActivityPollGuard({
640
657
  checking = false;
641
658
  }
642
659
  };
660
+
643
661
  return Object.freeze({
644
662
  signal: controller.signal,
645
663
  observe(content) {
@@ -989,6 +1007,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
989
1007
  mcp: assignment.mcp,
990
1008
  role: assignment.role,
991
1009
  model: assignment.model,
1010
+ profile: assignment.profile,
992
1011
  cwd: assignment.cwd || assignment.workdir,
993
1012
  },
994
1013
  );
@@ -1071,6 +1090,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
1071
1090
  mcp: assignment.mcp,
1072
1091
  role: assignment.role,
1073
1092
  model: assignment.model,
1093
+ profile: assignment.profile,
1074
1094
  cwd: assignment.cwd || assignment.workdir,
1075
1095
  },
1076
1096
  );
@@ -1134,6 +1154,7 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
1134
1154
  mcp: assignment.mcp,
1135
1155
  role: assignment.role,
1136
1156
  model: assignment.model,
1157
+ profile: assignment.profile,
1137
1158
  cwd: assignment.cwd || assignment.workdir,
1138
1159
  },
1139
1160
  );
@@ -1306,12 +1327,11 @@ async function awaitAll(
1306
1327
  {
1307
1328
  pollInterval: stallOpts.pollInterval,
1308
1329
  stallTimeout: stallOpts.stallTimeout,
1309
- hardCeiling:
1310
- lifecycle?.enabled
1311
- ? (stallOpts.hardCeiling ??
1312
- stallOpts.completionTimeout ??
1313
- lifecycle.hardCeilingMs)
1314
- : (stallOpts.completionTimeout ?? timeoutSec * 1000),
1330
+ hardCeiling: lifecycle?.enabled
1331
+ ? (stallOpts.hardCeiling ??
1332
+ stallOpts.completionTimeout ??
1333
+ lifecycle.hardCeilingMs)
1334
+ : (stallOpts.completionTimeout ?? timeoutSec * 1000),
1315
1335
  maxRestarts: stallOpts.maxRestarts,
1316
1336
  maxInterventions: stallOpts.maxInterventions,
1317
1337
  command: d.command,
@@ -1443,7 +1463,10 @@ export async function waitForDaemonCompletion(
1443
1463
  ? createActivityLifecycle({
1444
1464
  interventionMs: lifecycle.interventionMs,
1445
1465
  hardCeilingMs: lifecycle.hardCeilingMs,
1446
- onIntervene: createHeadlessIntervention(dispatch, lifecycle.onIntervene),
1466
+ onIntervene: createHeadlessIntervention(
1467
+ dispatch,
1468
+ lifecycle.onIntervene,
1469
+ ),
1447
1470
  })
1448
1471
  : null;
1449
1472
 
@@ -1873,11 +1896,24 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
1873
1896
  const idx = parseInt(match[1], 10) - 1;
1874
1897
  const agentId = getHeadlessWorkerAgentId(sessionName, idx);
1875
1898
  const nowMs = Date.now();
1876
- if ((lastHubBeatByWorker.get(agentId) || 0) + HUB_ACTIVITY_MIN_INTERVAL_MS > nowMs) return;
1899
+ if (
1900
+ (lastHubBeatByWorker.get(agentId) || 0) + HUB_ACTIVITY_MIN_INTERVAL_MS >
1901
+ nowMs
1902
+ )
1903
+ return;
1877
1904
  lastHubBeatByWorker.set(agentId, nowMs);
1878
- requestJson("/bridge/heartbeat", { method: "POST", body: { agent_id: agentId }, timeoutMs: 500 }).catch(() => {});
1905
+ requestJson("/bridge/heartbeat", {
1906
+ method: "POST",
1907
+ body: { agent_id: agentId },
1908
+ timeoutMs: 500,
1909
+ }).catch(() => {});
1879
1910
  const assignJobId = normalizedAssignments[idx]?.assignJobId;
1880
- if (assignJobId) requestJson("/bridge/assign/result", { method: "POST", body: { job_id: assignJobId, worker_agent: agentId, status: "running" }, timeoutMs: 500 }).catch(() => {});
1911
+ if (assignJobId)
1912
+ requestJson("/bridge/assign/result", {
1913
+ method: "POST",
1914
+ body: { job_id: assignJobId, worker_agent: agentId, status: "running" },
1915
+ timeoutMs: 500,
1916
+ }).catch(() => {});
1881
1917
  };
1882
1918
 
1883
1919
  // onProgress 예외를 삼켜 실행 흐름 보호 (onPoll과 동일 패턴)
@@ -5,11 +5,11 @@ import { statSync } from "node:fs";
5
5
  import { readdir, readlink } from "node:fs/promises";
6
6
  import { promisify } from "node:util";
7
7
  import { isPidAlive } from "@triflux/core/hub/lib/process-utils.mjs";
8
- import { killProcess } from "@triflux/core/hub/platform.mjs";
9
8
  import {
10
9
  resolveHardCeilingMs,
11
10
  resolveStallInterventionMs as resolveStallThresholdMs,
12
11
  } from "@triflux/core/hub/lib/timeout-defaults.mjs";
12
+ import { killProcess } from "@triflux/core/hub/platform.mjs";
13
13
 
14
14
  export { resolveHardCeilingMs, resolveStallThresholdMs };
15
15
 
@@ -54,12 +54,17 @@ function normalizeEvent(event, defaults = {}) {
54
54
  });
55
55
  }
56
56
 
57
- function defaultChannelConfig(name, env) {
57
+ function defaultChannelConfig(name, env, platform = process.platform) {
58
58
  switch (name) {
59
59
  case "bell":
60
- return { enabled: true };
60
+ return { enabled: envFlag(env?.TRIFLUX_NOTIFY_BELL, true) };
61
61
  case "toast":
62
- return { enabled: true };
62
+ return {
63
+ // macOS `osascript display notification` can surface Script Editor
64
+ // authorization/UI prompts. Keep it opt-in there; Windows keeps the
65
+ // historical toast default, and other platforms skip as unsupported.
66
+ enabled: envFlag(env?.TRIFLUX_NOTIFY_TOAST, platform !== "darwin"),
67
+ };
63
68
  case "webhook": {
64
69
  const url = String(env?.TRIFLUX_NOTIFY_WEBHOOK || "");
65
70
  return { enabled: Boolean(url), url };
@@ -69,8 +74,13 @@ function defaultChannelConfig(name, env) {
69
74
  }
70
75
  }
71
76
 
72
- function normalizeChannelConfig(name, value, env) {
73
- const base = defaultChannelConfig(name, env);
77
+ function envFlag(value, fallback) {
78
+ if (value === undefined || value === null || value === "") return fallback;
79
+ return !["0", "false", "no", "off"].includes(String(value).toLowerCase());
80
+ }
81
+
82
+ function normalizeChannelConfig(name, value, env, platform) {
83
+ const base = defaultChannelConfig(name, env, platform);
74
84
  const patch =
75
85
  typeof value === "boolean"
76
86
  ? { enabled: value }
@@ -103,16 +113,21 @@ function normalizeChannelConfig(name, value, env) {
103
113
  return freezeRecord(next);
104
114
  }
105
115
 
106
- function normalizeChannels(channels, env) {
116
+ function normalizeChannels(channels, env, platform) {
107
117
  const source = channels && typeof channels === "object" ? channels : {};
108
118
  const normalized = {};
109
119
  for (const name of NOTIFY_CHANNELS) {
110
- normalized[name] = normalizeChannelConfig(name, source[name], env);
120
+ normalized[name] = normalizeChannelConfig(
121
+ name,
122
+ source[name],
123
+ env,
124
+ platform,
125
+ );
111
126
  }
112
127
  return Object.freeze(normalized);
113
128
  }
114
129
 
115
- function updateChannelConfig(channels, channel, config, env) {
130
+ function updateChannelConfig(channels, channel, config, env, platform) {
116
131
  if (!NOTIFY_CHANNELS.includes(channel)) {
117
132
  throw new TypeError(`Unknown notify channel: ${channel}`);
118
133
  }
@@ -126,6 +141,7 @@ function updateChannelConfig(channels, channel, config, env) {
126
141
  ...(typeof config === "boolean" ? { enabled: config } : config || {}),
127
142
  },
128
143
  env,
144
+ platform,
129
145
  ),
130
146
  });
131
147
  }
@@ -322,7 +338,7 @@ function createNotifierInstance(channels, deps) {
322
338
 
323
339
  function setChannel(channel, config) {
324
340
  return createNotifierInstance(
325
- updateChannelConfig(channels, channel, config, deps.env),
341
+ updateChannelConfig(channels, channel, config, deps.env, deps.platform),
326
342
  deps,
327
343
  );
328
344
  }
@@ -349,17 +365,21 @@ function createNotifierInstance(channels, deps) {
349
365
  */
350
366
  export function createNotifier(opts = {}) {
351
367
  const env = opts.env || process.env;
368
+ const platform = opts.platform || process.platform;
352
369
  const deps = Object.freeze({
353
370
  env,
354
371
  stdout: opts.stdout || process.stdout,
355
372
  execFile: opts.deps?.execFile || execFile,
356
373
  fetch: opts.deps?.fetch || globalThis.fetch?.bind(globalThis),
357
- platform: opts.platform || process.platform,
374
+ platform,
358
375
  hostname: opts.hostname || os.hostname(),
359
376
  powerShellCandidates: Object.freeze(
360
377
  opts.deps?.powerShellCandidates || ["pwsh", "powershell.exe"],
361
378
  ),
362
379
  });
363
380
 
364
- return createNotifierInstance(normalizeChannels(opts.channels, env), deps);
381
+ return createNotifierInstance(
382
+ normalizeChannels(opts.channels, env, platform),
383
+ deps,
384
+ );
365
385
  }
@@ -41,13 +41,13 @@ export const MODES = Object.freeze({
41
41
  const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
42
42
 
43
43
  // Escalation chain (2026-05-27 정책):
44
- // 1. codex gpt-5.6-sol — 기존 최상위 Codex escalation 단계
44
+ // 1. codex gpt-5.6-sol max — 최난도 단일 작업용 Codex escalation 단계
45
45
  // 2. claude opus-4-8 — 최종 수단
46
46
  const DEFAULT_ESCALATION_CHAIN = Object.freeze([
47
47
  Object.freeze({
48
48
  cli: "codex",
49
49
  model: "gpt-5.6-sol",
50
- profile: "gpt56_sol_xhigh",
50
+ profile: "gpt56_sol_max",
51
51
  }),
52
52
  Object.freeze({ cli: "claude", model: "opus-4-8" }),
53
53
  ]);
@@ -295,7 +295,12 @@ function normalizeEscalationChainEntry(entry, index, source) {
295
295
  model: entry.model,
296
296
  };
297
297
  if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
298
- normalized.profile = entry.profile;
298
+ const profile = entry.profile.trim();
299
+ normalized.profile =
300
+ entry.cli === "codex" &&
301
+ (profile === "ultra" || profile === "gpt56_sol_ultra")
302
+ ? "gpt56_sol_max"
303
+ : profile;
299
304
  }
300
305
  return normalized;
301
306
  }
@@ -20,8 +20,8 @@ import {
20
20
  isDynamicRoutingEnabled,
21
21
  } from "@triflux/core/hub/dynamic-routing-engine.mjs";
22
22
  import { cleanupShardProcesses } from "@triflux/core/hub/lib/process-utils.mjs";
23
- import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
24
23
  import { getHostConfig } from "@triflux/core/hub/lib/ssh-command.mjs";
24
+ import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
25
25
  import { buildWorkerPrompt } from "./build-worker-prompt.mjs";
26
26
  import { registerSwarmShard } from "./claude-native-bridge.mjs";
27
27
  import { createConductor, STATES } from "./conductor.mjs";
@@ -678,6 +678,7 @@ export function createSwarmHypervisor(opts) {
678
678
  const config = {
679
679
  id: buildSwarmSessionId(shard),
680
680
  agent: shard.agent,
681
+ role: shard.role,
681
682
  // #125: append Completion Protocol appendix so workers emit a
682
683
  // sentinel-framed JSON payload that conductor can reliably capture.
683
684
  prompt: buildWorkerPrompt(shard.prompt, {
@@ -1964,7 +1965,8 @@ export function createSwarmHypervisor(opts) {
1964
1965
  lockRenewTimer = setInterval(() => {
1965
1966
  if (!lockManager) return;
1966
1967
  for (const shardName of workers.keys()) {
1967
- if (!completedShards.has(shardName) && !failures.has(shardName)) lockManager.renew(shardName);
1968
+ if (!completedShards.has(shardName) && !failures.has(shardName))
1969
+ lockManager.renew(shardName);
1968
1970
  }
1969
1971
  }, DEFAULT_SWARM_LOCK_TTL_MS / 2);
1970
1972
  lockRenewTimer.unref?.();
@@ -1,6 +1,7 @@
1
1
  // hub/team/swarm-preflight.mjs — PRD swarm preflight report/gate (#188)
2
2
  // Predicts launch-time blockers before any worker session or worktree spawn.
3
3
 
4
+ import { execFileSync } from "node:child_process";
4
5
  import { resolve } from "node:path";
5
6
  import { readHosts, resolveHost } from "@triflux/core/hub/lib/hosts-compat.mjs";
6
7
  import { whichCommand } from "@triflux/core/hub/platform.mjs";
@@ -83,6 +84,44 @@ function checkSensitive(plan, sensitiveDeny) {
83
84
  return makeCheck("sensitiveDeny", true, { matches });
84
85
  }
85
86
 
87
+ function parseTrackedStatus(rawStatus) {
88
+ const files = [];
89
+ const records = String(rawStatus || "").split("\0");
90
+ for (let index = 0; index < records.length; index++) {
91
+ const record = records[index];
92
+ if (record.length < 4) continue;
93
+ const status = record.slice(0, 2);
94
+ files.push(normalizePath(record.slice(3)));
95
+ if (/[RC]/u.test(status) && records[index + 1]) {
96
+ files.push(normalizePath(records[++index]));
97
+ }
98
+ }
99
+ return unique(files);
100
+ }
101
+
102
+ function checkRootDirtyLease(plan, repoRoot, deps = {}) {
103
+ const gitStatus =
104
+ deps.gitStatus ||
105
+ ((cwd) =>
106
+ execFileSync(
107
+ "git",
108
+ ["status", "--porcelain=v1", "-z", "--untracked-files=no"],
109
+ { cwd, encoding: "utf8", windowsHide: true },
110
+ ));
111
+ const dirtyFiles = parseTrackedStatus(gitStatus(repoRoot));
112
+ const leasedFiles = unique(
113
+ [...plan.leaseMap.values()].flat().map(normalizePath),
114
+ );
115
+ const leaseSet = new Set(leasedFiles);
116
+ const overlappingFiles = dirtyFiles.filter((file) => leaseSet.has(file));
117
+
118
+ return makeCheck("rootDirtyLease", overlappingFiles.length === 0, {
119
+ dirtyFiles,
120
+ leasedFiles,
121
+ overlappingFiles,
122
+ });
123
+ }
124
+
86
125
  function checkHosts(plan, repoRoot) {
87
126
  const registry = readHosts(repoRoot);
88
127
  const missing = [];
@@ -216,6 +255,13 @@ function collectMessages(checks) {
216
255
  );
217
256
  }
218
257
 
258
+ const rootDirtyLease = checks.rootDirtyLease;
259
+ if (rootDirtyLease && !rootDirtyLease.ok) {
260
+ errors.push(
261
+ `rootDir tracked changes overlap shard leases: ${rootDirtyLease.overlappingFiles.join(", ")}`,
262
+ );
263
+ }
264
+
219
265
  return { errors: unique(errors), warnings: unique(warnings) };
220
266
  }
221
267
 
@@ -258,6 +304,7 @@ export function runSwarmPreflight(prdPath, opts = {}) {
258
304
  leases: makeCheck("leases", plan.conflicts.length === 0, {
259
305
  conflicts: [...plan.conflicts],
260
306
  }),
307
+ rootDirtyLease: checkRootDirtyLease(plan, repoRoot, opts.deps),
261
308
  sensitiveDeny: checkSensitive(plan, sensitiveDeny),
262
309
  hosts: checkHosts(plan, repoRoot),
263
310
  workerCli: checkLocalWorkerClis(plan, opts.deps),