@adhdev/daemon-core 0.9.77-rc.44 → 0.9.77-rc.45

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/dist/index.mjs CHANGED
@@ -123,7 +123,33 @@ async function removeWorktree(repoRoot, worktreePath, opts = {}) {
123
123
  });
124
124
  } catch (error) {
125
125
  const stderr = typeof error.stderr === "string" ? error.stderr : "";
126
- throw new Error(`git worktree remove failed: ${stderr.trim() || error.message}`);
126
+ const stdout = typeof error.stdout === "string" ? error.stdout : "";
127
+ const detail = `${stderr}
128
+ ${stdout}
129
+ ${error.message || ""}`;
130
+ if (opts.allowSubmoduleForceFallback && SUBMODULE_WORKTREE_REMOVE_RE.test(detail)) {
131
+ try {
132
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], {
133
+ cwd: repoRoot,
134
+ encoding: "utf8",
135
+ timeout: GIT_TIMEOUT_MS,
136
+ maxBuffer: GIT_MAX_BUFFER,
137
+ windowsHide: true
138
+ });
139
+ } catch (forceError) {
140
+ const forceStderr = typeof forceError.stderr === "string" ? forceError.stderr : "";
141
+ const forceStdout = typeof forceError.stdout === "string" ? forceError.stdout : "";
142
+ throw new Error(`git worktree remove --force fallback failed: ${forceStderr.trim() || forceStdout.trim() || forceError.message}`);
143
+ }
144
+ return {
145
+ success: true,
146
+ removedPath: worktreePath,
147
+ fallback: "git_worktree_remove_force_submodule",
148
+ forced: true,
149
+ reason: "working_trees_containing_submodules"
150
+ };
151
+ }
152
+ throw new Error(`git worktree remove failed: ${stderr.trim() || stdout.trim() || error.message}`);
127
153
  }
128
154
  return { success: true, removedPath: worktreePath };
129
155
  }
@@ -173,7 +199,7 @@ async function pruneWorktrees(repoRoot) {
173
199
  } catch {
174
200
  }
175
201
  }
