@taskless-app/swarm 0.6.19 → 0.6.21

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.21",
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: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
2426
2427
  };
2427
2428
  }
2428
2429
  });
@@ -8509,10 +8510,24 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
8509
8510
  }
8510
8511
  }
8511
8512
  var NO_CHECKS_STDERR = /no checks reported/i;
8513
+ var CiAccessDeniedError = class extends Error {
8514
+ detail;
8515
+ constructor(detail) {
8516
+ super(`\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C: ${detail}`);
8517
+ this.name = "CiAccessDeniedError";
8518
+ this.detail = detail;
8519
+ }
8520
+ };
8521
+ var DENIED_STDERR = /(?:^|\D)403(?:\D|$)|not accessible by integration|must have admin rights|resource not accessible/i;
8522
+ function firstLine(s) {
8523
+ return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
8524
+ }
8512
8525
  var realChecksRunner = async (pr) => {
8513
8526
  const r = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
8514
8527
  const stdout = r.stdout ?? "";
8515
- if (!stdout.trim() && NO_CHECKS_STDERR.test(r.stderr ?? "")) return "[]";
8528
+ const stderr = r.stderr ?? "";
8529
+ if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
8530
+ if (!stdout.trim() && DENIED_STDERR.test(stderr)) throw new CiAccessDeniedError(firstLine(stderr));
8516
8531
  return stdout;
8517
8532
  };
8518
8533
  async function prCheckRows(pr, run) {
@@ -8521,13 +8536,17 @@ async function prCheckRows(pr, run) {
8521
8536
  const rows = JSON.parse(out);
8522
8537
  if (!Array.isArray(rows)) return null;
8523
8538
  return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
8524
- } catch {
8539
+ } catch (e) {
8540
+ if (e instanceof CiAccessDeniedError) return { denied: e.detail };
8525
8541
  return null;
8526
8542
  }
8527
8543
  }
