@taskless-app/swarm 0.4.10 → 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 +1700 -610
  3. package/dist/overmind.js +603 -206
  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
 
@@ -432,7 +437,7 @@ var init_text = __esm({
432
437
  "use strict";
433
438
  ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
434
439
  CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
435
- EVENT_TEXT_MAX = 4e3;
440
+ EVENT_TEXT_MAX = 16e3;
436
441
  }
437
442
  });
438
443
 
@@ -793,6 +798,9 @@ var init_src = __esm({
793
798
  // src/config.ts
794
799
  var config_exports = {};
795
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,
796
804
  DAEMON_LOG_PATH: () => DAEMON_LOG_PATH,
797
805
  DB_PATH: () => DB_PATH,
798
806
  DRONE_CHANNEL_ENABLED: () => DRONE_CHANNEL_ENABLED,
@@ -805,6 +813,7 @@ __export(config_exports, {
805
813
  DRONE_ISOLATION: () => DRONE_ISOLATION,
806
814
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
807
815
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
816
+ REPO_MAP_PATH: () => REPO_MAP_PATH,
808
817
  REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
809
818
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
810
819
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
@@ -842,9 +851,11 @@ __export(config_exports, {
842
851
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
843
852
  SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
844
853
  SWARM_REPO: () => SWARM_REPO,
854
+ SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
845
855
  SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
846
856
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
847
857
  SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
858
+ SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
848
859
  SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
849
860
  SWARM_STORM_URL: () => SWARM_STORM_URL,
850
861
  SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
@@ -944,7 +955,7 @@ function reviewCutoffAt() {
944
955
  }
945
956
  return at;
946
957
  }
947
- 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;
948
959
  var init_config = __esm({
949
960
  "src/config.ts"() {
950
961
  "use strict";
@@ -960,6 +971,9 @@ var init_config = __esm({
960
971
  WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
961
972
  RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join2(SWARM_HOME, "run-logs");
962
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);
963
977
  DIST_BUILD = true;
964
978
  SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
965
979
  SWARM_OUTBOX = process.env.SWARM_OUTBOX || join2(SWARM_HOME, "outbox.jsonl");
@@ -994,6 +1008,9 @@ var init_config = __esm({
994
1008
  SWARM_REPO = process.env.SWARM_REPO || "";
995
1009
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
996
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);
997
1014
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
998
1015
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
999
1016
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -1486,7 +1503,7 @@ var init_procList = __esm({
1486
1503
 
1487
1504
  // src/procCleanup.ts
1488
1505
  function sleep(ms) {
1489
- return new Promise((resolve3) => setTimeout(resolve3, ms));
1506
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1490
1507
  }
1491
1508
  function parseLsofCwd(output) {
1492
1509
  const rows = [];
@@ -1704,16 +1721,16 @@ var init_procCleanup = __esm({
1704
1721
  });
1705
1722
 
1706
1723
  // src/git.ts
1707
- 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";
1708
1725
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
1709
- import { isAbsolute, join as join11 } from "path";
1726
+ import { isAbsolute as isAbsolute2, join as join12 } from "path";
1710
1727
  import { $ as $2 } from "execa";
1711
1728
  async function reclaimStaleWorktree(repo, worktree) {
1712
1729
  await detachWorktree(repo, worktree);
1713
1730
  }
1714
1731
  function isLinkedWorktree(dir) {
1715
1732
  try {
1716
- return statSync5(join11(dir, ".git")).isFile();
1733
+ return statSync6(join12(dir, ".git")).isFile();
1717
1734
  } catch {
1718
1735
  return false;
1719
1736
  }
@@ -1725,7 +1742,7 @@ async function detachWorktree(repo, worktree) {
1725
1742
  }
1726
1743
  }
1727
1744
  async function ensureWorktreeDirGone(worktree) {
1728
- if (!existsSync7(worktree)) return false;
1745
+ if (!existsSync8(worktree)) return false;
1729
1746
  if (!isLinkedWorktree(worktree)) {
1730
1747
  console.error(
1731
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.`
@@ -1740,7 +1757,7 @@ async function ensureWorktreeDirGone(worktree) {
1740
1757
  );
1741
1758
  return true;
1742
1759
  }
1743
- if (existsSync7(worktree)) {
1760
+ if (existsSync8(worktree)) {
1744
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.`);
1745
1762
  }
1746
1763
  return true;
@@ -1783,7 +1800,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
1783
1800
  }
1784
1801
  const busyAt = await worktreeForBranch(repo, branch);
1785
1802
  if (busyAt) {
1786
- const exists = existsSync7(busyAt);
1803
+ const exists = existsSync8(busyAt);
1787
1804
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
1788
1805
  if (live) throw new WorktreeBusyError(branch, busyAt);
1789
1806
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -1819,9 +1836,9 @@ async function ensureGitExcluded(worktree, pattern) {
1819
1836
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
1820
1837
  const raw = out.stdout.trim();
1821
1838
  if (out.exitCode !== 0 || !raw) return false;
1822
- const commonDir = isAbsolute(raw) ? raw : join11(worktree, raw);
1823
- const infoDir = join11(commonDir, "info");
1824
- 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");
1825
1842
  let current = "";
1826
1843
  try {
1827
1844
  current = await readFile(excludeFile, "utf8");
@@ -1854,7 +1871,7 @@ async function worktreeDirty(path) {
1854
1871
  async function repairMovedWorktrees(targetDir) {
1855
1872
  const subdirs = (dir) => {
1856
1873
  try {
1857
- 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));
1858
1875
  } catch {
1859
1876
  return [];
1860
1877
  }
@@ -1966,18 +1983,18 @@ __export(droneChannel_exports, {
1966
1983
  verifyLocalCliToken: () => verifyLocalCliToken
1967
1984
  });
1968
1985
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1969
- 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";
1970
1987
  import { dirname as dirname3 } from "path";
1971
1988
  import { serve } from "@hono/node-server";
1972
1989
  function loadChannelSecret(path) {
1973
1990
  try {
1974
- const s = readFileSync12(path, "utf8").trim();
1991
+ const s = readFileSync13(path, "utf8").trim();
1975
1992
  if (s.length >= 32) return s;
1976
1993
  } catch {
1977
1994
  }
1978
1995
  const secret = randomBytes(32).toString("hex");
1979
1996
  mkdirSync5(dirname3(path), { recursive: true });
1980
- writeFileSync8(path, secret, { mode: 384 });
1997
+ writeFileSync9(path, secret, { mode: 384 });
1981
1998
  try {
1982
1999
  chmodSync(path, 384);
1983
2000
  } catch {
@@ -2184,8 +2201,8 @@ function startDroneChannel(deps, port) {
2184
2201
  return { port, server };
2185
2202
  }
2186
2203
  function closeDroneChannel(server) {
2187
- return new Promise((resolve3) => {
2188
- server.close(() => resolve3());
2204
+ return new Promise((resolve4) => {
2205
+ server.close(() => resolve4());
2189
2206
  server.closeAllConnections?.();
2190
2207
  });
2191
2208
  }
@@ -2307,7 +2324,7 @@ var init_droneChannel = __esm({
2307
2324
 
2308
2325
  // src/attachments.ts
2309
2326
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
2310
- import { join as join13 } from "path";
2327
+ import { join as join14 } from "path";
2311
2328
  function attachmentsFromDoc(doc, origin) {
2312
2329
  const out = [];
2313
2330
  const walk2 = (node) => {
@@ -2398,7 +2415,7 @@ async function materializeAttachments(opts) {
2398
2415
  };
2399
2416
  const result = { files: [], skipped: [] };
2400
2417
  if (opts.refs.length === 0) return result;
2401
- const dir = join13(opts.worktree, ATTACHMENTS_DIR);
2418
+ const dir = join14(opts.worktree, ATTACHMENTS_DIR);
2402
2419
  await io.removeDir(dir);
2403
2420
  let dirReady = false;
2404
2421
  let total = 0;
@@ -2444,7 +2461,7 @@ async function materializeAttachments(opts) {
2444
2461
  await io.mkdir(dir);
2445
2462
  dirReady = true;
2446
2463
  }
2447
- await io.writeFile(join13(dir, name), file.bytes);
2464
+ await io.writeFile(join14(dir, name), file.bytes);
2448
2465
  } catch (e) {
2449
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)})`);
2450
2467
  continue;
@@ -2597,7 +2614,7 @@ var init_attachments = __esm({
2597
2614
 
2598
2615
  // src/shotUpload.ts
2599
2616
  import { readFile as fsReadFile } from "fs/promises";
2600
- 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";
2601
2618
  function shotMimetype(path) {
2602
2619
  const dot = path.lastIndexOf(".");
2603
2620
  if (dot < 0) return null;
@@ -2623,7 +2640,7 @@ async function readShots(paths, opts) {
2623
2640
  }
2624
2641
  let bytes;
2625
2642
  try {
2626
- bytes = await read(isAbsolute2(path) ? path : join14(opts.worktree, path));
2643
+ bytes = await read(isAbsolute3(path) ? path : join15(opts.worktree, path));
2627
2644
  } catch (e) {
2628
2645
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
2629
2646
  continue;
@@ -2701,8 +2718,8 @@ var init_shotUpload = __esm({
2701
2718
 
2702
2719
  // src/oauth.ts
2703
2720
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2704
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
2705
- 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";
2706
2723
  function makePkce() {
2707
2724
  const verifier = randomBytes2(48).toString("base64url");
2708
2725
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -2786,23 +2803,23 @@ function toStore(clientId, t, now) {
2786
2803
  }
2787
2804
  function loadStore() {
2788
2805
  try {
2789
- return JSON.parse(readFileSync13(STORE_PATH, "utf8"));
2806
+ return JSON.parse(readFileSync14(STORE_PATH, "utf8"));
2790
2807
  } catch {
2791
2808
  return null;
2792
2809
  }
2793
2810
  }
2794
2811
  function saveStore(s) {
2795
- writeFileSync9(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
2812
+ writeFileSync10(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
2796
2813
  }
2797
2814
  function clearStore() {
2798
2815
  try {
2799
- unlinkSync2(STORE_PATH);
2816
+ unlinkSync3(STORE_PATH);
2800
2817
  } catch {
2801
2818
  }
2802
2819
  }
2803
2820
  function loadClientId() {
2804
2821
  try {
2805
- return JSON.parse(readFileSync13(CLIENT_PATH, "utf8")).clientId;
2822
+ return JSON.parse(readFileSync14(CLIENT_PATH, "utf8")).clientId;
2806
2823
  } catch {
2807
2824
  return null;
2808
2825
  }
@@ -2811,7 +2828,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
2811
2828
  const existing = loadClientId();
2812
2829
  if (existing) return existing;
2813
2830
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
2814
- writeFileSync9(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
2831
+ writeFileSync10(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
2815
2832
  return clientId;
2816
2833
  }
2817
2834
  async function ensureAccess(store, opts = {}) {
@@ -2837,18 +2854,18 @@ var init_oauth = __esm({
2837
2854
  AUTHORIZE = "/api/oauth/authorize";
2838
2855
  TOKEN = "/api/oauth/token";
2839
2856
  SCOPE = "openid profile email taskless:full";
2840
- STORE_PATH = join15(SWARM_HOME, "oauth.json");
2841
- 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");
2842
2859
  base = (api) => api.replace(/\/$/, "");
2843
2860
  }
2844
2861
  });
2845
2862
 
2846
2863
  // src/agentToken.ts
2847
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2848
- 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";
2849
2866
  function loadAgentToken() {
2850
2867
  try {
2851
- const s = JSON.parse(readFileSync14(STORE_PATH2, "utf8"));
2868
+ const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
2852
2869
  return s.token ? s : null;
2853
2870
  } catch {
2854
2871
  return null;
@@ -2885,17 +2902,17 @@ var init_agentToken = __esm({
2885
2902
  "src/agentToken.ts"() {
2886
2903
  "use strict";
2887
2904
  init_config();
2888
- STORE_PATH2 = join16(SWARM_HOME, "agent.json");
2905
+ STORE_PATH2 = join17(SWARM_HOME, "agent.json");
2889
2906
  base2 = (api) => api.replace(/\/$/, "");
2890
2907
  }
2891
2908
  });
2892
2909
 
2893
2910
  // src/workspace.ts
2894
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
2895
- 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";
2896
2913
  function loadWorkspace() {
2897
2914
  try {
2898
- const w = JSON.parse(readFileSync15(WORKSPACE_PATH, "utf8"));
2915
+ const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
2899
2916
  return w && typeof w.id === "string" && w.id ? w : null;
2900
2917
  } catch {
2901
2918
  return null;
@@ -2909,7 +2926,7 @@ var init_workspace = __esm({
2909
2926
  "src/workspace.ts"() {
2910
2927
  "use strict";
2911
2928
  init_config();
2912
- WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
2929
+ WORKSPACE_PATH = join18(SWARM_HOME, "workspace.json");
2913
2930
  }
2914
2931
  });
2915
2932
 
@@ -3336,8 +3353,8 @@ __export(db_exports, {
3336
3353
  SwarmDb: () => SwarmDb
3337
3354
  });
3338
3355
  import { createRequire as createRequire2 } from "module";
3339
- import { existsSync as existsSync9 } from "fs";
3340
- import { resolve as resolve2 } from "path";
3356
+ import { existsSync as existsSync10 } from "fs";
3357
+ import { resolve as resolve3 } from "path";
3341
3358
  function rowToQuestion(row) {
3342
3359
  return {
3343
3360
  id: row.id,
@@ -3817,11 +3834,11 @@ CREATE TABLE IF NOT EXISTS imports (
3817
3834
  * транзакции: прервётся — маркер не встанет, повтор безопасен.
3818
3835
  */
3819
3836
  importDb(sourcePath, repo) {
3820
- const abs = resolve2(sourcePath);
3837
+ const abs = resolve3(sourcePath);
3821
3838
  const repoN = normalizeRepo(repo);
3822
3839
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
3823
3840
  if (already) return { imported: false, runs: already.run_count };
3824
- 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}`);
3825
3842
  const src = new this.databaseCtor(abs);
3826
3843
  try {
3827
3844
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -4189,9 +4206,9 @@ var init_droneStore = __esm({
4189
4206
  });
4190
4207
 
4191
4208
  // src/overmind.ts
4192
- import { homedir as homedir3 } from "os";
4193
- import { existsSync as existsSync13, readFileSync as readFileSync18, readdirSync as readdirSync10, renameSync as renameSync8 } from "fs";
4194
- import { basename as basename4, dirname as dirname5, join as join23 } 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";
4195
4212
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4196
4213
  import { spawn as spawn3 } from "child_process";
4197
4214
 
@@ -4601,6 +4618,18 @@ var SwarmClient = class _SwarmClient {
4601
4618
  runKeyFor(localId) {
4602
4619
  return this.keyByLocal.get(localId);
4603
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
+ }
4604
4633
  // cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
4605
4634
  // Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
4606
4635
  // прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
@@ -5820,8 +5849,9 @@ var SwarmClient = class _SwarmClient {
5820
5849
  this.runs.set(id, local);
5821
5850
  this.keyByLocal.set(id, runKey);
5822
5851
  }
5823
- const slug = pr.taskSlug ?? pr.task?.slug ?? pr.taskId ?? local.task_slug;
5824
- 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;
5825
5855
  local.task_title = pr.taskTitle ?? local.task_title;
5826
5856
  local.task_desc = pr.taskDesc ?? local.task_desc;
5827
5857
  local.name = pr.droneName ?? local.name;
@@ -6140,6 +6170,66 @@ function parseDecision(v) {
6140
6170
  init_config();
6141
6171
  init_heartbeat();
6142
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
+
6143
6233
  // src/repoRegistry.ts
6144
6234
  init_config();
6145
6235
  import { $ } from "execa";
@@ -6209,8 +6299,15 @@ function pickRepoPaths(args) {
6209
6299
  return { paths: [], source: "none" };
6210
6300
  }
6211
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
+
6212
6309
  // src/repoScan.ts
6213
- 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";
6214
6311
  import { join as join5 } from "path";
6215
6312
  var SKIP = /* @__PURE__ */ new Set([
6216
6313
  "node_modules",
@@ -6246,7 +6343,11 @@ function originOf(dir) {
6246
6343
  return null;
6247
6344
  }
6248
6345
  }
6249
- 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 = {}) {
6250
6351
  const maxDepth = opts.maxDepth ?? 3;
6251
6352
  const found = /* @__PURE__ */ new Map();
6252
6353
  for (const root of roots) walk(root, 0, maxDepth, found);
@@ -6257,7 +6358,11 @@ function walk(dir, depth, maxDepth, found) {
6257
6358
  const origin = originOf(dir);
6258
6359
  if (origin) {
6259
6360
  const full = parseRepoFullName(origin);
6260
- 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
+ }
6261
6366
  return;
6262
6367
  }
6263
6368
  let entries;
@@ -6270,7 +6375,7 @@ function walk(dir, depth, maxDepth, found) {
6270
6375
  if (name.startsWith(".") || SKIP.has(name)) continue;
6271
6376
  const child = join5(dir, name);
6272
6377
  try {
6273
- if (!statSync2(child).isDirectory()) continue;
6378
+ if (!statSync3(child).isDirectory()) continue;
6274
6379
  } catch {
6275
6380
  continue;
6276
6381
  }
@@ -6295,20 +6400,173 @@ function defaultScanRoots(home, knownPaths = []) {
6295
6400
  return out;
6296
6401
  }
6297
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
+
6298
6556
  // src/hostIdentity.ts
6299
6557
  import { hostname } from "os";
6300
- import { existsSync as existsSync4, readFileSync as readFileSync8, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
6301
- 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";
6302
6560
  import { randomUUID as randomUUID2 } from "crypto";
6303
6561
  function hostIdentityPath(home) {
6304
- return join6(home, "host.json");
6562
+ return join7(home, "host.json");
6305
6563
  }
6306
6564
  function loadHostIdentity(deps) {
6307
- const exists = deps.exists ?? existsSync4;
6308
- const readFile2 = deps.readFile ?? ((p) => readFileSync8(p, "utf8"));
6565
+ const exists = deps.exists ?? existsSync5;
6566
+ const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
6309
6567
  const writeFile2 = deps.writeFile ?? ((p, data) => {
6310
- writeFileSync6(`${p}.tmp`, data);
6311
- renameSync4(`${p}.tmp`, p);
6568
+ writeFileSync7(`${p}.tmp`, data);
6569
+ renameSync6(`${p}.tmp`, p);
6312
6570
  });
6313
6571
  const host = deps.hostname ?? hostname;
6314
6572
  const newId = deps.newId ?? randomUUID2;
@@ -6490,14 +6748,14 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
6490
6748
 
6491
6749
  // src/launcher.ts
6492
6750
  import { spawn } from "child_process";
6493
- 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";
6494
6752
  import { createRequire } from "module";
6495
- import { homedir as homedir2 } from "os";
6496
- 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";
6497
6755
  import { fileURLToPath } from "url";
6498
6756
 
6499
6757
  // src/claudeBin.ts
6500
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
6758
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
6501
6759
  import { win32 as winPath } from "path";
6502
6760
  var CLAUDE_FALLBACK = "claude";
6503
6761
  function compareVersionDirs(a, b) {
@@ -6534,7 +6792,7 @@ function winVersionRoots(env, listDir) {
6534
6792
  function findWindowsClaude(opts = {}) {
6535
6793
  const env = opts.env ?? process.env;
6536
6794
  const listDir = opts.listDir ?? realListDir;
6537
- const fileExists = opts.fileExists ?? existsSync5;
6795
+ const fileExists = opts.fileExists ?? existsSync6;
6538
6796
  let best = null;
6539
6797
  for (const root of winVersionRoots(env, listDir)) {
6540
6798
  for (const version of listDir(root)) {
@@ -6569,8 +6827,8 @@ function resolveClaudeBin(opts = {}) {
6569
6827
  }
6570
6828
 
6571
6829
  // src/droneHome.ts
6572
- import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync4, rmSync, statSync as statSync3, symlinkSync } from "fs";
6573
- 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";
6574
6832
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
6575
6833
  function droneHomeDeny(env = process.env) {
6576
6834
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -6594,23 +6852,23 @@ function buildDroneHome(opts) {
6594
6852
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
6595
6853
  for (const existing of readdirSync4(droneHome)) {
6596
6854
  if (wanted.has(existing)) continue;
6597
- const p = join7(droneHome, existing);
6855
+ const p = join8(droneHome, existing);
6598
6856
  try {
6599
6857
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
6600
6858
  } catch {
6601
6859
  }
6602
6860
  }
6603
6861
  for (const name of wanted) {
6604
- const link = join7(droneHome, name);
6862
+ const link = join8(droneHome, name);
6605
6863
  try {
6606
6864
  lstatSync(link);
6607
6865
  continue;
6608
6866
  } catch {
6609
6867
  }
6610
- const target = join7(realHome, name);
6868
+ const target = join8(realHome, name);
6611
6869
  let isDir;
6612
6870
  try {
6613
- isDir = statSync3(target).isDirectory();
6871
+ isDir = statSync4(target).isDirectory();
6614
6872
  } catch {
6615
6873
  continue;
6616
6874
  }
@@ -6628,7 +6886,7 @@ var REAL_PATH_ENV_PINS = [
6628
6886
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
6629
6887
  ];
6630
6888
  function realPathEnvPins(realHome) {
6631
- 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)]));
6632
6890
  }
6633
6891
  function droneDenyRules(realHome, swarmHome) {
6634
6892
  const paths = [
@@ -6720,7 +6978,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
6720
6978
  const env = { ...base3 };
6721
6979
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
6722
6980
  if (isolate && !base3.TURBO_CACHE_DIR) {
6723
- env.TURBO_CACHE_DIR = join8(worktree, ".turbo", "cache");
6981
+ env.TURBO_CACHE_DIR = join9(worktree, ".turbo", "cache");
6724
6982
  }
6725
6983
  Object.assign(env, droneTestEnv(droneCap, base3));
6726
6984
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -6748,25 +7006,25 @@ var DAEMON_STATE_ENV = [
6748
7006
  "TASKLESS_TOKEN"
6749
7007
  ];
6750
7008
  function bundledRoleDir(kind) {
6751
- return join8(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
7009
+ return join9(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
6752
7010
  }
6753
7011
  function roleDir(worktree, kind) {
6754
- return join8(worktree, ".claude", "roles", kind);
7012
+ return join9(worktree, ".claude", "roles", kind);
6755
7013
  }
6756
7014
  function roleDirs(worktree, kind) {
6757
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync6(dir));
7015
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
6758
7016
  }
6759
7017
  function transcriptDir(worktree, projectsDir) {
6760
- return join8(projectsDir, worktree.replace(/[/.]/g, "-"));
7018
+ return join9(projectsDir, worktree.replace(/[/.]/g, "-"));
6761
7019
  }
6762
- function sessionTranscriptExists(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
7020
+ function sessionTranscriptExists(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
6763
7021
  try {
6764
7022
  return readdirSync5(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
6765
7023
  } catch {
6766
7024
  return false;
6767
7025
  }
6768
7026
  }
6769
- function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
7027
+ function transcriptMtimeMs(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
6770
7028
  const dir = transcriptDir(worktree, projectsDir);
6771
7029
  let files;
6772
7030
  try {
@@ -6778,7 +7036,7 @@ function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude",
6778
7036
  for (const f of files) {
6779
7037
  if (!f.endsWith(".jsonl")) continue;
6780
7038
  try {
6781
- const mtime2 = statSync4(join8(dir, f)).mtimeMs;
7039
+ const mtime2 = statSync5(join9(dir, f)).mtimeMs;
6782
7040
  if (latest === null || mtime2 > latest) latest = mtime2;
6783
7041
  } catch {
6784
7042
  }
@@ -6786,7 +7044,7 @@ function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude",
6786
7044
  return latest;
6787
7045
  }
6788
7046
  function nextRunLogPath(runLogsDir, runId) {
6789
- const dir = join8(runLogsDir, String(runId));
7047
+ const dir = join9(runLogsDir, String(runId));
6790
7048
  mkdirSync4(dir, { recursive: true });
6791
7049
  let files = [];
6792
7050
  try {
@@ -6798,7 +7056,7 @@ function nextRunLogPath(runLogsDir, runId) {
6798
7056
  const m = /^(\d+)\.json/.exec(f);
6799
7057
  if (m) next = Math.max(next, Number(m[1]) + 1);
6800
7058
  }
6801
- return join8(dir, `${next}.json`);
7059
+ return join9(dir, `${next}.json`);
6802
7060
  }
6803
7061
  function openNextRunLog(runLogsDir, runId) {
6804
7062
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -6872,16 +7130,16 @@ function makeLauncher(cfg) {
6872
7130
  }
6873
7131
  }
6874
7132
  };
6875
- const cfgPath = join8(worktree, ".swarm-mcp.json");
6876
- writeFileSync7(cfgPath, JSON.stringify(mcpConfig, null, 2));
7133
+ const cfgPath = join9(worktree, ".swarm-mcp.json");
7134
+ writeFileSync8(cfgPath, JSON.stringify(mcpConfig, null, 2));
6877
7135
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
6878
7136
  const pluginDirs = roleDirs(worktree, run.kind);
6879
- const realHome = cfg.droneIsolation?.realHome ?? homedir2();
7137
+ const realHome = cfg.droneIsolation?.realHome ?? homedir3();
6880
7138
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
6881
7139
  const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
6882
- 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"))) {
6883
7141
  console.error(
6884
- `[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.`
6885
7143
  );
6886
7144
  }
6887
7145
  let stdio = "ignore";
@@ -6915,8 +7173,8 @@ function makeLauncher(cfg) {
6915
7173
  init_procCleanup();
6916
7174
 
6917
7175
  // src/cost.ts
6918
- import { readFileSync as readFileSync9, readdirSync as readdirSync6, renameSync as renameSync5 } from "fs";
6919
- 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";
6920
7178
 
6921
7179
  // src/pricing.ts
6922
7180
  var MODEL_PRICING = {
@@ -6973,17 +7231,17 @@ function unprocessedLogFiles(dir) {
6973
7231
  }
6974
7232
  }
6975
7233
  function accountRunCost(deps, run) {
6976
- const dir = join9(deps.runLogsDir, String(run.id));
7234
+ const dir = join10(deps.runLogsDir, String(run.id));
6977
7235
  const files = unprocessedLogFiles(dir);
6978
7236
  if (files.length === 0) return;
6979
7237
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
6980
7238
  let added = 0;
6981
7239
  const processed = [];
6982
7240
  for (const f of files) {
6983
- const full = join9(dir, f);
7241
+ const full = join10(dir, f);
6984
7242
  let raw = null;
6985
7243
  try {
6986
- raw = readFileSync9(full, "utf8");
7244
+ raw = readFileSync10(full, "utf8");
6987
7245
  } catch {
6988
7246
  continue;
6989
7247
  }
@@ -7000,7 +7258,7 @@ function accountRunCost(deps, run) {
7000
7258
  if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
7001
7259
  for (const full of processed) {
7002
7260
  try {
7003
- renameSync5(full, `${full}.done`);
7261
+ renameSync7(full, `${full}.done`);
7004
7262
  } catch {
7005
7263
  }
7006
7264
  }
@@ -7010,11 +7268,11 @@ function makeCostAccountant(deps) {
7010
7268
  }
7011
7269
 
7012
7270
  // src/runOutcome.ts
7013
- import { readdirSync as readdirSync7, readFileSync as readFileSync10 } from "fs";
7014
- import { join as join10 } from "path";
7271
+ import { readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
7272
+ import { join as join11 } from "path";
7015
7273
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
7016
7274
  function classifyExit(runLogsDir, runId) {
7017
- const dir = join10(runLogsDir, String(runId));
7275
+ const dir = join11(runLogsDir, String(runId));
7018
7276
  let files;
7019
7277
  try {
7020
7278
  files = readdirSync7(dir);
@@ -7031,7 +7289,7 @@ function classifyExit(runLogsDir, runId) {
7031
7289
  if (!latest) return "unknown";
7032
7290
  let raw;
7033
7291
  try {
7034
- raw = readFileSync10(join10(dir, latest.name), "utf8");
7292
+ raw = readFileSync11(join11(dir, latest.name), "utf8");
7035
7293
  } catch {
7036
7294
  return "unknown";
7037
7295
  }
@@ -7042,11 +7300,12 @@ function makeExitClassifier(runLogsDir) {
7042
7300
  }
7043
7301
 
7044
7302
  // src/orchestrator.ts
7303
+ init_src();
7045
7304
  init_git();
7046
7305
 
7047
7306
  // src/deps.ts
7048
- import { existsSync as existsSync8, readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
7049
- 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";
7050
7309
  import { execa } from "execa";
7051
7310
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
7052
7311
  var MAX_PLAN_COMMANDS = 12;
@@ -7062,7 +7321,7 @@ async function runCommand(cmd, worktree) {
7062
7321
  }
7063
7322
  function safeReadText(p) {
7064
7323
  try {
7065
- return readFileSync11(p, "utf8");
7324
+ return readFileSync12(p, "utf8");
7066
7325
  } catch {
7067
7326
  return null;
7068
7327
  }
@@ -7102,21 +7361,21 @@ function overridePlan(override) {
7102
7361
  };
7103
7362
  }
7104
7363
  function detectPackageManager(worktree, io = {}) {
7105
- const exists = io.exists ?? existsSync8;
7364
+ const exists = io.exists ?? existsSync9;
7106
7365
  const readText = io.readText ?? safeReadText;
7107
- const hasPkgJson = exists(join12(worktree, "package.json"));
7366
+ const hasPkgJson = exists(join13(worktree, "package.json"));
7108
7367
  if (!hasPkgJson) {
7109
- 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";
7110
7369
  return null;
7111
7370
  }
7112
- const pkg = readJson(readText, join12(worktree, "package.json"));
7371
+ const pkg = readJson(readText, join13(worktree, "package.json"));
7113
7372
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
7114
7373
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
7115
- if (exists(join12(worktree, "bun.lock")) || exists(join12(worktree, "bun.lockb"))) return "bun";
7116
- if (exists(join12(worktree, "pnpm-lock.yaml"))) return "pnpm";
7117
- if (exists(join12(worktree, "yarn.lock"))) return "yarn";
7118
- if (exists(join12(worktree, "package-lock.json"))) return "npm";
7119
- 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";
7120
7379
  return "npm";
7121
7380
  }
7122
7381
  function jsInstallCommand(pm) {
@@ -7169,7 +7428,7 @@ var ECOSYSTEM_MARKERS = [
7169
7428
  // конфигурация — дело сборки, не подготовки
7170
7429
  ];
7171
7430
  function detectPlan(worktree, io) {
7172
- const exists = io.exists ?? existsSync8;
7431
+ const exists = io.exists ?? existsSync9;
7173
7432
  const listFiles = io.listFiles ?? safeListFiles;
7174
7433
  const pm = detectPackageManager(worktree, io);
7175
7434
  if (pm) {
@@ -7177,7 +7436,7 @@ function detectPlan(worktree, io) {
7177
7436
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
7178
7437
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
7179
7438
  // заново, но он дешёвый: тянет из того же общего кэша.
7180
- { 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" }
7181
7440
  ];
7182
7441
  for (const target of workspaceBuildTargets(worktree, io)) {
7183
7442
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -7185,12 +7444,12 @@ function detectPlan(worktree, io) {
7185
7444
  return { source: "detect", ecosystem: `node/${pm}`, commands };
7186
7445
  }
7187
7446
  for (const marker of ECOSYSTEM_MARKERS) {
7188
- 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));
7189
7448
  if (!hit) continue;
7190
7449
  let ecosystem = marker.ecosystem;
7191
7450
  let command = marker.command;
7192
7451
  for (const refine of marker.refine ?? []) {
7193
- if (!exists(join12(worktree, refine.file))) continue;
7452
+ if (!exists(join13(worktree, refine.file))) continue;
7194
7453
  ecosystem = refine.ecosystem;
7195
7454
  command = refine.command;
7196
7455
  break;
@@ -7247,12 +7506,12 @@ var CI_INSTALL_PATTERNS = [
7247
7506
  function ciWorkflowPlan(worktree, io) {
7248
7507
  const readText = io.readText ?? safeReadText;
7249
7508
  const listFiles = io.listFiles ?? safeListFiles;
7250
- const dir = join12(worktree, ".github", "workflows");
7509
+ const dir = join13(worktree, ".github", "workflows");
7251
7510
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
7252
7511
  const seen = /* @__PURE__ */ new Set();
7253
7512
  const commands = [];
7254
7513
  for (const name of files.sort()) {
7255
- const yaml = readText(join12(dir, name));
7514
+ const yaml = readText(join13(dir, name));
7256
7515
  if (yaml === null) continue;
7257
7516
  for (const raw of workflowRunLines(yaml)) {
7258
7517
  const line = raw.replace(/\s+/g, " ").trim();
@@ -7267,7 +7526,7 @@ function ciWorkflowPlan(worktree, io) {
7267
7526
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
7268
7527
  }
7269
7528
  function workspaceGlobs(worktree, readText) {
7270
- const yaml = readText(join12(worktree, "pnpm-workspace.yaml"));
7529
+ const yaml = readText(join13(worktree, "pnpm-workspace.yaml"));
7271
7530
  if (yaml !== null) {
7272
7531
  const globs = [];
7273
7532
  let inPackages = false;
@@ -7287,7 +7546,7 @@ function workspaceGlobs(worktree, readText) {
7287
7546
  }
7288
7547
  if (globs.length) return globs;
7289
7548
  }
7290
- const pkg = readJson(readText, join12(worktree, "package.json"));
7549
+ const pkg = readJson(readText, join13(worktree, "package.json"));
7291
7550
  const ws = pkg?.workspaces;
7292
7551
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
7293
7552
  return list.filter((g) => typeof g === "string");
@@ -7298,14 +7557,14 @@ function workspaceDirs(worktree, globs, io) {
7298
7557
  if (glob.startsWith("!")) continue;
7299
7558
  const star = glob.indexOf("*");
7300
7559
  if (star === -1) {
7301
- const dir = join12(worktree, glob);
7302
- 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);
7303
7562
  continue;
7304
7563
  }
7305
- const base3 = join12(worktree, glob.slice(0, star).replace(/\/$/, ""));
7564
+ const base3 = join13(worktree, glob.slice(0, star).replace(/\/$/, ""));
7306
7565
  for (const name of io.listDirs(base3)) {
7307
- const dir = join12(base3, name);
7308
- 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);
7309
7568
  }
7310
7569
  }
7311
7570
  return [...dirs];
@@ -7326,18 +7585,18 @@ function entryPaths(pkg) {
7326
7585
  return out;
7327
7586
  }
7328
7587
  function workspaceBuildTargets(worktree, io = {}) {
7329
- const exists = io.exists ?? existsSync8;
7588
+ const exists = io.exists ?? existsSync9;
7330
7589
  const readText = io.readText ?? safeReadText;
7331
7590
  const listDirs = io.listDirs ?? safeListDirs;
7332
7591
  const globs = workspaceGlobs(worktree, readText);
7333
7592
  if (!globs.length) return [];
7334
7593
  const pkgs = [];
7335
7594
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
7336
- const pkg = readJson(readText, join12(dir, "package.json"));
7595
+ const pkg = readJson(readText, join13(dir, "package.json"));
7337
7596
  if (!pkg || typeof pkg.name !== "string") continue;
7338
7597
  const scripts = pkg.scripts ?? {};
7339
7598
  const entries = entryPaths(pkg);
7340
- 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)));
7341
7600
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
7342
7601
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
7343
7602
  }
@@ -7365,7 +7624,7 @@ function planPrepare(worktree, io = {}) {
7365
7624
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
7366
7625
  }
7367
7626
  async function ensureWorktreeDeps(worktree, io = {}) {
7368
- const exists = io.exists ?? existsSync8;
7627
+ const exists = io.exists ?? existsSync9;
7369
7628
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
7370
7629
  const approve = io.approve ?? (() => true);
7371
7630
  const plan = planPrepare(worktree, io);
@@ -7464,6 +7723,12 @@ var AreaBusyError = class extends Error {
7464
7723
  }
7465
7724
  area;
7466
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
+ }
7467
7732
  async function spawnDrone(deps, run) {
7468
7733
  const repo = normalizeRepo(run.repo || deps.repo || "");
7469
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`);
@@ -8910,10 +9175,31 @@ function makeExclusiveReader(opts) {
8910
9175
  }
8911
9176
 
8912
9177
  // src/envGate.ts
8913
- import { existsSync as existsSync10 } from "fs";
8914
- import { join as join18, delimiter } from "path";
9178
+ import { existsSync as existsSync12 } from "fs";
9179
+ import { join as join20 } from "path";
8915
9180
  import { createConnection } from "net";
8916
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
8917
9203
  var PROBE_TIMEOUT_MS = 2500;
8918
9204
  var PORT_TIMEOUT_MS = 1e3;
8919
9205
  function parseRequirements(lines) {
@@ -8962,8 +9248,8 @@ var COMPOSE_MARKERS = [
8962
9248
  "compose.yaml"
8963
9249
  ];
8964
9250
  function detectRequirements(repo, io = {}) {
8965
- const exists = io.exists ?? existsSync10;
8966
- 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)));
8967
9253
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
8968
9254
  }
8969
9255
  function resolveRequirements(repo, declared, io = {}) {
@@ -8982,17 +9268,14 @@ async function dockerAlive() {
8982
9268
  }
8983
9269
  }
8984
9270
  async function hasCommand(name, env) {
8985
- if (name.includes("/")) return existsSync10(name);
8986
- const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
8987
- const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
8988
- return dirs.some((dir) => exts.some((ext) => existsSync10(join18(dir, name + ext))));
9271
+ return hasCommandInPath(name, env);
8989
9272
  }
8990
9273
  async function portOpen(host, port) {
8991
- return new Promise((resolve3) => {
9274
+ return new Promise((resolve4) => {
8992
9275
  const socket = createConnection({ host, port });
8993
9276
  const finish = (ok) => {
8994
9277
  socket.destroy();
8995
- resolve3(ok);
9278
+ resolve4(ok);
8996
9279
  };
8997
9280
  socket.setTimeout(PORT_TIMEOUT_MS);
8998
9281
  socket.once("connect", () => finish(true));
@@ -9102,8 +9385,8 @@ import { basename as basename3 } from "path";
9102
9385
  import { serve as serve2 } from "@hono/node-server";
9103
9386
 
9104
9387
  // src/diff.ts
9105
- import { statSync as statSync6 } from "fs";
9106
- import { join as join19 } from "path";
9388
+ import { statSync as statSync7 } from "fs";
9389
+ import { join as join21 } from "path";
9107
9390
  import { $ as $4 } from "execa";
9108
9391
  var MAX_FILES = 200;
9109
9392
  var MAX_LINES_PER_FILE = 1500;
@@ -9246,7 +9529,7 @@ async function resolveBase(wt, baseBranch) {
9246
9529
  }
9247
9530
  function mtime(path) {
9248
9531
  try {
9249
- return statSync6(path).mtimeMs;
9532
+ return statSync7(path).mtimeMs;
9250
9533
  } catch {
9251
9534
  return 0;
9252
9535
  }
@@ -9271,7 +9554,7 @@ async function collectRunDiff(worktree, opts = {}) {
9271
9554
  files.push(pf);
9272
9555
  }
9273
9556
  }
9274
- 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)));
9275
9558
  const truncatedFiles = files.length > MAX_FILES;
9276
9559
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9277
9560
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -9290,7 +9573,7 @@ async function collectRunDiff(worktree, opts = {}) {
9290
9573
  // package.json
9291
9574
  var package_default = {
9292
9575
  name: "@taskless-app/swarm",
9293
- version: "0.4.10",
9576
+ version: "0.5.0",
9294
9577
  type: "module",
9295
9578
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9296
9579
  keywords: [
@@ -9342,13 +9625,16 @@ var package_default = {
9342
9625
  }
9343
9626
  };
9344
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
+
9345
9632
  // src/web.ts
9346
9633
  init_storm();
9347
9634
  init_resources();
9348
9635
  init_config();
9349
9636
  init_droneChannel();
9350
9637
  init_oauth();
9351
- var PACKAGE_VERSION = package_default.version;
9352
9638
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
9353
9639
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
9354
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>`;
@@ -10656,7 +10942,8 @@ function daemonStatus(deps) {
10656
10942
  restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
10657
10943
  counts,
10658
10944
  live,
10659
- version: PACKAGE_VERSION
10945
+ version: PACKAGE_VERSION,
10946
+ repos: deps.repoMap?.() ?? []
10660
10947
  };
10661
10948
  }
10662
10949
  function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
@@ -10904,8 +11191,8 @@ function startWeb(deps, port = WEB_PORT) {
10904
11191
  return { port, server };
10905
11192
  }
10906
11193
  function closeWebServer(server) {
10907
- return new Promise((resolve3) => {
10908
- server.close(() => resolve3());
11194
+ return new Promise((resolve4) => {
11195
+ server.close(() => resolve4());
10909
11196
  server.closeAllConnections?.();
10910
11197
  });
10911
11198
  }
@@ -10949,24 +11236,56 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
10949
11236
  }
10950
11237
  }
10951
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
+
10952
11271
  // src/hostCommands.ts
10953
11272
  init_src();
10954
- import { readFileSync as readFileSync16, renameSync as renameSync6, writeFileSync as writeFileSync12 } from "fs";
10955
- import { join as join20 } from "path";
11273
+ import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
11274
+ import { join as join22 } from "path";
10956
11275
  function hostCommandStatePath(home) {
10957
- return join20(home, "host-command.json");
11276
+ return join22(home, "host-command.json");
10958
11277
  }
10959
11278
  function readState(path) {
10960
11279
  try {
10961
- return JSON.parse(readFileSync16(path, "utf8"));
11280
+ return JSON.parse(readFileSync18(path, "utf8"));
10962
11281
  } catch {
10963
11282
  return {};
10964
11283
  }
10965
11284
  }
10966
11285
  function writeState(path, state) {
10967
11286
  try {
10968
- writeFileSync12(`${path}.tmp`, JSON.stringify(state));
10969
- renameSync6(`${path}.tmp`, path);
11287
+ writeFileSync13(`${path}.tmp`, JSON.stringify(state));
11288
+ renameSync8(`${path}.tmp`, path);
10970
11289
  } catch {
10971
11290
  }
10972
11291
  }
@@ -11101,8 +11420,8 @@ async function doRestart(cmd, opts, note) {
11101
11420
 
11102
11421
  // src/selfUpdate.ts
11103
11422
  init_git();
11104
- import { existsSync as existsSync11 } from "fs";
11105
- import { join as join21 } from "path";
11423
+ import { existsSync as existsSync13 } from "fs";
11424
+ import { join as join23 } from "path";
11106
11425
  import { $ as $6 } from "execa";
11107
11426
  var DAEMON_PACKAGE = "@taskless-app/swarm";
11108
11427
  var defaultRun = async (cmd, args, cwd) => {
@@ -11115,7 +11434,7 @@ function tail(text, max = 400) {
11115
11434
  }
11116
11435
  async function runSelfUpdate(io) {
11117
11436
  const run = io.run ?? defaultRun;
11118
- const exists = io.exists ?? existsSync11;
11437
+ const exists = io.exists ?? existsSync13;
11119
11438
  const repo = io.repo;
11120
11439
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
11121
11440
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -11144,7 +11463,7 @@ async function runSelfUpdate(io) {
11144
11463
  };
11145
11464
  }
11146
11465
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
11147
- if (!exists(join21(repo, "pnpm-lock.yaml"))) {
11466
+ if (!exists(join23(repo, "pnpm-lock.yaml"))) {
11148
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" };
11149
11468
  }
11150
11469
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -11184,7 +11503,7 @@ var RESET_RE = /resets[^\n]*/i;
11184
11503
  function anthropicHealthy(opts = {}) {
11185
11504
  const bin = opts.claudeBin ?? resolveClaudeBin();
11186
11505
  const timeoutMs = opts.timeoutMs ?? 2e4;
11187
- return new Promise((resolve3) => {
11506
+ return new Promise((resolve4) => {
11188
11507
  let done = false;
11189
11508
  let out = "";
11190
11509
  const finish = (r) => {
@@ -11195,7 +11514,7 @@ function anthropicHealthy(opts = {}) {
11195
11514
  child.kill("SIGKILL");
11196
11515
  } catch {
11197
11516
  }
11198
- resolve3(r);
11517
+ resolve4(r);
11199
11518
  };
11200
11519
  const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: ["ignore", "pipe", "pipe"] });
11201
11520
  child.stdout?.on("data", (d) => {
@@ -11234,16 +11553,16 @@ async function fetchExternalStorm(url, opts = {}) {
11234
11553
  }
11235
11554
 
11236
11555
  // src/anomalyState.ts
11237
- import { existsSync as existsSync12, readFileSync as readFileSync17, renameSync as renameSync7, writeFileSync as writeFileSync13 } from "fs";
11238
- import { join as join22 } 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";
11239
11558
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
11240
11559
  var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
11241
11560
  function anomalyStatePath(home) {
11242
- return join22(home, "anomaly-state.json");
11561
+ return join24(home, "anomaly-state.json");
11243
11562
  }
11244
11563
  function loadAnomalyState(home, now = Date.now(), io = {}) {
11245
- const exists = io.exists ?? existsSync12;
11246
- const readFile2 = io.readFile ?? ((p) => readFileSync17(p, "utf8"));
11564
+ const exists = io.exists ?? existsSync14;
11565
+ const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
11247
11566
  const path = anomalyStatePath(home);
11248
11567
  if (!exists(path)) return { ...EMPTY };
11249
11568
  try {
@@ -11269,8 +11588,8 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
11269
11588
  function saveAnomalyState(home, state, io = {}) {
11270
11589
  const path = anomalyStatePath(home);
11271
11590
  const writeFile2 = io.writeFile ?? ((p, data) => {
11272
- writeFileSync13(`${p}.tmp`, data);
11273
- renameSync7(`${p}.tmp`, p);
11591
+ writeFileSync14(`${p}.tmp`, data);
11592
+ renameSync9(`${p}.tmp`, p);
11274
11593
  });
11275
11594
  writeFile2(path, JSON.stringify(state, null, 2));
11276
11595
  }
@@ -11912,10 +12231,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
11912
12231
  }
11913
12232
  return parts.join("\n");
11914
12233
  }
11915
- var HERO_CHARTER_PATH = join23(dirname5(THIS_FILE), "..", "docs", "HERO.md");
12234
+ var HERO_CHARTER_PATH = join25(dirname5(THIS_FILE), "..", "docs", "HERO.md");
11916
12235
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
11917
12236
  const name = run.name || `#${run.id}`;
11918
- const charter = readFileSync18(HERO_CHARTER_PATH, "utf8");
12237
+ const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
11919
12238
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
11920
12239
 
11921
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).`;
@@ -12166,7 +12485,7 @@ async function wakeRun(deps, runId) {
12166
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` };
12167
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` };
12168
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` };
12169
- const worktreeExists = deps.worktreeExists ?? existsSync13;
12488
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12170
12489
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12171
12490
  const wt = run.worktree_path;
12172
12491
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -12251,7 +12570,7 @@ function repoHasCi(deps, run) {
12251
12570
  function repoHasCiWorkflows(worktreePath, repo) {
12252
12571
  for (const base3 of [worktreePath, repo]) {
12253
12572
  if (!base3) continue;
12254
- const dir = join23(base3, ".github", "workflows");
12573
+ const dir = join25(base3, ".github", "workflows");
12255
12574
  try {
12256
12575
  if (readdirSync10(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
12257
12576
  } catch {
@@ -12400,7 +12719,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
12400
12719
  return true;
12401
12720
  }
12402
12721
  function worktreeAndTranscriptAlive(deps, worktree) {
12403
- const exists = deps.worktreeExists ?? existsSync13;
12722
+ const exists = deps.worktreeExists ?? existsSync15;
12404
12723
  const transcript = deps.hasTranscript ?? sessionTranscriptExists;
12405
12724
  return exists(worktree) && transcript(worktree);
12406
12725
  }
@@ -12543,6 +12862,15 @@ async function exclusiveGateAllows(deps, run) {
12543
12862
  }
12544
12863
  return true;
12545
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
+ }
12546
12874
  async function exclusivityBlockedRunIds(deps) {
12547
12875
  const blocked = /* @__PURE__ */ new Set();
12548
12876
  if (!deps.exclusiveProject) return blocked;
@@ -12605,7 +12933,7 @@ async function resumeWatchdogParked(deps, droneCap) {
12605
12933
  }
12606
12934
  if (!candidate) return;
12607
12935
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
12608
- const worktreeExists = deps.worktreeExists ?? existsSync13;
12936
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12609
12937
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12610
12938
  const wt = candidate.worktree_path;
12611
12939
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -12774,7 +13102,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
12774
13102
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
12775
13103
  if (!deps.respawn) return;
12776
13104
  const wt = resumeCandidate.worktree_path;
12777
- const worktreeExists = deps.worktreeExists ?? existsSync13;
13105
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12778
13106
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12779
13107
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12780
13108
  const mode = canContinue ? "continue" : "fresh";
@@ -12936,7 +13264,7 @@ function buildEscalationCandidates(db) {
12936
13264
  }
12937
13265
  async function tickOnce(deps) {
12938
13266
  const now = deps.now ? deps.now() : Date.now();
12939
- const worktreeExists = deps.worktreeExists ?? existsSync13;
13267
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
12940
13268
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
12941
13269
  if (deps.rehydrate) {
12942
13270
  const rh = deps.rehydrate;
@@ -12988,6 +13316,18 @@ async function tickOnce(deps) {
12988
13316
  await dv.run();
12989
13317
  }
12990
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
+ }
12991
13331
  if (deps.rehydrate && deps.kill) {
12992
13332
  const kill = deps.kill;
12993
13333
  for (const run of deps.db.listRuns()) {
@@ -13013,6 +13353,19 @@ async function tickOnce(deps) {
13013
13353
  }
13014
13354
  }
13015
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);
13016
13369
  let stormActive = false;
13017
13370
  if (deps.storm) {
13018
13371
  for (const run of deps.db.listRuns("running")) {
@@ -13542,7 +13895,7 @@ async function resolveConflict(deps, reviewRun) {
13542
13895
  workspace_id: reviewRun.workspace_id
13543
13896
  // та же задача, тот же воркспейс (TASWM-288)
13544
13897
  });
13545
- const wt = join23(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13898
+ const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13546
13899
  try {
13547
13900
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13548
13901
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -13594,7 +13947,7 @@ async function spawnReviewer(deps, reviewRun, files) {
13594
13947
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
13595
13948
  workspace_id: reviewRun.workspace_id
13596
13949
  });
13597
- const wt = join23(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13950
+ const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13598
13951
  try {
13599
13952
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13600
13953
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -13629,7 +13982,7 @@ async function respawnRun(deps, run, mode, nudge) {
13629
13982
  ]);
13630
13983
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13631
13984
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
13632
- if (!existsSync13(wt)) {
13985
+ if (!existsSync15(wt)) {
13633
13986
  const branch = runToSpawn.branch || runToSpawn.task_slug;
13634
13987
  const source = await addWorktree(repo, wt, branch, {
13635
13988
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -13654,11 +14007,11 @@ async function respawnRun(deps, run, mode, nudge) {
13654
14007
  const prompt = notice ? `${freshPrompt}
13655
14008
 
13656
14009
  ${notice}` : freshPrompt;
13657
- const pid = deps.launch({ worktree: wt, run, prompt });
14010
+ const pid = deps.launch({ worktree: wt, run: runToSpawn, prompt });
13658
14011
  deps.db.updateRun(run.id, { status: "running", pid });
13659
14012
  }
13660
14013
  async function syncCrashedOnStartup(deps) {
13661
- const worktreeExists = deps.worktreeExists ?? existsSync13;
14014
+ const worktreeExists = deps.worktreeExists ?? existsSync15;
13662
14015
  let orphaned = 0;
13663
14016
  let merged = 0;
13664
14017
  let requeued = 0;
@@ -13721,39 +14074,37 @@ async function makeStore(repo, worktreesDir, repoFlag) {
13721
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`);
13722
14075
  }
13723
14076
  }
14077
+ const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir4(), repoPaths);
14078
+ const repoMap = emptyRepoMapState(scanRootsList);
13724
14079
  if (SWARM_BACKEND !== "client") {
13725
14080
  const { SwarmDb: SwarmDb2 } = await loadSqlite();
13726
- return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host };
14081
+ return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host, repoMap };
13727
14082
  }
13728
14083
  const call = resilientSwarmMcp({
13729
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.`),
13730
14085
  onUp: () => console.warn("[overmind] \u043F\u0440\u043E\u0434-MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
13731
14086
  });
13732
14087
  const explicitRegistry = await buildRepoRegistry(repoPaths);
13733
- const byName = new Map(explicitRegistry.map((e) => [e.repoFullName, e.path]));
13734
- const scanRootsList = defaultScanRoots(homedir3(), repoPaths);
13735
- const scanStartedAt = Date.now();
13736
- const scanned = scanRoots(scanRootsList, { maxDepth: SWARM_SCAN_DEPTH });
13737
- let addedByScan = 0;
13738
- for (const [full, path] of scanned) {
13739
- if (byName.has(full)) continue;
13740
- byName.set(full, path);
13741
- addedByScan++;
13742
- }
13743
- if (addedByScan > 0) {
13744
- console.log(
13745
- `[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}` : ""}`
13746
- );
13747
- }
13748
- const repoRegistry = [...byName].map(([repoFullName, path]) => ({ repoFullName, path }));
13749
- if (repoPaths.length > 0 && repoRegistry.length < repoPaths.length) {
14088
+ if (repoPaths.length > 0 && explicitRegistry.length < repoPaths.length) {
13750
14089
  console.warn(
13751
- `[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)`
13752
14091
  );
13753
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();
13754
14103
  if (repoRegistry.length === 0) {
13755
14104
  console.warn(
13756
- `[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)."
13757
14108
  );
13758
14109
  }
13759
14110
  const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry, host });
@@ -13768,7 +14119,17 @@ async function makeStore(repo, worktreesDir, repoFlag) {
13768
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`
13769
14120
  );
13770
14121
  }
13771
- 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
+ };
13772
14133
  }
13773
14134
  var crashGuardsInstalled = false;
13774
14135
  function installCrashGuards() {
@@ -13787,7 +14148,7 @@ async function migrateWorktreesAtStartup() {
13787
14148
  legacyDir: WORKTREES_LEGACY_DIR,
13788
14149
  targetDir: WORKTREES_DIR,
13789
14150
  envOverride: process.env.SWARM_WORKTREES || "",
13790
- exists: (d) => existsSync13(d),
14151
+ exists: (d) => existsSync15(d),
13791
14152
  isNonEmptyDir: (d) => {
13792
14153
  try {
13793
14154
  return readdirSync10(d).length > 0;
@@ -13796,13 +14157,13 @@ async function migrateWorktreesAtStartup() {
13796
14157
  }
13797
14158
  },
13798
14159
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
13799
- rename: (from, to) => renameSync8(from, to),
14160
+ rename: (from, to) => renameSync10(from, to),
13800
14161
  repairMoved: (t) => repairMovedWorktrees(t),
13801
14162
  log: (m) => console.log(m)
13802
14163
  });
13803
14164
  } catch (e) {
13804
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);
13805
- return existsSync13(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
14166
+ return existsSync15(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
13806
14167
  }
13807
14168
  }
13808
14169
  async function reportWorkspaceAccess(tl, db) {
@@ -13820,13 +14181,22 @@ async function reportWorkspaceAccess(tl, db) {
13820
14181
  async function runDaemon(opts) {
13821
14182
  installCrashGuards();
13822
14183
  const daemonStart = await captureDaemonStart(THIS_FILE);
14184
+ const latestRelease = fetchLatestVersion();
13823
14185
  const worktreesDir = await migrateWorktreesAtStartup();
13824
14186
  if (!opts.repo) {
13825
14187
  const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
13826
14188
  if (remembered) opts.repo = remembered;
13827
14189
  }
13828
- 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);
13829
14197
  await preflightAuth();
14198
+ const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
14199
+ if (outdated) console.warn(`[overmind] ${outdated}`);
13830
14200
  const tl = await connectTaskless();
13831
14201
  const tlFor = (workspaceId) => tl ? tl.forWorkspace(workspaceId) : null;
13832
14202
  if (tl) await reportWorkspaceAccess(tl, db);
@@ -13886,6 +14256,10 @@ async function runDaemon(opts) {
13886
14256
  const client = db;
13887
14257
  rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
13888
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
+ }
13889
14263
  let daemonVersion;
13890
14264
  if (daemonStart && SWARM_VERSION_CHECK_MS > 0) {
13891
14265
  let cached = null;
@@ -14117,8 +14491,9 @@ async function runDaemon(opts) {
14117
14491
  baseBranchReader ? baseBranchReader(repoOfRun) : Promise.resolve("")
14118
14492
  ]);
14119
14493
  const task = taskClient ? requireTaskBody(rawTask, run.task_slug, run.workspace_id) : null;
14120
- const base3 = effectiveBaseBranch(run.base_branch, projectBase);
14121
- 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;
14122
14497
  const taskContext = task && taskClient ? await fetchTaskContext(taskClient, task) : void 0;
14123
14498
  await spawnDrone(
14124
14499
  {
@@ -14136,7 +14511,7 @@ async function runDaemon(opts) {
14136
14511
  runToSpawn
14137
14512
  );
14138
14513
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
14139
- if (taskClient) await taskClient.startTask(run.task_slug);
14514
+ if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
14140
14515
  },
14141
14516
  prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
14142
14517
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
@@ -14249,6 +14624,22 @@ async function runDaemon(opts) {
14249
14624
  ...exclusiveProject ? { exclusiveProject } : {},
14250
14625
  repoFallback: opts.repo,
14251
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
+ },
14252
14643
  // merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
14253
14644
  // фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
14254
14645
  resolveConflict: (reviewRun) => resolveConflict(
@@ -14288,7 +14679,7 @@ async function runDaemon(opts) {
14288
14679
  mode,
14289
14680
  nudge
14290
14681
  ),
14291
- worktreeExists: (p) => existsSync13(p),
14682
+ worktreeExists: (p) => existsSync15(p),
14292
14683
  hasTranscript: (p) => sessionTranscriptExists(p),
14293
14684
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
14294
14685
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -14316,6 +14707,8 @@ async function runDaemon(opts) {
14316
14707
  ...heartbeat ? { heartbeat } : {},
14317
14708
  // Сверка кода демона с main (TASWM-251): ключ есть только когда entryFile в git-репо.
14318
14709
  ...daemonVersion ? { daemonVersion } : {},
14710
+ // Карта репозиториев машины (TASWM-336): ключ есть только в client-режиме с непустым TTL.
14711
+ ...repoMap ? { repoMap } : {},
14319
14712
  // Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
14320
14713
  ...anomalies ? { anomalies } : {},
14321
14714
  // Сторож прогресса роя (TASWM-270): в отличие от anomalies выше — заведён БЕЗУСЛОВНО
@@ -14529,6 +14922,10 @@ async function runDaemon(opts) {
14529
14922
  // Устаревший код демона (TASWM-251): тот же кэш, что читает heartbeat.push выше —
14530
14923
  // локальный дашборд :7420 видит ту же сверку, что уходит в прод, без отдельного git-похода.
14531
14924
  daemonVersion: () => daemonVersion?.get() ?? null,
14925
+ // Карта репозиториев машины (TASWM-336): то же состояние, что мутирует шаг тика, — его
14926
+ // печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
14927
+ // считает своими и в каком дереве работает при двух копиях одного клона.
14928
+ repoMap: () => repoMapRows(repoMapState),
14532
14929
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
14533
14930
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
14534
14931
  control,