176
- var execFileAsync2, WORKTREE_DIR_NAME, GIT_TIMEOUT_MS, GIT_MAX_BUFFER;
202
+ var execFileAsync2, WORKTREE_DIR_NAME, GIT_TIMEOUT_MS, GIT_MAX_BUFFER, SUBMODULE_WORKTREE_REMOVE_RE;
177
203
  var init_git_worktree = __esm({
178
204
  "src/git/git-worktree.ts"() {
179
205
  "use strict";
@@ -181,6 +207,7 @@ var init_git_worktree = __esm({
181
207
  WORKTREE_DIR_NAME = ".adhdev-worktrees";
182
208
  GIT_TIMEOUT_MS = 3e4;
183
209
  GIT_MAX_BUFFER = 4 * 1024 * 1024;
210
+ SUBMODULE_WORKTREE_REMOVE_RE = /working trees containing submodules cannot be moved or removed/i;
184
211
  }
185
212
  });
186
213
 
@@ -1004,6 +1031,7 @@ __export(mesh_work_queue_exports, {
1004
1031
  enqueueTask: () => enqueueTask,
1005
1032
  getMeshQueueStats: () => getMeshQueueStats,
1006
1033
  getQueue: () => getQueue,
1034
+ recordTaskAutoLaunch: () => recordTaskAutoLaunch,
1007
1035
  requeueTask: () => requeueTask,
1008
1036
  updateSessionTaskStatus: () => updateSessionTaskStatus,
1009
1037
  updateTaskStatus: () => updateTaskStatus
@@ -1082,6 +1110,19 @@ function updateTaskStatus(meshId, taskId, status) {
1082
1110
  writeQueue(meshId, queue);
1083
1111
  return queue[idx];
1084
1112
  }
1113
+ function recordTaskAutoLaunch(meshId, taskId, autoLaunch) {
1114
+ const queue = readQueue(meshId);
1115
+ const idx = queue.findIndex((q) => q.id === taskId);
1116
+ if (idx === -1) return null;
1117
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1118
+ queue[idx].autoLaunch = {
1119
+ ...autoLaunch,
1120
+ updatedAt: now
1121
+ };
1122
+ queue[idx].updatedAt = now;
1123
+ writeQueue(meshId, queue);
1124
+ return queue[idx];
1125
+ }
1085
1126
  function cancelTask(meshId, taskId, opts) {
1086
1127
  const queue = readQueue(meshId);
1087
1128
  const idx = queue.findIndex((q) => q.id === taskId);
@@ -1151,10 +1192,144 @@ var init_mesh_work_queue = __esm({
1151
1192
  }
1152
1193
  });
1153
1194
 
1195
+ // src/detection/cli-detector.ts
1196
+ import { exec } from "child_process";
1197
+ import * as os2 from "os";
1198
+ import * as path8 from "path";
1199
+ import { existsSync as existsSync7 } from "fs";
1200
+ function parseVersion(raw) {
1201
+ const match = raw.match(/v?(\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?)/);
1202
+ return match ? match[1] : raw.split("\n")[0].slice(0, 100);
1203
+ }
1204
+ function shellQuote(value) {
1205
+ if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) return value;
1206
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
1207
+ }
1208
+ function expandHome(value) {
1209
+ const trimmed = value.trim();
1210
+ if (!trimmed.startsWith("~")) return trimmed;
1211
+ return path8.join(os2.homedir(), trimmed.slice(1));
1212
+ }
1213
+ function isExplicitCommandPath(command) {
1214
+ const trimmed = command.trim();
1215
+ return path8.isAbsolute(trimmed) || trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("~");
1216
+ }
1217
+ function resolveCommandPath(command) {
1218
+ const trimmed = command.trim();
1219
+ if (!trimmed) return null;
1220
+ if (isExplicitCommandPath(trimmed)) {
1221
+ const expanded = expandHome(trimmed);
1222
+ const candidate = path8.isAbsolute(expanded) ? expanded : path8.resolve(expanded);
1223
+ return existsSync7(candidate) ? candidate : null;
1224
+ }
1225
+ return null;
1226
+ }
1227
+ function execAsync(cmd, timeoutMs = 5e3) {
1228
+ return new Promise((resolve16) => {
1229
+ const child = exec(cmd, {
1230
+ encoding: "utf-8",
1231
+ timeout: timeoutMs,
1232
+ ...process.platform === "win32" ? { windowsHide: true } : {}
1233
+ }, (err, stdout) => {
1234
+ if (err || !stdout?.trim()) {
1235
+ resolve16(null);
1236
+ } else {
1237
+ resolve16(stdout.trim());
1238
+ }
1239
+ });
1240
+ child.on("error", () => resolve16(null));
1241
+ });
1242
+ }
1243
+ async function detectCLIs(providerLoader, options) {
1244
+ const platform10 = os2.platform();
1245
+ const whichCmd = platform10 === "win32" ? "where" : "which";
1246
+ const includeVersion = options?.includeVersion !== false;
1247
+ const cliList = providerLoader ? providerLoader.getCliDetectionList() : [];
1248
+ const results = await Promise.all(
1249
+ cliList.map(async (cli) => {
1250
+ try {
1251
+ const explicitPath = resolveCommandPath(cli.command);
1252
+ const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
1253
+ if (!pathResult) return { ...cli, installed: false };
1254
+ const firstPath = explicitPath || pathResult.split("\n")[0];
1255
+ let version;
1256
+ if (includeVersion) {
1257
+ const versionCommands = [
1258
+ `"${firstPath}" --version`,
1259
+ `"${firstPath}" -V`,
1260
+ `"${firstPath}" -v`,
1261
+ cli.versionCommand
1262
+ ].filter((v) => !!v);
1263
+ try {
1264
+ for (const versionCommand of versionCommands) {
1265
+ const versionResult = await execAsync(versionCommand, 3e3);
1266
+ if (versionResult) {
1267
+ version = parseVersion(versionResult);
1268
+ break;
1269
+ }
1270
+ }
1271
+ } catch {
1272
+ }
1273
+ }
1274
+ return { ...cli, installed: true, version, path: firstPath };
1275
+ } catch {
1276
+ return { ...cli, installed: false };
1277
+ }
1278
+ })
1279
+ );
1280
+ return results;
1281
+ }
1282
+ async function detectCLI(cliId, providerLoader, options) {
1283
+ const resolvedId = providerLoader ? providerLoader.resolveAlias(cliId) : cliId;
1284
+ if (providerLoader) {
1285
+ const cliList = providerLoader.getCliDetectionList();
1286
+ const target = cliList.find((c) => c.id === resolvedId);
1287
+ if (target) {
1288
+ const platform10 = os2.platform();
1289
+ const whichCmd = platform10 === "win32" ? "where" : "which";
1290
+ try {
1291
+ const explicitPath = resolveCommandPath(target.command);
1292
+ const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
1293
+ if (!pathResult) return null;
1294
+ const firstPath = explicitPath || pathResult.split("\n")[0];
1295
+ let version;
1296
+ if (options?.includeVersion !== false) {
1297
+ const versionCommands = [
1298
+ `"${firstPath}" --version`,
1299
+ `"${firstPath}" -V`,
1300
+ `"${firstPath}" -v`,
1301
+ target.versionCommand
1302
+ ].filter((v) => !!v);
1303
+ try {
1304
+ for (const versionCommand of versionCommands) {
1305
+ const versionResult = await execAsync(versionCommand, 3e3);
1306
+ if (versionResult) {
1307
+ version = parseVersion(versionResult);
1308
+ break;
1309
+ }
1310
+ }
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ return { ...target, installed: true, version, path: firstPath };
1315
+ } catch {
1316
+ return null;
1317
+ }
1318
+ }
1319
+ }
1320
+ const all = await detectCLIs(providerLoader, options);
1321
+ return all.find((c) => c.id === resolvedId && c.installed) || null;
1322
+ }
1323
+ var init_cli_detector = __esm({
1324
+ "src/detection/cli-detector.ts"() {
1325
+ "use strict";
1326
+ }
1327
+ });
1328
+
1154
1329
  // src/logging/logger.ts
1155
1330
  import * as fs2 from "fs";
1156
- import * as path8 from "path";
1157
- import * as os2 from "os";
1331
+ import * as path9 from "path";
1332
+ import * as os3 from "os";
1158
1333
  function setLogLevel(level) {
1159
1334
  currentLevel = level;
1160
1335
  daemonLog("Logger", `Log level set to: ${level}`, "info");
@@ -1169,13 +1344,13 @@ function getDaemonLogDir() {
1169
1344
  return LOG_DIR;
1170
1345
  }
1171
1346
  function getCurrentDaemonLogPath(date = /* @__PURE__ */ new Date()) {
1172
- return path8.join(LOG_DIR, `daemon-${date.toISOString().slice(0, 10)}.log`);
1347
+ return path9.join(LOG_DIR, `daemon-${date.toISOString().slice(0, 10)}.log`);
1173
1348
  }
1174
1349
  function checkDateRotation() {
1175
1350
  const today = getDateStr();
1176
1351
  if (today !== currentDate) {
1177
1352
  currentDate = today;
1178
- currentLogFile = path8.join(LOG_DIR, `daemon-${currentDate}.log`);
1353
+ currentLogFile = path9.join(LOG_DIR, `daemon-${currentDate}.log`);
1179
1354
  cleanOldLogs();
1180
1355
  }
1181
1356
  }
@@ -1189,7 +1364,7 @@ function cleanOldLogs() {
1189
1364
  const dateMatch = file.match(/daemon-(\d{4}-\d{2}-\d{2})/);
1190
1365
  if (dateMatch && dateMatch[1] < cutoffStr) {
1191
1366
  try {
1192
- fs2.unlinkSync(path8.join(LOG_DIR, file));
1367
+ fs2.unlinkSync(path9.join(LOG_DIR, file));
1193
1368
  } catch {
1194
1369
  }
1195
1370
  }
@@ -1312,7 +1487,7 @@ var init_logger = __esm({
1312
1487
  LEVEL_NUM = { debug: 0, info: 1, warn: 2, error: 3 };
1313
1488
  LEVEL_LABEL = { debug: "DBG", info: "INF", warn: "WRN", error: "ERR" };
1314
1489
  currentLevel = "info";
1315
- LOG_DIR = process.platform === "win32" ? path8.join(process.env.LOCALAPPDATA || process.env.APPDATA || path8.join(os2.homedir(), "AppData", "Local"), "adhdev", "logs") : process.platform === "darwin" ? path8.join(os2.homedir(), "Library", "Logs", "adhdev") : path8.join(os2.homedir(), ".local", "share", "adhdev", "logs");
1490
+ LOG_DIR = process.platform === "win32" ? path9.join(process.env.LOCALAPPDATA || process.env.APPDATA || path9.join(os3.homedir(), "AppData", "Local"), "adhdev", "logs") : process.platform === "darwin" ? path9.join(os3.homedir(), "Library", "Logs", "adhdev") : path9.join(os3.homedir(), ".local", "share", "adhdev", "logs");
1316
1491
  MAX_LOG_SIZE = 5 * 1024 * 1024;
1317
1492
  MAX_LOG_DAYS = 7;
1318
1493
  try {
@@ -1320,16 +1495,16 @@ var init_logger = __esm({
1320
1495
  } catch {
1321
1496
  }
1322
1497
  currentDate = getDateStr();
1323
- currentLogFile = path8.join(LOG_DIR, `daemon-${currentDate}.log`);
1498
+ currentLogFile = path9.join(LOG_DIR, `daemon-${currentDate}.log`);
1324
1499
  cleanOldLogs();
1325
1500
  try {
1326
- const oldLog = path8.join(LOG_DIR, "daemon.log");
1501
+ const oldLog = path9.join(LOG_DIR, "daemon.log");
1327
1502
  if (fs2.existsSync(oldLog)) {
1328
1503
  const stat2 = fs2.statSync(oldLog);
1329
1504
  const oldDate = stat2.mtime.toISOString().slice(0, 10);
1330
- fs2.renameSync(oldLog, path8.join(LOG_DIR, `daemon-${oldDate}.log`));
1505
+ fs2.renameSync(oldLog, path9.join(LOG_DIR, `daemon-${oldDate}.log`));
1331
1506
  }
1332
- const oldLogBackup = path8.join(LOG_DIR, "daemon.log.old");
1507
+ const oldLogBackup = path9.join(LOG_DIR, "daemon.log.old");
1333
1508
  if (fs2.existsSync(oldLogBackup)) {
1334
1509
  fs2.unlinkSync(oldLogBackup);
1335
1510
  }
@@ -1361,7 +1536,7 @@ var init_logger = __esm({
1361
1536
  }
1362
1537
  };
1363
1538
  interceptorInstalled = false;
1364
- LOG_PATH = path8.join(LOG_DIR, `daemon-${getDateStr()}.log`);
1539
+ LOG_PATH = path9.join(LOG_DIR, `daemon-${getDateStr()}.log`);
1365
1540
  }
1366
1541
  });
1367
1542
 
@@ -1433,7 +1608,235 @@ function tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType)
1433
1608
  });
1434
1609
  return true;
1435
1610
  }
1436
- function triggerMeshQueue(components, meshId) {
1611
+ function normalizeProviderPriority(policy) {
1612
+ const raw = policy && typeof policy === "object" && !Array.isArray(policy) ? policy.providerPriority : void 0;
1613
+ if (!Array.isArray(raw)) return [];
1614
+ const seen = /* @__PURE__ */ new Set();
1615
+ return raw.map((type) => typeof type === "string" ? type.trim() : "").filter(Boolean).filter((type) => {
1616
+ if (seen.has(type)) return false;
1617
+ seen.add(type);
1618
+ return true;
1619
+ });
1620
+ }
1621
+ function isTerminalSessionStatus(status) {
1622
+ return ["stopped", "failed", "terminated", "exited", "closed"].includes(status);
1623
+ }
1624
+ function isIdleSessionState(state) {
1625
+ const status = readNonEmptyString(state?.status).toLowerCase();
1626
+ if (isTerminalSessionStatus(status)) return false;
1627
+ return status === "idle" || state?.activeChat?.status === "waiting_input";
1628
+ }
1629
+ function isDirtyNode(node) {
1630
+ return node?.health === "dirty" || node?.git?.dirty === true;
1631
+ }
1632
+ function isLaunchableNode(node) {
1633
+ if (!node || node.status === "disabled" || node.status === "removed") return false;
1634
+ const health = readNonEmptyString(node.health).toLowerCase();
1635
+ if (!health) return true;
1636
+ return health === "online" || health === "unknown";
1637
+ }
1638
+ function localAutoLaunchSkipReason(node) {
1639
+ const daemonId = readNonEmptyString(node?.daemonId);
1640
+ const machineId = readNonEmptyString(node?.machineId);
1641
+ const appConfig = loadConfig();
1642
+ const localMachineId = readNonEmptyString(appConfig.machineId) || readNonEmptyString(appConfig.registeredMachineId);
1643
+ const cloudDaemonId = localMachineId ? `daemon_${localMachineId}` : "";
1644
+ const standaloneDaemonId = localMachineId ? `standalone_${localMachineId}` : "";
1645
+ const daemonMatchesLocal = !daemonId || daemonId === cloudDaemonId || daemonId === standaloneDaemonId;
1646
+ const machineMatchesLocal = !machineId || localMachineId && machineId === localMachineId;
1647
+ if (node?.isLocalWorktree === true) {
1648
+ return daemonMatchesLocal && machineMatchesLocal ? null : "remote_auto_launch_unsupported";
1649
+ }
1650
+ if (daemonId || machineId) {
1651
+ return daemonMatchesLocal && machineMatchesLocal ? null : "remote_auto_launch_unsupported";
1652
+ }
1653
+ return null;
1654
+ }
1655
+ function activeAssignedCount(meshId) {
1656
+ return getQueue(meshId, { status: ["assigned"] }).length;
1657
+ }
1658
+ function nodeHasActiveAssignment(meshId, nodeId) {
1659
+ return getQueue(meshId, { status: ["assigned"] }).some((task) => task.assignedNodeId === nodeId);
1660
+ }
1661
+ function liveSessionCountForNode(components, meshId, nodeId) {
1662
+ return components.instanceManager.getByCategory("cli").filter((inst) => {
1663
+ const state = inst.getState();
1664
+ const settings = state.settings || {};
1665
+ if (readNonEmptyString(settings.meshNodeFor) !== meshId) return false;
1666
+ const instNodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
1667
+ if (instNodeId !== nodeId) return false;
1668
+ const status = readNonEmptyString(state.status).toLowerCase();
1669
+ return !isTerminalSessionStatus(status);
1670
+ }).length;
1671
+ }
1672
+ function recordAutoLaunchEvent(meshId, args) {
1673
+ try {
1674
+ appendLedgerEntry(meshId, {
1675
+ kind: "session_auto_launch",
1676
+ nodeId: args.nodeId,
1677
+ sessionId: args.sessionId,
1678
+ providerType: args.providerType,
1679
+ payload: {
1680
+ phase: args.phase,
1681
+ taskId: args.taskId,
1682
+ reason: args.reason,
1683
+ error: args.error
1684
+ }
1685
+ });
1686
+ } catch (e) {
1687
+ LOG.warn("MeshQueue", `Failed to record auto-launch ledger event: ${e?.message || e}`);
1688
+ }
1689
+ }
1690
+ function markAutoLaunch(meshId, taskId, args) {
1691
+ recordTaskAutoLaunch(meshId, taskId, {
1692
+ status: args.status,
1693
+ reason: args.reason || args.error,
1694
+ nodeId: args.nodeId,
1695
+ providerType: args.providerType,
1696
+ sessionId: args.sessionId
1697
+ });
1698
+ recordAutoLaunchEvent(meshId, {
1699
+ phase: args.status,
1700
+ taskId,
1701
+ nodeId: args.nodeId,
1702
+ providerType: args.providerType,
1703
+ sessionId: args.sessionId,
1704
+ reason: args.reason,
1705
+ error: args.error
1706
+ });
1707
+ }
1708
+ async function resolveUsableProvider(components, nodeId, node) {
1709
+ const providerPriority = normalizeProviderPriority(node?.policy);
1710
+ if (!providerPriority.length) return { reason: "missing_provider_priority" };
1711
+ const providerLoader = components.providerLoader;
1712
+ if (!providerLoader) return { reason: "provider_loader_unavailable" };
1713
+ const failed = [];
1714
+ for (const requestedType of providerPriority) {
1715
+ const normalizedType = typeof providerLoader.resolveAlias === "function" ? providerLoader.resolveAlias(requestedType) : requestedType;
1716
+ if (typeof providerLoader.isMachineProviderEnabled === "function" && !providerLoader.isMachineProviderEnabled(normalizedType)) {
1717
+ failed.push(`${requestedType}: disabled`);
1718
+ continue;
1719
+ }
1720
+ let detected;
1721
+ try {
1722
+ detected = await detectCLI(normalizedType, providerLoader, { includeVersion: false });
1723
+ } catch (e) {
1724
+ failed.push(`${requestedType}: detect failed: ${e?.message || e}`);
1725
+ continue;
1726
+ }
1727
+ if (typeof providerLoader.setCliDetectionResults === "function") {
1728
+ providerLoader.setCliDetectionResults([{
1729
+ id: normalizedType,
1730
+ installed: !!detected,
1731
+ path: detected?.path
1732
+ }], false);
1733
+ }
1734
+ components.onStatusChange?.();
1735
+ if (detected) return { providerType: normalizedType };
1736
+ failed.push(`${requestedType}: not detected`);
1737
+ }
1738
+ return { reason: `provider_priority_unusable: ${failed.join("; ") || nodeId}` };
1739
+ }
1740
+ async function maybeAutoLaunchOneQueueSession(components, meshId, mesh) {
1741
+ const queue = getQueue(meshId);
1742
+ const pending = queue.filter((task) => task.status === "pending");
1743
+ if (!pending.length) return false;
1744
+ const maxParallelTasks = Math.max(1, Math.floor(Number(mesh?.policy?.maxParallelTasks) || 2));
1745
+ for (const task of pending) {
1746
+ if (activeAssignedCount(meshId) >= maxParallelTasks) {
1747
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "max_parallel_tasks_reached" });
1748
+ return false;
1749
+ }
1750
+ if (task.targetSessionId) {
1751
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "target_session_constraint" });
1752
+ continue;
1753
+ }
1754
+ const candidateNodes = Array.isArray(mesh?.nodes) ? mesh.nodes.filter((node) => task.targetNodeId ? node?.id === task.targetNodeId : true) : [];
1755
+ if (!candidateNodes.length) {
1756
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "no_matching_node", nodeId: task.targetNodeId });
1757
+ continue;
1758
+ }
1759
+ for (const node of candidateNodes) {
1760
+ const nodeId = readNonEmptyString(node?.id);
1761
+ if (!nodeId) continue;
1762
+ const launchKey = `${meshId}:${nodeId}`;
1763
+ const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
1764
+ if (autoLaunchInProgress.has(launchKey)) {
1765
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "auto_launch_in_progress", nodeId });
1766
+ continue;
1767
+ }
1768
+ if (Date.now() < cooldownUntil) {
1769
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "auto_launch_cooldown", nodeId });
1770
+ continue;
1771
+ }
1772
+ if (isDirtyNode(node)) {
1773
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "dirty_workspace", nodeId });
1774
+ continue;
1775
+ }
1776
+ if (!isLaunchableNode(node)) {
1777
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "node_not_launch_ready", nodeId });
1778
+ continue;
1779
+ }
1780
+ const localSkipReason = localAutoLaunchSkipReason(node);
1781
+ if (localSkipReason) {
1782
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: localSkipReason, nodeId });
1783
+ continue;
1784
+ }
1785
+ if (nodeHasActiveAssignment(meshId, nodeId)) {
1786
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "node_has_active_assignment", nodeId });
1787
+ continue;
1788
+ }
1789
+ const maxConcurrentSessions = Number(node?.policy?.maxConcurrentSessions);
1790
+ if (Number.isFinite(maxConcurrentSessions) && maxConcurrentSessions >= 0 && liveSessionCountForNode(components, meshId, nodeId) >= maxConcurrentSessions) {
1791
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: "max_concurrent_sessions_reached", nodeId });
1792
+ continue;
1793
+ }
1794
+ autoLaunchInProgress.add(launchKey);
1795
+ try {
1796
+ const resolved = await resolveUsableProvider(components, nodeId, node);
1797
+ if (!resolved.providerType) {
1798
+ markAutoLaunch(meshId, task.id, { status: "skipped", reason: resolved.reason || "provider_unusable", nodeId });
1799
+ continue;
1800
+ }
1801
+ markAutoLaunch(meshId, task.id, { status: "started", nodeId, providerType: resolved.providerType });
1802
+ const launchResult = await components.cliManager.handleCliCommand("launch_cli", {
1803
+ cliType: resolved.providerType,
1804
+ dir: node.workspace,
1805
+ settings: {
1806
+ meshNodeFor: meshId,
1807
+ meshNodeId: nodeId,
1808
+ spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || "hidden",
1809
+ launchedByCoordinator: true,
1810
+ autoLaunchedForQueueTaskId: task.id
1811
+ }
1812
+ });
1813
+ if (!launchResult?.success) {
1814
+ const reason = launchResult?.error || "launch_cli_failed";
1815
+ markAutoLaunch(meshId, task.id, { status: "failed", reason, nodeId, providerType: resolved.providerType });
1816
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
1817
+ return false;
1818
+ }
1819
+ const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
1820
+ if (!sessionId) {
1821
+ markAutoLaunch(meshId, task.id, { status: "failed", reason: "launch_missing_session_id", nodeId, providerType: resolved.providerType });
1822
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
1823
+ return false;
1824
+ }
1825
+ markAutoLaunch(meshId, task.id, { status: "completed", nodeId, providerType: resolved.providerType, sessionId });
1826
+ tryAssignQueueTask(components, meshId, nodeId, sessionId, resolved.providerType);
1827
+ return true;
1828
+ } catch (e) {
1829
+ markAutoLaunch(meshId, task.id, { status: "failed", error: e?.message || String(e), nodeId });
1830
+ autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
1831
+ return false;
1832
+ } finally {
1833
+ autoLaunchInProgress.delete(launchKey);
1834
+ }
1835
+ }
1836
+ }
1837
+ return false;
1838
+ }
1839
+ async function triggerMeshQueue(components, meshId) {
1437
1840
  const mesh = getMeshWithCache(components, meshId);
1438
1841
  if (!mesh) return;
1439
1842
  const cliInstances = components.instanceManager.getByCategory("cli");
@@ -1444,9 +1847,7 @@ function triggerMeshQueue(components, meshId) {
1444
1847
  if (instMeshId !== meshId) continue;
1445
1848
  const nodeId = readNonEmptyString(settings.meshNodeId) || readNonEmptyString(settings.nodeId);
1446
1849
  if (!nodeId) continue;
1447
- const status = readNonEmptyString(state.status).toLowerCase();
1448
- if (["stopped", "failed", "terminated", "exited", "closed"].includes(status)) continue;
1449
- if (status !== "idle" && state.activeChat?.status !== "waiting_input") continue;
1850
+ if (!isIdleSessionState(state)) continue;
1450
1851
  const sessionId = state.instanceId;
1451
1852
  const providerType = state.type || readNonEmptyString(settings.providerType);
1452
1853
  if (providerType) {
@@ -1462,6 +1863,7 @@ function triggerMeshQueue(components, meshId) {
1462
1863
  }
1463
1864
  }
1464
1865
  }
1866
+ await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
1465
1867
  }
