@taskless-app/swarm 0.6.19 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2167,7 +2167,7 @@ function tokenFromFile() {
2167
2167
  return "";
2168
2168
  }
2169
2169
  }
2170
- var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, 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_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
2170
+ var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, 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, SWARM_WORKTREE_REAP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, 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_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
2171
2171
  var init_config = __esm({
2172
2172
  "src/config.ts"() {
2173
2173
  "use strict";
@@ -2216,6 +2216,7 @@ var init_config = __esm({
2216
2216
  SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
2217
2217
  SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
2218
2218
  SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
2219
+ SWARM_WORKTREE_REAP_INTERVAL_MS = envNum("SWARM_WORKTREE_REAP_INTERVAL_MS", 5 * 6e4);
2219
2220
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
2220
2221
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
2221
2222
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
@@ -2372,7 +2373,7 @@ var init_package = __esm({
2372
2373
  "package.json"() {
2373
2374
  package_default = {
2374
2375
  name: "@taskless-app/swarm",
2375
- version: "0.6.19",
2376
+ version: "0.6.20",
2376
2377
  type: "module",
2377
2378
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
2378
2379
  keywords: [
@@ -2422,7 +2423,7 @@ var init_package = __esm({
2422
2423
  execa: "^10.0.0",
2423
2424
  zod: "3.25.76"
2424
2425
  },
2425
- gitHead: "a61c26b5e254ef5383d918f14a9f6e8e8a09904a"
2426
+ gitHead: "2d05a2e2b7c197ab916f7ff756e3a829b138df16"
2426
2427
  };
2427
2428
  }
2428
2429
  });
@@ -11022,12 +11023,15 @@ async function previousHooksPath(worktree) {
11022
11023
  }
11023
11024
  async function removeWorktree(repo, path) {
11024
11025
  const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
11026
+ await releaseWorktree(repo, path);
11027
+ if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
11028
+ }
11029
+ async function releaseWorktree(repo, path) {
11025
11030
  await cleanupWorktreeProcesses(path);
11026
11031
  await detachWorktree(repo, path);
11027
- if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
11028
11032
  }
11029
11033
  async function worktreeDirty(path) {
11030
- const st = await $5({ reject: false })`git -C ${path} status --porcelain`;
11034
+ const st = await $5({ reject: false })`git -C ${path} status --porcelain --untracked-files=all`;
11031
11035
  if (st.exitCode !== 0) return true;
11032
11036
  const status = st.stdout.trim();
11033
11037
  if (status) return true;
package/dist/overmind.js CHANGED
@@ -2275,6 +2275,7 @@ __export(config_exports, {
2275
2275
  SWARM_VERIFY: () => SWARM_VERIFY,
2276
2276
  SWARM_VERSION_CHECK_MS: () => SWARM_VERSION_CHECK_MS,
2277
2277
  SWARM_WATCHDOG_STALL_MS: () => SWARM_WATCHDOG_STALL_MS,
2278
+ SWARM_WORKTREE_REAP_INTERVAL_MS: () => SWARM_WORKTREE_REAP_INTERVAL_MS,
2278
2279
  TASKLESS_API: () => TASKLESS_API,
2279
2280
  TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
2280
2281
  TASKLESS_TASK_URL_BASE: () => TASKLESS_TASK_URL_BASE,
@@ -2371,7 +2372,7 @@ function reviewCutoffAt() {
2371
2372
  }
2372
2373
  return at;
2373
2374
  }
2374
- var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, 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_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
2375
+ var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, 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, SWARM_WORKTREE_REAP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, 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_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
2375
2376
  var init_config = __esm({
2376
2377
  "src/config.ts"() {
2377
2378
  "use strict";
@@ -2420,6 +2421,7 @@ var init_config = __esm({
2420
2421
  SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
2421
2422
  SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
2422
2423
  SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
2424
+ SWARM_WORKTREE_REAP_INTERVAL_MS = envNum("SWARM_WORKTREE_REAP_INTERVAL_MS", 5 * 6e4);
2423
2425
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
2424
2426
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
2425
2427
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
@@ -3486,12 +3488,18 @@ async function previousHooksPath(worktree) {
3486
3488
  }
3487
3489
  async function removeWorktree(repo, path) {
3488
3490
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
3491
+ await releaseWorktree(repo, path);
3492
+ if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
3493
+ }
3494
+ async function releaseWorktree(repo, path) {
3489
3495
  await cleanupWorktreeProcesses(path);
3490
3496
  await detachWorktree(repo, path);
3491
- if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
3497
+ }
3498
+ async function pruneWorktrees(repo) {
3499
+ await $2({ reject: false })`git -C ${repo} worktree prune`;
3492
3500
  }
3493
3501
  async function worktreeDirty(path) {
3494
- const st = await $2({ reject: false })`git -C ${path} status --porcelain`;
3502
+ const st = await $2({ reject: false })`git -C ${path} status --porcelain --untracked-files=all`;
3495
3503
  if (st.exitCode !== 0) return true;
3496
3504
  const status = st.stdout.trim();
3497
3505
  if (status) return true;
@@ -10507,6 +10515,70 @@ function parkReason(attempts, reason) {
10507
10515
  return `${PRE_START_MARK} ${attempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u043E \u043A\u0440\u0443\u0433\u0443. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
10508
10516
  }
10509
10517
 
10518
+ // src/worktreeReap.ts
10519
+ init_src();
10520
+ var DIRTY_WORKTREE_MARK = "worktree \u043D\u0435 \u0443\u0431\u0440\u0430\u043D: \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430";
10521
+ var HOLDS_WORKTREE = /* @__PURE__ */ new Set(["running", "blocked", "review", "paused", "storm_dead", "stalled"]);
10522
+ function isHelperRun(run) {
10523
+ return run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null;
10524
+ }
10525
+ function prIsDead(st) {
10526
+ return st.merged || st.state.toUpperCase() === "MERGED" || st.state.toUpperCase() === "CLOSED";
10527
+ }
10528
+ function heldByLive(deps, all, run, path) {
10529
+ if (deps.liveWorktrees?.().has(path)) return true;
10530
+ return all.some((r) => r.id !== run.id && r.worktree_path === path && HOLDS_WORKTREE.has(r.status));
10531
+ }
10532
+ async function reapFinishedWorktrees(deps) {
10533
+ const report = { removed: 0, keptDirty: 0 };
10534
+ const prunedRepos = /* @__PURE__ */ new Set();
10535
+ const all = deps.db.listRuns();
10536
+ for (const run of all) {
10537
+ const path = run.worktree_path;
10538
+ if (!path || !isTerminalStatus(run.status)) continue;
10539
+ if (heldByLive(deps, all, run, path)) continue;
10540
+ if (!deps.worktreeExists(path)) {
10541
+ if (run.repo && deps.pruneWorktrees && !prunedRepos.has(run.repo)) {
10542
+ prunedRepos.add(run.repo);
10543
+ await deps.pruneWorktrees(run.repo).catch(() => {
10544
+ });
10545
+ }
10546
+ continue;
10547
+ }
10548
+ if (!isHelperRun(run)) {
10549
+ if (!run.pr_url) continue;
10550
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
10551
+ if (!st || !prIsDead(st)) continue;
10552
+ }
10553
+ if (await keepDirty(deps, run, path)) {
10554
+ report.keptDirty += 1;
10555
+ continue;
10556
+ }
10557
+ await deps.releaseWorktree(run.repo, path).catch(() => {
10558
+ });
10559
+ deps.db.addEvent(run.id, "step", `worktree \u0443\u0431\u0440\u0430\u043D: ${path} (\u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D, ${reapReason(run)}) \u2014 \u0441\u043B\u0430\u0433 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430`);
10560
+ report.removed += 1;
10561
+ }
10562
+ return report;
10563
+ }
10564
+ function reapReason(run) {
10565
+ return isHelperRun(run) ? "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0441\u043A\u0430\u044F \u043A\u043E\u043F\u0438\u044F \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430" : "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0438\u043B\u0438 \u0437\u0430\u043A\u0440\u044B\u0442";
10566
+ }
10567
+ async function keepDirty(deps, run, path) {
10568
+ const dirty = deps.isDirty ? await deps.isDirty(path).catch(() => true) : true;
10569
+ if (!dirty) return false;
10570
+ const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(DIRTY_WORKTREE_MARK));
10571
+ if (!already) {
10572
+ deps.db.addEvent(
10573
+ run.id,
10574
+ "anomaly",
10575
+ `${DIRTY_WORKTREE_MARK}. \u041F\u0443\u0442\u044C: ${path}. \u0412\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u0438\u043B\u0438 \u043D\u0435\u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u043B\u0438\u0431\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u044B, \u043D\u0435 \u0434\u043E\u0448\u0435\u0434\u0448\u0438\u0435 \u0434\u043E origin, \u2014 \u0441\u043D\u043E\u0441\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F, \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C \u0438 \u0443\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`).`
10576
+ );
10577
+ deps.narrate?.(`${run.task_slug}: worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u2014 \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (${path}).`);
10578
+ }
10579
+ return true;
10580
+ }
10581
+
10510
10582
  // src/overmind.ts
10511
10583
  init_git();
10512
10584
 
@@ -11740,6 +11812,52 @@ import { createHash as createHash3 } from "crypto";
11740
11812
 
11741
11813
  // src/anomalies.ts
11742
11814
  init_src();
11815
+
11816
+ // src/reviewRescue.ts
11817
+ init_src();
11818
+ var REVIEW_RESCUE_AFTER_MS = Math.max(6e4, Number(process.env.SWARM_REVIEW_RESCUE_MS) || 10 * 6e4);
11819
+ var REVIEW_RESCUE_MARK = "\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E";
11820
+ var AUTHOR_GONE_STATUSES = /* @__PURE__ */ new Set(["paused", "failed", "stalled"]);
11821
+ function isExecutorRun(run) {
11822
+ if (run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null) return false;
11823
+ const kind = run.kind ?? "drone";
11824
+ return kind === "drone" || kind === "hero";
11825
+ }
11826
+ function prOpenedAtMs(events) {
11827
+ const opened = events.filter((e) => e.kind === "pr_opened");
11828
+ if (opened.length === 0) return null;
11829
+ const ms = sqliteMs(opened[opened.length - 1].at);
11830
+ return Number.isFinite(ms) ? ms : null;
11831
+ }
11832
+ function reviewlessPrCandidates(db, now, afterMs = REVIEW_RESCUE_AFTER_MS) {
11833
+ const out = [];
11834
+ for (const run of db.listRuns()) {
11835
+ if (!run.pr_url) continue;
11836
+ if (run.status !== "review" && !AUTHOR_GONE_STATUSES.has(run.status)) continue;
11837
+ if (!isExecutorRun(run)) continue;
11838
+ if (run.review_verdict != null) continue;
11839
+ if (run.status === "review") {
11840
+ if (db.reviewerCountFor(run.id) > 0) continue;
11841
+ if (db.activeReviewersFor(run.id).length > 0) continue;
11842
+ }
11843
+ const events = db.events(run.id);
11844
+ const openedAt = prOpenedAtMs(events) ?? sqliteMs(run.updated_at);
11845
+ if (!Number.isFinite(openedAt) || now - openedAt < afterMs) continue;
11846
+ out.push(run);
11847
+ }
11848
+ return out;
11849
+ }
11850
+ function lastReviewBlockReason(db, runId) {
11851
+ const hit = db.events(runId).findLast((e) => (e.text ?? "").includes(`${REVIEW_RESCUE_MARK}:`));
11852
+ if (!hit?.text) return null;
11853
+ const idx = hit.text.indexOf(`${REVIEW_RESCUE_MARK}:`);
11854
+ return hit.text.slice(idx + REVIEW_RESCUE_MARK.length + 1).trim() || null;
11855
+ }
11856
+ function shouldNoteReason(db, runId, reason) {
11857
+ return lastReviewBlockReason(db, runId) !== reason;
11858
+ }
11859
+
11860
+ // src/anomalies.ts
11743
11861
  var LEVEL_RANK = { note: 0, feed: 1, decision: 2 };
11744
11862
  function plural2(n, forms) {
11745
11863
  const mod100 = n % 100;
@@ -12107,15 +12225,18 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
12107
12225
  key: `pr_no_reviewer:${run.id}`,
12108
12226
  minDurationMs: thresholds.prNoReviewerMs,
12109
12227
  group: PR_GROUP,
12110
- build: () => ({
12111
- runId: run.id,
12112
- // В ленту: ревьюер не поднялся из-за гейта (cap/maxDrones/cutoff) — как только
12113
- // гейт откроется, рой поднимет его сам, решения человека тут не требуется.
12114
- level: "feed",
12115
- title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
12116
- 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}.`,
12117
- ...hrefOf(run.task_url)
12118
- })
12228
+ build: () => {
12229
+ const why = lastReviewBlockReason(db, run.id);
12230
+ return {
12231
+ runId: run.id,
12232
+ // В ленту: рой действует сам (автоподъём), решения человека тут не требуется —
12233
+ // если и подъём не вышел, причина в теле и есть то, с чем человек идёт разбираться.
12234
+ level: "feed",
12235
+ title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
12236
+ 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 \u2014 ${why ?? "\u0440\u043E\u0439 \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B \u043F\u0440\u0438\u0447\u0438\u043D\u0443 (\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0434\u043E\u0448\u0451\u043B \u0434\u043E \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430)"} \u2014 ${run.pr_url}.`,
12237
+ ...hrefOf(run.task_url)
12238
+ };
12239
+ }
12119
12240
  });
12120
12241
  } else if (run.review_verdict === "changes" && db.activeReviewersFor(run.id).length === 0) {
12121
12242
  candidates.push({
@@ -13191,7 +13312,7 @@ async function changedPaths(worktree, opts = {}) {
13191
13312
  // package.json
13192
13313
  var package_default = {
13193
13314
  name: "@taskless-app/swarm",
13194
- version: "0.6.19",
13315
+ version: "0.6.20",
13195
13316
  type: "module",
13196
13317
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13197
13318
  keywords: [
@@ -13241,7 +13362,7 @@ var package_default = {
13241
13362
  execa: "^10.0.0",
13242
13363
  zod: "3.25.76"
13243
13364
  },
13244
- gitHead: "a61c26b5e254ef5383d918f14a9f6e8e8a09904a"
13365
+ gitHead: "2d05a2e2b7c197ab916f7ff756e3a829b138df16"
13245
13366
  };
13246
13367
 
13247
13368
  // src/version.ts
@@ -16091,6 +16212,92 @@ async function closeMergedTaskRuns(deps) {
16091
16212
  }
16092
16213
  }
16093
16214
  }
16215
+ function relinkUnboundReviewers(deps) {
16216
+ const all = deps.db.listRuns();
16217
+ const unbound = all.filter((r) => r.kind === "reviewer" && r.reviews_run_id == null && r.pr_url && REVIEWER_ALIVE_STATUSES.has(r.status));
16218
+ if (unbound.length === 0) return;
16219
+ for (const rv of unbound) {
16220
+ const candidates = all.filter(
16221
+ (r) => r.id !== rv.id && r.pr_url === rv.pr_url && (r.kind ?? "drone") !== "reviewer" && (r.kind ?? "drone") !== "verifier" && r.reviews_run_id == null && r.verifies_run_id == null && r.resolves_run_id == null
16222
+ );
16223
+ if (candidates.length !== 1) {
16224
+ const why = candidates.length === 0 ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 ${rv.pr_url} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430` : `\u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 ${rv.pr_url} \u043D\u0430\u0439\u0434\u0435\u043D\u043E ${candidates.length} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u2014 \u043A\u0430\u043A\u043E\u0439 \u0438\u0437 \u043D\u0438\u0445 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439, \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F`;
16225
+ if (!deps.db.events(rv.id).some((e) => (e.text ?? "").includes(why))) deps.db.addEvent(rv.id, "step", why);
16226
+ continue;
16227
+ }
16228
+ const parent = candidates[0];
16229
+ deps.db.updateRun(rv.id, { reviews_run_id: parent.id, review_cycle: parent.review_cycle });
16230
+ deps.db.addEvent(rv.id, "step", `\u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 PR: #${parent.id} (${parent.task_slug}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u043A\u0443\u0434\u0430 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C (TASWM-375)`);
16231
+ deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u043F\u043E\u0434\u043D\u044F\u0442 \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u0435\u0433\u043E \u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043B\u044F\u0436\u0435\u0442 \u0441\u044E\u0434\u0430`);
16232
+ narrate(deps, `${droneLabel(rv)} \u0440\u0430\u0431\u043E\u0442\u0430\u043B \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0442\u0435\u043F\u0435\u0440\u044C \u043B\u044F\u0436\u0435\u0442 \u0432 ${parent.task_slug}, \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442 GitHub.`);
16233
+ }
16234
+ }
16235
+ function deliberateReviewStop(db, run) {
16236
+ const texts = db.events(run.id).map((e) => e.text ?? "");
16237
+ const has = (mark) => texts.some((t) => t.includes(mark));
16238
+ if (has(SELF_REFERENCE_MARK)) {
16239
+ return `PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 (${DAEMON_CODE_PATHS.join(", ")}) \u2014 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E (TASWM-270): \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 \u0438 \u0441\u0432\u043E\u044E \u0436\u0435 \u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0435 \u0430\u0442\u0442\u0435\u0441\u0442\u0443\u0435\u0442. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
16240
+ }
16241
+ if (has(REVIEW_ESCALATION_MARK)) return `\u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B (${REVIEW_SPAWN_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
16242
+ if (has(REVIEW_CYCLE_ESCALATION_MARK)) return `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0446\u0438\u043A\u043B\u043E\u0432 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (${REVIEW_CYCLE_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
16243
+ if (has(reviewBudgetMark(run.review_cycle ?? 0))) return "\u0440\u0435\u0432\u044C\u044E \u044D\u0442\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0441\u0433\u043E\u0440\u0435\u043B\u043E \u043D\u0430 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 \u0442\u0440\u0430\u0442 \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0442\u043E\u0442 \u0436\u0435, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
16244
+ if (has(reviewUnrecoverableMark(run.branch || run.task_slug))) return "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
16245
+ return null;
16246
+ }
16247
+ function noteReviewBlocked(deps, run, reason) {
16248
+ if (!shouldNoteReason(deps.db, run.id, reason)) return;
16249
+ deps.db.addEvent(run.id, "step", `${REVIEW_RESCUE_MARK}: ${reason}`);
16250
+ }
16251
+ async function rescueReviewlessPrs(deps, stormActive, now) {
16252
+ for (const run of reviewlessPrCandidates(deps.db, now)) {
16253
+ try {
16254
+ const stop = deliberateReviewStop(deps.db, run);
16255
+ if (stop) {
16256
+ noteReviewBlocked(deps, run, stop);
16257
+ continue;
16258
+ }
16259
+ const authorGone = AUTHOR_GONE_STATUSES.has(run.status) && (run.status !== "paused" || NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r)));
16260
+ if (authorGone) {
16261
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
16262
+ const liveness = prLiveness(st);
16263
+ if (liveness !== "open") {
16264
+ noteReviewBlocked(deps, run, liveness === "unknown" ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043F\u0440\u043E PR \u2014 \u043D\u0435 \u0437\u043D\u0430\u044E, \u0436\u0438\u0432 \u043B\u0438 \u043E\u043D; \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" : `PR ${liveness === "merged" ? "\u0441\u043C\u0435\u0440\u0436\u0435\u043D" : "\u0437\u0430\u043A\u0440\u044B\u0442"} \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
16265
+ continue;
16266
+ }
16267
+ if (st.isDraft) {
16268
+ noteReviewBlocked(deps, run, "PR \u2014 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A: \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u043D\u0435 \u0441\u043C\u043E\u0442\u0440\u0438\u0442 (\u0441\u043D\u0438\u043C\u0438 draft)");
16269
+ continue;
16270
+ }
16271
+ const wasLabel = STATUS_META[run.status]?.label ?? run.status;
16272
+ deps.db.updateRun(run.id, { status: "review" });
16273
+ deps.db.addEvent(
16274
+ run.id,
16275
+ "step",
16276
+ `${REVIEW_RESCUE_MARK}: \u0430\u0432\u0442\u043E\u0440 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (${wasLabel}), \u0430 PR \u043E\u0442\u043A\u0440\u044B\u0442 \u0438 \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0438\u0439 \u043D\u0443\u0436\u0435\u043D \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0441\u0443\u0434\u044C\u0431\u044B \u0430\u0432\u0442\u043E\u0440\u0430`
16277
+ );
16278
+ narrate(deps, `PR ${droneLabel(run)} \u0436\u0438\u0432, \u0430 \u0435\u0433\u043E \u0430\u0432\u0442\u043E\u0440 \u2014 \u043D\u0435\u0442 (${wasLabel}): \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0443\u0436\u0435\u043D.`);
16279
+ continue;
16280
+ }
16281
+ if (run.status !== "review") continue;
16282
+ noteReviewBlocked(deps, run, reviewGateReason(deps, run, stormActive));
16283
+ } catch (e) {
16284
+ console.error(`[overmind] \u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
16285
+ }
16286
+ }
16287
+ }
16288
+ function reviewGateReason(deps, run, stormActive) {
16289
+ if (!deps.spawnReviewer) return "\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 (SWARM_AUTO_REVIEW=0)";
16290
+ if (stormActive) return "\u0448\u0442\u043E\u0440\u043C Anthropic \u2014 \u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u0440\u043E\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442, \u0440\u0435\u0432\u044C\u044E \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F";
16291
+ if (deps.reviewCutoffAt && run.updated_at < deps.reviewCutoffAt) {
16292
+ return `\u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u0430\u0440\u0448\u0435 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E (cutoff ${deps.reviewCutoffAt}) \u2014 \u0440\u043E\u0439 \u0435\u0433\u043E \u043D\u0435 \u0431\u0435\u0440\u0451\u0442, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0440\u0443\u043A\u0430\u043C\u0438`;
16293
+ }
16294
+ if (deps.resourcesOk && !deps.resourcesOk()) return "\u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0436\u0434\u0451\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0445 \u0440\u0435\u0441\u0443\u0440\u0441\u043E\u0432";
16295
+ if (deps.reviewCap !== void 0) {
16296
+ const busy = countActiveReviewers(deps.db);
16297
+ if (busy >= deps.reviewCap) return `\u0437\u0430\u043D\u044F\u0442 \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 ${busy} \u043F\u0440\u0438 SWARM_REVIEW_CAP=${deps.reviewCap} \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043B\u043E\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F`;
16298
+ }
16299
+ return "\u0432\u0441\u0435 \u0433\u0435\u0439\u0442\u044B \u043E\u0442\u043A\u0440\u044B\u0442\u044B, \u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u2014 \u0441\u043C\u043E\u0442\u0440\u0438 \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u043E\u0448\u0438\u0431\u043A\u0443 \u0441\u043F\u0430\u0432\u043D\u0430";
16300
+ }
16094
16301
  async function closeOrphanedReviewers(deps) {
16095
16302
  for (const rv of deps.db.listRuns()) {
16096
16303
  const checkedId = rv.kind === "reviewer" ? rv.reviews_run_id : rv.kind === "verifier" ? rv.verifies_run_id : null;
@@ -17166,9 +17373,42 @@ async function trySpawnQueuedRun(deps, run) {
17166
17373
  return "skipped";
17167
17374
  }
17168
17375
  if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
17376
+ if (e instanceof WorktreeBusyError && await healBusyWorktree(deps, run, e)) {
17377
+ try {
17378
+ await deps.spawn(run);
17379
+ if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
17380
+ narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)} (\u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0437\u0430 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439).`);
17381
+ return "spawned";
17382
+ } catch (again) {
17383
+ return await failBeforeStart(deps, run, String(again));
17384
+ }
17385
+ }
17169
17386
  return await failBeforeStart(deps, run, String(e));
17170
17387
  }
17171
17388
  }
17389
+ async function healBusyWorktree(deps, run, e) {
17390
+ const path = e.worktreePath;
17391
+ if (!deps.releaseWorktree) return false;
17392
+ if (deps.liveWorktrees?.().has(path)) return false;
17393
+ const holder = deps.db.listRuns().find((r) => r.id !== run.id && r.worktree_path === path && !isTerminalStatus(r.status));
17394
+ if (holder && (holder.pid == null || deps.isAlive(holder.pid))) {
17395
+ return false;
17396
+ }
17397
+ if (deps.isDirty && await deps.isDirty(path).catch(() => true)) {
17398
+ deps.db.addEvent(
17399
+ run.id,
17400
+ "anomaly",
17401
+ `${DIRTY_WORKTREE_MARK}. \u041F\u0443\u0442\u044C: ${path}. \u0416\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043F\u0440\u0438\u0431\u0438\u0440\u0430\u0442\u044C \u043D\u0435 \u0441\u0442\u0430\u043B, \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C: \`git -C ${path} status --untracked-files=all\`.`
17402
+ );
17403
+ narrate(deps, `${run.task_slug}: worktree \u0437\u0430\u043D\u044F\u0442 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439, \u043D\u043E \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
17404
+ return false;
17405
+ }
17406
+ await deps.releaseWorktree(run.repo, path).catch(() => {
17407
+ });
17408
+ deps.db.addEvent(run.id, "step", `worktree ${path} \u0434\u0435\u0440\u0436\u0430\u043B\u0430 \u043C\u0451\u0440\u0442\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u2014 \u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E \u0441\u043F\u0430\u0432\u043D (TASWM-476)`);
17409
+ if (holder) deps.db.addEvent(holder.id, "step", `worktree ${path} \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D: \u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u0441\u043B\u0430\u0433 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id}`);
17410
+ return true;
17411
+ }
17172
17412
  async function failBeforeStart(deps, run, reason) {
17173
17413
  deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
17174
17414
  deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
@@ -17733,6 +17973,18 @@ async function tickOnce(deps) {
17733
17973
  }
17734
17974
  }
17735
17975
  }
17976
+ if (deps.reapWorktrees) {
17977
+ const rw = deps.reapWorktrees;
17978
+ const sinceLast = rw.lastAt == null ? Infinity : now - rw.lastAt;
17979
+ if (sinceLast >= rw.intervalMs) {
17980
+ rw.lastAt = now;
17981
+ try {
17982
+ await rw.run();
17983
+ } catch (e) {
17984
+ console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
17985
+ }
17986
+ }
17987
+ }
17736
17988
  if (deps.logRotate) {
17737
17989
  const lr = deps.logRotate;
17738
17990
  const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
@@ -17969,6 +18221,8 @@ async function tickOnce(deps) {
17969
18221
  }
17970
18222
  }
17971
18223
  await closeMergedTaskRuns(deps);
18224
+ await rescueReviewlessPrs(deps, stormActive, now);
18225
+ relinkUnboundReviewers(deps);
17972
18226
  await closeOrphanedReviewers(deps);
17973
18227
  await spawnVerifiersForOpenPrs(deps, stormActive);
17974
18228
  refreshReviewPhases(deps);
@@ -19362,6 +19616,32 @@ async function runDaemon(opts) {
19362
19616
  intervalMs: SWARM_SWEEP_INTERVAL_MS,
19363
19617
  lastAt: null
19364
19618
  },
19619
+ // Жнец worktree (TASWM-476): убирает копии терминальных прогонов, чьи PR смержены/закрыты.
19620
+ // Живость каталога определяем ПО ПРОЦЕССАМ (listDroneProcesses — тот же снимок, по которому
19621
+ // идёт реконсайл рантайма), а не только по статусу в зеркале: статус мог не успеть
19622
+ // обновиться, а сносить каталог из-под работающего дрона нельзя ни при каком статусе.
19623
+ reapWorktrees: {
19624
+ run: async () => {
19625
+ const report = await reapFinishedWorktrees({
19626
+ db,
19627
+ prStatus: (url) => prStatus(url),
19628
+ isDirty: (p) => worktreeDirty(p),
19629
+ releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
19630
+ pruneWorktrees: (repo) => pruneWorktrees(repo || opts.repo),
19631
+ worktreeExists: existsSync17,
19632
+ liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
19633
+ narrate: narrator.narrate
19634
+ });
19635
+ if (report.removed || report.keptDirty) {
19636
+ console.log(`[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree: \u0441\u043D\u0435\u0441\u0435\u043D\u043E ${report.removed}, \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E \u0441 \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 ${report.keptDirty}`);
19637
+ }
19638
+ },
19639
+ intervalMs: SWARM_WORKTREE_REAP_INTERVAL_MS,
19640
+ lastAt: null
19641
+ },
19642
+ // Самолечение залипшего слага (TASWM-476): те же два кирпича, что у жнеца выше.
19643
+ releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
19644
+ liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
19365
19645
  isDirty: (p) => worktreeDirty(p),
19366
19646
  // git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
19367
19647
  lastCommitMs: (p) => worktreeLastCommitMs(p),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "type": "module",
5
5
  "description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "execa": "^10.0.0",
39
39
  "zod": "3.25.76"
40
40
  },
41
- "gitHead": "a61c26b5e254ef5383d918f14a9f6e8e8a09904a",
41
+ "gitHead": "2d05a2e2b7c197ab916f7ff756e3a829b138df16",
42
42
  "scripts": {
43
43
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
44
44
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",