8544
+ function isDenied(rows) {
8545
+ return rows != null && !Array.isArray(rows);
8546
+ }
8528
8547
  async function ghChecks(pr, run = realChecksRunner) {
8529
8548
  const rows = await prCheckRows(pr, run);
8530
- if (!rows || rows.length === 0) return null;
8549
+ if (!rows || isDenied(rows) || rows.length === 0) return null;
8531
8550
  let pass = 0, fail = 0, pending2 = 0;
8532
8551
  for (const r of rows) {
8533
8552
  if (r.bucket === "pass") pass++;
@@ -11022,12 +11041,15 @@ async function previousHooksPath(worktree) {
11022
11041
  }
11023
11042
  async function removeWorktree(repo, path) {
11024
11043
  const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
11044
+ await releaseWorktree(repo, path);
11045
+ if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
11046
+ }
11047
+ async function releaseWorktree(repo, path) {
11025
11048
  await cleanupWorktreeProcesses(path);
11026
11049
  await detachWorktree(repo, path);
11027
- if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
11028
11050
  }
11029
11051
  async function worktreeDirty(path) {
11030
- const st = await $5({ reject: false })`git -C ${path} status --porcelain`;
11052
+ const st = await $5({ reject: false })`git -C ${path} status --porcelain --untracked-files=all`;
11031
11053
  if (st.exitCode !== 0) return true;
11032
11054
  const status = st.stdout.trim();
11033
11055
  if (status) return true;
@@ -11080,6 +11102,44 @@ init_config();
11080
11102
  init_agentToken();
11081
11103
  init_linkFailure();
11082
11104
 
11105
+ // src/toolFailure.ts
11106
+ init_linkFailure();
11107
+ var TOOL_FAILURE = {
11108
+ /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
11109
+ NOT_FOUND: "not_found",
11110
+ /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
11111
+ FORBIDDEN: "forbidden",
11112
+ /** Токен роя не состоит в воркспейсе, которым искали. */
11113
+ WORKSPACE_MISMATCH: "workspace_mismatch",
11114
+ /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
11115
+ UNREACHABLE: "unreachable",
11116
+ /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
11117
+ UNAUTHORIZED: "unauthorized",
11118
+ /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
11119
+ TOOL_ERROR: "tool_error",
11120
+ /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
11121
+ EMPTY: "empty"
11122
+ };
11123
+ var TOOL_FAILURE_TEXT = {
11124
+ [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
11125
+ [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
11126
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
11127
+ [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
11128
+ [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
11129
+ [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
11130
+ [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
11131
+ };
11132
+ var TOOL_FAILURE_RETRIABLE = {
11133
+ [TOOL_FAILURE.NOT_FOUND]: false,
11134
+ [TOOL_FAILURE.FORBIDDEN]: false,
11135
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
11136
+ [TOOL_FAILURE.UNREACHABLE]: true,
11137
+ // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
11138
+ [TOOL_FAILURE.UNAUTHORIZED]: false,
11139
+ [TOOL_FAILURE.TOOL_ERROR]: false,
11140
+ [TOOL_FAILURE.EMPTY]: false
11141
+ };
11142
+
11083
11143
  // src/workspace.ts
11084
11144
  init_config();
11085
11145
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4 } from "fs";
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;
@@ -4519,6 +4527,78 @@ var init_oauth = __esm({
4519
4527
  }
4520
4528
  });
4521
4529
 
4530
+ // src/toolFailure.ts
4531
+ function trimDetail(s) {
4532
+ const t = s.trim();
4533
+ return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
4534
+ }
4535
+ function classifyToolFailure(raw) {
4536
+ if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
4537
+ if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
4538
+ if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
4539
+ return TOOL_FAILURE.TOOL_ERROR;
4540
+ }
4541
+ function classifyThrown(e) {
4542
+ const { kind, detail } = explainLinkFailure(e);
4543
+ const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
4544
+ // «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
4545
+ // и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
4546
+ classifyToolFailure(detail)
4547
+ );
4548
+ return { code, detail };
4549
+ }
4550
+ function describeToolFailure(f) {
4551
+ const text = TOOL_FAILURE_TEXT[f.code];
4552
+ const detail = trimDetail(f.detail);
4553
+ return detail ? `${text} (${detail})` : text;
4554
+ }
4555
+ var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
4556
+ var init_toolFailure = __esm({
4557
+ "src/toolFailure.ts"() {
4558
+ "use strict";
4559
+ init_linkFailure();
4560
+ TOOL_FAILURE = {
4561
+ /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
4562
+ NOT_FOUND: "not_found",
4563
+ /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
4564
+ FORBIDDEN: "forbidden",
4565
+ /** Токен роя не состоит в воркспейсе, которым искали. */
4566
+ WORKSPACE_MISMATCH: "workspace_mismatch",
4567
+ /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
4568
+ UNREACHABLE: "unreachable",
4569
+ /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
4570
+ UNAUTHORIZED: "unauthorized",
4571
+ /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
4572
+ TOOL_ERROR: "tool_error",
4573
+ /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
4574
+ EMPTY: "empty"
4575
+ };
4576
+ TOOL_FAILURE_TEXT = {
4577
+ [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
4578
+ [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
4579
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
4580
+ [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
4581
+ [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
4582
+ [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
4583
+ [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
4584
+ };
4585
+ TOOL_FAILURE_RETRIABLE = {
4586
+ [TOOL_FAILURE.NOT_FOUND]: false,
4587
+ [TOOL_FAILURE.FORBIDDEN]: false,
4588
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
4589
+ [TOOL_FAILURE.UNREACHABLE]: true,
4590
+ // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
4591
+ [TOOL_FAILURE.UNAUTHORIZED]: false,
4592
+ [TOOL_FAILURE.TOOL_ERROR]: false,
4593
+ [TOOL_FAILURE.EMPTY]: false
4594
+ };
4595
+ FAILURE_DETAIL_MAX = 300;
4596
+ NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
4597
+ FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
4598
+ MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
4599
+ }
4600
+ });
4601
+
4522
4602
  // src/authState.ts
4523
4603
  function noteTokenRejected(nowMs = Date.now()) {
4524
4604
  if (!denial) {
@@ -4615,25 +4695,44 @@ function isClosingTaskCategory(category) {
4615
4695
  function taskAuthor(u) {
4616
4696
  return u?.id ? { id: u.id, name: u.displayName || "\u0430\u0432\u0442\u043E\u0440 \u0437\u0430\u0434\u0430\u0447\u0438" } : null;
4617
4697
  }
4698
+ function toolErrorText(r) {
4699
+ const fromContent = (r.content ?? []).map((c) => c.text ?? "").filter(Boolean).join(" ").trim();
4700
+ return fromContent || JSON.stringify(r);
4701
+ }
4618
4702
  function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl = fetch) {
4619
4703
  const ws = workspaceId ? { workspaceId } : {};
4620
4704
  const idCache = /* @__PURE__ */ new Map();
4621
4705
  const byWorkspace = /* @__PURE__ */ new Map();
4622
- async function safeCall(name, args) {
4706
+ async function callTool(name, args) {
4623
4707
  try {
4624
4708
  const r = await call(name, WS_FREE_TOOLS.has(name) ? args : { ...args, ...ws });
4625
4709
  if (r?.isError) {
4626
- console.error(`[taskless] \u0442\u0443\u043B ${name} \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443:`, JSON.stringify(r).slice(0, 400));
4627
- return null;
4710
+ const raw = toolErrorText(r);
4711
+ const detail = trimDetail(raw);
4712
+ console.error(`[taskless] \u0442\u0443\u043B ${name} \u0432\u0435\u0440\u043D\u0443\u043B \u043E\u0448\u0438\u0431\u043A\u0443:`, detail);
4713
+ return { ok: false, failure: { code: classifyToolFailure(raw), detail } };
4628
4714
  }
4629
- return r;
4715
+ return { ok: true, res: r };
4630
4716
  } catch (e) {
4631
- console.error(`[taskless] \u0442\u0443\u043B ${name} \u0443\u043F\u0430\u043B:`, e instanceof Error ? e.message : String(e));
4632
- return null;
4717
+ const failure = classifyThrown(e);
4718
+ console.error(`[taskless] \u0442\u0443\u043B ${name} \u0443\u043F\u0430\u043B:`, failure.detail);
4719
+ return { ok: false, failure };
4633
4720
  }
4634
4721
  }
4635
- async function getTask(idOrSlug) {
4636
- const res = await safeCall("get_task", { id: idOrSlug });
4722
+ async function safeCall(name, args) {
4723
+ const out = await callTool(name, args);
4724
+ return out.ok ? out.res : null;
4725
+ }
4726
+ async function getTaskDetailed(idOrSlug) {
4727
+ const out = await callTool("get_task", { id: idOrSlug });
4728
+ if (!out.ok) return { ok: false, failure: out.failure };
4729
+ const body = taskBodyFrom(out.res, idOrSlug);
4730
+ if (!body) {
4731
+ return { ok: false, failure: { code: TOOL_FAILURE.EMPTY, detail: "get_task: task \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435" } };
4732
+ }
4733
+ return { ok: true, task: body };
4734
+ }
4735
+ function taskBodyFrom(res, idOrSlug) {
4637
4736
  const task = res?.structuredContent?.task ?? null;
4638
4737
  if (!task?.id) return null;
4639
4738
  const slug = task.prefix && task.number != null ? `${task.prefix}-${task.number}` : idOrSlug;
@@ -4663,6 +4762,9 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4663
4762
  idCache.set(body.id, body.id);
4664
4763
  return body;
4665
4764
  }
4765
+ async function getTask(idOrSlug) {
4766
+ return taskBodyFrom(await safeCall("get_task", { id: idOrSlug }), idOrSlug);
4767
+ }
4666
4768
  async function getDocument(id) {
4667
4769
  const res = await safeCall("get_document", { id });
4668
4770
  const doc = res?.structuredContent?.document ?? null;
@@ -4754,6 +4856,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4754
4856
  }
4755
4857
  const self = {
4756
4858
  getTask,
4859
+ getTaskDetailed,
4757
4860
  ...fetchFile ? { fetchFile } : {},
4758
4861
  startTask: (idOrSlug) => transition("start_task", idOrSlug),
4759
4862
  // TASWM-294: intent — «человек, посмотри», а не «Готово» (см. докблок в TasklessClient).
@@ -4849,6 +4952,10 @@ function parseWorkspaces(res) {
4849
4952
  return arr.filter((w) => !!w && typeof w.id === "string" && typeof w.slug === "string").map((w) => ({ id: w.id, slug: w.slug, name: typeof w.name === "string" ? w.name : w.slug, role: w.role }));
4850
4953
  }
4851
4954
  function requireTaskBody(task, slug, workspaceId) {
4955
+ if (task && "ok" in task) {
4956
+ if (task.ok) return task.task;
4957
+ throw new TaskBodyUnavailableError(slug, workspaceId, task.failure);
4958
+ }
4852
4959
  if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
4853
4960
  return task;
4854
4961
  }
@@ -5080,16 +5187,25 @@ var init_taskless = __esm({
5080
5187
  init_oauth();
5081
5188
  init_agentToken();
5082
5189
  init_linkFailure();
5190
+ init_toolFailure();
5083
5191
  init_authState();
5084
5192
  init_workspace();
5085
5193
  SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
5086
5194
  WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload", "report_acceptance_check"]);
5087
5195
  TaskBodyUnavailableError = class extends Error {
5088
- constructor(slug, workspaceId) {
5196
+ /** Код причины — по нему решают, повторять ли (TOOL_FAILURE_RETRIABLE), а не по тексту. */
5197
+ code;
5198
+ // TASWM-484: причина называется, а не перечисляется. Прежний текст («задачи там нет
5199
+ // ЛИБО токену её не видно») заставлял человека проверять обе версии руками — 21.08 на
5200
+ // этом ушло полдня, и обе оказались не теми. Теперь код приходит от классификатора
5201
+ // ответа прода, а человеку достаётся объяснение и то, что прод сказал дословно.
5202
+ constructor(slug, workspaceId, failure) {
5203
+ const f = failure ?? { code: TOOL_FAILURE.EMPTY, detail: "" };
5089
5204
  super(
5090
- `\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F: ${slug} \u0432 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 ${workspaceId || "(\u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0438\u043D \u0434\u0435\u043C\u043E\u043D\u0430)"} \u2014 Taskless \u043D\u0430 get_task \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0443\u0441\u0442\u043E (\u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0430\u043C \u043D\u0435\u0442 \u043B\u0438\u0431\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0435\u0451 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E, \u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430). \u0414\u0440\u043E\u043D \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u0442: \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u0441\u043B\u0430\u0433\u0443, \u0431\u0435\u0437 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u0432, \u0434\u043E\u0440\u043E\u0436\u0435 \u0438 \u0432\u0440\u0435\u0434\u043D\u0435\u0435, \u0447\u0435\u043C \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0442\u044C (TASWM-288).`
5205
+ `\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F: ${slug} \u0432 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 ${workspaceId || "(\u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0438\u043D \u0434\u0435\u043C\u043E\u043D\u0430)"} \u2014 ${describeToolFailure(f)}. \u0414\u0440\u043E\u043D \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u0442: \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u0441\u043B\u0430\u0433\u0443, \u0431\u0435\u0437 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u0432, \u0434\u043E\u0440\u043E\u0436\u0435 \u0438 \u0432\u0440\u0435\u0434\u043D\u0435\u0435, \u0447\u0435\u043C \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0442\u044C (TASWM-288).`
5091
5206
  );
5092
5207
  this.name = "TaskBodyUnavailableError";
5208
+ this.code = f.code;
5093
5209
  }
5094
5210
  };
5095
5211
  TASK_CONTEXT_MAX_DOCS = 5;
@@ -10507,6 +10623,70 @@ function parkReason(attempts, reason) {
10507
10623
  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
10624
  }
10509
10625
 
10626
+ // src/worktreeReap.ts
10627
+ init_src();
10628
+ 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";
10629
+ var HOLDS_WORKTREE = /* @__PURE__ */ new Set(["running", "blocked", "review", "paused", "storm_dead", "stalled"]);
10630
+ function isHelperRun(run) {
10631
+ return run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null;
10632
+ }
10633
+ function prIsDead(st) {
10634
+ return st.merged || st.state.toUpperCase() === "MERGED" || st.state.toUpperCase() === "CLOSED";
10635
+ }
10636
+ function heldByLive(deps, all, run, path) {
10637
+ if (deps.liveWorktrees?.().has(path)) return true;
10638
+ return all.some((r) => r.id !== run.id && r.worktree_path === path && HOLDS_WORKTREE.has(r.status));
10639
+ }
10640
+ async function reapFinishedWorktrees(deps) {
10641
+ const report = { removed: 0, keptDirty: 0 };
10642
+ const prunedRepos = /* @__PURE__ */ new Set();
10643
+ const all = deps.db.listRuns();
10644
+ for (const run of all) {
10645
+ const path = run.worktree_path;
10646
+ if (!path || !isTerminalStatus(run.status)) continue;
10647
+ if (heldByLive(deps, all, run, path)) continue;
10648
+ if (!deps.worktreeExists(path)) {
10649
+ if (run.repo && deps.pruneWorktrees && !prunedRepos.has(run.repo)) {
10650
+ prunedRepos.add(run.repo);
10651
+ await deps.pruneWorktrees(run.repo).catch(() => {
10652
+ });
10653
+ }
10654
+ continue;
10655
+ }
10656
+ if (!isHelperRun(run)) {
10657
+ if (!run.pr_url) continue;
10658
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
10659
+ if (!st || !prIsDead(st)) continue;
10660
+ }
10661
+ if (await keepDirty(deps, run, path)) {
10662
+ report.keptDirty += 1;
10663
+ continue;
10664
+ }
10665
+ await deps.releaseWorktree(run.repo, path).catch(() => {
10666
+ });
10667
+ 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`);
10668
+ report.removed += 1;
10669
+ }
10670
+ return report;
10671
+ }
10672
+ function reapReason(run) {
10673
+ 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";
10674
+ }
10675
+ async function keepDirty(deps, run, path) {
10676
+ const dirty = deps.isDirty ? await deps.isDirty(path).catch(() => true) : true;
10677
+ if (!dirty) return false;
10678
+ const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(DIRTY_WORKTREE_MARK));
10679
+ if (!already) {
10680
+ deps.db.addEvent(
10681
+ run.id,
10682
+ "anomaly",
10683
+ `${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\`).`
10684
+ );
10685
+ 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}).`);
10686
+ }
10687
+ return true;
10688
+ }
10689
+
10510
10690
  // src/overmind.ts
10511
10691
  init_git();
10512
10692
 
@@ -10696,10 +10876,24 @@ function fullCommitHeadline(headline, body) {
10696
10876
  return head.slice(0, -1) + tail2;
10697
10877
  }
10698
10878
  var NO_CHECKS_STDERR = /no checks reported/i;
10879
+ var CiAccessDeniedError = class extends Error {
10880
+ detail;
10881
+ constructor(detail) {
10882
+ super(`\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C: ${detail}`);
10883
+ this.name = "CiAccessDeniedError";
10884
+ this.detail = detail;
10885
+ }
10886
+ };
10887
+ var DENIED_STDERR = /(?:^|\D)403(?:\D|$)|not accessible by integration|must have admin rights|resource not accessible/i;
10888
+ function firstLine(s) {
10889
+ return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
10890
+ }
10699
10891
  var realChecksRunner = async (pr) => {
10700
10892
  const r = await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
10701
10893
  const stdout = r.stdout ?? "";
10702
- if (!stdout.trim() && NO_CHECKS_STDERR.test(r.stderr ?? "")) return "[]";
10894
+ const stderr = r.stderr ?? "";
10895
+ if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
10896
+ if (!stdout.trim() && DENIED_STDERR.test(stderr)) throw new CiAccessDeniedError(firstLine(stderr));
10703
10897
  return stdout;
10704
10898
  };
10705
10899
  async function prCheckRows(pr, run) {
@@ -10708,13 +10902,17 @@ async function prCheckRows(pr, run) {
10708
10902
  const rows = JSON.parse(out);
10709
10903
  if (!Array.isArray(rows)) return null;
10710
10904
  return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
10711
- } catch {
10905
+ } catch (e) {
10906
+ if (e instanceof CiAccessDeniedError) return { denied: e.detail };
10712
10907
  return null;
10713
10908
  }
10714
10909
  }
10910
+ function isDenied(rows) {
10911
+ return rows != null && !Array.isArray(rows);
10912
+ }
10715
10913
  async function ghChecks(pr, run = realChecksRunner) {
10716
10914
  const rows = await prCheckRows(pr, run);
10717
- if (!rows || rows.length === 0) return null;
10915
+ if (!rows || isDenied(rows) || rows.length === 0) return null;
10718
10916
  let pass = 0, fail = 0, pending2 = 0;
10719
10917
  for (const r of rows) {
10720
10918
  if (r.bucket === "pass") pass++;
@@ -10745,6 +10943,7 @@ function runIdFromLink(link) {
10745
10943
  async function prCiStatus(pr, run = realChecksRunner) {
10746
10944
  const rows = await prCheckRows(pr, run);
10747
10945
  if (!rows) return null;
10946
+ if (isDenied(rows)) return { bucket: "denied", failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0, detail: rows.denied };
10748
10947
  if (rows.length === 0) return { bucket: "none", failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0 };
10749
10948
  const total = rows.length;
10750
10949
  const pass = rows.filter((r) => r.bucket === "pass").length;
@@ -11740,6 +11939,52 @@ import { createHash as createHash3 } from "crypto";
11740
11939
 
11741
11940
  // src/anomalies.ts
11742
11941
  init_src();
11942
+
11943
+ // src/reviewRescue.ts
11944
+ init_src();
11945
+ var REVIEW_RESCUE_AFTER_MS = Math.max(6e4, Number(process.env.SWARM_REVIEW_RESCUE_MS) || 10 * 6e4);
11946
+ var REVIEW_RESCUE_MARK = "\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E";
11947
+ var AUTHOR_GONE_STATUSES = /* @__PURE__ */ new Set(["paused", "failed", "stalled"]);
11948
+ function isExecutorRun(run) {
11949
+ if (run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null) return false;
11950
+ const kind = run.kind ?? "drone";
11951
+ return kind === "drone" || kind === "hero";
11952
+ }
11953
+ function prOpenedAtMs(events) {
11954
+ const opened = events.filter((e) => e.kind === "pr_opened");
11955
+ if (opened.length === 0) return null;
11956
+ const ms = sqliteMs(opened[opened.length - 1].at);
11957
+ return Number.isFinite(ms) ? ms : null;
11958
+ }
11959
+ function reviewlessPrCandidates(db, now, afterMs = REVIEW_RESCUE_AFTER_MS) {
11960
+ const out = [];
11961
+ for (const run of db.listRuns()) {
11962
+ if (!run.pr_url) continue;
11963
+ if (run.status !== "review" && !AUTHOR_GONE_STATUSES.has(run.status)) continue;
11964
+ if (!isExecutorRun(run)) continue;
11965
+ if (run.review_verdict != null) continue;
11966
+ if (run.status === "review") {
11967
+ if (db.reviewerCountFor(run.id) > 0) continue;
11968
+ if (db.activeReviewersFor(run.id).length > 0) continue;
11969
+ }
11970
+ const events = db.events(run.id);
11971
+ const openedAt = prOpenedAtMs(events) ?? sqliteMs(run.updated_at);
11972
+ if (!Number.isFinite(openedAt) || now - openedAt < afterMs) continue;
11973
+ out.push(run);
11974
+ }
11975
+ return out;
11976
+ }
11977
+ function lastReviewBlockReason(db, runId) {
11978
+ const hit = db.events(runId).findLast((e) => (e.text ?? "").includes(`${REVIEW_RESCUE_MARK}:`));
11979
+ if (!hit?.text) return null;
11980
+ const idx = hit.text.indexOf(`${REVIEW_RESCUE_MARK}:`);
11981
+ return hit.text.slice(idx + REVIEW_RESCUE_MARK.length + 1).trim() || null;
11982
+ }
11983
+ function shouldNoteReason(db, runId, reason) {
11984
+ return lastReviewBlockReason(db, runId) !== reason;
11985
+ }
11986
+
11987
+ // src/anomalies.ts
11743
11988
  var LEVEL_RANK = { note: 0, feed: 1, decision: 2 };
11744
11989
  function plural2(n, forms) {
11745
11990
  const mod100 = n % 100;
@@ -12107,15 +12352,18 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
12107
12352
  key: `pr_no_reviewer:${run.id}`,
12108
12353
  minDurationMs: thresholds.prNoReviewerMs,
12109
12354
  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
- })
12355
+ build: () => {
12356
+ const why = lastReviewBlockReason(db, run.id);
12357
+ return {
12358
+ runId: run.id,
12359
+ // В ленту: рой действует сам (автоподъём), решения человека тут не требуется —
12360
+ // если и подъём не вышел, причина в теле и есть то, с чем человек идёт разбираться.
12361
+ level: "feed",
12362
+ title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
12363
+ 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}.`,
12364
+ ...hrefOf(run.task_url)
12365
+ };
12366
+ }
12119
12367
  });
12120
12368
  } else if (run.review_verdict === "changes" && db.activeReviewersFor(run.id).length === 0) {
12121
12369
  candidates.push({
@@ -13191,7 +13439,7 @@ async function changedPaths(worktree, opts = {}) {
13191
13439
  // package.json
13192
13440
  var package_default = {
13193
13441
  name: "@taskless-app/swarm",
13194
- version: "0.6.19",
13442
+ version: "0.6.21",
13195
13443
  type: "module",
13196
13444
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13197
13445
  keywords: [
@@ -13241,7 +13489,7 @@ var package_default = {
13241
13489
  execa: "^10.0.0",
13242
13490
  zod: "3.25.76"
13243
13491
  },
13244
- gitHead: "a61c26b5e254ef5383d918f14a9f6e8e8a09904a"
13492
+ gitHead: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
13245
13493
  };
13246
13494
 
13247
13495
  // src/version.ts
@@ -15745,6 +15993,9 @@ var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u04
15745
15993
  var CI_NO_CHECKS_MARK = "\u0443 PR \u043D\u0435\u0442 \u0447\u0435\u043A\u043E\u0432 \u0438 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442";
15746
15994
  var CI_WAIT_POLL_MARK = "\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A";
15747
15995
  var CI_POLL_LIMIT_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u043E \u043F\u043E\u0442\u043E\u043B\u043A\u0443";
15996
+ var CI_DENIED_LIMIT = Number(process.env.SWARM_CI_DENIED_LIMIT) || 3;
15997
+ var CI_DENIED_MARK = "\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C (403)";
15998
+ var CI_DENIED_STOP_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 (403)";
15748
15999
  var CI_WAIT_LIMIT_MS = Number(process.env.SWARM_CI_WAIT_LIMIT_MS) || 45 * 60 * 1e3;
15749
16000
  var CI_POLL_LIMIT = Math.max(
15750
16001
  Number(process.env.SWARM_CI_POLL_LIMIT) || 90,
@@ -16091,6 +16342,92 @@ async function closeMergedTaskRuns(deps) {
16091
16342
  }
16092
16343
  }
16093
16344
  }
16345
+ function relinkUnboundReviewers(deps) {
16346
+ const all = deps.db.listRuns();
16347
+ const unbound = all.filter((r) => r.kind === "reviewer" && r.reviews_run_id == null && r.pr_url && REVIEWER_ALIVE_STATUSES.has(r.status));
16348
+ if (unbound.length === 0) return;
16349
+ for (const rv of unbound) {
16350
+ const candidates = all.filter(
16351
+ (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
16352
+ );
16353
+ if (candidates.length !== 1) {
16354
+ 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`;
16355
+ if (!deps.db.events(rv.id).some((e) => (e.text ?? "").includes(why))) deps.db.addEvent(rv.id, "step", why);
16356
+ continue;
16357
+ }
16358
+ const parent = candidates[0];
16359
+ deps.db.updateRun(rv.id, { reviews_run_id: parent.id, review_cycle: parent.review_cycle });
16360
+ 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)`);
16361
+ 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`);
16362
+ 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.`);
16363
+ }
16364
+ }
16365
+ function deliberateReviewStop(db, run) {
16366
+ const texts = db.events(run.id).map((e) => e.text ?? "");
16367
+ const has = (mark) => texts.some((t) => t.includes(mark));
16368
+ if (has(SELF_REFERENCE_MARK)) {
16369
+ 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`;
16370
+ }
16371
+ 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`;
16372
+ 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`;
16373
+ 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";
16374
+ 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";
16375
+ return null;
16376
+ }
16377
+ function noteReviewBlocked(deps, run, reason) {
16378
+ if (!shouldNoteReason(deps.db, run.id, reason)) return;
16379
+ deps.db.addEvent(run.id, "step", `${REVIEW_RESCUE_MARK}: ${reason}`);
16380
+ }
16381
+ async function rescueReviewlessPrs(deps, stormActive, now) {
16382
+ for (const run of reviewlessPrCandidates(deps.db, now)) {
16383
+ try {
16384
+ const stop = deliberateReviewStop(deps.db, run);
16385
+ if (stop) {
16386
+ noteReviewBlocked(deps, run, stop);
16387
+ continue;
16388
+ }
16389
+ const authorGone = AUTHOR_GONE_STATUSES.has(run.status) && (run.status !== "paused" || NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r)));
16390
+ if (authorGone) {
16391
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
16392
+ const liveness = prLiveness(st);
16393
+ if (liveness !== "open") {
16394
+ 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`);
16395
+ continue;
16396
+ }
16397
+ if (st.isDraft) {
16398
+ 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)");
16399
+ continue;
16400
+ }
16401
+ const wasLabel = STATUS_META[run.status]?.label ?? run.status;
16402
+ deps.db.updateRun(run.id, { status: "review" });
16403
+ deps.db.addEvent(
16404
+ run.id,
16405
+ "step",
16406
+ `${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`
16407
+ );
16408
+ 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.`);
16409
+ continue;
16410
+ }
16411
+ if (run.status !== "review") continue;
16412
+ noteReviewBlocked(deps, run, reviewGateReason(deps, run, stormActive));
16413
+ } catch (e) {
16414
+ 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);
16415
+ }
16416
+ }
16417
+ }
16418
+ function reviewGateReason(deps, run, stormActive) {
16419
+ 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)";
16420
+ 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";
16421
+ if (deps.reviewCutoffAt && run.updated_at < deps.reviewCutoffAt) {
16422
+ 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`;
16423
+ }
16424
+ 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";
16425
+ if (deps.reviewCap !== void 0) {
16426
+ const busy = countActiveReviewers(deps.db);
16427
+ 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`;
16428
+ }
16429
+ 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";
16430
+ }
16094
16431
  async function closeOrphanedReviewers(deps) {
16095
16432
  for (const rv of deps.db.listRuns()) {
16096
16433
  const checkedId = rv.kind === "reviewer" ? rv.reviews_run_id : rv.kind === "verifier" ? rv.verifies_run_id : null;
@@ -16156,8 +16493,13 @@ function buildCiWakeNudge(summary) {
16156
16493
  ].join("\n");
16157
16494
  }
16158
16495
  function buildCiReadyNudge(prUrl, over) {
16159
- const counts = over.kind !== "gaveup" && over.ci.total > 0 ? ` (${over.ci.pass}/${over.ci.total})` : "";
16160
- const head = over.kind === "none" ? `\u2139\uFE0F \u0416\u0434\u0430\u0442\u044C CI \u043D\u0430 PR ${prUrl} \u041D\u0415\u0427\u0415\u0413\u041E: \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 \u0443 PR \u043D\u0435\u0442, \u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E workflow \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0442\u0443\u0442 \u043D\u0435 \u0431\u044B\u0432\u0430\u0435\u0442. \u0420\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D), \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u044D\u0442\u043E \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.` : over.kind === "gaveup" ? `\u26A0\uFE0F \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0442\u0430\u043A \u0438 \u041D\u0415 \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u2014 CI \u043E\u0442\u0432\u0430\u043B\u0438\u043B\u0441\u044F \u0438\u043B\u0438 \u043D\u0430\u043C\u0435\u0440\u0442\u0432\u043E \u0432\u0438\u0441\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u0438 \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u0435\u0433\u043E \u043E\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C (\u0441\u0440\u043E\u043A\u0438 \u0438 \u0441\u0447\u0451\u0442\u0447\u0438\u043A \u2014 \u0432 \u0441\u043E\u0431\u044B\u0442\u0438\u044F\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430). \u0420\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0432\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D). \u041D\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0439 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u043F\u0440\u043E \u044D\u0442\u043E \u0440\u043E\u0439 \u0443\u0436\u0435 \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C.` : `\u2705 \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u0438 \u043E\u043D\u0438 \u0437\u0435\u043B\u0451\u043D\u044B\u0435${counts} \u2014 \u0440\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E, \u0436\u0434\u0430\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0447\u0435\u0433\u043E.`;
16496
+ const counts = (over.kind === "pass" || over.kind === "none") && over.ci.total > 0 ? ` (${over.ci.pass}/${over.ci.total})` : "";
16497
+ const head = over.kind === "none" ? `\u2139\uFE0F \u0416\u0434\u0430\u0442\u044C CI \u043D\u0430 PR ${prUrl} \u041D\u0415\u0427\u0415\u0413\u041E: \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 \u0443 PR \u043D\u0435\u0442, \u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E workflow \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0442\u0443\u0442 \u043D\u0435 \u0431\u044B\u0432\u0430\u0435\u0442. \u0420\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D), \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u044D\u0442\u043E \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.` : over.kind === "gaveup" ? `\u26A0\uFE0F \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0442\u0430\u043A \u0438 \u041D\u0415 \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u2014 CI \u043E\u0442\u0432\u0430\u043B\u0438\u043B\u0441\u044F \u0438\u043B\u0438 \u043D\u0430\u043C\u0435\u0440\u0442\u0432\u043E \u0432\u0438\u0441\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u0438 \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u0435\u0433\u043E \u043E\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C (\u0441\u0440\u043E\u043A\u0438 \u0438 \u0441\u0447\u0451\u0442\u0447\u0438\u043A \u2014 \u0432 \u0441\u043E\u0431\u044B\u0442\u0438\u044F\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430). \u0420\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0432\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D). \u041D\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0439 CI \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u043D\u0435 \u043F\u0440\u043E\u0441\u0438 \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u043F\u0440\u043E \u044D\u0442\u043E \u0440\u043E\u0439 \u0443\u0436\u0435 \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C.` : (
16498
+ // TASWM-483: чеки, скорее всего, ЕСТЬ и даже зелёные — их просто не видно рою (403).
16499
+ // Врать «CI не прошёл» тут так же нельзя, как «CI зелёный»: единственная честная
16500
+ // формулировка — «не проверялся», и она обязана доехать до вердикта.
16501
+ over.kind === "denied" ? `\u26A0\uFE0F \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0440\u043E\u044E \u041D\u0415 \u0412\u0418\u0414\u041D\u042B: GitHub \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043E\u0442\u043A\u0430\u0437\u043E\u043C \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C \u2014 403${over.detail ? ` (${over.detail})` : ""}. \u0427\u0435\u043A\u0438 \u043D\u0430 PR, \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E, \u0435\u0441\u0442\u044C \u0438 \u0434\u0430\u0436\u0435 \u0437\u0435\u043B\u0451\u043D\u044B\u0435, \u043D\u043E \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0438\u0445 \u0440\u043E\u0439 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442, \u043F\u043E\u043A\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043D\u0435 \u0432\u044B\u0434\u0430\u0441\u0442 \u0434\u043E\u0441\u0442\u0443\u043F; \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0440\u043E\u0439 \u0443\u0436\u0435 \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C. \u0420\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0432\u043E\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E: \u0432\u044B\u043D\u043E\u0441\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0441\u0430\u043C (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D), \u0438 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439 \xABCI \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0441\u044F: \u0440\u043E\u044E \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C (403)\xBB. \u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u043C PR \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0441\u0438.` : `\u2705 \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 CI \u043D\u0430 PR ${prUrl} \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u041B\u0418\u0421\u042C \u0438 \u043E\u043D\u0438 \u0437\u0435\u043B\u0451\u043D\u044B\u0435${counts} \u2014 \u0440\u043E\u0439 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B \u0442\u0435\u0431\u044F \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E, \u0436\u0434\u0430\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0447\u0435\u0433\u043E.`
16502
+ );
16161
16503
  return [
16162
16504
  head,
16163
16505
  "",
@@ -16627,6 +16969,27 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
16627
16969
  return;
16628
16970
  }
16629
16971
  const ci = await deps.ci.checks(run.pr_url).catch(() => null);
16972
+ if (ci?.bucket === "denied") {
16973
+ const seen = ciDeniedStreak(deps.db, run.id) + 1;
16974
+ if (seen < CI_DENIED_LIMIT) {
16975
+ deps.db.addEvent(
16976
+ run.id,
16977
+ "ci_checked",
16978
+ `CI \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${CI_DENIED_MARK}${ci.detail ? ` \u2014 ${ci.detail}` : ""}; \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${seen}/${CI_DENIED_LIMIT} [${CI_WAIT_POLL_MARK}]`
16979
+ );
16980
+ return;
16981
+ }
16982
+ closeCiWatch(
16983
+ deps,
16984
+ run,
16985
+ CI_DENIED_STOP_MARK,
16986
+ `GitHub \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 403 \u043D\u0430 \u0447\u0442\u0435\u043D\u0438\u0435 \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432${ci.detail ? ` (${ci.detail})` : ""} \u2014 ${seen} \u043E\u043F\u0440\u043E\u0441(\u043E\u0432) \u043F\u043E\u0434\u0440\u044F\u0434. \u042D\u0442\u043E \u043D\u0435 \u0441\u0431\u043E\u0439 \u0441\u0435\u0442\u0438 \u0438 \u043D\u0435 \u043C\u0435\u0434\u043B\u0435\u043D\u043D\u044B\u0439 CI: \u0443 \u0440\u043E\u044F \u043D\u0435\u0442 \u043F\u0440\u0430\u0432 \u043D\u0430 \u0447\u0435\u043A\u0438 \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438, \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0442\u044C \u0431\u0435\u0441\u0441\u043C\u044B\u0441\u043B\u0435\u043D\u043D\u043E. \u041F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u044F \u043D\u0435 \u0432\u0438\u0436\u0443 \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u044E; \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0439\u0434\u0451\u0442 \u0411\u0415\u0417 CI (\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 \u044D\u0442\u043E \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435), \u0434\u043E\u0441\u0442\u0443\u043F \u0432\u044B\u0434\u0430\u0451\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A: \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C GitHub App \u0440\u043E\u044F \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439/\u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044E.`,
16987
+ `${droneLabel(run)}: GitHub \u043D\u0435 \u0434\u0430\u0451\u0442 \u0440\u043E\u044E \u0447\u0438\u0442\u0430\u0442\u044C \u0447\u0435\u043A\u0438 (403) \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043D\u0435 \u0432\u0438\u0436\u0443, \u0437\u043E\u0432\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0438 \u0432\u0435\u0434\u0443 \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 CI.`
16988
+ );
16989
+ await escalateCiDenied(deps, run, ci.detail ?? null, seen);
16990
+ await wakeReviewerOnCiReady(deps, run, { kind: "denied", ...ci.detail ? { detail: ci.detail } : {} });
16991
+ return;
16992
+ }
16630
16993
  if (ci?.bucket === "none" && !repoHasCi(deps, run)) {
16631
16994
  closeCiWatch(
16632
16995
  deps,
@@ -16670,7 +17033,27 @@ function ciWatchMark(base3, cycle) {
16670
17033
  return `${base3} [\u0446\u0438\u043A\u043B ${cycle}]`;
16671
17034
  }
16672
17035
  function isCiWatchClose(text) {
16673
- return text.includes(CI_NO_CHECKS_MARK) || text.includes(CI_POLL_LIMIT_MARK);
17036
+ return text.includes(CI_NO_CHECKS_MARK) || text.includes(CI_POLL_LIMIT_MARK) || text.includes(CI_DENIED_STOP_MARK);
17037
+ }
17038
+ function ciDeniedStreak(db, runId) {
17039
+ return ciStreak(db, runId, CI_DENIED_MARK).count;
17040
+ }
17041
+ async function escalateCiDenied(deps, run, detail, polls) {
17042
+ const why = `${CI_DENIED_MARK}: GitHub \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 403 \u043D\u0430 \u0447\u0442\u0435\u043D\u0438\u0435 \u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 PR${detail ? ` (${detail})` : ""} \u2014 ${polls} \u043E\u043F\u0440\u043E\u0441(\u043E\u0432) \u043F\u043E\u0434\u0440\u044F\u0434. \u0420\u043E\u0439 \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0442\u044C PR \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443\u043C\u0435\u0435\u0442, \u0430 \u0447\u0438\u0442\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u2014 \u043D\u0435\u0442: \u043F\u0440\u0430\u0432\u0430 \u0432\u044B\u0434\u0430\u043D\u044B \u0447\u0430\u0441\u0442\u0438\u0447\u043D\u043E. \u041E\u043F\u0440\u043E\u0441 CI \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u043F\u0440\u0435\u043A\u0440\u0430\u0449\u0451\u043D, \u0440\u0435\u0432\u044C\u044E \u0438\u0434\u0451\u0442 \u0431\u0435\u0437 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A: \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C GitHub App \u0440\u043E\u044F \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439/\u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044E.`;
17043
+ deps.db.addEvent(run.id, "anomaly", why);
17044
+ if (!deps.watchdog?.notify) return;
17045
+ const signal = {
17046
+ key: `ci_denied:${run.id}:${run.review_cycle}`,
17047
+ title: signalTitle(run, "GitHub \u043D\u0435 \u0434\u0430\u0451\u0442 \u0440\u043E\u044E \u0447\u0438\u0442\u0430\u0442\u044C \u0447\u0435\u043A\u0438 (403)"),
17048
+ body: `${run.task_slug}: ${polls} \u043E\u043F\u0440\u043E\u0441(\u043E\u0432) \u043F\u043E\u0434\u0440\u044F\u0434 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0438\u0441\u044C 403${detail ? ` (${detail})` : ""} \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0440\u043E\u0439 \u043D\u0435 \u0432\u0438\u0434\u0438\u0442 \u0438 \u0436\u0434\u0430\u0442\u044C \u0438\u0445 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0420\u0435\u0432\u044C\u044E \u043F\u043E\u0439\u0434\u0451\u0442 \u0431\u0435\u0437 CI (\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 \u044D\u0442\u043E \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435). \u0412\u044B\u0434\u0430\u0439 \u0434\u043E\u0441\u0442\u0443\u043F: GitHub App \u0440\u043E\u044F \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439/\u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044E. PR: ${run.pr_url ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D"}.`,
17049
+ runId: run.id,
17050
+ ...hrefOf(run.task_url)
17051
+ };
17052
+ try {
17053
+ await deps.watchdog.notify(signal);
17054
+ } catch (e) {
17055
+ console.error(`[overmind] \u0441\u0438\u0433\u043D\u0430\u043B \u043E\u0431 \u043E\u0442\u043A\u0430\u0437\u0435 GitHub \u043F\u043E \u0447\u0435\u043A\u0430\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B:`, e);
17056
+ }
16674
17057
  }
16675
17058
  function ciWatchClosed(db, run) {
16676
17059
  return ciWatchCloseKind(db, run) != null;
@@ -16679,20 +17062,31 @@ var NO_CHECKS_STATUS = { bucket: "none", failing: [], failingRunIds: [], total:
16679
17062
  function ciWatchCloseKind(db, run) {
16680
17063
  const noChecks = ciWatchMark(CI_NO_CHECKS_MARK, run.review_cycle);
16681
17064
  const gaveUp = ciWatchMark(CI_POLL_LIMIT_MARK, run.review_cycle);
17065
+ const denied = ciWatchMark(CI_DENIED_STOP_MARK, run.review_cycle);
16682
17066
  for (const e of db.events(run.id)) {
16683
17067
  const text = e.text ?? "";
16684
17068
  if (text.includes(noChecks)) return { kind: "none", ci: NO_CHECKS_STATUS };
16685
17069
  if (text.includes(gaveUp)) return { kind: "gaveup" };
17070
+ if (text.includes(denied)) {
17071
+ const detail = deniedDetailFrom(text);
17072
+ return { kind: "denied", ...detail ? { detail } : {} };
17073
+ }
16686
17074
  }
16687
17075
  return null;
16688
17076
  }
17077
+ function deniedDetailFrom(text) {
17078
+ return text.match(/на чтение чек-ранов \(([^)]+)\)/)?.[1];
17079
+ }
16689
17080
  function ciPollStreak(db, runId) {
17081
+ return ciStreak(db, runId, CI_WAIT_POLL_MARK);
17082
+ }
17083
+ function ciStreak(db, runId, mark) {
16690
17084
  const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked" || isCiWatchClose(e.text ?? ""));
16691
17085
  let count = 0;
16692
17086
  let sinceMs = null;
16693
17087
  for (let i = evs.length - 1; i >= 0; i -= 1) {
16694
17088
  const e = evs[i];
16695
- if (e.kind !== "ci_checked" || !(e.text ?? "").includes(CI_WAIT_POLL_MARK)) break;
17089
+ if (e.kind !== "ci_checked" || !(e.text ?? "").includes(mark)) break;
16696
17090
  count += 1;
16697
17091
  sinceMs = sqliteMs(e.at);
16698
17092
  }
@@ -16747,6 +17141,10 @@ function ciReadyLabel(over) {
16747
17141
  // (isCiWatchClose/ciWatchCloseKind ищут его по подстроке), и событие о том, что
16748
17142
  // побудка ждёт слота, не должно выглядеть вторым решением о закрытии.
16749
17143
  "\u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0442\u0430\u043A \u0438 \u043D\u0435 \u0434\u043E\u0436\u0434\u0430\u043B\u0438\u0441\u044C, \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u043E\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C CI"
17144
+ ) : over.kind === "denied" ? (
17145
+ // По той же причине не дословно CI_DENIED_STOP_MARK (TASWM-483) — но «403» в тексте
17146
+ // остаётся: по журналу должна быть видна причина, а не «проверок нет».
17147
+ "\u0447\u0435\u043A\u0438 \u0440\u043E\u044E \u043D\u0435 \u0432\u0438\u0434\u043D\u044B: GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C (403)"
16750
17148
  ) : `CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 (${over.ci.pass}/${over.ci.total})`;
16751
17149
  }
16752
17150
  async function wakeReviewerOnCiReady(deps, run, over) {
@@ -17166,9 +17564,42 @@ async function trySpawnQueuedRun(deps, run) {
17166
17564
  return "skipped";
17167
17565
  }
17168
17566
  if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
17567
+ if (e instanceof WorktreeBusyError && await healBusyWorktree(deps, run, e)) {
17568
+ try {
17569
+ await deps.spawn(run);
17570
+ if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
17571
+ 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).`);
17572
+ return "spawned";
17573
+ } catch (again) {
17574
+ return await failBeforeStart(deps, run, String(again));
17575
+ }
17576
+ }
17169
17577
  return await failBeforeStart(deps, run, String(e));
17170
17578
  }
17171
17579
  }
17580
+ async function healBusyWorktree(deps, run, e) {
17581
+ const path = e.worktreePath;
17582
+ if (!deps.releaseWorktree) return false;
17583
+ if (deps.liveWorktrees?.().has(path)) return false;
17584
+ const holder = deps.db.listRuns().find((r) => r.id !== run.id && r.worktree_path === path && !isTerminalStatus(r.status));
17585
+ if (holder && (holder.pid == null || deps.isAlive(holder.pid))) {
17586
+ return false;
17587
+ }
17588
+ if (deps.isDirty && await deps.isDirty(path).catch(() => true)) {
17589
+ deps.db.addEvent(
17590
+ run.id,
17591
+ "anomaly",
17592
+ `${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\`.`
17593
+ );
17594
+ 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.`);
17595
+ return false;
17596
+ }
17597
+ await deps.releaseWorktree(run.repo, path).catch(() => {
17598
+ });
17599
+ 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)`);
17600
+ 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}`);
17601
+ return true;
17602
+ }
17172
17603
  async function failBeforeStart(deps, run, reason) {
17173
17604
  deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
17174
17605
  deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
@@ -17733,6 +18164,18 @@ async function tickOnce(deps) {
17733
18164
  }
17734
18165
  }
17735
18166
  }
18167
+ if (deps.reapWorktrees) {
18168
+ const rw = deps.reapWorktrees;
18169
+ const sinceLast = rw.lastAt == null ? Infinity : now - rw.lastAt;
18170
+ if (sinceLast >= rw.intervalMs) {
18171
+ rw.lastAt = now;
18172
+ try {
18173
+ await rw.run();
18174
+ } catch (e) {
18175
+ console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
18176
+ }
18177
+ }
18178
+ }
17736
18179
  if (deps.logRotate) {
17737
18180
  const lr = deps.logRotate;
17738
18181
  const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
@@ -17969,6 +18412,8 @@ async function tickOnce(deps) {
17969
18412
  }
17970
18413
  }
17971
18414
  await closeMergedTaskRuns(deps);
18415
+ await rescueReviewlessPrs(deps, stormActive, now);
18416
+ relinkUnboundReviewers(deps);
17972
18417
  await closeOrphanedReviewers(deps);
17973
18418
  await spawnVerifiersForOpenPrs(deps, stormActive);
17974
18419
  refreshReviewPhases(deps);
@@ -18130,8 +18575,8 @@ async function tickOnce(deps) {
18130
18575
  const ciWatchOff = parent != null && ciWatchClosed(deps.db, parent);
18131
18576
  const ci = ciWatchOff ? null : await deps.ci.checks(run.pr_url).catch(() => null);
18132
18577
  const noCiRepo = ci?.bucket === "none" && !repoHasCi(deps, run);
18133
- if (!ciWatchOff && !noCiRepo && (ci == null || ci.bucket === "pending" || ci.bucket === "none")) {
18134
- const what = ci == null ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E \u0447\u0435\u043A\u0438 \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 \u043F\u0440\u043E\u044F\u0441\u043D\u0438\u0442\u0441\u044F" : ci.bucket === "none" ? "\u0447\u0435\u043A\u043E\u0432 \u043D\u0430 PR \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0447\u0435\u043A-\u0440\u0430\u043D\u0430, \u043D\u043E workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C" : `CI ${ci.pass}/${ci.total}`;
18578
+ if (!ciWatchOff && !noCiRepo && (ci == null || ci.bucket === "pending" || ci.bucket === "none" || ci.bucket === "denied")) {
18579
+ const what = ci == null ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E \u0447\u0435\u043A\u0438 \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 \u043F\u0440\u043E\u044F\u0441\u043D\u0438\u0442\u0441\u044F" : ci.bucket === "none" ? "\u0447\u0435\u043A\u043E\u0432 \u043D\u0430 PR \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0447\u0435\u043A-\u0440\u0430\u043D\u0430, \u043D\u043E workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C" : ci.bucket === "denied" ? `\u0447\u0435\u043A\u0438 \u0440\u043E\u044E \u043D\u0435 \u0432\u0438\u0434\u043D\u044B: GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C (403)${ci.detail ? ` \u2014 ${ci.detail}` : ""}` : `CI ${ci.pass}/${ci.total}`;
18135
18580
  deps.db.updateRun(run.id, { status: "paused", stop_reason: AWAITING_CI_REASON });
18136
18581
  deps.db.addEvent(run.id, "paused", `${AWAITING_CI_REASON} (${what}) \u2014 \u043D\u0435 \u0441\u043C\u0435\u0440\u0442\u044C: \u0440\u0430\u0437\u0431\u0443\u0436\u0443 \u0441\u0430\u043C, \u043A\u043E\u0433\u0434\u0430 \u0447\u0435\u043A\u0438 \u0434\u043E\u0439\u0434\u0443\u0442 \u0434\u043E \u043A\u043E\u043D\u0446\u0430`);
18137
18582
  narrate(deps, `${droneLabel(run)} \u0443\u0448\u0451\u043B \u0436\u0434\u0430\u0442\u044C CI (${what}) \u2014 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u044E, \u0440\u0430\u0437\u0431\u0443\u0436\u0443 \u043F\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A.`);
@@ -19212,7 +19657,18 @@ async function runDaemon(opts) {
19212
19657
  // TASWM-472: третья ось — ОЧЕРЕДЬ. Вливаем по одному и только на свежей базе; ридер
19213
19658
  // свежести ходит в gh (`compare/base...head` → behind_by). Ключа нет при
19214
19659
  // SWARM_MERGE_QUEUE=0 — тогда гейт отвечает ровно как до TASWM-472.
19215
- ...SWARM_MERGE_QUEUE ? { baseSync: (url) => prBaseSync(url), checks: (url) => prCiStatus(url) } : {}
19660
+ // TASWM-483: отказ по правам (403) очереди мержа приходит как «спросить не удалось» (null),
19661
+ // а не как отдельная ось: для НЕЁ это одно и то же — цвет проверок неизвестен, значит
19662
+ // вливать нельзя (checks_unknown, см. докблок computeMergeQueue про несимметричную цену).
19663
+ // Отличать 403 от таймаута нужно тому, кто ЖДЁТ и повторяет (handleCi), а очередь не ждёт.
19664
+ ...SWARM_MERGE_QUEUE ? {
19665
+ baseSync: (url) => prBaseSync(url),
19666
+ checks: async (url) => {
19667
+ const ci = await prCiStatus(url);
19668
+ if (ci == null || ci.bucket === "denied") return null;
19669
+ return { bucket: ci.bucket };
19670
+ }
19671
+ } : {}
19216
19672
  });
19217
19673
  const deps = {
19218
19674
  db,
@@ -19224,7 +19680,9 @@ async function runDaemon(opts) {
19224
19680
  const repoOfRun = normalizeRepo(run.repo || opts.repo);
19225
19681
  const taskClient = tlFor(run.workspace_id);
19226
19682
  const [rawTask, verifyMode, projectBase, zoneNotes] = await Promise.all([
19227
- taskClient ? taskClient.getTask(run.task_slug) : Promise.resolve(null),
19683
+ // TASWM-484: с причиной — на этом чтении стоит fail-loud ниже, и именно его
19684
+ // сообщение читает человек, когда дрон не поднялся.
19685
+ taskClient ? taskClient.getTaskDetailed(run.task_slug) : Promise.resolve(null),
19228
19686
  verifyModeReader ? verifyModeReader(repoOfRun) : Promise.resolve(void 0),
19229
19687
  baseBranchReader ? baseBranchReader(repoOfRun) : Promise.resolve(""),
19230
19688
  // TASWM-466: память зоны прогона — тем же параллельным чтением, что и остальные
@@ -19362,6 +19820,32 @@ async function runDaemon(opts) {
19362
19820
  intervalMs: SWARM_SWEEP_INTERVAL_MS,
19363
19821
  lastAt: null
19364
19822
  },
19823
+ // Жнец worktree (TASWM-476): убирает копии терминальных прогонов, чьи PR смержены/закрыты.
19824
+ // Живость каталога определяем ПО ПРОЦЕССАМ (listDroneProcesses — тот же снимок, по которому
19825
+ // идёт реконсайл рантайма), а не только по статусу в зеркале: статус мог не успеть
19826
+ // обновиться, а сносить каталог из-под работающего дрона нельзя ни при каком статусе.
19827
+ reapWorktrees: {
19828
+ run: async () => {
19829
+ const report = await reapFinishedWorktrees({
19830
+ db,
19831
+ prStatus: (url) => prStatus(url),
19832
+ isDirty: (p) => worktreeDirty(p),
19833
+ releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
19834
+ pruneWorktrees: (repo) => pruneWorktrees(repo || opts.repo),
19835
+ worktreeExists: existsSync17,
19836
+ liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
19837
+ narrate: narrator.narrate
19838
+ });
19839
+ if (report.removed || report.keptDirty) {
19840
+ 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}`);
19841
+ }
19842
+ },
19843
+ intervalMs: SWARM_WORKTREE_REAP_INTERVAL_MS,
19844
+ lastAt: null
19845
+ },
19846
+ // Самолечение залипшего слага (TASWM-476): те же два кирпича, что у жнеца выше.
19847
+ releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
19848
+ liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
19365
19849
  isDirty: (p) => worktreeDirty(p),
19366
19850
  // git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
19367
19851
  lastCommitMs: (p) => worktreeLastCommitMs(p),
@@ -19455,7 +19939,7 @@ async function runDaemon(opts) {
19455
19939
  // fresh-оживление строит промпт с нуля, и без тела задачи это тот же слепой дрон.
19456
19940
  getTask: async (slug, ws) => {
19457
19941
  const client = tlFor(ws);
19458
- return client ? requireTaskBody(await client.getTask(slug), slug, ws) : null;
19942
+ return client ? requireTaskBody(await client.getTaskDetailed(slug), slug, ws) : null;
19459
19943
  },
19460
19944
  ...verifyModeReader ? { verifyMode: verifyModeReader } : {},
19461
19945
  // TASWM-466: тот же ридер памяти зоны, что на спавне — оживлённый дрон строит
@@ -19906,6 +20390,9 @@ export {
19906
20390
  CHANGES_WAKE_LIMIT,
19907
20391
  CHANGES_WAKE_MARK,
19908
20392
  CI_CHECK_COOLDOWN_MS,
20393
+ CI_DENIED_LIMIT,
20394
+ CI_DENIED_MARK,
20395
+ CI_DENIED_STOP_MARK,
19909
20396
  CI_NO_CHECKS_MARK,
19910
20397
  CI_POLL_LIMIT,
19911
20398
  CI_POLL_LIMIT_MARK,
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.21",
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": "584411cbe69583a8727e89caa8ae7e626f8fd8ca",
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",