1466
1868
  function buildMeshSystemMessage(args) {
1467
1869
  const metadata = formatCompletionMetadata(args.metadataEvent);
@@ -1738,11 +2140,13 @@ function setupMeshEventForwarding(components) {
1738
2140
  });
1739
2141
  });
1740
2142
  }
1741
- var remoteIdleSessions, MAX_PENDING_EVENTS, pendingMeshCoordinatorEvents, MESH_COORDINATOR_EVENTS, EVENT_TO_LEDGER_KIND;
2143
+ var remoteIdleSessions, MAX_PENDING_EVENTS, pendingMeshCoordinatorEvents, MESH_COORDINATOR_EVENTS, EVENT_TO_LEDGER_KIND, autoLaunchInProgress, autoLaunchCooldownUntil, AUTO_LAUNCH_COOLDOWN_MS;
1742
2144
  var init_mesh_events = __esm({
1743
2145
  "src/mesh/mesh-events.ts"() {
1744
2146
  "use strict";
2147
+ init_config();
1745
2148
  init_mesh_config();
2149
+ init_cli_detector();
1746
2150
  init_logger();
1747
2151
  init_mesh_ledger();
1748
2152
  init_mesh_work_queue();
@@ -1763,6 +2167,9 @@ var init_mesh_events = __esm({
1763
2167
  "agent:stopped": "task_failed",
1764
2168
  "monitor:long_generating": "task_stalled"
1765
2169
  };
2170
+ autoLaunchInProgress = /* @__PURE__ */ new Set();
2171
+ autoLaunchCooldownUntil = /* @__PURE__ */ new Map();
2172
+ AUTO_LAUNCH_COOLDOWN_MS = 5e3;
1766
2173
  }
1767
2174
  });
1768
2175
 
@@ -6729,10 +7136,120 @@ init_mesh_ledger();
6729
7136
  init_mesh_work_queue();
6730
7137
  init_mesh_events();
6731
7138
 
7139
+ // src/mesh/p2p-relay-failure.ts
7140
+ var NO_FALLBACK_REASON = "Repo Mesh command/data-plane is P2P-only; WS/REST command fallback is intentionally disabled to preserve the transport boundary.";
7141
+ var P2P_NEXT_ACTION = "Check daemon/P2P health, wait briefly for connection establishment, then do one bounded retry or requeue the mesh task after clearing stale target session metadata.";
7142
+ var NON_P2P_NEXT_ACTION = "Inspect the provider/command error and fix the underlying logic or configuration before retrying.";
7143
+ function messageFromError(error) {
7144
+ if (error instanceof Error) return error.message;
7145
+ if (typeof error === "string") return error;
7146
+ if (error && typeof error === "object") {
7147
+ const candidate = error.error ?? error.message ?? error.reason;
7148
+ if (typeof candidate === "string") return candidate;
7149
+ }
7150
+ return String(error || "mesh relay command failed");
7151
+ }
7152
+ function classifyP2pRelayFailure(error, _context = {}) {
7153
+ const message = messageFromError(error);
7154
+ const lower = message.toLowerCase();
7155
+ const hasP2pSignal = /p2p|datachannel|node-datachannel|webrtc|ice|mesh_relay_command|daemon_mesh_p2p_transport/i.test(message);
7156
+ const hasFailureSignal = /unavailable|missing|failed|failure|timeout|timed out|not connected|closed|disconnected|offline|no route|route unavailable|cannot send|cannot establish/i.test(message);
7157
+ if (/requires targetdaemonid and command|providerpriority|no inference provider|permission denied|read-only|not a member/i.test(message)) {
7158
+ return {
7159
+ code: "mesh_logic_or_provider_failure",
7160
+ reason: "mesh_logic_or_provider_failure",
7161
+ transport: "unknown",
7162
+ recoverable: false,
7163
+ retryRecommended: false,
7164
+ nextAction: NON_P2P_NEXT_ACTION,
7165
+ noFallbackReason: NO_FALLBACK_REASON
7166
+ };
7167
+ }
7168
+ let code = null;
7169
+ let reason = "";
7170
+ if (/timeout|timed out/i.test(message) && (hasP2pSignal || /mesh transport/i.test(message))) {
7171
+ code = "p2p_timeout";
7172
+ reason = "daemon_mesh_p2p_timeout";
7173
+ } else if (/no route|route unavailable/i.test(message)) {
7174
+ code = "p2p_no_route";
7175
+ reason = "daemon_mesh_p2p_no_route";
7176
+ } else if (/offline|not owned|not found|not connected to server/i.test(message) && /daemon|peer|target/i.test(message)) {
7177
+ code = "p2p_daemon_offline";
7178
+ reason = "daemon_mesh_target_offline";
7179
+ } else if (/closed|disconnected/i.test(message) && (hasP2pSignal || /state changed/i.test(message))) {
7180
+ code = "p2p_datachannel_closed";
7181
+ reason = "daemon_mesh_p2p_datachannel_closed";
7182
+ } else if (/not connected|cannot send|cannot establish/i.test(message) && hasP2pSignal) {
7183
+ code = "p2p_not_connected";
7184
+ reason = "daemon_mesh_p2p_not_connected";
7185
+ } else if (hasP2pSignal && hasFailureSignal) {
7186
+ code = "p2p_unavailable";
7187
+ reason = "daemon_mesh_p2p_transport_unavailable";
7188
+ }
7189
+ if (!code) {
7190
+ return {
7191
+ code: "mesh_logic_or_provider_failure",
7192
+ reason: "mesh_logic_or_provider_failure",
7193
+ transport: "unknown",
7194
+ recoverable: false,
7195
+ retryRecommended: false,
7196
+ nextAction: NON_P2P_NEXT_ACTION,
7197
+ noFallbackReason: NO_FALLBACK_REASON
7198
+ };
7199
+ }
7200
+ return {
7201
+ code,
7202
+ reason,
7203
+ transport: "p2p",
7204
+ recoverable: true,
7205
+ retryRecommended: true,
7206
+ nextAction: P2P_NEXT_ACTION,
7207
+ noFallbackReason: NO_FALLBACK_REASON
7208
+ };
7209
+ }
7210
+ function isP2pRelayTransportFailure(error) {
7211
+ return classifyP2pRelayFailure(error).recoverable === true;
7212
+ }
7213
+ function buildP2pRelayFailurePayload(error, context = {}) {
7214
+ const classification = classifyP2pRelayFailure(error, context);
7215
+ return {
7216
+ success: false,
7217
+ ...classification,
7218
+ error: messageFromError(error),
7219
+ ...context.command ? { command: context.command } : {},
7220
+ ...context.targetDaemonId ? { targetDaemonId: context.targetDaemonId } : {}
7221
+ };
7222
+ }
7223
+ var P2pRelayFailureError = class extends Error {
7224
+ code;
7225
+ reason;
7226
+ transport;
7227
+ recoverable;
7228
+ retryRecommended;
7229
+ nextAction;
7230
+ noFallbackReason;
7231
+ command;
7232
+ targetDaemonId;
7233
+ constructor(message, context = {}) {
7234
+ super(message);
7235
+ this.name = "P2pRelayFailureError";
7236
+ const payload = buildP2pRelayFailurePayload(message, context);
7237
+ this.code = payload.code;
7238
+ this.reason = payload.reason;
7239
+ this.transport = payload.transport;
7240
+ this.recoverable = payload.recoverable;
7241
+ this.retryRecommended = payload.retryRecommended;
7242
+ this.nextAction = payload.nextAction;
7243
+ this.noFallbackReason = payload.noFallbackReason;
7244
+ this.command = context.command;
7245
+ this.targetDaemonId = context.targetDaemonId;
7246
+ }
7247
+ };
7248
+
6732
7249
  // src/config/state-store.ts
6733
7250
  init_config();
6734
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6735
- import { join as join8 } from "path";
7251
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
7252
+ import { join as join9 } from "path";
6736
7253
  var DEFAULT_STATE = {
6737
7254
  recentActivity: [],
6738
7255
  savedProviderSessions: [],
@@ -6745,7 +7262,7 @@ function isPlainObject2(value) {
6745
7262
  return !!value && typeof value === "object" && !Array.isArray(value);
6746
7263
  }
6747
7264
  function getStatePath() {
6748
- return join8(getConfigDir(), "state.json");
7265
+ return join9(getConfigDir(), "state.json");
6749
7266
  }
6750
7267
  function normalizeState(raw) {
6751
7268
  const parsed = isPlainObject2(raw) ? raw : {};
@@ -6781,7 +7298,7 @@ function normalizeState(raw) {
6781
7298
  }
6782
7299
  function loadState() {
6783
7300
  const statePath = getStatePath();
6784
- if (!existsSync8(statePath)) {
7301
+ if (!existsSync9(statePath)) {
6785
7302
  return { ...DEFAULT_STATE };
6786
7303
  }
6787
7304
  try {
@@ -6802,9 +7319,9 @@ function resetState() {
6802
7319
 
6803
7320
  // src/detection/ide-detector.ts
6804
7321
  import { execSync } from "child_process";
6805
- import { existsSync as existsSync9 } from "fs";
6806
- import { platform, homedir as homedir4 } from "os";
6807
- import * as path9 from "path";
7322
+ import { existsSync as existsSync10 } from "fs";
7323
+ import { platform as platform2, homedir as homedir5 } from "os";
7324
+ import * as path10 from "path";
6808
7325
  var BUILTIN_IDE_DEFINITIONS = [];
6809
7326
  var registeredIDEs = /* @__PURE__ */ new Map();
6810
7327
  function registerIDEDefinition(def) {
@@ -6823,14 +7340,14 @@ function getMergedDefinitions() {
6823
7340
  function findCliCommand(command) {
6824
7341
  const trimmed = String(command || "").trim();
6825
7342
  if (!trimmed) return null;
6826
- if (path9.isAbsolute(trimmed) || trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("~")) {
6827
- const candidate = trimmed.startsWith("~") ? path9.join(homedir4(), trimmed.slice(1)) : trimmed;
6828
- const resolved = path9.isAbsolute(candidate) ? candidate : path9.resolve(candidate);
6829
- return existsSync9(resolved) ? resolved : null;
7343
+ if (path10.isAbsolute(trimmed) || trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("~")) {
7344
+ const candidate = trimmed.startsWith("~") ? path10.join(homedir5(), trimmed.slice(1)) : trimmed;
7345
+ const resolved = path10.isAbsolute(candidate) ? candidate : path10.resolve(candidate);
7346
+ return existsSync10(resolved) ? resolved : null;
6830
7347
  }
6831
7348
  try {
6832
7349
  const result = execSync(
6833
- platform() === "win32" ? `where ${trimmed}` : `which ${trimmed}`,
7350
+ platform2() === "win32" ? `where ${trimmed}` : `which ${trimmed}`,
6834
7351
  { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
6835
7352
  ).trim();
6836
7353
  return result.split("\n")[0] || null;
@@ -6851,21 +7368,21 @@ function getIdeVersion(cliCommand) {
6851
7368
  }
6852
7369
  }
6853
7370
  function checkPathExists(paths) {
6854
- const home = homedir4();
7371
+ const home = homedir5();
6855
7372
  for (const p of paths) {
6856
- const normalized = p.startsWith("~") ? path9.join(home, p.slice(1)) : p;
7373
+ const normalized = p.startsWith("~") ? path10.join(home, p.slice(1)) : p;
6857
7374
  if (normalized.includes("*")) {
6858
7375
  const username = home.split(/[\\/]/).pop() || "";
6859
7376
  const resolved = normalized.replace("*", username);
6860
- if (existsSync9(resolved)) return resolved;
7377
+ if (existsSync10(resolved)) return resolved;
6861
7378
  } else {
6862
- if (existsSync9(normalized)) return normalized;
7379
+ if (existsSync10(normalized)) return normalized;
6863
7380
  }
6864
7381
  }
6865
7382
  return null;
6866
7383
  }
6867
7384
  async function detectIDEs(providerLoader) {
6868
- const os22 = platform();
7385
+ const os22 = platform2();
6869
7386
  const results = [];
6870
7387
  for (const def of getMergedDefinitions()) {
6871
7388
  const cliPath = findCliCommand(providerLoader?.getIdeCliCommand(def.id, def.cli) || def.cli);
@@ -6873,7 +7390,7 @@ async function detectIDEs(providerLoader) {
6873
7390
  let resolvedCli = cliPath;
6874
7391
  if (!resolvedCli && appPath && os22 === "darwin") {
6875
7392
  const bundledCli = `${appPath}/Contents/Resources/app/bin/${def.cli}`;
6876
- if (existsSync9(bundledCli)) resolvedCli = bundledCli;
7393
+ if (existsSync10(bundledCli)) resolvedCli = bundledCli;
6877
7394
  }
6878
7395
  if (!resolvedCli && appPath && os22 === "win32") {
6879
7396
  const { dirname: dirname9 } = await import("path");
@@ -6886,7 +7403,7 @@ async function detectIDEs(providerLoader) {
6886
7403
  `${appDir}\\\\resources\\\\app\\\\bin\\\\${def.cli}.cmd`
6887
7404
  ];
6888
7405
  for (const c of candidates) {
6889
- if (existsSync9(c)) {
7406
+ if (existsSync10(c)) {
6890
7407
  resolvedCli = c;
6891
7408
  break;
6892
7409
  }
@@ -6908,134 +7425,8 @@ async function detectIDEs(providerLoader) {
6908
7425
  return results;
6909
7426
  }
6910
7427
 
6911
- // src/detection/cli-detector.ts
6912
- import { exec } from "child_process";
6913
- import * as os3 from "os";
6914
- import * as path10 from "path";
6915
- import { existsSync as existsSync10 } from "fs";
6916
- function parseVersion(raw) {
6917
- const match = raw.match(/v?(\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?)/);
6918
- return match ? match[1] : raw.split("\n")[0].slice(0, 100);
6919
- }
6920
- function shellQuote(value) {
6921
- if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) return value;
6922
- return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
6923
- }
6924
- function expandHome(value) {
6925
- const trimmed = value.trim();
6926
- if (!trimmed.startsWith("~")) return trimmed;
6927
- return path10.join(os3.homedir(), trimmed.slice(1));
6928
- }
6929
- function isExplicitCommandPath(command) {
6930
- const trimmed = command.trim();
6931
- return path10.isAbsolute(trimmed) || trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("~");
6932
- }
6933
- function resolveCommandPath(command) {
6934
- const trimmed = command.trim();
6935
- if (!trimmed) return null;
6936
- if (isExplicitCommandPath(trimmed)) {
6937
- const expanded = expandHome(trimmed);
6938
- const candidate = path10.isAbsolute(expanded) ? expanded : path10.resolve(expanded);
6939
- return existsSync10(candidate) ? candidate : null;
6940
- }
6941
- return null;
6942
- }
6943
- function execAsync(cmd, timeoutMs = 5e3) {
6944
- return new Promise((resolve16) => {
6945
- const child = exec(cmd, {
6946
- encoding: "utf-8",
6947
- timeout: timeoutMs,
6948
- ...process.platform === "win32" ? { windowsHide: true } : {}
6949
- }, (err, stdout) => {
6950
- if (err || !stdout?.trim()) {
6951
- resolve16(null);
6952
- } else {
6953
- resolve16(stdout.trim());
6954
- }
6955
- });
6956
- child.on("error", () => resolve16(null));
6957
- });
6958
- }
6959
- async function detectCLIs(providerLoader, options) {
6960
- const platform10 = os3.platform();
6961
- const whichCmd = platform10 === "win32" ? "where" : "which";
6962
- const includeVersion = options?.includeVersion !== false;
6963
- const cliList = providerLoader ? providerLoader.getCliDetectionList() : [];
6964
- const results = await Promise.all(
6965
- cliList.map(async (cli) => {
6966
- try {
6967
- const explicitPath = resolveCommandPath(cli.command);
6968
- const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
6969
- if (!pathResult) return { ...cli, installed: false };
6970
- const firstPath = explicitPath || pathResult.split("\n")[0];
6971
- let version;
6972
- if (includeVersion) {
6973
- const versionCommands = [
6974
- `"${firstPath}" --version`,
6975
- `"${firstPath}" -V`,
6976
- `"${firstPath}" -v`,
6977
- cli.versionCommand
6978
- ].filter((v) => !!v);
6979
- try {
6980
- for (const versionCommand of versionCommands) {
6981
- const versionResult = await execAsync(versionCommand, 3e3);
6982
- if (versionResult) {
6983
- version = parseVersion(versionResult);
6984
- break;
6985
- }
6986
- }
6987
- } catch {
6988
- }
6989
- }
6990
- return { ...cli, installed: true, version, path: firstPath };
6991
- } catch {
6992
- return { ...cli, installed: false };
6993
- }
6994
- })
6995
- );
6996
- return results;
6997
- }
6998
- async function detectCLI(cliId, providerLoader, options) {
6999
- const resolvedId = providerLoader ? providerLoader.resolveAlias(cliId) : cliId;
7000
- if (providerLoader) {
7001
- const cliList = providerLoader.getCliDetectionList();
7002
- const target = cliList.find((c) => c.id === resolvedId);
7003
- if (target) {
7004
- const platform10 = os3.platform();
7005
- const whichCmd = platform10 === "win32" ? "where" : "which";
7006
- try {
7007
- const explicitPath = resolveCommandPath(target.command);
7008
- const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
7009
- if (!pathResult) return null;
7010
- const firstPath = explicitPath || pathResult.split("\n")[0];
7011
- let version;
7012
- if (options?.includeVersion !== false) {
7013
- const versionCommands = [
7014
- `"${firstPath}" --version`,
7015
- `"${firstPath}" -V`,
7016
- `"${firstPath}" -v`,
7017
- target.versionCommand
7018
- ].filter((v) => !!v);
7019
- try {
7020
- for (const versionCommand of versionCommands) {
7021
- const versionResult = await execAsync(versionCommand, 3e3);
7022
- if (versionResult) {
7023
- version = parseVersion(versionResult);
7024
- break;
7025
- }
7026
- }
7027
- } catch {
7028
- }
7029
- }
7030
- return { ...target, installed: true, version, path: firstPath };
7031
- } catch {
7032
- return null;
7033
- }
7034
- }
7035
- }
7036
- const all = await detectCLIs(providerLoader, options);
7037
- return all.find((c) => c.id === resolvedId && c.installed) || null;
7038
- }
7428
+ // src/index.ts
7429
+ init_cli_detector();
7039
7430
 
7040
7431
  // src/system/host-memory.ts
7041
7432
  import * as os4 from "os";
@@ -16139,13 +16530,14 @@ var DaemonCommandHandler = class {
16139
16530
 
16140
16531
  // src/commands/cli-manager.ts
16141
16532
  init_provider_cli_adapter();
16533
+ init_cli_detector();
16534
+ init_config();
16142
16535
  import * as os13 from "os";
16143
16536
  import * as path18 from "path";
16144
16537
  import * as crypto4 from "crypto";
16145
16538
  import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
16146
16539
  import { execFileSync } from "child_process";
16147
16540
  import chalk from "chalk";
16148
- init_config();
16149
16541
 
16150
16542
  // src/providers/cli-provider-instance.ts
16151
16543
  import * as os12 from "os";
@@ -21731,6 +22123,7 @@ function getAvailableIdeIds() {
21731
22123
 
21732
22124
  // src/commands/router.ts
21733
22125
  init_config();
22126
+ init_cli_detector();
21734
22127
  init_logger();
21735
22128
 
21736
22129
  // src/logging/command-log.ts
@@ -22877,6 +23270,209 @@ async function resolveProviderTypeFromPriority(args) {
22877
23270
  }
22878
23271
  return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join("; ")}` };
22879
23272
  }
23273
+ var REFINE_VALIDATION_CATEGORIES = ["typecheck", "test", "lint", "build"];
23274
+ var REFINE_VALIDATION_TIMEOUT_MS = 12e4;
23275
+ var REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
23276
+ var REFINE_VALIDATION_SUMMARY_CHARS = 2e3;
23277
+ var REFINE_VALIDATION_MAX_COMMANDS = 4;
23278
+ function truncateValidationOutput(value) {
23279
+ const text = typeof value === "string" ? value : value == null ? "" : String(value);
23280
+ if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
23281
+ return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}
23282
+ [truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
23283
+ }
23284
+ function readPackageScripts(workspace) {
23285
+ try {
23286
+ const packageJsonPath = pathJoin(workspace, "package.json");
23287
+ const parsed = JSON.parse(fs10.readFileSync(packageJsonPath, "utf-8"));
23288
+ return parsed?.scripts && typeof parsed.scripts === "object" && !Array.isArray(parsed.scripts) ? parsed.scripts : {};
23289
+ } catch {
23290
+ return {};
23291
+ }
23292
+ }
23293
+ function tokenizeValidationCommand(command) {
23294
+ const trimmed = command.trim();
23295
+ if (!trimmed) return null;
23296
+ if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
23297
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
23298
+ if (!tokens.length) return null;
23299
+ if (tokens.some((token) => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
23300
+ return tokens;
23301
+ }
23302
+ function scriptMatchesValidationCategory(scriptName, category) {
23303
+ return scriptName === category || scriptName.startsWith(`${category}:`);
23304
+ }
23305
+ function parsePackageManagerValidationCommand(rawCommand, category, scripts, source) {
23306
+ const tokens = tokenizeValidationCommand(rawCommand);
23307
+ if (!tokens) {
23308
+ return { rejected: { command: rawCommand, category, source, reason: "unsafe command string is not allowlisted" } };
23309
+ }
23310
+ const [binary, second, third, ...rest] = tokens;
23311
+ let scriptName = "";
23312
+ let command = binary;
23313
+ let args = [];
23314
+ if ((binary === "npm" || binary === "pnpm" || binary === "bun") && second === "run" && third) {
23315
+ scriptName = third;
23316
+ args = ["run", scriptName, ...rest];
23317
+ } else if (binary === "npm" && second === "test" && !third) {
23318
+ scriptName = "test";
23319
+ args = ["test"];
23320
+ } else if (binary === "yarn" && second === "run" && third) {
23321
+ scriptName = third;
23322
+ args = ["run", scriptName, ...rest];
23323
+ } else if (binary === "yarn" && second && !third) {
23324
+ scriptName = second;
23325
+ args = [scriptName];
23326
+ } else {
23327
+ return { rejected: { command: rawCommand, category, source, reason: "command is not a supported package-manager script invocation" } };
23328
+ }
23329
+ if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
23330
+ return { rejected: { command: rawCommand, category, source, script: scriptName, reason: "script is not declared in package.json" } };
23331
+ }
23332
+ if (!scriptMatchesValidationCategory(scriptName, category)) {
23333
+ return { rejected: { command: rawCommand, category, source, script: scriptName, reason: "script name is outside the validation category allowlist" } };
23334
+ }
23335
+ return {
23336
+ command: {
23337
+ command,
23338
+ args,
23339
+ displayCommand: [command, ...args].join(" "),
23340
+ category,
23341
+ source
23342
+ }
23343
+ };
23344
+ }
23345
+ function collectProjectContextValidationCandidates(mesh) {
23346
+ const commands = mesh?.projectContext?.commands;
23347
+ if (!commands || typeof commands !== "object" || Array.isArray(commands)) return [];
23348
+ const candidates = [];
23349
+ for (const category of REFINE_VALIDATION_CATEGORIES) {
23350
+ const entries = Array.isArray(commands[category]) ? commands[category] : [];
23351
+ for (const entry of entries) {
23352
+ if (typeof entry?.command !== "string") continue;
23353
+ candidates.push({
23354
+ command: entry.command,
23355
+ category,
23356
+ source: typeof entry.sourcePath === "string" ? entry.sourcePath : "projectContext.commands",
23357
+ confidence: typeof entry.confidence === "string" ? entry.confidence : void 0
23358
+ });
23359
+ }
23360
+ }
23361
+ return candidates.sort((a, b) => {
23362
+ const rank = (value) => value === "high" ? 0 : value === "medium" ? 1 : 2;
23363
+ return rank(a.confidence) - rank(b.confidence);
23364
+ });
23365
+ }
23366
+ function collectPolicyValidationCandidates(mesh) {
23367
+ const policy = mesh?.policy && typeof mesh.policy === "object" && !Array.isArray(mesh.policy) ? mesh.policy : {};
23368
+ const configured = Array.isArray(policy.validationCommands) ? policy.validationCommands : Array.isArray(policy.validationGate?.commands) ? policy.validationGate.commands : [];
23369
+ return configured.map((entry) => typeof entry === "string" ? { command: entry, category: "", source: "mesh.policy.validationCommands" } : entry).filter((entry) => entry && typeof entry.command === "string").map((entry) => {
23370
+ const commandText = entry.command.trim();
23371
+ const category = REFINE_VALIDATION_CATEGORIES.find((cat) => commandText.includes(` ${cat}`)) ?? "";
23372
+ return { command: commandText, category, source: "mesh.policy.validationCommands" };
23373
+ }).filter((entry) => !!entry.category);
23374
+ }
23375
+ function selectMeshRefineValidationCommands(mesh, workspace) {
23376
+ const scripts = readPackageScripts(workspace);
23377
+ const rejectedCommands = [];
23378
+ const selected = [];
23379
+ const seen = /* @__PURE__ */ new Set();
23380
+ const candidates = [
23381
+ ...collectPolicyValidationCandidates(mesh),
23382
+ ...collectProjectContextValidationCandidates(mesh)
23383
+ ];
23384
+ for (const candidate of candidates) {
23385
+ const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
23386
+ if (parsed.rejected) {
23387
+ rejectedCommands.push(parsed.rejected);
23388
+ continue;
23389
+ }
23390
+ if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
23391
+ selected.push(parsed.command);
23392
+ seen.add(parsed.command.displayCommand);
23393
+ if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
23394
+ }
23395
+ if (!selected.length && candidates.length === 0) {
23396
+ for (const category of REFINE_VALIDATION_CATEGORIES) {
23397
+ if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
23398
+ const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, "package.json:scripts");
23399
+ if (fallback.command && !seen.has(fallback.command.displayCommand)) {
23400
+ selected.push(fallback.command);
23401
+ seen.add(fallback.command.displayCommand);
23402
+ } else if (fallback.rejected) {
23403
+ rejectedCommands.push(fallback.rejected);
23404
+ }
23405
+ if (selected.length >= 2) break;
23406
+ }
23407
+ }
23408
+ return {
23409
+ commands: selected,
23410
+ rejectedCommands,
23411
+ source: selected.some((command) => command.source === "mesh.policy.validationCommands") ? "mesh_policy" : selected.some((command) => command.source !== "package.json:scripts") ? "project_context" : selected.length ? "package_json_scripts" : "unavailable"
23412
+ };
23413
+ }
23414
+ async function runMeshRefineValidationGate(mesh, workspace) {
23415
+ const { execFile: execFile3 } = await import("child_process");
23416
+ const { promisify: promisify3 } = await import("util");
23417
+ const execFileAsync3 = promisify3(execFile3);
23418
+ const selection = selectMeshRefineValidationCommands(mesh, workspace);
23419
+ const summary = {
23420
+ status: "skipped",
23421
+ required: true,
23422
+ commandsRun: [],
23423
+ rejectedCommands: selection.rejectedCommands,
23424
+ skippedReason: void 0,
23425
+ timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
23426
+ outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES
23427
+ };
23428
+ if (!selection.commands.length) {
23429
+ summary.skippedReason = "validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available";
23430
+ return summary;
23431
+ }
23432
+ for (const candidate of selection.commands) {
23433
+ const startedAt = Date.now();
23434
+ try {
23435
+ const result = await execFileAsync3(candidate.command, candidate.args, {
23436
+ cwd: workspace,
23437
+ encoding: "utf8",
23438
+ timeout: REFINE_VALIDATION_TIMEOUT_MS,
23439
+ maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
23440
+ env: { ...process.env, CI: process.env.CI || "1" }
23441
+ });
23442
+ summary.commandsRun.push({
23443
+ command: candidate.command,
23444
+ args: candidate.args,
23445
+ displayCommand: candidate.displayCommand,
23446
+ category: candidate.category,
23447
+ source: candidate.source,
23448
+ passed: true,
23449
+ exitCode: 0,
23450
+ durationMs: Date.now() - startedAt,
23451
+ stdout: truncateValidationOutput(result.stdout),
23452
+ stderr: truncateValidationOutput(result.stderr)
23453
+ });
23454
+ } catch (error) {
23455
+ summary.commandsRun.push({
23456
+ command: candidate.command,
23457
+ args: candidate.args,
23458
+ displayCommand: candidate.displayCommand,
23459
+ category: candidate.category,
23460
+ source: candidate.source,
23461
+ passed: false,
23462
+ exitCode: typeof error?.code === "number" ? error.code : null,
23463
+ signal: typeof error?.signal === "string" ? error.signal : null,
23464
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || "")),
23465
+ durationMs: Date.now() - startedAt,
23466
+ stdout: truncateValidationOutput(error?.stdout),
23467
+ stderr: truncateValidationOutput(error?.stderr || error?.message)
23468
+ });
23469
+ summary.status = "failed";
23470
+ return summary;
23471
+ }
23472
+ }
23473
+ summary.status = "passed";
23474
+ return summary;
23475
+ }
22880
23476
  function loadYamlModule() {
22881
23477
  return yaml;
22882
23478
  }
@@ -23160,20 +23756,98 @@ var DaemonCommandRouter = class {
23160
23756
  recoveryHint: "Inspect the worktree branch and mesh metadata before retrying cleanup."
23161
23757
  };
23162
23758
  }
23759
+ const forceFallbackConvergence = await this.getWorktreeForceCleanupConvergence({
23760
+ repoRoot,
23761
+ workspace,
23762
+ node: args.node
23763
+ });
23163
23764
  try {
23164
- const result = await removeWorktree2(repoRoot, workspace, { requireClean: true });
23165
- return { success: true, removedPath: result.removedPath, repoRoot };
23765
+ const result = await removeWorktree2(repoRoot, workspace, {
23766
+ requireClean: true,
23767
+ allowSubmoduleForceFallback: forceFallbackConvergence.allow
23768
+ });
23769
+ return {
23770
+ success: true,
23771
+ removedPath: result.removedPath,
23772
+ repoRoot,
23773
+ ...result.fallback ? {
23774
+ fallback: result.fallback,
23775
+ forced: result.forced,
23776
+ reason: result.reason,
23777
+ convergence: forceFallbackConvergence
23778
+ } : {}
23779
+ };
23166
23780
  } catch (e) {
23167
23781
  const message = String(e?.message || e || "worktree cleanup failed");
23168
23782
  const dirty = message.includes("dirty worktree") || message.includes("local changes");
23783
+ const submoduleForceBlocked = /working trees containing submodules cannot be moved or removed/i.test(message) && !forceFallbackConvergence.allow;
23169
23784
  return {
23170
23785
  success: false,
23171
- code: dirty ? "mesh_worktree_cleanup_dirty" : "mesh_worktree_cleanup_failed",
23172
- error: message,
23173
- recoveryHint: dirty ? "Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe." : "Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure."
23786
+ code: dirty ? "mesh_worktree_cleanup_dirty" : submoduleForceBlocked ? "mesh_worktree_cleanup_force_fallback_blocked" : "mesh_worktree_cleanup_failed",
23787
+ error: submoduleForceBlocked ? `${message}; refusing --force fallback because convergence could not be verified: ${forceFallbackConvergence.error || "unknown convergence state"}` : message,
23788
+ recoveryHint: dirty ? "Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe." : submoduleForceBlocked ? "Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state before retrying. The mesh registry entry is preserved." : "Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.",
23789
+ ...submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}
23174
23790
  };
23175
23791
  }
23176
23792
  }
23793
+ async getWorktreeForceCleanupConvergence(args) {
23794
+ const metadataStatus = typeof args.node?.branchConvergence?.status === "string" ? args.node.branchConvergence.status : "";
23795
+ if (metadataStatus === "merged_to_main" || metadataStatus === "cleanup_candidate") {
23796
+ return { allow: true, status: metadataStatus, source: "node_branch_convergence" };
23797
+ }
23798
+ const { execFile: execFile3 } = await import("child_process");
23799
+ const { promisify: promisify3 } = await import("util");
23800
+ const execFileAsync3 = promisify3(execFile3);
23801
+ const runGit2 = async (gitArgs, cwd) => {
23802
+ const { stdout } = await execFileAsync3("git", gitArgs, {
23803
+ cwd,
23804
+ encoding: "utf8",
23805
+ timeout: 3e4,
23806
+ maxBuffer: 4 * 1024 * 1024,
23807
+ windowsHide: true
23808
+ });
23809
+ return String(stdout || "").trim();
23810
+ };
23811
+ let head = "";
23812
+ try {
23813
+ head = await runGit2(["rev-parse", "HEAD"], args.workspace);
23814
+ } catch (e) {
23815
+ return { allow: false, error: `could not resolve worktree HEAD: ${e?.message || e}` };
23816
+ }
23817
+ if (!head) return { allow: false, error: "worktree HEAD is empty" };
23818
+ const candidateRefs = [];
23819
+ try {
23820
+ const defaultBranch = await runGit2(["branch", "--show-current"], args.repoRoot);
23821
+ if (defaultBranch) {
23822
+ candidateRefs.push(defaultBranch, `origin/${defaultBranch}`);
23823
+ }
23824
+ } catch {
23825
+ }
23826
+ candidateRefs.push("origin/main", "origin/master", "main", "master");
23827
+ const seen = /* @__PURE__ */ new Set();
23828
+ const checkedRefs = [];
23829
+ for (const ref of candidateRefs) {
23830
+ if (!ref || seen.has(ref)) continue;
23831
+ seen.add(ref);
23832
+ let commit = "";
23833
+ try {
23834
+ commit = await runGit2(["rev-parse", "--verify", `${ref}^{commit}`], args.repoRoot);
23835
+ } catch {
23836
+ continue;
23837
+ }
23838
+ checkedRefs.push(ref);
23839
+ try {
23840
+ await runGit2(["merge-base", "--is-ancestor", head, commit], args.repoRoot);
23841
+ return { allow: true, status: "merged_to_default_ref", source: "git_merge_base", ref };
23842
+ } catch {
23843
+ }
23844
+ }
23845
+ return {
23846
+ allow: false,
23847
+ status: metadataStatus || void 0,
23848
+ error: checkedRefs.length ? `worktree HEAD is not contained in checked refs: ${checkedRefs.join(", ")}` : "no default/main refs were available for convergence verification"
23849
+ };
23850
+ }
23177
23851
  isCompletedHostedSession(record) {
23178
23852
  return record?.lifecycle === "stopped" || record?.lifecycle === "failed" || record?.lifecycle === "interrupted";
23179
23853
  }
@@ -24133,10 +24807,61 @@ var DaemonCommandRouter = class {
24133
24807
  if (!branch) return { success: false, error: "Could not determine branch of the worktree node" };
24134
24808
  const { stdout: baseBranchStdout } = await execFileAsync3("git", ["branch", "--show-current"], { cwd: repoRoot, encoding: "utf8" });
24135
24809
  const baseBranch = baseBranchStdout.trim();
24810
+ const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
24811
+ if (validationSummary.status === "failed") {
24812
+ return {
24813
+ success: false,
24814
+ code: "validation_failed",
24815
+ convergenceStatus: "blocked_review",
24816
+ error: "Refinery validation gate failed; merge/refine was not attempted.",
24817
+ branch,
24818
+ into: baseBranch,
24819
+ validationSummary,
24820
+ finalBranchConvergenceState: {
24821
+ branch,
24822
+ baseBranch,
24823
+ merged: false,
24824
+ removed: false,
24825
+ validation: "failed",
24826
+ status: "blocked_review"
24827
+ }
24828
+ };
24829
+ }
24830
+ if (validationSummary.status === "skipped") {
24831
+ return {
24832
+ success: false,
24833
+ code: "validation_unavailable",
24834
+ convergenceStatus: "blocked_review",
24835
+ error: "Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.",
24836
+ branch,
24837
+ into: baseBranch,
24838
+ validationSummary,
24839
+ finalBranchConvergenceState: {
24840
+ branch,
24841
+ baseBranch,
24842
+ merged: false,
24843
+ removed: false,
24844
+ validation: "unavailable",
24845
+ status: "blocked_review"
24846
+ }
24847
+ };
24848
+ }
24136
24849
  try {
24137
24850
  await execFileAsync3("git", ["merge", "--no-ff", branch, "-m", `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: "utf8" });
24138
24851
  } catch (e) {
24139
- return { success: false, error: `Merge failed (conflicts?): ${e.message}` };
24852
+ return {
24853
+ success: false,
24854
+ error: `Merge failed (conflicts?): ${e.message}`,
24855
+ validationSummary,
24856
+ finalBranchConvergenceState: {
24857
+ branch,
24858
+ baseBranch,
24859
+ merged: false,
24860
+ removed: false,
24861
+ validation: "passed",
24862
+ status: "not_mergeable"
24863
+ }
24864
+ };
24140
24865
  }
24141
24866
  const removeResult = await this.execute("remove_mesh_node", {
24142
24867
  meshId,
@@ -24149,11 +24874,27 @@ var DaemonCommandRouter = class {
24149
24874
  appendLedgerEntry2(meshId, {
24150
24875
  kind: "node_removed",
24151
24876
  nodeId,
24152
- payload: { refined: true, mergedBranch: branch, into: baseBranch }
24877
+ payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary }
24153
24878
  });
24154
24879
  } catch {
24155
24880
  }
24156
- return { success: true, merged: true, branch, into: baseBranch, removeResult };
24881
+ return {
24882
+ success: true,
24883
+ merged: true,
24884
+ branch,
24885
+ into: baseBranch,
24886
+ removeResult,
24887
+ validationSummary,
24888
+ finalBranchConvergenceState: {
24889
+ branch: baseBranch,
24890
+ mergedBranch: branch,
24891
+ baseBranch,
24892
+ merged: true,
24893
+ removed: removeResult?.success !== false,
24894
+ validation: "passed",
24895
+ status: removeResult?.success === false ? "merged_cleanup_failed" : "merged"
24896
+ }
24897
+ };
24157
24898
  } catch (e) {
24158
24899
  return { success: false, error: e.message };
24159
24900
  }
@@ -24208,7 +24949,10 @@ var DaemonCommandRouter = class {
24208
24949
  sessionCleanupMode,
24209
24950
  workspace: typeof node?.workspace === "string" ? node.workspace : void 0,
24210
24951
  daemonId: typeof node?.daemonId === "string" ? node.daemonId : void 0,
24211
- worktreeBranch: typeof node?.worktreeBranch === "string" ? node.worktreeBranch : void 0
24952
+ worktreeBranch: typeof node?.worktreeBranch === "string" ? node.worktreeBranch : void 0,
24953
+ worktreeCleanupFallback: typeof worktreeCleanup?.fallback === "string" ? worktreeCleanup.fallback : void 0,
24954
+ forced: worktreeCleanup?.forced === true ? true : void 0,
24955
+ forceFallbackReason: typeof worktreeCleanup?.reason === "string" ? worktreeCleanup.reason : void 0
24212
24956
  }
24213
24957
  });
24214
24958
  } catch {
@@ -32345,6 +33089,9 @@ function launchIDE(ide, workspacePath) {
32345
33089
  }
32346
33090
  }
32347
33091
 
33092
+ // src/boot/daemon-lifecycle.ts
33093
+ init_cli_detector();
33094
+
32348
33095
  // src/sessions/registry.ts
32349
33096
  var SessionRegistry = class {
32350
33097
  bySessionId = /* @__PURE__ */ new Map();
@@ -32681,6 +33428,7 @@ export {
32681
33428
  MIN_MACHINE_RUNTIME_SUBSCRIPTION_INTERVAL_MS,
32682
33429
  MIN_SESSION_HOST_DIAGNOSTICS_SUBSCRIPTION_INTERVAL_MS,
32683
33430
  NodePtyTransportFactory,
33431
+ P2pRelayFailureError,
32684
33432
  ProviderCliAdapter,
32685
33433
  ProviderInstanceManager,
32686
33434
  ProviderLoader,
@@ -32697,6 +33445,7 @@ export {
32697
33445
  buildChatTailDeliverySignature,
32698
33446
  buildCoordinatorSystemPrompt,
32699
33447
  buildMachineInfo,
33448
+ buildP2pRelayFailurePayload,
32700
33449
  buildPinnedGlobalInstallCommand,
32701
33450
  buildRuntimeSystemChatMessage,
32702
33451
  buildSessionEntries,
@@ -32711,6 +33460,7 @@ export {
32711
33460
  claimNextTask,
32712
33461
  classifyChatMessageVisibility,
32713
33462
  classifyHotChatSessionsForSubscriptionFlush,
33463
+ classifyP2pRelayFailure,
32714
33464
  clearDebugTrace,
32715
33465
  compareGitSnapshots,
32716
33466
  configureDebugTraceStore,
@@ -32778,6 +33528,7 @@ export {
32778
33528
  isInternalChatMessage,
32779
33529
  isManagedStatusWaiting,
32780
33530
  isManagedStatusWorking,
33531
+ isP2pRelayTransportFailure,
32781
33532
  isPathInside,
32782
33533
  isSessionHostLiveRuntime,
32783
33534
  isSessionHostRecoverySnapshot,