@taskless-app/swarm 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { basename as basename4 } from "path";
5
- import { existsSync as existsSync8, readdirSync as readdirSync6 } from "fs";
5
+ import { existsSync as existsSync9, readdirSync as readdirSync6 } from "fs";
6
6
  import { createInterface } from "readline/promises";
7
7
  import { setTimeout as sleep2 } from "timers/promises";
8
8
 
@@ -448,6 +448,8 @@ function groupByHost(items, ref, opts = {}) {
448
448
  unknown,
449
449
  lastSeenAt: unknown ? null : info?.lastSeenAt ?? null,
450
450
  offline: false,
451
+ paused: unknown ? false : info?.paused === true,
452
+ pauseReason: unknown ? null : info?.pauseReason ?? null,
451
453
  runs: []
452
454
  };
453
455
  byKey.set(key, g);
@@ -477,7 +479,9 @@ function hostStateText(host, nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_
477
479
  if (!host.lastSeenAt) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
478
480
  const at = Date.parse(host.lastSeenAt);
479
481
  if (Number.isNaN(at)) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
480
- if (!isHostOffline(host.lastSeenAt, nowMs, staleMs)) return "\u043D\u0430 \u0441\u0432\u044F\u0437\u0438";
482
+ if (!isHostOffline(host.lastSeenAt, nowMs, staleMs)) {
483
+ return host.paused ? host.pauseReason ? `\u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 ${host.pauseReason}` : "\u043D\u0430 \u043F\u0430\u0443\u0437\u0435" : "\u043D\u0430 \u0441\u0432\u044F\u0437\u0438";
484
+ }
481
485
  const min = Math.max(1, Math.round((nowMs - at) / 6e4));
482
486
  return `\u043C\u043E\u043B\u0447\u0438\u0442 ${min} \u043C\u0438\u043D`;
483
487
  }
@@ -581,6 +585,8 @@ var SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
581
585
  var WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
582
586
  var WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
583
587
  var SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
588
+ var SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
589
+ var SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
584
590
  var SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
585
591
  var SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
586
592
  var SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
@@ -1670,6 +1676,10 @@ var SwarmClient = class _SwarmClient {
1670
1676
  depsDoneInProd = /* @__PURE__ */ new Set();
1671
1677
  depsAskedAt = /* @__PURE__ */ new Map();
1672
1678
  static DEPS_TTL_MS = 6e4;
1679
+ // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
1680
+ // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
1681
+ // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
1682
+ notifyLevelUnsupported = false;
1673
1683
  // Корреляция с продом.
1674
1684
  keyByLocal = /* @__PURE__ */ new Map();
1675
1685
  // local run id → runKey
@@ -2731,9 +2741,32 @@ var SwarmClient = class _SwarmClient {
2731
2741
  lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
2732
2742
  // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
2733
2743
  // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
2734
- reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : []
2744
+ reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
2745
+ // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
2746
+ // версии поля не пришлёт → undefined, строка выглядит как раньше.
2747
+ ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
2748
+ ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
2749
+ ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
2735
2750
  }));
2736
2751
  }
2752
+ /**
2753
+ * TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
2754
+ * SwarmHost и освобождает её незавершённые прогоны — ровно то же, что делает свипер
2755
+ * с молчащей машиной, только сразу и по явной команде. Без этого выведенная из роя
2756
+ * машина висела в списке вечно с подписью «молчит N минут», неотличимо от упавшей.
2757
+ *
2758
+ * ПРЯМОЙ вызов, не outbox: это не durable-факт роя, который надо досылать после
2759
+ * восстановления сети, — команда имеет смысл только сейчас. Бросает при сбое:
2760
+ * вызывающий обязан сказать человеку, что из парка машину убрать не удалось, а не
2761
+ * молча стереть локальные токены (тогда её вообще нечем будет убрать).
2762
+ */
2763
+ async forgetHost() {
2764
+ if (!this.host) throw new Error("\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043B\u0430\u0441\u044C (\u043D\u0435\u0442 hostId) \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0443\u0431\u0438\u0440\u0430\u0442\u044C \u0438\u0437 \u043F\u0430\u0440\u043A\u0430");
2765
+ const res = await this.call("swarm_host_forget", { hostId: this.host.hostId });
2766
+ if (res?.isError) throw new Error("swarm_host_forget \u043E\u0442\u0432\u0435\u0440\u0433 \u0437\u0430\u043F\u0440\u043E\u0441 \u2014 \u043C\u0430\u0448\u0438\u043D\u0430 \u043E\u0441\u0442\u0430\u043B\u0430\u0441\u044C \u0432 \u043F\u0430\u0440\u043A\u0435");
2767
+ const sc = res.structuredContent ?? {};
2768
+ return { removed: sc.removed !== false, released: typeof sc.released === "number" ? sc.released : 0 };
2769
+ }
2737
2770
  /**
2738
2771
  * Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
2739
2772
  * дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
@@ -2761,10 +2794,21 @@ var SwarmClient = class _SwarmClient {
2761
2794
  * «пришли хвост лога» (logRequests). Возвращаем их вызывающему; пульс недоступен или
2762
2795
  * прод старый (поля нет) → пустой список, всё как раньше.
2763
2796
  */
