@taskless-app/swarm 0.4.1 → 0.4.3

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/overmind.js CHANGED
@@ -758,6 +758,7 @@ __export(config_exports, {
758
758
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
759
759
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
760
760
  SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
761
+ SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
761
762
  SWARM_ANOMALY_OUTBOX_STUCK_MS: () => SWARM_ANOMALY_OUTBOX_STUCK_MS,
762
763
  SWARM_ANOMALY_PR_NO_REVIEWER_MS: () => SWARM_ANOMALY_PR_NO_REVIEWER_MS,
763
764
  SWARM_ANOMALY_PR_STUCK_MERGE_MS: () => SWARM_ANOMALY_PR_STUCK_MERGE_MS,
@@ -765,6 +766,7 @@ __export(config_exports, {
765
766
  SWARM_ANOMALY_QUEUE_STALLED_MS: () => SWARM_ANOMALY_QUEUE_STALLED_MS,
766
767
  SWARM_ANOMALY_RENOTIFY_MS: () => SWARM_ANOMALY_RENOTIFY_MS,
767
768
  SWARM_ANOMALY_RESOURCE_GATE_STALL_MS: () => SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
769
+ SWARM_ANOMALY_RUN_DOWN_WINDOW_MS: () => SWARM_ANOMALY_RUN_DOWN_WINDOW_MS,
768
770
  SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
769
771
  SWARM_BACKEND: () => SWARM_BACKEND,
770
772
  SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
@@ -890,7 +892,7 @@ function reviewCutoffAt() {
890
892
  }
891
893
  return at;
892
894
  }
893
- 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_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_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;
895
+ 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_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;
894
896
  var init_config = __esm({
895
897
  "src/config.ts"() {
896
898
  "use strict";
@@ -954,6 +956,8 @@ var init_config = __esm({
954
956
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
955
957
  WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
956
958
  SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
959
+ SWARM_ANOMALY_MAX_REPEATS = Math.trunc(envNum("SWARM_ANOMALY_MAX_REPEATS", 3));
960
+ SWARM_ANOMALY_RUN_DOWN_WINDOW_MS = envNum("SWARM_ANOMALY_RUN_DOWN_WINDOW_MS", 24 * 60 * 6e4);
957
961
  SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
958
962
  SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
959
963
  SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
@@ -1909,7 +1913,7 @@ CREATE TABLE IF NOT EXISTS imports (
1909
1913
  // src/resources.ts
1910
1914
  import os from "os";
1911
1915
  import { execFileSync } from "child_process";
1912
- import { readFileSync as readFileSync2, statfsSync } from "fs";
1916
+ import { readFileSync as readFileSync3, statfsSync } from "fs";
1913
1917
  function isTransientResourceError(e) {
1914
1918
  const code = e?.code;
1915
1919
  return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
@@ -1967,7 +1971,7 @@ function fileDescriptorLoadRatio() {
1967
1971
  }
1968
1972
  if (process.platform === "linux") {
1969
1973
  try {
1970
- const [allocated, , max] = readFileSync2("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
1974
+ const [allocated, , max] = readFileSync3("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
1971
1975
  if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
1972
1976
  } catch {
1973
1977
  }
@@ -2103,15 +2107,15 @@ var init_resources = __esm({
2103
2107
  });
2104
2108
 
2105
2109
  // src/heartbeat.ts
2106
- import { readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
2110
+ import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
2107
2111
  function writeHeartbeat(path, now) {
2108
2112
  const tmp = `${path}.tmp`;
2109
- writeFileSync3(tmp, String(now));
2110
- renameSync2(tmp, path);
2113
+ writeFileSync4(tmp, String(now));
2114
+ renameSync3(tmp, path);
2111
2115
  }
2112
2116
  function readHeartbeat(path) {
2113
2117
  try {
2114
- const ms = Number(readFileSync4(path, "utf8").trim());
2118
+ const ms = Number(readFileSync5(path, "utf8").trim());
2115
2119
  return Number.isFinite(ms) ? ms : null;
2116
2120
  } catch {
2117
2121
  return null;
@@ -2447,16 +2451,16 @@ var init_procCleanup = __esm({
2447
2451
  });
2448
2452
 
2449
2453
  // src/git.ts
2450
- import { existsSync as existsSync7, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
2454
+ import { existsSync as existsSync8, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
2451
2455
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
2452
- import { isAbsolute, join as join8 } from "path";
2456
+ import { isAbsolute, join as join9 } from "path";
2453
2457
  import { $ as $2 } from "execa";
2454
2458
  async function reclaimStaleWorktree(repo, worktree) {
2455
2459
  await detachWorktree(repo, worktree);
2456
2460
  }
2457
2461
  function isLinkedWorktree(dir) {
2458
2462
  try {
2459
- return statSync4(join8(dir, ".git")).isFile();
2463
+ return statSync4(join9(dir, ".git")).isFile();
2460
2464
  } catch {
2461
2465
  return false;
2462
2466
  }
@@ -2468,7 +2472,7 @@ async function detachWorktree(repo, worktree) {
2468
2472
  }
2469
2473
  }
2470
2474
  async function ensureWorktreeDirGone(worktree) {
2471
- if (!existsSync7(worktree)) return false;
2475
+ if (!existsSync8(worktree)) return false;
2472
2476
  if (!isLinkedWorktree(worktree)) {
2473
2477
  console.error(
2474
2478
  `[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.`
@@ -2483,7 +2487,7 @@ async function ensureWorktreeDirGone(worktree) {
2483
2487
  );
2484
2488
  return true;
2485
2489
  }
2486
- if (existsSync7(worktree)) {
2490
+ if (existsSync8(worktree)) {
2487
2491
  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.`);
2488
2492
  }
2489
2493
  return true;
@@ -2526,7 +2530,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
2526
2530
  }
2527
2531
  const busyAt = await worktreeForBranch(repo, branch);
2528
2532
  if (busyAt) {
2529
- const exists = existsSync7(busyAt);
2533
+ const exists = existsSync8(busyAt);
2530
2534
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
2531
2535
  if (live) throw new WorktreeBusyError(branch, busyAt);
2532
2536
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -2562,9 +2566,9 @@ async function ensureGitExcluded(worktree, pattern) {
2562
2566
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
2563
2567
  const raw = out.stdout.trim();
2564
2568
  if (out.exitCode !== 0 || !raw) return false;
2565
- const commonDir = isAbsolute(raw) ? raw : join8(worktree, raw);
2566
- const infoDir = join8(commonDir, "info");
2567
- const excludeFile = join8(infoDir, "exclude");
2569
+ const commonDir = isAbsolute(raw) ? raw : join9(worktree, raw);
2570
+ const infoDir = join9(commonDir, "info");
2571
+ const excludeFile = join9(infoDir, "exclude");
2568
2572
  let current = "";
2569
2573
  try {
2570
2574
  current = await readFile(excludeFile, "utf8");
@@ -2597,7 +2601,7 @@ async function worktreeDirty(path) {
2597
2601
  async function repairMovedWorktrees(targetDir) {
2598
2602
  const subdirs = (dir) => {
2599
2603
  try {
2600
- return readdirSync7(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join8(dir, d.name));
2604
+ return readdirSync7(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join9(dir, d.name));
2601
2605
  } catch {
2602
2606
  return [];
2603
2607
  }
@@ -2709,18 +2713,18 @@ __export(droneChannel_exports, {
2709
2713
  verifyLocalCliToken: () => verifyLocalCliToken
2710
2714
  });
2711
2715
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
2712
- import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
2716
+ import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
2713
2717
  import { dirname as dirname3 } from "path";
2714
2718
  import { serve } from "@hono/node-server";
2715
2719
  function loadChannelSecret(path) {
2716
2720
  try {
2717
- const s = readFileSync9(path, "utf8").trim();
2721
+ const s = readFileSync10(path, "utf8").trim();
2718
2722
  if (s.length >= 32) return s;
2719
2723
  } catch {
2720
2724
  }
2721
2725
  const secret = randomBytes(32).toString("hex");
2722
2726
  mkdirSync5(dirname3(path), { recursive: true });
2723
- writeFileSync6(path, secret, { mode: 384 });
2727
+ writeFileSync7(path, secret, { mode: 384 });
2724
2728
  try {
2725
2729
  chmodSync(path, 384);
2726
2730
  } catch {
@@ -3050,7 +3054,7 @@ var init_droneChannel = __esm({
3050
3054
 
3051
3055
  // src/attachments.ts
3052
3056
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
3053
- import { join as join10 } from "path";
3057
+ import { join as join11 } from "path";
3054
3058
  function attachmentsFromDoc(doc, origin) {
3055
3059
  const out = [];
3056
3060
  const walk = (node) => {
@@ -3141,7 +3145,7 @@ async function materializeAttachments(opts) {
3141
3145
  };
3142
3146
  const result = { files: [], skipped: [] };
3143
3147
  if (opts.refs.length === 0) return result;
3144
- const dir = join10(opts.worktree, ATTACHMENTS_DIR);
3148
+ const dir = join11(opts.worktree, ATTACHMENTS_DIR);
3145
3149
  await io.removeDir(dir);
3146
3150
  let dirReady = false;
3147
3151
  let total = 0;
@@ -3187,7 +3191,7 @@ async function materializeAttachments(opts) {
3187
3191
  await io.mkdir(dir);
3188
3192
  dirReady = true;
3189
3193
  }
3190
- await io.writeFile(join10(dir, name), file.bytes);
3194
+ await io.writeFile(join11(dir, name), file.bytes);
3191
3195
  } catch (e) {
3192
3196
  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)})`);
3193
3197
  continue;
@@ -3340,7 +3344,7 @@ var init_attachments = __esm({
3340
3344
 
3341
3345
  // src/shotUpload.ts
3342
3346
  import { readFile as fsReadFile } from "fs/promises";
3343
- import { basename as basename2, isAbsolute as isAbsolute2, join as join11 } from "path";
3347
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join12 } from "path";
3344
3348
  function shotMimetype(path) {
3345
3349
  const dot = path.lastIndexOf(".");
3346
3350
  if (dot < 0) return null;
@@ -3366,7 +3370,7 @@ async function readShots(paths, opts) {
3366
3370
  }
3367
3371
  let bytes;
3368
3372
  try {
3369
- bytes = await read(isAbsolute2(path) ? path : join11(opts.worktree, path));
3373
+ bytes = await read(isAbsolute2(path) ? path : join12(opts.worktree, path));
3370
3374
  } catch (e) {
3371
3375
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
3372
3376
  continue;
@@ -3444,8 +3448,8 @@ var init_shotUpload = __esm({
3444
3448
 
3445
3449
  // src/oauth.ts
3446
3450
  import { createHash, randomBytes as randomBytes2 } from "crypto";
3447
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, unlinkSync } from "fs";
3448
- import { join as join12 } from "path";
3451
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2 } from "fs";
3452
+ import { join as join13 } from "path";
3449
3453
  function makePkce() {
3450
3454
  const verifier = randomBytes2(48).toString("base64url");
3451
3455
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -3529,23 +3533,23 @@ function toStore(clientId, t, now) {
3529
3533
  }
3530
3534
  function loadStore() {
3531
3535
  try {
3532
- return JSON.parse(readFileSync10(STORE_PATH, "utf8"));
3536
+ return JSON.parse(readFileSync11(STORE_PATH, "utf8"));
3533
3537
  } catch {
3534
3538
  return null;
3535
3539
  }
3536
3540
  }
3537
3541
  function saveStore(s) {
3538
- writeFileSync7(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
3542
+ writeFileSync8(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
3539
3543
  }
3540
3544
  function clearStore() {
3541
3545
  try {
3542
- unlinkSync(STORE_PATH);
3546
+ unlinkSync2(STORE_PATH);
3543
3547
  } catch {
3544
3548
  }
3545
3549
  }
3546
3550
  function loadClientId() {
3547
3551
  try {
3548
- return JSON.parse(readFileSync10(CLIENT_PATH, "utf8")).clientId;
3552
+ return JSON.parse(readFileSync11(CLIENT_PATH, "utf8")).clientId;
3549
3553
  } catch {
3550
3554
  return null;
3551
3555
  }
@@ -3554,7 +3558,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
3554
3558
  const existing = loadClientId();
3555
3559
  if (existing) return existing;
3556
3560
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
3557
- writeFileSync7(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
3561
+ writeFileSync8(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
3558
3562
  return clientId;
3559
3563
  }
3560
3564
  async function ensureAccess(store, opts = {}) {
@@ -3580,18 +3584,18 @@ var init_oauth = __esm({
3580
3584
  AUTHORIZE = "/api/oauth/authorize";
3581
3585
  TOKEN = "/api/oauth/token";
3582
3586
  SCOPE = "openid profile email taskless:full";
3583
- STORE_PATH = join12(SWARM_HOME, "oauth.json");
3584
- CLIENT_PATH = join12(SWARM_HOME, "oauth-client.json");
3587
+ STORE_PATH = join13(SWARM_HOME, "oauth.json");
3588
+ CLIENT_PATH = join13(SWARM_HOME, "oauth-client.json");
3585
3589
  base = (api) => api.replace(/\/$/, "");
3586
3590
  }
3587
3591
  });
3588
3592
 
3589
3593
  // src/agentToken.ts
3590
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2 } from "fs";
3591
- import { join as join13 } from "path";
3594
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3 } from "fs";
3595
+ import { join as join14 } from "path";
3592
3596
  function loadAgentToken() {
3593
3597
  try {
3594
- const s = JSON.parse(readFileSync11(STORE_PATH2, "utf8"));
3598
+ const s = JSON.parse(readFileSync12(STORE_PATH2, "utf8"));
3595
3599
  return s.token ? s : null;
3596
3600
  } catch {
3597
3601
  return null;
@@ -3628,17 +3632,17 @@ var init_agentToken = __esm({
3628
3632
  "src/agentToken.ts"() {
3629
3633
  "use strict";
3630
3634
  init_config();
3631
- STORE_PATH2 = join13(SWARM_HOME, "agent.json");
3635
+ STORE_PATH2 = join14(SWARM_HOME, "agent.json");
3632
3636
  base2 = (api) => api.replace(/\/$/, "");
3633
3637
  }
3634
3638
  });
3635
3639
 
3636
3640
  // src/workspace.ts
3637
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3 } from "fs";
3638
- import { join as join14 } from "path";
3641
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
3642
+ import { join as join15 } from "path";
3639
3643
  function loadWorkspace() {
3640
3644
  try {
3641
- const w = JSON.parse(readFileSync12(WORKSPACE_PATH, "utf8"));
3645
+ const w = JSON.parse(readFileSync13(WORKSPACE_PATH, "utf8"));
3642
3646
  return w && typeof w.id === "string" && w.id ? w : null;
3643
3647
  } catch {
3644
3648
  return null;
@@ -3652,7 +3656,7 @@ var init_workspace = __esm({
3652
3656
  "src/workspace.ts"() {
3653
3657
  "use strict";
3654
3658
  init_config();
3655
- WORKSPACE_PATH = join14(SWARM_HOME, "workspace.json");
3659
+ WORKSPACE_PATH = join15(SWARM_HOME, "workspace.json");
3656
3660
  }
3657
3661
  });
3658
3662
 
@@ -4116,8 +4120,8 @@ var init_droneStore = __esm({
4116
4120
 
4117
4121
  // src/overmind.ts
4118
4122
  init_db();
4119
- import { existsSync as existsSync10, readFileSync as readFileSync13, readdirSync as readdirSync9, renameSync as renameSync5 } from "fs";
4120
- import { basename as basename4, dirname as dirname5, join as join17 } from "path";
4123
+ import { existsSync as existsSync12, readFileSync as readFileSync15, readdirSync as readdirSync9, renameSync as renameSync7 } from "fs";
4124
+ import { basename as basename4, dirname as dirname5, join as join19 } from "path";
4121
4125
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4122
4126
  import { spawn as spawn3 } from "child_process";
4123
4127
 
@@ -4127,7 +4131,69 @@ function liveSessionIds(db, isAlive) {
4127
4131
  return db.listRuns().filter((r) => !isTerminalStatus(r.status) && r.pid != null && isAlive(r.pid)).map((r) => r.id);
4128
4132
  }
4129
4133
  function initialRestartState() {
4130
- return { phase: "idle", requestedAt: null };
4134
+ return { phase: "idle", requestedAt: null, intent: "restart" };
4135
+ }
4136
+
4137
+ // src/pauseState.ts
4138
+ import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
4139
+ import { join as join2 } from "path";
4140
+ function pauseStatePath(home) {
4141
+ return join2(home, "pause.json");
4142
+ }
4143
+ var ACTIVE = { paused: false, at: "", source: "cli" };
4144
+ function loadPauseState(home, io = {}) {
4145
+ const exists = io.exists ?? existsSync2;
4146
+ const readFile2 = io.readFile ?? ((p) => readFileSync2(p, "utf8"));
4147
+ const path = pauseStatePath(home);
4148
+ if (!exists(path)) return ACTIVE;
4149
+ try {
4150
+ const parsed = JSON.parse(readFile2(path));
4151
+ if (parsed.paused !== true) return ACTIVE;
4152
+ return {
4153
+ paused: true,
4154
+ at: typeof parsed.at === "string" ? parsed.at : "",
4155
+ source: parsed.source === "web" || parsed.source === "remote" ? parsed.source : "cli",
4156
+ ...typeof parsed.reason === "string" && parsed.reason.trim() ? { reason: parsed.reason.trim() } : {}
4157
+ };
4158
+ } catch {
4159
+ return ACTIVE;
4160
+ }
4161
+ }
4162
+ function savePauseState(home, state, io = {}) {
4163
+ const path = pauseStatePath(home);
4164
+ if (!state.paused) {
4165
+ const remove = io.remove ?? ((p) => existsSync2(p) && unlinkSync(p));
4166
+ remove(path);
4167
+ return;
4168
+ }
4169
+ const writeFile2 = io.writeFile ?? ((p, data) => {
4170
+ writeFileSync2(`${p}.tmp`, data);
4171
+ renameSync2(`${p}.tmp`, p);
4172
+ });
4173
+ writeFile2(path, JSON.stringify(state, null, 2));
4174
+ }
4175
+ function makePauseState(source, reason, now = Date.now()) {
4176
+ return {
4177
+ paused: true,
4178
+ at: new Date(now).toISOString(),
4179
+ source,
4180
+ ...reason && reason.trim() ? { reason: reason.trim() } : {}
4181
+ };
4182
+ }
4183
+ function pauseText(state, now = Date.now()) {
4184
+ 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";
4185
+ const since = state.at ? sinceText(state.at, now) : "";
4186
+ 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];
4187
+ const why = state.reason ? ` \u2014 ${state.reason}` : "";
4188
+ return `\u0440\u043E\u0439 \u041D\u0410 \u041F\u0410\u0423\u0417\u0415${since} (${who})${why}`;
4189
+ }
4190
+ function sinceText(at, now) {
4191
+ const ms = Date.parse(at);
4192
+ if (Number.isNaN(ms)) return "";
4193
+ const min = Math.max(0, Math.round((now - ms) / 6e4));
4194
+ if (min < 1) return " (\u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E)";
4195
+ if (min < 60) return ` (${min} \u043C\u0438\u043D)`;
4196
+ return ` (${Math.round(min / 60)} \u0447)`;
4131
4197
  }
4132
4198
 
4133
4199
  // src/swarmClient.ts
@@ -4135,13 +4201,13 @@ init_config();
4135
4201
  init_resources();
4136
4202
  init_names2();
4137
4203
  init_storm();
4138
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4204
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4139
4205
  import { dirname } from "path";
4140
4206
  import { randomUUID } from "crypto";
4141
4207
 
4142
4208
  // src/logTail.ts
4143
- import { closeSync, existsSync as existsSync2, openSync, readSync, readdirSync, statSync } from "fs";
4144
- import { join as join2 } from "path";
4209
+ import { closeSync, existsSync as existsSync3, openSync, readSync, readdirSync, statSync } from "fs";
4210
+ import { join as join3 } from "path";
4145
4211
  var TAIL_LINES_FALLBACK = 200;
4146
4212
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
4147
4213
  function fileHeader(name, size, shown) {
@@ -4182,7 +4248,7 @@ function readTail(path, opts) {
4182
4248
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
4183
4249
  }
4184
4250
  function latestRunLaunch(runLogsDir, localRunId) {
4185
- const dir = join2(runLogsDir, String(localRunId));
4251
+ const dir = join3(runLogsDir, String(localRunId));
4186
4252
  let files;
4187
4253
  try {
4188
4254
  files = readdirSync(dir);
@@ -4196,13 +4262,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
4196
4262
  }
4197
4263
  if (indexes.size === 0) return null;
4198
4264
  const index = Math.max(...indexes);
4199
- const out = [join2(dir, `${index}.json`), join2(dir, `${index}.json.done`)].find((p) => existsSync2(p)) ?? null;
4200
- const err = existsSync2(join2(dir, `${index}.err.log`)) ? join2(dir, `${index}.err.log`) : null;
4265
+ const out = [join3(dir, `${index}.json`), join3(dir, `${index}.json.done`)].find((p) => existsSync3(p)) ?? null;
4266
+ const err = existsSync3(join3(dir, `${index}.err.log`)) ? join3(dir, `${index}.err.log`) : null;
4201
4267
  return { index, total: indexes.size, out, err };
4202
4268
  }
4203
4269
  function runLogTail(runLogsDir, localRunId, opts) {
4204
4270
  const launch = latestRunLaunch(runLogsDir, localRunId);
4205
- if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join2(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
4271
+ if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join3(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
4206
4272
  const parts = [];
4207
4273
  let budget = opts.maxBytes;
4208
4274
  if (launch.err) {
@@ -4336,6 +4402,10 @@ var SwarmClient = class _SwarmClient {
4336
4402
  depsDoneInProd = /* @__PURE__ */ new Set();
4337
4403
  depsAskedAt = /* @__PURE__ */ new Map();
4338
4404
  static DEPS_TTL_MS = 6e4;
4405
+ // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
4406
+ // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
4407
+ // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
4408
+ notifyLevelUnsupported = false;
4339
4409
  // Корреляция с продом.
4340
4410
  keyByLocal = /* @__PURE__ */ new Map();
4341
4411
  // local run id → runKey
@@ -4375,7 +4445,7 @@ var SwarmClient = class _SwarmClient {
4375
4445
  this.repoFullNames = registry.map((e) => e.repoFullName);
4376
4446
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
4377
4447
  this.host = opts.host ?? null;
4378
- this.repoExists = opts.repoExists ?? existsSync3;
4448
+ this.repoExists = opts.repoExists ?? existsSync4;
4379
4449
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
4380
4450
  this.outboxPath = opts.outboxPath;
4381
4451
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
@@ -4385,15 +4455,15 @@ var SwarmClient = class _SwarmClient {
4385
4455
  }
4386
4456
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
4387
4457
  recover() {
4388
- if (existsSync3(this.mapPath)) {
4458
+ if (existsSync4(this.mapPath)) {
4389
4459
  try {
4390
- const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
4460
+ const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
4391
4461
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
4392
4462
  } catch {
4393
4463
  }
4394
4464
  }
4395
- if (existsSync3(this.outboxPath)) {
4396
- const txt = readFileSync3(this.outboxPath, "utf8");
4465
+ if (existsSync4(this.outboxPath)) {
4466
+ const txt = readFileSync4(this.outboxPath, "utf8");
4397
4467
  for (const line of txt.split("\n")) {
4398
4468
  const s = line.trim();
4399
4469
  if (!s) continue;
@@ -4413,10 +4483,10 @@ var SwarmClient = class _SwarmClient {
4413
4483
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
4414
4484
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
4415
4485
  replayGlobalEvents() {
4416
- if (!existsSync3(this.stormPath)) return;
4486
+ if (!existsSync4(this.stormPath)) return;
4417
4487
  let txt;
4418
4488
  try {
4419
- txt = readFileSync3(this.stormPath, "utf8");
4489
+ txt = readFileSync4(this.stormPath, "utf8");
4420
4490
  } catch {
4421
4491
  return;
4422
4492
  }
@@ -5084,7 +5154,7 @@ var SwarmClient = class _SwarmClient {
5084
5154
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
5085
5155
  */
5086
5156
  reconcileRuntime(opts) {
5087
- const wtExists = opts.worktreeExists ?? existsSync3;
5157
+ const wtExists = opts.worktreeExists ?? existsSync4;
5088
5158
  const pidByWorktree = /* @__PURE__ */ new Map();
5089
5159
  try {
5090
5160
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -5397,9 +5467,32 @@ var SwarmClient = class _SwarmClient {
5397
5467
  lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
5398
5468
  // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
5399
5469
  // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
5400
- reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : []
5470
+ reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
5471
+ // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
5472
+ // версии поля не пришлёт → undefined, строка выглядит как раньше.
5473
+ ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
5474
+ ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
5475
+ ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
5401
5476
  }));
5402
5477
  }
5478
+ /**
5479
+ * TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
5480
+ * SwarmHost и освобождает её незавершённые прогоны — ровно то же, что делает свипер
5481
+ * с молчащей машиной, только сразу и по явной команде. Без этого выведенная из роя
5482
+ * машина висела в списке вечно с подписью «молчит N минут», неотличимо от упавшей.
5483
+ *
5484
+ * ПРЯМОЙ вызов, не outbox: это не durable-факт роя, который надо досылать после
5485
+ * восстановления сети, — команда имеет смысл только сейчас. Бросает при сбое:
5486
+ * вызывающий обязан сказать человеку, что из парка машину убрать не удалось, а не
5487
+ * молча стереть локальные токены (тогда её вообще нечем будет убрать).
5488
+ */
5489
+ async forgetHost() {
5490
+ 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");
5491
+ const res = await this.call("swarm_host_forget", { hostId: this.host.hostId });
5492
+ 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");
5493
+ const sc = res.structuredContent ?? {};
5494
+ return { removed: sc.removed !== false, released: typeof sc.released === "number" ? sc.released : 0 };
5495
+ }
5403
5496
  /**
5404
5497
  * Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
5405
5498
  * дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
@@ -5427,10 +5520,21 @@ var SwarmClient = class _SwarmClient {
5427
5520
  * «пришли хвост лога» (logRequests). Возвращаем их вызывающему; пульс недоступен или
5428
5521
  * прод старый (поля нет) → пустой список, всё как раньше.
5429
5522
  */
5430
- async heartbeat(daemonVersion) {
5523
+ /**
5524
+ * TASWM-326: тем же пульсом уходит ПАУЗА этой машины (`pause` — durable-запись из
5525
+ * pause.json либо null). В парке машин «на паузе» обязано отличаться от «нет работы»
5526
+ * и от «молчит», а единственный канал «демон → прод» с нужной кадансностью — пульс.
5527
+ * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
5528
+ * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
5529
+ */
5530
+ async heartbeat(daemonVersion, pause) {
5431
5531
  try {
5432
5532
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
5433
5533
  if (daemonVersion) args.daemonVersion = daemonVersion;
5534
+ if (pause !== void 0) {
5535
+ args.paused = !!pause?.paused;
5536
+ if (pause?.reason) args.pauseReason = pause.reason;
5537
+ }
5434
5538
  if (this.host) {
5435
5539
  args.hostId = this.host.hostId;
5436
5540
  args.hostName = this.host.hostName;
@@ -5486,13 +5590,26 @@ var SwarmClient = class _SwarmClient {
5486
5590
  */
5487
5591
  async notifyAnomaly(signal) {
5488
5592
  const prodRunId = signal.runId != null ? this.prodIdByKey.get(this.runKeyFor(signal.runId) ?? "") : void 0;
5489
- const res = await this.call("swarm_notify", {
5593
+ const base3 = {
5490
5594
  tag: signal.key,
5491
5595
  title: signal.title,
5492
5596
  body: signal.body,
5493
5597
  ...signal.href ? { href: signal.href } : {},
5494
5598
  ...prodRunId ? { runId: prodRunId } : {}
5495
- });
5599
+ };
5600
+ if (!this.notifyLevelUnsupported) {
5601
+ const res2 = await this.call("swarm_notify", { ...base3, level: signal.level ?? "decision" });
5602
+ if (!res2?.isError) return;
5603
+ const text = JSON.stringify(res2);
5604
+ 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"))) {
5605
+ throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
5606
+ }
5607
+ this.notifyLevelUnsupported = true;
5608
+ console.warn(
5609
+ "[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."
5610
+ );
5611
+ }
5612
+ const res = await this.call("swarm_notify", base3);
5496
5613
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
5497
5614
  }
5498
5615
  /**
@@ -5863,13 +5980,13 @@ var SwarmClient = class _SwarmClient {
5863
5980
  }
5864
5981
  persistPending() {
5865
5982
  try {
5866
- writeFileSync2(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
5983
+ writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
5867
5984
  } catch {
5868
5985
  }
5869
5986
  }
5870
5987
  persistMap() {
5871
5988
  try {
5872
- writeFileSync2(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
5989
+ writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
5873
5990
  } catch {
5874
5991
  }
5875
5992
  }
@@ -5925,18 +6042,18 @@ async function buildRepoRegistry(paths) {
5925
6042
 
5926
6043
  // src/hostIdentity.ts
5927
6044
  import { hostname } from "os";
5928
- import { existsSync as existsSync4, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
5929
- import { join as join3 } from "path";
6045
+ import { existsSync as existsSync5, readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
6046
+ import { join as join4 } from "path";
5930
6047
  import { randomUUID as randomUUID2 } from "crypto";
5931
6048
  function hostIdentityPath(home) {
5932
- return join3(home, "host.json");
6049
+ return join4(home, "host.json");
5933
6050
  }
5934
6051
  function loadHostIdentity(deps) {
5935
- const exists = deps.exists ?? existsSync4;
5936
- const readFile2 = deps.readFile ?? ((p) => readFileSync5(p, "utf8"));
6052
+ const exists = deps.exists ?? existsSync5;
6053
+ const readFile2 = deps.readFile ?? ((p) => readFileSync6(p, "utf8"));
5937
6054
  const writeFile2 = deps.writeFile ?? ((p, data) => {
5938
- writeFileSync4(`${p}.tmp`, data);
5939
- renameSync3(`${p}.tmp`, p);
6055
+ writeFileSync5(`${p}.tmp`, data);
6056
+ renameSync4(`${p}.tmp`, p);
5940
6057
  });
5941
6058
  const host = deps.hostname ?? hostname;
5942
6059
  const newId = deps.newId ?? randomUUID2;
@@ -6118,14 +6235,14 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
6118
6235
 
6119
6236
  // src/launcher.ts
6120
6237
  import { spawn } from "child_process";
6121
- import { closeSync as closeSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
6238
+ import { closeSync as closeSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
6122
6239
  import { createRequire as createRequire2 } from "module";
6123
6240
  import { homedir as homedir2 } from "os";
6124
- import { dirname as dirname2, join as join5 } from "path";
6241
+ import { dirname as dirname2, join as join6 } from "path";
6125
6242
  import { fileURLToPath } from "url";
6126
6243
 
6127
6244
  // src/claudeBin.ts
6128
- import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
6245
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
6129
6246
  import { win32 as winPath } from "path";
6130
6247
  var CLAUDE_FALLBACK = "claude";
6131
6248
  function compareVersionDirs(a, b) {
@@ -6162,7 +6279,7 @@ function winVersionRoots(env, listDir) {
6162
6279
  function findWindowsClaude(opts = {}) {
6163
6280
  const env = opts.env ?? process.env;
6164
6281
  const listDir = opts.listDir ?? realListDir;
6165
- const fileExists = opts.fileExists ?? existsSync5;
6282
+ const fileExists = opts.fileExists ?? existsSync6;
6166
6283
  let best = null;
6167
6284
  for (const root of winVersionRoots(env, listDir)) {
6168
6285
  for (const version of listDir(root)) {
@@ -6198,7 +6315,7 @@ function resolveClaudeBin(opts = {}) {
6198
6315
 
6199
6316
  // src/droneHome.ts
6200
6317
  import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync, statSync as statSync2, symlinkSync } from "fs";
6201
- import { join as join4, relative, sep } from "path";
6318
+ import { join as join5, relative, sep } from "path";
6202
6319
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
6203
6320
  function droneHomeDeny(env = process.env) {
6204
6321
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -6222,20 +6339,20 @@ function buildDroneHome(opts) {
6222
6339
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
6223
6340
  for (const existing of readdirSync3(droneHome)) {
6224
6341
  if (wanted.has(existing)) continue;
6225
- const p = join4(droneHome, existing);
6342
+ const p = join5(droneHome, existing);
6226
6343
  try {
6227
6344
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
6228
6345
  } catch {
6229
6346
  }
6230
6347
  }
6231
6348
  for (const name of wanted) {
6232
- const link = join4(droneHome, name);
6349
+ const link = join5(droneHome, name);
6233
6350
  try {
6234
6351
  lstatSync(link);
6235
6352
  continue;
6236
6353
  } catch {
6237
6354
  }
6238
- const target = join4(realHome, name);
6355
+ const target = join5(realHome, name);
6239
6356
  let isDir;
6240
6357
  try {
6241
6358
  isDir = statSync2(target).isDirectory();
@@ -6256,7 +6373,7 @@ var REAL_PATH_ENV_PINS = [
6256
6373
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
6257
6374
  ];
6258
6375
  function realPathEnvPins(realHome) {
6259
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join4(realHome, ...segments)]));
6376
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join5(realHome, ...segments)]));
6260
6377
  }
6261
6378
  function droneDenyRules(realHome, swarmHome) {
6262
6379
  const paths = [
@@ -6348,7 +6465,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
6348
6465
  const env = { ...base3 };
6349
6466
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
6350
6467
  if (isolate && !base3.TURBO_CACHE_DIR) {
6351
- env.TURBO_CACHE_DIR = join5(worktree, ".turbo", "cache");
6468
+ env.TURBO_CACHE_DIR = join6(worktree, ".turbo", "cache");
6352
6469
  }
6353
6470
  Object.assign(env, droneTestEnv(droneCap, base3));
6354
6471
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -6376,25 +6493,25 @@ var DAEMON_STATE_ENV = [
6376
6493
  "TASKLESS_TOKEN"
6377
6494
  ];
6378
6495
  function bundledRoleDir(kind) {
6379
- return join5(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
6496
+ return join6(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
6380
6497
  }
6381
6498
  function roleDir(worktree, kind) {
6382
- return join5(worktree, ".claude", "roles", kind);
6499
+ return join6(worktree, ".claude", "roles", kind);
6383
6500
  }
6384
6501
  function roleDirs(worktree, kind) {
6385
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync6(dir));
6502
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
6386
6503
  }
6387
6504
  function transcriptDir(worktree, projectsDir) {
6388
- return join5(projectsDir, worktree.replace(/[/.]/g, "-"));
6505
+ return join6(projectsDir, worktree.replace(/[/.]/g, "-"));
6389
6506
  }
6390
- function sessionTranscriptExists(worktree, projectsDir = join5(homedir2(), ".claude", "projects")) {
6507
+ function sessionTranscriptExists(worktree, projectsDir = join6(homedir2(), ".claude", "projects")) {
6391
6508
  try {
6392
6509
  return readdirSync4(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
6393
6510
  } catch {
6394
6511
  return false;
6395
6512
  }
6396
6513
  }
6397
- function transcriptMtimeMs(worktree, projectsDir = join5(homedir2(), ".claude", "projects")) {
6514
+ function transcriptMtimeMs(worktree, projectsDir = join6(homedir2(), ".claude", "projects")) {
6398
6515
  const dir = transcriptDir(worktree, projectsDir);
6399
6516
  let files;
6400
6517
  try {
@@ -6406,7 +6523,7 @@ function transcriptMtimeMs(worktree, projectsDir = join5(homedir2(), ".claude",
6406
6523
  for (const f of files) {
6407
6524
  if (!f.endsWith(".jsonl")) continue;
6408
6525
  try {
6409
- const mtime2 = statSync3(join5(dir, f)).mtimeMs;
6526
+ const mtime2 = statSync3(join6(dir, f)).mtimeMs;
6410
6527
  if (latest === null || mtime2 > latest) latest = mtime2;
6411
6528
  } catch {
6412
6529
  }
@@ -6414,7 +6531,7 @@ function transcriptMtimeMs(worktree, projectsDir = join5(homedir2(), ".claude",
6414
6531
  return latest;
6415
6532
  }
6416
6533
  function nextRunLogPath(runLogsDir, runId) {
6417
- const dir = join5(runLogsDir, String(runId));
6534
+ const dir = join6(runLogsDir, String(runId));
6418
6535
  mkdirSync4(dir, { recursive: true });
6419
6536
  let files = [];
6420
6537
  try {
@@ -6426,7 +6543,7 @@ function nextRunLogPath(runLogsDir, runId) {
6426
6543
  const m = /^(\d+)\.json/.exec(f);
6427
6544
  if (m) next = Math.max(next, Number(m[1]) + 1);
6428
6545
  }
6429
- return join5(dir, `${next}.json`);
6546
+ return join6(dir, `${next}.json`);
6430
6547
  }
6431
6548
  function openNextRunLog(runLogsDir, runId) {
6432
6549
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -6500,16 +6617,16 @@ function makeLauncher(cfg) {
6500
6617
  }
6501
6618
  }
6502
6619
  };
6503
- const cfgPath = join5(worktree, ".swarm-mcp.json");
6504
- writeFileSync5(cfgPath, JSON.stringify(mcpConfig, null, 2));
6620
+ const cfgPath = join6(worktree, ".swarm-mcp.json");
6621
+ writeFileSync6(cfgPath, JSON.stringify(mcpConfig, null, 2));
6505
6622
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
6506
6623
  const pluginDirs = roleDirs(worktree, run.kind);
6507
6624
  const realHome = cfg.droneIsolation?.realHome ?? homedir2();
6508
6625
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
6509
6626
  const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
6510
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync6(join5(cfg.droneIsolation.swarmHome, "token"))) {
6627
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join6(cfg.droneIsolation.swarmHome, "token"))) {
6511
6628
  console.error(
6512
- `[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 ${join5(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.`
6629
+ `[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 ${join6(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.`
6513
6630
  );
6514
6631
  }
6515
6632
  let stdio = "ignore";
@@ -6543,8 +6660,8 @@ function makeLauncher(cfg) {
6543
6660
  init_procCleanup();
6544
6661
 
6545
6662
  // src/cost.ts
6546
- import { readFileSync as readFileSync6, readdirSync as readdirSync5, renameSync as renameSync4 } from "fs";
6547
- import { join as join6 } from "path";
6663
+ import { readFileSync as readFileSync7, readdirSync as readdirSync5, renameSync as renameSync5 } from "fs";
6664
+ import { join as join7 } from "path";
6548
6665
 
6549
6666
  // src/pricing.ts
6550
6667
  var MODEL_PRICING = {
@@ -6601,17 +6718,17 @@ function unprocessedLogFiles(dir) {
6601
6718
  }
6602
6719
  }
6603
6720
  function accountRunCost(deps, run) {
6604
- const dir = join6(deps.runLogsDir, String(run.id));
6721
+ const dir = join7(deps.runLogsDir, String(run.id));
6605
6722
  const files = unprocessedLogFiles(dir);
6606
6723
  if (files.length === 0) return;
6607
6724
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
6608
6725
  let added = 0;
6609
6726
  const processed = [];
6610
6727
  for (const f of files) {
6611
- const full = join6(dir, f);
6728
+ const full = join7(dir, f);
6612
6729
  let raw = null;
6613
6730
  try {
6614
- raw = readFileSync6(full, "utf8");
6731
+ raw = readFileSync7(full, "utf8");
6615
6732
  } catch {
6616
6733
  continue;
6617
6734
  }
@@ -6628,7 +6745,7 @@ function accountRunCost(deps, run) {
6628
6745
  if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
6629
6746
  for (const full of processed) {
6630
6747
  try {
6631
- renameSync4(full, `${full}.done`);
6748
+ renameSync5(full, `${full}.done`);
6632
6749
  } catch {
6633
6750
  }
6634
6751
  }
@@ -6638,11 +6755,11 @@ function makeCostAccountant(deps) {
6638
6755
  }
6639
6756
 
6640
6757
  // src/runOutcome.ts
6641
- import { readdirSync as readdirSync6, readFileSync as readFileSync7 } from "fs";
6642
- import { join as join7 } from "path";
6758
+ import { readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
6759
+ import { join as join8 } from "path";
6643
6760
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
6644
6761
  function classifyExit(runLogsDir, runId) {
6645
- const dir = join7(runLogsDir, String(runId));
6762
+ const dir = join8(runLogsDir, String(runId));
6646
6763
  let files;
6647
6764
  try {
6648
6765
  files = readdirSync6(dir);
@@ -6659,7 +6776,7 @@ function classifyExit(runLogsDir, runId) {
6659
6776
  if (!latest) return "unknown";
6660
6777
  let raw;
6661
6778
  try {
6662
- raw = readFileSync7(join7(dir, latest.name), "utf8");
6779
+ raw = readFileSync8(join8(dir, latest.name), "utf8");
6663
6780
  } catch {
6664
6781
  return "unknown";
6665
6782
  }
@@ -6673,8 +6790,8 @@ function makeExitClassifier(runLogsDir) {
6673
6790
  init_git();
6674
6791
 
6675
6792
  // src/deps.ts
6676
- import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync8 } from "fs";
6677
- import { join as join9 } from "path";
6793
+ import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync9 } from "fs";
6794
+ import { join as join10 } from "path";
6678
6795
  import { execa } from "execa";
6679
6796
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
6680
6797
  var MAX_PLAN_COMMANDS = 12;
@@ -6690,7 +6807,7 @@ async function runCommand(cmd, worktree) {
6690
6807
  }
6691
6808
  function safeReadText(p) {
6692
6809
  try {
6693
- return readFileSync8(p, "utf8");
6810
+ return readFileSync9(p, "utf8");
6694
6811
  } catch {
6695
6812
  return null;
6696
6813
  }
@@ -6730,21 +6847,21 @@ function overridePlan(override) {
6730
6847
  };
6731
6848
  }
6732
6849
  function detectPackageManager(worktree, io = {}) {
6733
- const exists = io.exists ?? existsSync8;
6850
+ const exists = io.exists ?? existsSync9;
6734
6851
  const readText = io.readText ?? safeReadText;
6735
- const hasPkgJson = exists(join9(worktree, "package.json"));
6852
+ const hasPkgJson = exists(join10(worktree, "package.json"));
6736
6853
  if (!hasPkgJson) {
6737
- if (exists(join9(worktree, "deno.json")) || exists(join9(worktree, "deno.jsonc")) || exists(join9(worktree, "deno.lock"))) return "deno";
6854
+ if (exists(join10(worktree, "deno.json")) || exists(join10(worktree, "deno.jsonc")) || exists(join10(worktree, "deno.lock"))) return "deno";
6738
6855
  return null;
6739
6856
  }
6740
- const pkg = readJson(readText, join9(worktree, "package.json"));
6857
+ const pkg = readJson(readText, join10(worktree, "package.json"));
6741
6858
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
6742
6859
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
6743
- if (exists(join9(worktree, "bun.lock")) || exists(join9(worktree, "bun.lockb"))) return "bun";
6744
- if (exists(join9(worktree, "pnpm-lock.yaml"))) return "pnpm";
6745
- if (exists(join9(worktree, "yarn.lock"))) return "yarn";
6746
- if (exists(join9(worktree, "package-lock.json"))) return "npm";
6747
- if (exists(join9(worktree, "deno.lock"))) return "deno";
6860
+ if (exists(join10(worktree, "bun.lock")) || exists(join10(worktree, "bun.lockb"))) return "bun";
6861
+ if (exists(join10(worktree, "pnpm-lock.yaml"))) return "pnpm";
6862
+ if (exists(join10(worktree, "yarn.lock"))) return "yarn";
6863
+ if (exists(join10(worktree, "package-lock.json"))) return "npm";
6864
+ if (exists(join10(worktree, "deno.lock"))) return "deno";
6748
6865
  return "npm";
6749
6866
  }
6750
6867
  function jsInstallCommand(pm) {
@@ -6797,7 +6914,7 @@ var ECOSYSTEM_MARKERS = [
6797
6914
  // конфигурация — дело сборки, не подготовки
6798
6915
  ];
6799
6916
  function detectPlan(worktree, io) {
6800
- const exists = io.exists ?? existsSync8;
6917
+ const exists = io.exists ?? existsSync9;
6801
6918
  const listFiles = io.listFiles ?? safeListFiles;
6802
6919
  const pm = detectPackageManager(worktree, io);
6803
6920
  if (pm) {
@@ -6805,7 +6922,7 @@ function detectPlan(worktree, io) {
6805
6922
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
6806
6923
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
6807
6924
  // заново, но он дешёвый: тянет из того же общего кэша.
6808
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join9(worktree, "node_modules"), source: "detect" }
6925
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join10(worktree, "node_modules"), source: "detect" }
6809
6926
  ];
6810
6927
  for (const target of workspaceBuildTargets(worktree, io)) {
6811
6928
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -6813,12 +6930,12 @@ function detectPlan(worktree, io) {
6813
6930
  return { source: "detect", ecosystem: `node/${pm}`, commands };
6814
6931
  }
6815
6932
  for (const marker of ECOSYSTEM_MARKERS) {
6816
- const hit = marker.files.some((f) => exists(join9(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
6933
+ const hit = marker.files.some((f) => exists(join10(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
6817
6934
  if (!hit) continue;
6818
6935
  let ecosystem = marker.ecosystem;
6819
6936
  let command = marker.command;
6820
6937
  for (const refine of marker.refine ?? []) {
6821
- if (!exists(join9(worktree, refine.file))) continue;
6938
+ if (!exists(join10(worktree, refine.file))) continue;
6822
6939
  ecosystem = refine.ecosystem;
6823
6940
  command = refine.command;
6824
6941
  break;
@@ -6875,12 +6992,12 @@ var CI_INSTALL_PATTERNS = [
6875
6992
  function ciWorkflowPlan(worktree, io) {
6876
6993
  const readText = io.readText ?? safeReadText;
6877
6994
  const listFiles = io.listFiles ?? safeListFiles;
6878
- const dir = join9(worktree, ".github", "workflows");
6995
+ const dir = join10(worktree, ".github", "workflows");
6879
6996
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
6880
6997
  const seen = /* @__PURE__ */ new Set();
6881
6998
  const commands = [];
6882
6999
  for (const name of files.sort()) {
6883
- const yaml = readText(join9(dir, name));
7000
+ const yaml = readText(join10(dir, name));
6884
7001
  if (yaml === null) continue;
6885
7002
  for (const raw of workflowRunLines(yaml)) {
6886
7003
  const line = raw.replace(/\s+/g, " ").trim();
@@ -6895,7 +7012,7 @@ function ciWorkflowPlan(worktree, io) {
6895
7012
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
6896
7013
  }
6897
7014
  function workspaceGlobs(worktree, readText) {
6898
- const yaml = readText(join9(worktree, "pnpm-workspace.yaml"));
7015
+ const yaml = readText(join10(worktree, "pnpm-workspace.yaml"));
6899
7016
  if (yaml !== null) {
6900
7017
  const globs = [];
6901
7018
  let inPackages = false;
@@ -6915,7 +7032,7 @@ function workspaceGlobs(worktree, readText) {
6915
7032
  }
6916
7033
  if (globs.length) return globs;
6917
7034
  }
6918
- const pkg = readJson(readText, join9(worktree, "package.json"));
7035
+ const pkg = readJson(readText, join10(worktree, "package.json"));
6919
7036
  const ws = pkg?.workspaces;
6920
7037
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
6921
7038
  return list.filter((g) => typeof g === "string");
@@ -6926,14 +7043,14 @@ function workspaceDirs(worktree, globs, io) {
6926
7043
  if (glob.startsWith("!")) continue;
6927
7044
  const star = glob.indexOf("*");
6928
7045
  if (star === -1) {
6929
- const dir = join9(worktree, glob);
6930
- if (io.exists(join9(dir, "package.json"))) dirs.add(dir);
7046
+ const dir = join10(worktree, glob);
7047
+ if (io.exists(join10(dir, "package.json"))) dirs.add(dir);
6931
7048
  continue;
6932
7049
  }
6933
- const base3 = join9(worktree, glob.slice(0, star).replace(/\/$/, ""));
7050
+ const base3 = join10(worktree, glob.slice(0, star).replace(/\/$/, ""));
6934
7051
  for (const name of io.listDirs(base3)) {
6935
- const dir = join9(base3, name);
6936
- if (io.exists(join9(dir, "package.json"))) dirs.add(dir);
7052
+ const dir = join10(base3, name);
7053
+ if (io.exists(join10(dir, "package.json"))) dirs.add(dir);
6937
7054
  }
6938
7055
  }
6939
7056
  return [...dirs];
@@ -6954,18 +7071,18 @@ function entryPaths(pkg) {
6954
7071
  return out;
6955
7072
  }
6956
7073
  function workspaceBuildTargets(worktree, io = {}) {
6957
- const exists = io.exists ?? existsSync8;
7074
+ const exists = io.exists ?? existsSync9;
6958
7075
  const readText = io.readText ?? safeReadText;
6959
7076
  const listDirs = io.listDirs ?? safeListDirs;
6960
7077
  const globs = workspaceGlobs(worktree, readText);
6961
7078
  if (!globs.length) return [];
6962
7079
  const pkgs = [];
6963
7080
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
6964
- const pkg = readJson(readText, join9(dir, "package.json"));
7081
+ const pkg = readJson(readText, join10(dir, "package.json"));
6965
7082
  if (!pkg || typeof pkg.name !== "string") continue;
6966
7083
  const scripts = pkg.scripts ?? {};
6967
7084
  const entries = entryPaths(pkg);
6968
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join9(dir, e)));
7085
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join10(dir, e)));
6969
7086
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
6970
7087
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
6971
7088
  }
@@ -6993,7 +7110,7 @@ function planPrepare(worktree, io = {}) {
6993
7110
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
6994
7111
  }
6995
7112
  async function ensureWorktreeDeps(worktree, io = {}) {
6996
- const exists = io.exists ?? existsSync8;
7113
+ const exists = io.exists ?? existsSync9;
6997
7114
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
6998
7115
  const approve = io.approve ?? (() => true);
6999
7116
  const plan = planPrepare(worktree, io);
@@ -7892,13 +8009,74 @@ import { createHash as createHash2 } from "crypto";
7892
8009
 
7893
8010
  // src/anomalies.ts
7894
8011
  init_src();
8012
+ var LEVEL_RANK = { note: 0, feed: 1, decision: 2 };
8013
+ function plural2(n, forms) {
8014
+ const mod100 = n % 100;
8015
+ if (mod100 >= 11 && mod100 <= 14) return forms[2];
8016
+ const mod10 = n % 10;
8017
+ if (mod10 === 1) return forms[0];
8018
+ if (mod10 >= 2 && mod10 <= 4) return forms[1];
8019
+ return forms[2];
8020
+ }
8021
+ function collapseSignals(candidates) {
8022
+ const single = (c) => {
8023
+ const signal = { key: c.key, ...c.build() };
8024
+ return { signal, keys: [c.key], events: signal.runId != null ? [{ runId: signal.runId, text: signal.body }] : [] };
8025
+ };
8026
+ const out = [];
8027
+ const grouped = /* @__PURE__ */ new Map();
8028
+ for (const c of candidates) {
8029
+ if (!c.group) {
8030
+ out.push(single(c));
8031
+ continue;
8032
+ }
8033
+ const bucket = grouped.get(c.group.id);
8034
+ if (bucket) bucket.members.push(c);
8035
+ else grouped.set(c.group.id, { group: c.group, members: [c] });
8036
+ }
8037
+ for (const { group, members } of grouped.values()) {
8038
+ const first = members[0];
8039
+ if (members.length === 1 && first) {
8040
+ out.push(single(first));
8041
+ continue;
8042
+ }
8043
+ const built = members.map((m) => ({ key: m.key, signal: m.build() }));
8044
+ const level = built.reduce(
8045
+ (acc, b) => LEVEL_RANK[b.signal.level ?? "decision"] > LEVEL_RANK[acc] ? b.signal.level ?? "decision" : acc,
8046
+ "note"
8047
+ );
8048
+ out.push({
8049
+ signal: {
8050
+ key: `${group.id}_group`,
8051
+ title: group.title(built.length),
8052
+ body: joinCapped(built.map((b) => b.signal.title)),
8053
+ level
8054
+ },
8055
+ keys: built.map((b) => b.key),
8056
+ events: built.flatMap((b) => b.signal.runId != null ? [{ runId: b.signal.runId, text: b.signal.body }] : [])
8057
+ });
8058
+ }
8059
+ return out;
8060
+ }
8061
+ var LAST_REMINDER_SUFFIX = "\u041D\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u044E \u043E\u0431 \u044D\u0442\u043E\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0440\u0430\u0437 \u2014 \u0440\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0438\u043B\u0438 \u0441\u043D\u0438\u043C\u0438 \u043F\u0440\u043E\u0433\u043E\u043D, \u0434\u0430\u043B\u044C\u0448\u0435 \u0431\u0443\u0434\u0443 \u043C\u043E\u043B\u0447\u0430\u0442\u044C.";
7895
8062
  var AnomalyTracker = class {
7896
- constructor(renotifyCooldownMs) {
8063
+ constructor(renotifyCooldownMs, opts = {}) {
7897
8064
  this.renotifyCooldownMs = renotifyCooldownMs;
8065
+ this.maxRepeats = opts.maxRepeats && opts.maxRepeats > 0 ? Math.trunc(opts.maxRepeats) : Number.POSITIVE_INFINITY;
8066
+ this.io = opts.io;
8067
+ const restored = safeLoad(this.io);
8068
+ if (restored) {
8069
+ for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
8070
+ for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
8071
+ for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
8072
+ }
7898
8073
  }
7899
8074
  renotifyCooldownMs;
7900
8075
  since = /* @__PURE__ */ new Map();
7901
8076
  lastFiredAt = /* @__PURE__ */ new Map();
8077
+ fires = /* @__PURE__ */ new Map();
8078
+ maxRepeats;
8079
+ io;
7902
8080
  /** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
7903
8081
  eligible(now, candidates) {
7904
8082
  const result = [];
@@ -7910,17 +8088,58 @@ var AnomalyTracker = class {
7910
8088
  if (now - startedAt < c.minDurationMs) continue;
7911
8089
  const lastFired = this.lastFiredAt.get(c.key);
7912
8090
  if (lastFired != null && now - lastFired < this.renotifyCooldownMs) continue;
8091
+ if ((this.fires.get(c.key) ?? 0) >= this.maxRepeats) continue;
7913
8092
  result.push(c);
7914
8093
  }
7915
- for (const k of [...this.since.keys()]) if (!seen.has(k)) this.since.delete(k);
7916
- for (const k of [...this.lastFiredAt.keys()]) if (!seen.has(k)) this.lastFiredAt.delete(k);
8094
+ let pruned = false;
8095
+ for (const map of [this.since, this.lastFiredAt, this.fires]) {
8096
+ for (const k of [...map.keys()]) {
8097
+ if (!seen.has(k)) {
8098
+ map.delete(k);
8099
+ pruned = true;
8100
+ }
8101
+ }
8102
+ }
8103
+ if (pruned) this.persist();
7917
8104
  return result;
7918
8105
  }
7919
8106
  /** Доставка signal с этим key успешно ушла в этот момент — начинает кулдаун повтора. */
7920
8107
  commit(key, now) {
7921
8108
  this.lastFiredAt.set(key, now);
8109
+ this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
8110
+ this.persist();
8111
+ }
8112
+ /**
8113
+ * Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
8114
+ * формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
8115
+ * Без потолка (maxRepeats не задан) — всегда false.
8116
+ */
8117
+ isFinalRepeat(key) {
8118
+ if (!Number.isFinite(this.maxRepeats)) return false;
8119
+ return (this.fires.get(key) ?? 0) === this.maxRepeats - 1;
8120
+ }
8121
+ persist() {
8122
+ if (!this.io) return;
8123
+ try {
8124
+ this.io.save({
8125
+ since: Object.fromEntries(this.since),
8126
+ lastFiredAt: Object.fromEntries(this.lastFiredAt),
8127
+ fires: Object.fromEntries(this.fires)
8128
+ });
8129
+ } catch (e) {
8130
+ console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
8131
+ }
7922
8132
  }
7923
8133
  };
8134
+ function safeLoad(io) {
8135
+ if (!io) return null;
8136
+ try {
8137
+ return io.load();
8138
+ } catch (e) {
8139
+ console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E (\u043D\u0430\u0447\u0438\u043D\u0430\u044E \u0441 \u0447\u0438\u0441\u0442\u043E\u0433\u043E):", e);
8140
+ return null;
8141
+ }
8142
+ }
7924
8143
  var REVIEW_LIKE_STATUS = "review";
7925
8144
  var MAX_BODY_CHARS = 1800;
7926
8145
  function ageMs(now, iso) {
@@ -7929,6 +8148,12 @@ function ageMs(now, iso) {
7929
8148
  function runLabel(run) {
7930
8149
  return run.name || `#${run.id}`;
7931
8150
  }
8151
+ function runTitle(run, what) {
8152
+ return `${run.task_slug || runLabel(run)}: ${what}`;
8153
+ }
8154
+ function droneOf(run) {
8155
+ return `\u0414\u0440\u043E\u043D ${runLabel(run)}`;
8156
+ }
7932
8157
  function hrefOf(url) {
7933
8158
  return url ? { href: url } : {};
7934
8159
  }
@@ -7946,31 +8171,62 @@ function joinCapped(labels) {
7946
8171
  }
7947
8172
  function buildAnomalyCandidates(db, now, thresholds, opts) {
7948
8173
  const candidates = [];
7949
- const allRuns = db.listRuns();
8174
+ const allRuns = db.listRuns().filter((r) => r.superseded_by == null);
8175
+ const killedByOperator = (run) => {
8176
+ const q = opts.stopQuiet;
8177
+ if (!q) return false;
8178
+ const changedAt = sqliteMs(run.updated_at);
8179
+ return changedAt >= q.from && (q.to == null || changedAt <= q.to);
8180
+ };
7950
8181
  const recentFailed = allRuns.filter(
7951
- (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs
8182
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
7952
8183
  );
7953
8184
  if (recentFailed.length >= thresholds.failedSpikeCount) {
7954
8185
  candidates.push({
7955
8186
  key: "failed_spike",
7956
8187
  minDurationMs: 0,
7957
8188
  build: () => ({
7958
- title: `\u0420\u043E\u0439: ${recentFailed.length} \u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0437\u0430 ${Math.round(thresholds.failedSpikeWindowMs / 6e4)} \u043C\u0438\u043D`,
7959
- body: `${joinCapped(recentFailed.map(runLabel))} \u2014 \u0440\u043E\u0441\u0442 failed \u0437\u0430 \u043E\u043A\u043D\u043E, \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0441\u0438\u0441\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0443\u044E \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443, \u0430 \u043D\u0435 \u0435\u0434\u0438\u043D\u0438\u0447\u043D\u044B\u0439 \u0441\u0431\u043E\u0439.`
8189
+ // Единственный ПУШ про падения (TASWM-327): одиночное падение рой перезапустит сам
8190
+ // (оно уходит в ленту, см. ниже), а «падают все» — это уже систематика, где без
8191
+ // человека не поедет. Раньше владелец получал и восемь адресных пушей, и агрегат
8192
+ // поверх них.
8193
+ level: "decision",
8194
+ title: `\u0420\u043E\u0439: ${recentFailed.length} ${plural2(recentFailed.length, ["\u0443\u043F\u0430\u0432\u0448\u0438\u0439 \u043F\u0440\u043E\u0433\u043E\u043D", "\u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430", "\u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432"])} \u0437\u0430 ${Math.round(thresholds.failedSpikeWindowMs / 6e4)} \u043C\u0438\u043D`,
8195
+ body: `${joinCapped(recentFailed.map((r) => r.task_slug || runLabel(r)))} \u2014 \u0440\u043E\u0441\u0442 failed \u0437\u0430 \u043E\u043A\u043D\u043E, \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0441\u0438\u0441\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0443\u044E \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443, \u0430 \u043D\u0435 \u0435\u0434\u0438\u043D\u0438\u0447\u043D\u044B\u0439 \u0441\u0431\u043E\u0439.`
7960
8196
  })
7961
8197
  });
7962
8198
  }
8199
+ const downBySlug = /* @__PURE__ */ new Map();
8200
+ for (const r of allRuns) {
8201
+ if (r.status !== "failed" && r.status !== "stalled") continue;
8202
+ if (r.resolves_run_id != null || r.reviews_run_id != null) continue;
8203
+ downBySlug.set(r.task_slug, (downBySlug.get(r.task_slug) ?? 0) + 1);
8204
+ }
8205
+ const isRepeatDown = (run) => (downBySlug.get(run.task_slug) ?? 0) >= 2 || run.respawns > 0;
8206
+ const DOWN_GROUP = {
8207
+ id: "run_down",
8208
+ title: (n) => `${n} ${plural2(n, ["\u0434\u0440\u043E\u043D \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B", "\u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B\u0438", "\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B\u0438"])}`
8209
+ };
8210
+ const PR_GROUP = {
8211
+ id: "pr_stuck",
8212
+ title: (n) => `${n} PR ${plural2(n, ["\u0436\u0434\u0451\u0442", "\u0436\u0434\u0443\u0442", "\u0436\u0434\u0443\u0442"])} \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044F`
8213
+ };
7963
8214
  for (const run of allRuns) {
7964
8215
  if (run.status !== "stalled") continue;
7965
8216
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8217
+ if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8218
+ if (killedByOperator(run)) continue;
8219
+ const repeat = isRepeatDown(run);
7966
8220
  candidates.push({
7967
8221
  key: `run_stalled:${run.id}`,
7968
8222
  minDurationMs: 0,
7969
8223
  // само по себе попадание в stalled УЖЕ прошло свой STALL_MS/RESPAWN_LIMIT
8224
+ group: DOWN_GROUP,
7970
8225
  build: () => ({
7971
8226
  runId: run.id,
7972
- title: `\u0414\u0440\u043E\u043D ${runLabel(run)} \u0437\u0430\u0432\u0438\u0441`,
7973
- body: `\u041F\u0440\u043E\u0433\u043E\u043D ${run.task_slug} \u043F\u043E\u043C\u0435\u0447\u0435\u043D stalled \u2014 \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`,
8227
+ level: repeat ? "decision" : "feed",
8228
+ title: runTitle(run, "\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441"),
8229
+ body: `${droneOf(run)}: \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u043E\u0438\u0442.${repeat ? " \u041F\u043E \u044D\u0442\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435 \u044D\u0442\u043E \u0443\u0436\u0435 \u0432\u0442\u043E\u0440\u043E\u0439 \u0437\u0430\u0445\u043E\u0434 \u2014 \u0441\u0430\u043C \u043F\u043E \u0441\u0435\u0431\u0435 \u043E\u043D \u043D\u0435 \u0440\u0430\u0437\u043E\u0439\u0434\u0451\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440." : ""}`,
7974
8230
  ...hrefOf(run.task_url)
7975
8231
  })
7976
8232
  });
@@ -7978,13 +8234,18 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
7978
8234
  for (const run of allRuns) {
7979
8235
  if (run.status !== "failed") continue;
7980
8236
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8237
+ if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8238
+ if (killedByOperator(run)) continue;
8239
+ const repeat = isRepeatDown(run);
7981
8240
  candidates.push({
7982
8241
  key: `run_failed:${run.id}`,
7983
8242
  minDurationMs: 0,
8243
+ group: DOWN_GROUP,
7984
8244
  build: () => ({
7985
8245
  runId: run.id,
7986
- title: `\u0414\u0440\u043E\u043D ${runLabel(run)} \u0443\u043F\u0430\u043B`,
7987
- body: `\u041F\u0440\u043E\u0433\u043E\u043D ${run.task_slug} \u043F\u043E\u043C\u0435\u0447\u0435\u043D failed \u2014 \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`,
8246
+ level: repeat ? "decision" : "feed",
8247
+ title: runTitle(run, "\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B"),
8248
+ body: `${droneOf(run)}: \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u043E\u043C\u0435\u0447\u0435\u043D failed.${repeat ? " \u041F\u043E \u044D\u0442\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0435 \u043F\u0430\u0434\u0430\u0435\u0442 \u0443\u0436\u0435 \u0432\u0442\u043E\u0440\u043E\u0439 \u0440\u0430\u0437 \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u043D\u0435 \u0432\u044B\u0431\u0435\u0440\u0435\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440." : " \u0420\u043E\u0439 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0430\u043C."}`,
7988
8249
  ...hrefOf(run.task_url)
7989
8250
  })
7990
8251
  });
@@ -7996,6 +8257,11 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
7996
8257
  key: "queue_stalled",
7997
8258
  minDurationMs: thresholds.queueStalledMs,
7998
8259
  build: () => ({
8260
+ // В ленту, без пуша (TASWM-327): «очередь стоит» — состояние, из которого рой
8261
+ // штатно выходит сам (гейт откроется / шторм отпустит / слот освободится), а
8262
+ // настоящий тупик ловит и РАЗБИРАЕТ сторож прогресса (watchdog.ts) — вот он и
8263
+ // звонит человеку, когда исчерпал лестницу.
8264
+ level: "feed",
7999
8265
  title: `\u0420\u043E\u0439 \u0441\u0442\u043E\u0438\u0442: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
8000
8266
  body: `${queuedCount} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432) \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0440\u043E\u043D\u0430 \u2265${Math.round(thresholds.queueStalledMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, \u0433\u0435\u0439\u0442 (\u0440\u0435\u0441\u0443\u0440\u0441\u044B/maxDrones/\u0448\u0442\u043E\u0440\u043C) \u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043D\u0442\u0435\u0439\u043A \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E.`
8001
8267
  })
@@ -8007,6 +8273,10 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8007
8273
  key: "resource-gate-stall",
8008
8274
  minDurationMs: thresholds.resourceGateStallMs,
8009
8275
  build: () => ({
8276
+ // В ленту: гейт откроется сам, как только машина разгрузится. Отдельный ПУШ про
8277
+ // «машина не тянет, вот цена» шлёт reliefCost.ts — там есть числа и рекомендация,
8278
+ // то есть человеку действительно есть что решать.
8279
+ level: "feed",
8010
8280
  title: `\u0420\u043E\u0439 \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442 ${Math.round(thresholds.resourceGateStallMs / 6e4)} \u043C\u0438\u043D \u2014 \u0434\u0435\u0440\u0436\u0438\u0442 \u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442`,
8011
8281
  body: `${queuedCount} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432) \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0436\u0434\u0443\u0442 \u0441\u043F\u0430\u0432\u043D\u0430: ${reason}. \u0420\u0430\u0437\u0433\u0440\u0443\u0437\u0438 \u043C\u0430\u0448\u0438\u043D\u0443.`
8012
8282
  })
@@ -8020,10 +8290,13 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8020
8290
  candidates.push({
8021
8291
  key: `pr_stuck_merge:${run.id}`,
8022
8292
  minDurationMs: thresholds.prStuckMergeMs,
8293
+ group: PR_GROUP,
8023
8294
  build: () => ({
8024
8295
  runId: run.id,
8025
- title: `PR ${runLabel(run)} \u043E\u0434\u043E\u0431\u0440\u0435\u043D, \u043D\u043E \u043D\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D`,
8026
- body: `${run.task_slug}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B APPROVE \u2265${Math.round(thresholds.prStuckMergeMs / 6e4)} \u043C\u0438\u043D \u043D\u0430\u0437\u0430\u0434, PR \u0432\u0441\u0451 \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 \u2014 ${run.pr_url}.`,
8296
+ // Пуш: ревью одобрено, дальше ход человека — ровно тот сигнал, который тонул в шуме.
8297
+ level: "decision",
8298
+ title: runTitle(run, "PR \u043E\u0434\u043E\u0431\u0440\u0435\u043D, \u043D\u043E \u043D\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D"),
8299
+ body: `${droneOf(run)}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B APPROVE \u2265${Math.round(thresholds.prStuckMergeMs / 6e4)} \u043C\u0438\u043D \u043D\u0430\u0437\u0430\u0434, PR \u0432\u0441\u0451 \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 \u2014 ${run.pr_url}.`,
8027
8300
  ...hrefOf(run.task_url)
8028
8301
  })
8029
8302
  });
@@ -8031,10 +8304,14 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8031
8304
  candidates.push({
8032
8305
  key: `pr_no_reviewer:${run.id}`,
8033
8306
  minDurationMs: thresholds.prNoReviewerMs,
8307
+ group: PR_GROUP,
8034
8308
  build: () => ({
8035
8309
  runId: run.id,
8036
- title: `PR ${runLabel(run)} \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430`,
8037
- body: `${run.task_slug}: PR \u043E\u0442\u043A\u0440\u044B\u0442 \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0442\u0430\u043A \u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (SWARM_REVIEW_CAP/maxDrones/cutoff?) \u2014 ${run.pr_url}.`,
8310
+ // В ленту: ревьюер не поднялся из-за гейта (cap/maxDrones/cutoff) — как только
8311
+ // гейт откроется, рой поднимет его сам, решения человека тут не требуется.
8312
+ level: "feed",
8313
+ title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
8314
+ body: `${droneOf(run)}: PR \u043E\u0442\u043A\u0440\u044B\u0442 \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0442\u0430\u043A \u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (SWARM_REVIEW_CAP/maxDrones/cutoff?) \u2014 ${run.pr_url}.`,
8038
8315
  ...hrefOf(run.task_url)
8039
8316
  })
8040
8317
  });
@@ -8042,10 +8319,14 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8042
8319
  candidates.push({
8043
8320
  key: `pr_stuck_changes:${run.id}`,
8044
8321
  minDurationMs: thresholds.prNoReviewerMs,
8322
+ group: PR_GROUP,
8045
8323
  build: () => ({
8046
8324
  runId: run.id,
8047
- title: `PR ${runLabel(run)} \u0437\u0430\u0441\u0442\u0440\u044F\u043B \u043D\u0430 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB`,
8048
- body: `${run.task_slug}: \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044F \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u043D\u043E \u043D\u0438 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u043D\u0438 \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u2014 \u0440\u0435\u0432\u044C\u044E \u0432\u0441\u0442\u0430\u043B\u043E (\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 \u043D\u0430 \u0444\u0438\u043A\u0441\u0435 \u0434\u043E done()?) \u2014 ${run.pr_url}.`,
8325
+ // Пуш: конвейер ревью встал так, что рой этого сам не расшивает (дрон умер на
8326
+ // фиксе до done()) — дальше без человека никак.
8327
+ level: "decision",
8328
+ title: runTitle(run, "PR \u0437\u0430\u0441\u0442\u0440\u044F\u043B \u043D\u0430 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB"),
8329
+ body: `${droneOf(run)}: \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044F \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u043D\u043E \u043D\u0438 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u043D\u0438 \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u2014 \u0440\u0435\u0432\u044C\u044E \u0432\u0441\u0442\u0430\u043B\u043E (\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 \u043D\u0430 \u0444\u0438\u043A\u0441\u0435 \u0434\u043E done()?) \u2014 ${run.pr_url}.`,
8049
8330
  ...hrefOf(run.task_url)
8050
8331
  })
8051
8332
  });
@@ -8054,14 +8335,21 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8054
8335
  const runsById = new Map(allRuns.map((r) => [r.id, r]));
8055
8336
  for (const q of db.openQuestions()) {
8056
8337
  const run = runsById.get(q.run_id);
8338
+ if (!run) continue;
8057
8339
  candidates.push({
8058
8340
  key: `question_unanswered:${q.id}`,
8059
8341
  minDurationMs: thresholds.questionUnansweredMs,
8342
+ group: {
8343
+ id: "question_unanswered",
8344
+ title: (n) => `${n} ${plural2(n, ["\u0434\u0440\u043E\u043D \u0436\u0434\u0451\u0442", "\u0434\u0440\u043E\u043D\u0430 \u0436\u0434\u0443\u0442", "\u0434\u0440\u043E\u043D\u043E\u0432 \u0436\u0434\u0443\u0442"])} \u043E\u0442\u0432\u0435\u0442\u0430`
8345
+ },
8060
8346
  build: () => ({
8061
8347
  runId: q.run_id,
8062
- title: `${run ? runLabel(run) : `\u043F\u0440\u043E\u0433\u043E\u043D #${q.run_id}`} \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430`,
8063
- body: `\u0412\u043E\u043F\u0440\u043E\u0441 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2265${Math.round(thresholds.questionUnansweredMs / 6e4)} \u043C\u0438\u043D: \xAB${q.text}\xBB.`,
8064
- ...hrefOf(run?.task_url)
8348
+ // Пуш — это ровно «без тебя не поедет»: дрон стоит и жжёт время сессии.
8349
+ level: "decision",
8350
+ title: runTitle(run, "\u0434\u0440\u043E\u043D \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430"),
8351
+ body: `${droneOf(run)} \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2265${Math.round(thresholds.questionUnansweredMs / 6e4)} \u043C\u0438\u043D: \xAB${q.text}\xBB.`,
8352
+ ...hrefOf(run.task_url)
8065
8353
  })
8066
8354
  });
8067
8355
  }
@@ -8071,6 +8359,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8071
8359
  key: "outbox_stuck",
8072
8360
  minDurationMs: thresholds.outboxStuckMs,
8073
8361
  build: () => ({
8362
+ // В ленту: недоступный прод рой переживает сам (outbox durable, флашится, когда
8363
+ // связь вернётся) — человеку тут нечего решать.
8364
+ level: "feed",
8074
8365
  title: `Outbox \u0440\u043E\u044F \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
8075
8366
  body: `${pending2} \u043E\u043F\u0435\u0440\u0430\u0446\u0438(\u0439) \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2265${Math.round(thresholds.outboxStuckMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u0440\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0434\u043E\u043B\u044C\u0448\u0435 \u043E\u0431\u044B\u0447\u043D\u043E\u0433\u043E \u0441\u0431\u043E\u044F.`
8076
8367
  })
@@ -8146,7 +8437,14 @@ async function liftNotes(opts, result) {
8146
8437
  continue;
8147
8438
  }
8148
8439
  try {
8149
- await opts.notify({ key, title: `\u0414\u0440\u043E\u043D #${run.id}: \u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438`, body: text, ...hrefOf(opts.hrefFor?.(run.id)), runId: run.id });
8440
+ await opts.notify({
8441
+ key,
8442
+ level: "note",
8443
+ title: `${run.task_slug || `#${run.id}`}: \u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438`,
8444
+ body: `\u0414\u0440\u043E\u043D ${run.name || `#${run.id}`}: ${text}`,
8445
+ ...hrefOf(opts.hrefFor?.(run.id)),
8446
+ runId: run.id
8447
+ });
8150
8448
  opts.seenNotes.add(key);
8151
8449
  result.notes.push(run.id);
8152
8450
  } catch (e) {
@@ -8357,8 +8655,8 @@ function makeExclusiveReader(opts) {
8357
8655
  }
8358
8656
 
8359
8657
  // src/envGate.ts
8360
- import { existsSync as existsSync9 } from "fs";
8361
- import { join as join15, delimiter } from "path";
8658
+ import { existsSync as existsSync10 } from "fs";
8659
+ import { join as join16, delimiter } from "path";
8362
8660
  import { createConnection } from "net";
8363
8661
  import { execa as execa2 } from "execa";
8364
8662
  var PROBE_TIMEOUT_MS = 2500;
@@ -8409,8 +8707,8 @@ var COMPOSE_MARKERS = [
8409
8707
  "compose.yaml"
8410
8708
  ];
8411
8709
  function detectRequirements(repo, io = {}) {
8412
- const exists = io.exists ?? existsSync9;
8413
- const marker = COMPOSE_MARKERS.find((f) => exists(join15(repo, f)));
8710
+ const exists = io.exists ?? existsSync10;
8711
+ const marker = COMPOSE_MARKERS.find((f) => exists(join16(repo, f)));
8414
8712
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
8415
8713
  }
8416
8714
  function resolveRequirements(repo, declared, io = {}) {
@@ -8429,10 +8727,10 @@ async function dockerAlive() {
8429
8727
  }
8430
8728
  }
8431
8729
  async function hasCommand(name, env) {
8432
- if (name.includes("/")) return existsSync9(name);
8730
+ if (name.includes("/")) return existsSync10(name);
8433
8731
  const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
8434
8732
  const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
8435
- return dirs.some((dir) => exts.some((ext) => existsSync9(join15(dir, name + ext))));
8733
+ return dirs.some((dir) => exts.some((ext) => existsSync10(join16(dir, name + ext))));
8436
8734
  }
8437
8735
  async function portOpen(host, port) {
8438
8736
  return new Promise((resolve3) => {
@@ -8550,7 +8848,7 @@ import { serve as serve2 } from "@hono/node-server";
8550
8848
 
8551
8849
  // src/diff.ts
8552
8850
  import { statSync as statSync5 } from "fs";
8553
- import { join as join16 } from "path";
8851
+ import { join as join17 } from "path";
8554
8852
  import { $ as $4 } from "execa";
8555
8853
  var MAX_FILES = 200;
8556
8854
  var MAX_LINES_PER_FILE = 1500;
@@ -8718,7 +9016,7 @@ async function collectRunDiff(worktree, opts = {}) {
8718
9016
  files.push(pf);
8719
9017
  }
8720
9018
  }
8721
- files.sort((a, b) => mtime(join16(worktree, b.path)) - mtime(join16(worktree, a.path)));
9019
+ files.sort((a, b) => mtime(join17(worktree, b.path)) - mtime(join17(worktree, a.path)));
8722
9020
  const truncatedFiles = files.length > MAX_FILES;
8723
9021
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
8724
9022
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -8734,12 +9032,71 @@ async function collectRunDiff(worktree, opts = {}) {
8734
9032
  };
8735
9033
  }
8736
9034
 
9035
+ // package.json
9036
+ var package_default = {
9037
+ name: "@taskless-app/swarm",
9038
+ version: "0.4.3",
9039
+ type: "module",
9040
+ description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9041
+ keywords: [
9042
+ "ai-agents",
9043
+ "coding-agents",
9044
+ "swarm",
9045
+ "orchestration",
9046
+ "autonomous-agents",
9047
+ "mcp",
9048
+ "taskless",
9049
+ "cli"
9050
+ ],
9051
+ license: "SEE LICENSE IN LICENSE",
9052
+ author: "Ilya Smurov <ilya@smurov.com>",
9053
+ homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
9054
+ repository: {
9055
+ type: "git",
9056
+ url: "git+https://github.com/ilyasmurov/taskless.git",
9057
+ directory: "packages/swarm-daemon"
9058
+ },
9059
+ engines: {
9060
+ node: ">=22"
9061
+ },
9062
+ bin: {
9063
+ swarm: "dist/cli.js"
9064
+ },
9065
+ files: [
9066
+ "dist",
9067
+ "docs/HERO.md",
9068
+ "roles"
9069
+ ],
9070
+ scripts: {
9071
+ overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
9072
+ swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
9073
+ build: "tsup",
9074
+ start: "node dist/overmind.js",
9075
+ prepublishOnly: "pnpm run build",
9076
+ test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
9077
+ "test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
9078
+ "test:watch": "vitest",
9079
+ typecheck: "tsc -p tsconfig.json --noEmit",
9080
+ lint: "echo 'no lint in swarm-daemon yet'"
9081
+ },
9082
+ dependencies: {
9083
+ "@hono/node-server": "^2.0.12",
9084
+ "@modelcontextprotocol/sdk": "^1.29.0",
9085
+ execa: "^10.0.0",
9086
+ zod: "3.25.76"
9087
+ },
9088
+ optionalDependencies: {
9089
+ "better-sqlite3": "^12.11.1"
9090
+ }
9091
+ };
9092
+
8737
9093
  // src/web.ts
8738
9094
  init_storm();
8739
9095
  init_resources();
8740
9096
  init_config();
8741
9097
  init_droneChannel();
8742
9098
  init_oauth();
9099
+ var PACKAGE_VERSION = package_default.version;
8743
9100
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8744
9101
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
8745
9102
  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>`;
@@ -10020,6 +10377,36 @@ async function runDiff(db, id, now = Date.now()) {
10020
10377
  }
10021
10378
  var readNarratorLog = (log) => log?.() ?? [];
10022
10379
  var readRestartSnap = (restart, liveSessionIds2) => restart ? { phase: restart.phase, sessionIds: liveSessionIds2?.() ?? [] } : null;
10380
+ function pauseSourceOf(raw) {
10381
+ return raw === "cli" || raw === "remote" ? raw : "web";
10382
+ }
10383
+ function setPausedVia(deps, paused, source, reason) {
10384
+ const control = deps.control;
10385
+ if (control.set) {
10386
+ control.set(paused, { source, reason });
10387
+ return;
10388
+ }
10389
+ control.paused = paused;
10390
+ deps.narrate?.(
10391
+ 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."
10392
+ );
10393
+ }
10394
+ function daemonStatus(deps) {
10395
+ const counts = {};
10396
+ for (const r of deps.db.listRuns()) counts[r.status] = (counts[r.status] ?? 0) + 1;
10397
+ const live = deps.liveSessionIds?.() ?? [];
10398
+ return {
10399
+ hostId: deps.selfHost?.hostId ?? null,
10400
+ hostName: deps.selfHost?.hostName ?? null,
10401
+ backend: SWARM_BACKEND,
10402
+ paused: deps.control?.paused ?? false,
10403
+ pause: deps.control?.state ?? null,
10404
+ restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
10405
+ counts,
10406
+ live,
10407
+ version: PACKAGE_VERSION
10408
+ };
10409
+ }
10023
10410
  function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
10024
10411
  let iv;
10025
10412
  let last = "";
@@ -10188,18 +10575,25 @@ function makeHandler(deps) {
10188
10575
  return redirect("/");
10189
10576
  }
10190
10577
  if (p === "/pause" && req.method === "POST") {
10191
- if (!auth()) return unauthorized();
10578
+ if (!authed(req)) return unauthorized();
10192
10579
  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 });
10193
- deps.control.paused = true;
10194
- 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.");
10195
- return redirect("/");
10580
+ const f = await formValue(req);
10581
+ const reason = (f.get("reason") ?? "").trim() || null;
10582
+ setPausedVia(deps, true, pauseSourceOf(f.get("source")), reason);
10583
+ 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" });
10196
10584
  }
10197
10585
  if (p === "/resume" && req.method === "POST") {
10198
- if (!auth()) return unauthorized();
10586
+ if (!authed(req)) return unauthorized();
10199
10587
  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 });
10200
- deps.control.paused = false;
10201
- 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.");
10202
- return redirect("/");
10588
+ setPausedVia(deps, false, pauseSourceOf((await formValue(req)).get("source")), null);
10589
+ 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" });
10590
+ }
10591
+ if (p === "/status") return Response.json(daemonStatus(deps));
10592
+ if (p === "/shutdown" && req.method === "POST") {
10593
+ if (!authed(req)) return unauthorized();
10594
+ 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 });
10595
+ const f = await formValue(req);
10596
+ return Response.json(deps.requestStop({ force: f.get("force") === "1", wait: f.get("wait") === "1" }));
10203
10597
  }
10204
10598
  if (p === "/revive" && req.method === "POST") {
10205
10599
  if (!auth()) return unauthorized();
@@ -10372,6 +10766,115 @@ async function fetchExternalStorm(url, opts = {}) {
10372
10766
  }
10373
10767
  }
10374
10768
 
10769
+ // src/anomalyState.ts
10770
+ import { existsSync as existsSync11, readFileSync as readFileSync14, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "fs";
10771
+ import { join as join18 } from "path";
10772
+ var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
10773
+ var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
10774
+ function anomalyStatePath(home) {
10775
+ return join18(home, "anomaly-state.json");
10776
+ }
10777
+ function loadAnomalyState(home, now = Date.now(), io = {}) {
10778
+ const exists = io.exists ?? existsSync11;
10779
+ const readFile2 = io.readFile ?? ((p) => readFileSync14(p, "utf8"));
10780
+ const path = anomalyStatePath(home);
10781
+ if (!exists(path)) return { ...EMPTY };
10782
+ try {
10783
+ const raw = JSON.parse(readFile2(path));
10784
+ const since = numbers(raw.since);
10785
+ const lastFiredAt = numbers(raw.lastFiredAt);
10786
+ const fires = numbers(raw.fires);
10787
+ const fresh = new Set(
10788
+ [...Object.keys(since), ...Object.keys(lastFiredAt)].filter(
10789
+ (k) => now - Math.max(since[k] ?? 0, lastFiredAt[k] ?? 0) <= KEY_TTL_MS
10790
+ )
10791
+ );
10792
+ return {
10793
+ since: pick(since, fresh),
10794
+ lastFiredAt: pick(lastFiredAt, fresh),
10795
+ fires: pick(fires, fresh),
10796
+ stopQuiet: parseQuiet(raw.stopQuiet)
10797
+ };
10798
+ } catch {
10799
+ return { ...EMPTY };
10800
+ }
10801
+ }
10802
+ function saveAnomalyState(home, state, io = {}) {
10803
+ const path = anomalyStatePath(home);
10804
+ const writeFile2 = io.writeFile ?? ((p, data) => {
10805
+ writeFileSync11(`${p}.tmp`, data);
10806
+ renameSync6(`${p}.tmp`, p);
10807
+ });
10808
+ writeFile2(path, JSON.stringify(state, null, 2));
10809
+ }
10810
+ var AnomalyStateStore = class {
10811
+ constructor(home, now = Date.now(), io = {}) {
10812
+ this.home = home;
10813
+ this.io = io;
10814
+ this.state = loadAnomalyState(home, now, io);
10815
+ }
10816
+ home;
10817
+ io;
10818
+ state;
10819
+ /** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
10820
+ trackerIo() {
10821
+ return {
10822
+ load: () => ({ since: this.state.since, lastFiredAt: this.state.lastFiredAt, fires: this.state.fires }),
10823
+ save: (snapshot) => {
10824
+ this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
10825
+ this.flush();
10826
+ }
10827
+ };
10828
+ }
10829
+ /** Окно штатной остановки для buildAnomalyCandidates (null — рой ни разу не останавливали). */
10830
+ stopQuiet() {
10831
+ return this.state.stopQuiet;
10832
+ }
10833
+ /**
10834
+ * Рой ушёл на паузу/остановку — открываем окно тишины. Повторный вызов на уже открытом
10835
+ * окне ничего не меняет: пауза, поставленная дважды, это одна пауза.
10836
+ */
10837
+ markPaused(now) {
10838
+ const open = this.state.stopQuiet;
10839
+ if (open && open.to == null) return;
10840
+ this.state = { ...this.state, stopQuiet: { from: now, to: null } };
10841
+ this.flush();
10842
+ }
10843
+ /** Рой снят с паузы — закрываем окно. Окна не было (рестарт потерял) → ничего не выдумываем. */
10844
+ markResumed(now) {
10845
+ const open = this.state.stopQuiet;
10846
+ if (!open || open.to != null) return;
10847
+ this.state = { ...this.state, stopQuiet: { from: open.from, to: now } };
10848
+ this.flush();
10849
+ }
10850
+ flush() {
10851
+ try {
10852
+ saveAnomalyState(this.home, this.state, this.io);
10853
+ } catch (e) {
10854
+ console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
10855
+ }
10856
+ }
10857
+ };
10858
+ function numbers(raw) {
10859
+ if (!raw || typeof raw !== "object") return {};
10860
+ const out = {};
10861
+ for (const [k, v] of Object.entries(raw)) {
10862
+ if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
10863
+ }
10864
+ return out;
10865
+ }
10866
+ function pick(src, keys) {
10867
+ const out = {};
10868
+ for (const k of keys) if (src[k] != null) out[k] = src[k];
10869
+ return out;
10870
+ }
10871
+ function parseQuiet(raw) {
10872
+ if (!raw || typeof raw !== "object") return null;
10873
+ const q = raw;
10874
+ if (typeof q.from !== "number" || !Number.isFinite(q.from)) return null;
10875
+ return { from: q.from, to: typeof q.to === "number" && Number.isFinite(q.to) ? q.to : null };
10876
+ }
10877
+
10375
10878
  // src/queueOrder.ts
10376
10879
  function compareQueuePriority(a, b) {
10377
10880
  if (a.priority !== b.priority) return b.priority - a.priority;
@@ -10737,7 +11240,7 @@ async function escalateStaleCiWaits(deps, now) {
10737
11240
  if (deps.watchdog?.notify) {
10738
11241
  const signal = {
10739
11242
  key: `ci_wait_stale:${run.id}:${since}`,
10740
- title: `${droneLabel(run)} \u0436\u0434\u0451\u0442 CI \u0434\u043E\u043B\u044C\u0448\u0435 ${minutes} \u043C\u0438\u043D`,
11243
+ title: signalTitle(run, `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0436\u0434\u0451\u0442 CI \u0434\u043E\u043B\u044C\u0448\u0435 ${minutes} \u043C\u0438\u043D`),
10741
11244
  body: `${run.task_slug}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0448\u0442\u0430\u0442\u043D\u043E \u0432\u044B\u0448\u0435\u043B \u0436\u0434\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 PR, \u043D\u043E \u043E\u043D\u0438 \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C \u0437\u0430 ${minutes} \u043C\u0438\u043D (\u0437\u0430\u0441\u0442\u0440\u044F\u043B\u0438 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u0430\u043D\u043D\u0435\u0440\u043E\u0432 / \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u044B / gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442). \u0412\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u0440\u043E\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442. ${until} \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C: ${run.pr_url ?? "PR \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D"}.`,
10742
11245
  runId: run.id,
10743
11246
  ...hrefOf(run.task_url)
@@ -10942,10 +11445,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
10942
11445
  }
10943
11446
  return parts.join("\n");
10944
11447
  }
10945
- var HERO_CHARTER_PATH = join17(dirname5(THIS_FILE), "..", "docs", "HERO.md");
11448
+ var HERO_CHARTER_PATH = join19(dirname5(THIS_FILE), "..", "docs", "HERO.md");
10946
11449
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
10947
11450
  const name = run.name || `#${run.id}`;
10948
- const charter = readFileSync13(HERO_CHARTER_PATH, "utf8");
11451
+ const charter = readFileSync15(HERO_CHARTER_PATH, "utf8");
10949
11452
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
10950
11453
 
10951
11454
  ${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).`;
@@ -11085,6 +11588,9 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
11085
11588
  function droneLabel(run) {
11086
11589
  return run.name || `#${run.id}`;
11087
11590
  }
11591
+ function signalTitle(run, what) {
11592
+ return `${run.task_slug || droneLabel(run)}: ${what}`;
11593
+ }
11088
11594
  function narrate(deps, text) {
11089
11595
  try {
11090
11596
  deps.narrate?.(text);
@@ -11193,7 +11699,7 @@ async function wakeRun(deps, runId) {
11193
11699
  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` };
11194
11700
  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` };
11195
11701
  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` };
11196
- const worktreeExists = deps.worktreeExists ?? existsSync10;
11702
+ const worktreeExists = deps.worktreeExists ?? existsSync12;
11197
11703
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
11198
11704
  const wt = run.worktree_path;
11199
11705
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -11278,7 +11784,7 @@ function repoHasCi(deps, run) {
11278
11784
  function repoHasCiWorkflows(worktreePath, repo) {
11279
11785
  for (const base3 of [worktreePath, repo]) {
11280
11786
  if (!base3) continue;
11281
- const dir = join17(base3, ".github", "workflows");
11787
+ const dir = join19(base3, ".github", "workflows");
11282
11788
  try {
11283
11789
  if (readdirSync9(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
11284
11790
  } catch {
@@ -11394,8 +11900,12 @@ async function wakeReviewerOnCiReady(deps, run, over) {
11394
11900
  if (deps.watchdog?.notify) {
11395
11901
  const signal = {
11396
11902
  key: `reviewer_ci_wake_failed:${reviewer.id}`,
11397
- title: `\u0420\u0435\u0432\u044C\u044E\u0435\u0440 ${droneLabel(run)} \u043F\u043E\u0442\u0435\u0440\u044F\u043B \u0441\u0435\u0441\u0441\u0438\u044E \u043D\u0430 CI-\u043F\u0430\u0443\u0437\u0435`,
11398
- body: `${run.task_slug}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043E\u0436\u0434\u0430\u043B\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}). \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR \u2014 ${run.pr_url ?? "?"}.`,
11903
+ title: signalTitle(run, "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u0442\u0435\u0440\u044F\u043B \u0441\u0435\u0441\u0441\u0438\u044E \u043D\u0430 CI-\u043F\u0430\u0443\u0437\u0435"),
11904
+ // В ленту, без пуша (TASWM-327): следующая же строка тела и есть ответ — рой сам
11905
+ // поднимает свежего ревьюера на том же PR. Решать человеку тут нечего; если и
11906
+ // свежий не доедет, конвейер встанет и об этом скажет уже pr_stuck_* пушем.
11907
+ level: "feed",
11908
+ body: `\u0420\u0435\u0432\u044C\u044E\u0435\u0440 ${droneLabel(reviewer)} \u0434\u043E\u0436\u0434\u0430\u043B\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}). \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR \u2014 ${run.pr_url ?? "?"}.`,
11399
11909
  runId: reviewer.id,
11400
11910
  ...hrefOf(run.task_url)
11401
11911
  };
@@ -11423,7 +11933,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
11423
11933
  return true;
11424
11934
  }
11425
11935
  function worktreeAndTranscriptAlive(deps, worktree) {
11426
- const exists = deps.worktreeExists ?? existsSync10;
11936
+ const exists = deps.worktreeExists ?? existsSync12;
11427
11937
  const transcript = deps.hasTranscript ?? sessionTranscriptExists;
11428
11938
  return exists(worktree) && transcript(worktree);
11429
11939
  }
@@ -11628,7 +12138,7 @@ async function resumeWatchdogParked(deps, droneCap) {
11628
12138
  }
11629
12139
  if (!candidate) return;
11630
12140
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
11631
- const worktreeExists = deps.worktreeExists ?? existsSync10;
12141
+ const worktreeExists = deps.worktreeExists ?? existsSync12;
11632
12142
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
11633
12143
  const wt = candidate.worktree_path;
11634
12144
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -11797,7 +12307,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
11797
12307
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
11798
12308
  if (!deps.respawn) return;
11799
12309
  const wt = resumeCandidate.worktree_path;
11800
- const worktreeExists = deps.worktreeExists ?? existsSync10;
12310
+ const worktreeExists = deps.worktreeExists ?? existsSync12;
11801
12311
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
11802
12312
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
11803
12313
  const mode = canContinue ? "continue" : "fresh";
@@ -11949,7 +12459,7 @@ function buildEscalationCandidates(db) {
11949
12459
  // маркер сам по себе УЖЕ прошёл свой RESOLVE_LIMIT/REVIEW_SPAWN_LIMIT/…
11950
12460
  build: () => ({
11951
12461
  runId: run.id,
11952
- title: `${droneLabel(run)} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430`,
12462
+ title: signalTitle(run, "\u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440"),
11953
12463
  body: `${run.task_slug}: ${hit.text ?? "\u044D\u0441\u043A\u0430\u043B\u0430\u0446\u0438\u044F \u0431\u0435\u0437 \u0442\u0435\u043A\u0441\u0442\u0430"} \u2014 ${run.pr_url ?? "?"}.`,
11954
12464
  ...hrefOf(run.task_url)
11955
12465
  })
@@ -11959,7 +12469,7 @@ function buildEscalationCandidates(db) {
11959
12469
  }
11960
12470
  async function tickOnce(deps) {
11961
12471
  const now = deps.now ? deps.now() : Date.now();
11962
- const worktreeExists = deps.worktreeExists ?? existsSync10;
12472
+ const worktreeExists = deps.worktreeExists ?? existsSync12;
11963
12473
  const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
11964
12474
  if (deps.rehydrate) {
11965
12475
  const rh = deps.rehydrate;
@@ -12091,7 +12601,7 @@ async function tickOnce(deps) {
12091
12601
  if (!already) {
12092
12602
  const signal = {
12093
12603
  key: `daemon_self_reference:${run.id}`,
12094
- title: `PR ${droneLabel(run)} \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430`,
12604
+ title: signalTitle(run, "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430"),
12095
12605
  body: `${run.task_slug}: \u043F\u0440\u0430\u0432\u043A\u0430 \u0437\u0430\u0434\u0435\u0432\u0430\u0435\u0442 ${DAEMON_CODE_PATHS.join("/, ")}/ \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0438\u0439 \u0434\u0435\u043C\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u043C \u043A\u043E\u0434\u0435 (TASWM-251) \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0430\u0442\u0442\u0435\u0441\u0442\u043E\u0432\u0430\u0442\u044C \u0435\u0451 \u043E\u0431\u044B\u0447\u043D\u044B\u043C \u043F\u0443\u0442\u0451\u043C. \u0410\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u2014 ${run.pr_url}.`,
12096
12606
  runId: run.id,
12097
12607
  ...hrefOf(run.task_url)
@@ -12449,9 +12959,9 @@ async function tickOnce(deps) {
12449
12959
  if (deps.respawn && deps.resourcesOk && !stormActive) {
12450
12960
  await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
12451
12961
  }
12452
- if (deps.anomalies) {
12962
+ if (deps.anomalies && !deps.control?.paused) {
12453
12963
  const az = deps.anomalies;
12454
- let eligible = [];
12964
+ let ready = [];
12455
12965
  try {
12456
12966
  const candidates = [
12457
12967
  ...buildAnomalyCandidates(deps.db, now, az.thresholds, {
@@ -12459,29 +12969,31 @@ async function tickOnce(deps) {
12459
12969
  ...az.outboxPendingCount ? { outboxPendingCount: az.outboxPendingCount() } : {},
12460
12970
  // resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
12461
12971
  // deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
12462
- ...deps.gateReason ? { gateReason: deps.gateReason() } : {}
12972
+ ...deps.gateReason ? { gateReason: deps.gateReason() } : {},
12973
+ ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
12463
12974
  }),
12464
12975
  ...buildEscalationCandidates(deps.db),
12465
12976
  // ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
12466
12977
  // труба, тот же дедуп «один раз на эпизод». Пустой список при нормальной работе.
12467
12978
  ...await buildReliefCostCandidatesFor(deps, now, droneCap)
12468
12979
  ];
12469
- eligible = az.tracker.eligible(now, candidates);
12980
+ ready = collapseSignals(az.tracker.eligible(now, candidates));
12470
12981
  } catch (e) {
12471
12982
  console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0434\u0435\u0442\u0435\u043A\u0442 \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
12472
12983
  }
12473
- for (const candidate of eligible) {
12474
- const signal = { key: candidate.key, ...candidate.build() };
12984
+ for (const item of ready) {
12985
+ const finalCall = item.keys.some((k) => az.tracker.isFinalRepeat(k));
12986
+ const signal = finalCall ? { ...item.signal, body: `${item.signal.body} ${LAST_REMINDER_SUFFIX}` } : item.signal;
12475
12987
  try {
12476
12988
  await az.notify(signal);
12477
12989
  } catch (e) {
12478
12990
  console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
12479
12991
  continue;
12480
12992
  }
12481
- az.tracker.commit(candidate.key, now);
12482
- if (signal.runId != null) {
12993
+ for (const key of item.keys) az.tracker.commit(key, now);
12994
+ for (const ev of item.events) {
12483
12995
  try {
12484
- deps.db.addEvent(signal.runId, "anomaly", signal.body);
12996
+ deps.db.addEvent(ev.runId, "anomaly", ev.text);
12485
12997
  } catch (e) {
12486
12998
  console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0432\u0435\u0445\u0430 \xAB${signal.key}\xBB \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u0430\u0441\u044C:`, e);
12487
12999
  }
@@ -12563,7 +13075,7 @@ async function resolveConflict(deps, reviewRun) {
12563
13075
  workspace_id: reviewRun.workspace_id
12564
13076
  // та же задача, тот же воркспейс (TASWM-288)
12565
13077
  });
12566
- const wt = join17(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13078
+ const wt = join19(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
12567
13079
  try {
12568
13080
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
12569
13081
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -12615,7 +13127,7 @@ async function spawnReviewer(deps, reviewRun, files) {
12615
13127
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
12616
13128
  workspace_id: reviewRun.workspace_id
12617
13129
  });
12618
- const wt = join17(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13130
+ const wt = join19(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
12619
13131
  try {
12620
13132
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
12621
13133
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -12650,7 +13162,7 @@ async function respawnRun(deps, run, mode, nudge) {
12650
13162
  ]);
12651
13163
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
12652
13164
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
12653
- if (!existsSync10(wt)) {
13165
+ if (!existsSync12(wt)) {
12654
13166
  const branch = runToSpawn.branch || runToSpawn.task_slug;
12655
13167
  const source = await addWorktree(repo, wt, branch, {
12656
13168
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -12679,7 +13191,7 @@ ${notice}` : freshPrompt;
12679
13191
  deps.db.updateRun(run.id, { status: "running", pid });
12680
13192
  }
12681
13193
  async function syncCrashedOnStartup(deps) {
12682
- const worktreeExists = deps.worktreeExists ?? existsSync10;
13194
+ const worktreeExists = deps.worktreeExists ?? existsSync12;
12683
13195
  let orphaned = 0;
12684
13196
  let merged = 0;
12685
13197
  let requeued = 0;
@@ -12759,7 +13271,7 @@ async function migrateWorktreesAtStartup() {
12759
13271
  legacyDir: WORKTREES_LEGACY_DIR,
12760
13272
  targetDir: WORKTREES_DIR,
12761
13273
  envOverride: process.env.SWARM_WORKTREES || "",
12762
- exists: (d) => existsSync10(d),
13274
+ exists: (d) => existsSync12(d),
12763
13275
  isNonEmptyDir: (d) => {
12764
13276
  try {
12765
13277
  return readdirSync9(d).length > 0;
@@ -12768,13 +13280,13 @@ async function migrateWorktreesAtStartup() {
12768
13280
  }
12769
13281
  },
12770
13282
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
12771
- rename: (from, to) => renameSync5(from, to),
13283
+ rename: (from, to) => renameSync7(from, to),
12772
13284
  repairMoved: (t) => repairMovedWorktrees(t),
12773
13285
  log: (m) => console.log(m)
12774
13286
  });
12775
13287
  } catch (e) {
12776
13288
  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);
12777
- return existsSync10(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
13289
+ return existsSync12(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
12778
13290
  }
12779
13291
  }
12780
13292
  async function reportWorkspaceAccess(tl, db) {
@@ -12868,7 +13380,7 @@ async function runDaemon(opts) {
12868
13380
  const client = db;
12869
13381
  heartbeat = {
12870
13382
  push: async () => {
12871
- const commands = await client.heartbeat(daemonVersion?.get() ?? null);
13383
+ const commands = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
12872
13384
  if (commands.length === 0) return;
12873
13385
  await serveLogRequests(
12874
13386
  {
@@ -12912,11 +13424,15 @@ async function runDaemon(opts) {
12912
13424
  console.warn("[overmind] \u043C\u043E\u0441\u0442 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u0437\u0430\u0432\u0435\u0434\u0451\u043D (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F sqlite \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430) \u2014 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F \u0432 \u043D\u0435\u0439:", e);
12913
13425
  }
12914
13426
  }
13427
+ const anomalyState = new AnomalyStateStore(SWARM_HOME);
12915
13428
  let anomalies;
12916
13429
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
12917
13430
  const client = db;
12918
13431
  anomalies = {
12919
- tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS),
13432
+ tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS, {
13433
+ maxRepeats: SWARM_ANOMALY_MAX_REPEATS,
13434
+ io: anomalyState.trackerIo()
13435
+ }),
12920
13436
  thresholds: {
12921
13437
  failedSpikeCount: SWARM_ANOMALY_FAILED_SPIKE_COUNT,
12922
13438
  failedSpikeWindowMs: SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
@@ -12925,11 +13441,13 @@ async function runDaemon(opts) {
12925
13441
  prNoReviewerMs: SWARM_ANOMALY_PR_NO_REVIEWER_MS,
12926
13442
  questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
12927
13443
  outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
12928
- resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS
13444
+ resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
13445
+ runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
12929
13446
  },
12930
13447
  autoReviewEnabled: SWARM_AUTO_REVIEW,
12931
13448
  outboxPendingCount: () => client.pendingCount(),
12932
- notify: (signal) => client.notifyAnomaly(signal)
13449
+ notify: (signal) => client.notifyAnomaly(signal),
13450
+ stopQuiet: () => anomalyState.stopQuiet()
12933
13451
  };
12934
13452
  }
12935
13453
  const watchdog = {
@@ -13031,7 +13549,33 @@ async function runDaemon(opts) {
13031
13549
  reviewCutoffAtValue = reviewCutoffAt();
13032
13550
  }
13033
13551
  const narrator = makeOvermindNarrator();
13034
- const control = { paused: false };
13552
+ const bootPause = loadPauseState(SWARM_HOME);
13553
+ const control = { paused: bootPause.paused, state: bootPause.paused ? bootPause : null };
13554
+ if (bootPause.paused) {
13555
+ console.log(`[overmind] \u23F8 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u041D\u0410 \u041F\u0410\u0423\u0417\u0415: ${pauseText(bootPause)}. \u0421\u043D\u044F\u0442\u044C: swarm resume`);
13556
+ const pausedAt = Date.parse(bootPause.at);
13557
+ anomalyState.markPaused(Number.isNaN(pausedAt) ? Date.now() : pausedAt);
13558
+ } else {
13559
+ anomalyState.markResumed(Date.now());
13560
+ }
13561
+ const setPaused = (paused, opts2) => {
13562
+ const next = paused ? makePauseState(opts2.source, opts2.reason ?? null) : null;
13563
+ control.paused = paused;
13564
+ control.state = next;
13565
+ if (paused) anomalyState.markPaused(Date.now());
13566
+ else anomalyState.markResumed(Date.now());
13567
+ try {
13568
+ savePauseState(SWARM_HOME, next ?? { paused: false, at: "", source: opts2.source });
13569
+ } catch (e) {
13570
+ console.error(`[overmind] \u043F\u0430\u0443\u0437\u0430 \u041D\u0415 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u0430 \u043D\u0430 \u0434\u0438\u0441\u043A (${e instanceof Error ? e.message : e}) \u2014 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0451\u0442 \u0442\u0438\u043A\u0438, \u043D\u043E \u043D\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430`);
13571
+ }
13572
+ narrator.narrate(
13573
+ paused ? `\u23F8 \u0420\u043E\u0439 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435 (${opts2.source === "cli" ? "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0441 \u043C\u0430\u0448\u0438\u043D\u044B" : opts2.source === "remote" ? "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless" : "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u043C\u043E\u0440\u0434\u0430"})${opts2.reason ? `: ${opts2.reason}` : ""} \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 \u0420\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."
13574
+ );
13575
+ void heartbeat?.push().catch(() => {
13576
+ });
13577
+ };
13578
+ control.set = setPaused;
13035
13579
  const restart = initialRestartState();
13036
13580
  const deps = {
13037
13581
  db,
@@ -13217,7 +13761,7 @@ async function runDaemon(opts) {
13217
13761
  mode,
13218
13762
  nudge
13219
13763
  ),
13220
- worktreeExists: (p) => existsSync10(p),
13764
+ worktreeExists: (p) => existsSync12(p),
13221
13765
  hasTranscript: (p) => sessionTranscriptExists(p),
13222
13766
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
13223
13767
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -13257,31 +13801,39 @@ async function runDaemon(opts) {
13257
13801
  };
13258
13802
  let web;
13259
13803
  let droneChannel = null;
13260
- function performRestart(orphanedIds) {
13804
+ function performRestart(orphanedIds, intent = "restart") {
13261
13805
  if (restart.phase === "restarting") return;
13262
13806
  restart.phase = "restarting";
13807
+ restart.intent = intent;
13808
+ const stopping = intent === "stop";
13263
13809
  if (orphanedIds.length) {
13264
13810
  for (const id of orphanedIds) {
13265
13811
  db.addEvent(
13266
13812
  id,
13267
13813
  "anomaly",
13268
- "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C, \u043F\u043E\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0436\u0438\u0432\u0430 \u2014 heartbeat-\u043A\u0430\u043D\u0430\u043B \u043F\u0440\u0435\u0440\u0432\u0430\u043B\u0441\u044F \u043D\u0430 \u0432\u0440\u0435\u043C\u044F \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 (TASWM-250)"
13814
+ stopping ? "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C (swarm stop --force), \u043F\u043E\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0436\u0438\u0432\u0430 \u2014 \u043E\u043D\u0430 \u0437\u0430\u043C\u0440\u0451\u0442 \u043D\u0430 \u0431\u043B\u0438\u0436\u0430\u0439\u0448\u0435\u043C \u043E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u0438 \u043A \u0434\u0435\u043C\u043E\u043D\u0443 (TASWM-326)" : "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0449\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C, \u043F\u043E\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0436\u0438\u0432\u0430 \u2014 heartbeat-\u043A\u0430\u043D\u0430\u043B \u043F\u0440\u0435\u0440\u0432\u0430\u043B\u0441\u044F \u043D\u0430 \u0432\u0440\u0435\u043C\u044F \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 (TASWM-250)"
13269
13815
  );
13270
13816
  }
13271
- narrator.narrate(`\u{1F501} \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0444\u043E\u0440\u0441\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${orphanedIds.length}`);
13817
+ narrator.narrate(
13818
+ stopping ? `\u23F9 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0433\u0430\u0441\u0438\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u2014 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${orphanedIds.length}` : `\u{1F501} \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0444\u043E\u0440\u0441\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${orphanedIds.length}`
13819
+ );
13272
13820
  } else {
13273
- narrator.narrate("\u{1F501} \u0414\u0435\u043C\u043E\u043D \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442");
13821
+ narrator.narrate(stopping ? "\u23F9 \u0414\u0435\u043C\u043E\u043D \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442" : "\u{1F501} \u0414\u0435\u043C\u043E\u043D \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u2014 \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435\u0442");
13274
13822
  }
13275
13823
  const isCompiled = THIS_FILE.endsWith(".js");
13276
13824
  const respawnArgs = isCompiled ? [THIS_FILE] : ["--conditions=development", resolveTsxCli(), THIS_FILE];
13277
13825
  setTimeout(() => {
13278
13826
  Promise.all([closeWebServer(web.server), droneChannel ? closeDroneChannel(droneChannel.server) : Promise.resolve()]).then(() => {
13279
- spawn3(process.execPath, respawnArgs, {
13280
- detached: true,
13281
- stdio: "ignore",
13282
- cwd: process.cwd(),
13283
- env: { ...process.env, SWARM_REPO: opts.repo }
13284
- }).unref();
13827
+ if (!stopping) {
13828
+ spawn3(process.execPath, respawnArgs, {
13829
+ detached: true,
13830
+ stdio: "ignore",
13831
+ cwd: process.cwd(),
13832
+ env: { ...process.env, SWARM_REPO: opts.repo }
13833
+ }).unref();
13834
+ } else {
13835
+ console.log("[overmind] \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 `swarm stop`. \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u043E\u0431\u0440\u0430\u0442\u043D\u043E: swarm start");
13836
+ }
13285
13837
  process.exit(0);
13286
13838
  });
13287
13839
  }, 300);
@@ -13295,22 +13847,43 @@ async function runDaemon(opts) {
13295
13847
  }
13296
13848
  restart.phase = "waiting";
13297
13849
  restart.requestedAt = Date.now();
13298
- if (!control.paused) {
13299
- control.paused = true;
13300
- restartAutoPaused = true;
13301
- }
13850
+ restart.intent = "restart";
13851
+ autoPauseForWait();
13302
13852
  narrator.narrate(`\u23F3 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u0440\u0435\u0441\u0442\u0430\u0440\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${sessions.length}`);
13303
13853
  return { waiting: true, sessions };
13304
13854
  };
13855
+ const requestStop = (o) => {
13856
+ const sessions = liveSessionIds(db, pidAlive);
13857
+ if (o.force || sessions.length === 0) {
13858
+ performRestart(o.force ? sessions : [], "stop");
13859
+ return { stopping: true, waiting: false, sessions };
13860
+ }
13861
+ if (!o.wait) return { stopping: false, waiting: false, sessions };
13862
+ restart.phase = "waiting";
13863
+ restart.requestedAt = Date.now();
13864
+ restart.intent = "stop";
13865
+ autoPauseForWait();
13866
+ narrator.narrate(`\u23F3 \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0433\u0430\u0441\u0438\u0442 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${sessions.length}`);
13867
+ return { stopping: false, waiting: true, sessions };
13868
+ };
13869
+ function autoPauseForWait() {
13870
+ if (control.paused) return;
13871
+ control.paused = true;
13872
+ restartAutoPaused = true;
13873
+ anomalyState.markPaused(Date.now());
13874
+ }
13305
13875
  const cancelRestartWait = () => {
13306
13876
  if (restart.phase !== "waiting") return;
13877
+ const wasStop = restart.intent === "stop";
13307
13878
  restart.phase = "idle";
13308
13879
  restart.requestedAt = null;
13880
+ restart.intent = "restart";
13309
13881
  if (restartAutoPaused) {
13310
13882
  control.paused = false;
13311
13883
  restartAutoPaused = false;
13884
+ anomalyState.markResumed(Date.now());
13312
13885
  }
13313
- narrator.narrate("\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430");
13886
+ narrator.narrate(wasStop ? "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0434\u0435\u043C\u043E\u043D\u0430" : "\u25B6\uFE0F \u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0442\u043C\u0435\u043D\u0438\u043B \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430");
13314
13887
  };
13315
13888
  if (channelSecret) {
13316
13889
  droneChannel = startDroneChannel(
@@ -13319,10 +13892,13 @@ async function runDaemon(opts) {
13319
13892
  secret: channelSecret,
13320
13893
  log: (m) => console.log(m),
13321
13894
  ...db instanceof SwarmClient ? {
13895
+ // level: "note" — та же дисциплина, что у моста (TASWM-327): заметка идёт в
13896
+ // ленту, никогда пушем; заголовок — со слага задачи, имя дрона в теле.
13322
13897
  onNote: (run, text) => db.notifyAnomaly({
13323
13898
  key: noteNotifyKey(run.id, text),
13324
- title: `\u0414\u0440\u043E\u043D #${run.id}: \u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438`,
13325
- body: text,
13899
+ level: "note",
13900
+ title: `${run.task_slug || `#${run.id}`}: \u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438`,
13901
+ body: `\u0414\u0440\u043E\u043D ${run.name || `#${run.id}`}: ${text}`,
13326
13902
  ...hrefOf(run.task_url),
13327
13903
  runId: run.id
13328
13904
  })
@@ -13366,6 +13942,9 @@ async function runDaemon(opts) {
13366
13942
  liveSessionIds: () => liveSessionIds(db, pidAlive),
13367
13943
  requestRestart,
13368
13944
  cancelRestartWait,
13945
+ // `swarm stop` (TASWM-326) — та же механика ожидания, что у рестарта, но в конце
13946
+ // процесс выходит. Штатная замена `pkill -f overmind`.
13947
+ requestStop,
13369
13948
  // Побудка прогона по id (TASWM-240): вход для `swarm wake <id>` — тот же respawn с
13370
13949
  // полными deps демона (getTask/verifyMode/реестр репо), что и авто-оживление, и по
13371
13950
  // зеркалу демона, а не по короткоживущему стору CLI.
@@ -13413,7 +13992,7 @@ async function runDaemon(opts) {
13413
13992
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
13414
13993
  await tickOnce(deps);
13415
13994
  if (restart.phase === "waiting" && liveSessionIds(db, pidAlive).length === 0) {
13416
- performRestart([]);
13995
+ performRestart([], restart.intent);
13417
13996
  }
13418
13997
  } catch (e) {
13419
13998
  console.error("[overmind] tick error:", e);