@taskless-app/swarm 0.4.9 → 0.5.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 (4) hide show
  1. package/README.md +98 -16
  2. package/dist/cli.js +1762 -616
  3. package/dist/overmind.js +1022 -243
  4. package/package.json +1 -1
package/dist/overmind.js CHANGED
@@ -397,9 +397,14 @@ var init_windows = __esm({
397
397
  });
398
398
 
399
399
  // ../swarm-core/src/slug.ts
400
+ function isTaskSlug(s) {
401
+ return !!s && TASK_SLUG_RE.test(s);
402
+ }
403
+ var TASK_SLUG_RE;
400
404
  var init_slug = __esm({
401
405
  "../swarm-core/src/slug.ts"() {
402
406
  "use strict";
407
+ TASK_SLUG_RE = /^(?:[A-Za-z0-9]{1,8}-\d+|#[A-Za-z0-9]{4,})$/;
403
408
  }
404
409
  });
405
410
 
@@ -419,14 +424,20 @@ function extractText(d) {
419
424
  function sanitizeEventText(text) {
420
425
  if (text == null) return null;
421
426
  const cleaned = text.replace(ANSI_CSI_RE, "").replace(CONTROL_CHARS_RE, " ").replace(/ {2,}/g, " ").trim();
422
- return cleaned.length ? cleaned : null;
427
+ if (!cleaned.length) return null;
428
+ return cleaned.length <= EVENT_TEXT_MAX ? cleaned : clampEventText(cleaned);
429
+ }
430
+ function clampEventText(text) {
431
+ const note = ` \u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u0431\u044B\u043B\u043E ${text.length} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432)`;
432
+ return text.slice(0, Math.max(0, EVENT_TEXT_MAX - note.length)) + note;
423
433
  }
424
- var ANSI_CSI_RE, CONTROL_CHARS_RE;
434
+ var ANSI_CSI_RE, CONTROL_CHARS_RE, EVENT_TEXT_MAX;
425
435
  var init_text = __esm({
426
436
  "../swarm-core/src/text.ts"() {
427
437
  "use strict";
428
438
  ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
429
439
  CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
440
+ EVENT_TEXT_MAX = 16e3;
430
441
  }
431
442
  });
432
443
 
@@ -709,6 +720,50 @@ var init_hosts = __esm({
709
720
  }
710
721
  });
711
722
 
723
+ // ../swarm-core/src/hostCommands.ts
724
+ function isHostCommandKind(v) {
725
+ return typeof v === "string" && HOST_COMMAND_KINDS.includes(v);
726
+ }
727
+ function isHostCommandExpired(expiresAt, nowMs) {
728
+ if (!expiresAt) return false;
729
+ const at = Date.parse(expiresAt);
730
+ return Number.isFinite(at) && at <= nowMs;
731
+ }
732
+ function hostCommandLabel(cmd) {
733
+ const base3 = HOST_COMMAND_LABELS[cmd.kind];
734
+ if (cmd.kind === "set_max_drones" && cmd.maxDrones != null) return `${base3} \u2192 ${cmd.maxDrones}`;
735
+ if (cmd.kind === "stop_run" && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
736
+ return base3;
737
+ }
738
+ var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, MAX_DRONES_LIMIT;
739
+ var init_hostCommands = __esm({
740
+ "../swarm-core/src/hostCommands.ts"() {
741
+ "use strict";
742
+ HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run"];
743
+ HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
744
+ TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
745
+ HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
746
+ UPDATE_STEP_LABELS = {
747
+ fetch: "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434",
748
+ checkout: "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0430\u044E\u0441\u044C \u043D\u0430 \u0441\u0432\u0435\u0436\u0438\u0439 main",
749
+ install: "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438",
750
+ build: "\u0441\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u043A\u0435\u0442\u044B \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430",
751
+ restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u044E\u0441\u044C"
752
+ };
753
+ HOST_COMMAND_TTL_MS = 30 * 6e4;
754
+ HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
755
+ HOST_COMMAND_LABELS = {
756
+ pause: "\u043F\u0430\u0443\u0437\u0430",
757
+ resume: "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
758
+ restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
759
+ update: "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043A\u043E\u0434\u0430",
760
+ set_max_drones: "\u0441\u043C\u0435\u043D\u0430 \u0447\u0438\u0441\u043B\u0430 \u0434\u0440\u043E\u043D\u043E\u0432",
761
+ stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"
762
+ };
763
+ MAX_DRONES_LIMIT = 8;
764
+ }
765
+ });
766
+
712
767
  // ../swarm-core/src/relief.ts
713
768
  var RELIEF_PAUSE_MARK;
714
769
  var init_relief = __esm({
@@ -735,6 +790,7 @@ var init_src = __esm({
735
790
  init_names();
736
791
  init_baseBranch();
737
792
  init_hosts();
793
+ init_hostCommands();
738
794
  init_relief();
739
795
  }
740
796
  });
@@ -742,6 +798,9 @@ var init_src = __esm({
742
798
  // src/config.ts
743
799
  var config_exports = {};
744
800
  __export(config_exports, {
801
+ DAEMON_LOG_CHECK_MS: () => DAEMON_LOG_CHECK_MS,
802
+ DAEMON_LOG_KEEP: () => DAEMON_LOG_KEEP,
803
+ DAEMON_LOG_MAX_BYTES: () => DAEMON_LOG_MAX_BYTES,
745
804
  DAEMON_LOG_PATH: () => DAEMON_LOG_PATH,
746
805
  DB_PATH: () => DB_PATH,
747
806
  DRONE_CHANNEL_ENABLED: () => DRONE_CHANNEL_ENABLED,
@@ -754,6 +813,7 @@ __export(config_exports, {
754
813
  DRONE_ISOLATION: () => DRONE_ISOLATION,
755
814
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
756
815
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
816
+ REPO_MAP_PATH: () => REPO_MAP_PATH,
757
817
  REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
758
818
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
759
819
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
@@ -791,9 +851,11 @@ __export(config_exports, {
791
851
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
792
852
  SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
793
853
  SWARM_REPO: () => SWARM_REPO,
854
+ SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
794
855
  SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
795
856
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
796
857
  SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
858
+ SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
797
859
  SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
798
860
  SWARM_STORM_URL: () => SWARM_STORM_URL,
799
861
  SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
@@ -893,7 +955,7 @@ function reviewCutoffAt() {
893
955
  }
894
956
  return at;
895
957
  }
896
- var SWARM_HOME, DB_PATH, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
958
+ var SWARM_HOME, DB_PATH, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
897
959
  var init_config = __esm({
898
960
  "src/config.ts"() {
899
961
  "use strict";
@@ -909,6 +971,9 @@ var init_config = __esm({
909
971
  WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
910
972
  RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join2(SWARM_HOME, "run-logs");
911
973
  DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join2(SWARM_HOME, "overmind.log");
974
+ DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
975
+ DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
976
+ DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
912
977
  DIST_BUILD = true;
913
978
  SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
914
979
  SWARM_OUTBOX = process.env.SWARM_OUTBOX || join2(SWARM_HOME, "outbox.jsonl");
@@ -943,6 +1008,9 @@ var init_config = __esm({
943
1008
  SWARM_REPO = process.env.SWARM_REPO || "";
944
1009
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
945
1010
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
1011
+ SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
1012
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join2(SWARM_HOME, "repo-map.json");
1013
+ SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
946
1014
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
947
1015
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
948
1016
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -1435,7 +1503,7 @@ var init_procList = __esm({
1435
1503
 
1436
1504
  // src/procCleanup.ts
1437
1505
  function sleep(ms) {
1438
- return new Promise((resolve3) => setTimeout(resolve3, ms));
1506
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1439
1507
  }
1440
1508
  function parseLsofCwd(output) {
1441
1509
  const rows = [];
@@ -1498,8 +1566,8 @@ function pathUnderInCmd(cmd, dir) {
1498
1566
  if (before && !/["'\s=;]/.test(before)) continue;
1499
1567
  const after = hay.slice(at + needle.length);
1500
1568
  if (after && !after.startsWith("/")) continue;
1501
- const tail = after.match(/^(?:\/[^"';]*)?/)[0];
1502
- return needle + tail;
1569
+ const tail2 = after.match(/^(?:\/[^"';]*)?/)[0];
1570
+ return needle + tail2;
1503
1571
  }
1504
1572
  return null;
1505
1573
  }
@@ -1653,16 +1721,16 @@ var init_procCleanup = __esm({
1653
1721
  });
1654
1722
 
1655
1723
  // src/git.ts
1656
- import { existsSync as existsSync7, readdirSync as readdirSync8, statSync as statSync5 } from "fs";
1724
+ import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync6 } from "fs";
1657
1725
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
1658
- import { isAbsolute, join as join11 } from "path";
1726
+ import { isAbsolute as isAbsolute2, join as join12 } from "path";
1659
1727
  import { $ as $2 } from "execa";
1660
1728
  async function reclaimStaleWorktree(repo, worktree) {
1661
1729
  await detachWorktree(repo, worktree);
1662
1730
  }
1663
1731
  function isLinkedWorktree(dir) {
1664
1732
  try {
1665
- return statSync5(join11(dir, ".git")).isFile();
1733
+ return statSync6(join12(dir, ".git")).isFile();
1666
1734
  } catch {
1667
1735
  return false;
1668
1736
  }
@@ -1674,7 +1742,7 @@ async function detachWorktree(repo, worktree) {
1674
1742
  }
1675
1743
  }
1676
1744
  async function ensureWorktreeDirGone(worktree) {
1677
- if (!existsSync7(worktree)) return false;
1745
+ if (!existsSync8(worktree)) return false;
1678
1746
  if (!isLinkedWorktree(worktree)) {
1679
1747
  console.error(
1680
1748
  `[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
@@ -1689,7 +1757,7 @@ async function ensureWorktreeDirGone(worktree) {
1689
1757
  );
1690
1758
  return true;
1691
1759
  }
1692
- if (existsSync7(worktree)) {
1760
+ if (existsSync8(worktree)) {
1693
1761
  console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
1694
1762
  }
1695
1763
  return true;
@@ -1732,7 +1800,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
1732
1800
  }
1733
1801
  const busyAt = await worktreeForBranch(repo, branch);
1734
1802
  if (busyAt) {
1735
- const exists = existsSync7(busyAt);
1803
+ const exists = existsSync8(busyAt);
1736
1804
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
1737
1805
  if (live) throw new WorktreeBusyError(branch, busyAt);
1738
1806
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -1768,9 +1836,9 @@ async function ensureGitExcluded(worktree, pattern) {
1768
1836
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
1769
1837
  const raw = out.stdout.trim();
1770
1838
  if (out.exitCode !== 0 || !raw) return false;
1771
- const commonDir = isAbsolute(raw) ? raw : join11(worktree, raw);
1772
- const infoDir = join11(commonDir, "info");
1773
- const excludeFile = join11(infoDir, "exclude");
1839
+ const commonDir = isAbsolute2(raw) ? raw : join12(worktree, raw);
1840
+ const infoDir = join12(commonDir, "info");
1841
+ const excludeFile = join12(infoDir, "exclude");
1774
1842
  let current = "";
1775
1843
  try {
1776
1844
  current = await readFile(excludeFile, "utf8");
@@ -1803,7 +1871,7 @@ async function worktreeDirty(path) {
1803
1871
  async function repairMovedWorktrees(targetDir) {
1804
1872
  const subdirs = (dir) => {
1805
1873
  try {
1806
- return readdirSync8(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join11(dir, d.name));
1874
+ return readdirSync8(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join12(dir, d.name));
1807
1875
  } catch {
1808
1876
  return [];
1809
1877
  }
@@ -1915,18 +1983,18 @@ __export(droneChannel_exports, {
1915
1983
  verifyLocalCliToken: () => verifyLocalCliToken
1916
1984
  });
1917
1985
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1918
- import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
1986
+ import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
1919
1987
  import { dirname as dirname3 } from "path";
1920
1988
  import { serve } from "@hono/node-server";
1921
1989
  function loadChannelSecret(path) {
1922
1990
  try {
1923
- const s = readFileSync12(path, "utf8").trim();
1991
+ const s = readFileSync13(path, "utf8").trim();
1924
1992
  if (s.length >= 32) return s;
1925
1993
  } catch {
1926
1994
  }
1927
1995
  const secret = randomBytes(32).toString("hex");
1928
1996
  mkdirSync5(dirname3(path), { recursive: true });
1929
- writeFileSync8(path, secret, { mode: 384 });
1997
+ writeFileSync9(path, secret, { mode: 384 });
1930
1998
  try {
1931
1999
  chmodSync(path, 384);
1932
2000
  } catch {
@@ -2133,8 +2201,8 @@ function startDroneChannel(deps, port) {
2133
2201
  return { port, server };
2134
2202
  }
2135
2203
  function closeDroneChannel(server) {
2136
- return new Promise((resolve3) => {
2137
- server.close(() => resolve3());
2204
+ return new Promise((resolve4) => {
2205
+ server.close(() => resolve4());
2138
2206
  server.closeAllConnections?.();
2139
2207
  });
2140
2208
  }
@@ -2256,7 +2324,7 @@ var init_droneChannel = __esm({
2256
2324
 
2257
2325
  // src/attachments.ts
2258
2326
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
2259
- import { join as join13 } from "path";
2327
+ import { join as join14 } from "path";
2260
2328
  function attachmentsFromDoc(doc, origin) {
2261
2329
  const out = [];
2262
2330
  const walk2 = (node) => {
@@ -2347,7 +2415,7 @@ async function materializeAttachments(opts) {
2347
2415
  };
2348
2416
  const result = { files: [], skipped: [] };
2349
2417
  if (opts.refs.length === 0) return result;
2350
- const dir = join13(opts.worktree, ATTACHMENTS_DIR);
2418
+ const dir = join14(opts.worktree, ATTACHMENTS_DIR);
2351
2419
  await io.removeDir(dir);
2352
2420
  let dirReady = false;
2353
2421
  let total = 0;
@@ -2393,7 +2461,7 @@ async function materializeAttachments(opts) {
2393
2461
  await io.mkdir(dir);
2394
2462
  dirReady = true;
2395
2463
  }
2396
- await io.writeFile(join13(dir, name), file.bytes);
2464
+ await io.writeFile(join14(dir, name), file.bytes);
2397
2465
  } catch (e) {
2398
2466
  result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
2399
2467
  continue;
@@ -2464,8 +2532,8 @@ function pathOf(url) {
2464
2532
  return url.split(/[?#]/)[0] ?? "";
2465
2533
  }
2466
2534
  function extOf(nameOrPath) {
2467
- const tail = nameOrPath.split("/").pop() ?? "";
2468
- const m = /\.([A-Za-z0-9]{1,8})$/.exec(tail);
2535
+ const tail2 = nameOrPath.split("/").pop() ?? "";
2536
+ const m = /\.([A-Za-z0-9]{1,8})$/.exec(tail2);
2469
2537
  return m ? m[1].toLowerCase() : "";
2470
2538
  }
2471
2539
  function mb(bytes) {
@@ -2546,7 +2614,7 @@ var init_attachments = __esm({
2546
2614
 
2547
2615
  // src/shotUpload.ts
2548
2616
  import { readFile as fsReadFile } from "fs/promises";
2549
- import { basename as basename2, isAbsolute as isAbsolute2, join as join14 } from "path";
2617
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join15 } from "path";
2550
2618
  function shotMimetype(path) {
2551
2619
  const dot = path.lastIndexOf(".");
2552
2620
  if (dot < 0) return null;
@@ -2572,7 +2640,7 @@ async function readShots(paths, opts) {
2572
2640
  }
2573
2641
  let bytes;
2574
2642
  try {
2575
- bytes = await read(isAbsolute2(path) ? path : join14(opts.worktree, path));
2643
+ bytes = await read(isAbsolute3(path) ? path : join15(opts.worktree, path));
2576
2644
  } catch (e) {
2577
2645
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
2578
2646
  continue;
@@ -2650,8 +2718,8 @@ var init_shotUpload = __esm({
2650
2718
 
2651
2719
  // src/oauth.ts
2652
2720
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2653
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
2654
- import { join as join15 } from "path";
2721
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2722
+ import { join as join16 } from "path";
2655
2723
  function makePkce() {
2656
2724
  const verifier = randomBytes2(48).toString("base64url");
2657
2725
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -2735,23 +2803,23 @@ function toStore(clientId, t, now) {
2735
2803
  }
2736
2804
  function loadStore() {
2737
2805
  try {
2738
- return JSON.parse(readFileSync13(STORE_PATH, "utf8"));
2806
+ return JSON.parse(readFileSync14(STORE_PATH, "utf8"));
2739
2807
  } catch {
2740
2808
  return null;
2741
2809
  }
2742
2810
  }
2743
2811
  function saveStore(s) {
2744
- writeFileSync9(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
2812
+ writeFileSync10(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
2745
2813
  }
2746
2814
  function clearStore() {
2747
2815
  try {
2748
- unlinkSync2(STORE_PATH);
2816
+ unlinkSync3(STORE_PATH);
2749
2817
  } catch {
2750
2818
  }
2751
2819
  }
2752
2820
  function loadClientId() {
2753
2821
  try {
2754
- return JSON.parse(readFileSync13(CLIENT_PATH, "utf8")).clientId;
2822
+ return JSON.parse(readFileSync14(CLIENT_PATH, "utf8")).clientId;
2755
2823
  } catch {
2756
2824
  return null;
2757
2825
  }
@@ -2760,7 +2828,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
2760
2828
  const existing = loadClientId();
2761
2829
  if (existing) return existing;
2762
2830
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
2763
- writeFileSync9(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
2831
+ writeFileSync10(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
2764
2832
  return clientId;
2765
2833
  }
2766
2834
  async function ensureAccess(store, opts = {}) {
@@ -2786,18 +2854,18 @@ var init_oauth = __esm({
2786
2854
  AUTHORIZE = "/api/oauth/authorize";
2787
2855
  TOKEN = "/api/oauth/token";
2788
2856
  SCOPE = "openid profile email taskless:full";
2789
- STORE_PATH = join15(SWARM_HOME, "oauth.json");
2790
- CLIENT_PATH = join15(SWARM_HOME, "oauth-client.json");
2857
+ STORE_PATH = join16(SWARM_HOME, "oauth.json");
2858
+ CLIENT_PATH = join16(SWARM_HOME, "oauth-client.json");
2791
2859
  base = (api) => api.replace(/\/$/, "");
2792
2860
  }
2793
2861
  });
2794
2862
 
2795
2863
  // src/agentToken.ts
2796
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2797
- import { join as join16 } from "path";
2864
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
2865
+ import { join as join17 } from "path";
2798
2866
  function loadAgentToken() {
2799
2867
  try {
2800
- const s = JSON.parse(readFileSync14(STORE_PATH2, "utf8"));
2868
+ const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
2801
2869
  return s.token ? s : null;
2802
2870
  } catch {
2803
2871
  return null;
@@ -2834,17 +2902,17 @@ var init_agentToken = __esm({
2834
2902
  "src/agentToken.ts"() {
2835
2903
  "use strict";
2836
2904
  init_config();
2837
- STORE_PATH2 = join16(SWARM_HOME, "agent.json");
2905
+ STORE_PATH2 = join17(SWARM_HOME, "agent.json");
2838
2906
  base2 = (api) => api.replace(/\/$/, "");
2839
2907
  }
2840
2908
  });
2841
2909
 
2842
2910
  // src/workspace.ts
2843
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
2844
- import { join as join17 } from "path";
2911
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
2912
+ import { join as join18 } from "path";
2845
2913
  function loadWorkspace() {
2846
2914
  try {
2847
- const w = JSON.parse(readFileSync15(WORKSPACE_PATH, "utf8"));
2915
+ const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
2848
2916
  return w && typeof w.id === "string" && w.id ? w : null;
2849
2917
  } catch {
2850
2918
  return null;
@@ -2858,7 +2926,7 @@ var init_workspace = __esm({
2858
2926
  "src/workspace.ts"() {
2859
2927
  "use strict";
2860
2928
  init_config();
2861
- WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
2929
+ WORKSPACE_PATH = join18(SWARM_HOME, "workspace.json");
2862
2930
  }
2863
2931
  });
2864
2932
 
@@ -3051,14 +3119,14 @@ async function fetchTaskContext(client, task) {
3051
3119
  const parts = [];
3052
3120
  const comments = task.comments ?? [];
3053
3121
  if (comments.length > 0) {
3054
- const tail = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
3055
- const skipped = comments.length - tail.length;
3056
- const lines = tail.map((c) => {
3122
+ const tail2 = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
3123
+ const skipped = comments.length - tail2.length;
3124
+ const lines = tail2.map((c) => {
3057
3125
  const cut = c.text.length > TASK_CONTEXT_MAX_COMMENT_CHARS;
3058
3126
  const text = cut ? `${c.text.slice(0, TASK_CONTEXT_MAX_COMMENT_CHARS)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : c.text;
3059
3127
  return `\u2014 [${c.createdAt} \xB7 ${c.author}] ${text}`;
3060
3128
  });
3061
- const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
3129
+ const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail2.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
3062
3130
  parts.push([header, ...lines].join("\n"));
3063
3131
  }
3064
3132
  const documents = task.documents ?? [];
@@ -3285,8 +3353,8 @@ __export(db_exports, {
3285
3353
  SwarmDb: () => SwarmDb
3286
3354
  });
3287
3355
  import { createRequire as createRequire2 } from "module";
3288
- import { existsSync as existsSync9 } from "fs";
3289
- import { resolve as resolve2 } from "path";
3356
+ import { existsSync as existsSync10 } from "fs";
3357
+ import { resolve as resolve3 } from "path";
3290
3358
  function rowToQuestion(row) {
3291
3359
  return {
3292
3360
  id: row.id,
@@ -3766,11 +3834,11 @@ CREATE TABLE IF NOT EXISTS imports (
3766
3834
  * транзакции: прервётся — маркер не встанет, повтор безопасен.
3767
3835
  */
3768
3836
  importDb(sourcePath, repo) {
3769
- const abs = resolve2(sourcePath);
3837
+ const abs = resolve3(sourcePath);
3770
3838
  const repoN = normalizeRepo(repo);
3771
3839
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
3772
3840
  if (already) return { imported: false, runs: already.run_count };
3773
- if (!existsSync9(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
3841
+ if (!existsSync10(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
3774
3842
  const src = new this.databaseCtor(abs);
3775
3843
  try {
3776
3844
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -4138,9 +4206,9 @@ var init_droneStore = __esm({
4138
4206
  });
4139
4207
 
4140
4208
  // src/overmind.ts
4141
- import { homedir as homedir3 } from "os";
4142
- import { existsSync as existsSync12, readFileSync as readFileSync17, readdirSync as readdirSync10, renameSync as renameSync7 } from "fs";
4143
- import { basename as basename4, dirname as dirname5, join as join21 } from "path";
4209
+ import { homedir as homedir4 } from "os";
4210
+ import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync10, renameSync as renameSync10 } from "fs";
4211
+ import { basename as basename4, dirname as dirname5, join as join25 } from "path";
4144
4212
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4145
4213
  import { spawn as spawn3 } from "child_process";
4146
4214
 
@@ -4291,25 +4359,25 @@ function runLogTail(runLogsDir, localRunId, opts) {
4291
4359
  const parts = [];
4292
4360
  let budget = opts.maxBytes;
4293
4361
  if (launch.err) {
4294
- const tail = readTail(launch.err, { lines: opts.lines, maxBytes: Math.floor(opts.maxBytes / 2) });
4295
- if (tail && tail.text.trim() !== "") {
4296
- const block = `${fileHeader(`${launch.index}.err.log`, tail.size, tail.shown)}
4297
- ${tail.text}`;
4362
+ const tail2 = readTail(launch.err, { lines: opts.lines, maxBytes: Math.floor(opts.maxBytes / 2) });
4363
+ if (tail2 && tail2.text.trim() !== "") {
4364
+ const block = `${fileHeader(`${launch.index}.err.log`, tail2.size, tail2.shown)}
4365
+ ${tail2.text}`;
4298
4366
  parts.push(block);
4299
4367
  budget -= Buffer.byteLength(block, "utf8");
4300
4368
  }
4301
4369
  }
4302
4370
  if (launch.out) {
4303
- const tail = readTail(launch.out, { lines: opts.lines, maxBytes: Math.max(1024, budget) });
4304
- if (tail) {
4371
+ const tail2 = readTail(launch.out, { lines: opts.lines, maxBytes: Math.max(1024, budget) });
4372
+ if (tail2) {
4305
4373
  parts.push(
4306
- tail.size === 0 ? (
4374
+ tail2.size === 0 ? (
4307
4375
  // Пустой stdout при живом дроне — норма, а не поломка: claude в headless-режиме
4308
4376
  // (`--output-format json`) печатает результат ОДНИМ объектом в самом конце.
4309
4377
  `=== ${launch.index}.json (\u043F\u0443\u0441\u0442) ===
4310
4378
  Stdout \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D: claude \u043E\u0442\u0434\u0430\u0451\u0442 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u043E\u0434\u043D\u0438\u043C JSON \u0432 \u043A\u043E\u043D\u0446\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430. \u0414\u0440\u043E\u043D, \u0441\u043A\u043E\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043E, \u0435\u0449\u0451 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u2014 \u0441\u043C\u043E\u0442\u0440\u0438 stderr \u0432\u044B\u0448\u0435 \u0438 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430.`
4311
- ) : `${fileHeader(`${launch.index}.json`, tail.size, tail.shown)}
4312
- ${tail.text}`
4379
+ ) : `${fileHeader(`${launch.index}.json`, tail2.size, tail2.shown)}
4380
+ ${tail2.text}`
4313
4381
  );
4314
4382
  }
4315
4383
  }
@@ -4319,15 +4387,15 @@ ${tail.text}`
4319
4387
  return { text: head + parts.join("\n\n") };
4320
4388
  }
4321
4389
  function daemonLogTail(path, opts) {
4322
- const tail = readTail(path, opts);
4323
- if (!tail) {
4390
+ const tail2 = readTail(path, opts);
4391
+ if (!tail2) {
4324
4392
  return {
4325
4393
  error: `\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442: ${path} \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F. \u0414\u0435\u043C\u043E\u043D \u043F\u0438\u0448\u0435\u0442 \u0432 stdout \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u0444\u0430\u0439\u043B \u043F\u0435\u0440\u0435\u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435\u043C (\`swarm start >> ${path} 2>&1\`) \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438 \u0441\u0432\u043E\u0439 \u043F\u0443\u0442\u044C \u0432 SWARM_DAEMON_LOG.`
4326
4394
  };
4327
4395
  }
4328
- if (tail.size === 0) return { error: `\u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043F\u0443\u0441\u0442 (${path})` };
4329
- return { text: `${fileHeader(path, tail.size, tail.shown)}
4330
- ${tail.text}` };
4396
+ if (tail2.size === 0) return { error: `\u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043F\u0443\u0441\u0442 (${path})` };
4397
+ return { text: `${fileHeader(path, tail2.size, tail2.shown)}
4398
+ ${tail2.text}` };
4331
4399
  }
4332
4400
  async function serveLogRequests(deps, commands) {
4333
4401
  for (const cmd of commands) {
@@ -4366,6 +4434,11 @@ function runRef(runKey) {
4366
4434
  function isRunRef(v) {
4367
4435
  return typeof v === "object" && v !== null && "__runRef" in v;
4368
4436
  }
4437
+ function isHostCommandShape(v) {
4438
+ const c = v;
4439
+ return !!c && typeof c === "object" && typeof c.id === "string" && !!c.id && typeof c.kind === "string";
4440
+ }
4441
+ var EMPTY_HEARTBEAT_REPLY = { commands: [], logRequests: [] };
4369
4442
  function jsonFromTextContent(res) {
4370
4443
  const text = res?.content?.find((c) => c.type === "text")?.text;
4371
4444
  if (text) {
@@ -4545,6 +4618,18 @@ var SwarmClient = class _SwarmClient {
4545
4618
  runKeyFor(localId) {
4546
4619
  return this.keyByLocal.get(localId);
4547
4620
  }
4621
+ /**
4622
+ * TASWM-336: заменить репо-реестр на лету. До этой задачи реестр собирался ОДИН РАЗ на
4623
+ * старте из окружения и жил до рестарта демона; теперь машина сама находит клоны своих
4624
+ * репозиториев и переспрашивает прод по TTL — у владельца появился новый swarm-проект,
4625
+ * и машина обязана подхватить его без рестарта. Меняем обе стороны разом: и список для
4626
+ * claim/heartbeat, и обратную карту `owner/repo` → путь (иначе прогон нового репо
4627
+ * пришёл бы именем, которое не во что перевести).
4628
+ */
4629
+ setRepoRegistry(registry) {
4630
+ this.repoFullNames = registry.map((e) => e.repoFullName);
4631
+ this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
4632
+ }
4548
4633
  // cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
4549
4634
  // Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
4550
4635
  // прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
@@ -5537,17 +5622,24 @@ var SwarmClient = class _SwarmClient {
5537
5622
  * прислал предыдущий (более свежий по коду) процесс демона.
5538
5623
  */
5539
5624
  /**
5540
- * TASWM-322: ответ на пульс — ЕДИНСТВЕННЫЙ канал «прод → демон» (демон за домашним
5541
- * роутером, прод к нему не постучится). Оттуда приезжают команды машине — сейчас одна:
5542
- * «пришли хвост лога» (logRequests). Возвращаем их вызывающему; пульс недоступен или
5543
- * прод старый (поля нет) → пустой список, всё как раньше.
5544
- */
5545
- /**
5625
+ * TASWM-321: ВОЗВРАЩАЕТ команды этой машине (пауза/рестарт/обновление/…), приехавшие
5626
+ * в ответе на пульс. Это единственный момент, когда прод может что-то сказать демону:
5627
+ * тот живёт за домашним роутером без белого адреса, и связь всегда инициирует он сам.
5628
+ * Прод, отдав команды, метит их доставленными — повторный пульс их не вернёт, поэтому
5629
+ * потерять ответ здесь значит потерять команду; парсим бережно и не бросаем.
5630
+ *
5631
+ * TASWM-322: тем же ответом приезжают запросы «пришли хвост лога» (logRequests) —
5632
+ * канал «прод → демон» один на оба вида поручений, поэтому и разбираем их вместе.
5633
+ * Возвращаем ОБА списка объектом: два разных потребителя в overmind, и слепить их в
5634
+ * один список значило бы заставить каждого фильтровать чужое.
5635
+ *
5546
5636
  * TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
5547
5637
  * pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
5548
5638
  * и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
5549
5639
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
5550
5640
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
5641
+ *
5642
+ * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5551
5643
  */
5552
5644
  async heartbeat(daemonVersion, pause) {
5553
5645
  try {
@@ -5562,9 +5654,33 @@ var SwarmClient = class _SwarmClient {
5562
5654
  args.hostName = this.host.hostName;
5563
5655
  }
5564
5656
  const res = await this.call("swarm_heartbeat", args);
5565
- return parseLogRequests(res?.structuredContent?.logRequests);
5657
+ if (res?.isError) return EMPTY_HEARTBEAT_REPLY;
5658
+ const data = res?.structuredContent ?? {};
5659
+ return {
5660
+ commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
5661
+ logRequests: parseLogRequests(data.logRequests)
5662
+ };
5663
+ } catch {
5664
+ return EMPTY_HEARTBEAT_REPLY;
5665
+ }
5666
+ }
5667
+ /**
5668
+ * TASWM-321: подтвердить исполнение команды (`swarm_command_ack`). ПРЯМОЙ вызов, не
5669
+ * outbox: подтверждение — «скажи СЕЙЧАС», ради него команду и заводили («без
5670
+ * подтверждения непонятно, обновился он или команда потерялась по дороге»), а
5671
+ * промежуточные шаги обновления, доехавшие после самого обновления, бессмысленны.
5672
+ * Не бросает: сорвавшийся ack не должен рушить тик демона — команда останется в
5673
+ * прошлом состоянии, следующий шаг/финал её всё равно подвинут.
5674
+ */
5675
+ async ackCommand(commandId, status, patch = {}) {
5676
+ try {
5677
+ await this.call("swarm_command_ack", {
5678
+ commandId,
5679
+ status,
5680
+ ...patch.step ? { step: patch.step } : {},
5681
+ ...patch.result ? { result: patch.result.slice(0, 2e3) } : {}
5682
+ });
5566
5683
  } catch {
5567
- return [];
5568
5684
  }
5569
5685
  }
5570
5686
  /**
@@ -5733,8 +5849,9 @@ var SwarmClient = class _SwarmClient {
5733
5849
  this.runs.set(id, local);
5734
5850
  this.keyByLocal.set(id, runKey);
5735
5851
  }
5736
- const slug = pr.taskSlug ?? pr.task?.slug ?? pr.taskId ?? local.task_slug;
5737
- local.task_slug = slug || local.task_slug;
5852
+ const prodSlug = pr.taskSlug ?? pr.task?.slug ?? null;
5853
+ if (prodSlug) local.task_slug = prodSlug;
5854
+ else if (!isTaskSlug(local.task_slug)) local.task_slug = pr.taskId || local.task_slug;
5738
5855
  local.task_title = pr.taskTitle ?? local.task_title;
5739
5856
  local.task_desc = pr.taskDesc ?? local.task_desc;
5740
5857
  local.name = pr.droneName ?? local.name;
@@ -6053,6 +6170,66 @@ function parseDecision(v) {
6053
6170
  init_config();
6054
6171
  init_heartbeat();
6055
6172
 
6173
+ // src/logRotate.ts
6174
+ import { copyFileSync, renameSync as renameSync4, statSync as statSync2, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync4 } from "fs";
6175
+ var realIo = {
6176
+ size: (path) => {
6177
+ try {
6178
+ return statSync2(path).size;
6179
+ } catch {
6180
+ return null;
6181
+ }
6182
+ },
6183
+ copy: copyFileSync,
6184
+ rename: renameSync4,
6185
+ truncate: (path) => truncateSync(path, 0),
6186
+ unlink: unlinkSync2,
6187
+ exists: existsSync4
6188
+ };
6189
+ var realOwnerIo = {
6190
+ statFd: (fd) => {
6191
+ try {
6192
+ const s = fstatSync(fd);
6193
+ return { dev: s.dev, ino: s.ino, isFile: s.isFile() };
6194
+ } catch {
6195
+ return null;
6196
+ }
6197
+ },
6198
+ statPath: (path) => {
6199
+ try {
6200
+ const s = statSync2(path);
6201
+ return { dev: s.dev, ino: s.ino };
6202
+ } catch {
6203
+ return null;
6204
+ }
6205
+ }
6206
+ };
6207
+ function stdoutGoesTo(path, io = realOwnerIo) {
6208
+ const out = io.statFd(1);
6209
+ if (!out || !out.isFile) return false;
6210
+ const file = io.statPath(path);
6211
+ return file != null && file.dev === out.dev && file.ino === out.ino;
6212
+ }
6213
+ var STRAY_GENERATIONS = 8;
6214
+ function rotateDaemonLog(path, opts, io = realIo) {
6215
+ const size = io.size(path);
6216
+ if (size == null) return "missing";
6217
+ if (size <= opts.maxBytes) return "kept";
6218
+ const keep = Math.max(0, Math.trunc(opts.keep));
6219
+ const gen = (n) => `${path}.${n}`;
6220
+ for (let i = keep; i >= 1; i--) {
6221
+ if (!io.exists(gen(i))) continue;
6222
+ if (i === keep) io.unlink(gen(i));
6223
+ else io.rename(gen(i), gen(i + 1));
6224
+ }
6225
+ for (let i = keep + 1; i <= keep + STRAY_GENERATIONS; i++) {
6226
+ if (io.exists(gen(i))) io.unlink(gen(i));
6227
+ }
6228
+ if (keep >= 1) io.copy(path, gen(1));
6229
+ io.truncate(path);
6230
+ return "rotated";
6231
+ }
6232
+
6056
6233
  // src/repoRegistry.ts
6057
6234
  init_config();
6058
6235
  import { $ } from "execa";
@@ -6122,8 +6299,15 @@ function pickRepoPaths(args) {
6122
6299
  return { paths: [], source: "none" };
6123
6300
  }
6124
6301
 
6302
+ // src/repoDiscovery.ts
6303
+ init_src();
6304
+ init_config();
6305
+ import { homedir as homedir2 } from "os";
6306
+ import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "fs";
6307
+ import { isAbsolute, join as join6, resolve as resolve2 } from "path";
6308
+
6125
6309
  // src/repoScan.ts
6126
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
6310
+ import { readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
6127
6311
  import { join as join5 } from "path";
6128
6312
  var SKIP = /* @__PURE__ */ new Set([
6129
6313
  "node_modules",
@@ -6159,7 +6343,11 @@ function originOf(dir) {
6159
6343
  return null;
6160
6344
  }
6161
6345
  }
6162
- function scanRoots(roots, opts = {}) {
6346
+ function repoFullNameAt(dir) {
6347
+ const origin = originOf(dir);
6348
+ return origin ? parseRepoFullName(origin) : null;
6349
+ }
6350
+ function scanRootsAll(roots, opts = {}) {
6163
6351
  const maxDepth = opts.maxDepth ?? 3;
6164
6352
  const found = /* @__PURE__ */ new Map();
6165
6353
  for (const root of roots) walk(root, 0, maxDepth, found);
@@ -6170,7 +6358,11 @@ function walk(dir, depth, maxDepth, found) {
6170
6358
  const origin = originOf(dir);
6171
6359
  if (origin) {
6172
6360
  const full = parseRepoFullName(origin);
6173
- if (full && !found.has(full)) found.set(full, dir);
6361
+ if (full) {
6362
+ const paths = found.get(full) ?? [];
6363
+ if (!paths.includes(dir)) paths.push(dir);
6364
+ found.set(full, paths);
6365
+ }
6174
6366
  return;
6175
6367
  }
6176
6368
  let entries;
@@ -6183,7 +6375,7 @@ function walk(dir, depth, maxDepth, found) {
6183
6375
  if (name.startsWith(".") || SKIP.has(name)) continue;
6184
6376
  const child = join5(dir, name);
6185
6377
  try {
6186
- if (!statSync2(child).isDirectory()) continue;
6378
+ if (!statSync3(child).isDirectory()) continue;
6187
6379
  } catch {
6188
6380
  continue;
6189
6381
  }
@@ -6208,20 +6400,173 @@ function defaultScanRoots(home, knownPaths = []) {
6208
6400
  return out;
6209
6401
  }
6210
6402
 
6403
+ // src/repoDiscovery.ts
6404
+ function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
6405
+ const v = (raw ?? "").trim();
6406
+ if (!v) return defaultScanRoots(home, knownPaths);
6407
+ if (["off", "none", "0"].includes(v.toLowerCase())) return [];
6408
+ const out = [];
6409
+ for (const part of v.split(",")) {
6410
+ const p = part.trim();
6411
+ if (!p) continue;
6412
+ const abs = p === "~" ? home : p.startsWith("~/") ? join6(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
6413
+ if (!out.includes(abs)) out.push(abs);
6414
+ }
6415
+ return out;
6416
+ }
6417
+ function normalizeFullNames(list) {
6418
+ const out = [];
6419
+ for (const raw of list) {
6420
+ const v = (raw ?? "").trim().toLowerCase();
6421
+ if (v && !out.includes(v)) out.push(v);
6422
+ }
6423
+ return out;
6424
+ }
6425
+ async function fetchSwarmRepos(call) {
6426
+ let res;
6427
+ try {
6428
+ res = await call("swarm_my_repos", {});
6429
+ } catch {
6430
+ return null;
6431
+ }
6432
+ if (!res || res.isError) return null;
6433
+ const raw = res.structuredContent?.repos;
6434
+ if (!Array.isArray(raw)) return null;
6435
+ const out = [];
6436
+ for (const item of raw) {
6437
+ if (!item || typeof item !== "object") continue;
6438
+ const r = item;
6439
+ const repoFullName = typeof r.repoFullName === "string" ? r.repoFullName.trim().toLowerCase() : "";
6440
+ if (!repoFullName) continue;
6441
+ out.push({
6442
+ repoFullName,
6443
+ taskPrefix: typeof r.taskPrefix === "string" ? r.taskPrefix : null,
6444
+ workspaceSlug: typeof r.workspaceSlug === "string" ? r.workspaceSlug : ""
6445
+ });
6446
+ }
6447
+ return out;
6448
+ }
6449
+ function emptyRepoMapState(roots) {
6450
+ return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
6451
+ }
6452
+ async function refreshRepoMap(opts) {
6453
+ const log = opts.log ?? (() => {
6454
+ });
6455
+ const scan = opts.scan ?? scanRootsAll;
6456
+ const repoAt = opts.repoAt ?? repoFullNameAt;
6457
+ const wanted = await fetchSwarmRepos(opts.call);
6458
+ const clones = /* @__PURE__ */ new Map();
6459
+ const take = (repoFullName, path, source, alternatives = []) => {
6460
+ if (clones.has(repoFullName)) return;
6461
+ clones.set(repoFullName, { repoFullName, path, source, alternatives });
6462
+ };
6463
+ for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
6464
+ for (const e of readRepoMapCache(opts.cachePath)) {
6465
+ const name = e.repoFullName.toLowerCase();
6466
+ if (clones.has(name)) continue;
6467
+ if (repoAt(e.path) === name) take(name, e.path, "cache");
6468
+ }
6469
+ const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
6470
+ const needScan = wanted ? wantedNames.some((n) => !clones.has(n)) : clones.size === 0;
6471
+ if (needScan) {
6472
+ const startedAt = Date.now();
6473
+ const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
6474
+ let added = 0;
6475
+ for (const [full, paths] of scanned) {
6476
+ if (clones.has(full) || !paths.length) continue;
6477
+ take(full, paths[0], "scan", paths.slice(1));
6478
+ added++;
6479
+ }
6480
+ if (added > 0) {
6481
+ log(`[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${added} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441`);
6482
+ }
6483
+ }
6484
+ const registry = [...clones.values()].map((c) => ({
6485
+ repoFullName: c.repoFullName,
6486
+ path: normalizeRepo(c.path)
6487
+ }));
6488
+ writeRepoMapCache(
6489
+ opts.cachePath,
6490
+ [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
6491
+ );
6492
+ if (!wanted) {
6493
+ log(
6494
+ "[overmind] \u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F (swarm_my_repos) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435. \u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u044D\u0442\u043E \u043E\u0436\u0438\u0434\u0430\u0435\u043C\u043E \u0438 \u043B\u0435\u0447\u0438\u0442\u0441\u044F \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u043F\u0440\u043E\u0434\u0430."
6495
+ );
6496
+ return registry;
6497
+ }
6498
+ opts.state.unknown = false;
6499
+ opts.state.clones = wantedNames.map((n) => clones.get(n)).filter((c) => !!c);
6500
+ opts.state.missing = wantedNames.filter((n) => !clones.has(n));
6501
+ opts.state.roots = [...opts.roots];
6502
+ opts.state.at = stamp();
6503
+ if (wantedNames.length === 0) {
6504
+ log("[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435");
6505
+ return registry;
6506
+ }
6507
+ log(`[overmind] \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${wantedNames.length}), \u043A\u0430\u0440\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435:`);
6508
+ for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
6509
+ const prefixOf = new Map(wanted.map((r) => [r.repoFullName, r.taskPrefix]));
6510
+ for (const name of opts.state.missing) {
6511
+ const prefix = prefixOf.get(name);
6512
+ log(`[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`);
6513
+ }
6514
+ return registry;
6515
+ }
6516
+ function describeClone(c) {
6517
+ const how = c.source === "configured" ? "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C" : c.source === "cache" ? "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B" : "\u043D\u0430\u0439\u0434\u0435\u043D \u0441\u043A\u0430\u043D\u043E\u043C";
6518
+ const alt = c.alternatives.length ? ` \u26A0 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0435\u0449\u0451 ${c.alternatives.length}: ${c.alternatives.join(", ")} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0432 \u043F\u0435\u0440\u0432\u043E\u043C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443 \u043A\u043E\u0440\u043D\u0435\u0439` : "";
6519
+ return `${c.repoFullName} \u2192 ${c.path} (${how})${alt}`;
6520
+ }
6521
+ function describeMissing(repoFullName, roots) {
6522
+ const where = roots.length ? `\u0438\u0441\u043A\u0430\u043B \u0432: ${roots.slice(0, 6).join(", ")}` : "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)";
6523
+ return `${repoFullName} \u2014 \u041A\u041B\u041E\u041D\u0410 \u041D\u0410 \u042D\u0422\u041E\u0419 \u041C\u0410\u0428\u0418\u041D\u0415 \u041D\u0415\u0422 (${where}). \u0421\u0430\u043C \u043D\u0435 \u043A\u043B\u043E\u043D\u0438\u0440\u0443\u044E: \u0441\u043A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0440\u0443\u043A\u0430\u043C\u0438 \u043B\u0438\u0431\u043E \u0443\u043A\u0430\u0436\u0438 \u043F\u0443\u0442\u044C \`swarm start --repo <\u043F\u0443\u0442\u044C>\`.`;
6524
+ }
6525
+ function repoMapRows(state) {
6526
+ return [
6527
+ ...state.clones.map((c) => ({
6528
+ repoFullName: c.repoFullName,
6529
+ path: c.path,
6530
+ source: c.source,
6531
+ alternatives: c.alternatives
6532
+ })),
6533
+ ...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
6534
+ ];
6535
+ }
6536
+ function readRepoMapCache(path) {
6537
+ try {
6538
+ const raw = JSON.parse(readFileSync8(path, "utf8"));
6539
+ if (!Array.isArray(raw?.entries)) return [];
6540
+ return raw.entries.filter(
6541
+ (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
6542
+ );
6543
+ } catch {
6544
+ return [];
6545
+ }
6546
+ }
6547
+ function writeRepoMapCache(path, entries) {
6548
+ try {
6549
+ const tmp = `${path}.tmp`;
6550
+ writeFileSync6(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
6551
+ renameSync5(tmp, path);
6552
+ } catch {
6553
+ }
6554
+ }
6555
+
6211
6556
  // src/hostIdentity.ts
6212
6557
  import { hostname } from "os";
6213
- import { existsSync as existsSync4, readFileSync as readFileSync8, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
6214
- import { join as join6 } from "path";
6558
+ import { existsSync as existsSync5, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "fs";
6559
+ import { join as join7 } from "path";
6215
6560
  import { randomUUID as randomUUID2 } from "crypto";
6216
6561
  function hostIdentityPath(home) {
6217
- return join6(home, "host.json");
6562
+ return join7(home, "host.json");
6218
6563
  }
6219
6564
  function loadHostIdentity(deps) {
6220
- const exists = deps.exists ?? existsSync4;
6221
- const readFile2 = deps.readFile ?? ((p) => readFileSync8(p, "utf8"));
6565
+ const exists = deps.exists ?? existsSync5;
6566
+ const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
6222
6567
  const writeFile2 = deps.writeFile ?? ((p, data) => {
6223
- writeFileSync6(`${p}.tmp`, data);
6224
- renameSync4(`${p}.tmp`, p);
6568
+ writeFileSync7(`${p}.tmp`, data);
6569
+ renameSync6(`${p}.tmp`, p);
6225
6570
  });
6226
6571
  const host = deps.hostname ?? hostname;
6227
6572
  const newId = deps.newId ?? randomUUID2;
@@ -6403,14 +6748,14 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
6403
6748
 
6404
6749
  // src/launcher.ts
6405
6750
  import { spawn } from "child_process";
6406
- import { closeSync as closeSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
6751
+ import { closeSync as closeSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
6407
6752
  import { createRequire } from "module";
6408
- import { homedir as homedir2 } from "os";
6409
- import { dirname as dirname2, join as join8 } from "path";
6753
+ import { homedir as homedir3 } from "os";
6754
+ import { dirname as dirname2, join as join9 } from "path";
6410
6755
  import { fileURLToPath } from "url";
6411
6756
 
6412
6757
  // src/claudeBin.ts
6413
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
6758
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
6414
6759
  import { win32 as winPath } from "path";
6415
6760
  var CLAUDE_FALLBACK = "claude";
6416
6761
  function compareVersionDirs(a, b) {
@@ -6447,7 +6792,7 @@ function winVersionRoots(env, listDir) {
6447
6792
  function findWindowsClaude(opts = {}) {
6448
6793
  const env = opts.env ?? process.env;
6449
6794
  const listDir = opts.listDir ?? realListDir;
6450
- const fileExists = opts.fileExists ?? existsSync5;
6795
+ const fileExists = opts.fileExists ?? existsSync6;
6451
6796
  let best = null;
6452
6797
  for (const root of winVersionRoots(env, listDir)) {
6453
6798
  for (const version of listDir(root)) {
@@ -6482,8 +6827,8 @@ function resolveClaudeBin(opts = {}) {
6482
6827
  }
6483
6828
 
6484
6829
  // src/droneHome.ts
6485
- import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync4, rmSync, statSync as statSync3, symlinkSync } from "fs";
6486
- import { join as join7, relative, sep } from "path";
6830
+ import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync4, rmSync, statSync as statSync4, symlinkSync } from "fs";
6831
+ import { join as join8, relative, sep } from "path";
6487
6832
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
6488
6833
  function droneHomeDeny(env = process.env) {
6489
6834
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -6507,23 +6852,23 @@ function buildDroneHome(opts) {
6507
6852
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
6508
6853
  for (const existing of readdirSync4(droneHome)) {
6509
6854
  if (wanted.has(existing)) continue;
6510
- const p = join7(droneHome, existing);
6855
+ const p = join8(droneHome, existing);
6511
6856
  try {
6512
6857
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
6513
6858
  } catch {
6514
6859
  }
6515
6860
  }
6516
6861
  for (const name of wanted) {
6517
- const link = join7(droneHome, name);
6862
+ const link = join8(droneHome, name);
6518
6863
  try {
6519
6864
  lstatSync(link);
6520
6865
  continue;
6521
6866
  } catch {
6522
6867
  }
6523
- const target = join7(realHome, name);
6868
+ const target = join8(realHome, name);
6524
6869
  let isDir;
6525
6870
  try {
6526
- isDir = statSync3(target).isDirectory();
6871
+ isDir = statSync4(target).isDirectory();
6527
6872
  } catch {
6528
6873
  continue;
6529
6874
  }
@@ -6541,7 +6886,7 @@ var REAL_PATH_ENV_PINS = [
6541
6886
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
6542
6887
  ];
6543
6888
  function realPathEnvPins(realHome) {
6544
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join7(realHome, ...segments)]));
6889
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join8(realHome, ...segments)]));
6545
6890
  }
6546
6891
  function droneDenyRules(realHome, swarmHome) {
6547
6892
  const paths = [
@@ -6633,7 +6978,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
6633
6978
  const env = { ...base3 };
6634
6979
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
6635
6980
  if (isolate && !base3.TURBO_CACHE_DIR) {
6636
- env.TURBO_CACHE_DIR = join8(worktree, ".turbo", "cache");
6981
+ env.TURBO_CACHE_DIR = join9(worktree, ".turbo", "cache");
6637
6982
  }
6638
6983
  Object.assign(env, droneTestEnv(droneCap, base3));
6639
6984
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -6661,25 +7006,25 @@ var DAEMON_STATE_ENV = [
6661
7006
  "TASKLESS_TOKEN"
6662
7007
  ];
6663
7008
  function bundledRoleDir(kind) {
6664
- return join8(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
7009
+ return join9(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
6665
7010
  }
6666
7011
  function roleDir(worktree, kind) {
6667
- return join8(worktree, ".claude", "roles", kind);
7012
+ return join9(worktree, ".claude", "roles", kind);
6668
7013
  }
6669
7014
  function roleDirs(worktree, kind) {
6670
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync6(dir));
7015
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
6671
7016
  }
6672
7017
  function transcriptDir(worktree, projectsDir) {
6673
- return join8(projectsDir, worktree.replace(/[/.]/g, "-"));
7018
+ return join9(projectsDir, worktree.replace(/[/.]/g, "-"));
6674
7019
  }
6675
- function sessionTranscriptExists(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
7020
+ function sessionTranscriptExists(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
6676
7021
  try {
6677
7022
  return readdirSync5(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
6678
7023
  } catch {
6679
7024
  return false;
6680
7025
  }
6681
7026
  }
6682
- function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
7027
+ function transcriptMtimeMs(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
6683
7028
  const dir = transcriptDir(worktree, projectsDir);
6684
7029
  let files;
6685
7030
  try {
@@ -6691,7 +7036,7 @@ function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude",
6691
7036
  for (const f of files) {
6692
7037
  if (!f.endsWith(".jsonl")) continue;
6693
7038
  try {
6694
- const mtime2 = statSync4(join8(dir, f)).mtimeMs;
7039
+ const mtime2 = statSync5(join9(dir, f)).mtimeMs;
6695
7040
  if (latest === null || mtime2 > latest) latest = mtime2;
6696
7041
  } catch {
6697
7042
  }
@@ -6699,7 +7044,7 @@ function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude",
6699
7044
  return latest;
6700
7045
  }
6701
7046
  function nextRunLogPath(runLogsDir, runId) {
6702
- const dir = join8(runLogsDir, String(runId));
7047
+ const dir = join9(runLogsDir, String(runId));
6703
7048
  mkdirSync4(dir, { recursive: true });
6704
7049
  let files = [];
6705
7050
  try {
@@ -6711,7 +7056,7 @@ function nextRunLogPath(runLogsDir, runId) {
6711
7056
  const m = /^(\d+)\.json/.exec(f);
6712
7057
  if (m) next = Math.max(next, Number(m[1]) + 1);
6713
7058
  }
6714
- return join8(dir, `${next}.json`);
7059
+ return join9(dir, `${next}.json`);
6715
7060
  }
6716
7061
  function openNextRunLog(runLogsDir, runId) {
6717
7062
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -6785,16 +7130,16 @@ function makeLauncher(cfg) {
6785
7130
  }
6786
7131
  }
6787
7132
  };
6788
- const cfgPath = join8(worktree, ".swarm-mcp.json");
6789
- writeFileSync7(cfgPath, JSON.stringify(mcpConfig, null, 2));
7133
+ const cfgPath = join9(worktree, ".swarm-mcp.json");
7134
+ writeFileSync8(cfgPath, JSON.stringify(mcpConfig, null, 2));
6790
7135
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
6791
7136
  const pluginDirs = roleDirs(worktree, run.kind);
6792
- const realHome = cfg.droneIsolation?.realHome ?? homedir2();
7137
+ const realHome = cfg.droneIsolation?.realHome ?? homedir3();
6793
7138
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
6794
7139
  const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
6795
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync6(join8(cfg.droneIsolation.swarmHome, "token"))) {
7140
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join9(cfg.droneIsolation.swarmHome, "token"))) {
6796
7141
  console.error(
6797
- `[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join8(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
7142
+ `[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join9(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
6798
7143
  );
6799
7144
  }
6800
7145
  let stdio = "ignore";
@@ -6828,8 +7173,8 @@ function makeLauncher(cfg) {
6828
7173
  init_procCleanup();
6829
7174
 
6830
7175
  // src/cost.ts
6831
- import { readFileSync as readFileSync9, readdirSync as readdirSync6, renameSync as renameSync5 } from "fs";
6832
- import { join as join9 } from "path";
7176
+ import { readFileSync as readFileSync10, readdirSync as readdirSync6, renameSync as renameSync7 } from "fs";
7177
+ import { join as join10 } from "path";
6833
7178
 
6834
7179
  // src/pricing.ts
6835
7180
  var MODEL_PRICING = {
@@ -6886,17 +7231,17 @@ function unprocessedLogFiles(dir) {
6886
7231
  }
6887
7232
  }
6888
7233
  function accountRunCost(deps, run) {
6889
- const dir = join9(deps.runLogsDir, String(run.id));
7234
+ const dir = join10(deps.runLogsDir, String(run.id));
6890
7235
  const files = unprocessedLogFiles(dir);
6891
7236
  if (files.length === 0) return;
6892
7237
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
6893
7238
  let added = 0;
6894
7239
  const processed = [];
6895
7240
  for (const f of files) {
6896
- const full = join9(dir, f);
7241
+ const full = join10(dir, f);
6897
7242
  let raw = null;
6898
7243
  try {
6899
- raw = readFileSync9(full, "utf8");
7244
+ raw = readFileSync10(full, "utf8");
6900
7245
  } catch {
6901
7246
  continue;
6902
7247
  }
@@ -6913,7 +7258,7 @@ function accountRunCost(deps, run) {
6913
7258
  if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
6914
7259
  for (const full of processed) {
6915
7260
  try {
6916
- renameSync5(full, `${full}.done`);
7261
+ renameSync7(full, `${full}.done`);
6917
7262
  } catch {
6918
7263
  }
6919
7264
  }
@@ -6923,11 +7268,11 @@ function makeCostAccountant(deps) {
6923
7268
  }
6924
7269
 
6925
7270
  // src/runOutcome.ts
6926
- import { readdirSync as readdirSync7, readFileSync as readFileSync10 } from "fs";
6927
- import { join as join10 } from "path";
7271
+ import { readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
7272
+ import { join as join11 } from "path";
6928
7273
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
6929
7274
  function classifyExit(runLogsDir, runId) {
6930
- const dir = join10(runLogsDir, String(runId));
7275
+ const dir = join11(runLogsDir, String(runId));
6931
7276
  let files;
6932
7277
  try {
6933
7278
  files = readdirSync7(dir);
@@ -6944,7 +7289,7 @@ function classifyExit(runLogsDir, runId) {
6944
7289
  if (!latest) return "unknown";
6945
7290
  let raw;
6946
7291
  try {
6947
- raw = readFileSync10(join10(dir, latest.name), "utf8");
7292
+ raw = readFileSync11(join11(dir, latest.name), "utf8");
6948
7293
  } catch {
6949
7294
  return "unknown";
6950
7295
  }
@@ -6955,11 +7300,12 @@ function makeExitClassifier(runLogsDir) {
6955
7300
  }
6956
7301
 
6957
7302
  // src/orchestrator.ts
7303
+ init_src();
6958
7304
  init_git();
6959
7305
 
6960
7306
  // src/deps.ts
6961
- import { existsSync as existsSync8, readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
6962
- import { join as join12 } from "path";
7307
+ import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
7308
+ import { join as join13 } from "path";
6963
7309
  import { execa } from "execa";
6964
7310
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
6965
7311
  var MAX_PLAN_COMMANDS = 12;
@@ -6975,7 +7321,7 @@ async function runCommand(cmd, worktree) {
6975
7321
  }
6976
7322
  function safeReadText(p) {
6977
7323
  try {
6978
- return readFileSync11(p, "utf8");
7324
+ return readFileSync12(p, "utf8");
6979
7325
  } catch {
6980
7326
  return null;
6981
7327
  }
@@ -7015,21 +7361,21 @@ function overridePlan(override) {
7015
7361
  };
7016
7362
  }
7017
7363
  function detectPackageManager(worktree, io = {}) {
7018
- const exists = io.exists ?? existsSync8;
7364
+ const exists = io.exists ?? existsSync9;
7019
7365
  const readText = io.readText ?? safeReadText;
7020
- const hasPkgJson = exists(join12(worktree, "package.json"));
7366
+ const hasPkgJson = exists(join13(worktree, "package.json"));
7021
7367
  if (!hasPkgJson) {
7022
- if (exists(join12(worktree, "deno.json")) || exists(join12(worktree, "deno.jsonc")) || exists(join12(worktree, "deno.lock"))) return "deno";
7368
+ if (exists(join13(worktree, "deno.json")) || exists(join13(worktree, "deno.jsonc")) || exists(join13(worktree, "deno.lock"))) return "deno";
7023
7369
  return null;
7024
7370
  }
7025
- const pkg = readJson(readText, join12(worktree, "package.json"));
7371
+ const pkg = readJson(readText, join13(worktree, "package.json"));
7026
7372
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
7027
7373
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
7028
- if (exists(join12(worktree, "bun.lock")) || exists(join12(worktree, "bun.lockb"))) return "bun";
7029
- if (exists(join12(worktree, "pnpm-lock.yaml"))) return "pnpm";
7030
- if (exists(join12(worktree, "yarn.lock"))) return "yarn";
7031
- if (exists(join12(worktree, "package-lock.json"))) return "npm";
7032
- if (exists(join12(worktree, "deno.lock"))) return "deno";
7374
+ if (exists(join13(worktree, "bun.lock")) || exists(join13(worktree, "bun.lockb"))) return "bun";
7375
+ if (exists(join13(worktree, "pnpm-lock.yaml"))) return "pnpm";
7376
+ if (exists(join13(worktree, "yarn.lock"))) return "yarn";
7377
+ if (exists(join13(worktree, "package-lock.json"))) return "npm";
7378
+ if (exists(join13(worktree, "deno.lock"))) return "deno";
7033
7379
  return "npm";
7034
7380
  }
7035
7381
  function jsInstallCommand(pm) {
@@ -7082,7 +7428,7 @@ var ECOSYSTEM_MARKERS = [
7082
7428
  // конфигурация — дело сборки, не подготовки
7083
7429
  ];
7084
7430
  function detectPlan(worktree, io) {
7085
- const exists = io.exists ?? existsSync8;
7431
+ const exists = io.exists ?? existsSync9;
7086
7432
  const listFiles = io.listFiles ?? safeListFiles;
7087
7433
  const pm = detectPackageManager(worktree, io);
7088
7434
  if (pm) {
@@ -7090,7 +7436,7 @@ function detectPlan(worktree, io) {
7090
7436
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
7091
7437
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
7092
7438
  // заново, но он дешёвый: тянет из того же общего кэша.
7093
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join12(worktree, "node_modules"), source: "detect" }
7439
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join13(worktree, "node_modules"), source: "detect" }
7094
7440
  ];
7095
7441
  for (const target of workspaceBuildTargets(worktree, io)) {
7096
7442
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -7098,12 +7444,12 @@ function detectPlan(worktree, io) {
7098
7444
  return { source: "detect", ecosystem: `node/${pm}`, commands };
7099
7445
  }
7100
7446
  for (const marker of ECOSYSTEM_MARKERS) {
7101
- const hit = marker.files.some((f) => exists(join12(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
7447
+ const hit = marker.files.some((f) => exists(join13(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
7102
7448
  if (!hit) continue;
7103
7449
  let ecosystem = marker.ecosystem;
7104
7450
  let command = marker.command;
7105
7451
  for (const refine of marker.refine ?? []) {
7106
- if (!exists(join12(worktree, refine.file))) continue;
7452
+ if (!exists(join13(worktree, refine.file))) continue;
7107
7453
  ecosystem = refine.ecosystem;
7108
7454
  command = refine.command;
7109
7455
  break;
@@ -7160,12 +7506,12 @@ var CI_INSTALL_PATTERNS = [
7160
7506
  function ciWorkflowPlan(worktree, io) {
7161
7507
  const readText = io.readText ?? safeReadText;
7162
7508
  const listFiles = io.listFiles ?? safeListFiles;
7163
- const dir = join12(worktree, ".github", "workflows");
7509
+ const dir = join13(worktree, ".github", "workflows");
7164
7510
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
7165
7511
  const seen = /* @__PURE__ */ new Set();
7166
7512
  const commands = [];
7167
7513
  for (const name of files.sort()) {
7168
- const yaml = readText(join12(dir, name));
7514
+ const yaml = readText(join13(dir, name));
7169
7515
  if (yaml === null) continue;
7170
7516
  for (const raw of workflowRunLines(yaml)) {
7171
7517
  const line = raw.replace(/\s+/g, " ").trim();
@@ -7180,7 +7526,7 @@ function ciWorkflowPlan(worktree, io) {
7180
7526
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
7181
7527
  }
7182
7528
  function workspaceGlobs(worktree, readText) {
7183
- const yaml = readText(join12(worktree, "pnpm-workspace.yaml"));
7529
+ const yaml = readText(join13(worktree, "pnpm-workspace.yaml"));
7184
7530
  if (yaml !== null) {
7185
7531
  const globs = [];
7186
7532
  let inPackages = false;
@@ -7200,7 +7546,7 @@ function workspaceGlobs(worktree, readText) {
7200
7546
  }
7201
7547
  if (globs.length) return globs;
7202
7548
  }
7203
- const pkg = readJson(readText, join12(worktree, "package.json"));
7549
+ const pkg = readJson(readText, join13(worktree, "package.json"));
7204
7550
  const ws = pkg?.workspaces;
7205
7551
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
7206
7552
  return list.filter((g) => typeof g === "string");
@@ -7211,14 +7557,14 @@ function workspaceDirs(worktree, globs, io) {
7211
7557
  if (glob.startsWith("!")) continue;
7212
7558
  const star = glob.indexOf("*");
7213
7559
  if (star === -1) {
7214
- const dir = join12(worktree, glob);
7215
- if (io.exists(join12(dir, "package.json"))) dirs.add(dir);
7560
+ const dir = join13(worktree, glob);
7561
+ if (io.exists(join13(dir, "package.json"))) dirs.add(dir);
7216
7562
  continue;
7217
7563
  }
7218
- const base3 = join12(worktree, glob.slice(0, star).replace(/\/$/, ""));
7564
+ const base3 = join13(worktree, glob.slice(0, star).replace(/\/$/, ""));
7219
7565
  for (const name of io.listDirs(base3)) {
7220
- const dir = join12(base3, name);
7221
- if (io.exists(join12(dir, "package.json"))) dirs.add(dir);
7566
+ const dir = join13(base3, name);
7567
+ if (io.exists(join13(dir, "package.json"))) dirs.add(dir);
7222
7568
  }
7223
7569
  }
7224
7570
  return [...dirs];
@@ -7239,18 +7585,18 @@ function entryPaths(pkg) {
7239
7585
  return out;
7240
7586
  }
7241
7587
  function workspaceBuildTargets(worktree, io = {}) {
7242
- const exists = io.exists ?? existsSync8;
7588
+ const exists = io.exists ?? existsSync9;
7243
7589
  const readText = io.readText ?? safeReadText;
7244
7590
  const listDirs = io.listDirs ?? safeListDirs;
7245
7591
  const globs = workspaceGlobs(worktree, readText);
7246
7592
  if (!globs.length) return [];
7247
7593
  const pkgs = [];
7248
7594
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
7249
- const pkg = readJson(readText, join12(dir, "package.json"));
7595
+ const pkg = readJson(readText, join13(dir, "package.json"));
7250
7596
  if (!pkg || typeof pkg.name !== "string") continue;
7251
7597
  const scripts = pkg.scripts ?? {};
7252
7598
  const entries = entryPaths(pkg);
7253
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join12(dir, e)));
7599
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join13(dir, e)));
7254
7600
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
7255
7601
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
7256
7602
  }
@@ -7278,7 +7624,7 @@ function planPrepare(worktree, io = {}) {
7278
7624
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
7279
7625
  }
7280
7626
  async function ensureWorktreeDeps(worktree, io = {}) {
7281
- const exists = io.exists ?? existsSync8;
7627
+ const exists = io.exists ?? existsSync9;
7282
7628
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
7283
7629
  const approve = io.approve ?? (() => true);
7284
7630
  const plan = planPrepare(worktree, io);
@@ -7377,6 +7723,12 @@ var AreaBusyError = class extends Error {
7377
7723
  }
7378
7724
  area;
7379
7725
  };
7726
+ function healTaskSlug(db, run, task) {
7727
+ const slug = task?.slug;
7728
+ if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
7729
+ db.addEvent(run.id, "step", `\u0441\u043B\u0430\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${run.task_slug} \u2192 ${slug} (\u043F\u0440\u043E\u0434 \u043E\u0442\u0434\u0430\u043B \u043F\u0440\u043E\u0433\u043E\u043D \u0431\u0435\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-329)`);
7730
+ return db.updateRun(run.id, { task_slug: slug }) ?? run;
7731
+ }
7380
7732
  async function spawnDrone(deps, run) {
7381
7733
  const repo = normalizeRepo(run.repo || deps.repo || "");
7382
7734
  if (!repo) throw new Error(`\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u043D\u0435\u0442 repo \u0438 \u043D\u0435\u0442 \u0434\u0435\u0444\u043E\u043B\u0442\u0430 SWARM_REPO`);
@@ -7564,8 +7916,8 @@ function fullCommitHeadline(headline, body) {
7564
7916
  const head = typeof headline === "string" ? headline : "";
7565
7917
  if (!head) return null;
7566
7918
  if (!head.endsWith("\u2026") || typeof body !== "string" || !body.startsWith("\u2026")) return head;
7567
- const tail = body.slice(1).split("\n", 1)[0] ?? "";
7568
- return head.slice(0, -1) + tail;
7919
+ const tail2 = body.slice(1).split("\n", 1)[0] ?? "";
7920
+ return head.slice(0, -1) + tail2;
7569
7921
  }
7570
7922
  var NO_CHECKS_STDERR = /no checks reported/i;
7571
7923
  var realChecksRunner = async (pr) => {
@@ -8823,10 +9175,31 @@ function makeExclusiveReader(opts) {
8823
9175
  }
8824
9176
 
8825
9177
  // src/envGate.ts
8826
- import { existsSync as existsSync10 } from "fs";
8827
- import { join as join18, delimiter } from "path";
9178
+ import { existsSync as existsSync12 } from "fs";
9179
+ import { join as join20 } from "path";
8828
9180
  import { createConnection } from "net";
8829
9181
  import { execa as execa2 } from "execa";
9182
+
9183
+ // src/platform.ts
9184
+ import { existsSync as existsSync11, readFileSync as readFileSync17 } from "fs";
9185
+ import { delimiter, join as join19 } from "path";
9186
+ function resolveCommandPath(name, env = process.env, platform = process.platform) {
9187
+ if (name.includes("/") || name.includes("\\")) return existsSync11(name) ? name : null;
9188
+ const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
9189
+ const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
9190
+ for (const dir of dirs) {
9191
+ for (const ext of exts) {
9192
+ const candidate = join19(dir, name + ext);
9193
+ if (existsSync11(candidate)) return candidate;
9194
+ }
9195
+ }
9196
+ return null;
9197
+ }
9198
+ function hasCommandInPath(name, env = process.env, platform = process.platform) {
9199
+ return resolveCommandPath(name, env, platform) !== null;
9200
+ }
9201
+
9202
+ // src/envGate.ts
8830
9203
  var PROBE_TIMEOUT_MS = 2500;
8831
9204
  var PORT_TIMEOUT_MS = 1e3;
8832
9205
  function parseRequirements(lines) {
@@ -8875,8 +9248,8 @@ var COMPOSE_MARKERS = [
8875
9248
  "compose.yaml"
8876
9249
  ];
8877
9250
  function detectRequirements(repo, io = {}) {
8878
- const exists = io.exists ?? existsSync10;
8879
- const marker = COMPOSE_MARKERS.find((f) => exists(join18(repo, f)));
9251
+ const exists = io.exists ?? existsSync12;
9252
+ const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
8880
9253
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
8881
9254
  }
8882
9255
  function resolveRequirements(repo, declared, io = {}) {
@@ -8895,17 +9268,14 @@ async function dockerAlive() {
8895
9268
  }
8896
9269
  }
8897
9270
  async function hasCommand(name, env) {
8898
- if (name.includes("/")) return existsSync10(name);
8899
- const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
8900
- const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
8901
- return dirs.some((dir) => exts.some((ext) => existsSync10(join18(dir, name + ext))));
9271
+ return hasCommandInPath(name, env);
8902
9272
  }
8903
9273
  async function portOpen(host, port) {
8904
- return new Promise((resolve3) => {
9274
+ return new Promise((resolve4) => {
8905
9275
  const socket = createConnection({ host, port });
8906
9276
  const finish = (ok) => {
8907
9277
  socket.destroy();
8908
- resolve3(ok);
9278
+ resolve4(ok);
8909
9279
  };
8910
9280
  socket.setTimeout(PORT_TIMEOUT_MS);
8911
9281
  socket.once("connect", () => finish(true));
@@ -9015,8 +9385,8 @@ import { basename as basename3 } from "path";
9015
9385
  import { serve as serve2 } from "@hono/node-server";
9016
9386
 
9017
9387
  // src/diff.ts
9018
- import { statSync as statSync6 } from "fs";
9019
- import { join as join19 } from "path";
9388
+ import { statSync as statSync7 } from "fs";
9389
+ import { join as join21 } from "path";
9020
9390
  import { $ as $4 } from "execa";
9021
9391
  var MAX_FILES = 200;
9022
9392
  var MAX_LINES_PER_FILE = 1500;
@@ -9159,7 +9529,7 @@ async function resolveBase(wt, baseBranch) {
9159
9529
  }
9160
9530
  function mtime(path) {
9161
9531
  try {
9162
- return statSync6(path).mtimeMs;
9532
+ return statSync7(path).mtimeMs;
9163
9533
  } catch {
9164
9534
  return 0;
9165
9535
  }
@@ -9184,7 +9554,7 @@ async function collectRunDiff(worktree, opts = {}) {
9184
9554
  files.push(pf);
9185
9555
  }
9186
9556
  }
9187
- files.sort((a, b) => mtime(join19(worktree, b.path)) - mtime(join19(worktree, a.path)));
9557
+ files.sort((a, b) => mtime(join21(worktree, b.path)) - mtime(join21(worktree, a.path)));
9188
9558
  const truncatedFiles = files.length > MAX_FILES;
9189
9559
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9190
9560
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -9203,7 +9573,7 @@ async function collectRunDiff(worktree, opts = {}) {
9203
9573
  // package.json
9204
9574
  var package_default = {
9205
9575
  name: "@taskless-app/swarm",
9206
- version: "0.4.9",
9576
+ version: "0.5.0",
9207
9577
  type: "module",
9208
9578
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9209
9579
  keywords: [
@@ -9255,13 +9625,16 @@ var package_default = {
9255
9625
  }
9256
9626
  };
9257
9627
 
9628
+ // src/version.ts
9629
+ var PACKAGE_VERSION = package_default.version;
9630
+ var MIN_NODE_MAJOR = Number(/(\d+)/.exec(package_default.engines?.node ?? "")?.[1] ?? 0);
9631
+
9258
9632
  // src/web.ts
9259
9633
  init_storm();
9260
9634
  init_resources();
9261
9635
  init_config();
9262
9636
  init_droneChannel();
9263
9637
  init_oauth();
9264
- var PACKAGE_VERSION = package_default.version;
9265
9638
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
9266
9639
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
9267
9640
  var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
@@ -10569,7 +10942,8 @@ function daemonStatus(deps) {
10569
10942
  restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
10570
10943
  counts,
10571
10944
  live,
10572
- version: PACKAGE_VERSION
10945
+ version: PACKAGE_VERSION,
10946
+ repos: deps.repoMap?.() ?? []
10573
10947
  };
10574
10948
  }
10575
10949
  function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
@@ -10817,8 +11191,8 @@ function startWeb(deps, port = WEB_PORT) {
10817
11191
  return { port, server };
10818
11192
  }
10819
11193
  function closeWebServer(server) {
10820
- return new Promise((resolve3) => {
10821
- server.close(() => resolve3());
11194
+ return new Promise((resolve4) => {
11195
+ server.close(() => resolve4());
10822
11196
  server.closeAllConnections?.();
10823
11197
  });
10824
11198
  }
@@ -10862,6 +11236,253 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
10862
11236
  }
10863
11237
  }
10864
11238
 
11239
+ // src/release.ts
11240
+ var SWARM_PACKAGE = "@taskless-app/swarm";
11241
+ var LATEST_TIMEOUT_MS = 4e3;
11242
+ async function fetchLatestVersion(opts = {}) {
11243
+ const fetchFn = opts.fetchFn ?? fetch;
11244
+ const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
11245
+ const pkg = opts.pkg ?? SWARM_PACKAGE;
11246
+ const ctrl = new AbortController();
11247
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
11248
+ try {
11249
+ const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
11250
+ signal: ctrl.signal,
11251
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
11252
+ });
11253
+ if (!res.ok) return null;
11254
+ const body = await res.json();
11255
+ return typeof body.version === "string" && body.version ? body.version : null;
11256
+ } catch {
11257
+ return null;
11258
+ } finally {
11259
+ clearTimeout(timer);
11260
+ }
11261
+ }
11262
+ function isOutdated(current, latest) {
11263
+ if (!latest || !current) return false;
11264
+ return compareVersionDirs(latest, current) > 0;
11265
+ }
11266
+ function outdatedLine(current, latest, updateCommand = "swarm update") {
11267
+ if (!isOutdated(current, latest)) return null;
11268
+ return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
11269
+ }
11270
+
11271
+ // src/hostCommands.ts
11272
+ init_src();
11273
+ import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
11274
+ import { join as join22 } from "path";
11275
+ function hostCommandStatePath(home) {
11276
+ return join22(home, "host-command.json");
11277
+ }
11278
+ function readState(path) {
11279
+ try {
11280
+ return JSON.parse(readFileSync18(path, "utf8"));
11281
+ } catch {
11282
+ return {};
11283
+ }
11284
+ }
11285
+ function writeState(path, state) {
11286
+ try {
11287
+ writeFileSync13(`${path}.tmp`, JSON.stringify(state));
11288
+ renameSync8(`${path}.tmp`, path);
11289
+ } catch {
11290
+ }
11291
+ }
11292
+ function savePendingAck(path, ack) {
11293
+ writeState(path, { ...readState(path), pendingAck: ack });
11294
+ }
11295
+ function readPendingAck(path) {
11296
+ return readState(path).pendingAck ?? null;
11297
+ }
11298
+ function clearPendingAck(path) {
11299
+ const state = readState(path);
11300
+ const pending2 = state.pendingAck ?? null;
11301
+ if (pending2) writeState(path, { ...state, pendingAck: null });
11302
+ return pending2;
11303
+ }
11304
+ async function flushPendingAck(path, ack) {
11305
+ const state = readState(path);
11306
+ const pending2 = state.pendingAck;
11307
+ if (!pending2) return null;
11308
+ writeState(path, { ...state, pendingAck: null });
11309
+ try {
11310
+ await ack(pending2.commandId, "done", { step: "restart", result: pending2.note });
11311
+ } catch {
11312
+ }
11313
+ return pending2;
11314
+ }
11315
+ function loadMaxDronesOverride(path) {
11316
+ const n = readState(path).maxDrones;
11317
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
11318
+ }
11319
+ function saveMaxDronesOverride(path, n) {
11320
+ writeState(path, { ...readState(path), maxDrones: n });
11321
+ }
11322
+ async function executeHostCommands(opts) {
11323
+ const now = opts.nowMs ?? Date.now();
11324
+ let restarting = false;
11325
+ for (const cmd of opts.commands) {
11326
+ if (restarting) {
11327
+ await opts.ack(cmd.id, "rejected", { result: "\u0434\u0435\u043C\u043E\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u2014 \u043E\u0442\u0434\u0430\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u0437\u0430\u043D\u043E\u0432\u043E, \u043A\u043E\u0433\u0434\u0430 \u043E\u043D \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F" });
11328
+ continue;
11329
+ }
11330
+ if (isHostCommandExpired(cmd.expiresAt, now)) {
11331
+ await opts.ack(cmd.id, "rejected", { result: "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u0430 \u2014 \u044F \u0431\u044B\u043B \u043D\u0435 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E" });
11332
+ continue;
11333
+ }
11334
+ if (!isHostCommandKind(cmd.kind)) {
11335
+ await opts.ack(cmd.id, "rejected", { result: `\u044D\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \xAB${cmd.kind}\xBB \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u0434\u0435\u043C\u043E\u043D` });
11336
+ continue;
11337
+ }
11338
+ restarting = await executeOne(cmd, opts);
11339
+ }
11340
+ }
11341
+ async function executeOne(cmd, opts) {
11342
+ const { effects, ack, statePath } = opts;
11343
+ const label = hostCommandLabel(cmd);
11344
+ effects.narrate(`\u{1F4DF} \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless: ${label}`);
11345
+ try {
11346
+ switch (cmd.kind) {
11347
+ case "pause":
11348
+ effects.pause();
11349
+ await ack(cmd.id, "done", { result: "\u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0443, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442" });
11350
+ return false;
11351
+ case "resume":
11352
+ if (!effects.resume()) {
11353
+ await ack(cmd.id, "rejected", {
11354
+ result: "\u0436\u0434\u0443 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u2014 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u043D\u0438\u043C\u0443 \u043F\u0430\u0443\u0437\u0443, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u043E\u0442\u044B \u043D\u0435 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u044F\u0442\u0441\u044F \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u043D\u0435 \u0441\u043B\u0443\u0447\u0438\u0442\u0441\u044F"
11355
+ });
11356
+ return false;
11357
+ }
11358
+ await ack(cmd.id, "done", { result: "\u0441\u043D\u044F\u043B \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0443 \u0437\u0430\u0434\u0430\u0447\u0438" });
11359
+ return false;
11360
+ case "set_max_drones": {
11361
+ const n = cmd.maxDrones;
11362
+ if (n == null || !Number.isFinite(n) || n < 1 || n > MAX_DRONES_LIMIT) {
11363
+ await ack(cmd.id, "rejected", { result: `\u0447\u0438\u0441\u043B\u043E \u0434\u0440\u043E\u043D\u043E\u0432 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043E\u0442 1 \u0434\u043E ${MAX_DRONES_LIMIT}` });
11364
+ return false;
11365
+ }
11366
+ const value = Math.trunc(n);
11367
+ saveMaxDronesOverride(statePath, value);
11368
+ effects.setMaxDrones(value);
11369
+ await ack(cmd.id, "done", { result: `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: ${value}` });
11370
+ return false;
11371
+ }
11372
+ case "stop_run": {
11373
+ const slug = cmd.runSlug?.trim();
11374
+ if (!slug) {
11375
+ await ack(cmd.id, "rejected", { result: "\u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043D\u043E, \u043A\u0430\u043A\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0442\u044C" });
11376
+ return false;
11377
+ }
11378
+ const stopped = await effects.stopRun(slug, "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u0438\u0437 Taskless");
11379
+ if (!stopped) {
11380
+ await ack(cmd.id, "rejected", { result: `\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u043E ${slug} \u0443 \u043C\u0435\u043D\u044F \u043D\u0435\u0442` });
11381
+ return false;
11382
+ }
11383
+ await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
11384
+ return false;
11385
+ }
11386
+ case "restart":
11387
+ return await doRestart(cmd, opts, "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless");
11388
+ case "update": {
11389
+ const res = await effects.update(async (step, note) => {
11390
+ await ack(cmd.id, "running", { step, result: note });
11391
+ });
11392
+ if (!res.ok) {
11393
+ await ack(cmd.id, "failed", { step: res.step, result: `${UPDATE_STEP_LABELS[res.step]}: ${res.message}` });
11394
+ effects.narrate(`\u26A0\uFE0F \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0448\u0430\u0433\u0435 \xAB${UPDATE_STEP_LABELS[res.step]}\xBB: ${res.message}`);
11395
+ return false;
11396
+ }
11397
+ return await doRestart(cmd, opts, res.message);
11398
+ }
11399
+ }
11400
+ } catch (e) {
11401
+ await ack(cmd.id, "failed", { result: e instanceof Error ? e.message : String(e) });
11402
+ return false;
11403
+ }
11404
+ }
11405
+ async function doRestart(cmd, opts, note) {
11406
+ const { effects, ack, statePath } = opts;
11407
+ savePendingAck(statePath, { commandId: cmd.id, kind: cmd.kind, note });
11408
+ const { waiting, sessions } = effects.requestRestart();
11409
+ if (waiting) {
11410
+ await ack(cmd.id, "running", {
11411
+ step: "restart",
11412
+ result: `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0443, \u043A\u043E\u0433\u0434\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u044F\u0442\u0441\u044F \u0441\u043B\u043E\u0442\u044B: \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 ${sessions.length} \u2014 \u043E\u043D\u0438 \u0431\u044B \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u0438`
11413
+ });
11414
+ effects.narrate(`\u23F3 \u0420\u0435\u0441\u0442\u0430\u0440\u0442 \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 (${sessions.length})`);
11415
+ return true;
11416
+ }
11417
+ await ack(cmd.id, "running", { step: "restart", result: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u044E\u0441\u044C" });
11418
+ return true;
11419
+ }
11420
+
11421
+ // src/selfUpdate.ts
11422
+ init_git();
11423
+ import { existsSync as existsSync13 } from "fs";
11424
+ import { join as join23 } from "path";
11425
+ import { $ as $6 } from "execa";
11426
+ var DAEMON_PACKAGE = "@taskless-app/swarm";
11427
+ var defaultRun = async (cmd, args, cwd) => {
11428
+ const res = await $6({ reject: false, cwd })(cmd, args);
11429
+ return { exitCode: res.exitCode ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
11430
+ };
11431
+ function tail(text, max = 400) {
11432
+ const t = text.trim();
11433
+ return t.length <= max ? t : `\u2026${t.slice(-max)}`;
11434
+ }
11435
+ async function runSelfUpdate(io) {
11436
+ const run = io.run ?? defaultRun;
11437
+ const exists = io.exists ?? existsSync13;
11438
+ const repo = io.repo;
11439
+ await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
11440
+ const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
11441
+ if (fetched.exitCode !== 0) {
11442
+ return { ok: false, step: "fetch", message: `git fetch \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(fetched.stderr || fetched.stdout)}` };
11443
+ }
11444
+ await io.onStep("checkout", "\u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0430\u044E\u0441\u044C \u043D\u0430 \u0441\u0432\u0435\u0436\u0438\u0439 main");
11445
+ const dirty = await run("git", ["-C", repo, "status", "--porcelain"], repo);
11446
+ if (dirty.exitCode !== 0) {
11447
+ return { ok: false, step: "checkout", message: `\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F: ${tail(dirty.stderr)}` };
11448
+ }
11449
+ if (dirty.stdout.trim()) {
11450
+ return {
11451
+ ok: false,
11452
+ step: "checkout",
11453
+ message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u0435\u0441\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E \u0438\u0445. \u041F\u0440\u0438\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435."
11454
+ };
11455
+ }
11456
+ const ref = await io.mainRef(repo);
11457
+ const merged = await run("git", ["-C", repo, "merge", "--ff-only", ref], repo);
11458
+ if (merged.exitCode !== 0) {
11459
+ return {
11460
+ ok: false,
11461
+ step: "checkout",
11462
+ message: `\u043D\u0435 \u043F\u0435\u0440\u0435\u043C\u0430\u0442\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0430 ${ref} (\u0441\u0432\u043E\u044F \u0432\u0435\u0442\u043A\u0430/\u0440\u0430\u0441\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0441 main): ${tail(merged.stderr || merged.stdout)}`
11463
+ };
11464
+ }
11465
+ await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
11466
+ if (!exists(join23(repo, "pnpm-lock.yaml"))) {
11467
+ return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
11468
+ }
11469
+ const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
11470
+ if (installed.exitCode !== 0) {
11471
+ return { ok: false, step: "install", message: `pnpm install \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(installed.stderr || installed.stdout)}` };
11472
+ }
11473
+ await io.onStep("build", "\u0441\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u043A\u0435\u0442\u044B \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430");
11474
+ const built = await run("pnpm", ["--filter", `${DAEMON_PACKAGE}...`, "build"], repo);
11475
+ if (built.exitCode !== 0) {
11476
+ return { ok: false, step: "build", message: `\u0441\u0431\u043E\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430: ${tail(built.stderr || built.stdout)}` };
11477
+ }
11478
+ const head = await run("git", ["-C", repo, "rev-parse", "--short", "HEAD"], repo);
11479
+ const sha = head.exitCode === 0 ? head.stdout.trim() : "";
11480
+ return { ok: true, step: "build", message: sha ? `\u043A\u043E\u0434 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0434\u043E ${sha}, \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0438 \u0441\u0431\u043E\u0440\u043A\u0430 \u0433\u043E\u0442\u043E\u0432\u044B` : "\u043A\u043E\u0434 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D, \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0438 \u0441\u0431\u043E\u0440\u043A\u0430 \u0433\u043E\u0442\u043E\u0432\u044B" };
11481
+ }
11482
+ function defaultSelfUpdateIO(repo, onStep) {
11483
+ return { repo, run: defaultRun, mainRef: remoteMainRef, onStep };
11484
+ }
11485
+
10865
11486
  // src/narration.ts
10866
11487
  var RING_LIMIT = 20;
10867
11488
  function makeOvermindNarrator() {
@@ -10882,7 +11503,7 @@ var RESET_RE = /resets[^\n]*/i;
10882
11503
  function anthropicHealthy(opts = {}) {
10883
11504
  const bin = opts.claudeBin ?? resolveClaudeBin();
10884
11505
  const timeoutMs = opts.timeoutMs ?? 2e4;
10885
- return new Promise((resolve3) => {
11506
+ return new Promise((resolve4) => {
10886
11507
  let done = false;
10887
11508
  let out = "";
10888
11509
  const finish = (r) => {
@@ -10893,7 +11514,7 @@ function anthropicHealthy(opts = {}) {
10893
11514
  child.kill("SIGKILL");
10894
11515
  } catch {
10895
11516
  }
10896
- resolve3(r);
11517
+ resolve4(r);
10897
11518
  };
10898
11519
  const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: ["ignore", "pipe", "pipe"] });
10899
11520
  child.stdout?.on("data", (d) => {
@@ -10932,16 +11553,16 @@ async function fetchExternalStorm(url, opts = {}) {
10932
11553
  }
10933
11554
 
10934
11555
  // src/anomalyState.ts
10935
- import { existsSync as existsSync11, readFileSync as readFileSync16, renameSync as renameSync6, writeFileSync as writeFileSync12 } from "fs";
10936
- import { join as join20 } from "path";
11556
+ import { existsSync as existsSync14, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
11557
+ import { join as join24 } from "path";
10937
11558
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
10938
11559
  var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
10939
11560
  function anomalyStatePath(home) {
10940
- return join20(home, "anomaly-state.json");
11561
+ return join24(home, "anomaly-state.json");
10941
11562
  }
10942
11563
  function loadAnomalyState(home, now = Date.now(), io = {}) {
10943
- const exists = io.exists ?? existsSync11;
10944
- const readFile2 = io.readFile ?? ((p) => readFileSync16(p, "utf8"));
11564
+ const exists = io.exists ?? existsSync14;
11565
+ const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
10945
11566
  const path = anomalyStatePath(home);
10946
11567
  if (!exists(path)) return { ...EMPTY };
10947
11568
  try {
@@ -10967,8 +11588,8 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
10967
11588
  function saveAnomalyState(home, state, io = {}) {
10968
11589
  const path = anomalyStatePath(home);
10969
11590
  const writeFile2 = io.writeFile ?? ((p, data) => {
10970
- writeFileSync12(`${p}.tmp`, data);
10971
- renameSync6(`${p}.tmp`, p);
11591
+ writeFileSync14(`${p}.tmp`, data);
11592
+ renameSync9(`${p}.tmp`, p);
10972
11593
  });
10973
11594
  writeFile2(path, JSON.stringify(state, null, 2));
10974
11595
  }
@@ -11610,10 +12231,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
11610
12231
  }
11611
12232
  return parts.join("\n");
11612
12233
  }
11613
- var HERO_CHARTER_PATH = join21(dirname5(THIS_FILE), "..", "docs", "HERO.md");
12234
+ var HERO_CHARTER_PATH = join25(dirname5(THIS_FILE), "..", "docs", "HERO.md");
11614
12235
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
11615
12236
  const name = run.name || `#${run.id}`;
11616
- const charter = readFileSync17(HERO_CHARTER_PATH, "utf8");
12237
+ const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
11617
12238
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
11618
12239
 
11619
12240
  ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
@@ -11864,7 +12485,7 @@ async function wakeRun(deps, runId) {
11864
12485
  if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
11865
12486
  if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
11866
12487
  if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
11867
- const worktreeExists = deps.worktreeExists ?? existsSync12;
12488
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
11868
12489
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
11869
12490
  const wt = run.worktree_path;
11870
12491
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -11949,7 +12570,7 @@ function repoHasCi(deps, run) {
11949
12570
  function repoHasCiWorkflows(worktreePath, repo) {
11950
12571
  for (const base3 of [worktreePath, repo]) {
11951
12572
  if (!base3) continue;
11952
- const dir = join21(base3, ".github", "workflows");
12573
+ const dir = join25(base3, ".github", "workflows");
11953
12574
  try {
11954
12575
  if (readdirSync10(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
11955
12576
  } catch {
@@ -12098,7 +12719,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
12098
12719
  return true;
12099
12720
  }
12100
12721
  function worktreeAndTranscriptAlive(deps, worktree) {
12101
- const exists = deps.worktreeExists ?? existsSync12;
12722
+ const exists = deps.worktreeExists ?? existsSync15;
12102
12723
  const transcript = deps.hasTranscript ?? sessionTranscriptExists;
12103
12724
  return exists(worktree) && transcript(worktree);
12104
12725
  }
@@ -12241,6 +12862,15 @@ async function exclusiveGateAllows(deps, run) {
12241
12862
  }
12242
12863
  return true;
12243
12864
  }
12865
+ function pruneGateLogs(deps) {
12866
+ if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size) return;
12867
+ const open = /* @__PURE__ */ new Set();
12868
+ for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
12869
+ for (const map of [deps.envGateLogged, deps.exclusiveLogged]) {
12870
+ if (!map) continue;
12871
+ for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
12872
+ }
12873
+ }
12244
12874
  async function exclusivityBlockedRunIds(deps) {
12245
12875
  const blocked = /* @__PURE__ */ new Set();
12246
12876
  if (!deps.exclusiveProject) return blocked;
@@ -12303,7 +12933,7 @@ async function resumeWatchdogParked(deps, droneCap) {
12303
12933
  }
12304
12934
  if (!candidate) return;
12305
12935
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
12306
- const worktreeExists = deps.worktreeExists ?? existsSync12;
12936
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12307
12937
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12308
12938
  const wt = candidate.worktree_path;
12309
12939
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -12472,7 +13102,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
12472
13102
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
12473
13103
  if (!deps.respawn) return;
12474
13104
  const wt = resumeCandidate.worktree_path;
12475
- const worktreeExists = deps.worktreeExists ?? existsSync12;
13105
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12476
13106
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12477
13107
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12478
13108
  const mode = canContinue ? "continue" : "fresh";
@@ -12634,7 +13264,7 @@ function buildEscalationCandidates(db) {
12634
13264
  }
12635
13265
  async function tickOnce(deps) {
12636
13266
  const now = deps.now ? deps.now() : Date.now();
12637
- const worktreeExists = deps.worktreeExists ?? existsSync12;
13267
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12638
13268
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12639
13269
  if (deps.rehydrate) {
12640
13270
  const rh = deps.rehydrate;
@@ -12686,6 +13316,18 @@ async function tickOnce(deps) {
12686
13316
  await dv.run();
12687
13317
  }
12688
13318
  }
13319
+ if (deps.repoMap) {
13320
+ const rm3 = deps.repoMap;
13321
+ const sinceLast = rm3.lastAt == null ? Infinity : now - rm3.lastAt;
13322
+ if (sinceLast >= rm3.intervalMs) {
13323
+ rm3.lastAt = now;
13324
+ try {
13325
+ await rm3.run();
13326
+ } catch (e) {
13327
+ console.error("[overmind] \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043A\u0430\u0440\u0442\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:", e);
13328
+ }
13329
+ }
13330
+ }
12689
13331
  if (deps.rehydrate && deps.kill) {
12690
13332
  const kill = deps.kill;
12691
13333
  for (const run of deps.db.listRuns()) {
@@ -12711,6 +13353,19 @@ async function tickOnce(deps) {
12711
13353
  }
12712
13354
  }
12713
13355
  }
13356
+ if (deps.logRotate) {
13357
+ const lr = deps.logRotate;
13358
+ const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
13359
+ if (sinceLast >= lr.intervalMs) {
13360
+ lr.lastAt = now;
13361
+ try {
13362
+ lr.run();
13363
+ } catch (e) {
13364
+ console.error("[overmind] \u0440\u043E\u0442\u0430\u0446\u0438\u044F \u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
13365
+ }
13366
+ }
13367
+ }
13368
+ pruneGateLogs(deps);
12714
13369
  let stormActive = false;
12715
13370
  if (deps.storm) {
12716
13371
  for (const run of deps.db.listRuns("running")) {
@@ -13240,7 +13895,7 @@ async function resolveConflict(deps, reviewRun) {
13240
13895
  workspace_id: reviewRun.workspace_id
13241
13896
  // та же задача, тот же воркспейс (TASWM-288)
13242
13897
  });
13243
- const wt = join21(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13898
+ const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13244
13899
  try {
13245
13900
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13246
13901
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -13292,7 +13947,7 @@ async function spawnReviewer(deps, reviewRun, files) {
13292
13947
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
13293
13948
  workspace_id: reviewRun.workspace_id
13294
13949
  });
13295
- const wt = join21(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13950
+ const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13296
13951
  try {
13297
13952
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13298
13953
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -13327,7 +13982,7 @@ async function respawnRun(deps, run, mode, nudge) {
13327
13982
  ]);
13328
13983
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13329
13984
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
13330
- if (!existsSync12(wt)) {
13985
+ if (!existsSync15(wt)) {
13331
13986
  const branch = runToSpawn.branch || runToSpawn.task_slug;
13332
13987
  const source = await addWorktree(repo, wt, branch, {
13333
13988
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -13352,11 +14007,11 @@ async function respawnRun(deps, run, mode, nudge) {
13352
14007
  const prompt = notice ? `${freshPrompt}
13353
14008
 
13354
14009
  ${notice}` : freshPrompt;
13355
- const pid = deps.launch({ worktree: wt, run, prompt });
14010
+ const pid = deps.launch({ worktree: wt, run: runToSpawn, prompt });
13356
14011
  deps.db.updateRun(run.id, { status: "running", pid });
13357
14012
  }
13358
14013
  async function syncCrashedOnStartup(deps) {
13359
- const worktreeExists = deps.worktreeExists ?? existsSync12;
14014
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
13360
14015
  let orphaned = 0;
13361
14016
  let merged = 0;
13362
14017
  let requeued = 0;
@@ -13419,39 +14074,37 @@ async function makeStore(repo, worktreesDir, repoFlag) {
13419
14074
  console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C \u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E (${e instanceof Error ? e.message : e}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0437\u0430\u043F\u0443\u0441\u043A \u0441\u043D\u043E\u0432\u0430 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u0442 --repo`);
13420
14075
  }
13421
14076
  }
14077
+ const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir4(), repoPaths);
14078
+ const repoMap = emptyRepoMapState(scanRootsList);
13422
14079
  if (SWARM_BACKEND !== "client") {
13423
14080
  const { SwarmDb: SwarmDb2 } = await loadSqlite();
13424
- return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host };
14081
+ return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host, repoMap };
13425
14082
  }
13426
14083
  const call = resilientSwarmMcp({
13427
14084
  onDown: (ms) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP \u043C\u043E\u043B\u0447\u0438\u0442 ${Math.round(ms / 1e3)}\u0441 \u2014 \u0440\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u0441\u0434\u0430\u0447\u0438 \u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u044B \u043D\u0435 \u0434\u043E\u0435\u0437\u0436\u0430\u044E\u0442.`),
13428
14085
  onUp: () => console.warn("[overmind] \u043F\u0440\u043E\u0434-MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
13429
14086
  });
13430
14087
  const explicitRegistry = await buildRepoRegistry(repoPaths);
13431
- const byName = new Map(explicitRegistry.map((e) => [e.repoFullName, e.path]));
13432
- const scanRootsList = defaultScanRoots(homedir3(), repoPaths);
13433
- const scanStartedAt = Date.now();
13434
- const scanned = scanRoots(scanRootsList, { maxDepth: SWARM_SCAN_DEPTH });
13435
- let addedByScan = 0;
13436
- for (const [full, path] of scanned) {
13437
- if (byName.has(full)) continue;
13438
- byName.set(full, path);
13439
- addedByScan++;
13440
- }
13441
- if (addedByScan > 0) {
13442
- console.log(
13443
- `[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${addedByScan} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - scanStartedAt}\u043C\u0441: ${[...scanned.keys()].slice(0, 8).join(", ")}${scanned.size > 8 ? ` \u0438 \u0435\u0449\u0451 ${scanned.size - 8}` : ""}`
13444
- );
13445
- }
13446
- const repoRegistry = [...byName].map(([repoFullName, path]) => ({ repoFullName, path }));
13447
- if (repoPaths.length > 0 && repoRegistry.length < repoPaths.length) {
14088
+ if (repoPaths.length > 0 && explicitRegistry.length < repoPaths.length) {
13448
14089
  console.warn(
13449
- `[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${repoRegistry.length}/${repoPaths.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
14090
+ `[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${explicitRegistry.length}/${repoPaths.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
13450
14091
  );
13451
14092
  }
14093
+ const rebuildRegistry = () => refreshRepoMap({
14094
+ call,
14095
+ configured: explicitRegistry,
14096
+ roots: scanRootsList,
14097
+ maxDepth: SWARM_SCAN_DEPTH,
14098
+ cachePath: REPO_MAP_PATH,
14099
+ state: repoMap,
14100
+ log: (m) => console.log(m)
14101
+ });
14102
+ const repoRegistry = await rebuildRegistry();
13452
14103
  if (repoRegistry.length === 0) {
13453
14104
  console.warn(
13454
- `[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}. \u0415\u0441\u043B\u0438 \u043A\u043B\u043E\u043D\u044B \u043B\u0435\u0436\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433\u043E\u043C \u043C\u0435\u0441\u0442\u0435 \u2014 \u0443\u043A\u0430\u0436\u0438: swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E> (\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u0441\u044F).`
14105
+ `[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}. ` + // TASWM-336: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
14106
+ // общая фраза заставляла бы человека гадать, что из этого его случай.
14107
+ (repoMap.unknown ? "\u041F\u0440\u043E\u0434 \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F, \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C `swarm login` \u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u044C \u0441\u0435\u0440\u0432\u0435\u0440\u0430. " : repoMap.missing.length ? `\u041D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0438\u0437 ${repoMap.missing.length} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u0442\u0443\u0442 \u043D\u0435\u0442. ` : "\u0423 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C. ") + "\u0415\u0441\u043B\u0438 \u043A\u043B\u043E\u043D\u044B \u043B\u0435\u0436\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433\u043E\u043C \u043C\u0435\u0441\u0442\u0435 \u2014 \u0443\u043A\u0430\u0436\u0438: swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E> (\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u0441\u044F)."
13455
14108
  );
13456
14109
  }
13457
14110
  const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry, host });
@@ -13466,7 +14119,17 @@ async function makeStore(repo, worktreesDir, repoFlag) {
13466
14119
  `overmind backend=client (\u0442\u043E\u043D\u043A\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442, outbox=${SWARM_OUTBOX}); \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F (ps \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D) \u2014 running/blocked \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0435\u0440\u0435\u043A\u043E\u043D\u0441\u0438\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u044B, startup-sync (TASWM-128) \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D`
13467
14120
  );
13468
14121
  }
13469
- return { store: client, reconciled: rec.reconciled, host };
14122
+ return {
14123
+ store: client,
14124
+ reconciled: rec.reconciled,
14125
+ host,
14126
+ repoMap,
14127
+ // Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
14128
+ // и список для claim/heartbeat, и обратную карту owner/repo → путь.
14129
+ refreshRepoMap: async () => {
14130
+ client.setRepoRegistry(await rebuildRegistry());
14131
+ }
14132
+ };
13470
14133
  }
13471
14134
  var crashGuardsInstalled = false;
13472
14135
  function installCrashGuards() {
@@ -13485,7 +14148,7 @@ async function migrateWorktreesAtStartup() {
13485
14148
  legacyDir: WORKTREES_LEGACY_DIR,
13486
14149
  targetDir: WORKTREES_DIR,
13487
14150
  envOverride: process.env.SWARM_WORKTREES || "",
13488
- exists: (d) => existsSync12(d),
14151
+ exists: (d) => existsSync15(d),
13489
14152
  isNonEmptyDir: (d) => {
13490
14153
  try {
13491
14154
  return readdirSync10(d).length > 0;
@@ -13494,13 +14157,13 @@ async function migrateWorktreesAtStartup() {
13494
14157
  }
13495
14158
  },
13496
14159
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
13497
- rename: (from, to) => renameSync7(from, to),
14160
+ rename: (from, to) => renameSync10(from, to),
13498
14161
  repairMoved: (t) => repairMovedWorktrees(t),
13499
14162
  log: (m) => console.log(m)
13500
14163
  });
13501
14164
  } catch (e) {
13502
14165
  console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
13503
- return existsSync12(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
14166
+ return existsSync15(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
13504
14167
  }
13505
14168
  }
13506
14169
  async function reportWorkspaceAccess(tl, db) {
@@ -13518,13 +14181,22 @@ async function reportWorkspaceAccess(tl, db) {
13518
14181
  async function runDaemon(opts) {
13519
14182
  installCrashGuards();
13520
14183
  const daemonStart = await captureDaemonStart(THIS_FILE);
14184
+ const latestRelease = fetchLatestVersion();
13521
14185
  const worktreesDir = await migrateWorktreesAtStartup();
13522
14186
  if (!opts.repo) {
13523
14187
  const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
13524
14188
  if (remembered) opts.repo = remembered;
13525
14189
  }
13526
- const { store: db, reconciled, host } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
14190
+ const {
14191
+ store: db,
14192
+ reconciled,
14193
+ host,
14194
+ repoMap: repoMapState,
14195
+ refreshRepoMap: rebuildRepoMap
14196
+ } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
13527
14197
  await preflightAuth();
14198
+ const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
14199
+ if (outdated) console.warn(`[overmind] ${outdated}`);
13528
14200
  const tl = await connectTaskless();
13529
14201
  const tlFor = (workspaceId) => tl ? tl.forWorkspace(workspaceId) : null;
13530
14202
  if (tl) await reportWorkspaceAccess(tl, db);
@@ -13555,6 +14227,8 @@ async function runDaemon(opts) {
13555
14227
  }
13556
14228
  } : void 0;
13557
14229
  let lastDroneCap = null;
14230
+ const hostCommandState = hostCommandStatePath(SWARM_HOME);
14231
+ let hostMaxDrones = loadMaxDronesOverride(hostCommandState);
13558
14232
  const launch = makeLauncher({
13559
14233
  overmindEntry: THIS_FILE,
13560
14234
  dbPath: DB_PATH,
@@ -13582,6 +14256,10 @@ async function runDaemon(opts) {
13582
14256
  const client = db;
13583
14257
  rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
13584
14258
  }
14259
+ let repoMap;
14260
+ if (rebuildRepoMap && SWARM_REPO_MAP_MS > 0) {
14261
+ repoMap = { run: rebuildRepoMap, intervalMs: SWARM_REPO_MAP_MS, lastAt: Date.now() };
14262
+ }
13585
14263
  let daemonVersion;
13586
14264
  if (daemonStart && SWARM_VERSION_CHECK_MS > 0) {
13587
14265
  let cached = null;
@@ -13593,22 +14271,26 @@ async function runDaemon(opts) {
13593
14271
  intervalMs: SWARM_VERSION_CHECK_MS
13594
14272
  };
13595
14273
  }
14274
+ let onHostCommands = () => {
14275
+ };
13596
14276
  let heartbeat;
13597
14277
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_PROD_HEARTBEAT_MS > 0) {
13598
14278
  const client = db;
13599
14279
  heartbeat = {
13600
14280
  push: async () => {
13601
- const commands = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
13602
- if (commands.length === 0) return;
13603
- await serveLogRequests(
13604
- {
13605
- runLogsDir: RUN_LOGS_DIR,
13606
- daemonLogPath: DAEMON_LOG_PATH,
13607
- localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
13608
- deliver: (requestId, payload) => client.deliverLog(requestId, payload)
13609
- },
13610
- commands
13611
- );
14281
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
14282
+ if (reply.commands.length) onHostCommands(reply.commands);
14283
+ if (reply.logRequests.length) {
14284
+ await serveLogRequests(
14285
+ {
14286
+ runLogsDir: RUN_LOGS_DIR,
14287
+ daemonLogPath: DAEMON_LOG_PATH,
14288
+ localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
14289
+ deliver: (requestId, payload) => client.deliverLog(requestId, payload)
14290
+ },
14291
+ reply.logRequests
14292
+ );
14293
+ }
13612
14294
  },
13613
14295
  intervalMs: SWARM_PROD_HEARTBEAT_MS
13614
14296
  };
@@ -13682,7 +14364,7 @@ async function runDaemon(opts) {
13682
14364
  fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
13683
14365
  ttlMs: 1e4
13684
14366
  });
13685
- maxDrones = async () => lastDroneCap = await readMaxDrones();
14367
+ maxDrones = async () => lastDroneCap = hostMaxDrones ?? await readMaxDrones();
13686
14368
  }
13687
14369
  let verifyModeReader;
13688
14370
  let baseBranchReader;
@@ -13809,8 +14491,9 @@ async function runDaemon(opts) {
13809
14491
  baseBranchReader ? baseBranchReader(repoOfRun) : Promise.resolve("")
13810
14492
  ]);
13811
14493
  const task = taskClient ? requireTaskBody(rawTask, run.task_slug, run.workspace_id) : null;
13812
- const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13813
- const runToSpawn = base3 && run.base_branch !== base3 ? db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
14494
+ const healed = healTaskSlug(db, run, task);
14495
+ const base3 = effectiveBaseBranch(healed.base_branch, projectBase);
14496
+ const runToSpawn = base3 && healed.base_branch !== base3 ? db.updateRun(healed.id, { base_branch: base3 }) ?? healed : healed;
13814
14497
  const taskContext = task && taskClient ? await fetchTaskContext(taskClient, task) : void 0;
13815
14498
  await spawnDrone(
13816
14499
  {
@@ -13828,7 +14511,7 @@ async function runDaemon(opts) {
13828
14511
  runToSpawn
13829
14512
  );
13830
14513
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
13831
- if (taskClient) await taskClient.startTask(run.task_slug);
14514
+ if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
13832
14515
  },
13833
14516
  prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
13834
14517
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
@@ -13941,6 +14624,22 @@ async function runDaemon(opts) {
13941
14624
  ...exclusiveProject ? { exclusiveProject } : {},
13942
14625
  repoFallback: opts.repo,
13943
14626
  exclusiveLogged: /* @__PURE__ */ new Map(),
14627
+ // Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
14628
+ // только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
14629
+ // видно, что история не потерялась, а уехала в `.1`.
14630
+ logRotate: {
14631
+ run: () => {
14632
+ if (!stdoutGoesTo(DAEMON_LOG_PATH)) return;
14633
+ const outcome = rotateDaemonLog(DAEMON_LOG_PATH, { maxBytes: DAEMON_LOG_MAX_BYTES, keep: DAEMON_LOG_KEEP });
14634
+ if (outcome === "rotated") {
14635
+ console.log(
14636
+ `[overmind] \u043B\u043E\u0433 \u0434\u043E\u0440\u043E\u0441 \u0434\u043E ${Math.round(DAEMON_LOG_MAX_BYTES / 1024 / 1024)} \u041C\u0411 \u2014 \u043F\u0440\u043E\u0448\u043B\u043E\u0435 \u043F\u043E\u043A\u043E\u043B\u0435\u043D\u0438\u0435 \u0432 ${DAEMON_LOG_PATH}.1 (\u0434\u0435\u0440\u0436\u0438\u043C ${DAEMON_LOG_KEEP}; \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u2014 SWARM_DAEMON_LOG_MAX_BYTES)`
14637
+ );
14638
+ }
14639
+ },
14640
+ intervalMs: DAEMON_LOG_CHECK_MS,
14641
+ lastAt: null
14642
+ },
13944
14643
  // merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
13945
14644
  // фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
13946
14645
  resolveConflict: (reviewRun) => resolveConflict(
@@ -13980,7 +14679,7 @@ async function runDaemon(opts) {
13980
14679
  mode,
13981
14680
  nudge
13982
14681
  ),
13983
- worktreeExists: (p) => existsSync12(p),
14682
+ worktreeExists: (p) => existsSync15(p),
13984
14683
  hasTranscript: (p) => sessionTranscriptExists(p),
13985
14684
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
13986
14685
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -14008,6 +14707,8 @@ async function runDaemon(opts) {
14008
14707
  ...heartbeat ? { heartbeat } : {},
14009
14708
  // Сверка кода демона с main (TASWM-251): ключ есть только когда entryFile в git-репо.
14010
14709
  ...daemonVersion ? { daemonVersion } : {},
14710
+ // Карта репозиториев машины (TASWM-336): ключ есть только в client-режиме с непустым TTL.
14711
+ ...repoMap ? { repoMap } : {},
14011
14712
  // Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
14012
14713
  ...anomalies ? { anomalies } : {},
14013
14714
  // Сторож прогресса роя (TASWM-270): в отличие от anomalies выше — заведён БЕЗУСЛОВНО
@@ -14071,6 +14772,8 @@ async function runDaemon(opts) {
14071
14772
  narrator.narrate(`\u23F3 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${sessions.length}`);
14072
14773
  return { waiting: true, sessions };
14073
14774
  };
14775
+ let onRestartWaitCancelled = null;
14776
+ let onRestartStillWaiting = null;
14074
14777
  const requestStop = (o) => {
14075
14778
  const sessions = liveSessionIds(db, pidAlive);
14076
14779
  if (o.force || sessions.length === 0) {
@@ -14102,8 +14805,78 @@ async function runDaemon(opts) {
14102
14805
  restartAutoPaused = false;
14103
14806
  anomalyState.markResumed(Date.now());
14104
14807
  }
14808
+ onRestartWaitCancelled?.();
14105
14809
  narrator.narrate(wasStop ? "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0434\u0435\u043C\u043E\u043D\u0430" : "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430");
14106
14810
  };
14811
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
14812
+ const client = db;
14813
+ const ack = (id, status, patch) => client.ackCommand(id, status, patch ?? {});
14814
+ const effects = {
14815
+ pause: () => {
14816
+ control.paused = true;
14817
+ },
14818
+ resume: () => {
14819
+ if (restart.phase === "waiting") return false;
14820
+ control.paused = false;
14821
+ return true;
14822
+ },
14823
+ requestRestart: () => requestRestart(false),
14824
+ setMaxDrones: (n) => {
14825
+ hostMaxDrones = n;
14826
+ lastDroneCap = n;
14827
+ },
14828
+ stopRun: async (slug, reason) => {
14829
+ const run = await cancelRun(
14830
+ {
14831
+ db,
14832
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
14833
+ removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
14834
+ },
14835
+ slug,
14836
+ reason
14837
+ );
14838
+ return run ? { id: run.id } : null;
14839
+ },
14840
+ update: async (onStep) => {
14841
+ if (!daemonStart) {
14842
+ return { ok: false, step: "fetch", message: "\u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u043B\u0435\u0436\u0438\u0442 \u043D\u0435 \u0432 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
14843
+ }
14844
+ return runSelfUpdate(defaultSelfUpdateIO(daemonStart.repo, onStep));
14845
+ },
14846
+ narrate: narrator.narrate
14847
+ };
14848
+ let commandQueue = Promise.resolve();
14849
+ onHostCommands = (commands) => {
14850
+ commandQueue = commandQueue.then(() => executeHostCommands({ commands, effects, ack, statePath: hostCommandState })).catch((e) => {
14851
+ console.error("[overmind] \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u043C\u0430\u0448\u0438\u043D\u044B \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:", e);
14852
+ });
14853
+ };
14854
+ onRestartWaitCancelled = () => {
14855
+ const pending2 = clearPendingAck(hostCommandState);
14856
+ if (!pending2) return;
14857
+ void ack(pending2.commandId, "rejected", {
14858
+ step: "restart",
14859
+ result: "\u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E \u043D\u0430 \u0441\u0430\u043C\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u043E\u0442\u0434\u0430\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u0437\u0430\u043D\u043E\u0432\u043E"
14860
+ }).catch(() => {
14861
+ });
14862
+ };
14863
+ let lastWaitAck = 0;
14864
+ onRestartStillWaiting = (sessions) => {
14865
+ const nowMs = Date.now();
14866
+ if (nowMs - lastWaitAck < HOST_COMMAND_KEEPALIVE_MS) return;
14867
+ const pending2 = readPendingAck(hostCommandState);
14868
+ if (!pending2) return;
14869
+ lastWaitAck = nowMs;
14870
+ void ack(pending2.commandId, "running", {
14871
+ step: "restart",
14872
+ result: `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0443, \u043A\u043E\u0433\u0434\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u044F\u0442\u0441\u044F \u0441\u043B\u043E\u0442\u044B: \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 ${sessions} \u2014 \u043E\u043D\u0438 \u0431\u044B \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u0438`
14873
+ }).catch(() => {
14874
+ });
14875
+ };
14876
+ void flushPendingAck(hostCommandState, ack).then((done) => {
14877
+ if (done) narrator.narrate(`\u2705 \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0430: ${done.note}`);
14878
+ });
14879
+ }
14107
14880
  if (channelSecret) {
14108
14881
  droneChannel = startDroneChannel(
14109
14882
  {
@@ -14149,6 +14922,10 @@ async function runDaemon(opts) {
14149
14922
  // Устаревший код демона (TASWM-251): тот же кэш, что читает heartbeat.push выше —
14150
14923
  // локальный дашборд :7420 видит ту же сверку, что уходит в прод, без отдельного git-похода.
14151
14924
  daemonVersion: () => daemonVersion?.get() ?? null,
14925
+ // Карта репозиториев машины (TASWM-336): то же состояние, что мутирует шаг тика, — его
14926
+ // печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
14927
+ // считает своими и в каком дереве работает при двух копиях одного клона.
14928
+ repoMap: () => repoMapRows(repoMapState),
14152
14929
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
14153
14930
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
14154
14931
  control,
@@ -14210,8 +14987,10 @@ async function runDaemon(opts) {
14210
14987
  try {
14211
14988
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
14212
14989
  await tickOnce(deps);
14213
- if (restart.phase === "waiting" && liveSessionIds(db, pidAlive).length === 0) {
14214
- performRestart([], restart.intent);
14990
+ if (restart.phase === "waiting") {
14991
+ const live = liveSessionIds(db, pidAlive);
14992
+ if (live.length === 0) performRestart([], restart.intent);
14993
+ else onRestartStillWaiting?.(live.length);
14215
14994
  }
14216
14995
  } catch (e) {
14217
14996
  console.error("[overmind] tick error:", e);