2764
- async heartbeat(daemonVersion) {
2797
+ /**
2798
+ * TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
2799
+ * pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
2800
+ * и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
2801
+ * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
2802
+ * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
2803
+ */
2804
+ async heartbeat(daemonVersion, pause) {
2765
2805
  try {
2766
2806
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
2767
2807
  if (daemonVersion) args.daemonVersion = daemonVersion;
2808
+ if (pause !== void 0) {
2809
+ args.paused = !!pause?.paused;
2810
+ if (pause?.reason) args.pauseReason = pause.reason;
2811
+ }
2768
2812
  if (this.host) {
2769
2813
  args.hostId = this.host.hostId;
2770
2814
  args.hostName = this.host.hostName;
@@ -2820,13 +2864,26 @@ var SwarmClient = class _SwarmClient {
2820
2864
  */
2821
2865
  async notifyAnomaly(signal) {
2822
2866
  const prodRunId = signal.runId != null ? this.prodIdByKey.get(this.runKeyFor(signal.runId) ?? "") : void 0;
2823
- const res = await this.call("swarm_notify", {
2867
+ const base3 = {
2824
2868
  tag: signal.key,
2825
2869
  title: signal.title,
2826
2870
  body: signal.body,
2827
2871
  ...signal.href ? { href: signal.href } : {},
2828
2872
  ...prodRunId ? { runId: prodRunId } : {}
2829
- });
2873
+ };
2874
+ if (!this.notifyLevelUnsupported) {
2875
+ const res2 = await this.call("swarm_notify", { ...base3, level: signal.level ?? "decision" });
2876
+ if (!res2?.isError) return;
2877
+ const text = JSON.stringify(res2);
2878
+ if (!(text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D") && text.includes("\u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442") && text.includes("level"))) {
2879
+ throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
2880
+ }
2881
+ this.notifyLevelUnsupported = true;
2882
+ console.warn(
2883
+ "[swarm-client] \u043F\u0440\u043E\u0434 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 (swarm_notify \u0431\u0435\u0437 level) \u2014 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F \u0440\u043E\u044F \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u0438\u0434\u0443\u0442 \u043E\u0434\u043D\u0438\u043C \u0443\u0440\u043E\u0432\u043D\u0435\u043C, \u043A\u0430\u043A \u0434\u043E TASWM-327. \u041E\u0431\u043D\u043E\u0432\u0438 \u043F\u0440\u043E\u0434 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0434\u0435\u043C\u043E\u043D."
2884
+ );
2885
+ }
2886
+ const res = await this.call("swarm_notify", base3);
2830
2887
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
2831
2888
  }
2832
2889
  /**
@@ -3793,6 +3850,64 @@ async function collectRunDiff(worktree, opts = {}) {
3793
3850
  };
3794
3851
  }
3795
3852
 
3853
+ // package.json
3854
+ var package_default = {
3855
+ name: "@taskless-app/swarm",
3856
+ version: "0.4.2",
3857
+ type: "module",
3858
+ description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3859
+ keywords: [
3860
+ "ai-agents",
3861
+ "coding-agents",
3862
+ "swarm",
3863
+ "orchestration",
3864
+ "autonomous-agents",
3865
+ "mcp",
3866
+ "taskless",
3867
+ "cli"
3868
+ ],
3869
+ license: "SEE LICENSE IN LICENSE",
3870
+ author: "Ilya Smurov <ilya@smurov.com>",
3871
+ homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
3872
+ repository: {
3873
+ type: "git",
3874
+ url: "git+https://github.com/ilyasmurov/taskless.git",
3875
+ directory: "packages/swarm-daemon"
3876
+ },
3877
+ engines: {
3878
+ node: ">=22"
3879
+ },
3880
+ bin: {
3881
+ swarm: "dist/cli.js"
3882
+ },
3883
+ files: [
3884
+ "dist",
3885
+ "docs/HERO.md",
3886
+ "roles"
3887
+ ],
3888
+ scripts: {
3889
+ overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
3890
+ swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
3891
+ build: "tsup",
3892
+ start: "node dist/overmind.js",
3893
+ prepublishOnly: "pnpm run build",
3894
+ test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
3895
+ "test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
3896
+ "test:watch": "vitest",
3897
+ typecheck: "tsc -p tsconfig.json --noEmit",
3898
+ lint: "echo 'no lint in swarm-daemon yet'"
3899
+ },
3900
+ dependencies: {
3901
+ "@hono/node-server": "^2.0.12",
3902
+ "@modelcontextprotocol/sdk": "^1.29.0",
3903
+ execa: "^10.0.0",
3904
+ zod: "3.25.76"
3905
+ },
3906
+ optionalDependencies: {
3907
+ "better-sqlite3": "^12.11.1"
3908
+ }
3909
+ };
3910
+
3796
3911
  // src/gh.ts
3797
3912
  import { $ as $3 } from "execa";
3798
3913
  var NO_CHECKS_STDERR = /no checks reported/i;
@@ -4022,6 +4137,7 @@ async function ensureAccess(store, opts = {}) {
4022
4137
  }
4023
4138
 
4024
4139
  // src/web.ts
4140
+ var PACKAGE_VERSION = package_default.version;
4025
4141
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
4026
4142
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
4027
4143
  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>`;
@@ -5302,6 +5418,36 @@ async function runDiff(db2, id, now = Date.now()) {
5302
5418
  }
5303
5419
  var readNarratorLog = (log) => log?.() ?? [];
5304
5420
  var readRestartSnap = (restart, liveSessionIds) => restart ? { phase: restart.phase, sessionIds: liveSessionIds?.() ?? [] } : null;
5421
+ function pauseSourceOf(raw) {
5422
+ return raw === "cli" || raw === "remote" ? raw : "web";
5423
+ }
5424
+ function setPausedVia(deps, paused, source, reason) {
5425
+ const control = deps.control;
5426
+ if (control.set) {
5427
+ control.set(paused, { source, reason });
5428
+ return;
5429
+ }
5430
+ control.paused = paused;
5431
+ deps.narrate?.(
5432
+ paused ? "\u23F8 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442." : "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0441\u043D\u044F\u043B \u0440\u043E\u0439 \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0451\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438 \u0438\u043D\u0442\u0435\u0439\u043A\u0430."
5433
+ );
5434
+ }
5435
+ function daemonStatus(deps) {
5436
+ const counts = {};
5437
+ for (const r of deps.db.listRuns()) counts[r.status] = (counts[r.status] ?? 0) + 1;
5438
+ const live = deps.liveSessionIds?.() ?? [];
5439
+ return {
5440
+ hostId: deps.selfHost?.hostId ?? null,
5441
+ hostName: deps.selfHost?.hostName ?? null,
5442
+ backend: SWARM_BACKEND,
5443
+ paused: deps.control?.paused ?? false,
5444
+ pause: deps.control?.state ?? null,
5445
+ restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
5446
+ counts,
5447
+ live,
5448
+ version: PACKAGE_VERSION
5449
+ };
5450
+ }
5305
5451
  function eventsStream(db2, narratorLog, control, daemonVersion, restart, liveSessionIds, selfHost) {
5306
5452
  let iv;
5307
5453
  let last = "";
@@ -5470,18 +5616,25 @@ function makeHandler(deps) {
5470
5616
  return redirect("/");
5471
5617
  }
5472
5618
  if (p === "/pause" && req.method === "POST") {
5473
- if (!auth()) return unauthorized();
5619
+ if (!authed(req)) return unauthorized();
5474
5620
  if (!deps.control) return new Response("\u043F\u0430\u0443\u0437\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u043C\u043E\u0440\u0434\u0430 \u0431\u0435\u0437 \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430", { status: 503 });
5475
- deps.control.paused = true;
5476
- deps.narrate?.("\u23F8 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442.");
5477
- return redirect("/");
5621
+ const f = await formValue(req);
5622
+ const reason = (f.get("reason") ?? "").trim() || null;
5623
+ setPausedVia(deps, true, pauseSourceOf(f.get("source")), reason);
5624
+ return Response.json({ ok: true, paused: true, why: "\u0440\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u043C, \u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442" });
5478
5625
  }
5479
5626
  if (p === "/resume" && req.method === "POST") {
5480
- if (!auth()) return unauthorized();
5627
+ if (!authed(req)) return unauthorized();
5481
5628
  if (!deps.control) return new Response("\u043F\u0430\u0443\u0437\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u043C\u043E\u0440\u0434\u0430 \u0431\u0435\u0437 \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430", { status: 503 });
5482
- deps.control.paused = false;
5483
- deps.narrate?.("\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0441\u043D\u044F\u043B \u0440\u043E\u0439 \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0451\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438 \u0438\u043D\u0442\u0435\u0439\u043A\u0430.");
5484
- return redirect("/");
5629
+ setPausedVia(deps, false, pauseSourceOf((await formValue(req)).get("source")), null);
5630
+ return Response.json({ ok: true, paused: false, why: "\u0440\u043E\u0439 \u0441\u043D\u044F\u0442 \u0441 \u043F\u0430\u0443\u0437\u044B \u2014 \u0441\u043D\u043E\u0432\u0430 \u0431\u0435\u0440\u0451\u043C \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438 \u0438\u043D\u0442\u0435\u0439\u043A\u0430" });
5631
+ }
5632
+ if (p === "/status") return Response.json(daemonStatus(deps));
5633
+ if (p === "/shutdown" && req.method === "POST") {
5634
+ if (!authed(req)) return unauthorized();
5635
+ if (!deps.requestStop) return new Response("\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: \u043C\u043E\u0440\u0434\u0430 \u0431\u0435\u0437 \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430", { status: 503 });
5636
+ const f = await formValue(req);
5637
+ return Response.json(deps.requestStop({ force: f.get("force") === "1", wait: f.get("wait") === "1" }));
5485
5638
  }
5486
5639
  if (p === "/revive" && req.method === "POST") {
5487
5640
  if (!auth()) return unauthorized();
@@ -6516,78 +6669,106 @@ function daemonCancel(slug, reason) {
6516
6669
  function daemonWake(runId) {
6517
6670
  return post("/wake", { runId: String(runId) });
6518
6671
  }
6672
+ function daemonPause(reason) {
6673
+ return post("/pause", { source: "cli", ...reason ? { reason } : {} });
6674
+ }
6675
+ function daemonResume() {
6676
+ return post("/resume", { source: "cli" });
6677
+ }
6678
+ async function daemonStop(opts) {
6679
+ const res = await post("/shutdown", { force: opts.force ? "1" : "0", wait: opts.wait ? "1" : "0" });
6680
+ if (!res) return null;
6681
+ const raw = res;
6682
+ if (raw.stopping === void 0 && raw.waiting === void 0) return null;
6683
+ return { stopping: !!raw.stopping, waiting: !!raw.waiting, sessions: Array.isArray(raw.sessions) ? raw.sessions : [] };
6684
+ }
6685
+ async function daemonStatus2() {
6686
+ try {
6687
+ const res = await fetch(`${BASE}/status`, { headers: headers(), signal: AbortSignal.timeout(TIMEOUT_MS) });
6688
+ if (!res.ok) return null;
6689
+ const body = await res.json();
6690
+ return typeof body?.paused === "boolean" ? body : null;
6691
+ } catch {
6692
+ return null;
6693
+ }
6694
+ }
6519
6695
  var NO_DAEMON_NOTE = `\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT}) \u2014 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E \u041B\u041E\u041A\u0410\u041B\u042C\u041D\u042B\u0419 \u0441\u0442\u043E\u0440 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u044B, \u043E\u043D \u043C\u043E\u0436\u0435\u0442 \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u0442\u044C\u0441\u044F \u0441 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u043E\u043C. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0434\u0435\u043C\u043E\u043D\u0430: swarm start`;
6520
6696
 
6521
- // package.json
6522
- var package_default = {
6523
- name: "@taskless-app/swarm",
6524
- version: "0.4.0",
6525
- type: "module",
6526
- description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6527
- keywords: [
6528
- "ai-agents",
6529
- "coding-agents",
6530
- "swarm",
6531
- "orchestration",
6532
- "autonomous-agents",
6533
- "mcp",
6534
- "taskless",
6535
- "cli"
6536
- ],
6537
- license: "SEE LICENSE IN LICENSE",
6538
- author: "Ilya Smurov <ilya@smurov.com>",
6539
- homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
6540
- repository: {
6541
- type: "git",
6542
- url: "git+https://github.com/ilyasmurov/taskless.git",
6543
- directory: "packages/swarm-daemon"
6544
- },
6545
- engines: {
6546
- node: ">=22"
6547
- },
6548
- bin: {
6549
- swarm: "dist/cli.js"
6550
- },
6551
- files: [
6552
- "dist",
6553
- "docs/HERO.md",
6554
- "roles"
6555
- ],
6556
- scripts: {
6557
- overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
6558
- swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
6559
- build: "tsup",
6560
- start: "node dist/overmind.js",
6561
- prepublishOnly: "pnpm run build",
6562
- test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
6563
- "test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
6564
- "test:watch": "vitest",
6565
- typecheck: "tsc -p tsconfig.json --noEmit",
6566
- lint: "echo 'no lint in swarm-daemon yet'"
6567
- },
6568
- dependencies: {
6569
- "@hono/node-server": "^2.0.12",
6570
- "@modelcontextprotocol/sdk": "^1.29.0",
6571
- execa: "^10.0.0",
6572
- zod: "3.25.76"
6573
- },
6574
- optionalDependencies: {
6575
- "better-sqlite3": "^12.11.1"
6576
- },
6577
- devDependencies: {
6578
- "@taskless/swarm-core": "workspace:*",
6579
- "@types/better-sqlite3": "^7.6.13",
6580
- "@types/node": "22.10.5",
6581
- "playwright-core": "1.49.1",
6582
- tsup: "^8.5.1",
6583
- tsx: "4.19.2",
6584
- typescript: "7.0.2",
6585
- vitest: "2.1.8"
6697
+ // src/pauseState.ts
6698
+ import { existsSync as existsSync8, readFileSync as readFileSync9, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync9 } from "fs";
6699
+ import { join as join13 } from "path";
6700
+ function pauseStatePath(home) {
6701
+ return join13(home, "pause.json");
6702
+ }
6703
+ var ACTIVE = { paused: false, at: "", source: "cli" };
6704
+ function loadPauseState(home, io = {}) {
6705
+ const exists = io.exists ?? existsSync8;
6706
+ const readFile2 = io.readFile ?? ((p) => readFileSync9(p, "utf8"));
6707
+ const path = pauseStatePath(home);
6708
+ if (!exists(path)) return ACTIVE;
6709
+ try {
6710
+ const parsed = JSON.parse(readFile2(path));
6711
+ if (parsed.paused !== true) return ACTIVE;
6712
+ return {
6713
+ paused: true,
6714
+ at: typeof parsed.at === "string" ? parsed.at : "",
6715
+ source: parsed.source === "web" || parsed.source === "remote" ? parsed.source : "cli",
6716
+ ...typeof parsed.reason === "string" && parsed.reason.trim() ? { reason: parsed.reason.trim() } : {}
6717
+ };
6718
+ } catch {
6719
+ return ACTIVE;
6586
6720
  }
6587
- };
6721
+ }
6722
+ function savePauseState(home, state, io = {}) {
6723
+ const path = pauseStatePath(home);
6724
+ if (!state.paused) {
6725
+ const remove = io.remove ?? ((p) => existsSync8(p) && unlinkSync4(p));
6726
+ remove(path);
6727
+ return;
6728
+ }
6729
+ const writeFile2 = io.writeFile ?? ((p, data) => {
6730
+ writeFileSync9(`${p}.tmp`, data);
6731
+ renameSync3(`${p}.tmp`, p);
6732
+ });
6733
+ writeFile2(path, JSON.stringify(state, null, 2));
6734
+ }
6735
+ function makePauseState(source, reason, now = Date.now()) {
6736
+ return {
6737
+ paused: true,
6738
+ at: new Date(now).toISOString(),
6739
+ source,
6740
+ ...reason && reason.trim() ? { reason: reason.trim() } : {}
6741
+ };
6742
+ }
6743
+ function pauseText(state, now = Date.now()) {
6744
+ if (!state?.paused) return "\u0440\u043E\u0439 \u0430\u043A\u0442\u0438\u0432\u0435\u043D \u2014 \u0431\u0435\u0440\u0451\u0442 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443";
6745
+ const since = state.at ? sinceText(state.at, now) : "";
6746
+ const who = { cli: "\u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435", web: "\u0441 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0439 \u043C\u043E\u0440\u0434\u044B", remote: "\u0438\u0437 Taskless" }[state.source];
6747
+ const why = state.reason ? ` \u2014 ${state.reason}` : "";
6748
+ return `\u0440\u043E\u0439 \u041D\u0410 \u041F\u0410\u0423\u0417\u0415${since} (${who})${why}`;
6749
+ }
6750
+ function sinceText(at, now) {
6751
+ const ms = Date.parse(at);
6752
+ if (Number.isNaN(ms)) return "";
6753
+ const min = Math.max(0, Math.round((now - ms) / 6e4));
6754
+ if (min < 1) return " (\u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E)";
6755
+ if (min < 60) return ` (${min} \u043C\u0438\u043D)`;
6756
+ return ` (${Math.round(min / 60)} \u0447)`;
6757
+ }
6758
+
6759
+ // src/heartbeat.ts
6760
+ import { readFileSync as readFileSync10, renameSync as renameSync4, writeFileSync as writeFileSync10 } from "fs";
6761
+ function readHeartbeat(path) {
6762
+ try {
6763
+ const ms = Number(readFileSync10(path, "utf8").trim());
6764
+ return Number.isFinite(ms) ? ms : null;
6765
+ } catch {
6766
+ return null;
6767
+ }
6768
+ }
6588
6769
 
6589
6770
  // src/cli.ts
6590
- var PACKAGE_VERSION = package_default.version;
6771
+ var PACKAGE_VERSION2 = package_default.version;
6591
6772
  function takeRepo(argv) {
6592
6773
  const i = argv.indexOf("--repo");
6593
6774
  if (i === -1) return { repo: null, rest: argv };
@@ -6743,6 +6924,123 @@ async function runLogin(db2, opts) {
6743
6924
  web.server.close();
6744
6925
  }
6745
6926
  }
6927
+ async function liveSessionNames(ids) {
6928
+ if (ids.length === 0) return "";
6929
+ const snap = await daemonRuns();
6930
+ const byId = new Map((snap?.runs ?? []).map((r) => [r.id, r]));
6931
+ return ids.map((id) => {
6932
+ const r = byId.get(id);
6933
+ return r ? `${r.name} (${r.task_slug})` : `#${id}`;
6934
+ }).join(", ");
6935
+ }
6936
+ async function waitDaemonDown(opts) {
6937
+ const pollMs = opts.pollMs ?? 3e3;
6938
+ const until = Date.now() + opts.timeoutMs;
6939
+ let lastLive = -1;
6940
+ for (; ; ) {
6941
+ const st = await daemonStatus2();
6942
+ if (!st) return true;
6943
+ if (st.live.length !== lastLive) {
6944
+ lastLive = st.live.length;
6945
+ console.log(lastLive ? `\u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${lastLive}\u2026` : "\u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C \u2014 \u0433\u0430\u0448\u0443\u2026");
6946
+ }
6947
+ if (Date.now() >= until) return false;
6948
+ await sleep2(pollMs);
6949
+ }
6950
+ }
6951
+ async function runStop(argv) {
6952
+ const force = argv.includes("--force");
6953
+ const wait = argv.includes("--wait");
6954
+ if (force && wait) {
6955
+ console.error("--wait \u0438 --force \u0432\u043C\u0435\u0441\u0442\u0435 \u043D\u0435 \u0438\u043C\u0435\u044E\u0442 \u0441\u043C\u044B\u0441\u043B\u0430: \u043B\u0438\u0431\u043E \u0436\u0434\u0451\u043C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0434\u0440\u043E\u043D\u043E\u0432, \u043B\u0438\u0431\u043E \u043E\u0431\u0440\u044B\u0432\u0430\u0435\u043C \u0438\u0445 \u0441\u0435\u0439\u0447\u0430\u0441");
6956
+ return 1;
6957
+ }
6958
+ const res = await daemonStop({ force, wait });
6959
+ if (!res) {
6960
+ const beat = readHeartbeat(HEARTBEAT_PATH);
6961
+ const freshBeat = beat != null && Date.now() - beat < 6e4;
6962
+ if (freshBeat) {
6963
+ console.error(
6964
+ `\u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u0430\u0451\u0442 \u043F\u0443\u043B\u044C\u0441 (${HEARTBEAT_PATH}), \u043D\u043E \u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u0441\u0430\u043C \u0441\u0435\u0431\u044F \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C \u043E\u043D \u043D\u0435 \u043C\u043E\u0436\u0435\u0442. \u041F\u043E\u0445\u043E\u0436\u0435, \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u0432\u0438\u0441: \u043D\u0430\u0439\u0434\u0438 \u0435\u0433\u043E (ps) \u0438 \u043F\u043E\u0433\u0430\u0441\u0438 \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C \u0432\u0440\u0443\u0447\u043D\u0443\u044E.`
6965
+ );
6966
+ return 1;
6967
+ }
6968
+ console.log(`\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442 (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT} \u043C\u043E\u043B\u0447\u0438\u0442) \u2014 \u0433\u0430\u0441\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.`);
6969
+ return 0;
6970
+ }
6971
+ if (res.stopping) {
6972
+ if (res.sessions.length) {
6973
+ console.log(`\u0433\u0430\u0448\u0443 \u0434\u0435\u043C\u043E\u043D\u0430 \u041F\u0420\u0418\u041D\u0423\u0414\u0418\u0422\u0415\u041B\u042C\u041D\u041E \u2014 \u043E\u0431\u043E\u0440\u0432\u0430\u043D\u044B \u0436\u0438\u0432\u044B\u0435 \u0441\u0435\u0441\u0441\u0438\u0438 (${res.sessions.length}): ${await liveSessionNames(res.sessions)}`);
6974
+ console.log("\u0438\u0445 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443\u0442\u0441\u044F \u0437\u0430\u043D\u043E\u0432\u043E \u043F\u043E\u0441\u043B\u0435 swarm start (--continue \u043F\u043E \u0441\u0432\u043E\u0435\u043C\u0443 worktree)");
6975
+ } else {
6976
+ console.log("\u0433\u0430\u0448\u0443 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u0431\u044B\u043B\u043E");
6977
+ }
6978
+ const down = await waitDaemonDown({ timeoutMs: 3e4, pollMs: 500 });
6979
+ console.log(down ? "\u0434\u0435\u043C\u043E\u043D \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u043E\u0431\u0440\u0430\u0442\u043D\u043E: swarm start" : "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u043E\u043C\u0430\u043D\u0434\u0443, \u043D\u043E \u0435\u0449\u0451 \u043D\u0435 \u0432\u044B\u0448\u0435\u043B \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C `swarm status`");
6980
+ return down ? 0 : 1;
6981
+ }
6982
+ if (res.waiting) {
6983
+ console.log(`\u0434\u0435\u043C\u043E\u043D \u0433\u0430\u0441\u043D\u0435\u0442 \u041F\u041E\u0421\u041B\u0415 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 (${res.sessions.length}): ${await liveSessionNames(res.sessions)}`);
6984
+ console.log("\u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043E\u043D \u0443\u0436\u0435 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442. \u041F\u0440\u0435\u0440\u0432\u0430\u0442\u044C \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u2014 \xAB\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435\xBB \u043D\u0430 \u043C\u043E\u0440\u0434\u0435 \u0434\u0435\u043C\u043E\u043D\u0430; \u043E\u0431\u043E\u0440\u0432\u0430\u0442\u044C \u0441\u0435\u0439\u0447\u0430\u0441 \u2014 swarm stop --force");
6985
+ const down = await waitDaemonDown({ timeoutMs: 6 * 60 * 6e4 });
6986
+ console.log(down ? "\u0434\u0435\u043C\u043E\u043D \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u043E\u0431\u0440\u0430\u0442\u043D\u043E: swarm start" : "\u0436\u0434\u0443 \u0431\u043E\u043B\u044C\u0448\u0435 6 \u0447\u0430\u0441\u043E\u0432 \u2014 \u0431\u0440\u043E\u0441\u0430\u044E \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435, \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442 \u0433\u0430\u0441\u043D\u0443\u0442\u044C \u0441\u0430\u043C (swarm status \u043F\u043E\u043A\u0430\u0436\u0435\u0442)");
6987
+ return down ? 0 : 1;
6988
+ }
6989
+ console.error(`\u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 ${res.sessions.length} \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${await liveSessionNames(res.sessions)}`);
6990
+ console.error(
6991
+ "\u043E\u043D\u0438 \u043F\u0438\u0448\u0443\u0442 \u043A\u043E\u0434 \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441, \u0438 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0440\u0451\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043D\u0438\u043C\u0438 \u2014 \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u043D\u0435 \u0433\u0430\u0448\u0443.\n swarm stop --wait \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F, \u043F\u043E\u043A\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 (\u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u0440\u043E\u0439 \u0443\u0436\u0435 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442)\n swarm stop --force \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C \u0441\u0435\u0439\u0447\u0430\u0441, \u043E\u0431\u043E\u0440\u0432\u0430\u0432 \u0438\u0445\n swarm pause \u043F\u0440\u043E\u0441\u0442\u043E \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0442\u044C \u0431\u0440\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443, \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435 \u0433\u0430\u0441\u0438\u0442\u044C"
6992
+ );
6993
+ return 1;
6994
+ }
6995
+ async function runRemove(argv) {
6996
+ const yes = argv.includes("--yes") || argv.includes("-y");
6997
+ if (!yes) {
6998
+ if (!process.stdin.isTTY) {
6999
+ console.error("swarm remove \u043E\u0442\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u043C\u0430\u0448\u0438\u043D\u0443 \u043E\u0442 \u0440\u043E\u044F (\u0433\u0430\u0441\u0438\u0442 \u0434\u0435\u043C\u043E\u043D\u0430, \u0443\u0431\u0438\u0440\u0430\u0435\u0442 \u0435\u0451 \u0438\u0437 \u043F\u0430\u0440\u043A\u0430, \u0441\u0442\u0438\u0440\u0430\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u044B). \u041D\u0435\u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0437\u0430\u043F\u0443\u0441\u043A \u2014 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u044F\u0432\u043D\u043E: swarm remove --yes");
7000
+ return 1;
7001
+ }
7002
+ const answer = (await askTty(`\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043C\u0430\u0448\u0438\u043D\u0443 \xAB${thisHost.hostName}\xBB \u043E\u0442 \u0440\u043E\u044F? \u0414\u0435\u043C\u043E\u043D \u0431\u0443\u0434\u0435\u0442 \u043F\u043E\u0433\u0430\u0448\u0435\u043D, \u043C\u0430\u0448\u0438\u043D\u0430 \u0438\u0441\u0447\u0435\u0437\u043D\u0435\u0442 \u0438\u0437 \u043F\u0430\u0440\u043A\u0430, \u0442\u043E\u043A\u0435\u043D\u044B \u0441\u0442\u0451\u0440\u0442\u044B [y/N]: `)).trim().toLowerCase();
7003
+ if (answer !== "y" && answer !== "yes" && answer !== "\u0434" && answer !== "\u0434\u0430") {
7004
+ console.log("\u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E \u2014 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442\u043E");
7005
+ return 1;
7006
+ }
7007
+ }
7008
+ const stopCode = await runStop(argv.filter((a) => a !== "--yes" && a !== "-y"));
7009
+ if (stopCode !== 0) {
7010
+ console.error("\u043C\u0430\u0448\u0438\u043D\u0430 \u041D\u0415 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430: \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043D\u0443\u0436\u043D\u043E \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C \u0434\u0435\u043C\u043E\u043D\u0430 (\u0441\u043C. \u0432\u044B\u0448\u0435) \u2014 \u0442\u043E\u043A\u0435\u043D\u044B \u0438 \u043F\u0430\u0440\u043A \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442\u044B");
7011
+ return stopCode;
7012
+ }
7013
+ if (SWARM_BACKEND !== "client") {
7014
+ console.log("\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 sqlite-\u0431\u044D\u043A\u0435\u043D\u0434: \u043F\u0430\u0440\u043A\u0430 \u043C\u0430\u0448\u0438\u043D \u043D\u0435\u0442 \u2014 \u0443\u0431\u0438\u0440\u0430\u0442\u044C \u0438\u0437 \u043D\u0435\u0433\u043E \u043D\u0435\u0447\u0435\u0433\u043E, \u0447\u0438\u0449\u0443 \u0442\u043E\u043B\u044C\u043A\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435");
7015
+ } else {
7016
+ const db2 = await makeCliStore(SWARM_REPO);
7017
+ try {
7018
+ if (db2 instanceof SwarmClient) {
7019
+ const res = await db2.forgetHost();
7020
+ console.log(
7021
+ res.removed ? `\u043C\u0430\u0448\u0438\u043D\u0430 \u0443\u0431\u0440\u0430\u043D\u0430 \u0438\u0437 \u043F\u0430\u0440\u043A\u0430${res.released ? ` \u2014 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u043E \u0435\u0451 \u043D\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432: ${res.released}` : ""}` : "\u043C\u0430\u0448\u0438\u043D\u044B \u0432 \u043F\u0430\u0440\u043A\u0435 \u0438 \u043D\u0435 \u0431\u044B\u043B\u043E (\u043F\u0443\u043B\u044C\u0441 \u043D\u0438 \u0440\u0430\u0437\u0443 \u043D\u0435 \u0434\u043E\u0445\u043E\u0434\u0438\u043B) \u2014 \u0443\u0431\u0438\u0440\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E"
7022
+ );
7023
+ }
7024
+ } catch (e) {
7025
+ console.error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0431\u0440\u0430\u0442\u044C \u043C\u0430\u0448\u0438\u043D\u0443 \u0438\u0437 \u043F\u0430\u0440\u043A\u0430: ${e instanceof Error ? e.message : e}`);
7026
+ console.error("\u0442\u043E\u043A\u0435\u043D\u044B \u041D\u0415 \u0441\u0442\u0451\u0440\u0442\u044B \u2014 \u0438\u043D\u0430\u0447\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u0431\u044B\u043B\u043E \u0431\u044B \u043D\u0435\u0447\u0435\u043C. \u041F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0434\u043E\u043C \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438: swarm remove --yes");
7027
+ db2.close();
7028
+ return 1;
7029
+ }
7030
+ db2.close();
7031
+ }
7032
+ clearAgentToken();
7033
+ clearStore();
7034
+ clearWorkspace();
7035
+ try {
7036
+ savePauseState(SWARM_HOME, { paused: false, at: "", source: "cli" });
7037
+ } catch {
7038
+ }
7039
+ console.log("\u043C\u0430\u0448\u0438\u043D\u0430 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u043E\u0442 \u0440\u043E\u044F: \u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0433\u0430\u0448\u0435\u043D, \u0442\u043E\u043A\u0435\u043D\u044B (agent.json/oauth.json/workspace.json) \u0441\u0442\u0451\u0440\u0442\u044B.");
7040
+ console.log("\u043D\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u043D\u0438\u0435: \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435 \u043E\u0442\u0437\u044B\u0432\u0430\u0435\u0442 \u0435\u0433\u043E \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u2014 \u043E\u0442\u0437\u044B\u0432 \u0432 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430, \u041F\u0440\u043E\u0444\u0438\u043B\u044C \u2192 \u0420\u043E\u0439.");
7041
+ console.log("\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043C\u0430\u0448\u0438\u043D\u0443 \u0432 \u0440\u043E\u0439: swarm login && swarm start");
7042
+ return 0;
7043
+ }
6746
7044
  var STATUS_MARK = {
6747
7045
  queued: "\xB7",
6748
7046
  running: "\u25B6",
@@ -6794,10 +7092,16 @@ function printRuns(runs, view) {
6794
7092
  }).forEach((l) => console.log(l));
6795
7093
  }
6796
7094
  function usage() {
6797
- console.log(`swarm \u2014 \u0440\u043E\u0439 \u0434\u0440\u043E\u043D\u043E\u0432 (v${PACKAGE_VERSION})
7095
+ console.log(`swarm \u2014 \u0440\u043E\u0439 \u0434\u0440\u043E\u043D\u043E\u0432 (v${PACKAGE_VERSION2})
6798
7096
 
6799
7097
  swarm login [--workspace <slug|id>] [--no-open] \u0432\u0445\u043E\u0434 \u0432 \u043F\u0440\u043E\u0434 + \u0432\u044B\u0431\u043E\u0440 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430
6800
7098
  swarm start [--repo <path>] \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0434\u0435\u043C\u043E\u043D\u0430: \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u043E\u0447\u0435\u0440\u0435\u0434\u044C, \u0441\u043F\u0430\u0432\u043D\u0438\u0442/\u0431\u0443\u0434\u0438\u0442 \u0434\u0440\u043E\u043D\u043E\u0432
7099
+ swarm status \u0436\u0438\u0432 \u043B\u0438 \u0434\u0435\u043C\u043E\u043D, \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u043B\u0438 \u0440\u043E\u0439, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0434\u0440\u043E\u043D\u043E\u0432 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435
7100
+ swarm pause [\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026] \u043D\u0435 \u0431\u0440\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 (\u0436\u0438\u0432\u044B\u0435 \u0434\u0440\u043E\u043D\u044B \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442); \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430
7101
+ swarm resume \u0441\u043D\u044F\u0442\u044C \u043F\u0430\u0443\u0437\u0443
7102
+ swarm stop [--wait|--force] \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: \u0431\u0435\u0437 \u0444\u043B\u0430\u0433\u043E\u0432 \u043F\u0440\u0438 \u0436\u0438\u0432\u044B\u0445 \u0434\u0440\u043E\u043D\u0430\u0445 \u041D\u0415 \u0433\u0430\u0441\u0438\u0442,
7103
+ \u0430 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0438\u0445; --wait \u2014 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u0438\u0445 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F; --force \u2014 \u0441\u0435\u0439\u0447\u0430\u0441
7104
+ swarm remove [--yes] [--wait|--force] \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043C\u0430\u0448\u0438\u043D\u0443 \u043E\u0442 \u0440\u043E\u044F: \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C \u0434\u0435\u043C\u043E\u043D\u0430, \u0443\u0431\u0440\u0430\u0442\u044C \u0435\u0451 \u0438\u0437 \u043F\u0430\u0440\u043A\u0430 \u0438 \u0441\u0442\u0435\u0440\u0435\u0442\u044C \u0442\u043E\u043A\u0435\u043D\u044B
6801
7105
  swarm ls [status] \u0441\u043F\u0438\u0441\u043E\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0416\u0418\u0412\u041E\u0413\u041E \u0414\u0415\u041C\u041E\u041D\u0410 \u2014 \u0440\u043E\u0432\u043D\u043E \u0442\u0435, \u0447\u0442\u043E \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 (\u043E\u043F\u0446. \u0444\u0438\u043B\u044C\u0442\u0440 \u043F\u043E \u0441\u0442\u0430\u0442\u0443\u0441\u0443)
6802
7106
  swarm spawn <slug> [area] [--repo <path>] [--after <s1,s2>] [--priority <n>] [--base <\u0432\u0435\u0442\u043A\u0430>] [--host <\u043C\u0430\u0448\u0438\u043D\u0430|any>] \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C (--after \u2192 \u0436\u0434\u0430\u0442\u044C done \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439, --priority \u2192 \u0432\u044B\u0448\u0435 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0440\u0430\u043D\u044C\u0448\u0435, --base \u2192 PR \u0432 \u044D\u0442\u0443 \u0432\u0435\u0442\u043A\u0443, \u0430 \u043D\u0435 \u0432 \u0434\u0435\u0444\u043E\u043B\u0442\u043D\u0443\u044E, --host \u2192 \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430: \u041F\u041E \u0423\u041C\u041E\u041B\u0427\u0410\u041D\u0418\u042E \u044D\u0442\u0430, \xABany\xBB \u2192 \u043B\u044E\u0431\u0430\u044F \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0430\u044F)
6803
7107
  swarm hosts \u043C\u0430\u0448\u0438\u043D\u044B \u0440\u043E\u044F: \u0438\u043C\u044F, \u043F\u0443\u043B\u044C\u0441, \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438, id (\u0438\u043C\u044F \u0434\u043B\u044F --host)
@@ -6824,7 +7128,7 @@ function effectiveWorktreesDir() {
6824
7128
  legacyDir: WORKTREES_LEGACY_DIR,
6825
7129
  targetDir: WORKTREES_DIR,
6826
7130
  envOverride: process.env.SWARM_WORKTREES || "",
6827
- exists: (d) => existsSync8(d),
7131
+ exists: (d) => existsSync9(d),
6828
7132
  isNonEmptyDir: (d) => {
6829
7133
  try {
6830
7134
  return readdirSync6(d).length > 0;
@@ -6857,7 +7161,7 @@ async function finishClient(db2) {
6857
7161
  }
6858
7162
  var [cmd, ...rawArgs] = process.argv.slice(2);
6859
7163
  if (cmd === "--version" || cmd === "-v") {
6860
- console.log(PACKAGE_VERSION);
7164
+ console.log(PACKAGE_VERSION2);
6861
7165
  process.exit(0);
6862
7166
  }
6863
7167
  if (cmd === "logout") {
@@ -6956,6 +7260,54 @@ if (cmd === "cancel") {
6956
7260
  }
6957
7261
  console.warn(NO_DAEMON_NOTE);
6958
7262
  }
7263
+ if (cmd === "status") {
7264
+ const st = await daemonStatus2();
7265
+ if (!st) {
7266
+ const local = loadPauseState(SWARM_HOME);
7267
+ console.log(`\u0434\u0435\u043C\u043E\u043D \u041D\u0415 \u041F\u041E\u0414\u041D\u042F\u0422 (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT} \u043C\u043E\u043B\u0447\u0438\u0442). \u041F\u043E\u0434\u043D\u044F\u0442\u044C: swarm start`);
7268
+ console.log(local.paused ? `${pauseText(local)} \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u0423\u0416\u0415 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435, \u0441\u043D\u044F\u0442\u044C: swarm resume` : "\u043F\u0430\u0443\u0437\u044B \u043D\u0435\u0442 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u0432 \u0440\u0430\u0431\u043E\u0442\u0443");
7269
+ process.exit(1);
7270
+ }
7271
+ console.log(`\u0434\u0435\u043C\u043E\u043D \u0436\u0438\u0432 (v${st.version})${st.hostName ? `, \u043C\u0430\u0448\u0438\u043D\u0430 \xAB${st.hostName}\xBB` : ""}, \u0431\u044D\u043A\u0435\u043D\u0434 ${st.backend}`);
7272
+ console.log(pauseText(st.pause ?? (st.paused ? { paused: true, at: "", source: "web" } : null)));
7273
+ if (st.restart?.phase === "waiting") {
7274
+ console.log(
7275
+ `\u0434\u0435\u043C\u043E\u043D \u0416\u0414\u0401\u0422 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 (${st.restart.sessions.length}), \u0447\u0442\u043E\u0431\u044B ${st.restart.intent === "stop" ? "\u043F\u043E\u0433\u0430\u0441\u043D\u0443\u0442\u044C" : "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C\u0441\u044F"} \u2014 \u043F\u0440\u0438\u0451\u043C \u043D\u043E\u0432\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043D\u0430 \u044D\u0442\u043E \u0432\u0440\u0435\u043C\u044F`
7276
+ );
7277
+ }
7278
+ const counts = Object.entries(st.counts).filter(([, n]) => n > 0);
7279
+ console.log(counts.length ? `\u043F\u0440\u043E\u0433\u043E\u043D\u044B: ${counts.map(([s, n]) => `${s} ${n}`).join(", ")}` : "\u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u043D\u0435\u0442");
7280
+ console.log(st.live.length ? `\u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u0441\u0435\u0439\u0447\u0430\u0441: ${st.live.length} (#${st.live.join(", #")})` : "\u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442");
7281
+ process.exit(0);
7282
+ }
7283
+ if (cmd === "pause" || cmd === "resume") {
7284
+ const wantPause = cmd === "pause";
7285
+ const reason = wantPause ? rawArgs.join(" ").trim() || null : null;
7286
+ const verdict = wantPause ? await daemonPause(reason) : await daemonResume();
7287
+ if (verdict) {
7288
+ console.log(verdict.ok ? verdict.why : verdict.why);
7289
+ if (verdict.ok && wantPause) console.log("\u043F\u0430\u0443\u0437\u0430 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u0430 \u043D\u0430 \u0434\u0438\u0441\u043A \u2014 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0451\u0442 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0443 \u043C\u0430\u0448\u0438\u043D\u044B");
7290
+ process.exit(verdict.ok ? 0 : 1);
7291
+ }
7292
+ try {
7293
+ savePauseState(SWARM_HOME, wantPause ? makePauseState("cli", reason) : { paused: false, at: "", source: "cli" });
7294
+ } catch (e) {
7295
+ console.error(`\u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043F\u0430\u0443\u0437\u044B: ${e instanceof Error ? e.message : e}`);
7296
+ process.exit(1);
7297
+ }
7298
+ console.log(
7299
+ wantPause ? `\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u0437\u0430\u043F\u0438\u0441\u0430\u043B \u043F\u0430\u0443\u0437\u0443 \u043D\u0430 \u0434\u0438\u0441\u043A: \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F (swarm start) \u0423\u0416\u0415 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435. \u0421\u043D\u044F\u0442\u044C: swarm resume` : "\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u0441\u043D\u044F\u043B \u043F\u0430\u0443\u0437\u0443 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435: \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 swarm start \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u0432 \u0440\u0430\u0431\u043E\u0442\u0443"
7300
+ );
7301
+ process.exit(0);
7302
+ }
7303
+ if (cmd === "stop") {
7304
+ const code = await runStop(rawArgs);
7305
+ process.exit(code);
7306
+ }
7307
+ if (cmd === "remove") {
7308
+ const code = await runRemove(rawArgs);
7309
+ process.exit(code);
7310
+ }
6959
7311
  if (cmd === "shot") {
6960
7312
  const code = await runShotCli(rawArgs);
6961
7313
  process.exit(code);